UNPKG

satsterminal-sdk

Version:

A TypeScript SDK for interacting with the SatsTerminal ecosystem.

757 lines (756 loc) 31.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SatsTerminal = void 0; const node_fetch_1 = __importDefault(require("node-fetch")); const bitcoinjs_lib_1 = require("bitcoinjs-lib"); const API_BASE_URL = 'https://tba-7448181ebd11.herokuapp.com'; class SatsTerminal { constructor(config) { if (!config.apiKey) { throw new Error('API key is required'); } this.config = { apiKey: config.apiKey, }; } /** * Fetches a quote based on the provided BTC amount and rune name. * @param btcAmount The amount of BTC. * @param runeName The name of the rune. */ async fetchQuote(btcAmount, runeName) { try { const response = await (0, node_fetch_1.default)(`${API_BASE_URL}/v1/tba/fetch-quote`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify({ btcAmount: btcAmount.toString(), runeName }), }); if (!response.ok) { const data = await response.json(); console.error('Error fetching quote:', data.message); throw new Error(data.message); } const data = await response.json(); console.log('Received QUOTE from TBA:', data); return { marketplace: data.bestMarketplace, selectedOrders: data.selectedOrders, totalFormattedAmount: data.totalFormattedAmount, totalPrice: data.totalPrice, metrics: data.metrics, }; } catch (error) { console.error('Error fetching quote:', error); throw error; } } /** * Generates a PSBT for Odinswap orders. * @param orders The list of orders. * @param address The recipient address. * @param publicKey The public key of the recipient. * @param paymentAddress The payment address. * @param paymentPublicKey The public key of the payment address. * @param runeName The name of the rune. */ async getOdinswapPSBT(orders, address, publicKey, paymentAddress, paymentPublicKey, runeName) { try { const payload = { fromToken: 'BTC', fromTokenAmount: orders[0].fromTokenAmount, toToken: runeName, slippage: orders[0].slippage, address: paymentAddress, receiverAddress: address, publicKey: paymentPublicKey, costPayerAddress: paymentAddress, costPayerPublicKey: paymentPublicKey, }; const response = await (0, node_fetch_1.default)(`${API_BASE_URL}/v1/swap/preview`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify(payload), }); const data = await response.json(); if (data.error) { return { odinswapError: data.error, base64: '', hex: '', swapId: '' }; } return { base64: data.psbt, hex: data.psbtHex, swapId: data.swapId, }; } catch (error) { console.error('Error creating Odinswap order:', error); throw new Error('Failed to create Odinswap order'); } } /** * Generates a PSBT for Magic Eden orders. * @param orders The list of orders. * @param address The recipient address. * @param publicKey The public key of the recipient. * @param paymentAddress The payment address. * @param paymentPublicKey The public key of the payment address. * @param runeName The name of the rune. */ async getMagicEdenPSBT(orders, address, publicKey, paymentAddress, paymentPublicKey, runeName) { try { const sanitizedRune = runeName.replace(/•/g, ''); const response = await (0, node_fetch_1.default)(`${API_BASE_URL}/v1/runes/unsigned_psbt`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify({ orderIds: orders.map((order) => order.id), runeSymbol: sanitizedRune, takerPaymentAddress: paymentAddress, takerPublicKey: paymentPublicKey, takerReceiveAddress: address, takerReceivePublicKey: publicKey, }), }); const data = await response.json(); console.log('MagicEden Response:', data); if (data.error) { throw new Error(data.error); } return { ...data }; } catch (error) { console.error('Error generating MagicEden PSBT:', error); return { magicEdenError: error.message || 'Failed to generate MagicEden PSBT', base64: '', hex: '' }; } } /** * Generates a PSBT for OKX orders. * @param orders The list of orders. * @param ordinalsAddress The ordinals address. * @param publicKey The public key of the recipient. * @param paymentAddress The payment address. * @param utxos The list of UTXOs. */ async getOkxPSBT(orders, ordinalsAddress, publicKey, paymentAddress, utxos) { try { const okxPsbt = await this.fetchOkxPSBT(orders, ordinalsAddress, publicKey); const makerFee = okxPsbt.makerFee; const takerFee = okxPsbt.takerFee; const makerFeeAddress = okxPsbt.makerFeeAddress; const takerFeeAddress = okxPsbt.takerFeeAddress; const psbt = bitcoinjs_lib_1.Psbt.fromBase64(okxPsbt.base64); let totalOutputValue = psbt.txOutputs.reduce((sum, output) => sum + output.value, 0); if (makerFee && makerFee !== '') { totalOutputValue += parseInt(makerFee, 10); } if (takerFee && takerFee !== '') { totalOutputValue += parseInt(takerFee, 10); } let selectedUTXOs = []; let accumulatedValue = 0; const feeRate = await this.getMidFeeRate(); const estimatedTxSize = (inputs, outputs) => inputs * 148 + outputs * 34 + 10; let estimatedFee = 0; const desiredOutputs = psbt.txOutputs.length + (makerFee ? 1 : 0) + (takerFee ? 1 : 0) + 1; const sortedUtxos = utxos .filter((utxo) => utxo.status.confirmed) .sort((a, b) => b.value - a.value); for (const utxo of sortedUtxos) { selectedUTXOs.push(utxo); accumulatedValue += utxo.value; estimatedFee = feeRate * estimatedTxSize(selectedUTXOs.length, desiredOutputs); if (accumulatedValue >= totalOutputValue + estimatedFee) { break; } } if (accumulatedValue < totalOutputValue + estimatedFee) { throw new Error('Insufficient funds to cover outputs and fees.'); } const newPsbt = new bitcoinjs_lib_1.Psbt(); if (selectedUTXOs.length > 0) { newPsbt.addInput({ hash: selectedUTXOs[0].txid, index: selectedUTXOs[0].vout, witnessUtxo: { script: bitcoinjs_lib_1.address.toOutputScript(paymentAddress), value: selectedUTXOs[0].value, }, }); } for (let i = 0; i < psbt.txInputs.length; i++) { const input = psbt.txInputs[i]; newPsbt.addInput({ ...input, }); } for (let i = 1; i < selectedUTXOs.length; i++) { newPsbt.addInput({ hash: selectedUTXOs[i].txid, index: selectedUTXOs[i].vout, witnessUtxo: { script: bitcoinjs_lib_1.address.toOutputScript(paymentAddress), value: selectedUTXOs[i].value, }, }); } newPsbt.addOutput({ ...psbt.txOutputs[0], address: ordinalsAddress, }); for (let i = 1; i < psbt.txOutputs.length; i++) { newPsbt.addOutput(psbt.txOutputs[i]); } if (makerFee) { newPsbt.addOutput({ address: makerFeeAddress, value: parseInt(makerFee, 10), }); } if (takerFee) { newPsbt.addOutput({ address: takerFeeAddress, value: parseInt(takerFee, 10), }); } const change = accumulatedValue - totalOutputValue - estimatedFee; if (change > 0) { newPsbt.addOutput({ address: paymentAddress, value: change, }); } const newPsbtBase64 = newPsbt.toBase64(); console.log('Constructed new PSBT:', newPsbtBase64); return { base64: newPsbtBase64, hex: newPsbt.toHex(), estimatedFee, selectedUTXOs, makerFee, takerFee, makerFeeAddress, takerFeeAddress, }; } catch (error) { console.error('Error constructing new PSBT:', error); return { okxError: error.message || 'Failed to construct new PSBT', base64: '', hex: '', estimatedFee: 0, selectedUTXOs: [], makerFee: '', takerFee: '', makerFeeAddress: '', takerFeeAddress: '', }; } } /** * Generates a PSBT for Unisat orders. * @param orders The list of orders. * @param address The recipient address. * @param publicKey The public key of the recipient. * @param paymentAddress The payment address. * @param paymentPublicKey The public key of the payment address. * @param runeName The name of the rune. */ async getUnisatPSBT(orders, address, publicKey, paymentAddress, paymentPublicKey, runeName) { try { const payload = { auctionIds: orders.map((order) => order.id), bidPrices: orders.map((order) => order.price), address: paymentAddress, pubkey: paymentPublicKey, runeName: runeName, amount: orders.reduce((sum, order) => sum + (order.formattedAmount || 0), 0), totalPrice: orders.reduce((sum, order) => sum + (order.price || 0), 0), }; if (paymentAddress !== address) { payload.nftAddress = address; } const response = await (0, node_fetch_1.default)(`${API_BASE_URL}/v1/unisat/createPurchaseOrder`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify(payload), }); console.log('Unisat Response:', response); const data = await response.json(); if (data.error) { throw new Error(data.error); } if (!data.base64 || !data.hex) { throw new Error('Invalid PSBT response from Unisat'); } return { ...data }; } catch (error) { console.error('Error generating Unisat PSBT:', error); throw error; } } /** * Confirms a PSBT for OKX orders. * @param orders The list of orders. * @param address The recipient address. * @param publicKey The public key of the recipient. * @param paymentAddress The payment address. * @param paymentPublicKey The public key of the payment address. * @param signedPsbtBase64 The signed PSBT in base64 format. */ async confirmOkxPSBT(orders, address, publicKey, paymentAddress, paymentPublicKey, signedPsbtBase64) { try { const response = await (0, node_fetch_1.default)(`${API_BASE_URL}/v1/okx/bulk-purchase`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify({ orderIds: orders.map((order) => order.id), fromAddress: paymentAddress, buyerPSBT: signedPsbtBase64, }), }); const data = await response.json(); if (data.error) { return { okxError: data.error }; } return { okxError: null, txidOkx: data.txid }; } catch (error) { console.error('Error confirming OKX PSBT:', error); return { okxError: error.message || 'Failed to confirm OKX PSBT' }; } } /** * Confirms a PSBT for Magic Eden orders. * @param orders The list of orders. * @param address The recipient address. * @param publicKey The public key of the recipient. * @param paymentAddress The payment address. * @param paymentPublicKey The public key of the payment address. * @param signedPsbtBase64 The signed PSBT in base64 format. */ async confirmMagicEdenPSBT(orders, address, publicKey, paymentAddress, paymentPublicKey, signedPsbtBase64, swapId) { try { const response = await (0, node_fetch_1.default)(`${API_BASE_URL}/v1/runes/submit_psbt`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify({ orderIds: orders.map((order) => order.id), takerPaymentAddress: paymentAddress, takerReceiveAddress: address, signedPsbtBase64: signedPsbtBase64, swapId: swapId, }), }); const data = await response.json(); if (data.error) { return { magicEdenError: data.error }; } return { magicEdenError: null, txidMagicEden: data.txid }; } catch (error) { console.error('Error confirming MagicEden PSBT:', error); return { magicEdenError: error.message || 'Failed to confirm MagicEden PSBT' }; } } /** * Confirms a PSBT for Unisat orders. * @param swapId The swap ID. * @param signedPsbtHex The signed PSBT in hex format. */ async confirmUnisatPSBT(swapId, signedPsbtHex) { try { const response = await (0, node_fetch_1.default)(`${API_BASE_URL}/v1/unisat/confirmPurchaseOrder`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify({ swapId: swapId, psbtBid: signedPsbtHex, }), }); const data = await response.json(); if (data.error) { return { unisatError: data.error }; } return { unisatError: null, txidUnisat: data.txid }; } catch (error) { console.error('Error confirming Unisat PSBT:', error); return { unisatError: error.message || 'Failed to confirm Unisat PSBT' }; } } /** * Confirms a PSBT for Odinswap orders. * @param bidId The swap ID. * @param signedPsbtBase64 The signed PSBT in base64 format. */ async confirmOdinswapPSBT(bidId, signedPsbtBase64) { try { if (!bidId) { throw new Error('Swap ID is required for Odinswap confirmation'); } const payload = { swapId: bidId, signedPsbt: signedPsbtBase64, }; const response = await (0, node_fetch_1.default)(`${API_BASE_URL}/v1/swap/execute`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify(payload), }); const data = await response.json(); if (data.error) { return { odinswapError: data.error }; } return { odinswapError: null, txidOdinswap: data.txHash }; } catch (error) { console.error('Error confirming Odinswap PSBT:', error); return { odinswapError: error.message || 'Failed to confirm Odinswap PSBT' }; } } /** * Fetches PSBT from the relevant marketplace based on the orders. * @param orders The list of orders from a single marketplace * @param address The recipient address * @param publicKey The public key of the recipient * @param paymentAddress The payment address * @param paymentPublicKey The public key of the payment address * @param runeName The name of the rune * @param utxos The list of UTXOs (required for OKX) */ async fetchPSBTs(orders, address, publicKey, paymentAddress, paymentPublicKey, runeName, utxos) { try { if (orders.length === 0) { throw new Error('No orders provided'); } const marketplace = orders[0].market.toLowerCase(); let psbtBase64 = ''; let psbtHex = ''; let swapId = ''; switch (marketplace) { case 'okx': try { const okxResult = await this.getOkxPSBT(orders, address, publicKey, paymentAddress, utxos); psbtBase64 = okxResult.base64; psbtHex = okxResult.hex; } catch (error) { throw new Error(`OKX Error: ${error instanceof Error ? error.message : 'Unknown error'}`); } break; case 'magiceden': try { const meResult = await this.getMagicEdenPSBT(orders, address, publicKey, paymentAddress, paymentPublicKey, runeName); psbtBase64 = meResult.base64; psbtHex = meResult.hex; swapId = meResult.swapId || ''; console.log('swapId', meResult.swapId); } catch (error) { throw new Error(`MagicEden Error: ${error instanceof Error ? error.message : 'Unknown error'}`); } break; case 'unisat': try { const unisatResult = await this.getUnisatPSBT(orders, address, publicKey, paymentAddress, paymentPublicKey, runeName); psbtBase64 = unisatResult.base64; psbtHex = unisatResult.hex; swapId = unisatResult.swapId || ''; } catch (error) { throw new Error(`Unisat Error: ${error instanceof Error ? error.message : 'Unknown error'}`); } break; case 'odinswap': try { const odinswapResult = await this.getOdinswapPSBT(orders, address, publicKey, paymentAddress, paymentPublicKey, runeName); psbtBase64 = odinswapResult.base64; psbtHex = odinswapResult.hex; swapId = odinswapResult.swapId || ''; } catch (error) { throw new Error(`Odinswap Error: ${error instanceof Error ? error.message : 'Unknown error'}`); } break; default: throw new Error(`Unsupported marketplace: ${marketplace}`); } if (!psbtBase64 || !psbtHex) { throw new Error(`Failed to get valid PSBT from ${marketplace}`); } let marketplacePSBTs = { marketplace, psbtBase64, psbtHex, swapId }; return marketplacePSBTs; } catch (error) { console.error('Error fetching PSBT:', error); throw error; // Propagate the error with marketplace context already added } } /** * Confirms PSBTs for all relevant marketplaces. * @param params The parameters for confirming PSBTs */ async confirmPSBT(params) { try { const marketplace = params.marketplace.toLowerCase(); const orders = params.orders; if (!marketplace) { throw new Error('Marketplace is required'); } if (!orders || orders.length === 0) { throw new Error('No orders provided for confirmation'); } const results = []; switch (marketplace) { case 'okx': try { if (!params.signedPsbt.psbtHex) { throw new Error('Signed PSBT hex is required for OKX confirmation'); } const okxResult = await this.confirmOkxPSBT(orders, params.address, params.publicKey, params.paymentAddress, params.paymentPublicKey, params.signedPsbt.psbtHex); results.push({ marketplace: 'okx', txid: okxResult.txidOkx || null, error: okxResult.okxError || null }); } catch (error) { results.push({ marketplace: 'okx', txid: null, error: error instanceof Error ? error.message : 'Unknown error' }); } break; case 'magiceden': try { if (!params.signedPsbt.psbtBase64) { throw new Error('Signed PSBT base64 is required for MagicEden confirmation'); } if (!params.signedPsbt.swapId) { throw new Error('Swap ID is required for MagicEden confirmation'); } const meResult = await this.confirmMagicEdenPSBT(orders, params.address, params.publicKey, params.paymentAddress, params.paymentPublicKey, params.signedPsbt.psbtBase64, params.signedPsbt.swapId); results.push({ marketplace: 'magiceden', txid: meResult.txidMagicEden || null, error: meResult.magicEdenError || null }); } catch (error) { results.push({ marketplace: 'magiceden', txid: null, error: error instanceof Error ? error.message : 'Unknown error' }); } break; case 'unisat': try { if (!params.signedPsbt.psbtHex) { throw new Error('Signed PSBT hex is required for Unisat confirmation'); } if (!params.signedPsbt.swapId) { throw new Error('Order ID is required for Unisat confirmation'); } const unisatResult = await this.confirmUnisatPSBT(params.signedPsbt.swapId, params.signedPsbt.psbtHex); results.push({ marketplace: 'unisat', txid: unisatResult.txidUnisat || null, error: unisatResult.unisatError || null }); } catch (error) { results.push({ marketplace: 'unisat', txid: null, error: error instanceof Error ? error.message : 'Unknown error' }); } break; case 'odinswap': try { if (!params.signedPsbt.psbtBase64) { throw new Error('Signed PSBT base64 is required for Odinswap confirmation'); } if (!params.signedPsbt.swapId) { throw new Error('Swap ID is required for Odinswap confirmation'); } const odinswapResult = await this.confirmOdinswapPSBT(params.signedPsbt.swapId, params.signedPsbt.psbtBase64); results.push({ marketplace: 'odinswap', txid: odinswapResult.txidOdinswap || null, error: odinswapResult.odinswapError || null }); } catch (error) { results.push({ marketplace: 'odinswap', txid: null, error: error instanceof Error ? error.message : 'Unknown error' }); } break; default: results.push({ marketplace, txid: null, error: `Unsupported marketplace: ${marketplace}` }); } return results; } catch (error) { // Handle any unexpected errors at the top level return [{ marketplace: params.marketplace.toLowerCase(), txid: null, error: error instanceof Error ? error.message : 'Unknown error' }]; } } // Private helper methods (endpoints are encapsulated and not exposed) async fetchOkxPSBT(orders, address, publicKey) { try { const response = await (0, node_fetch_1.default)(`${API_BASE_URL}/v1/okx/bulk-psbt`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify({ orderIds: orders.map((order) => order.id).join(','), walletAddress: address, walletPubkey: publicKey, }), }); if (!response.ok) { throw new Error('Failed to fetch OKX PSBT'); } return await response.json(); } catch (error) { console.error('Error fetching OKX PSBT:', error); throw error; } } async getMidFeeRate() { try { const response = await (0, node_fetch_1.default)('https://mempool.space/api/v1/fees/recommended'); const data = await response.json(); return data.halfHourFee; } catch (error) { console.error('Error fetching fee rate:', error); return 20; } } getHeaders() { return { 'Content-Type': 'application/json', 'x-api-key': this.config.apiKey, }; } /** * Fetches PSBT for any marketplace. * @param params The parameters for fetching PSBT */ async fetchPSBT(orders, address, publicKey, paymentAddress, paymentPublicKey, utxos, feeRate, runeName) { console.log('fetchPSBT', orders); try { // if (orders.length === 0) { // throw new Error('No orders provided'); // } const marketplace = orders[0].market.toLowerCase(); const payload = { orders, address, publicKey, paymentAddress, paymentPublicKey, utxos, feeRate, runeName }; const response = await (0, node_fetch_1.default)(`${API_BASE_URL}/v1/tba/get-psbt`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify(payload), }); const data = await response.json(); if (data.error) { throw new Error(data.error); } if (!data.psbtBase64 || !data.psbtHex || !data.swapId) { throw new Error(`Invalid PSBT response from ${marketplace}: missing required fields`); } return { marketplace, psbtBase64: data.psbtBase64, psbtHex: data.psbtHex, swapId: data.swapId }; } catch (error) { console.error('Error fetching PSBT:', error); throw error; } } /** * Confirms PSBT for any marketplace. * @param params The parameters for confirming PSBT */ async confirmPSBTs(params) { try { if (!params.orders || params.orders.length === 0) { throw new Error('No orders provided for confirmation'); } const marketplace = params.orders[0].market.toLowerCase(); const results = []; let payload = params; const response = await (0, node_fetch_1.default)(`${API_BASE_URL}/v1/tba/confirm-psbt`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify(payload), }); if (!response.ok) { throw new Error(`Failed to confirm PSBT for ${marketplace}`); } const data = await response.json(); if (!data.txid || !data.error) { throw new Error(`Failed to confirm PSBT for ${marketplace}`); } results.push({ marketplace: marketplace, txid: data.txid, error: data.error || null }); return results; } catch (error) { // Handle any unexpected errors at the top level const marketplace = params.orders[0].market.toLowerCase(); return [{ marketplace: marketplace || 'unknown', txid: null, error: error instanceof Error ? error.message : 'Unknown error' }]; } } } exports.SatsTerminal = SatsTerminal;