@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.
- package/dist/cjs/providers/onchain.js +8 -3
- package/dist/cjs/repositories/contractRepository.js +4 -0
- package/dist/cjs/repositories/walletRepository.js +23 -6
- package/dist/cjs/wallet/serviceWorker/utils.js +9 -0
- package/dist/cjs/wallet/serviceWorker/worker.js +32 -52
- package/dist/cjs/wallet/wallet.js +2 -1
- package/dist/esm/providers/onchain.js +8 -3
- package/dist/esm/repositories/contractRepository.js +4 -0
- package/dist/esm/repositories/walletRepository.js +23 -6
- package/dist/esm/wallet/serviceWorker/utils.js +8 -0
- package/dist/esm/wallet/serviceWorker/worker.js +32 -52
- package/dist/esm/wallet/wallet.js +2 -1
- package/dist/types/providers/onchain.d.ts +1 -0
- package/dist/types/repositories/contractRepository.d.ts +2 -0
- package/dist/types/repositories/walletRepository.d.ts +9 -12
- package/dist/types/wallet/serviceWorker/utils.d.ts +2 -0
- package/dist/types/wallet/serviceWorker/worker.d.ts +1 -2
- package/dist/types/wallet/wallet.d.ts +2 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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.
|
|
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"
|
|
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.
|
|
76
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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.
|
|
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"
|
|
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.
|
|
73
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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<
|
|
21
|
-
saveTransaction(address: string, tx:
|
|
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<
|
|
35
|
-
saveTransaction(address: string, tx:
|
|
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
|
|
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,
|
|
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:
|
|
18
|
+
vtxos: ExtendedVirtualCoin[];
|
|
19
19
|
};
|
|
20
20
|
/**
|
|
21
21
|
* Main wallet implementation for Bitcoin transactions with Ark protocol support.
|