UNPKG

axie-tools

Version:

TypeScript SDK for building Axie Infinity trading bots and AI agents on Ronin network. Programmatic marketplace operations for Axies, Materials, and Consumables (buy/sell/delist), batch transfers, floor price detection, and wallet management. Includes int

1,707 lines (1,684 loc) 71.8 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 AXIE_CONSUMABLE_ERC_1155_PROXY from "@roninbuilders/contracts/axie_consumable_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"; import MULTICALL_3 from "@roninbuilders/contracts/multicall_3"; 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 getConsumableContract(signerOrProvider) { const address = AXIE_CONSUMABLE_ERC_1155_PROXY.address; const abi = new Interface(AXIE_CONSUMABLE_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); } function getMulticall3Contract(signerOrProvider) { const address = MULTICALL_3.address; const abi = new Interface(MULTICALL_3.abi); return new Contract(address, abi, signerOrProvider); } // lib/utils.ts import prompts from "prompts"; import { JsonRpcProvider, formatEther, parseUnits } 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 async function getGasPrice(signerOrProvider, options) { if (options?.gasPrice !== void 0) { return options.gasPrice; } let provider; if ("provider" in signerOrProvider && signerOrProvider.provider) { provider = signerOrProvider.provider; } else { provider = signerOrProvider; } try { const feeData = await provider.getFeeData(); if (feeData.gasPrice) { return feeData.gasPrice; } } catch (error) { console.warn( "Could not fetch gas price from network, using fallback.", error ); } return parseUnits("26", "gwei"); } var DEFAULT_RONIN_RPC_URL = "https://api.roninchain.com/rpc"; function createProvider(_skyMavisApiKey, rpcUrl = process.env.RONIN_RPC_URL ?? DEFAULT_RONIN_RPC_URL) { return new JsonRpcProvider(rpcUrl); } 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 multicall3Contract = getMulticall3Contract(provider); const axiesBalance = await axieContract.balanceOf(address); if (axiesBalance === 0n) { return []; } const balanceNum = Number(axiesBalance); const axieContractAddress = await axieContract.getAddress(); const calls = Array.from({ length: balanceNum }, (_, i) => ({ target: axieContractAddress, callData: axieContract.interface.encodeFunctionData("tokenOfOwnerByIndex", [ address, i ]) })); const results = await multicall3Contract.tryAggregate.staticCall( false, calls ); const axieIds = []; for (const result of results) { if (result.success) { try { const decoded = axieContract.interface.decodeFunctionResult( "tokenOfOwnerByIndex", result.returnData ); axieIds.push(Number(decoded[0])); } 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 decodeJWT = (token) => { const parts = token.split("."); if (parts.length !== 3) throw new Error("Invalid JWT format"); const payload = parts[1]; const decoded = Buffer.from(payload, "base64").toString("utf-8"); return JSON.parse(decoded); }; var getTokenExpirationInfo = (token) => { const payload = decodeJWT(token); if (!payload.exp) throw new Error("Token missing exp claim"); const expiresAt = new Date(payload.exp * 1e3); const now = Date.now(); const expiresInMs = expiresAt.getTime() - now; const expiresInSeconds = Math.floor(expiresInMs / 1e3); const isExpired = expiresInSeconds <= 0; let humanReadable; if (isExpired) { const pastSeconds = Math.abs(expiresInSeconds); const hours = Math.floor(pastSeconds / 3600); const minutes = Math.floor(pastSeconds % 3600 / 60); humanReadable = hours > 0 ? `expired ${hours}h ${minutes}m ago` : `expired ${minutes}m ago`; } else { const hours = Math.floor(expiresInSeconds / 3600); const minutes = Math.floor(expiresInSeconds % 3600 / 60); humanReadable = hours > 0 ? `expires in ${hours}h ${minutes}m` : `expires in ${minutes}m`; } return { expiresAt, expiresIn: expiresInSeconds, isExpired, humanReadable }; }; 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) ); } const expirationInfo = getTokenExpirationInfo(newAccessToken); return { newAccessToken, newRefreshToken, expirationInfo }; }; // lib/marketplace/approve.ts async function approveMarketplaceContract(signer, options) { 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 gasPrice = await getGasPrice(signer, options); const tx = await axieContract.setApprovalForAll(marketplaceAddress, true, { gasPrice }); const receipt = await tx.wait(); } return isApproved; } async function approveWETH(signer, options) { 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 gasPrice = await getGasPrice(signer, options); const txApproveWETH = await wethContract.approve( marketplaceAddress, amountToApprove, { gasPrice } ); const txApproveReceipt = await txApproveWETH.wait(); } return currentAllowance; } async function approveBatchTransfer(signer, batchTransferAddress, options) { const address = await signer.getAddress(); const axieContract = getAxieContract(signer); const isApproved = await axieContract.isApprovedForAll( address, batchTransferAddress ); if (!isApproved) { const gasPrice = await getGasPrice(signer, options); const approveTx = await axieContract.setApprovalForAll( batchTransferAddress, true, { gasPrice } ); await approveTx.wait(); } } async function approveMaterialMarketplace(signer, options) { 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 gasPrice = await getGasPrice(signer, options); const tx = await materialContract.setApprovalForAll( marketplaceAddress, true, { gasPrice } ); const receipt = await tx.wait(); } return isApproved; } async function approveConsumableMarketplace(signer, options) { const consumableContract = getConsumableContract(signer); const marketplaceContract = getMarketplaceContract(); const address = await signer.getAddress(); const marketplaceAddress = await marketplaceContract.getAddress(); let isApproved = await consumableContract.isApprovedForAll( address, marketplaceAddress ); if (!isApproved) { const gasPrice = await getGasPrice(signer, options); const tx = await consumableContract.setApprovalForAll( marketplaceAddress, true, { gasPrice } ); await tx.wait(); isApproved = true; } return isApproved; } // lib/transfers.ts async function transferAxie(signer, addressTo, axieId, options) { const addressFrom = await signer.getAddress(); const writeAxieContract = getAxieContract(signer); const formattedAxieId = typeof axieId === "string" ? axieId : axieId.toString(); const gasPrice = await getGasPrice(signer, options); const tx = await writeAxieContract["safeTransferFrom(address,address,uint256)"]( addressFrom, addressTo.replace("ronin:", "0x").toLowerCase(), formattedAxieId, { gasPrice } ); const receipt = await tx.wait(); return receipt; } async function batchTransferAxies(signer, addressTo, axieIds, options) { const writeBatchTransferContract = getBatchTransferContract(signer); const writeAxieContract = getAxieContract(signer); const batchTransferAddress = await writeBatchTransferContract.getAddress(); const axieContractAddress = await writeAxieContract.getAddress(); await approveBatchTransfer(signer, batchTransferAddress, options); 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 gasPrice = await getGasPrice(signer, options); const tx = await writeBatchTransferContract["safeBatchTransfer(address,uint256[],address)"](axieContractAddress, axies, finalAddressTo, { gasPrice }); const receipt = await tx.wait(); return receipt; } // lib/consumable.ts import { AbiCoder as AbiCoder3 } from "ethers"; var CONSUMABLE_QUERIES = { GET_CONSUMABLES: ` query GetConsumables($owner: String, $includeMinPrice: Boolean = false, $includeQuantity: Boolean = false) { erc1155Tokens(owner: $owner, tokenType: Consumable, 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_CONSUMABLE_DETAIL: ` query GetConsumableDetail($tokenId: String) { erc1155Token(tokenType: Consumable, 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_CONSUMABLE_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_CONSUMABLE_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_CONSUMABLE_OWNERSHIP: ` query GetErc1155Token($owner: String!, $tokenId: String!) { erc1155Token(tokenType: Consumable, 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 checkConsumableOwnership(consumableId, address, skyMavisApiKey, accessToken) { const { graphqlUrl, headers } = getMarketplaceApi(skyMavisApiKey); const variables = { tokenId: consumableId, owner: address }; const apiHeaders = { ...headers, authorization: `Bearer ${accessToken}` }; try { const result = await apiRequest( graphqlUrl, JSON.stringify({ operationName: "GetErc1155Token", query: CONSUMABLE_QUERIES.GET_CONSUMABLE_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 consumable`); return token; } return token; } catch (error) { console.log(`\u274C Error in checkConsumableOwnership:`, error); return null; } } async function validateConsumableToken(tokenId, skyMavisApiKey) { const variables = { tokenId }; const { graphqlUrl, headers } = getMarketplaceApi(skyMavisApiKey); try { const result = await apiRequest( graphqlUrl, JSON.stringify({ operationName: "GetConsumableDetail", query: CONSUMABLE_QUERIES.GET_CONSUMABLE_DETAIL, variables }), headers ); if (result?.data?.erc1155Token) { return result.data.erc1155Token; } return null; } catch (error) { return null; } } async function getConsumableFloorPrice(consumableId, skyMavisApiKey, requestedQuantity) { const { graphqlUrl, headers } = getMarketplaceApi(skyMavisApiKey); try { const pageSize = 50; let from = 0; let totalOrders = Number.POSITIVE_INFINITY; const quantityNeeded = requestedQuantity ?? 0; let remainingQuantity = quantityNeeded; let totalCost = 0; while (from < totalOrders) { const response = await fetch(graphqlUrl, { method: "POST", headers: { "Content-Type": "application/json", ...headers }, body: JSON.stringify({ query: `query GetErc1155Orders($tokenType: Erc1155Type!, $tokenId: String, $from: Int!, $size: Int!, $sort: SortBy = PriceAsc) { erc1155Token(tokenType: $tokenType, tokenId: $tokenId) { orders(from: $from, size: $size, sort: $sort) { total data { currentPrice expiredAt assets { availableQuantity } } } } }`, variables: { tokenType: "Consumable", tokenId: consumableId, from, size: pageSize, sort: "PriceAsc" } }) }); const { data } = await response.json(); const orders = data?.erc1155Token?.orders?.data || []; totalOrders = data?.erc1155Token?.orders?.total ?? from + orders.length; const validOrders = orders.filter( (order) => order.expiredAt * 1e3 > Date.now() && order.assets?.[0]?.availableQuantity && parseInt(order.assets[0].availableQuantity) > 0 ); if (quantityNeeded <= 0) { const cheapestOrder = validOrders[0]; if (cheapestOrder) { return (Number(cheapestOrder.currentPrice) / 1e18).toFixed(6); } } 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; if (remainingQuantity <= 0) { const averagePrice = totalCost / quantityNeeded; return averagePrice.toFixed(6); } } if (orders.length < pageSize) { break; } from += orders.length; } return null; } catch (error) { return null; } } function encodeConsumableOrderData(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 AbiCoder3.defaultAbiCoder().encode(orderTypes, [orderData]); } // lib/marketplace/cancel-order.ts import { AbiCoder as AbiCoder4, Interface as Interface2 } from "ethers"; import APP_AXIE_ORDER_EXCHANGE from "@roninbuilders/contracts/app_axie_order_exchange"; async function cancelMarketplaceOrder(axieId, signer, skyMavisApiKey, options) { let orderToCancel = options?.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 = AbiCoder4.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); const gasPrice = await getGasPrice(signer, options); console.time("Transaction Send and Wait"); const tx = await marketGatewayContract.interactWith( "ORDER_EXCHANGE", orderExchangePayload, { gasPrice } ); const receipt = await tx.wait(); console.timeEnd("Transaction Send and Wait"); return receipt; } // lib/marketplace/cancel-material-order.ts async function cancelMaterialOrder(materialId, signer, skyMavisApiKey, options) { 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 gasPrice = await getGasPrice(signer, options); const tx = await marketGatewayContract.interactWith( "ERC1155_EXCHANGE", cancelOrderPayload, { gasPrice, 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/cancel-consumable-order.ts async function cancelConsumableOrder(consumableId, signer, skyMavisApiKey, options) { const userAddress = await signer.getAddress(); const { graphqlUrl, headers } = getMarketplaceApi(skyMavisApiKey); const variables = { tokenType: "Consumable", tokenId: consumableId, owner: userAddress, skipId: true }; const result = await apiRequest( graphqlUrl, JSON.stringify({ operationName: "GetErc1155DetailByOwner", query: CONSUMABLE_QUERIES.GET_CONSUMABLE_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 = encodeConsumableOrderData(order); const marketGatewayContract = getMarketplaceContract(signer); const ERC1155_EXCHANGE_CONTRACT = getERC1155ExchangeContract(); const cancelOrderPayload = ERC1155_EXCHANGE_CONTRACT.interface.encodeFunctionData("cancelOrder", [ encodedOrderData ]); const gasPrice = await getGasPrice(signer, options); const tx = await marketGatewayContract.interactWith( "ERC1155_EXCHANGE", cancelOrderPayload, { gasPrice, 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