Promises
What is a Promise?​
A Promise is a core concept in the SOCKET framework, allowing users to initiate, manage, and track asynchronous actions reliably. Think of a promise as a digital agreement between the sender and the network — once created, it can be fulfilled, rejected.
In SOCKET, a Promise represents a future outcome of an action initiated by a user or application. It encapsulates a request that might take time to complete, such as sending a message or executing a transaction.
Promise Lifecycle​
- Created: The promise is initialized with specific details.
- Pending: The promise is awaiting completion.
- Fulfilled: The promise was successfully completed.
- Rejected: The promise failed to complete.
- Completed: The promise reaches a final state.
Creating a Promise​
To create a promise in SOCKET, initiate an async
action that will generate an immutable promise tracked by the SOCKET protocol. Before diving into promises, it's important to understand how to configure transaction behaviors using overrides.
Setting Transaction Overrides​
SOCKET provides flexible transaction configuration through the _setOverrides
helper functions. These settings control how your transactions are processed:
-
Read vs Write Mode (
isReadCall
)- Use
_setOverrides(Read.ON)
before read operations - Use
_setOverrides(Read.OFF)
to return to write mode - By default, all calls are treated as write operations
- Use
-
Parallel Execution (
isParallelCall
)- Enable with
_setOverrides(Parallel.ON)
to batch multiple calls - Disable with
_setOverrides(Parallel.OFF)
for sequential execution - Default is OFF (sequential execution)
Example of reading values across multiple chains
function multiChainOperation() external async returns (bytes32 asyncId) {
// Enable read mode for balance checks
_setOverrides(Read.ON);
// Read balances from multiple chains
ISuperToken(chain1Forwarder).balanceOf(msg.sender);
ISuperToken(chain2Forwarder).balanceOf(msg.sender);
// Set up promise callbacks
IPromise(chain1Forwarder).then(this.chainOperation.selector, abi.encode(1, asyncId));
IPromise(chain2Forwarder).then(this.chainOperation.selector, abi.encode(2, asyncId));
// Consecutive reads on the same chain need to be handled as first come first serve
ISuperToken(chain3Forwarder).balanceOf(msg.sender);
IPromise(chain3Forwarder).then(this.chainOperation.selector, abi.encode(3, asyncId));
ISuperToken(chain3Forwarder).balanceOf(msg.sender);
IPromise(chain3Forwarder).then(this.chainOperation.selector, abi.encode(3, asyncId));
// Return to write mode for subsequent operations
_setOverrides(Read.OFF);
} - Enable with
-
Gas Limits
- Set per-payload gas limits:
_setOverrides(gasLimitValue)
- Not required for read operations
- Set per-payload gas limits:
You can combine these settings using the multi-parameter version:
_setOverrides(Read isReadCall, Parallel isParallelCall, uint256 gasLimit, Fees memory fees);
Here's an example of a token transfer that creates a promise to check the user's balance before proceeding:
contract SuperTokenAppGateway is AppGatewayBase, Ownable {
(...)
function transfer(uint256 amount, address srcForwarder, address dstForwarder)
external
async
returns (bytes32 asyncId)
{
// Check user balance on src chain
asyncId = _getCurrentAsyncId();
_setOverrides(Read.ON);
// Request to forwarder and deploys immutable promise contract and stores it
ISuperToken(srcForwarder).balanceOf(msg.sender);
IPromise(srcForwarder).then(this.checkBalance.selector, abi.encode(amount, asyncId));
_setOverrides(Read.OFF);
ISuperToken(srcForwarder).burn(msg.sender, amount);
ISuperToken(dstForwarder).mint(msg.sender, amount);
}
}
Handling a Promise - the .then
method​
When a promise is created, it can be resolved or rejected asynchronously. Use the .then()
method to handle the successful resolution of a promise and a callback function to process the returned data.
IPromise(srcForwarder).then(this.checkBalance.selector, abi.encode(amount, asyncId));
Callback Example​
The callback function checkBalance
processes the returned data, ensuring the user has sufficient balance before completing the transaction.
function checkBalance(bytes memory data, bytes memory returnData) external onlyPromises {
(uint256 amount, bytes32 asyncId) = abi.decode(data, (uint256, bytes32));
uint256 balance = abi.decode(returnData, (uint256));
if (balance < amount) {
_revertTx(asyncId);
return;
}
}
The callback function checkBalance
:
- Uses the
onlyPromises
modifier to ensure it's only called by the promises system - Takes two parameters:
data
: The encoded data from the original function (amount and asyncId)returnData
: The actual data returned from the source chain (balance). This data is automatically returned by the Watcher.
Example Use Case​
Consider a chain-abstracted token bridge transaction where the user's balance must be validated before tokens are transferred. Here's how it works:
- Initiate the transfer: The
transfer
function creates a promise to check the user's balance. - Set up a promise callback: The
then
method sets a callback to handle the balance check. - Process the returned data: The
checkBalance
function ensures the balance is sufficient before completing the transfer.
contract SuperTokenAppGateway is AppGatewayBase, Ownable {
(...)
function checkBalance(bytes memory data, bytes memory returnData) external onlyPromises {
(uint256 amount, bytes32 asyncId) = abi.decode(data, (uint256, bytes32));
uint256 balance = abi.decode(returnData, (uint256));
if (balance < amount) {
_revertTx(asyncId);
return;
}
}
function transfer(uint256 amount, address srcForwarder, address dstForwarder)
external
async
returns (bytes32 asyncId)
{
// Check user balance on src chain
_readCallOn();
// Request to forwarder and deploys immutable promise contract and stores it
ISuperToken(srcForwarder).balanceOf(msg.sender);
IPromise(srcForwarder).then(this.checkBalance.selector, abi.encode(amount, asyncId));
_readCallOff();
ISuperToken(srcForwarder).burn(msg.sender, amount);
ISuperToken(dstForwarder).mint(msg.sender, amount);
}
}