Skip to content

Signing & transactions

Sigil wallets are EOA-based. Every wallet holds one private key per curve (secp256k1 for EVM chains, ed25519 for Solana), protected by Shamir secret sharing. The SDK exposes three signing primitives: message signing, typed-data signing, and raw transaction sending.

Multi-network signing

Sigil supports 8 networks across 2 curves:

NetworkCurveChain type
Ethereumsecp256k1EVM
Polygonsecp256k1EVM
Arbitrumsecp256k1EVM
Basesecp256k1EVM
Optimismsecp256k1EVM
BSCsecp256k1EVM
Avalanchesecp256k1EVM
Solanaed25519SVM

All EVM networks share the same secp256k1 key, so the wallet address is identical across all EVM chains. Solana uses a separate ed25519 key with its own address.

signMessage — message signing

Signs an arbitrary message with EIP-191 personal_sign semantics (EVM) or the equivalent on Solana. Pass { network } to select which network’s key signs the message.

// Vanilla
const sig = await sigil.signMessage('hello', { network: 'ethereum' });
// React
const { signMessage } = useSignMessage();
const sig = await signMessage('hello', { network: 'arbitrum' });

The default network is your primary network (Ethereum unless configured otherwise). For most EVM use cases you can omit { network } entirely since all EVM chains share the same key.

signTypedData — EIP-712 structured signing

For off-chain orders, EIP-2612 permit, Permit2, SIWE, and any protocol that requires EIP-712 typed-data signatures.

const { signature, digest } = await sigil.signTypedData({
domain: { name: 'USD Coin', version: '2', chainId: 42161, verifyingContract: '0x...' },
types: { Permit: [/* ... */] },
primaryType: 'Permit',
message: { owner, spender, value, nonce, deadline },
});

Returns { signature, digest }. The signature is 65 bytes (r || s || v). digest is the 32-byte EIP-712 hash that was signed, useful for server-side ecrecover validation.

sendTransaction — EVM transaction sending

Sends a raw EIP-1559 transaction from the wallet’s EOA. The user pays gas from their native balance on the target chain.

// Vanilla
const r = await sigil.sendTransaction({
chain: 'arbitrum',
to: BRIDGE_CONTRACT,
data: encodeDeposit(amountUSDC),
value: '0',
preview: { title: 'Deposit to Hyperliquid', description: `${usdc(amountUSDC)} USDC` },
});
// -> { chain, chainId, txHash, from }
// React
const { sendTransaction, isLoading } = useSendTransaction();
const r = await sendTransaction({
chain: 'base',
to: NFT_CONTRACT,
data: encodeMint(1),
preview: { title: 'Mint #42' },
});

What happens when you call it

  1. SDK asks the iframe to handle the transaction (the iframe holds the end-user’s session token).
  2. Iframe POSTs to Sigil’s /v1/wallets/evm/prepare-tx with the chain, to, data, and value. Sigil resolves nonce, gas estimation, and fee parameters.
  3. Iframe shows the user a confirmation screen with your preview (title + description + fields). The user approves.
  4. Iframe reconstructs the SSS-protected key in memory, signs the EIP-1559 envelope, wipes the buffer.
  5. Iframe POSTs /v1/wallets/evm/broadcast-tx with the signed transaction. Sigil forwards it to the chain’s RPC.
  6. SDK returns { chain, chainId, txHash, from }.

The function returns once the RPC accepted the broadcast. Finality is your responsibility — chains differ in confirmation times.

Parameters

FieldRequiredNotes
chainyes'ethereum', 'polygon', 'arbitrum', 'base', 'optimism', 'bsc', 'avalanche'.
toyesContract or recipient address.
datano0x-prefixed call data. Omit for pure native transfers.
valuenoDecimal wei as a string. Default '0'.
gasLimitnoOverride the gas estimate. Useful when some contracts under-estimate.
previewno{ title, description?, fields? } shown on the confirm screen.

What you write

The bytes for the calls you want. That’s specific to your product — which contracts, which arguments — and is the same code you’d write without Sigil. Use viem / ethers / your own ABI encoder; the SDK just takes the resulting (to, data) pair.

Errors

sendTransaction rejects with Error whose message describes what failed. Common cases:

  • user rejected — the user clicked Reject on the iframe confirm screen.
  • signer_mismatch: ... — the signature recovered to a different EOA than expected. Usually means the device share in localStorage is from a previous wallet. The iframe wipes the stale share automatically; the next attempt routes through the recovery flow.
  • chain_unsupported — the chain handle isn’t wired in Sigil.
  • estimate_gas_reverted — the destination contract reverted during gas estimation. Pass gasLimit explicitly if you’ve confirmed the call is correct.
  • broadcast_failed — the RPC rejected the signed tx. Most common reasons: insufficient funds (the wallet can’t afford gas + value), or a nonce gap from a previous in-flight tx.

Routing through a smart account

sendTransaction automatically routes through a smart account when one is provisioned for the target chain — see Smart accounts for the model. The quick version: call sigil.enableSmartAccount('base') once per chain, and from that point on any sendTransaction({ chain: 'base', ... }) becomes a sponsored ERC-4337 UserOp instead of an EOA EIP-1559 tx.

await sigil.enableSmartAccount('base');
const r = await sigil.sendTransaction({
chain: 'base',
to: NFT_CONTRACT,
data: encodeMint(1),
preview: { title: 'Mint #42' },
});
// Sigil paid the gas. r.from = smart-account address.

Watch for the gas_cap_exceeded error code — that’s the wallet having burned its €10 monthly sponsorship budget. The Errors page lists the full set; Smart accounts covers the cap behaviour.

Server-to-server signing (TEE mode)

For backend-initiated signing without user interaction, Sigil offers a server-to-server (S2S) API. In TEE mode, the signing key is held inside a trusted execution environment and never leaves it.

S2S signing is covered in the Server-to-server API documentation. It uses API key authentication instead of the iframe flow, and supports the same networks and curves as client-side signing.

Recipes