My pattern for async transactions of smart contracts written in Ethereum’s Solidity
Many developers who implemented Java, Go, Python… before developing Solidity feel like travelling back to the late 80’s with a DeLorean. Solidity is solid but very limited. Oracles grew up and stayed forever. Thus, asynchronous Solidity contracts are real. Here’s my pattern…
I’m using the captain’s NodeJS oracle called #ScriptIt for the following use case:
- A new user gets 256 points
- With every new call the user’s points will be reduced by log2
The Smart Contract
The asynchronous contact will derive from usingCaptainJS
which includes asynchronous call and callback functionality.
To remember asynchronous calls whenever a callback happens you need a a JobCounter
and a mapping
of job IDs and the sender’s address:
uint JobCounter = 0;
mapping (uint => address) JobToSenderMap;
The Events
In Ethereum a synchronous transactions are pending, then failed or success(ful). An asynchonous transaction will require Events to be emitted which inform a user if a transaction was pending, successful or if it failed.
Therefore you define three these events and every event should at least contain the sender’s address:
event GetPoints_Success(address Sender, uint Points);
event GetPoints_Pending(address Sender);
event GetPoints_Failed(address Sender, string ErrorMsg);
The Function
Ethereum’s default pattern is that every user calls a contract function and pays for the gas that is required to execute the code within one synchronous transaction context.
But now we have an asynchronous transaction context. And this means that additional gas will be required after the synchronous function call terminated.
Therefore your function must be payable
and your first check must be to verify that the user transferred enough additional gas:
uint GasRequired = DEFAULT_GAS_UNITS * tx.gasprice + 70 szabo;require(msg.value >= GasRequired, "please send some extra gas...");
In this demo use case we will require the default gas units defined in usingCaptainJS
multiplied by the current transaction gas price plus a transaction fee of 70 Szabo (which the captain needs to pay his container ship which executes your mathjs call).
Once it is clear that the user has transferred enough gas you can invoke the log2 function of mathjs according to the captain’s description at GitHub:
Run(
JobCounter,
concat("math:log2(",uintToString(PointsPerUser[msg.sender]), ")"),
"", "", 1, DEFAULT_GAS_UNITS, tx.gasprice
);emit GetPoints_Pending(msg.sender);
After the invocation of Run(...)
you must emit
the pending event. If the invocation of Run(...)
fails the synchronous call will fail.
The Callbacks
Once the captain calculated the log2 value of the user’s points he’s going to send the result back to your contract by calling the CaptainsResult
function. Make sure that only the captain is invoking this function by adding onlyCaptainsOrdersAllowed
.
Make sure to emit
the success event at the end of the function.
function CaptainsResult(uint JobCounter, string Log2Result) external onlyCaptainsOrdersAllowed {
// the return of the async call
address sender = JobToSenderMap[JobCounter];
uint Points = StringToUint(Log2Result); PointsPerUser[sender] = Points;
emit GetPoints_Success(sender, Points);
}
If the captain is not able to invoke the code you submitted (maybe you have a typo in your JavaScript code), he’s going to inform you by invoking the CaptainsError
function of your contract.
Make sure to emit
the failed event at the end of the function.
function CaptainsError(uint JobCounter, string ErrorMsg) external onlyCaptainsOrdersAllowed {
// the return of the async call
address sender = JobToSenderMap[JobCounter];
emit GetPoints_Failed(sender, ErrorMsg);
}
That’s it.
Updates @ https://twitter.com/captainjs_v2
Solidity code @ https://github.com/CaptainJavaScript/Solidity
Client test code @ https://github.com/CaptainJavaScript/Seaman-Client
Here’s the complete code:
pragma solidity ^0.4.25;import "./usingCaptainJS_v2.sol";contract AsyncPattern is usingCaptainJS {// to identify async calls
uint JobCounter = 0;
mapping (uint => address) JobToSenderMap;// demo use case: points per sender
mapping (address => uint) PointsPerUser;
event GetPoints_Success(address Sender, uint Points);
event GetPoints_Pending(address Sender);
event GetPoints_Failed(address Sender, string ErrorMsg);function GetPoints() public payable {
// make sure to have enough gas for the async callback
uint GasRequired = DEFAULT_GAS_UNITS * tx.gasprice + 70 szabo;
require(msg.value >= GasRequired, "please send some extra gas..."); // remember this call
JobToSenderMap[++JobCounter] = msg.sender;
// now do the math - but mix async + async...
// every user has 256 points at the beginning and with every next
// call it is log2 of his points
if(PointsPerUser[msg.sender] == 0) {
// first call!
PointsPerUser[msg.sender] = 256;
emit GetPoints_Success(msg.sender, 256);
}
else {
// every other call
Run(
JobCounter, concat("math:log2(", uintToString(PointsPerUser[msg.sender]), ")"),
"", "", 1, DEFAULT_GAS_UNITS, tx.gasprice
); emit GetPoints_Pending(msg.sender);
}
} function CaptainsResult(uint JobCounter, string Log2Result) external onlyCaptainsOrdersAllowed {
// the return of the async call
address sender = JobToSenderMap[JobCounter];
uint Points = StringToUint(Log2Result);
PointsPerUser[sender] = Points;
emit GetPoints_Success(sender, Points);
} function CaptainsError(uint JobCounter, string ErrorMsg) external onlyCaptainsOrdersAllowed {
// the return of the async call
address sender = JobToSenderMap[JobCounter];
emit GetPoints_Failed(sender, ErrorMsg);
}}