SDK Interface Guide
The SDK provides a TypeScript interface for Storail mutations. It is not intended to hide the protocol model. It constructs AEH calls, signs ERC-2771 requests, submits them to the relay, tracks receipts, and waits for subgraph visibility when requested.
Client Setup
import { createStorailMutationClient } from "@storail/sdk";
const client = createStorailMutationClient({
chainId,
aehAddress,
forwarderAddress,
relayUrl,
subgraphUrl,
wallet,
publicClient,
});
Required fields:
chainId: expected EVM chain id.aehAddress: deployedAuthorizedEventHub.forwarderAddress: deployedERC2771Forwarder.relayUrl: relay worker base URL.wallet: adapter that exposes address and signing methods.publicClient: RPC client used for preflight and receipt tracking, orrpcUrlto let the SDK create one.
Optional fields:
subgraphUrl: required only when waiting forindexed.operationStore: persistence for operation snapshots.confirmationThreshold: receipt confirmation count.indexingTimeoutMs: maximum time to wait for subgraph visibility.indexingPollMs: polling interval while waiting for the subgraph.defaultGasLimit: forward request gas limit.fetch: custom fetch implementation.forwarderName: EIP-712 domain name. Defaults toStorail Forwarder.forwarderVersion: EIP-712 domain version. Defaults to1.paymentDemoAppId: app id used by the payment demo helpers.
Wallet Adapter
The wallet adapter must provide:
getAddress()signTypedData(...)
Optional wallet methods:
getChainId(): if present, the SDK checks it againstchainIdbefore starting an operation.sendTransaction(...): required only for{ mode: "direct" }.
Relay mode does not require sendTransaction. Direct mode does not use signTypedData.
Public-Domain Methods
await client.publish({
path,
providerId,
pointer,
contentHash,
metadata,
});
await client.update({
path,
providerId,
pointer,
contentHash,
metadata,
});
await client.remove({ path });
await client.grantWriter({ domain, writer });
await client.revokeWriter({ domain, writer });
The SDK validates paths, addresses, and bytes32 content hashes before signing.
Application Methods
Generic application action:
await client.submitToApp({
appId,
actionType,
payload,
});
Payment demo helpers:
await client.paymentDemoTransfer({
to,
amount,
});
await client.paymentDemoInitializeSupply({
amount,
});
These helpers only encode the demo instructions. The payment balances are still derived by the payment subgraph.
Operation States
SDK calls return a StorailOperation.
Important successful states:
confirmed: the transaction is on-chain and has reached the configured confirmation threshold.indexed: the subgraph has processed the receipt block and the expected derived state is visible.
Other states include:
draftsigningsignedsubmittingsubmittedrejectedrevertedrate_limitedrelay_unavailableindexing_delayedfailed
The SDK type also reserves mined and expired, but the current implementation does not emit them.
Use onStatus to update UI state:
const operation = await client.publish(record, {
onStatus(next) {
setOperation(next);
},
});
Relay Mode
Relay mode is the default.
The internal flow is:
1. Build AEH calldata. 2. Read the user's forwarder nonce. 3. Ask the wallet to sign the EIP-712 ForwardRequest. 4. Call forwarder.verify(request). 5. Simulate forwarder.execute(request). 6. Submit the signed request to POST /v1/relay. 7. Wait for the transaction receipt. 8. If subgraphUrl is configured, wait until _meta.block.number >= receipt.blockNumber. 9. Query the expected derived state.
By default, public-domain methods wait for indexing. If the application only needs the chain confirmation boundary, pass:
await client.publish(record, { waitForIndex: false });
This flow is why applications should not manually sign and submit relay requests unless they need lower-level control.
Direct Mode
Use direct mode when the user should pay gas or when the relay is unavailable:
await client.update(record, { mode: "direct" });
Direct mode sends a transaction directly to AuthorizedEventHub. It can still wait for receipt confirmation and subgraph indexing.
Direct mode does not emit signing or signed. Its normal successful state sequence is:
draft -> submitting -> submitted -> confirmed -> indexed
If publicClient.call is available and preflight is not disabled, the SDK performs an eth_call against AuthorizedEventHub before asking the wallet to send the transaction. If publicClient.call is not available, direct mode skips that preflight step.
Indexed Verification
The SDK does not treat any query result as sufficient by itself. It first checks subgraph progress:
{
_meta {
block {
number
}
}
}
Only after the subgraph has reached the receipt block does the SDK check the expected derived state.
Built-in verifiers cover:
- record exists after
publish/update - record no longer exists after
remove - writer permission active after
grantWriter - writer permission inactive after
revokeWriter
For application-specific calls, pass indexedVerifier.
Operation Persistence
Provide operationStore when the frontend needs retry or recovery across reloads.
The SDK stores:
operationId- signed forward request
requestId- transaction hashes
- current status
If a request was signed but not submitted, resumeOperation can resubmit the same signed request. This avoids consuming a new forwarder nonce.
const recovered = await client.resumeOperation(operationId);
Current resume behavior:
- if there is a signed request but no
requestId, the SDK can resubmit the same request - if there is a
requestId, the SDK can query relay status - if there is a transaction hash, the SDK can recover receipt state
- it does not automatically re-run application-specific indexed verification during resume
Error Handling
Errors are exposed as StorailError with stable codes.
Common codes:
VALIDATION_ERRORSIGNATURE_REJECTEDFORWARDER_NONCE_CHANGEDCONTRACT_REVERTEDRATE_LIMITEDRELAYER_POOL_EXHAUSTEDRPC_UNAVAILABLERELAY_UNAVAILABLETRANSACTION_REVERTEDINDEXING_DELAYEDFAILED
Applications should branch on error.code, not on RPC or relay text.