@arkade-os/sdk 0.3.10 → 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 -9
- 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 +51 -34
- 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 -9
- 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 +51 -34
- 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 +1 -0
- package/dist/types/wallet/ramps.d.ts +2 -1
- package/dist/types/wallet/serviceWorker/worker.d.ts +0 -1
- package/package.json +3 -2
|
@@ -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 = [];
|
|
@@ -657,25 +635,64 @@ export class Wallet extends ReadonlyWallet {
|
|
|
657
635
|
// if no params are provided, use all non expired boarding utxos and offchain vtxos as inputs
|
|
658
636
|
// and send all to the offchain address
|
|
659
637
|
if (!params) {
|
|
638
|
+
const { fees } = await this.arkProvider.getInfo();
|
|
639
|
+
const estimator = new Estimator(fees.intentFee);
|
|
660
640
|
let amount = 0;
|
|
661
641
|
const exitScript = CSVMultisigTapscript.decode(hex.decode(this.boardingTapscript.exitScript));
|
|
662
642
|
const boardingTimelock = exitScript.params.timelock;
|
|
663
643
|
const boardingUtxos = (await this.getBoardingUtxos()).filter((utxo) => !hasBoardingTxExpired(utxo, boardingTimelock));
|
|
664
|
-
|
|
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
|
+
}
|
|
665
656
|
const vtxos = await this.getVtxos({ withRecoverable: true });
|
|
666
|
-
|
|
667
|
-
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];
|
|
668
678
|
if (inputs.length === 0) {
|
|
669
679
|
throw new Error("No inputs found");
|
|
670
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
|
+
}
|
|
671
693
|
params = {
|
|
672
694
|
inputs,
|
|
673
|
-
outputs: [
|
|
674
|
-
{
|
|
675
|
-
address: await this.getAddress(),
|
|
676
|
-
amount: BigInt(amount),
|
|
677
|
-
},
|
|
678
|
-
],
|
|
695
|
+
outputs: [output],
|
|
679
696
|
};
|
|
680
697
|
}
|
|
681
698
|
const onchainOutputIndexes = [];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Environment } from "@marcbachmann/cel-js";
|
|
2
|
+
/**
|
|
3
|
+
* Variable names used in CEL expressions
|
|
4
|
+
*/
|
|
5
|
+
export declare const AmountVariableName = "amount";
|
|
6
|
+
export declare const ExpiryVariableName = "expiry";
|
|
7
|
+
export declare const BirthVariableName = "birth";
|
|
8
|
+
export declare const WeightVariableName = "weight";
|
|
9
|
+
export declare const InputTypeVariableName = "inputType";
|
|
10
|
+
export declare const OutputScriptVariableName = "script";
|
|
11
|
+
/**
|
|
12
|
+
* IntentOutputEnv is the CEL environment for output fee calculation
|
|
13
|
+
* Variables: amount, script
|
|
14
|
+
*/
|
|
15
|
+
export declare const IntentOutputEnv: Environment;
|
|
16
|
+
/**
|
|
17
|
+
* IntentOffchainInputEnv is the CEL environment for offchain input fee calculation
|
|
18
|
+
* Variables: amount, expiry, birth, weight, inputType
|
|
19
|
+
*/
|
|
20
|
+
export declare const IntentOffchainInputEnv: Environment;
|
|
21
|
+
/**
|
|
22
|
+
* IntentOnchainInputEnv is the CEL environment for onchain input fee calculation
|
|
23
|
+
* Variables: amount
|
|
24
|
+
*/
|
|
25
|
+
export declare const IntentOnchainInputEnv: Environment;
|