Skip to main content

Financial Transaction

Suppose Alice wants to transfer 100 tokens to Bob. In this case we are talking about native Near Protocol tokens, oppose to user-defined tokens implemented through a smart contract. There are several way this can be done:

  • Direct transfer through a transaction containing transfer action;
  • Alice calling a smart contract that in turn creates a financial transaction towards Bob.

In this section we are talking about the former simpler scenario.

Pre-requisites

For this to work both Alice and Bob need to have accounts and an access to them through the full access keys.

Suppose Alice has account alice_near and Bob has account bob_near. Also, some time in the past, each of them has created a public-secret key-pair, saved the secret key somewhere (e.g. in a wallet application) and created a full access key with the public key for the account.

We also need to assume that both Alice and Bob have some number of tokens on their accounts. Alice needs >100 tokens on her account so that she could transfer 100 tokens to Bob, but also Alice and Bob need to have some tokens on balance to pay for the data they store on-chain for their accounts -- which is essentially the refundable cost of the storage occupied by the account in the Near Protocol network.

Creating a transaction

To send the transaction neither Alice nor Bob need to run a node. However, Alice needs a way to create and sign a transaction structure. Suppose Alice uses near-shell or any other third-party tool for that. The tool then creates the following structure:

Transaction {
signer_id: "alice_near",
public_key: "ed25519:32zVgoqtuyRuDvSMZjWQ774kK36UTwuGRZMmPsS6xpMy",
nonce: 57,
receiver_id: "bob_near",
block_hash: "CjNSmWXTWhC3EhRVtqLhRmWMTkRbU96wUACqxMtV1uGf",
actions: vec![
Action::Transfer(TransferAction {deposit: 100} )
],
}

Which contains one token transfer action, the id of the account that signs this transaction (alice_near) the account towards which this transaction is addressed (bob_near). Alice also uses the public key associated with one of the full access keys of alice_near account.

Additionally, Alice uses the nonce which is unique value that allows Near Protocol to differentiate the transactions (in case there are several transfers coming in rapid succession) which should be strictly increasing with each transaction. Unlike in Ethereum, nonces are associated with access keys, oppose to the entire accounts, so several users using the same account through different access keys need not to worry about accidentally reusing each other's nonces.

The block hash is used to calculate a transaction's "freshness". It is used to make sure a transaction does not get lost (let's say somewhere in the network) and then arrive days, weeks, or years later when it is not longer relevant and would be undesirable to execute. A transaction does not need to arrive at a specific block, instead it is required to arrive within a certain number of blocks from the block identified by the block_hash. Any transaction arriving outside this threshold is considered to be invalid. Allowed delay is defined by transaction_validity_period option in the chain’s genesis file. On mainnet, this value is 86400 (which corresponds to roughly a day) and on testnet it is 100.

near-shell or other tool that Alice uses then signs this transaction, by: computing the hash of the transaction and signing it with the secret key, resulting in a SignedTransaction object.

Sending the transaction

To send the transaction, near-shell connects through the RPC to any Near Protocol node and submits it. If users wants to wait until the transaction is processed they can use send_tx_commit JSONRPC method which waits for the transaction to appear in a block. Otherwise the user can use send_tx_async.

Transaction to receipt

We skip the details on how the transaction arrives to be processed by the runtime, since it is a part of the blockchain layer discussion. We consider the moment where SignedTransaction is getting passed to Runtime::apply of the runtime crate. Runtime::apply immediately passes transaction to Runtime::process_transaction which in turn does the following:

  • Verifies that transaction is valid;
  • Applies initial reversible and irreversible charges to alice_near account;
  • Creates a receipt with the same set of actions directed towards bob_near.

The first two items are performed inside Runtime::verify_and_charge_transaction method. Specifically it does the following checks:

  • Verifies that the signature of the transaction is correct based on the transaction hash and the attached public key;
  • Retrieves the latest state of the alice_near account, and simultaneously checks that it exists;
  • Retrieves the state of the access key of that alice_near used to sign the transaction;
  • Checks that transaction nonce is greater than the nonce of the latest transaction executed with that access key;
  • Subtracts the total_cost of the transaction from the account balance, or throws InvalidTxError::NotEnoughBalance. If the transaction is part of a transaction by a FunctionCall Access Key, subtracts the total_cost from the allowance or throws InvalidAccessKeyError::NotEnoughAllowance;
  • Checks whether the signer account has insufficient balance for the storage deposit and throws InvalidTxError::LackBalanceForState if so
  • If the transaction is part of a transaction by a FunctionCall Access Key, throws InvalidAccessKeyError::RequiresFullAccess;
  • Updates the alice_near account with the new balance and the used access key with the new nonce;

If any of the above operations fail all of the changes will be reverted.

Processing receipt

The receipt created in the previous section will eventually arrive to a runtime on the shard that hosts bob_near account. Again, it will be processed by Runtime::apply which will immediately call Runtime::process_receipt. It will check that this receipt does not have data dependencies (which is only the case of function calls) and will then call Runtime::apply_action_receipt on TransferAction.

Runtime::apply_action_receipt will perform the following checks:

  • Retrieves the state of bob_near account, if it still exists (it is possible that Bob has deleted his account concurrently with the transfer transaction);
  • Computes the cost of processing a receipt and a transfer action;
  • Computes how much reward should be burnt;
  • Checks if bob_near still exists and if it does, deposits the transferred tokens to bob_near;
  • Checks if bob_near still exists and if so, deposits the gas reward to the bob_near account;
  • Checks if bob_near has enough balance for the storage deposit, the transaction's temporary state is dropped/rolled back if there is not enough balance;