Cavos

Wallet actions

What you do with a connected wallet — read balances, send tokens, sign messages, and sign without submitting.

Once connected, this page covers the common things you do — all via useCavos() inside a <CavosProvider>. See React for the provider/hook setup.

When can you execute? Both undeployed and ready wallets can execute. The first execution on an undeployed wallet deploys the account atomically. Only needs-device-approval blocks execution — route that to a passkey or multi-device approval.

Every chain-specific call narrows on wallet.chain first, because execute keeps each chain's native signature. See Chains.

Read the native balance

The kit exposes each chain's balance read differently.

Solana

CavosSolana exposes the underlying @solana/web3.js Connection, so you read SOL directly. Balance is in lamports (1 SOL = 10⁹ lamports).

TypeScript
const { wallet, address } = useCavos();
if (wallet?.chain === "solana") {
  const lamports = await wallet.connection.getBalance(address); // number
  const sol = lamports / 1e9;
}

Stellar

CavosStellar has a balance() method. It returns native XLM in stroops (1 XLM = 10⁷ stroops).

TypeScript
if (wallet?.chain === "stellar") {
  const stroops = await wallet.balance(); // bigint
  const xlm = Number(stroops) / 1e7;
}

Starknet

There is no single "native balance" call in the kit for Starknet (the fee token is an ERC-20). Read a specific token with a balanceOf call via execute, or query the RPC directly with starknet.js.

Fund from a testnet faucet

Only on testnet. Handy for demos and first-run UX.

Solana — airdrop

TypeScript
if (wallet?.chain === "solana") {
  const sig = await wallet.connection.requestAirdrop(address, 1_000_000_000); // 1 SOL
  // confirm with connection.getSignatureStatus(sig) if you need to wait
}

Public Solana devnet rate-limits airdrops. For reliable funding, use a private RPC (Helius/Alchemy) as your rpcUrl, or the official Solana faucet.

Stellar — friendbot

Stellar testnet is funded by the public friendbot:

TypeScript
await fetch(`https://friendbot.stellar.org?addr=${encodeURIComponent(address)}`);

Send native tokens

Solana — SOL transfer

CavosSolana.execute(amount, destination) transfers SOL, sponsored by the relayer when appId is set. amount is in lamports (bigint).

TypeScript
if (wallet?.chain === "solana") {
  const txHash = await wallet.execute(1_000_000_000n, destinationAddress); // 1 SOL
}

Stellar — XLM transfer

Same signature, amount in stroops (bigint).

TypeScript
if (wallet?.chain === "stellar") {
  const txHash = await wallet.execute(10_000_0000n, destinationAddress); // 1 XLM
}

Both return the transaction hash as a string. Build explorer links per chain — Solana: https://explorer.solana.com/tx/${hash}?cluster=devnet; Stellar testnet: https://stellar.expert/explorer/testnet/tx/${hash}.

Starknet — arbitrary calls

On Starknet, use the context's execute(calls) for a multicall (transfers, swaps, any contract interaction). It's sponsored by default.

TypeScript
const { execute } = useCavos();
const { transactionHash } = await execute([
  { contractAddress: TOKEN, entrypoint: "transfer", calldata: [to, amountLow, amountHigh] },
]);

Formatting amounts? Parse a human input ("1.5") to base units by multiplying by 10 ** decimals and rounding; format a base-unit bigint back by dividing. Decimals per chain: Solana 9, Stellar 7, Starknet fee token 18.

Security after sign-in

These are shared wrappers on useCavos() for the three current adapters. Their cryptographic implementation remains chain-specific. They are the same primitives documented in Passkeys and Recovery, surfaced through the React hook.

Passkey (secure account access)

TypeScript
const { walletStatus, enrollPasskeyDefault, passkeySupported } = useCavos();

if (passkeySupported && !walletStatus.hasPasskey) {
  await enrollPasskeyDefault(); // prompts the platform authenticator once
}

walletStatus.hasPasskey flips to true once enrolled.

Recovery code

Starknet (and grandfathered Stellar). Native Solana/Stellar: setupRecovery is a no-op — restore with the enclave or approveDeviceWithPasskey().

TypeScript
const { setupRecovery } = useCavos();

const code = await setupRecovery(); // string | undefined — show it ONCE when present

The recovery code is returned once. Display it immediately and tell the user to store it somewhere safe. Only its derived public key is registered on-chain — the code itself never leaves the device.

Approve a new device

When a user signs in on a fresh device, walletStatus.needsDeviceApproval is true and they can't transact yet. If they have a synced passkey:

TypeScript
const { walletStatus, approveDeviceWithPasskey } = useCavos();

if (walletStatus.needsDeviceApproval) {
  await approveDeviceWithPasskey(); // one passkey prompt authorizes this device
}

See Multi-device for the full device-approval flow.

Sign messages & transactions

Beyond sending transactions, a wallet needs to sign data off-chain (prove ownership without moving funds) and sign transactions without submitting (hand them to your own relayer). Both are available on every current adapter; future adapters must document and implement them before they are advertised.

signMessage — off-chain signature

Sign an arbitrary message with the wallet's signing key. Nothing is submitted, no gas is paid, no state changes. Returns a uniform MessageSignature:

const { signMessage } = useCavos();               // or wallet.signMessage(...)
const { signature, publicKey, curve } = await signMessage("Sign in to dapp.com");
FieldMeaning
signature64-byte signature (r‖s for secp256r1, ed25519 raw).
publicKeyThe verifier input — Starknet: device P-256 hex; Solana/Stellar: the account address (base58 / G…).
curve"secp256r1" (Starknet) or "ed25519" (Solana, Stellar).

Per-chain details:

  • Starknet — signs sha256("Cavos Signed Message:\n<len>\n" + message) with the device P-256 key. publicKey is the uncompressed 04‖x‖y hex.
  • Solana — signs the prefixed message with the Ed25519 spend key. publicKey is the base58 address. curve is "ed25519".
  • Stellar — the signing key is the ed25519 control key (not P-256), so curve is "ed25519" and publicKey is the G… control address.

Verifying off-chain (what a third party / backend does):

TypeScript
import { p256 } from "@noble/curves/p256";
import { sha256 } from "@noble/hashes/sha256";
import { Keypair } from "@stellar/stellar-sdk";

if (curve === "secp256r1") {
  // Starknet — verify the P-256 signature over sha256(prefixedMessage).
  const valid = p256.verify(signature, sha256(prefixedMessage), publicKeyHex);
} else {
  // Solana / Stellar — ed25519.
}

The verifier must reconstruct the same prefixed message the signer used (see the prefix per chain above). For Sign-In-with-X flows, embed a nonce + domain in the message string.

signTransaction — sign without submit

Sign a transaction WITHOUT broadcasting it. Each chain returns its own signed artifact (narrow on chain):

TypeScript
const { wallet } = useCavos();
if (wallet?.chain === "starknet") {
  const signed = await wallet.signTransaction(calls);   // → { calldata, signature, nonce, resourceBounds, version }
} else if (wallet?.chain === "stellar") {
  const signed = await wallet.signTransaction(amount, destination);  // → { xdr }
} else if (wallet?.chain === "solana") {
  const signed = await wallet.signTransaction(amount, destination);  // → { message, signature, publicKey }
}

What you get back, and what to do with it:

  • Starknet — the signed invoke (calldata, the 5-felt device signature, nonce, resourceBounds, version). A relayer broadcasts it via the account's invokeFunction.
  • Stellar — the control-signed inner transaction as base64 XDR. Submit it directly via Horizon, or hand it to a relayer to fee-bump (the control signature stays valid through the wrap).
  • Solana — { message, signature, publicKey } where signature is Ed25519 over the Solana message. A relayer can fee-pay; it must not replace the user signature.

Solana signTransaction is a user-signed message, not a fully submitted transaction. Attach the relayer (or feePayer) signature and broadcast.

All signTransaction outputs are single-use and bind to on-chain state. Starknet binds to the nonce + resource bounds; Stellar to the sequence number (and a 180s timeout); Solana to the on-chain account nonce. If any other transaction from the same account is submitted first, the signature becomes invalid. Broadcast promptly.

On this page