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 aswitch
statement, just like we did invalidateTransaction
. 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 intoapply
.
// 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 fromwrappedStates
in order to properly modify any account state for that particular transaction.