@arklabs/wallet-sdk
Version:
Bitcoin wallet SDK with Taproot and Ark integration
453 lines (452 loc) • 19.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Worker = void 0;
/// <reference lib="webworker" />
const inMemoryKey_1 = require("../../identity/inMemoryKey");
const wallet_1 = require("../wallet");
const request_1 = require("./request");
const response_1 = require("./response");
const ark_1 = require("../../providers/ark");
const default_1 = require("../../script/default");
const idb_1 = require("./db/vtxo/idb");
const transactionHistory_1 = require("../../utils/transactionHistory");
// Worker is a class letting to interact with ServiceWorkerWallet from the client
// it aims to be run in a service worker context
class Worker {
constructor(vtxoRepository = new idb_1.IndexedDBVtxoRepository(), messageCallback = () => { }) {
this.vtxoRepository = vtxoRepository;
this.messageCallback = messageCallback;
}
async start() {
self.addEventListener("message", async (event) => {
await this.handleMessage(event);
});
}
async clear() {
if (this.vtxoSubscription) {
this.vtxoSubscription.abort();
}
await this.vtxoRepository.close();
this.wallet = undefined;
this.arkProvider = undefined;
this.vtxoSubscription = undefined;
}
async onWalletInitialized() {
if (!this.wallet ||
!this.arkProvider ||
!this.wallet.offchainTapscript ||
!this.wallet.boardingTapscript) {
return;
}
// subscribe to address updates
const addressInfo = await this.wallet.getAddressInfo();
if (!addressInfo.offchain) {
return;
}
await this.vtxoRepository.open();
// set the initial vtxos state
const { spendableVtxos, spentVtxos } = await this.arkProvider.getVirtualCoins(addressInfo.offchain.address);
const encodedOffchainTapscript = this.wallet.offchainTapscript.encode();
const forfeit = this.wallet.offchainTapscript.forfeit();
const vtxos = [...spendableVtxos, ...spentVtxos].map((vtxo) => ({
...vtxo,
tapLeafScript: forfeit,
scripts: encodedOffchainTapscript,
}));
await this.vtxoRepository.addOrUpdate(vtxos);
this.processVtxoSubscription(addressInfo.offchain);
}
async processVtxoSubscription({ address, scripts, }) {
try {
const addressScripts = [...scripts.exit, ...scripts.forfeit];
const vtxoScript = default_1.DefaultVtxo.Script.decode(addressScripts);
const tapLeafScript = vtxoScript.findLeaf(scripts.forfeit[0]);
const abortController = new AbortController();
const subscription = this.arkProvider.subscribeForAddress(address, abortController.signal);
this.vtxoSubscription = abortController;
for await (const update of subscription) {
const vtxos = [...update.newVtxos, ...update.spentVtxos];
if (vtxos.length === 0) {
continue;
}
const extendedVtxos = vtxos.map((vtxo) => ({
...vtxo,
tapLeafScript,
scripts: addressScripts,
}));
await this.vtxoRepository.addOrUpdate(extendedVtxos);
}
}
catch (error) {
console.error("Error processing address updates:", error);
}
}
async handleClear(event) {
this.clear();
if (request_1.Request.isBase(event.data)) {
event.source?.postMessage(response_1.Response.clearResponse(event.data.id, true));
}
}
async handleInitWallet(event) {
const message = event.data;
if (!request_1.Request.isInitWallet(message)) {
console.error("Invalid INIT_WALLET message format", message);
event.source?.postMessage(response_1.Response.error(message.id, "Invalid INIT_WALLET message format"));
return;
}
try {
this.arkProvider = new ark_1.RestArkProvider(message.arkServerUrl);
this.wallet = await wallet_1.Wallet.create({
network: message.network,
identity: inMemoryKey_1.InMemoryKey.fromHex(message.privateKey),
arkServerUrl: message.arkServerUrl,
arkServerPublicKey: message.arkServerPublicKey,
});
event.source?.postMessage(response_1.Response.walletInitialized(message.id));
await this.onWalletInitialized();
}
catch (error) {
console.error("Error initializing wallet:", error);
const errorMessage = error instanceof Error
? error.message
: "Unknown error occurred";
event.source?.postMessage(response_1.Response.error(message.id, errorMessage));
}
}
async handleSettle(event) {
const message = event.data;
if (!request_1.Request.isSettle(message)) {
console.error("Invalid SETTLE message format", message);
event.source?.postMessage(response_1.Response.error(message.id, "Invalid SETTLE message format"));
return;
}
try {
if (!this.wallet) {
console.error("Wallet not initialized");
event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized"));
return;
}
const txid = await this.wallet.settle(message.params, (e) => {
event.source?.postMessage(response_1.Response.settleEvent(message.id, e));
});
event.source?.postMessage(response_1.Response.settleSuccess(message.id, txid));
}
catch (error) {
console.error("Error settling:", error);
const errorMessage = error instanceof Error
? error.message
: "Unknown error occurred";
event.source?.postMessage(response_1.Response.error(message.id, errorMessage));
}
}
async handleSendBitcoin(event) {
const message = event.data;
if (!request_1.Request.isSendBitcoin(message)) {
console.error("Invalid SEND_BITCOIN message format", message);
event.source?.postMessage(response_1.Response.error(message.id, "Invalid SEND_BITCOIN message format"));
return;
}
if (!this.wallet) {
console.error("Wallet not initialized");
event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized"));
return;
}
try {
const txid = await this.wallet.sendBitcoin(message.params, message.zeroFee);
event.source?.postMessage(response_1.Response.sendBitcoinSuccess(message.id, txid));
}
catch (error) {
console.error("Error sending bitcoin:", error);
const errorMessage = error instanceof Error
? error.message
: "Unknown error occurred";
event.source?.postMessage(response_1.Response.error(message.id, errorMessage));
}
}
async handleGetAddress(event) {
const message = event.data;
if (!request_1.Request.isGetAddress(message)) {
console.error("Invalid GET_ADDRESS message format", message);
event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_ADDRESS message format"));
return;
}
if (!this.wallet) {
console.error("Wallet not initialized");
event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized"));
return;
}
try {
const addresses = await this.wallet.getAddress();
event.source?.postMessage(response_1.Response.addresses(message.id, addresses));
}
catch (error) {
console.error("Error getting address:", error);
const errorMessage = error instanceof Error
? error.message
: "Unknown error occurred";
event.source?.postMessage(response_1.Response.error(message.id, errorMessage));
}
}
async handleGetAddressInfo(event) {
const message = event.data;
if (!request_1.Request.isGetAddressInfo(message)) {
console.error("Invalid GET_ADDRESS_INFO message format", message);
event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_ADDRESS_INFO message format"));
return;
}
if (!this.wallet) {
console.error("Wallet not initialized");
event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized"));
return;
}
try {
const addressInfo = await this.wallet.getAddressInfo();
event.source?.postMessage(response_1.Response.addressInfo(message.id, addressInfo));
}
catch (error) {
console.error("Error getting address info:", error);
const errorMessage = error instanceof Error
? error.message
: "Unknown error occurred";
event.source?.postMessage(response_1.Response.error(message.id, errorMessage));
}
}
async handleGetBalance(event) {
const message = event.data;
if (!request_1.Request.isGetBalance(message)) {
console.error("Invalid GET_BALANCE message format", message);
event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_BALANCE message format"));
return;
}
if (!this.wallet) {
console.error("Wallet not initialized");
event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized"));
return;
}
try {
const coins = await this.wallet.getCoins();
const onchainConfirmed = coins
.filter((coin) => coin.status.confirmed)
.reduce((sum, coin) => sum + coin.value, 0);
const onchainUnconfirmed = coins
.filter((coin) => !coin.status.confirmed)
.reduce((sum, coin) => sum + coin.value, 0);
const onchainTotal = onchainConfirmed + onchainUnconfirmed;
const spendableVtxos = await this.vtxoRepository.getSpendableVtxos();
const offchainSettledBalance = spendableVtxos.reduce((sum, vtxo) => vtxo.virtualStatus.state === "settled"
? sum + vtxo.value
: sum, 0);
const offchainPendingBalance = spendableVtxos.reduce((sum, vtxo) => vtxo.virtualStatus.state === "pending"
? sum + vtxo.value
: sum, 0);
const offchainSweptBalance = spendableVtxos.reduce((sum, vtxo) => vtxo.virtualStatus.state === "swept"
? sum + vtxo.value
: sum, 0);
const offchainTotal = offchainSettledBalance +
offchainPendingBalance +
offchainSweptBalance;
event.source?.postMessage(response_1.Response.balance(message.id, {
onchain: {
confirmed: onchainConfirmed,
unconfirmed: onchainUnconfirmed,
total: onchainTotal,
},
offchain: {
swept: offchainSweptBalance,
settled: offchainSettledBalance,
pending: offchainPendingBalance,
total: offchainTotal,
},
total: onchainTotal + offchainTotal,
}));
}
catch (error) {
console.error("Error getting balance:", error);
const errorMessage = error instanceof Error
? error.message
: "Unknown error occurred";
event.source?.postMessage(response_1.Response.error(message.id, errorMessage));
}
}
async handleGetCoins(event) {
const message = event.data;
if (!request_1.Request.isGetCoins(message)) {
console.error("Invalid GET_COINS message format", message);
event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_COINS message format"));
return;
}
if (!this.wallet) {
console.error("Wallet not initialized");
event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized"));
return;
}
try {
const coins = await this.wallet.getCoins();
event.source?.postMessage(response_1.Response.coins(message.id, coins));
}
catch (error) {
console.error("Error getting coins:", error);
const errorMessage = error instanceof Error
? error.message
: "Unknown error occurred";
event.source?.postMessage(response_1.Response.error(message.id, errorMessage));
}
}
async handleGetVtxos(event) {
const message = event.data;
if (!request_1.Request.isGetVtxos(message)) {
console.error("Invalid GET_VTXOS message format", message);
event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_VTXOS message format"));
return;
}
if (!this.wallet) {
console.error("Wallet not initialized");
event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized"));
return;
}
try {
const vtxos = await this.vtxoRepository.getSpendableVtxos();
event.source?.postMessage(response_1.Response.vtxos(message.id, vtxos));
}
catch (error) {
console.error("Error getting vtxos:", error);
const errorMessage = error instanceof Error
? error.message
: "Unknown error occurred";
event.source?.postMessage(response_1.Response.error(message.id, errorMessage));
}
}
async handleGetBoardingUtxos(event) {
const message = event.data;
if (!request_1.Request.isGetBoardingUtxos(message)) {
console.error("Invalid GET_BOARDING_UTXOS message format", message);
event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_BOARDING_UTXOS message format"));
return;
}
if (!this.wallet) {
console.error("Wallet not initialized");
event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized"));
return;
}
try {
const boardingUtxos = await this.wallet.getBoardingUtxos();
event.source?.postMessage(response_1.Response.boardingUtxos(message.id, boardingUtxos));
}
catch (error) {
console.error("Error getting boarding utxos:", error);
const errorMessage = error instanceof Error
? error.message
: "Unknown error occurred";
event.source?.postMessage(response_1.Response.error(message.id, errorMessage));
}
}
async handleGetTransactionHistory(event) {
const message = event.data;
if (!request_1.Request.isGetTransactionHistory(message)) {
console.error("Invalid GET_TRANSACTION_HISTORY message format", message);
event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_TRANSACTION_HISTORY message format"));
return;
}
if (!this.wallet) {
console.error("Wallet not initialized");
event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized"));
return;
}
try {
const { boardingTxs, roundsToIgnore } = await this.wallet.getBoardingTxs();
const { spendable, spent } = await this.vtxoRepository.getAllVtxos();
// convert VTXOs to offchain transactions
const offchainTxs = (0, transactionHistory_1.vtxosToTxs)(spendable, spent, roundsToIgnore);
const txs = [...boardingTxs, ...offchainTxs];
// sort transactions by creation time in descending order (newest first)
txs.sort(
// place createdAt = 0 (unconfirmed txs) first, then descending
(a, b) => {
if (a.createdAt === 0)
return -1;
if (b.createdAt === 0)
return 1;
return b.createdAt - a.createdAt;
});
event.source?.postMessage(response_1.Response.transactionHistory(message.id, txs));
}
catch (error) {
console.error("Error getting transaction history:", error);
const errorMessage = error instanceof Error
? error.message
: "Unknown error occurred";
event.source?.postMessage(response_1.Response.error(message.id, errorMessage));
}
}
async handleGetStatus(event) {
const message = event.data;
if (!request_1.Request.isGetStatus(message)) {
console.error("Invalid GET_STATUS message format", message);
event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_STATUS message format"));
return;
}
event.source?.postMessage(response_1.Response.walletStatus(message.id, this.wallet !== undefined));
}
async handleMessage(event) {
this.messageCallback(event);
const message = event.data;
if (!request_1.Request.isBase(message)) {
console.warn("Invalid message format", JSON.stringify(message));
// ignore invalid messages
return;
}
switch (message.type) {
case "INIT_WALLET": {
await this.handleInitWallet(event);
break;
}
case "SETTLE": {
await this.handleSettle(event);
break;
}
case "SEND_BITCOIN": {
await this.handleSendBitcoin(event);
break;
}
case "GET_ADDRESS": {
await this.handleGetAddress(event);
break;
}
case "GET_ADDRESS_INFO": {
await this.handleGetAddressInfo(event);
break;
}
case "GET_BALANCE": {
await this.handleGetBalance(event);
break;
}
case "GET_COINS": {
await this.handleGetCoins(event);
break;
}
case "GET_VTXOS": {
await this.handleGetVtxos(event);
break;
}
case "GET_BOARDING_UTXOS": {
await this.handleGetBoardingUtxos(event);
break;
}
case "GET_TRANSACTION_HISTORY": {
await this.handleGetTransactionHistory(event);
break;
}
case "GET_STATUS": {
await this.handleGetStatus(event);
break;
}
case "CLEAR": {
await this.handleClear(event);
break;
}
default:
event.source?.postMessage(response_1.Response.error(message.id, "Unknown message type"));
}
}
}
exports.Worker = Worker;