@waaskey/sdk 0.0.1 → 0.1.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 CHANGED
@@ -16,28 +16,170 @@ pnpm add @waaskey/sdk
16
16
  ## Quickstart
17
17
 
18
18
  ```ts
19
- import { Waaskey } from '@waaskey/sdk';
19
+ import { Waaskey, WasmMpcCore, EncryptedShareStore } from '@waaskey/sdk';
20
20
 
21
- const waaskey = new Waaskey({ apiKey: process.env.WAASKEY_PUBLISHABLE_KEY! });
21
+ const waaskey = new Waaskey({
22
+ apiKey: process.env.WAASKEY_PUBLISHABLE_KEY!,
23
+ // Non-custodial: the device runs its half of the ceremony and seals its key share.
24
+ mpc: new WasmMpcCore(loadClientWasm), // device-party crypto core (WASM)
25
+ shareStore: EncryptedShareStore.browser(sessionSecret), // sealed in IndexedDB
26
+ });
22
27
 
23
- // Create an MPC wallet
24
- const wallet = await waaskey.wallets.create({ userId: user.id, chain: 'ethereum' });
25
- // → { id: 'wlt_…', address: '0xabc…' }
28
+ // Create an MPC wallet — runs the device keygen, seals the share, waits until active.
29
+ const wallet = await waaskey.wallets.create({ chain: 'ethereum' });
30
+ // → wallet.id, wallet.address, wallet.status === 'active'
26
31
 
27
- // Sign a message (interactive threshold-MPC, one awaited call)
28
- const signature = await wallet.signMessage('Hello Waaskey');
32
+ // Sign a 32-byte digest (hex; 0x optional)
33
+ const signature = await wallet.sign(digestHex);
34
+ ```
35
+
36
+ ### Send a transaction — WaaS signs, **you** broadcast
37
+
38
+ WaaS is a **signing service, not a broadcaster** (non-custodial: it never owns the
39
+ mempool, nonce, or status-tracking). `wallet.send(...)` builds and MPC-signs the
40
+ transaction and returns the **signed raw tx** — you submit it from your own
41
+ node/provider:
42
+
43
+ ```ts
44
+ const { signedTx, txHash } = await wallet.send({ chainId: 'evm:1', to, value });
45
+ // signedTx → the signed raw tx to broadcast; txHash → a deterministic offline id (not a confirmation)
46
+
47
+ // Broadcast from YOUR node — either the optional helper…
48
+ const { txHash: broadcastHash } = await waaskey.broadcast(signedTx, { rpcUrl: 'https://your-rpc' });
49
+ // …or your own submitter / provider (recommended for production — you own tracking & retries).
50
+ ```
51
+
52
+ The `broadcast` helper is best-effort by design — one `eth_sendRawTransaction`
53
+ call, **no status polling and no retries**. Tracking the tx to confirmation is the
54
+ integrator's concern.
55
+
56
+ ### Custody policy — choose the threshold topology (advanced)
57
+
58
+ `wallets.create({ chain })` defaults to the **embedded 2-of-3** topology (the platform
59
+ alone can meet the threshold — the honest default posture). To change the `(t, n)`
60
+ threshold or the custody guarantee, pass a custody policy; the SDK validates it
61
+ client-side and the backend enforces the attested invariant `platformShares < t ⟺
62
+ non-custodial`:
63
+
64
+ ```ts
65
+ const wallet = await waaskey.wallets.create({
66
+ chain: 'ethereum',
67
+ threshold: 2, // t
68
+ parties: ['device', 'backup', 'server'], // n = 3 party roles
69
+ custodyKinds: ['user_device', 'user_backup', 'platform_signer'], // parallel to parties
70
+ custodyType: 'shared', // requested posture — the server refuses (400) on a mismatch
71
+ });
72
+
73
+ // The returned wallet surfaces the attestation so you can display the guarantee:
74
+ wallet.threshold; // 2
75
+ wallet.parties; // ['device','backup','server']
76
+ wallet.custodyKinds; // ['user_device','user_backup','platform_signer']
77
+ wallet.platformShareCount; // 1
78
+ wallet.custodyType; // 'shared'
79
+ wallet.isNonCustodial; // true — platform's 1 share alone can't reach t=2
80
+ ```
81
+
82
+ `CustodyKind` ∈ `user_device | user_backup | platform_signer | platform_recovery |
83
+ external_party`; `CustodyType` ∈ `embedded | shared | self_custody`. The standalone
84
+ `isNonCustodial(wallet)` helper is also exported.
85
+
86
+ > The device runs whatever `t`-of-`n` the create ceremony describes — keygen is not
87
+ > pinned to 2-of-3. Coordinating an **interactive** `t`-of-`n` _sign_ across more than
88
+ > one user-held party (each running its half over the relay) is a separate follow-up;
89
+ > today's `wallet.sign(...)` covers the device+server quorum.
90
+
91
+ Read the wallet's signing activity (raw signs + send/sweep signed txs, newest first):
92
+
93
+ ```ts
94
+ const { items } = await wallet.signatures({ page: 1, limit: 20 });
29
95
  ```
30
96
 
31
97
  ## API
32
98
 
33
- | Method | Description |
34
- | -------------------------------------------- | ----------------------------------------- |
35
- | `new Waaskey({ apiKey, baseUrl?, fetch? })` | Create a client. |
36
- | `waaskey.wallets.create({ chain, userId? })` | Create a new wallet. Returns a `Wallet`. |
37
- | `waaskey.wallets.get(id)` | Load an existing wallet. |
38
- | `wallet.signMessage(message)` | Sign a message with the wallet's MPC key. |
99
+ | Method | Description |
100
+ | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
101
+ | `new Waaskey({ apiKey, mpc, shareStore, baseUrl?, fetch? })` | Create a client. `mpc` + `shareStore` are required to create wallets. |
102
+ | `waaskey.wallets.create({ chain, label?, threshold?, parties?, custodyKinds?, custodyType? }, options?)` | Create (keygen + sealed share). Optional custody policy (default embedded 2-of-3). Returns a `Wallet`. |
103
+ | `waaskey.wallets.get(id, options?)` | Load an existing wallet. |
104
+ | `wallet.sign(digestHex, options?)` | Sign a 32-byte digest with the wallet's MPC key. |
105
+ | `wallet.send(params, options?)` | Build + MPC-sign a tx. Returns the **signed raw tx** (you broadcast). |
106
+ | `wallet.signatures(query?, options?)` | The wallet's signing activity (paginated). |
107
+ | `waaskey.broadcast(signedTx, { rpcUrl })` | **Optional** best-effort submit of a signed tx from your own node. |
108
+
109
+ `create` / `sign` accept `{ signal }` for cancellation; `create` also takes
110
+ `{ waitForActive?, activationTimeoutMs?, pollIntervalMs? }`.
111
+
112
+ Errors are thrown as `WaaskeyError` with a typed `code` (e.g. `unauthorized`,
113
+ `forbidden`, `validation`, `device_core_required`, `keygen_failed`, `aborted`) plus
114
+ `status?` and `details?` — branch on `error.code`, never on the message text.
115
+
116
+ ### Secure share storage
117
+
118
+ The device key share is the user's half of the key. `EncryptedShareStore` seals it
119
+ with AES-256-GCM (key derived from a secret **you** supply — the user's session,
120
+ a passkey, or a device secret — never embedded) and persists the ciphertext in
121
+ IndexedDB (`MemoryKeyValueStore` for tests/SSR). Wipe everything on logout:
122
+
123
+ ```ts
124
+ await shareStore.clear();
125
+ ```
126
+
127
+ ### Multi-factor recovery
128
+
129
+ Back up the device share so a user can recover after losing their device. The share
130
+ is encrypted **client-side** with a recovery code (the server only ever stores the
131
+ opaque ciphertext), and its release is gated behind 3 factors: recovery code, TOTP,
132
+ and email OTP.
133
+
134
+ ```ts
135
+ // Enrol — show the returned recoveryCode to the user once (it's the only key).
136
+ const { recoveryCode } = await waaskey.recovery.register(wallet.id, {
137
+ share: (await shareStore.get(wallet.id))!,
138
+ totpSecret, // base32 authenticator secret
139
+ email,
140
+ });
141
+
142
+ // Later, on a new device — verify factors, re-key, and restore the share locally.
143
+ const { challengeId, requiredFactors } = await waaskey.recovery.challenge(wallet.id);
144
+ await waaskey.recovery.recover(wallet.id, {
145
+ challengeId,
146
+ recoveryCode,
147
+ verifications: [
148
+ { type: 'recovery_code', token: recoveryCode },
149
+ { type: 'totp', token: otpFromAuthenticator },
150
+ { type: 'email_otp', token: otpFromEmail },
151
+ ],
152
+ });
153
+ ```
154
+
155
+ | Method | Description |
156
+ | ------------------------------------------ | -------------------------------------------------------------- |
157
+ | `recovery.register(walletId, params)` | Encrypt the share + enrol factors. Returns the `recoveryCode`. |
158
+ | `recovery.getInfo(walletId)` | The wallet's registered factors. |
159
+ | `recovery.challenge(walletId)` | Start a recovery session (`challengeId` + required factors). |
160
+ | `recovery.recover(walletId, params)` | Verify, re-key, decrypt, and restore the share to the store. |
161
+ | `recovery.retrieveShare(walletId, params)` | Verify + decrypt the share without re-keying (read-only). |
162
+
163
+ ### Balances (client-side, no backend)
164
+
165
+ WaaS is non-custodial and the backend does not index chain state — balances are read
166
+ **directly from a chain provider**, dApp-style. EVM chains work out of the box via a
167
+ default public RPC; override per chain (or plug a custom provider for non-EVM):
168
+
169
+ ```ts
170
+ const waaskey = new Waaskey({
171
+ apiKey,
172
+ chains: { ethereum: { rpcUrl: 'https://your-rpc' } }, // optional; EVM has defaults
173
+ });
174
+
175
+ const eth = await waaskey.balances.getBalance('ethereum', wallet.address!);
176
+ // → { raw: 1000000000000000000n, decimals: 18, symbol: 'ETH', formatted: '1' }
177
+
178
+ const usdc = await waaskey.balances.getTokenBalance('ethereum', usdcAddress, wallet.address!, { symbol: 'USDC' });
179
+ ```
39
180
 
40
- Errors are thrown as `WaaskeyError` (`{ message, status, code }`).
181
+ Balances are exact `bigint` base units plus a `formatted` decimal string (no float).
182
+ `formatUnits(raw, decimals)` is exported for rendering.
41
183
 
42
184
  ## Development
43
185
 
@@ -51,4 +193,4 @@ pnpm build # tsup → dist (esm + cjs + d.ts)
51
193
 
52
194
  ## License
53
195
 
54
- [MIT](./LICENSE) © Waaskey
196
+ [MIT](./LICENSE) © WAASKey