UNPKG

diamwallet-sdk-new

Version:

The DIAM Wallet Mobile SDK allows developers to seamlessly connect their dApps with both web apps (on Android mobile browsers) and mobile apps (on Android and iOS) through the DIAM Wallet App.

846 lines (805 loc) 27.7 kB
'use strict'; function encrypt(payload) { const encrypted = payload; // CryptoJS.AES.encrypt( // payload, // CryptoJS.enc.Hex.parse(AesKey), // { // mode: CryptoJS.mode.ECB, // padding: CryptoJS.pad.Pkcs7, // } // ).toString(); return encrypted; } const getBalanceData = async params => { let encryptData = encrypt(JSON.stringify({ address: params.address, network: params.network })); let response = await fetch(`${params.serverUrl}/api/wallet/get-wallet-balance`, { method: "POST", body: JSON.stringify({ encryptedWalletData: encryptData }), headers: { Accept: "application/json", "Content-Type": "application/json" } }); const data = await response.json(); return data; }; // WebDIAMWalletSDK.js const { io } = require("socket.io-client"); class WebSDK { constructor(options) { this.serverUrl = options.serverUrl || "https://dwsprod.diamante.io"; this.appCallback = options.appCallback; this.scheme = "diamwallet"; this.ws = null; this.sessionId = null; this.pollInterval = null; this.disconnected = null; this.appName = options.appName; this.browser = this.detectBrowser(); this.platform = options.platform; this.transData = null; this.address = null; this.xdrData = null; this.reconnectTimeout = null; this.network = options.network; } detectBrowser() { const userAgent = navigator.userAgent; let browserName = "Unknown"; let browserVersion = "Unknown"; if (/Chrome/.test(userAgent) && !/Chromium|Edge|Edg/.test(userAgent)) { browserName = "Chrome"; const match = userAgent.match(/Chrome\/(\d+\.\d+)/); browserVersion = match ? match[1] : "Unknown"; } else if (/Firefox/.test(userAgent)) { browserName = "Firefox"; const match = userAgent.match(/Firefox\/(\d+\.\d+)/); browserVersion = match ? match[1] : "Unknown"; } else if (/Safari/.test(userAgent) && !/Chrome/.test(userAgent)) { browserName = "Safari"; const match = userAgent.match(/Version\/(\d+\.\d+)/); browserVersion = match ? match[1] : "Unknown"; } else if (/Edg/.test(userAgent)) { browserName = "Edge"; const match = userAgent.match(/Edg\/(\d+\.\d+)/); browserVersion = match ? match[1] : "Unknown"; } else if (/OPR/.test(userAgent)) { browserName = "Opera"; const match = userAgent.match(/OPR\/(\d+\.\d+)/); browserVersion = match ? match[1] : "Unknown"; } return { name: browserName, version: browserVersion }; } async registerSession(type = "wallet") { const endpoint = type === "wallet" ? "api/session/register-session" : "api/session/register-transaction"; const response = await fetch(`${this.serverUrl}/${endpoint}`, { method: "GET" }); const { sessionId } = await response.json(); this.sessionId = sessionId; return sessionId; } createDeeplinkUrl(action, params = {}) { const url = new URL(`${this.scheme}://${action}`); Object.entries(params).forEach(([key, value]) => { url.searchParams.append(key, value); }); return url.toString(); } openWallet() { try { if (!this.sessionId) { throw new Error("No active session. Call registerSession first."); } // Create the deep link URL for the DIAM Wallet const url = this.createDeeplinkUrl("connect", { appName: this.appName, sessionId: this.sessionId, callback: this.appCallback, browser: JSON.stringify(this.browser), platform: this.platform, network: this.network }); // Detect iOS device and Safari const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); // App store fallback URL const appStoreUrl = isIOS ? "https://apps.apple.com/in/app/diam-wallet/id6450691849" : "https://play.google.com/store/apps/details?id=com.diamante.diamwallet&hl=en_IN"; // Control flag to track app opening let appOpened = false; // Visibility change handler const handleVisibilityChange = () => { if (document.hidden && !appOpened) { appOpened = true; cleanup(); } }; // Add visibility change listener document.addEventListener("visibilitychange", handleVisibilityChange); // Timeout for fallback to app store const timeoutId = setTimeout(() => { if (!appOpened) { appOpened = true; window.location.href = appStoreUrl; } cleanup(); }, 3000); // 3-second timeout // Attempt to open the app if (!appOpened) { if (isSafari && isIOS) { const link = document.createElement("a"); link.setAttribute("href", url); link.style.display = "none"; document.body.appendChild(link); link.click(); document.body.removeChild(link); } else if (isIOS && !isSafari) { const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = url; document.body.appendChild(iframe); // Remove the iframe after some time setTimeout(() => { if (document.body.contains(iframe)) { document.body.removeChild(iframe); } }, 1000); } else { window.location.href = url; } } // Cleanup function const cleanup = () => { document.removeEventListener("visibilitychange", handleVisibilityChange); clearTimeout(timeoutId); }; } catch (error) { console.error("Error in openWallet:", error); } } async initializeSdk() { const existingDta = JSON.parse(sessionStorage.getItem("DIAMWALLETDATA")); if (existingDta === null) { // this.openWallet(); return null; } else { return { address: existingDta.walletAddress, status: true }; } } // Add visibility change listener async connectWallet(timeout = 300000) { try { await this.registerSession("wallet"); return new Promise((resolve, reject) => { // Log the URL being used // Configure Socket.IO with more detailed options this.ws = io(this.serverUrl, { origin: window.location.origin, transports: ["websocket"], reconnection: false }); let heartbeatIntervalId; const HEARTBEAT_INTERVAL = 30000; // Add connection status logging this.ws.on("connecting", () => { console.log("Socket attempting connection..."); }); this.ws.on("connect_attempt", () => { console.log("Socket connection attempt..."); }); this.ws.on("connect", () => { // Send initial subscription message this.ws.emit("subscribe", { sessionId: this.sessionId, action: "connect" }, acknowledgement => { // Add acknowledgement callback console.log("Subscribe acknowledgement:", acknowledgement); }); this.openWallet(); heartbeatIntervalId = setInterval(() => { if (this.ws.connected) { console.log("Sending ping..."); this.ws.emit("ping"); } else { console.log("Socket disconnected, clearing heartbeat"); clearInterval(heartbeatIntervalId); } }, HEARTBEAT_INTERVAL); }); // Add visibility change listener // Enhanced error handling this.ws.on("connect_error", error => { console.error("Connection error:", error); console.error("Error details:", { message: error.message, description: error.description, type: error.type, context: this.ws.io.engine?.transport }); }); this.ws.on("error", error => { console.error("Socket error:", error); }); this.ws.on("disconnect", reason => { this.startPolling(resolve, reject, timeout); console.log(`Socket disconnected. Reason: ${reason}`); clearInterval(heartbeatIntervalId); }); // Handle incoming data this.ws.on("data", data => { console.log("Received data:", data); try { if (data.status === true) { sessionStorage.setItem("DIAMWALLETDATA", JSON.stringify({ walletAddress: data.address })); this.ws.disconnect(); resolve(data); } else { // reject(new Error("Failed to connect wallet")); } } catch (error) { console.error("Error processing data:", error); reject(error); } }); // Handle timeout setTimeout(() => { if (!this.ws.connected) { console.log("Connection timeout reached"); this.ws.disconnect(); reject(new Error("Connection timeout")); } }, timeout); }); } catch (error) { console.error("Error during session registration:", error); throw error; } } async sendTransaction(transData, timeout = 300000) { try { const walletData = sessionStorage.getItem("DIAMWALLETDATA"); if (!walletData) { throw new Error("No wallet data found"); } const { walletAddress } = JSON.parse(walletData); this.address = walletAddress; this.transData = transData; // Register transaction session await this.registerSession("transaction"); // Open wallet for transaction return new Promise((resolve, reject) => { this.ws = io(this.serverUrl, { origin: window.location.origin, transports: ["websocket"], reconnection: false }); console.log(this.ws); let heartbeatIntervalId; const HEARTBEAT_INTERVAL = 30000; // Add connection status logging this.ws.on("connecting", () => { console.log("Socket attempting connection..."); }); this.ws.on("connect_attempt", () => { console.log("Socket connection attempt..."); }); const closeWebSocket = (reason = "Client closing connection") => { if (this.ws) { this.ws.close(); } if (heartbeatIntervalId) clearInterval(heartbeatIntervalId); // if (timeoutId) clearTimeout(timeoutId); }; this.ws.on("connect", () => { // Send initial subscription message this.ws.emit("subscribe", { sessionId: this.sessionId, action: "transaction", transData: this.transData, walletAddress: this.address }, acknowledgement => { // Add acknowledgement callback console.log("Subscribe acknowledgement:", acknowledgement); }); this.sendOpenWallet(); // Start heartbeat heartbeatIntervalId = setInterval(() => { if (this.ws.connected) { console.log("Sending ping..."); this.ws.emit("ping"); } else { console.log("Socket disconnected, clearing heartbeat"); clearInterval(heartbeatIntervalId); } }, HEARTBEAT_INTERVAL); }); this.ws.on("connect_error", error => { console.error("Connection error:", error); console.error("Error details:", { message: error.message, description: error.description, type: error.type, context: this.ws.io.engine?.transport }); }); this.ws.on("error", error => { console.error("Socket error:", error); }); this.ws.on("disconnect", reason => { console.log(`Socket disconnected. Reason: ${reason}`); this.startTransactionPolling(resolve, reject, timeout); clearInterval(heartbeatIntervalId); }); // Handle incoming data this.ws.on("data", data => { try { // Handle "pong" response for the heartbeat switch (data.status) { case "completed": closeWebSocket("Transaction completed"); resolve(data.data); break; case "failed": closeWebSocket("Transaction failed"); reject(new Error(data.error)); break; case "cancelled": closeWebSocket("Transaction cancelled"); reject(new Error(data.error)); break; case "processing": resolve(data); break; default: console.log("Unknown transaction status:", data); } } catch (error) { console.error("WebSocket message handling error:", error); closeWebSocket("Transaction failed"); // Close WebSocket on error reject(error); } }); setTimeout(() => { if (this.ws && this.ws.readyState === WebSocket.OPEN) { console.warn("Transaction timeout."); closeWebSocket("Transaction timeout"); } reject(new Error("Transaction timeout")); }, timeout); }); } catch (error) { throw new Error(`Transaction failed: ${error.message}`); } } async signTransaction(transData, timeout = 300000) { try { const walletData = sessionStorage.getItem("DIAMWALLETDATA"); if (!walletData) { throw new Error("No wallet data found"); } const { walletAddress } = JSON.parse(walletData); this.address = walletAddress; this.transData = transData; // Register transaction session await this.registerSession("transaction"); return new Promise((resolve, reject) => { this.ws = io(this.serverUrl, { origin: window.location.origin, transports: ["websocket"], reconnection: false }); let heartbeatIntervalId; const HEARTBEAT_INTERVAL = 30000; // Add connection status logging this.ws.on("connecting", () => { console.log("Socket attempting connection..."); }); this.ws.on("connect_attempt", () => { console.log("Socket connection attempt..."); }); const closeWebSocket = (reason = "Client closing connection") => { if (this.ws) { this.ws.close(); } if (heartbeatIntervalId) clearInterval(heartbeatIntervalId); }; this.ws.on("connect", () => { // Send initial subscription message this.ws.emit("subscribe", { sessionId: this.sessionId, action: "transaction", transData: this.transData, walletAddress: this.address }, acknowledgement => { // Add acknowledgement callback console.log("Subscribe acknowledgement:", acknowledgement); }); this.sendOpenWallet(); // Start heartbeat heartbeatIntervalId = setInterval(() => { if (this.ws.connected) { console.log("Sending ping..."); this.ws.emit("ping"); } else { console.log("Socket disconnected, clearing heartbeat"); clearInterval(heartbeatIntervalId); } }, HEARTBEAT_INTERVAL); }); this.ws.on("connect_error", error => { console.error("Connection error:", error); console.error("Error details:", { message: error.message, description: error.description, type: error.type, context: this.ws.io.engine?.transport }); }); this.ws.on("error", error => { console.error("Socket error:", error); }); this.ws.on("disconnect", reason => { console.log(`Socket disconnected. Reason: ${reason}`); this.startSignTransactionPolling(resolve, reject, timeout); clearInterval(heartbeatIntervalId); }); this.ws.on("data", data => { try { switch (data.status) { case "sign-completed": closeWebSocket("Transaction Signing completed"); // Close WebSocket on success resolve(data.data); break; case "sign-failed": closeWebSocket("Transaction Signing failed"); // Close WebSocket on failure reject(new Error(data.error)); break; case "sign-cancelled": closeWebSocket("Transaction Signing cancelled"); // Close WebSocket on cancellation reject(new Error(data.error)); break; case "sign-processing": resolve(data); break; case "sign-transaction": resolve(data); break; default: console.log("Unknown signing status:", data); } } catch (error) { closeWebSocket("Error processing signing message"); reject(error); } }); setTimeout(() => { if (this.ws && this.ws.readyState === WebSocket.OPEN) { console.warn("Transaction Signing timeout."); closeWebSocket("Transaction Signing timeout."); } reject(new Error("Transaction Signing timeout")); }, timeout); }); } catch (error) { throw new Error(`Transaction Signing failed: ${error.message}`); } } async getXdr(data) { const response = await fetch(`${this.serverUrl}/api/transaction/getXdr`, { method: "POST", body: JSON.stringify(data), headers: { Accept: "application/json", "Content-Type": "application/json" } }); let xdrData = await response.json(); return xdrData.data.xdr; } handleReconnect() { console.warn("Attempting to reconnect..."); clearTimeout(this.reconnectTimeout); this.reconnectTimeout = setTimeout(() => { this.connect(); // Try to reconnect after a delay }, 5000); // Reconnect after 5 seconds } async sendOpenWallet() { try { if (!this.sessionId) { throw new Error("No active session. Call registerSession first."); } const params = { sessionId: this.sessionId, callback: this.appCallback, appName: this.appName, xdr: this.xdrData, signTransaction: this.transData.signTransaction, network: this.network }; let encryptData = encrypt(JSON.stringify({ toAddress: this.transData.toAddress, amount: this.transData.amount, fromAddress: this.address })); if (this.transData) { if (this.transData.xdr) { let response = await fetch(`${this.serverUrl}/api/transaction/decode-XDR`, { method: "POST", body: JSON.stringify({ xdr: this.transData.xdr }), headers: { Accept: "application/json", "Content-Type": "application/json" } }); let data = await response.json(); Object.assign(params, { toAddress: data.data.toAddress, amount: data.data.amount, fromAddress: this.address, xdr: this.transData.xdr, signed: true }); } else { Object.assign(params, { toAddress: this.transData.toAddress, amount: this.transData.amount, fromAddress: this.address, xdr: await this.getXdr({ encryptedTransData: encryptData }) }); } } const action = this.transData ? "send" : "connect"; const url = this.createDeeplinkUrl(action, params); // Detect iOS device and Safari const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); // App store fallback URL const appStoreUrl = isIOS ? "https://apps.apple.com/in/app/diam-wallet/id6450691849" : "https://play.google.com/store/apps/details?id=com.diamante.diamwallet&hl=en_IN"; // Control flag to track app opening let appOpened = false; // Visibility change handler const handleVisibilityChange = () => { if (document.hidden && !appOpened) { appOpened = true; cleanup(); } }; // Add visibility change listener document.addEventListener("visibilitychange", handleVisibilityChange); // Timeout for fallback to app store const timeoutId = setTimeout(() => { if (!appOpened) { appOpened = true; window.location.href = appStoreUrl; } cleanup(); }, 3000); // 3-second timeout // Attempt to open the app if (!appOpened) { if (isSafari && isIOS) { const link = document.createElement("a"); link.setAttribute("href", url); link.style.display = "none"; document.body.appendChild(link); link.click(); document.body.removeChild(link); } else if (isIOS && !isSafari) { const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = url; document.body.appendChild(iframe); // Remove the iframe after some time setTimeout(() => { if (document.body.contains(iframe)) { document.body.removeChild(iframe); } }, 1000); } else { window.location.href = url; } } // Cleanup function const cleanup = () => { document.removeEventListener("visibilitychange", handleVisibilityChange); clearTimeout(timeoutId); }; } catch (error) { console.error("Error in openWallet:", error); } } startPolling(resolve, reject, timeout) { const startTime = Date.now(); this.pollInterval = setInterval(async () => { try { const response = await fetch(`${this.serverUrl}/api/session/check-connection/${this.sessionId}`); const data = await response.json(); if (data.status === true) { clearInterval(this.pollInterval); sessionStorage.setItem("DIAMWALLETDATA", JSON.stringify({ walletAddress: data.address })); this.ws.disconnect(); resolve({ status: data.status, address: data.address }); } else if (Date.now() - startTime > timeout) { clearInterval(this.pollInterval); reject(new Error("Connection timeout")); } } catch (error) { console.error("Polling error:", error); } }, 2000); } startTransactionPolling(resolve, reject, timeout) { const closeWebSocket = (reason = "Client closing connection") => { if (this.ws) { this.ws.close(); clearInterval(this.pollInterval); } // if (timeoutId) clearTimeout(timeoutId); }; this.pollInterval = setInterval(async () => { try { const response = await fetch(`${this.serverUrl}/api/transaction/check-transaction/${this.sessionId}`); const data = await response.json(); console.log(data); switch (data.status) { case "completed": closeWebSocket("Transaction completed"); resolve(data.data); break; case "failed": closeWebSocket("Transaction failed"); clearInterval(this.pollInterval); reject(new Error(data.error)); break; case "cancelled": closeWebSocket("Transaction cancelled"); clearInterval(this.pollInterval); reject(new Error(data.error)); break; case "processing": resolve(data); break; default: console.log("Unknown transaction status:", data); } } catch (error) { clearInterval(this.pollInterval); reject(error); } }, 2000); } startSignTransactionPolling(resolve, reject, timeout) { const closeWebSocket = (reason = "Client closing connection") => { if (this.ws) { this.ws.close(); clearInterval(this.pollInterval); } }; this.pollInterval = setInterval(async () => { try { const response = await fetch(`${this.serverUrl}/api/transaction/check-signed-transaction/${this.sessionId}`); const data = await response.json(); switch (data.status) { case "sign-completed": closeWebSocket("Transaction Signing completed"); // Close WebSocket on success resolve(data.data); break; case "sign-failed": closeWebSocket("Transaction Signing failed"); // Close WebSocket on failure reject(new Error(data.error)); break; case "sign-cancelled": closeWebSocket("Transaction Signing cancelled"); // Close WebSocket on cancellation reject(new Error(data.error)); break; case "sign-processing": resolve(data); break; case "sign-transaction": resolve(data); break; default: console.log("Unknown signing status:", data); } } catch (error) { clearInterval(this.pollInterval); reject(error); } }, 2000); } async getBalance() { try { const walletData = sessionStorage.getItem("DIAMWALLETDATA"); if (!walletData) { throw new Error("No wallet data found"); } const { walletAddress } = JSON.parse(walletData); return await getBalanceData({ address: walletAddress, serverUrl: this.serverUrl, network: this.network }); } catch (error) { console.error("Balance fetch error:", error); throw error; } } async validatePublicAddress(address) { const response = await fetch(`${this.serverUrl}/api/transaction/wallet-address-validation`, { method: "POST", body: JSON.stringify({ address: address, network: this.network }), headers: { Accept: "application/json", "Content-Type": "application/json" } }); let validData = await response.json(); if (validData.data.status === 200) { return { valid: true }; } else { return { valid: false }; } } async disconnect() { try { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.disconnected = false; this.ws.close(); } if (this.pollInterval) { clearInterval(this.pollInterval); } sessionStorage.removeItem("DIAMWALLETDATA"); this.sessionId = null; this.transData = null; this.address = null; } catch (error) { console.error("Disconnect error:", error); throw error; } } getBrowserInfo() { return this.browser; } } module.exports = WebSDK;