UNPKG

axie-tools

Version:

TypeScript library and CLI tool for interacting with Axie Infinity marketplace and NFTs on Ronin network. Features marketplace operations for Axies and Materials (buy/sell/delist), batch transfers, and wallet information.

1,589 lines (1,567 loc) 45.9 kB
// lib/axie.ts import { AbiCoder as AbiCoder2 } from "ethers"; // lib/contracts.ts import { Interface, Contract } from "ethers"; import AXIE_PROXY from "@roninbuilders/contracts/axie_proxy"; import ERC721_BATCH_TRANSFER from "@roninbuilders/contracts/erc_721_batch_transfer"; import MARKETPLACE_GATEWAY_PROXY from "@roninbuilders/contracts/market_gateway_proxy"; import MATERIAL_ERC_1155_PROXY from "@roninbuilders/contracts/material_erc_1155_proxy"; import ERC_1155_EXCHANGE from "@roninbuilders/contracts/erc_1155_exchange_21a3764f"; import USD_COIN from "@roninbuilders/contracts/usd_coin"; import WRAPPED_ETHER from "@roninbuilders/contracts/wrapped_ether"; function getAxieContract(signerOrProvider) { const address = AXIE_PROXY.address; const abi = new Interface(AXIE_PROXY.proxy_abi); return new Contract(address, abi, signerOrProvider); } function getMarketplaceContract(signerOrProvider) { const address = MARKETPLACE_GATEWAY_PROXY.address; const abi = new Interface(MARKETPLACE_GATEWAY_PROXY.proxy_abi); return new Contract(address, abi, signerOrProvider); } function getBatchTransferContract(signerOrProvider) { const address = ERC721_BATCH_TRANSFER.address; const abi = new Interface(ERC721_BATCH_TRANSFER.abi); return new Contract(address, abi, signerOrProvider); } function getWETHContract(signerOrProvider) { const address = WRAPPED_ETHER.address; const abi = new Interface(WRAPPED_ETHER.abi); return new Contract(address, abi, signerOrProvider); } function getUSDCContract(signerOrProvider) { const address = USD_COIN.address; const abi = new Interface(USD_COIN.proxy_abi); return new Contract(address, abi, signerOrProvider); } function getMaterialContract(signerOrProvider) { const address = MATERIAL_ERC_1155_PROXY.address; const abi = new Interface(MATERIAL_ERC_1155_PROXY.proxy_abi); return new Contract(address, abi, signerOrProvider); } function getERC1155ExchangeContract(signerOrProvider) { const address = ERC_1155_EXCHANGE.address; const abi = new Interface(ERC_1155_EXCHANGE.abi); return new Contract(address, abi, signerOrProvider); } // lib/utils.ts import prompts from "prompts"; import { JsonRpcProvider, formatEther } from "ethers"; // lib/material.ts import { AbiCoder } from "ethers"; // lib/marketplace.ts var ORDER_FRAGMENTS = ` fragment OrderInfo on Order { ...PartialOrderFields makerProfile { name addresses { ronin __typename } __typename } assets { ...AssetInfo availableQuantity remainingQuantity __typename } __typename } fragment PartialOrderFields on Order { id maker kind expiredAt paymentToken startedAt basePrice endedAt endedPrice expectedState nonce marketFeePercentage signature hash duration timeLeft currentPrice suggestedPrice currentPriceUsd status __typename } fragment AssetInfo on Asset { erc address id quantity orderId __typename } `; // lib/material.ts var MATERIAL_QUERIES = { GET_MATERIALS: ` query GetMaterials($owner: String, $includeMinPrice: Boolean = false, $includeQuantity: Boolean = false) { erc1155Tokens(owner: $owner, tokenType: Material, from: 0, size: 32) { total results { ...Erc1155Metadata quantity: total @include(if: $includeQuantity) minPrice @include(if: $includeMinPrice) orders(from: 0, size: 1) { total __typename } __typename } __typename } } fragment Erc1155Metadata on Erc1155Token { attributes description imageUrl name tokenAddress tokenId tokenType __typename } `, GET_MATERIAL_DETAIL: ` query GetMaterialDetail($tokenId: String) { erc1155Token(tokenType: Material, tokenId: $tokenId) { ...Erc1155Metadata minPrice totalSupply: total totalOwners orders(from: 0, size: 1, sort: PriceAsc) { totalListed: quantity totalOrders: total __typename } __typename } } fragment Erc1155Metadata on Erc1155Token { attributes description imageUrl name tokenAddress tokenId tokenType __typename } `, GET_MATERIAL_ORDERS: ` query GetBuyNowErc1155Orders($tokenType: Erc1155Type!, $tokenId: String, $from: Int!, $size: Int!, $sort: SortBy = PriceAsc) { erc1155Token(tokenType: $tokenType, tokenId: $tokenId) { tokenId tokenType total minPrice orders(from: $from, size: $size, sort: $sort) { total quantity data { ...OrderInfo __typename } __typename } __typename } } ${ORDER_FRAGMENTS} `, GET_MATERIAL_BY_OWNER: ` query GetErc1155DetailByOwner($tokenType: Erc1155Type!, $owner: String, $tokenId: String, $skipId: Boolean = false) { erc1155ByOwner: erc1155Token( tokenType: $tokenType owner: $owner tokenId: $tokenId ) { id: tokenId @skip(if: $skipId) tokenId tokenType totalOwned: total orders(from: 0, size: 100, maker: $owner, sort: PriceAsc, includeInvalid: true) { totalListed: quantity totalOrders: total data { ...OrderInfo __typename } __typename } __typename } } ${ORDER_FRAGMENTS} `, GET_MATERIAL_OWNERSHIP: ` query GetErc1155Token($owner: String!, $tokenId: String!) { erc1155Token(tokenType: Material, tokenId: $tokenId, owner: $owner) { tokenId tokenType total orders(from: 0, size: 100, sort: PriceAsc) { quantity total data { ...OrderInfo __typename } __typename } __typename } } ${ORDER_FRAGMENTS} `, CREATE_ORDER: ` mutation CreateOrder($order: InputOrder!, $signature: String!) { createOrder(order: $order, signature: $signature) { ...OrderInfo __typename } } ${ORDER_FRAGMENTS} ` }; async function checkMaterialOwnership(materialId, address, skyMavisApiKey, accessToken) { const { graphqlUrl, headers } = getMarketplaceApi(skyMavisApiKey); const variables = { tokenId: materialId, owner: address }; const apiHeaders = { ...headers, authorization: `Bearer ${accessToken}` }; try { const result = await apiRequest( graphqlUrl, JSON.stringify({ operationName: "GetErc1155Token", query: MATERIAL_QUERIES.GET_MATERIAL_OWNERSHIP, variables }), apiHeaders ); const token = result.data?.erc1155Token; console.log(`\u{1F50D} Ownership query result:`, JSON.stringify(token, null, 2)); if (token && token.total === 0) { console.log(`\u2139\uFE0F User owns 0 of this material`); return token; } return token; } catch (error) { console.log(`\u274C Error in checkMaterialOwnership:`, error); return null; } } async function getUserMaterials(address, skyMavisApiKey) { const query = MATERIAL_QUERIES.GET_MATERIALS; const variables = { owner: address, includeMinPrice: false, includeQuantity: true }; const { graphqlUrl, headers } = getMarketplaceApi(skyMavisApiKey); try { const results = await apiRequest( graphqlUrl, JSON.stringify({ operationName: "GetMaterials", query, variables }), headers ); return results.data?.erc1155Tokens?.results || []; } catch (error) { return []; } } async function validateMaterialToken(tokenId, skyMavisApiKey) { const variables = { tokenId }; const { graphqlUrl, headers } = getMarketplaceApi(skyMavisApiKey); try { const result = await apiRequest( graphqlUrl, JSON.stringify({ operationName: "GetMaterialDetail", query: MATERIAL_QUERIES.GET_MATERIAL_DETAIL, variables }), headers ); if (result?.data?.erc1155Token) { return result.data.erc1155Token; } return null; } catch (error) { return null; } } async function getMaterialFloorPrice(materialId, skyMavisApiKey, requestedQuantity) { const { graphqlUrl, headers } = getMarketplaceApi(skyMavisApiKey); try { const response = await fetch(graphqlUrl, { method: "POST", headers: { "Content-Type": "application/json", ...headers }, body: JSON.stringify({ query: `query GetErc1155Orders($tokenType: Erc1155Type!, $tokenId: String, $sort: SortBy = PriceAsc) { erc1155Token(tokenType: $tokenType, tokenId: $tokenId) { orders(from: 0, size: 50, sort: $sort) { data { currentPrice expiredAt assets { availableQuantity } } } } }`, variables: { tokenType: "Material", tokenId: materialId, sort: "PriceAsc" } }) }); const { data } = await response.json(); const orders = data?.erc1155Token?.orders?.data || []; const validOrders = orders.filter( (order) => order.expiredAt * 1e3 > Date.now() && order.assets?.[0]?.availableQuantity && parseInt(order.assets[0].availableQuantity) > 0 ); if (validOrders.length === 0) { return null; } if (!requestedQuantity || requestedQuantity <= 0) { const cheapestOrder = validOrders[0]; return (Number(cheapestOrder.currentPrice) / 1e18).toFixed(6); } let remainingQuantity = requestedQuantity; let totalCost = 0; let ordersUsed = 0; for (const order of validOrders) { const availableQuantity = parseInt(order.assets[0].availableQuantity); const quantityToUse = Math.min(remainingQuantity, availableQuantity); const orderPrice = Number(order.currentPrice) / 1e18; totalCost += quantityToUse * orderPrice; remainingQuantity -= quantityToUse; ordersUsed++; if (remainingQuantity <= 0) { const averagePrice = totalCost / requestedQuantity; return averagePrice.toFixed(6); } } return null; } catch (error) { return null; } } function encodeMaterialOrderData(order) { const orderTypes = [ "(address,uint8,(uint8,address,uint256,uint256),uint256,address,uint256,uint256,uint256,uint256,uint256,uint256)" ]; const orderData = [ order.maker, 1, // kind: sell order [ 2, // ERC1155 type order.assets[0].address, parseInt(order.assets[0].id), parseInt(order.assets[0].quantity) ], order.expiredAt, order.paymentToken, order.startedAt, order.basePrice, order.endedAt, order.endedPrice, order.expectedState || 0, order.nonce ]; return AbiCoder.defaultAbiCoder().encode(orderTypes, [orderData]); } // lib/utils.ts function createProvider(skyMavisApiKey) { return new JsonRpcProvider( `https://api-gateway.skymavis.com/rpc?apikey=${skyMavisApiKey}` ); } function getMarketplaceApi(skyMavisApiKey) { const graphqlUrl = "https://api-gateway.skymavis.com/graphql/axie-marketplace"; const headers = { "x-api-key": skyMavisApiKey }; return { graphqlUrl, headers }; } async function getAccountInfo(address, provider, skyMavisApiKey) { const axieIds = await getAxieIdsFromAccount(address, provider); const wethContract = getWETHContract(provider); const marketplaceContract = getMarketplaceContract(provider); const marketplaceAddress = await marketplaceContract.getAddress(); const balance = await provider.getBalance(address); const wethBalance = await wethContract.balanceOf(address); const allowance = await wethContract.allowance(address, marketplaceAddress); const axieContract = getAxieContract(provider); const isApprovedForAll = await axieContract.isApprovedForAll( address, marketplaceAddress ); const materialContract = getMaterialContract(provider); const isMaterialApprovedForAll = await materialContract.isApprovedForAll( address, marketplaceAddress ); const materials = await getUserMaterials(address, skyMavisApiKey); return { address, ronBalance: formatEther(balance), wethBalance: formatEther(wethBalance), allowance, isApprovedForAll, isMaterialApprovedForAll, axieIds, materials }; } async function apiRequest(url, body = null, headers = {}, method = "POST") { if (method === "POST" && body) { try { const parsedBody = JSON.parse(body); if (parsedBody.query) { if (parsedBody.variables) { } } } catch (e) { console.error("Failed to parse GraphQL request body:", e); } } const response = await fetch(url, { method, headers: { ...headers, "Content-Type": "application/json" }, ...method === "GET" ? {} : { body } }); if (!response.ok) { const errorText = await response.text(); console.timeEnd("\u{1F680} Fetch"); throw new Error(`HTTP ${response.status}: ${errorText}`); } const responseText = await response.text(); console.timeEnd("\u{1F680} Fetch"); try { const res = JSON.parse(responseText); return res; } catch (error) { throw new Error(`Failed to parse JSON response: ${responseText}`); } } var askToContinue = async () => { const response = await prompts({ type: "confirm", name: "continue", message: "\u{1F504} Would you like to do something else?" }); if (!response.continue) { console.log("\u{1F44B} Goodbye!"); process.exit(0); } }; async function ensureMarketplaceToken() { if (!process.env.MARKETPLACE_ACCESS_TOKEN) { const response = await prompts({ type: "password", name: "token", message: "\u{1F511} Enter your Marketplace access token:", validate: (value) => value !== void 0 && value !== "" }); if (!response.token) { process.exit(1); } process.env.MARKETPLACE_ACCESS_TOKEN = response.token; } return process.env.MARKETPLACE_ACCESS_TOKEN; } // lib/axie.ts var AXIE_FRAGMENTS = ` fragment AxieDetail on Axie { id order { ...OrderInfo __typename } __typename } `; var AXIE_QUERIES = { GET_AXIE_DETAIL: ` query GetAxieDetail($axieId: ID!) { axie(axieId: $axieId) { ...AxieDetail __typename } } ${AXIE_FRAGMENTS} ${ORDER_FRAGMENTS} ` }; async function getAxieIdsFromAccount(address, provider) { const axieContract = getAxieContract(provider); const axiesBalance = await axieContract.balanceOf(address); let axieIds = []; for (let i = 0; i < axiesBalance; i++) { try { const axieId = await axieContract.tokenOfOwnerByIndex(address, i); axieIds.push(Number(axieId)); } catch (error) { } } return axieIds; } async function getAxieDetails(axieId, accessToken, skyMavisApiKey) { const variables = { axieId }; const { graphqlUrl, headers: apiHeaders } = getMarketplaceApi(skyMavisApiKey); const headers = { ...apiHeaders, authorization: `Bearer ${accessToken}` }; try { const results = await apiRequest( graphqlUrl, JSON.stringify({ query: AXIE_QUERIES.GET_AXIE_DETAIL, variables }), headers ); return results.data?.axie.order || null; } catch (error) { console.error("Error fetching axie details:", error); return null; } } function encodeAxieOrderData(order) { const orderTypes = [ "(address maker, uint8 kind, (uint8 erc,address addr,uint256 id,uint256 quantity)[] assets, uint256 expiredAt, address paymentToken, uint256 startedAt, uint256 basePrice, uint256 endedAt, uint256 endedPrice, uint256 expectedState, uint256 nonce, uint256 marketFeePercentage)" ]; const orderData = [ order.maker, 1, // kind: sell order [ [ 1, // ERC721 type order.assets[0].address, parseInt(order.assets[0].id), parseInt(order.assets[0].quantity) ] ], order.expiredAt, order.paymentToken, order.startedAt, order.basePrice, order.endedAt, order.endedPrice, order.expectedState || "0", order.nonce, order.marketFeePercentage ]; return AbiCoder2.defaultAbiCoder().encode(orderTypes, [orderData]); } async function getAxieFloorPrice(skyMavisApiKey) { const { graphqlUrl, headers } = getMarketplaceApi(skyMavisApiKey); try { const response = await fetch(graphqlUrl, { method: "POST", headers: { "Content-Type": "application/json", ...headers }, body: JSON.stringify({ query: `query GetAxieLatest($from: Int!, $size: Int!, $sort: SortBy, $auctionType: AuctionType) { axies(from: $from, size: $size, sort: $sort, auctionType: $auctionType) { total results { id order { id currentPrice expiredAt __typename } __typename } __typename } }`, variables: { from: 0, size: 20, sort: "PriceAsc", auctionType: "Sale" } }) }); const { data } = await response.json(); const axies = data?.axies?.results || []; const validAxies = axies.filter( (axie) => axie.order && axie.order.currentPrice && axie.order.expiredAt * 1e3 > Date.now() ); if (validAxies.length === 0) { return null; } const cheapestAxie = validAxies[0]; return (Number(cheapestAxie.order.currentPrice) / 1e18).toFixed(6); } catch (error) { return null; } } // lib/marketplace/access-token.ts var AUTH_TOKEN_REFRESH_URL = "https://athena.skymavis.com/v2/public/auth/token/refresh"; var refreshToken = async (refreshToken2) => { const data = await apiRequest( AUTH_TOKEN_REFRESH_URL, JSON.stringify({ refreshToken: refreshToken2 }) ); const newAccessToken = data.accessToken; const newRefreshToken = data.refreshToken; if (!newAccessToken || !newRefreshToken) { throw new Error( "Error refreshing token, API response: " + JSON.stringify(data) ); } return { newAccessToken, newRefreshToken }; }; // lib/transfers.ts import { parseUnits as parseUnits2 } from "ethers"; // lib/marketplace/approve.ts import { parseUnits } from "ethers"; async function approveMarketplaceContract(signer) { const axieContract = getAxieContract(signer); const marketplaceContract = getMarketplaceContract(); const address = await signer.getAddress(); const marketplaceAddress = await marketplaceContract.getAddress(); let isApproved = await axieContract.isApprovedForAll( address, marketplaceAddress ); if (!isApproved) { const tx = await axieContract.setApprovalForAll(marketplaceAddress, true, { gasPrice: parseUnits("26", "gwei") }); const receipt = await tx.wait(); } return isApproved; } async function approveWETH(signer) { const address = await signer.getAddress(); const wethContract = getWETHContract(signer); const marketplaceContract = getMarketplaceContract(); const marketplaceAddress = await marketplaceContract.getAddress(); const currentAllowance = await wethContract.allowance( address, marketplaceAddress ); if (currentAllowance === 0n) { const amountToApprove = "115792089237316195423570985008687907853269984665640564039457584007913129639935"; const txApproveWETH = await wethContract.approve( marketplaceAddress, amountToApprove, { gasPrice: parseUnits("26", "gwei") } ); const txApproveReceipt = await txApproveWETH.wait(); } return currentAllowance; } async function approveBatchTransfer(signer, batchTransferAddress) { const address = await signer.getAddress(); const axieContract = getAxieContract(signer); const isApproved = await axieContract.isApprovedForAll( address, batchTransferAddress ); if (!isApproved) { const approveTx = await axieContract.setApprovalForAll( batchTransferAddress, true, { gasPrice: parseUnits("26", "gwei") } ); await approveTx.wait(); } } async function approveMaterialMarketplace(signer) { const materialContract = getMaterialContract(signer); const marketplaceContract = getMarketplaceContract(); const address = await signer.getAddress(); const marketplaceAddress = await marketplaceContract.getAddress(); let isApproved = await materialContract.isApprovedForAll( address, marketplaceAddress ); if (!isApproved) { const tx = await materialContract.setApprovalForAll( marketplaceAddress, true, { gasPrice: parseUnits("26", "gwei") } ); const receipt = await tx.wait(); } return isApproved; } // lib/transfers.ts async function transferAxie(signer, addressTo, axieId) { const addressFrom = await signer.getAddress(); const writeAxieContract = getAxieContract(signer); const formattedAxieId = typeof axieId === "string" ? axieId : axieId.toString(); const tx = await writeAxieContract["safeTransferFrom(address,address,uint256)"]( addressFrom, addressTo.replace("ronin:", "0x").toLowerCase(), formattedAxieId, { gasPrice: parseUnits2("26", "gwei") } ); const receipt = await tx.wait(); return receipt; } async function batchTransferAxies(signer, addressTo, axieIds) { const writeBatchTransferContract = getBatchTransferContract(signer); const writeAxieContract = getAxieContract(signer); const batchTransferAddress = await writeBatchTransferContract.getAddress(); const axieContractAddress = await writeAxieContract.getAddress(); await approveBatchTransfer(signer, batchTransferAddress); const addressFrom = await signer.getAddress(); const axies = axieIds.map((axieId) => { return typeof axieId === "string" ? axieId : axieId.toString(); }); if (axies.length === 0) { throw new Error("You must provide at least one axie ID"); } const normalizedAddressTo = addressTo.replace("ronin:", "").toLowerCase(); const finalAddressTo = normalizedAddressTo.startsWith("0x") ? normalizedAddressTo : `0x${normalizedAddressTo}`; const tx = await writeBatchTransferContract["safeBatchTransfer(address,uint256[],address)"](axieContractAddress, axies, finalAddressTo, { gasPrice: parseUnits2("26", "gwei") }); const receipt = await tx.wait(); return receipt; } // lib/marketplace/cancel-order.ts import { AbiCoder as AbiCoder3, Interface as Interface2, parseUnits as parseUnits3 } from "ethers"; import APP_AXIE_ORDER_EXCHANGE from "@roninbuilders/contracts/app_axie_order_exchange"; async function cancelMarketplaceOrder(axieId, signer, skyMavisApiKey, order) { let orderToCancel = order; if (!orderToCancel) { const query = ` query GetAxieDetail($axieId: ID!) { axie(axieId: $axieId) { id order { ... on Order { id maker kind assets { ... on Asset { erc address id quantity orderId } } expiredAt paymentToken startedAt basePrice endedAt endedPrice expectedState nonce marketFeePercentage signature hash duration timeLeft currentPrice suggestedPrice currentPriceUsd } } } } `; const variables = { axieId }; const { graphqlUrl, headers } = getMarketplaceApi(skyMavisApiKey); const result = await apiRequest( graphqlUrl, JSON.stringify({ query, variables }), headers ); if (result === null || result.data === void 0 || result.data.axie.order == null) { throw new Error(`Could not find an active order for Axie ID: ${axieId}`); } orderToCancel = result.data.axie.order; } const orderData = [ orderToCancel.maker, orderToCancel.kind === "Sell" ? 1 : 0, [ [ orderToCancel.assets[0].erc === "Erc721" ? 1 : 0, orderToCancel.assets[0].address, +orderToCancel.assets[0].id, +orderToCancel.assets[0].quantity ] ], orderToCancel.expiredAt, orderToCancel.paymentToken, orderToCancel.startedAt, orderToCancel.basePrice, orderToCancel.endedAt, orderToCancel.endedPrice, orderToCancel.expectedState || 0, orderToCancel.nonce, orderToCancel.marketFeePercentage ]; const encodedOrderData = AbiCoder3.defaultAbiCoder().encode( [ "(address maker, uint8 kind, (uint8 erc,address addr,uint256 id,uint256 quantity)[] assets, uint256 expiredAt, address paymentToken, uint256 startedAt, uint256 basePrice, uint256 endedAt, uint256 endedPrice, uint256 expectedState, uint256 nonce, uint256 marketFeePercentage)" ], [orderData] ); const axieOrderExchangeInterface = new Interface2(APP_AXIE_ORDER_EXCHANGE.abi); const orderExchangePayload = axieOrderExchangeInterface.encodeFunctionData( "cancelOrder", [encodedOrderData] ); const marketGatewayContract = getMarketplaceContract(signer); console.time("Transaction Send and Wait"); const tx = await marketGatewayContract.interactWith( "ORDER_EXCHANGE", orderExchangePayload, { gasPrice: parseUnits3("26", "gwei") } ); const receipt = await tx.wait(); console.timeEnd("Transaction Send and Wait"); return receipt; } // lib/marketplace/cancel-material-order.ts import { parseUnits as parseUnits4 } from "ethers"; async function cancelMaterialOrder(materialId, signer, skyMavisApiKey) { const userAddress = await signer.getAddress(); const { graphqlUrl, headers } = getMarketplaceApi(skyMavisApiKey); const variables = { tokenType: "Material", tokenId: materialId, owner: userAddress, skipId: true }; const result = await apiRequest( graphqlUrl, JSON.stringify({ operationName: "GetErc1155DetailByOwner", query: MATERIAL_QUERIES.GET_MATERIAL_BY_OWNER, variables }), headers ); if (!result.data?.erc1155ByOwner?.orders?.data) { return { totalOrders: 0, canceled: 0, failed: 0, canceledOrders: [], failedCancellations: [], message: "No orders found to cancel" }; } const userOrders = result.data.erc1155ByOwner.orders.data; const canceledOrders = []; const failedCancellations = []; for (const order of userOrders) { try { const encodedOrderData = encodeMaterialOrderData(order); const marketGatewayContract = getMarketplaceContract(signer); const ERC1155_EXCHANGE_CONTRACT = getERC1155ExchangeContract(); const cancelOrderPayload = ERC1155_EXCHANGE_CONTRACT.interface.encodeFunctionData("cancelOrder", [ encodedOrderData ]); const tx = await marketGatewayContract.interactWith( "ERC1155_EXCHANGE", cancelOrderPayload, { gasPrice: parseUnits4("26", "gwei"), gasLimit: 11e4 } ); const receipt = await tx.wait(); canceledOrders.push({ orderId: order.id, transactionHash: receipt.hash, quantity: order.assets[0]?.quantity || "0", price: order.currentPrice }); } catch (error) { failedCancellations.push({ orderId: order.id, error: error.message }); } } const summary = { totalOrders: userOrders.length, canceled: canceledOrders.length, failed: failedCancellations.length, canceledOrders, failedCancellations }; return summary; } // lib/marketplace/create-order.ts var axieOrderTypes = { Asset: [ { name: "erc", type: "uint8" }, { name: "addr", type: "address" }, { name: "id", type: "uint256" }, { name: "quantity", type: "uint256" } ], Order: [ { name: "maker", type: "address" }, { name: "kind", type: "uint8" }, { name: "assets", type: "Asset[]" }, { name: "expiredAt", type: "uint256" }, { name: "paymentToken", type: "address" }, { name: "startedAt", type: "uint256" }, { name: "basePrice", type: "uint256" }, { name: "endedAt", type: "uint256" }, { name: "endedPrice", type: "uint256" }, { name: "expectedState", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "marketFeePercentage", type: "uint256" } ] }; async function createMarketplaceOrder(orderData, accessToken, signer, skyMavisApiKey) { const { address, axieId, basePrice, endedPrice, startedAt, endedAt, expiredAt } = orderData; const AXIE_CONTRACT_ADDRESS = await getAxieContract().getAddress(); const WETH_CONTRACT_ADDRESS = await getWETHContract().getAddress(); const MARKETPLACE_CONTRACT_ADDRESS = await getMarketplaceContract().getAddress(); const domain = { name: "MarketGateway", version: "1", chainId: "2020", // ✅ CRITICAL FIX: Use string "2020" not number 2020 verifyingContract: MARKETPLACE_CONTRACT_ADDRESS // 0x3b3adf1422f84254b7fbb0e7ca62bd0865133fe3 }; const marketGatewayContract = getMarketplaceContract(signer); const nonce = await marketGatewayContract.makerNonce(address); const orderToSign = { maker: address, kind: "1", // Sell order assets: [ { erc: "1", // ERC721 addr: AXIE_CONTRACT_ADDRESS, id: axieId, quantity: "0" // ERC721 quantity is 0 for API compatibility } ], expiredAt: expiredAt.toString(), paymentToken: WETH_CONTRACT_ADDRESS, startedAt: startedAt.toString(), basePrice, endedAt: endedAt.toString(), endedPrice, expectedState: "0", nonce: nonce.toString(), marketFeePercentage: "425" // Standard 4.25% fee }; const signature = await signer.signTypedData( domain, axieOrderTypes, orderToSign ); const query = ` mutation CreateOrder($order: InputOrder!, $signature: String!) { createOrder(order: $order, signature: $signature) { ...OrderInfo __typename } } fragment OrderInfo on Order { ...PartialOrderFields makerProfile { name addresses { ronin __typename } __typename } assets { erc address id quantity orderId __typename } __typename } fragment PartialOrderFields on Order { id maker kind expiredAt paymentToken startedAt basePrice endedAt endedPrice expectedState nonce marketFeePercentage signature hash duration timeLeft currentPrice suggestedPrice currentPriceUsd assets { erc address id quantity orderId __typename } __typename } `; const variables = { order: { maker: address, nonce: Number(nonce), assets: [ { id: axieId, address: AXIE_CONTRACT_ADDRESS, erc: "Erc721", quantity: "0" // API expects "0" for ERC721 } ], kind: "Sell", expectedState: "", basePrice, endedPrice, startedAt, endedAt, expiredAt }, signature }; const { graphqlUrl, headers: apiHeaders } = getMarketplaceApi(skyMavisApiKey); const headers = { ...apiHeaders, authorization: `Bearer ${accessToken}` }; const result = await apiRequest( graphqlUrl, JSON.stringify({ operationName: "CreateOrder", query, variables }), headers ); return result; } // lib/marketplace/create-material-order.ts var materialOrderTypes = { Asset: [ { name: "erc", type: "uint8" }, { name: "addr", type: "address" }, { name: "id", type: "uint256" }, { name: "quantity", type: "uint256" } ], ERC1155Order: [ { name: "maker", type: "address" }, { name: "kind", type: "uint8" }, { name: "asset", type: "Asset" }, { name: "expiredAt", type: "uint256" }, { name: "paymentToken", type: "address" }, { name: "startedAt", type: "uint256" }, { name: "unitPrice", type: "uint256" }, { name: "endedAt", type: "uint256" }, { name: "endedUnitPrice", type: "uint256" }, { name: "expectedState", type: "uint256" }, { name: "nonce", type: "uint256" } ] }; async function createMaterialMarketplaceOrder(orderData, accessToken, signer, skyMavisApiKey, options) { const { address, materialId, quantity: inputQuantity, unitPrice, endedUnitPrice, startedAt, endedAt, expiredAt } = orderData; const MATERIAL_CONTRACT_ADDRESS = await getMaterialContract().getAddress(); const WETH_CONTRACT_ADDRESS = await getWETHContract().getAddress(); const MARKETPLACE_CONTRACT_ADDRESS = await getMarketplaceContract().getAddress(); const domain = { name: "MarketGateway", version: "1", chainId: "2020", verifyingContract: MARKETPLACE_CONTRACT_ADDRESS }; console.log(`\u{1F50D} Checking ownership for address: ${address}`); const ownership = await checkMaterialOwnership( materialId, address, skyMavisApiKey, accessToken ); if (!ownership) { throw new Error( `\u274C Unable to verify ownership of material ${materialId}. Please check if you own this material.` ); } const ownedQuantity = ownership.total; if (ownedQuantity === 0) { throw new Error( `\u274C You don't own any of material ${materialId}. You need to own at least 1 to create an order.` ); } let quantity; if (!inputQuantity) { quantity = ownedQuantity.toString(); } else { quantity = inputQuantity; if (parseInt(quantity) > ownedQuantity) { throw new Error( `\u274C Insufficient quantity! \u2022 Requested: ${quantity} \u2022 Owned: ${ownedQuantity} \u2022 Cannot list more materials than you own.` ); } } const orderToSign = { maker: address.replace("ronin:", "0x"), // Use Ethereum address format for signing kind: "1", // Sell order asset: { // Note: 'asset' (singular) like working example erc: "2", // ERC1155 addr: MATERIAL_CONTRACT_ADDRESS, id: materialId, quantity }, expiredAt: expiredAt.toString(), paymentToken: WETH_CONTRACT_ADDRESS, startedAt: startedAt.toString(), unitPrice, // Use unitPrice like working example endedAt: "0", // Fixed price listing endedUnitPrice: "0", expectedState: "0", nonce: options?.nonce || "0" }; const provider = createProvider(skyMavisApiKey); const signerWithProvider = signer.connect(provider); const signature = await signerWithProvider.signTypedData( domain, materialOrderTypes, orderToSign ); const query = ` mutation CreateOrder($order: InputOrder!, $signature: String!) { createOrder(order: $order, signature: $signature) { ...OrderInfo __typename } } fragment OrderInfo on Order { ...PartialOrderFields makerProfile { name addresses { ronin __typename } __typename } assets { erc address id quantity orderId __typename } __typename } fragment PartialOrderFields on Order { id maker kind expiredAt paymentToken startedAt basePrice endedAt endedPrice expectedState nonce marketFeePercentage signature hash duration timeLeft currentPrice suggestedPrice currentPriceUsd assets { erc address id quantity orderId __typename } __typename } `; const variables = { order: { maker: address.replace("ronin:", "0x"), // Use 0x format for API nonce: 0, // Hardcoded as per working example assets: [ { id: materialId, address: MATERIAL_CONTRACT_ADDRESS, erc: "Erc1155", quantity } ], kind: "Sell", expectedState: "", basePrice: unitPrice, // API uses basePrice endedPrice: "0", // Fixed price listing startedAt, endedAt: 0, // Fixed price listing expiredAt }, signature }; const { graphqlUrl, headers: apiHeaders } = getMarketplaceApi(skyMavisApiKey); const headers = { ...apiHeaders, authorization: `Bearer ${accessToken}` }; const result = await apiRequest( graphqlUrl, JSON.stringify({ operationName: "CreateOrder", query, variables }), headers ); return result; } // lib/marketplace/settle-order.ts import { Interface as Interface3, parseUnits as parseUnits5 } from "ethers"; import APP_AXIE_ORDER_EXCHANGE2 from "@roninbuilders/contracts/app_axie_order_exchange"; async function buyMarketplaceOrder(axieId, signer, accessToken, skyMavisApiKey, existingOrder) { try { let order = existingOrder; if (!order) { order = await getAxieDetails(axieId, accessToken, skyMavisApiKey); } if (!order) { console.error("No order found for axie", axieId); return false; } const address = await signer.getAddress(); const wethContract = getWETHContract(signer); const wethBalance = await wethContract.balanceOf(address); if (BigInt(wethBalance) < BigInt(order.currentPrice)) { console.error("Insufficient WETH balance"); return false; } const contract = getMarketplaceContract(signer); const encodedOrderData = encodeAxieOrderData(order); const referralAddr = "0xa7d8ca624656922c633732fa2f327f504678d132"; const settleInfo = { orderData: encodedOrderData, signature: order.signature, referralAddr, expectedState: BigInt(0), recipient: address, refunder: address }; const axieOrderExchangeInterface = new Interface3( APP_AXIE_ORDER_EXCHANGE2.abi ); const orderExchangeData = axieOrderExchangeInterface.encodeFunctionData( "settleOrder", [settleInfo, BigInt(order.currentPrice)] ); const txBuyAxie = await contract.interactWith( "ORDER_EXCHANGE", orderExchangeData, { gasPrice: parseUnits5("50", "gwei"), gasLimit: 1e6 } ); const receipt = await txBuyAxie.wait(); if (receipt?.status === 0) { return false; } return receipt; } catch (error) { console.error("Error settling order:", error); return false; } } // lib/marketplace/settle-material-order.ts import { AbiCoder as AbiCoder4, parseUnits as parseUnits6 } from "ethers"; async function buyMaterialOrder(materialId, quantity, signer, accessToken, skyMavisApiKey) { const query = MATERIAL_QUERIES.GET_MATERIAL_ORDERS; const variables = { tokenType: "Material", tokenId: materialId, from: 0, size: 50, sort: "PriceAsc" }; const { graphqlUrl, headers: apiHeaders } = getMarketplaceApi(skyMavisApiKey); const headers = { ...apiHeaders, authorization: `Bearer ${accessToken}` }; try { const results = await apiRequest( graphqlUrl, JSON.stringify({ operationName: "GetBuyNowErc1155Orders", query, variables }), headers ); const orders = results.data?.erc1155Token?.orders?.data || []; if (!orders || orders.length === 0) { return false; } const address = await signer.getAddress(); const validOrders = orders.filter((order2) => { const isExpired = order2.expiredAt * 1e3 <= Date.now(); const hasQuantity = order2.assets?.[0]?.availableQuantity && parseInt(order2.assets[0].availableQuantity) > 0; const isSelfOrder = order2.maker.toLowerCase() === address.toLowerCase(); return !isExpired && hasQuantity && !isSelfOrder; }); if (validOrders.length === 0) { return false; } const sortedValidOrders = validOrders.sort((a, b) => { const priceA = parseFloat(a.currentPrice); const priceB = parseFloat(b.currentPrice); return priceA - priceB; }); const ordersToTry = sortedValidOrders.slice(3); let order = null; for (const candidateOrder of ordersToTry) { const availableQuantity = parseInt( candidateOrder.assets[0].availableQuantity ); if (quantity <= availableQuantity) { order = candidateOrder; break; } } if (!order) { return false; } const wethContract = getWETHContract(signer); const wethBalance = await wethContract.balanceOf(address); const totalCost = BigInt(order.currentPrice) * BigInt(quantity); if (BigInt(wethBalance) < totalCost) { return false; } const erc1155ExchangeAddress = "0xb36c9027ed4353fdd7a59d8c40e0df5221a3764f"; const allowance = await wethContract.allowance( address, erc1155ExchangeAddress ); if (BigInt(allowance) < totalCost) { const approveTx = await wethContract.approve( erc1155ExchangeAddress, totalCost ); await approveTx.wait(); } const orderTypes = [ "(address,uint8,uint8,address,uint256,uint256,uint256,address,uint256,uint256,uint256,uint256,uint256,uint256,uint256)" ]; const orderData = [ [ order.maker, // address maker 1, // uint8 kind (sell = 1) 2, // uint8 erc type (ERC1155 = 2) order.assets[0].address, // address asset contract BigInt(order.assets[0].id), // uint256 asset id BigInt(order.assets[0].quantity), // uint256 original order quantity BigInt(order.expiredAt), // uint256 expiredAt order.paymentToken, // address paymentToken BigInt(order.startedAt), // uint256 startedAt BigInt(order.basePrice), // uint256 basePrice BigInt(order.endedAt || 0), // uint256 endedAt BigInt(order.endedPrice || 0), // uint256 endedPrice BigInt(order.expectedState || 0), // uint256 expectedState BigInt(order.nonce || 0), // uint256 nonce BigInt(order.marketFeePercentage || 425) // uint256 marketFeePercentage ] ]; const encodedOrderData = AbiCoder4.defaultAbiCoder().encode( orderTypes, orderData ); const referralAddr = "0xa7d8ca624656922c633732fa2f327f504678d132"; const parsedExpectedState = order.expectedState && order.expectedState !== "" ? BigInt(order.expectedState) : 0n; const settleInfo = { orderData: encodedOrderData, signature: order.signature, referralAddr, expectedState: parsedExpectedState, recipient: address, refunder: address }; try { const ERC1155_EXCHANGE_CONTRACT = getERC1155ExchangeContract(); const settleInfoTuple = [ settleInfo.orderData, settleInfo.signature, settleInfo.referralAddr, settleInfo.expectedState, settleInfo.recipient, settleInfo.refunder ]; const totalSettlePrice = BigInt(order.currentPrice) * BigInt(quantity); const orderExchangeData = ERC1155_EXCHANGE_CONTRACT.interface.encodeFunctionData("settleOrder", [ settleInfoTuple, BigInt(quantity), totalSettlePrice ]); const marketplaceContract = getMarketplaceContract(signer); const gatewayGasEstimate = await marketplaceContract.interactWith.estimateGas( "ERC1155_EXCHANGE", orderExchangeData, { gasPrice: parseUnits6("26", "gwei") } ); const gatewayTx = await marketplaceContract.interactWith( "ERC1155_EXCHANGE", orderExchangeData, { gasPrice: parseUnits6("26", "gwei"), gasLimit: Math.min(Number(gatewayGasEstimate) + 5e4, 6e5) } ); const gatewayReceipt = await gatewayTx.wait(); if (gatewayReceipt?.status === 1) { return gatewayReceipt; } else { return false; } } catch (gatewayError) { return false; } } catch (error) { return false; } } export { approveBatchTransfer, approveMarketplaceContract, approveMaterialMarketplace, approveWETH, askToContinue, batchTransferAxies, buyMarketplaceOrder, buyMaterialOrder, cancelMarketplaceOrder, cancelMaterialOrder, createMarketplaceOrder, createMaterialMarketplaceOrder, createProvider, ensureMarketplaceToken, getAccountInfo, getAxieContract, getAxieFloorPrice, getAxieIdsFromAccount, getMaterialFloorPrice, getUSDCContract, getWETHContract, refreshToken, transferAxie, validateMaterialToken };