@zerodev/solana-sponsorship-sdk 0.0.1 → 0.1.0-beta.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.
Files changed (40) hide show
  1. package/README.md +260 -0
  2. package/dist/index.d.ts +3 -1
  3. package/dist/index.js +3 -1
  4. package/dist/passkey/challenge.d.ts +6 -0
  5. package/dist/passkey/challenge.js +40 -0
  6. package/dist/passkey/compile.d.ts +3 -0
  7. package/dist/passkey/compile.js +12 -0
  8. package/dist/passkey/confirmation.d.ts +4 -0
  9. package/dist/passkey/confirmation.js +48 -0
  10. package/dist/passkey/constants.d.ts +1 -0
  11. package/dist/passkey/constants.js +5 -0
  12. package/dist/passkey/cose.d.ts +12 -0
  13. package/dist/passkey/cose.js +108 -0
  14. package/dist/passkey/errors.d.ts +11 -0
  15. package/dist/passkey/errors.js +15 -0
  16. package/dist/passkey/extract.d.ts +23 -0
  17. package/dist/passkey/extract.js +21 -0
  18. package/dist/passkey/index.d.ts +12 -0
  19. package/dist/passkey/index.js +11 -0
  20. package/dist/passkey/role.d.ts +4 -0
  21. package/dist/passkey/role.js +10 -0
  22. package/dist/passkey/session.d.ts +8 -0
  23. package/dist/passkey/session.js +115 -0
  24. package/dist/passkey/sign.d.ts +3 -0
  25. package/dist/passkey/sign.js +33 -0
  26. package/dist/passkey/signer.d.ts +7 -0
  27. package/dist/passkey/signer.js +106 -0
  28. package/dist/passkey/signing-fn.d.ts +2 -0
  29. package/dist/passkey/signing-fn.js +17 -0
  30. package/dist/passkey/spki.d.ts +12 -0
  31. package/dist/passkey/spki.js +106 -0
  32. package/dist/passkey/testing.d.ts +31 -0
  33. package/dist/passkey/testing.js +77 -0
  34. package/dist/passkey/types.d.ts +109 -0
  35. package/dist/passkey/types.js +1 -0
  36. package/dist/passkey/wallet.d.ts +11 -0
  37. package/dist/passkey/wallet.js +82 -0
  38. package/dist/testing.d.ts +3 -0
  39. package/dist/testing.js +1 -0
  40. package/package.json +17 -5
package/README.md CHANGED
@@ -83,6 +83,238 @@ async function main() {
83
83
  }
84
84
  ```
85
85
 
86
+ ## Usage with Privy
87
+
88
+ With Privy, the wallet is managed for you. Instead of `partiallySignTransactionMessageWithSigners`, compile the transaction to bytes and use Privy's `signTransaction`:
89
+
90
+ ```typescript
91
+ import { compileTransaction, getTransactionEncoder, getTransactionDecoder } from "@solana/kit";
92
+ import { useSignTransaction } from "@privy-io/react-auth/solana";
93
+
94
+ // Inside your React component:
95
+ const { signTransaction } = useSignTransaction();
96
+
97
+ // Build the transaction message with pipe() as in Quick Start, then:
98
+ const unsignedTx = compileTransaction(message);
99
+ const encoded = new Uint8Array(getTransactionEncoder().encode(unsignedTx));
100
+
101
+ // Sign with Privy, then decode and sponsor
102
+ const { signedTransaction } = await signTransaction({ transaction: encoded, wallet });
103
+ const decoded = getTransactionDecoder().decode(signedTransaction);
104
+ const result = await sponsorTransaction(sponsorshipRpc, decoded);
105
+ ```
106
+
107
+ See [`examples/sponsorship-with-privy.ts`](./examples/sponsorship-with-privy.ts) for a complete example.
108
+
109
+ ## Usage with Dynamic
110
+
111
+ Dynamic's signer uses `@solana/web3.js` `VersionedTransaction` internally (already a transitive dependency of `@dynamic-labs/solana` — no extra install needed). Compile the transaction to bytes, deserialize for Dynamic's signer via dynamic import, then decode back for sponsorship:
112
+
113
+ ```typescript
114
+ import { compileTransaction, getTransactionEncoder, getTransactionDecoder } from "@solana/kit";
115
+ import { isSolanaWallet } from "@dynamic-labs/solana";
116
+ import { useDynamicContext } from "@dynamic-labs/sdk-react-core";
117
+
118
+ const { primaryWallet } = useDynamicContext();
119
+ if (!primaryWallet || !isSolanaWallet(primaryWallet)) throw new Error("No Solana wallet");
120
+
121
+ // Build the transaction message with pipe() as in Quick Start, then:
122
+ const unsignedTx = compileTransaction(message);
123
+ const encoded = new Uint8Array(getTransactionEncoder().encode(unsignedTx));
124
+
125
+ // Sign with Dynamic, then decode and sponsor.
126
+ // The `as any` casts are needed because Dynamic's signer types may differ
127
+ // from the @solana/web3.js VersionedTransaction import.
128
+ const { VersionedTransaction } = await import("@solana/web3.js");
129
+ const signer = await primaryWallet.getSigner();
130
+ const versionedTx = VersionedTransaction.deserialize(encoded);
131
+ const signedTx = await signer.signTransaction(versionedTx as any);
132
+ const decoded = getTransactionDecoder().decode((signedTx as any).serialize());
133
+ const result = await sponsorTransaction(sponsorshipRpc, decoded);
134
+ ```
135
+
136
+ See [`examples/sponsorship-with-dynamic.ts`](./examples/sponsorship-with-dynamic.ts) for a complete example.
137
+
138
+ ## Passkeys & Session Keys
139
+
140
+ The SDK ships a complete browser-passkey flow on top of a sponsored [swig wallet](https://swig.zerodev.app/). You get:
141
+
142
+ - **Passkey-controlled wallets** — a swig wallet whose root authority is a Secp256r1 (P-256) WebAuthn credential. The user holds the only private key; you never see it.
143
+ - **Session keys** — a short-lived plain Ed25519 keypair that the passkey delegates to, so most transactions go through without a WebAuthn prompt. Useful for high-frequency UX (gameplay, in-app trades, batched ops).
144
+ - **Sponsored every step** — wallet creation, session creation, session-signed sends, and direct passkey-signed sends are all paid by the sponsor server.
145
+
146
+ All snippets below run in a browser. For Node / CI use, see [Testing without a browser](#testing-without-a-browser).
147
+
148
+ ### Concepts
149
+
150
+ | Term | What it is |
151
+ | --- | --- |
152
+ | **`PasskeySigner`** | Wraps a registered WebAuthn credential (`credentialId`, 33-byte compressed P-256 pubkey, `rpId`) and exposes `sign(challenge)`. Built with `createBrowserPasskeySigner`. |
153
+ | **Passkey wallet** | A swig PDA whose root authority is the passkey. Created via `createPasskeyWallet` (session-backed) or `createDirectPasskeyWallet` (plain). |
154
+ | **Session key** | Plain `KeyPairSigner` minted by the SDK and registered on the wallet as a delegate authority that expires at a fixed slot. Created via `createSessionKey`. |
155
+ | **`MAX_SESSION_SLOTS`** | Swig's hard cap on session duration (`216_000` ≈ 24h at 400 ms/slot). `createSessionKey` rejects anything larger. |
156
+
157
+ ### Step 1 — Register a passkey
158
+
159
+ The SDK doesn't dictate which passkey server you use; it just needs the `credentialId`, the 33-byte compressed P-256 pubkey, and the `rpId` you registered against. To pull the compressed pubkey out of a WebAuthn registration response:
160
+
161
+ ```typescript
162
+ import { extractCompressedP256 } from "@zerodev/solana-sponsorship-sdk";
163
+
164
+ const cred = (await navigator.credentials.create({ publicKey: createOptions })) as PublicKeyCredential;
165
+ const publicKeyBytes = extractCompressedP256(cred); // 33-byte compressed P-256
166
+
167
+ const credential = {
168
+ credentialId: new Uint8Array(cred.rawId),
169
+ publicKeyBytes,
170
+ rpId: createOptions.rp.id ?? "localhost",
171
+ };
172
+ ```
173
+
174
+ `extractCompressedP256` enforces ES256 (`alg = -7`) — the only curve the on-chain swig verifier accepts.
175
+
176
+ ### Step 2 — Build a `PasskeySigner`
177
+
178
+ ```typescript
179
+ import { passkey } from "@zerodev/solana-sponsorship-sdk";
180
+
181
+ const signer = passkey.createBrowserPasskeySigner({
182
+ credentialId: credential.credentialId,
183
+ publicKeyBytes: credential.publicKeyBytes,
184
+ rpId: credential.rpId,
185
+ });
186
+ ```
187
+
188
+ The signer holds no state — cache it per credential rather than rebuilding per-tx.
189
+
190
+ ### Step 3 — Create a wallet + session in one shot
191
+
192
+ The most common shape: one sponsored swig create, one sponsored session-create, one signer the rest of the session can use without WebAuthn prompts.
193
+
194
+ ```typescript
195
+ import { createSolanaRpc } from "@solana/kit";
196
+ import { createSponsorshipRpc, passkey } from "@zerodev/solana-sponsorship-sdk";
197
+
198
+ const sponsorshipRpc = createSponsorshipRpc({ endpoint: SPONSOR_RPC_URL });
199
+ const solanaRpc = createSolanaRpc(SOLANA_RPC_URL);
200
+
201
+ const result = await passkey.createPasskeyWalletWithSession({
202
+ sponsorshipRpc,
203
+ solanaRpc,
204
+ credentialId: credential.credentialId,
205
+ publicKeyBytes: credential.publicKeyBytes,
206
+ passkeySigner: signer,
207
+ sessionDurationSlots: 5_000n, // ~33 min on mainnet (400 ms/slot)
208
+ });
209
+
210
+ if ("sessionError" in result) {
211
+ // Wallet was created successfully but session bootstrap failed.
212
+ // `result.wallet` is usable; retry session creation later via createSessionKey.
213
+ throw result.sessionError;
214
+ }
215
+
216
+ const { wallet, walletSignature, session, sessionSignature } = result;
217
+ ```
218
+
219
+ Or do the two steps separately if you want more control over confirmation between them:
220
+
221
+ ```typescript
222
+ const { wallet, signature: createSig } = await passkey.createPasskeyWallet({
223
+ sponsorshipRpc,
224
+ solanaRpc,
225
+ credentialId: credential.credentialId,
226
+ publicKeyBytes: credential.publicKeyBytes,
227
+ });
228
+ // await your own confirmation here...
229
+
230
+ const { session, signature: sessionSig } = await passkey.createSessionKey({
231
+ wallet: wallet.address,
232
+ passkeySigner: signer,
233
+ durationSlots: 5_000n,
234
+ sponsorshipRpc,
235
+ solanaRpc,
236
+ });
237
+ ```
238
+
239
+ ### Step 4 — Send session-signed transactions
240
+
241
+ The session signer pays no SOL and prompts no WebAuthn UI. The sponsor server still pays fees and broadcasts.
242
+
243
+ ```typescript
244
+ import { sponsorTransaction, passkey } from "@zerodev/solana-sponsorship-sdk";
245
+
246
+ const tx = await passkey.buildSessionSignedTransaction({
247
+ wallet: wallet.address,
248
+ session, // from createSessionKey
249
+ innerInstructions: [transferIx], // your app's instructions
250
+ sponsorshipRpc,
251
+ solanaRpc,
252
+ });
253
+
254
+ const { signature } = await sponsorTransaction(sponsorshipRpc, tx);
255
+ ```
256
+
257
+ `buildSessionSignedTransaction` throws `SessionExpiredError` if the current slot is past `session.expiresAtSlot` — catch it and prompt the user to re-authorise via `createSessionKey`.
258
+
259
+ ### Direct passkey signing (no session)
260
+
261
+ For high-value or admin operations you may want every tx to go through a WebAuthn prompt. That requires a **direct** passkey wallet — its root authority is `createSecp256r1AuthorityInfo` (plain), not `createSecp256r1SessionAuthorityInfo`. Only the direct flavour supports swig's `signV1` path.
262
+
263
+ ```typescript
264
+ const { wallet: directWallet } = await passkey.createDirectPasskeyWallet({
265
+ sponsorshipRpc,
266
+ solanaRpc,
267
+ credentialId: credential.credentialId,
268
+ publicKeyBytes: credential.publicKeyBytes,
269
+ });
270
+
271
+ const tx = await passkey.buildPasskeySignedTransaction({
272
+ wallet: directWallet.address,
273
+ signer, // PasskeySigner — triggers a WebAuthn prompt
274
+ innerInstructions: [transferIx],
275
+ sponsorshipRpc,
276
+ solanaRpc,
277
+ });
278
+
279
+ const { signature } = await sponsorTransaction(sponsorshipRpc, tx);
280
+ ```
281
+
282
+ ### One passkey, two wallets
283
+
284
+ If your app needs **both** session-signed UX and direct passkey signing for the same user, mint both wallet types from the same credential at registration time:
285
+
286
+ ```typescript
287
+ const sessionBacked = await passkey.createPasskeyWallet({ ...common });
288
+ const direct = await passkey.createDirectPasskeyWallet({ ...common });
289
+ ```
290
+
291
+ This is the pattern the [`solana-passkey-e2e`](https://github.com/zerodevapp/solana-passkey-e2e) demo uses. Two wallets is the simplest way to support both signing paths — each wallet has exactly one passkey role, which keeps the SDK's "exactly one passkey role" invariant satisfied.
292
+
293
+ ### Testing without a browser
294
+
295
+ `@zerodev/solana-sponsorship-sdk/testing` exports a software passkey signer that signs with a held-in-memory P-256 keypair, no WebAuthn ceremony required. Use it from Node, Vitest, Playwright fixtures, or anywhere `navigator.credentials` is not available:
296
+
297
+ ```typescript
298
+ import { createSoftwarePasskeySigner } from "@zerodev/solana-sponsorship-sdk/testing";
299
+
300
+ const signer = await createSoftwarePasskeySigner({ rpId: "localhost" });
301
+
302
+ // `signer` is a `PasskeySigner` — drop into createPasskeyWallet, createSessionKey,
303
+ // buildPasskeySignedTransaction, buildSessionSignedTransaction unchanged.
304
+ ```
305
+
306
+ ### Errors
307
+
308
+ | Error | Meaning |
309
+ | --- | --- |
310
+ | `SessionExpiredError` | `currentSlot >= session.expiresAtSlot` at build time. Re-create the session. |
311
+ | `SessionBootstrapError` | Returned by `createPasskeyWalletWithSession` when the wallet was created but session creation failed or timed out. `result.wallet` is still valid; retry `createSessionKey`. |
312
+ | `passkey-signed transaction requires unsupported non-sponsor signature(s)` | You called `buildPasskeySignedTransaction` against a session-backed wallet. Either use `buildSessionSignedTransaction` with a session key, or mint a direct wallet with `createDirectPasskeyWallet`. |
313
+
314
+ ### End-to-end reference
315
+
316
+ The full canonical flow — register → both wallets → session key → session-signed transfer → direct passkey-signed transfer — lives in [`solana-passkey-e2e`](https://github.com/zerodevapp/solana-passkey-e2e). The Playwright `real mode` spec drives the same code path against a local validator and a live passkey server, so it's the closest thing to a runnable production example.
317
+
86
318
  ## Documentation
87
319
 
88
320
  ### Core Concepts
@@ -104,6 +336,34 @@ Sponsors a transaction and returns a `SponsorshipResponse`.
104
336
  #### `getFeePayer()`
105
337
  Gets the sponsor's fee payer address from the sponsorship server.
106
338
 
339
+ ### Passkey API (`passkey.*`)
340
+
341
+ All re-exported under both `import { passkey } from "..."` and as named top-level imports (`coseToCompressedP256`, `spkiToCompressedP256`, `extractCompressedP256`).
342
+
343
+ | Symbol | Purpose |
344
+ | --- | --- |
345
+ | `createBrowserPasskeySigner({ credentialId, publicKeyBytes, rpId })` | Build a `PasskeySigner` from a registered credential. |
346
+ | `createPasskeyWallet(params)` | Mint a session-backed swig wallet (use with `createSessionKey`). |
347
+ | `createDirectPasskeyWallet(params)` | Mint a direct-Secp256r1 swig wallet (use with `buildPasskeySignedTransaction`). |
348
+ | `createPasskeyWalletWithSession(params)` | Convenience: create the session-backed wallet **and** its first session key in one call. |
349
+ | `createSessionKey(params)` | Delegate a fresh `KeyPairSigner` as a time-bound authority on an existing wallet. |
350
+ | `buildSessionSignedTransaction(params)` | Compile a sponsor-payable tx signed by a session key. |
351
+ | `buildPasskeySignedTransaction(params)` | Compile a sponsor-payable tx signed via WebAuthn (direct wallets only). |
352
+ | `buildCreatePasskeyWalletTx(params)` / `buildCreateDirectPasskeyWalletTx(params)` | Lower-level: return the unsigned `TransactionMessage` instead of sponsoring + broadcasting. |
353
+ | `computePasskeyChallenge(message)` | SHA-256 of the wire-message preimage used as the WebAuthn challenge. |
354
+ | `passkeySignerToSigningFn(signer)` | Adapter from `PasskeySigner` to `@swig-wallet/lib`'s `UpstreamSigningFn`. |
355
+ | `extractCompressedP256(cred)` | Pull a 33-byte compressed P-256 pubkey out of a WebAuthn `PublicKeyCredential`. |
356
+ | `coseToCompressedP256(cose)` / `spkiToCompressedP256(spki)` | Lower-level conversions for COSE and SPKI inputs. |
357
+ | `MAX_SESSION_SLOTS` | `216_000n` — swig's hard cap on session duration. |
358
+ | `SessionExpiredError`, `SessionBootstrapError` | Typed errors — see [Errors](#errors). |
359
+
360
+ ### Testing helpers (`@zerodev/solana-sponsorship-sdk/testing`)
361
+
362
+ | Symbol | Purpose |
363
+ | --- | --- |
364
+ | `createSoftwarePasskeySigner({ rpId })` | In-memory P-256 `PasskeySigner` for Node tests; no WebAuthn ceremony. |
365
+ | `buildClientDataJSON`, `buildAuthenticatorData` | Low-level helpers for constructing canonical WebAuthn assertion components in fixtures. |
366
+
107
367
  ## License
108
368
 
109
369
  MIT License
package/dist/index.d.ts CHANGED
@@ -1 +1,3 @@
1
- export { type SponsorshipApi, type SponsorshipRpc, type SponsorshipRpcRequest, type SponsorshipRequest, type SponsorshipResponse, type SponsorshipConfig, type GetFeePayerResponse, type RpcError, type RpcResponse, SponsorshipError, createSponsorshipRpc, sponsorTransaction, setTransactionMessageSponsorFeePayer, setTransactionMessageSponsorFeePayerNoopSigner } from './sponsorship';
1
+ export { type SponsorshipApi, type SponsorshipRpc, type SponsorshipRpcRequest, type SponsorshipRequest, type SponsorshipResponse, type SponsorshipConfig, type GetFeePayerResponse, type RpcError, type RpcResponse, SponsorshipError, createSponsorshipRpc, sponsorTransaction, setTransactionMessageSponsorFeePayer, setTransactionMessageSponsorFeePayerNoopSigner } from './sponsorship.js';
2
+ export * as passkey from './passkey/index.js';
3
+ export { coseToCompressedP256, spkiToCompressedP256, extractCompressedP256, type PublicKeyCredentialLike } from './passkey/index.js';
package/dist/index.js CHANGED
@@ -1 +1,3 @@
1
- export { SponsorshipError, createSponsorshipRpc, sponsorTransaction, setTransactionMessageSponsorFeePayer, setTransactionMessageSponsorFeePayerNoopSigner } from './sponsorship';
1
+ export { SponsorshipError, createSponsorshipRpc, sponsorTransaction, setTransactionMessageSponsorFeePayer, setTransactionMessageSponsorFeePayerNoopSigner } from './sponsorship.js';
2
+ export * as passkey from './passkey/index.js';
3
+ export { coseToCompressedP256, spkiToCompressedP256, extractCompressedP256 } from './passkey/index.js';
@@ -0,0 +1,6 @@
1
+ export declare function computePasskeyChallenge(params: {
2
+ dataPayload: Uint8Array;
3
+ accountsPayload: Uint8Array;
4
+ slot: bigint;
5
+ odometer: number;
6
+ }): Uint8Array;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Challenge derivation for swig-wallet Secp256r1 (passkey) authorities.
3
+ *
4
+ * @swig-wallet/kit@1.9.1 does NOT export a standalone challenge hash helper.
5
+ * The digest logic lives inside `prepareSecp256r1Payload` in @swig-wallet/lib@1.9.1
6
+ * (node_modules/@swig-wallet/lib/dist/index.js, line 3925).
7
+ *
8
+ * Canonical preimage (mirrors upstream exactly):
9
+ * message = dataPayload || accountsPayload || slot_le64 || odometer_le32
10
+ * challenge = keccak256(message)
11
+ *
12
+ * `accountsPayload` is the already-encoded account-metas bytes returned by
13
+ * `getSignV2Instruction.preview(...)` — callers must NOT re-encode it.
14
+ *
15
+ * If @swig-wallet/kit is upgraded and the preimage layout changes, the
16
+ * pinned fixture in challenge.test.ts will fail, forcing an explicit bump.
17
+ */
18
+ import { keccak_256 } from "@noble/hashes/sha3";
19
+ export function computePasskeyChallenge(params) {
20
+ const { dataPayload, accountsPayload, slot, odometer } = params;
21
+ // 8-byte little-endian slot
22
+ const slotBytes = new Uint8Array(8);
23
+ new DataView(slotBytes.buffer).setBigUint64(0, slot, /* littleEndian= */ true);
24
+ // 4-byte little-endian odometer
25
+ const odometerBytes = new Uint8Array(4);
26
+ new DataView(odometerBytes.buffer).setUint32(0, odometer, /* littleEndian= */ true);
27
+ const message = new Uint8Array(dataPayload.length +
28
+ accountsPayload.length +
29
+ slotBytes.length +
30
+ odometerBytes.length);
31
+ let offset = 0;
32
+ message.set(dataPayload, offset);
33
+ offset += dataPayload.length;
34
+ message.set(accountsPayload, offset);
35
+ offset += accountsPayload.length;
36
+ message.set(slotBytes, offset);
37
+ offset += slotBytes.length;
38
+ message.set(odometerBytes, offset);
39
+ return keccak_256(message);
40
+ }
@@ -0,0 +1,3 @@
1
+ import { type Transaction } from "@solana/kit";
2
+ import type { CompileWithSponsorParams } from "./types.js";
3
+ export declare function compileWithSponsor(params: CompileWithSponsorParams): Promise<Transaction>;
@@ -0,0 +1,12 @@
1
+ import { appendTransactionMessageInstructions, blockhash, compileTransaction, createNoopSigner, createTransactionMessage, partiallySignTransaction, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, } from "@solana/kit";
2
+ export async function compileWithSponsor(params) {
3
+ const sponsorSigner = createNoopSigner(params.feePayer);
4
+ const message = pipe(createTransactionMessage({ version: 0 }), (m) => setTransactionMessageFeePayerSigner(sponsorSigner, m), (m) => appendTransactionMessageInstructions(params.instructions, m), (m) => setTransactionMessageLifetimeUsingBlockhash({
5
+ blockhash: blockhash(params.latestBlockhash.blockhash),
6
+ lastValidBlockHeight: params.latestBlockhash.lastValidBlockHeight,
7
+ }, m));
8
+ const compiled = compileTransaction(message);
9
+ if (!params.extraSigner)
10
+ return compiled;
11
+ return partiallySignTransaction([params.extraSigner.keyPair], compiled);
12
+ }
@@ -0,0 +1,4 @@
1
+ import { type Rpc, type SolanaRpcApi } from "@solana/kit";
2
+ import type { SignatureConfirmationOptions, SignatureConfirmationResult, SignatureConfirmationSettings } from "./types.js";
3
+ export declare function resolveSignatureConfirmationOptions(options?: SignatureConfirmationOptions): SignatureConfirmationSettings;
4
+ export declare function waitForSignatureConfirmation(rpc: Rpc<SolanaRpcApi>, rawSignature: string, options: SignatureConfirmationSettings): Promise<SignatureConfirmationResult>;
@@ -0,0 +1,48 @@
1
+ import { commitmentComparator, } from "@solana/kit";
2
+ export function resolveSignatureConfirmationOptions(options = {}) {
3
+ const settings = {
4
+ commitment: options.commitment ?? "confirmed",
5
+ maxAttempts: options.maxAttempts ?? 30,
6
+ intervalMs: options.intervalMs ?? 1000,
7
+ searchTransactionHistory: options.searchTransactionHistory ?? true,
8
+ };
9
+ if (settings.maxAttempts < 1) {
10
+ throw new RangeError("walletConfirmation.maxAttempts must be at least 1");
11
+ }
12
+ if (settings.intervalMs < 0) {
13
+ throw new RangeError("walletConfirmation.intervalMs must be non-negative");
14
+ }
15
+ return settings;
16
+ }
17
+ export async function waitForSignatureConfirmation(rpc, rawSignature, options) {
18
+ const { commitment, maxAttempts, intervalMs, searchTransactionHistory } = options;
19
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
20
+ const { value } = await rpc
21
+ .getSignatureStatuses([rawSignature], { searchTransactionHistory })
22
+ .send();
23
+ const status = value[0];
24
+ if (status?.err) {
25
+ return {
26
+ kind: "failed",
27
+ message: `wallet creation transaction failed: ${formatRpcValue(status.err)}`,
28
+ };
29
+ }
30
+ if (status?.confirmationStatus &&
31
+ commitmentComparator(status.confirmationStatus, commitment) >= 0) {
32
+ return { kind: "confirmed" };
33
+ }
34
+ if (attempt < maxAttempts - 1) {
35
+ await sleep(intervalMs);
36
+ }
37
+ }
38
+ return {
39
+ kind: "timeout",
40
+ message: `wallet creation transaction ${rawSignature} was not ${commitment} after ${maxAttempts} status checks`,
41
+ };
42
+ }
43
+ function sleep(ms) {
44
+ return new Promise((resolve) => setTimeout(resolve, ms));
45
+ }
46
+ function formatRpcValue(value) {
47
+ return (JSON.stringify(value, (_key, v) => typeof v === "bigint" ? v.toString() : v) ?? String(value));
48
+ }
@@ -0,0 +1 @@
1
+ export declare const MAX_SESSION_SLOTS = 216000n;
@@ -0,0 +1,5 @@
1
+ // Solana ~400ms slots: 24h ≈ 216,000 slots. Cap the root authority's
2
+ // session window at 24h so sessions can never outlive the wallet's
3
+ // security model. Per-session durations passed to createSessionKey may
4
+ // be shorter but never longer.
5
+ export const MAX_SESSION_SLOTS = 216000n;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Converts a COSE_Key blob (ES256 / P-256) into a 33-byte compressed P-256 pubkey.
3
+ *
4
+ * Accepts the minimal subset of CBOR needed for an ES256 EC2 key:
5
+ * {1: 2 (kty=EC2), 3: -7 (alg=ES256), -1: 1 (crv=P-256), -2: bstr(32) x, -3: bstr(32) y}
6
+ *
7
+ * Throws:
8
+ * - 'algorithm_not_supported' if alg !== -7
9
+ * - 'unsupported_curve' if crv !== 1
10
+ * - 'malformed_cose' on any structural problem (wrong kty, missing coord, truncation, etc.)
11
+ */
12
+ export declare function coseToCompressedP256(cose: Uint8Array): Uint8Array;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Converts a COSE_Key blob (ES256 / P-256) into a 33-byte compressed P-256 pubkey.
3
+ *
4
+ * Accepts the minimal subset of CBOR needed for an ES256 EC2 key:
5
+ * {1: 2 (kty=EC2), 3: -7 (alg=ES256), -1: 1 (crv=P-256), -2: bstr(32) x, -3: bstr(32) y}
6
+ *
7
+ * Throws:
8
+ * - 'algorithm_not_supported' if alg !== -7
9
+ * - 'unsupported_curve' if crv !== 1
10
+ * - 'malformed_cose' on any structural problem (wrong kty, missing coord, truncation, etc.)
11
+ */
12
+ export function coseToCompressedP256(cose) {
13
+ const map = readItem(cose, 0);
14
+ if (map.kind !== "map")
15
+ throw new Error("malformed_cose: expected map");
16
+ const fields = new Map();
17
+ let off = map.next;
18
+ for (let i = 0; i < map.value; i++) {
19
+ if (off >= cose.length)
20
+ throw new Error("malformed_cose: truncated map");
21
+ const key = readItem(cose, off);
22
+ if (key.kind !== "int")
23
+ throw new Error("malformed_cose: non-int key");
24
+ off = key.next;
25
+ if (off >= cose.length)
26
+ throw new Error("malformed_cose: truncated value");
27
+ const value = readItem(cose, off);
28
+ off = value.next;
29
+ fields.set(key.value, value);
30
+ }
31
+ const kty = fields.get(1);
32
+ if (!kty || kty.kind !== "int" || kty.value !== 2) {
33
+ throw new Error("malformed_cose: kty must be 2 (EC2)");
34
+ }
35
+ const alg = fields.get(3);
36
+ if (!alg || alg.kind !== "int" || alg.value !== -7) {
37
+ throw new Error("algorithm_not_supported: only ES256 (-7) is supported");
38
+ }
39
+ const crv = fields.get(-1);
40
+ if (!crv || crv.kind !== "int" || crv.value !== 1) {
41
+ throw new Error("unsupported_curve: only P-256 (crv=1) is supported");
42
+ }
43
+ const x = fields.get(-2);
44
+ const y = fields.get(-3);
45
+ if (!x || x.kind !== "bstr" || x.bytes.length !== 32) {
46
+ throw new Error("malformed_cose: x coord missing or not 32 bytes");
47
+ }
48
+ if (!y || y.kind !== "bstr" || y.bytes.length !== 32) {
49
+ throw new Error("malformed_cose: y coord missing or not 32 bytes");
50
+ }
51
+ const yOdd = (y.bytes[31] & 1) === 1;
52
+ const out = new Uint8Array(33);
53
+ out[0] = yOdd ? 0x03 : 0x02;
54
+ out.set(x.bytes, 1);
55
+ return out;
56
+ }
57
+ function readItem(bytes, offset) {
58
+ if (offset >= bytes.length)
59
+ throw new Error("malformed_cose: unexpected end of input");
60
+ const initial = bytes[offset];
61
+ const majorType = initial >> 5;
62
+ const additionalInfo = initial & 0x1f;
63
+ let next = offset + 1;
64
+ let value;
65
+ if (additionalInfo < 24) {
66
+ value = additionalInfo;
67
+ }
68
+ else if (additionalInfo === 24) {
69
+ if (next + 1 > bytes.length)
70
+ throw new Error("malformed_cose: truncated 1-byte length");
71
+ value = bytes[next];
72
+ next += 1;
73
+ }
74
+ else if (additionalInfo === 25) {
75
+ if (next + 2 > bytes.length)
76
+ throw new Error("malformed_cose: truncated 2-byte length");
77
+ value = (bytes[next] << 8) | bytes[next + 1];
78
+ next += 2;
79
+ }
80
+ else if (additionalInfo === 26) {
81
+ if (next + 4 > bytes.length)
82
+ throw new Error("malformed_cose: truncated 4-byte length");
83
+ value =
84
+ bytes[next] * 0x1000000 +
85
+ (bytes[next + 1] << 16) +
86
+ (bytes[next + 2] << 8) +
87
+ bytes[next + 3];
88
+ next += 4;
89
+ }
90
+ else {
91
+ throw new Error("malformed_cose: unsupported additional info");
92
+ }
93
+ switch (majorType) {
94
+ case 0:
95
+ return { kind: "int", value, next };
96
+ case 1:
97
+ return { kind: "int", value: -(value + 1), next };
98
+ case 2: {
99
+ if (next + value > bytes.length)
100
+ throw new Error("malformed_cose: truncated byte string");
101
+ return { kind: "bstr", bytes: bytes.subarray(next, next + value), next: next + value };
102
+ }
103
+ case 5:
104
+ return { kind: "map", value, next };
105
+ default:
106
+ throw new Error("malformed_cose: unsupported major type " + majorType);
107
+ }
108
+ }
@@ -0,0 +1,11 @@
1
+ export declare class SessionExpiredError extends Error {
2
+ readonly currentSlot: bigint;
3
+ readonly expiresAtSlot: bigint;
4
+ readonly name = "SessionExpiredError";
5
+ constructor(currentSlot: bigint, expiresAtSlot: bigint);
6
+ }
7
+ export declare class SessionBootstrapError extends Error {
8
+ readonly name = "SessionBootstrapError";
9
+ readonly cause: unknown;
10
+ constructor(message: string, cause: unknown);
11
+ }
@@ -0,0 +1,15 @@
1
+ export class SessionExpiredError extends Error {
2
+ constructor(currentSlot, expiresAtSlot) {
3
+ super(`session expired: currentSlot=${currentSlot} >= expiresAtSlot=${expiresAtSlot}`);
4
+ this.currentSlot = currentSlot;
5
+ this.expiresAtSlot = expiresAtSlot;
6
+ this.name = "SessionExpiredError";
7
+ }
8
+ }
9
+ export class SessionBootstrapError extends Error {
10
+ constructor(message, cause) {
11
+ super(message);
12
+ this.name = "SessionBootstrapError";
13
+ this.cause = cause;
14
+ }
15
+ }
@@ -0,0 +1,23 @@
1
+ export type PublicKeyCredentialLike = {
2
+ response: PublicKeyCredentialResponseLike;
3
+ };
4
+ type PublicKeyCredentialResponseLike = {
5
+ getPublicKey(): ArrayBuffer | Uint8Array | null;
6
+ getPublicKeyAlgorithm(): number;
7
+ };
8
+ /**
9
+ * Extracts a 33-byte compressed P-256 pubkey from a browser `PublicKeyCredential`
10
+ * registration response.
11
+ *
12
+ * Also accepts a structural shape (just `response.getPublicKey()` +
13
+ * `response.getPublicKeyAlgorithm()`) so the helper can be unit-tested in Node
14
+ * without DOM lib types.
15
+ *
16
+ * Throws:
17
+ * - 'algorithm_not_supported' if the credential's algorithm is not ES256 (-7)
18
+ * - 'malformed_spki' if `getPublicKey()` returns null or a non-conforming SPKI
19
+ * - 'unsupported_curve' if the SPKI's curve OID is not prime256v1
20
+ */
21
+ export declare function extractCompressedP256(cred: PublicKeyCredential): Uint8Array;
22
+ export declare function extractCompressedP256(cred: PublicKeyCredentialLike): Uint8Array;
23
+ export {};
@@ -0,0 +1,21 @@
1
+ import { spkiToCompressedP256 } from "./spki.js";
2
+ export function extractCompressedP256(cred) {
3
+ const response = cred.response;
4
+ if (!hasPublicKeyMethods(response)) {
5
+ throw new Error("malformed_spki: credential response does not expose public key methods");
6
+ }
7
+ const alg = response.getPublicKeyAlgorithm();
8
+ if (alg !== -7) {
9
+ throw new Error("algorithm_not_supported: expected ES256 (-7), got " + alg);
10
+ }
11
+ const raw = response.getPublicKey();
12
+ if (raw === null || raw === undefined) {
13
+ throw new Error("malformed_spki: getPublicKey() returned null");
14
+ }
15
+ const spki = raw instanceof Uint8Array ? raw : new Uint8Array(raw);
16
+ return spkiToCompressedP256(spki);
17
+ }
18
+ function hasPublicKeyMethods(response) {
19
+ const candidate = response;
20
+ return typeof candidate.getPublicKey === "function" && typeof candidate.getPublicKeyAlgorithm === "function";
21
+ }
@@ -0,0 +1,12 @@
1
+ export type { BuildCreatePasskeyWalletTxParams, BuildCreatePasskeyWalletTxResult, BuildPasskeySignedTransactionParams, BuildSessionSignedTransactionParams, CreatePasskeyWalletParams, CreatePasskeyWalletWithSessionParams, CreatePasskeyWalletWithSessionResult, CreateSessionKeyParams, PasskeySigner, PasskeyWallet, SessionKey, UpstreamSigningFn, SignatureConfirmationOptions, WebAuthnAssertion, } from "./types.js";
2
+ export { createBrowserPasskeySigner } from "./signer.js";
3
+ export { computePasskeyChallenge } from "./challenge.js";
4
+ export { MAX_SESSION_SLOTS } from "./constants.js";
5
+ export { buildCreateDirectPasskeyWalletTx, buildCreatePasskeyWalletTx, createDirectPasskeyWallet, createPasskeyWallet, } from "./wallet.js";
6
+ export { passkeySignerToSigningFn, } from "./signing-fn.js";
7
+ export { buildPasskeySignedTransaction, } from "./sign.js";
8
+ export { buildSessionSignedTransaction, createPasskeyWalletWithSession, createSessionKey, } from "./session.js";
9
+ export { SessionBootstrapError, SessionExpiredError } from "./errors.js";
10
+ export { coseToCompressedP256 } from "./cose.js";
11
+ export { spkiToCompressedP256 } from "./spki.js";
12
+ export { extractCompressedP256, type PublicKeyCredentialLike } from "./extract.js";
@@ -0,0 +1,11 @@
1
+ export { createBrowserPasskeySigner } from "./signer.js";
2
+ export { computePasskeyChallenge } from "./challenge.js";
3
+ export { MAX_SESSION_SLOTS } from "./constants.js";
4
+ export { buildCreateDirectPasskeyWalletTx, buildCreatePasskeyWalletTx, createDirectPasskeyWallet, createPasskeyWallet, } from "./wallet.js";
5
+ export { passkeySignerToSigningFn, } from "./signing-fn.js";
6
+ export { buildPasskeySignedTransaction, } from "./sign.js";
7
+ export { buildSessionSignedTransaction, createPasskeyWalletWithSession, createSessionKey, } from "./session.js";
8
+ export { SessionBootstrapError, SessionExpiredError } from "./errors.js";
9
+ export { coseToCompressedP256 } from "./cose.js";
10
+ export { spkiToCompressedP256 } from "./spki.js";
11
+ export { extractCompressedP256 } from "./extract.js";
@@ -0,0 +1,4 @@
1
+ import type { Address } from "@solana/kit";
2
+ import type { Swig } from "@swig-wallet/lib";
3
+ import type { PasskeySigner } from "./types.js";
4
+ export declare function requirePasskeyRoleId(swig: Swig, signer: PasskeySigner, wallet: Address): number;
@@ -0,0 +1,10 @@
1
+ export function requirePasskeyRoleId(swig, signer, wallet) {
2
+ const roles = swig.findRolesByAuthorityAddress(signer.publicKeyBytes);
3
+ if (roles.length === 0) {
4
+ throw new Error(`no role found for passkey on swig ${wallet}`);
5
+ }
6
+ if (roles.length > 1) {
7
+ throw new Error(`expected exactly one passkey role on swig ${wallet}, got ${roles.length}`);
8
+ }
9
+ return roles[0].id;
10
+ }