@arkade-os/sdk 0.4.22 → 0.4.24
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 +116 -13
- package/dist/cjs/contracts/arkcontract.js +2 -1
- package/dist/cjs/contracts/contractManager.js +29 -4
- package/dist/cjs/contracts/contractWatcher.js +9 -3
- package/dist/cjs/contracts/handlers/default.js +3 -2
- package/dist/cjs/contracts/handlers/delegate.js +3 -2
- package/dist/cjs/contracts/handlers/helpers.js +2 -58
- package/dist/cjs/contracts/handlers/vhtlc.js +7 -6
- package/dist/cjs/contracts/vtxoOwnership.js +60 -0
- package/dist/cjs/identity/descriptor.js +75 -4
- package/dist/cjs/identity/hdCapableIdentity.js +2 -0
- package/dist/cjs/identity/seedIdentity.js +225 -103
- package/dist/cjs/identity/serialize.js +5 -0
- package/dist/cjs/identity/staticDescriptorProvider.js +1 -1
- package/dist/cjs/index.js +12 -3
- package/dist/cjs/providers/electrum.js +285 -79
- package/dist/cjs/providers/expoIndexer.js +1 -1
- package/dist/cjs/providers/indexer.js +2 -2
- package/dist/cjs/providers/onchain.js +9 -3
- package/dist/cjs/repositories/migrations/walletRepositoryImpl.js +6 -2
- package/dist/cjs/repositories/realm/walletRepository.js +2 -2
- package/dist/cjs/repositories/serialization.js +34 -1
- package/dist/cjs/repositories/sqlite/walletRepository.js +4 -2
- package/dist/cjs/script/address.js +2 -1
- package/dist/cjs/script/base.js +12 -47
- package/dist/cjs/script/tapscript.js +97 -73
- package/dist/cjs/utils/timelock.js +59 -0
- package/dist/cjs/utils/transactionHistory.js +4 -4
- package/dist/cjs/utils/unknownFields.js +2 -39
- package/dist/cjs/wallet/asset-manager.js +18 -18
- package/dist/cjs/wallet/asset.js +10 -8
- package/dist/cjs/wallet/delegator.js +2 -2
- package/dist/cjs/wallet/hdDescriptorProvider.js +159 -0
- package/dist/cjs/wallet/index.js +5 -1
- package/dist/cjs/wallet/onchain.js +2 -1
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +60 -10
- package/dist/cjs/wallet/serviceWorker/wallet.js +5 -4
- package/dist/cjs/wallet/unroll.js +79 -67
- package/dist/cjs/wallet/validation.js +2 -3
- package/dist/cjs/wallet/wallet.js +91 -22
- package/dist/cjs/worker/expo/processors/contractPollProcessor.js +7 -2
- package/dist/esm/contracts/arkcontract.js +2 -1
- package/dist/esm/contracts/contractManager.js +29 -4
- package/dist/esm/contracts/contractWatcher.js +9 -3
- package/dist/esm/contracts/handlers/default.js +2 -1
- package/dist/esm/contracts/handlers/delegate.js +2 -1
- package/dist/esm/contracts/handlers/helpers.js +1 -22
- package/dist/esm/contracts/handlers/vhtlc.js +2 -1
- package/dist/esm/contracts/vtxoOwnership.js +53 -0
- package/dist/esm/identity/descriptor.js +74 -5
- package/dist/esm/identity/hdCapableIdentity.js +1 -0
- package/dist/esm/identity/seedIdentity.js +225 -103
- package/dist/esm/identity/serialize.js +5 -0
- package/dist/esm/identity/staticDescriptorProvider.js +1 -1
- package/dist/esm/index.js +7 -4
- package/dist/esm/providers/electrum.js +284 -78
- package/dist/esm/providers/expoIndexer.js +1 -1
- package/dist/esm/providers/indexer.js +2 -2
- package/dist/esm/providers/onchain.js +9 -3
- package/dist/esm/repositories/migrations/walletRepositoryImpl.js +6 -2
- package/dist/esm/repositories/realm/walletRepository.js +3 -3
- package/dist/esm/repositories/serialization.js +27 -0
- package/dist/esm/repositories/sqlite/walletRepository.js +5 -3
- package/dist/esm/script/address.js +2 -1
- package/dist/esm/script/base.js +12 -14
- package/dist/esm/script/tapscript.js +97 -40
- package/dist/esm/utils/timelock.js +22 -0
- package/dist/esm/utils/transactionHistory.js +4 -4
- package/dist/esm/utils/unknownFields.js +2 -6
- package/dist/esm/wallet/asset-manager.js +18 -18
- package/dist/esm/wallet/asset.js +10 -8
- package/dist/esm/wallet/delegator.js +2 -2
- package/dist/esm/wallet/hdDescriptorProvider.js +155 -0
- package/dist/esm/wallet/index.js +4 -0
- package/dist/esm/wallet/onchain.js +2 -1
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +60 -10
- package/dist/esm/wallet/serviceWorker/wallet.js +5 -4
- package/dist/esm/wallet/unroll.js +78 -67
- package/dist/esm/wallet/validation.js +2 -3
- package/dist/esm/wallet/wallet.js +88 -20
- package/dist/esm/worker/expo/processors/contractPollProcessor.js +7 -2
- package/dist/types/contracts/arkcontract.d.ts +1 -1
- package/dist/types/contracts/handlers/helpers.d.ts +0 -9
- package/dist/types/contracts/vtxoOwnership.d.ts +25 -0
- package/dist/types/identity/descriptor.d.ts +26 -0
- package/dist/types/identity/descriptorProvider.d.ts +11 -4
- package/dist/types/identity/hdCapableIdentity.d.ts +44 -0
- package/dist/types/identity/index.d.ts +1 -0
- package/dist/types/identity/seedIdentity.d.ts +113 -29
- package/dist/types/identity/serialize.d.ts +12 -0
- package/dist/types/identity/staticDescriptorProvider.d.ts +1 -1
- package/dist/types/index.d.ts +6 -3
- package/dist/types/providers/electrum.d.ts +115 -15
- package/dist/types/providers/onchain.d.ts +6 -0
- package/dist/types/repositories/serialization.d.ts +26 -2
- package/dist/types/script/address.d.ts +1 -1
- package/dist/types/script/tapscript.d.ts +4 -0
- package/dist/types/utils/timelock.d.ts +9 -0
- package/dist/types/wallet/hdDescriptorProvider.d.ts +93 -0
- package/dist/types/wallet/index.d.ts +19 -10
- package/dist/types/wallet/onchain.d.ts +1 -1
- package/dist/types/wallet/serviceWorker/wallet.d.ts +1 -1
- package/dist/types/wallet/unroll.d.ts +10 -0
- package/dist/types/wallet/wallet.d.ts +4 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
174
|
-
const
|
|
175
|
-
const
|
|
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
|
|
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,
|
|
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
|
|
@@ -650,7 +733,7 @@ for await (const step of session) {
|
|
|
650
733
|
console.log(`Waiting for transaction ${step.txid} to be confirmed`);
|
|
651
734
|
break;
|
|
652
735
|
case Unroll.StepType.UNROLL:
|
|
653
|
-
console.log(`
|
|
736
|
+
console.log(`Transaction ${step.tx.id} unrolled`);
|
|
654
737
|
break;
|
|
655
738
|
case Unroll.StepType.DONE:
|
|
656
739
|
console.log(`Unrolling complete for virtual output ${step.vtxoTxid}`);
|
|
@@ -666,6 +749,26 @@ The unrolling process works by:
|
|
|
666
749
|
- Waiting for confirmations between steps
|
|
667
750
|
- Using P2A (Pay-to-Anchor) transactions to pay for fees
|
|
668
751
|
|
|
752
|
+
Optionally, you can use `session.next()` to control the broadcasting process manually.
|
|
753
|
+
|
|
754
|
+
```typescript
|
|
755
|
+
const step = await session.next();
|
|
756
|
+
switch (step.type) {
|
|
757
|
+
case Unroll.StepType.WAIT:
|
|
758
|
+
await step.do(); // wait for the transaction to be confirmed
|
|
759
|
+
break;
|
|
760
|
+
case Unroll.StepType.UNROLL:
|
|
761
|
+
const [parent, child] = step.pkg;
|
|
762
|
+
console.log(`Parent: ${parent}`)
|
|
763
|
+
console.log(`Child: ${child}`)
|
|
764
|
+
await step.do(); // broadcast the 1C1P package
|
|
765
|
+
break;
|
|
766
|
+
case Unroll.StepType.DONE:
|
|
767
|
+
console.log(`Unrolling complete for VTXO ${step.vtxoTxid}`);
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
669
772
|
#### Step 2: Completing the Exit
|
|
670
773
|
|
|
671
774
|
Once virtual outputs are fully unrolled and the unilateral exit timelock has expired, you can complete the exit:
|
|
@@ -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;
|
|
@@ -6,6 +6,7 @@ const contractWatcher_1 = require("./contractWatcher");
|
|
|
6
6
|
const handlers_1 = require("./handlers");
|
|
7
7
|
const utils_1 = require("../wallet/utils");
|
|
8
8
|
const syncCursors_1 = require("../utils/syncCursors");
|
|
9
|
+
const vtxoOwnership_1 = require("./vtxoOwnership");
|
|
9
10
|
const DEFAULT_PAGE_SIZE = 500;
|
|
10
11
|
/**
|
|
11
12
|
* Central manager for contract lifecycle and operations.
|
|
@@ -357,9 +358,19 @@ class ContractManager {
|
|
|
357
358
|
const contracts = opts?.scripts
|
|
358
359
|
? await this.getContracts({ script: opts.scripts })
|
|
359
360
|
: undefined;
|
|
361
|
+
// Only forward an explicit window when the caller supplied one. An
|
|
362
|
+
// empty `{ after: undefined, before: undefined }` would short-circuit
|
|
363
|
+
// both the cursor-derived `?after=` query in `syncContracts` (because
|
|
364
|
+
// `??` doesn't fire on a non-nullish object) AND the cursor-advance
|
|
365
|
+
// gate (which requires `options.window === undefined`), turning every
|
|
366
|
+
// `refreshVtxos()` call into an unbounded full re-scan whose cursor
|
|
367
|
+
// never moves forward.
|
|
368
|
+
const hasExplicitWindow = opts?.after !== undefined || opts?.before !== undefined;
|
|
360
369
|
await this.syncContracts({
|
|
361
370
|
contracts,
|
|
362
|
-
window:
|
|
371
|
+
window: hasExplicitWindow
|
|
372
|
+
? { after: opts?.after, before: opts?.before }
|
|
373
|
+
: undefined,
|
|
363
374
|
});
|
|
364
375
|
}
|
|
365
376
|
/**
|
|
@@ -402,7 +413,11 @@ class ContractManager {
|
|
|
402
413
|
this.emitEvent(event);
|
|
403
414
|
}
|
|
404
415
|
async getVtxosForContracts(contracts) {
|
|
405
|
-
const res = await Promise.all(contracts.map(({ script, address }) => this.config.walletRepository.getVtxos(address).then((vtxos) =>
|
|
416
|
+
const res = await Promise.all(contracts.map(({ script, address }) => this.config.walletRepository.getVtxos(address).then((vtxos) =>
|
|
417
|
+
// Address buckets may carry legacy duplicate rows from
|
|
418
|
+
// other contracts that once shared the same address —
|
|
419
|
+
// gate by script so the wrong-script row never wins.
|
|
420
|
+
(0, vtxoOwnership_1.filterVtxosForScript)(vtxos, script).map((vtxo) => ({
|
|
406
421
|
...vtxo,
|
|
407
422
|
contractScript: script,
|
|
408
423
|
})))));
|
|
@@ -470,7 +485,14 @@ class ContractManager {
|
|
|
470
485
|
});
|
|
471
486
|
}
|
|
472
487
|
for (const [addr, contractVtxos] of byContract) {
|
|
473
|
-
|
|
488
|
+
// The bucket is keyed by contract address, so the script filter
|
|
489
|
+
// here is the same as the contract's. Skip wrong-script rows
|
|
490
|
+
// rather than crash the reconcile loop.
|
|
491
|
+
const contract = contracts.find((c) => c.address === addr);
|
|
492
|
+
const filtered = (0, vtxoOwnership_1.warnAndFilterVtxosForScript)(contractVtxos, contract.script, "ContractManager.reconcilePendingFrontier");
|
|
493
|
+
if (filtered.length === 0)
|
|
494
|
+
continue;
|
|
495
|
+
await this.config.walletRepository.saveVtxos(addr, filtered);
|
|
474
496
|
}
|
|
475
497
|
}
|
|
476
498
|
async fetchContractVxosFromIndexer(contracts, pageSize, syncWindow) {
|
|
@@ -480,7 +502,10 @@ class ContractManager {
|
|
|
480
502
|
result.set(contractScript, vtxos);
|
|
481
503
|
const contract = contracts.find((c) => c.script === contractScript);
|
|
482
504
|
if (contract) {
|
|
483
|
-
|
|
505
|
+
const filtered = (0, vtxoOwnership_1.warnAndFilterVtxosForScript)(vtxos, contract.script, "ContractManager.fetchContractVxosFromIndexer");
|
|
506
|
+
if (filtered.length === 0)
|
|
507
|
+
continue;
|
|
508
|
+
await this.config.walletRepository.saveVtxos(contract.address, filtered);
|
|
484
509
|
}
|
|
485
510
|
}
|
|
486
511
|
return result;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ContractWatcher = void 0;
|
|
4
4
|
const utils_1 = require("../wallet/utils");
|
|
5
5
|
const utils_2 = require("../providers/utils");
|
|
6
|
+
const vtxoOwnership_1 = require("./vtxoOwnership");
|
|
6
7
|
/**
|
|
7
8
|
* Watches multiple contracts for virtual output state changes with resilient connection handling.
|
|
8
9
|
*
|
|
@@ -90,7 +91,10 @@ class ContractWatcher {
|
|
|
90
91
|
*/
|
|
91
92
|
async seedLastKnownVtxos(state) {
|
|
92
93
|
try {
|
|
93
|
-
|
|
94
|
+
// Apply the same script gate used by getContractVtxos so a legacy
|
|
95
|
+
// wrong-script row in the address bucket can't seed the baseline
|
|
96
|
+
// and then look "spent" on the first poll.
|
|
97
|
+
const cached = (0, vtxoOwnership_1.filterVtxosForScript)(await this.config.walletRepository.getVtxos(state.contract.address), state.contract.script);
|
|
94
98
|
for (const vtxo of cached) {
|
|
95
99
|
if (vtxo.isSpent)
|
|
96
100
|
continue;
|
|
@@ -169,8 +173,10 @@ class ContractWatcher {
|
|
|
169
173
|
return true;
|
|
170
174
|
})
|
|
171
175
|
.map(async (state) => {
|
|
172
|
-
// Use contract address as cache key
|
|
173
|
-
|
|
176
|
+
// Use contract address as cache key. Legacy address buckets
|
|
177
|
+
// can contain rows from other contracts; gate by script before
|
|
178
|
+
// converting so a wrong-script row never reaches the watcher.
|
|
179
|
+
const cached = (0, vtxoOwnership_1.filterVtxosForScript)(await repo.getVtxos(state.contract.address), state.contract.script);
|
|
174
180
|
if (cached.length > 0) {
|
|
175
181
|
// Convert to ContractVtxo with contractScript
|
|
176
182
|
const contractVtxos = cached.map((v) => ({
|
|
@@ -4,6 +4,7 @@ exports.DefaultContractHandler = void 0;
|
|
|
4
4
|
const base_1 = require("@scure/base");
|
|
5
5
|
const default_1 = require("../../script/default");
|
|
6
6
|
const helpers_1 = require("./helpers");
|
|
7
|
+
const timelock_1 = require("../../utils/timelock");
|
|
7
8
|
const descriptor_1 = require("../../identity/descriptor");
|
|
8
9
|
/**
|
|
9
10
|
* Extract pubkey bytes from a descriptor or hex string.
|
|
@@ -28,12 +29,12 @@ exports.DefaultContractHandler = {
|
|
|
28
29
|
return {
|
|
29
30
|
pubKey: base_1.hex.encode(params.pubKey),
|
|
30
31
|
serverPubKey: base_1.hex.encode(params.serverPubKey),
|
|
31
|
-
csvTimelock: (0,
|
|
32
|
+
csvTimelock: (0, timelock_1.timelockToSequence)(params.csvTimelock).toString(),
|
|
32
33
|
};
|
|
33
34
|
},
|
|
34
35
|
deserializeParams(params) {
|
|
35
36
|
const csvTimelock = params.csvTimelock
|
|
36
|
-
? (0,
|
|
37
|
+
? (0, timelock_1.sequenceToTimelock)(Number(params.csvTimelock))
|
|
37
38
|
: default_1.DefaultVtxo.Script.DEFAULT_TIMELOCK;
|
|
38
39
|
return {
|
|
39
40
|
pubKey: extractPubKeyBytes(params.pubKey),
|
|
@@ -5,6 +5,7 @@ const base_1 = require("@scure/base");
|
|
|
5
5
|
const delegate_1 = require("../../script/delegate");
|
|
6
6
|
const default_1 = require("../../script/default");
|
|
7
7
|
const helpers_1 = require("./helpers");
|
|
8
|
+
const timelock_1 = require("../../utils/timelock");
|
|
8
9
|
/**
|
|
9
10
|
* Handler for delegate wallet virtual outputs.
|
|
10
11
|
*
|
|
@@ -24,12 +25,12 @@ exports.DelegateContractHandler = {
|
|
|
24
25
|
pubKey: base_1.hex.encode(params.pubKey),
|
|
25
26
|
serverPubKey: base_1.hex.encode(params.serverPubKey),
|
|
26
27
|
delegatePubKey: base_1.hex.encode(params.delegatePubKey),
|
|
27
|
-
csvTimelock: (0,
|
|
28
|
+
csvTimelock: (0, timelock_1.timelockToSequence)(params.csvTimelock).toString(),
|
|
28
29
|
};
|
|
29
30
|
},
|
|
30
31
|
deserializeParams(params) {
|
|
31
32
|
const csvTimelock = params.csvTimelock
|
|
32
|
-
? (0,
|
|
33
|
+
? (0, timelock_1.sequenceToTimelock)(Number(params.csvTimelock))
|
|
33
34
|
: default_1.DefaultVtxo.Script.DEFAULT_TIMELOCK;
|
|
34
35
|
return {
|
|
35
36
|
pubKey: base_1.hex.decode(params.pubKey),
|
|
@@ -1,44 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.timelockToSequence = timelockToSequence;
|
|
37
|
-
exports.sequenceToTimelock = sequenceToTimelock;
|
|
38
3
|
exports.resolveRole = resolveRole;
|
|
39
4
|
exports.isCltvSatisfied = isCltvSatisfied;
|
|
40
5
|
exports.isCsvSpendable = isCsvSpendable;
|
|
41
|
-
const
|
|
6
|
+
const timelock_1 = require("../../utils/timelock");
|
|
42
7
|
const descriptor_1 = require("../../identity/descriptor");
|
|
43
8
|
/**
|
|
44
9
|
* Extract raw hex pubkey from a value that may be a descriptor or raw hex.
|
|
@@ -56,27 +21,6 @@ function extractRawPubKey(value) {
|
|
|
56
21
|
return undefined;
|
|
57
22
|
}
|
|
58
23
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Convert RelativeTimelock to BIP68 sequence number.
|
|
61
|
-
*/
|
|
62
|
-
function timelockToSequence(timelock) {
|
|
63
|
-
return bip68.encode(timelock.type === "blocks"
|
|
64
|
-
? { blocks: Number(timelock.value) }
|
|
65
|
-
: { seconds: Number(timelock.value) });
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Convert BIP68 sequence number back to RelativeTimelock.
|
|
69
|
-
*/
|
|
70
|
-
function sequenceToTimelock(sequence) {
|
|
71
|
-
const decoded = bip68.decode(sequence);
|
|
72
|
-
if ("blocks" in decoded && decoded.blocks !== undefined) {
|
|
73
|
-
return { type: "blocks", value: BigInt(decoded.blocks) };
|
|
74
|
-
}
|
|
75
|
-
if ("seconds" in decoded && decoded.seconds !== undefined) {
|
|
76
|
-
return { type: "seconds", value: BigInt(decoded.seconds) };
|
|
77
|
-
}
|
|
78
|
-
throw new Error(`Invalid BIP68 sequence: ${sequence}`);
|
|
79
|
-
}
|
|
80
24
|
/**
|
|
81
25
|
* Resolve wallet's role from explicit role or by matching descriptor/pubkey.
|
|
82
26
|
*/
|
|
@@ -152,7 +96,7 @@ function isCsvSpendable(context, sequence) {
|
|
|
152
96
|
return true;
|
|
153
97
|
if (!context.vtxo)
|
|
154
98
|
return false;
|
|
155
|
-
const timelock = sequenceToTimelock(sequence);
|
|
99
|
+
const timelock = (0, timelock_1.sequenceToTimelock)(sequence);
|
|
156
100
|
if (timelock.type === "blocks") {
|
|
157
101
|
if (context.blockHeight === undefined ||
|
|
158
102
|
context.vtxo.status.block_height === undefined) {
|
|
@@ -4,6 +4,7 @@ exports.VHTLCContractHandler = void 0;
|
|
|
4
4
|
const base_1 = require("@scure/base");
|
|
5
5
|
const vhtlc_1 = require("../../script/vhtlc");
|
|
6
6
|
const helpers_1 = require("./helpers");
|
|
7
|
+
const timelock_1 = require("../../utils/timelock");
|
|
7
8
|
/**
|
|
8
9
|
* Handler for Virtual Hash Time Lock Contract (VHTLC).
|
|
9
10
|
*
|
|
@@ -32,9 +33,9 @@ exports.VHTLCContractHandler = {
|
|
|
32
33
|
server: base_1.hex.encode(params.server),
|
|
33
34
|
hash: base_1.hex.encode(params.preimageHash),
|
|
34
35
|
refundLocktime: params.refundLocktime.toString(),
|
|
35
|
-
claimDelay: (0,
|
|
36
|
-
refundDelay: (0,
|
|
37
|
-
refundNoReceiverDelay: (0,
|
|
36
|
+
claimDelay: (0, timelock_1.timelockToSequence)(params.unilateralClaimDelay).toString(),
|
|
37
|
+
refundDelay: (0, timelock_1.timelockToSequence)(params.unilateralRefundDelay).toString(),
|
|
38
|
+
refundNoReceiverDelay: (0, timelock_1.timelockToSequence)(params.unilateralRefundWithoutReceiverDelay).toString(),
|
|
38
39
|
};
|
|
39
40
|
},
|
|
40
41
|
deserializeParams(params) {
|
|
@@ -44,9 +45,9 @@ exports.VHTLCContractHandler = {
|
|
|
44
45
|
server: base_1.hex.decode(params.server),
|
|
45
46
|
preimageHash: base_1.hex.decode(params.hash),
|
|
46
47
|
refundLocktime: BigInt(params.refundLocktime),
|
|
47
|
-
unilateralClaimDelay: (0,
|
|
48
|
-
unilateralRefundDelay: (0,
|
|
49
|
-
unilateralRefundWithoutReceiverDelay: (0,
|
|
48
|
+
unilateralClaimDelay: (0, timelock_1.sequenceToTimelock)(Number(params.claimDelay)),
|
|
49
|
+
unilateralRefundDelay: (0, timelock_1.sequenceToTimelock)(Number(params.refundDelay)),
|
|
50
|
+
unilateralRefundWithoutReceiverDelay: (0, timelock_1.sequenceToTimelock)(Number(params.refundNoReceiverDelay)),
|
|
50
51
|
};
|
|
51
52
|
},
|
|
52
53
|
/**
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.vtxoOutpoint = vtxoOutpoint;
|
|
4
|
+
exports.isVtxoForScript = isVtxoForScript;
|
|
5
|
+
exports.filterVtxosForScript = filterVtxosForScript;
|
|
6
|
+
exports.warnAndFilterVtxosForScript = warnAndFilterVtxosForScript;
|
|
7
|
+
exports.validateVtxosForScript = validateVtxosForScript;
|
|
8
|
+
/**
|
|
9
|
+
* Tier 1 helpers that enforce VTXO ownership at call sites that already know
|
|
10
|
+
* the intended contract script. Address-keyed repositories may still hand back
|
|
11
|
+
* legacy duplicate rows under the wrong bucket; these helpers gate reads and
|
|
12
|
+
* writes so a wrong-script row never wins.
|
|
13
|
+
*
|
|
14
|
+
* `script` is the authoritative ownership key. Equality is strict: a missing
|
|
15
|
+
* or empty `vtxo.script` never matches.
|
|
16
|
+
*/
|
|
17
|
+
function vtxoOutpoint(vtxo) {
|
|
18
|
+
return `${vtxo.txid}:${vtxo.vout}`;
|
|
19
|
+
}
|
|
20
|
+
function isVtxoForScript(vtxo, script) {
|
|
21
|
+
return !!vtxo.script && vtxo.script === script;
|
|
22
|
+
}
|
|
23
|
+
function filterVtxosForScript(vtxos, script) {
|
|
24
|
+
return vtxos.filter((v) => isVtxoForScript(v, script));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Background/indexer sync flavour: drop wrong-script rows and log enough
|
|
28
|
+
* context to identify each rejection. Returns only matching rows so the
|
|
29
|
+
* caller can keep going.
|
|
30
|
+
*/
|
|
31
|
+
function warnAndFilterVtxosForScript(vtxos, script, context) {
|
|
32
|
+
const matches = [];
|
|
33
|
+
const rejected = [];
|
|
34
|
+
for (const v of vtxos) {
|
|
35
|
+
if (isVtxoForScript(v, script)) {
|
|
36
|
+
matches.push(v);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
rejected.push(`${vtxoOutpoint(v)}(script=${v.script ?? ""})`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (rejected.length > 0) {
|
|
43
|
+
console.warn(`${context}: dropped ${rejected.length} wrong-script VTXO(s) for script ${script}: ${rejected.join(", ")}`);
|
|
44
|
+
}
|
|
45
|
+
return matches;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* User-initiated transaction/signing flavour: throw before persisting or
|
|
49
|
+
* signing inconsistent ownership state. Silently skipping here would hide a
|
|
50
|
+
* serious bug in the wallet's spend path.
|
|
51
|
+
*/
|
|
52
|
+
function validateVtxosForScript(vtxos, script, context) {
|
|
53
|
+
const mismatches = vtxos.filter((v) => !isVtxoForScript(v, script));
|
|
54
|
+
if (mismatches.length === 0)
|
|
55
|
+
return;
|
|
56
|
+
const detail = mismatches
|
|
57
|
+
.map((v) => `${vtxoOutpoint(v)}(script=${v.script ?? ""})`)
|
|
58
|
+
.join(", ");
|
|
59
|
+
throw new Error(`${context}: refusing to persist ${mismatches.length} VTXO(s) whose script does not match ${script}: ${detail}`);
|
|
60
|
+
}
|
|
@@ -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
|
-
|
|
10
|
-
|
|
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 =
|
|
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 =
|
|
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 {
|