Phase 3 (Factory Contract) is open for contributors — see open issues
Contract API

Contract API

The Veil smart contract (invisible_wallet) is a Soroban custom account contract that implements __check_auth for WebAuthn-based transaction authorization.

Contract crate: invisible-wallet v0.1.0 Soroban SDK: v20.0.0 Source: contracts/invisible_wallet/src/


Types

WalletError

All public functions return Result<_, WalletError>. The variants map to Soroban error codes.

#[contracterror]
pub enum WalletError {
    AlreadyInitialized        = 1,
    InvalidSignatureFormat    = 2,
    SignerNotAuthorized        = 3,
    InvalidPublicKey           = 4,
    InvalidSignature           = 5,
    SignatureVerificationFailed = 6,
    InvalidChallenge           = 7,
}
CodeVariantTrigger
1AlreadyInitializedinit() called when a signer already exists
2InvalidSignatureFormat__check_auth signature is not a Vec<Val> of length 4
3SignerNotAuthorizedProvided public key is not stored in the contract
4InvalidPublicKeyCannot parse the 65-byte SEC1 public key
5InvalidSignatureCannot parse the 64-byte raw signature
6SignatureVerificationFailedP-256 ECDSA verification failed
7InvalidChallengebase64url(signaturePayload) not found in clientDataJSON

Functions

init

Initialize the wallet with a P-256 public key. Can only be called once.

pub fn init(
    env: Env,
    initial_signer: BytesN<65>,
) -> Result<(), WalletError>

Parameters

NameTypeDescription
initial_signerBytesN<65>Uncompressed P-256 public key: 0x04 || x (32B) || y (32B)

Errors

  • AlreadyInitialized — a signer is already stored

Example (Stellar CLI)

stellar contract invoke \
  --id <CONTRACT_ID> \
  --fn init \
  -- \
  --initial_signer <HEX_PUBKEY>

add_signer

Add an additional authorized signer. Requires the contract’s own authorization (i.e., an existing signer must sign the transaction).

pub fn add_signer(env: Env, new_signer: BytesN<65>)

Requires: env.current_contract_address().require_auth()

Parameters

NameTypeDescription
new_signerBytesN<65>New uncompressed P-256 public key to authorize

remove_signer

Remove an authorized signer. Requires contract self-authorization.

pub fn remove_signer(env: Env, signer: BytesN<65>)

Requires: env.current_contract_address().require_auth()

⚠️

Be careful not to remove the last signer — this will lock the contract permanently. Guardian recovery (Phase 5) will provide a safety net.


set_guardian

Set a recovery/guardian key for account recovery (Phase 5).

pub fn set_guardian(env: Env, guardian: BytesN<65>)

Requires: env.current_contract_address().require_auth()


execute

Invoke a function on another contract after the wallet has authorized the call.

pub fn execute(
    env: Env,
    target: Address,
    func: Symbol,
    args: Vec<Val>,
)

Requires: env.current_contract_address().require_auth()

Parameters

NameTypeDescription
targetAddressThe contract to call
funcSymbolFunction name
argsVec<Val>Arguments to pass

__check_auth

Called automatically by the Soroban runtime for every transaction that requires this wallet’s authorization. Do not call directly.

fn __check_auth(
    env: Env,
    signature_payload: BytesN<32>,
    signature: Val,
    _auth_contexts: Vec<Context>,
) -> Result<(), WalletError>

Signature format

The signature argument must be a Vec<Val> with exactly 4 elements:

[0]  BytesN<65>  — Uncompressed P-256 public key (0x04 || x || y)
[1]  Bytes       — WebAuthn authenticatorData (variable length)
[2]  Bytes       — WebAuthn clientDataJSON (must contain base64url(payload))
[3]  BytesN<64>  — Raw ECDSA signature (r || s, 32B each)

Verification steps (in order)

  1. Decode signature as Vec<Val>, assert length = 4
  2. Extract and type-check each element
  3. Verify has_signer(pub_key) — reject unauthorized keys
  4. Call auth::verify_webauthn(payload, pub_key, auth_data, client_data_json, sig)
    • challenge_is_present: find base64url(payload) in clientDataJSON
    • Compute SHA256(authData || SHA256(clientDataJSON))
    • Verify P-256 ECDSA signature using p256::ecdsa::VerifyingKey::verify_prehash

Storage

Defined in storage.rs:

pub fn add_signer(env: &Env, key: &BytesN<65>)
pub fn remove_signer(env: &Env, key: &BytesN<65>)
pub fn has_signer(env: &Env, key: &BytesN<65>) -> bool
pub fn set_guardian(env: &Env, guardian_key: &BytesN<65>)
pub fn get_guardian(env: &Env) -> Option<BytesN<65>>
FunctionStorage typeKey
add/remove/has_signerPersistentDataKey::Signer(pubkey)
set/get_guardianInstanceDataKey::Guardian

Cargo Dependencies

[dependencies]
soroban-sdk = { version = "20.0.0", features = ["alloc"] }
p256        = { version = "0.13", features = ["ecdsa", "sha2"] }
sha2        = { version = "0.10", default-features = false }