@arkade-os/sdk 0.4.23 → 0.4.25
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 +21 -1
- package/dist/cjs/contracts/contractManager.js +66 -5
- 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 +78 -0
- package/dist/cjs/index.js +3 -3
- package/dist/cjs/repositories/inMemory/walletRepository.js +35 -0
- package/dist/cjs/repositories/indexedDB/walletRepository.js +117 -0
- package/dist/cjs/repositories/realm/walletRepository.js +28 -0
- package/dist/cjs/repositories/sqlite/walletRepository.js +23 -0
- 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/unknownFields.js +2 -39
- package/dist/cjs/wallet/serviceWorker/wallet-message-handler.js +71 -10
- package/dist/cjs/wallet/serviceWorker/wallet.js +10 -0
- package/dist/cjs/wallet/unroll.js +79 -67
- package/dist/cjs/wallet/vtxo-manager.js +112 -16
- package/dist/cjs/wallet/wallet.js +64 -8
- package/dist/cjs/worker/expo/processors/contractPollProcessor.js +7 -2
- package/dist/esm/contracts/contractManager.js +66 -5
- 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 +69 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/repositories/inMemory/walletRepository.js +35 -0
- package/dist/esm/repositories/indexedDB/walletRepository.js +117 -0
- package/dist/esm/repositories/realm/walletRepository.js +28 -0
- package/dist/esm/repositories/sqlite/walletRepository.js +23 -0
- 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/unknownFields.js +2 -6
- package/dist/esm/wallet/serviceWorker/wallet-message-handler.js +71 -10
- package/dist/esm/wallet/serviceWorker/wallet.js +10 -0
- package/dist/esm/wallet/unroll.js +78 -67
- package/dist/esm/wallet/vtxo-manager.js +112 -16
- package/dist/esm/wallet/wallet.js +62 -6
- package/dist/esm/worker/expo/processors/contractPollProcessor.js +7 -2
- package/dist/types/contracts/contractManager.d.ts +17 -1
- package/dist/types/contracts/handlers/helpers.d.ts +0 -9
- package/dist/types/contracts/vtxoOwnership.d.ts +33 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/repositories/inMemory/walletRepository.d.ts +4 -1
- package/dist/types/repositories/indexedDB/walletRepository.d.ts +4 -1
- package/dist/types/repositories/realm/walletRepository.d.ts +4 -1
- package/dist/types/repositories/sqlite/walletRepository.d.ts +4 -1
- package/dist/types/repositories/walletRepository.d.ts +21 -0
- package/dist/types/script/tapscript.d.ts +4 -0
- package/dist/types/utils/timelock.d.ts +9 -0
- package/dist/types/wallet/serviceWorker/wallet-message-handler.d.ts +14 -2
- package/dist/types/wallet/unroll.d.ts +10 -0
- package/dist/types/wallet/vtxo-manager.d.ts +32 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -733,7 +733,7 @@ for await (const step of session) {
|
|
|
733
733
|
console.log(`Waiting for transaction ${step.txid} to be confirmed`);
|
|
734
734
|
break;
|
|
735
735
|
case Unroll.StepType.UNROLL:
|
|
736
|
-
console.log(`
|
|
736
|
+
console.log(`Transaction ${step.tx.id} unrolled`);
|
|
737
737
|
break;
|
|
738
738
|
case Unroll.StepType.DONE:
|
|
739
739
|
console.log(`Unrolling complete for virtual output ${step.vtxoTxid}`);
|
|
@@ -749,6 +749,26 @@ The unrolling process works by:
|
|
|
749
749
|
- Waiting for confirmations between steps
|
|
750
750
|
- Using P2A (Pay-to-Anchor) transactions to pay for fees
|
|
751
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
|
+
|
|
752
772
|
#### Step 2: Completing the Exit
|
|
753
773
|
|
|
754
774
|
Once virtual outputs are fully unrolled and the unilateral exit timelock has expired, you can complete the exit:
|
|
@@ -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,11 +358,61 @@ 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
|
}
|
|
376
|
+
async refreshOutpoints(outpoints) {
|
|
377
|
+
if (outpoints.length === 0)
|
|
378
|
+
return;
|
|
379
|
+
const { vtxos } = await this.config.indexerProvider.getVtxos({
|
|
380
|
+
outpoints,
|
|
381
|
+
});
|
|
382
|
+
if (vtxos.length === 0)
|
|
383
|
+
return;
|
|
384
|
+
// Filter to outputs whose script we own. Map them to their owning
|
|
385
|
+
// contract so we can write through to the right per-address entry
|
|
386
|
+
// in the wallet repository.
|
|
387
|
+
const scripts = Array.from(new Set(vtxos.map((v) => v.script)));
|
|
388
|
+
const contracts = await this.config.contractRepository.getContracts({
|
|
389
|
+
script: scripts,
|
|
390
|
+
});
|
|
391
|
+
const scriptToContract = new Map(contracts.map((c) => [c.script, c]));
|
|
392
|
+
const owned = vtxos.filter((v) => scriptToContract.has(v.script));
|
|
393
|
+
if (owned.length === 0)
|
|
394
|
+
return;
|
|
395
|
+
const annotated = await this.annotateVtxos(owned);
|
|
396
|
+
const byAddress = new Map();
|
|
397
|
+
for (const vtxo of annotated) {
|
|
398
|
+
const contract = scriptToContract.get(vtxo.script);
|
|
399
|
+
if (!contract)
|
|
400
|
+
continue;
|
|
401
|
+
const address = contract.address;
|
|
402
|
+
const arr = byAddress.get(address) ?? [];
|
|
403
|
+
arr.push(vtxo);
|
|
404
|
+
byAddress.set(address, arr);
|
|
405
|
+
}
|
|
406
|
+
for (const [address, addressVtxos] of byAddress) {
|
|
407
|
+
const contract = contracts.find((c) => c.address === address);
|
|
408
|
+
if (contract) {
|
|
409
|
+
await (0, vtxoOwnership_1.saveVtxosForContract)(this.config.walletRepository, contract, addressVtxos);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
await this.config.walletRepository.saveVtxos(address, addressVtxos);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
365
416
|
/**
|
|
366
417
|
* Check if currently watching.
|
|
367
418
|
*/
|
|
@@ -402,9 +453,9 @@ class ContractManager {
|
|
|
402
453
|
this.emitEvent(event);
|
|
403
454
|
}
|
|
404
455
|
async getVtxosForContracts(contracts) {
|
|
405
|
-
const res = await Promise.all(contracts.map((
|
|
456
|
+
const res = await Promise.all(contracts.map((contract) => (0, vtxoOwnership_1.getVtxosForContract)(this.config.walletRepository, contract).then((vtxos) => vtxos.map((vtxo) => ({
|
|
406
457
|
...vtxo,
|
|
407
|
-
contractScript: script,
|
|
458
|
+
contractScript: contract.script,
|
|
408
459
|
})))));
|
|
409
460
|
return res.flat();
|
|
410
461
|
}
|
|
@@ -470,7 +521,14 @@ class ContractManager {
|
|
|
470
521
|
});
|
|
471
522
|
}
|
|
472
523
|
for (const [addr, contractVtxos] of byContract) {
|
|
473
|
-
|
|
524
|
+
// The bucket is keyed by contract address, so the script filter
|
|
525
|
+
// here is the same as the contract's. Skip wrong-script rows
|
|
526
|
+
// rather than crash the reconcile loop.
|
|
527
|
+
const contract = contracts.find((c) => c.address === addr);
|
|
528
|
+
const filtered = (0, vtxoOwnership_1.warnAndFilterVtxosForScript)(contractVtxos, contract.script, "ContractManager.reconcilePendingFrontier");
|
|
529
|
+
if (filtered.length === 0)
|
|
530
|
+
continue;
|
|
531
|
+
await (0, vtxoOwnership_1.saveVtxosForContract)(this.config.walletRepository, contract, filtered);
|
|
474
532
|
}
|
|
475
533
|
}
|
|
476
534
|
async fetchContractVxosFromIndexer(contracts, pageSize, syncWindow) {
|
|
@@ -480,7 +538,10 @@ class ContractManager {
|
|
|
480
538
|
result.set(contractScript, vtxos);
|
|
481
539
|
const contract = contracts.find((c) => c.script === contractScript);
|
|
482
540
|
if (contract) {
|
|
483
|
-
|
|
541
|
+
const filtered = (0, vtxoOwnership_1.warnAndFilterVtxosForScript)(vtxos, contract.script, "ContractManager.fetchContractVxosFromIndexer");
|
|
542
|
+
if (filtered.length === 0)
|
|
543
|
+
continue;
|
|
544
|
+
await (0, vtxoOwnership_1.saveVtxosForContract)(this.config.walletRepository, contract, filtered);
|
|
484
545
|
}
|
|
485
546
|
}
|
|
486
547
|
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 = await (0, vtxoOwnership_1.getVtxosForContract)(this.config.walletRepository, state.contract);
|
|
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 = await (0, vtxoOwnership_1.getVtxosForContract)(repo, state.contract);
|
|
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,78 @@
|
|
|
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
|
+
exports.getVtxosForContract = getVtxosForContract;
|
|
9
|
+
exports.saveVtxosForContract = saveVtxosForContract;
|
|
10
|
+
/**
|
|
11
|
+
* Tier 1 helpers that enforce VTXO ownership at call sites that already know
|
|
12
|
+
* the intended contract script. Address-keyed repositories may still hand back
|
|
13
|
+
* legacy duplicate rows under the wrong bucket; these helpers gate reads and
|
|
14
|
+
* writes so a wrong-script row never wins.
|
|
15
|
+
*
|
|
16
|
+
* `script` is the authoritative ownership key. Equality is strict: a missing
|
|
17
|
+
* or empty `vtxo.script` never matches.
|
|
18
|
+
*/
|
|
19
|
+
function vtxoOutpoint(vtxo) {
|
|
20
|
+
return `${vtxo.txid}:${vtxo.vout}`;
|
|
21
|
+
}
|
|
22
|
+
function isVtxoForScript(vtxo, script) {
|
|
23
|
+
return !!vtxo.script && vtxo.script === script;
|
|
24
|
+
}
|
|
25
|
+
function filterVtxosForScript(vtxos, script) {
|
|
26
|
+
return vtxos.filter((v) => isVtxoForScript(v, script));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Background/indexer sync flavour: drop wrong-script rows and log enough
|
|
30
|
+
* context to identify each rejection. Returns only matching rows so the
|
|
31
|
+
* caller can keep going.
|
|
32
|
+
*/
|
|
33
|
+
function warnAndFilterVtxosForScript(vtxos, script, context) {
|
|
34
|
+
const matches = [];
|
|
35
|
+
const rejected = [];
|
|
36
|
+
for (const v of vtxos) {
|
|
37
|
+
if (isVtxoForScript(v, script)) {
|
|
38
|
+
matches.push(v);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
rejected.push(`${vtxoOutpoint(v)}(script=${v.script ?? ""})`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (rejected.length > 0) {
|
|
45
|
+
console.warn(`${context}: dropped ${rejected.length} wrong-script VTXO(s) for script ${script}: ${rejected.join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
return matches;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* User-initiated transaction/signing flavour: throw before persisting or
|
|
51
|
+
* signing inconsistent ownership state. Silently skipping here would hide a
|
|
52
|
+
* serious bug in the wallet's spend path.
|
|
53
|
+
*/
|
|
54
|
+
function validateVtxosForScript(vtxos, script, context) {
|
|
55
|
+
const mismatches = vtxos.filter((v) => !isVtxoForScript(v, script));
|
|
56
|
+
if (mismatches.length === 0)
|
|
57
|
+
return;
|
|
58
|
+
const detail = mismatches
|
|
59
|
+
.map((v) => `${vtxoOutpoint(v)}(script=${v.script ?? ""})`)
|
|
60
|
+
.join(", ");
|
|
61
|
+
throw new Error(`${context}: refusing to persist ${mismatches.length} VTXO(s) whose script does not match ${script}: ${detail}`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Tier 2 dispatch helpers: route to script-scoped repository methods when
|
|
65
|
+
* available, falling back to Tier 1 address-based filtering otherwise.
|
|
66
|
+
*/
|
|
67
|
+
async function getVtxosForContract(repo, contract) {
|
|
68
|
+
return repo.getVtxosForScript
|
|
69
|
+
? repo.getVtxosForScript(contract.script)
|
|
70
|
+
: filterVtxosForScript(await repo.getVtxos(contract.address), contract.script);
|
|
71
|
+
}
|
|
72
|
+
async function saveVtxosForContract(repo, contract, vtxos) {
|
|
73
|
+
if (repo.saveVtxosForScript) {
|
|
74
|
+
return repo.saveVtxosForScript({ script: contract.script, address: contract.address }, vtxos);
|
|
75
|
+
}
|
|
76
|
+
validateVtxosForScript(vtxos, contract.script, "saveVtxosForContract");
|
|
77
|
+
return repo.saveVtxos(contract.address, vtxos);
|
|
78
|
+
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -183,9 +183,9 @@ Object.defineProperty(exports, "decodeArkContract", { enumerable: true, get: fun
|
|
|
183
183
|
Object.defineProperty(exports, "contractFromArkContract", { enumerable: true, get: function () { return contracts_1.contractFromArkContract; } });
|
|
184
184
|
Object.defineProperty(exports, "contractFromArkContractWithAddress", { enumerable: true, get: function () { return contracts_1.contractFromArkContractWithAddress; } });
|
|
185
185
|
Object.defineProperty(exports, "isArkContract", { enumerable: true, get: function () { return contracts_1.isArkContract; } });
|
|
186
|
-
const
|
|
187
|
-
Object.defineProperty(exports, "timelockToSequence", { enumerable: true, get: function () { return
|
|
188
|
-
Object.defineProperty(exports, "sequenceToTimelock", { enumerable: true, get: function () { return
|
|
186
|
+
const timelock_1 = require("./utils/timelock");
|
|
187
|
+
Object.defineProperty(exports, "timelockToSequence", { enumerable: true, get: function () { return timelock_1.timelockToSequence; } });
|
|
188
|
+
Object.defineProperty(exports, "sequenceToTimelock", { enumerable: true, get: function () { return timelock_1.sequenceToTimelock; } });
|
|
189
189
|
const manager_1 = require("./repositories/indexedDB/manager");
|
|
190
190
|
Object.defineProperty(exports, "closeDatabase", { enumerable: true, get: function () { return manager_1.closeDatabase; } });
|
|
191
191
|
Object.defineProperty(exports, "openDatabase", { enumerable: true, get: function () { return manager_1.openDatabase; } });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.InMemoryWalletRepository = void 0;
|
|
4
|
+
const vtxoOwnership_1 = require("../../contracts/vtxoOwnership");
|
|
4
5
|
/**
|
|
5
6
|
* In-memory implementation of WalletRepository.
|
|
6
7
|
* Data is ephemeral and scoped to the instance.
|
|
@@ -24,6 +25,40 @@ class InMemoryWalletRepository {
|
|
|
24
25
|
async deleteVtxos(address) {
|
|
25
26
|
this.vtxosByAddress.delete(address);
|
|
26
27
|
}
|
|
28
|
+
async getVtxosForScript(script) {
|
|
29
|
+
const allMatches = [];
|
|
30
|
+
for (const bucket of this.vtxosByAddress.values()) {
|
|
31
|
+
for (const vtxo of bucket) {
|
|
32
|
+
if ((0, vtxoOwnership_1.isVtxoForScript)(vtxo, script)) {
|
|
33
|
+
allMatches.push(vtxo);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Dedup by outpoint (last-write-wins across address buckets)
|
|
38
|
+
return mergeByKey([], allMatches, (item) => `${item.txid}:${item.vout}`);
|
|
39
|
+
}
|
|
40
|
+
async saveVtxosForScript(key, vtxos) {
|
|
41
|
+
if (!key.address) {
|
|
42
|
+
throw new Error("InMemoryWalletRepository requires an address");
|
|
43
|
+
}
|
|
44
|
+
for (const vtxo of vtxos) {
|
|
45
|
+
if (!(0, vtxoOwnership_1.isVtxoForScript)(vtxo, key.script)) {
|
|
46
|
+
throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return this.saveVtxos(key.address, vtxos);
|
|
50
|
+
}
|
|
51
|
+
async deleteVtxosForScript(script) {
|
|
52
|
+
for (const [address, bucket] of this.vtxosByAddress.entries()) {
|
|
53
|
+
const next = bucket.filter((v) => !(0, vtxoOwnership_1.isVtxoForScript)(v, script));
|
|
54
|
+
if (next.length === 0) {
|
|
55
|
+
this.vtxosByAddress.delete(address);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
this.vtxosByAddress.set(address, next);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
27
62
|
async getUtxos(address) {
|
|
28
63
|
return this.utxosByAddress.get(address) ?? [];
|
|
29
64
|
}
|
|
@@ -6,6 +6,7 @@ const manager_1 = require("./manager");
|
|
|
6
6
|
const schema_1 = require("./schema");
|
|
7
7
|
const scriptFromAddress_1 = require("../scriptFromAddress");
|
|
8
8
|
const utils_1 = require("../../worker/browser/utils");
|
|
9
|
+
const vtxoOwnership_1 = require("../../contracts/vtxoOwnership");
|
|
9
10
|
/**
|
|
10
11
|
* IndexedDB-based implementation of WalletRepository.
|
|
11
12
|
*/
|
|
@@ -144,6 +145,85 @@ class IndexedDBWalletRepository {
|
|
|
144
145
|
throw error;
|
|
145
146
|
}
|
|
146
147
|
}
|
|
148
|
+
async getVtxosForScript(script) {
|
|
149
|
+
try {
|
|
150
|
+
const db = await this.getDB();
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
const transaction = db.transaction([db_1.STORE_VTXOS], "readonly");
|
|
153
|
+
const store = transaction.objectStore(db_1.STORE_VTXOS);
|
|
154
|
+
const index = store.index("script");
|
|
155
|
+
const request = index.getAll(script);
|
|
156
|
+
request.onerror = () => reject(request.error);
|
|
157
|
+
request.onsuccess = () => {
|
|
158
|
+
const results = request.result || [];
|
|
159
|
+
try {
|
|
160
|
+
// Defensive filter: only rows whose script matches.
|
|
161
|
+
const matching = results.filter((r) => r.script === script);
|
|
162
|
+
// Dedup same outpoint rows across address buckets.
|
|
163
|
+
// Work on raw rows so the address field is available
|
|
164
|
+
// for the canonicality tiebreaker.
|
|
165
|
+
const byOutpoint = new Map();
|
|
166
|
+
for (const row of matching) {
|
|
167
|
+
const outpoint = `${row.txid}:${row.vout}`;
|
|
168
|
+
const existing = byOutpoint.get(outpoint);
|
|
169
|
+
if (!existing) {
|
|
170
|
+
byOutpoint.set(outpoint, row);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (shouldReplaceVtxo(existing, row)) {
|
|
174
|
+
byOutpoint.set(outpoint, row);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
resolve(Array.from(byOutpoint.values()).map(deserializeVtxoWithBackfill));
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
reject(err);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
console.error(`Failed to get VTXOs for script ${script}:`, error);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async saveVtxosForScript(key, vtxos) {
|
|
191
|
+
if (!key.address) {
|
|
192
|
+
throw new Error("IndexedDBWalletRepository requires an address");
|
|
193
|
+
}
|
|
194
|
+
for (const vtxo of vtxos) {
|
|
195
|
+
if (!(0, vtxoOwnership_1.isVtxoForScript)(vtxo, key.script)) {
|
|
196
|
+
throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return this.saveVtxos(key.address, vtxos);
|
|
200
|
+
}
|
|
201
|
+
async deleteVtxosForScript(script) {
|
|
202
|
+
try {
|
|
203
|
+
const db = await this.getDB();
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
const transaction = db.transaction([db_1.STORE_VTXOS], "readwrite");
|
|
206
|
+
const store = transaction.objectStore(db_1.STORE_VTXOS);
|
|
207
|
+
const index = store.index("script");
|
|
208
|
+
const request = index.openCursor(IDBKeyRange.only(script));
|
|
209
|
+
request.onerror = () => reject(request.error);
|
|
210
|
+
request.onsuccess = () => {
|
|
211
|
+
const cursor = request.result;
|
|
212
|
+
if (cursor) {
|
|
213
|
+
cursor.delete();
|
|
214
|
+
cursor.continue();
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
resolve();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
console.error(`Failed to clear VTXOs for script ${script}:`, error);
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
147
227
|
async getUtxos(address) {
|
|
148
228
|
try {
|
|
149
229
|
const db = await this.getDB();
|
|
@@ -355,3 +435,40 @@ function deserializeVtxoWithBackfill(o) {
|
|
|
355
435
|
}
|
|
356
436
|
return (0, db_1.deserializeVtxo)(o);
|
|
357
437
|
}
|
|
438
|
+
function isCanonicalRow(row) {
|
|
439
|
+
try {
|
|
440
|
+
return (0, scriptFromAddress_1.scriptFromArkAddress)(row.address) === row.script;
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function shouldReplaceVtxo(existing, incoming) {
|
|
447
|
+
const existingCanonical = isCanonicalRow(existing);
|
|
448
|
+
const incomingCanonical = isCanonicalRow(incoming);
|
|
449
|
+
if (incomingCanonical && !existingCanonical)
|
|
450
|
+
return true;
|
|
451
|
+
if (existingCanonical && !incomingCanonical)
|
|
452
|
+
return false;
|
|
453
|
+
// Tie on canonicality, check lifecycle completeness
|
|
454
|
+
const existingWeight = getLifecycleWeight(existing);
|
|
455
|
+
const incomingWeight = getLifecycleWeight(incoming);
|
|
456
|
+
if (incomingWeight > existingWeight)
|
|
457
|
+
return true;
|
|
458
|
+
if (existingWeight > incomingWeight)
|
|
459
|
+
return false;
|
|
460
|
+
// Tie on weight, stable sort by address
|
|
461
|
+
return incoming.address < existing.address;
|
|
462
|
+
}
|
|
463
|
+
function getLifecycleWeight(v) {
|
|
464
|
+
let weight = 0;
|
|
465
|
+
if (v.isSpent !== undefined)
|
|
466
|
+
weight += 1;
|
|
467
|
+
if (v.spentBy)
|
|
468
|
+
weight += 2;
|
|
469
|
+
if (v.settledBy)
|
|
470
|
+
weight += 2;
|
|
471
|
+
if (v.arkTxId)
|
|
472
|
+
weight += 2;
|
|
473
|
+
return weight;
|
|
474
|
+
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.RealmWalletRepository = void 0;
|
|
4
4
|
const serialization_1 = require("../serialization");
|
|
5
5
|
const scriptFromAddress_1 = require("../scriptFromAddress");
|
|
6
|
+
const vtxoOwnership_1 = require("../../contracts/vtxoOwnership");
|
|
6
7
|
/**
|
|
7
8
|
* Realm-based implementation of WalletRepository.
|
|
8
9
|
*
|
|
@@ -88,6 +89,33 @@ class RealmWalletRepository {
|
|
|
88
89
|
this.realm.delete(toDelete);
|
|
89
90
|
});
|
|
90
91
|
}
|
|
92
|
+
async getVtxosForScript(script) {
|
|
93
|
+
await this.ensureInit();
|
|
94
|
+
const results = this.realm
|
|
95
|
+
.objects("ArkVtxo")
|
|
96
|
+
.filtered("script == $0", script);
|
|
97
|
+
return [...results].map(vtxoObjectToDomain);
|
|
98
|
+
}
|
|
99
|
+
async saveVtxosForScript(key, vtxos) {
|
|
100
|
+
if (!key.address) {
|
|
101
|
+
throw new Error("RealmWalletRepository requires an address");
|
|
102
|
+
}
|
|
103
|
+
for (const vtxo of vtxos) {
|
|
104
|
+
if (!(0, vtxoOwnership_1.isVtxoForScript)(vtxo, key.script)) {
|
|
105
|
+
throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return this.saveVtxos(key.address, vtxos);
|
|
109
|
+
}
|
|
110
|
+
async deleteVtxosForScript(script) {
|
|
111
|
+
await this.ensureInit();
|
|
112
|
+
this.realm.write(() => {
|
|
113
|
+
const toDelete = this.realm
|
|
114
|
+
.objects("ArkVtxo")
|
|
115
|
+
.filtered("script == $0", script);
|
|
116
|
+
this.realm.delete(toDelete);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
91
119
|
// ── UTXO management ────────────────────────────────────────────────
|
|
92
120
|
async getUtxos(address) {
|
|
93
121
|
await this.ensureInit();
|