Initialise the wallet with its first signer and domain-binding parameters.
rp_id - the WebAuthn relying party ID (e.g. "localhost" for dev,
"veil.app" for production). Must match the domain that
serves the frontend. Keep it configurable - do not hardcode.
origin - the exact WebAuthn origin (e.g. "https://veil.app").
Must match the origin field the browser embeds in every
clientDataJSON for this deployment.
fn init(
env: soroban_sdk::Env,
initial_signer: soroban_sdk::BytesN<65>,
rp_id: soroban_sdk::Bytes,
origin: soroban_sdk::Bytes,
) -> Result<(), WalletError>
Add a new signer key to the wallet. Requires authorization from the
contract itself (i.e. an existing signer must authorize via __check_auth).
Returns the index assigned to the new signer.
fn add_signer(env: soroban_sdk::Env, new_public_key: soroban_sdk::BytesN<65>) -> u32
Remove a signer by index. Requires authorization from the contract. Rejects removal if it would leave the wallet with zero signers.
fn remove_signer(env: soroban_sdk::Env, index: u32) -> Result<(), WalletError>
Atomically replace a registered signer key (old_public_key) with a new
one (new_public_key) — the device-rotation flow for a lost or replaced
passkey.
Requires authorization from the contract itself, so an existing signer
must authorize the call via __check_auth. This means only the current
owner can rotate the key: a lost device cannot be silently swapped out by
a third party.
The new key takes over the same index as the old one, so the signer set size is unchanged and — critically — the wallet's contract address and balances are untouched. No redeploy is needed.
Errors:
InvalidPublicKey — old and new are the same key (no-op).SignerAlreadyExists — new_public_key is already registered.SignerNotFound — old_public_key is not a registered signer.fn rotate_signer(
env: soroban_sdk::Env,
old_public_key: soroban_sdk::BytesN<65>,
new_public_key: soroban_sdk::BytesN<65>,
) -> Result<(), WalletError>
Called by the Soroban runtime to authorize a transaction.
Three credential branches are handled, tried in order:
Branch 1 — Allowance (spender Address)
signature is an Address. The spender presents itself and the
on-chain allowance record is debited. No cryptographic work needed
because spender.require_auth() already verified the spender's sig.
Branch 2 — Session key Vec<Val>[key_id, ed25519_sig, nonce]
signature is a Vec<Val> with exactly 3 elements:
[0] BytesN<32> — key_id (storage lookup handle, NOT a secret)
[1] BytesN<64> — ed25519 signature of signature_payload
[2] u64 — current contract nonce (replay binding)
Authorization requires:
signature_payload that verifies against
the public key registered in the ACL for key_id. The key_id is
public; possession of it is NOT sufficient — the holder must produce
a fresh signature on the host-provided payload for every call.fn __check_auth(
env: soroban_sdk::Env,
signature_payload: soroban_sdk::BytesN<32>,
signature: soroban_sdk::Val,
_auth_contexts: soroban_sdk::Vec,
) -> Result<(), WalletError>
Return the current monotonic nonce for this wallet.
fn get_nonce(env: soroban_sdk::Env) -> u64
fn has_signer(env: soroban_sdk::Env, key: soroban_sdk::BytesN<65>) -> bool
fn get_signers(env: soroban_sdk::Env) -> soroban_sdk::Map>
fn execute(
env: soroban_sdk::Env,
target: soroban_sdk::Address,
func: soroban_sdk::Symbol,
args: soroban_sdk::Vec,
)
Set spending limit for a specific token and spender.
Requires passkey authorization (i.e. from the contract itself).
fn approve(
env: soroban_sdk::Env,
spender: soroban_sdk::Address,
token: soroban_sdk::Address,
amount: i128,
expiry: Option,
)
Get the current allowance for a spender and token.
fn get_allowance(
env: soroban_sdk::Env,
spender: soroban_sdk::Address,
token: soroban_sdk::Address,
) -> Option
Set or update the guardian address for this wallet.
Only callable by the current wallet signer (authenticated via __check_auth). The guardian is authorized to initiate key recovery if the signer key is lost.
env - The Soroban environment handle.guardian - The Address of the new guardian.fn set_guardian(env: soroban_sdk::Env, guardian: soroban_sdk::Address)
Initiate a guardian recovery to replace the wallet signer key.
Only callable by the designated guardian. Records the new public key
and starts a timelock countdown. After the timelock expires,
complete_recovery can be called to finalize the key replacement.
env - The Soroban environment handle.new_public_key - The 65-byte uncompressed public key of the new signer.WalletError::NoGuardianSet - if no guardian has been configured.WalletError::RecoveryAlreadyPending - if a recovery is already in progress.fn initiate_recovery(
env: soroban_sdk::Env,
new_public_key: soroban_sdk::BytesN<65>,
) -> Result<(), WalletError>
Complete a pending guardian recovery after the timelock has expired.
This function is permissionless - anyone can call it once the timelock
has expired. It replaces the wallet signer with the new public key
that was specified during initiate_recovery.
WalletError::RecoveryNotPending - if no recovery has been initiated.WalletError::RecoveryTimelockActive - if the timelock has not yet expired.fn complete_recovery(env: soroban_sdk::Env) -> Result<(), WalletError>
Cancel a pending guardian recovery.
Only callable by the current wallet signer (the contract itself must authorize). This allows a wallet owner who still has their key to abort an unwanted or malicious recovery attempt.
WalletError::RecoveryNotPending - if no recovery has been initiated.fn cancel_recovery(env: soroban_sdk::Env) -> Result<(), WalletError>
Register a scoped session key with an ACL.
The caller must supply the ed25519 public key (pubkey) of the session
key holder in addition to the key_id storage handle. Every future
__check_auth call using this key must carry an ed25519 signature of
signature_payload produced by the corresponding private key.
Requires wallet owner authorization (existing signer via __check_auth).
fn register_session_key(
env: soroban_sdk::Env,
pubkey: soroban_sdk::BytesN<32>,
key_id: soroban_sdk::BytesN<32>,
target_contract: soroban_sdk::Address,
selector: soroban_sdk::Symbol,
amount_cap: i128,
expiry: u64,
)
Immediately revoke a session key. Requires the wallet owner to authorize.
fn revoke_session_key(env: soroban_sdk::Env, key_id: soroban_sdk::BytesN<32>)
Set a 24-hour rolling spend limit for a WebAuthn signer key.
Once set, every __check_auth call using key_id sums all context
amounts and checks the total against cap within a rolling 24-hour window.
Pass cap = 0 to block all spending. Call remove_key_spend_limit to
lift the restriction entirely.
Requires wallet owner authorization.
fn set_key_spend_limit(
env: soroban_sdk::Env,
key_id: soroban_sdk::BytesN<65>,
cap: i128,
)
Remove the 24-hour spend limit for a signer key (no limit = no cap).
Requires wallet owner authorization.
fn remove_key_spend_limit(env: soroban_sdk::Env, key_id: soroban_sdk::BytesN<65>)
Get the current spend window record for a signer key, if any.
fn get_key_spend_limit(
env: soroban_sdk::Env,
key_id: soroban_sdk::BytesN<65>,
) -> Option
Register a designated recovery key for this wallet.
The recovery key is an Address (e.g. a cold-wallet account) that is
authorized to initiate signer rotation via request_recovery.
Requires current wallet signer authorization.
fn set_recovery_key(env: soroban_sdk::Env, key: soroban_sdk::Address)
Start a 7-day recovery cooldown to replace the active signer.
Only the designated recovery key (set via set_recovery_key) may call
this. After the 7-day timelock expires, anyone can call finalize_recovery
to complete the rotation.
NoRecoveryKeySet — no recovery key has been registered.RecoveryAlreadyPending — a recovery request is already in progress.fn request_recovery(
env: soroban_sdk::Env,
new_signer: soroban_sdk::BytesN<65>,
) -> Result<(), WalletError>
Finalize a pending recovery request after the 7-day timelock has expired.
Permissionless — anyone may call this once ledger.timestamp > unlock_at.
RecoveryNotPending — no recovery request is in progress.RecoveryTimelockActive — the 7-day cooldown has not yet elapsed.fn finalize_recovery(env: soroban_sdk::Env) -> Result<(), WalletError>
Cancel a pending recovery-key request.
Only the current wallet signer (the contract itself) may cancel. This lets a wallet owner who still holds their key abort a recovery attempt before it is finalized.
RecoveryNotPending — no recovery request is in progress.fn cancel_recovery_request(env: soroban_sdk::Env) -> Result<(), WalletError>