@arkade-os/sdk 0.1.4 → 0.2.1

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.
Files changed (114) hide show
  1. package/README.md +157 -174
  2. package/dist/cjs/arknote/index.js +61 -58
  3. package/dist/cjs/bip322/errors.js +13 -0
  4. package/dist/cjs/bip322/index.js +178 -0
  5. package/dist/cjs/forfeit.js +14 -25
  6. package/dist/cjs/identity/singleKey.js +68 -0
  7. package/dist/cjs/index.js +43 -17
  8. package/dist/cjs/providers/ark.js +261 -321
  9. package/dist/cjs/providers/indexer.js +525 -0
  10. package/dist/cjs/providers/onchain.js +193 -15
  11. package/dist/cjs/script/address.js +48 -17
  12. package/dist/cjs/script/base.js +120 -3
  13. package/dist/cjs/script/default.js +18 -4
  14. package/dist/cjs/script/tapscript.js +61 -20
  15. package/dist/cjs/script/vhtlc.js +85 -7
  16. package/dist/cjs/tree/signingSession.js +63 -106
  17. package/dist/cjs/tree/txTree.js +193 -0
  18. package/dist/cjs/tree/validation.js +79 -155
  19. package/dist/cjs/utils/anchor.js +35 -0
  20. package/dist/cjs/utils/arkTransaction.js +108 -0
  21. package/dist/cjs/utils/transactionHistory.js +84 -72
  22. package/dist/cjs/utils/txSizeEstimator.js +12 -0
  23. package/dist/cjs/utils/unknownFields.js +211 -0
  24. package/dist/cjs/wallet/index.js +12 -0
  25. package/dist/cjs/wallet/onchain.js +201 -0
  26. package/dist/cjs/wallet/ramps.js +95 -0
  27. package/dist/cjs/wallet/serviceWorker/db/vtxo/idb.js +32 -0
  28. package/dist/cjs/wallet/serviceWorker/request.js +15 -12
  29. package/dist/cjs/wallet/serviceWorker/response.js +22 -27
  30. package/dist/cjs/wallet/serviceWorker/utils.js +8 -0
  31. package/dist/cjs/wallet/serviceWorker/wallet.js +61 -34
  32. package/dist/cjs/wallet/serviceWorker/worker.js +120 -108
  33. package/dist/cjs/wallet/unroll.js +270 -0
  34. package/dist/cjs/wallet/wallet.js +701 -454
  35. package/dist/esm/arknote/index.js +61 -57
  36. package/dist/esm/bip322/errors.js +9 -0
  37. package/dist/esm/bip322/index.js +174 -0
  38. package/dist/esm/forfeit.js +15 -26
  39. package/dist/esm/identity/singleKey.js +64 -0
  40. package/dist/esm/index.js +31 -12
  41. package/dist/esm/providers/ark.js +259 -320
  42. package/dist/esm/providers/indexer.js +521 -0
  43. package/dist/esm/providers/onchain.js +193 -15
  44. package/dist/esm/script/address.js +48 -17
  45. package/dist/esm/script/base.js +120 -3
  46. package/dist/esm/script/default.js +18 -4
  47. package/dist/esm/script/tapscript.js +61 -20
  48. package/dist/esm/script/vhtlc.js +85 -7
  49. package/dist/esm/tree/signingSession.js +65 -108
  50. package/dist/esm/tree/txTree.js +189 -0
  51. package/dist/esm/tree/validation.js +75 -152
  52. package/dist/esm/utils/anchor.js +31 -0
  53. package/dist/esm/utils/arkTransaction.js +105 -0
  54. package/dist/esm/utils/transactionHistory.js +84 -72
  55. package/dist/esm/utils/txSizeEstimator.js +12 -0
  56. package/dist/esm/utils/unknownFields.js +173 -0
  57. package/dist/esm/wallet/index.js +9 -0
  58. package/dist/esm/wallet/onchain.js +196 -0
  59. package/dist/esm/wallet/ramps.js +91 -0
  60. package/dist/esm/wallet/serviceWorker/db/vtxo/idb.js +32 -0
  61. package/dist/esm/wallet/serviceWorker/request.js +15 -12
  62. package/dist/esm/wallet/serviceWorker/response.js +22 -27
  63. package/dist/esm/wallet/serviceWorker/utils.js +8 -0
  64. package/dist/esm/wallet/serviceWorker/wallet.js +62 -35
  65. package/dist/esm/wallet/serviceWorker/worker.js +120 -108
  66. package/dist/esm/wallet/unroll.js +267 -0
  67. package/dist/esm/wallet/wallet.js +674 -461
  68. package/dist/types/arknote/index.d.ts +40 -13
  69. package/dist/types/bip322/errors.d.ts +6 -0
  70. package/dist/types/bip322/index.d.ts +57 -0
  71. package/dist/types/forfeit.d.ts +2 -14
  72. package/dist/types/identity/singleKey.d.ts +27 -0
  73. package/dist/types/index.d.ts +24 -12
  74. package/dist/types/providers/ark.d.ts +114 -95
  75. package/dist/types/providers/indexer.d.ts +186 -0
  76. package/dist/types/providers/onchain.d.ts +41 -11
  77. package/dist/types/script/address.d.ts +26 -2
  78. package/dist/types/script/base.d.ts +13 -3
  79. package/dist/types/script/default.d.ts +22 -0
  80. package/dist/types/script/tapscript.d.ts +61 -5
  81. package/dist/types/script/vhtlc.d.ts +27 -0
  82. package/dist/types/tree/signingSession.d.ts +5 -5
  83. package/dist/types/tree/txTree.d.ts +28 -0
  84. package/dist/types/tree/validation.d.ts +15 -22
  85. package/dist/types/utils/anchor.d.ts +19 -0
  86. package/dist/types/utils/arkTransaction.d.ts +27 -0
  87. package/dist/types/utils/transactionHistory.d.ts +7 -1
  88. package/dist/types/utils/txSizeEstimator.d.ts +3 -0
  89. package/dist/types/utils/unknownFields.d.ts +83 -0
  90. package/dist/types/wallet/index.d.ts +51 -50
  91. package/dist/types/wallet/onchain.d.ts +49 -0
  92. package/dist/types/wallet/ramps.d.ts +32 -0
  93. package/dist/types/wallet/serviceWorker/db/vtxo/idb.d.ts +2 -0
  94. package/dist/types/wallet/serviceWorker/db/vtxo/index.d.ts +2 -0
  95. package/dist/types/wallet/serviceWorker/request.d.ts +14 -16
  96. package/dist/types/wallet/serviceWorker/response.d.ts +17 -19
  97. package/dist/types/wallet/serviceWorker/utils.d.ts +8 -0
  98. package/dist/types/wallet/serviceWorker/wallet.d.ts +36 -8
  99. package/dist/types/wallet/serviceWorker/worker.d.ts +7 -3
  100. package/dist/types/wallet/unroll.d.ts +102 -0
  101. package/dist/types/wallet/wallet.d.ts +71 -25
  102. package/package.json +37 -35
  103. package/dist/cjs/identity/inMemoryKey.js +0 -40
  104. package/dist/cjs/tree/vtxoTree.js +0 -231
  105. package/dist/cjs/utils/coinselect.js +0 -73
  106. package/dist/cjs/utils/psbt.js +0 -137
  107. package/dist/esm/identity/inMemoryKey.js +0 -36
  108. package/dist/esm/tree/vtxoTree.js +0 -191
  109. package/dist/esm/utils/coinselect.js +0 -69
  110. package/dist/esm/utils/psbt.js +0 -131
  111. package/dist/types/identity/inMemoryKey.d.ts +0 -12
  112. package/dist/types/tree/vtxoTree.d.ts +0 -33
  113. package/dist/types/utils/coinselect.d.ts +0 -21
  114. package/dist/types/utils/psbt.d.ts +0 -11
@@ -1,55 +1,11 @@
1
1
  import { TxType } from '../wallet/index.js';
2
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
3
+ * @param spendable - Vtxos that are spendable
4
+ * @param spent - Vtxos that are spent
5
+ * @param boardingBatchTxids - Set of boarding batch txids
6
+ * @returns Ark transactions
39
7
  */
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) {
8
+ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
53
9
  const txs = [];
54
10
  // Receive case
55
11
  // All vtxos are received unless:
@@ -57,8 +13,9 @@ export function vtxosToTxs(spendable, spent, boardingRounds) {
57
13
  // - they are the change of a spend tx
58
14
  let vtxosLeftToCheck = [...spent];
59
15
  for (const vtxo of [...spendable, ...spent]) {
60
- if (vtxo.virtualStatus.state !== "pending" &&
61
- boardingRounds.has(vtxo.virtualStatus.batchTxID || "")) {
16
+ if (vtxo.virtualStatus.state !== "preconfirmed" &&
17
+ vtxo.virtualStatus.commitmentTxIds &&
18
+ vtxo.virtualStatus.commitmentTxIds.some((txid) => boardingBatchTxids.has(txid))) {
62
19
  continue;
63
20
  }
64
21
  const settleVtxos = findVtxosSpentInSettlement(vtxosLeftToCheck, vtxo);
@@ -74,13 +31,13 @@ export function vtxosToTxs(spendable, spent, boardingRounds) {
74
31
  continue; // settlement or change, ignore
75
32
  }
76
33
  const txKey = {
77
- roundTxid: vtxo.virtualStatus.batchTxID || "",
34
+ commitmentTxid: vtxo.spentBy || "",
78
35
  boardingTxid: "",
79
- redeemTxid: "",
36
+ arkTxid: "",
80
37
  };
81
- let settled = vtxo.virtualStatus.state !== "pending";
82
- if (vtxo.virtualStatus.state === "pending") {
83
- txKey.redeemTxid = vtxo.txid;
38
+ let settled = vtxo.virtualStatus.state !== "preconfirmed";
39
+ if (vtxo.virtualStatus.state === "preconfirmed") {
40
+ txKey.arkTxid = vtxo.txid;
84
41
  if (vtxo.spentBy) {
85
42
  settled = true;
86
43
  }
@@ -93,22 +50,27 @@ export function vtxosToTxs(spendable, spent, boardingRounds) {
93
50
  settled,
94
51
  });
95
52
  }
96
- // send case
97
- // All "spentBy" vtxos are payments unless:
98
- // - they are settlements
99
- // aggregate spent by spentId
100
- const vtxosBySpentBy = new Map();
53
+ // vtxos by settled by or ark txid
54
+ const vtxosByTxid = new Map();
101
55
  for (const v of spent) {
102
- if (!v.spentBy)
56
+ if (v.settledBy) {
57
+ if (!vtxosByTxid.has(v.settledBy)) {
58
+ vtxosByTxid.set(v.settledBy, []);
59
+ }
60
+ const currentVtxos = vtxosByTxid.get(v.settledBy);
61
+ vtxosByTxid.set(v.settledBy, [...currentVtxos, v]);
62
+ }
63
+ if (!v.arkTxId) {
103
64
  continue;
104
- if (!vtxosBySpentBy.has(v.spentBy)) {
105
- vtxosBySpentBy.set(v.spentBy, []);
106
65
  }
107
- const currentVtxos = vtxosBySpentBy.get(v.spentBy);
108
- vtxosBySpentBy.set(v.spentBy, [...currentVtxos, v]);
66
+ if (!vtxosByTxid.has(v.arkTxId)) {
67
+ vtxosByTxid.set(v.arkTxId, []);
68
+ }
69
+ const currentVtxos = vtxosByTxid.get(v.arkTxId);
70
+ vtxosByTxid.set(v.arkTxId, [...currentVtxos, v]);
109
71
  }
110
- for (const [sb, vtxos] of vtxosBySpentBy) {
111
- const resultedVtxos = findVtxosResultedFromSpentBy([...spendable, ...spent], sb);
72
+ for (const [sb, vtxos] of vtxosByTxid) {
73
+ const resultedVtxos = findVtxosResultedFromTxid([...spendable, ...spent], sb);
112
74
  const resultedAmount = reduceVtxosAmount(resultedVtxos);
113
75
  const spentAmount = reduceVtxosAmount(vtxos);
114
76
  if (spentAmount <= resultedAmount) {
@@ -116,12 +78,12 @@ export function vtxosToTxs(spendable, spent, boardingRounds) {
116
78
  }
117
79
  const vtxo = getVtxo(resultedVtxos, vtxos);
118
80
  const txKey = {
119
- roundTxid: vtxo.virtualStatus.batchTxID || "",
81
+ commitmentTxid: vtxo.virtualStatus.commitmentTxIds?.[0] || "",
120
82
  boardingTxid: "",
121
- redeemTxid: "",
83
+ arkTxid: "",
122
84
  };
123
- if (vtxo.virtualStatus.state === "pending") {
124
- txKey.redeemTxid = vtxo.txid;
85
+ if (vtxo.virtualStatus.state === "preconfirmed") {
86
+ txKey.arkTxid = vtxo.txid;
125
87
  }
126
88
  txs.push({
127
89
  key: txKey,
@@ -133,6 +95,56 @@ export function vtxosToTxs(spendable, spent, boardingRounds) {
133
95
  }
134
96
  return txs;
135
97
  }
98
+ /**
99
+ * Helper function to find vtxos that were spent in a settlement
100
+ */
101
+ function findVtxosSpentInSettlement(vtxos, vtxo) {
102
+ if (vtxo.virtualStatus.state === "preconfirmed") {
103
+ return [];
104
+ }
105
+ return vtxos.filter((v) => {
106
+ if (!v.settledBy)
107
+ return false;
108
+ return (vtxo.virtualStatus.commitmentTxIds?.includes(v.settledBy) ?? false);
109
+ });
110
+ }
111
+ /**
112
+ * Helper function to find vtxos that were spent in a payment
113
+ */
114
+ function findVtxosSpentInPayment(vtxos, vtxo) {
115
+ return vtxos.filter((v) => {
116
+ if (!v.arkTxId)
117
+ return false;
118
+ return v.arkTxId === vtxo.txid;
119
+ });
120
+ }
121
+ /**
122
+ * Helper function to find vtxos that resulted from a spentBy transaction
123
+ */
124
+ function findVtxosResultedFromTxid(vtxos, txid) {
125
+ return vtxos.filter((v) => {
126
+ if (v.virtualStatus.state !== "preconfirmed" &&
127
+ v.virtualStatus.commitmentTxIds?.includes(txid)) {
128
+ return true;
129
+ }
130
+ return v.txid === txid;
131
+ });
132
+ }
133
+ /**
134
+ * Helper function to reduce vtxos to their total amount
135
+ */
136
+ function reduceVtxosAmount(vtxos) {
137
+ return vtxos.reduce((sum, v) => sum + v.value, 0);
138
+ }
139
+ /**
140
+ * Helper function to get a vtxo from a list of vtxos
141
+ */
142
+ function getVtxo(resultedVtxos, spentVtxos) {
143
+ if (resultedVtxos.length === 0) {
144
+ return spentVtxos[0];
145
+ }
146
+ return resultedVtxos[0];
147
+ }
136
148
  function removeVtxosFromList(vtxos, vtxosToRemove) {
137
149
  return vtxos.filter((v) => {
138
150
  for (const vtxoToRemove of vtxosToRemove) {
@@ -10,6 +10,11 @@ export class TxWeightEstimator {
10
10
  static create() {
11
11
  return new TxWeightEstimator(false, 0, 0, 0, 0, 0);
12
12
  }
13
+ addP2AInput() {
14
+ this.inputCount++;
15
+ this.inputSize += TxWeightEstimator.INPUT_SIZE;
16
+ return this;
17
+ }
13
18
  addKeySpendInput(isDefault = true) {
14
19
  this.inputCount++;
15
20
  this.inputWitnessSize += 64 + 1 + (isDefault ? 0 : 1);
@@ -45,6 +50,12 @@ export class TxWeightEstimator {
45
50
  TxWeightEstimator.OUTPUT_SIZE + TxWeightEstimator.P2WKH_OUTPUT_SIZE;
46
51
  return this;
47
52
  }
53
+ addP2TROutput() {
54
+ this.outputCount++;
55
+ this.outputSize +=
56
+ TxWeightEstimator.OUTPUT_SIZE + TxWeightEstimator.P2TR_OUTPUT_SIZE;
57
+ return this;
58
+ }
48
59
  vsize() {
49
60
  const getVarIntSize = (n) => {
50
61
  if (n < 0xfd)
@@ -82,6 +93,7 @@ TxWeightEstimator.P2WKH_OUTPUT_SIZE = 1 + 1 + 20;
82
93
  TxWeightEstimator.BASE_TX_SIZE = 8 + 2; // Version + LockTime
83
94
  TxWeightEstimator.WITNESS_HEADER_SIZE = 2; // Flag + Marker
84
95
  TxWeightEstimator.WITNESS_SCALE_FACTOR = 4;
96
+ TxWeightEstimator.P2TR_OUTPUT_SIZE = 1 + 1 + 32;
85
97
  const vsize = (weight) => {
86
98
  const value = BigInt(Math.ceil(weight / TxWeightEstimator.WITNESS_SCALE_FACTOR));
87
99
  return {
@@ -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
+ }
@@ -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
+ }