passkey-kit 0.12.0 → 0.13.1
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 +399 -227
- package/{types → dist}/base.d.ts +2 -1
- 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 +87 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +111 -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 +129 -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 +88 -0
- package/dist/indexer/mercury.d.ts.map +1 -0
- package/dist/indexer/mercury.js +237 -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 +89 -0
- package/dist/kit/auth-payload.d.ts.map +1 -0
- package/dist/kit/auth-payload.js +203 -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 +92 -0
- package/dist/kit/tx-ops.d.ts.map +1 -0
- package/dist/kit/tx-ops.js +113 -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 +128 -0
- package/dist/kit.d.ts.map +1 -0
- package/dist/kit.js +276 -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 +51 -0
- package/dist/managers/signer-manager.d.ts.map +1 -0
- package/dist/managers/signer-manager.js +92 -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 +119 -0
- package/dist/relayer.js.map +1 -0
- package/dist/rpc-data.d.ts +35 -0
- package/dist/rpc-data.d.ts.map +1 -0
- package/dist/rpc-data.js +49 -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 +52 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +153 -0
- package/dist/utils.js.map +1 -0
- package/dist/validation.d.ts +40 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +73 -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 +48 -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 -18
- 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 -18
- 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 -698
- 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 -89
- 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,511 @@
|
|
|
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.** The v1 smart-wallet contract underwent an internal multi-reviewer adversarial review and remediation (see the [CHANGELOG](./CHANGELOG.md)), but it has **not** been reviewed by a third-party security firm. Review it yourself before holding meaningful value, and read [Caveats & footguns](#caveats--footguns).
|
|
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 & footguns](#caveats--footguns)
|
|
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: "84924c53a413318df2ce753e30de53ec651404c916d30e861718ad155c94b319",
|
|
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
|
-
- `keyId` - Passkey ID (string or Uint8Array)
|
|
157
|
-
- `publicKey` - Public key (string or Uint8Array for Secp256r1, Stellar public key for Ed25519)
|
|
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
|
|
162
226
|
|
|
163
|
-
|
|
227
|
+
Each method builds an `AssembledTransaction` (`WalletTx`) that wraps one contract admin function — submit it via `PasskeyServer.send`. A wallet must be connected.
|
|
164
228
|
|
|
165
|
-
|
|
229
|
+
| Method | Wrapped contract fn | Description |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `addSecp256r1(keyId, publicKey, limits, store, expiration?)` | `add_signer` | Add a passkey signer. |
|
|
232
|
+
| `updateSecp256r1(keyId, publicKey, limits, store, expiration?)` | `update_signer` | Replace a passkey signer's value/storage. |
|
|
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`. |
|
|
166
240
|
|
|
167
|
-
|
|
241
|
+
Parameters:
|
|
168
242
|
|
|
169
|
-
|
|
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.
|
|
170
249
|
|
|
171
250
|
```ts
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
251
|
+
import { SignerStore, SignerKey } from "passkey-kit";
|
|
252
|
+
|
|
253
|
+
// Add an unlimited Ed25519 co-signer, stored persistently.
|
|
254
|
+
const tx = await kit.addEd25519("G…", undefined, SignerStore.Persistent);
|
|
255
|
+
await kit.sign(tx); // authorize with the connected passkey
|
|
256
|
+
await server.send(tx);
|
|
257
|
+
|
|
258
|
+
// Remove it.
|
|
259
|
+
const rm = await kit.remove(SignerKey.Ed25519("G…"));
|
|
260
|
+
await kit.sign(rm);
|
|
261
|
+
await server.send(rm);
|
|
181
262
|
```
|
|
182
263
|
|
|
264
|
+
## `PasskeyServer` (server)
|
|
265
|
+
|
|
266
|
+
Server-only facade (`passkey-kit/server`). Holds the relayer secret — never import it from browser code.
|
|
267
|
+
|
|
268
|
+
### Configuration
|
|
269
|
+
|
|
270
|
+
| Option | Type | Required | Description |
|
|
271
|
+
|---|---|---|---|
|
|
272
|
+
| `networkPassphrase` | `string` | Yes | Network passphrase. |
|
|
273
|
+
| `rpcUrl` | `string` | No | Stellar RPC URL. Enables the temporary-signer eviction probe in `getSigners`. |
|
|
274
|
+
| `relayer` | `RelayerClientConfig` | No | Fee-sponsored submission (below). |
|
|
275
|
+
| `mercury` | `MercuryConfig` | No | Mercury's keyless hosted passkey-indexer (below). |
|
|
276
|
+
|
|
277
|
+
`RelayerClientConfig`: `{ baseUrl: string, apiKey: string, adminSecret?: string, timeout?: number }` (default timeout 6 min).
|
|
278
|
+
`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.
|
|
279
|
+
|
|
183
280
|
### Methods
|
|
184
281
|
|
|
185
|
-
|
|
186
|
-
|
|
282
|
+
| Method | Returns | Description |
|
|
283
|
+
|---|---|---|
|
|
284
|
+
| `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.** |
|
|
285
|
+
| `getTransaction(transactionId)` | `TransactionResult` | Poll a `skipWait` submission by its relayer id. |
|
|
286
|
+
| `getSigners(contractId)` | `WalletSigner[]` | Enumerate a wallet's signers via the indexer (flags evicted temporary signers when `rpcUrl` is set). |
|
|
287
|
+
| `getContractId(options, index?)` | `string \| undefined` | Reverse lookup: the wallet address for a signer. `options` = exactly one of `{ keyId }`, `{ publicKey }`, `{ policy }`. |
|
|
288
|
+
|
|
289
|
+
`options` for `send`/`getTransaction` is `{ skipWait?: boolean, fundRelayerId?: string }`. `getSigners`/`getContractId` delegate to a [`MercuryIndexer`](#discovery-indexer) over the keyless hosted endpoint.
|
|
290
|
+
|
|
291
|
+
## Submission (relayer)
|
|
292
|
+
|
|
293
|
+
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).
|
|
294
|
+
|
|
295
|
+
Two submission modes are chosen automatically:
|
|
296
|
+
|
|
297
|
+
- **`{ func, auth }`** (`submitSorobanTransaction`) — for wallet invocations (transfers, signer management) whose auth is carried by Address credentials. The relayer builds the envelope.
|
|
298
|
+
- **`{ xdr }`** (`submitTransaction`) — for an already-signed envelope that needs a fee bump (deploys / source-account auth).
|
|
299
|
+
|
|
300
|
+
### Browsers: the relayer-proxy worker
|
|
301
|
+
|
|
302
|
+
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).
|
|
303
|
+
|
|
304
|
+
## Discovery (indexer)
|
|
305
|
+
|
|
306
|
+
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.
|
|
307
|
+
|
|
308
|
+
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):
|
|
309
|
+
|
|
310
|
+
| Backend | Config | Wire | Status |
|
|
311
|
+
|---|---|---|---|
|
|
312
|
+
| `MercuryIndexer` | `MercuryIndexerConfig` (`url?`, `rpc?`) | Keyless REST (`GET /api/wallet/*`, `/api/lookup/*`) | **Live on testnet + mainnet** — both signer generations, full history. Resolve with `MercuryIndexer.forNetwork(...)`. |
|
|
313
|
+
|
|
314
|
+
> [!NOTE]
|
|
315
|
+
> 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
316
|
|
|
188
317
|
```ts
|
|
189
|
-
const
|
|
318
|
+
const indexer = MercuryIndexer.forNetwork({ rpc }, networkPassphrase);
|
|
319
|
+
const wallets = await lookupWithRetry(() => indexer!.findWallets(SignerKey.Secp256r1(keyId)));
|
|
190
320
|
```
|
|
191
321
|
|
|
192
|
-
|
|
193
|
-
|
|
322
|
+
`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.
|
|
323
|
+
|
|
324
|
+
## Tokens (`SACClient`)
|
|
325
|
+
|
|
326
|
+
Helper for [Stellar Asset Contracts](https://developers.stellar.org/docs/tokens/stellar-asset-contract) (SEP-41): balances, metadata, and transfers.
|
|
194
327
|
|
|
195
328
|
```ts
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
329
|
+
import { SACClient, buildTokenTransferHostFunction } from "passkey-kit";
|
|
330
|
+
|
|
331
|
+
const sac = new SACClient({ rpcUrl, networkPassphrase });
|
|
332
|
+
const token = sac.getSACClient("C…tokenId");
|
|
333
|
+
|
|
334
|
+
const tx = await token.transfer({ from: kit.contractId!, to, amount: 1_000_000n });
|
|
335
|
+
await kit.sign(tx);
|
|
336
|
+
await server.send(tx);
|
|
201
337
|
```
|
|
202
338
|
|
|
203
|
-
|
|
204
|
-
|
|
339
|
+
`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.
|
|
340
|
+
|
|
341
|
+
## Storage adapters
|
|
342
|
+
|
|
343
|
+
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.
|
|
344
|
+
|
|
345
|
+
| Adapter | Backing store | Use |
|
|
346
|
+
|---|---|---|
|
|
347
|
+
| `IndexedDBStorage` | IndexedDB | Browser (recommended). |
|
|
348
|
+
| `LocalStorageAdapter` | `localStorage` | Browser (simple/synchronous). |
|
|
349
|
+
| `MemoryStorage` | in-memory | Tests / SSR. |
|
|
205
350
|
|
|
206
351
|
```ts
|
|
207
|
-
|
|
352
|
+
import { IndexedDBStorage } from "passkey-kit/storage";
|
|
353
|
+
const kit = new PasskeyKit({ rpcUrl, networkPassphrase, walletWasmHash, storage: new IndexedDBStorage() });
|
|
208
354
|
```
|
|
209
355
|
|
|
210
|
-
|
|
356
|
+
All adapters implement `StorageAdapter` (`save` / `get` / `getByContract` / `getAll` / `delete` / `update` / `clear`) over `StoredPasskey` records.
|
|
211
357
|
|
|
212
|
-
##
|
|
358
|
+
## Errors
|
|
213
359
|
|
|
214
|
-
|
|
360
|
+
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
361
|
|
|
216
|
-
|
|
217
|
-
const sac = new SACClient({
|
|
218
|
-
networkPassphrase: string,
|
|
219
|
-
rpcUrl: string
|
|
220
|
-
})
|
|
362
|
+
**One deliberate exception:** submission methods (`server.send`, `getTransaction`) **do not throw** for expected on-chain/relayer failures. They return a discriminated `TransactionResult`:
|
|
221
363
|
|
|
222
|
-
|
|
364
|
+
```ts
|
|
365
|
+
const result = await server.send(tx);
|
|
366
|
+
if (result.success) {
|
|
367
|
+
// TransactionSuccess: { success: true, hash, ledger?, transactionId? }
|
|
368
|
+
console.log(result.hash);
|
|
369
|
+
} else {
|
|
370
|
+
// TransactionFailure: { success: false, error: PasskeyKitError, hash? }
|
|
371
|
+
if (result.error instanceof ContractError && result.error.contractErrorName === "SignerExpired") {
|
|
372
|
+
// handle an on-chain contract failure by its decoded name
|
|
373
|
+
}
|
|
374
|
+
}
|
|
223
375
|
```
|
|
224
376
|
|
|
225
|
-
|
|
377
|
+
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).
|
|
378
|
+
|
|
379
|
+
### Contract error decoding
|
|
380
|
+
|
|
381
|
+
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`):
|
|
382
|
+
|
|
383
|
+
| Code | Name | Meaning |
|
|
384
|
+
|---|---|---|
|
|
385
|
+
| 100 | `SignerNotFound` | The requested signer does not exist. |
|
|
386
|
+
| 101 | `SignerAlreadyExists` | `add_signer` on an existing key. |
|
|
387
|
+
| 102 | `SignerExpired` | Expiration timestamp is in the past. |
|
|
388
|
+
| 110 | `MissingContext` | No signer in the map may authorize a requested context. |
|
|
389
|
+
| 111 | `SignatureKeyValueMismatch` | A signature's variant doesn't match its stored signer. |
|
|
390
|
+
| 120 | `ClientDataJsonTooLarge` | `clientDataJSON` exceeds the 1024-byte parse buffer. |
|
|
391
|
+
| 121 | `ClientDataJsonParseError` | `clientDataJSON` is not parseable. |
|
|
392
|
+
| 122 | `ClientDataJsonChallengeIncorrect` | WebAuthn challenge ≠ signature payload (binding). |
|
|
393
|
+
| 123 | `InvalidWebAuthnType` | `type` is not `"webauthn.get"`. |
|
|
394
|
+
| 124 | `InvalidAuthenticatorData` | `authenticatorData` shorter than 37 bytes. |
|
|
395
|
+
| 125 | `UserPresenceRequired` | Authenticator did not set the User Present (UP) flag. |
|
|
226
396
|
|
|
227
397
|
## Types
|
|
228
398
|
|
|
229
|
-
### SignerKey
|
|
399
|
+
### `SignerKey`
|
|
400
|
+
|
|
401
|
+
Identifies a signer. The `value` is the string you work with.
|
|
230
402
|
|
|
231
403
|
```ts
|
|
232
|
-
SignerKey.
|
|
233
|
-
SignerKey.Ed25519(publicKey)
|
|
234
|
-
SignerKey.
|
|
404
|
+
SignerKey.Secp256r1(keyId) // base64url passkey credential id
|
|
405
|
+
SignerKey.Ed25519(publicKey) // G… public key
|
|
406
|
+
SignerKey.Policy(address) // C… policy contract
|
|
235
407
|
```
|
|
236
408
|
|
|
237
|
-
### SignerLimits
|
|
409
|
+
### `SignerLimits`
|
|
238
410
|
|
|
239
411
|
```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
|
-
])
|
|
412
|
+
type SignerLimits = Map<string, SignerKey[] | undefined> | undefined;
|
|
246
413
|
```
|
|
247
414
|
|
|
248
|
-
|
|
415
|
+
- `undefined` (whole map) — **fully unlimited**: may authorize anything, including deploys and this wallet's own admin functions.
|
|
416
|
+
- `Map` present but a contract → `undefined` — may authorize any call to that contract, no co-signers.
|
|
417
|
+
- `Map` present, contract → `[keys]` — may authorize calls to that contract **only if every listed key also approves** (required co-signers).
|
|
249
418
|
|
|
250
419
|
```ts
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
Temporary = 'Temporary' // Expires, cheaper
|
|
254
|
-
}
|
|
420
|
+
// This signer may only call C…token, and only alongside a passkey co-signer.
|
|
421
|
+
const limits = new Map([["C…token", [SignerKey.Secp256r1(keyId)]]]);
|
|
255
422
|
```
|
|
256
423
|
|
|
257
|
-
|
|
424
|
+
> [!IMPORTANT]
|
|
425
|
+
> **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.
|
|
426
|
+
|
|
427
|
+
### `SignerStore`
|
|
258
428
|
|
|
259
429
|
```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
|
-
}
|
|
430
|
+
enum SignerStore { Persistent = "Persistent", Temporary = "Temporary" }
|
|
269
431
|
```
|
|
270
432
|
|
|
271
|
-
|
|
433
|
+
`Temporary` entries are cheaper but **can be evicted** when their ledger TTL lapses — see [Caveats](#caveats--footguns).
|
|
272
434
|
|
|
273
|
-
|
|
435
|
+
### Expiration
|
|
274
436
|
|
|
275
|
-
|
|
437
|
+
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
438
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
```
|
|
439
|
+
## Caveats & footguns
|
|
440
|
+
|
|
441
|
+
> [!WARNING]
|
|
442
|
+
> These are inherent to the wallet model. The SDK does not guard against them — handle them in your app.
|
|
285
443
|
|
|
286
|
-
|
|
444
|
+
- **Don't remove your last usable signer → the wallet bricks.** The contract does not enforce a minimum signer count. If you remove (or let expire) every signer that can authorize admin functions, the wallet becomes permanently uncontrollable. Always keep at least one live, unlimited signer, and add the replacement *before* removing the old one.
|
|
445
|
+
- **A sole `Temporary` signer can be evicted → the wallet bricks.** Temporary entries are reclaimed when their TTL lapses. Never let a wallet's only admin signer live in `Temporary` storage; keep at least one `Persistent` signer.
|
|
446
|
+
- **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).
|
|
447
|
+
- **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.
|
|
448
|
+
- **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.
|
|
449
|
+
- **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`.
|
|
287
450
|
|
|
288
|
-
##
|
|
451
|
+
## Contract interface
|
|
289
452
|
|
|
290
|
-
|
|
453
|
+
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.
|
|
291
454
|
|
|
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
|
-
}
|
|
455
|
+
**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`).
|
|
303
456
|
|
|
304
|
-
|
|
305
|
-
```
|
|
457
|
+
**Signer kinds:** `Policy(Address)` · `Ed25519(BytesN<32>)` · `Secp256r1(Bytes keyId)`, each with a `SignerExpiration`, `SignerLimits`, and `SignerStorage`.
|
|
306
458
|
|
|
307
|
-
|
|
459
|
+
**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.
|
|
308
460
|
|
|
309
|
-
|
|
461
|
+
**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()`).
|
|
310
462
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
463
|
+
**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.
|
|
464
|
+
|
|
465
|
+
See [`contracts/smart-wallet-interface/src/`](./contracts/smart-wallet-interface/src) for the canonical trait and types.
|
|
466
|
+
|
|
467
|
+
### Deterministic derivation
|
|
314
468
|
|
|
315
|
-
|
|
316
|
-
pnpm run build
|
|
469
|
+
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
470
|
|
|
318
|
-
|
|
319
|
-
|
|
471
|
+
```text
|
|
472
|
+
contractId = sha256(XDR(HashIdPreimage::EnvelopeTypeContractId {
|
|
473
|
+
networkId: sha256(networkPassphrase),
|
|
474
|
+
contractIdPreimage: ContractIdPreimageFromAddress {
|
|
475
|
+
address: G-address of the canonical deployer keypair,
|
|
476
|
+
salt: sha256(keyId),
|
|
477
|
+
},
|
|
478
|
+
}))
|
|
320
479
|
```
|
|
321
480
|
|
|
322
|
-
**
|
|
323
|
-
-
|
|
324
|
-
- `./demo` - Demo application
|
|
325
|
-
- `./contracts` - Rust Soroban smart contracts
|
|
326
|
-
- `./zephyr` - Mercury Zephyr indexer program
|
|
481
|
+
- 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.
|
|
482
|
+
- The WASM hash is deliberately **not** in the preimage, so an `upgrade` never moves a wallet's address.
|
|
327
483
|
|
|
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`.
|
|
484
|
+
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--footguns)).
|
|
330
485
|
|
|
331
|
-
|
|
332
|
-
|
|
486
|
+
## Repository layout & development
|
|
487
|
+
|
|
488
|
+
| Path | Contents |
|
|
489
|
+
|---|---|
|
|
490
|
+
| `src/` | The `passkey-kit` SDK (client, server, signers, indexer, storage). |
|
|
491
|
+
| `packages/passkey-kit-sdk` | Generated smart-wallet contract bindings (do not hand-edit — see [releasing](./docs/releasing.md)). |
|
|
492
|
+
| `packages/sac-sdk` | Generated SEP-41 SAC bindings. |
|
|
493
|
+
| `contracts/` | Rust Soroban contracts: `smart-wallet`, `smart-wallet-interface`, `sample-policy`, `example-contract`. |
|
|
494
|
+
| `relayer-proxy/` | Cloudflare Worker for keyless, fee-sponsored submission. |
|
|
495
|
+
| `demo/` | Svelte 5 demo exercising the full client API. |
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
pnpm install
|
|
499
|
+
pnpm build # regenerate bindings, compile to dist/, verify Node-ESM import
|
|
500
|
+
pnpm test # vitest (co-located src/*.test.ts)
|
|
501
|
+
pnpm verify:bindings # assert the committed bindings match the canonical WASM
|
|
502
|
+
```
|
|
333
503
|
|
|
334
|
-
|
|
504
|
+
- **[CHANGELOG.md](./CHANGELOG.md)** — the v1 overhaul, by component.
|
|
505
|
+
- **[docs/migration-v1.md](./docs/migration-v1.md)** — upgrading from 0.12.x, with Before/After and a gap analysis.
|
|
506
|
+
- **[docs/releasing.md](./docs/releasing.md)** — the dependency-ordered publish flow.
|
|
335
507
|
|
|
336
508
|
## Resources
|
|
337
509
|
|
|
338
|
-
- [Super Peach](https://github.com/kalepail/superpeach)
|
|
339
|
-
- [Discord
|
|
510
|
+
- [Super Peach](https://github.com/kalepail/superpeach) — a real-world implementation example.
|
|
511
|
+
- [Stellar Developers Discord `#passkeys`](https://discord.gg/stellardev) — questions and showcase.
|