Simulating a Call

Overview

Simulation is the first phase of any contract interaction on OP_NET. During simulation, contract code executes within a virtual environment against the current blockchain state, producing a complete execution report without broadcasting a transaction or consuming any Bitcoin.

The simulation captures return values, gas consumption, emitted events, and potential failures, all encapsulated in a CallResult object. This mechanism enables validation and cost estimation prior to committing resources to an on-chain operation. For read-only functions such as balance queries or metadata retrieval, simulation is the only required step since no state modification occurs.

Read-Only Method Calls

Read-only methods retrieve data from the contract without modifying blockchain state. These calls require only the simulation phase, as no transaction needs to be broadcast. Common examples include querying token balances, retrieving metadata, checking allowances, and reading contract configuration.

The response from the node is automatically decoded based on the contract's ABI definition. For detailed information on accessing decoded values, refer to the Decoded Outputs section.

typescript
Calling Read-only Methods
// Read token metadata
const name = await token.name();
const symbol = await token.symbol();
const decimals = await token.decimals();
const totalSupply = await token.totalSupply();

// Access results
console.log('Name:', name.properties.name);
console.log('Symbol:', symbol.properties.symbol);
console.log('Decimals:', decimals.properties.decimals);
console.log('Total Supply:', totalSupply.properties.totalSupply);
Batch Read Operations

Batching operations can significantly improve efficiency.

typescript
Batching Read-only Operations
// Parallel reads are efficient
const [name, symbol, decimals, totalSupply, balance] = await Promise.all([
    token.name(),
    token.symbol(),
    token.decimals(),
    token.totalSupply(),
    token.balanceOf(myAddress),
]);

Decoded Outputs

When a contract method returns data, the raw response from the node is a binary-encoded payload. The library automatically decodes this payload based on the method's output definition in the ABI, transforming it into a structured TypeScript object. This decoded data is accessible through the properties property of the CallResult.

The structure of properties matches the return type defined in the contract interface. TypeScript generics ensure compile-time type safety, providing full IntelliSense support for property names and types.

The decoding process handles all supported ABI data types. If the ABI definition does not match the actual response, the decoding will fail and an error will be thrown.

typescript
Single Return Value
// The balanceOf method returns { balance: bigint }
const result = await token.balanceOf(address);
console.log('Balance:', result.properties.balance);

// The name method returns { name: string }
const nameResult = await token.name();
console.log('Token name:', nameResult.properties.name);

// The decimals method returns { decimals: number }
const decimalsResult = await token.decimals();
console.log('Decimals:', decimalsResult.properties.decimals);

For methods that return multiple values, each value is accessible as a separate property.

typescript
Multiple Return Values
// A method returning multiple values
const result = await contract.getPoolInfo();
console.log('Reserve A:', result.properties.reserveA);
console.log('Reserve B:', result.properties.reserveB);
console.log('Total supply:', result.properties.totalSupply);

Handling Reverts

Contract calls may fail for various reasons. When a simulation detects that a call would fail, the CallResult captures the failure reason in the revert property. Checking for reverts before accessing return values or sending transactions prevents runtime errors and provides clear feedback about why an operation cannot proceed.

typescript
Checking Revert
const result = await token.transfer(recipient, amount, new Uint8Array(0));

if (result.revert) {
    // Call would fail
    console.error('Transfer failed:', result.revert);
    return;
}

// Safe to proceed
console.log('Transfer would succeed');
const tx = await result.sendTransaction(params);

Common revert reasons

Revert Message Cause
Insufficient balance Sender doesn't have enough tokens.
Insufficient allowance Spender not approved for amount.
Invalid address Zero address or invalid format.
Overflow Amount exceeds maximum.
Paused Contract is paused.
Not authorized Caller lacks permission.
Invalid amount Amount is zero or invalid.

Historical Queries

By default, simulations execute against the current blockchain state. However, the library supports querying contract state at a specific block height, enabling historical data retrieval and state analysis at past points in time. This capability is useful for auditing, debugging, and building applications that require historical context.

typescript
Setting a Block Height
// Set simulation height before calling
token.setSimulatedHeight(12345n);

// This query executes as of block 12345
const historicalBalance = await token.balanceOf(address);

// Reset to current height
token.setSimulatedHeight(undefined);

Using Access Lists

Access lists record which storage slots are accessed during contract execution. When provided to subsequent calls, they allow the node to prepare the required storage data in advance, potentially reducing gas consumption.

typescript
Specifying an Access Lists
const result = await token.transfer(recipient, amount, new Uint8Array(0));

// Access list shows storage accessed
console.log('Access list:', result.accessList);

// Use access list to optimize future calls
token.setAccessList(result.accessList);
const optimizedResult = await token.transfer(recipient, amount, new Uint8Array(0));

Transaction Details for Simulation

Certain contract operations require knowledge of the Bitcoin transaction structure to execute correctly. For example, an NFT claim contract may need to verify that a payment was sent to a treasury address, or a swap contract may need to validate specific input UTXOs.

The setTransactionDetails() method allows simulation of these scenarios by providing the contract with information about the transaction inputs and outputs that will accompany the contract call.

Understanding Transaction Details

When a contract executes on-chain, it has access to the full Bitcoin transaction context, including all inputs and outputs. During simulation, this context does not exist since no actual transaction is being created. The setTransactionDetails() method bridges this gap by allowing the simulated transaction structure to be defined, enabling the contract to perform its validation logic as if a real transaction were being processed.

Related Interfaces

Transaction details are defined using the ParsedSimulatedTransaction interface, which contains arrays of inputs and outputs.

typescript
ParsedSimulatedTransaction Interface
interface ParsedSimulatedTransaction {
    readonly inputs: StrippedTransactionInput[];
    readonly outputs: StrippedTransactionOutput[];
}

Each input represents a UTXO being spent in the transaction.

typescript
StrippedTransactionInput Interface
interface StrippedTransactionInput {
    readonly txId: Uint8Array;
    readonly outputIndex: number;
    readonly scriptSig: Uint8Array;
    readonly witnesses: Uint8Array[];
    readonly flags: number;
    readonly coinbase?: Uint8Array;
}

Each output represents a destination for funds in the transaction.

typescript
StrippedTransactionOutput Interface
interface StrippedTransactionOutput {
    readonly value: bigint;
    readonly index: number;          // Output index (0 is reserved)
    readonly flags: number;
    readonly scriptPubKey?: Uint8Array;
    readonly to?: string;            // P2OP/P2TR address string
}

Transaction Flags

Flags indicate which optional fields are present in inputs and outputs. The library provides enums for these flags.

typescript
Transaction Flags enum
// Required imports when using input and output flags
import { TransactionInputFlags, TransactionOutputFlags } from 'opnet';

// Output flags definition
enum TransactionOutputFlags {
    hasScriptPubKey = 1,  // Output contains raw scriptPubKey
    hasTo = 2,            // Output contains address string
}

// Input flags definition
enum TransactionInputFlags {
    hasWitnesses = 1,     // Input contains witness data
    hasCoinbase = 2,      // Input is a coinbase input
}

Simulating with Extra Outputs

The most common use case is simulating a contract call that verifies payment to a specific address. For example, when claiming an NFT that requires a treasury payment.

typescript
Simulating with Extra Outputs
import { TransactionOutputFlags } from 'opnet';

const treasuryAddress: string = 'bcrt1q...';

// Define the payment output the contract expects to see
contract.setTransactionDetails({
    inputs: [],
    outputs: [
        {
            to: treasuryAddress,
            value: 10000n,           // 10,000 satoshis payment
            index: 1,                // Output index (0 is reserved)
            scriptPubKey: undefined,
            flags: TransactionOutputFlags.hasTo,
        },
    ],
});

// Simulation will now include the treasury output
const result = await contract.claim();

if (!result.revert) {
    console.log('Claim would succeed with treasury payment');
}

Simulating with Multiple Outputs

Complex operations may require multiple outputs, such as payments to different recipients or fee distributions.

typescript
Simulating with Multiple Outputs
import { TransactionOutputFlags } from 'opnet';

contract.setTransactionDetails({
    inputs: [],
    outputs: [
        // Output 1: Treasury payment
        {
            to: treasuryAddress,
            value: 5000n,
            index: 1,
            scriptPubKey: undefined,
            flags: TransactionOutputFlags.hasTo,
        },
        // Output 2: Fee recipient
        {
            to: feeRecipient,
            value: 1000n,
            index: 2,
            scriptPubKey: undefined,
            flags: TransactionOutputFlags.hasTo,
        },
    ],
});

const result = await contract.complexOperation();

Simulating with Inputs

For contracts that verify specific input transactions, such as validating that funds come from a particular source, input details can be provided.

typescript
Simulating with Inputs
import { TransactionInputFlags } from 'opnet';
import { fromHex } from '@btc-vision/bitcoin';

contract.setTransactionDetails({
    inputs: [
        {
            txId: fromHex('previous_tx_hash_hex'),
            outputIndex: 0,
            scriptSig: new Uint8Array(0),
            witnesses: [],
            flags: 0,
        },
    ],
    outputs: [
        {
            to: recipientAddress,
            value: 50000n,
            index: 1,
            scriptPubKey: undefined,
            flags: TransactionOutputFlags.hasTo,
        },
    ],
});

const result = await contract.verifyAndProcess();

Using Raw ScriptPubKey

For non-standard outputs that cannot be represented by a simple address, the raw scriptPubKey can be provided instead.

typescript
Using Raw ScriptPubKey
import { TransactionOutputFlags } from 'opnet';
import { fromHex } from '@btc-vision/bitcoin';

contract.setTransactionDetails({
    inputs: [],
    outputs: [
        {
            value: 10000n,
            index: 1,
            scriptPubKey: fromHex('76a914...88ac'),  // P2PKH script
            to: undefined,
            flags: TransactionOutputFlags.hasScriptPubKey,
        },
    ],
});

Important Considerations

When using transaction details for simulation, keep the following points in mind:

  • Output index 0 is reserved: The contract uses output index 0 for internal purposes. Always use index 1 or higher for custom outputs.
  • Details are cleared after each call: The setTransactionDetails() method only applies to the next contract call. Subsequent calls require the details to be set again.
  • Simulation must match execution: When sending the actual transaction, the extraOutputs parameter in TransactionParameters must match what was simulated to ensure consistent behavior.

Complete Workflow Example

The following example demonstrates the complete flow from simulation with transaction details to transaction execution.

typescript
NFT Claim with Payment Verification
import {
    getContract,
    IOP721Contract,
    JSONRpcProvider,
    OP_721_ABI,
    TransactionOutputFlags,
    TransactionParameters,
} from 'opnet';
import {
    Address,
    AddressTypes,
    Mnemonic,
    MLDSASecurityLevel,
} from '@btc-vision/transaction';
import { networks, PsbtOutputExtended } from '@btc-vision/bitcoin';

async function claimNFTWithPayment(): Promise<void> {
    const network = networks.regtest;
    const provider = new JSONRpcProvider('https://regtest.opnet.org', network);

    const mnemonic = new Mnemonic(
        'your seed phrase here ...',
        '',
        network,
        MLDSASecurityLevel.LEVEL2
    );
    const wallet = mnemonic.deriveOPWallet(AddressTypes.P2TR, 0);

    const nftContract = getContract<IOP721Contract>(
        Address.fromString('0x...'),
        OP_721_ABI,
        provider,
        network,
        wallet.address
    );

    const treasuryAddress: string = 'bcrt1q...';
    const paymentAmount: bigint = 10000n;

    // Step 1: Set transaction details for simulation
    nftContract.setTransactionDetails({
        inputs: [],
        outputs: [
            {
                to: treasuryAddress,
                value: paymentAmount,
                index: 1,
                scriptPubKey: undefined,
                flags: TransactionOutputFlags.hasTo,
            },
        ],
    });

    // Step 2: Simulate the claim
    const simulation = await nftContract.claim();

    if (simulation.revert) {
        console.error('Claim would fail:', simulation.revert);
        return;
    }

    console.log('Simulation succeeded, gas:', simulation.estimatedSatGas);

    // Step 3: Build transaction with matching outputs
    const treasuryOutput: PsbtOutputExtended = {
        address: treasuryAddress,
        value: Number(paymentAmount),
    };

    const params: TransactionParameters = {
        signer: wallet.keypair,
        mldsaSigner: null,
        refundTo: wallet.p2tr,
        maximumAllowedSatToSpend: 50000n,
        feeRate: 10,
        network: network,
        extraOutputs: [treasuryOutput],  // Must match simulation
    };

    // Step 4: Send transaction
    const receipt = await simulation.sendTransaction(params);
    console.log('Transaction sent:', receipt.transactionId);

    await provider.close();
}

Examples

Check Balance Before Transfer

typescript
Check Balance Before Transfer
async function safeTransfer(
    token: IOP20Contract,
    recipient: Address,
    amount: bigint
): Promise<boolean> {
    // Check sender balance first
    const balance = await token.balanceOf(token.from!);

    if (balance.properties.balance < amount) {
        console.error('Insufficient balance');
        return false;
    }

    // Simulate transfer
    const transfer = await token.transfer(recipient, amount, new Uint8Array(0));

    if (transfer.revert) {
        console.error('Transfer would fail:', transfer.revert);
        return false;
    }

    console.log('Transfer would succeed');
    console.log('Gas required:', transfer.estimatedGas);
    return true;
}

Check Allowance Before TransferFrom

typescript
Check Allowance Before TransferFrom
async function safeTransferFrom(
    token: IOP20Contract,
    from: Address,
    to: Address,
    amount: bigint
): Promise<boolean> {
    // Check allowance
    const allowance = await token.allowance(from, token.from!);

    if (allowance.properties.remaining < amount) {
        console.error('Insufficient allowance');
        return false;
    }

    // Check balance
    const balance = await token.balanceOf(from);

    if (balance.properties.balance < amount) {
        console.error('Insufficient balance');
        return false;
    }

    // Simulate
    const transfer = await token.transferFrom(from, to, amount);

    if (transfer.revert) {
        console.error('TransferFrom would fail:', transfer.revert);
        return false;
    }

    return true;
}

Read Multiple Balances

typescript
Read Multiple Balances
async function getBalances(
    token: IOP20Contract,
    addresses: Address[]
): Promise<Map<string, bigint>> {
    const results = await Promise.all(
        addresses.map((addr) => token.balanceOf(addr))
    );

    const balances = new Map<string, bigint>();

    addresses.forEach((addr, i) => {
        const result = results[i];
        if (!result.revert) {
            balances.set(addr.toHex(), result.properties.balance);
        }
    });

    return balances;
}