Skip to content

Integration Guide

Setup

Install Dependencies

bash
npm install ethers@6

Network 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

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

RevertCondition
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% backed

Delegated 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

ContractError SelectorCondition
XPowerExpiredBlockHash(bytes32)Block hash outside current hour interval
XPowerDuplicatePairIndex(bytes32)Nonce+block pair already used
XPowerEmptyNonceHash(bytes32)Nonce has zero leading zeros
XPowerNftNonTernaryLevel(uint256)Level not divisible by 3
XPowerNftIrredeemableIssue(uint256)NFT not yet matured for redemption
XPowerNftUnauthorizedAccount(address)Caller not owner or approved operator
APowerNftInsufficientBalance(uint256)Burn amount exceeds balance
APowerNftAlreadyInitialized(address)init() called twice
NftTreasuryInvalidAmount(uint256)Zero amount in stake/unstake
NftTreasuryUnauthorizedAccount(address)Not owner or approved staker
MoeTreasuryNonUnique(uint256[])Duplicate/unsorted IDs in batch claim
MoeTreasuryInvalidClaim(uint256)Claim produced zero APOW
BanqCapped(uint256)Free supply cap exceeded (no pool set)
BanqInsufficientAmount(uint256,uint256,uint256)Net-to-rip ratio below MIN_NET2RIP (100)
MigratableDeadlinePassed(uint256)Migration deadline exceeded
MigratableMigrationSealed(uint256)Immigration sealed for this base