@arkade-os/sdk 0.3.6 → 0.3.8
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/dist/cjs/utils/transactionHistory.js +23 -31
- package/dist/cjs/wallet/serviceWorker/worker.js +9 -0
- package/dist/cjs/wallet/wallet.js +87 -2
- package/dist/esm/utils/transactionHistory.js +23 -31
- package/dist/esm/wallet/serviceWorker/worker.js +9 -0
- package/dist/esm/wallet/wallet.js +87 -2
- package/dist/types/wallet/wallet.d.ts +11 -0
- package/package.json +2 -2
|
@@ -14,10 +14,6 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
14
14
|
// All vtxos are received unless:
|
|
15
15
|
// - they resulted from a settlement (either boarding or refresh)
|
|
16
16
|
// - they are the change of a spend tx
|
|
17
|
-
// - they were spent in a payment (have arkTxId set)
|
|
18
|
-
// - they resulted from a payment (their txid matches an arkTxId of a spent vtxo)
|
|
19
|
-
// First, collect all arkTxIds from spent vtxos to identify payment transactions
|
|
20
|
-
const paymentArkTxIds = new Set(spent.filter((v) => v.arkTxId).map((v) => v.arkTxId));
|
|
21
17
|
let vtxosLeftToCheck = [...spent];
|
|
22
18
|
for (const vtxo of [...spendable, ...spent]) {
|
|
23
19
|
if (vtxo.virtualStatus.state !== "preconfirmed" &&
|
|
@@ -25,16 +21,6 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
25
21
|
vtxo.virtualStatus.commitmentTxIds.some((txid) => boardingBatchTxids.has(txid))) {
|
|
26
22
|
continue;
|
|
27
23
|
}
|
|
28
|
-
// Skip vtxos that were spent in a payment transaction
|
|
29
|
-
// These will be handled in the sent transaction section below
|
|
30
|
-
if (vtxo.arkTxId) {
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
// Skip vtxos that resulted from a payment transaction
|
|
34
|
-
// (their txid matches an arkTxId from a spent vtxo)
|
|
35
|
-
if (paymentArkTxIds.has(vtxo.txid)) {
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
24
|
const settleVtxos = findVtxosSpentInSettlement(vtxosLeftToCheck, vtxo);
|
|
39
25
|
vtxosLeftToCheck = removeVtxosFromList(vtxosLeftToCheck, settleVtxos);
|
|
40
26
|
const settleAmount = reduceVtxosAmount(settleVtxos);
|
|
@@ -48,7 +34,7 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
48
34
|
continue; // settlement or change, ignore
|
|
49
35
|
}
|
|
50
36
|
const txKey = {
|
|
51
|
-
commitmentTxid:
|
|
37
|
+
commitmentTxid: "",
|
|
52
38
|
boardingTxid: "",
|
|
53
39
|
arkTxid: "",
|
|
54
40
|
};
|
|
@@ -59,6 +45,10 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
59
45
|
settled = true;
|
|
60
46
|
}
|
|
61
47
|
}
|
|
48
|
+
else {
|
|
49
|
+
txKey.commitmentTxid =
|
|
50
|
+
vtxo.virtualStatus.commitmentTxIds?.[0] || "";
|
|
51
|
+
}
|
|
62
52
|
txs.push({
|
|
63
53
|
key: txKey,
|
|
64
54
|
amount: vtxo.value - settleAmount - spentAmount,
|
|
@@ -70,17 +60,21 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
70
60
|
// vtxos by settled by or ark txid
|
|
71
61
|
const vtxosByTxid = new Map();
|
|
72
62
|
for (const v of spent) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
if (v.settledBy) {
|
|
64
|
+
if (!vtxosByTxid.has(v.settledBy)) {
|
|
65
|
+
vtxosByTxid.set(v.settledBy, []);
|
|
66
|
+
}
|
|
67
|
+
const currentVtxos = vtxosByTxid.get(v.settledBy);
|
|
68
|
+
vtxosByTxid.set(v.settledBy, [...currentVtxos, v]);
|
|
69
|
+
}
|
|
70
|
+
if (!v.arkTxId) {
|
|
77
71
|
continue;
|
|
78
72
|
}
|
|
79
|
-
if (!vtxosByTxid.has(
|
|
80
|
-
vtxosByTxid.set(
|
|
73
|
+
if (!vtxosByTxid.has(v.arkTxId)) {
|
|
74
|
+
vtxosByTxid.set(v.arkTxId, []);
|
|
81
75
|
}
|
|
82
|
-
const currentVtxos = vtxosByTxid.get(
|
|
83
|
-
vtxosByTxid.set(
|
|
76
|
+
const currentVtxos = vtxosByTxid.get(v.arkTxId);
|
|
77
|
+
vtxosByTxid.set(v.arkTxId, [...currentVtxos, v]);
|
|
84
78
|
}
|
|
85
79
|
for (const [sb, vtxos] of vtxosByTxid) {
|
|
86
80
|
const resultedVtxos = findVtxosResultedFromTxid([...spendable, ...spent], sb);
|
|
@@ -91,18 +85,16 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
91
85
|
}
|
|
92
86
|
const vtxo = getVtxo(resultedVtxos, vtxos);
|
|
93
87
|
const txKey = {
|
|
94
|
-
commitmentTxid:
|
|
88
|
+
commitmentTxid: "",
|
|
95
89
|
boardingTxid: "",
|
|
96
90
|
arkTxid: "",
|
|
97
91
|
};
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const isArkTxId = vtxos.some((v) => v.arkTxId === sb);
|
|
101
|
-
if (isArkTxId) {
|
|
102
|
-
txKey.arkTxid = sb;
|
|
92
|
+
if (vtxo.virtualStatus.state === "preconfirmed") {
|
|
93
|
+
txKey.arkTxid = resultedAmount === 0 ? vtxo.arkTxId : vtxo.txid;
|
|
103
94
|
}
|
|
104
|
-
else
|
|
105
|
-
txKey.
|
|
95
|
+
else {
|
|
96
|
+
txKey.commitmentTxid =
|
|
97
|
+
vtxo.virtualStatus.commitmentTxIds?.[0] || "";
|
|
106
98
|
}
|
|
107
99
|
txs.push({
|
|
108
100
|
key: txKey,
|
|
@@ -138,6 +138,15 @@ class Worker {
|
|
|
138
138
|
scripts: [script],
|
|
139
139
|
});
|
|
140
140
|
const vtxos = response.vtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this.wallet, vtxo));
|
|
141
|
+
try {
|
|
142
|
+
// recover pending transactions
|
|
143
|
+
const { finalized, pending } = await this.wallet.finalizePendingTxs(vtxos.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
|
|
144
|
+
vtxo.virtualStatus.state !== "settled"));
|
|
145
|
+
console.info(`Recovered ${finalized.length}/${pending.length} pending transactions: ${finalized.join(", ")}`);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.error("Error recovering pending transactions:", error);
|
|
149
|
+
}
|
|
141
150
|
// Get wallet address and save vtxos using unified repository
|
|
142
151
|
const address = await this.wallet.getAddress();
|
|
143
152
|
await this.walletRepository.saveVtxos(address, vtxos);
|
|
@@ -62,6 +62,7 @@ const inMemory_1 = require("../storage/inMemory");
|
|
|
62
62
|
const walletRepository_1 = require("../repositories/walletRepository");
|
|
63
63
|
const contractRepository_1 = require("../repositories/contractRepository");
|
|
64
64
|
const utils_1 = require("./utils");
|
|
65
|
+
const errors_1 = require("../providers/errors");
|
|
65
66
|
/**
|
|
66
67
|
* Main wallet implementation for Bitcoin transactions with Ark protocol support.
|
|
67
68
|
* The wallet does not store any data locally and relies on Ark and onchain
|
|
@@ -589,7 +590,7 @@ class Wallet {
|
|
|
589
590
|
this.makeRegisterIntentSignature(params.inputs, outputs, onchainOutputIndexes, signingPublicKeys),
|
|
590
591
|
this.makeDeleteIntentSignature(params.inputs),
|
|
591
592
|
]);
|
|
592
|
-
const intentId = await this.
|
|
593
|
+
const intentId = await this.safeRegisterIntent(intent);
|
|
593
594
|
const abortController = new AbortController();
|
|
594
595
|
// listen to settlement events
|
|
595
596
|
try {
|
|
@@ -741,7 +742,9 @@ class Wallet {
|
|
|
741
742
|
// delete the intent to not be stuck in the queue
|
|
742
743
|
await this.arkProvider.deleteIntent(deleteIntent);
|
|
743
744
|
}
|
|
744
|
-
catch {
|
|
745
|
+
catch (error) {
|
|
746
|
+
console.error("failed to delete intent: ", error);
|
|
747
|
+
}
|
|
745
748
|
throw error;
|
|
746
749
|
}
|
|
747
750
|
throw new Error("Settlement failed");
|
|
@@ -956,6 +959,27 @@ class Wallet {
|
|
|
956
959
|
: undefined);
|
|
957
960
|
}
|
|
958
961
|
}
|
|
962
|
+
async safeRegisterIntent(intent) {
|
|
963
|
+
try {
|
|
964
|
+
return this.arkProvider.registerIntent(intent);
|
|
965
|
+
}
|
|
966
|
+
catch (error) {
|
|
967
|
+
// catch the "already registered by another intent" error
|
|
968
|
+
if (error instanceof errors_1.ArkError &&
|
|
969
|
+
error.code === 0 &&
|
|
970
|
+
error.message.includes("duplicated input")) {
|
|
971
|
+
// delete all intents spending one of the wallet coins
|
|
972
|
+
const allSpendableCoins = await this.getVtxos({
|
|
973
|
+
withRecoverable: true,
|
|
974
|
+
});
|
|
975
|
+
const deleteIntent = await this.makeDeleteIntentSignature(allSpendableCoins);
|
|
976
|
+
await this.arkProvider.deleteIntent(deleteIntent);
|
|
977
|
+
// try again
|
|
978
|
+
return this.arkProvider.registerIntent(intent);
|
|
979
|
+
}
|
|
980
|
+
throw error;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
959
983
|
async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys) {
|
|
960
984
|
const inputs = this.prepareIntentProofInputs(coins);
|
|
961
985
|
const message = {
|
|
@@ -987,6 +1011,67 @@ class Wallet {
|
|
|
987
1011
|
message: encodedMessage,
|
|
988
1012
|
};
|
|
989
1013
|
}
|
|
1014
|
+
async makeGetPendingTxIntentSignature(vtxos) {
|
|
1015
|
+
const inputs = this.prepareIntentProofInputs(vtxos);
|
|
1016
|
+
const message = {
|
|
1017
|
+
type: "get-pending-tx",
|
|
1018
|
+
expire_at: 0,
|
|
1019
|
+
};
|
|
1020
|
+
const encodedMessage = JSON.stringify(message, null, 0);
|
|
1021
|
+
const proof = intent_1.Intent.create(encodedMessage, inputs, []);
|
|
1022
|
+
const signedProof = await this.identity.sign(proof);
|
|
1023
|
+
return {
|
|
1024
|
+
proof: base_1.base64.encode(signedProof.toPSBT()),
|
|
1025
|
+
message: encodedMessage,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Finalizes pending transactions by retrieving them from the server and finalizing each one.
|
|
1030
|
+
* @param vtxos - Optional list of VTXOs to use instead of retrieving them from the server
|
|
1031
|
+
* @returns Array of transaction IDs that were finalized
|
|
1032
|
+
*/
|
|
1033
|
+
async finalizePendingTxs(vtxos) {
|
|
1034
|
+
const MAX_INPUTS_PER_INTENT = 20;
|
|
1035
|
+
if (!vtxos || vtxos.length === 0) {
|
|
1036
|
+
// get non-swept VTXOs, rely on the indexer only in case DB doesn't have the right state
|
|
1037
|
+
const scripts = [base_1.hex.encode(this.offchainTapscript.pkScript)];
|
|
1038
|
+
let { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
|
|
1039
|
+
scripts,
|
|
1040
|
+
});
|
|
1041
|
+
fetchedVtxos = fetchedVtxos.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
|
|
1042
|
+
vtxo.virtualStatus.state !== "settled");
|
|
1043
|
+
if (fetchedVtxos.length === 0) {
|
|
1044
|
+
return { finalized: [], pending: [] };
|
|
1045
|
+
}
|
|
1046
|
+
vtxos = fetchedVtxos.map((v) => (0, utils_1.extendVirtualCoin)(this, v));
|
|
1047
|
+
}
|
|
1048
|
+
const finalized = [];
|
|
1049
|
+
const pending = [];
|
|
1050
|
+
for (let i = 0; i < vtxos.length; i += MAX_INPUTS_PER_INTENT) {
|
|
1051
|
+
const batch = vtxos.slice(i, i + MAX_INPUTS_PER_INTENT);
|
|
1052
|
+
const intent = await this.makeGetPendingTxIntentSignature(batch);
|
|
1053
|
+
const pendingTxs = await this.arkProvider.getPendingTxs(intent);
|
|
1054
|
+
// finalize each transaction by signing the checkpoints
|
|
1055
|
+
for (const pendingTx of pendingTxs) {
|
|
1056
|
+
pending.push(pendingTx.arkTxid);
|
|
1057
|
+
try {
|
|
1058
|
+
// sign the checkpoints
|
|
1059
|
+
const finalCheckpoints = await Promise.all(pendingTx.signedCheckpointTxs.map(async (c) => {
|
|
1060
|
+
const tx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(c));
|
|
1061
|
+
const signedCheckpoint = await this.identity.sign(tx);
|
|
1062
|
+
return base_1.base64.encode(signedCheckpoint.toPSBT());
|
|
1063
|
+
}));
|
|
1064
|
+
await this.arkProvider.finalizeTx(pendingTx.arkTxid, finalCheckpoints);
|
|
1065
|
+
finalized.push(pendingTx.arkTxid);
|
|
1066
|
+
}
|
|
1067
|
+
catch (error) {
|
|
1068
|
+
console.error(`Failed to finalize transaction ${pendingTx.arkTxid}:`, error);
|
|
1069
|
+
// continue with other transactions even if one fails
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return { finalized, pending };
|
|
1074
|
+
}
|
|
990
1075
|
prepareIntentProofInputs(coins) {
|
|
991
1076
|
const inputs = [];
|
|
992
1077
|
for (const input of coins) {
|
|
@@ -11,10 +11,6 @@ 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));
|
|
18
14
|
let vtxosLeftToCheck = [...spent];
|
|
19
15
|
for (const vtxo of [...spendable, ...spent]) {
|
|
20
16
|
if (vtxo.virtualStatus.state !== "preconfirmed" &&
|
|
@@ -22,16 +18,6 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
22
18
|
vtxo.virtualStatus.commitmentTxIds.some((txid) => boardingBatchTxids.has(txid))) {
|
|
23
19
|
continue;
|
|
24
20
|
}
|
|
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
|
-
}
|
|
35
21
|
const settleVtxos = findVtxosSpentInSettlement(vtxosLeftToCheck, vtxo);
|
|
36
22
|
vtxosLeftToCheck = removeVtxosFromList(vtxosLeftToCheck, settleVtxos);
|
|
37
23
|
const settleAmount = reduceVtxosAmount(settleVtxos);
|
|
@@ -45,7 +31,7 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
45
31
|
continue; // settlement or change, ignore
|
|
46
32
|
}
|
|
47
33
|
const txKey = {
|
|
48
|
-
commitmentTxid:
|
|
34
|
+
commitmentTxid: "",
|
|
49
35
|
boardingTxid: "",
|
|
50
36
|
arkTxid: "",
|
|
51
37
|
};
|
|
@@ -56,6 +42,10 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
56
42
|
settled = true;
|
|
57
43
|
}
|
|
58
44
|
}
|
|
45
|
+
else {
|
|
46
|
+
txKey.commitmentTxid =
|
|
47
|
+
vtxo.virtualStatus.commitmentTxIds?.[0] || "";
|
|
48
|
+
}
|
|
59
49
|
txs.push({
|
|
60
50
|
key: txKey,
|
|
61
51
|
amount: vtxo.value - settleAmount - spentAmount,
|
|
@@ -67,17 +57,21 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
67
57
|
// vtxos by settled by or ark txid
|
|
68
58
|
const vtxosByTxid = new Map();
|
|
69
59
|
for (const v of spent) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
60
|
+
if (v.settledBy) {
|
|
61
|
+
if (!vtxosByTxid.has(v.settledBy)) {
|
|
62
|
+
vtxosByTxid.set(v.settledBy, []);
|
|
63
|
+
}
|
|
64
|
+
const currentVtxos = vtxosByTxid.get(v.settledBy);
|
|
65
|
+
vtxosByTxid.set(v.settledBy, [...currentVtxos, v]);
|
|
66
|
+
}
|
|
67
|
+
if (!v.arkTxId) {
|
|
74
68
|
continue;
|
|
75
69
|
}
|
|
76
|
-
if (!vtxosByTxid.has(
|
|
77
|
-
vtxosByTxid.set(
|
|
70
|
+
if (!vtxosByTxid.has(v.arkTxId)) {
|
|
71
|
+
vtxosByTxid.set(v.arkTxId, []);
|
|
78
72
|
}
|
|
79
|
-
const currentVtxos = vtxosByTxid.get(
|
|
80
|
-
vtxosByTxid.set(
|
|
73
|
+
const currentVtxos = vtxosByTxid.get(v.arkTxId);
|
|
74
|
+
vtxosByTxid.set(v.arkTxId, [...currentVtxos, v]);
|
|
81
75
|
}
|
|
82
76
|
for (const [sb, vtxos] of vtxosByTxid) {
|
|
83
77
|
const resultedVtxos = findVtxosResultedFromTxid([...spendable, ...spent], sb);
|
|
@@ -88,18 +82,16 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
|
|
|
88
82
|
}
|
|
89
83
|
const vtxo = getVtxo(resultedVtxos, vtxos);
|
|
90
84
|
const txKey = {
|
|
91
|
-
commitmentTxid:
|
|
85
|
+
commitmentTxid: "",
|
|
92
86
|
boardingTxid: "",
|
|
93
87
|
arkTxid: "",
|
|
94
88
|
};
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const isArkTxId = vtxos.some((v) => v.arkTxId === sb);
|
|
98
|
-
if (isArkTxId) {
|
|
99
|
-
txKey.arkTxid = sb;
|
|
89
|
+
if (vtxo.virtualStatus.state === "preconfirmed") {
|
|
90
|
+
txKey.arkTxid = resultedAmount === 0 ? vtxo.arkTxId : vtxo.txid;
|
|
100
91
|
}
|
|
101
|
-
else
|
|
102
|
-
txKey.
|
|
92
|
+
else {
|
|
93
|
+
txKey.commitmentTxid =
|
|
94
|
+
vtxo.virtualStatus.commitmentTxIds?.[0] || "";
|
|
103
95
|
}
|
|
104
96
|
txs.push({
|
|
105
97
|
key: txKey,
|
|
@@ -135,6 +135,15 @@ export class Worker {
|
|
|
135
135
|
scripts: [script],
|
|
136
136
|
});
|
|
137
137
|
const vtxos = response.vtxos.map((vtxo) => extendVirtualCoin(this.wallet, vtxo));
|
|
138
|
+
try {
|
|
139
|
+
// recover pending transactions
|
|
140
|
+
const { finalized, pending } = await this.wallet.finalizePendingTxs(vtxos.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
|
|
141
|
+
vtxo.virtualStatus.state !== "settled"));
|
|
142
|
+
console.info(`Recovered ${finalized.length}/${pending.length} pending transactions: ${finalized.join(", ")}`);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
console.error("Error recovering pending transactions:", error);
|
|
146
|
+
}
|
|
138
147
|
// Get wallet address and save vtxos using unified repository
|
|
139
148
|
const address = await this.wallet.getAddress();
|
|
140
149
|
await this.walletRepository.saveVtxos(address, vtxos);
|
|
@@ -25,6 +25,7 @@ import { InMemoryStorageAdapter } from '../storage/inMemory.js';
|
|
|
25
25
|
import { WalletRepositoryImpl, } from '../repositories/walletRepository.js';
|
|
26
26
|
import { ContractRepositoryImpl, } from '../repositories/contractRepository.js';
|
|
27
27
|
import { extendCoin, extendVirtualCoin } from './utils.js';
|
|
28
|
+
import { ArkError } from '../providers/errors.js';
|
|
28
29
|
/**
|
|
29
30
|
* Main wallet implementation for Bitcoin transactions with Ark protocol support.
|
|
30
31
|
* The wallet does not store any data locally and relies on Ark and onchain
|
|
@@ -552,7 +553,7 @@ export class Wallet {
|
|
|
552
553
|
this.makeRegisterIntentSignature(params.inputs, outputs, onchainOutputIndexes, signingPublicKeys),
|
|
553
554
|
this.makeDeleteIntentSignature(params.inputs),
|
|
554
555
|
]);
|
|
555
|
-
const intentId = await this.
|
|
556
|
+
const intentId = await this.safeRegisterIntent(intent);
|
|
556
557
|
const abortController = new AbortController();
|
|
557
558
|
// listen to settlement events
|
|
558
559
|
try {
|
|
@@ -704,7 +705,9 @@ export class Wallet {
|
|
|
704
705
|
// delete the intent to not be stuck in the queue
|
|
705
706
|
await this.arkProvider.deleteIntent(deleteIntent);
|
|
706
707
|
}
|
|
707
|
-
catch {
|
|
708
|
+
catch (error) {
|
|
709
|
+
console.error("failed to delete intent: ", error);
|
|
710
|
+
}
|
|
708
711
|
throw error;
|
|
709
712
|
}
|
|
710
713
|
throw new Error("Settlement failed");
|
|
@@ -919,6 +922,27 @@ export class Wallet {
|
|
|
919
922
|
: undefined);
|
|
920
923
|
}
|
|
921
924
|
}
|
|
925
|
+
async safeRegisterIntent(intent) {
|
|
926
|
+
try {
|
|
927
|
+
return this.arkProvider.registerIntent(intent);
|
|
928
|
+
}
|
|
929
|
+
catch (error) {
|
|
930
|
+
// catch the "already registered by another intent" error
|
|
931
|
+
if (error instanceof ArkError &&
|
|
932
|
+
error.code === 0 &&
|
|
933
|
+
error.message.includes("duplicated input")) {
|
|
934
|
+
// delete all intents spending one of the wallet coins
|
|
935
|
+
const allSpendableCoins = await this.getVtxos({
|
|
936
|
+
withRecoverable: true,
|
|
937
|
+
});
|
|
938
|
+
const deleteIntent = await this.makeDeleteIntentSignature(allSpendableCoins);
|
|
939
|
+
await this.arkProvider.deleteIntent(deleteIntent);
|
|
940
|
+
// try again
|
|
941
|
+
return this.arkProvider.registerIntent(intent);
|
|
942
|
+
}
|
|
943
|
+
throw error;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
922
946
|
async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys) {
|
|
923
947
|
const inputs = this.prepareIntentProofInputs(coins);
|
|
924
948
|
const message = {
|
|
@@ -950,6 +974,67 @@ export class Wallet {
|
|
|
950
974
|
message: encodedMessage,
|
|
951
975
|
};
|
|
952
976
|
}
|
|
977
|
+
async makeGetPendingTxIntentSignature(vtxos) {
|
|
978
|
+
const inputs = this.prepareIntentProofInputs(vtxos);
|
|
979
|
+
const message = {
|
|
980
|
+
type: "get-pending-tx",
|
|
981
|
+
expire_at: 0,
|
|
982
|
+
};
|
|
983
|
+
const encodedMessage = JSON.stringify(message, null, 0);
|
|
984
|
+
const proof = Intent.create(encodedMessage, inputs, []);
|
|
985
|
+
const signedProof = await this.identity.sign(proof);
|
|
986
|
+
return {
|
|
987
|
+
proof: base64.encode(signedProof.toPSBT()),
|
|
988
|
+
message: encodedMessage,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Finalizes pending transactions by retrieving them from the server and finalizing each one.
|
|
993
|
+
* @param vtxos - Optional list of VTXOs to use instead of retrieving them from the server
|
|
994
|
+
* @returns Array of transaction IDs that were finalized
|
|
995
|
+
*/
|
|
996
|
+
async finalizePendingTxs(vtxos) {
|
|
997
|
+
const MAX_INPUTS_PER_INTENT = 20;
|
|
998
|
+
if (!vtxos || vtxos.length === 0) {
|
|
999
|
+
// get non-swept VTXOs, rely on the indexer only in case DB doesn't have the right state
|
|
1000
|
+
const scripts = [hex.encode(this.offchainTapscript.pkScript)];
|
|
1001
|
+
let { vtxos: fetchedVtxos } = await this.indexerProvider.getVtxos({
|
|
1002
|
+
scripts,
|
|
1003
|
+
});
|
|
1004
|
+
fetchedVtxos = fetchedVtxos.filter((vtxo) => vtxo.virtualStatus.state !== "swept" &&
|
|
1005
|
+
vtxo.virtualStatus.state !== "settled");
|
|
1006
|
+
if (fetchedVtxos.length === 0) {
|
|
1007
|
+
return { finalized: [], pending: [] };
|
|
1008
|
+
}
|
|
1009
|
+
vtxos = fetchedVtxos.map((v) => extendVirtualCoin(this, v));
|
|
1010
|
+
}
|
|
1011
|
+
const finalized = [];
|
|
1012
|
+
const pending = [];
|
|
1013
|
+
for (let i = 0; i < vtxos.length; i += MAX_INPUTS_PER_INTENT) {
|
|
1014
|
+
const batch = vtxos.slice(i, i + MAX_INPUTS_PER_INTENT);
|
|
1015
|
+
const intent = await this.makeGetPendingTxIntentSignature(batch);
|
|
1016
|
+
const pendingTxs = await this.arkProvider.getPendingTxs(intent);
|
|
1017
|
+
// finalize each transaction by signing the checkpoints
|
|
1018
|
+
for (const pendingTx of pendingTxs) {
|
|
1019
|
+
pending.push(pendingTx.arkTxid);
|
|
1020
|
+
try {
|
|
1021
|
+
// sign the checkpoints
|
|
1022
|
+
const finalCheckpoints = await Promise.all(pendingTx.signedCheckpointTxs.map(async (c) => {
|
|
1023
|
+
const tx = Transaction.fromPSBT(base64.decode(c));
|
|
1024
|
+
const signedCheckpoint = await this.identity.sign(tx);
|
|
1025
|
+
return base64.encode(signedCheckpoint.toPSBT());
|
|
1026
|
+
}));
|
|
1027
|
+
await this.arkProvider.finalizeTx(pendingTx.arkTxid, finalCheckpoints);
|
|
1028
|
+
finalized.push(pendingTx.arkTxid);
|
|
1029
|
+
}
|
|
1030
|
+
catch (error) {
|
|
1031
|
+
console.error(`Failed to finalize transaction ${pendingTx.arkTxid}:`, error);
|
|
1032
|
+
// continue with other transactions even if one fails
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return { finalized, pending };
|
|
1037
|
+
}
|
|
953
1038
|
prepareIntentProofInputs(coins) {
|
|
954
1039
|
const inputs = [];
|
|
955
1040
|
for (const input of coins) {
|
|
@@ -94,8 +94,19 @@ export declare class Wallet implements IWallet {
|
|
|
94
94
|
private handleSettlementSigningEvent;
|
|
95
95
|
private handleSettlementTreeNoncesEvent;
|
|
96
96
|
private handleSettlementFinalizationEvent;
|
|
97
|
+
safeRegisterIntent(intent: SignedIntent): Promise<string>;
|
|
97
98
|
makeRegisterIntentSignature(coins: ExtendedCoin[], outputs: TransactionOutput[], onchainOutputsIndexes: number[], cosignerPubKeys: string[]): Promise<SignedIntent>;
|
|
98
99
|
makeDeleteIntentSignature(coins: ExtendedCoin[]): Promise<SignedIntent>;
|
|
100
|
+
makeGetPendingTxIntentSignature(vtxos: ExtendedVirtualCoin[]): Promise<SignedIntent>;
|
|
101
|
+
/**
|
|
102
|
+
* Finalizes pending transactions by retrieving them from the server and finalizing each one.
|
|
103
|
+
* @param vtxos - Optional list of VTXOs to use instead of retrieving them from the server
|
|
104
|
+
* @returns Array of transaction IDs that were finalized
|
|
105
|
+
*/
|
|
106
|
+
finalizePendingTxs(vtxos?: ExtendedVirtualCoin[]): Promise<{
|
|
107
|
+
finalized: string[];
|
|
108
|
+
pending: string[];
|
|
109
|
+
}>;
|
|
99
110
|
private prepareIntentProofInputs;
|
|
100
111
|
}
|
|
101
112
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arkade-os/sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Bitcoin wallet SDK with Taproot and Ark integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"@types/node": "24.3.1",
|
|
64
64
|
"@vitest/coverage-v8": "3.2.4",
|
|
65
65
|
"esbuild": "^0.25.9",
|
|
66
|
-
"eventsource": "4.0.0",
|
|
67
66
|
"expo": "~52.0.47",
|
|
67
|
+
"eventsource": "4.0.0",
|
|
68
68
|
"glob": "11.0.3",
|
|
69
69
|
"husky": "9.1.7",
|
|
70
70
|
"prettier": "3.6.2",
|