@tatumio/utxo-wallet-provider
Version:
UTXO provider with local wallet operations
550 lines • 23.8 kB
JavaScript
"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