UNPKG

cashtab-connect

Version:

A developer-friendly API for integrating with the Cashtab browser extension

267 lines 11.2 kB
"use strict"; // Copyright (c) 2025 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. Object.defineProperty(exports, "__esModule", { value: true }); exports.CashtabConnect = exports.CashtabTimeoutError = exports.CashtabTransactionDeniedError = exports.CashtabAddressDeniedError = exports.CashtabExtensionUnavailableError = void 0; class CashtabExtensionUnavailableError extends Error { constructor(message) { super(message || 'Cashtab extension is not available'); this.name = 'CashtabExtensionUnavailableError'; } } exports.CashtabExtensionUnavailableError = CashtabExtensionUnavailableError; class CashtabAddressDeniedError extends Error { constructor(reason) { super(reason || 'User denied address request'); this.name = 'CashtabAddressDeniedError'; } } exports.CashtabAddressDeniedError = CashtabAddressDeniedError; class CashtabTransactionDeniedError extends Error { constructor(reason) { super(reason || 'User denied transaction request'); this.name = 'CashtabTransactionDeniedError'; } } exports.CashtabTransactionDeniedError = CashtabTransactionDeniedError; class CashtabTimeoutError extends Error { constructor() { super('Request timed out'); this.name = 'CashtabTimeoutError'; } } exports.CashtabTimeoutError = CashtabTimeoutError; class CashtabConnect { constructor(timeout = 30000) { this.messageListeners = new Map(); this.timeout = timeout; this.setupMessageListener(); } setupMessageListener() { if (typeof window !== 'undefined' && window.addEventListener) { window.addEventListener('message', event => { if (event.source !== window) return; if (event.data && event.data.type === 'FROM_CASHTAB') { // Handle address response - support both new and old formats const addressListener = this.messageListeners.get('address'); if (addressListener) { // New format: success, address, reason if (typeof event.data.success !== 'undefined') { addressListener({ success: event.data.success, address: event.data.address, reason: event.data.reason, }); this.messageListeners.delete('address'); } // Old format: approval else if (typeof event.data.address !== 'undefined') { if (event.data.address !== 'Address request denied by user') { // User approved - address should be available if (event.data.address) { addressListener({ success: true, address: event.data.address, }); } else { // Address not provided in old format - this is an error addressListener({ success: false, address: undefined, reason: 'Address not provided in response', }); } } else { // User denied addressListener({ success: false, address: undefined, reason: 'User denied the address request', }); } this.messageListeners.delete('address'); } // Old format: explicit denial else if (event.data.addressRequestApproved === false) { addressListener({ success: false, address: undefined, reason: 'User denied the address request', }); this.messageListeners.delete('address'); } } // Handle transaction response const transactionListener = this.messageListeners.get('transaction'); if (transactionListener && event.data.txResponse) { transactionListener({ success: event.data.txResponse.approved, txid: event.data.txResponse.txid, reason: event.data.txResponse.reason, }); this.messageListeners.delete('transaction'); } } }); } } async checkExtensionAvailability() { // Check if the extension is available in the window object const isAvailable = typeof window !== 'undefined' && window.bitcoinAbc === 'cashtab'; return isAvailable; } // When a web page requests the user address, a response is expected sendMessage(message) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { this.messageListeners.delete('address'); this.messageListeners.delete('transaction'); reject(new CashtabTimeoutError()); }, this.timeout); this.messageListeners.set('address', response => { clearTimeout(timeoutId); resolve(response); }); if (typeof window !== 'undefined' && window.postMessage) { window.postMessage(message, '*'); } else { reject(new CashtabExtensionUnavailableError()); } }); } // When a web page creates a transaction, a response is expected sendTransactionMessage(message) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { this.messageListeners.delete('transaction'); reject(new CashtabTimeoutError()); }, this.timeout); this.messageListeners.set('transaction', response => { clearTimeout(timeoutId); resolve(response); }); if (typeof window !== 'undefined' && window.postMessage) { window.postMessage(message, '*'); } else { reject(new CashtabExtensionUnavailableError()); } }); } // No response is expect when a web page creates a transaction // TODO expect response of the txid or cancel notice sendMessageNoResponse(message) { if (typeof window !== 'undefined' && window.postMessage) { window.postMessage(message, '*'); } else { throw new CashtabExtensionUnavailableError(); } } async requestAddress() { const request = { text: 'Cashtab', type: 'FROM_PAGE', addressRequest: true, }; const response = await this.sendMessage(request); if (!response.success) { throw new CashtabAddressDeniedError(response.reason); } if (!response.address) { throw new Error('No address received from extension'); } return response.address; } /** * Create a transaction using a BIP21 URI directly * @param bip21Uri - The BIP21 URI (e.g., "ecash:address?amount=0.001&memo=Payment") * @returns Promise that resolves with transaction response */ async createTransactionFromBip21(bip21Uri) { const request = { text: 'Cashtab', type: 'FROM_PAGE', txInfo: { bip21: bip21Uri }, }; const response = await this.sendTransactionMessage(request); if (!response.success) { throw new CashtabTransactionDeniedError(response.reason); } return response; } /** * Send XEC to an address using Cashtab (dev-friendly) * @param address - eCash address * @param amount - Amount in XEC (string or number) * @returns Promise that resolves with transaction response */ async sendXec(address, amount) { const bip21 = `${address}?amount=${amount}`; return this.sendBip21(bip21); } /** * Send a token using Cashtab (dev-friendly) * @param address - eCash address * @param tokenId - Token ID * @param tokenDecimalizedQty - Decimalized token quantity (string or number) * @returns Promise that resolves with transaction response */ async sendToken(address, tokenId, tokenDecimalizedQty) { const bip21 = `${address}?token_id=${tokenId}&token_decimalized_qty=${tokenDecimalizedQty}`; return this.sendBip21(bip21); } /** * Send a raw BIP21 string using Cashtab * @param bip21 - BIP21 URI string * @returns Promise that resolves with transaction response */ async sendBip21(bip21) { const request = { text: 'Cashtab', type: 'FROM_PAGE', txInfo: { bip21 }, }; const response = await this.sendTransactionMessage(request); if (!response.success) { throw new CashtabTransactionDeniedError(response.reason); } return response; } destroy() { this.messageListeners.clear(); } /** * Wait for the extension to become available * In practice this takes less than 1s * @param timeout - Maximum time to wait in milliseconds (default: 3000) * @returns Promise that resolves when extension is available or rejects on timeout */ async waitForExtension(timeout = 3000) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { if (await this.checkExtensionAvailability()) { return; } // Wait a bit before checking again await new Promise(resolve => setTimeout(resolve, 300)); } throw new CashtabExtensionUnavailableError('Extension did not become available within the timeout period'); } /** * Check if the extension is currently available * @returns Promise that resolves to true if extension is available, false otherwise */ async isExtensionAvailable() { return this.checkExtensionAvailability(); } } exports.CashtabConnect = CashtabConnect; //# sourceMappingURL=index.js.map