import {
CreateOrderEntity,
CreateOrderV2Params,
DeleteOrderApiPayload,
OrderHashType,
OrderType,
SplitSignature,
UpdateOrderPrice,
UpdateOrderPriceParams,
UpdateOrderPriceResponse,
} from "../types/OrderTypes";
import { IMyriaClient, MyriaClient } from "../clients/MyriaClient";
import { OrderAPI } from "../core/apis/order.api";
import {
CreateOrderAPIRequest,
DeleteOrderPayload,
GetOrderById,
OrderEntity,
SignableOrderInput,
SignableOrderResponseData,
} from "../types/OrderTypes";
import { CommonModule } from "./CommonModule";
import { APIResponseType } from "../types/APIResponseType";
import { convertAmountToQuantizedAmount } from "../utils/Converter";
import { CommonAPI } from "../core/apis";
import { TokenType } from "../types";
import { DEFAULT_QUANTUM } from "../utils";
import { TransactionManager } from "./TransactionManager";
// const StarkwareLib = require("@starkware-industries/starkware-crypto-utils");
/**
* Create OrderManager instance object
* @class OrderManager
* @param {MyriaClient} MyriaClient Interface of Myria Client
* @example <caption>Constructor for OrderManager</caption>
*
*
// Approach #1
const mClient: IMyriaClient = {
networkId: Network.SEPOLIA,
provider: web3Instance.currentProvider,
web3: web3Instance,
env: EnvTypes.STAGING,
};
const moduleFactory = ModuleFactory.getInstance(mClient);
const orderManager = moduleFactory.getOrderManager();
// Approach #2
const mClient: IMyriaClient = {
networkId: Network.SEPOLIA,
provider: web3Instance.currentProvider,
web3: web3Instance,
env: EnvTypes.STAGING,
};
const myriaClient = new MyriaClient(mClient);
const orderManager = new OrderManager(myriaClient);
*/
export class OrderManager {
private orderAPI: OrderAPI;
private myriaClient: MyriaClient;
private commonModule: CommonModule;
private commonAPI: CommonAPI;
private transactionManager: TransactionManager;
constructor(mClient: IMyriaClient) {
this.myriaClient = new MyriaClient(mClient);
this.orderAPI = new OrderAPI(mClient.env);
this.commonAPI = new CommonAPI(mClient.env);
this.commonModule = new CommonModule(mClient);
this.transactionManager = new TransactionManager(mClient);
}
/**
* @summary Create order V2 (supported both of listing NFTs by ETH/MYR)
* @param {CreateOrderV2Params} params Create order v2 params including the price/nft/assets information
* @returns {OrderEntity} Details order information including fees and Asset_ID_Buy/Asset_ID_Sell/AmountBuy/AmountSell
* @throws {string} Exception: Order type is required
* @throws {string} Exception: Owner wallet address is required
* @throws {string} Exception: Owner stark key is required
* @throws {string} Exception: Price must be define and must be > 0
* @throws {string} Exception: Asset Ref ID in Marketplace is required
* @throws {string} Exception: Order is only supported for MINTABLE_ERC721
* @throws {string} Exception: Token address is required for listing MINTABLE_ERC721
* @throws {string} Exception: Token ID is required for listing MINTABLE_ERC721
* @throws {string} Exception: Required the token received to be well-defined and tokenAddress is mandatory
* @throws {string} Exception: New listing method for ERC20 isn't available on Mainnet yet. Please try with Testnet only.
* @example <caption>Sample of createOrderV2({}) on Staging env</caption>
*
* const mClient: IMyriaClient = {
networkId: Network.SEPOLIA,
provider: web3Instance.currentProvider,
web3: web3Instance,
env: EnvTypes.STAGING,
};
const orderManager: OrderManager = new OrderManager(mClient);
// Define createOrderV2 params
const feeData: FeeDto[] = [
{
feeType: AssetDetailsInfo?.fee?.[0].feeType, // FeeType in asset details info
percentage: AssetDetailsInfo?.fee?.[0].feeType, // Percentage of Fee as apart of asset details info
address: AssetDetailsInfo?.fee?.[0].address, // The destination wallet address which would receive the percentage of Fee
},
];
const paramCreateOrder: CreateOrderV2Params = {
orderType: OrderType.SELL,
ownerWalletAddress: 'owner_wallet_address_that_own_nft',
ownerStarkKey: 'owner_stark_key_0x...',
assetRefId: 'Reference asset ID that intend to listing',
tokenSell: {
tokenType: TokenType.MINTABLE_ERC721,
data: {
tokenId: 'id of token',
tokenAddress: 'token address of NFT',
},
},
tokenReceived: {
tokenType: 'TokenType.ETH or TokenType.ERC20',
data: {
tokenAddress: 'token address of currency',
},
},
price: "100", // Set the price for the NFTs
fees: feeData, // Only having data or undefined
};
const createdOrderResponse: OrderEntity = await orderManager.createOrderV2(paramCreateOrder);
console.log("Order details response:");
console.log(JSON.stringify(data, null, 2));
*/
public async createOrderV2(
params: CreateOrderV2Params
): Promise<OrderEntity> {
if (!params.orderType) {
throw new Error("Order type is required");
}
if (!params.ownerWalletAddress) {
throw new Error("Owner wallet address is required");
}
if (!params.ownerStarkKey) {
throw new Error("Owner stark key is required");
}
if (!params.price || params.price == "0") {
throw new Error("Price must be define and must be > 0");
}
if (!params.assetRefId) {
throw new Error("Asset Ref ID in Marketplace is required");
}
if (params.tokenSell.tokenType !== TokenType.MINTABLE_ERC721) {
throw new Error("Order is only supported for MINTABLE_ERC721");
}
if (!params.tokenSell.data.tokenAddress) {
throw new Error("Token address is required for listing MINTABLE_ERC721");
}
if (!params.tokenSell.data.tokenId) {
throw new Error("Token ID is required for listing MINTABLE_ERC721");
}
if (!params.tokenReceived.data) {
throw new Error('Token Receiver is required to specify');
}
if (
params.tokenReceived.tokenType !== TokenType.ETH &&
!params.tokenReceived.data.tokenAddress
) {
throw new Error(
"Required the token received to be well-defined and tokenAddress is mandatory"
);
}
/**
* Disable listing myria token in Prod until services get new deployment
*/
// if ((this.myriaClient.env === EnvTypes.PREPROD || this.myriaClient.env === EnvTypes.PRODUCTION)
// && params.tokenReceived.tokenType === TokenType.ERC20) {
// throw new Error("New listing method for ERC20 isn't available on Mainnet yet. Please try with Testnet only.");
// }
const buildSignablePayload: SignableOrderInput = {
orderType: params.orderType,
ethAddress: params.ownerWalletAddress,
assetRefId: params.assetRefId,
starkKey: params.ownerStarkKey,
tokenSell: {
type: params.tokenSell.tokenType,
data: params.tokenSell.data,
},
amountSell: "1", // Default as always for nFT
tokenBuy: {
type: params.tokenReceived.tokenType,
data: {
quantum: DEFAULT_QUANTUM,
tokenAddress: params.tokenReceived.data.tokenAddress,
},
},
amountBuy: params.price + "",
includeFees: params.fees ? true : false,
fees: params.fees,
};
const signableData: SignableOrderResponseData = await this.signableOrder(
buildSignablePayload
);
const feeSign = signableData?.feeInfo
? {
feeLimit: signableData?.feeInfo?.feeLimit,
feeToken: signableData?.feeInfo?.assetId,
feeVaultId: signableData?.feeInfo?.sourceVaultId,
}
: undefined;
const feeData = signableData?.feeInfo
? [
{
feeType: params.fees[0].feeType,
percentage: params.fees[0].percentage,
address: params.fees[0].address,
},
]
: undefined;
if (!signableData) {
throw new Error(
"Exception during fetch signable order data - cant execute further operation"
);
}
// Check if asset type is available for listing
let isAssetIdBuySupportListing = false;
const availableAssetForListing =
await this.transactionManager.getSupportedTokens();
if (availableAssetForListing.status === "success") {
availableAssetForListing.data.forEach((asset) => {
if (
asset.assetType.toLowerCase() === signableData.assetIdBuy?.toLowerCase() && asset.isSupportListing) {
isAssetIdBuySupportListing = true;
}
});
} else {
throw new Error(
"Fetch assets listing supported failed, cannot create order currently."
);
}
if (!isAssetIdBuySupportListing) {
throw new Error("This currency token is not supporting for listing yet");
}
const paramCreateOrder: CreateOrderEntity = {
assetRefId: params.assetRefId,
orderType: params.orderType,
feeSign: feeSign,
includeFees: feeData ? true : false,
amountSell: signableData.amountSell,
amountBuy: signableData.amountBuy,
sellerStarkKey: params.ownerStarkKey,
vaultIdSell: signableData.vaultIdSell,
vaultIdBuy: signableData.vaultIdBuy,
sellerAddress: params.ownerWalletAddress,
nonce: signableData.nonce,
assetIdBuy: signableData.assetIdBuy,
assetIdSell: signableData.assetIdSell,
fees: feeData,
};
const orderResponse = await this.createOrder(paramCreateOrder);
return orderResponse;
}
public async createOrder(payload: CreateOrderEntity): Promise<OrderEntity> {
let createOrderData;
if (!payload.assetRefId) {
throw new Error("Asset reference Id is required");
}
if (!payload.sellerAddress) {
throw new Error("Seller address is not defined");
}
if (!payload.sellerStarkKey) {
throw new Error("Seller stark key is not defined");
}
if (!payload.amountBuy) {
throw new Error("Amount for buyer shoud be defined.");
}
if (!payload.amountSell) {
throw new Error("Amount for seller should be defined.");
}
if (!payload.assetIdBuy) {
throw new Error("Missing the assetIDBuy");
}
if (!payload.assetIdSell) {
throw new Error("Missing the assetIDSell");
}
if (!payload.vaultIdBuy) {
throw new Error("Missing the vaultIdBuy");
}
if (!payload.vaultIdSell) {
throw new Error("Missing the vaultIdSell");
}
// Check if asset type is available for listing
let isAssetIdBuySupportListing = false;
const availableAssetForListing =
await this.transactionManager.getSupportedTokens();
if (availableAssetForListing.status === "success") {
availableAssetForListing.data.forEach((asset) => {
if (
asset.assetType.toLowerCase() === payload.assetIdBuy?.toLowerCase() &&
asset.isSupportListing
) {
isAssetIdBuySupportListing = true;
}
});
} else {
throw new Error(
"Fetch assets listing supported failed, cannot create order currently."
);
}
if (!isAssetIdBuySupportListing) {
throw new Error("This currency token is not supporting for listing yet");
}
// SET EXPIRATION TO BE EXPIRE IN 12 YEARS
const expirationTimestamp = new Date();
expirationTimestamp.setFullYear(expirationTimestamp.getFullYear() + 12);
const expirationTime = Math.floor(
expirationTimestamp.getTime() / (3600 * 1000)
).toString();
try {
// TODO - update with new API to get nonce
// const nonceByStarkKey = await this.commonAPI.getNonceByStarkKey(payload.sellerStarkKey);
const nonce = payload.nonce || Math.floor(Math.random() * 100000000) + 1;
const quantizedAmountBuy = convertAmountToQuantizedAmount(
String(payload.amountBuy)
);
const amountBuyNonFee =
quantizedAmountBuy - Number(payload?.feeSign?.feeLimit);
// console.log("Amount Buy Non Fee ->", amountBuyNonFee);
// const quantizedAmountSell = convertAmountToQuantizedAmount(payload.amountSell);
let orderPayload: OrderHashType = {
includeFees: false,
walletAddress: "",
vaultIdSell: 0,
vaultIdBuy: 0,
amountSell: "",
amountBuy: "",
assetIdSell: "",
assetIdBuy: "",
nonce: 0,
expirationTimestamp: 0,
};
let starkSignature: SplitSignature | undefined;
if (payload?.includeFees && payload?.feeSign) {
orderPayload = {
includeFees: true,
walletAddress: payload.sellerAddress,
vaultIdSell: payload.vaultIdSell,
vaultIdBuy: payload.vaultIdBuy,
amountSell: String(payload.amountSell),
amountBuy: String(amountBuyNonFee),
assetIdSell: payload.assetIdSell,
assetIdBuy: payload.assetIdBuy,
nonce: nonce,
expirationTimestamp: parseInt(expirationTime),
fee: {
feeLimit: payload?.feeSign?.feeLimit,
feeToken: payload?.feeSign?.feeToken,
feeVaultId: payload?.feeSign?.feeVaultId,
},
};
console.log("orderPayload with fee Sign => ", orderPayload);
starkSignature =
await this.commonModule.generateStarkSignatureForOrderWithFee(
orderPayload
);
if (!starkSignature) {
throw new Error("Stark signature generation error ");
}
} else {
orderPayload = {
includeFees: false,
walletAddress: payload.sellerAddress,
vaultIdSell: payload.vaultIdSell,
vaultIdBuy: payload.vaultIdBuy,
amountSell: String(payload.amountSell),
amountBuy: String(quantizedAmountBuy),
assetIdSell: payload.assetIdSell,
assetIdBuy: payload.assetIdBuy,
nonce: nonce,
expirationTimestamp: parseInt(expirationTime),
};
console.log("orderPayload => ", orderPayload);
starkSignature = await this.commonModule.generateStarkSignatureForOrder(
orderPayload
);
if (!starkSignature) {
throw new Error("Stark signature generation error ");
}
}
console.log("orderPayload ->", JSON.stringify(orderPayload));
const requestBody: CreateOrderAPIRequest = {
assetRefId: payload.assetRefId,
quantizedAmountBuy: Number(quantizedAmountBuy),
quantizedAmountSell: payload.amountSell,
nonQuantizedAmountSell: payload.amountSell,
nonQuantizedAmountBuy: payload.amountBuy,
assetIdBuy: payload.assetIdBuy,
assetIdSell: payload.assetIdSell,
fees: payload.fees,
includeFees: payload.includeFees,
nonce: nonce,
orderType: payload.orderType,
starkKey: payload.sellerStarkKey,
vaultIdBuy: payload.vaultIdBuy,
vaultIdSell: payload.vaultIdSell,
starkSignature: starkSignature,
expirationTimestamp: expirationTime,
sellerAddress: payload.sellerAddress,
};
const createOrderRes = await this.orderAPI.createOrder(requestBody);
if (createOrderRes?.status === "success") {
createOrderData = createOrderRes?.data;
} else {
throw new Error("Create Orders failure");
}
} catch (err) {
throw new Error(err);
}
return createOrderData;
}
public async signableOrder(
payload: SignableOrderInput
): Promise<SignableOrderResponseData> {
if (!payload.orderType) {
throw new Error("One field is missing: orderType");
}
if (payload.includeFees && !payload.assetRefId) {
throw new Error("Missing asset ref id for fee");
}
if (!payload.ethAddress) {
throw new Error("One field is missing: ethAddress");
}
if (!payload.starkKey) {
throw new Error("One field is missing: starkKey");
}
if (!payload.tokenSell) {
throw new Error("One field is missing: tokenSell");
}
if (!payload.amountSell) {
throw new Error("One field is missing: amountSell");
}
if (!payload.tokenBuy) {
throw new Error("One field is missing: tokenBuy");
}
if (!payload.amountBuy) {
throw new Error("One field is missing: amountBuy");
}
try {
const response = await this.orderAPI.signableOrder(payload);
if (response.status === "success") {
return response.data;
} else {
throw new Error("Creaate signable order failed!");
}
} catch (err) {
throw new Error(err);
}
}
public async getOrders(): Promise<OrderEntity[]> {
let ordersData;
try {
const getOrdersRes = await this.orderAPI.getOrders();
if (getOrdersRes) {
ordersData = getOrdersRes;
} else {
throw new Error("Get Orders failed!");
}
} catch (err) {
throw new Error(err);
}
return ordersData;
}
public async getOrderById(payload: GetOrderById): Promise<OrderEntity> {
let orderByIdData;
if (payload.id) {
try {
const orderByIdResponse = await this.orderAPI.getOrderById(payload);
if (orderByIdResponse) {
orderByIdData = orderByIdResponse;
} else {
throw new Error("Get Order By Id failed!");
}
} catch (err) {
throw new Error(err);
}
} else {
throw new Error("Id shoud be provided.");
}
return orderByIdData;
}
public async deleteOrderById(
payload: DeleteOrderPayload
): Promise<OrderEntity> {
let deletedOrder;
const orderDetails = await this.orderAPI.getOrderById({
id: payload.orderId,
});
console.log("[Core-SDK] order details", JSON.stringify(orderDetails));
if (!payload.orderId) {
throw new Error("One field is missing: orderId");
}
if (!payload.sellerWalletAddress) {
throw new Error("One field is missing: sellerWalletAddress");
}
try {
const orderHashPayload: OrderHashType = {
vaultIdSell: 0,
vaultIdBuy: 0,
amountBuy: "0",
amountSell: "0",
assetIdSell: orderDetails.assetIdSell,
assetIdBuy: orderDetails.assetIdBuy,
nonce: orderDetails.nonce,
expirationTimestamp: 0,
includeFees: false,
walletAddress: payload.sellerWalletAddress,
};
console.log(
"[Core-SDK] order hash payload ->",
JSON.stringify(orderHashPayload)
);
const starkSignature =
await this.commonModule.generateStarkSignatureForOrder(
orderHashPayload
);
const requestPayload: DeleteOrderApiPayload = {
orderId: payload.orderId,
nonce: orderDetails.nonce,
signature: starkSignature,
};
const deleteOrderRes = await this.orderAPI.deleteOrderById(
requestPayload
);
if (deleteOrderRes) {
deletedOrder = deleteOrderRes;
} else {
throw new Error("Delete Order failed!");
}
} catch (err) {
throw new Error(err);
}
return deletedOrder;
}
public async updateOrderPrice(
orderId: string,
payload: UpdateOrderPriceParams
): Promise<APIResponseType<UpdateOrderPriceResponse> | undefined> {
let orderData: any;
if (!orderId) {
throw new Error("One field is missing: orderId");
}
const currentOrder = await this.orderAPI.getOrderById({
id: Number(orderId),
});
if (!currentOrder.assetIdBuy) {
throw new Error("One field is missing: assetIdBuy");
}
if (!currentOrder.assetIdSell) {
throw new Error("One field is missing: assetIdSell");
}
if (!payload.newAmountBuy || Number(payload.newAmountBuy) == 0) {
throw new Error("New amount should be defined and greater 0");
}
if (!payload.sellerWalletAddress) {
throw new Error("WalletAddress should be provided.");
}
if (!payload.sellerStarkKey) {
throw new Error("Seller starkKey is required!");
}
if (!payload.tokenBuy) {
throw new Error('Token buy is required')
} else if (payload.tokenBuy.type === TokenType.ERC20 && !payload.tokenBuy.data.tokenAddress) {
throw new Error ("Token buy with ERC20 is required token address");
}
if (!payload.tokenSell) {
throw new Error("Token sell is required");
} else if (payload.tokenSell.type !== TokenType.MINTABLE_ERC721) {
throw new Error("Only support sell MINTABLE_ERC721 tokens for now");
}
const newQuantizedAmountBuy = String(
convertAmountToQuantizedAmount(payload.newAmountBuy)
);
// Get signable order
const buildSignablePayload: SignableOrderInput = {
orderType: OrderType.SELL,
ethAddress: payload.sellerWalletAddress,
assetRefId: currentOrder.assetRefId,
starkKey: payload.sellerStarkKey,
tokenSell: {
type: TokenType.MINTABLE_ERC721,
data: {
tokenAddress: payload.tokenSell.data.tokenAddress,
tokenId: payload.tokenSell.data.tokenId,
},
},
amountSell: "1", // Default as always for nFT
tokenBuy: {
type: payload.tokenBuy.type,
data: {
quantum: DEFAULT_QUANTUM,
tokenAddress: payload.tokenBuy.data.tokenAddress,
},
},
amountBuy: payload.newAmountBuy + "",
includeFees: payload.fees ? true : false,
fees: payload.fees,
};
const signableData: SignableOrderResponseData = await this.signableOrder(
buildSignablePayload
);
try {
// get expirationTimestamp
let expiredAt;
expiredAt = new Date();
expiredAt.setFullYear(expiredAt.getFullYear() + 12);
expiredAt = Math.floor(expiredAt.getTime() / 3600 / 1000);
// TODO - update with new API to get nonce
// const nonceByStarkKey = await this.commonAPI.getNonceByStarkKey(payload.sellerStarkKey);
const nonceData = payload.nonce || Math.floor(Math.random() * 100000000) + 1;
let sellOrderMsg: OrderHashType;
// stark signature of created sell order
let starkSignatureSell;
if (buildSignablePayload?.includeFees && signableData?.feeInfo) {
const amountBuyNonFee = Number(newQuantizedAmountBuy) - Number(signableData.feeInfo.feeLimit);
sellOrderMsg = {
includeFees: true,
walletAddress: payload.sellerWalletAddress,
vaultIdSell: signableData.vaultIdSell,
vaultIdBuy: signableData.vaultIdBuy,
amountSell: String(signableData.amountSell),
amountBuy: String(amountBuyNonFee),
assetIdSell: signableData.assetIdSell,
assetIdBuy: signableData.assetIdBuy,
nonce: signableData.nonce ?? Number(nonceData),
expirationTimestamp: Number(expiredAt),
fee: {
feeLimit: signableData.feeInfo.feeLimit,
feeToken: signableData.feeInfo.assetId,
feeVaultId: signableData.feeInfo.sourceVaultId,
},
};
starkSignatureSell = await this.commonModule.generateStarkSignatureForOrderWithFee(sellOrderMsg);
} else {
sellOrderMsg = {
includeFees: false,
walletAddress: payload.sellerWalletAddress,
vaultIdSell: currentOrder.vaultIdSell,
vaultIdBuy: currentOrder.vaultIdBuy,
amountSell: String(currentOrder.amountSell),
amountBuy: newQuantizedAmountBuy,
assetIdSell: currentOrder.assetIdSell,
assetIdBuy: currentOrder.assetIdBuy,
nonce: signableData.nonce ?? Number(nonceData),
expirationTimestamp: expiredAt,
};
starkSignatureSell = await this.commonModule.generateStarkSignatureForOrder(sellOrderMsg);
}
console.log("orderPayload ->", JSON.stringify(sellOrderMsg));
// stark signature of sell order which is going to be canceled
const cancelOrderMsg: OrderHashType = {
includeFees: false,
walletAddress: payload.sellerWalletAddress,
vaultIdBuy: 0,
vaultIdSell: 0,
amountBuy: "0",
amountSell: "0",
assetIdBuy: currentOrder.assetIdBuy,
assetIdSell: currentOrder.assetIdSell,
nonce: currentOrder.nonce,
expirationTimestamp: 0,
};
const starkSignatureCancel =
await this.commonModule.generateStarkSignatureForOrder(cancelOrderMsg);
if (payload.expirationTimestamp) {
expiredAt = payload.expirationTimestamp;
}
if (starkSignatureCancel && starkSignatureSell) {
const upatePricePayload: UpdateOrderPrice = {
expirationTimestamp: expiredAt,
newAmountBuy: newQuantizedAmountBuy,
nonQuantizedAmountBuy: payload.newAmountBuy,
nonce: signableData.nonce ?? Number(nonceData),
starkKey: payload.sellerStarkKey,
starkSignatureCancelOrder: starkSignatureCancel,
starkSignatureSellOrder: starkSignatureSell,
nonQuantizedAmountSell: "1",
orderType: OrderType.SELL,
includeFees: sellOrderMsg.includeFees,
fees: payload.fees
};
const orderRes = await this.orderAPI.requestUpdateOrderPrice(
orderId,
upatePricePayload
);
if (orderRes?.status === "success") {
orderData = orderRes?.data;
} else {
throw new Error("Update Order Price failure");
}
} else {
throw new Error("Signaturing Error!");
}
} catch (error) {
throw new Error(error);
}
return orderData;
}
}
Source