axie-tools
Version:
TypeScript library and CLI tool for interacting with Axie Infinity marketplace and NFTs on Ronin network. Features marketplace operations (buy/sell/delist), batch transfers, and wallet information.
600 lines (529 loc) ⢠21.6 kB
text/typescript
#!/usr/bin/env node
import { select, input, confirm, password } from "@inquirer/prompts";
import { JsonRpcProvider, Wallet, parseEther, isHexString, isAddress } from "ethers";
import { getAxieIdsFromAccount, delegateAxie, batchDelegateAxies, batchRevokeDelegations, revokeDelegation } from "./lib/axie";
import { refreshToken } from "./lib/marketplace/access-token";
import { batchTransferAxies, transferAxie } from "./lib/transfers";
import {
askToContinue,
ensureMarketplaceToken,
getAccountInfo,
getAxieId,
} from "./lib/utils";
import {
approveMarketplaceContract,
approveWETH,
} from "./lib/marketplace/approve";
import buyMarketplaceOrder from "./lib/marketplace/settle-order";
import cancelMarketplaceOrder from "./lib/marketplace/cancel-order";
import createMarketplaceOrder from "./lib/marketplace/create-order";
import { checkBlessings, isActivated } from "./lib/atia";
import { signDelegation } from "./lib/atia/delegation";
import { delegateAtiaBlessing, getAtiaBlessingDelegations, revokeAtiaBlessingDelegation } from "./lib/atia/graphql";
import "dotenv/config";
async function main() {
// Check for PRIVATE_KEY before the main loop
let privateKey = process.env.PRIVATE_KEY;
if (!privateKey) {
privateKey = await password({
message: "š Enter your private key:",
validate: (value) => {
if (!value) return false;
// Private keys are 32 bytes (64 characters) + optional "0x" prefix
return isHexString(value, 32) || isHexString(`0x${value}`, 32);
},
});
// Ensure "0x" prefix
privateKey = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
}
// Initialize provider with a reliable RPC endpoint
const provider = new JsonRpcProvider("https://api.roninchain.com/rpc");
// Initialize wallet with the previously obtained private key
const wallet = new Wallet(privateKey, provider);
const address = await wallet.getAddress();
while (true) {
try {
const action = await select({
message: "What would you like to do?",
choices: [
{ name: "Get account info", value: "account" },
{ name: "Refresh access token", value: "refresh-token" },
{ name: "Approve WETH", value: "approve-weth" },
{ name: "Approve marketplace", value: "approve-marketplace" },
{ name: "Buy axie", value: "buy" },
{ name: "Delist axie", value: "delist" },
{ name: "Delist multiple axies", value: "delist-all" },
{ name: "List axie", value: "list" },
{ name: "List multiple axies", value: "list-all" },
{ name: "Transfer axie", value: "transfer" },
{ name: "Transfer multiple axies", value: "transfer-all" },
{ name: "Delegate axie", value: "delegate-axie" },
{ name: "Revoke axie delegation", value: "revoke-delegation-axie" },
{ name: "Delegate multiple axies", value: "batch-delegate" },
{ name: "Revoke multiple delegations", value: "batch-revoke" },
{ name: "Check Atia blessing status", value: "check-blessing" },
{ name: "Pray for Atia blessing", value: "pray-blessing" },
{ name: "Create Atia Blessing delegation", value: "delegate" },
{ name: "List Atia Blessing delegations", value: "list-delegations" },
{ name: "Revoke Atia Blessing delegation", value: "revoke-delegation" },
],
});
switch (action) {
case "account": {
const info = await getAccountInfo(address, provider);
console.log(`š¬ Address: ${info.address}`);
console.log("š° RON Balance:", info.ronBalance);
console.log("š° WETH Balance:", info.wethBalance);
console.log("š° USDC Balance:", info.usdcBalance);
console.log(
"š Marketplace WETH allowance:",
info.allowance !== 0n ? "ā
Granted" : "ā Not granted",
);
console.log(
"š Marketplace approval for Axies:",
info.isApprovedForAll ? "ā
Approved" : "ā Not approved",
);
console.log(`š¾ Number of Axies: ${info.axieIds.length}`);
if (info.axieIds.length > 0) {
console.log(`š Axie IDs: ${info.axieIds.join(", ")}`);
}
break;
}
case "refresh-token": {
let refreshTokenValue = process.env.MARKETPLACE_REFRESH_TOKEN;
if (!refreshTokenValue) {
refreshTokenValue = await input({
message: "Enter refresh token",
validate: (value) => value.length > 0,
});
}
const result = await refreshToken(refreshTokenValue);
console.log("New access token:", result.newAccessToken);
console.log("New refresh token:", result.newRefreshToken);
process.env.MARKETPLACE_ACCESS_TOKEN = result.newAccessToken;
process.env.MARKETPLACE_REFRESH_TOKEN = result.newRefreshToken;
break;
}
case "approve-weth": {
await approveWETH(wallet);
break;
}
case "approve-marketplace": {
await approveMarketplaceContract(wallet);
break;
}
case "buy": {
const token = await ensureMarketplaceToken();
const axieId = await getAxieId();
if (!axieId) break;
await approveWETH(wallet);
const receipt = await buyMarketplaceOrder(
axieId,
wallet,
token
);
if (receipt) {
console.log("š Transaction successful! Hash:", receipt.hash);
console.log(
`š View transaction: https://app.roninchain.com/tx/${receipt.hash}`,
);
}
break;
}
case "list": {
const axieId = await getAxieId();
if (!axieId) break;
const token = await ensureMarketplaceToken();
const basePrice = await input({
message: "Enter base price in ETH",
validate: (value) => parseEther(value) > 0n,
});
await approveMarketplaceContract(wallet);
const currentBlock = await provider.getBlock("latest");
const startedAt = currentBlock?.timestamp ?? Math.floor(Date.now() / 1000);
const expiredAt = startedAt + 15634800; // ~6 months
const orderData = {
address,
axieId: axieId.toString(),
basePrice: parseEther(basePrice).toString(),
endedPrice: "0",
startedAt,
endedAt: 0,
expiredAt,
};
const result = await createMarketplaceOrder(
orderData,
token,
wallet
);
if (result === null || result.errors || !result.data) {
console.error(
"ā Error:",
result?.errors?.[0]?.message || "Unknown error",
);
break;
}
console.log(
`ā
Listed Axie ${axieId}! Current price in USD: ${result.data.createOrder.currentPriceUsd}`,
);
break;
}
case "list-all": {
const token = await ensureMarketplaceToken();
const basePrice = await input({
message: "Enter base price in ETH (for all Axies)",
validate: (value) => parseEther(value) > 0n,
});
await approveMarketplaceContract(wallet);
let axieIds = await getAxieIdsFromAccount(address, provider);
if (axieIds.length > 100) {
console.log(
"ā ļø Warning: Can only list up to 100 Axies at once, only listing the first 100",
);
axieIds = axieIds.slice(0, 100);
}
const currentBlock = await provider.getBlock("latest");
const startedAt = currentBlock?.timestamp ?? Math.floor(Date.now() / 1000);
const expiredAt = startedAt + 15634800; // ~6 months
for (const axieId of axieIds) {
const orderData = {
address,
axieId: axieId.toString(),
basePrice: parseEther(basePrice).toString(),
endedPrice: "0",
startedAt,
endedAt: 0,
expiredAt,
};
const result = await createMarketplaceOrder(
orderData, token,
wallet,
);
if (result === null || result.errors || !result.data) {
console.error(
`ā Error listing Axie ${axieId}:`,
result?.errors?.[0]?.message || "Unknown error",
);
continue;
}
console.log(
`ā
Listed Axie ${axieId}! Current price in USD: ${result.data.createOrder.currentPriceUsd}`,
);
}
break;
}
case "delist": {
const axieId = await getAxieId();
if (!axieId) break;
const receipt = await cancelMarketplaceOrder(
axieId,
wallet
);
if (receipt) {
console.log("ā
Axie delisted! Transaction hash:", receipt.hash);
console.log(
`š View transaction: https://app.roninchain.com/tx/${receipt.hash}`,
);
}
break;
}
case "delist-all": {
const axieIdsInput = await input({
message: "Enter comma-separated Axie IDs to delist (e.g. 123, 456, 789)",
validate: (value) => {
const ids = value.split(",").map(id => id.trim());
return ids.length > 0 && ids.every(id => !Number.isNaN(Number(id)));
},
});
let axieIds = axieIdsInput.split(",").map(id => id.trim());
if (axieIds.length > 100) {
console.log(
"ā ļø Warning: Can only delist up to 100 Axies at once, only delisting the first 100",
);
axieIds = axieIds.slice(0, 100);
}
const receipt = await batchTransferAxies(
wallet,
await wallet.getAddress(),
axieIds,
);
if (receipt) {
console.log("ā
Axies delisted! Transaction hash:", receipt.hash);
console.log(
`š View transaction: https://app.roninchain.com/tx/${receipt.hash}`,
);
} else {
console.log("ā Error delisting Axies");
}
break;
}
case "transfer": {
const axieId = await getAxieId();
if (!axieId) break;
const address = await input({
message: "Enter recipient address",
validate: (value) => value.length > 0,
});
const receipt = await transferAxie(wallet, address, axieId);
if (receipt) {
console.log("ā
Axie transferred! Transaction hash:", receipt.hash);
}
break;
}
case "transfer-all": {
const axieIdsInput = await input({
message: "Enter comma-separated Axie IDs (e.g. 123, 456, 789)",
validate: (value) => {
const ids = value.split(",").map(id => id.trim());
return ids.length > 0 && ids.every(id => !Number.isNaN(Number(id)));
},
});
let axieIds = axieIdsInput.split(",").map(id => id.trim());
if (axieIds.length > 100) {
console.log(
"ā ļø Warning: Can only transfer up to 100 Axies at once, only transferring the first 100",
);
axieIds = axieIds.slice(0, 100);
}
const address = await input({
message: "Enter recipient address",
validate: (value) => value.length > 0,
});
const receipt = await batchTransferAxies(wallet, address, axieIds);
if (receipt) {
console.log(
"ā
Axies transferred! Transaction hash:",
receipt.hash,
);
}
break;
}
case "delegate-axie": {
const axieId = await getAxieId();
if (!axieId) break;
const address = await input({
message: "Enter delegatee address",
validate: (value) => value.length > 0,
});
const receipt = await delegateAxie(wallet, axieId, address);
if (receipt) {
console.log("ā
Axie delegated! Transaction hash:", receipt.hash);
console.log(
`š View transaction: https://app.roninchain.com/tx/${receipt.hash}`,
);
}
break;
}
case "revoke-delegation-axie": {
const axieId = await getAxieId();
if (!axieId) break;
console.log("š Starting delegation revocation process (this requires multiple transactions)...");
const receipt = await revokeDelegation(wallet, axieId);
if (receipt) {
console.log("ā
Axie delegation fully revoked! Final transaction hash:", receipt.hash);
console.log(
`š View transaction: https://app.roninchain.com/tx/${receipt.hash}`,
);
}
break;
}
case "batch-delegate": {
const axieIdsInput = await input({
message: "Enter comma-separated Axie IDs to delegate (e.g. 123, 456, 789)",
validate: (value) => {
const ids = value.split(",").map(id => id.trim());
return ids.length > 0 && ids.every(id => !Number.isNaN(Number(id)));
},
});
let axieIds = axieIdsInput.split(",").map(id => id.trim());
if (axieIds.length === 0) {
console.log("ā No Axie IDs provided");
break;
}
if (axieIds.length > 100) {
console.log(
"ā ļø Warning: Can only delegate up to 100 Axies at once, only delegating the first 100",
);
axieIds = axieIds.slice(0, 100);
}
const address = await input({
message: "Enter delegatee address",
validate: (value) => value.length > 0,
});
console.log("\nš Starting batch delegation process...");
try {
const receipt = await batchDelegateAxies(wallet, axieIds, address);
if (receipt) {
console.log("\nā
Batch operation completed! Final transaction hash:", receipt.hash);
console.log(
`š View transaction: https://app.roninchain.com/tx/${receipt.hash}`,
);
}
} catch (error: unknown) {
console.log("\nā Some operations failed. Check the logs above for details.");
}
break;
}
case "batch-revoke": {
const axieIdsInput = await input({
message: "Enter comma-separated Axie IDs to revoke delegation (e.g. 123, 456, 789)",
validate: (value) => {
const ids = value.split(",").map(id => id.trim());
return ids.length > 0 && ids.every(id => !Number.isNaN(Number(id)));
},
});
let axieIds = axieIdsInput.split(",").map(id => id.trim());
if (axieIds.length === 0) {
console.log("ā No Axie IDs provided");
break;
}
if (axieIds.length > 100) {
console.log(
"ā ļø Warning: Can only revoke up to 100 Axies at once, only revoking the first 100",
);
axieIds = axieIds.slice(0, 100);
}
console.log("\nš Starting batch revocation process...");
console.log("This will:");
console.log("1. Check and approve delegation contract if needed");
console.log("2. Request detachment for each Axie");
console.log("3. Perform the revocation\n");
try {
const receipt = await batchRevokeDelegations(wallet, axieIds);
if (receipt) {
console.log("\nā
Batch operation completed! Final transaction hash:", receipt.hash);
console.log(
`š View transaction: https://app.roninchain.com/tx/${receipt.hash}`,
);
}
} catch (error: unknown) {
console.log("\nā Some operations failed. Check the logs above for details.");
}
break;
}
case "check-blessing": {
const address = await input({
message: "Enter address to check blessing status (press Enter to use your address)",
validate: () => true,
});
const targetAddress = address || wallet.address;
const { status, streak } = await isActivated(targetAddress, provider);
if (status) {
console.log(`ā
Address ${targetAddress.slice(-4)} has already prayed today (Current streak: ${streak})`);
} else {
console.log(`ā Address ${targetAddress.slice(-4)} has not prayed today (Current streak: ${streak})`);
}
break;
}
case "pray-blessing": {
const token = await ensureMarketplaceToken();
await checkBlessings(wallet, token);
break;
}
case "delegate": {
const token = await ensureMarketplaceToken();
if (!token) {
console.log("ā No marketplace token available. Please refresh token first.");
break;
}
const toAddress = await input({
message: "Enter delegatee address:",
validate: (value) => isAddress(value),
});
const percentage = await input({
message: "Enter delegator slips percentage (default: 100):",
validate: (value) => {
if (value === "") return true;
const num = Number.parseInt(value);
return !Number.isNaN(num) && num > 0 && num <= 100;
},
default: "100"
});
const delegatedAt = Math.floor(Date.now() / 1000);
console.log("\nš Creating delegation signature...");
const { message, signature } = await signDelegation(
wallet,
toAddress,
delegatedAt,
Number.parseInt(percentage)
);
console.log("ā
Signature created!");
console.log("\nš Message:", message);
console.log("āļø Signature:", signature);
console.log("\nš Submitting delegation...");
const success = await delegateAtiaBlessing(
token,
signature,
wallet.address,
toAddress,
delegatedAt,
Number.parseInt(percentage)
);
if (success) {
console.log("ā
Delegation submitted successfully!");
} else {
console.log("ā Failed to submit delegation");
}
break;
}
case "revoke-delegation": {
const token = await ensureMarketplaceToken();
if (!token) {
console.log("ā No marketplace token available. Please refresh token first.");
break;
}
const toAddress = await input({
message: "Enter delegatee address to revoke:",
validate: (value) => isAddress(value),
});
console.log("\nš Revoking delegation...");
const success = await revokeAtiaBlessingDelegation(
token,
wallet.address, // userAddress
toAddress,
wallet.address // fromAddress
);
if (success) {
console.log("ā
Delegation revoked successfully!");
} else {
console.log("ā Failed to revoke delegation");
}
break;
}
case "list-delegations": {
const token = await ensureMarketplaceToken();
if (!token) {
console.log("ā No marketplace token available. Please refresh token first.");
break;
}
console.log("\nš Checking delegations...");
const delegations = await getAtiaBlessingDelegations(token, wallet.address);
if (delegations.length === 0) {
console.log("ā¹ļø No delegations found");
break;
}
console.log("\nš Delegations:");
for (const delegation of delegations) {
const fromName = delegation.fromProfile?.name || delegation.fromAddress.slice(-4);
const toName = delegation.toProfile?.name || delegation.toAddress.slice(-4);
console.log(`\nš From: ${fromName} (${delegation.fromAddress})`);
console.log(` To: ${toName} (${delegation.toAddress})`);
console.log(` Percentage: ${delegation.delegatorSlipsPercent}%`);
console.log(` Last Prayed: ${delegation.lastPrayedAt ? new Date(delegation.lastPrayedAt).toLocaleString() : 'Never'}`);
console.log(` Delegated At: ${new Date(delegation.delegatedAt * 1000).toLocaleString()}`);
}
break;
}
}
} catch (error) {
if (error instanceof Error) {
console.error("ā Error:", error.message);
} else {
console.error("ā Error:", error);
}
} finally {
await askToContinue();
}
}
}
main();