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, and relayer must 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

  1. Fetch /v1/config and select the chain that matches the connected wallet.
  2. Optionally call /v1/wallet-assets to show spendable assets.
  3. Call /v1/fee-estimate to display the relay fee budget.
  4. Call /v1/send with the selected chain, sender address, recipient email, asset, amount, and configured timeout.
  5. Submit the returned WalletAction with the connected wallet.
  6. Call /v1/send-notification after the send transaction is submitted or confirmed, depending on the wallet flow.

Chain-specific: from is the sender address on the selected chain. asset and amount follow 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

  1. Call /v1/balance with the recipient email and selected chain.
  2. Sign in with Google and obtain a fresh compact Google ID token in the browser.
  3. Call parse_google_jwt(compactJwt) and read the kid.
  4. Call /v1/google-key/{kid} to get the RSA key material.
  5. Call prove_claim(...) with the compact JWT, Google key, selected cells, recipient wallet address, and relayer address from /v1/config.
  6. POST the returned object to /v1/claim.

Chain-specific: balance ref values are also the claim refs. EVM uses cell IDs, Cardano uses UTxO references, and to is 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 by key_share_email_hash(email)
  • clientId: partner application identifier registered in the audience set

Store flow:

  1. Split the private key bytes with split_secret.
  2. Keep one share on the end-user device, give one share to the partner app, and store zkFold's share through the backend.
  3. Request a store challenge with /v1/key-share/challenge.
  4. Build a store proof with prove_key_share.
  5. PUT /v1/key-share with keyShareEmailHash, clientId, challenge, proof, and share.

Retrieve flow:

  1. Request a retrieve challenge with /v1/key-share/challenge.
  2. Build a retrieve proof with prove_key_share.
  3. POST /v1/key-share/retrieve.
  4. 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.