Smart Wallet v2 integration
Smart Wallet v2 lets an application escrow funds for a Gmail address. The recipient later proves ownership of a valid Google ID token in the browser and claims the escrowed funds to a normal blockchain wallet.
The backend API supports the enabled chains returned by /v1/config. Current
integrations should start from that endpoint instead of hard-coding chain
addresses, relayer addresses, refund timeouts, or request limits.
Only /v1/send-notification requires the gateway header issued to the partner
app, because it sends email from zkFold infrastructure:
x-smart-wallet-gateway-token: <token>
Chain and asset identifiers
Every request that targets a chain includes a tagged chain object.
EVM:
{ "type": "evm", "chainId": 11155111 }
Cardano:
{ "type": "cardano", "network": "preprod" }
Asset IDs are chain-specific strings.
For EVM, asset is the token contract address. The native asset uses:
0x0000000000000000000000000000000000000000
For Cardano, ada uses an empty string ("").
Cardano native assets use the policy ID followed by the asset name hex, without a separator. The policy ID is always 28 bytes.
Amounts are decimal strings in the asset base unit: wei for ETH, lovelace for ada, and the smallest unit of the selected token for other assets.
Cell references are also chain-specific. EVM balance and claim responses use a
32-byte 0x-prefixed cell ID. Cardano uses txHash#index.
Use this table as the quick reference for chain-specific fields:
| Field | EVM | Cardano |
|---|---|---|
chain |
{ "type": "evm", "chainId": 11155111 } |
{ "type": "cardano", "network": "preprod" } |
| Address fields | 0x EVM address |
bech32 Cardano address |
Native asset |
0x0000000000000000000000000000000000000000 |
"" |
Token asset |
ERC-20 contract address | policy ID plus asset name hex |
amount |
wei for ETH or token base units | lovelace for ada or native asset quantity |
Cell ref |
32-byte 0x cell ID |
txHash#index |
WalletAction.value |
wei decimal string | Cardano value string for the wallet adapter |
WalletAction.data |
calldata | inline datum hex |
Public configuration
Call /v1/config before rendering send or claim UI:
GET /v1/config
The response includes one entry per enabled chain:
{
"chains": [
{
"chain": { "type": "evm", "chainId": 11155111 },
"name": "Sepolia",
"relayer": "0x2222222222222222222222222222222222222222",
"escrow": "0x1111111111111111111111111111111111111111",
"nativeAsset": "0x0000000000000000000000000000000000000000",
"defaults": { "refundTimeoutSeconds": 604800 },
"limits": { "claimRequestCap": 32 }
},
{
"chain": { "type": "cardano", "network": "preprod" },
"name": "Cardano Preprod",
"relayer": "addr_test1...",
"escrow": "addr_test1...",
"nativeAsset": "",
"defaults": { "refundTimeoutSeconds": 604800 },
"limits": { "claimRequestCap": 32 }
}
]
}
Use the selected chain entry as the source for chain, relayer, escrow,
nativeAsset, timeout, and claim batching limits.
Browser SDK
Install the browser SDK:
npm install @zkfold/smart-wallet-sdk
Initialize it once before calling exported functions:
import init, {
canonicalize_email,
email_hash,
key_share_email_hash,
parse_google_jwt,
prove_claim,
prove_key_share,
split_secret,
combine_secret_shares,
} from "@zkfold/smart-wallet-sdk";
await init();
The SDK does not connect wallets, submit transactions, call the backend, or generate wallet keys. It provides the browser-side protocol helpers.
canonicalize_email(email)
Returns the canonical Gmail address string. Use it for user-entered email inputs before showing them back to the user or deriving display state.
const recipient = canonicalize_email(" Recipient@Gmail.com ");
email_hash(email)
Returns the 0x-prefixed Keccak-256 hash of the supplied email string. For
user-entered email, call canonicalize_email first.
const hash = email_hash(recipient);
key_share_email_hash(email)
Canonicalizes the email and returns the 0x-prefixed SHA-256 hash used to key
optional key-share storage.
const keyShareEmailHash = key_share_email_hash("recipient@gmail.com");
parse_google_jwt(compactJwt)
Accepts a compact Google ID token and returns a JSON string. Parse it to get the
Google kid, public JWT segments, email, and email hash.
const parsedJwt = JSON.parse(parse_google_jwt(compactJwt));
const kid = parsedJwt.kid;
The JWT signature stays in the browser. The claim and key-share API requests send only the public JWT header and payload segments plus a proof.
prove_claim(inputJson)
Builds the /v1/claim request body. The function verifies the Google RSA
signature locally and returns a JSON string containing public JWT parts and one
serialized proof per requested cell.
Chain-specific:
chain,refs,to, andrelayermust use the selected chain's formats. EVM proof input also includes the escrow contract address; Cardano proof input uses the selected network and UTxO references.
EVM input:
const claimBody = JSON.parse(
prove_claim(
JSON.stringify({
compactJwt,
googleKey: {
kid: googleKey.kid,
modulus: googleKey.modulus,
exponent: googleKey.exponent,
},
chain: {
type: "evm",
chainId: 11155111,
escrow: "0x1111111111111111111111111111111111111111",
},
refs: [
"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
],
to: "0x3333333333333333333333333333333333333333",
relayer: "0x2222222222222222222222222222222222222222",
}),
),
);
Cardano input:
const claimBody = JSON.parse(
prove_claim(
JSON.stringify({
compactJwt,
googleKey: {
kid: googleKey.kid,
modulus: googleKey.modulus,
exponent: googleKey.exponent,
},
chain: { type: "cardano", network: "preprod" },
refs: [
"f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0#0",
],
to: "addr_test1...",
relayer: "addr_test1...",
}),
),
);
Submit the returned object directly to /v1/claim.
split_secret(secretBase64url)
Splits opaque private-key bytes into three serialized shares and returns a JSON array string. The input is base64url without padding.
const shares = JSON.parse(split_secret(secretBase64url));
Any two shares can reconstruct the original secret.
combine_secret_shares(firstShare, secondShare)
Combines two serialized shares from the same split and returns the original secret as base64url without padding.
const secretBase64url = combine_secret_shares(userShare, zkFoldShare);
prove_key_share(inputJson)
Builds the proof object for key-share storage or retrieval. Request a fresh challenge from the backend first.
Store proof:
const proof = JSON.parse(
prove_key_share(
JSON.stringify({
compactJwt,
googleKey: {
kid: googleKey.kid,
modulus: googleKey.modulus,
exponent: googleKey.exponent,
},
operation: "store",
keyShareEmailHash,
clientId: "partner-app",
challenge,
share: zkFoldShare,
}),
),
);
Retrieve proof:
const proof = JSON.parse(
prove_key_share(
JSON.stringify({
compactJwt,
googleKey: {
kid: googleKey.kid,
modulus: googleKey.modulus,
exponent: googleKey.exponent,
},
operation: "retrieve",
keyShareEmailHash,
clientId: "partner-app",
challenge,
}),
),
);
Send flow
- Fetch
/v1/configand select the chain that matches the connected wallet. - Optionally call
/v1/wallet-assetsto show spendable assets. - Call
/v1/fee-estimateto display the relay fee budget. -
Call
/v1/sendwith the selected chain, sender address, recipient email, asset, amount, and configured timeout. - Submit the returned
WalletActionwith the connected wallet. -
Call
/v1/send-notificationafter the send transaction is submitted or confirmed, depending on the wallet flow.
Chain-specific:
fromis the sender address on the selected chain.assetandamountfollow the formats in the chain-specific fields table.
EVM /v1/send request:
{
"chain": { "type": "evm", "chainId": 11155111 },
"from": "0x3333333333333333333333333333333333333333",
"toEmail": "recipient@gmail.com",
"asset": "0x0000000000000000000000000000000000000000",
"amount": "1000000000000000000",
"timeout": 604800
}
Cardano /v1/send request:
{
"chain": { "type": "cardano", "network": "preprod" },
"from": "addr_test1...",
"toEmail": "recipient@gmail.com",
"asset": "",
"amount": "10000000",
"timeout": 604800
}
Both chains return the same action envelope:
{
"to": "chain-specific destination",
"value": "chain-specific value",
"data": "chain-specific data"
}
For EVM, to is the escrow contract, value is wei, and data is calldata.
For Cardano, to is the escrow script address, value is the Cardano value
string consumed by the frontend wallet adapter, and data is the inline datum
hex.
Claim flow
- Call
/v1/balancewith the recipient email and selected chain. - Sign in with Google and obtain a fresh compact Google ID token in the browser.
- Call
parse_google_jwt(compactJwt)and read thekid. - Call
/v1/google-key/{kid}to get the RSA key material. -
Call
prove_claim(...)with the compact JWT, Google key, selected cells, recipient wallet address, and relayer address from/v1/config. - POST the returned object to
/v1/claim.
Chain-specific: balance
refvalues are also the claimrefs. EVM uses cell IDs, Cardano uses UTxO references, andtois the recipient wallet address on the selected chain.
Balance request:
{
"chain": { "type": "cardano", "network": "preprod" },
"email": "recipient@gmail.com"
}
Balance response:
[
{
"ref": "f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0#0",
"asset": "",
"amount": "10000000",
"refundTime": 1780384523,
"display": {
"name": "Cardano",
"symbol": "ADA",
"decimals": 6
}
}
]
The claim endpoint returns chain-specific transaction hashes:
[
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
]
Optional key shares
Key shares are optional 2-of-3 custody helpers. The SDK treats private keys as opaque bytes. Applications remain responsible for key generation, address derivation, public-key checks, wallet state, and deciding which two shares are combined.
Storage is keyed by:
keyShareEmailHash: returned bykey_share_email_hash(email)clientId: partner application identifier registered in the audience set
Store flow:
- Split the private key bytes with
split_secret. - Keep one share on the end-user device, give one share to the partner app, and store zkFold's share through the backend.
- Request a store challenge with
/v1/key-share/challenge. - Build a store proof with
prove_key_share. -
PUT
/v1/key-sharewithkeyShareEmailHash,clientId,challenge,proof, andshare.
Retrieve flow:
- Request a retrieve challenge with
/v1/key-share/challenge. - Build a retrieve proof with
prove_key_share. - POST
/v1/key-share/retrieve. -
Combine zkFold's returned share with one other share using
combine_secret_shares.
Key-share challenges are short-lived and one-time use. On-chain claim proofs are not accepted for key-share retrieval because the key-share proof includes a fresh backend challenge.