@arkade-os/sdk 0.3.3 → 0.3.5

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.
@@ -53,10 +53,11 @@ export class Ramps {
53
53
  * Offboard vtxos, or "collaborative exit" vtxos to onchain address.
54
54
  *
55
55
  * @param destinationAddress - The destination address to offboard to.
56
+ * @param feeInfo - The fee info to deduct from the offboard amount.
56
57
  * @param amount - The amount to offboard. If not provided, the total amount of vtxos will be offboarded.
57
58
  * @param eventCallback - The callback to receive settlement events. optional.
58
59
  */
59
- async offboard(destinationAddress, amount, eventCallback) {
60
+ async offboard(destinationAddress, feeInfo, amount, eventCallback) {
60
61
  const vtxos = await this.wallet.getVtxos({
61
62
  withRecoverable: true,
62
63
  withUnrolled: false,
@@ -70,6 +71,11 @@ export class Ramps {
70
71
  change = totalAmount - amount;
71
72
  }
72
73
  amount = amount ?? totalAmount;
74
+ const fees = feeInfo.intentFee.onchainOutput;
75
+ if (fees > amount) {
76
+ throw new Error(`can't deduct fees from offboard amount (${fees} > ${amount})`);
77
+ }
78
+ amount -= fees;
73
79
  const outputs = [
74
80
  {
75
81
  address: destinationAddress,
@@ -74,7 +74,6 @@ export class Worker {
74
74
  const { boardingTxs, commitmentsToIgnore: roundsToIgnore } = await this.wallet.getBoardingTxs();
75
75
  const { spendable, spent } = await this.getAllVtxos();
76
76
  // convert VTXOs to offchain transactions
77
- console.log("getTransactionHistory - vtxosToTxs:", spendable);
78
77
  const offchainTxs = vtxosToTxs(spendable, spent, roundsToIgnore);
79
78
  txs = [...boardingTxs, ...offchainTxs];
80
79
  // sort transactions by creation time in descending order (newest first)
@@ -109,12 +109,29 @@ export class Wallet {
109
109
  const esploraUrl = config.esploraUrl || ESPLORA_URL[info.network];
110
110
  // Use provided onchainProvider instance or create a new one
111
111
  const onchainProvider = config.onchainProvider || new EsploraProvider(esploraUrl);
112
- // Generate timelocks
113
- const exitTimelock = {
112
+ // validate unilateral exit timelock passed in config if any
113
+ if (config.exitTimelock) {
114
+ const { value, type } = config.exitTimelock;
115
+ if ((value < 512n && type !== "blocks") ||
116
+ (value >= 512n && type !== "seconds")) {
117
+ throw new Error("invalid exitTimelock");
118
+ }
119
+ }
120
+ // create unilateral exit timelock
121
+ const exitTimelock = config.exitTimelock ?? {
114
122
  value: info.unilateralExitDelay,
115
123
  type: info.unilateralExitDelay < 512n ? "blocks" : "seconds",
116
124
  };
117
- const boardingTimelock = {
125
+ // validate boarding timelock passed in config if any
126
+ if (config.boardingTimelock) {
127
+ const { value, type } = config.boardingTimelock;
128
+ if ((value < 512n && type !== "blocks") ||
129
+ (value >= 512n && type !== "seconds")) {
130
+ throw new Error("invalid boardingTimelock");
131
+ }
132
+ }
133
+ // create boarding timelock
134
+ const boardingTimelock = config.boardingTimelock ?? {
118
135
  value: info.boardingExitDelay,
119
136
  type: info.boardingExitDelay < 512n ? "blocks" : "seconds",
120
137
  };
@@ -377,14 +394,13 @@ export class Wallet {
377
394
  });
378
395
  }
379
396
  const tapTree = this.offchainTapscript.encode();
380
- let offchainTx = buildOffchainTx(selected.inputs.map((input) => ({
397
+ const offchainTx = buildOffchainTx(selected.inputs.map((input) => ({
381
398
  ...input,
382
399
  tapLeafScript: selectedLeaf,
383
400
  tapTree,
384
401
  })), outputs, this.serverUnrollScript);
385
402
  const signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
386
403
  const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base64.encode(c.toPSBT())));
387
- // TODO persist final virtual tx and checkpoints to repository
388
404
  // sign the checkpoints
389
405
  const finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
390
406
  const tx = Transaction.fromPSBT(base64.decode(c));
@@ -392,7 +408,78 @@ export class Wallet {
392
408
  return base64.encode(signedCheckpoint.toPSBT());
393
409
  }));
394
410
  await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
395
- return arkTxid;
411
+ try {
412
+ // mark VTXOs as spent and optionally add the change VTXO
413
+ const spentVtxos = [];
414
+ const commitmentTxIds = new Set();
415
+ let batchExpiry = Number.MAX_SAFE_INTEGER;
416
+ for (const [inputIndex, input] of selected.inputs.entries()) {
417
+ const vtxo = extendVirtualCoin(this, input);
418
+ const checkpointB64 = signedCheckpointTxs[inputIndex];
419
+ const checkpoint = Transaction.fromPSBT(base64.decode(checkpointB64));
420
+ spentVtxos.push({
421
+ ...vtxo,
422
+ virtualStatus: { ...vtxo.virtualStatus, state: "spent" },
423
+ spentBy: checkpoint.id,
424
+ arkTxId: arkTxid,
425
+ isSpent: true,
426
+ });
427
+ if (vtxo.virtualStatus.commitmentTxIds) {
428
+ for (const commitmentTxId of vtxo.virtualStatus
429
+ .commitmentTxIds) {
430
+ commitmentTxIds.add(commitmentTxId);
431
+ }
432
+ }
433
+ if (vtxo.virtualStatus.batchExpiry) {
434
+ batchExpiry = Math.min(batchExpiry, vtxo.virtualStatus.batchExpiry);
435
+ }
436
+ }
437
+ const createdAt = Date.now();
438
+ const addr = this.arkAddress.encode();
439
+ if (selected.changeAmount > 0n &&
440
+ batchExpiry !== Number.MAX_SAFE_INTEGER) {
441
+ const changeVtxo = {
442
+ txid: arkTxid,
443
+ vout: outputs.length - 1,
444
+ createdAt: new Date(createdAt),
445
+ forfeitTapLeafScript: this.offchainTapscript.forfeit(),
446
+ intentTapLeafScript: this.offchainTapscript.exit(),
447
+ isUnrolled: false,
448
+ isSpent: false,
449
+ tapTree: this.offchainTapscript.encode(),
450
+ value: Number(selected.changeAmount),
451
+ virtualStatus: {
452
+ state: "preconfirmed",
453
+ commitmentTxIds: Array.from(commitmentTxIds),
454
+ batchExpiry,
455
+ },
456
+ status: {
457
+ confirmed: false,
458
+ },
459
+ };
460
+ await this.walletRepository.saveVtxos(addr, [changeVtxo]);
461
+ }
462
+ await this.walletRepository.saveVtxos(addr, spentVtxos);
463
+ await this.walletRepository.saveTransactions(addr, [
464
+ {
465
+ key: {
466
+ boardingTxid: "",
467
+ commitmentTxid: "",
468
+ arkTxid: arkTxid,
469
+ },
470
+ amount: params.amount,
471
+ type: TxType.TxSent,
472
+ settled: false,
473
+ createdAt: Date.now(),
474
+ },
475
+ ]);
476
+ }
477
+ catch (e) {
478
+ console.warn("error saving offchain tx to repository", e);
479
+ }
480
+ finally {
481
+ return arkTxid;
482
+ }
396
483
  }
397
484
  async settle(params, eventCallback) {
398
485
  if (params?.inputs) {
@@ -665,7 +752,8 @@ export class Wallet {
665
752
  (async () => {
666
753
  try {
667
754
  for await (const update of subscription) {
668
- if (update.newVtxos?.length > 0) {
755
+ if (update.newVtxos?.length > 0 ||
756
+ update.spentVtxos?.length > 0) {
669
757
  eventCallback({
670
758
  type: "vtxo",
671
759
  newVtxos: update.newVtxos.map((vtxo) => extendVirtualCoin(this, vtxo)),
@@ -832,13 +920,12 @@ export class Wallet {
832
920
  }
833
921
  }
834
922
  async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys) {
835
- const nowSeconds = Math.floor(Date.now() / 1000);
836
923
  const inputs = this.prepareIntentProofInputs(coins);
837
924
  const message = {
838
925
  type: "register",
839
926
  onchain_output_indexes: onchainOutputsIndexes,
840
- valid_at: nowSeconds,
841
- expire_at: nowSeconds + 2 * 60, // valid for 2 minutes
927
+ valid_at: 0,
928
+ expire_at: 0,
842
929
  cosigners_public_keys: cosignerPubKeys,
843
930
  };
844
931
  const encodedMessage = JSON.stringify(message, null, 0);
@@ -850,11 +937,10 @@ export class Wallet {
850
937
  };
851
938
  }
852
939
  async makeDeleteIntentSignature(coins) {
853
- const nowSeconds = Math.floor(Date.now() / 1000);
854
940
  const inputs = this.prepareIntentProofInputs(coins);
855
941
  const message = {
856
942
  type: "delete",
857
- expire_at: nowSeconds + 2 * 60, // valid for 2 minutes
943
+ expire_at: 0,
858
944
  };
859
945
  const encodedMessage = JSON.stringify(message, null, 0);
860
946
  const proof = Intent.create(encodedMessage, inputs, []);
@@ -18,9 +18,9 @@ import { Worker } from "./wallet/serviceWorker/worker";
18
18
  import { Request } from "./wallet/serviceWorker/request";
19
19
  import { Response } from "./wallet/serviceWorker/response";
20
20
  import { ESPLORA_URL, EsploraProvider, OnchainProvider, ExplorerTransaction } from "./providers/onchain";
21
- import { RestArkProvider, ArkProvider, SettlementEvent, SettlementEventType, ArkInfo, SignedIntent, Output, TxNotification, BatchFinalizationEvent, BatchFinalizedEvent, BatchFailedEvent, TreeSigningStartedEvent, TreeNoncesEvent, BatchStartedEvent, TreeTxEvent, TreeSignatureEvent, ScheduledSession } from "./providers/ark";
21
+ import { RestArkProvider, ArkProvider, SettlementEvent, SettlementEventType, ArkInfo, SignedIntent, Output, TxNotification, BatchFinalizationEvent, BatchFinalizedEvent, BatchFailedEvent, TreeSigningStartedEvent, TreeNoncesEvent, BatchStartedEvent, TreeTxEvent, TreeSignatureEvent, ScheduledSession, FeeInfo } from "./providers/ark";
22
22
  import { CLTVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CSVMultisigTapscript, decodeTapscript, MultisigTapscript, TapscriptType, ArkTapscript, RelativeTimelock } from "./script/tapscript";
23
- import { hasBoardingTxExpired, buildOffchainTx, verifyTapscriptSignatures, ArkTxInput, OffchainTx } from "./utils/arkTransaction";
23
+ import { hasBoardingTxExpired, buildOffchainTx, verifyTapscriptSignatures, ArkTxInput, OffchainTx, combineTapscriptSigs } from "./utils/arkTransaction";
24
24
  import { VtxoTaprootTree, ConditionWitness, getArkPsbtFields, setArkPsbtField, ArkPsbtFieldCoder, ArkPsbtFieldKey, ArkPsbtFieldKeyType, CosignerPublicKey, VtxoTreeExpiry } from "./utils/unknownFields";
25
25
  import { Intent } from "./intent";
26
26
  import { ArkNote } from "./arknote";
@@ -33,5 +33,5 @@ import { Unroll } from "./wallet/unroll";
33
33
  import { WalletRepositoryImpl } from "./repositories/walletRepository";
34
34
  import { ContractRepositoryImpl } from "./repositories/contractRepository";
35
35
  import { ArkError, maybeArkError } from "./providers/errors";
36
- export { Wallet, SingleKey, OnchainWallet, Ramps, VtxoManager, ESPLORA_URL, EsploraProvider, RestArkProvider, RestIndexerProvider, ArkAddress, DefaultVtxo, VtxoScript, VHTLC, TxType, IndexerTxType, ChainTxType, SettlementEventType, setupServiceWorker, Worker, ServiceWorkerWallet, Request, Response, decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript, TapTreeCoder, ArkPsbtFieldKey, ArkPsbtFieldKeyType, setArkPsbtField, getArkPsbtFields, CosignerPublicKey, VtxoTreeExpiry, VtxoTaprootTree, ConditionWitness, buildOffchainTx, verifyTapscriptSignatures, waitForIncomingFunds, hasBoardingTxExpired, ArkNote, networks, WalletRepositoryImpl, ContractRepositoryImpl, Intent, TxTree, P2A, Unroll, Transaction, ArkError, maybeArkError, };
37
- export type { Identity, IWallet, WalletConfig, ProviderClass, ArkTransaction, Coin, ExtendedCoin, ExtendedVirtualCoin, WalletBalance, SendBitcoinParams, Recipient, SettleParams, Status, VirtualStatus, Outpoint, VirtualCoin, TxKey, TapscriptType, ArkTxInput, OffchainTx, TapLeaves, IncomingFunds, IndexerProvider, PageResponse, Batch, ChainTx, CommitmentTx, TxHistoryRecord, Vtxo, VtxoChain, Tx, OnchainProvider, ArkProvider, SettlementEvent, ArkInfo, SignedIntent, Output, TxNotification, ExplorerTransaction, BatchFinalizationEvent, BatchFinalizedEvent, BatchFailedEvent, TreeSigningStartedEvent, TreeNoncesEvent, BatchStartedEvent, TreeTxEvent, TreeSignatureEvent, ScheduledSession, PaginationOptions, SubscriptionResponse, SubscriptionHeartbeat, SubscriptionEvent, Network, NetworkName, ArkTapscript, RelativeTimelock, EncodedVtxoScript, TapLeafScript, SignerSession, TreeNonces, TreePartialSigs, GetVtxosFilter, Nonces, PartialSig, ArkPsbtFieldCoder, TxTreeNode, AnchorBumper, };
36
+ export { Wallet, SingleKey, OnchainWallet, Ramps, VtxoManager, ESPLORA_URL, EsploraProvider, RestArkProvider, RestIndexerProvider, ArkAddress, DefaultVtxo, VtxoScript, VHTLC, TxType, IndexerTxType, ChainTxType, SettlementEventType, setupServiceWorker, Worker, ServiceWorkerWallet, Request, Response, decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript, TapTreeCoder, ArkPsbtFieldKey, ArkPsbtFieldKeyType, setArkPsbtField, getArkPsbtFields, CosignerPublicKey, VtxoTreeExpiry, VtxoTaprootTree, ConditionWitness, buildOffchainTx, verifyTapscriptSignatures, waitForIncomingFunds, hasBoardingTxExpired, combineTapscriptSigs, ArkNote, networks, WalletRepositoryImpl, ContractRepositoryImpl, Intent, TxTree, P2A, Unroll, Transaction, ArkError, maybeArkError, };
37
+ export type { Identity, IWallet, WalletConfig, ProviderClass, ArkTransaction, Coin, ExtendedCoin, ExtendedVirtualCoin, WalletBalance, SendBitcoinParams, Recipient, SettleParams, Status, VirtualStatus, Outpoint, VirtualCoin, TxKey, TapscriptType, ArkTxInput, OffchainTx, TapLeaves, IncomingFunds, IndexerProvider, PageResponse, Batch, ChainTx, CommitmentTx, TxHistoryRecord, Vtxo, VtxoChain, Tx, OnchainProvider, ArkProvider, SettlementEvent, FeeInfo, ArkInfo, SignedIntent, Output, TxNotification, ExplorerTransaction, BatchFinalizationEvent, BatchFinalizedEvent, BatchFailedEvent, TreeSigningStartedEvent, TreeNoncesEvent, BatchStartedEvent, TreeTxEvent, TreeSignatureEvent, ScheduledSession, PaginationOptions, SubscriptionResponse, SubscriptionHeartbeat, SubscriptionEvent, Network, NetworkName, ArkTapscript, RelativeTimelock, EncodedVtxoScript, TapLeafScript, SignerSession, TreeNonces, TreePartialSigs, GetVtxosFilter, Nonces, PartialSig, ArkPsbtFieldCoder, TxTreeNode, AnchorBumper, };
@@ -75,8 +75,8 @@ export interface ScheduledSession {
75
75
  export interface IntentFeeInfo {
76
76
  offchainInput: string;
77
77
  offchainOutput: string;
78
- onchainInput: string;
79
- onchainOutput: string;
78
+ onchainInput: bigint;
79
+ onchainOutput: bigint;
80
80
  }
81
81
  export interface FeeInfo {
82
82
  intentFee: IntentFeeInfo;
@@ -10,7 +10,6 @@ export interface ContractRepository {
10
10
  }
11
11
  export declare class ContractRepositoryImpl implements ContractRepository {
12
12
  private storage;
13
- private cache;
14
13
  constructor(storage: StorageAdapter);
15
14
  getContractData<T>(contractId: string, key: string): Promise<T | null>;
16
15
  setContractData<T>(contractId: string, key: string, data: T): Promise<void>;
@@ -21,7 +21,6 @@ export interface WalletRepository {
21
21
  }
22
22
  export declare class WalletRepositoryImpl implements WalletRepository {
23
23
  private storage;
24
- private cache;
25
24
  constructor(storage: StorageAdapter);
26
25
  getVtxos(address: string): Promise<ExtendedVirtualCoin[]>;
27
26
  saveVtxos(address: string, vtxos: ExtendedVirtualCoin[]): Promise<void>;
@@ -35,3 +35,9 @@ export declare function hasBoardingTxExpired(coin: ExtendedCoin, boardingTimeloc
35
35
  * @throws Error if verification fails
36
36
  */
37
37
  export declare function verifyTapscriptSignatures(tx: Transaction, inputIndex: number, requiredSigners: string[], excludePubkeys?: string[], allowedSighashTypes?: number[]): void;
38
+ /**
39
+ * Merges the signed transaction with the original transaction
40
+ * @param signedTx signed transaction
41
+ * @param originalTx original transaction
42
+ */
43
+ export declare function combineTapscriptSigs(signedTx: Transaction, originalTx: Transaction): Transaction;
@@ -1,5 +1,5 @@
1
1
  import { ExtendedCoin, IWallet } from ".";
2
- import { SettlementEvent } from "../providers/ark";
2
+ import { FeeInfo, SettlementEvent } from "../providers/ark";
3
3
  /**
4
4
  * Ramps is a class wrapping IWallet.settle method to provide a more convenient interface for onboarding and offboarding operations.
5
5
  *
@@ -25,8 +25,9 @@ export declare class Ramps {
25
25
  * Offboard vtxos, or "collaborative exit" vtxos to onchain address.
26
26
  *
27
27
  * @param destinationAddress - The destination address to offboard to.
28
+ * @param feeInfo - The fee info to deduct from the offboard amount.
28
29
  * @param amount - The amount to offboard. If not provided, the total amount of vtxos will be offboarded.
29
30
  * @param eventCallback - The callback to receive settlement events. optional.
30
31
  */
31
- offboard(destinationAddress: string, amount?: bigint, eventCallback?: (event: SettlementEvent) => void): ReturnType<IWallet["settle"]>;
32
+ offboard(destinationAddress: string, feeInfo: FeeInfo, amount?: bigint, eventCallback?: (event: SettlementEvent) => void): ReturnType<IWallet["settle"]>;
32
33
  }
@@ -1,9 +1,10 @@
1
+ import { TransactionOutput } from "@scure/btc-signer/psbt.js";
1
2
  import { Bytes } from "@scure/btc-signer/utils.js";
2
3
  import { ArkAddress } from "../script/address";
3
4
  import { DefaultVtxo } from "../script/default";
4
5
  import { Network, NetworkName } from "../networks";
5
6
  import { OnchainProvider } from "../providers/onchain";
6
- import { SettlementEvent, ArkProvider } from "../providers/ark";
7
+ import { SettlementEvent, ArkProvider, SignedIntent } from "../providers/ark";
7
8
  import { Identity } from "../identity";
8
9
  import { ArkTransaction, Coin, ExtendedCoin, ExtendedVirtualCoin, GetVtxosFilter, IWallet, SendBitcoinParams, SettleParams, WalletBalance, WalletConfig } from ".";
9
10
  import { CSVMultisigTapscript } from "../script/tapscript";
@@ -93,8 +94,8 @@ export declare class Wallet implements IWallet {
93
94
  private handleSettlementSigningEvent;
94
95
  private handleSettlementTreeNoncesEvent;
95
96
  private handleSettlementFinalizationEvent;
96
- private makeRegisterIntentSignature;
97
- private makeDeleteIntentSignature;
97
+ makeRegisterIntentSignature(coins: ExtendedCoin[], outputs: TransactionOutput[], onchainOutputsIndexes: number[], cosignerPubKeys: string[]): Promise<SignedIntent>;
98
+ makeDeleteIntentSignature(coins: ExtendedCoin[]): Promise<SignedIntent>;
98
99
  private prepareIntentProofInputs;
99
100
  }
100
101
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkade-os/sdk",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Bitcoin wallet SDK with Taproot and Ark integration",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -63,8 +63,8 @@
63
63
  "@types/node": "24.3.1",
64
64
  "@vitest/coverage-v8": "3.2.4",
65
65
  "esbuild": "^0.25.9",
66
- "expo": "~52.0.47",
67
66
  "eventsource": "4.0.0",
67
+ "expo": "~52.0.47",
68
68
  "glob": "11.0.3",
69
69
  "husky": "9.1.7",
70
70
  "prettier": "3.6.2",