@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { hex } from "@scure/base";
|
|
2
2
|
import { DefaultVtxo } from '../../script/default.js';
|
|
3
|
-
import { isCsvSpendable
|
|
3
|
+
import { isCsvSpendable } from './helpers.js';
|
|
4
|
+
import { sequenceToTimelock, timelockToSequence } from '../../utils/timelock.js';
|
|
4
5
|
import { normalizeToDescriptor, extractPubKey, } from '../../identity/descriptor.js';
|
|
5
6
|
/**
|
|
6
7
|
* Extract pubkey bytes from a descriptor or hex string.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { hex } from "@scure/base";
|
|
2
2
|
import { DelegateVtxo } from '../../script/delegate.js';
|
|
3
3
|
import { DefaultVtxo } from '../../script/default.js';
|
|
4
|
-
import { isCsvSpendable
|
|
4
|
+
import { isCsvSpendable } from './helpers.js';
|
|
5
|
+
import { sequenceToTimelock, timelockToSequence } from '../../utils/timelock.js';
|
|
5
6
|
/**
|
|
6
7
|
* Handler for delegate wallet virtual outputs.
|
|
7
8
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { sequenceToTimelock } from '../../utils/timelock.js';
|
|
2
2
|
import { isDescriptor, extractPubKey } from '../../identity/descriptor.js';
|
|
3
3
|
/**
|
|
4
4
|
* Extract raw hex pubkey from a value that may be a descriptor or raw hex.
|
|
@@ -16,27 +16,6 @@ function extractRawPubKey(value) {
|
|
|
16
16
|
return undefined;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Convert RelativeTimelock to BIP68 sequence number.
|
|
21
|
-
*/
|
|
22
|
-
export function timelockToSequence(timelock) {
|
|
23
|
-
return bip68.encode(timelock.type === "blocks"
|
|
24
|
-
? { blocks: Number(timelock.value) }
|
|
25
|
-
: { seconds: Number(timelock.value) });
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Convert BIP68 sequence number back to RelativeTimelock.
|
|
29
|
-
*/
|
|
30
|
-
export function sequenceToTimelock(sequence) {
|
|
31
|
-
const decoded = bip68.decode(sequence);
|
|
32
|
-
if ("blocks" in decoded && decoded.blocks !== undefined) {
|
|
33
|
-
return { type: "blocks", value: BigInt(decoded.blocks) };
|
|
34
|
-
}
|
|
35
|
-
if ("seconds" in decoded && decoded.seconds !== undefined) {
|
|
36
|
-
return { type: "seconds", value: BigInt(decoded.seconds) };
|
|
37
|
-
}
|
|
38
|
-
throw new Error(`Invalid BIP68 sequence: ${sequence}`);
|
|
39
|
-
}
|
|
40
19
|
/**
|
|
41
20
|
* Resolve wallet's role from explicit role or by matching descriptor/pubkey.
|
|
42
21
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { hex } from "@scure/base";
|
|
2
2
|
import { VHTLC } from '../../script/vhtlc.js';
|
|
3
|
-
import { isCltvSatisfied, isCsvSpendable, resolveRole
|
|
3
|
+
import { isCltvSatisfied, isCsvSpendable, resolveRole } from './helpers.js';
|
|
4
|
+
import { sequenceToTimelock, timelockToSequence } from '../../utils/timelock.js';
|
|
4
5
|
/**
|
|
5
6
|
* Handler for Virtual Hash Time Lock Contract (VHTLC).
|
|
6
7
|
*
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 1 helpers that enforce VTXO ownership at call sites that already know
|
|
3
|
+
* the intended contract script. Address-keyed repositories may still hand back
|
|
4
|
+
* legacy duplicate rows under the wrong bucket; these helpers gate reads and
|
|
5
|
+
* writes so a wrong-script row never wins.
|
|
6
|
+
*
|
|
7
|
+
* `script` is the authoritative ownership key. Equality is strict: a missing
|
|
8
|
+
* or empty `vtxo.script` never matches.
|
|
9
|
+
*/
|
|
10
|
+
export function vtxoOutpoint(vtxo) {
|
|
11
|
+
return `${vtxo.txid}:${vtxo.vout}`;
|
|
12
|
+
}
|
|
13
|
+
export function isVtxoForScript(vtxo, script) {
|
|
14
|
+
return !!vtxo.script && vtxo.script === script;
|
|
15
|
+
}
|
|
16
|
+
export function filterVtxosForScript(vtxos, script) {
|
|
17
|
+
return vtxos.filter((v) => isVtxoForScript(v, script));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Background/indexer sync flavour: drop wrong-script rows and log enough
|
|
21
|
+
* context to identify each rejection. Returns only matching rows so the
|
|
22
|
+
* caller can keep going.
|
|
23
|
+
*/
|
|
24
|
+
export function warnAndFilterVtxosForScript(vtxos, script, context) {
|
|
25
|
+
const matches = [];
|
|
26
|
+
const rejected = [];
|
|
27
|
+
for (const v of vtxos) {
|
|
28
|
+
if (isVtxoForScript(v, script)) {
|
|
29
|
+
matches.push(v);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
rejected.push(`${vtxoOutpoint(v)}(script=${v.script ?? ""})`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (rejected.length > 0) {
|
|
36
|
+
console.warn(`${context}: dropped ${rejected.length} wrong-script VTXO(s) for script ${script}: ${rejected.join(", ")}`);
|
|
37
|
+
}
|
|
38
|
+
return matches;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* User-initiated transaction/signing flavour: throw before persisting or
|
|
42
|
+
* signing inconsistent ownership state. Silently skipping here would hide a
|
|
43
|
+
* serious bug in the wallet's spend path.
|
|
44
|
+
*/
|
|
45
|
+
export function validateVtxosForScript(vtxos, script, context) {
|
|
46
|
+
const mismatches = vtxos.filter((v) => !isVtxoForScript(v, script));
|
|
47
|
+
if (mismatches.length === 0)
|
|
48
|
+
return;
|
|
49
|
+
const detail = mismatches
|
|
50
|
+
.map((v) => `${vtxoOutpoint(v)}(script=${v.script ?? ""})`)
|
|
51
|
+
.join(", ");
|
|
52
|
+
throw new Error(`${context}: refusing to persist ${mismatches.length} VTXO(s) whose script does not match ${script}: ${detail}`);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Tier 2 dispatch helpers: route to script-scoped repository methods when
|
|
56
|
+
* available, falling back to Tier 1 address-based filtering otherwise.
|
|
57
|
+
*/
|
|
58
|
+
export async function getVtxosForContract(repo, contract) {
|
|
59
|
+
return repo.getVtxosForScript
|
|
60
|
+
? repo.getVtxosForScript(contract.script)
|
|
61
|
+
: filterVtxosForScript(await repo.getVtxos(contract.address), contract.script);
|
|
62
|
+
}
|
|
63
|
+
export async function saveVtxosForContract(repo, contract, vtxos) {
|
|
64
|
+
if (repo.saveVtxosForScript) {
|
|
65
|
+
return repo.saveVtxosForScript({ script: contract.script, address: contract.address }, vtxos);
|
|
66
|
+
}
|
|
67
|
+
validateVtxosForScript(vtxos, contract.script, "saveVtxosForContract");
|
|
68
|
+
return repo.saveVtxos(contract.address, vtxos);
|
|
69
|
+
}
|
package/dist/esm/index.js
CHANGED
|
@@ -42,7 +42,7 @@ export * from './arkfee/index.js';
|
|
|
42
42
|
export * as asset from './extension/asset/index.js';
|
|
43
43
|
// Contracts
|
|
44
44
|
import { ContractManager, ContractWatcher, contractHandlers, DefaultContractHandler, DelegateContractHandler, VHTLCContractHandler, encodeArkContract, decodeArkContract, contractFromArkContract, contractFromArkContractWithAddress, isArkContract, } from './contracts/index.js';
|
|
45
|
-
import { timelockToSequence, sequenceToTimelock
|
|
45
|
+
import { timelockToSequence, sequenceToTimelock } from './utils/timelock.js';
|
|
46
46
|
import { closeDatabase, openDatabase } from './repositories/indexedDB/manager.js';
|
|
47
47
|
import { WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, DelegatorNotConfiguredError, } from './wallet/serviceWorker/wallet-message-handler.js';
|
|
48
48
|
import { MESSAGE_BUS_NOT_INITIALIZED, MessageBusNotInitializedError, ServiceWorkerTimeoutError, } from './worker/errors.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isVtxoForScript } from '../../contracts/vtxoOwnership.js';
|
|
1
2
|
/**
|
|
2
3
|
* In-memory implementation of WalletRepository.
|
|
3
4
|
* Data is ephemeral and scoped to the instance.
|
|
@@ -21,6 +22,40 @@ export class InMemoryWalletRepository {
|
|
|
21
22
|
async deleteVtxos(address) {
|
|
22
23
|
this.vtxosByAddress.delete(address);
|
|
23
24
|
}
|
|
25
|
+
async getVtxosForScript(script) {
|
|
26
|
+
const allMatches = [];
|
|
27
|
+
for (const bucket of this.vtxosByAddress.values()) {
|
|
28
|
+
for (const vtxo of bucket) {
|
|
29
|
+
if (isVtxoForScript(vtxo, script)) {
|
|
30
|
+
allMatches.push(vtxo);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Dedup by outpoint (last-write-wins across address buckets)
|
|
35
|
+
return mergeByKey([], allMatches, (item) => `${item.txid}:${item.vout}`);
|
|
36
|
+
}
|
|
37
|
+
async saveVtxosForScript(key, vtxos) {
|
|
38
|
+
if (!key.address) {
|
|
39
|
+
throw new Error("InMemoryWalletRepository requires an address");
|
|
40
|
+
}
|
|
41
|
+
for (const vtxo of vtxos) {
|
|
42
|
+
if (!isVtxoForScript(vtxo, key.script)) {
|
|
43
|
+
throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return this.saveVtxos(key.address, vtxos);
|
|
47
|
+
}
|
|
48
|
+
async deleteVtxosForScript(script) {
|
|
49
|
+
for (const [address, bucket] of this.vtxosByAddress.entries()) {
|
|
50
|
+
const next = bucket.filter((v) => !isVtxoForScript(v, script));
|
|
51
|
+
if (next.length === 0) {
|
|
52
|
+
this.vtxosByAddress.delete(address);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
this.vtxosByAddress.set(address, next);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
24
59
|
async getUtxos(address) {
|
|
25
60
|
return this.utxosByAddress.get(address) ?? [];
|
|
26
61
|
}
|
|
@@ -3,6 +3,7 @@ import { closeDatabase, openDatabase } from './manager.js';
|
|
|
3
3
|
import { initDatabase } from './schema.js';
|
|
4
4
|
import { scriptFromArkAddress } from '../scriptFromAddress.js';
|
|
5
5
|
import { DEFAULT_DB_NAME } from '../../worker/browser/utils.js';
|
|
6
|
+
import { isVtxoForScript } from '../../contracts/vtxoOwnership.js';
|
|
6
7
|
/**
|
|
7
8
|
* IndexedDB-based implementation of WalletRepository.
|
|
8
9
|
*/
|
|
@@ -141,6 +142,85 @@ export class IndexedDBWalletRepository {
|
|
|
141
142
|
throw error;
|
|
142
143
|
}
|
|
143
144
|
}
|
|
145
|
+
async getVtxosForScript(script) {
|
|
146
|
+
try {
|
|
147
|
+
const db = await this.getDB();
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const transaction = db.transaction([STORE_VTXOS], "readonly");
|
|
150
|
+
const store = transaction.objectStore(STORE_VTXOS);
|
|
151
|
+
const index = store.index("script");
|
|
152
|
+
const request = index.getAll(script);
|
|
153
|
+
request.onerror = () => reject(request.error);
|
|
154
|
+
request.onsuccess = () => {
|
|
155
|
+
const results = request.result || [];
|
|
156
|
+
try {
|
|
157
|
+
// Defensive filter: only rows whose script matches.
|
|
158
|
+
const matching = results.filter((r) => r.script === script);
|
|
159
|
+
// Dedup same outpoint rows across address buckets.
|
|
160
|
+
// Work on raw rows so the address field is available
|
|
161
|
+
// for the canonicality tiebreaker.
|
|
162
|
+
const byOutpoint = new Map();
|
|
163
|
+
for (const row of matching) {
|
|
164
|
+
const outpoint = `${row.txid}:${row.vout}`;
|
|
165
|
+
const existing = byOutpoint.get(outpoint);
|
|
166
|
+
if (!existing) {
|
|
167
|
+
byOutpoint.set(outpoint, row);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (shouldReplaceVtxo(existing, row)) {
|
|
171
|
+
byOutpoint.set(outpoint, row);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
resolve(Array.from(byOutpoint.values()).map(deserializeVtxoWithBackfill));
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
reject(err);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
console.error(`Failed to get VTXOs for script ${script}:`, error);
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async saveVtxosForScript(key, vtxos) {
|
|
188
|
+
if (!key.address) {
|
|
189
|
+
throw new Error("IndexedDBWalletRepository requires an address");
|
|
190
|
+
}
|
|
191
|
+
for (const vtxo of vtxos) {
|
|
192
|
+
if (!isVtxoForScript(vtxo, key.script)) {
|
|
193
|
+
throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return this.saveVtxos(key.address, vtxos);
|
|
197
|
+
}
|
|
198
|
+
async deleteVtxosForScript(script) {
|
|
199
|
+
try {
|
|
200
|
+
const db = await this.getDB();
|
|
201
|
+
return new Promise((resolve, reject) => {
|
|
202
|
+
const transaction = db.transaction([STORE_VTXOS], "readwrite");
|
|
203
|
+
const store = transaction.objectStore(STORE_VTXOS);
|
|
204
|
+
const index = store.index("script");
|
|
205
|
+
const request = index.openCursor(IDBKeyRange.only(script));
|
|
206
|
+
request.onerror = () => reject(request.error);
|
|
207
|
+
request.onsuccess = () => {
|
|
208
|
+
const cursor = request.result;
|
|
209
|
+
if (cursor) {
|
|
210
|
+
cursor.delete();
|
|
211
|
+
cursor.continue();
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
resolve();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
console.error(`Failed to clear VTXOs for script ${script}:`, error);
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
144
224
|
async getUtxos(address) {
|
|
145
225
|
try {
|
|
146
226
|
const db = await this.getDB();
|
|
@@ -351,3 +431,40 @@ function deserializeVtxoWithBackfill(o) {
|
|
|
351
431
|
}
|
|
352
432
|
return deserializeVtxo(o);
|
|
353
433
|
}
|
|
434
|
+
function isCanonicalRow(row) {
|
|
435
|
+
try {
|
|
436
|
+
return scriptFromArkAddress(row.address) === row.script;
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function shouldReplaceVtxo(existing, incoming) {
|
|
443
|
+
const existingCanonical = isCanonicalRow(existing);
|
|
444
|
+
const incomingCanonical = isCanonicalRow(incoming);
|
|
445
|
+
if (incomingCanonical && !existingCanonical)
|
|
446
|
+
return true;
|
|
447
|
+
if (existingCanonical && !incomingCanonical)
|
|
448
|
+
return false;
|
|
449
|
+
// Tie on canonicality, check lifecycle completeness
|
|
450
|
+
const existingWeight = getLifecycleWeight(existing);
|
|
451
|
+
const incomingWeight = getLifecycleWeight(incoming);
|
|
452
|
+
if (incomingWeight > existingWeight)
|
|
453
|
+
return true;
|
|
454
|
+
if (existingWeight > incomingWeight)
|
|
455
|
+
return false;
|
|
456
|
+
// Tie on weight, stable sort by address
|
|
457
|
+
return incoming.address < existing.address;
|
|
458
|
+
}
|
|
459
|
+
function getLifecycleWeight(v) {
|
|
460
|
+
let weight = 0;
|
|
461
|
+
if (v.isSpent !== undefined)
|
|
462
|
+
weight += 1;
|
|
463
|
+
if (v.spentBy)
|
|
464
|
+
weight += 2;
|
|
465
|
+
if (v.settledBy)
|
|
466
|
+
weight += 2;
|
|
467
|
+
if (v.arkTxId)
|
|
468
|
+
weight += 2;
|
|
469
|
+
return weight;
|
|
470
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { serializeVtxo, serializeUtxo, deserializeVtxo, deserializeUtxo, serializeAssets, deserializeAssets, } from '../serialization.js';
|
|
2
2
|
import { scriptFromArkAddress } from '../scriptFromAddress.js';
|
|
3
|
+
import { isVtxoForScript } from '../../contracts/vtxoOwnership.js';
|
|
3
4
|
/**
|
|
4
5
|
* Realm-based implementation of WalletRepository.
|
|
5
6
|
*
|
|
@@ -85,6 +86,33 @@ export class RealmWalletRepository {
|
|
|
85
86
|
this.realm.delete(toDelete);
|
|
86
87
|
});
|
|
87
88
|
}
|
|
89
|
+
async getVtxosForScript(script) {
|
|
90
|
+
await this.ensureInit();
|
|
91
|
+
const results = this.realm
|
|
92
|
+
.objects("ArkVtxo")
|
|
93
|
+
.filtered("script == $0", script);
|
|
94
|
+
return [...results].map(vtxoObjectToDomain);
|
|
95
|
+
}
|
|
96
|
+
async saveVtxosForScript(key, vtxos) {
|
|
97
|
+
if (!key.address) {
|
|
98
|
+
throw new Error("RealmWalletRepository requires an address");
|
|
99
|
+
}
|
|
100
|
+
for (const vtxo of vtxos) {
|
|
101
|
+
if (!isVtxoForScript(vtxo, key.script)) {
|
|
102
|
+
throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return this.saveVtxos(key.address, vtxos);
|
|
106
|
+
}
|
|
107
|
+
async deleteVtxosForScript(script) {
|
|
108
|
+
await this.ensureInit();
|
|
109
|
+
this.realm.write(() => {
|
|
110
|
+
const toDelete = this.realm
|
|
111
|
+
.objects("ArkVtxo")
|
|
112
|
+
.filtered("script == $0", script);
|
|
113
|
+
this.realm.delete(toDelete);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
88
116
|
// ── UTXO management ────────────────────────────────────────────────
|
|
89
117
|
async getUtxos(address) {
|
|
90
118
|
await this.ensureInit();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { serializeVtxo, serializeUtxo, deserializeVtxo, deserializeUtxo, serializeAssets, deserializeAssets, } from '../serialization.js';
|
|
2
2
|
import { scriptFromArkAddress } from '../scriptFromAddress.js';
|
|
3
|
+
import { isVtxoForScript } from '../../contracts/vtxoOwnership.js';
|
|
3
4
|
/**
|
|
4
5
|
* SQLite-based implementation of WalletRepository.
|
|
5
6
|
*
|
|
@@ -242,6 +243,28 @@ export class SQLiteWalletRepository {
|
|
|
242
243
|
await this.ensureInit();
|
|
243
244
|
await this.db.run(`DELETE FROM ${this.tables.vtxos} WHERE address = ?`, [address]);
|
|
244
245
|
}
|
|
246
|
+
async getVtxosForScript(script) {
|
|
247
|
+
await this.ensureInit();
|
|
248
|
+
const rows = await this.db.all(`SELECT * FROM ${this.tables.vtxos} WHERE script = ?`, [script]);
|
|
249
|
+
return rows.map(vtxoRowToDomain);
|
|
250
|
+
}
|
|
251
|
+
async saveVtxosForScript(key, vtxos) {
|
|
252
|
+
if (!key.address) {
|
|
253
|
+
throw new Error("SQLiteWalletRepository requires an address");
|
|
254
|
+
}
|
|
255
|
+
for (const vtxo of vtxos) {
|
|
256
|
+
if (!isVtxoForScript(vtxo, key.script)) {
|
|
257
|
+
throw new Error(`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return this.saveVtxos(key.address, vtxos);
|
|
261
|
+
}
|
|
262
|
+
async deleteVtxosForScript(script) {
|
|
263
|
+
await this.ensureInit();
|
|
264
|
+
await this.db.run(`DELETE FROM ${this.tables.vtxos} WHERE script = ?`, [
|
|
265
|
+
script,
|
|
266
|
+
]);
|
|
267
|
+
}
|
|
245
268
|
// ── UTXO management ────────────────────────────────────────────────
|
|
246
269
|
async getUtxos(address) {
|
|
247
270
|
await this.ensureInit();
|
package/dist/esm/script/base.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Script, Address, p2tr, taprootListToTree, TAPROOT_UNSPENDABLE_KEY, } from "@scure/btc-signer";
|
|
2
|
-
import * as bip68 from "bip68";
|
|
3
2
|
import { TAP_LEAF_VERSION } from "@scure/btc-signer/payment.js";
|
|
4
3
|
import { PSBTOutput } from "@scure/btc-signer/psbt.js";
|
|
5
4
|
import { hex } from "@scure/base";
|
|
6
5
|
import { ArkAddress } from './address.js';
|
|
6
|
+
import { timelockToSequence } from '../utils/timelock.js';
|
|
7
7
|
import { CLTVMultisigTapscript, ConditionCSVMultisigTapscript, CSVMultisigTapscript, } from './tapscript.js';
|
|
8
8
|
export const TapTreeCoder = PSBTOutput.tapTree[2];
|
|
9
9
|
export function scriptFromTapLeafScript(leaf) {
|
|
@@ -125,19 +125,19 @@ export class VtxoScript {
|
|
|
125
125
|
const paths = [];
|
|
126
126
|
for (const leaf of this.leaves) {
|
|
127
127
|
try {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
catch (e) {
|
|
133
|
-
try {
|
|
134
|
-
const tapscript = ConditionCSVMultisigTapscript.decode(scriptFromTapLeafScript(leaf));
|
|
135
|
-
paths.push(tapscript);
|
|
128
|
+
const script = scriptFromTapLeafScript(leaf);
|
|
129
|
+
if (CSVMultisigTapscript.isScriptValid(script) === true) {
|
|
130
|
+
const tapScript = CSVMultisigTapscript.decode(script);
|
|
131
|
+
paths.push(tapScript);
|
|
136
132
|
}
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
else if (ConditionCSVMultisigTapscript.isScriptValid(script) === true) {
|
|
134
|
+
const tapScript = ConditionCSVMultisigTapscript.decode(script);
|
|
135
|
+
paths.push(tapScript);
|
|
139
136
|
}
|
|
140
137
|
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
console.debug("Failed to decode script", e);
|
|
140
|
+
}
|
|
141
141
|
}
|
|
142
142
|
return paths;
|
|
143
143
|
}
|
|
@@ -167,9 +167,7 @@ export function getSequence(tapLeafScript) {
|
|
|
167
167
|
const script = scriptWithLeafVersion.subarray(0, scriptWithLeafVersion.length - 1);
|
|
168
168
|
try {
|
|
169
169
|
const params = CSVMultisigTapscript.decode(script).params;
|
|
170
|
-
sequence =
|
|
171
|
-
? { blocks: Number(params.timelock.value) }
|
|
172
|
-
: { seconds: Number(params.timelock.value) });
|
|
170
|
+
sequence = timelockToSequence(params.timelock);
|
|
173
171
|
}
|
|
174
172
|
catch {
|
|
175
173
|
const params = CLTVMultisigTapscript.decode(script).params;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as bip68 from "bip68";
|
|
2
1
|
import { Script, ScriptNum, p2tr_ms } from "@scure/btc-signer";
|
|
3
2
|
import { hex } from "@scure/base";
|
|
3
|
+
import { sequenceToTimelock, timelockToSequence } from '../utils/timelock.js';
|
|
4
4
|
const MinimalScriptNum = ScriptNum(undefined, true);
|
|
5
5
|
export var TapscriptType;
|
|
6
6
|
(function (TapscriptType) {
|
|
@@ -234,9 +234,7 @@ export var CSVMultisigTapscript;
|
|
|
234
234
|
throw new Error(`Invalid pubkey length: expected 32, got ${pubkey.length}`);
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
|
-
const sequence = MinimalScriptNum.encode(BigInt(
|
|
238
|
-
? { blocks: Number(params.timelock.value) }
|
|
239
|
-
: { seconds: Number(params.timelock.value) })));
|
|
237
|
+
const sequence = MinimalScriptNum.encode(BigInt(timelockToSequence(params.timelock)));
|
|
240
238
|
const asm = [
|
|
241
239
|
sequence.length === 1 ? sequence[0] : sequence,
|
|
242
240
|
"CHECKSEQUENCEVERIFY",
|
|
@@ -259,17 +257,12 @@ export var CSVMultisigTapscript;
|
|
|
259
257
|
if (script.length === 0) {
|
|
260
258
|
throw new Error("Failed to decode: script is empty");
|
|
261
259
|
}
|
|
262
|
-
const
|
|
263
|
-
if (
|
|
264
|
-
throw
|
|
260
|
+
const isValid = isScriptValid(script);
|
|
261
|
+
if (isValid instanceof Error) {
|
|
262
|
+
throw isValid;
|
|
265
263
|
}
|
|
264
|
+
const asm = Script.decode(script);
|
|
266
265
|
const sequence = asm[0];
|
|
267
|
-
if (typeof sequence === "string") {
|
|
268
|
-
throw new Error("Invalid script: expected sequence number");
|
|
269
|
-
}
|
|
270
|
-
if (asm[1] !== "CHECKSEQUENCEVERIFY" || asm[2] !== "DROP") {
|
|
271
|
-
throw new Error("Invalid script: expected CHECKSEQUENCEVERIFY DROP");
|
|
272
|
-
}
|
|
273
266
|
const multisigScript = new Uint8Array(Script.encode(asm.slice(3)));
|
|
274
267
|
let multisig;
|
|
275
268
|
try {
|
|
@@ -285,10 +278,7 @@ export var CSVMultisigTapscript;
|
|
|
285
278
|
else {
|
|
286
279
|
sequenceNum = Number(MinimalScriptNum.decode(sequence));
|
|
287
280
|
}
|
|
288
|
-
const
|
|
289
|
-
const timelock = decodedTimelock.blocks !== undefined
|
|
290
|
-
? { type: "blocks", value: BigInt(decodedTimelock.blocks) }
|
|
291
|
-
: { type: "seconds", value: BigInt(decodedTimelock.seconds) };
|
|
281
|
+
const timelock = sequenceToTimelock(sequenceNum);
|
|
292
282
|
const reconstructed = encode({
|
|
293
283
|
timelock,
|
|
294
284
|
...multisig.params,
|
|
@@ -311,6 +301,21 @@ export var CSVMultisigTapscript;
|
|
|
311
301
|
return tapscript.type === TapscriptType.CSVMultisig;
|
|
312
302
|
}
|
|
313
303
|
CSVMultisigTapscript.is = is;
|
|
304
|
+
function isScriptValid(script) {
|
|
305
|
+
const asm = Script.decode(script);
|
|
306
|
+
if (asm.length < 3) {
|
|
307
|
+
return new Error(`Invalid script: too short (expected at least 3)`);
|
|
308
|
+
}
|
|
309
|
+
const sequence = asm[0];
|
|
310
|
+
if (typeof sequence === "string") {
|
|
311
|
+
return new Error("Invalid script: expected sequence number");
|
|
312
|
+
}
|
|
313
|
+
if (asm[1] !== "CHECKSEQUENCEVERIFY" || asm[2] !== "DROP") {
|
|
314
|
+
return new Error("Invalid script: expected CHECKSEQUENCEVERIFY DROP");
|
|
315
|
+
}
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
CSVMultisigTapscript.isScriptValid = isScriptValid;
|
|
314
319
|
})(CSVMultisigTapscript || (CSVMultisigTapscript = {}));
|
|
315
320
|
/**
|
|
316
321
|
* Combines a condition script with an exit closure. The resulting script requires
|
|
@@ -345,18 +350,14 @@ export var ConditionCSVMultisigTapscript;
|
|
|
345
350
|
if (script.length === 0) {
|
|
346
351
|
throw new Error("Failed to decode: script is empty");
|
|
347
352
|
}
|
|
348
|
-
const
|
|
349
|
-
if (
|
|
350
|
-
throw
|
|
351
|
-
}
|
|
352
|
-
let verifyIndex = -1;
|
|
353
|
-
for (let i = asm.length - 1; i >= 0; i--) {
|
|
354
|
-
if (asm[i] === "VERIFY") {
|
|
355
|
-
verifyIndex = i;
|
|
356
|
-
}
|
|
353
|
+
const isValid = isScriptValid(script);
|
|
354
|
+
if (isValid instanceof Error) {
|
|
355
|
+
throw isValid;
|
|
357
356
|
}
|
|
357
|
+
const asm = Script.decode(script);
|
|
358
|
+
let verifyIndex = getVerifyIndex(asm);
|
|
358
359
|
if (verifyIndex === -1) {
|
|
359
|
-
throw
|
|
360
|
+
throw Error("Invalid script: missing VERIFY operation");
|
|
360
361
|
}
|
|
361
362
|
const conditionScript = new Uint8Array(Script.encode(asm.slice(0, verifyIndex)));
|
|
362
363
|
const csvMultisigScript = new Uint8Array(Script.encode(asm.slice(verifyIndex + 1)));
|
|
@@ -389,6 +390,28 @@ export var ConditionCSVMultisigTapscript;
|
|
|
389
390
|
return tapscript.type === TapscriptType.ConditionCSVMultisig;
|
|
390
391
|
}
|
|
391
392
|
ConditionCSVMultisigTapscript.is = is;
|
|
393
|
+
function getVerifyIndex(asm) {
|
|
394
|
+
let verifyIndex = -1;
|
|
395
|
+
for (let i = asm.length - 1; i >= 0; i--) {
|
|
396
|
+
if (asm[i] === "VERIFY") {
|
|
397
|
+
verifyIndex = i;
|
|
398
|
+
return verifyIndex;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return verifyIndex;
|
|
402
|
+
}
|
|
403
|
+
function isScriptValid(script) {
|
|
404
|
+
const asm = Script.decode(script);
|
|
405
|
+
if (asm.length < 1) {
|
|
406
|
+
return new Error(`Invalid script: too short (expected at least 1)`);
|
|
407
|
+
}
|
|
408
|
+
let verifyIndex = getVerifyIndex(asm);
|
|
409
|
+
if (verifyIndex === -1) {
|
|
410
|
+
return new Error("Invalid script: missing VERIFY operation");
|
|
411
|
+
}
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
ConditionCSVMultisigTapscript.isScriptValid = isScriptValid;
|
|
392
415
|
})(ConditionCSVMultisigTapscript || (ConditionCSVMultisigTapscript = {}));
|
|
393
416
|
/**
|
|
394
417
|
* Combines a condition script with a forfeit closure. The resulting script requires
|
|
@@ -423,18 +446,14 @@ export var ConditionMultisigTapscript;
|
|
|
423
446
|
if (script.length === 0) {
|
|
424
447
|
throw new Error("Failed to decode: script is empty");
|
|
425
448
|
}
|
|
426
|
-
const
|
|
427
|
-
if (
|
|
428
|
-
throw
|
|
429
|
-
}
|
|
430
|
-
let verifyIndex = -1;
|
|
431
|
-
for (let i = asm.length - 1; i >= 0; i--) {
|
|
432
|
-
if (asm[i] === "VERIFY") {
|
|
433
|
-
verifyIndex = i;
|
|
434
|
-
}
|
|
449
|
+
const isValid = isScriptValid(script);
|
|
450
|
+
if (isValid instanceof Error) {
|
|
451
|
+
throw isValid;
|
|
435
452
|
}
|
|
453
|
+
const asm = Script.decode(script);
|
|
454
|
+
let verifyIndex = getVerifyIndex(asm);
|
|
436
455
|
if (verifyIndex === -1) {
|
|
437
|
-
throw
|
|
456
|
+
throw Error("Invalid script: missing VERIFY operation");
|
|
438
457
|
}
|
|
439
458
|
const conditionScript = new Uint8Array(Script.encode(asm.slice(0, verifyIndex)));
|
|
440
459
|
const multisigScript = new Uint8Array(Script.encode(asm.slice(verifyIndex + 1)));
|
|
@@ -467,6 +486,28 @@ export var ConditionMultisigTapscript;
|
|
|
467
486
|
return tapscript.type === TapscriptType.ConditionMultisig;
|
|
468
487
|
}
|
|
469
488
|
ConditionMultisigTapscript.is = is;
|
|
489
|
+
function getVerifyIndex(asm) {
|
|
490
|
+
let verifyIndex = -1;
|
|
491
|
+
for (let i = asm.length - 1; i >= 0; i--) {
|
|
492
|
+
if (asm[i] === "VERIFY") {
|
|
493
|
+
verifyIndex = i;
|
|
494
|
+
return verifyIndex;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return verifyIndex;
|
|
498
|
+
}
|
|
499
|
+
function isScriptValid(script) {
|
|
500
|
+
const asm = Script.decode(script);
|
|
501
|
+
if (asm.length < 1) {
|
|
502
|
+
return new Error(`Invalid script: too short (expected at least 1)`);
|
|
503
|
+
}
|
|
504
|
+
let verifyIndex = getVerifyIndex(asm);
|
|
505
|
+
if (verifyIndex === -1) {
|
|
506
|
+
return new Error("Invalid script: missing VERIFY operation");
|
|
507
|
+
}
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
ConditionMultisigTapscript.isScriptValid = isScriptValid;
|
|
470
511
|
})(ConditionMultisigTapscript || (ConditionMultisigTapscript = {}));
|
|
471
512
|
/**
|
|
472
513
|
* Implements an absolute timelock (CLTV) script combined with a forfeit closure.
|
|
@@ -507,10 +548,11 @@ export var CLTVMultisigTapscript;
|
|
|
507
548
|
if (script.length === 0) {
|
|
508
549
|
throw new Error("Failed to decode: script is empty");
|
|
509
550
|
}
|
|
510
|
-
const
|
|
511
|
-
if (
|
|
512
|
-
throw
|
|
551
|
+
const isValid = isScriptValid(script);
|
|
552
|
+
if (isValid instanceof Error) {
|
|
553
|
+
throw isValid;
|
|
513
554
|
}
|
|
555
|
+
const asm = Script.decode(script);
|
|
514
556
|
const locktime = asm[0];
|
|
515
557
|
if (typeof locktime === "string") {
|
|
516
558
|
throw new Error("Invalid script: expected locktime number");
|
|
@@ -555,4 +597,19 @@ export var CLTVMultisigTapscript;
|
|
|
555
597
|
return tapscript.type === TapscriptType.CLTVMultisig;
|
|
556
598
|
}
|
|
557
599
|
CLTVMultisigTapscript.is = is;
|
|
600
|
+
function isScriptValid(script) {
|
|
601
|
+
const asm = Script.decode(script);
|
|
602
|
+
if (asm.length < 3) {
|
|
603
|
+
return new Error(`Invalid script: too short (expected at least 3)`);
|
|
604
|
+
}
|
|
605
|
+
const locktime = asm[0];
|
|
606
|
+
if (typeof locktime === "string") {
|
|
607
|
+
return new Error("Invalid script: expected locktime as number or bytes");
|
|
608
|
+
}
|
|
609
|
+
if (asm[1] !== "CHECKLOCKTIMEVERIFY" || asm[2] !== "DROP") {
|
|
610
|
+
return new Error("Invalid script: expected CHECKLOCKTIMEVERIFY DROP");
|
|
611
|
+
}
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
CLTVMultisigTapscript.isScriptValid = isScriptValid;
|
|
558
615
|
})(CLTVMultisigTapscript || (CLTVMultisigTapscript = {}));
|