@arkade-os/sdk 0.1.4 → 0.2.0
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 +156 -174
- package/dist/cjs/arknote/index.js +61 -58
- package/dist/cjs/bip322/errors.js +13 -0
- package/dist/cjs/bip322/index.js +178 -0
- package/dist/cjs/forfeit.js +14 -25
- package/dist/cjs/identity/singleKey.js +68 -0
- package/dist/cjs/index.js +41 -17
- package/dist/cjs/providers/ark.js +253 -317
- package/dist/cjs/providers/indexer.js +525 -0
- package/dist/cjs/providers/onchain.js +193 -15
- package/dist/cjs/script/address.js +48 -17
- package/dist/cjs/script/base.js +120 -3
- package/dist/cjs/script/default.js +18 -4
- package/dist/cjs/script/tapscript.js +46 -14
- package/dist/cjs/script/vhtlc.js +27 -7
- package/dist/cjs/tree/signingSession.js +63 -106
- package/dist/cjs/tree/txTree.js +193 -0
- package/dist/cjs/tree/validation.js +79 -155
- package/dist/cjs/utils/anchor.js +35 -0
- package/dist/cjs/utils/arkTransaction.js +108 -0
- package/dist/cjs/utils/transactionHistory.js +84 -72
- package/dist/cjs/utils/txSizeEstimator.js +12 -0
- package/dist/cjs/utils/unknownFields.js +211 -0
- package/dist/cjs/wallet/index.js +12 -0
- package/dist/cjs/wallet/onchain.js +201 -0
- package/dist/cjs/wallet/ramps.js +95 -0
- package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +32 -0
- package/dist/cjs/wallet/serviceWorker/request.js +15 -12
- package/dist/cjs/wallet/serviceWorker/response.js +22 -27
- package/dist/cjs/wallet/serviceWorker/utils.js +8 -0
- package/dist/cjs/wallet/serviceWorker/wallet.js +58 -34
- package/dist/cjs/wallet/serviceWorker/worker.js +117 -108
- package/dist/cjs/wallet/unroll.js +270 -0
- package/dist/cjs/wallet/wallet.js +701 -454
- package/dist/esm/arknote/index.js +61 -57
- package/dist/esm/bip322/errors.js +9 -0
- package/dist/esm/bip322/index.js +174 -0
- package/dist/esm/forfeit.js +15 -26
- package/dist/esm/identity/singleKey.js +64 -0
- package/dist/esm/index.js +30 -12
- package/dist/esm/providers/ark.js +252 -317
- package/dist/esm/providers/indexer.js +521 -0
- package/dist/esm/providers/onchain.js +193 -15
- package/dist/esm/script/address.js +48 -17
- package/dist/esm/script/base.js +120 -3
- package/dist/esm/script/default.js +18 -4
- package/dist/esm/script/tapscript.js +46 -14
- package/dist/esm/script/vhtlc.js +27 -7
- package/dist/esm/tree/signingSession.js +65 -108
- package/dist/esm/tree/txTree.js +189 -0
- package/dist/esm/tree/validation.js +75 -152
- package/dist/esm/utils/anchor.js +31 -0
- package/dist/esm/utils/arkTransaction.js +105 -0
- package/dist/esm/utils/transactionHistory.js +84 -72
- package/dist/esm/utils/txSizeEstimator.js +12 -0
- package/dist/esm/utils/unknownFields.js +173 -0
- package/dist/esm/wallet/index.js +9 -0
- package/dist/esm/wallet/onchain.js +196 -0
- package/dist/esm/wallet/ramps.js +91 -0
- package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +32 -0
- package/dist/esm/wallet/serviceWorker/request.js +15 -12
- package/dist/esm/wallet/serviceWorker/response.js +22 -27
- package/dist/esm/wallet/serviceWorker/utils.js +8 -0
- package/dist/esm/wallet/serviceWorker/wallet.js +59 -35
- package/dist/esm/wallet/serviceWorker/worker.js +117 -108
- package/dist/esm/wallet/unroll.js +267 -0
- package/dist/esm/wallet/wallet.js +674 -461
- package/dist/types/arknote/index.d.ts +40 -13
- package/dist/types/bip322/errors.d.ts +6 -0
- package/dist/types/bip322/index.d.ts +57 -0
- package/dist/types/forfeit.d.ts +2 -14
- package/dist/types/identity/singleKey.d.ts +27 -0
- package/dist/types/index.d.ts +23 -12
- package/dist/types/providers/ark.d.ts +114 -95
- package/dist/types/providers/indexer.d.ts +186 -0
- package/dist/types/providers/onchain.d.ts +41 -11
- package/dist/types/script/address.d.ts +26 -2
- package/dist/types/script/base.d.ts +13 -3
- package/dist/types/script/default.d.ts +22 -0
- package/dist/types/script/tapscript.d.ts +61 -5
- package/dist/types/script/vhtlc.d.ts +27 -0
- package/dist/types/tree/signingSession.d.ts +5 -5
- package/dist/types/tree/txTree.d.ts +28 -0
- package/dist/types/tree/validation.d.ts +15 -22
- package/dist/types/utils/anchor.d.ts +19 -0
- package/dist/types/utils/arkTransaction.d.ts +27 -0
- package/dist/types/utils/transactionHistory.d.ts +7 -1
- package/dist/types/utils/txSizeEstimator.d.ts +3 -0
- package/dist/types/utils/unknownFields.d.ts +83 -0
- package/dist/types/wallet/index.d.ts +51 -50
- package/dist/types/wallet/onchain.d.ts +49 -0
- package/dist/types/wallet/ramps.d.ts +32 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +2 -0
- package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +2 -0
- package/dist/types/wallet/serviceWorker/request.d.ts +14 -16
- package/dist/types/wallet/serviceWorker/response.d.ts +17 -19
- package/dist/types/wallet/serviceWorker/utils.d.ts +8 -0
- package/dist/types/wallet/serviceWorker/wallet.d.ts +36 -8
- package/dist/types/wallet/serviceWorker/worker.d.ts +7 -3
- package/dist/types/wallet/unroll.d.ts +102 -0
- package/dist/types/wallet/wallet.d.ts +71 -25
- package/package.json +14 -15
- package/dist/cjs/identity/inMemoryKey.js +0 -40
- package/dist/cjs/tree/vtxoTree.js +0 -231
- package/dist/cjs/utils/coinselect.js +0 -73
- package/dist/cjs/utils/psbt.js +0 -137
- package/dist/esm/identity/inMemoryKey.js +0 -36
- package/dist/esm/tree/vtxoTree.js +0 -191
- package/dist/esm/utils/coinselect.js +0 -69
- package/dist/esm/utils/psbt.js +0 -131
- package/dist/types/identity/inMemoryKey.d.ts +0 -12
- package/dist/types/tree/vtxoTree.d.ts +0 -33
- package/dist/types/utils/coinselect.d.ts +0 -21
- package/dist/types/utils/psbt.d.ts +0 -11
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import * as bip68 from "bip68";
|
|
2
|
+
import { RawWitness, ScriptNum } from "@scure/btc-signer";
|
|
3
|
+
import { hex } from "@scure/base";
|
|
4
|
+
/**
|
|
5
|
+
* ArkPsbtFieldKey is the key values for ark psbt fields.
|
|
6
|
+
*/
|
|
7
|
+
export var ArkPsbtFieldKey;
|
|
8
|
+
(function (ArkPsbtFieldKey) {
|
|
9
|
+
ArkPsbtFieldKey["VtxoTaprootTree"] = "taptree";
|
|
10
|
+
ArkPsbtFieldKey["VtxoTreeExpiry"] = "expiry";
|
|
11
|
+
ArkPsbtFieldKey["Cosigner"] = "cosigner";
|
|
12
|
+
ArkPsbtFieldKey["ConditionWitness"] = "condition";
|
|
13
|
+
})(ArkPsbtFieldKey || (ArkPsbtFieldKey = {}));
|
|
14
|
+
/**
|
|
15
|
+
* ArkPsbtFieldKeyType is the type of the ark psbt field key.
|
|
16
|
+
* Every ark psbt field has key type 255.
|
|
17
|
+
*/
|
|
18
|
+
export const ArkPsbtFieldKeyType = 255;
|
|
19
|
+
/**
|
|
20
|
+
* setArkPsbtField appends a new unknown field to the input at inputIndex
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* setArkPsbtField(tx, 0, VtxoTaprootTree, myTaprootTree);
|
|
25
|
+
* setArkPsbtField(tx, 0, VtxoTreeExpiry, myVtxoTreeExpiry);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function setArkPsbtField(tx, inputIndex, coder, value) {
|
|
29
|
+
tx.updateInput(inputIndex, {
|
|
30
|
+
unknown: [
|
|
31
|
+
...(tx.getInput(inputIndex)?.unknown ?? []),
|
|
32
|
+
coder.encode(value),
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* getArkPsbtFields returns all the values of the given coder for the input at inputIndex
|
|
38
|
+
* Multiple fields of the same type can exist in a single input.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const vtxoTaprootTreeFields = getArkPsbtFields(tx, 0, VtxoTaprootTree);
|
|
43
|
+
* console.log(`input has ${vtxoTaprootTreeFields.length} vtxoTaprootTree fields`);
|
|
44
|
+
*/
|
|
45
|
+
export function getArkPsbtFields(tx, inputIndex, coder) {
|
|
46
|
+
const unknown = tx.getInput(inputIndex)?.unknown ?? [];
|
|
47
|
+
const fields = [];
|
|
48
|
+
for (const u of unknown) {
|
|
49
|
+
const v = coder.decode(u);
|
|
50
|
+
if (v)
|
|
51
|
+
fields.push(v);
|
|
52
|
+
}
|
|
53
|
+
return fields;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* VtxoTaprootTree is set to pass all spending leaves of the vtxo input
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const vtxoTaprootTree = VtxoTaprootTree.encode(myTaprootTree);
|
|
61
|
+
*/
|
|
62
|
+
export const VtxoTaprootTree = {
|
|
63
|
+
key: ArkPsbtFieldKey.VtxoTaprootTree,
|
|
64
|
+
encode: (value) => [
|
|
65
|
+
{
|
|
66
|
+
type: ArkPsbtFieldKeyType,
|
|
67
|
+
key: encodedPsbtFieldKey[ArkPsbtFieldKey.VtxoTaprootTree],
|
|
68
|
+
},
|
|
69
|
+
value,
|
|
70
|
+
],
|
|
71
|
+
decode: (value) => nullIfCatch(() => {
|
|
72
|
+
if (!checkKeyIncludes(value[0], ArkPsbtFieldKey.VtxoTaprootTree))
|
|
73
|
+
return null;
|
|
74
|
+
return value[1];
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* ConditionWitness is set to pass the witness data used to finalize the conditionMultisigClosure
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* const conditionWitness = ConditionWitness.encode(myConditionWitness);
|
|
83
|
+
*/
|
|
84
|
+
export const ConditionWitness = {
|
|
85
|
+
key: ArkPsbtFieldKey.ConditionWitness,
|
|
86
|
+
encode: (value) => [
|
|
87
|
+
{
|
|
88
|
+
type: ArkPsbtFieldKeyType,
|
|
89
|
+
key: encodedPsbtFieldKey[ArkPsbtFieldKey.ConditionWitness],
|
|
90
|
+
},
|
|
91
|
+
RawWitness.encode(value),
|
|
92
|
+
],
|
|
93
|
+
decode: (value) => nullIfCatch(() => {
|
|
94
|
+
if (!checkKeyIncludes(value[0], ArkPsbtFieldKey.ConditionWitness))
|
|
95
|
+
return null;
|
|
96
|
+
return RawWitness.decode(value[1]);
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* CosignerPublicKey is set on every TxGraph transactions to identify the musig2 public keys
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* const cosignerPublicKey = CosignerPublicKey.encode(myCosignerPublicKey);
|
|
105
|
+
*/
|
|
106
|
+
export const CosignerPublicKey = {
|
|
107
|
+
key: ArkPsbtFieldKey.Cosigner,
|
|
108
|
+
encode: (value) => [
|
|
109
|
+
{
|
|
110
|
+
type: ArkPsbtFieldKeyType,
|
|
111
|
+
key: new Uint8Array([
|
|
112
|
+
...encodedPsbtFieldKey[ArkPsbtFieldKey.Cosigner],
|
|
113
|
+
value.index,
|
|
114
|
+
]),
|
|
115
|
+
},
|
|
116
|
+
value.key,
|
|
117
|
+
],
|
|
118
|
+
decode: (unknown) => nullIfCatch(() => {
|
|
119
|
+
if (!checkKeyIncludes(unknown[0], ArkPsbtFieldKey.Cosigner))
|
|
120
|
+
return null;
|
|
121
|
+
return {
|
|
122
|
+
index: unknown[0].key[unknown[0].key.length - 1],
|
|
123
|
+
key: unknown[1],
|
|
124
|
+
};
|
|
125
|
+
}),
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* VtxoTreeExpiry is set to pass the expiry time of the input
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* const vtxoTreeExpiry = VtxoTreeExpiry.encode(myVtxoTreeExpiry);
|
|
133
|
+
*/
|
|
134
|
+
export const VtxoTreeExpiry = {
|
|
135
|
+
key: ArkPsbtFieldKey.VtxoTreeExpiry,
|
|
136
|
+
encode: (value) => [
|
|
137
|
+
{
|
|
138
|
+
type: ArkPsbtFieldKeyType,
|
|
139
|
+
key: encodedPsbtFieldKey[ArkPsbtFieldKey.VtxoTreeExpiry],
|
|
140
|
+
},
|
|
141
|
+
ScriptNum(6, true).encode(value.value === 0n ? 0n : value.value),
|
|
142
|
+
],
|
|
143
|
+
decode: (unknown) => nullIfCatch(() => {
|
|
144
|
+
if (!checkKeyIncludes(unknown[0], ArkPsbtFieldKey.VtxoTreeExpiry))
|
|
145
|
+
return null;
|
|
146
|
+
const v = ScriptNum(6, true).decode(unknown[1]);
|
|
147
|
+
if (!v)
|
|
148
|
+
return null;
|
|
149
|
+
const { blocks, seconds } = bip68.decode(Number(v));
|
|
150
|
+
return {
|
|
151
|
+
type: blocks ? "blocks" : "seconds",
|
|
152
|
+
value: BigInt(blocks ?? seconds ?? 0),
|
|
153
|
+
};
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
const encodedPsbtFieldKey = Object.fromEntries(Object.values(ArkPsbtFieldKey).map((key) => [
|
|
157
|
+
key,
|
|
158
|
+
new TextEncoder().encode(key),
|
|
159
|
+
]));
|
|
160
|
+
const nullIfCatch = (fn) => {
|
|
161
|
+
try {
|
|
162
|
+
return fn();
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
function checkKeyIncludes(key, arkPsbtFieldKey) {
|
|
169
|
+
const expected = hex.encode(encodedPsbtFieldKey[arkPsbtFieldKey]);
|
|
170
|
+
return hex
|
|
171
|
+
.encode(new Uint8Array([key.type, ...key.key]))
|
|
172
|
+
.includes(expected);
|
|
173
|
+
}
|
package/dist/esm/wallet/index.js
CHANGED
|
@@ -3,3 +3,12 @@ export var TxType;
|
|
|
3
3
|
TxType["TxSent"] = "SENT";
|
|
4
4
|
TxType["TxReceived"] = "RECEIVED";
|
|
5
5
|
})(TxType || (TxType = {}));
|
|
6
|
+
export function isSpendable(vtxo) {
|
|
7
|
+
return vtxo.spentBy === undefined || vtxo.spentBy === "";
|
|
8
|
+
}
|
|
9
|
+
export function isRecoverable(vtxo) {
|
|
10
|
+
return vtxo.virtualStatus.state === "swept" && isSpendable(vtxo);
|
|
11
|
+
}
|
|
12
|
+
export function isSubdust(vtxo, dust) {
|
|
13
|
+
return vtxo.value < dust;
|
|
14
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { p2tr } from "@scure/btc-signer/payment";
|
|
2
|
+
import { getNetwork } from '../networks.js';
|
|
3
|
+
import { ESPLORA_URL, EsploraProvider, } from '../providers/onchain.js';
|
|
4
|
+
import { Transaction } from "@scure/btc-signer";
|
|
5
|
+
import { findP2AOutput, P2A } from '../utils/anchor.js';
|
|
6
|
+
import { TxWeightEstimator } from '../utils/txSizeEstimator.js';
|
|
7
|
+
/**
|
|
8
|
+
* Onchain Bitcoin wallet implementation for traditional Bitcoin transactions.
|
|
9
|
+
*
|
|
10
|
+
* This wallet handles regular Bitcoin transactions on the blockchain without
|
|
11
|
+
* using the Ark protocol. It supports P2TR (Pay-to-Taproot) addresses and
|
|
12
|
+
* provides basic Bitcoin wallet functionality.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const wallet = new OnchainWallet(identity, 'mainnet');
|
|
17
|
+
* const balance = await wallet.getBalance();
|
|
18
|
+
* const txid = await wallet.send({
|
|
19
|
+
* address: 'bc1...',
|
|
20
|
+
* amount: 50000
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export class OnchainWallet {
|
|
25
|
+
constructor(identity, network, provider) {
|
|
26
|
+
this.identity = identity;
|
|
27
|
+
const pubkey = identity.xOnlyPublicKey();
|
|
28
|
+
if (!pubkey) {
|
|
29
|
+
throw new Error("Invalid configured public key");
|
|
30
|
+
}
|
|
31
|
+
this.provider = provider || new EsploraProvider(ESPLORA_URL[network]);
|
|
32
|
+
this.network = getNetwork(network);
|
|
33
|
+
this.onchainP2TR = p2tr(pubkey, undefined, this.network);
|
|
34
|
+
}
|
|
35
|
+
get address() {
|
|
36
|
+
return this.onchainP2TR.address || "";
|
|
37
|
+
}
|
|
38
|
+
async getCoins() {
|
|
39
|
+
return this.provider.getCoins(this.address);
|
|
40
|
+
}
|
|
41
|
+
async getBalance() {
|
|
42
|
+
const coins = await this.getCoins();
|
|
43
|
+
const onchainConfirmed = coins
|
|
44
|
+
.filter((coin) => coin.status.confirmed)
|
|
45
|
+
.reduce((sum, coin) => sum + coin.value, 0);
|
|
46
|
+
const onchainUnconfirmed = coins
|
|
47
|
+
.filter((coin) => !coin.status.confirmed)
|
|
48
|
+
.reduce((sum, coin) => sum + coin.value, 0);
|
|
49
|
+
const onchainTotal = onchainConfirmed + onchainUnconfirmed;
|
|
50
|
+
return onchainTotal;
|
|
51
|
+
}
|
|
52
|
+
async send(params) {
|
|
53
|
+
if (params.amount <= 0) {
|
|
54
|
+
throw new Error("Amount must be positive");
|
|
55
|
+
}
|
|
56
|
+
if (params.amount < OnchainWallet.DUST_AMOUNT) {
|
|
57
|
+
throw new Error("Amount is below dust limit");
|
|
58
|
+
}
|
|
59
|
+
const coins = await this.getCoins();
|
|
60
|
+
let feeRate = params.feeRate;
|
|
61
|
+
if (!feeRate) {
|
|
62
|
+
feeRate = await this.provider.getFeeRate();
|
|
63
|
+
}
|
|
64
|
+
if (!feeRate || feeRate < OnchainWallet.MIN_FEE_RATE) {
|
|
65
|
+
feeRate = OnchainWallet.MIN_FEE_RATE;
|
|
66
|
+
}
|
|
67
|
+
// Ensure fee is an integer by rounding up
|
|
68
|
+
const estimatedFee = Math.ceil(174 * feeRate);
|
|
69
|
+
const totalNeeded = params.amount + estimatedFee;
|
|
70
|
+
// Select coins
|
|
71
|
+
const selected = selectCoins(coins, totalNeeded);
|
|
72
|
+
// Create transaction
|
|
73
|
+
let tx = new Transaction();
|
|
74
|
+
// Add inputs
|
|
75
|
+
for (const input of selected.inputs) {
|
|
76
|
+
tx.addInput({
|
|
77
|
+
txid: input.txid,
|
|
78
|
+
index: input.vout,
|
|
79
|
+
witnessUtxo: {
|
|
80
|
+
script: this.onchainP2TR.script,
|
|
81
|
+
amount: BigInt(input.value),
|
|
82
|
+
},
|
|
83
|
+
tapInternalKey: this.onchainP2TR.tapInternalKey,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// Add payment output
|
|
87
|
+
tx.addOutputAddress(params.address, BigInt(params.amount), this.network);
|
|
88
|
+
// Add change output if needed
|
|
89
|
+
if (selected.changeAmount > 0n) {
|
|
90
|
+
tx.addOutputAddress(this.address, selected.changeAmount, this.network);
|
|
91
|
+
}
|
|
92
|
+
// Sign inputs and Finalize
|
|
93
|
+
tx = await this.identity.sign(tx);
|
|
94
|
+
tx.finalize();
|
|
95
|
+
// Broadcast
|
|
96
|
+
const txid = await this.provider.broadcastTransaction(tx.hex);
|
|
97
|
+
return txid;
|
|
98
|
+
}
|
|
99
|
+
async bumpP2A(parent) {
|
|
100
|
+
const parentVsize = parent.vsize;
|
|
101
|
+
let child = new Transaction({
|
|
102
|
+
allowUnknownInputs: true,
|
|
103
|
+
allowLegacyWitnessUtxo: true,
|
|
104
|
+
version: 3,
|
|
105
|
+
});
|
|
106
|
+
child.addInput(findP2AOutput(parent)); // throws if not found
|
|
107
|
+
const childVsize = TxWeightEstimator.create()
|
|
108
|
+
.addKeySpendInput(true)
|
|
109
|
+
.addP2AInput()
|
|
110
|
+
.addP2TROutput()
|
|
111
|
+
.vsize().value;
|
|
112
|
+
const packageVSize = parentVsize + Number(childVsize);
|
|
113
|
+
let feeRate = await this.provider.getFeeRate();
|
|
114
|
+
if (!feeRate || feeRate < OnchainWallet.MIN_FEE_RATE) {
|
|
115
|
+
feeRate = OnchainWallet.MIN_FEE_RATE;
|
|
116
|
+
}
|
|
117
|
+
const fee = Math.ceil(feeRate * packageVSize);
|
|
118
|
+
if (!fee) {
|
|
119
|
+
throw new Error(`invalid fee, got ${fee} with vsize ${packageVSize}, feeRate ${feeRate}`);
|
|
120
|
+
}
|
|
121
|
+
// Select coins
|
|
122
|
+
const coins = await this.getCoins();
|
|
123
|
+
const selected = selectCoins(coins, fee, true);
|
|
124
|
+
for (const input of selected.inputs) {
|
|
125
|
+
child.addInput({
|
|
126
|
+
txid: input.txid,
|
|
127
|
+
index: input.vout,
|
|
128
|
+
witnessUtxo: {
|
|
129
|
+
script: this.onchainP2TR.script,
|
|
130
|
+
amount: BigInt(input.value),
|
|
131
|
+
},
|
|
132
|
+
tapInternalKey: this.onchainP2TR.tapInternalKey,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
child.addOutputAddress(this.address, P2A.amount + selected.changeAmount, this.network);
|
|
136
|
+
// Sign inputs and Finalize
|
|
137
|
+
child = await this.identity.sign(child);
|
|
138
|
+
for (let i = 1; i < child.inputsLength; i++) {
|
|
139
|
+
child.finalizeIdx(i);
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
await this.provider.broadcastTransaction(parent.hex, child.hex);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
console.error(error);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
return [parent.hex, child.hex];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
OnchainWallet.MIN_FEE_RATE = 1; // sat/vbyte
|
|
153
|
+
OnchainWallet.DUST_AMOUNT = 546; // sats
|
|
154
|
+
/**
|
|
155
|
+
* Select coins to reach a target amount, prioritizing those closer to expiry
|
|
156
|
+
* @param coins List of coins to select from
|
|
157
|
+
* @param targetAmount Target amount to reach in satoshis
|
|
158
|
+
* @param forceChange If true, ensure the coin selection will require a change output
|
|
159
|
+
* @returns Selected coins and change amount, or null if insufficient funds
|
|
160
|
+
*/
|
|
161
|
+
export function selectCoins(coins, targetAmount, forceChange = false) {
|
|
162
|
+
if (isNaN(targetAmount)) {
|
|
163
|
+
throw new Error("Target amount is NaN, got " + targetAmount);
|
|
164
|
+
}
|
|
165
|
+
if (targetAmount < 0) {
|
|
166
|
+
throw new Error("Target amount is negative, got " + targetAmount);
|
|
167
|
+
}
|
|
168
|
+
if (targetAmount === 0) {
|
|
169
|
+
return { inputs: [], changeAmount: 0n };
|
|
170
|
+
}
|
|
171
|
+
// Sort coins by amount (descending)
|
|
172
|
+
const sortedCoins = [...coins].sort((a, b) => b.value - a.value);
|
|
173
|
+
const selectedCoins = [];
|
|
174
|
+
let selectedAmount = 0;
|
|
175
|
+
// Select coins until we have enough
|
|
176
|
+
for (const coin of sortedCoins) {
|
|
177
|
+
selectedCoins.push(coin);
|
|
178
|
+
selectedAmount += coin.value;
|
|
179
|
+
if (forceChange
|
|
180
|
+
? selectedAmount > targetAmount
|
|
181
|
+
: selectedAmount >= targetAmount) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (selectedAmount === targetAmount) {
|
|
186
|
+
return { inputs: selectedCoins, changeAmount: 0n };
|
|
187
|
+
}
|
|
188
|
+
if (selectedAmount < targetAmount) {
|
|
189
|
+
throw new Error("Insufficient funds");
|
|
190
|
+
}
|
|
191
|
+
const changeAmount = BigInt(selectedAmount - targetAmount);
|
|
192
|
+
return {
|
|
193
|
+
inputs: selectedCoins,
|
|
194
|
+
changeAmount,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ramps is a class wrapping IWallet.settle method to provide a more convenient interface for onboarding and offboarding operations.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* const ramps = new Ramps(wallet);
|
|
7
|
+
* await ramps.onboard(); // onboard all boarding utxos
|
|
8
|
+
* await ramps.offboard(myOnchainAddress); // collaborative exit all vtxos to onchain address
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export class Ramps {
|
|
12
|
+
constructor(wallet) {
|
|
13
|
+
this.wallet = wallet;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Onboard boarding utxos.
|
|
17
|
+
*
|
|
18
|
+
* @param boardingUtxos - The boarding utxos to onboard. If not provided, all boarding utxos will be used.
|
|
19
|
+
* @param amount - The amount to onboard. If not provided, the total amount of boarding utxos will be onboarded.
|
|
20
|
+
* @param eventCallback - The callback to receive settlement events. optional.
|
|
21
|
+
*/
|
|
22
|
+
async onboard(boardingUtxos, amount, eventCallback) {
|
|
23
|
+
boardingUtxos = boardingUtxos ?? (await this.wallet.getBoardingUtxos());
|
|
24
|
+
const totalAmount = boardingUtxos.reduce((acc, coin) => acc + BigInt(coin.value), 0n);
|
|
25
|
+
let change = 0n;
|
|
26
|
+
if (amount) {
|
|
27
|
+
if (amount > totalAmount) {
|
|
28
|
+
throw new Error("Amount is greater than total amount of boarding utxos");
|
|
29
|
+
}
|
|
30
|
+
change = totalAmount - amount;
|
|
31
|
+
}
|
|
32
|
+
amount = amount ?? totalAmount;
|
|
33
|
+
const offchainAddress = await this.wallet.getAddress();
|
|
34
|
+
const outputs = [
|
|
35
|
+
{
|
|
36
|
+
address: offchainAddress,
|
|
37
|
+
amount,
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
if (change > 0n) {
|
|
41
|
+
const boardingAddress = await this.wallet.getBoardingAddress();
|
|
42
|
+
outputs.push({
|
|
43
|
+
address: boardingAddress,
|
|
44
|
+
amount: change,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return this.wallet.settle({
|
|
48
|
+
inputs: boardingUtxos,
|
|
49
|
+
outputs,
|
|
50
|
+
}, eventCallback);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Offboard vtxos, or "collaborative exit" vtxos to onchain address.
|
|
54
|
+
*
|
|
55
|
+
* @param destinationAddress - The destination address to offboard to.
|
|
56
|
+
* @param amount - The amount to offboard. If not provided, the total amount of vtxos will be offboarded.
|
|
57
|
+
* @param eventCallback - The callback to receive settlement events. optional.
|
|
58
|
+
*/
|
|
59
|
+
async offboard(destinationAddress, amount, eventCallback) {
|
|
60
|
+
const vtxos = await this.wallet.getVtxos({
|
|
61
|
+
withRecoverable: true,
|
|
62
|
+
withUnrolled: false,
|
|
63
|
+
});
|
|
64
|
+
const totalAmount = vtxos.reduce((acc, coin) => acc + BigInt(coin.value), 0n);
|
|
65
|
+
let change = 0n;
|
|
66
|
+
if (amount) {
|
|
67
|
+
if (amount > totalAmount) {
|
|
68
|
+
throw new Error("Amount is greater than total amount of vtxos");
|
|
69
|
+
}
|
|
70
|
+
change = totalAmount - amount;
|
|
71
|
+
}
|
|
72
|
+
amount = amount ?? totalAmount;
|
|
73
|
+
const outputs = [
|
|
74
|
+
{
|
|
75
|
+
address: destinationAddress,
|
|
76
|
+
amount,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
if (change > 0n) {
|
|
80
|
+
const offchainAddress = await this.wallet.getAddress();
|
|
81
|
+
outputs.push({
|
|
82
|
+
address: offchainAddress,
|
|
83
|
+
amount: change,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return this.wallet.settle({
|
|
87
|
+
inputs: vtxos,
|
|
88
|
+
outputs,
|
|
89
|
+
}, eventCallback);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -108,6 +108,38 @@ export class IndexedDBVtxoRepository {
|
|
|
108
108
|
request.onerror = () => reject(request.error);
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
|
+
async getSweptVtxos() {
|
|
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 stateIndex = store.index("state");
|
|
119
|
+
// Get vtxos where state is "swept"
|
|
120
|
+
const request = stateIndex.getAll(IDBKeyRange.only("swept"));
|
|
121
|
+
request.onsuccess = () => {
|
|
122
|
+
resolve(request.result);
|
|
123
|
+
};
|
|
124
|
+
request.onerror = () => reject(request.error);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async getSpentVtxos() {
|
|
128
|
+
if (!this.db) {
|
|
129
|
+
throw new Error("Database not opened");
|
|
130
|
+
}
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const transaction = this.db.transaction(IndexedDBVtxoRepository.STORE_NAME, "readonly");
|
|
133
|
+
const store = transaction.objectStore(IndexedDBVtxoRepository.STORE_NAME);
|
|
134
|
+
const spentByIndex = store.index("spentBy");
|
|
135
|
+
// Get vtxos where spentBy is not empty string
|
|
136
|
+
const request = spentByIndex.getAll(IDBKeyRange.lowerBound("", true));
|
|
137
|
+
request.onsuccess = () => {
|
|
138
|
+
resolve(request.result);
|
|
139
|
+
};
|
|
140
|
+
request.onerror = () => reject(request.error);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
111
143
|
async getAllVtxos() {
|
|
112
144
|
if (!this.db) {
|
|
113
145
|
throw new Error("Database not opened");
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request is the namespace that contains the request types for the service worker.
|
|
3
|
+
*/
|
|
1
4
|
export var Request;
|
|
2
5
|
(function (Request) {
|
|
3
6
|
function isBase(message) {
|
|
@@ -10,8 +13,6 @@ export var Request;
|
|
|
10
13
|
typeof message.privateKey === "string" &&
|
|
11
14
|
"arkServerUrl" in message &&
|
|
12
15
|
typeof message.arkServerUrl === "string" &&
|
|
13
|
-
"network" in message &&
|
|
14
|
-
typeof message.network === "string" &&
|
|
15
16
|
("arkServerPublicKey" in message
|
|
16
17
|
? typeof message.arkServerPublicKey === "string" ||
|
|
17
18
|
message.arkServerPublicKey === undefined
|
|
@@ -26,18 +27,14 @@ export var Request;
|
|
|
26
27
|
return message.type === "GET_ADDRESS";
|
|
27
28
|
}
|
|
28
29
|
Request.isGetAddress = isGetAddress;
|
|
29
|
-
function
|
|
30
|
-
return message.type === "
|
|
30
|
+
function isGetBoardingAddress(message) {
|
|
31
|
+
return message.type === "GET_BOARDING_ADDRESS";
|
|
31
32
|
}
|
|
32
|
-
Request.
|
|
33
|
+
Request.isGetBoardingAddress = isGetBoardingAddress;
|
|
33
34
|
function isGetBalance(message) {
|
|
34
35
|
return message.type === "GET_BALANCE";
|
|
35
36
|
}
|
|
36
37
|
Request.isGetBalance = isGetBalance;
|
|
37
|
-
function isGetCoins(message) {
|
|
38
|
-
return message.type === "GET_COINS";
|
|
39
|
-
}
|
|
40
|
-
Request.isGetCoins = isGetCoins;
|
|
41
38
|
function isGetVtxos(message) {
|
|
42
39
|
return message.type === "GET_VTXOS";
|
|
43
40
|
}
|
|
@@ -69,8 +66,14 @@ export var Request;
|
|
|
69
66
|
return message.type === "GET_STATUS";
|
|
70
67
|
}
|
|
71
68
|
Request.isGetStatus = isGetStatus;
|
|
72
|
-
function
|
|
73
|
-
return message.type === "
|
|
69
|
+
function isSign(message) {
|
|
70
|
+
return (message.type === "SIGN" &&
|
|
71
|
+
"tx" in message &&
|
|
72
|
+
typeof message.tx === "string" &&
|
|
73
|
+
("inputIndexes" in message
|
|
74
|
+
? Array.isArray(message.inputIndexes) &&
|
|
75
|
+
message.inputIndexes.every((index) => typeof index === "number")
|
|
76
|
+
: true));
|
|
74
77
|
}
|
|
75
|
-
Request.
|
|
78
|
+
Request.isSign = isSign;
|
|
76
79
|
})(Request || (Request = {}));
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response is the namespace that contains the response types for the service worker.
|
|
3
|
+
*/
|
|
1
4
|
export var Response;
|
|
2
5
|
(function (Response) {
|
|
3
6
|
Response.walletInitialized = (id) => ({
|
|
@@ -40,28 +43,28 @@ export var Response;
|
|
|
40
43
|
return response.type === "ADDRESS" && response.success === true;
|
|
41
44
|
}
|
|
42
45
|
Response.isAddress = isAddress;
|
|
43
|
-
function
|
|
46
|
+
function isBoardingAddress(response) {
|
|
47
|
+
return (response.type === "BOARDING_ADDRESS" && response.success === true);
|
|
48
|
+
}
|
|
49
|
+
Response.isBoardingAddress = isBoardingAddress;
|
|
50
|
+
function address(id, address) {
|
|
44
51
|
return {
|
|
45
52
|
type: "ADDRESS",
|
|
46
53
|
success: true,
|
|
47
|
-
|
|
54
|
+
address,
|
|
48
55
|
id,
|
|
49
56
|
};
|
|
50
57
|
}
|
|
51
|
-
Response.
|
|
52
|
-
function
|
|
53
|
-
return response.type === "ADDRESS_INFO" && response.success === true;
|
|
54
|
-
}
|
|
55
|
-
Response.isAddressInfo = isAddressInfo;
|
|
56
|
-
function addressInfo(id, addressInfo) {
|
|
58
|
+
Response.address = address;
|
|
59
|
+
function boardingAddress(id, address) {
|
|
57
60
|
return {
|
|
58
|
-
type: "
|
|
61
|
+
type: "BOARDING_ADDRESS",
|
|
59
62
|
success: true,
|
|
60
|
-
|
|
63
|
+
address,
|
|
61
64
|
id,
|
|
62
65
|
};
|
|
63
66
|
}
|
|
64
|
-
Response.
|
|
67
|
+
Response.boardingAddress = boardingAddress;
|
|
65
68
|
function isBalance(response) {
|
|
66
69
|
return response.type === "BALANCE" && response.success === true;
|
|
67
70
|
}
|
|
@@ -75,19 +78,6 @@ export var Response;
|
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
80
|
Response.balance = balance;
|
|
78
|
-
function isCoins(response) {
|
|
79
|
-
return response.type === "COINS" && response.success === true;
|
|
80
|
-
}
|
|
81
|
-
Response.isCoins = isCoins;
|
|
82
|
-
function coins(id, coins) {
|
|
83
|
-
return {
|
|
84
|
-
type: "COINS",
|
|
85
|
-
success: true,
|
|
86
|
-
coins,
|
|
87
|
-
id,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
Response.coins = coins;
|
|
91
81
|
function isVtxos(response) {
|
|
92
82
|
return response.type === "VTXOS" && response.success === true;
|
|
93
83
|
}
|
|
@@ -181,12 +171,17 @@ export var Response;
|
|
|
181
171
|
};
|
|
182
172
|
}
|
|
183
173
|
Response.clearResponse = clearResponse;
|
|
184
|
-
function
|
|
174
|
+
function signSuccess(id, tx) {
|
|
185
175
|
return {
|
|
186
|
-
type: "
|
|
176
|
+
type: "SIGN_SUCCESS",
|
|
187
177
|
success: true,
|
|
178
|
+
tx,
|
|
188
179
|
id,
|
|
189
180
|
};
|
|
190
181
|
}
|
|
191
|
-
Response.
|
|
182
|
+
Response.signSuccess = signSuccess;
|
|
183
|
+
function isSignSuccess(response) {
|
|
184
|
+
return response.type === "SIGN_SUCCESS" && response.success === true;
|
|
185
|
+
}
|
|
186
|
+
Response.isSignSuccess = isSignSuccess;
|
|
192
187
|
})(Response || (Response = {}));
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* setupServiceWorker sets up the service worker.
|
|
3
|
+
* @param path - the path to the service worker script
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* const serviceWorker = await setupServiceWorker("/service-worker.js");
|
|
7
|
+
* ```
|
|
8
|
+
*/
|
|
1
9
|
export async function setupServiceWorker(path) {
|
|
2
10
|
// check if service workers are supported
|
|
3
11
|
if (!("serviceWorker" in navigator)) {
|