Ethereum requires a transaction to be on-chain (meaning the transaction is included in the latest block) before returning a transaction receipt with final status to users, so they know whether the transaction is a success or not.
Godwoken provides a faster way to confirm transactions. Once a transaction is verified in the mem-pool, the instant transaction receipt will be generated immediately. This feature is called Instant Finality
.
If you want to build a low-latency user experience for on-chain interactions in your dApp, you could turn on Instant Finality
feature by using the RPC with an additional path or query parameters:
# http
https://example_web3_rpc_url?instant-finality-hack=true
https://example_web3_rpc_url/instant-finality-hack
# websocket
ws://example_web3_rpc_url/ws?instant-finality-hack=true
Note: Environments like Hardhat will swallow the HTTP URL's query parameter, so you might want to use the /instant-finality-hack
path to overcome that.
Also notice that under instant-finality-hack
mode, there might be some compatibility issues with Ethereum toolchain like ether.js
. If you care more about compatibility, please use the bare RPC URL https://example_web3_rpc_url
, which is considered to be the most compatible with Ethereum.
The gas fee is preventing new users step into the web3 world. Users must learn to get the native token(CKB or ETH) before playing a blockchain game or exchanging tokens with a DEX. The gasless feature can provide a way for developers to sponsor transaction fees for users to give them a smooth experience.
The gas feature is based on the ERC-4337 solution but way simpler. To use a such feature, users sign and send a special gasless transaction to call a specific smart contract named Entrypoint
, then Entrypoint
will call another smart contract named Paymaster
deployed by developers to check if they are willing to pay the gas fee for this transaction. The special gasless transaction must satisfy the requirements:
tx.data
must be the call data of callingEntrypoint
'shandleOp(UserOperation calldata op)
function, which contains the target contract and the paymaster address.- Must set
tx.gasPrice
to 0 - The
tx.to
must be set to theEntrypoint
contract.
struct UserOperation {
address callContract; # address of the target contract
bytes callData; # call data of the target contract
uint256 callGasLimit; # gas used to execute the call
uint256 verificationGasLimit; # gas used to verification
uint256 maxFeePerGas; # gas price
uint256 maxPriorityFeePerGas; # must equals to maxFeePerGas, reserved for EIP-1559
bytes paymasterAndData; # pay master address and extra data
}
More specs can be found here
Let's say we have the entrypoint contract at 0x9a11f47c0729fc56d9c44c059987d40703249569
and as a game developer, we want to pay the gas fee for some whitelist users, so we wrote a paymaster contract just like this one and deployed it at 0x6b019795aa36dd19eb4a4d76f3b9a40239b7c19f
.
dapp frontend using ethers.js
// define UserOp
const userOp: UserOperationStruct = {
callContract: realGameContract.address,
callData: realGameContractCallData,
callGasLimit: gasToExecuteRealGameContractCallData,
verificationGasLimit: gasToVerifyPaymaster,
maxFeePerGas: gasPrice,
maxPriorityFeePerGas: gasPrice,
paymasterAndData: "0x6b019795aa36dd19eb4a4d76f3b9a40239b7c19f"
}
// 1. construct and send gasless transaction
const abiCoder = new ethers.utils.AbiCoder();
const userOp = abiCoder.encode(["tuple(address callContract, bytes callData, uint256 callGasLimit, uint256 verificationGasLimit, uint256 maxFeePerGas, uint256 maxPriorityFeePerGas, bytes paymasterAndData) UserOperation"], [userOp]);
// first 4 bytes of keccak hash of handleOp((address,bytes,uint256,uint256,uint256,uint256,bytes))
const fnSelector = "fb4350d8";
// gasless payload = ENTRYPOINT_HANDLE_OP_SELECTOR + abiEncode(UserOperation)
const payload = "0x" + fnSelector + userOp.slice(2);
const gaslessTx = {
from: whitelistUser.address,
to: '0x9a11f47c0729fc56d9c44c059987d40703249569',
data: payload,
gasPrice: 0,
gasLimit: 1000000,
value: 0,
}
const signer = new ethers.Wallet("private key");
const tx = await signer.sendTransaction(gaslessTx);
await tx.wait();
// 2. or just use ethers contract factory
{
// Send tx with a valid user.
const EntryPoint = await ethers.getContractFactory("EntryPoint");
const entrypoint = await EntryPoint.attach('0x9a11f47c0729fc56d9c44c059987d40703249569');
const tx = await entryPoint.connect(whitelistUser).handleOp(userOp, {gasLimit: 100000, gasPrice: 0});
await tx.wait();
}