@vela-ventures/ao-sync-sdk
Version:
JavaScript SDK for Beacon wallet
600 lines (599 loc) • 24.7 kB
JavaScript
"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");
const version_1 = require("./constants/version");
class WalletClient {
constructor(responseTimeoutMs = 30000, txTimeoutMs = 300000) {
var _a;
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.autoSign = 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")}`);
const userAgent = window.navigator.userAgent;
this.isAppleMobileDevice = /iPad|iPhone|iPod/.test(userAgent);
this.isInappBrowser = !!((_a = window["beaconwallet"]) === null || _a === void 0 ? void 0 : _a.version);
window.__AOSYNC_VERSION__ = version_1.VERSION;
}
}
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, _d;
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 (buffer_1.Buffer.isBuffer(packet.payload)) {
const bufferString = packet.payload.toString("utf8");
try {
const bufferJson = JSON.parse(bufferString);
this.autoSign = (_d = bufferJson.connectionOptions) === null || _d === void 0 ? void 0 : _d.autoSign;
}
catch {
console.log("Buffer content is not JSON");
}
}
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) {
if (this.autoSign) {
const actionTag = payload.dataItem.tags.find((tag) => tag.name === "Action");
if ((actionTag === null || actionTag === void 0 ? void 0 : actionTag.value) === "Transfer") {
this.createApprovalModal();
}
}
else {
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;
}
if (this.isAppleMobileDevice && !this.isInappBrowser) {
window.open(`beaconwallet://aosync?websiteURL=${window.location.href}`);
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);
if (!this.isAppleMobileDevice && !this.isInappBrowser) {
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 () => {
var _a;
try {
await new Promise((res, rej) => {
this.client.subscribe(responseChannel, (err) => {
err ? rej(err) : res();
});
});
if (this.isAppleMobileDevice && this.isInappBrowser) {
(_a = window["beaconwallet"]) === null || _a === void 0 ? void 0 : _a.connect(this.uid);
}
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",
aosyncVersion: version_1.VERSION,
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;