@arklabs/wallet-sdk
Version:
Bitcoin wallet SDK with Taproot and Ark integration
329 lines (328 loc) • 11.5 kB
JavaScript
import { Response } from './response.js';
import { hex } from "@scure/base";
class UnexpectedResponseError extends Error {
constructor(response) {
super(`Unexpected response type. Got: ${JSON.stringify(response, null, 2)}`);
this.name = "UnexpectedResponseError";
}
}
// ServiceWorkerWallet is a wallet that uses a service worker as "backend" to handle the wallet logic
export class ServiceWorkerWallet {
static async create(svcWorkerPath) {
try {
const wallet = new ServiceWorkerWallet();
await wallet.setupServiceWorker(svcWorkerPath);
return wallet;
}
catch (error) {
throw new Error(`Failed to initialize service worker wallet: ${error}`);
}
}
async getStatus() {
const message = {
type: "GET_STATUS",
id: getRandomId(),
};
const response = await this.sendMessage(message);
if (Response.isWalletStatus(response)) {
return response.status;
}
throw new UnexpectedResponseError(response);
}
async init(config, failIfInitialized = false) {
// Check if wallet is already initialized
const statusMessage = {
type: "GET_STATUS",
id: getRandomId(),
};
const response = await this.sendMessage(statusMessage);
if (Response.isWalletStatus(response) &&
response.status.walletInitialized) {
if (failIfInitialized) {
throw new Error("Wallet already initialized");
}
return;
}
// If not initialized, proceed with initialization
const message = {
type: "INIT_WALLET",
id: getRandomId(),
privateKey: config.privateKey,
network: config.network,
arkServerUrl: config.arkServerUrl || "",
arkServerPublicKey: config.arkServerPublicKey,
};
await this.sendMessage(message);
}
async clear() {
const message = {
type: "CLEAR",
id: getRandomId(),
};
await this.sendMessage(message);
}
// register the service worker
async setupServiceWorker(path) {
// check if service workers are supported
if (!("serviceWorker" in navigator)) {
throw new Error("Service workers are not supported in this browser");
}
try {
// check for existing registration
const existingRegistration = await navigator.serviceWorker.getRegistration(path);
let registration;
if (existingRegistration) {
registration = existingRegistration;
// Force unregister and re-register to ensure we get the latest version
await existingRegistration.unregister();
}
registration = await navigator.serviceWorker.register(path);
// Handle updates
registration.addEventListener("updatefound", () => {
console.info("@arklabs/wallet-sdk: Service worker auto-update...");
const newWorker = registration.installing;
if (!newWorker)
return;
newWorker.addEventListener("statechange", () => {
if (newWorker.state === "installed" &&
navigator.serviceWorker.controller) {
console.info("@arklabs/wallet-sdk: Service worker updated, reloading...");
window.location.reload();
}
});
});
// Check for updates
await registration.update();
const sw = registration.active ||
registration.waiting ||
registration.installing;
if (!sw) {
throw new Error("Failed to get service worker instance");
}
this.serviceWorker = sw;
// wait for the service worker to be ready
if (this.serviceWorker?.state !== "activated") {
await new Promise((resolve) => {
if (!this.serviceWorker)
return resolve();
this.serviceWorker.addEventListener("statechange", () => {
if (this.serviceWorker?.state === "activated") {
resolve();
}
});
});
}
}
catch (error) {
throw new Error(`Failed to setup service worker: ${error}`);
}
}
// send a message and wait for a response
async sendMessage(message) {
if (!this.serviceWorker) {
throw new Error("Service worker not initialized");
}
return new Promise((resolve, reject) => {
const messageHandler = (event) => {
const response = event.data;
if (response.id === "") {
reject(new Error("Invalid response id"));
return;
}
if (response.id !== message.id) {
return;
}
navigator.serviceWorker.removeEventListener("message", messageHandler);
if (!response.success) {
reject(new Error(response.message));
}
else {
resolve(response);
}
};
navigator.serviceWorker.addEventListener("message", messageHandler);
if (this.serviceWorker) {
this.serviceWorker.postMessage(message);
}
else {
reject(new Error("Service worker not initialized"));
}
});
}
async getAddress() {
const message = {
type: "GET_ADDRESS",
id: getRandomId(),
};
try {
const response = await this.sendMessage(message);
if (Response.isAddress(response)) {
return response.addresses;
}
throw new UnexpectedResponseError(response);
}
catch (error) {
throw new Error(`Failed to get address: ${error}`);
}
}
async getAddressInfo() {
const message = {
type: "GET_ADDRESS_INFO",
id: getRandomId(),
};
try {
const response = await this.sendMessage(message);
if (Response.isAddressInfo(response)) {
return response.addressInfo;
}
throw new UnexpectedResponseError(response);
}
catch (error) {
throw new Error(`Failed to get address info: ${error}`);
}
}
async getBalance() {
const message = {
type: "GET_BALANCE",
id: getRandomId(),
};
try {
const response = await this.sendMessage(message);
if (Response.isBalance(response)) {
return response.balance;
}
throw new UnexpectedResponseError(response);
}
catch (error) {
throw new Error(`Failed to get balance: ${error}`);
}
}
async getCoins() {
const message = {
type: "GET_COINS",
id: getRandomId(),
};
try {
const response = await this.sendMessage(message);
if (Response.isCoins(response)) {
return response.coins;
}
throw new UnexpectedResponseError(response);
}
catch (error) {
throw new Error(`Failed to get coins: ${error}`);
}
}
async getVtxos() {
const message = {
type: "GET_VTXOS",
id: getRandomId(),
};
try {
const response = await this.sendMessage(message);
if (Response.isVtxos(response)) {
return response.vtxos;
}
throw new UnexpectedResponseError(response);
}
catch (error) {
throw new Error(`Failed to get vtxos: ${error}`);
}
}
async getBoardingUtxos() {
const message = {
type: "GET_BOARDING_UTXOS",
id: getRandomId(),
};
try {
const response = await this.sendMessage(message);
if (Response.isBoardingUtxos(response)) {
return response.boardingUtxos;
}
throw new UnexpectedResponseError(response);
}
catch (error) {
throw new Error(`Failed to get boarding UTXOs: ${error}`);
}
}
async sendBitcoin(params, zeroFee) {
const message = {
type: "SEND_BITCOIN",
params,
zeroFee,
id: getRandomId(),
};
try {
const response = await this.sendMessage(message);
if (Response.isSendBitcoinSuccess(response)) {
return response.txid;
}
throw new UnexpectedResponseError(response);
}
catch (error) {
throw new Error(`Failed to send bitcoin: ${error}`);
}
}
async settle(params, callback) {
const message = {
type: "SETTLE",
params,
id: getRandomId(),
};
try {
return new Promise((resolve, reject) => {
const messageHandler = (event) => {
const response = event.data;
if (!response.success) {
navigator.serviceWorker.removeEventListener("message", messageHandler);
reject(new Error(response.message));
return;
}
switch (response.type) {
case "SETTLE_EVENT":
if (callback) {
callback(response.event);
}
break;
case "SETTLE_SUCCESS":
navigator.serviceWorker.removeEventListener("message", messageHandler);
resolve(response.txid);
break;
default:
break;
}
};
navigator.serviceWorker.addEventListener("message", messageHandler);
if (this.serviceWorker) {
this.serviceWorker.postMessage(message);
}
else {
reject(new Error("Service worker not initialized"));
}
});
}
catch (error) {
throw new Error(`Settlement failed: ${error}`);
}
}
async getTransactionHistory() {
const message = {
type: "GET_TRANSACTION_HISTORY",
id: getRandomId(),
};
try {
const response = await this.sendMessage(message);
if (Response.isTransactionHistory(response)) {
return response.transactions;
}
throw new UnexpectedResponseError(response);
}
catch (error) {
throw new Error(`Failed to get transaction history: ${error}`);
}
}
}
function getRandomId() {
const randomValue = crypto.getRandomValues(new Uint8Array(16));
return hex.encode(randomValue);
}