Common Setup

The API documentation is currently heavily geared toward using javascript examples that make use of the neon-js library (version 3.11.9). In order to make the examples shorter, some common code using neon-js is needed. In the future this common code may all be wrapped up in an Aphelion API library for javascript.

A sample node.js application including the common setup code and some examples will be made available shortly and linked here when it is available. Currently, the methods below will need to be copied into your project in order to use the aphelion API examples that make use of contract operations.

Endpoints and Contract Script Hashes

The examples given use the MainNet endpoint, but you can adjust it to the TestNet endpoint if desired for testing.

  • MainNet RPC Endpoint: https://mainneo.aphelion-neo.com:10331

  • Latest MainNet Contract ScriptHash: 9488220e8654d798f9b9cb9e74bd611ecc83fd0f

  • TestNet RPC Endpoint: https://testneo.aphelion-neo.com:20331

  • Latest TestNet Contract ScriptHash: 6bc1ae05a6694357a6db104231d56aa9427e63e4

For the calls that use Aphelion REST API these are the url prefixes for MainNet and TestNet:

  • MainNet Aphelion REST API Endpoint: https://mainnet.aphelion-neo.com/api/
  • TestNet Aphelion REST API Endpoint: https://testnet.aphelion-neo.com:62443/api/

Getting an account object

The APIs that require building a contract transaction need an account object. Here is an example of how to obtain the account object expected.

import { wallet } from '@cityofzion/neon-js';

const account = new wallet.Account(wif); 

Executing a read only javascript contract operation

import { sc, rpc } from '@cityofzion/neon-js';

const DEX_HASH = '9488220e8654d798f9b9cb9e74bd611ecc83fd0f';

async function executeReadOnlyContractOperation(operation, parameters) {
    const network = 'MainNet'; // Could also use rpc url here, such as: 'https://mainneo.aphelion-neo.com:10331'
    const rpcClient = new rpc.RPCClient(network);

    const scriptBuilder = new sc.ScriptBuilder();
    scriptBuilder.emitAppCall(DEX_HASH, operation, parameters);
    const script = scriptBuilder.str;

    const res = await rpcClient.query({
        method: 'invokescript',
        params: [script],
    });
    return {
        success: res.result.state && res.result.state.indexOf('FAULT') === -1,
        result: res.result.stack.length > 0 ? res.result.stack[res.result.stack.length - 1].value : '',
    };
}

Creating a contract transaction

The following common code is used for building a contract transaction. The url of the RPC server given below is Aphelion's endpoint, but this could be omitted to use any RPC node as decided by neon-js, or it could be a url for TestNet for testing.

import { BigNumber } from 'bignumber.js';
import {
    wallet,
    u,
    api,
} from '@cityofzion/neon-js';

const TX_ATTR_USAGE_VERIFICATION = 0x20;
const TX_ATTR_USAGE_NONCE = 0xf0;
const DEX_HASH = '9488220e8654d798f9b9cb9e74bd611ecc83fd0f';
const account = new wallet.Account(wif);  // TODO: define wif to be the wallet wif

async function buildContractTransaction(operation, parameters, neoToSend, gasToSend, fee = 0, bringDexOnAsWitness = false, 
                                        additionalTxAttributes, specificInputs, specificOutputs) {
    const config = {
        net: 'MainNet',
        // url will be picked from the library if not specified here
        url: 'https://mainneo.aphelion-neo.com:10331',
        script: {
            scriptHash: DEX_HASH,
            operation,
            args: parameters,
        },
        fees: fee,
        gas: 0,
    };
    config.account = account;
    let intents;
    if (neoToSend > 0 || gasToSend > 0) {
        const assetsForIntent = {};
        if (neoToSend > 0) {
            assetsForIntent.NEO = neoToSend;
        }
        if (gasToSend > 0) {
            assetsForIntent.GAS = gasToSend;
        }
        intents = api.makeIntent(assetsForIntent, DEX_HASH);
        if (!specificInputs) {
          config.intents = intents;
        }
    }

    let neededGasUtxos = (gasToSend && BigNumber(gasToSend).isGreaterThan(0)) ? 1 : 0;
    let configResponse = await api.fillKeys(config);
    if (!configResponse.intents && fee === 0 && !neoToSend && !gasToSend) {
        configResponse.balance = new wallet.Balance({ address: configResponse.address, net: configResponse.net });
    } else {
        try {
            configResponse.balance = await api.neoscan.getBalance('MainNet', account.address);
        } catch (e) {
            throw new Error(`Failed to fetch address balance. ${e}`);
        }
        if (fee) {
            neededGasUtxos += 1;
            if (configResponse.balance.assets.GAS.unspent.length < neededGasUtxos) {
                throw new Error('No unspent GAS available to pay network fee.');
            }
        }
    }

    if (neededGasUtxos > 1 && gasToSend && BigNumber(gasToSend).isGreaterThanOrEqualTo(
        configResponse.balance.assets.GAS.balance)) {
        throw new Error('Gas balance insufficient to send amount specified.');
    }

    try {
        configResponse = await api.createTx(configResponse, 'invocation');
        if (intents && specificInputs && !specificOutputs) {
            config.intents = intents;
        } 
        if (specificInputs) {
            specificInputs.forEach((input) => {
                configResponse.tx.inputs.push(input)
            });
        }
        if (specificOutputs) {
            specificOutputs.forEach((output) => {
                configResponse.tx.outputs.push(output)
            });
        }

        const senderScriptHash = wallet.getScriptHashFromAddress(account.address);
        configResponse.tx.addAttribute(TX_ATTR_USAGE_VERIFICATION, u.reverseHex(senderScriptHash).padEnd(40, '0'));
        // NOTE: claim and compound as the manager need this:
        // configResponse.tx.addAttribute(TX_ATTR_USAGE_WITHDRAW_ADDRESS, u.reverseHex(senderScriptHash).padEnd(64, '0'));
        configResponse.tx.addAttribute(TX_ATTR_USAGE_NONCE,
            u.num2fixed8(new Date().getTime() * 0.00000001).padEnd(64, '0'));
        if (additionalTxAttributes) {
            additionalTxAttributes.forEach((pair) => {
                if (pair.length === 2) {
                    configResponse.tx.addAttribute(pair[0], pair[1]);
                }
            })
        }
        if (bringDexOnAsWitness) configResponse.tx.addAttribute(TX_ATTR_USAGE_VERIFICATION, u.reverseHex(DEX_HASH));

        configResponse = await api.signTx(configResponse);

        if (bringDexOnAsWitness) {
            const attachInvokedContract = {
                invocationScript: ('00').repeat(2),
                verificationScript: '',
            };
            // We need to order this for the VM. The vm pulls out the verification script hashes added as attributes
            // in order lexicographically and expects the witness verification scripts to match that order.
            if (parseInt(DEX_HASH, 16) > parseInt(senderScriptHash, 16)) {
                configResponse.tx.scripts.push(attachInvokedContract);
            } else {
                configResponse.tx.scripts.unshift(attachInvokedContract);
            }
        }
    } catch (e) {
        throw new Error(`Failed to create transaction. Error: ${e}`);
    }

    return configResponse;
}

Execute a contract transaction

This is the common code for executing a contract transaction. An exception may be thrown if a failure occurs building the transaction or if a failure occurs from the api call to send the transaction. This code makes use of async and expects the caller to handle exceptions by wrapping these calls in try / catch blocks.

import {
  api, wallet
} from '@cityofzion/neon-js';

async function executeContractTransaction(operation, parameters, neoToSend, gasToSend, fee=0, bringDexOnAsWitness = false, additionalTxAttributes, specificInputs, specificOutputs) {
    const configResp = await buildContractTransaction(operation, parameters, neoToSend, gasToSend, fee, bringDexOnAsWitness, additionalTxAttributes, specificInputs, specificOutputs);
    // neon-js will use the url from the config for sending configResp.tx.
    await api.sendTx(configResp);
    // wait for transaction success
    await monitorTxForConfirmation(configResp.tx);
}

Wait for TX execution completion

import { rpc } from '@cityofzion/neon-js';

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function monitorTxForConfirmation(tx, pollingIntervalMillis=5000, retries=12*60) {
    // Setting to MainNet here will use a random RPC server chosen by neon-js library
    const network = 'MainNet'; // Could also use rpc url here, such as: 'https://mainneo.aphelion-neo.com:10331'
    const rpcClient = new rpc.RPCClient('https://mainneo.aphelion-neo.com:10331');
    for (let retry = 0; retry < retries; retry++) {
        try {
            const transaction = await rpcClient.getRawTransaction(hash, 1);
            if (transaction.confirmations > 0) {
                return true;
            }
        } catch (ex) {
            console.log(`Error obtaining transaction information: ${ex}`);
        }
        await delay(pollingIntervalMillis);
    }
    return false;
}