Integration Guide
Setup
Install Dependencies
bash
npm install ethers@6Network Configuration
typescript
import { ethers } from "ethers";
const MAINNET_RPC = "https://api.avax.network/ext/bc/C/rpc";
const TESTNET_RPC = "https://api.avax-test.network/ext/bc/C/rpc";
const provider = new ethers.JsonRpcProvider(MAINNET_RPC);
const signer = await provider.getSigner(); // or Wallet.fromPhrase / Wallet(privateKey, provider)Contract Addresses (v10c Mainnet)
typescript
const ADDRESSES = {
moe: "0xa2Ecb675Ad2A2E199c98F5206C5C3fFE5a8D5753", // XPower
nft: "0x659875b46B2f8ed94f1A40a8F2D2A92a2eBCaedf", // XPowerNft
ppt: "0x7352CbB7264Aaf9CA076fb5384839Af995b9B5FE", // APowerNft
sov: "0x96178A1443039E806F8Ffd56dCE7A80f2161aaA2", // APower
mty: "0x398F6c2AC2daCaBd2F4776AF39c6fBeD87779840", // MoeTreasury
nty: "0xF113c3fb6b557d84B5A96E19b7918ba8fc21a741", // NftTreasury
};ABI Access
Option 1: From Snowtrace API
typescript
async function fetchABI(address: string): Promise<any> {
const url = `https://api.snowtrace.io/api?module=contract&action=getabi&address=${address}`;
const response = await fetch(url);
const data = await response.json();
return JSON.parse(data.result);
}Option 2: From Foundry Build Output
The out/ directory in xpower-fy.git contains compiled artifacts:
typescript
import XPowerABI from "./out/XPower.sol/XPower.json";
import XPowerNftABI from "./out/XPowerNft.sol/XPowerNft.json";Option 3: Manual Instantiation with Human-Readable ABI
typescript
const moe = new ethers.Contract(ADDRESSES.moe, [
"function init() external",
"function mint(address to, bytes32 blockHash, bytes calldata data) external",
"function balanceOf(address account) view returns (uint256)",
"function decimals() view returns (uint8)",
"function currentInterval() view returns (uint256)",
"function blockHashOf(uint256 interval) view returns (bytes32)",
"event Init(bytes32 blockHash, uint256 timestamp)",
], signer);Reading State
Token Balances
typescript
const moe = new ethers.Contract(ADDRESSES.moe, moeABI, provider);
const sov = new ethers.Contract(ADDRESSES.sov, sovABI, provider);
const xpowBalance = await moe.balanceOf(userAddress);
const apowBalance = await sov.balanceOf(userAddress);NFT Holdings
typescript
const nft = new ethers.Contract(ADDRESSES.nft, nftABI, provider);
// Check balance of a specific NFT ID
const nftId = 202421n; // year=2024, level=21
const nftBalance = await nft.balanceOf(userAddress, nftId);
// Get NFT metadata
const level = await nft.levelOf(nftId);
const year = await nft.yearOf(nftId);
const denomination = await nft.denominationOf(level); // 10^level
// Get URI
const uri = await nft.uri(nftId);Staking Info
typescript
const ppt = new ethers.Contract(ADDRESSES.ppt, pptABI, provider);
const mty = new ethers.Contract(ADDRESSES.mty, mtyABI, provider);
const stakedBalance = await ppt.balanceOf(userAddress, nftId);
const age = await ppt.ageOf(userAddress, nftId);
const claimable = await mty.claimable(userAddress, nftId);
const reward = await mty.rewardOf(userAddress, nftId);
const mintableAPOW = await mty.mintable(userAddress, nftId);Rate Information
typescript
const apr = await mty.aprOf(nftId);
const apb = await mty.apbOf(nftId);
const aprTarget = await mty.aprTargetOf(nftId);
const apbTarget = await mty.apbTargetOf(nftId);
// APOW backing ratio (scaled 1e18)
const metric = await sov.metric();Mining Integration
Off-Chain Nonce Search
The PoW search is performed client-side (browser/Node.js). The miner finds a nonce such that keccak256(contractAddr ^ userAddr, blockHash, nonce) produces leading zero nibbles.
typescript
import { ethers } from "ethers";
async function mine(moeAddress: string, userAddress: string, blockHash: string, zeroTarget: number) {
const prefix = ethers.solidityPacked(
["bytes20", "bytes20"],
[ethers.zeroPadValue(moeAddress, 20), ethers.zeroPadValue(userAddress, 20)]
);
const prefixXor = ethers.dataSlice(ethers.keccak256(prefix), 0, 20);
// XOR XOR pattern: contractAddress ^ userAddress
// Simplified: use ethers.solidityPackedKeccak256
let nonce = 0n;
while (true) {
const data = ethers.AbiCoder.defaultAbiCoder().encode(["uint256"], [nonce]);
const hash = ethers.keccak256(
ethers.concat([
prefixXor,
ethers.zeroPadValue(blockHash, 32),
data,
])
);
if (countLeadingZeroNibbles(hash) >= zeroTarget) {
return { nonce, hash };
}
nonce++;
}
}
function countLeadingZeroNibbles(hash: string): number {
let count = 0;
for (let i = 2; i < hash.length; i++) {
if (hash[i] === "0") { count++; }
else break;
}
return count;
}On-Chain Submission
typescript
async function submitMine(moe: ethers.Contract, userAddress: string, blockHash: string, nonce: bigint) {
// 1. Cache the block hash if not already done
const interval = await moe.currentInterval();
const cachedHash = await moe.blockHashOf(interval);
if (cachedHash === ethers.ZeroHash) {
const tx = await moe.init();
await tx.wait();
}
// 2. Encode nonce as bytes
const data = ethers.AbiCoder.defaultAbiCoder().encode(["uint256"], [nonce]);
// 3. Submit mint transaction
const tx = await moe.mint(userAddress, blockHash, data);
const receipt = await tx.wait();
// 4. Parse reward from Transfer events to userAddress
return receipt;
}Mining Error Handling
| Revert | Condition |
|---|---|
ExpiredBlockHash(bytes32) | Block hash is not from the current 1-hour interval |
DuplicatePairIndex(bytes32) | The nonce+block pair has already been used |
EmptyNonceHash(bytes32) | Nonce produced zero leading zeros |
typescript
try {
const receipt = await submitMine(moe, user, blockHash, nonce);
console.log("Mined!", receipt.hash);
} catch (err: any) {
if (err.data === ExpiredBlockHashSelector) {
// Re-init with a newer block hash
} else if (err.data === DuplicatePairIndexSelector) {
// Try a different nonce
} else if (err.data === EmptyNonceHashSelector) {
// Increase mining difficulty
}
}Staking Integration
Full Flow: Deposit → Stake → Claim
Step 1: Approve XPOW Spend
typescript
const moe = new ethers.Contract(ADDRESSES.moe, moeABI, signer);
const approveTx = await moe.approve(ADDRESSES.nft, ethers.MaxUint256);
await approveTx.wait();Step 2: Mint XPowerNft (Deposit)
typescript
const nft = new ethers.Contract(ADDRESSES.nft, nftABI, signer);
const level = 21; // 10^21 = 1e21 XPOW per NFT
const amount = 1n;
const mintTx = await nft.mint(userAddress, level, amount);
await mintTx.wait();Step 3: Approve NftTreasury for XPowerNft
typescript
const approveNftTx = await nft.setApprovalForAll(ADDRESSES.nty, true);
await approveNftTx.wait();Step 4: Stake
typescript
const nty = new ethers.Contract(ADDRESSES.nty, ntyABI, signer);
const nftId = await nft.idBy(2024, level);
const stakeTx = await nty.stake(userAddress, nftId, amount);
await stakeTx.wait();Step 5: Claim Rewards
typescript
const mty = new ethers.Contract(ADDRESSES.mty, mtyABI, signer);
const claimAmount = await mty.claimable(userAddress, nftId);
if (claimAmount > 0n) {
const claimTx = await mty.claim(userAddress, nftId, claimAmount, 0);
await claimTx.wait();
}Batch Operations
typescript
// Batch stake
const ids = [nftId1, nftId2];
const amounts = [amount1, amount2];
await nty.stakeBatch(userAddress, ids, amounts);
// Batch claim
const nftIds = [nftId1, nftId2];
await mty.claimBatch(userAddress, nftIds, claimAmount, 0);
// Batch unstake
await nty.unstakeBatch(userAddress, ids, amounts);Unstaking
typescript
const unstakeTx = await nty.unstake(userAddress, nftId, amount);
await unstakeTx.wait();
// Optionally burn NFT to redeem XPOW
const burnTx = await nft.burn(userAddress, nftId, amount);
await burnTx.wait();Reward Claiming
Preview Rewards Before Claiming
typescript
// Raw XPOW reward amount
const claimableXPOW = await mty.claimable(userAddress, nftId);
// Estimated APOW (accounts for rate limiting)
const estimatedAPOW = await mty.mintable(userAddress, nftId);
// Total reward accrued (including already claimed)
const totalReward = await mty.rewardOf(userAddress, nftId);
// Age of stake in seconds
const age = await ppt.ageOf(userAddress, nftId);APOW Unwrapping
typescript
// Burn APOW to get proportional XPOW back
const apowBalance = await sov.balanceOf(userAddress);
const burnTx = await sov.burn(apowBalance);
await burnTx.wait();
// Check conversion rate before burning
const metric = await sov.metric(); // 1e18 = 100% backedDelegated Operations
Users can approve operators to act on their behalf:
typescript
// Allow operator to mint XPowerNft
await nft.approveMint(operatorAddress, true);
// Allow operator to stake
await nty.approveStake(operatorAddress, true);
// Allow operator to unstake
await nty.approveUnstake(operatorAddress, true);
// Allow operator to migrate NFTs
await nft.approveMigrate(operatorAddress, true);Check approval status:
typescript
const canMint = await nft.approvedMint(userAddress, operatorAddress);
const canStake = await nty.approvedStake(userAddress, operatorAddress);Common Reverts Reference
| Contract | Error Selector | Condition |
|---|---|---|
| XPower | ExpiredBlockHash(bytes32) | Block hash outside current hour interval |
| XPower | DuplicatePairIndex(bytes32) | Nonce+block pair already used |
| XPower | EmptyNonceHash(bytes32) | Nonce has zero leading zeros |
| XPowerNft | NonTernaryLevel(uint256) | Level not divisible by 3 |
| XPowerNft | IrredeemableIssue(uint256) | NFT not yet matured for redemption |
| XPowerNft | UnauthorizedAccount(address) | Caller not owner or approved operator |
| APowerNft | InsufficientBalance(uint256) | Burn amount exceeds balance |
| APowerNft | AlreadyInitialized(address) | init() called twice |
| NftTreasury | InvalidAmount(uint256) | Zero amount in stake/unstake |
| NftTreasury | UnauthorizedAccount(address) | Not owner or approved staker |
| MoeTreasury | NonUnique(uint256[]) | Duplicate/unsorted IDs in batch claim |
| MoeTreasury | InvalidClaim(uint256) | Claim produced zero APOW |
| Banq | Capped(uint256) | Free supply cap exceeded (no pool set) |
| Banq | InsufficientAmount(uint256,uint256,uint256) | Net-to-rip ratio below MIN_NET2RIP (100) |
| Migratable | DeadlinePassed(uint256) | Migration deadline exceeded |
| Migratable | MigrationSealed(uint256) | Immigration sealed for this base |