UNPKG

@xchainjs/xchain-litecoin

Version:

Custom Litecoin client and utilities used by XChainJS clients

636 lines (618 loc) 25.3 kB
'use strict'; var xchainClient = require('@xchainjs/xchain-client'); var xchainUtxo = require('@xchainjs/xchain-utxo'); var Litecoin = require('bitcoinjs-lib'); var accumulative = require('coinselect/accumulative'); var xchainUtil = require('@xchainjs/xchain-util'); var xchainUtxoProviders = require('@xchainjs/xchain-utxo-providers'); var axios = require('axios'); var xchainCrypto = require('@xchainjs/xchain-crypto'); var AppBtc = require('@ledgerhq/hw-app-btc'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var Litecoin__namespace = /*#__PURE__*/_interopNamespace(Litecoin); var accumulative__default = /*#__PURE__*/_interopDefault(accumulative); var axios__default = /*#__PURE__*/_interopDefault(axios); var AppBtc__default = /*#__PURE__*/_interopDefault(AppBtc); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /** * Minimum transaction fee in satoshis. * 1000 satoshi/kB (similar to current `minrelaytxfee`) * @see https://github.com/bitcoin/bitcoin/blob/db88db47278d2e7208c50d16ab10cb355067d071/src/validation.h#L56 */ const MIN_TX_FEE = 1000; /** * Lower bound for transaction fee rate in satoshis per byte. */ const LOWER_FEE_BOUND = 0.5; /** * Upper bound for transaction fee rate in satoshis per byte. */ const UPPER_FEE_BOUND = 500; /** * Number of decimal places for Litecoin. */ const LTC_DECIMAL = 8; /** * Chain identifier for Litecoin. */ const LTCChain = 'LTC'; /** * Base "chain" asset on Litecoin mainnet. * * Based on definition in Thorchain `common`. * @see https://gitlab.com/thorchain/thornode/-/blob/master/common/asset.go#L12-24 */ const AssetLTC = { chain: LTCChain, symbol: 'LTC', ticker: 'LTC', type: xchainUtil.AssetType.NATIVE }; const LTC_MAINNET_EXPLORER = new xchainClient.ExplorerProvider('https://blockchair.com/litecoin/', 'https://blockchair.com/litecoin/address/%%ADDRESS%%', 'https://blockchair.com/litecoin/transaction/%%TX_ID%%'); const LTC_TESTNET_EXPLORER = new xchainClient.ExplorerProvider('https://blockexplorer.one/litecoin/testnet/', 'https://blockexplorer.one/litecoin/testnet/address/%%ADDRESS%%', 'https://blockexplorer.one/litecoin/testnet/blockHash/%%TX_ID%%'); /** * Explorer providers for Litecoin. */ const explorerProviders = { [xchainClient.Network.Testnet]: LTC_TESTNET_EXPLORER, [xchainClient.Network.Stagenet]: LTC_MAINNET_EXPLORER, [xchainClient.Network.Mainnet]: LTC_MAINNET_EXPLORER, }; //====================== // sochain //====================== const testnetSochainProvider = new xchainUtxoProviders.SochainProvider('https://sochain.com/api/v3', process.env.SOCHAIN_API_KEY || '', LTCChain, AssetLTC, 8, xchainUtxoProviders.SochainNetwork.LTCTEST); const mainnetSochainProvider = new xchainUtxoProviders.SochainProvider('https://sochain.com/api/v3', process.env.SOCHAIN_API_KEY || '', LTCChain, AssetLTC, 8, xchainUtxoProviders.SochainNetwork.LTC); /** * Sochain data providers for Litecoin. */ const sochainDataProviders = { [xchainClient.Network.Testnet]: testnetSochainProvider, [xchainClient.Network.Stagenet]: mainnetSochainProvider, [xchainClient.Network.Mainnet]: mainnetSochainProvider, }; //====================== // Blockcypher //====================== const mainnetBlockcypherProvider = new xchainUtxoProviders.BlockcypherProvider('https://api.blockcypher.com/v1', LTCChain, AssetLTC, 8, xchainUtxoProviders.BlockcypherNetwork.LTC, process.env.BLOCKCYPHER_API_KEY || ''); /** * Blockcypher data providers for Litecoin. */ const BlockcypherDataProviders = { [xchainClient.Network.Testnet]: undefined, [xchainClient.Network.Stagenet]: mainnetBlockcypherProvider, [xchainClient.Network.Mainnet]: mainnetBlockcypherProvider, }; /** * Bitgo data providers for Litecoin. */ const mainnetBitgoProvider = new xchainUtxoProviders.BitgoProvider({ baseUrl: 'https://app.bitgo.com', chain: LTCChain, }); const BitgoProviders = { [xchainClient.Network.Testnet]: undefined, [xchainClient.Network.Stagenet]: mainnetBitgoProvider, [xchainClient.Network.Mainnet]: mainnetBitgoProvider, }; /** * Broadcasts a transaction to the blockchain network. * * @see https://developer.bitcoin.org/reference/rpc/sendrawtransaction.html * @param {BroadcastTxParams} params Parameters for broadcasting the transaction. * @returns {Promise<string>} The transaction ID if successful. */ const broadcastTx$1 = ({ txHex, auth, nodeUrl }) => __awaiter(void 0, void 0, void 0, function* () { const uniqueId = new Date().getTime().toString(); // Generates a unique ID for the request const postData = { jsonrpc: '2.0', method: 'sendrawtransaction', params: [txHex], id: uniqueId, }; let response; // Posts the transaction data to the specified node URL, optionally with authentication if (auth) { response = (yield axios__default.default.post(nodeUrl, postData, { auth })).data; } else { response = (yield axios__default.default.post(nodeUrl, postData)).data; } // Throws an error if the response contains an error message if (response.error) { throw new Error(`Failed to broadcast the transaction: ${response.error}`); } // Returns the transaction ID from the response result return response.result; }); /** * Size of an empty transaction in bytes. */ const TX_EMPTY_SIZE = 4 + 1 + 1 + 4; // 10 /** * Base size of a transaction input in bytes. */ const TX_INPUT_BASE = 32 + 4 + 1 + 4; // 41 /** * Size of a transaction input with a pubkey hash in bytes. */ const TX_INPUT_PUBKEYHASH = 107; /** * Base size of a transaction output in bytes. */ const TX_OUTPUT_BASE = 8 + 1; // 9 /** * Size of a transaction output with a pubkey hash in bytes. */ const TX_OUTPUT_PUBKEYHASH = 25; /** * Calculate the size of a transaction input in bytes. * * @param {UTXO} input The UTXO. * @returns {number} The size of the transaction input. */ function inputBytes(input) { var _a; return TX_INPUT_BASE + (((_a = input.witnessUtxo) === null || _a === void 0 ? void 0 : _a.script) ? input.witnessUtxo.script.length : TX_INPUT_PUBKEYHASH); } /** * Get the Litecoin network to be used with bitcoinjs. * * @param {Network} network The network identifier. * @returns {Litecoin.Network} The Litecoin network. */ const ltcNetwork = (network) => { switch (network) { case xchainClient.Network.Mainnet: case xchainClient.Network.Stagenet: return xchainUtxo.toBitcoinJS('litecoin', 'main'); case xchainClient.Network.Testnet: return xchainUtxo.toBitcoinJS('litecoin', 'test'); } }; /** * Validate a Litecoin address. * * @param {Address} address The Litecoin address to validate. * @param {Network} network The network identifier. * @returns {boolean} `true` if the address is valid, `false` otherwise. */ const validateAddress = (address, network) => { try { Litecoin__namespace.address.toOutputScript(address, ltcNetwork(network)); return true; } catch (error) { return false; } }; /** * Broadcast a transaction. * * @param {BroadcastTxParams} params The parameters for broadcasting the transaction. * @returns {Promise<TxHash>} The hash of the broadcasted transaction. */ const broadcastTx = (params) => __awaiter(void 0, void 0, void 0, function* () { return yield broadcastTx$1(params); }); /** * Get the address prefix based on the network. * * @param {Network} network The network identifier. * @returns {string} The address prefix based on the network. **/ const getPrefix = (network) => { switch (network) { case xchainClient.Network.Mainnet: case xchainClient.Network.Stagenet: return 'ltc1'; case xchainClient.Network.Testnet: return 'tltc1'; } }; /** * Default parameters for the Litecoin client. */ const defaultLtcParams = { network: xchainClient.Network.Mainnet, phrase: '', explorerProviders: explorerProviders, dataProviders: [BitgoProviders, BlockcypherDataProviders], rootDerivationPaths: { [xchainClient.Network.Mainnet]: `m/84'/2'/0'/0/`, [xchainClient.Network.Testnet]: `m/84'/1'/0'/0/`, [xchainClient.Network.Stagenet]: `m/84'/2'/0'/0/`, }, feeBounds: { lower: LOWER_FEE_BOUND, upper: UPPER_FEE_BOUND, }, nodeUrls: { [xchainClient.Network.Mainnet]: 'https://litecoin.ninerealms.com', [xchainClient.Network.Stagenet]: 'https://litecoin.ninerealms.com', [xchainClient.Network.Testnet]: 'https://testnet.ltc.thorchain.info', }, }; /** * Custom Litecoin client. */ class Client extends xchainUtxo.Client { /** * Constructs a new `Client` with the provided parameters. * * @param {UtxoClientParams} params The parameters for initializing the client. */ constructor(params = defaultLtcParams) { super(LTCChain, { network: params.network, rootDerivationPaths: params.rootDerivationPaths, phrase: params.phrase, feeBounds: params.feeBounds, explorerProviders: params.explorerProviders, dataProviders: params.dataProviders, }); this.nodeUrls = params.nodeUrls; this.nodeAuth = params.nodeAuth; } /** * Returns information about the asset used by the client. * * @returns {AssetInfo} Information about the asset. */ getAssetInfo() { const assetInfo = { asset: AssetLTC, decimal: LTC_DECIMAL, }; return assetInfo; } /** * Validates the given Litecoin address. * * @param {string} address The Litecoin address to validate. * @returns {boolean} `true` if the address is valid, `false` otherwise. */ validateAddress(address) { return validateAddress(address, this.network); } /** * Builds a Litecoin (LTC) transaction. * * @param {BuildParams} params The transaction build options. * @returns {Transaction} A promise that resolves to the PSBT (Partially Signed Bitcoin Transaction) and UTXOs (Unspent Transaction Outputs). * @deprecated This function will eventually be removed. Use `prepareTx` instead. */ buildTx({ amount, recipient, memo, feeRate, sender, }) { return __awaiter(this, void 0, void 0, function* () { if (!this.validateAddress(recipient)) throw new Error('Invalid address'); const utxos = yield this.scanUTXOs(sender, false); if (utxos.length === 0) throw new Error('No utxos to send'); const feeRateWhole = Number(feeRate.toFixed(0)); const compiledMemo = memo ? this.compileMemo(memo) : null; const targetOutputs = []; //1. add output amount and recipient to targets targetOutputs.push({ address: recipient, value: amount.amount().toNumber(), }); //2. add output memo to targets (optional) if (compiledMemo) { targetOutputs.push({ script: compiledMemo, value: 0 }); } const { inputs, outputs } = accumulative__default.default(utxos, targetOutputs, feeRateWhole); // .inputs and .outputs will be undefined if no solution was found if (!inputs || !outputs) throw new Error('Insufficient Balance for transaction'); const psbt = new Litecoin__namespace.Psbt({ network: ltcNetwork(this.network) }); // Network-specific // psbt add input from accumulative inputs inputs.forEach((utxo) => psbt.addInput({ hash: utxo.hash, index: utxo.index, witnessUtxo: utxo.witnessUtxo, })); // Outputs outputs.forEach((output) => { if (!output.address) { //an empty address means this is the change address output.address = sender; } if (!output.script) { psbt.addOutput(output); } else { //we need to add the compiled memo this way to //avoid dust error tx when accumulating memo output with 0 value if (compiledMemo) { psbt.addOutput({ script: compiledMemo, value: 0 }); } } }); return { psbt, utxos, inputs }; }); } /** * Prepares a Litecoin (LTC) transaction. * * @param {TxParams&Address&FeeRate&boolean} params The transfer options. * @returns {PreparedTx} A promise that resolves to the raw unsigned transaction. */ prepareTx({ sender, memo, amount, recipient, feeRate, }) { return __awaiter(this, void 0, void 0, function* () { const { psbt, utxos, inputs } = yield this.buildTx({ sender, recipient, amount, feeRate, memo, }); return { rawUnsignedTx: psbt.toBase64(), utxos, inputs }; }); } /** * Compile memo. * * @param {string} memo The memo to be compiled. * @returns {Buffer} The compiled memo. */ compileMemo(memo) { const data = Buffer.from(memo, 'utf8'); // converts MEMO to buffer return Litecoin__namespace.script.compile([Litecoin__namespace.opcodes.OP_RETURN, data]); // Compile OP_RETURN script } /** * Calculates the transaction fee based on the provided UTXOs, fee rate, and optional compiled memo. * * @param {UTXO[]} inputs The The UTXOs used as inputs in the transaction. * @param {FeeRate} feeRate The fee rate. * @param {Buffer} data The compiled memo (Optional). * @returns {number} The calculated fee amount. */ getFeeFromUtxos(inputs, feeRate, data = null) { // Calculate transaction size based on inputs and outputs const inputSizeBasedOnInputs = inputs.length > 0 ? inputs.reduce((a, x) => a + inputBytes(x), 0) + inputs.length // +1 byte for each input signature : 0; let sum = TX_EMPTY_SIZE + inputSizeBasedOnInputs + inputs.length + // +1 byte for each input signature TX_OUTPUT_BASE + TX_OUTPUT_PUBKEYHASH + TX_OUTPUT_BASE + TX_OUTPUT_PUBKEYHASH; // Add additional output size if memo is provided if (data) { sum += TX_OUTPUT_BASE + data.length; } // Calculate fee const fee = sum * feeRate; return fee > MIN_TX_FEE ? fee : MIN_TX_FEE; } } class ClientKeystore extends Client { /** * [DEPRECATED] Retrieves the address at the specified index. * * @deprecated Use `getAddressAsync` instead. * @param {number} index The index of the address. * @returns {Address} The address at the specified index. * @throws {Error} Thrown when the index is less than zero. * @throws {Error} Thrown when the phrase has not been set. * @throws {Error} Thrown when the address cannot be defined. */ getAddress(index = 0) { if (index < 0) { throw new Error('index must be greater than zero'); } if (this.phrase) { const ltcNetwork$1 = ltcNetwork(this.network); const ltcKeys = this.getLtcKeys(this.phrase, index); const { address } = Litecoin__namespace.payments.p2wpkh({ pubkey: ltcKeys.publicKey, network: ltcNetwork$1, }); if (!address) { throw new Error('Address not defined'); } return address; } throw new Error('Phrase must be provided'); } /** * Retrieves the current address asynchronously. * * Generates a network-specific key-pair by first converting the buffer to a Wallet-Import-Format (WIF) * The address is then decoded into type P2WPKH and returned. * * @returns {Address} A promise that resolves to the current address * * @throws {"Phrase must be provided"} Thrown if phrase has not been set before. * @throws {"Address not defined"} Thrown if failed creating account from phrase. */ getAddressAsync(walletIndex = 0) { return __awaiter(this, void 0, void 0, function* () { return this.getAddress(walletIndex); }); } /** * Transfers Litecoin (LTC) from one address to another. * * @param {TxParams & { feeRate?: FeeRate }} params The transfer options. * @returns {Promise<TxHash>} A promise that resolves to the transaction hash. */ transfer(params) { return __awaiter(this, void 0, void 0, function* () { // set the default fee rate to `fast` const feeRate = params.feeRate || (yield this.getFeeRates())[xchainClient.FeeOption.Fast]; xchainClient.checkFeeBounds(this.feeBounds, feeRate); const fromAddressIndex = params.walletIndex || 0; const { rawUnsignedTx } = yield this.prepareTx(Object.assign(Object.assign({}, params), { feeRate, sender: yield this.getAddressAsync(fromAddressIndex) })); const psbt = Litecoin__namespace.Psbt.fromBase64(rawUnsignedTx); const ltcKeys = this.getLtcKeys(this.phrase, fromAddressIndex); psbt.signAllInputs(ltcKeys); // Sign all inputs psbt.finalizeAllInputs(); // Finalise inputs const txHex = psbt.extractTransaction().toHex(); // TX extracted and formatted to hex return yield broadcastTx({ txHex, nodeUrl: this.nodeUrls[this.network], auth: this.nodeAuth, }); }); } /** * @private * [PRIVATE] Retrieves the private key. * * Private function to get keyPair from the this.phrase * * @param {string} phrase The phrase used to generate the private key. * @returns {ECPairInterface} The privkey generated from the given phrase * * @throws {"Could not get private key from phrase"} Throws an error if failed creating LTC keys from the given phrase * */ getLtcKeys(phrase, index = 0) { const ltcNetwork$1 = ltcNetwork(this.network); const seed = xchainCrypto.getSeed(phrase); const master = Litecoin__namespace.bip32.fromSeed(seed, ltcNetwork$1).derivePath(this.getFullDerivationPath(index)); if (!master.privateKey) { throw new Error('Could not get private key from phrase'); } return Litecoin__namespace.ECPair.fromPrivateKey(master.privateKey, { network: ltcNetwork$1 }); } } /** * Custom Ledger Litecoin client */ class ClientLedger extends Client { // Constructor // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(params) { super(params); this.transport = params.transport; } // Get the Ledger BTC application instance getApp() { return __awaiter(this, void 0, void 0, function* () { if (this.app) { return this.app; } this.app = new AppBtc__default.default({ transport: this.transport, currency: 'litecoin' }); return this.app; }); } // Get the current address synchronously getAddress() { throw Error('Sync method not supported for Ledger'); } // Get the current address asynchronously getAddressAsync(index = 0, verify = false) { return __awaiter(this, void 0, void 0, function* () { const app = yield this.getApp(); const result = yield app.getWalletPublicKey(this.getFullDerivationPath(index), { format: 'bech32', verify, }); return result.bitcoinAddress; }); } // Transfer LTC from Ledger transfer(params) { return __awaiter(this, void 0, void 0, function* () { const app = yield this.getApp(); const fromAddressIndex = (params === null || params === void 0 ? void 0 : params.walletIndex) || 0; // Get fee rate const feeRate = params.feeRate || (yield this.getFeeRates())[xchainClient.FeeOption.Fast]; xchainClient.checkFeeBounds(this.feeBounds, feeRate); // Get sender address const sender = yield this.getAddressAsync(fromAddressIndex); // Prepare transaction const { rawUnsignedTx, inputs } = yield this.prepareTx(Object.assign(Object.assign({}, params), { sender, feeRate })); const psbt = Litecoin__namespace.Psbt.fromBase64(rawUnsignedTx); // Prepare Ledger inputs const ledgerInputs = inputs.map(({ txHex, hash, index }) => { if (!txHex) { throw Error(`Missing 'txHex' for UTXO (txHash ${hash})`); } const utxoTx = Litecoin__namespace.Transaction.fromHex(txHex); const splittedTx = app.splitTransaction(txHex, utxoTx.hasWitnesses()); return [splittedTx, index, null, null]; }); // Prepare associated keysets const associatedKeysets = ledgerInputs.map(() => this.getFullDerivationPath(fromAddressIndex)); // Serialize unsigned transaction const unsignedHex = psbt.data.globalMap.unsignedTx.toBuffer().toString('hex'); const newTx = app.splitTransaction(unsignedHex, true); const outputScriptHex = app.serializeTransactionOutputs(newTx).toString('hex'); // Create payment transaction const txHex = yield app.createPaymentTransaction({ inputs: ledgerInputs, associatedKeysets, outputScriptHex, segwit: true, useTrustedInputForSegwit: true, additionals: ['bech32'], }); // Broadcast transaction const txHash = yield this.broadcastTx(txHex); // Throw error if no transaction hash is received if (!txHash) { throw Error('No Tx hash'); } return txHash; }); } } exports.AssetLTC = AssetLTC; exports.BitgoProviders = BitgoProviders; exports.BlockcypherDataProviders = BlockcypherDataProviders; exports.Client = ClientKeystore; exports.ClientKeystore = ClientKeystore; exports.ClientLedger = ClientLedger; exports.LOWER_FEE_BOUND = LOWER_FEE_BOUND; exports.LTCChain = LTCChain; exports.LTC_DECIMAL = LTC_DECIMAL; exports.MIN_TX_FEE = MIN_TX_FEE; exports.UPPER_FEE_BOUND = UPPER_FEE_BOUND; exports.broadcastTx = broadcastTx; exports.defaultLtcParams = defaultLtcParams; exports.explorerProviders = explorerProviders; exports.getPrefix = getPrefix; exports.sochainDataProviders = sochainDataProviders; exports.validateAddress = validateAddress;