@arkade-os/sdk 0.3.9 → 0.3.11
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/arkfee/celenv.js +43 -0
- package/dist/cjs/arkfee/estimator.js +143 -0
- package/dist/cjs/arkfee/index.js +5 -0
- package/dist/cjs/arkfee/types.js +25 -0
- package/dist/cjs/index.js +15 -0
- package/dist/cjs/intent/index.js +21 -0
- package/dist/cjs/providers/ark.js +2 -5
- package/dist/cjs/providers/indexer.js +1 -0
- package/dist/cjs/utils/transactionHistory.js +118 -156
- package/dist/cjs/wallet/ramps.js +96 -11
- package/dist/cjs/wallet/serviceWorker/worker.js +4 -31
- package/dist/cjs/wallet/wallet.js +68 -35
- package/dist/esm/arkfee/celenv.js +40 -0
- package/dist/esm/arkfee/estimator.js +139 -0
- package/dist/esm/arkfee/index.js +1 -0
- package/dist/esm/arkfee/types.js +21 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/intent/index.js +21 -0
- package/dist/esm/providers/ark.js +2 -5
- package/dist/esm/providers/indexer.js +1 -0
- package/dist/esm/utils/transactionHistory.js +117 -155
- package/dist/esm/wallet/ramps.js +96 -11
- package/dist/esm/wallet/serviceWorker/worker.js +4 -31
- package/dist/esm/wallet/wallet.js +68 -35
- package/dist/types/arkfee/celenv.d.ts +25 -0
- package/dist/types/arkfee/estimator.d.ts +49 -0
- package/dist/types/arkfee/index.d.ts +2 -0
- package/dist/types/arkfee/types.d.ts +37 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/intent/index.d.ts +1 -0
- package/dist/types/providers/ark.d.ts +3 -8
- package/dist/types/utils/transactionHistory.d.ts +12 -5
- package/dist/types/wallet/index.d.ts +2 -0
- package/dist/types/wallet/ramps.d.ts +2 -1
- package/dist/types/wallet/serviceWorker/worker.d.ts +0 -1
- package/package.json +114 -123
|
@@ -1,165 +1,127 @@
|
|
|
1
1
|
import { TxType } from '../wallet/index.js';
|
|
2
|
+
const txKey = {
|
|
3
|
+
commitmentTxid: "",
|
|
4
|
+
boardingTxid: "",
|
|
5
|
+
arkTxid: "",
|
|
6
|
+
};
|
|
2
7
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* @
|
|
8
|
+
* Builds the transaction history by analyzing virtual coins (VTXOs), boarding transactions, and ignored commitments.
|
|
9
|
+
* History is sorted from newest to oldest and is composed only of SENT and RECEIVED transactions.
|
|
10
|
+
*
|
|
11
|
+
* @param {VirtualCoin[]} vtxos - An array of virtual coins representing the user's transactions and balances.
|
|
12
|
+
* @param {ArkTransaction[]} allBoardingTxs - An array of boarding transactions to include in the history.
|
|
13
|
+
* @param {Set<string>} commitmentsToIgnore - A set of commitment IDs that should be excluded from processing.
|
|
14
|
+
* @return {ExtendedArkTransaction[]} A sorted array of extended Ark transactions, representing the transaction history.
|
|
7
15
|
*/
|
|
8
|
-
export function
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const spentAmount = reduceVtxosAmount(spentVtxos);
|
|
30
|
-
if (vtxo.value <= spentAmount) {
|
|
31
|
-
continue; // settlement or change, ignore
|
|
32
|
-
}
|
|
33
|
-
const txKey = {
|
|
34
|
-
commitmentTxid: "",
|
|
35
|
-
boardingTxid: "",
|
|
36
|
-
arkTxid: "",
|
|
37
|
-
};
|
|
38
|
-
let settled = vtxo.virtualStatus.state !== "preconfirmed";
|
|
39
|
-
if (vtxo.virtualStatus.state === "preconfirmed") {
|
|
40
|
-
txKey.arkTxid = vtxo.txid;
|
|
41
|
-
if (vtxo.spentBy) {
|
|
42
|
-
settled = true;
|
|
16
|
+
export function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnore) {
|
|
17
|
+
const fromOldestVtxo = [...vtxos].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
18
|
+
const sent = [];
|
|
19
|
+
let received = [];
|
|
20
|
+
for (const vtxo of fromOldestVtxo) {
|
|
21
|
+
if (vtxo.status.isLeaf) {
|
|
22
|
+
// If this vtxo is a leaf and it's not the settlement of a boarding or there's no vtxo refreshed by it,
|
|
23
|
+
// it's translated into a received batch transaction
|
|
24
|
+
if (!commitmentsToIgnore.has(vtxo.virtualStatus.commitmentTxIds[0]) &&
|
|
25
|
+
fromOldestVtxo.filter((v) => v.settledBy === vtxo.virtualStatus.commitmentTxIds[0]).length === 0) {
|
|
26
|
+
received.push({
|
|
27
|
+
key: {
|
|
28
|
+
...txKey,
|
|
29
|
+
commitmentTxid: vtxo.virtualStatus.commitmentTxIds[0],
|
|
30
|
+
},
|
|
31
|
+
tag: "batch",
|
|
32
|
+
type: TxType.TxReceived,
|
|
33
|
+
amount: vtxo.value,
|
|
34
|
+
settled: vtxo.status.isLeaf || vtxo.isSpent,
|
|
35
|
+
createdAt: vtxo.createdAt.getTime(),
|
|
36
|
+
});
|
|
43
37
|
}
|
|
44
38
|
}
|
|
45
|
-
else {
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
else if (fromOldestVtxo.filter((v) => v.arkTxId === vtxo.txid).length === 0) {
|
|
40
|
+
// If this vtxo is preconfirmed and does not spend any other vtxos,
|
|
41
|
+
// it's translated into a received offchain transaction
|
|
42
|
+
received.push({
|
|
43
|
+
key: { ...txKey, arkTxid: vtxo.txid },
|
|
44
|
+
tag: "offchain",
|
|
45
|
+
type: TxType.TxReceived,
|
|
46
|
+
amount: vtxo.value,
|
|
47
|
+
settled: vtxo.status.isLeaf || vtxo.isSpent,
|
|
48
|
+
createdAt: vtxo.createdAt.getTime(),
|
|
49
|
+
});
|
|
48
50
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
// If the vtxo is spent, it's translated into a sent transaction unless:
|
|
52
|
+
// - it's been refreshed (we don't want to add any record in this case)
|
|
53
|
+
// - a sent transaction has been already added to avoid duplicates (can happen if many vtxos have been spent in the same tx or forfeited in the same batch)
|
|
54
|
+
if (vtxo.isSpent) {
|
|
55
|
+
// If the vtxo is spent offchain, it's translated into offchain sent tx
|
|
56
|
+
if (vtxo.arkTxId &&
|
|
57
|
+
!sent.some((s) => s.key.arkTxid === vtxo.arkTxId)) {
|
|
58
|
+
const changes = fromOldestVtxo.filter((_) => _.txid === vtxo.arkTxId);
|
|
59
|
+
let txAmount = 0;
|
|
60
|
+
let txTime = 0;
|
|
61
|
+
if (changes.length > 0) {
|
|
62
|
+
const changeAmount = changes.reduce((acc, v) => acc + v.value, 0);
|
|
63
|
+
// We want to find all the other VTXOs spent by the same transaction to
|
|
64
|
+
// calculate the full amount of the change.
|
|
65
|
+
const allSpent = fromOldestVtxo.filter((v) => v.arkTxId === vtxo.arkTxId);
|
|
66
|
+
const spentAmount = allSpent.reduce((acc, v) => acc + v.value, 0);
|
|
67
|
+
txAmount = spentAmount - changeAmount;
|
|
68
|
+
txTime = changes[0].createdAt.getTime();
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
txAmount = vtxo.value;
|
|
72
|
+
// TODO: fetch the vtxo with /v1/indexer/vtxos?outpoints=<vtxo.arkTxid:0> to know when the tx was made
|
|
73
|
+
txTime = vtxo.createdAt.getTime() + 1;
|
|
74
|
+
}
|
|
75
|
+
sent.push({
|
|
76
|
+
key: { ...txKey, arkTxid: vtxo.arkTxId },
|
|
77
|
+
tag: "offchain",
|
|
78
|
+
type: TxType.TxSent,
|
|
79
|
+
amount: txAmount,
|
|
80
|
+
settled: true,
|
|
81
|
+
createdAt: txTime,
|
|
82
|
+
});
|
|
63
83
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
createdAt: vtxo.createdAt.getTime(),
|
|
101
|
-
settled: true,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
return txs;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Helper function to find vtxos that were spent in a settlement
|
|
108
|
-
*/
|
|
109
|
-
function findVtxosSpentInSettlement(vtxos, vtxo) {
|
|
110
|
-
if (vtxo.virtualStatus.state === "preconfirmed") {
|
|
111
|
-
return [];
|
|
112
|
-
}
|
|
113
|
-
return vtxos.filter((v) => {
|
|
114
|
-
if (!v.settledBy)
|
|
115
|
-
return false;
|
|
116
|
-
return (vtxo.virtualStatus.commitmentTxIds?.includes(v.settledBy) ?? false);
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Helper function to find vtxos that were spent in a payment
|
|
121
|
-
*/
|
|
122
|
-
function findVtxosSpentInPayment(vtxos, vtxo) {
|
|
123
|
-
return vtxos.filter((v) => {
|
|
124
|
-
if (!v.arkTxId)
|
|
125
|
-
return false;
|
|
126
|
-
return v.arkTxId === vtxo.txid;
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Helper function to find vtxos that resulted from a spentBy transaction
|
|
131
|
-
*/
|
|
132
|
-
function findVtxosResultedFromTxid(vtxos, txid) {
|
|
133
|
-
return vtxos.filter((v) => {
|
|
134
|
-
if (v.virtualStatus.state !== "preconfirmed" &&
|
|
135
|
-
v.virtualStatus.commitmentTxIds?.includes(txid)) {
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
return v.txid === txid;
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Helper function to reduce vtxos to their total amount
|
|
143
|
-
*/
|
|
144
|
-
function reduceVtxosAmount(vtxos) {
|
|
145
|
-
return vtxos.reduce((sum, v) => sum + v.value, 0);
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Helper function to get a vtxo from a list of vtxos
|
|
149
|
-
*/
|
|
150
|
-
function getVtxo(resultedVtxos, spentVtxos) {
|
|
151
|
-
if (resultedVtxos.length === 0) {
|
|
152
|
-
return spentVtxos[0];
|
|
153
|
-
}
|
|
154
|
-
return resultedVtxos[0];
|
|
155
|
-
}
|
|
156
|
-
function removeVtxosFromList(vtxos, vtxosToRemove) {
|
|
157
|
-
return vtxos.filter((v) => {
|
|
158
|
-
for (const vtxoToRemove of vtxosToRemove) {
|
|
159
|
-
if (v.txid === vtxoToRemove.txid && v.vout === vtxoToRemove.vout) {
|
|
160
|
-
return false;
|
|
84
|
+
// If the vtxo is forfeited in a batch and the total sum of forfeited vtxos is bigger than the sum of new vtxos,
|
|
85
|
+
// it's translated into an exit sent tx
|
|
86
|
+
if (vtxo.settledBy &&
|
|
87
|
+
!commitmentsToIgnore.has(vtxo.settledBy) &&
|
|
88
|
+
!sent.some((s) => s.key.commitmentTxid === vtxo.settledBy)) {
|
|
89
|
+
const changes = fromOldestVtxo.filter((v) => v.status.isLeaf &&
|
|
90
|
+
v.virtualStatus.commitmentTxIds?.every((_) => vtxo.settledBy === _));
|
|
91
|
+
const forfeitVtxos = fromOldestVtxo.filter((v) => v.settledBy === vtxo.settledBy);
|
|
92
|
+
const forfeitAmount = forfeitVtxos.reduce((acc, v) => acc + v.value, 0);
|
|
93
|
+
if (changes.length > 0) {
|
|
94
|
+
const settledAmount = changes.reduce((acc, v) => acc + v.value, 0);
|
|
95
|
+
// forfeitAmount > settledAmount --> collaborative exit with offchain change
|
|
96
|
+
// TODO: make this support fees!
|
|
97
|
+
if (forfeitAmount > settledAmount) {
|
|
98
|
+
sent.push({
|
|
99
|
+
key: { ...txKey, commitmentTxid: vtxo.settledBy },
|
|
100
|
+
tag: "exit",
|
|
101
|
+
type: TxType.TxSent,
|
|
102
|
+
amount: forfeitAmount - settledAmount,
|
|
103
|
+
settled: true,
|
|
104
|
+
createdAt: changes[0].createdAt.getTime(),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// forfeitAmount > 0 && settledAmount == 0 --> collaborative exit without any offchain change
|
|
110
|
+
sent.push({
|
|
111
|
+
key: { ...txKey, commitmentTxid: vtxo.settledBy },
|
|
112
|
+
tag: "exit",
|
|
113
|
+
type: TxType.TxSent,
|
|
114
|
+
amount: forfeitAmount,
|
|
115
|
+
settled: true,
|
|
116
|
+
// TODO: fetch commitment tx with /v1/indexer/commitmentTx/<commitmentTxid> to know when the tx was made
|
|
117
|
+
createdAt: vtxo.createdAt.getTime() + 1,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
161
120
|
}
|
|
162
121
|
}
|
|
163
|
-
|
|
164
|
-
|
|
122
|
+
}
|
|
123
|
+
// Boardings are always inbound amounts, and we only hide the ones to ignore.
|
|
124
|
+
const boardingTx = allBoardingTxs.map((tx) => ({ ...tx, tag: "boarding" }));
|
|
125
|
+
const sorted = [...boardingTx, ...sent, ...received].sort((a, b) => b.createdAt - a.createdAt);
|
|
126
|
+
return sorted;
|
|
165
127
|
}
|
package/dist/esm/wallet/ramps.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { Estimator } from '../arkfee/index.js';
|
|
2
|
+
import { Address, OutScript } from "@scure/btc-signer";
|
|
3
|
+
import { hex } from "@scure/base";
|
|
4
|
+
import { networks } from '../networks.js';
|
|
5
|
+
import { ArkAddress } from '../script/address.js';
|
|
1
6
|
/**
|
|
2
7
|
* Ramps is a class wrapping IWallet.settle method to provide a more convenient interface for onboarding and offboarding operations.
|
|
3
8
|
*
|
|
@@ -15,22 +20,51 @@ export class Ramps {
|
|
|
15
20
|
/**
|
|
16
21
|
* Onboard boarding utxos.
|
|
17
22
|
*
|
|
23
|
+
* @param feeInfo - The fee info to deduct from the onboard amount.
|
|
18
24
|
* @param boardingUtxos - The boarding utxos to onboard. If not provided, all boarding utxos will be used.
|
|
19
25
|
* @param amount - The amount to onboard. If not provided, the total amount of boarding utxos will be onboarded.
|
|
20
26
|
* @param eventCallback - The callback to receive settlement events. optional.
|
|
21
27
|
*/
|
|
22
|
-
async onboard(boardingUtxos, amount, eventCallback) {
|
|
28
|
+
async onboard(feeInfo, boardingUtxos, amount, eventCallback) {
|
|
23
29
|
boardingUtxos = boardingUtxos ?? (await this.wallet.getBoardingUtxos());
|
|
24
|
-
|
|
30
|
+
// Calculate input fees and filter out utxos where fee >= value
|
|
31
|
+
const estimator = new Estimator(feeInfo?.intentFee ?? {});
|
|
32
|
+
const filteredBoardingUtxos = [];
|
|
33
|
+
let totalAmount = 0n;
|
|
34
|
+
for (const utxo of boardingUtxos) {
|
|
35
|
+
const inputFee = estimator.evalOnchainInput({
|
|
36
|
+
amount: BigInt(utxo.value),
|
|
37
|
+
});
|
|
38
|
+
if (inputFee.satoshis >= utxo.value) {
|
|
39
|
+
// skip if fees are greater than or equal to the utxo value
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
filteredBoardingUtxos.push(utxo);
|
|
43
|
+
totalAmount += BigInt(utxo.value) - BigInt(inputFee.satoshis);
|
|
44
|
+
}
|
|
45
|
+
if (filteredBoardingUtxos.length === 0) {
|
|
46
|
+
throw new Error("No boarding utxos available after deducting fees");
|
|
47
|
+
}
|
|
25
48
|
let change = 0n;
|
|
26
49
|
if (amount) {
|
|
27
50
|
if (amount > totalAmount) {
|
|
28
|
-
throw new Error("Amount is greater than total amount of boarding utxos");
|
|
51
|
+
throw new Error("Amount is greater than total amount of boarding utxos after fees");
|
|
29
52
|
}
|
|
30
53
|
change = totalAmount - amount;
|
|
31
54
|
}
|
|
32
55
|
amount = amount ?? totalAmount;
|
|
56
|
+
// Calculate offchain output fee using Estimator
|
|
33
57
|
const offchainAddress = await this.wallet.getAddress();
|
|
58
|
+
const offchainAddr = ArkAddress.decode(offchainAddress);
|
|
59
|
+
const offchainScript = hex.encode(offchainAddr.pkScript);
|
|
60
|
+
const outputFee = estimator.evalOffchainOutput({
|
|
61
|
+
amount,
|
|
62
|
+
script: offchainScript,
|
|
63
|
+
});
|
|
64
|
+
if (BigInt(outputFee.satoshis) > amount) {
|
|
65
|
+
throw new Error(`can't deduct fees from onboard amount (${outputFee.satoshis} > ${amount})`);
|
|
66
|
+
}
|
|
67
|
+
amount -= BigInt(outputFee.satoshis);
|
|
34
68
|
const outputs = [
|
|
35
69
|
{
|
|
36
70
|
address: offchainAddress,
|
|
@@ -45,7 +79,7 @@ export class Ramps {
|
|
|
45
79
|
});
|
|
46
80
|
}
|
|
47
81
|
return this.wallet.settle({
|
|
48
|
-
inputs:
|
|
82
|
+
inputs: filteredBoardingUtxos,
|
|
49
83
|
outputs,
|
|
50
84
|
}, eventCallback);
|
|
51
85
|
}
|
|
@@ -62,20 +96,71 @@ export class Ramps {
|
|
|
62
96
|
withRecoverable: true,
|
|
63
97
|
withUnrolled: false,
|
|
64
98
|
});
|
|
65
|
-
|
|
99
|
+
// Calculate input fees and filter out vtxos where fee >= value
|
|
100
|
+
const estimator = new Estimator(feeInfo?.intentFee ?? {});
|
|
101
|
+
const filteredVtxos = [];
|
|
102
|
+
let totalAmount = 0n;
|
|
103
|
+
for (const vtxo of vtxos) {
|
|
104
|
+
const inputFee = estimator.evalOffchainInput({
|
|
105
|
+
amount: BigInt(vtxo.value),
|
|
106
|
+
type: vtxo.virtualStatus.state === "swept"
|
|
107
|
+
? "recoverable"
|
|
108
|
+
: "vtxo",
|
|
109
|
+
weight: 0,
|
|
110
|
+
birth: vtxo.createdAt,
|
|
111
|
+
expiry: vtxo.virtualStatus.batchExpiry
|
|
112
|
+
? new Date(vtxo.virtualStatus.batchExpiry * 1000)
|
|
113
|
+
: undefined,
|
|
114
|
+
});
|
|
115
|
+
if (inputFee.satoshis >= vtxo.value) {
|
|
116
|
+
// skip if fees are greater than or equal to the vtxo value
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
filteredVtxos.push(vtxo);
|
|
120
|
+
totalAmount += BigInt(vtxo.value) - BigInt(inputFee.satoshis);
|
|
121
|
+
}
|
|
122
|
+
if (filteredVtxos.length === 0) {
|
|
123
|
+
throw new Error("No vtxos available after deducting fees");
|
|
124
|
+
}
|
|
66
125
|
let change = 0n;
|
|
67
126
|
if (amount) {
|
|
68
127
|
if (amount > totalAmount) {
|
|
69
|
-
throw new Error("Amount is greater than total amount of vtxos");
|
|
128
|
+
throw new Error("Amount is greater than total amount of vtxos after fees");
|
|
70
129
|
}
|
|
71
130
|
change = totalAmount - amount;
|
|
72
131
|
}
|
|
73
132
|
amount = amount ?? totalAmount;
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
133
|
+
const networkNames = [
|
|
134
|
+
"bitcoin",
|
|
135
|
+
"regtest",
|
|
136
|
+
"testnet",
|
|
137
|
+
"signet",
|
|
138
|
+
"mutinynet",
|
|
139
|
+
];
|
|
140
|
+
let destinationScript;
|
|
141
|
+
for (const networkName of networkNames) {
|
|
142
|
+
try {
|
|
143
|
+
const network = networks[networkName];
|
|
144
|
+
const addr = Address(network).decode(destinationAddress);
|
|
145
|
+
destinationScript = OutScript.encode(addr);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Try next network
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!destinationScript) {
|
|
154
|
+
throw new Error(`Failed to decode destination address: ${destinationAddress}`);
|
|
155
|
+
}
|
|
156
|
+
const outputFee = estimator.evalOnchainOutput({
|
|
157
|
+
amount,
|
|
158
|
+
script: hex.encode(destinationScript),
|
|
159
|
+
});
|
|
160
|
+
if (BigInt(outputFee.satoshis) > amount) {
|
|
161
|
+
throw new Error(`can't deduct fees from offboard amount (${outputFee.satoshis} > ${amount})`);
|
|
77
162
|
}
|
|
78
|
-
amount -=
|
|
163
|
+
amount -= BigInt(outputFee.satoshis);
|
|
79
164
|
const outputs = [
|
|
80
165
|
{
|
|
81
166
|
address: destinationAddress,
|
|
@@ -90,7 +175,7 @@ export class Ramps {
|
|
|
90
175
|
});
|
|
91
176
|
}
|
|
92
177
|
return this.wallet.settle({
|
|
93
|
-
inputs:
|
|
178
|
+
inputs: filteredVtxos,
|
|
94
179
|
outputs,
|
|
95
180
|
}, eventCallback);
|
|
96
181
|
}
|
|
@@ -5,7 +5,6 @@ import { ReadonlyWallet, Wallet } from '../wallet.js';
|
|
|
5
5
|
import { Request } from './request.js';
|
|
6
6
|
import { Response } from './response.js';
|
|
7
7
|
import { RestArkProvider } from '../../providers/ark.js';
|
|
8
|
-
import { vtxosToTxs } from '../../utils/transactionHistory.js';
|
|
9
8
|
import { RestIndexerProvider } from '../../providers/indexer.js';
|
|
10
9
|
import { hex } from "@scure/base";
|
|
11
10
|
import { IndexedDBStorageAdapter } from '../../storage/indexedDB.js';
|
|
@@ -40,8 +39,8 @@ class ReadonlyHandler {
|
|
|
40
39
|
getBoardingAddress() {
|
|
41
40
|
return this.wallet.getBoardingAddress();
|
|
42
41
|
}
|
|
43
|
-
|
|
44
|
-
return this.wallet.
|
|
42
|
+
getTransactionHistory() {
|
|
43
|
+
return this.wallet.getTransactionHistory();
|
|
45
44
|
}
|
|
46
45
|
async handleReload(_) {
|
|
47
46
|
const pending = await this.wallet.fetchPendingTxs();
|
|
@@ -127,32 +126,6 @@ export class Worker {
|
|
|
127
126
|
const address = await this.handler.getBoardingAddress();
|
|
128
127
|
return await this.walletRepository.getUtxos(address);
|
|
129
128
|
}
|
|
130
|
-
async getTransactionHistory() {
|
|
131
|
-
if (!this.handler)
|
|
132
|
-
return [];
|
|
133
|
-
let txs = [];
|
|
134
|
-
try {
|
|
135
|
-
const { boardingTxs, commitmentsToIgnore: roundsToIgnore } = await this.handler.getBoardingTxs();
|
|
136
|
-
const { spendable, spent } = await this.getAllVtxos();
|
|
137
|
-
// convert VTXOs to offchain transactions
|
|
138
|
-
const offchainTxs = vtxosToTxs(spendable, spent, roundsToIgnore);
|
|
139
|
-
txs = [...boardingTxs, ...offchainTxs];
|
|
140
|
-
// sort transactions by creation time in descending order (newest first)
|
|
141
|
-
txs.sort(
|
|
142
|
-
// place createdAt = 0 (unconfirmed txs) first, then descending
|
|
143
|
-
(a, b) => {
|
|
144
|
-
if (a.createdAt === 0)
|
|
145
|
-
return -1;
|
|
146
|
-
if (b.createdAt === 0)
|
|
147
|
-
return 1;
|
|
148
|
-
return b.createdAt - a.createdAt;
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
console.error("Error getting transaction history:", error);
|
|
153
|
-
}
|
|
154
|
-
return txs;
|
|
155
|
-
}
|
|
156
129
|
async start(withServiceWorkerUpdate = true) {
|
|
157
130
|
self.addEventListener("message", async (event) => {
|
|
158
131
|
await this.handleMessage(event);
|
|
@@ -212,7 +185,7 @@ export class Worker {
|
|
|
212
185
|
const coins = await this.handler.onchainProvider.getCoins(boardingAddress);
|
|
213
186
|
await this.walletRepository.saveUtxos(boardingAddress, coins.map((utxo) => extendCoin(this.handler, utxo)));
|
|
214
187
|
// Get transaction history to cache boarding txs
|
|
215
|
-
const txs = await this.getTransactionHistory();
|
|
188
|
+
const txs = await this.handler.getTransactionHistory();
|
|
216
189
|
if (txs)
|
|
217
190
|
await this.walletRepository.saveTransactions(address, txs);
|
|
218
191
|
// unsubscribe previous subscription if any
|
|
@@ -561,7 +534,7 @@ export class Worker {
|
|
|
561
534
|
return;
|
|
562
535
|
}
|
|
563
536
|
try {
|
|
564
|
-
const txs = await this.getTransactionHistory();
|
|
537
|
+
const txs = await this.handler.getTransactionHistory();
|
|
565
538
|
event.source?.postMessage(Response.transactionHistory(message.id, txs));
|
|
566
539
|
}
|
|
567
540
|
catch (error) {
|
|
@@ -3,7 +3,6 @@ import * as bip68 from "bip68";
|
|
|
3
3
|
import { tapLeafHash } from "@scure/btc-signer/payment.js";
|
|
4
4
|
import { SigHash, Transaction, Address, OutScript } from "@scure/btc-signer";
|
|
5
5
|
import { sha256 } from "@scure/btc-signer/utils.js";
|
|
6
|
-
import { vtxosToTxs } from '../utils/transactionHistory.js';
|
|
7
6
|
import { ArkAddress } from '../script/address.js';
|
|
8
7
|
import { DefaultVtxo } from '../script/default.js';
|
|
9
8
|
import { getNetwork } from '../networks.js';
|
|
@@ -26,6 +25,8 @@ import { ContractRepositoryImpl, } from '../repositories/contractRepository.js';
|
|
|
26
25
|
import { extendCoin, extendVirtualCoin } from './utils.js';
|
|
27
26
|
import { ArkError } from '../providers/errors.js';
|
|
28
27
|
import { Batch } from './batch.js';
|
|
28
|
+
import { Estimator } from '../arkfee/index.js';
|
|
29
|
+
import { buildTransactionHistory } from '../utils/transactionHistory.js';
|
|
29
30
|
/**
|
|
30
31
|
* Type guard function to check if an identity has a toReadonly method.
|
|
31
32
|
*/
|
|
@@ -229,30 +230,7 @@ export class ReadonlyWallet {
|
|
|
229
230
|
scripts: [hex.encode(this.offchainTapscript.pkScript)],
|
|
230
231
|
});
|
|
231
232
|
const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
|
|
232
|
-
|
|
233
|
-
const spentVtxos = [];
|
|
234
|
-
for (const vtxo of response.vtxos) {
|
|
235
|
-
if (isSpendable(vtxo)) {
|
|
236
|
-
spendableVtxos.push(vtxo);
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
spentVtxos.push(vtxo);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
// convert VTXOs to offchain transactions
|
|
243
|
-
const offchainTxs = vtxosToTxs(spendableVtxos, spentVtxos, commitmentsToIgnore);
|
|
244
|
-
const txs = [...boardingTxs, ...offchainTxs];
|
|
245
|
-
// sort transactions by creation time in descending order (newest first)
|
|
246
|
-
txs.sort(
|
|
247
|
-
// place createdAt = 0 (unconfirmed txs) first, then descending
|
|
248
|
-
(a, b) => {
|
|
249
|
-
if (a.createdAt === 0)
|
|
250
|
-
return -1;
|
|
251
|
-
if (b.createdAt === 0)
|
|
252
|
-
return 1;
|
|
253
|
-
return b.createdAt - a.createdAt;
|
|
254
|
-
});
|
|
255
|
-
return txs;
|
|
233
|
+
return buildTransactionHistory(response.vtxos, boardingTxs, commitmentsToIgnore);
|
|
256
234
|
}
|
|
257
235
|
async getBoardingTxs() {
|
|
258
236
|
const utxos = [];
|
|
@@ -511,7 +489,23 @@ export class Wallet extends ReadonlyWallet {
|
|
|
511
489
|
const virtualCoins = await this.getVirtualCoins({
|
|
512
490
|
withRecoverable: false,
|
|
513
491
|
});
|
|
514
|
-
|
|
492
|
+
let selected;
|
|
493
|
+
if (params.selectedVtxos) {
|
|
494
|
+
const selectedVtxoSum = params.selectedVtxos
|
|
495
|
+
.map((v) => v.value)
|
|
496
|
+
.reduce((a, b) => a + b, 0);
|
|
497
|
+
if (selectedVtxoSum < params.amount) {
|
|
498
|
+
throw new Error("Selected VTXOs do not cover specified amount");
|
|
499
|
+
}
|
|
500
|
+
const changeAmount = selectedVtxoSum - params.amount;
|
|
501
|
+
selected = {
|
|
502
|
+
inputs: params.selectedVtxos,
|
|
503
|
+
changeAmount: BigInt(changeAmount),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
selected = selectVirtualCoins(virtualCoins, params.amount);
|
|
508
|
+
}
|
|
515
509
|
const selectedLeaf = this.offchainTapscript.forfeit();
|
|
516
510
|
if (!selectedLeaf) {
|
|
517
511
|
throw new Error("Selected leaf not found");
|
|
@@ -641,25 +635,64 @@ export class Wallet extends ReadonlyWallet {
|
|
|
641
635
|
// if no params are provided, use all non expired boarding utxos and offchain vtxos as inputs
|
|
642
636
|
// and send all to the offchain address
|
|
643
637
|
if (!params) {
|
|
638
|
+
const { fees } = await this.arkProvider.getInfo();
|
|
639
|
+
const estimator = new Estimator(fees.intentFee);
|
|
644
640
|
let amount = 0;
|
|
645
641
|
const exitScript = CSVMultisigTapscript.decode(hex.decode(this.boardingTapscript.exitScript));
|
|
646
642
|
const boardingTimelock = exitScript.params.timelock;
|
|
647
643
|
const boardingUtxos = (await this.getBoardingUtxos()).filter((utxo) => !hasBoardingTxExpired(utxo, boardingTimelock));
|
|
648
|
-
|
|
644
|
+
const filteredBoardingUtxos = [];
|
|
645
|
+
for (const utxo of boardingUtxos) {
|
|
646
|
+
const inputFee = estimator.evalOnchainInput({
|
|
647
|
+
amount: BigInt(utxo.value),
|
|
648
|
+
});
|
|
649
|
+
if (inputFee.value >= utxo.value) {
|
|
650
|
+
// skip if fees are greater than the utxo value
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
filteredBoardingUtxos.push(utxo);
|
|
654
|
+
amount += utxo.value - inputFee.satoshis;
|
|
655
|
+
}
|
|
649
656
|
const vtxos = await this.getVtxos({ withRecoverable: true });
|
|
650
|
-
|
|
651
|
-
const
|
|
657
|
+
const filteredVtxos = [];
|
|
658
|
+
for (const vtxo of vtxos) {
|
|
659
|
+
const inputFee = estimator.evalOffchainInput({
|
|
660
|
+
amount: BigInt(vtxo.value),
|
|
661
|
+
type: vtxo.virtualStatus.state === "swept"
|
|
662
|
+
? "recoverable"
|
|
663
|
+
: "vtxo",
|
|
664
|
+
weight: 0,
|
|
665
|
+
birth: vtxo.createdAt,
|
|
666
|
+
expiry: vtxo.virtualStatus.batchExpiry
|
|
667
|
+
? new Date(vtxo.virtualStatus.batchExpiry * 1000)
|
|
668
|
+
: new Date(),
|
|
669
|
+
});
|
|
670
|
+
if (inputFee.value >= vtxo.value) {
|
|
671
|
+
// skip if fees are greater than the vtxo value
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
filteredVtxos.push(vtxo);
|
|
675
|
+
amount += vtxo.value - inputFee.satoshis;
|
|
676
|
+
}
|
|
677
|
+
const inputs = [...filteredBoardingUtxos, ...filteredVtxos];
|
|
652
678
|
if (inputs.length === 0) {
|
|
653
679
|
throw new Error("No inputs found");
|
|
654
680
|
}
|
|
681
|
+
const output = {
|
|
682
|
+
address: await this.getAddress(),
|
|
683
|
+
amount: BigInt(amount),
|
|
684
|
+
};
|
|
685
|
+
const outputFee = estimator.evalOffchainOutput({
|
|
686
|
+
amount: output.amount,
|
|
687
|
+
script: hex.encode(ArkAddress.decode(output.address).pkScript),
|
|
688
|
+
});
|
|
689
|
+
output.amount -= BigInt(outputFee.satoshis);
|
|
690
|
+
if (output.amount <= this.dustAmount) {
|
|
691
|
+
throw new Error("Output amount is below dust limit");
|
|
692
|
+
}
|
|
655
693
|
params = {
|
|
656
694
|
inputs,
|
|
657
|
-
outputs: [
|
|
658
|
-
{
|
|
659
|
-
address: await this.getAddress(),
|
|
660
|
-
amount: BigInt(amount),
|
|
661
|
-
},
|
|
662
|
-
],
|
|
695
|
+
outputs: [output],
|
|
663
696
|
};
|
|
664
697
|
}
|
|
665
698
|
const onchainOutputIndexes = [];
|