Add Acki Nacki to your backend

This document describes the various ways to accomplish the most important tasks of running a backend project that supports Acki Nacki

How to connect

  • Blockchain access may be set up through

    • the public endpoint

    • self-hosted Validator GQL endpoint

    • self-hosted Block Manager GQL endpoint

Public endpoint

The public endpoint: https://ackinacki-testnet.tvmlabs.dev/graphql.

Setting up Self-Hosted Validator

System Requirements

ConfigurationCPU (cores)RAM (GiB)Storage (TB)Network (Gbit/s)

Minimum

16c/32t

128

4 (> 200000 IOPS, > 6900 MB/s)

2

Recommended

24c/48t

256

4 (> 200000 IOPS, > 6900 MB/s)

2

How to run: guide is coming soon

After successfully provisioned validator the gql endpoint will be on localhost:3000/graphql

Setting up Self-Hosted Block Manager

Requirements and how-to-run guide are coming soon.

After successfully provisioned Block Manager the gql endpoint will be on localhost:3000/graphql

Setting up Wallet Account

Soon the official multisig wallet files will be published here https://github.com/gosh-sh/ackinacki-wallet.

This guide implies the latest wallet API and changes in the guide may be very small. So it is good to estimate the potential integration effort.

Using CLI tool

Build and install CLI tool

cd ~
git clone https://github.com/tvmlabs/tvm-sdk
cd tvm-sdk
cargo build --release
cd target/release
cp tvm-cli ~/.cargo/bin

Now path to tvm-cli is publicly accessible. You can also add it to your ENVs

export PATH=$PATH:~/tvm-sdk/target/release/tvm-cli

Configure network connection

tvm-cli config --url ackinacki-testnet.tvmlabs.dev/graphql

Get wallet account contract files

Soon the wallet files will be published here https://github.com/gosh-sh/ackinacki-wallet

Generate wallet keys and get the address

tvm-cli genaddr wallet.tvc --genkey wallet.keys.json

Top up the new address from your sponsor wallet

tvm-cli call <your-sponsor-wallet-address> sendTransaction '{"dest":"new-wallet-address", "value":10000000000, "bounce":false}' --abi your-sponsor-wallet.abi.json --sign your-sponsor-wallet.keys.json

Deploy the wallet account contract to blockchain

Use the following command for a simple one-owner account:

tvm-cli deploy MultisigWallet.tvc "{\"owners\":[\"0xnew-wallet-pub-key\",\"reqConfirms\":1}" --abi wallet.abi.json --sign wallet.keys.json

Using SDK

You may integrate above described process of wallet account deployment into your backend code.

Note, that similar to the CLI approach described above, you have to sponsor a user account before deploying contract code. The sample assumes you use Acki Nacki faucet, where you can request test tokens to the contract address generated by the sample. In a production environment you may set up a giver to sponsor your contract deployment operations.


 async function main(client: TvmClient) {
    // 
    // 1. ------------------ Deploy multisig wallet --------------------------------
    // 
    // Generate a key pair for the wallet to be deployed
    const keypair = await client.crypto.generate_random_sign_keys();

    // TODO: Save generated keypair!
    console.log('Generated wallet keys:', JSON.stringify(keypair))
    console.log('Do not forget to save the keys!')

    // To deploy a wallet we need its TVC and ABI files
    const msigTVC: string =
        readFileSync(path.resolve(__dirname, "../contract/wallet.tvc")).toString("base64")
    const msigABI: string =
        readFileSync(path.resolve(__dirname, "../contract/wallet.abi.json")).toString("utf8")

    // We need to know the future address of the wallet account,
    // because its balance must be positive for the contract to be deployed
    // Future address can be calculated by encoding the deploy message.
    // https://docs.everos.dev/ever-sdk/reference/types-and-methods/mod_abi#encode_message

    const messageParams: ParamsOfEncodeMessage = {
        abi: { type: 'Json', value: msigABI },
        deploy_set: { tvc: msigTVC, initial_data: {} },
        signer: { type: 'Keys', keys: keypair },
        processing_try_index: 1
    }

    const encoded: ResultOfEncodeMessage = await client.abi.encode_message(messageParams)

    const msigAddress = encoded.address

    console.log(`You can topup your wallet from Acki Nacki faucet at https://coming-soon`)
    console.log(`Please send >= ${MINIMAL_BALANCE} tokens to ${msigAddress}`)
    console.log(`awaiting...`)

    // Blocking here, waiting for account balance changes.
    // It is assumed that you replanish the account now
    let balance: number
    for (; ;) {
        // The idiomatic way to send a request is to specify 
        // query and variables as separate properties.
        const getBalanceQuery = `
                query getBalance($address: String!) {
                    blockchain {
                    account(address: $address) {
                            info {
                            balance
                        }
                    }
                }
            }
            `
        const resultOfQuery: ResultOfQuery = await client.net.query({
            query: getBalanceQuery,
            variables: { address: msigAddress }
        })

        const nanotokens = parseInt(resultOfQuery.result.data.blockchain.account.info?.balance, 16)
        if (nanotokens > MINIMAL_BALANCE * 1e9) {
            balance = nanotokens / 1e9
            break
        }
        // TODO: rate limiting
        await sleep(1000)
    }
    console.log(`Account balance is: ${balance.toString(10)} tokens`)

    console.log(`Deploying wallet contract to address: ${msigAddress} and waiting for transaction...`)

    // This function returns type `ResultOfProcessMessage`, see: 
    // https://docs.everos.dev/ever-sdk/reference/types-and-methods/mod_processing#process_message
    let result: ResultOfProcessMessage = await client.processing.process_message({
        message_encode_params: {
            ...messageParams,  // use the same params as for `encode_message`,
            call_set: {        // plus add `call_set`
                function_name: 'constructor',
                input: {
                    owners: [`0x${keypair.public}`],
                    reqConfirms: 1,
                    lifetime: 3600
                }
            },
        },
        send_events: false,
    })
    console.log('Contract deployed. Transaction hash', result.transaction?.id)
    assert.equal(result.transaction?.status, 3)
    assert.equal(result.transaction?.status_name, "finalized")

    //

Monitoring transactions

Lets assume we need to reliably know when customers receive or transfer funds from their wallets. Sample of transaction pagination is available below.

Pagination of all transactions

This sample queries and displays all transactions from the beginning. We can get all the transaction and filter by account addresses on the backend side.

Note: By default the Blockchain API queries, such as the one used here provide only data from the past 7 days. To retrieve older data, make sure to use the archive: true flag, as shown in the sample:

   async function main(client: TonClient) {
    // In this example, we want the query to return 2 items per page.
    const itemsPerPage = 25

    // Pagination connection pattern requires a cursor, which will be set latter
    let cursor: string = undefined

    // The idiomatic way to send a request is to specify 
    // query and variables as separate properties.
    const transactionsQuery = `
        query listTransactions($cursor: String, $count: Int) {
            blockchain {
                transactions(
                    first: $count
                    after: $cursor
                 ) {
                    edges {
                        node { 
                            id
                            balance_delta
                            account_addr
                            # other transaction fields
                     }
                    }
                    pageInfo { hasNextPage endCursor }
                }
            }
        }`

    for (; ;) {
        const queryResult: ResultOfQuery = await client.net.query({
            query: transactionsQuery,
            variables: {
                count: itemsPerPage,
                cursor
            }
        });
        const transactions = queryResult.result.data.blockchain.transactions;

        for (const edge of transactions.edges) {
            console.log("Transaction id:", edge.node.id);
        }
        if (transactions.pageInfo.hasNextPage === false) {
            break;
        }
        // To read next page we initialize the cursor:
        cursor = transactions.pageInfo.endCursor;
        // TODO: rate limiting
        await sleep(1000);
    }

}
console.log("Getting all transactions from the beginning/")
console.log("Most likely this process will never end, so press CTRL+C to interrupt it")
main(client)
    .then(() => {
        process.exit(0)
    })
    .catch(error => {
        console.error(error);
        process.exit(1);
    })



// This helper function is used for limiting request rate
function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)) }

Not all transactions that are successful are valid transfers and not all transactions that are aborted actually failed. Read below how to understand which transfers are successful transfers and which are not.

Pagination of account transactions

Simply replace the query used in the sample above with this one:

    const transactionsQuery = `
        query listTransactions($address: String, $cursor: String, $count: Int) {
            blockchain {
                account(address: $address) {
                    transactions(
                        first: $count
                        after: $cursor
                     ) {
                        edges {
                            node { 
                                id
                                balance_delta
                                account_addr
                                # other transaction fields
                         }
                        }
                        pageInfo { hasNextPage endCursor }
                    }
                }
            }
        }`

How to mitigate risks during transfers

Using CLI tool

The are two main cases regarding transfers to user accounts: a user may already have an active account to which they want to withdraw funds (set bounce to true), or they may want to withdraw funds to a completely new account, that doesn't exist at the time withdraw is requested (set bounce to false).

The status of the account provided by the user may be checked with the following Everdev command:

tvm-cli account <YourAddress>

The possible results of this command are the following:

Doesn't exist - account does not exist. It needs to be sponsored, then deployed, and only then will it be active.

Uninit - account already has some funds on it but contract code has not been deployed yet. User needs to deploy it.

Active - account already exists, and its code is deployed.

In the first to cases, the service might first transfer a small portion of the requested amount (~1 Shell) and request that the user deploys their contract. Upon the user's confirmation that the account is deployed, its status may be rechecked, and if it became active, the remaining amount of requested funds may be safely transferred.

Using SDK

Same way you can check it using Client Libraries:

    let balance: number
    let accType: number
    for (; ;) {
        // The idiomatic way to send a request is to specify 
        // query and variables as separate properties.
        const getInfoQuery = `
                query getBalance($address: String!) {
                    blockchain {
                    account(address: $address) {
                            info {
                            balance
                            acc_type
                        }
                    }
                }
            }
            `
        const resultOfQuery: ResultOfQuery = await client.net.query({
            query: getInfoQuery,
            variables: { address: msigAddress }
        })
        

        const nanotokens = parseInt(resultOfQuery.result.data.blockchain.account.info?.balance, 16)
        accType = resultOfQuery.result.data.blockchain.account.info?.acc_type;
        if (nanotokens > MINIMAL_BALANCE * 1e9) {
            balance = nanotokens / 1e9
            break
        }
        // TODO: rate limiting
        await sleep(1000)
    }
    console.log(`Account balance is: ${balance.toString(10)} tokens. Account type is ${accType}`)

Withdrawing from wallet accounts

Using CLI tool

This example shows how to generate a withdrawal transaction from the wallet, using its sendTransaction method. Note, that if Wallet has multiple custodians, the transaction will have to be confirmed with the confirmTransaction method.

In this example tokens are withdrawn from the user account to the account specified in dest. In a proper implementation, the account given by user should be used instead.

tvm-cli call <wallet-address> sendTransaction '{"dest":"receiver-wallet-address", "value":10000000000, "bounce":false}' --abi wallet.abi.json --sign wallet.keys.json

Using SDK

You may integrate withdrawals from wallet account into your backend using SDK as well.

This example shows how to generate a withdrawal transaction from the wallet, using its sendTransaction method. Note, that if Wallet has multiple custodians, the transaction will have to be confirmed with the confirmTransaction method.

In this example tokens are withdrawn from the user account to the account specified in dest. In a proper implementation, the account given by user should be used instead.

    // We send 0.5 tokens. Value is written in nanotokens
    const amount = 0.5e9
    const dest = "-1:7777777777777777777777777777777777777777777777777777777777777777"

    console.log('Sending 0.5 token to', dest)

    result = await client.processing.process_message({
        message_encode_params: {
            address: msigAddress,
            ...messageParams, // use the same params as for `encode_message`,
            call_set: {       // plus add `call_set`
                function_name: 'sendTransaction',
                input: {
                    dest: dest,
                    value: amount,
                    bounce: true,
                    flags: 64,
                    payload: ''
                }
            },
        },
        send_events: false, // do not send intermidate events
    })
    console.log('Transfer completed. Transaction hash', result.transaction?.id)
    assert.equal(result.transaction?.status, 3)
    assert.equal(result.transaction?.status_name, "finalized")

How to determine a successful transaction?

Not all aborted = true transactions are failed.

It depends on the account state before and after the transaction (fields orig_status and end_status):

  • If the account was already deployed, i.e. if (tx.orig_status == tx.end_status == active) then you can use tx.aborted field. If it is true, then the transaction is not successful.

  • If the account was not yet deployed then

    • if (orig_status == nonExist && end_status == uninit && aborted == true) then transaction is successful.

      All the transactions executed on non-deployed accounts are aborted by definition but if we see the state has changed to uninit, it means that the transfer was successfully received.

    • if (orig_status == uninit && end_status == uninit && aborted == true && in_message.bounce==false)then transaction is successful.

      Non-bounced messages are successfully received by non-deployed accounts, though the transaction status is aborted.

      Instead of checking tx.in_message.bounce==false you can check if tx.bounce.bounce_type<2 (tx.bounce.bounce_type==2(Ok) is equal to in_message.bounce==true)

Last updated