@arkade-os/sdk 0.0.16
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 +312 -0
- package/dist/cjs/arknote/index.js +86 -0
- package/dist/cjs/forfeit.js +38 -0
- package/dist/cjs/identity/inMemoryKey.js +40 -0
- package/dist/cjs/identity/index.js +2 -0
- package/dist/cjs/index.js +48 -0
- package/dist/cjs/musig2/index.js +10 -0
- package/dist/cjs/musig2/keys.js +57 -0
- package/dist/cjs/musig2/nonces.js +44 -0
- package/dist/cjs/musig2/sign.js +102 -0
- package/dist/cjs/networks.js +26 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/providers/ark.js +530 -0
- package/dist/cjs/providers/onchain.js +61 -0
- package/dist/cjs/script/address.js +45 -0
- package/dist/cjs/script/base.js +51 -0
- package/dist/cjs/script/default.js +40 -0
- package/dist/cjs/script/tapscript.js +528 -0
- package/dist/cjs/script/vhtlc.js +84 -0
- package/dist/cjs/tree/signingSession.js +238 -0
- package/dist/cjs/tree/validation.js +184 -0
- package/dist/cjs/tree/vtxoTree.js +197 -0
- package/dist/cjs/utils/bip21.js +114 -0
- package/dist/cjs/utils/coinselect.js +73 -0
- package/dist/cjs/utils/psbt.js +124 -0
- package/dist/cjs/utils/transactionHistory.js +148 -0
- package/dist/cjs/utils/txSizeEstimator.js +95 -0
- package/dist/cjs/wallet/index.js +8 -0
- package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +153 -0
- package/dist/cjs/wallet/serviceWorker/db/vtxo/index.js +2 -0
- package/dist/cjs/wallet/serviceWorker/request.js +75 -0
- package/dist/cjs/wallet/serviceWorker/response.js +187 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +332 -0
- package/dist/cjs/wallet/serviceWorker/worker.js +452 -0
- package/dist/cjs/wallet/wallet.js +720 -0
- package/dist/esm/arknote/index.js +81 -0
- package/dist/esm/forfeit.js +35 -0
- package/dist/esm/identity/inMemoryKey.js +36 -0
- package/dist/esm/identity/index.js +1 -0
- package/dist/esm/index.js +39 -0
- package/dist/esm/musig2/index.js +3 -0
- package/dist/esm/musig2/keys.js +21 -0
- package/dist/esm/musig2/nonces.js +8 -0
- package/dist/esm/musig2/sign.js +63 -0
- package/dist/esm/networks.js +22 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/providers/ark.js +526 -0
- package/dist/esm/providers/onchain.js +57 -0
- package/dist/esm/script/address.js +41 -0
- package/dist/esm/script/base.js +46 -0
- package/dist/esm/script/default.js +37 -0
- package/dist/esm/script/tapscript.js +491 -0
- package/dist/esm/script/vhtlc.js +81 -0
- package/dist/esm/tree/signingSession.js +200 -0
- package/dist/esm/tree/validation.js +179 -0
- package/dist/esm/tree/vtxoTree.js +157 -0
- package/dist/esm/utils/bip21.js +110 -0
- package/dist/esm/utils/coinselect.js +69 -0
- package/dist/esm/utils/psbt.js +118 -0
- package/dist/esm/utils/transactionHistory.js +145 -0
- package/dist/esm/utils/txSizeEstimator.js +91 -0
- package/dist/esm/wallet/index.js +5 -0
- package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +149 -0
- package/dist/esm/wallet/serviceWorker/db/vtxo/index.js +1 -0
- package/dist/esm/wallet/serviceWorker/request.js +72 -0
- package/dist/esm/wallet/serviceWorker/response.js +184 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +328 -0
- package/dist/esm/wallet/serviceWorker/worker.js +448 -0
- package/dist/esm/wallet/wallet.js +716 -0
- package/dist/types/arknote/index.d.ts +17 -0
- package/dist/types/forfeit.d.ts +15 -0
- package/dist/types/identity/inMemoryKey.d.ts +12 -0
- package/dist/types/identity/index.d.ts +7 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/musig2/index.d.ts +4 -0
- package/dist/types/musig2/keys.d.ts +9 -0
- package/dist/types/musig2/nonces.d.ts +13 -0
- package/dist/types/musig2/sign.d.ts +27 -0
- package/dist/types/networks.d.ts +16 -0
- package/dist/types/providers/ark.d.ts +126 -0
- package/dist/types/providers/onchain.d.ts +36 -0
- package/dist/types/script/address.d.ts +10 -0
- package/dist/types/script/base.d.ts +26 -0
- package/dist/types/script/default.d.ts +19 -0
- package/dist/types/script/tapscript.d.ts +94 -0
- package/dist/types/script/vhtlc.d.ts +31 -0
- package/dist/types/tree/signingSession.d.ts +32 -0
- package/dist/types/tree/validation.d.ts +22 -0
- package/dist/types/tree/vtxoTree.d.ts +32 -0
- package/dist/types/utils/bip21.d.ts +21 -0
- package/dist/types/utils/coinselect.d.ts +21 -0
- package/dist/types/utils/psbt.d.ts +11 -0
- package/dist/types/utils/transactionHistory.d.ts +2 -0
- package/dist/types/utils/txSizeEstimator.d.ts +27 -0
- package/dist/types/wallet/index.d.ts +122 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +18 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +12 -0
- package/dist/types/wallet/serviceWorker/request.d.ts +68 -0
- package/dist/types/wallet/serviceWorker/response.d.ts +107 -0
- package/dist/types/wallet/serviceWorker/wallet.d.ts +23 -0
- package/dist/types/wallet/serviceWorker/worker.d.ts +26 -0
- package/dist/types/wallet/wallet.d.ts +42 -0
- package/package.json +88 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Select coins to reach a target amount, prioritizing those closer to expiry
|
|
3
|
+
* @param coins List of coins to select from
|
|
4
|
+
* @param targetAmount Target amount to reach in satoshis
|
|
5
|
+
* @returns Selected coins and change amount, or null if insufficient funds
|
|
6
|
+
*/
|
|
7
|
+
export function selectCoins(coins, targetAmount) {
|
|
8
|
+
// Sort coins by amount (descending)
|
|
9
|
+
const sortedCoins = [...coins].sort((a, b) => b.value - a.value);
|
|
10
|
+
const selectedCoins = [];
|
|
11
|
+
let selectedAmount = 0;
|
|
12
|
+
// Select coins until we have enough
|
|
13
|
+
for (const coin of sortedCoins) {
|
|
14
|
+
selectedCoins.push(coin);
|
|
15
|
+
selectedAmount += coin.value;
|
|
16
|
+
if (selectedAmount >= targetAmount) {
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// Check if we have enough
|
|
21
|
+
if (selectedAmount < targetAmount) {
|
|
22
|
+
return { inputs: null, changeAmount: 0 };
|
|
23
|
+
}
|
|
24
|
+
// Calculate change
|
|
25
|
+
const changeAmount = selectedAmount - targetAmount;
|
|
26
|
+
return {
|
|
27
|
+
inputs: selectedCoins,
|
|
28
|
+
changeAmount,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Select virtual coins to reach a target amount, prioritizing those closer to expiry
|
|
33
|
+
* @param coins List of virtual coins to select from
|
|
34
|
+
* @param targetAmount Target amount to reach in satoshis
|
|
35
|
+
* @returns Selected coins and change amount, or null if insufficient funds
|
|
36
|
+
*/
|
|
37
|
+
export function selectVirtualCoins(coins, targetAmount) {
|
|
38
|
+
// Sort VTXOs by expiry (ascending) and amount (descending)
|
|
39
|
+
const sortedCoins = [...coins].sort((a, b) => {
|
|
40
|
+
// First sort by expiry if available
|
|
41
|
+
const expiryA = a.virtualStatus.batchExpiry || Number.MAX_SAFE_INTEGER;
|
|
42
|
+
const expiryB = b.virtualStatus.batchExpiry || Number.MAX_SAFE_INTEGER;
|
|
43
|
+
if (expiryA !== expiryB) {
|
|
44
|
+
return expiryA - expiryB; // Earlier expiry first
|
|
45
|
+
}
|
|
46
|
+
// Then sort by amount
|
|
47
|
+
return b.value - a.value; // Larger amount first
|
|
48
|
+
});
|
|
49
|
+
const selectedCoins = [];
|
|
50
|
+
let selectedAmount = 0;
|
|
51
|
+
// Select coins until we have enough
|
|
52
|
+
for (const coin of sortedCoins) {
|
|
53
|
+
selectedCoins.push(coin);
|
|
54
|
+
selectedAmount += coin.value;
|
|
55
|
+
if (selectedAmount >= targetAmount) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Check if we have enough
|
|
60
|
+
if (selectedAmount < targetAmount) {
|
|
61
|
+
return { inputs: null, changeAmount: 0 };
|
|
62
|
+
}
|
|
63
|
+
// Calculate change
|
|
64
|
+
const changeAmount = selectedAmount - targetAmount;
|
|
65
|
+
return {
|
|
66
|
+
inputs: selectedCoins,
|
|
67
|
+
changeAmount,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { DEFAULT_SEQUENCE, RawWitness, Transaction } from "@scure/btc-signer";
|
|
2
|
+
import { CLTVMultisigTapscript, decodeTapscript } from '../script/tapscript.js';
|
|
3
|
+
import { scriptFromTapLeafScript, VtxoScript, } from '../script/base.js';
|
|
4
|
+
import { ArkAddress } from '../script/address.js';
|
|
5
|
+
import { hex } from "@scure/base";
|
|
6
|
+
const ARK_UNKNOWN_KEY_TYPE = 255;
|
|
7
|
+
// Constant for condition witness key prefix
|
|
8
|
+
export const CONDITION_WITNESS_KEY_PREFIX = new TextEncoder().encode("condition");
|
|
9
|
+
export const VTXO_TAPROOT_TREE_KEY_PREFIX = new TextEncoder().encode("taptree");
|
|
10
|
+
export function addVtxoTaprootTree(inIndex, tx, scripts) {
|
|
11
|
+
tx.updateInput(inIndex, {
|
|
12
|
+
unknown: [
|
|
13
|
+
...(tx.getInput(inIndex)?.unknown ?? []),
|
|
14
|
+
[
|
|
15
|
+
{
|
|
16
|
+
type: ARK_UNKNOWN_KEY_TYPE,
|
|
17
|
+
key: VTXO_TAPROOT_TREE_KEY_PREFIX,
|
|
18
|
+
},
|
|
19
|
+
encodeTaprootTree(scripts),
|
|
20
|
+
],
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function addConditionWitness(inIndex, tx, witness) {
|
|
25
|
+
const witnessBytes = RawWitness.encode(witness);
|
|
26
|
+
tx.updateInput(inIndex, {
|
|
27
|
+
unknown: [
|
|
28
|
+
...(tx.getInput(inIndex)?.unknown ?? []),
|
|
29
|
+
[
|
|
30
|
+
{
|
|
31
|
+
type: ARK_UNKNOWN_KEY_TYPE,
|
|
32
|
+
key: CONDITION_WITNESS_KEY_PREFIX,
|
|
33
|
+
},
|
|
34
|
+
witnessBytes,
|
|
35
|
+
],
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function createVirtualTx(inputs, outputs) {
|
|
40
|
+
let lockTime;
|
|
41
|
+
for (const input of inputs) {
|
|
42
|
+
const tapscript = decodeTapscript(scriptFromTapLeafScript(input.tapLeafScript));
|
|
43
|
+
if (CLTVMultisigTapscript.is(tapscript)) {
|
|
44
|
+
lockTime = Number(tapscript.params.absoluteTimelock);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const tx = new Transaction({
|
|
48
|
+
allowUnknown: true,
|
|
49
|
+
lockTime,
|
|
50
|
+
});
|
|
51
|
+
for (const [i, input] of inputs.entries()) {
|
|
52
|
+
tx.addInput({
|
|
53
|
+
txid: input.txid,
|
|
54
|
+
index: input.vout,
|
|
55
|
+
sequence: lockTime ? DEFAULT_SEQUENCE - 1 : undefined,
|
|
56
|
+
witnessUtxo: {
|
|
57
|
+
script: VtxoScript.decode(input.scripts).pkScript,
|
|
58
|
+
amount: BigInt(input.value),
|
|
59
|
+
},
|
|
60
|
+
tapLeafScript: [input.tapLeafScript],
|
|
61
|
+
});
|
|
62
|
+
// add BIP371 encoded taproot tree to the unknown key field
|
|
63
|
+
addVtxoTaprootTree(i, tx, input.scripts.map(hex.decode));
|
|
64
|
+
}
|
|
65
|
+
for (const output of outputs) {
|
|
66
|
+
tx.addOutput({
|
|
67
|
+
amount: output.amount,
|
|
68
|
+
script: ArkAddress.decode(output.address).pkScript,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return tx;
|
|
72
|
+
}
|
|
73
|
+
function encodeTaprootTree(leaves) {
|
|
74
|
+
const chunks = [];
|
|
75
|
+
// Write number of leaves as compact size uint
|
|
76
|
+
chunks.push(encodeCompactSizeUint(leaves.length));
|
|
77
|
+
for (const tapscript of leaves) {
|
|
78
|
+
// Write depth (always 1 for now)
|
|
79
|
+
chunks.push(new Uint8Array([1]));
|
|
80
|
+
// Write leaf version (0xc0 for tapscript)
|
|
81
|
+
chunks.push(new Uint8Array([0xc0]));
|
|
82
|
+
// Write script length and script
|
|
83
|
+
chunks.push(encodeCompactSizeUint(tapscript.length));
|
|
84
|
+
chunks.push(tapscript);
|
|
85
|
+
}
|
|
86
|
+
// Concatenate all chunks
|
|
87
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
88
|
+
const result = new Uint8Array(totalLength);
|
|
89
|
+
let offset = 0;
|
|
90
|
+
for (const chunk of chunks) {
|
|
91
|
+
result.set(chunk, offset);
|
|
92
|
+
offset += chunk.length;
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
function encodeCompactSizeUint(value) {
|
|
97
|
+
if (value < 0xfd) {
|
|
98
|
+
return new Uint8Array([value]);
|
|
99
|
+
}
|
|
100
|
+
else if (value <= 0xffff) {
|
|
101
|
+
const buffer = new Uint8Array(3);
|
|
102
|
+
buffer[0] = 0xfd;
|
|
103
|
+
new DataView(buffer.buffer).setUint16(1, value, true);
|
|
104
|
+
return buffer;
|
|
105
|
+
}
|
|
106
|
+
else if (value <= 0xffffffff) {
|
|
107
|
+
const buffer = new Uint8Array(5);
|
|
108
|
+
buffer[0] = 0xfe;
|
|
109
|
+
new DataView(buffer.buffer).setUint32(1, value, true);
|
|
110
|
+
return buffer;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const buffer = new Uint8Array(9);
|
|
114
|
+
buffer[0] = 0xff;
|
|
115
|
+
new DataView(buffer.buffer).setBigUint64(1, BigInt(value), true);
|
|
116
|
+
return buffer;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { TxType } from '../wallet/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Helper function to find vtxos that were spent in a settlement
|
|
4
|
+
*/
|
|
5
|
+
function findVtxosSpentInSettlement(vtxos, vtxo) {
|
|
6
|
+
if (vtxo.virtualStatus.state === "pending") {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
return vtxos.filter((v) => {
|
|
10
|
+
if (!v.spentBy)
|
|
11
|
+
return false;
|
|
12
|
+
return v.spentBy === vtxo.virtualStatus.batchTxID;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Helper function to find vtxos that were spent in a payment
|
|
17
|
+
*/
|
|
18
|
+
function findVtxosSpentInPayment(vtxos, vtxo) {
|
|
19
|
+
return vtxos.filter((v) => {
|
|
20
|
+
if (!v.spentBy)
|
|
21
|
+
return false;
|
|
22
|
+
return v.spentBy === vtxo.txid;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Helper function to find vtxos that resulted from a spentBy transaction
|
|
27
|
+
*/
|
|
28
|
+
function findVtxosResultedFromSpentBy(vtxos, spentBy) {
|
|
29
|
+
return vtxos.filter((v) => {
|
|
30
|
+
if (v.virtualStatus.state !== "pending" &&
|
|
31
|
+
v.virtualStatus.batchTxID === spentBy) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return v.txid === spentBy;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Helper function to reduce vtxos to their total amount
|
|
39
|
+
*/
|
|
40
|
+
function reduceVtxosAmount(vtxos) {
|
|
41
|
+
return vtxos.reduce((sum, v) => sum + v.value, 0);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Helper function to get a vtxo from a list of vtxos
|
|
45
|
+
*/
|
|
46
|
+
function getVtxo(resultedVtxos, spentVtxos) {
|
|
47
|
+
if (resultedVtxos.length === 0) {
|
|
48
|
+
return spentVtxos[0];
|
|
49
|
+
}
|
|
50
|
+
return resultedVtxos[0];
|
|
51
|
+
}
|
|
52
|
+
export function vtxosToTxs(spendable, spent, boardingRounds) {
|
|
53
|
+
const txs = [];
|
|
54
|
+
// Receive case
|
|
55
|
+
// All vtxos are received unless:
|
|
56
|
+
// - they resulted from a settlement (either boarding or refresh)
|
|
57
|
+
// - they are the change of a spend tx
|
|
58
|
+
let vtxosLeftToCheck = [...spent];
|
|
59
|
+
for (const vtxo of [...spendable, ...spent]) {
|
|
60
|
+
if (vtxo.virtualStatus.state !== "pending" &&
|
|
61
|
+
boardingRounds.has(vtxo.virtualStatus.batchTxID || "")) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const settleVtxos = findVtxosSpentInSettlement(vtxosLeftToCheck, vtxo);
|
|
65
|
+
vtxosLeftToCheck = removeVtxosFromList(vtxosLeftToCheck, settleVtxos);
|
|
66
|
+
const settleAmount = reduceVtxosAmount(settleVtxos);
|
|
67
|
+
if (vtxo.value <= settleAmount) {
|
|
68
|
+
continue; // settlement or change, ignore
|
|
69
|
+
}
|
|
70
|
+
const spentVtxos = findVtxosSpentInPayment(vtxosLeftToCheck, vtxo);
|
|
71
|
+
vtxosLeftToCheck = removeVtxosFromList(vtxosLeftToCheck, spentVtxos);
|
|
72
|
+
const spentAmount = reduceVtxosAmount(spentVtxos);
|
|
73
|
+
if (vtxo.value <= spentAmount) {
|
|
74
|
+
continue; // settlement or change, ignore
|
|
75
|
+
}
|
|
76
|
+
const txKey = {
|
|
77
|
+
roundTxid: vtxo.virtualStatus.batchTxID || "",
|
|
78
|
+
boardingTxid: "",
|
|
79
|
+
redeemTxid: "",
|
|
80
|
+
};
|
|
81
|
+
let settled = vtxo.virtualStatus.state !== "pending";
|
|
82
|
+
if (vtxo.virtualStatus.state === "pending") {
|
|
83
|
+
txKey.redeemTxid = vtxo.txid;
|
|
84
|
+
if (vtxo.spentBy) {
|
|
85
|
+
settled = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
txs.push({
|
|
89
|
+
key: txKey,
|
|
90
|
+
amount: vtxo.value - settleAmount - spentAmount,
|
|
91
|
+
type: TxType.TxReceived,
|
|
92
|
+
createdAt: vtxo.createdAt.getTime(),
|
|
93
|
+
settled,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// send case
|
|
97
|
+
// All "spentBy" vtxos are payments unless:
|
|
98
|
+
// - they are settlements
|
|
99
|
+
// aggregate spent by spentId
|
|
100
|
+
const vtxosBySpentBy = new Map();
|
|
101
|
+
for (const v of spent) {
|
|
102
|
+
if (!v.spentBy)
|
|
103
|
+
continue;
|
|
104
|
+
if (!vtxosBySpentBy.has(v.spentBy)) {
|
|
105
|
+
vtxosBySpentBy.set(v.spentBy, []);
|
|
106
|
+
}
|
|
107
|
+
const currentVtxos = vtxosBySpentBy.get(v.spentBy);
|
|
108
|
+
vtxosBySpentBy.set(v.spentBy, [...currentVtxos, v]);
|
|
109
|
+
}
|
|
110
|
+
for (const [sb, vtxos] of vtxosBySpentBy) {
|
|
111
|
+
const resultedVtxos = findVtxosResultedFromSpentBy([...spendable, ...spent], sb);
|
|
112
|
+
const resultedAmount = reduceVtxosAmount(resultedVtxos);
|
|
113
|
+
const spentAmount = reduceVtxosAmount(vtxos);
|
|
114
|
+
if (spentAmount <= resultedAmount) {
|
|
115
|
+
continue; // settlement or change, ignore
|
|
116
|
+
}
|
|
117
|
+
const vtxo = getVtxo(resultedVtxos, vtxos);
|
|
118
|
+
const txKey = {
|
|
119
|
+
roundTxid: vtxo.virtualStatus.batchTxID || "",
|
|
120
|
+
boardingTxid: "",
|
|
121
|
+
redeemTxid: "",
|
|
122
|
+
};
|
|
123
|
+
if (vtxo.virtualStatus.state === "pending") {
|
|
124
|
+
txKey.redeemTxid = vtxo.txid;
|
|
125
|
+
}
|
|
126
|
+
txs.push({
|
|
127
|
+
key: txKey,
|
|
128
|
+
amount: spentAmount - resultedAmount,
|
|
129
|
+
type: TxType.TxSent,
|
|
130
|
+
createdAt: vtxo.createdAt.getTime(),
|
|
131
|
+
settled: true,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return txs;
|
|
135
|
+
}
|
|
136
|
+
function removeVtxosFromList(vtxos, vtxosToRemove) {
|
|
137
|
+
return vtxos.filter((v) => {
|
|
138
|
+
for (const vtxoToRemove of vtxosToRemove) {
|
|
139
|
+
if (v.txid === vtxoToRemove.txid && v.vout === vtxoToRemove.vout) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export class TxWeightEstimator {
|
|
2
|
+
constructor(hasWitness, inputCount, outputCount, inputSize, inputWitnessSize, outputSize) {
|
|
3
|
+
this.hasWitness = hasWitness;
|
|
4
|
+
this.inputCount = inputCount;
|
|
5
|
+
this.outputCount = outputCount;
|
|
6
|
+
this.inputSize = inputSize;
|
|
7
|
+
this.inputWitnessSize = inputWitnessSize;
|
|
8
|
+
this.outputSize = outputSize;
|
|
9
|
+
}
|
|
10
|
+
static create() {
|
|
11
|
+
return new TxWeightEstimator(false, 0, 0, 0, 0, 0);
|
|
12
|
+
}
|
|
13
|
+
addKeySpendInput(isDefault = true) {
|
|
14
|
+
this.inputCount++;
|
|
15
|
+
this.inputWitnessSize += 64 + 1 + (isDefault ? 0 : 1);
|
|
16
|
+
this.inputSize += TxWeightEstimator.INPUT_SIZE;
|
|
17
|
+
this.hasWitness = true;
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
addP2PKHInput() {
|
|
21
|
+
this.inputCount++;
|
|
22
|
+
this.inputWitnessSize++;
|
|
23
|
+
this.inputSize +=
|
|
24
|
+
TxWeightEstimator.INPUT_SIZE +
|
|
25
|
+
TxWeightEstimator.P2PKH_SCRIPT_SIG_SIZE;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
addTapscriptInput(leafWitnessSize, leafScriptSize, leafControlBlockSize) {
|
|
29
|
+
const controlBlockWitnessSize = 1 +
|
|
30
|
+
TxWeightEstimator.BASE_CONTROL_BLOCK_SIZE +
|
|
31
|
+
1 +
|
|
32
|
+
leafScriptSize +
|
|
33
|
+
1 +
|
|
34
|
+
leafControlBlockSize;
|
|
35
|
+
this.inputCount++;
|
|
36
|
+
this.inputWitnessSize += leafWitnessSize + controlBlockWitnessSize;
|
|
37
|
+
this.inputSize += TxWeightEstimator.INPUT_SIZE;
|
|
38
|
+
this.hasWitness = true;
|
|
39
|
+
this.inputCount++;
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
addP2WKHOutput() {
|
|
43
|
+
this.outputCount++;
|
|
44
|
+
this.outputSize +=
|
|
45
|
+
TxWeightEstimator.OUTPUT_SIZE + TxWeightEstimator.P2WKH_OUTPUT_SIZE;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
vsize() {
|
|
49
|
+
const getVarIntSize = (n) => {
|
|
50
|
+
if (n < 0xfd)
|
|
51
|
+
return 1;
|
|
52
|
+
if (n < 0xffff)
|
|
53
|
+
return 3;
|
|
54
|
+
if (n < 0xffffffff)
|
|
55
|
+
return 5;
|
|
56
|
+
return 9;
|
|
57
|
+
};
|
|
58
|
+
const inputCount = getVarIntSize(this.inputCount);
|
|
59
|
+
const outputCount = getVarIntSize(this.outputCount);
|
|
60
|
+
// Calculate the size of the transaction without witness data
|
|
61
|
+
const txSizeStripped = TxWeightEstimator.BASE_TX_SIZE +
|
|
62
|
+
inputCount +
|
|
63
|
+
this.inputSize +
|
|
64
|
+
outputCount +
|
|
65
|
+
this.outputSize;
|
|
66
|
+
// Calculate the total weight
|
|
67
|
+
let weight = txSizeStripped * TxWeightEstimator.WITNESS_SCALE_FACTOR;
|
|
68
|
+
// Add witness data if present
|
|
69
|
+
if (this.hasWitness) {
|
|
70
|
+
weight +=
|
|
71
|
+
TxWeightEstimator.WITNESS_HEADER_SIZE + this.inputWitnessSize;
|
|
72
|
+
}
|
|
73
|
+
// Convert weight to vsize (weight / 4, rounded up)
|
|
74
|
+
return vsize(weight);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
TxWeightEstimator.P2PKH_SCRIPT_SIG_SIZE = 1 + 73 + 1 + 33;
|
|
78
|
+
TxWeightEstimator.INPUT_SIZE = 32 + 4 + 1 + 4;
|
|
79
|
+
TxWeightEstimator.BASE_CONTROL_BLOCK_SIZE = 1 + 32;
|
|
80
|
+
TxWeightEstimator.OUTPUT_SIZE = 8 + 1;
|
|
81
|
+
TxWeightEstimator.P2WKH_OUTPUT_SIZE = 1 + 1 + 20;
|
|
82
|
+
TxWeightEstimator.BASE_TX_SIZE = 8 + 2; // Version + LockTime
|
|
83
|
+
TxWeightEstimator.WITNESS_HEADER_SIZE = 2; // Flag + Marker
|
|
84
|
+
TxWeightEstimator.WITNESS_SCALE_FACTOR = 4;
|
|
85
|
+
const vsize = (weight) => {
|
|
86
|
+
const value = BigInt(Math.ceil(weight / TxWeightEstimator.WITNESS_SCALE_FACTOR));
|
|
87
|
+
return {
|
|
88
|
+
value,
|
|
89
|
+
fee: (feeRate) => feeRate * value,
|
|
90
|
+
};
|
|
91
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export class IndexedDBVtxoRepository {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.db = null;
|
|
4
|
+
}
|
|
5
|
+
static delete() {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
try {
|
|
8
|
+
const request = indexedDB.deleteDatabase(IndexedDBVtxoRepository.DB_NAME);
|
|
9
|
+
request.onblocked = () => {
|
|
10
|
+
// If blocked, wait a bit and try again
|
|
11
|
+
setTimeout(() => {
|
|
12
|
+
const retryRequest = indexedDB.deleteDatabase(IndexedDBVtxoRepository.DB_NAME);
|
|
13
|
+
retryRequest.onsuccess = () => resolve();
|
|
14
|
+
retryRequest.onerror = () => reject(retryRequest.error ||
|
|
15
|
+
new Error("Failed to delete database"));
|
|
16
|
+
}, 100);
|
|
17
|
+
};
|
|
18
|
+
request.onsuccess = () => {
|
|
19
|
+
resolve();
|
|
20
|
+
};
|
|
21
|
+
request.onerror = () => {
|
|
22
|
+
reject(request.error || new Error("Failed to delete database"));
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
reject(error instanceof Error
|
|
27
|
+
? error
|
|
28
|
+
: new Error("Failed to delete database"));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async close() {
|
|
33
|
+
if (this.db) {
|
|
34
|
+
this.db.close();
|
|
35
|
+
this.db = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async open() {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const request = indexedDB.open(IndexedDBVtxoRepository.DB_NAME, IndexedDBVtxoRepository.DB_VERSION);
|
|
41
|
+
request.onerror = () => {
|
|
42
|
+
reject(request.error);
|
|
43
|
+
};
|
|
44
|
+
request.onsuccess = () => {
|
|
45
|
+
this.db = request.result;
|
|
46
|
+
resolve();
|
|
47
|
+
};
|
|
48
|
+
request.onupgradeneeded = (event) => {
|
|
49
|
+
const db = event.target.result;
|
|
50
|
+
if (!db.objectStoreNames.contains(IndexedDBVtxoRepository.STORE_NAME)) {
|
|
51
|
+
const store = db.createObjectStore(IndexedDBVtxoRepository.STORE_NAME, {
|
|
52
|
+
keyPath: ["txid", "vout"],
|
|
53
|
+
});
|
|
54
|
+
store.createIndex("state", "virtualStatus.state", {
|
|
55
|
+
unique: false,
|
|
56
|
+
});
|
|
57
|
+
store.createIndex("spentBy", "spentBy", {
|
|
58
|
+
unique: false,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async addOrUpdate(vtxos) {
|
|
65
|
+
if (!this.db) {
|
|
66
|
+
throw new Error("Database not opened");
|
|
67
|
+
}
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const transaction = this.db.transaction(IndexedDBVtxoRepository.STORE_NAME, "readwrite");
|
|
70
|
+
const store = transaction.objectStore(IndexedDBVtxoRepository.STORE_NAME);
|
|
71
|
+
const requests = vtxos.map((vtxo) => {
|
|
72
|
+
return new Promise((resolveRequest, rejectRequest) => {
|
|
73
|
+
const request = store.put(vtxo);
|
|
74
|
+
request.onsuccess = () => resolveRequest();
|
|
75
|
+
request.onerror = () => rejectRequest(request.error);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
Promise.all(requests)
|
|
79
|
+
.then(() => resolve())
|
|
80
|
+
.catch(reject);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async deleteAll() {
|
|
84
|
+
if (!this.db) {
|
|
85
|
+
throw new Error("Database not opened");
|
|
86
|
+
}
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const transaction = this.db.transaction(IndexedDBVtxoRepository.STORE_NAME, "readwrite");
|
|
89
|
+
const store = transaction.objectStore(IndexedDBVtxoRepository.STORE_NAME);
|
|
90
|
+
const request = store.clear();
|
|
91
|
+
request.onsuccess = () => resolve();
|
|
92
|
+
request.onerror = () => reject(request.error);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async getSpendableVtxos() {
|
|
96
|
+
if (!this.db) {
|
|
97
|
+
throw new Error("Database not opened");
|
|
98
|
+
}
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const transaction = this.db.transaction(IndexedDBVtxoRepository.STORE_NAME, "readonly");
|
|
101
|
+
const store = transaction.objectStore(IndexedDBVtxoRepository.STORE_NAME);
|
|
102
|
+
const spentByIndex = store.index("spentBy");
|
|
103
|
+
// Get vtxos where spentBy is empty string
|
|
104
|
+
const request = spentByIndex.getAll(IDBKeyRange.only(""));
|
|
105
|
+
request.onsuccess = () => {
|
|
106
|
+
resolve(request.result);
|
|
107
|
+
};
|
|
108
|
+
request.onerror = () => reject(request.error);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async getAllVtxos() {
|
|
112
|
+
if (!this.db) {
|
|
113
|
+
throw new Error("Database not opened");
|
|
114
|
+
}
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const transaction = this.db.transaction(IndexedDBVtxoRepository.STORE_NAME, "readonly");
|
|
117
|
+
const store = transaction.objectStore(IndexedDBVtxoRepository.STORE_NAME);
|
|
118
|
+
const spentByIndex = store.index("spentBy");
|
|
119
|
+
// Get vtxos where spentBy is empty string
|
|
120
|
+
const spendableRequest = spentByIndex.getAll(IDBKeyRange.only(""));
|
|
121
|
+
// Get all vtxos where spentBy is populated
|
|
122
|
+
const spentRequest = spentByIndex.getAll(IDBKeyRange.lowerBound("", true));
|
|
123
|
+
Promise.all([
|
|
124
|
+
new Promise((resolveSpendable, rejectSpendable) => {
|
|
125
|
+
spendableRequest.onsuccess = () => {
|
|
126
|
+
resolveSpendable(spendableRequest.result);
|
|
127
|
+
};
|
|
128
|
+
spendableRequest.onerror = () => rejectSpendable(spendableRequest.error);
|
|
129
|
+
}),
|
|
130
|
+
new Promise((resolveSpent, rejectSpent) => {
|
|
131
|
+
spentRequest.onsuccess = () => {
|
|
132
|
+
resolveSpent(spentRequest.result);
|
|
133
|
+
};
|
|
134
|
+
spentRequest.onerror = () => rejectSpent(spentRequest.error);
|
|
135
|
+
}),
|
|
136
|
+
])
|
|
137
|
+
.then(([spendableVtxos, spentVtxos]) => {
|
|
138
|
+
resolve({
|
|
139
|
+
spendable: spendableVtxos,
|
|
140
|
+
spent: spentVtxos,
|
|
141
|
+
});
|
|
142
|
+
})
|
|
143
|
+
.catch(reject);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
IndexedDBVtxoRepository.DB_NAME = "wallet-db";
|
|
148
|
+
IndexedDBVtxoRepository.STORE_NAME = "vtxos";
|
|
149
|
+
IndexedDBVtxoRepository.DB_VERSION = 1;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export var Request;
|
|
2
|
+
(function (Request) {
|
|
3
|
+
function isBase(message) {
|
|
4
|
+
return (typeof message === "object" && message !== null && "type" in message);
|
|
5
|
+
}
|
|
6
|
+
Request.isBase = isBase;
|
|
7
|
+
function isInitWallet(message) {
|
|
8
|
+
return (message.type === "INIT_WALLET" &&
|
|
9
|
+
"privateKey" in message &&
|
|
10
|
+
typeof message.privateKey === "string" &&
|
|
11
|
+
"arkServerUrl" in message &&
|
|
12
|
+
typeof message.arkServerUrl === "string" &&
|
|
13
|
+
"network" in message &&
|
|
14
|
+
typeof message.network === "string" &&
|
|
15
|
+
("arkServerPublicKey" in message
|
|
16
|
+
? typeof message.arkServerPublicKey === "string" ||
|
|
17
|
+
message.arkServerPublicKey === undefined
|
|
18
|
+
: true));
|
|
19
|
+
}
|
|
20
|
+
Request.isInitWallet = isInitWallet;
|
|
21
|
+
function isSettle(message) {
|
|
22
|
+
return message.type === "SETTLE";
|
|
23
|
+
}
|
|
24
|
+
Request.isSettle = isSettle;
|
|
25
|
+
function isGetAddress(message) {
|
|
26
|
+
return message.type === "GET_ADDRESS";
|
|
27
|
+
}
|
|
28
|
+
Request.isGetAddress = isGetAddress;
|
|
29
|
+
function isGetAddressInfo(message) {
|
|
30
|
+
return message.type === "GET_ADDRESS_INFO";
|
|
31
|
+
}
|
|
32
|
+
Request.isGetAddressInfo = isGetAddressInfo;
|
|
33
|
+
function isGetBalance(message) {
|
|
34
|
+
return message.type === "GET_BALANCE";
|
|
35
|
+
}
|
|
36
|
+
Request.isGetBalance = isGetBalance;
|
|
37
|
+
function isGetCoins(message) {
|
|
38
|
+
return message.type === "GET_COINS";
|
|
39
|
+
}
|
|
40
|
+
Request.isGetCoins = isGetCoins;
|
|
41
|
+
function isGetVtxos(message) {
|
|
42
|
+
return message.type === "GET_VTXOS";
|
|
43
|
+
}
|
|
44
|
+
Request.isGetVtxos = isGetVtxos;
|
|
45
|
+
function isGetVirtualCoins(message) {
|
|
46
|
+
return message.type === "GET_VIRTUAL_COINS";
|
|
47
|
+
}
|
|
48
|
+
Request.isGetVirtualCoins = isGetVirtualCoins;
|
|
49
|
+
function isGetBoardingUtxos(message) {
|
|
50
|
+
return message.type === "GET_BOARDING_UTXOS";
|
|
51
|
+
}
|
|
52
|
+
Request.isGetBoardingUtxos = isGetBoardingUtxos;
|
|
53
|
+
function isSendBitcoin(message) {
|
|
54
|
+
return (message.type === "SEND_BITCOIN" &&
|
|
55
|
+
"params" in message &&
|
|
56
|
+
message.params !== null &&
|
|
57
|
+
typeof message.params === "object" &&
|
|
58
|
+
"address" in message.params &&
|
|
59
|
+
typeof message.params.address === "string" &&
|
|
60
|
+
"amount" in message.params &&
|
|
61
|
+
typeof message.params.amount === "number");
|
|
62
|
+
}
|
|
63
|
+
Request.isSendBitcoin = isSendBitcoin;
|
|
64
|
+
function isGetTransactionHistory(message) {
|
|
65
|
+
return message.type === "GET_TRANSACTION_HISTORY";
|
|
66
|
+
}
|
|
67
|
+
Request.isGetTransactionHistory = isGetTransactionHistory;
|
|
68
|
+
function isGetStatus(message) {
|
|
69
|
+
return message.type === "GET_STATUS";
|
|
70
|
+
}
|
|
71
|
+
Request.isGetStatus = isGetStatus;
|
|
72
|
+
})(Request || (Request = {}));
|