cashtab-connect
Version:
A developer-friendly API for integrating with the Cashtab browser extension
267 lines • 11.2 kB
JavaScript
;
// 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