Skip to content

Latest commit

 

History

History
533 lines (401 loc) · 15.1 KB

File metadata and controls

533 lines (401 loc) · 15.1 KB

Testing and Debugging on XPR Network

This guide covers testing smart contracts, debugging failed transactions, and resolving common issues.

Contract Testing

Local Testing Setup

# Install test dependencies
npm install --save-dev proton-tsc @proton/vert jest @types/jest ts-jest

Note: proton-tsc provides types and includes proton-asc compiler. Tests use @proton/vert for blockchain simulation.

Test Configuration

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testMatch: ['**/tests/**/*.test.ts'],
  moduleFileExtensions: ['ts', 'js'],
};

Basic Contract Test

// tests/mycontract.test.ts
import { Blockchain, nameToBigInt, expectToThrow } from '@proton/vert';

describe('MyContract', () => {
  let blockchain: Blockchain;
  let contract: any;
  let user1: any;
  let user2: any;

  beforeAll(async () => {
    // Initialize blockchain
    blockchain = new Blockchain();

    // Create accounts
    [user1, user2] = blockchain.createAccounts('user1', 'user2');

    // Deploy contract (reads .wasm and .abi from the target dir)
    contract = blockchain.createContract('mycontract', 'assembly/target/mycontract.contract');
  });

  beforeEach(async () => {
    // Reset state between tests
    blockchain.resetTables();
  });

  test('should store value', async () => {
    await contract.actions.store(['user1', 'Hello World']).send('user1@active');

    // Check table
    const rows = contract.tables.mydata().getTableRows();
    expect(rows.length).toBe(1);
    expect(rows[0].value).toBe('Hello World');
  });

  test('should fail without auth', async () => {
    await expectToThrow(
      contract.actions.store(['user1', 'Hello']).send('user2@active'),
      'missing required authority user1'   // vert's wording; a real node says "missing authority of user1"
    );
  });

  test('should validate input', async () => {
    await expectToThrow(
      contract.actions.store(['user1', '']).send('user1@active'),
      'Value cannot be empty'
    );
  });
});

Testing Token Transfers

test('should handle incoming transfer', async () => {
  // Setup token contract
  // proton-tsc ships a prebuilt eosio.token (abi + wasm) for tests
  const eosioToken = blockchain.createContract('eosio.token', 'node_modules/proton-tsc/external/eosio.token/eosio.token');

  // Create and issue tokens
  await eosioToken.actions.create(['eosio.token', '1000000.0000 XPR']).send();
  await eosioToken.actions.issue(['user1', '1000.0000 XPR', 'initial']).send();

  // Transfer to contract
  await eosioToken.actions.transfer([
    'user1', 'mycontract', '100.0000 XPR', 'deposit'
  ]).send('user1@active');

  // Verify contract received and processed it
  const deposits = contract.tables.deposits().getTableRows();
  expect(deposits[0].amount).toBe('100.0000 XPR');
});

Testing Time-Dependent Logic

test('should expire after duration', async () => {
  // Create challenge
  await contract.actions.create(['user1', '100.0000 XPR', 1, 300]).send('user1@active');

  // Advance blockchain time by 5 minutes
  blockchain.addTime(TimePointSec.from(300));  // import { TimePointSec } from '@greymass/eosio'

  // Now it should be resolvable
  await contract.actions.resolve([1]).send('resolver@active');

  const challenge = contract.tables.challenges().getTableRow(1n);
  expect(challenge.status).toBe(2); // RESOLVED
});

Testnet Testing

Setup Testnet Account

# Switch to testnet
proton chain:set proton-test

# Create testnet account (if needed; prompts for email verification)
proton account:create mytestaccount

# Or create extra accounts from one you already control: no email, and no private
# key is printed when you pass -k (best for agent-driven rehearsals)
proton account:create-funded mytestuser2 -c mytestaccount -k PUB_K1_xxxxx -o backupowner

# Get test tokens (1,000 test XPR per account per 24 h)
proton faucet:claim XPR mytestaccount

Deploy to Testnet

# Build
npm run build

# Deploy (contract:set asks "Continue? (y/N)" and has no --yes flag)
echo y | proton contract:set mytestaccount ./assembly/target

# Confirm the WASM landed: an all-zero code_hash means only the ABI was set
curl -s -X POST https://api-xprnetwork-test.saltant.io/v1/chain/get_code_hash \
  -d '{"account_name":"mytestaccount"}'

# Initialize
proton action mytestaccount init '{"owner":"mytestaccount"}' mytestaccount

Deploy to testnet early, not only after the unit tests pass. The local test VM does not check that the contract's imported intrinsics match the node's signatures (@proton/vert 0.3.24 implements get_code_hash with the same 3 parameters as proton-tsc, so that bug is invisible locally). On a testnet rehearsal (2026-09-24), a contract passed 15/15 local tests and was then rejected at setcode with wrong type for imported function get_code_hash (see smart-contracts.md → Known SDK bug). A deploy on the first day of development catches this.

Testnet Verification Checklist

  • Contract deploys without errors, and get_code_hash is non-zero
  • Init action succeeds
  • All actions work as expected
  • Table data persists correctly
  • Inline actions execute
  • Notifications fire to other contracts
  • Error messages are clear
  • Oracle callbacks (rng::receiverand) arrive, and a late or cancelled callback is ignored
  • Tested with an account that has contract code as well as a plain WebAuth account

Rehearse at full size. Some failures only show up at the real volume. In a 3,333-asset mint rehearsal on testnet (September 2026), none of these appeared in small runs: the minter's free NET ran out after ~80 transactions, a monitor that read one get_table_rows page missed 2,300 rows, and one CLI push had an ambiguous result. Run the whole job on testnet with the real data, the real batch sizes and the same scripts, including one deliberate interruption, before you run it on mainnet.


Debugging Failed Transactions

Reading Error Messages

When a transaction fails, the error message contains clues:

Error: assertion failure with message: Insufficient balance

The message after "with message:" is from your contract's check() calls.

Common Error Patterns

Error Cause Solution
missing authority of X (chain) / missing required authority X (vert) Wrong authorization Use correct account in authorization. Match both wordings in shared error parsing
assertion failure with message: ... Contract validation failed Check the condition in your contract
account does not exist Invalid account name Verify account exists on chain
table not found Querying non-existent table Check contract is deployed, table name correct
your requireGet() message (C++ contracts: unable to find key) Row doesn't exist Use get() with a null check instead of requireGet()
transaction has expired, expiration is ... (expired_tx_exception) Transaction took too long Increase expireSeconds
duplicate transaction <id> Same tx submitted twice Add unique data or wait
... has insufficient objective cpu resources / ... was executing for too long (tx_cpu_usage_exceeded) Not enough CPU Stake more CPU or optimize contract
account X has insufficient ram; needs N bytes has M bytes (ram_usage_exceeded) Not enough RAM Buy more RAM

Debugging with Proton CLI

# Check transaction details
proton transaction:get TRANSACTION_ID

# Check account resources
proton account myaccount

# Check table data
proton table mycontract mytable

# Execute an action (no flags; output includes the transaction id)
proton action mycontract myaction '{}' myaccount

Debugging with Hyperion

# Get recent actions for account
curl "https://proton.eosusa.io/v2/history/get_actions?account=mycontract&limit=10"

# Get specific transaction
curl "https://proton.eosusa.io/v2/history/get_transaction?id=TX_ID"

# Filter by action name
curl "https://proton.eosusa.io/v2/history/get_actions?account=mycontract&filter=mycontract:myaction"

Using Console Logs

In contracts, use print() for debugging (visible in local tests, not on mainnet):

import { print, printi, printui } from 'proton-tsc';

@action("debug")
debugAction(value: u64): void {
  print("Starting debug action\n");
  printui(value);
  print("\n");

  // Your logic here
  const result = value * 2;
  print("Result: ");
  printui(result);
  print("\n");
}

Debugging Frontend Issues

Transaction Errors

try {
  const result = await session.transact({
    actions: [/* ... */]
  }, { broadcast: true });
} catch (error: any) {
  console.error('Transaction failed:', error);

  // Parse the error
  if (error.message.includes('assertion failure')) {
    const match = error.message.match(/assertion failure with message: (.+)/);
    const contractError = match ? match[1] : 'Unknown contract error';
    showUserError(contractError);
  } else if (error.message.includes('User cancelled')) {
    // User rejected in wallet
    showUserError('Transaction cancelled');
  } else if (error.message.includes('expired')) {
    showUserError('Transaction expired, please try again');
  } else {
    showUserError('Transaction failed: ' + error.message);
  }
}

RPC Query Debugging

async function debugTableQuery(code: string, table: string, scope: string) {
  console.log(`Querying: ${code}::${table} (scope: ${scope})`);

  try {
    const result = await rpc.get_table_rows({
      code,
      scope,
      table,
      json: true,
      limit: 10
    });

    console.log('Result:', JSON.stringify(result, null, 2));
    console.log(`Found ${result.rows.length} rows, more: ${result.more}`);

    return result.rows;
  } catch (error) {
    console.error('Query failed:', error);

    // Check if table exists
    const abi = await rpc.get_abi(code);
    const tableExists = abi.abi?.tables?.some(t => t.name === table);
    console.log(`Table "${table}" exists in ABI: ${tableExists}`);

    throw error;
  }
}

Session Debugging

// Debug session state
function debugSession(session: any) {
  console.log('Session debug:');
  console.log('  Actor:', session?.auth?.actor);
  console.log('  Permission:', session?.auth?.permission);
  console.log('  Chain ID:', session?.chainId);
  console.log('  Link type:', session?.link?.walletType);

  // Check if session is valid
  if (!session) {
    console.error('Session is null/undefined');
  } else if (!session.auth) {
    console.error('Session has no auth');
  } else if (!session.transact) {
    console.error('Session missing transact method');
  }
}

Block Explorer Debugging

Proton Scan

For any transaction, check the block explorer:

https://explorer.xprnetwork.org/tx/TRANSACTION_ID

This shows:

  • All actions in the transaction
  • Inline actions triggered
  • Table deltas (data changes)
  • CPU/NET/RAM usage
  • Error messages (if failed)

Useful Explorer Views

# Account overview
https://explorer.xprnetwork.org/account/ACCOUNT_NAME

# Contract ABI
https://explorer.xprnetwork.org/account/ACCOUNT_NAME?tab=contract

# Recent transactions
https://explorer.xprnetwork.org/account/ACCOUNT_NAME?tab=transactions

# Table data
https://explorer.xprnetwork.org/account/ACCOUNT_NAME?tab=tables

Automated Testing Script

#!/bin/bash
# test-contract.sh

set -e

CONTRACT="mycontract"
TESTNET_ACCOUNT="mytest"

echo "=== Building contract ==="
npm run build

echo "=== Deploying to testnet ==="
proton chain:set proton-test
echo y | proton contract:set $TESTNET_ACCOUNT ./assembly/target
# contract:set can set the ABI, fail the WASM, and still exit 0: check the code hash
if curl -s -X POST https://api-xprnetwork-test.saltant.io/v1/chain/get_code_hash \
     -d '{"account_name":"'$TESTNET_ACCOUNT'"}' | grep -q '"code_hash":"0\{64\}"'; then
  echo "✗ No WASM on $TESTNET_ACCOUNT (ABI only)"
  exit 1
fi

echo "=== Running tests ==="

# Test 1: Initialize
echo "Test 1: Init"
proton action $CONTRACT init '{"owner":"'$TESTNET_ACCOUNT'"}' $TESTNET_ACCOUNT

# Test 2: Store value
echo "Test 2: Store"
proton action $CONTRACT store '{"owner":"'$TESTNET_ACCOUNT'","value":"test123"}' $TESTNET_ACCOUNT

# Test 3: Verify table
echo "Test 3: Verify"
RESULT=$(proton table $CONTRACT mydata)
if echo "$RESULT" | grep -q "test123"; then
  echo "✓ Value stored correctly"
else
  echo "✗ Value not found in table"
  exit 1
fi

# Test 4: Error case (should fail)
echo "Test 4: Error handling"
if proton action $CONTRACT store '{"owner":"'$TESTNET_ACCOUNT'","value":""}' $TESTNET_ACCOUNT 2>&1 | grep -q "assertion failure"; then
  echo "✓ Empty value correctly rejected"
else
  echo "✗ Should have rejected empty value"
  exit 1
fi

echo "=== All tests passed ==="

Performance Profiling

Measure CPU Usage

// In contract, time-intensive operations
const startTime = currentTimeMs();

// ... expensive operation ...

const elapsed = currentTimeMs() - startTime;
print(`Operation took ${elapsed}ms\n`);

Optimize Table Queries

// BAD: Iterating entire table
let total: u64 = 0;
let cursor = this.dataTable.first();
while (cursor) {
  total += cursor.value;
  cursor = this.dataTable.next(cursor);
}

// GOOD: Use a secondary index for point lookups
// getBySecondaryU64(value, indexPosition) returns ONE row (or null), not an array;
// for ranges, use lowerBound/upperBound on the index and walk with next()
const row = this.dataTable.getBySecondaryU64(owner.N, 0);  // 0 = first @secondary index
const total: u64 = row ? row.value : 0;

RAM Optimization

There is no in-contract RAM query in proton-tsc. Measure RAM per action from the outside: the explorer shows the RAM delta per action, and proton table output shows the payer of each row. Keep rows small (fixed-width fields, no unbounded strings) and let the caller pay (store(row, payer)).


CI/CD Integration

GitHub Actions Example

# .github/workflows/test.yml
name: Test Contract

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm install

      - name: Build contract
        run: npm run build

      - name: Run tests
        run: npm test

      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        with:
          name: contract-build
          path: assembly/target/

Quick Debug Checklist

When something doesn't work:

  1. Check the error message - Read it carefully
  2. Verify authorization - Is the right account signing?
  3. Check table state - Is the data what you expect?
  4. Test on testnet first - Never debug on mainnet
  5. Check ABI - Is the contract deployed with current ABI?
  6. Check resources - Does account have RAM/CPU/NET?
  7. Check block explorer - What does the transaction show?
  8. Add logging - Use print() in tests
  9. Isolate the issue - Test one thing at a time
  10. Check timestamps - Is time-dependent logic correct?