UNPKG

mochimo-mesh-api-client

Version:

A TypeScript client library for interacting with the Mochimo blockchain API

534 lines (527 loc) 17.8 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { MCM_CURRENCY: () => MCM_CURRENCY, MochimoApiClient: () => MochimoApiClient, NETWORK_IDENTIFIER: () => NETWORK_IDENTIFIER, TransactionBuilder: () => TransactionBuilder, formatMemo: () => formatMemo, isValidMemo: () => isValidMemo }); module.exports = __toCommonJS(index_exports); // src/types.ts var MCM_CURRENCY = { symbol: "MCM", decimals: 9 }; var NETWORK_IDENTIFIER = { blockchain: "mochimo", network: "mainnet" }; // src/utils/logger.ts var Logger = class _Logger { constructor() { this.isDebug = false; } static getInstance() { if (!_Logger.instance) { _Logger.instance = new _Logger(); } return _Logger.instance; } enableDebug() { this.isDebug = true; } replaceBigInt(key, value) { if (typeof value === "bigint") { return value.toString(); } return value; } formatMessage(level, message, data) { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const dataString = data ? ` Data: ${JSON.stringify(data, this.replaceBigInt, 2)}` : ""; return `[${timestamp}] ${level.toUpperCase()}: ${message}${dataString}`; } debug(message, data) { if (this.isDebug) { console.debug(this.formatMessage("debug", message, data)); } } info(message, data) { console.info(this.formatMessage("info", message, data)); } warn(message, data) { console.warn(this.formatMessage("warn", message, data)); } error(message, error) { console.error(this.formatMessage("error", message, error)); } }; var logger = Logger.getInstance(); // src/api.ts var MochimoApiClient = class { constructor(baseUrl) { this.baseUrl = baseUrl; this.networkIdentifier = NETWORK_IDENTIFIER; logger.debug("Construction initialized", { baseUrl, networkIdentifier: this.networkIdentifier }); } headersToObject(headers) { const result = {}; headers.forEach((value, key) => { result[key] = value; }); return result; } async handleResponse(response) { const data = await response.json(); logger.debug("API Response", { status: response.status, url: response.url, data, headers: this.headersToObject(response.headers) }); if ("code" in data) { logger.error("API Error", { endpoint: response.url, status: response.status, error: data }); throw new Error(`Rosetta API Error: ${data.message}`); } return data; } async makeRequest(endpoint, body) { const url = `${this.baseUrl}${endpoint}`; logger.debug(`Making request to ${endpoint}`, { url, method: "POST", headers: { "Content-Type": "application/json" }, body }); try { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); return this.handleResponse(response); } catch (error) { logger.error(`Request failed to ${endpoint}`, { url, error: error instanceof Error ? error.message : error }); throw error; } } async derive(publicKey, tag) { logger.debug("Deriving address", { publicKey, tag }); return this.makeRequest("/construction/derive", { network_identifier: this.networkIdentifier, public_key: { hex_bytes: publicKey, curve_type: "wotsp" }, metadata: { tag } }); } async preprocess(operations, metadata) { logger.debug("Preprocessing transaction", { operations, metadata }); return this.makeRequest("/construction/preprocess", { network_identifier: this.networkIdentifier, operations, metadata }); } async metadata(options, publicKeys) { logger.debug("Fetching metadata", { options, publicKeys }); return this.makeRequest("/construction/metadata", { network_identifier: this.networkIdentifier, options, public_keys: publicKeys }); } async payloads(operations, metadata, publicKeys) { logger.debug("Fetching payloads", { operations, metadata, publicKeys }); return this.makeRequest("/construction/payloads", { network_identifier: this.networkIdentifier, operations, metadata, public_keys: publicKeys }); } async combine(unsignedTransaction, signatures) { logger.debug("Combining transaction", { unsignedTransaction, signatures }); return this.makeRequest("/construction/combine", { network_identifier: this.networkIdentifier, unsigned_transaction: unsignedTransaction, signatures }); } async submit(signedTransaction) { logger.debug("Submitting transaction", { signedTransaction }); return this.makeRequest("/construction/submit", { network_identifier: this.networkIdentifier, signed_transaction: signedTransaction }); } async parse(transaction, signed) { logger.debug("Parsing transaction", { transaction, signed }); return this.makeRequest("/construction/parse", { network_identifier: this.networkIdentifier, transaction, signed }); } async resolveTag(tag) { return this.makeRequest("/call", { network_identifier: this.networkIdentifier, parameters: { tag }, method: "tag_resolve" }); } async getAccountBalance(address) { return this.makeRequest("/account/balance", { network_identifier: this.networkIdentifier, account_identifier: { address } }); } async getBlock(identifier) { return this.makeRequest("/block", { network_identifier: this.networkIdentifier, block_identifier: identifier }); } async getNetworkStatus() { return this.makeRequest("/network/status", { network_identifier: this.networkIdentifier }); } /** * Get all transaction identifiers in the mempool */ async getMempoolTransactions() { logger.debug("Fetching mempool transactions"); return this.makeRequest("/mempool", { network_identifier: this.networkIdentifier }); } /** * Get a specific transaction from the mempool * @param transactionHash - The hash of the transaction to fetch */ async getMempoolTransaction(transactionHash) { logger.debug("Fetching mempool transaction", { transactionHash }); return this.makeRequest("/mempool/transaction", { network_identifier: this.networkIdentifier, transaction_identifier: { hash: transactionHash } }); } /** * Monitor the mempool for a specific transaction * @param transactionHash - The hash of the transaction to monitor * @param timeout - Maximum time to wait in milliseconds * @param interval - Check interval in milliseconds */ async waitForTransaction(transactionHash, timeout = 6e4, interval = 1e3) { logger.debug("Monitoring mempool for transaction", { transactionHash, timeout, interval }); const startTime = Date.now(); while (Date.now() - startTime < timeout) { try { const response = await this.getMempoolTransaction(transactionHash); return response; } catch (error) { if (Date.now() - startTime >= timeout) { throw new Error(`Transaction ${transactionHash} not found in mempool after ${timeout}ms`); } await new Promise((resolve) => setTimeout(resolve, interval)); } } throw new Error(`Timeout waiting for transaction ${transactionHash}`); } }; // src/transaction.ts var import_mochimo_wots = require("mochimo-wots"); var import_crypto_js = __toESM(require("crypto-js")); var TransactionBuilder = class { constructor(baseUrl) { this.construction = new MochimoApiClient(baseUrl); logger.debug("TransactionBuilder initialized", { baseUrl }); } createTransactionBytes(params) { const sourceAddr = params.sourceAddress.startsWith("0x") ? params.sourceAddress.slice(2) : params.sourceAddress; const destAddr = params.destinationTag.startsWith("0x") ? params.destinationTag.slice(2) : params.destinationTag; const changePk = params.changePk.startsWith("0x") ? params.changePk.slice(2) : params.changePk; const txBuffer = Buffer.alloc(2304); txBuffer.writeUInt32LE(0, 0); Buffer.from(sourceAddr, "hex").copy(txBuffer, 4); Buffer.from(changePk, "hex").copy(txBuffer, 44); Buffer.from(destAddr, "hex").copy(txBuffer, 84); txBuffer.writeBigUInt64LE(params.amount, 124); txBuffer.writeBigUInt64LE(params.fee, 132); txBuffer.writeUInt32LE(params.blockToLive, 140); if (params.memo) { const memoBuffer = Buffer.from(params.memo); memoBuffer.copy(txBuffer, 144, 0, Math.min(memoBuffer.length, 32)); } return txBuffer; } async buildTransaction(params) { try { const logParams = { ...params, amount: params.amount.toString(), fee: params.fee.toString(), sourceBalance: params.sourceBalance?.toString() }; logger.info("Building transaction", logParams); const txBytes = this.createTransactionBytes(params); logger.debug("Created transaction bytes", { length: txBytes.length, hex: txBytes.toString("hex") }); const operations = [ { operation_identifier: { index: 0 }, type: "SOURCE_TRANSFER", status: "SUCCESS", account: { address: params.sourceTag }, amount: { value: (-params.amount).toString(), currency: MCM_CURRENCY } }, { operation_identifier: { index: 1 }, type: "DESTINATION_TRANSFER", status: "SUCCESS", account: { address: params.destinationTag }, amount: { value: params.amount.toString(), currency: MCM_CURRENCY }, metadata: { memo: params.memo || "" } }, { operation_identifier: { index: 2 }, type: "FEE", status: "SUCCESS", account: { address: params.sourceTag }, amount: { value: params.fee.toString(), currency: MCM_CURRENCY } } ]; logger.debug("Created operations", operations); const preprocessResponse = await this.construction.preprocess(operations, { block_to_live: params.blockToLive.toString(), change_pk: params.changePk, change_addr: params.changePk, source_balance: params.sourceBalance ? params.sourceBalance.toString() : "179999501" }); logger.debug("Preprocess response", preprocessResponse); const metadataResponse = await this.construction.metadata( preprocessResponse.options, [{ hex_bytes: params.publicKey, curve_type: "wotsp" }] ); logger.debug("Metadata response", metadataResponse); const results = await this.construction.payloads( operations, metadataResponse.metadata, [{ hex_bytes: params.publicKey, curve_type: "wotsp" }] ); return results; } catch (error) { logger.error("Error building transaction", error); throw error instanceof Error ? error : new Error("Unknown error occurred"); } } async submitSignedTransaction(signedTransaction) { return await this.construction.submit(signedTransaction); } createSignature(publicKey, unsignedTx, signatureBytes) { return { signing_payload: { hex_bytes: unsignedTx, signature_type: "wotsp" }, public_key: { hex_bytes: Buffer.from(publicKey).toString("hex"), curve_type: "wotsp" }, signature_type: "wotsp", hex_bytes: Buffer.from(signatureBytes).toString("hex") }; } /** * Submit a transaction and wait for it to appear in the mempool * @param signedTransaction - The signed transaction to submit * @param timeout - Maximum time to wait for mempool appearance */ async submitAndMonitor(signedTransaction, timeout = 6e4) { const submitResult = await this.submitSignedTransaction(signedTransaction); logger.debug("Transaction submitted", submitResult); if (!submitResult.transaction_identifier?.hash) { throw new Error("No transaction hash in submit response"); } return await this.construction.waitForTransaction( submitResult.transaction_identifier.hash, timeout ); } /** * Get all transactions currently in the mempool */ async getMempoolTransactions() { return this.construction.getMempoolTransactions(); } /** * Get a specific transaction from the mempool */ async getMempoolTransaction(transactionHash) { return this.construction.getMempoolTransaction(transactionHash); } async buildAndSignTransaction(sourceWallet, changeWallet, destinationTag, amount, fee, memo, blockToLive = 0) { const params = { sourceTag: "0x" + Buffer.from(sourceWallet.getAddrTag()).toString("hex"), sourceAddress: "0x" + Buffer.from(sourceWallet.getAddress()).toString("hex"), destinationTag, amount, fee, publicKey: Buffer.from(sourceWallet.getWots().slice(0, 2144)).toString("hex"), changePk: "0x" + Buffer.from(changeWallet.getAddrHash()).toString("hex"), memo, blockToLive, sourceBalance: amount + fee // This should be fetched from network in production }; const buildResult = await this.buildTransaction(params); const unsignedTransaction = buildResult.unsigned_transaction; const signedTransaction = sourceWallet.sign( import_mochimo_wots.MochimoHasher.hash(new Uint8Array(Buffer.from(unsignedTransaction, "hex"))) ); const pub = sourceWallet.getWots().slice(2144, 2144 + 32); const rnd = sourceWallet.getWots().slice(2144 + 32, 2144 + 32 + 32); const combinedSig = new Uint8Array([ ...signedTransaction, ...pub, ...rnd ]); const sig = this.createSignature( sourceWallet.getAddress(), unsignedTransaction, combinedSig ); const combined = await this.construction.combine(unsignedTransaction, [sig]); const submitResult = await this.submitSignedTransaction(combined.signed_transaction); return { buildResult, submitResult, signedTransaction: combined.signed_transaction }; } // used for testing; gives out two wots wallet instances from a seed and index static createWallets(seed, index, parentWallet) { const sourceWotsSeed = import_crypto_js.default.SHA256(seed + index).toString(); const changeWotsSeed = import_crypto_js.default.SHA256(seed + (index + 1)).toString(); const sourceWallet = import_mochimo_wots.WOTSWallet.create( "source", Buffer.from(sourceWotsSeed, "hex"), parentWallet?.getAddrHash() ); const changeWallet = import_mochimo_wots.WOTSWallet.create( "change", Buffer.from(changeWotsSeed, "hex"), parentWallet?.getAddrHash() ); return { sourceWallet, changeWallet }; } }; // src/utils/memo.ts function isValidMemo(memo) { if (!memo) return true; if (!/^[A-Z0-9-]+$/.test(memo)) { return false; } if (memo.startsWith("-") || memo.endsWith("-")) { return false; } const groups = memo.split("-"); for (let i = 0; i < groups.length; i++) { const group = groups[i]; if (!group) return false; const isLetters = /^[A-Z]+$/.test(group); const isNumbers = /^[0-9]+$/.test(group); if (!isLetters && !isNumbers) { return false; } if (i > 0) { const prevGroup = groups[i - 1]; const prevIsLetters = /^[A-Z]+$/.test(prevGroup); if (isLetters === prevIsLetters) { return false; } } } return true; } function formatMemo(memo) { const result = new Uint8Array(16).fill(0); if (!memo || !isValidMemo(memo)) { return result; } const memoBytes = Buffer.from(memo, "ascii"); const copyLength = Math.min(memoBytes.length, 15); result.set(memoBytes.subarray(0, copyLength)); result[copyLength] = 0; console.log("Result:", Buffer.from(result).toString("hex")); console.log("Result ascii:", Buffer.from(result).toString("ascii")); return result; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { MCM_CURRENCY, MochimoApiClient, NETWORK_IDENTIFIER, TransactionBuilder, formatMemo, isValidMemo });