UNPKG

gib-cli

Version:
311 lines (310 loc) 15.2 kB
import { Hash, P2PKH, PublicKey, Script, Transaction, TransactionSignature, UnlockingScript, } from "@bsv/sdk"; import axios from "axios"; import * as oneSat from "js-1sat-ord"; import { Utils } from "@bsv/sdk"; import CosignTemplate from "./mneeCosignTemplate.js"; export class MNEEService { constructor() { this.mneeApi = "https://proxy-api.mnee.net"; this.mneeApiToken = "92982ec1c0975f31979da515d46bae9f"; this.gorillaPoolApi = "https://ordinals.1sat.app"; this.getConfig = async () => { try { const { data } = await axios.get(`${this.mneeApi}/v1/config?auth_token=${this.mneeApiToken}`); return data; } catch (error) { console.error("Failed to fetch config:", error); } }; this.getBalance = async (address) => { try { const config = await this.getConfig(); if (!config) throw new Error("Config not fetched"); const res = await this.getUtxos(address); const balance = res.reduce((acc, utxo) => { if (utxo.data.bsv21.op === "transfer") { acc += utxo.data.bsv21.amt; } return acc; }, 0); const decimalAmount = parseFloat((balance / 10 ** (config.decimals || 0)).toFixed(config.decimals)); const mneeBalance = { amount: balance, decimalAmount }; return mneeBalance; } catch (error) { console.error("Failed to fetch balance:", error); return { amount: 0, decimalAmount: 0 }; } }; this.createInscription = (recipient, amount, config) => { const inscriptionData = { p: "bsv-20", op: "transfer", id: config.tokenId, amt: amount.toString(), }; return { lockingScript: oneSat.applyInscription(new CosignTemplate().lock(recipient, PublicKey.fromString(config.approver)), { dataB64: Buffer.from(JSON.stringify(inscriptionData)).toString("base64"), contentType: "application/bsv-20", }), satoshis: 1, }; }; this.getUtxos = async (address, ops = ["transfer", "deploy+mint"]) => { try { const { data } = await axios.post(`${this.mneeApi}/v1/utxos?auth_token=${this.mneeApiToken}`, [address]); if (ops.length) { return data.filter((utxo) => ops.includes(utxo.data.bsv21.op.toLowerCase())); } return data; } catch (error) { console.error("Failed to fetch UTXOs:", error); return []; } }; this.broadcast = async (tx) => { const url = `${this.gorillaPoolApi}/v5/tx`; const resp = await axios.post(url, Buffer.from(tx.toBinary()), { headers: { "Content-Type": "application/octet-stream", }, }); const body = resp.data; if (resp.status !== 200) { return { status: "error", code: resp.status.toString(), description: body.error, }; } return { status: "success", txid: body.txid, message: "Transaction broadcast successfully", }; }; this.fetchBeef = async (txid) => { const resp = await fetch(`${this.gorillaPoolApi}/v5/tx/${txid}/beef`); if (resp.status == 404) throw new Error("Transaction not found"); if (resp.status !== 200) { throw new Error(`${resp.status} - Failed to fetch beef for tx ${txid}`); } const beef = [...Buffer.from(await resp.arrayBuffer())]; return Transaction.fromAtomicBEEF(beef); }; this.getSignatures = async (request, privateKey // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => { try { const DEFAULT_SIGHASH_TYPE = 65; let tx; switch (request.format) { case "beef": tx = Transaction.fromHexBEEF(request.rawtx); break; case "ef": tx = Transaction.fromHexEF(request.rawtx); break; default: tx = Transaction.fromHex(request.rawtx); break; } const sigResponses = request.sigRequests.flatMap((sigReq) => { return [privateKey].map((privKey) => { // TODO: support multiple OP_CODESEPARATORs and get subScript according to `csIdx`. See SignatureRequest.csIdx in the GetSignatures type. const preimage = TransactionSignature.format({ sourceTXID: sigReq.prevTxid, sourceOutputIndex: sigReq.outputIndex, sourceSatoshis: sigReq.satoshis, transactionVersion: tx.version, otherInputs: tx.inputs.filter((_, index) => index !== sigReq.inputIndex), inputIndex: sigReq.inputIndex, outputs: tx.outputs, inputSequence: tx.inputs[sigReq.inputIndex].sequence || 0, subscript: sigReq.script ? Script.fromHex(sigReq.script) : new P2PKH().lock(privKey.toPublicKey().toAddress()), lockTime: tx.lockTime, scope: sigReq.sigHashType || DEFAULT_SIGHASH_TYPE, }); const rawSignature = privKey.sign(Hash.sha256(preimage)); const sig = new TransactionSignature(rawSignature.r, rawSignature.s, sigReq.sigHashType || DEFAULT_SIGHASH_TYPE); return { sig: Utils.toHex(sig.toChecksigFormat()), pubKey: privKey.toPublicKey().toString(), inputIndex: sigReq.inputIndex, sigHashType: sigReq.sigHashType || DEFAULT_SIGHASH_TYPE, csIdx: sigReq.csIdx, }; }); }); return Promise.resolve({ sigResponses }); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err) { console.error("getSignatures error", err); return { error: { message: err.message ?? "unknown", cause: err.cause, }, }; } }; this.transfer = async (address, request, privateKey, logger) => { try { const config = await this.getConfig(); if (!config) throw new Error("Config not fetched"); const totalAmount = request.reduce((sum, req) => sum + req.amount, 0); if (totalAmount <= 0) return { error: "Invalid amount" }; const totalAtomicTokenAmount = this.toAtomicAmount(totalAmount, config.decimals); // Fetch UTXOs logger.update("Fetching UTXOs..."); const utxos = await this.getUtxos(address); const totalUtxoAmount = utxos.reduce((sum, utxo) => sum + (utxo.data.bsv21.amt || 0), 0); if (totalUtxoAmount < totalAtomicTokenAmount) { return { error: "Insufficient MNEE balance" }; } // Determine fee const fee = request.find((req) => req.address === config.burnAddress) !== undefined ? 0 : config.fees.find((fee) => totalAtomicTokenAmount >= fee.min && totalAtomicTokenAmount <= fee.max)?.fee; if (fee === undefined) return { error: "Fee ranges inadequate" }; // Build transaction const tx = new Transaction(1, [], [], 0); let tokensIn = 0; const signingAddresses = []; let changeAddress = ""; while (tokensIn < totalAtomicTokenAmount + fee) { const utxo = utxos.shift(); if (!utxo) return { error: "Insufficient MNEE balance" }; const sourceTransaction = await this.fetchBeef(utxo.txid); if (!sourceTransaction) return { error: "Failed to fetch source transaction" }; signingAddresses.push(utxo.owners[0]); changeAddress = changeAddress || utxo.owners[0]; tx.addInput({ sourceTXID: utxo.txid, sourceOutputIndex: utxo.vout, sourceTransaction, unlockingScript: new UnlockingScript(), }); tokensIn += utxo.data.bsv21.amt; } for (const req of request) { tx.addOutput(this.createInscription(req.address, this.toAtomicAmount(req.amount, config.decimals), config)); } if (fee > 0) tx.addOutput(this.createInscription(config.feeAddress, fee, config)); const change = tokensIn - totalAtomicTokenAmount - fee; if (change > 0) { tx.addOutput(this.createInscription(changeAddress, change, config)); } // Signing transaction const sigRequests = tx.inputs.map((input, index) => { if (!input.sourceTXID) throw new Error("Source TXID is undefined"); return { prevTxid: input.sourceTXID, outputIndex: input.sourceOutputIndex, inputIndex: index, address: signingAddresses[index], script: input.sourceTransaction?.outputs[input.sourceOutputIndex].lockingScript.toHex(), satoshis: input.sourceTransaction?.outputs[input.sourceOutputIndex] .satoshis || 1, sigHashType: TransactionSignature.SIGHASH_ALL | TransactionSignature.SIGHASH_ANYONECANPAY | TransactionSignature.SIGHASH_FORKID, }; }); const rawtx = tx.toHex(); const res = await this.getSignatures({ rawtx, sigRequests }, privateKey); if (!res?.sigResponses) return { error: "Failed to get signatures" }; // Apply signatures for (const sigResponse of res.sigResponses) { tx.inputs[sigResponse.inputIndex].unlockingScript = new Script() .writeBin(Utils.toArray(sigResponse.sig, "hex")) .writeBin(Utils.toArray(sigResponse.pubKey, "hex")); } // Submit transaction using Axios logger.update("Getting signatures..."); const base64Tx = Utils.toBase64(tx.toBinary()); const response = await axios.post(`${this.mneeApi}/v1/transfer?auth_token=${this.mneeApiToken}`, { rawtx: base64Tx, }); if (!response.data.rawtx) return { error: "Failed to broadcast transaction" }; const decodedBase64AsBinary = Utils.toArray(response.data.rawtx, "base64"); const tx2 = Transaction.fromBinary(decodedBase64AsBinary); logger.update("Broadcasting transaction..."); await this.broadcast(tx2); return { txid: tx2.id("hex"), rawtx: Utils.toHex(decodedBase64AsBinary) }; } catch (error) { let errorMessage = "Transaction submission failed"; if (axios.isAxiosError(error) && error.response) { const { status, data } = error.response; if (data?.message) { if (status === 423) { if (data.message.includes("frozen")) { errorMessage = "Your address is currently frozen and cannot send tokens"; } else if (data.message.includes("blacklisted")) { errorMessage = "The recipient address is blacklisted and cannot receive tokens"; } else { errorMessage = "Transaction blocked: Address is either frozen or blacklisted"; } } else if (status === 503) { if (data.message.includes("cosigner is paused")) { errorMessage = "Token transfers are currently paused by the administrator"; } else errorMessage = "Service temporarily unavailable"; } else { errorMessage = data.message; } } } else { errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; } console.error("Failed to transfer tokens:", errorMessage); return { error: errorMessage }; } }; } toAtomicAmount(amount, decimals) { return Math.round(amount * 10 ** decimals); } async getTransactionHistory(address, limit = 10) { try { // Fetch latest transactions from Whatsonchain const response = await axios.get(`https://api.whatsonchain.com/v1/bsv/main/address/${address}/history`); // Extract only the TXIDs return response.data.slice(0, limit).map((tx) => tx.tx_hash); } catch (error) { console.error("❌ Error fetching transaction history:", error); return []; } } }