UNPKG

@tatumio/utxo-wallet-provider

Version:

UTXO provider with local wallet operations

550 lines 23.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UtxoWalletProvider = void 0; const tatum_1 = require("@tatumio/tatum"); const bignumber_js_1 = __importDefault(require("bignumber.js")); const bip32_1 = require("bip32"); const bip39_1 = require("bip39"); const bitcoinjs_lib_1 = require("bitcoinjs-lib"); const bitcore_lib_1 = require("bitcore-lib"); // no types for this guy sadly // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const bitcore_lib_doge_1 = require("bitcore-lib-doge"); // same story // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const bitcore_lib_ltc_1 = require("bitcore-lib-ltc"); const ecpair_1 = __importDefault(require("ecpair")); const ecc = __importStar(require("@bitcoinerlab/secp256k1")); const utils_1 = require("./utils"); const ECPair = (0, ecpair_1.default)(ecc); const bip32 = (0, bip32_1.BIP32Factory)(ecc); class UtxoWalletProvider extends tatum_1.TatumSdkWalletProvider { constructor(tatumSdkContainer, config) { super(tatumSdkContainer); this.config = config; this.supportedNetworks = [ tatum_1.Network.BITCOIN, tatum_1.Network.DOGECOIN, tatum_1.Network.LITECOIN, tatum_1.Network.BITCOIN_TESTNET, tatum_1.Network.DOGECOIN_TESTNET, tatum_1.Network.LITECOIN_TESTNET, ]; this.sdkConfig = this.tatumSdkContainer.getConfig(); this.connector = this.tatumSdkContainer.get(tatum_1.TatumConnector); this.utxoRpc = this.tatumSdkContainer.getRpc(); } /** * Generates a mnemonic seed phrase. * @returns {string} A mnemonic seed phrase. */ generateMnemonic() { return (0, bip39_1.generateMnemonic)(256); } /** * Generates an extended public key (xpub) based on a mnemonic and a derivation path. * If no mnemonic is provided, it is generated. * If no derivation path is provided, default is used. * @param {string} [mnemonic] - The mnemonic seed phrase. * @param {string} [path] - The derivation path. * @returns {XpubWithMnemonic} An object containing xpub, mnemonic, and derivation path. */ async generateXpub(mnemonic, path) { mnemonic = mnemonic || this.generateMnemonic(); path = path || (0, utils_1.getDefaultDerivationPath)(this.sdkConfig.network); const xpub = bip32 .fromSeed(await (0, bip39_1.mnemonicToSeed)(mnemonic), (0, utils_1.getNetworkConfig)(this.sdkConfig.network)) .derivePath(path) .neutered() .toBase58(); return { xpub: xpub, mnemonic, derivationPath: path, }; } /** * Generates a private key based on a mnemonic, index, and a derivation path. * If no derivation path is provided, default is used. * @param {string} mnemonic - The mnemonic seed phrase. * @param {number} index - The index to derive the private key from. * @param {string} [path] - The derivation path. * @returns {string} A private key in string format. */ async generatePrivateKeyFromMnemonic(mnemonic, index, path) { return bip32 .fromSeed(await (0, bip39_1.mnemonicToSeed)(mnemonic), (0, utils_1.getNetworkConfig)(this.sdkConfig.network)) .derivePath(path || (0, utils_1.getDefaultDerivationPath)(this.sdkConfig.network)) .derive(index) .toWIF(); } /** * Generates an address based on a mnemonic, index, and a derivation path. * If no derivation path is provided, default is used. * @param {string} mnemonic - The mnemonic seed phrase. * @param {number} index - The index to derive the address from. * @param {string} [path] - The derivation path. * @returns {string} An address in string format. */ async generateAddressFromMnemonic(mnemonic, index, path) { const pubkey = bip32 .fromSeed(await (0, bip39_1.mnemonicToSeed)(mnemonic), (0, utils_1.getNetworkConfig)(this.sdkConfig.network)) .derivePath(path || (0, utils_1.getDefaultDerivationPath)(this.sdkConfig.network)) .derive(index).publicKey; return bitcoinjs_lib_1.payments.p2pkh({ pubkey, network: (0, utils_1.getNetworkConfig)(this.sdkConfig.network) }).address; } /** * Generates an address from an extended public key (xpub) and an index. * @param {string} xpub - The extended public key. * @param {number} index - The index to derive the address from. * @returns {string} An address in string format. */ generateAddressFromXpub(xpub, index) { const pubkey = bip32 .fromBase58(xpub, (0, utils_1.getNetworkConfig)(this.sdkConfig.network)) .derivePath(String(index)).publicKey; return bitcoinjs_lib_1.payments.p2pkh({ pubkey, network: (0, utils_1.getNetworkConfig)(this.sdkConfig.network) }).address; } /** * Generates an address from a given private key. * @param {string} privateKey - The private key in string format. * @returns {string} An UTXO address in string format. */ generateAddressFromPrivateKey(privateKey) { const pubkey = ECPair.fromWIF(privateKey, (0, utils_1.getNetworkConfig)(this.sdkConfig.network)).publicKey; return bitcoinjs_lib_1.payments.p2pkh({ pubkey, network: (0, utils_1.getNetworkConfig)(this.sdkConfig.network) }).address; } /** * Generates an UTXO-compatible wallet, which includes an address, private key, and a mnemonic. * @returns {UtxoWallet} An object containing address, private key, and mnemonic. */ async getWallet() { const mnemonic = this.generateMnemonic(); const privateKey = await this.generatePrivateKeyFromMnemonic(mnemonic, 0); const address = this.generateAddressFromPrivateKey(privateKey); return { address, privateKey, mnemonic }; } /** * Signs and broadcasts an UTXO transaction payload. * @param {UtxoTxPayload} payload - The UTXO transaction payload, which includes private keys and transaction details. * @returns {Promise<string>} A promise that resolves to the transaction hash. */ async signAndBroadcast(payload) { let rawTransaction; switch (this.sdkConfig.network) { case tatum_1.Network.BITCOIN: case tatum_1.Network.BITCOIN_TESTNET: rawTransaction = await this.getRawTransaction(payload); break; case tatum_1.Network.LITECOIN: case tatum_1.Network.LITECOIN_TESTNET: rawTransaction = await this.getRawTransactionLtc(payload); break; case tatum_1.Network.DOGECOIN: case tatum_1.Network.DOGECOIN_TESTNET: rawTransaction = await this.getRawTransactionDoge(payload); break; default: throw new Error('Unsupported network'); } const response = await this.utxoRpc.sendRawTransaction(rawTransaction); if (!response?.result) { throw new Error(JSON.stringify(response.error)); } return response.result; } async getRawTransaction(payload) { const tx = new bitcore_lib_1.Transaction(); let privateKeysToSign = []; this.setChangeAddress(payload, tx); this.setFee(payload, tx); payload.to.forEach((to) => { this.setToAddress(tx, to); }); if ('fromAddress' in payload) { privateKeysToSign = await this.privateKeysFromAddress(tx, payload); } else if ('fromUTXO' in payload) { privateKeysToSign = await this.privateKeysFromUTXO(tx, payload); } new Set(privateKeysToSign).forEach((key) => { tx.sign(new bitcore_lib_1.PrivateKey(key)); }); return tx.serialize(this.config?.skipAllChecks); } setToAddress(tx, to) { try { tx.to(to.address, (0, utils_1.toSatoshis)(to.value)); } catch (e) { throw new Error(`'${to.address}' is not a valid address`); } } setChangeAddress(payload, tx) { if (payload.changeAddress) { try { tx.change(payload.changeAddress); } catch (e) { throw new Error(`'${payload.changeAddress}' is not a valid change address`); } } } setFee(payload, tx) { if (payload.fee) { try { tx.fee((0, utils_1.toSatoshis)(payload.fee)); } catch (e) { throw new Error(`'${payload.fee}' is not a valid fee value`); } } } async getRawTransactionLtc(payload) { const { to, fee, changeAddress } = payload; this.validateUtxoBody(payload); const transaction = new bitcore_lib_ltc_1.Transaction(); const hasFeeAndChange = !!(changeAddress && fee); let totalOutputs = hasFeeAndChange ? (0, utils_1.toSatoshis)(fee) : 0; for (const item of to) { const amount = (0, utils_1.toSatoshis)(item.value); totalOutputs += amount; this.setToAddress(transaction, item); } let totalInputs = 0; const privateKeysToSign = []; if ('fromUTXO' in payload) { const filteredUtxos = await this.getUtxoBatch(payload); let validUtxoFound = false; for (let i = 0; i < filteredUtxos.length; i++) { const utxo = filteredUtxos[i]; if (utxo === null || !utxo.address) continue; validUtxoFound = true; const utxoItem = payload.fromUTXO[i]; transaction.from([ bitcore_lib_ltc_1.Transaction.UnspentOutput.fromObject({ txId: utxoItem.txHash, outputIndex: utxoItem.index, script: bitcore_lib_1.Script.fromAddress(utxo.address).toString(), satoshis: utxo.value, }), ]); privateKeysToSign.push(utxoItem.privateKey); } if (!validUtxoFound) { throw new Error('No valid UTXOs found. They are probably already spent.'); } } else if ('fromAddress' in payload) { for (const item of payload.fromAddress) { if (totalInputs >= totalOutputs) { break; } const utxos = await this.getUtxos(item, totalOutputs, totalInputs); for (const utxo of utxos) { const satoshis = (0, utils_1.toSatoshis)(utxo.value); totalInputs += satoshis; transaction.from([ bitcore_lib_1.Transaction.UnspentOutput.fromObject({ txId: utxo.txHash, outputIndex: utxo.index, script: bitcore_lib_ltc_1.Script.fromAddress(utxo.address).toString(), satoshis, }), ]); privateKeysToSign.push(item.privateKey); } } } if (hasFeeAndChange) { this.setChangeAddress(payload, transaction); this.setFee(payload, transaction); } const shouldHaveChangeOutput = totalInputs > totalOutputs && hasFeeAndChange; this.checkDustAmountInChange(transaction, payload, shouldHaveChangeOutput); for (const pk of privateKeysToSign) { transaction.sign(bitcore_lib_ltc_1.PrivateKey.fromWIF(pk)); } return transaction.serialize(); } async getRawTransactionDoge(payload) { const { to, fee, changeAddress } = payload; this.validateUtxoBody(payload); const transaction = new bitcore_lib_doge_1.Transaction(); const hasFeeAndChange = !!(changeAddress && fee); let totalOutputs = hasFeeAndChange ? (0, utils_1.toSatoshis)(fee) : 0; for (const item of to) { const amount = (0, utils_1.toSatoshis)(item.value); totalOutputs += amount; this.setToAddress(transaction, item); } let totalInputs = 0; const privateKeysToSign = []; if ('fromUTXO' in payload) { const filteredUtxos = await this.getDogeUtxoBatch(payload); let validUtxoFound = false; for (let i = 0; i < filteredUtxos.length; i++) { const utxo = filteredUtxos[i]; const address = utxo?.scriptPubKey?.addresses?.[0]; if (utxo === null || utxo.scriptPubKey === null || !address) continue; validUtxoFound = true; const utxoItem = payload.fromUTXO[i]; transaction.from([ bitcore_lib_doge_1.Transaction.UnspentOutput.fromObject({ txId: utxoItem.txHash, outputIndex: utxoItem.index, script: bitcore_lib_1.Script.fromAddress(address).toString(), satoshis: utxo.value, }), ]); privateKeysToSign.push(utxoItem.privateKey); } if (!validUtxoFound) { throw new Error('No valid UTXOs found. They are probably already spent.'); } } else if ('fromAddress' in payload) { for (const item of payload.fromAddress) { if (totalInputs >= totalOutputs) { break; } const utxos = await this.getUtxos(item, totalOutputs, totalInputs); for (const utxo of utxos) { const satoshis = (0, utils_1.toSatoshis)(utxo.value); totalInputs += satoshis; transaction.from([ bitcore_lib_1.Transaction.UnspentOutput.fromObject({ txId: utxo.txHash, outputIndex: utxo.index, script: bitcore_lib_doge_1.Script.fromAddress(utxo.address).toString(), satoshis, }), ]); privateKeysToSign.push(item.privateKey); } } } if (hasFeeAndChange) { this.setChangeAddress(payload, transaction); this.setFee(payload, transaction); } const shouldHaveChangeOutput = totalInputs > totalOutputs && hasFeeAndChange; this.checkDustAmountInChange(transaction, payload, shouldHaveChangeOutput); for (const pk of privateKeysToSign) { transaction.sign(bitcore_lib_doge_1.PrivateKey.fromWIF(pk)); } return transaction.serialize(); } /** * In case if change amount is dust, its amount will be appended to fee. * We need to check it to prevent implicit amounts change */ checkDustAmountInChange(transaction, body, shouldHaveChangeOutput) { const outputsCount = transaction.outputs.length; const expectedOutputsCount = body.to.length + (shouldHaveChangeOutput ? 1 : 0); if (outputsCount !== expectedOutputsCount) { throw new Error('Transaction would result in dust amount being appended to the fee.'); } } validateUtxoBody(body) { if (!('fromUTXO' in body) && !('fromAddress' in body)) { throw new Error('Either fromUTXO or fromAddress must be provided'); } if (('fromUTXO' in body && body.fromUTXO.length === 0) || ('fromAddress' in body && body.fromAddress.length === 0)) { throw new Error('Either fromUTXO or fromAddress must be provided'); } if ((body.fee && !body.changeAddress) || (!body.fee && body.changeAddress)) { throw new Error('Either fee and changeAddress must be provided or none of them'); } } async privateKeysFromAddress(transaction, body) { if (body.fromAddress.length === 0 && !this.config?.skipAllChecks) { throw new Error('No fromAddress provided'); } let totalInputs = 0; let totalOutputs = body.fee ? (0, utils_1.toSatoshis)(body.fee) : 0; for (const item of transaction.outputs) { totalOutputs += item.satoshis; } const privateKeysToSign = []; for (const item of body.fromAddress) { if (totalInputs >= totalOutputs) { break; } const utxos = await this.getUtxos(item, totalOutputs, totalInputs); for (const utxo of utxos) { totalInputs += utxo.value; transaction.from([ bitcore_lib_1.Transaction.UnspentOutput.fromObject({ txId: utxo.txHash, outputIndex: utxo.index, script: bitcore_lib_1.Script.fromAddress(utxo.address).toString(), satoshis: (0, utils_1.toSatoshis)(utxo.value), }), ]); privateKeysToSign.push(item.privateKey); } } return privateKeysToSign; } async getUtxos(item, totalOutputs, totalInputs) { return this.connector.get({ path: `data/utxos?chain=${this.sdkConfig.network}&address=${item.address}&totalValue=${(0, utils_1.fromSatoshis)(totalOutputs - totalInputs)}`, }); } async privateKeysFromUTXO(transaction, body) { if (body.fromUTXO.length === 0 && !this.config?.skipAllChecks) { throw new Error('No fromUTXO provided'); } const privateKeysToSign = []; const utxos = []; const filteredUtxos = await this.getUtxoBatch(body); let validUtxoFound = false; for (let i = 0; i < filteredUtxos.length; i++) { const utxo = filteredUtxos[i]; if (utxo === null || !utxo.address) continue; validUtxoFound = true; const utxoItem = body.fromUTXO[i]; utxos.push(utxo); transaction.from([ bitcore_lib_1.Transaction.UnspentOutput.fromObject({ txId: utxo.hash, outputIndex: utxo.index, script: bitcore_lib_1.Script.fromAddress(utxo.address).toString(), satoshis: utxo.value, }), ]); privateKeysToSign.push(utxoItem.privateKey); } if (!validUtxoFound) { throw new Error('No valid UTXOs found. They are probably already spent.'); } if (!this.config?.skipAllChecks) { await this.validateBalanceFromUTXO(body, utxos); } return privateKeysToSign; } async getDogeUtxoBatch(body) { const fromUTXOs = body.fromUTXO.map((item) => ({ txHash: item.txHash, index: item.index })); const utxos = []; for (const utxoItem of fromUTXOs) { const utxo = await this.getUtxoSilent(utxoItem.txHash, utxoItem.index); if (utxo === null || !utxo.scriptPubKey?.addresses?.[0]) { utxos.push(null); } else { utxos.push(utxo); } } return utxos; } async getUtxoBatch(body) { const fromUTXOs = body.fromUTXO.map((item) => ({ txHash: item.txHash, index: item.index })); const utxos = []; for (const utxoItem of fromUTXOs) { const utxo = await this.getUtxoSilent(utxoItem.txHash, utxoItem.index); if (utxo === null || !utxo.address) { utxos.push(null); } else { utxos.push(utxo); } } return utxos; } async getUtxoSilent(hash, index) { try { return await this.connector.get({ path: `${this.getChainForUtxo()}/utxo/${hash}/${index}`, }); } catch (e) { return null; } } async validateBalanceFromUTXO(body, utxos) { const totalBalance = utxos.reduce((sum, u) => sum.plus(new bignumber_js_1.default(u.value ?? 0)), new bignumber_js_1.default(0)); const totalFee = body.fee ? new bignumber_js_1.default(body.fee) : await this.getEstimateFeeFromUtxo(body, utxos); const totalValue = body.to.reduce((sum, t) => sum.plus(new bignumber_js_1.default(t.value)), new bignumber_js_1.default(0)); if (totalBalance.isLessThan(totalValue.plus(totalFee))) { throw new Error(`Insufficient balance to send transaction. TotalBalance: ${totalBalance}, TotalValue: ${totalValue}, TotalFee: ${totalFee}`); } } async getEstimateFeeFromUtxo(body, utxos) { const fromUTXO = utxos.map((utxo) => ({ txHash: utxo.hash, index: utxo.index })); const fee = await this.connector.post({ path: `blockchain/estimate`, body: { chain: this.getChainForFee(), type: 'TRANSFER', fromUTXO, to: body.to, }, }); return new bignumber_js_1.default(fee.slow ?? 0); } getChainForFee() { switch (this.sdkConfig.network) { case tatum_1.Network.BITCOIN: case tatum_1.Network.BITCOIN_TESTNET: return 'BTC'; case tatum_1.Network.LITECOIN: case tatum_1.Network.LITECOIN_TESTNET: return 'LTC'; case tatum_1.Network.DOGECOIN: case tatum_1.Network.DOGECOIN_TESTNET: return 'DOGE'; default: throw new Error('Unsupported network'); } } getChainForUtxo() { switch (this.sdkConfig.network) { case tatum_1.Network.BITCOIN: case tatum_1.Network.BITCOIN_TESTNET: return 'bitcoin'; case tatum_1.Network.LITECOIN: case tatum_1.Network.LITECOIN_TESTNET: return 'litecoin'; case tatum_1.Network.DOGECOIN: case tatum_1.Network.DOGECOIN_TESTNET: return 'dogecoin'; default: throw new Error('Unsupported network'); } } } exports.UtxoWalletProvider = UtxoWalletProvider; //# sourceMappingURL=extension.js.map