satsterminal-sdk
Version:
A TypeScript SDK for interacting with the SatsTerminal ecosystem.
757 lines (756 loc) • 31.3 kB
JavaScript
"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;