Skip to content

apply

Shardus expects a very important function called apply to be passed into setup. apply is where all your business logic lives that mutates your application state. Within apply, you also need to create a transaction ID by hashing the transaction object, and pass it, along with the transaction timestamp as the second parameter, into createApplyResponse(), which is another method exposed by Shardus. This response should get returned at the end of apply.

It's crucially important that this is the only place where state data gets changed, because all nodes basically run the apply function simultaneously. This allows the nodes to agree on what order the changes to the state were made, and to be in sync with each other.


When you mutate your application state data anywhere outside of apply, you take the nodes out of sync. If you try this while running a network of nodes, you can actually see the node getting kicked out on the monitor server. Every other node has different state data than the one that changed its state prematurely, and they will boot it from the network.

The following is a good example of what basically goes on in apply:

if (type === "transfer") {
  let sourceAccount = accountStates[tx.sourceAddress].data;
  let targetAccount = accountStates[tx.targetAddress].data;

  sourceAccount.balance -= amount; // Take from source
  targetAccount.balance += amount; // Give to target

  sourceAccount.timestamp = tx.timestamp;
  targetAccount.timestamp = tx.timestamp;
}

For a simple transaction type like a transfer, all that's required to happen is the source account balance getting deducted and the target account balance getting credited. That's it! You can see how writing decentralized applications just got a whole lot easier.

Let's take a look at a more fully fledged chat application's implementation of apply:

A good way of implementing the apply function is to use a switch statement, just like we did in validateTransaction. This way, only the code in the case for the specific transaction type will be executed. Any time you need to access account states to modify data, you need to grab it from the second argument, wrappedStates, that gets passed into apply.

// wrappedStates is a wrappedVersion of all the account states, which it
// gets from the keys given to the transaction
apply(tx, wrappedStates) {
  // This is where the validateTransaction comes into play
  const { result, reason } = this.validateTransaction(tx);
  if (result !== "pass") {
    throw new Error(
      `invalid transaction, reason: ${reason}. tx: ${JSON.stringify(tx)}`
    );
  }

  // Get the source and target account data, if they exist
  let source = wrappedStates[tx.srcAcc] && wrappedStates[tx.srcAcc].data;
  let target = wrappedStates[tx.tgtAcc] && wrappedStates[tx.tgtAcc].data;

  // Create an applyResponse which will be used to tell Shardus that the tx has been applied
  const txId = crypto.hashObj(tx); // compute from tx
  console.log("Attempting to apply tx", txId, "...");
  const applyResponse = dapp.createApplyResponse(txId, tx.timestamp);

  // Apply the tx
  switch (tx.type) {
    case "register": {
      source.handle = tx.handle;
      // Just for testing purposes, we'll initially give the source account 1000 tokens to play with
      source.data.balance += 1000;
      source.timestamp = tx.timestamp;
      console.log("Applied register tx", txId, accounts[tx.srcAcc]);
      break;
    }
    case "message": {
      source.data.balance -= tx.amount;
      target.data.balance += tx.amount;

      if (!source.data.chats[tx.tgtAcc]) source.data.chats[tx.tgtAcc] = { sent: [tx.message], received: [] };
      else source.data.chats[tx.tgtAcc].sent.push(tx.message);

      if (!target.data.chats[tx.srcAcc]) target.data.chats[tx.srcAcc] = { sent: [], received: [tx.message] };
      else target.data.chats[tx.srcAcc].received.push(tx.message);

      source.timestamp = tx.timestamp;
      target.timestamp = tx.timestamp;

      console.log("Applied message tx", txId, accounts[tx.srcAcc], accounts[tx.tgtAcc]);
      break;
    }
    case "toll": {
      source.data.balance -= tx.amount;
      source.data.toll = tx.toll;
      source.timestamp = tx.timestamp;
      console.log("Applied toll tx", txId, accounts[tx.srcAcc]);
      break;
    }
    case "friend": {
      source.data.balance -= tx.amount;
      source.data.friends[tx.handle] = tx.tgtAcc;
      source.timestamp = timestamp;
      console.log("Applied friend tx", txId, accounts[tx.srcAcc]);
      break;
    }
    case "node_reward": {
      target.data.balance += tx.amount;
      source.nodeRewardTime = tx.timestamp;
      source.timestamp = tx.timestamp;
      target.timestamp = tx.timestamp;
      console.log("Applied node_reward tx", txId, accounts[tx.srcAcc], accounts[tx.tgtAcc]);
      break;
    }
    return applyResponse;
  }
}

wrappedStates is a very important argument that is generated from the keys sent in from the crack function. You need to parse the account data from wrappedStates in order to properly modify any account state for that particular transaction.