@arkade-os/sdk 0.4.22 → 0.4.23

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 (74) hide show
  1. package/README.md +95 -12
  2. package/dist/cjs/contracts/arkcontract.js +2 -1
  3. package/dist/cjs/identity/descriptor.js +75 -4
  4. package/dist/cjs/identity/hdCapableIdentity.js +2 -0
  5. package/dist/cjs/identity/seedIdentity.js +225 -103
  6. package/dist/cjs/identity/serialize.js +5 -0
  7. package/dist/cjs/identity/staticDescriptorProvider.js +1 -1
  8. package/dist/cjs/index.js +12 -3
  9. package/dist/cjs/providers/electrum.js +285 -79
  10. package/dist/cjs/providers/expoIndexer.js +1 -1
  11. package/dist/cjs/providers/indexer.js +2 -2
  12. package/dist/cjs/providers/onchain.js +9 -3
  13. package/dist/cjs/repositories/migrations/walletRepositoryImpl.js +6 -2
  14. package/dist/cjs/repositories/realm/walletRepository.js +2 -2
  15. package/dist/cjs/repositories/serialization.js +34 -1
  16. package/dist/cjs/repositories/sqlite/walletRepository.js +4 -2
  17. package/dist/cjs/script/address.js +2 -1
  18. package/dist/cjs/utils/transactionHistory.js +4 -4
  19. package/dist/cjs/wallet/asset-manager.js +18 -18
  20. package/dist/cjs/wallet/asset.js +10 -8
  21. package/dist/cjs/wallet/delegator.js +2 -2
  22. package/dist/cjs/wallet/hdDescriptorProvider.js +159 -0
  23. package/dist/cjs/wallet/index.js +5 -1
  24. package/dist/cjs/wallet/onchain.js +2 -1
  25. package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +1 -1
  26. package/dist/cjs/wallet/serviceWorker/wallet.js +5 -4
  27. package/dist/cjs/wallet/validation.js +2 -3
  28. package/dist/cjs/wallet/wallet.js +13 -14
  29. package/dist/esm/contracts/arkcontract.js +2 -1
  30. package/dist/esm/identity/descriptor.js +74 -5
  31. package/dist/esm/identity/hdCapableIdentity.js +1 -0
  32. package/dist/esm/identity/seedIdentity.js +225 -103
  33. package/dist/esm/identity/serialize.js +5 -0
  34. package/dist/esm/identity/staticDescriptorProvider.js +1 -1
  35. package/dist/esm/index.js +7 -4
  36. package/dist/esm/providers/electrum.js +284 -78
  37. package/dist/esm/providers/expoIndexer.js +1 -1
  38. package/dist/esm/providers/indexer.js +2 -2
  39. package/dist/esm/providers/onchain.js +9 -3
  40. package/dist/esm/repositories/migrations/walletRepositoryImpl.js +6 -2
  41. package/dist/esm/repositories/realm/walletRepository.js +3 -3
  42. package/dist/esm/repositories/serialization.js +27 -0
  43. package/dist/esm/repositories/sqlite/walletRepository.js +5 -3
  44. package/dist/esm/script/address.js +2 -1
  45. package/dist/esm/utils/transactionHistory.js +4 -4
  46. package/dist/esm/wallet/asset-manager.js +18 -18
  47. package/dist/esm/wallet/asset.js +10 -8
  48. package/dist/esm/wallet/delegator.js +2 -2
  49. package/dist/esm/wallet/hdDescriptorProvider.js +155 -0
  50. package/dist/esm/wallet/index.js +4 -0
  51. package/dist/esm/wallet/onchain.js +2 -1
  52. package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +1 -1
  53. package/dist/esm/wallet/serviceWorker/wallet.js +5 -4
  54. package/dist/esm/wallet/validation.js +2 -3
  55. package/dist/esm/wallet/wallet.js +12 -14
  56. package/dist/types/contracts/arkcontract.d.ts +1 -1
  57. package/dist/types/identity/descriptor.d.ts +26 -0
  58. package/dist/types/identity/descriptorProvider.d.ts +11 -4
  59. package/dist/types/identity/hdCapableIdentity.d.ts +44 -0
  60. package/dist/types/identity/index.d.ts +1 -0
  61. package/dist/types/identity/seedIdentity.d.ts +113 -29
  62. package/dist/types/identity/serialize.d.ts +12 -0
  63. package/dist/types/identity/staticDescriptorProvider.d.ts +1 -1
  64. package/dist/types/index.d.ts +6 -3
  65. package/dist/types/providers/electrum.d.ts +115 -15
  66. package/dist/types/providers/onchain.d.ts +6 -0
  67. package/dist/types/repositories/serialization.d.ts +26 -2
  68. package/dist/types/script/address.d.ts +1 -1
  69. package/dist/types/wallet/hdDescriptorProvider.d.ts +93 -0
  70. package/dist/types/wallet/index.d.ts +19 -10
  71. package/dist/types/wallet/onchain.d.ts +1 -1
  72. package/dist/types/wallet/serviceWorker/wallet.d.ts +1 -1
  73. package/dist/types/wallet/wallet.d.ts +4 -1
  74. package/package.json +1 -1
package/README.md CHANGED
@@ -146,19 +146,19 @@ import { mnemonicToSeedSync } from '@scure/bip39'
146
146
  const seed = mnemonicToSeedSync(mnemonic)
147
147
  const identity = SeedIdentity.fromSeed(seed)
148
148
 
149
- // Or with a custom output descriptor
150
- const identityWithDescriptor = SeedIdentity.fromSeed(seed, { descriptor })
149
+ // Or with a custom account-descriptor template (must end in "/*)")
150
+ const identityWithDescriptor = SeedIdentity.fromSeed(seed, { descriptor: template })
151
151
 
152
- // Or with a custom descriptor and passphrase (MnemonicIdentity)
152
+ // Or with a custom template and passphrase (MnemonicIdentity)
153
153
  const identityWithDescriptorAndPassphrase = MnemonicIdentity.fromMnemonic(mnemonic, {
154
- descriptor,
154
+ descriptor: template,
155
155
  passphrase: 'my secret passphrase'
156
156
  })
157
157
  ```
158
158
 
159
159
  #### Watch-Only with ReadonlyDescriptorIdentity
160
160
 
161
- Create watch-only wallets from an output descriptor:
161
+ Create watch-only wallets from an account-descriptor template:
162
162
 
163
163
  ```typescript
164
164
  import { MnemonicIdentity, ReadonlyDescriptorIdentity, ReadonlyWallet } from '@arkade-os/sdk'
@@ -170,9 +170,9 @@ const mnemonic = generateMnemonic(wordlist)
170
170
  const identity = MnemonicIdentity.fromMnemonic(mnemonic)
171
171
  const readonly = await identity.toReadonly()
172
172
 
173
- // Or directly from a descriptor (e.g., from another wallet)
174
- const descriptor = "tr([12345678/86'/0'/0']xpub.../0/0)"
175
- const readonlyFromDescriptor = ReadonlyDescriptorIdentity.fromDescriptor(descriptor)
173
+ // Or directly from a wildcard template (e.g., exported from another wallet)
174
+ const template = "tr([12345678/86'/0'/0']xpub.../0/*)"
175
+ const readonlyFromTemplate = ReadonlyDescriptorIdentity.fromDescriptor(template)
176
176
 
177
177
  // Use in a watch-only wallet
178
178
  const readonlyWallet = await ReadonlyWallet.create({
@@ -184,12 +184,10 @@ const readonlyWallet = await ReadonlyWallet.create({
184
184
  const balance = await readonlyWallet.getBalance()
185
185
  ```
186
186
 
187
- **Derivation Path:** `m/86'/{coinType}'/0'/0/0`
187
+ **Derivation Path:** `m/86'/{coinType}'/0'/0/*`
188
188
  - BIP86 (Taproot) purpose
189
189
  - Coin type 0 for mainnet, 1 for testnet
190
- - Account 0, external chain, first address
191
-
192
- The descriptor format (`tr([fingerprint/path']xpub.../0/0)`) is HD-ready — future versions will support deriving multiple addresses and change outputs from the same seed.
190
+ - Account 0, external chain, wildcard index — `identity.descriptor` is the wildcard template that drives HD rotation; consumers materialize a concrete descriptor at a specific index when they need one.
193
191
 
194
192
  ### Batch Signing for Browser Wallets
195
193
 
@@ -227,6 +225,91 @@ await wallet.send({ address: 'ark1q...', amount: 1000 })
227
225
 
228
226
  Identities without `signMultiple` continue to work unchanged — each checkpoint is signed individually via `sign()`.
229
227
 
228
+ ### Onchain Providers
229
+
230
+ Wallets read onchain state (UTXOs, transactions, fee rates, chain tip) through an `OnchainProvider`. The SDK ships with two implementations and a single transport-agnostic interface so you can swap them without touching wallet code.
231
+
232
+ | Provider | Transport | When to use |
233
+ |---|---|---|
234
+ | `EsploraProvider` | REST/HTTP (mempool.space-compatible) | Default for browser wallets, public mempool deployments, simple integrations. Both atomic 1P1C package broadcast and outspends are first-class. |
235
+ | `ElectrumOnchainProvider` | WebSocket (Electrum protocol) | Self-hosted nodes (Fulcrum, electrs), low-latency subscriptions, environments where you control the backend. Required if you need to talk to an Electrum server directly. |
236
+
237
+ If you don't pass a provider explicitly, `OnchainWallet` and `Wallet.create({ ... })` both default to `EsploraProvider` pointing at the URL in `ESPLORA_URL[networkName]`.
238
+
239
+ #### Default URLs
240
+
241
+ The SDK ships with reachable defaults for each network — bitcoin, signet, and mutinynet point at Ark Labs–operated deployments; testnet falls back to mempool.space; regtest assumes a local nigiri stack.
242
+
243
+ ```typescript
244
+ import {
245
+ ESPLORA_URL, // Record<NetworkName, string>
246
+ ELECTRUM_WS_URL, // Record<NetworkName, string>
247
+ ELECTRUM_TCP_HOST, // Record<NetworkName, string | null> — informational
248
+ } from '@arkade-os/sdk'
249
+
250
+ ESPLORA_URL.bitcoin // "https://mempool.arkade.sh/api"
251
+ ESPLORA_URL.signet // "https://mempool.signet.arkade.sh/api"
252
+ ESPLORA_URL.mutinynet // "https://mempool.mutinynet.arkade.sh/api"
253
+
254
+ ELECTRUM_WS_URL.bitcoin // "wss://electrum.arkade.sh"
255
+ ELECTRUM_WS_URL.signet // "wss://electrum.signet.arkade.sh"
256
+ ELECTRUM_WS_URL.mutinynet // "wss://electrum.mutinynet.arkade.sh"
257
+ ```
258
+
259
+ #### Using Esplora (default)
260
+
261
+ ```typescript
262
+ import { EsploraProvider, ESPLORA_URL, OnchainWallet } from '@arkade-os/sdk'
263
+
264
+ // Use the default URL for the network
265
+ const provider = new EsploraProvider(ESPLORA_URL.bitcoin)
266
+
267
+ // Or pass nothing — OnchainWallet picks the default for you
268
+ const wallet = await OnchainWallet.create(identity, 'bitcoin')
269
+
270
+ // Or override with your own mempool/esplora instance
271
+ const customProvider = new EsploraProvider('https://my-esplora.example/api')
272
+ ```
273
+
274
+ #### Using Electrum (WebSocket)
275
+
276
+ ```typescript
277
+ import { ElectrumWS } from 'ws-electrumx-client'
278
+ import {
279
+ ElectrumOnchainProvider,
280
+ ELECTRUM_WS_URL,
281
+ OnchainWallet,
282
+ networks,
283
+ } from '@arkade-os/sdk'
284
+
285
+ const ws = new ElectrumWS(ELECTRUM_WS_URL.bitcoin)
286
+ const provider = new ElectrumOnchainProvider(ws, networks.bitcoin)
287
+
288
+ const wallet = await OnchainWallet.create(identity, 'bitcoin', provider)
289
+
290
+ // Remember to close the connection when you're done
291
+ await provider.close()
292
+ ```
293
+
294
+ #### Atomic 1P1C package broadcast (TRUC / BIP 431)
295
+
296
+ Both providers expose `broadcastTransaction(...txs)` that accepts either a single tx or a 1P1C package (parent first, child last). The package path is **atomic** — the parent doesn't have to independently meet mempool minfee, which is the point of TRUC relay.
297
+
298
+ The Electrum provider implements this via `blockchain.transaction.broadcast_package` (Fulcrum ≥ 1.10 backed by bitcoind ≥ v28). **There is no fallback to sequential broadcast**: if the server doesn't support `broadcast_package`, the call surfaces a clear error so you can route through a different provider rather than have TRUC packages silently fail at the parent step. Ark Labs Fulcrum deployments at `electrum.arkade.sh` (and the `*.signet` / `*.mutinynet` variants) all support it.
299
+
300
+ #### Server compatibility notes
301
+
302
+ `ElectrumOnchainProvider` is built around methods supported by both **Fulcrum** and **electrs** (the two main Electrum server implementations):
303
+
304
+ - ✅ `blockchain.scripthash.{listunspent, get_history, subscribe}`
305
+ - ✅ `blockchain.transaction.{get, get_merkle, broadcast}`
306
+ - ✅ `blockchain.block.header`, `blockchain.headers.subscribe`
307
+ - ✅ `blockchain.estimatefee`, `blockchain.relayfee`
308
+ - ⚠️ `blockchain.transaction.broadcast_package` — **Fulcrum-only**. Required for atomic 1P1C; the provider throws a descriptive error against electrs.
309
+ - ❌ The provider does **not** call `blockchain.transaction.get` with `verbose=true` (Fulcrum-only and rejected by electrs); confirmation status is derived from `transaction.get_merkle` + raw block headers instead.
310
+
311
+ Output amounts are derived from parsed raw transaction bytes (exact bigints), never from floating-point `value` fields — protocol-level money handling shouldn't depend on `Math.round(value * 1e8)`.
312
+
230
313
  ### Receiving Bitcoin
231
314
 
232
315
  ```typescript
@@ -7,6 +7,7 @@ exports.contractFromArkContractWithAddress = contractFromArkContractWithAddress;
7
7
  exports.isArkContract = isArkContract;
8
8
  const base_1 = require("@scure/base");
9
9
  const handlers_1 = require("./handlers");
10
+ const wallet_1 = require("../wallet");
10
11
  /**
11
12
  * Prefix for arkcontract strings.
12
13
  */
@@ -122,7 +123,7 @@ function contractFromArkContract(encoded, options = {}) {
122
123
  * @param options - Additional options
123
124
  * @returns A complete Contract object
124
125
  */
125
- function contractFromArkContractWithAddress(encoded, serverPubKey, addressPrefix, options = {}) {
126
+ function contractFromArkContractWithAddress(encoded, serverPubKey, addressPrefix = wallet_1.DEFAULT_ARKADE_HRP, options = {}) {
126
127
  const parsed = decodeArkContract(encoded);
127
128
  const handler = handlers_1.contractHandlers.getOrThrow(parsed.type);
128
129
  const params = parsed.data;
@@ -1,13 +1,80 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isMainnetDescriptor = isMainnetDescriptor;
4
+ exports.descriptorIsOurs = descriptorIsOurs;
3
5
  exports.isDescriptor = isDescriptor;
4
6
  exports.normalizeToDescriptor = normalizeToDescriptor;
5
7
  exports.extractPubKey = extractPubKey;
6
8
  exports.parseHDDescriptor = parseHDDescriptor;
7
9
  const descriptors_scure_1 = require("@bitcoinerlab/descriptors-scure");
8
10
  const base_1 = require("@scure/base");
9
- function inferNetwork(descriptor) {
10
- return descriptor.includes("tpub") ? descriptors_scure_1.networks.testnet : descriptors_scure_1.networks.bitcoin;
11
+ /**
12
+ * True iff `descriptor` is a Bitcoin mainnet descriptor (xpub-prefixed
13
+ * BIP32 key).
14
+ *
15
+ * Note: testnet, signet, regtest, mutinynet, and other non-mainnet
16
+ * networks all share the same `tpub` BIP32 version bytes — they cannot
17
+ * be distinguished from one another at the descriptor level. Callers
18
+ * that need a `Network` constants object for `expand()` pick
19
+ * `networks.bitcoin` vs `networks.testnet` themselves; callers that
20
+ * need the *actual* network the wallet is on must track that
21
+ * out-of-band.
22
+ */
23
+ function isMainnetDescriptor(descriptor) {
24
+ return !descriptor.includes("tpub");
25
+ }
26
+ /**
27
+ * Shared "does `candidate` belong to the identity backed by
28
+ * `ourDescriptor`?" predicate.
29
+ *
30
+ * - HD descriptors (expanding to a `bip32` key) match by account xpub
31
+ * on both sides — index-agnostic, so a wildcard template and any
32
+ * concrete index under it all collapse to the same xpub.
33
+ * - Bare `tr(pubkey)` candidates fall back to comparing the candidate
34
+ * pubkey against `ourXOnlyPubkey` (the cached pubkey on the identity
35
+ * side, since pulling it from `ourDescriptor` would require an index
36
+ * substitution the caller already performed).
37
+ */
38
+ function descriptorIsOurs(candidate, ourDescriptor, ourXOnlyPubkey) {
39
+ if (!isDescriptor(candidate))
40
+ return false;
41
+ try {
42
+ const candidateInfo = (0, descriptors_scure_1.expand)({
43
+ descriptor: candidate,
44
+ network: isMainnetDescriptor(candidate)
45
+ ? descriptors_scure_1.networks.bitcoin
46
+ : descriptors_scure_1.networks.testnet,
47
+ }).expansionMap?.["@0"];
48
+ if (!candidateInfo)
49
+ return false;
50
+ if (candidateInfo.bip32) {
51
+ const ourBip32 = (0, descriptors_scure_1.expand)({
52
+ descriptor: ourDescriptor,
53
+ network: isMainnetDescriptor(ourDescriptor)
54
+ ? descriptors_scure_1.networks.bitcoin
55
+ : descriptors_scure_1.networks.testnet,
56
+ }).expansionMap?.["@0"]?.bip32;
57
+ if (!ourBip32)
58
+ return false;
59
+ return ourBip32.toBase58() === candidateInfo.bip32.toBase58();
60
+ }
61
+ if (candidateInfo.pubkey) {
62
+ // For tr() the library hands back a 32-byte x-only key, but
63
+ // strip a leading parity byte defensively so a 33-byte
64
+ // compressed key (mismatched length) doesn't silently
65
+ // false-negative against our 32-byte x-only side.
66
+ const candidatePub = candidateInfo.pubkey.length === 33
67
+ ? candidateInfo.pubkey.subarray(1)
68
+ : candidateInfo.pubkey;
69
+ if (candidatePub.length !== ourXOnlyPubkey.length)
70
+ return false;
71
+ return base_1.hex.encode(candidatePub) === base_1.hex.encode(ourXOnlyPubkey);
72
+ }
73
+ return false;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
11
78
  }
12
79
  /**
13
80
  * Check if a string is a descriptor of the shape `tr(...)`.
@@ -49,7 +116,9 @@ function extractPubKey(descriptor) {
49
116
  if (!isDescriptor(descriptor)) {
50
117
  return descriptor;
51
118
  }
52
- const network = inferNetwork(descriptor);
119
+ const network = isMainnetDescriptor(descriptor)
120
+ ? descriptors_scure_1.networks.bitcoin
121
+ : descriptors_scure_1.networks.testnet;
53
122
  const expansion = (0, descriptors_scure_1.expand)({ descriptor, network });
54
123
  if (!expansion.expansionMap) {
55
124
  throw new Error("Cannot extract pubkey from descriptor: expansion failed.");
@@ -76,7 +145,9 @@ function parseHDDescriptor(descriptor) {
76
145
  }
77
146
  let expansion;
78
147
  try {
79
- const network = inferNetwork(descriptor);
148
+ const network = isMainnetDescriptor(descriptor)
149
+ ? descriptors_scure_1.networks.bitcoin
150
+ : descriptors_scure_1.networks.testnet;
80
151
  expansion = (0, descriptors_scure_1.expand)({ descriptor, network });
81
152
  }
82
153
  catch {
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });