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
JavaScript
// 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
};