@arkade-os/sdk 0.3.3 → 0.3.5
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 +7 -1
- package/dist/cjs/identity/singleKey.js +2 -2
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/providers/ark.js +9 -2
- package/dist/cjs/repositories/contractRepository.js +9 -38
- package/dist/cjs/repositories/walletRepository.js +25 -74
- package/dist/cjs/utils/arkTransaction.js +29 -0
- package/dist/cjs/utils/transactionHistory.js +29 -13
- package/dist/cjs/wallet/ramps.js +7 -1
- package/dist/cjs/wallet/serviceWorker/worker.js +0 -1
- package/dist/cjs/wallet/wallet.js +98 -12
- package/dist/esm/identity/singleKey.js +3 -3
- package/dist/esm/index.js +2 -2
- package/dist/esm/providers/ark.js +9 -2
- package/dist/esm/repositories/contractRepository.js +9 -38
- package/dist/esm/repositories/walletRepository.js +25 -74
- package/dist/esm/utils/arkTransaction.js +29 -1
- package/dist/esm/utils/transactionHistory.js +29 -13
- package/dist/esm/wallet/ramps.js +7 -1
- package/dist/esm/wallet/serviceWorker/worker.js +0 -1
- package/dist/esm/wallet/wallet.js +98 -12
- package/dist/types/index.d.ts +4 -4
- package/dist/types/providers/ark.d.ts +2 -2
- package/dist/types/repositories/contractRepository.d.ts +0 -1
- package/dist/types/repositories/walletRepository.d.ts +0 -1
- package/dist/types/utils/arkTransaction.d.ts +6 -0
- package/dist/types/wallet/ramps.d.ts +3 -2
- package/dist/types/wallet/wallet.d.ts +4 -3
- package/package.json +2 -2
|
@@ -146,12 +146,29 @@ class Wallet {
|
|
|
146
146
|
const esploraUrl = config.esploraUrl || onchain_1.ESPLORA_URL[info.network];
|
|
147
147
|
// Use provided onchainProvider instance or create a new one
|
|
148
148
|
const onchainProvider = config.onchainProvider || new onchain_1.EsploraProvider(esploraUrl);
|
|
149
|
-
//
|
|
150
|
-
|
|
149
|
+
// validate unilateral exit timelock passed in config if any
|
|
150
|
+
if (config.exitTimelock) {
|
|
151
|
+
const { value, type } = config.exitTimelock;
|
|
152
|
+
if ((value < 512n && type !== "blocks") ||
|
|
153
|
+
(value >= 512n && type !== "seconds")) {
|
|
154
|
+
throw new Error("invalid exitTimelock");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// create unilateral exit timelock
|
|
158
|
+
const exitTimelock = config.exitTimelock ?? {
|
|
151
159
|
value: info.unilateralExitDelay,
|
|
152
160
|
type: info.unilateralExitDelay < 512n ? "blocks" : "seconds",
|
|
153
161
|
};
|
|
154
|
-
|
|
162
|
+
// validate boarding timelock passed in config if any
|
|
163
|
+
if (config.boardingTimelock) {
|
|
164
|
+
const { value, type } = config.boardingTimelock;
|
|
165
|
+
if ((value < 512n && type !== "blocks") ||
|
|
166
|
+
(value >= 512n && type !== "seconds")) {
|
|
167
|
+
throw new Error("invalid boardingTimelock");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// create boarding timelock
|
|
171
|
+
const boardingTimelock = config.boardingTimelock ?? {
|
|
155
172
|
value: info.boardingExitDelay,
|
|
156
173
|
type: info.boardingExitDelay < 512n ? "blocks" : "seconds",
|
|
157
174
|
};
|
|
@@ -414,14 +431,13 @@ class Wallet {
|
|
|
414
431
|
});
|
|
415
432
|
}
|
|
416
433
|
const tapTree = this.offchainTapscript.encode();
|
|
417
|
-
|
|
434
|
+
const offchainTx = (0, arkTransaction_1.buildOffchainTx)(selected.inputs.map((input) => ({
|
|
418
435
|
...input,
|
|
419
436
|
tapLeafScript: selectedLeaf,
|
|
420
437
|
tapTree,
|
|
421
438
|
})), outputs, this.serverUnrollScript);
|
|
422
439
|
const signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
|
|
423
440
|
const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base_1.base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base_1.base64.encode(c.toPSBT())));
|
|
424
|
-
// TODO persist final virtual tx and checkpoints to repository
|
|
425
441
|
// sign the checkpoints
|
|
426
442
|
const finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
|
|
427
443
|
const tx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(c));
|
|
@@ -429,7 +445,78 @@ class Wallet {
|
|
|
429
445
|
return base_1.base64.encode(signedCheckpoint.toPSBT());
|
|
430
446
|
}));
|
|
431
447
|
await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
|
|
432
|
-
|
|
448
|
+
try {
|
|
449
|
+
// mark VTXOs as spent and optionally add the change VTXO
|
|
450
|
+
const spentVtxos = [];
|
|
451
|
+
const commitmentTxIds = new Set();
|
|
452
|
+
let batchExpiry = Number.MAX_SAFE_INTEGER;
|
|
453
|
+
for (const [inputIndex, input] of selected.inputs.entries()) {
|
|
454
|
+
const vtxo = (0, utils_1.extendVirtualCoin)(this, input);
|
|
455
|
+
const checkpointB64 = signedCheckpointTxs[inputIndex];
|
|
456
|
+
const checkpoint = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(checkpointB64));
|
|
457
|
+
spentVtxos.push({
|
|
458
|
+
...vtxo,
|
|
459
|
+
virtualStatus: { ...vtxo.virtualStatus, state: "spent" },
|
|
460
|
+
spentBy: checkpoint.id,
|
|
461
|
+
arkTxId: arkTxid,
|
|
462
|
+
isSpent: true,
|
|
463
|
+
});
|
|
464
|
+
if (vtxo.virtualStatus.commitmentTxIds) {
|
|
465
|
+
for (const commitmentTxId of vtxo.virtualStatus
|
|
466
|
+
.commitmentTxIds) {
|
|
467
|
+
commitmentTxIds.add(commitmentTxId);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (vtxo.virtualStatus.batchExpiry) {
|
|
471
|
+
batchExpiry = Math.min(batchExpiry, vtxo.virtualStatus.batchExpiry);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const createdAt = Date.now();
|
|
475
|
+
const addr = this.arkAddress.encode();
|
|
476
|
+
if (selected.changeAmount > 0n &&
|
|
477
|
+
batchExpiry !== Number.MAX_SAFE_INTEGER) {
|
|
478
|
+
const changeVtxo = {
|
|
479
|
+
txid: arkTxid,
|
|
480
|
+
vout: outputs.length - 1,
|
|
481
|
+
createdAt: new Date(createdAt),
|
|
482
|
+
forfeitTapLeafScript: this.offchainTapscript.forfeit(),
|
|
483
|
+
intentTapLeafScript: this.offchainTapscript.exit(),
|
|
484
|
+
isUnrolled: false,
|
|
485
|
+
isSpent: false,
|
|
486
|
+
tapTree: this.offchainTapscript.encode(),
|
|
487
|
+
value: Number(selected.changeAmount),
|
|
488
|
+
virtualStatus: {
|
|
489
|
+
state: "preconfirmed",
|
|
490
|
+
commitmentTxIds: Array.from(commitmentTxIds),
|
|
491
|
+
batchExpiry,
|
|
492
|
+
},
|
|
493
|
+
status: {
|
|
494
|
+
confirmed: false,
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
await this.walletRepository.saveVtxos(addr, [changeVtxo]);
|
|
498
|
+
}
|
|
499
|
+
await this.walletRepository.saveVtxos(addr, spentVtxos);
|
|
500
|
+
await this.walletRepository.saveTransactions(addr, [
|
|
501
|
+
{
|
|
502
|
+
key: {
|
|
503
|
+
boardingTxid: "",
|
|
504
|
+
commitmentTxid: "",
|
|
505
|
+
arkTxid: arkTxid,
|
|
506
|
+
},
|
|
507
|
+
amount: params.amount,
|
|
508
|
+
type: _1.TxType.TxSent,
|
|
509
|
+
settled: false,
|
|
510
|
+
createdAt: Date.now(),
|
|
511
|
+
},
|
|
512
|
+
]);
|
|
513
|
+
}
|
|
514
|
+
catch (e) {
|
|
515
|
+
console.warn("error saving offchain tx to repository", e);
|
|
516
|
+
}
|
|
517
|
+
finally {
|
|
518
|
+
return arkTxid;
|
|
519
|
+
}
|
|
433
520
|
}
|
|
434
521
|
async settle(params, eventCallback) {
|
|
435
522
|
if (params?.inputs) {
|
|
@@ -702,7 +789,8 @@ class Wallet {
|
|
|
702
789
|
(async () => {
|
|
703
790
|
try {
|
|
704
791
|
for await (const update of subscription) {
|
|
705
|
-
if (update.newVtxos?.length > 0
|
|
792
|
+
if (update.newVtxos?.length > 0 ||
|
|
793
|
+
update.spentVtxos?.length > 0) {
|
|
706
794
|
eventCallback({
|
|
707
795
|
type: "vtxo",
|
|
708
796
|
newVtxos: update.newVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
|
|
@@ -869,13 +957,12 @@ class Wallet {
|
|
|
869
957
|
}
|
|
870
958
|
}
|
|
871
959
|
async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys) {
|
|
872
|
-
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
873
960
|
const inputs = this.prepareIntentProofInputs(coins);
|
|
874
961
|
const message = {
|
|
875
962
|
type: "register",
|
|
876
963
|
onchain_output_indexes: onchainOutputsIndexes,
|
|
877
|
-
valid_at:
|
|
878
|
-
expire_at:
|
|
964
|
+
valid_at: 0,
|
|
965
|
+
expire_at: 0,
|
|
879
966
|
cosigners_public_keys: cosignerPubKeys,
|
|
880
967
|
};
|
|
881
968
|
const encodedMessage = JSON.stringify(message, null, 0);
|
|
@@ -887,11 +974,10 @@ class Wallet {
|
|
|
887
974
|
};
|
|
888
975
|
}
|
|
889
976
|
async makeDeleteIntentSignature(coins) {
|
|
890
|
-
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
891
977
|
const inputs = this.prepareIntentProofInputs(coins);
|
|
892
978
|
const message = {
|
|
893
979
|
type: "delete",
|
|
894
|
-
expire_at:
|
|
980
|
+
expire_at: 0,
|
|
895
981
|
};
|
|
896
982
|
const encodedMessage = JSON.stringify(message, null, 0);
|
|
897
983
|
const proof = intent_1.Intent.create(encodedMessage, inputs, []);
|
|
@@ -2,7 +2,7 @@ import { pubECDSA, pubSchnorr, randomPrivateKeyBytes, } from "@scure/btc-signer/
|
|
|
2
2
|
import { SigHash } from "@scure/btc-signer";
|
|
3
3
|
import { hex } from "@scure/base";
|
|
4
4
|
import { TreeSignerSession } from '../tree/signingSession.js';
|
|
5
|
-
import { schnorr,
|
|
5
|
+
import { schnorr, signAsync } from "@noble/secp256k1";
|
|
6
6
|
const ALL_SIGHASH = Object.values(SigHash).filter((x) => typeof x === "number");
|
|
7
7
|
/**
|
|
8
8
|
* In-memory single key implementation for Bitcoin transaction signing.
|
|
@@ -80,7 +80,7 @@ export class SingleKey {
|
|
|
80
80
|
}
|
|
81
81
|
async signMessage(message, signatureType = "schnorr") {
|
|
82
82
|
if (signatureType === "ecdsa")
|
|
83
|
-
return
|
|
84
|
-
return schnorr.
|
|
83
|
+
return signAsync(message, this.key, { prehash: false });
|
|
84
|
+
return schnorr.signAsync(message, this.key);
|
|
85
85
|
}
|
|
86
86
|
}
|
package/dist/esm/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { Response } from './wallet/serviceWorker/response.js';
|
|
|
18
18
|
import { ESPLORA_URL, EsploraProvider, } from './providers/onchain.js';
|
|
19
19
|
import { RestArkProvider, SettlementEventType, } from './providers/ark.js';
|
|
20
20
|
import { CLTVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CSVMultisigTapscript, decodeTapscript, MultisigTapscript, } from './script/tapscript.js';
|
|
21
|
-
import { hasBoardingTxExpired, buildOffchainTx, verifyTapscriptSignatures, } from './utils/arkTransaction.js';
|
|
21
|
+
import { hasBoardingTxExpired, buildOffchainTx, verifyTapscriptSignatures, combineTapscriptSigs, } from './utils/arkTransaction.js';
|
|
22
22
|
import { VtxoTaprootTree, ConditionWitness, getArkPsbtFields, setArkPsbtField, ArkPsbtFieldKey, ArkPsbtFieldKeyType, CosignerPublicKey, VtxoTreeExpiry, } from './utils/unknownFields.js';
|
|
23
23
|
import { Intent } from './intent/index.js';
|
|
24
24
|
import { ArkNote } from './arknote/index.js';
|
|
@@ -45,7 +45,7 @@ decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTa
|
|
|
45
45
|
// Ark PSBT fields
|
|
46
46
|
ArkPsbtFieldKey, ArkPsbtFieldKeyType, setArkPsbtField, getArkPsbtFields, CosignerPublicKey, VtxoTreeExpiry, VtxoTaprootTree, ConditionWitness,
|
|
47
47
|
// Utils
|
|
48
|
-
buildOffchainTx, verifyTapscriptSignatures, waitForIncomingFunds, hasBoardingTxExpired,
|
|
48
|
+
buildOffchainTx, verifyTapscriptSignatures, waitForIncomingFunds, hasBoardingTxExpired, combineTapscriptSigs,
|
|
49
49
|
// Arknote
|
|
50
50
|
ArkNote,
|
|
51
51
|
// Network
|
|
@@ -42,7 +42,14 @@ export class RestArkProvider {
|
|
|
42
42
|
})) ?? [],
|
|
43
43
|
digest: fromServer.digest ?? "",
|
|
44
44
|
dust: BigInt(fromServer.dust ?? 0),
|
|
45
|
-
fees:
|
|
45
|
+
fees: {
|
|
46
|
+
intentFee: {
|
|
47
|
+
...fromServer.fees?.intentFee,
|
|
48
|
+
onchainInput: BigInt(fromServer.fees?.intentFee?.onchainInput ?? 0),
|
|
49
|
+
onchainOutput: BigInt(fromServer.fees?.intentFee?.onchainOutput ?? 0),
|
|
50
|
+
},
|
|
51
|
+
txFeeRate: fromServer?.fees?.txFeeRate ?? "",
|
|
52
|
+
},
|
|
46
53
|
forfeitAddress: fromServer.forfeitAddress ?? "",
|
|
47
54
|
forfeitPubkey: fromServer.forfeitPubkey ?? "",
|
|
48
55
|
network: fromServer.network ?? "",
|
|
@@ -135,7 +142,7 @@ export class RestArkProvider {
|
|
|
135
142
|
"Content-Type": "application/json",
|
|
136
143
|
},
|
|
137
144
|
body: JSON.stringify({
|
|
138
|
-
|
|
145
|
+
intent: {
|
|
139
146
|
proof: intent.proof,
|
|
140
147
|
message: intent.message,
|
|
141
148
|
},
|
|
@@ -1,19 +1,15 @@
|
|
|
1
|
+
const getContractStorageKey = (id, key) => `contract:${id}:${key}`;
|
|
2
|
+
const getCollectionStorageKey = (type) => `collection:${type}`;
|
|
1
3
|
export class ContractRepositoryImpl {
|
|
2
4
|
constructor(storage) {
|
|
3
|
-
this.cache = new Map();
|
|
4
5
|
this.storage = storage;
|
|
5
6
|
}
|
|
6
7
|
async getContractData(contractId, key) {
|
|
7
|
-
const
|
|
8
|
-
const cached = this.cache.get(storageKey);
|
|
9
|
-
if (cached !== undefined)
|
|
10
|
-
return cached;
|
|
11
|
-
const stored = await this.storage.getItem(storageKey);
|
|
8
|
+
const stored = await this.storage.getItem(getContractStorageKey(contractId, key));
|
|
12
9
|
if (!stored)
|
|
13
10
|
return null;
|
|
14
11
|
try {
|
|
15
12
|
const data = JSON.parse(stored);
|
|
16
|
-
this.cache.set(storageKey, data);
|
|
17
13
|
return data;
|
|
18
14
|
}
|
|
19
15
|
catch (error) {
|
|
@@ -22,49 +18,33 @@ export class ContractRepositoryImpl {
|
|
|
22
18
|
}
|
|
23
19
|
}
|
|
24
20
|
async setContractData(contractId, key, data) {
|
|
25
|
-
const storageKey = `contract:${contractId}:${key}`;
|
|
26
21
|
try {
|
|
27
|
-
|
|
28
|
-
await this.storage.setItem(storageKey, JSON.stringify(data));
|
|
29
|
-
this.cache.set(storageKey, data);
|
|
22
|
+
await this.storage.setItem(getContractStorageKey(contractId, key), JSON.stringify(data));
|
|
30
23
|
}
|
|
31
24
|
catch (error) {
|
|
32
|
-
// Storage operation failed, cache remains unchanged
|
|
33
25
|
console.error(`Failed to persist contract data for ${contractId}:${key}:`, error);
|
|
34
26
|
throw error; // Rethrow to notify caller of failure
|
|
35
27
|
}
|
|
36
28
|
}
|
|
37
29
|
async deleteContractData(contractId, key) {
|
|
38
|
-
const storageKey = `contract:${contractId}:${key}`;
|
|
39
30
|
try {
|
|
40
|
-
|
|
41
|
-
await this.storage.removeItem(storageKey);
|
|
42
|
-
this.cache.delete(storageKey);
|
|
31
|
+
await this.storage.removeItem(getContractStorageKey(contractId, key));
|
|
43
32
|
}
|
|
44
33
|
catch (error) {
|
|
45
|
-
// Storage operation failed, cache remains unchanged
|
|
46
34
|
console.error(`Failed to remove contract data for ${contractId}:${key}:`, error);
|
|
47
35
|
throw error; // Rethrow to notify caller of failure
|
|
48
36
|
}
|
|
49
37
|
}
|
|
50
38
|
async getContractCollection(contractType) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
if (cached !== undefined)
|
|
54
|
-
return cached;
|
|
55
|
-
const stored = await this.storage.getItem(storageKey);
|
|
56
|
-
if (!stored) {
|
|
57
|
-
this.cache.set(storageKey, []);
|
|
39
|
+
const stored = await this.storage.getItem(getCollectionStorageKey(contractType));
|
|
40
|
+
if (!stored)
|
|
58
41
|
return [];
|
|
59
|
-
}
|
|
60
42
|
try {
|
|
61
43
|
const collection = JSON.parse(stored);
|
|
62
|
-
this.cache.set(storageKey, collection);
|
|
63
44
|
return collection;
|
|
64
45
|
}
|
|
65
46
|
catch (error) {
|
|
66
47
|
console.error(`Failed to parse contract collection ${contractType}:`, error);
|
|
67
|
-
this.cache.set(storageKey, []);
|
|
68
48
|
return [];
|
|
69
49
|
}
|
|
70
50
|
}
|
|
@@ -91,14 +71,10 @@ export class ContractRepositoryImpl {
|
|
|
91
71
|
// Add new item
|
|
92
72
|
newCollection = [...collection, item];
|
|
93
73
|
}
|
|
94
|
-
const storageKey = `collection:${contractType}`;
|
|
95
74
|
try {
|
|
96
|
-
|
|
97
|
-
await this.storage.setItem(storageKey, JSON.stringify(newCollection));
|
|
98
|
-
this.cache.set(storageKey, newCollection);
|
|
75
|
+
await this.storage.setItem(getCollectionStorageKey(contractType), JSON.stringify(newCollection));
|
|
99
76
|
}
|
|
100
77
|
catch (error) {
|
|
101
|
-
// Storage operation failed, cache remains unchanged
|
|
102
78
|
console.error(`Failed to persist contract collection ${contractType}:`, error);
|
|
103
79
|
throw error; // Rethrow to notify caller of failure
|
|
104
80
|
}
|
|
@@ -111,20 +87,15 @@ export class ContractRepositoryImpl {
|
|
|
111
87
|
const collection = await this.getContractCollection(contractType);
|
|
112
88
|
// Build new collection without the specified item
|
|
113
89
|
const filtered = collection.filter((item) => item[idField] !== id);
|
|
114
|
-
const storageKey = `collection:${contractType}`;
|
|
115
90
|
try {
|
|
116
|
-
|
|
117
|
-
await this.storage.setItem(storageKey, JSON.stringify(filtered));
|
|
118
|
-
this.cache.set(storageKey, filtered);
|
|
91
|
+
await this.storage.setItem(getCollectionStorageKey(contractType), JSON.stringify(filtered));
|
|
119
92
|
}
|
|
120
93
|
catch (error) {
|
|
121
|
-
// Storage operation failed, cache remains unchanged
|
|
122
94
|
console.error(`Failed to persist contract collection removal for ${contractType}:`, error);
|
|
123
95
|
throw error; // Rethrow to notify caller of failure
|
|
124
96
|
}
|
|
125
97
|
}
|
|
126
98
|
async clearContractData() {
|
|
127
99
|
await this.storage.clear();
|
|
128
|
-
this.cache.clear();
|
|
129
100
|
}
|
|
130
101
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { hex } from "@scure/base";
|
|
2
2
|
import { TaprootControlBlock } from "@scure/btc-signer";
|
|
3
|
+
const getVtxosStorageKey = (address) => `vtxos:${address}`;
|
|
4
|
+
const getUtxosStorageKey = (address) => `utxos:${address}`;
|
|
5
|
+
const getTransactionsStorageKey = (address) => `tx:${address}`;
|
|
6
|
+
const walletStateStorageKey = "wallet:state";
|
|
3
7
|
// Utility functions for (de)serializing complex structures
|
|
4
8
|
const toHex = (b) => (b ? hex.encode(b) : undefined);
|
|
5
9
|
const fromHex = (h) => h ? hex.decode(h) : undefined;
|
|
@@ -44,33 +48,17 @@ const deserializeUtxo = (o) => ({
|
|
|
44
48
|
export class WalletRepositoryImpl {
|
|
45
49
|
constructor(storage) {
|
|
46
50
|
this.storage = storage;
|
|
47
|
-
this.cache = {
|
|
48
|
-
vtxos: new Map(),
|
|
49
|
-
utxos: new Map(),
|
|
50
|
-
transactions: new Map(),
|
|
51
|
-
walletState: null,
|
|
52
|
-
initialized: new Set(),
|
|
53
|
-
};
|
|
54
51
|
}
|
|
55
52
|
async getVtxos(address) {
|
|
56
|
-
const
|
|
57
|
-
if (
|
|
58
|
-
return this.cache.vtxos.get(address);
|
|
59
|
-
}
|
|
60
|
-
const stored = await this.storage.getItem(cacheKey);
|
|
61
|
-
if (!stored) {
|
|
62
|
-
this.cache.vtxos.set(address, []);
|
|
53
|
+
const stored = await this.storage.getItem(getVtxosStorageKey(address));
|
|
54
|
+
if (!stored)
|
|
63
55
|
return [];
|
|
64
|
-
}
|
|
65
56
|
try {
|
|
66
57
|
const parsed = JSON.parse(stored);
|
|
67
|
-
|
|
68
|
-
this.cache.vtxos.set(address, vtxos.slice());
|
|
69
|
-
return vtxos.slice();
|
|
58
|
+
return parsed.map(deserializeVtxo);
|
|
70
59
|
}
|
|
71
60
|
catch (error) {
|
|
72
61
|
console.error(`Failed to parse VTXOs for address ${address}:`, error);
|
|
73
|
-
this.cache.vtxos.set(address, []);
|
|
74
62
|
return [];
|
|
75
63
|
}
|
|
76
64
|
}
|
|
@@ -85,39 +73,27 @@ export class WalletRepositoryImpl {
|
|
|
85
73
|
storedVtxos.push(vtxo);
|
|
86
74
|
}
|
|
87
75
|
}
|
|
88
|
-
this.
|
|
89
|
-
await this.storage.setItem(`vtxos:${address}`, JSON.stringify(storedVtxos.map(serializeVtxo)));
|
|
76
|
+
await this.storage.setItem(getVtxosStorageKey(address), JSON.stringify(storedVtxos.map(serializeVtxo)));
|
|
90
77
|
}
|
|
91
78
|
async removeVtxo(address, vtxoId) {
|
|
92
79
|
const vtxos = await this.getVtxos(address);
|
|
93
80
|
const [txid, vout] = vtxoId.split(":");
|
|
94
81
|
const filtered = vtxos.filter((v) => !(v.txid === txid && v.vout === parseInt(vout, 10)));
|
|
95
|
-
this.
|
|
96
|
-
await this.storage.setItem(`vtxos:${address}`, JSON.stringify(filtered.map(serializeVtxo)));
|
|
82
|
+
await this.storage.setItem(getVtxosStorageKey(address), JSON.stringify(filtered.map(serializeVtxo)));
|
|
97
83
|
}
|
|
98
84
|
async clearVtxos(address) {
|
|
99
|
-
this.
|
|
100
|
-
await this.storage.removeItem(`vtxos:${address}`);
|
|
85
|
+
await this.storage.removeItem(getVtxosStorageKey(address));
|
|
101
86
|
}
|
|
102
87
|
async getUtxos(address) {
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
105
|
-
return this.cache.utxos.get(address);
|
|
106
|
-
}
|
|
107
|
-
const stored = await this.storage.getItem(cacheKey);
|
|
108
|
-
if (!stored) {
|
|
109
|
-
this.cache.utxos.set(address, []);
|
|
88
|
+
const stored = await this.storage.getItem(getUtxosStorageKey(address));
|
|
89
|
+
if (!stored)
|
|
110
90
|
return [];
|
|
111
|
-
}
|
|
112
91
|
try {
|
|
113
92
|
const parsed = JSON.parse(stored);
|
|
114
|
-
|
|
115
|
-
this.cache.utxos.set(address, utxos.slice());
|
|
116
|
-
return utxos.slice();
|
|
93
|
+
return parsed.map(deserializeUtxo);
|
|
117
94
|
}
|
|
118
95
|
catch (error) {
|
|
119
96
|
console.error(`Failed to parse UTXOs for address ${address}:`, error);
|
|
120
|
-
this.cache.utxos.set(address, []);
|
|
121
97
|
return [];
|
|
122
98
|
}
|
|
123
99
|
}
|
|
@@ -132,38 +108,27 @@ export class WalletRepositoryImpl {
|
|
|
132
108
|
storedUtxos.push(utxo);
|
|
133
109
|
}
|
|
134
110
|
});
|
|
135
|
-
this.
|
|
136
|
-
await this.storage.setItem(`utxos:${address}`, JSON.stringify(storedUtxos.map(serializeUtxo)));
|
|
111
|
+
await this.storage.setItem(getUtxosStorageKey(address), JSON.stringify(storedUtxos.map(serializeUtxo)));
|
|
137
112
|
}
|
|
138
113
|
async removeUtxo(address, utxoId) {
|
|
139
114
|
const utxos = await this.getUtxos(address);
|
|
140
115
|
const [txid, vout] = utxoId.split(":");
|
|
141
116
|
const filtered = utxos.filter((v) => !(v.txid === txid && v.vout === parseInt(vout, 10)));
|
|
142
|
-
this.
|
|
143
|
-
await this.storage.setItem(`utxos:${address}`, JSON.stringify(filtered.map(serializeUtxo)));
|
|
117
|
+
await this.storage.setItem(getUtxosStorageKey(address), JSON.stringify(filtered.map(serializeUtxo)));
|
|
144
118
|
}
|
|
145
119
|
async clearUtxos(address) {
|
|
146
|
-
this.
|
|
147
|
-
await this.storage.removeItem(`utxos:${address}`);
|
|
120
|
+
await this.storage.removeItem(getUtxosStorageKey(address));
|
|
148
121
|
}
|
|
149
122
|
async getTransactionHistory(address) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
const stored = await this.storage.getItem(cacheKey);
|
|
155
|
-
if (!stored) {
|
|
156
|
-
this.cache.transactions.set(address, []);
|
|
123
|
+
const storageKey = getTransactionsStorageKey(address);
|
|
124
|
+
const stored = await this.storage.getItem(storageKey);
|
|
125
|
+
if (!stored)
|
|
157
126
|
return [];
|
|
158
|
-
}
|
|
159
127
|
try {
|
|
160
|
-
|
|
161
|
-
this.cache.transactions.set(address, transactions);
|
|
162
|
-
return transactions.slice();
|
|
128
|
+
return JSON.parse(stored);
|
|
163
129
|
}
|
|
164
130
|
catch (error) {
|
|
165
131
|
console.error(`Failed to parse transactions for address ${address}:`, error);
|
|
166
|
-
this.cache.transactions.set(address, []);
|
|
167
132
|
return [];
|
|
168
133
|
}
|
|
169
134
|
}
|
|
@@ -178,39 +143,25 @@ export class WalletRepositoryImpl {
|
|
|
178
143
|
storedTransactions.push(tx);
|
|
179
144
|
}
|
|
180
145
|
}
|
|
181
|
-
this.
|
|
182
|
-
await this.storage.setItem(`tx:${address}`, JSON.stringify(storedTransactions));
|
|
146
|
+
await this.storage.setItem(getTransactionsStorageKey(address), JSON.stringify(storedTransactions));
|
|
183
147
|
}
|
|
184
148
|
async clearTransactions(address) {
|
|
185
|
-
this.
|
|
186
|
-
await this.storage.removeItem(`tx:${address}`);
|
|
149
|
+
await this.storage.removeItem(getTransactionsStorageKey(address));
|
|
187
150
|
}
|
|
188
151
|
async getWalletState() {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return this.cache.walletState;
|
|
192
|
-
}
|
|
193
|
-
const stored = await this.storage.getItem("wallet:state");
|
|
194
|
-
if (!stored) {
|
|
195
|
-
this.cache.walletState = null;
|
|
196
|
-
this.cache.initialized.add("walletState");
|
|
152
|
+
const stored = await this.storage.getItem(walletStateStorageKey);
|
|
153
|
+
if (!stored)
|
|
197
154
|
return null;
|
|
198
|
-
}
|
|
199
155
|
try {
|
|
200
156
|
const state = JSON.parse(stored);
|
|
201
|
-
this.cache.walletState = state;
|
|
202
|
-
this.cache.initialized.add("walletState");
|
|
203
157
|
return state;
|
|
204
158
|
}
|
|
205
159
|
catch (error) {
|
|
206
160
|
console.error("Failed to parse wallet state:", error);
|
|
207
|
-
this.cache.walletState = null;
|
|
208
|
-
this.cache.initialized.add("walletState");
|
|
209
161
|
return null;
|
|
210
162
|
}
|
|
211
163
|
}
|
|
212
164
|
async saveWalletState(state) {
|
|
213
|
-
this.
|
|
214
|
-
await this.storage.setItem("wallet:state", JSON.stringify(state));
|
|
165
|
+
await this.storage.setItem(walletStateStorageKey, JSON.stringify(state));
|
|
215
166
|
}
|
|
216
167
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
2
2
|
import { hex } from "@scure/base";
|
|
3
|
-
import { DEFAULT_SEQUENCE, SigHash } from "@scure/btc-signer";
|
|
3
|
+
import { DEFAULT_SEQUENCE, Script, SigHash } from "@scure/btc-signer";
|
|
4
4
|
import { tapLeafHash } from "@scure/btc-signer/payment.js";
|
|
5
5
|
import { CLTVMultisigTapscript, decodeTapscript, } from '../script/tapscript.js';
|
|
6
6
|
import { scriptFromTapLeafScript, VtxoScript, } from '../script/base.js';
|
|
@@ -20,6 +20,17 @@ import { Transaction } from './transaction.js';
|
|
|
20
20
|
* @returns Object containing the virtual transaction and checkpoint transactions
|
|
21
21
|
*/
|
|
22
22
|
export function buildOffchainTx(inputs, outputs, serverUnrollScript) {
|
|
23
|
+
let hasOpReturn = false;
|
|
24
|
+
for (const [index, output] of outputs.entries()) {
|
|
25
|
+
if (!output.script)
|
|
26
|
+
throw new Error(`missing output script ${index}`);
|
|
27
|
+
const isOpReturn = Script.decode(output.script)[0] === "RETURN";
|
|
28
|
+
if (!isOpReturn)
|
|
29
|
+
continue;
|
|
30
|
+
if (hasOpReturn)
|
|
31
|
+
throw new Error("multiple OP_RETURN outputs");
|
|
32
|
+
hasOpReturn = true;
|
|
33
|
+
}
|
|
23
34
|
const checkpoints = inputs.map((input) => buildCheckpointTx(input, serverUnrollScript));
|
|
24
35
|
const arkTx = buildVirtualTx(checkpoints.map((c) => c.input), outputs);
|
|
25
36
|
return {
|
|
@@ -204,3 +215,20 @@ export function verifyTapscriptSignatures(tx, inputIndex, requiredSigners, exclu
|
|
|
204
215
|
throw new Error(`Missing signatures from: ${missingSigners.map((pk) => pk.slice(0, 16)).join(", ")}...`);
|
|
205
216
|
}
|
|
206
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Merges the signed transaction with the original transaction
|
|
220
|
+
* @param signedTx signed transaction
|
|
221
|
+
* @param originalTx original transaction
|
|
222
|
+
*/
|
|
223
|
+
export function combineTapscriptSigs(signedTx, originalTx) {
|
|
224
|
+
for (let i = 0; i < signedTx.inputsLength; i++) {
|
|
225
|
+
const input = originalTx.getInput(i);
|
|
226
|
+
const signedInput = signedTx.getInput(i);
|
|
227
|
+
if (!input.tapScriptSig)
|
|
228
|
+
throw new Error("No tapScriptSig");
|
|
229
|
+
originalTx.updateInput(i, {
|
|
230
|
+
tapScriptSig: input.tapScriptSig?.concat(signedInput.tapScriptSig),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return originalTx;
|
|
234
|
+
}
|
|
@@ -11,6 +11,10 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
11
11
|
// All vtxos are received unless:
|
|
12
12
|
// - they resulted from a settlement (either boarding or refresh)
|
|
13
13
|
// - they are the change of a spend tx
|
|
14
|
+
// - they were spent in a payment (have arkTxId set)
|
|
15
|
+
// - they resulted from a payment (their txid matches an arkTxId of a spent vtxo)
|
|
16
|
+
// First, collect all arkTxIds from spent vtxos to identify payment transactions
|
|
17
|
+
const paymentArkTxIds = new Set(spent.filter((v) => v.arkTxId).map((v) => v.arkTxId));
|
|
14
18
|
let vtxosLeftToCheck = [...spent];
|
|
15
19
|
for (const vtxo of [...spendable, ...spent]) {
|
|
16
20
|
if (vtxo.virtualStatus.state !== "preconfirmed" &&
|
|
@@ -18,6 +22,16 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
18
22
|
vtxo.virtualStatus.commitmentTxIds.some((txid) => boardingBatchTxids.has(txid))) {
|
|
19
23
|
continue;
|
|
20
24
|
}
|
|
25
|
+
// Skip vtxos that were spent in a payment transaction
|
|
26
|
+
// These will be handled in the sent transaction section below
|
|
27
|
+
if (vtxo.arkTxId) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
// Skip vtxos that resulted from a payment transaction
|
|
31
|
+
// (their txid matches an arkTxId from a spent vtxo)
|
|
32
|
+
if (paymentArkTxIds.has(vtxo.txid)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
21
35
|
const settleVtxos = findVtxosSpentInSettlement(vtxosLeftToCheck, vtxo);
|
|
22
36
|
vtxosLeftToCheck = removeVtxosFromList(vtxosLeftToCheck, settleVtxos);
|
|
23
37
|
const settleAmount = reduceVtxosAmount(settleVtxos);
|
|
@@ -53,21 +67,17 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
53
67
|
// vtxos by settled by or ark txid
|
|
54
68
|
const vtxosByTxid = new Map();
|
|
55
69
|
for (const v of spent) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const currentVtxos = vtxosByTxid.get(v.settledBy);
|
|
61
|
-
vtxosByTxid.set(v.settledBy, [...currentVtxos, v]);
|
|
62
|
-
}
|
|
63
|
-
if (!v.arkTxId) {
|
|
70
|
+
// Prefer arkTxId over settledBy to avoid duplicates
|
|
71
|
+
// A vtxo should only be grouped once
|
|
72
|
+
const groupKey = v.arkTxId || v.settledBy;
|
|
73
|
+
if (!groupKey) {
|
|
64
74
|
continue;
|
|
65
75
|
}
|
|
66
|
-
if (!vtxosByTxid.has(
|
|
67
|
-
vtxosByTxid.set(
|
|
76
|
+
if (!vtxosByTxid.has(groupKey)) {
|
|
77
|
+
vtxosByTxid.set(groupKey, []);
|
|
68
78
|
}
|
|
69
|
-
const currentVtxos = vtxosByTxid.get(
|
|
70
|
-
vtxosByTxid.set(
|
|
79
|
+
const currentVtxos = vtxosByTxid.get(groupKey);
|
|
80
|
+
vtxosByTxid.set(groupKey, [...currentVtxos, v]);
|
|
71
81
|
}
|
|
72
82
|
for (const [sb, vtxos] of vtxosByTxid) {
|
|
73
83
|
const resultedVtxos = findVtxosResultedFromTxid([...spendable, ...spent], sb);
|
|
@@ -82,7 +92,13 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
82
92
|
boardingTxid: "",
|
|
83
93
|
arkTxid: "",
|
|
84
94
|
};
|
|
85
|
-
|
|
95
|
+
// Use the grouping key (sb) as arkTxid if it looks like an arkTxId
|
|
96
|
+
// (i.e., if the spent vtxos had arkTxId set, use that instead of result vtxo's txid)
|
|
97
|
+
const isArkTxId = vtxos.some((v) => v.arkTxId === sb);
|
|
98
|
+
if (isArkTxId) {
|
|
99
|
+
txKey.arkTxid = sb;
|
|
100
|
+
}
|
|
101
|
+
else if (vtxo.virtualStatus.state === "preconfirmed") {
|
|
86
102
|
txKey.arkTxid = vtxo.txid;
|
|
87
103
|
}
|
|
88
104
|
txs.push({
|