@wandevs/ca-sdk
v0.3.0
Published
Chain Abstraction SDK for DApps — unified wallet & session-key signing, batched cross-chain execution against the CARouter / Router-API stack.
Readme
@wandevs/ca-sdk
Frontend SDK for the Compass / Wandevs Chain Abstraction stack
(chain-abstraction-router-api + chain-abstraction-contracts CARouter).
A DApp that integrates this SDK gets:
- One-line wallet connect + EIP-712 batch signing
- Wallet mode (sign every batch with the main wallet) and Session mode (sign once, automate the rest with a scoped session key)
- Same external API for both — caller code does not change when you toggle the mode
- Pluggable wallet adapter (viem / ethers), pluggable storage (sessionStorage / localStorage / memory / custom)
- React provider + hooks subpackage
Install
bun add @wandevs/ca-sdk
# peer deps as needed
bun add viem # required
bun add ethers # only if you use the ethers adapter
bun add react # only if you use the React subpackageQuick start (vanilla, viem + session mode)
import { createChainAbstraction, ops, CA_ROUTER_ADDRESS, CA_API_URL } from '@wandevs/ca-sdk';
import {
viemAdapter,
viemSessionAuthVersionResolver,
viemSessionKeyRevoker,
} from '@wandevs/ca-sdk/adapters/viem';
import { createPublicClient, createWalletClient, custom, http, parseEther, toFunctionSelector } from 'viem';
import { mainnet } from 'viem/chains';
const publicClient = createPublicClient({ chain: mainnet, transport: http() });
const wallet = createWalletClient({ chain: mainnet, transport: custom(window.ethereum!) });
const ca = createChainAbstraction({
apiUrl: CA_API_URL, // https://chain-abstraction.wanscan.org/api/v1
routerAddress: CA_ROUTER_ADDRESS, // 0x534b95E0780B434026C8A9bfceFA7a9A4dF846cc — same on every chain via CREATE2
wallet: viemAdapter(wallet),
mode: 'session',
session: {
storage: 'sessionStorage',
authVersion: viemSessionAuthVersionResolver(publicClient),
revokeSessionKey: viemSessionKeyRevoker(wallet),
defaultDeadline: 24 * 3600,
scope: {
allowedSelectors: [
toFunctionSelector('stake(uint256)'),
toFunctionSelector('claim()'),
],
allowedTargets: ['0xVault1', '0xVault2'],
maxValuePerTx: parseEther('1'),
maxValueTotal: parseEther('10'),
// allowedRefundTos defaults to [user] — leave it alone unless you really need to broaden it.
},
},
// Browsers should NOT see apiSecret. Proxy through your own BFF instead.
// For server-side / trusted environments:
// auth: { apiKey: process.env.CA_API_KEY, apiSecret: process.env.CA_API_SECRET },
});
// 1. Connect — in session mode this prompts the wallet ONCE to sign a SessionAuth.
await ca.connect();
// 2. Submit a batch — no further wallet popups.
const { batchId } = await ca.execute([
ops.contractCall({
chainId: 1, target: '0xVault1', abi: VAULT_ABI, fn: 'stake', args: [amount],
}),
]);
// 3. Watch progress
for await (const status of ca.watchBatch(batchId)) {
console.log(status.status, status.chainTasks);
if (status.status === 'completed') break;
}In wallet mode the only change is mode: 'wallet' (and you can drop the session block).
The signature surface — connect, execute, getBatchStatus, watchBatch — is identical.
React
import { ChainAbstractionProvider, useExecuteBatch, useSession } from '@wandevs/ca-sdk/react';
import { ops } from '@wandevs/ca-sdk';
const config = { /* same shape as above */ };
function App() {
return (
<ChainAbstractionProvider config={config}>
<StakeButton />
</ChainAbstractionProvider>
);
}
function StakeButton() {
const { execute, loading, status } = useExecuteBatch();
const session = useSession();
return (
<button
disabled={!session.isActive || loading}
onClick={() => execute([ops.contractCall({ /* ... */ })])}
>
{loading ? 'Submitting…' : 'Stake'}
</button>
);
}useSession() exposes { isActive, timeRemaining, auth, refresh, revoke }.
What the SDK signs
The router-api / CARouter pair uses three EIP-712 typed structs (the SDK pins the typestrings — they MUST match the on-chain definitions byte-for-byte):
EIP712Domain(string name,string version,address verifyingContract)
Operation(uint256 chainId,uint256 nonce,address target,bytes data,uint256 value,
address refundTo,address refundToken,uint256 deadline)
Batch(Operation[] contents)
SessionAuth(address user,address sessionKey,uint256 authVersion,uint256 deadline,
bytes4[] allowedSelectors,address[] allowedTargets,address[] allowedRefundTos,
uint256 maxValuePerTx,uint256 maxValueTotal)
SessionOp(address user,bytes32 batchHash)Notes:
- The domain has no
chainId—SessionAuthis intentionally chain-agnostic (CARouter is at the same address on every chain via CREATE2). Cross-chain replay is prevented byOperation.chainIdinside the batch. SessionOpembedsuserto block cross-user opSig replay.- Pure ETH transfers (
data == "0x") require opting in by adding0x00000000toallowedSelectors. The SDK's default scope does not include it.
Operation builders
import { ops, randomNonce } from '@wandevs/ca-sdk';
import { flattenOps } from '@wandevs/ca-sdk/ops';
const entries = [
ops.erc20Approve({
chainId: 1, token: USDT, spender: VAULT, amount: 1_000_000n,
resetAllowanceFirst: true, // USDT requires approve(0) first — the SDK emits two ops.
}),
ops.contractCall({
chainId: 1, target: VAULT, abi: VAULT_ABI, fn: 'deposit', args: [1_000_000n],
}),
ops.bridge({
fromChainId: 1, toChainId: 56,
fromTokenAddress: USDT, toTokenAddress: USDT_BSC,
fromAddress: user, toAddress: user, fromAmount: 1_000_000n,
bridgeAdapter: '0xBridgeAdapter', data: bridgeCalldata,
bridge: 'wanbridge',
}),
];
const { drafts, metadata } = flattenOps(entries);
await ca.execute(drafts, { metadata });Session lifecycle
ca.session?.isActive(); // boolean — has unexpired SessionAuth in storage
ca.session?.timeRemaining(); // seconds until SessionAuth.deadline
await ca.session?.refresh(); // re-sign immediately, prompting the wallet
await ca.session?.revoke(); // calls CARouter.revokeSessionKey, then clears local state
await ca.session?.clearLocalSession(); // local-only cleanup; does not revoke on-chainThe SDK emits events you can subscribe to for UX prompts:
ca.on((e) => {
if (e.type === 'session:resign-required') showResignBanner(e.reason);
if (e.type === 'batch:submitted') trackEvent('batch_submitted', { batchId: e.batchId });
});If execute() is called and the SessionAuth has expired, the SDK transparently re-signs
(one wallet popup) and continues — the call resolves once the batch is submitted.
Security notes
- Never bundle
apiSecretinto a public web app. Proxy through your own backend. - Session mode must read
CARouter.sessionAuthVersions(user)before signing; useviemSessionAuthVersionResolver(publicClient)or pass an equivalent BFF-backed resolver. session.revoke()requires arevokeSessionKeycallback and only clears local credentials after the on-chain revoke transaction is submitted. UseclearLocalSession()for local-only cleanup.- The session keypair lives in the storage backend you choose.
sessionStorageis the default and clears on tab close; uselocalStorageonly when you intentionally want a longer-lived session key. For SSR / Node, use'memory'or a customSDKStorage. - The SDK runs the same scope checks the contract enforces (
precheckScope) so invalid batches fail locally before the HTTP roundtrip — but the contract is the source of truth. - High-risk actions (vault sweep, raw token transfers, raw approvals, generic swaps,
revokeAllSessions) should remain on wallet mode unless Router-API has strict calldata
policy for the exact action. Don't include broad asset-redirection selectors in
allowedSelectors.
Reference
| Surface | Purpose |
|---|---|
| createChainAbstraction(config) | Construct a client. |
| client.connect() | Resolve user; load/create SessionAuth (session mode). |
| client.execute(drafts, { metadata? }) | Sign + submit a batch. |
| client.getBatchStatus(id) / client.watchBatch(id) | Poll status (one-shot / async iterator). |
| client.session.{isActive,timeRemaining,refresh,revoke,getAuth} | Session lifecycle. |
| client.on(listener) | Subscribe to SDK events. |
| ops.{erc20Approve, contractCall, bridge, nativeTransfer, flatten} | Operation templates. |
| precheckScope(ops, auth) | Same scope rules the contract enforces. |
| viemAdapter(walletClient) / ethersAdapter(signer) | Wallet adapters. |
| <ChainAbstractionProvider> + useChainAbstraction / useExecuteBatch / useSession | React entry. |
Versioning
0.x tracks the router-api / CARouter feat/session rollout. 1.0 will freeze the public
surface once both are GA. Major bumps follow a deprecation-warning + one-minor overlap window.
