UNPKG

@vela-ventures/ao-sync-sdk

Version:
564 lines (563 loc) 22.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const mqtt_1 = require("mqtt"); const uuid_1 = require("uuid"); const qrcode_1 = require("qrcode"); const buffer_1 = require("buffer"); const templates_1 = require("./templates"); require("./fonts"); class WalletClient { constructor(responseTimeoutMs = 30000, txTimeoutMs = 300000) { this.client = null; this.uid = null; this.qrCode = null; this.modal = null; this.approvalModal = null; this.responseListeners = new Map(); this.connectionListener = null; this.reconnectListener = null; this.responseTimeoutMs = responseTimeoutMs; this.txTimeoutMs = txTimeoutMs; this.eventListeners = new Map(); this.activeTimeouts = new Set(); this.isConnected = false; this.reconnectionTimeout = null; this.connectOptions = null; this.pendingRequests = []; this.isDarkMode = typeof window !== "undefined" && (window === null || window === void 0 ? void 0 : window.matchMedia) && (window === null || window === void 0 ? void 0 : window.matchMedia("(prefers-color-scheme: dark)").matches); this.sessionActive = typeof window !== "undefined" && !!sessionStorage.getItem("aosync-topic-id"); if (typeof window !== "undefined") { sessionStorage.setItem("aosync-session-active", `${!!sessionStorage.getItem("aosync-topic-id")}`); } } createModal(qrCodeData, styles) { const modal = (0, templates_1.createModalTemplate)({ subTitle: "Scan with your beacon wallet", qrCodeData, description: "Don't have beacon yet?", walletClient: this, }); this.modal = modal; } createApprovalModal() { if (this.approvalModal) return; const modal = (0, templates_1.createModalTemplate)({ subTitle: "Approval pending ...", description: " ", animationData: true, }); this.approvalModal = modal; } async handleMQTTMessage(topic, message, packet) { var _a, _b, _c; const responseChannel = `${this.uid}/response`; if (topic !== responseChannel) return; const messageData = JSON.parse(message.toString()); if (messageData.action === "connect") { (0, templates_1.connectionModalMessage)("success"); if (this.modal) { this.modal = null; } await this.handleConnectResponse(packet); sessionStorage.setItem("aosync-topic-id", this.uid); return; } if (messageData.action === "disconnect") { await this.handleDisconnectResponse("Beacon wallet initiated disconnect"); return; } if ((packet === null || packet === void 0 ? void 0 : packet.properties.correlationData.toString()) == ((_a = this.reconnectListener) === null || _a === void 0 ? void 0 : _a.corellationId)) { clearTimeout(this.reconnectionTimeout); this.processPendingRequests(); this.isConnected = true; this.reconnectListener = null; this.populateWindowObject(); this.emit("connected", { status: "connected successfully" }); } const correlationId = (_c = (_b = packet === null || packet === void 0 ? void 0 : packet.properties) === null || _b === void 0 ? void 0 : _b.correlationData) === null || _c === void 0 ? void 0 : _c.toString(); if (correlationId && this.responseListeners.has(correlationId)) { const listenerData = this.responseListeners.get(correlationId); const isTransaction = ["sign", "dispatch", "signDataItem"].includes(listenerData.action); if (listenerData.action === "signDataItem") { const decodedData = this.base64UrlDecode(messageData.data); listenerData.resolve(decodedData); } else { listenerData.resolve(messageData.data); } if (isTransaction) { if (messageData.data === "declined") { (0, templates_1.connectionModalMessage)("fail"); if (this.approvalModal) { this.approvalModal = null; } } else { (0, templates_1.connectionModalMessage)("success"); if (this.approvalModal) { this.approvalModal = null; } } } this.responseListeners.delete(correlationId); } } base64UrlDecode(base64Url) { const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); const paddedBase64 = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "="); const decodedString = atob(paddedBase64); const byteArray = new Uint8Array(decodedString.length); for (let i = 0; i < decodedString.length; i++) { byteArray[i] = decodedString.charCodeAt(i); } return byteArray; } async handleConnectResponse(packet) { var _a, _b, _c; this.isConnected = true; this.populateWindowObject(); if (this.connectionListener) { this.connectionListener("connected"); } const topic = this.uid; const message = { appInfo: { name: ((_a = this.connectOptions.appInfo) === null || _a === void 0 ? void 0 : _a.name) || "unknown", url: "unknown", logo: ((_b = this.connectOptions.appInfo) === null || _b === void 0 ? void 0 : _b.logo) || "unknown", }, permissions: this.connectOptions.permissions, gateway: this.connectOptions.gateway, }; const publishOptions = ((_c = packet === null || packet === void 0 ? void 0 : packet.properties) === null || _c === void 0 ? void 0 : _c.correlationData) ? { properties: { correlationData: packet.properties.correlationData } } : {}; if (topic) { await this.publishMessage(topic, message, publishOptions); } this.emit("connected", { status: "connected successfully" }); } async handleDisconnectResponse(reason) { this.isConnected = false; this.approvalModal = null; this.emit("disconnected", { reason }); const modal = (0, templates_1.createModalTemplate)({ subTitle: "Beacon wallet disconnected", description: " ", autoClose: true, }); await this.disconnect(); } async publishMessage(topic, message, options = {}) { return new Promise((resolve, reject) => { var _a; (_a = this.client) === null || _a === void 0 ? void 0 : _a.publish(topic, JSON.stringify(message), options, (err) => err ? reject(err) : resolve()); }); } createResponsePromise(action, payload = {}) { if (sessionStorage.getItem("aosync-topic-id") && !this.client) { return new Promise((resolve, reject) => { this.pendingRequests.push({ method: action, args: [payload], resolve, reject, }); }); } if (!sessionStorage.getItem("aosync-topic-id") && !this.client) { this.isConnected = false; this.approvalModal = null; this.emit("disconnected", { reson: "AOsync connection not found" }); if (this.browserWalletBackup) { window.arweaveWallet = this.browserWalletBackup; } return; } const correlationData = (0, uuid_1.v4)(); const topic = this.uid; const isTransaction = ["sign", "dispatch", "signDataItem"].includes(action); const timeoutDuration = isTransaction ? this.txTimeoutMs : this.responseTimeoutMs; return new Promise((resolve, reject) => { if (!this.client) { reject(new Error(`Not connected to AOSync`)); return; } this.responseListeners.set(correlationData, { action, resolve, }); if (topic) { this.publishMessage(topic, { action, correlationData, ...payload }, { properties: { correlationData: buffer_1.Buffer.from(correlationData, "utf-8"), }, }).catch((err) => { this.responseListeners.delete(correlationData); reject(err); }); } if (isTransaction) { this.createApprovalModal(); } const timeout = setTimeout(() => { if (this.responseListeners.has(correlationData)) { this.responseListeners.delete(correlationData); reject(new Error(`${action} timeout`)); } if (isTransaction) { if (document.getElementById("aosync-modal")) { (0, templates_1.connectionModalMessage)("fail"); } } this.activeTimeouts.delete(timeout); }, timeoutDuration); this.activeTimeouts.add(timeout); }); } clearAllTimeouts() { this.activeTimeouts.forEach((timeout) => clearTimeout(timeout)); this.activeTimeouts.clear(); } async connect({ permissions = [ "ACCESS_ADDRESS", "ACCESS_ALL_ADDRESSES", "ACCESS_ARWEAVE_CONFIG", "ACCESS_PUBLIC_KEY", "ACCESS_TOKENS", "DECRYPT", "DISPATCH", "ENCRYPT", "SIGNATURE", "SIGN_TRANSACTION", ], appInfo = { name: "unknown", logo: "app logo" }, gateway = { host: "arweave.net", port: 443, protocol: "https", }, brokerUrl = "wss://aosync-broker-eu.beaconwallet.dev:8081", options = { protocolVersion: 5 }, }) { if (this.isConnected) return; if (this.client) { const qrCodeData = await qrcode_1.default.toDataURL("aosync=" + this.uid); this.createModal(qrCodeData); console.warn("Already connected to the broker."); return; } this.uid = (0, uuid_1.v4)(); this.client = mqtt_1.default.connect(brokerUrl, options); this.sessionActive = true; sessionStorage.setItem("aosync-session-active", "true"); const responseChannel = `${this.uid}/response`; let qrCodeOptions = {}; if (this.isDarkMode) { qrCodeOptions = { color: { dark: "#FFFFFF", light: "#0A0B19" }, }; } else { qrCodeOptions = { color: { dark: "#0A0B19", light: "#FFFFFF" }, }; } const qrCodeData = await qrcode_1.default.toDataURL("aosync=" + this.uid, qrCodeOptions); this.createModal(qrCodeData); this.connectOptions = { permissions, appInfo, gateway, }; return new Promise((resolve, reject) => { this.connectionListener = (response) => { if (response === "connection_canceled") { if (this.client) { this.client.end(false, () => { this.client = null; }); } reject(new Error("Connection canceled by user")); return; } resolve(response); }; this.client.on("connect", async () => { try { await new Promise((res, rej) => { this.client.subscribe(responseChannel, (err) => { err ? rej(err) : res(); }); }); this.client.on("message", this.handleMQTTMessage.bind(this)); } catch (err) { reject(err); } }); this.client.on("error", reject); }); } async reconnect(brokerUrl = "wss://aosync-broker-eu.beaconwallet.dev:8081", options = { protocolVersion: 5, }) { if (this.reconnectListener != null) return; const sessionStorageTopicId = sessionStorage.getItem("aosync-topic-id"); if (sessionStorageTopicId === null) return; try { this.uid = sessionStorageTopicId; this.populateWindowObject(); const responseChannel = `${this.uid}/response`; if (this.client) { return new Promise((resolve, reject) => { try { const correlationData = (0, uuid_1.v4)(); this.reconnectListener = { corellationId: correlationData, resolve, }; this.reconnectionTimeout = setTimeout(async () => { if (this.isConnected) return; console.warn("No response received during reconnection attempt"); clearTimeout(this.reconnectionTimeout); try { await this.disconnect(); } catch (err) { reject(err); return; } reject(new Error("Reconnection timeout")); }, 3000); this.publishMessage(this.uid, { action: "getActiveAddress", correlationData: correlationData }, { properties: { correlationData: buffer_1.Buffer.from(correlationData, "utf-8"), }, }); } catch (err) { reject(err); } }); } this.client = mqtt_1.default.connect(brokerUrl, options); return new Promise((resolve, reject) => { this.client.on("connect", async () => { try { const correlationData = (0, uuid_1.v4)(); this.reconnectListener = { corellationId: correlationData, resolve, }; await new Promise((res, rej) => { this.client.subscribe(responseChannel, (err) => { err ? rej(err) : res(); }); }); this.reconnectionTimeout = setTimeout(async () => { console.warn("No response received during reconnection attempt"); clearTimeout(this.reconnectionTimeout); try { await this.disconnect(); } catch (err) { reject(err); return; } reject(new Error("Reconnection timeout")); }, 3000); this.publishMessage(this.uid, { action: "getActiveAddress", correlationData: correlationData }, { properties: { correlationData: buffer_1.Buffer.from(correlationData, "utf-8"), }, }); this.client.on("message", this.handleMQTTMessage.bind(this)); } catch (err) { reject(err); } }); this.client.on("error", reject); }); } catch (error) { this.pendingRequests.forEach((request) => { request.reject(new Error("Reconnection failed")); }); this.pendingRequests = []; this.disconnect(); throw error; } } async processPendingRequests() { const requests = [...this.pendingRequests]; this.pendingRequests = []; for (const request of requests) { try { const result = await this[request.method](...request.args); request.resolve(result); } catch (error) { request.reject(error); } } } async disconnect() { if (this.browserWalletBackup) { window.arweaveWallet = this.browserWalletBackup; } if (sessionStorage.getItem("aosync-topic-id")) { sessionStorage.removeItem("aosync-topic-id"); this.sessionActive = false; sessionStorage.removeItem("aosync-session-active"); } if (!this.client) { return; } return new Promise((resolve, reject) => { if (this.uid) { this.client.publish(this.uid, JSON.stringify({ action: "disconnect" }), {}, (err) => { if (err) { reject(err); return; } this.client.end(false, () => { this.client = null; this.responseListeners.forEach((listener) => listener.resolve(new Error("Disconnected before response was received"))); this.handleDisconnectResponse("disconnected from wallet"); this.responseListeners.clear(); this.clearAllTimeouts(); resolve(); }); this.client.on("error", reject); }); } }); } async getActiveAddress() { return this.createResponsePromise("getActiveAddress"); } async getAllAddresses() { return this.createResponsePromise("getAllAddresses"); } async getPermissions() { if (!this.client) { return []; } return this.createResponsePromise("getPermissions"); } async getWalletNames() { return this.createResponsePromise("getWalletNames"); } async encrypt(data, algorithm) { return this.createResponsePromise("encrypt", { data: data + "", algorithm, }); } async decrypt(data, algorithm) { return this.createResponsePromise("decrypt", { data: data + "", algorithm, }); } async getArweaveConfig() { const config = { host: "arweave.net", port: 443, protocol: "https", }; return Promise.resolve(config); } async signature(data, algorithm) { const dataString = data.toString(); return this.createResponsePromise("signature", { data: dataString }); } async getActivePublicKey() { return this.createResponsePromise("getActivePublicKey"); } async addToken(id) { return this.createResponsePromise("addToken", { data: id }); } async sign(transaction) { return this.createResponsePromise("sign", { transaction }); } async dispatch(transaction) { return this.createResponsePromise("dispatch", { transaction }); } async signDataItem(dataItem) { return this.createResponsePromise("signDataItem", { dataItem }); } async isAvailable() { return this.client !== null; } on(event, listener) { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, new Set()); } this.eventListeners.get(event).add(listener); } off(event, listener) { var _a; (_a = this.eventListeners.get(event)) === null || _a === void 0 ? void 0 : _a.delete(listener); } emit(event, data) { if (this.eventListeners.has(event)) { this.eventListeners.get(event).forEach((listener) => listener(data)); } } async userTokens(options) { return this.createResponsePromise("userTokens"); } populateWindowObject() { var _a; if (typeof window !== "undefined") { if (((_a = window === null || window === void 0 ? void 0 : window.arweaveWallet) === null || _a === void 0 ? void 0 : _a.walletName) === "AOSync") return; const createMethodWrapper = (method) => { return async (...args) => { if (!this.isConnected) { throw new Error("Wallet is not connected. Please call connect() first."); } return method.apply(this, args); }; }; const walletApi = { walletName: "AOSync", connect: async (permissions, appInfo, gateway) => { await this.connect({ permissions, appInfo, gateway }); }, disconnect: this.disconnect.bind(this), getActiveAddress: createMethodWrapper(this.getActiveAddress), getAllAddresses: createMethodWrapper(this.getAllAddresses), getPermissions: createMethodWrapper(this.getPermissions), getWalletNames: createMethodWrapper(this.getWalletNames), encrypt: createMethodWrapper(this.encrypt), decrypt: createMethodWrapper(this.decrypt), getArweaveConfig: createMethodWrapper(this.getArweaveConfig), signature: createMethodWrapper(this.signature), getActivePublicKey: createMethodWrapper(this.getActivePublicKey), addToken: createMethodWrapper(this.addToken), sign: createMethodWrapper(this.sign), dispatch: createMethodWrapper(this.dispatch), signDataItem: createMethodWrapper(this.signDataItem), userTokens: createMethodWrapper(this.userTokens), }; if (window.arweaveWallet) { this.browserWalletBackup = window.arweaveWallet; } window.arweaveWallet = walletApi; } } } exports.default = WalletClient;