@arkade-os/sdk 0.3.0-alpha.4 → 0.3.0-alpha.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.
@@ -23,6 +23,7 @@ exports.ESPLORA_URL = {
23
23
  class EsploraProvider {
24
24
  constructor(baseUrl) {
25
25
  this.baseUrl = baseUrl;
26
+ this.polling = false;
26
27
  }
27
28
  async getCoins(address) {
28
29
  const response = await fetch(`${this.baseUrl}/address/${address}/utxo`);
@@ -93,6 +94,9 @@ class EsploraProvider {
93
94
  let intervalId = null;
94
95
  const wsUrl = this.baseUrl.replace(/^http(s)?:/, "ws$1:") + "/v1/ws";
95
96
  const poll = async () => {
97
+ if (this.polling)
98
+ return;
99
+ this.polling = true;
96
100
  // websocket is not reliable, so we will fallback to polling
97
101
  const pollingInterval = 5000; // 5 seconds
98
102
  const getAllTxs = () => {
@@ -102,19 +106,19 @@ class EsploraProvider {
102
106
  const initialTxs = await getAllTxs();
103
107
  // we use block_time in key to also notify when a transaction is confirmed
104
108
  const txKey = (tx) => `${tx.txid}_${tx.status.block_time}`;
109
+ // create a set of existing transactions to avoid duplicates
110
+ const existingTxs = new Set(initialTxs.map(txKey));
105
111
  // polling for new transactions
106
112
  intervalId = setInterval(async () => {
107
113
  try {
108
114
  // get current transactions
109
115
  // we will compare with initialTxs to find new ones
110
116
  const currentTxs = await getAllTxs();
111
- // create a set of existing transactions to avoid duplicates
112
- const existingTxs = new Set(initialTxs.map(txKey));
113
117
  // filter out transactions that are already in initialTxs
114
118
  const newTxs = currentTxs.filter((tx) => !existingTxs.has(txKey(tx)));
115
119
  if (newTxs.length > 0) {
116
120
  // Update the tracking set instead of growing the array
117
- initialTxs.push(...newTxs);
121
+ newTxs.forEach((tx) => existingTxs.add(txKey(tx)));
118
122
  callback(newTxs);
119
123
  }
120
124
  }
@@ -175,6 +179,7 @@ class EsploraProvider {
175
179
  ws.close();
176
180
  if (intervalId)
177
181
  clearInterval(intervalId);
182
+ this.polling = false;
178
183
  };
179
184
  return stopFunc;
180
185
  }
@@ -126,5 +126,9 @@ class ContractRepositoryImpl {
126
126
  throw error; // Rethrow to notify caller of failure
127
127
  }
128
128
  }
129
+ async clearContractData() {
130
+ await this.storage.clear();
131
+ this.cache.clear();
132
+ }
129
133
  }
130
134
  exports.ContractRepositoryImpl = ContractRepositoryImpl;
@@ -7,8 +7,7 @@ const btc_signer_1 = require("@scure/btc-signer");
7
7
  const toHex = (b) => (b ? base_1.hex.encode(b) : undefined);
8
8
  const fromHex = (h) => h ? base_1.hex.decode(h) : undefined;
9
9
  const serializeTapLeaf = ([cb, s]) => ({
10
- cb: btc_signer_1.TaprootControlBlock.encode(cb) &&
11
- base_1.hex.encode(btc_signer_1.TaprootControlBlock.encode(cb)),
10
+ cb: base_1.hex.encode(btc_signer_1.TaprootControlBlock.encode(cb)),
12
11
  s: base_1.hex.encode(s),
13
12
  });
14
13
  const serializeVtxo = (v) => ({
@@ -91,7 +90,7 @@ class WalletRepositoryImpl {
91
90
  async removeVtxo(address, vtxoId) {
92
91
  const vtxos = await this.getVtxos(address);
93
92
  const [txid, vout] = vtxoId.split(":");
94
- const filtered = vtxos.filter((v) => !(v.txid === txid && v.vout === parseInt(vout)));
93
+ const filtered = vtxos.filter((v) => !(v.txid === txid && v.vout === parseInt(vout, 10)));
95
94
  this.cache.vtxos.set(address, filtered);
96
95
  await this.storage.setItem(`vtxos:${address}`, JSON.stringify(filtered.map(serializeVtxo)));
97
96
  }
@@ -122,18 +121,36 @@ class WalletRepositoryImpl {
122
121
  }
123
122
  async saveTransaction(address, tx) {
124
123
  const transactions = await this.getTransactionHistory(address);
125
- const existing = transactions.findIndex((t) => t.id === tx.id);
124
+ const existing = transactions.findIndex((t) => t.key === tx.key);
126
125
  if (existing !== -1) {
127
126
  transactions[existing] = tx;
128
127
  }
129
128
  else {
130
129
  transactions.push(tx);
131
- // Sort by timestamp descending
132
- transactions.sort((a, b) => b.timestamp - a.timestamp);
133
130
  }
131
+ // Sort by createdAt descending
132
+ transactions.sort((a, b) => b.createdAt - a.createdAt);
134
133
  this.cache.transactions.set(address, transactions);
135
134
  await this.storage.setItem(`tx:${address}`, JSON.stringify(transactions));
136
135
  }
136
+ async saveTransactions(address, txs) {
137
+ const storedTransactions = await this.getTransactionHistory(address);
138
+ for (const tx of txs) {
139
+ const existing = storedTransactions.findIndex((t) => t.key === tx.key);
140
+ if (existing !== -1) {
141
+ storedTransactions[existing] = tx;
142
+ }
143
+ else {
144
+ storedTransactions.push(tx);
145
+ }
146
+ }
147
+ this.cache.transactions.set(address, storedTransactions);
148
+ await this.storage.setItem(`tx:${address}`, JSON.stringify(storedTransactions));
149
+ }
150
+ async clearTransactions(address) {
151
+ this.cache.transactions.set(address, []);
152
+ await this.storage.removeItem(`tx:${address}`);
153
+ }
137
154
  async getWalletState() {
138
155
  if (this.cache.walletState !== null ||
139
156
  this.cache.initialized.has("walletState")) {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.setupServiceWorker = setupServiceWorker;
4
+ exports.extendVirtualCoin = extendVirtualCoin;
4
5
  /**
5
6
  * setupServiceWorker sets up the service worker.
6
7
  * @param path - the path to the service worker script
@@ -47,3 +48,11 @@ async function setupServiceWorker(path) {
47
48
  navigator.serviceWorker.addEventListener("error", onError);
48
49
  });
49
50
  }
51
+ function extendVirtualCoin(wallet, vtxo) {
52
+ return {
53
+ ...vtxo,
54
+ forfeitTapLeafScript: wallet.offchainTapscript.forfeit(),
55
+ intentTapLeafScript: wallet.offchainTapscript.exit(),
56
+ tapTree: wallet.offchainTapscript.encode(),
57
+ };
58
+ }
@@ -13,6 +13,7 @@ const indexer_1 = require("../../providers/indexer");
13
13
  const base_1 = require("@scure/base");
14
14
  const indexedDB_1 = require("../../storage/indexedDB");
15
15
  const walletRepository_1 = require("../../repositories/walletRepository");
16
+ const utils_1 = require("./utils");
16
17
  /**
17
18
  * Worker is a class letting to interact with ServiceWorkerWallet from the client
18
19
  * it aims to be run in a service worker context
@@ -41,7 +42,7 @@ class Worker {
41
42
  return [];
42
43
  const address = await this.wallet.getAddress();
43
44
  const allVtxos = await this.walletRepository.getVtxos(address);
44
- return allVtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept" && (0, __1.isSpendable)(vtxo));
45
+ return allVtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
45
46
  }
46
47
  /**
47
48
  * Get all vtxos categorized by type
@@ -72,19 +73,15 @@ class Worker {
72
73
  }
73
74
  }
74
75
  async clear() {
75
- if (this.vtxoSubscription) {
76
- this.vtxoSubscription.abort();
77
- }
76
+ if (this.incomingFundsSubscription)
77
+ this.incomingFundsSubscription();
78
78
  // Clear storage - this replaces vtxoRepository.close()
79
79
  await this.storage.clear();
80
80
  this.wallet = undefined;
81
81
  this.arkProvider = undefined;
82
82
  this.indexerProvider = undefined;
83
- this.vtxoSubscription = undefined;
84
83
  }
85
84
  async reload() {
86
- if (this.vtxoSubscription)
87
- this.vtxoSubscription.abort();
88
85
  await this.onWalletInitialized();
89
86
  }
90
87
  async onWalletInitialized() {
@@ -95,58 +92,37 @@ class Worker {
95
92
  !this.wallet.boardingTapscript) {
96
93
  return;
97
94
  }
98
- const encodedOffchainTapscript = this.wallet.offchainTapscript.encode();
99
- const forfeit = this.wallet.offchainTapscript.forfeit();
100
- const exit = this.wallet.offchainTapscript.exit();
95
+ // Get public key script and set the initial vtxos state
101
96
  const script = base_1.hex.encode(this.wallet.offchainTapscript.pkScript);
102
- // set the initial vtxos state
103
97
  const response = await this.indexerProvider.getVtxos({
104
98
  scripts: [script],
105
99
  });
106
- const vtxos = response.vtxos.map((vtxo) => ({
107
- ...vtxo,
108
- forfeitTapLeafScript: forfeit,
109
- intentTapLeafScript: exit,
110
- tapTree: encodedOffchainTapscript,
111
- }));
100
+ const vtxos = response.vtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this.wallet, vtxo));
112
101
  // Get wallet address and save vtxos using unified repository
113
102
  const address = await this.wallet.getAddress();
114
103
  await this.walletRepository.saveVtxos(address, vtxos);
115
- this.processVtxoSubscription({
116
- script,
117
- vtxoScript: this.wallet.offchainTapscript,
118
- });
119
- }
120
- async processVtxoSubscription({ script, vtxoScript, }) {
121
- try {
122
- const forfeitTapLeafScript = vtxoScript.forfeit();
123
- const intentTapLeafScript = vtxoScript.exit();
124
- const abortController = new AbortController();
125
- const subscriptionId = await this.indexerProvider.subscribeForScripts([script]);
126
- const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
127
- this.vtxoSubscription = abortController;
128
- const tapTree = vtxoScript.encode();
129
- for await (const update of subscription) {
130
- const vtxos = [...update.newVtxos, ...update.spentVtxos];
131
- if (vtxos.length === 0) {
132
- continue;
133
- }
134
- const extendedVtxos = vtxos.map((vtxo) => ({
135
- ...vtxo,
136
- forfeitTapLeafScript,
137
- intentTapLeafScript,
138
- tapTree,
139
- }));
140
- // Get wallet address and save vtxos using unified repository
141
- const address = await this.wallet.getAddress();
142
- await this.walletRepository.saveVtxos(address, extendedVtxos);
143
- // Notify all clients about the vtxo update
144
- this.sendMessageToAllClients("VTXO_UPDATE", "");
104
+ // Get transaction history to cache boarding txs
105
+ const txs = await this.wallet.getTransactionHistory();
106
+ if (txs)
107
+ await this.walletRepository.saveTransactions(address, txs);
108
+ // stop previous subscriptions if any
109
+ if (this.incomingFundsSubscription)
110
+ this.incomingFundsSubscription();
111
+ // subscribe for incoming funds and notify all clients when new funds arrive
112
+ this.incomingFundsSubscription = await this.wallet.notifyIncomingFunds(async (funds) => {
113
+ if (funds.type === "vtxo" && funds.vtxos.length > 0) {
114
+ // extend vtxos with taproot scripts
115
+ const extendedVtxos = funds.vtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this.wallet, vtxo));
116
+ // save vtxos using unified repository
117
+ await this.walletRepository.saveVtxos(address, funds.vtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this.wallet, vtxo)));
118
+ // notify all clients about the vtxo update
119
+ this.sendMessageToAllClients("VTXO_UPDATE", JSON.stringify(extendedVtxos));
145
120
  }
146
- }
147
- catch (error) {
148
- console.error("Error processing address updates:", error);
149
- }
121
+ if (funds.type === "utxo" && funds.coins.length > 0) {
122
+ // notify all clients about the utxo update
123
+ this.sendMessageToAllClients("UTXO_UPDATE", JSON.stringify(funds.coins));
124
+ }
125
+ });
150
126
  }
151
127
  async handleClear(event) {
152
128
  await this.clear();
@@ -374,7 +350,11 @@ class Worker {
374
350
  if (!this.wallet)
375
351
  throw new Error("Wallet not initialized");
376
352
  // exclude subdust is we don't want recoverable
377
- vtxos = vtxos.filter((v) => !(0, __1.isSubdust)(v, this.wallet.dustAmount));
353
+ const dustAmount = this.wallet?.dustAmount;
354
+ vtxos =
355
+ dustAmount == null
356
+ ? vtxos
357
+ : vtxos.filter((v) => !(0, __1.isSubdust)(v, dustAmount));
378
358
  }
379
359
  if (message.filter?.withRecoverable) {
380
360
  // get also swept and spendable vtxos
@@ -60,6 +60,7 @@ const txTree_1 = require("../tree/txTree");
60
60
  const inMemory_1 = require("../storage/inMemory");
61
61
  const walletRepository_1 = require("../repositories/walletRepository");
62
62
  const contractRepository_1 = require("../repositories/contractRepository");
63
+ const utils_1 = require("./serviceWorker/utils");
63
64
  /**
64
65
  * Main wallet implementation for Bitcoin transactions with Ark protocol support.
65
66
  * The wallet does not store any data locally and relies on Ark and onchain
@@ -678,7 +679,7 @@ class Wallet {
678
679
  if (update.newVtxos?.length > 0) {
679
680
  eventCallback({
680
681
  type: "vtxo",
681
- vtxos: update.newVtxos,
682
+ vtxos: update.newVtxos.map((vtxo) => (0, utils_1.extendVirtualCoin)(this, vtxo)),
682
683
  });
683
684
  }
684
685
  }
@@ -20,6 +20,7 @@ export const ESPLORA_URL = {
20
20
  export class EsploraProvider {
21
21
  constructor(baseUrl) {
22
22
  this.baseUrl = baseUrl;
23
+ this.polling = false;
23
24
  }
24
25
  async getCoins(address) {
25
26
  const response = await fetch(`${this.baseUrl}/address/${address}/utxo`);
@@ -90,6 +91,9 @@ export class EsploraProvider {
90
91
  let intervalId = null;
91
92
  const wsUrl = this.baseUrl.replace(/^http(s)?:/, "ws$1:") + "/v1/ws";
92
93
  const poll = async () => {
94
+ if (this.polling)
95
+ return;
96
+ this.polling = true;
93
97
  // websocket is not reliable, so we will fallback to polling
94
98
  const pollingInterval = 5000; // 5 seconds
95
99
  const getAllTxs = () => {
@@ -99,19 +103,19 @@ export class EsploraProvider {
99
103
  const initialTxs = await getAllTxs();
100
104
  // we use block_time in key to also notify when a transaction is confirmed
101
105
  const txKey = (tx) => `${tx.txid}_${tx.status.block_time}`;
106
+ // create a set of existing transactions to avoid duplicates
107
+ const existingTxs = new Set(initialTxs.map(txKey));
102
108
  // polling for new transactions
103
109
  intervalId = setInterval(async () => {
104
110
  try {
105
111
  // get current transactions
106
112
  // we will compare with initialTxs to find new ones
107
113
  const currentTxs = await getAllTxs();
108
- // create a set of existing transactions to avoid duplicates
109
- const existingTxs = new Set(initialTxs.map(txKey));
110
114
  // filter out transactions that are already in initialTxs
111
115
  const newTxs = currentTxs.filter((tx) => !existingTxs.has(txKey(tx)));
112
116
  if (newTxs.length > 0) {
113
117
  // Update the tracking set instead of growing the array
114
- initialTxs.push(...newTxs);
118
+ newTxs.forEach((tx) => existingTxs.add(txKey(tx)));
115
119
  callback(newTxs);
116
120
  }
117
121
  }
@@ -172,6 +176,7 @@ export class EsploraProvider {
172
176
  ws.close();
173
177
  if (intervalId)
174
178
  clearInterval(intervalId);
179
+ this.polling = false;
175
180
  };
176
181
  return stopFunc;
177
182
  }
@@ -123,4 +123,8 @@ export class ContractRepositoryImpl {
123
123
  throw error; // Rethrow to notify caller of failure
124
124
  }
125
125
  }
126
+ async clearContractData() {
127
+ await this.storage.clear();
128
+ this.cache.clear();
129
+ }
126
130
  }
@@ -4,8 +4,7 @@ import { TaprootControlBlock } from "@scure/btc-signer";
4
4
  const toHex = (b) => (b ? hex.encode(b) : undefined);
5
5
  const fromHex = (h) => h ? hex.decode(h) : undefined;
6
6
  const serializeTapLeaf = ([cb, s]) => ({
7
- cb: TaprootControlBlock.encode(cb) &&
8
- hex.encode(TaprootControlBlock.encode(cb)),
7
+ cb: hex.encode(TaprootControlBlock.encode(cb)),
9
8
  s: hex.encode(s),
10
9
  });
11
10
  const serializeVtxo = (v) => ({
@@ -88,7 +87,7 @@ export class WalletRepositoryImpl {
88
87
  async removeVtxo(address, vtxoId) {
89
88
  const vtxos = await this.getVtxos(address);
90
89
  const [txid, vout] = vtxoId.split(":");
91
- const filtered = vtxos.filter((v) => !(v.txid === txid && v.vout === parseInt(vout)));
90
+ const filtered = vtxos.filter((v) => !(v.txid === txid && v.vout === parseInt(vout, 10)));
92
91
  this.cache.vtxos.set(address, filtered);
93
92
  await this.storage.setItem(`vtxos:${address}`, JSON.stringify(filtered.map(serializeVtxo)));
94
93
  }
@@ -119,18 +118,36 @@ export class WalletRepositoryImpl {
119
118
  }
120
119
  async saveTransaction(address, tx) {
121
120
  const transactions = await this.getTransactionHistory(address);
122
- const existing = transactions.findIndex((t) => t.id === tx.id);
121
+ const existing = transactions.findIndex((t) => t.key === tx.key);
123
122
  if (existing !== -1) {
124
123
  transactions[existing] = tx;
125
124
  }
126
125
  else {
127
126
  transactions.push(tx);
128
- // Sort by timestamp descending
129
- transactions.sort((a, b) => b.timestamp - a.timestamp);
130
127
  }
128
+ // Sort by createdAt descending
129
+ transactions.sort((a, b) => b.createdAt - a.createdAt);
131
130
  this.cache.transactions.set(address, transactions);
132
131
  await this.storage.setItem(`tx:${address}`, JSON.stringify(transactions));
133
132
  }
133
+ async saveTransactions(address, txs) {
134
+ const storedTransactions = await this.getTransactionHistory(address);
135
+ for (const tx of txs) {
136
+ const existing = storedTransactions.findIndex((t) => t.key === tx.key);
137
+ if (existing !== -1) {
138
+ storedTransactions[existing] = tx;
139
+ }
140
+ else {
141
+ storedTransactions.push(tx);
142
+ }
143
+ }
144
+ this.cache.transactions.set(address, storedTransactions);
145
+ await this.storage.setItem(`tx:${address}`, JSON.stringify(storedTransactions));
146
+ }
147
+ async clearTransactions(address) {
148
+ this.cache.transactions.set(address, []);
149
+ await this.storage.removeItem(`tx:${address}`);
150
+ }
134
151
  async getWalletState() {
135
152
  if (this.cache.walletState !== null ||
136
153
  this.cache.initialized.has("walletState")) {
@@ -44,3 +44,11 @@ export async function setupServiceWorker(path) {
44
44
  navigator.serviceWorker.addEventListener("error", onError);
45
45
  });
46
46
  }
47
+ export function extendVirtualCoin(wallet, vtxo) {
48
+ return {
49
+ ...vtxo,
50
+ forfeitTapLeafScript: wallet.offchainTapscript.forfeit(),
51
+ intentTapLeafScript: wallet.offchainTapscript.exit(),
52
+ tapTree: wallet.offchainTapscript.encode(),
53
+ };
54
+ }
@@ -10,6 +10,7 @@ import { RestIndexerProvider } from '../../providers/indexer.js';
10
10
  import { hex } from "@scure/base";
11
11
  import { IndexedDBStorageAdapter } from '../../storage/indexedDB.js';
12
12
  import { WalletRepositoryImpl, } from '../../repositories/walletRepository.js';
13
+ import { extendVirtualCoin } from './utils.js';
13
14
  /**
14
15
  * Worker is a class letting to interact with ServiceWorkerWallet from the client
15
16
  * it aims to be run in a service worker context
@@ -38,7 +39,7 @@ export class Worker {
38
39
  return [];
39
40
  const address = await this.wallet.getAddress();
40
41
  const allVtxos = await this.walletRepository.getVtxos(address);
41
- return allVtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept" && isSpendable(vtxo));
42
+ return allVtxos.filter((vtxo) => vtxo.virtualStatus.state === "swept");
42
43
  }
43
44
  /**
44
45
  * Get all vtxos categorized by type
@@ -69,19 +70,15 @@ export class Worker {
69
70
  }
70
71
  }
71
72
  async clear() {
72
- if (this.vtxoSubscription) {
73
- this.vtxoSubscription.abort();
74
- }
73
+ if (this.incomingFundsSubscription)
74
+ this.incomingFundsSubscription();
75
75
  // Clear storage - this replaces vtxoRepository.close()
76
76
  await this.storage.clear();
77
77
  this.wallet = undefined;
78
78
  this.arkProvider = undefined;
79
79
  this.indexerProvider = undefined;
80
- this.vtxoSubscription = undefined;
81
80
  }
82
81
  async reload() {
83
- if (this.vtxoSubscription)
84
- this.vtxoSubscription.abort();
85
82
  await this.onWalletInitialized();
86
83
  }
87
84
  async onWalletInitialized() {
@@ -92,58 +89,37 @@ export class Worker {
92
89
  !this.wallet.boardingTapscript) {
93
90
  return;
94
91
  }
95
- const encodedOffchainTapscript = this.wallet.offchainTapscript.encode();
96
- const forfeit = this.wallet.offchainTapscript.forfeit();
97
- const exit = this.wallet.offchainTapscript.exit();
92
+ // Get public key script and set the initial vtxos state
98
93
  const script = hex.encode(this.wallet.offchainTapscript.pkScript);
99
- // set the initial vtxos state
100
94
  const response = await this.indexerProvider.getVtxos({
101
95
  scripts: [script],
102
96
  });
103
- const vtxos = response.vtxos.map((vtxo) => ({
104
- ...vtxo,
105
- forfeitTapLeafScript: forfeit,
106
- intentTapLeafScript: exit,
107
- tapTree: encodedOffchainTapscript,
108
- }));
97
+ const vtxos = response.vtxos.map((vtxo) => extendVirtualCoin(this.wallet, vtxo));
109
98
  // Get wallet address and save vtxos using unified repository
110
99
  const address = await this.wallet.getAddress();
111
100
  await this.walletRepository.saveVtxos(address, vtxos);
112
- this.processVtxoSubscription({
113
- script,
114
- vtxoScript: this.wallet.offchainTapscript,
115
- });
116
- }
117
- async processVtxoSubscription({ script, vtxoScript, }) {
118
- try {
119
- const forfeitTapLeafScript = vtxoScript.forfeit();
120
- const intentTapLeafScript = vtxoScript.exit();
121
- const abortController = new AbortController();
122
- const subscriptionId = await this.indexerProvider.subscribeForScripts([script]);
123
- const subscription = this.indexerProvider.getSubscription(subscriptionId, abortController.signal);
124
- this.vtxoSubscription = abortController;
125
- const tapTree = vtxoScript.encode();
126
- for await (const update of subscription) {
127
- const vtxos = [...update.newVtxos, ...update.spentVtxos];
128
- if (vtxos.length === 0) {
129
- continue;
130
- }
131
- const extendedVtxos = vtxos.map((vtxo) => ({
132
- ...vtxo,
133
- forfeitTapLeafScript,
134
- intentTapLeafScript,
135
- tapTree,
136
- }));
137
- // Get wallet address and save vtxos using unified repository
138
- const address = await this.wallet.getAddress();
139
- await this.walletRepository.saveVtxos(address, extendedVtxos);
140
- // Notify all clients about the vtxo update
141
- this.sendMessageToAllClients("VTXO_UPDATE", "");
101
+ // Get transaction history to cache boarding txs
102
+ const txs = await this.wallet.getTransactionHistory();
103
+ if (txs)
104
+ await this.walletRepository.saveTransactions(address, txs);
105
+ // stop previous subscriptions if any
106
+ if (this.incomingFundsSubscription)
107
+ this.incomingFundsSubscription();
108
+ // subscribe for incoming funds and notify all clients when new funds arrive
109
+ this.incomingFundsSubscription = await this.wallet.notifyIncomingFunds(async (funds) => {
110
+ if (funds.type === "vtxo" && funds.vtxos.length > 0) {
111
+ // extend vtxos with taproot scripts
112
+ const extendedVtxos = funds.vtxos.map((vtxo) => extendVirtualCoin(this.wallet, vtxo));
113
+ // save vtxos using unified repository
114
+ await this.walletRepository.saveVtxos(address, funds.vtxos.map((vtxo) => extendVirtualCoin(this.wallet, vtxo)));
115
+ // notify all clients about the vtxo update
116
+ this.sendMessageToAllClients("VTXO_UPDATE", JSON.stringify(extendedVtxos));
142
117
  }
143
- }
144
- catch (error) {
145
- console.error("Error processing address updates:", error);
146
- }
118
+ if (funds.type === "utxo" && funds.coins.length > 0) {
119
+ // notify all clients about the utxo update
120
+ this.sendMessageToAllClients("UTXO_UPDATE", JSON.stringify(funds.coins));
121
+ }
122
+ });
147
123
  }
148
124
  async handleClear(event) {
149
125
  await this.clear();
@@ -371,7 +347,11 @@ export class Worker {
371
347
  if (!this.wallet)
372
348
  throw new Error("Wallet not initialized");
373
349
  // exclude subdust is we don't want recoverable
374
- vtxos = vtxos.filter((v) => !isSubdust(v, this.wallet.dustAmount));
350
+ const dustAmount = this.wallet?.dustAmount;
351
+ vtxos =
352
+ dustAmount == null
353
+ ? vtxos
354
+ : vtxos.filter((v) => !isSubdust(v, dustAmount));
375
355
  }
376
356
  if (message.filter?.withRecoverable) {
377
357
  // get also swept and spendable vtxos
@@ -23,6 +23,7 @@ import { TxTree } from '../tree/txTree.js';
23
23
  import { InMemoryStorageAdapter } from '../storage/inMemory.js';
24
24
  import { WalletRepositoryImpl, } from '../repositories/walletRepository.js';
25
25
  import { ContractRepositoryImpl, } from '../repositories/contractRepository.js';
26
+ import { extendVirtualCoin } from './serviceWorker/utils.js';
26
27
  /**
27
28
  * Main wallet implementation for Bitcoin transactions with Ark protocol support.
28
29
  * The wallet does not store any data locally and relies on Ark and onchain
@@ -641,7 +642,7 @@ export class Wallet {
641
642
  if (update.newVtxos?.length > 0) {
642
643
  eventCallback({
643
644
  type: "vtxo",
644
- vtxos: update.newVtxos,
645
+ vtxos: update.newVtxos.map((vtxo) => extendVirtualCoin(this, vtxo)),
645
646
  });
646
647
  }
647
648
  }
@@ -49,6 +49,7 @@ export interface OnchainProvider {
49
49
  */
50
50
  export declare class EsploraProvider implements OnchainProvider {
51
51
  private baseUrl;
52
+ private polling;
52
53
  constructor(baseUrl: string);
53
54
  getCoins(address: string): Promise<Coin[]>;
54
55
  getFeeRate(): Promise<number | undefined>;
@@ -3,6 +3,7 @@ export interface ContractRepository {
3
3
  getContractData<T>(contractId: string, key: string): Promise<T | null>;
4
4
  setContractData<T>(contractId: string, key: string, data: T): Promise<void>;
5
5
  deleteContractData(contractId: string, key: string): Promise<void>;
6
+ clearContractData(): Promise<void>;
6
7
  getContractCollection<T>(contractType: string): Promise<ReadonlyArray<T>>;
7
8
  saveToContractCollection<T, K extends keyof T>(contractType: string, item: T, idField: K): Promise<void>;
8
9
  removeFromContractCollection<T, K extends keyof T>(contractType: string, id: T[K], idField: K): Promise<void>;
@@ -17,4 +18,5 @@ export declare class ContractRepositoryImpl implements ContractRepository {
17
18
  getContractCollection<T>(contractType: string): Promise<ReadonlyArray<T>>;
18
19
  saveToContractCollection<T, K extends keyof T>(contractType: string, item: T, idField: K): Promise<void>;
19
20
  removeFromContractCollection<T, K extends keyof T>(contractType: string, id: T[K], idField: K): Promise<void>;
21
+ clearContractData(): Promise<void>;
20
22
  }
@@ -1,24 +1,19 @@
1
1
  import { StorageAdapter } from "../storage";
2
- import { ExtendedVirtualCoin } from "../wallet";
2
+ import { ArkTransaction, ExtendedVirtualCoin } from "../wallet";
3
3
  export interface WalletState {
4
4
  lastSyncTime?: number;
5
5
  settings?: Record<string, any>;
6
6
  }
7
- export interface Transaction {
8
- id: string;
9
- timestamp: number;
10
- amount: number;
11
- type: "send" | "receive";
12
- status: "pending" | "confirmed" | "failed";
13
- }
14
7
  export interface WalletRepository {
15
8
  getVtxos(address: string): Promise<ExtendedVirtualCoin[]>;
16
9
  saveVtxo(address: string, vtxo: ExtendedVirtualCoin): Promise<void>;
17
10
  saveVtxos(address: string, vtxos: ExtendedVirtualCoin[]): Promise<void>;
18
11
  removeVtxo(address: string, vtxoId: string): Promise<void>;
19
12
  clearVtxos(address: string): Promise<void>;
20
- getTransactionHistory(address: string): Promise<Transaction[]>;
21
- saveTransaction(address: string, tx: Transaction): Promise<void>;
13
+ getTransactionHistory(address: string): Promise<ArkTransaction[]>;
14
+ saveTransaction(address: string, tx: ArkTransaction): Promise<void>;
15
+ saveTransactions(address: string, txs: ArkTransaction[]): Promise<void>;
16
+ clearTransactions(address: string): Promise<void>;
22
17
  getWalletState(): Promise<WalletState | null>;
23
18
  saveWalletState(state: WalletState): Promise<void>;
24
19
  }
@@ -31,8 +26,10 @@ export declare class WalletRepositoryImpl implements WalletRepository {
31
26
  saveVtxos(address: string, vtxos: ExtendedVirtualCoin[]): Promise<void>;
32
27
  removeVtxo(address: string, vtxoId: string): Promise<void>;
33
28
  clearVtxos(address: string): Promise<void>;
34
- getTransactionHistory(address: string): Promise<Transaction[]>;
35
- saveTransaction(address: string, tx: Transaction): Promise<void>;
29
+ getTransactionHistory(address: string): Promise<ArkTransaction[]>;
30
+ saveTransaction(address: string, tx: ArkTransaction): Promise<void>;
31
+ saveTransactions(address: string, txs: ArkTransaction[]): Promise<void>;
32
+ clearTransactions(address: string): Promise<void>;
36
33
  getWalletState(): Promise<WalletState | null>;
37
34
  saveWalletState(state: WalletState): Promise<void>;
38
35
  }
@@ -1,3 +1,4 @@
1
+ import { ExtendedVirtualCoin, VirtualCoin, Wallet } from "../..";
1
2
  /**
2
3
  * setupServiceWorker sets up the service worker.
3
4
  * @param path - the path to the service worker script
@@ -7,3 +8,4 @@
7
8
  * ```
8
9
  */
9
10
  export declare function setupServiceWorker(path: string): Promise<ServiceWorker>;
11
+ export declare function extendVirtualCoin(wallet: Wallet, vtxo: VirtualCoin): ExtendedVirtualCoin;
@@ -7,7 +7,7 @@ export declare class Worker {
7
7
  private wallet;
8
8
  private arkProvider;
9
9
  private indexerProvider;
10
- private vtxoSubscription;
10
+ private incomingFundsSubscription;
11
11
  private walletRepository;
12
12
  private storage;
13
13
  constructor(messageCallback?: (message: ExtendableMessageEvent) => void);
@@ -27,7 +27,6 @@ export declare class Worker {
27
27
  clear(): Promise<void>;
28
28
  reload(): Promise<void>;
29
29
  private onWalletInitialized;
30
- private processVtxoSubscription;
31
30
  private handleClear;
32
31
  private handleInitWallet;
33
32
  private handleSettle;
@@ -4,7 +4,7 @@ import { Network, NetworkName } from "../networks";
4
4
  import { OnchainProvider } from "../providers/onchain";
5
5
  import { SettlementEvent, ArkProvider } from "../providers/ark";
6
6
  import { Identity } from "../identity";
7
- import { ArkTransaction, Coin, ExtendedCoin, ExtendedVirtualCoin, GetVtxosFilter, IWallet, SendBitcoinParams, SettleParams, VirtualCoin, WalletBalance, WalletConfig } from ".";
7
+ import { ArkTransaction, Coin, ExtendedCoin, ExtendedVirtualCoin, GetVtxosFilter, IWallet, SendBitcoinParams, SettleParams, WalletBalance, WalletConfig } from ".";
8
8
  import { Bytes } from "@scure/btc-signer/utils.js";
9
9
  import { CSVMultisigTapscript } from "../script/tapscript";
10
10
  import { IndexerProvider } from "../providers/indexer";
@@ -15,7 +15,7 @@ export type IncomingFunds = {
15
15
  coins: Coin[];
16
16
  } | {
17
17
  type: "vtxo";
18
- vtxos: VirtualCoin[];
18
+ vtxos: ExtendedVirtualCoin[];
19
19
  };
20
20
  /**
21
21
  * Main wallet implementation for Bitcoin transactions with Ark protocol support.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkade-os/sdk",
3
- "version": "0.3.0-alpha.4",
3
+ "version": "0.3.0-alpha.5",
4
4
  "description": "Bitcoin wallet SDK with Taproot and Ark integration",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",