passkey-kit 0.12.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +408 -227
- package/{types → dist}/base.d.ts +1 -0
- package/dist/base.d.ts.map +1 -0
- package/{src/base.ts → dist/base.js} +8 -10
- package/dist/base.js.map +1 -0
- package/dist/base64url.d.ts +18 -0
- package/dist/base64url.d.ts.map +1 -0
- package/dist/base64url.js +18 -0
- package/dist/base64url.js.map +1 -0
- package/dist/constants.d.ts +91 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +115 -0
- package/dist/constants.js.map +1 -0
- package/dist/contract-errors.d.ts +69 -0
- package/dist/contract-errors.d.ts.map +1 -0
- package/dist/contract-errors.js +132 -0
- package/dist/contract-errors.js.map +1 -0
- package/dist/errors.d.ts +163 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +244 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +94 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +92 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer/codec.d.ts +18 -0
- package/dist/indexer/codec.d.ts.map +1 -0
- package/dist/indexer/codec.js +32 -0
- package/dist/indexer/codec.js.map +1 -0
- package/dist/indexer/index.d.ts +25 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +34 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/mercury.d.ts +94 -0
- package/dist/indexer/mercury.d.ts.map +1 -0
- package/dist/indexer/mercury.js +245 -0
- package/dist/indexer/mercury.js.map +1 -0
- package/dist/indexer/stellar-indexer.d.ts +115 -0
- package/dist/indexer/stellar-indexer.d.ts.map +1 -0
- package/dist/indexer/stellar-indexer.js +279 -0
- package/dist/indexer/stellar-indexer.js.map +1 -0
- package/dist/indexer/types.d.ts +73 -0
- package/dist/indexer/types.d.ts.map +1 -0
- package/dist/indexer/types.js +9 -0
- package/dist/indexer/types.js.map +1 -0
- package/dist/kit/auth-payload.d.ts +106 -0
- package/dist/kit/auth-payload.d.ts.map +1 -0
- package/dist/kit/auth-payload.js +227 -0
- package/dist/kit/auth-payload.js.map +1 -0
- package/dist/kit/deploy-ops.d.ts +45 -0
- package/dist/kit/deploy-ops.d.ts.map +1 -0
- package/dist/kit/deploy-ops.js +69 -0
- package/dist/kit/deploy-ops.js.map +1 -0
- package/dist/kit/tx-ops.d.ts +95 -0
- package/dist/kit/tx-ops.d.ts.map +1 -0
- package/dist/kit/tx-ops.js +125 -0
- package/dist/kit/tx-ops.js.map +1 -0
- package/dist/kit/wallet-ops.d.ts +59 -0
- package/dist/kit/wallet-ops.d.ts.map +1 -0
- package/dist/kit/wallet-ops.js +122 -0
- package/dist/kit/wallet-ops.js.map +1 -0
- package/dist/kit/webauthn-ops.d.ts +55 -0
- package/dist/kit/webauthn-ops.d.ts.map +1 -0
- package/dist/kit/webauthn-ops.js +79 -0
- package/dist/kit/webauthn-ops.js.map +1 -0
- package/dist/kit.d.ts +132 -0
- package/dist/kit.d.ts.map +1 -0
- package/dist/kit.js +290 -0
- package/dist/kit.js.map +1 -0
- package/dist/managers/credential-manager.d.ts +39 -0
- package/dist/managers/credential-manager.d.ts.map +1 -0
- package/dist/managers/credential-manager.js +47 -0
- package/dist/managers/credential-manager.js.map +1 -0
- package/dist/managers/index.d.ts +7 -0
- package/dist/managers/index.d.ts.map +1 -0
- package/dist/managers/index.js +7 -0
- package/dist/managers/index.js.map +1 -0
- package/dist/managers/signer-manager.d.ts +62 -0
- package/dist/managers/signer-manager.d.ts.map +1 -0
- package/dist/managers/signer-manager.js +111 -0
- package/dist/managers/signer-manager.js.map +1 -0
- package/dist/managers/submission-manager.d.ts +52 -0
- package/dist/managers/submission-manager.d.ts.map +1 -0
- package/dist/managers/submission-manager.js +77 -0
- package/dist/managers/submission-manager.js.map +1 -0
- package/dist/relayer.d.ts +61 -0
- package/dist/relayer.d.ts.map +1 -0
- package/dist/relayer.js +122 -0
- package/dist/relayer.js.map +1 -0
- package/dist/rpc-data.d.ts +45 -0
- package/dist/rpc-data.d.ts.map +1 -0
- package/dist/rpc-data.js +61 -0
- package/dist/rpc-data.js.map +1 -0
- package/dist/sac.d.ts +31 -0
- package/dist/sac.d.ts.map +1 -0
- package/dist/sac.js +47 -0
- package/dist/sac.js.map +1 -0
- package/dist/server.d.ts +79 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +152 -0
- package/dist/server.js.map +1 -0
- package/dist/signers.d.ts +83 -0
- package/dist/signers.d.ts.map +1 -0
- package/dist/signers.js +124 -0
- package/dist/signers.js.map +1 -0
- package/dist/storage/index.d.ts +13 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +12 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/indexeddb.d.ts +26 -0
- package/dist/storage/indexeddb.d.ts.map +1 -0
- package/dist/storage/indexeddb.js +100 -0
- package/dist/storage/indexeddb.js.map +1 -0
- package/dist/storage/localStorage.d.ts +22 -0
- package/dist/storage/localStorage.d.ts.map +1 -0
- package/dist/storage/localStorage.js +86 -0
- package/dist/storage/localStorage.js.map +1 -0
- package/dist/storage/memory.d.ts +18 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +41 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/types.d.ts +136 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +39 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +72 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +253 -0
- package/dist/utils.js.map +1 -0
- package/dist/validation.d.ts +42 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +75 -0
- package/dist/validation.js.map +1 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +9 -0
- package/dist/version.js.map +1 -0
- package/package.json +47 -20
- package/.claude/settings.local.json +0 -20
- package/.cursorrules +0 -170
- package/.gitattributes +0 -2
- package/.github/workflows/release.yml +0 -21
- package/.vscode/settings.json +0 -6
- package/PROPOSAL.md +0 -397
- package/cheatsheet.txt +0 -18
- package/clone-js-sdk.sh +0 -21
- package/packages/passkey-kit-sdk/README.md +0 -54
- package/packages/passkey-kit-sdk/package.json +0 -21
- package/packages/passkey-kit-sdk/src/index.ts +0 -171
- package/packages/passkey-kit-sdk/tsconfig.json +0 -98
- package/packages/passkey-kit-sdk/types/index.d.ts +0 -190
- package/packages/sac-sdk/README.md +0 -54
- package/packages/sac-sdk/package.json +0 -21
- package/packages/sac-sdk/src/index.ts +0 -586
- package/packages/sac-sdk/tsconfig.json +0 -98
- package/packages/sac-sdk/types/index.d.ts +0 -532
- package/pnpm-workspace.yaml +0 -2
- package/src/index.ts +0 -5
- package/src/kit.ts +0 -823
- package/src/sac.ts +0 -29
- package/src/server.ts +0 -179
- package/src/types.ts +0 -32
- package/tsconfig.json +0 -28
- package/types/index.d.ts +0 -5
- package/types/kit.d.ts +0 -95
- package/types/sac.d.ts +0 -13
- package/types/server.d.ts +0 -29
- package/types/types.d.ts +0 -22
package/README.md
CHANGED
|
@@ -1,339 +1,520 @@
|
|
|
1
1
|
# Passkey Kit
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> **Looking for the latest smart wallet SDK?**
|
|
5
|
-
>
|
|
6
|
-
> This package is the **legacy precursor** to [OpenZeppelin Smart Accounts](https://docs.openzeppelin.com/stellar-contracts/accounts/smart-account). For new projects, use **[smart-account-kit](https://github.com/kalepail/smart-account-kit)** — a comprehensive SDK built on top of the audited [OpenZeppelin stellar-contracts](https://github.com/OpenZeppelin/stellar-contracts) library.
|
|
7
|
-
>
|
|
8
|
-
> Smart Account Kit includes:
|
|
9
|
-
> - Context rules with fine-grained authorization scopes
|
|
10
|
-
> - Policy support (threshold multisig, spending limits, custom policies)
|
|
11
|
-
> - Session management with automatic credential persistence
|
|
12
|
-
> - External wallet adapter support
|
|
13
|
-
> - Built-in indexer for contract discovery
|
|
14
|
-
>
|
|
15
|
-
> See the [OpenZeppelin Smart Account package](https://github.com/OpenZeppelin/stellar-contracts/tree/main/packages/accounts) and [multisig example](https://github.com/OpenZeppelin/stellar-contracts/tree/main/examples/multisig-smart-account/account) for more details.
|
|
3
|
+
A TypeScript SDK for creating and using **smart-wallet accounts on Stellar with WebAuthn passkeys**. A wallet is a Soroban smart contract whose signers can be secp256r1 passkeys, Ed25519 keys, or policy contracts. The kit handles the WebAuthn ceremonies, deterministic wallet-address derivation, the flat multi-signer signing pipeline, fee-sponsored submission, and signer discovery.
|
|
16
4
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
A TypeScript SDK for creating and managing Stellar smart wallets using passkeys. Works with [OpenZeppelin Relayer](https://docs.openzeppelin.com/relayer/1.3.x/guides/stellar-channels-guide) for submitting passkey-signed transactions onchain.
|
|
5
|
+
- **Client (`PasskeyKit`)** — runs in the browser: create/connect wallets, sign transactions, build signer-management transactions. Holds no secrets.
|
|
6
|
+
- **Server (`PasskeyServer`)** — runs server-side: submits transactions through a relayer (fee sponsorship), plus convenience signer-discovery helpers over the keyless Mercury indexer. Holds the relayer secret.
|
|
21
7
|
|
|
22
|
-
|
|
8
|
+
> [!IMPORTANT]
|
|
9
|
+
> **Security.** Review the contract and SDK yourself before holding meaningful value, and read [Caveats](#caveats).
|
|
10
|
+
|
|
11
|
+
> [!NOTE]
|
|
12
|
+
> **Looking for context rules, thresholds, and spending-limit policies?** [smart-account-kit](https://github.com/kalepail/smart-account-kit) is a sibling SDK built on the audited [OpenZeppelin stellar-contracts](https://github.com/OpenZeppelin/stellar-contracts) account. It uses a different on-chain authorization model (context rules + an auth digest) than passkey-kit's flat `Signatures` map, so the two are not drop-in compatible — pick the model that fits your app.
|
|
13
|
+
|
|
14
|
+
## Contents
|
|
15
|
+
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Packaging & exports](#packaging--exports)
|
|
18
|
+
- [Quick start](#quick-start)
|
|
19
|
+
- [`PasskeyKit` (client)](#passkeykit-client)
|
|
20
|
+
- [Signers](#signers)
|
|
21
|
+
- [Signer management](#signer-management)
|
|
22
|
+
- [`PasskeyServer` (server)](#passkeyserver-server)
|
|
23
|
+
- [Submission (relayer)](#submission-relayer)
|
|
24
|
+
- [Discovery (indexer)](#discovery-indexer)
|
|
25
|
+
- [Tokens (`SACClient`)](#tokens-sacclient)
|
|
26
|
+
- [Storage adapters](#storage-adapters)
|
|
27
|
+
- [Errors](#errors)
|
|
28
|
+
- [Types](#types)
|
|
29
|
+
- [Caveats](#caveats)
|
|
30
|
+
- [Contract interface](#contract-interface)
|
|
31
|
+
- [Repository layout & development](#repository-layout--development)
|
|
32
|
+
- [Resources](#resources)
|
|
23
33
|
|
|
24
34
|
## Installation
|
|
25
35
|
|
|
26
36
|
```bash
|
|
27
|
-
pnpm
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## Exports
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
import {
|
|
34
|
-
PasskeyKit, // Client-side wallet management
|
|
35
|
-
PasskeyServer, // Server-side utilities
|
|
36
|
-
SACClient, // Stellar Asset Contract helper
|
|
37
|
-
PasskeyClient, // Low-level contract client (from passkey-kit-sdk)
|
|
38
|
-
SignerKey, // Signer key type constructor
|
|
39
|
-
SignerStore, // Storage type enum
|
|
40
|
-
type Signer, // Signer type
|
|
41
|
-
type SignerLimits // Signer limits type
|
|
42
|
-
} from 'passkey-kit'
|
|
37
|
+
pnpm add passkey-kit
|
|
38
|
+
# peer dependency
|
|
39
|
+
pnpm add @stellar/stellar-sdk
|
|
43
40
|
```
|
|
44
41
|
|
|
45
|
-
|
|
42
|
+
`@stellar/stellar-sdk` is a **peer dependency** (`>=16.0.0`); the kit targets Protocol 27 smart accounts, which earlier SDKs cannot express.
|
|
46
43
|
|
|
47
|
-
##
|
|
44
|
+
## Packaging & exports
|
|
48
45
|
|
|
49
|
-
|
|
46
|
+
The package ships **compiled ESM + type declarations** from `dist/` (not raw TypeScript — no bundler transpile step is required). It exposes three entry points, split so server secrets can never be pulled into a browser bundle:
|
|
50
47
|
|
|
51
|
-
|
|
48
|
+
| Import | Contents | Where it runs |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `passkey-kit` | `PasskeyKit`, signers, types, errors, validation, crypto helpers, the keyless `MercuryIndexer` + indexer types | Browser or server |
|
|
51
|
+
| `passkey-kit/storage` | `MemoryStorage`, `LocalStorageAdapter`, `IndexedDBStorage` | Browser (persistence) |
|
|
52
|
+
| `passkey-kit/server` | `PasskeyServer`, `RelayerClient` — **holds the relayer secret** | Server only |
|
|
52
53
|
|
|
53
54
|
```ts
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
startRegistration,
|
|
61
|
-
startAuthentication
|
|
62
|
-
}
|
|
63
|
-
})
|
|
55
|
+
// Browser
|
|
56
|
+
import { PasskeyKit, PasskeySigner, Ed25519Signer, SACClient, SignerKey, SignerStore } from "passkey-kit";
|
|
57
|
+
import { IndexedDBStorage } from "passkey-kit/storage";
|
|
58
|
+
|
|
59
|
+
// Server ONLY (never import from browser code — it carries the relayer secret)
|
|
60
|
+
import { PasskeyServer } from "passkey-kit/server";
|
|
64
61
|
```
|
|
65
62
|
|
|
66
|
-
|
|
63
|
+
## Quick start
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
|----------|------|-------------|
|
|
70
|
-
| `keyId` | `string \| undefined` | Current passkey ID (base64url) |
|
|
71
|
-
| `wallet` | `PasskeyClient \| undefined` | Connected wallet client |
|
|
72
|
-
| `networkPassphrase` | `string` | Network passphrase |
|
|
65
|
+
### 1. Configure the client (browser)
|
|
73
66
|
|
|
74
|
-
|
|
67
|
+
```ts
|
|
68
|
+
import { PasskeyKit } from "passkey-kit";
|
|
69
|
+
|
|
70
|
+
const kit = new PasskeyKit({
|
|
71
|
+
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
72
|
+
networkPassphrase: "Test SDF Network ; September 2015",
|
|
73
|
+
// Canonical v1 smart-wallet WASM hash (testnet); see docs/deployments-*.md
|
|
74
|
+
walletWasmHash: "fdefad64b96837147e1c333e51f537b696eab925e9f147e63d597c04e3c903f0",
|
|
75
|
+
});
|
|
76
|
+
```
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
### 2. Create a wallet
|
|
79
|
+
|
|
80
|
+
`createWallet` runs the passkey registration ceremony and builds a **signed** deploy transaction. Submission is a separate, server-side step (below).
|
|
78
81
|
|
|
79
82
|
```ts
|
|
80
|
-
const {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
authenticatorSelection?: AuthenticatorSelectionCriteria
|
|
86
|
-
}
|
|
87
|
-
)
|
|
83
|
+
const { keyIdBase64, contractId, signedTx } = await kit.createWallet(
|
|
84
|
+
"My App", // shown in the passkey prompt
|
|
85
|
+
"user@example.com", // user identifier
|
|
86
|
+
);
|
|
87
|
+
// `signedTx` is a base64 XDR string ready to submit; `contractId` is the wallet address (C…).
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
### 3. Submit through the server
|
|
91
|
+
|
|
92
|
+
`PasskeyServer` submits via the relayer, which pays the fees. It never throws for expected failures — branch on `result.success`.
|
|
92
93
|
|
|
93
94
|
```ts
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
import { PasskeyServer } from "passkey-kit/server";
|
|
96
|
+
|
|
97
|
+
const server = new PasskeyServer({
|
|
98
|
+
networkPassphrase: "Test SDF Network ; September 2015",
|
|
99
|
+
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
100
|
+
relayer: {
|
|
101
|
+
baseUrl: process.env.RELAYER_BASE_URL!, // e.g. https://channels.openzeppelin.com/testnet
|
|
102
|
+
apiKey: process.env.RELAYER_API_KEY!,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = await server.send(signedTx);
|
|
107
|
+
if (result.success) {
|
|
108
|
+
console.log("deployed in tx", result.hash);
|
|
109
|
+
} else {
|
|
110
|
+
console.error(`[${result.error.code}] ${result.error.message}`);
|
|
111
|
+
}
|
|
99
112
|
```
|
|
100
113
|
|
|
101
|
-
|
|
102
|
-
Connects to an existing wallet using a passkey.
|
|
114
|
+
### 4. Reconnect later
|
|
103
115
|
|
|
104
116
|
```ts
|
|
105
|
-
const {
|
|
106
|
-
|
|
107
|
-
keyId?: string | Uint8Array, // Skip passkey prompt if provided
|
|
108
|
-
getContractId?: (keyId: string) => Promise<string | undefined>, // Lookup function
|
|
109
|
-
walletPublicKey?: string // For backwards compatibility
|
|
110
|
-
})
|
|
117
|
+
const { contractId } = await kit.connectWallet();
|
|
118
|
+
// Resolves the wallet from the passkey and VERIFIES the passkey is a live signer on it.
|
|
111
119
|
```
|
|
112
120
|
|
|
113
|
-
|
|
114
|
-
|
|
121
|
+
### 5. Sign & submit a transfer
|
|
122
|
+
|
|
123
|
+
Build any Soroban transaction, sign its wallet auth entries with the connected passkey, then submit.
|
|
115
124
|
|
|
116
125
|
```ts
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
126
|
+
import { SACClient } from "passkey-kit";
|
|
127
|
+
|
|
128
|
+
const sac = new SACClient({
|
|
129
|
+
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
130
|
+
networkPassphrase: "Test SDF Network ; September 2015",
|
|
131
|
+
});
|
|
132
|
+
const token = sac.getSACClient("C…nativeSacId");
|
|
133
|
+
|
|
134
|
+
const tx = await token.transfer({ from: kit.contractId!, to: "C…recipient", amount: 10_000_000n });
|
|
135
|
+
await kit.sign(tx); // default signer = the connected passkey
|
|
136
|
+
const res = await server.send(tx);
|
|
127
137
|
```
|
|
128
138
|
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
## `PasskeyKit` (client)
|
|
140
|
+
|
|
141
|
+
Browser-side facade for wallet lifecycle and signing. Holds no secrets.
|
|
142
|
+
|
|
143
|
+
### Configuration
|
|
144
|
+
|
|
145
|
+
| Option | Type | Required | Description |
|
|
146
|
+
|---|---|---|---|
|
|
147
|
+
| `rpcUrl` | `string` | Yes | Stellar RPC URL. |
|
|
148
|
+
| `networkPassphrase` | `string` | Yes | Network passphrase. |
|
|
149
|
+
| `walletWasmHash` | `string` (hex) | Yes | Smart-wallet WASM hash used to deploy new wallets. |
|
|
150
|
+
| `rpId` | `string` | No | WebAuthn Relying Party id (domain). Defaults to the current origin. |
|
|
151
|
+
| `deploySource` | `string` (`S…`) | No | Secret key for the fee-paying deployer. Defaults to the canonical deterministic deployer. **Overriding it changes derived wallet addresses** (see [Deterministic derivation](#deterministic-derivation)). |
|
|
152
|
+
| `timeoutInSeconds` | `number` | No | Transaction time bound (default `30`; the relayer requires `<= 30`). |
|
|
153
|
+
| `storage` | `StorageAdapter` | No | Passkey-record persistence (see [Storage adapters](#storage-adapters)). |
|
|
154
|
+
| `WebAuthn` | `WebAuthnClient` | No | Custom `startRegistration`/`startAuthentication` (for testing). |
|
|
155
|
+
|
|
156
|
+
### Properties
|
|
157
|
+
|
|
158
|
+
| Property | Type | Description |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| `keyId` | `string \| undefined` | Connected passkey's base64url credential id. |
|
|
161
|
+
| `wallet` | `PasskeyClient \| undefined` | Connected wallet's generated contract client. |
|
|
162
|
+
| `contractId` | `string \| undefined` | Connected wallet address (getter). |
|
|
163
|
+
| `deployerPublicKey` | `string` | The fee-paying deployer's `G…` address (getter). |
|
|
164
|
+
| `networkPassphrase` / `rpcUrl` / `walletWasmHash` / `rpId` | `string` | The resolved config. |
|
|
165
|
+
| `events` | `PasskeyEventEmitter` | Lifecycle events: `walletCreated`, `walletConnected`, `walletDisconnected`. |
|
|
166
|
+
|
|
167
|
+
### Lifecycle methods
|
|
168
|
+
|
|
169
|
+
| Method | Returns | Description |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `createKey(appName, userName, options?)` | `CreatedPasskey` | Run a passkey registration ceremony **without** deploying a wallet. |
|
|
172
|
+
| `createWallet(appName, userName, options?)` | `CreateWalletResult` | Register a passkey and build a signed deploy transaction (initializes the wallet via `__constructor` with the passkey as the first signer). Submit the returned `signedTx` via `PasskeyServer`. |
|
|
173
|
+
| `connectWallet(options?)` | `ConnectWalletResult` | Resolve a wallet from a passkey (derivation → storage → injected indexer lookup) and **verify** the passkey is a live signer on it. |
|
|
174
|
+
| `disconnect()` | `void` | Clear the connected wallet/keyId. |
|
|
175
|
+
| `requireWallet()` | `PasskeyClient` | Return the connected wallet or throw `WalletNotConnectedError`. |
|
|
176
|
+
|
|
177
|
+
`options` for `createKey`/`createWallet` is `{ authenticatorSelection?: AuthenticatorSelectionCriteria }`.
|
|
178
|
+
|
|
179
|
+
`connectWallet` options:
|
|
180
|
+
|
|
181
|
+
| Option | Type | Description |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| `keyId` | `string \| Uint8Array` | Connect a specific credential, skipping the discovery ceremony. |
|
|
184
|
+
| `getContractId` | `(keyId: string) => Promise<string \| undefined>` | Indexer-backed keyId → wallet lookup, used only when derivation and storage both miss. |
|
|
185
|
+
| `verifyWasmHash` | `boolean` | Also assert the wallet's on-chain WASM hash equals `walletWasmHash`. Off by default (an upgraded wallet legitimately runs a different hash). |
|
|
186
|
+
|
|
187
|
+
### Signing methods
|
|
188
|
+
|
|
189
|
+
| Method | Returns | Description |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `sign(txn, signer?, options?)` | `AssembledTransaction<T>` | Sign every wallet auth entry of an assembled transaction. `signer` defaults to a `PasskeySigner()` for the connected passkey. |
|
|
192
|
+
| `signAuthEntry(entry, signer?, options?)` | `xdr.SorobanAuthorizationEntry` | Sign a single auth entry. |
|
|
193
|
+
|
|
194
|
+
`options` is `{ expiration?: number }` — the signature-expiration ledger (defaults to the configured timeout window).
|
|
195
|
+
|
|
196
|
+
> [!IMPORTANT]
|
|
197
|
+
> `sign`/`signAuthEntry` now take a **`Signer` instance** as the second argument, replacing the old `sign(txn, { keyId | keypair | policy })` option trio. See [Signers](#signers).
|
|
198
|
+
|
|
199
|
+
## Signers
|
|
200
|
+
|
|
201
|
+
A `Signer` authenticates a signature payload and produces the on-chain `(SignerKey, Signature)` pair the wallet's `__check_auth` expects. Three concrete signers cover the three signer kinds:
|
|
202
|
+
|
|
203
|
+
| Signer | Constructs | Notes |
|
|
204
|
+
|---|---|---|
|
|
205
|
+
| `new PasskeySigner(keyId?)` | secp256r1 WebAuthn assertion | `keyId` selects a credential; `"any"` lets the authenticator pick a discoverable one; omit to use the kit's connected passkey. **Default** for `sign`. |
|
|
206
|
+
| `new Ed25519Signer(keypair)` / `Ed25519Signer.fromSecret("S…")` | Ed25519 signature | Local Stellar keypair. `.address` returns the `G…` key. |
|
|
207
|
+
| `new PolicySigner(policyAddress)` | policy authorization | No signature bytes; the wallet invokes the policy's `policy__` during `__check_auth`. |
|
|
131
208
|
|
|
132
209
|
```ts
|
|
133
|
-
|
|
134
|
-
```
|
|
210
|
+
import { PasskeySigner, Ed25519Signer, PolicySigner } from "passkey-kit";
|
|
135
211
|
|
|
136
|
-
|
|
212
|
+
await kit.sign(tx); // connected passkey
|
|
213
|
+
await kit.sign(tx, new Ed25519Signer(keypair)); // Ed25519
|
|
214
|
+
await kit.sign(tx, new PolicySigner("C…policyAddr")); // policy co-sign
|
|
215
|
+
await kit.sign(tx, new PasskeySigner("any")); // any discoverable passkey
|
|
216
|
+
```
|
|
137
217
|
|
|
138
|
-
|
|
218
|
+
Multi-sign by signing the same transaction with several signers in turn — each merges its entry into the flat `Signatures` map (host-ordered):
|
|
139
219
|
|
|
140
220
|
```ts
|
|
141
|
-
|
|
142
|
-
await
|
|
143
|
-
await account.addEd25519(publicKey, limits, store, expiration?)
|
|
144
|
-
await account.addPolicy(policy, limits, store, expiration?)
|
|
145
|
-
|
|
146
|
-
// Update signers
|
|
147
|
-
await account.updateSecp256r1(keyId, publicKey, limits, store, expiration?)
|
|
148
|
-
await account.updateEd25519(publicKey, limits, store, expiration?)
|
|
149
|
-
await account.updatePolicy(policy, limits, store, expiration?)
|
|
150
|
-
|
|
151
|
-
// Remove signer
|
|
152
|
-
await account.remove(signerKey)
|
|
221
|
+
await kit.sign(tx, new PasskeySigner());
|
|
222
|
+
await kit.sign(tx, new Ed25519Signer(cosignerKeypair));
|
|
153
223
|
```
|
|
154
224
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
- `policy` - Policy contract address
|
|
159
|
-
- `limits` - `SignerLimits` (see Types below)
|
|
160
|
-
- `store` - `SignerStore.Persistent` or `SignerStore.Temporary`
|
|
161
|
-
- `expiration` - Optional ledger expiration
|
|
225
|
+
## Signer management
|
|
226
|
+
|
|
227
|
+
Each method builds an `AssembledTransaction` (`WalletTx`) that wraps one contract admin function — submit it via `PasskeyServer.send`. A wallet must be connected.
|
|
162
228
|
|
|
163
|
-
|
|
229
|
+
| Method | Wrapped contract fn | Description |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `addSecp256r1(keyId, publicKey, limits, store, expiration?)` | `add_signer` | Add a passkey signer. |
|
|
232
|
+
| `updateSecp256r1(keyId, limits, store, expiration?)` | `update_signer` | Update a passkey signer's limits/storage/expiration. The public key is re-read from the ledger, never caller-supplied. |
|
|
233
|
+
| `addEd25519(publicKey, limits, store, expiration?)` | `add_signer` | Add an Ed25519 signer (`publicKey` = `G…`). |
|
|
234
|
+
| `updateEd25519(publicKey, limits, store, expiration?)` | `update_signer` | Update an Ed25519 signer. |
|
|
235
|
+
| `addPolicy(policy, limits, store, expiration?)` | `add_signer` | Add a policy signer (`policy` = `C…`). Invokes the policy's `install` hook. |
|
|
236
|
+
| `updatePolicy(policy, limits, store, expiration?)` | `update_signer` | Update a policy signer. |
|
|
237
|
+
| `remove(signerKey)` | `remove_signer` | Remove a signer. No policy code runs on this path. |
|
|
238
|
+
| `upgrade(newWasmHash)` | `upgrade` | Replace the wallet's WASM (`Buffer`/`Uint8Array`, 32 bytes). |
|
|
239
|
+
| `getSigner(signerKey)` | `get_signer` | Read a signer entry from the ledger (temporary before persistent). Returns `SignerVal \| null`. |
|
|
164
240
|
|
|
165
|
-
|
|
241
|
+
Parameters:
|
|
166
242
|
|
|
167
|
-
|
|
243
|
+
- `keyId` — passkey credential id (base64url `string` or raw `Uint8Array`).
|
|
244
|
+
- `publicKey` — 65-byte secp256r1 key (`string`/`Uint8Array`) for passkeys, or a `G…` Stellar public key for Ed25519.
|
|
245
|
+
- `policy` — policy contract address (`C…`).
|
|
246
|
+
- `limits` — [`SignerLimits`](#signerlimits) (`undefined` = fully unlimited).
|
|
247
|
+
- `store` — `SignerStore.Persistent` or `SignerStore.Temporary`.
|
|
248
|
+
- `expiration` — optional UNIX-timestamp (seconds) after which the signer is invalid.
|
|
168
249
|
|
|
169
|
-
|
|
250
|
+
> [!IMPORTANT]
|
|
251
|
+
> **Signer key material comes from the ledger or the authenticator, not from
|
|
252
|
+
> an indexer.** Indexer rows (`getSigners`, `WalletSigner.publicKey`) are for
|
|
253
|
+
> display/lookup only. The kit's `updateSecp256r1` re-reads the key from the
|
|
254
|
+
> ledger, and `addSecp256r1` should only ever receive the key from the
|
|
255
|
+
> WebAuthn `createKey`/`connectWallet` attestation. If you build
|
|
256
|
+
> `update_signer`/`add_signer` transactions yourself (e.g. via
|
|
257
|
+
> `buildSecp256r1SignerTx`), source key material from the ledger
|
|
258
|
+
> (`getSigner`) or the authenticator.
|
|
170
259
|
|
|
171
260
|
```ts
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
261
|
+
import { SignerStore, SignerKey } from "passkey-kit";
|
|
262
|
+
|
|
263
|
+
// Add an unlimited Ed25519 co-signer, stored persistently.
|
|
264
|
+
const tx = await kit.addEd25519("G…", undefined, SignerStore.Persistent);
|
|
265
|
+
await kit.sign(tx); // authorize with the connected passkey
|
|
266
|
+
await server.send(tx);
|
|
267
|
+
|
|
268
|
+
// Remove it.
|
|
269
|
+
const rm = await kit.remove(SignerKey.Ed25519("G…"));
|
|
270
|
+
await kit.sign(rm);
|
|
271
|
+
await server.send(rm);
|
|
181
272
|
```
|
|
182
273
|
|
|
274
|
+
## `PasskeyServer` (server)
|
|
275
|
+
|
|
276
|
+
Server-only facade (`passkey-kit/server`). Holds the relayer secret — never import it from browser code.
|
|
277
|
+
|
|
278
|
+
### Configuration
|
|
279
|
+
|
|
280
|
+
| Option | Type | Required | Description |
|
|
281
|
+
|---|---|---|---|
|
|
282
|
+
| `networkPassphrase` | `string` | Yes | Network passphrase. |
|
|
283
|
+
| `rpcUrl` | `string` | No | Stellar RPC URL. Enables the temporary-signer eviction probe in `getSigners`. |
|
|
284
|
+
| `relayer` | `RelayerClientConfig` | No | Fee-sponsored submission (below). |
|
|
285
|
+
| `mercury` | `MercuryConfig` | No | Mercury's keyless hosted passkey-indexer (below). |
|
|
286
|
+
|
|
287
|
+
`RelayerClientConfig`: `{ baseUrl: string, apiKey: string, adminSecret?: string, timeout?: number }` (default timeout 6 min).
|
|
288
|
+
`MercuryConfig`: `{ url?: string }` — the keyless passkey-indexer base URL; **defaults to the network's hosted endpoint** (`https://{testnet,mainnet}.mercurydata.app/rest/passkey-indexer`), so it can be omitted.
|
|
289
|
+
|
|
183
290
|
### Methods
|
|
184
291
|
|
|
185
|
-
|
|
186
|
-
|
|
292
|
+
| Method | Returns | Description |
|
|
293
|
+
|---|---|---|
|
|
294
|
+
| `send(input, options?)` | `TransactionResult` | Submit an `AssembledTransaction \| Transaction \| string` via the relayer. Picks the `{ func, auth }` Soroban path for wallet invocations and the `{ xdr }` fee-bump path for deploys / source-account auth. **Never throws.** |
|
|
295
|
+
| `getTransaction(transactionId)` | `TransactionResult` | Poll a `skipWait` submission by its relayer id. |
|
|
296
|
+
| `getSigners(contractId)` | `WalletSigner[]` | Enumerate a wallet's signers via the indexer (flags evicted temporary signers when `rpcUrl` is set). |
|
|
297
|
+
| `getContractId(options, index?)` | `string \| undefined` | Reverse lookup: the wallet address for a signer. `options` = exactly one of `{ keyId }`, `{ publicKey }`, `{ policy }`. |
|
|
298
|
+
|
|
299
|
+
`options` for `send`/`getTransaction` is `{ skipWait?: boolean, fundRelayerId?: string }`. `getSigners`/`getContractId` delegate to a [`MercuryIndexer`](#discovery-indexer) over the keyless hosted endpoint.
|
|
300
|
+
|
|
301
|
+
## Submission (relayer)
|
|
302
|
+
|
|
303
|
+
All wallet writes are fee-sponsored by the [OpenZeppelin Relayer Channels](https://docs.openzeppelin.com/relayer/stellar) service: a channel account builds/pays for the transaction so the wallet holds no XLM. `PasskeyServer.send` routes to the relayer and returns a discriminated [`TransactionResult`](#errors).
|
|
304
|
+
|
|
305
|
+
Two submission modes are chosen automatically:
|
|
306
|
+
|
|
307
|
+
- **`{ func, auth }`** (`submitSorobanTransaction`) — for wallet invocations (transfers, signer management) whose auth is carried by Address credentials. The relayer builds the envelope.
|
|
308
|
+
- **`{ xdr }`** (`submitTransaction`) — for an already-signed envelope that needs a fee bump (deploys / source-account auth).
|
|
309
|
+
|
|
310
|
+
### Browsers: the relayer-proxy worker
|
|
311
|
+
|
|
312
|
+
The relayer API key is a secret, so a browser must never hold it. The [`relayer-proxy/`](./relayer-proxy) Cloudflare Worker fronts the relayer and **mints one API key per client IP** (keyless, cached in a per-IP Durable Object). The browser POSTs `{ func, auth }` / `{ xdr }` to the worker with **zero secrets in the bundle**. See [relayer-proxy/README.md](./relayer-proxy/README.md).
|
|
313
|
+
|
|
314
|
+
## Discovery (indexer)
|
|
315
|
+
|
|
316
|
+
Because every wallet address is derived deterministically from its passkey credential id (see [Deterministic derivation](#deterministic-derivation)), the primary "reconnect" path needs no indexer — `connectWallet` re-derives the address and confirms ownership on-chain. An indexer is for **richer discovery**: enumerating a wallet's full signer set, and reverse-looking-up which wallets a given signer belongs to.
|
|
317
|
+
|
|
318
|
+
The SDK abstracts discovery behind a `SignerIndexer` interface (`getSigners` / `findWallets` / `health`), implemented by the **keyless** `MercuryIndexer` — exported from the main `passkey-kit` entry (no secret, so it runs in the browser):
|
|
319
|
+
|
|
320
|
+
| Backend | Config | Wire | Status |
|
|
321
|
+
|---|---|---|---|
|
|
322
|
+
| `MercuryIndexer` | `MercuryIndexerConfig` (`url?`, `rpc?`) | Keyless REST (`GET /api/wallet/*`, `/api/lookup/*`) | **Live on testnet + mainnet** — both signer generations, full history. Resolve with `MercuryIndexer.forNetwork(...)`. |
|
|
323
|
+
|
|
324
|
+
> [!NOTE]
|
|
325
|
+
> Mercury's hosted passkey-indexer is public and **keyless**, covering **testnet and mainnet** with full history across both signer generations (legacy `("sw_v1", …)` tuples and the v1 typed `#[contractevent]`s). It returns fully-decoded signers, so the client maps JSON straight onto `WalletSigner`. Resolve the base URL per network with `MercuryIndexer.forNetwork({ rpc? }, networkPassphrase)` (returns `null` off testnet/mainnet); passing an `rpc` lets it flag evicted temporary signers and confirm reverse-lookup candidates on-chain.
|
|
187
326
|
|
|
188
327
|
```ts
|
|
189
|
-
const
|
|
328
|
+
const indexer = MercuryIndexer.forNetwork({ rpc }, networkPassphrase);
|
|
329
|
+
const wallets = await lookupWithRetry(() => indexer!.findWallets(SignerKey.Secp256r1(keyId)));
|
|
190
330
|
```
|
|
191
331
|
|
|
192
|
-
|
|
193
|
-
|
|
332
|
+
`lookupWithRetry(fn, { attempts?, delayMs?, predicate? })` (browser-safe) polls a lookup until it returns a non-empty result — useful right after a write, while the indexer catches up to the ledger.
|
|
333
|
+
|
|
334
|
+
## Tokens (`SACClient`)
|
|
335
|
+
|
|
336
|
+
Helper for [Stellar Asset Contracts](https://developers.stellar.org/docs/tokens/stellar-asset-contract) (SEP-41): balances, metadata, and transfers.
|
|
194
337
|
|
|
195
338
|
```ts
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
339
|
+
import { SACClient, buildTokenTransferHostFunction } from "passkey-kit";
|
|
340
|
+
|
|
341
|
+
const sac = new SACClient({ rpcUrl, networkPassphrase });
|
|
342
|
+
const token = sac.getSACClient("C…tokenId");
|
|
343
|
+
|
|
344
|
+
const tx = await token.transfer({ from: kit.contractId!, to, amount: 1_000_000n });
|
|
345
|
+
await kit.sign(tx);
|
|
346
|
+
await server.send(tx);
|
|
201
347
|
```
|
|
202
348
|
|
|
203
|
-
|
|
204
|
-
|
|
349
|
+
`buildTokenTransferHostFunction(token, from, to, amountInStroops)` builds a raw `transfer` host function for the low-level relayer `{ func, auth }` path when you don't need a full client.
|
|
350
|
+
|
|
351
|
+
## Storage adapters
|
|
352
|
+
|
|
353
|
+
Persist the passkey → wallet association so `connectWallet` can resolve a wallet from a keyId without an indexer. Import from `passkey-kit/storage` and pass to the kit's `storage` config.
|
|
354
|
+
|
|
355
|
+
| Adapter | Backing store | Use |
|
|
356
|
+
|---|---|---|
|
|
357
|
+
| `IndexedDBStorage` | IndexedDB | Browser (recommended). |
|
|
358
|
+
| `LocalStorageAdapter` | `localStorage` | Browser (simple/synchronous). |
|
|
359
|
+
| `MemoryStorage` | in-memory | Tests / SSR. |
|
|
205
360
|
|
|
206
361
|
```ts
|
|
207
|
-
|
|
362
|
+
import { IndexedDBStorage } from "passkey-kit/storage";
|
|
363
|
+
const kit = new PasskeyKit({ rpcUrl, networkPassphrase, walletWasmHash, storage: new IndexedDBStorage() });
|
|
208
364
|
```
|
|
209
365
|
|
|
210
|
-
|
|
366
|
+
All adapters implement `StorageAdapter` (`save` / `get` / `getByContract` / `getAll` / `delete` / `update` / `clear`) over `StoredPasskey` records.
|
|
211
367
|
|
|
212
|
-
##
|
|
368
|
+
## Errors
|
|
213
369
|
|
|
214
|
-
|
|
370
|
+
Every error the kit throws is a `PasskeyKitError` (or subclass) carrying a numeric `code`, optional `context`, and `cause`. Branch on `error.code` (or `instanceof`) — never on message strings.
|
|
215
371
|
|
|
216
|
-
|
|
217
|
-
const sac = new SACClient({
|
|
218
|
-
networkPassphrase: string,
|
|
219
|
-
rpcUrl: string
|
|
220
|
-
})
|
|
372
|
+
**One deliberate exception:** submission methods (`server.send`, `getTransaction`) **do not throw** for expected on-chain/relayer failures. They return a discriminated `TransactionResult`:
|
|
221
373
|
|
|
222
|
-
|
|
374
|
+
```ts
|
|
375
|
+
const result = await server.send(tx);
|
|
376
|
+
if (result.success) {
|
|
377
|
+
// TransactionSuccess: { success: true, hash, ledger?, transactionId? }
|
|
378
|
+
console.log(result.hash);
|
|
379
|
+
} else {
|
|
380
|
+
// TransactionFailure: { success: false, error: PasskeyKitError, hash? }
|
|
381
|
+
if (result.error instanceof ContractError && result.error.contractErrorName === "SignerExpired") {
|
|
382
|
+
// handle an on-chain contract failure by its decoded name
|
|
383
|
+
}
|
|
384
|
+
}
|
|
223
385
|
```
|
|
224
386
|
|
|
225
|
-
|
|
387
|
+
Error classes: `ConfigurationError`, `WalletNotConnectedError`, `WalletOwnershipError`, `WebAuthnError`, `SigningError`, `SignerNotFoundError`, `SimulationError`, `SubmissionError`, `ValidationError`, `IndexerError`, `RelayerError`, and `ContractError`. Codes are grouped by concern (`1xxx` config, `2xxx` wallet, `3xxx` WebAuthn, `4xxx` signing, `5xxx` transaction, `6xxx` indexer, `7xxx` relayer, `8xxx` validation, `9xxx` storage, `10000` contract).
|
|
388
|
+
|
|
389
|
+
### Contract error decoding
|
|
390
|
+
|
|
391
|
+
On-chain failures surface as `Error(Contract, #N)` in diagnostics. `decodeContractError`, `contractErrorFromCode`, and `CONTRACT_ERROR_REGISTRY` map the code to a typed `ContractError` with its enum name. The v1 contract renumbered its error space to **100–129** (disjoint from the legacy 1–9 range, which is still decoded as family `SmartWalletLegacy`):
|
|
392
|
+
|
|
393
|
+
| Code | Name | Meaning |
|
|
394
|
+
|---|---|---|
|
|
395
|
+
| 100 | `SignerNotFound` | The requested signer does not exist. |
|
|
396
|
+
| 101 | `SignerAlreadyExists` | `add_signer` on an existing key. |
|
|
397
|
+
| 102 | `SignerExpired` | Expiration timestamp is in the past. |
|
|
398
|
+
| 110 | `MissingContext` | No signer in the map may authorize a requested context. |
|
|
399
|
+
| 111 | `SignatureKeyValueMismatch` | A signature's variant doesn't match its stored signer. |
|
|
400
|
+
| 120 | `ClientDataJsonTooLarge` | `clientDataJSON` exceeds the 1024-byte parse buffer. |
|
|
401
|
+
| 121 | `ClientDataJsonParseError` | `clientDataJSON` is not parseable. |
|
|
402
|
+
| 122 | `ClientDataJsonChallengeIncorrect` | WebAuthn challenge ≠ signature payload (binding). |
|
|
403
|
+
| 123 | `InvalidWebAuthnType` | `type` is not `"webauthn.get"`. |
|
|
404
|
+
| 124 | `InvalidAuthenticatorData` | `authenticatorData` shorter than 37 bytes. |
|
|
405
|
+
| 125 | `UserPresenceRequired` | Authenticator did not set the User Present (UP) flag. |
|
|
226
406
|
|
|
227
407
|
## Types
|
|
228
408
|
|
|
229
|
-
### SignerKey
|
|
409
|
+
### `SignerKey`
|
|
410
|
+
|
|
411
|
+
Identifies a signer. The `value` is the string you work with.
|
|
230
412
|
|
|
231
413
|
```ts
|
|
232
|
-
SignerKey.
|
|
233
|
-
SignerKey.Ed25519(publicKey)
|
|
234
|
-
SignerKey.
|
|
414
|
+
SignerKey.Secp256r1(keyId) // base64url passkey credential id
|
|
415
|
+
SignerKey.Ed25519(publicKey) // G… public key
|
|
416
|
+
SignerKey.Policy(address) // C… policy contract
|
|
235
417
|
```
|
|
236
418
|
|
|
237
|
-
### SignerLimits
|
|
419
|
+
### `SignerLimits`
|
|
238
420
|
|
|
239
421
|
```ts
|
|
240
|
-
type SignerLimits = Map<string, SignerKey[] | undefined> | undefined
|
|
241
|
-
|
|
242
|
-
// Example: Limit signer to specific contract, requires co-signer
|
|
243
|
-
const limits = new Map([
|
|
244
|
-
['C...contractAddress', [SignerKey.Ed25519('G...')]]
|
|
245
|
-
])
|
|
422
|
+
type SignerLimits = Map<string, SignerKey[] | undefined> | undefined;
|
|
246
423
|
```
|
|
247
424
|
|
|
248
|
-
|
|
425
|
+
- `undefined` (whole map) — **fully unlimited**: may authorize anything, including deploys and this wallet's own admin functions.
|
|
426
|
+
- `Map` present but a contract → `undefined` — may authorize any call to that contract, no co-signers.
|
|
427
|
+
- `Map` present, contract → `[keys]` — may authorize calls to that contract **only if every listed key also approves** (required co-signers).
|
|
249
428
|
|
|
250
429
|
```ts
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
Temporary = 'Temporary' // Expires, cheaper
|
|
254
|
-
}
|
|
430
|
+
// This signer may only call C…token, and only alongside a passkey co-signer.
|
|
431
|
+
const limits = new Map([["C…token", [SignerKey.Secp256r1(keyId)]]]);
|
|
255
432
|
```
|
|
256
433
|
|
|
257
|
-
|
|
434
|
+
> [!IMPORTANT]
|
|
435
|
+
> **v1 breaking change.** `Some(empty map)` now means **no permissions (fail-closed)** — pre-1.0 an empty map meant *unlimited*. Deploy permission is no longer grantable through a limits entry: `CreateContract*` contexts require a fully unlimited (`undefined`-limits) signer.
|
|
436
|
+
|
|
437
|
+
### `SignerStore`
|
|
258
438
|
|
|
259
439
|
```ts
|
|
260
|
-
|
|
261
|
-
kind: string // 'Secp256r1' | 'Ed25519' | 'Policy'
|
|
262
|
-
key: string // Signer identifier
|
|
263
|
-
val: string // Public key or empty
|
|
264
|
-
expiration: number | null
|
|
265
|
-
storage: 'Persistent' | 'Temporary'
|
|
266
|
-
limits: string // JSON stringified limits
|
|
267
|
-
evicted?: boolean // True if temporary signer was evicted
|
|
268
|
-
}
|
|
440
|
+
enum SignerStore { Persistent = "Persistent", Temporary = "Temporary" }
|
|
269
441
|
```
|
|
270
442
|
|
|
271
|
-
|
|
443
|
+
`Temporary` entries are cheaper but **can be evicted** when their ledger TTL lapses — see [Caveats](#caveats).
|
|
272
444
|
|
|
273
|
-
|
|
445
|
+
### Expiration
|
|
274
446
|
|
|
275
|
-
|
|
447
|
+
Signer and signature expiration are **UNIX timestamps in seconds** (inclusive: valid while `now <= expiration`). Pre-1.0 these were ledger sequence numbers; timestamps don't drift as ledger close-time changes.
|
|
276
448
|
|
|
277
|
-
|
|
278
|
-
cd ./zephyr
|
|
279
|
-
cargo install mercury-cli
|
|
280
|
-
# Get a JWT from https://test.mercurydata.app
|
|
281
|
-
export MERCURY_JWT="<YOUR.MERCURY.JWT>"
|
|
282
|
-
# Requires Rust 1.79.0+
|
|
283
|
-
mercury-cli --jwt $MERCURY_JWT --local false --mainnet false deploy
|
|
284
|
-
```
|
|
449
|
+
## Caveats
|
|
285
450
|
|
|
286
|
-
|
|
451
|
+
> [!WARNING]
|
|
452
|
+
> These are inherent to the wallet model. The SDK does not guard against them — handle them in your app.
|
|
287
453
|
|
|
288
|
-
|
|
454
|
+
- **Keep at least one durable admin signer.** The contract rejects any change that would remove or demote its last durable (`Persistent`, non-expiring) admin signer (`LastAdminSigner = 103`) or leave it without any durable signer (`LastSigner = 104`), so a wallet always retains one signer that cannot evict or expire. Signers outside that guard — `Temporary` storage or with an expiration — lapse on their own: add a replacement *before* removing or demoting an existing signer.
|
|
455
|
+
- **The default deployer is a shared, public keypair.** It only pays fees and salts the deploy (it never controls the wallet), but its determinism is load-bearing for discovery. Overriding `deploySource` changes every derived address and breaks keyId → wallet lookup. See [Deterministic derivation](#deterministic-derivation).
|
|
456
|
+
- **Deploy front-running.** Because the deployer is public and the WASM is not part of the address preimage, anyone who learns a `keyId` before the wallet is deployed could deploy other code at the derived address. `connectWallet` mitigates this by verifying the keyId is a live signer (and, with `verifyWasmHash: true`, checking the on-chain WASM hash) — never trust a bare derived/looked-up address without that check.
|
|
457
|
+
- **WebAuthn requires User Presence (UP), not User Verification (UV).** The contract requires the UP flag but not UV (biometric/PIN), so it stays compatible with non-UV authenticators. Enforce UV at the client/relayer layer if you need it.
|
|
458
|
+
- **Value-moving policies need a cumulative cap or a co-signer.** A `Signature::Policy` carries no secret, so a per-transfer cap alone is trivially drained by repeated capped transfers. See the [contract interface](#contract-interface) and `sample-policy`.
|
|
289
459
|
|
|
290
|
-
|
|
460
|
+
## Contract interface
|
|
291
461
|
|
|
292
|
-
|
|
293
|
-
```mjs
|
|
294
|
-
/** @type {import('next').NextConfig} */
|
|
295
|
-
const nextConfig = {
|
|
296
|
-
transpilePackages: [
|
|
297
|
-
'passkey-kit',
|
|
298
|
-
'passkey-factory-sdk',
|
|
299
|
-
'passkey-kit-sdk',
|
|
300
|
-
'sac-sdk',
|
|
301
|
-
]
|
|
302
|
-
}
|
|
462
|
+
The wallet is a Soroban smart contract (`soroban-sdk 27`, `wasm32v1-none`). Every user wallet is a separate instance deployed with a `Signer` constructor argument.
|
|
303
463
|
|
|
304
|
-
|
|
305
|
-
```
|
|
464
|
+
**Functions:** `__constructor(signer)` · `add_signer(signer)` · `update_signer(signer)` · `remove_signer(signer_key)` · `upgrade(new_wasm_hash)` · `get_signer(signer_key) -> Option<SignerVal>`. Admin functions require wallet auth (`__check_auth`).
|
|
306
465
|
|
|
307
|
-
|
|
466
|
+
**Signer kinds:** `Policy(Address)` · `Ed25519(BytesN<32>)` · `Secp256r1(Bytes keyId)`, each with a `SignerExpiration`, `SignerLimits`, and `SignerStorage`.
|
|
308
467
|
|
|
309
|
-
|
|
468
|
+
**Auth (`__check_auth`):** a flat `Signatures` map (`SignerKey → Signature`) signed over the plain signature payload. Pass 1 checks every requested context is covered by some permitted, unexpired signer; pass 2 verifies **every** entry in the map (existence, expiration, crypto/policy). Include only the signatures you need.
|
|
310
469
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
470
|
+
**Policy lifecycle:** policy signers get an `install(wallet)` hook on add (a hard call — a panic aborts the add) and a permissionless `uninstall(wallet)` self-clean entrypoint. `policy__` is publicly callable — stateful policies must authenticate the caller (`source.require_auth()`).
|
|
471
|
+
|
|
472
|
+
**Events** (`#[contractevent]`, SEP-48 schema in the WASM): `signer_added` · `signer_updated` · `signer_removed` · `upgraded`. These replace the legacy `("sw_v1", …)` tuple events; indexers consume them directly.
|
|
473
|
+
|
|
474
|
+
See [`contracts/smart-wallet-interface/src/`](./contracts/smart-wallet-interface/src) for the canonical trait and types.
|
|
475
|
+
|
|
476
|
+
### Deterministic derivation
|
|
314
477
|
|
|
315
|
-
|
|
316
|
-
pnpm run build
|
|
478
|
+
Every wallet address is derived from its passkey credential id (`keyId`) alone — this is what lets `connectWallet` and the indexers resolve a wallet without a lookup table:
|
|
317
479
|
|
|
318
|
-
|
|
319
|
-
|
|
480
|
+
```text
|
|
481
|
+
contractId = sha256(XDR(HashIdPreimage::EnvelopeTypeContractId {
|
|
482
|
+
networkId: sha256(networkPassphrase),
|
|
483
|
+
contractIdPreimage: ContractIdPreimageFromAddress {
|
|
484
|
+
address: G-address of the canonical deployer keypair,
|
|
485
|
+
salt: sha256(keyId),
|
|
486
|
+
},
|
|
487
|
+
}))
|
|
320
488
|
```
|
|
321
489
|
|
|
322
|
-
**
|
|
323
|
-
-
|
|
324
|
-
- `./demo` - Demo application
|
|
325
|
-
- `./contracts` - Rust Soroban smart contracts
|
|
326
|
-
- `./zephyr` - Mercury Zephyr indexer program
|
|
490
|
+
- The canonical deployer keypair is `Keypair.fromRawEd25519Seed(sha256("kalepail"))`. It only pays fees and salts the deploy — it never controls the wallet — but its determinism is **load-bearing**: overriding `deploySource` changes every derived address and breaks keyId → wallet discovery.
|
|
491
|
+
- The WASM hash is deliberately **not** in the preimage, so an `upgrade` never moves a wallet's address.
|
|
327
492
|
|
|
328
|
-
|
|
329
|
-
> If modifying contracts in `./contracts`, run the make commands. Update `SMART_WALLET_FACTORY` and `SMART_WALLET_WASM` values from `make deploy` before running `make init`.
|
|
493
|
+
This tuple is normative and must never change. See [`docs/deployments-testnet-2026-07-11.md`](./docs/deployments-testnet-2026-07-11.md) for the canonical WASM hashes, the deployer `G…` address, and the full derivation spec (including the deploy-front-running consequence in [Caveats](#caveats)).
|
|
330
494
|
|
|
331
|
-
|
|
332
|
-
|
|
495
|
+
## Repository layout & development
|
|
496
|
+
|
|
497
|
+
| Path | Contents |
|
|
498
|
+
|---|---|
|
|
499
|
+
| `src/` | The `passkey-kit` SDK (client, server, signers, indexer, storage). |
|
|
500
|
+
| `packages/passkey-kit-sdk` | Generated smart-wallet contract bindings (do not hand-edit — see [releasing](./docs/releasing.md)). |
|
|
501
|
+
| `packages/sac-sdk` | Generated SEP-41 SAC bindings. |
|
|
502
|
+
| `contracts/` | Rust Soroban contracts: `smart-wallet`, `smart-wallet-interface`, `sample-policy`, `example-contract`. |
|
|
503
|
+
| `relayer-proxy/` | Cloudflare Worker for keyless, fee-sponsored submission. |
|
|
504
|
+
| `demo/` | Svelte 5 demo exercising the full client API. |
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
pnpm install
|
|
508
|
+
pnpm build # regenerate bindings, compile to dist/, verify Node-ESM import
|
|
509
|
+
pnpm test # vitest (co-located src/*.test.ts)
|
|
510
|
+
pnpm verify:bindings # assert the committed bindings match the canonical WASM
|
|
511
|
+
```
|
|
333
512
|
|
|
334
|
-
|
|
513
|
+
- **[CHANGELOG.md](./CHANGELOG.md)** — the v1 overhaul, by component.
|
|
514
|
+
- **[docs/migration-v1.md](./docs/migration-v1.md)** — upgrading from 0.12.x, with Before/After and a gap analysis.
|
|
515
|
+
- **[docs/releasing.md](./docs/releasing.md)** — the dependency-ordered publish flow.
|
|
335
516
|
|
|
336
517
|
## Resources
|
|
337
518
|
|
|
338
|
-
- [Super Peach](https://github.com/kalepail/superpeach)
|
|
339
|
-
- [Discord
|
|
519
|
+
- [Super Peach](https://github.com/kalepail/superpeach) — a real-world implementation example.
|
|
520
|
+
- [Stellar Developers Discord `#passkeys`](https://discord.gg/stellardev) — questions and showcase.
|