@xchainjs/xchain-doge
Version:
Custom Doge client and utilities used by XChain clients
601 lines (587 loc) • 26.3 kB
JavaScript
import { ExplorerProvider, Network, FeeOption, checkFeeBounds } from '@xchainjs/xchain-client';
import { getSeed } from '@xchainjs/xchain-crypto';
import * as Dogecoin from 'bitcoinjs-lib';
import { toBitcoinJS, Client as Client$1 } from '@xchainjs/xchain-utxo';
import accumulative from 'coinselect/accumulative';
import { AssetType } from '@xchainjs/xchain-util';
import { SochainProvider, SochainNetwork, BlockcypherProvider, BlockcypherNetwork, BitgoProvider } from '@xchainjs/xchain-utxo-providers';
import AppBtc from '@ledgerhq/hw-app-btc';
/******************************************************************************
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 for Dogecoin transactions.
* Defined as 100000 satoshi/kB.
* @see https://github.com/dogecoin/dogecoin/blob/master/src/validation.h#L58
*/
const MIN_TX_FEE = 100000;
/**
* Decimal places for Dogecoin.
*/
const DOGE_DECIMAL = 8;
/**
* Lower fee bound for Dogecoin transactions.
* Referenced from Dogecoin fee recommendation documentation.
* @see https://github.com/dogecoin/dogecoin/blob/master/doc/fee-recommendation.md
*/
const LOWER_FEE_BOUND = 100;
/**
* Upper fee bound for Dogecoin transactions.
* Referenced from Dogecoin fee recommendation documentation.
* @see https://github.com/dogecoin/dogecoin/blob/master/doc/fee-recommendation.md
*/
const UPPER_FEE_BOUND = 20000000;
/**
* Chain identifier for Dogecoin.
*/
const DOGEChain = 'DOGE';
/**
* Base asset object for Dogecoin.
* Represents the Dogecoin asset in various contexts.
*/
const AssetDOGE = { chain: DOGEChain, symbol: 'DOGE', ticker: 'DOGE', type: AssetType.NATIVE };
/**
* Explorer provider for Dogecoin mainnet and testnet.
* Provides URLs for exploring Dogecoin transactions and addresses.
*/
const DOGE_MAINNET_EXPLORER = new ExplorerProvider('https://blockchair.com/dogecoin', 'https://blockchair.com/dogecoin/address/%%ADDRESS%%', 'https://blockchair.com/dogecoin/transaction/%%TX_ID%%');
const DOGE_TESTNET_EXPLORER = new ExplorerProvider('https://blockexplorer.one/dogecoin/testnet', 'https://blockexplorer.one/dogecoin/testnet/address/%%ADDRESS%%', 'https://blockexplorer.one/dogecoin/testnet/tx/%%TX_ID%%');
const blockstreamExplorerProviders = {
[Network.Testnet]: DOGE_TESTNET_EXPLORER,
[Network.Stagenet]: DOGE_MAINNET_EXPLORER,
[Network.Mainnet]: DOGE_MAINNET_EXPLORER,
};
/**
* Sochain data providers for Dogecoin mainnet and testnet.
* Provides API access to Sochain for Dogecoin.
*/
const testnetSochainProvider = new SochainProvider('https://sochain.com/api/v3', process.env.SOCHAIN_API_KEY || '', DOGEChain, AssetDOGE, 8, SochainNetwork.DOGETEST);
const mainnetSochainProvider = new SochainProvider('https://sochain.com/api/v3', process.env.SOCHAIN_API_KEY || '', DOGEChain, AssetDOGE, 8, SochainNetwork.DOGE);
const sochainDataProviders = {
[Network.Testnet]: testnetSochainProvider,
[Network.Stagenet]: mainnetSochainProvider,
[Network.Mainnet]: mainnetSochainProvider,
};
/**
* Blockcypher data providers for Dogecoin mainnet and stagenet.
* Provides API access to Blockcypher for Dogecoin.
*/
const mainnetBlockcypherProvider = new BlockcypherProvider('https://api.blockcypher.com/v1', DOGEChain, AssetDOGE, 8, BlockcypherNetwork.DOGE, process.env.BLOCKCYPHER_API_KEY || '');
const blockcypherDataProviders = {
[Network.Testnet]: undefined,
[Network.Stagenet]: mainnetBlockcypherProvider,
[Network.Mainnet]: mainnetBlockcypherProvider,
};
/**
* Bitgo data providers for Dogecoin mainnet and stagenet.
* Provides API access to Bitgo for Dogecoin.
*/
const mainnetBitgoProvider = new BitgoProvider({
baseUrl: 'https://app.bitgo.com',
chain: DOGEChain,
});
const BitgoProviders = {
[Network.Testnet]: undefined,
[Network.Stagenet]: mainnetBitgoProvider,
[Network.Mainnet]: mainnetBitgoProvider,
};
/**
* Import statements for required modules and types.
*/
/**
* Constant values representing transaction sizes and lengths.
*/
const TX_EMPTY_SIZE = 4 + 1 + 1 + 4; // 10
const TX_INPUT_BASE = 32 + 4 + 1 + 4; // 41
const TX_INPUT_PUBKEYHASH = 107;
const TX_OUTPUT_BASE = 8 + 1; // 9
const TX_OUTPUT_PUBKEYHASH = 25;
/**
* Calculate the number of bytes required for an input.
*
* @returns {number} The number of bytes required for an input.
*/
function inputBytes() {
return TX_INPUT_BASE + TX_INPUT_PUBKEYHASH;
}
/**
* Get the Dogecoin network configuration to be used with bitcoinjs.
*
* @param {Network} network - The network type.
* @returns {Dogecoin.networks.Network} The Dogecoin network configuration.
*/
const dogeNetwork = (network) => {
switch (network) {
case Network.Mainnet:
return toBitcoinJS('dogecoin', 'main');
case Network.Stagenet:
return toBitcoinJS('dogecoin', 'main');
case Network.Testnet: {
return toBitcoinJS('dogecoin', 'test');
}
}
};
/**
* Validate a Dogecoin address.
*
* @param {Address} address - The Dogecoin address to validate.
* @param {Network} network - The network type.
* @returns {boolean} `true` if the address is valid, `false` otherwise.
*/
const validateAddress = (address, network) => {
try {
Dogecoin.address.toOutputScript(address, dogeNetwork(network));
return true;
}
catch (error) {
return false;
}
};
/**
* Get the address prefix based on the network.
*
* @param {Network} network - The network type.
* @returns {string} The address prefix based on the network.
*/
const getPrefix = (network) => {
switch (network) {
case Network.Mainnet:
case Network.Stagenet:
return '';
case Network.Testnet:
return 'n';
}
};
/**
* Default parameters for Dogecoin UTXO client.
* Contains default values for network, phrase, explorer providers, data providers, root derivation paths, and fee bounds.
*/
const defaultDogeParams = {
network: Network.Mainnet,
phrase: '',
explorerProviders: blockstreamExplorerProviders,
dataProviders: [BitgoProviders, blockcypherDataProviders],
rootDerivationPaths: {
[Network.Mainnet]: `m/44'/3'/0'/0/`,
[Network.Stagenet]: `m/44'/3'/0'/0/`,
[Network.Testnet]: `m/44'/1'/0'/0/`, // Default root derivation path for Testnet
},
feeBounds: {
lower: LOWER_FEE_BOUND,
upper: UPPER_FEE_BOUND, // Default upper fee bound
},
};
/**
* Custom Dogecoin client extending UTXOClient.
* Implements methods for Dogecoin-specific functionality.
*/
class Client extends Client$1 {
/**
* Constructor for initializing the Dogecoin client.
* Initializes the client with the provided parameters.
*
* @param {DogecoinClientParams} params Parameters for initializing the Dogecoin client.
*/
constructor(params = defaultDogeParams) {
super(DOGEChain, {
// Call the superclass constructor with DOGEChain identifier and provided parameters
network: params.network,
rootDerivationPaths: params.rootDerivationPaths,
phrase: params.phrase,
feeBounds: params.feeBounds,
explorerProviders: params.explorerProviders,
dataProviders: params.dataProviders,
});
/**
* Builds a Dogecoin transaction (PSBT).
*
* Builds a Partially Signed Bitcoin Transaction (PSBT) with the specified parameters.
* @param {BuildParams} params The transaction build options including sender, recipient, amount, memo, and fee rate.
* @returns {Transaction} A promise that resolves to the built PSBT and the unspent transaction outputs (UTXOs) used in the transaction.
* @deprecated This method is deprecated. Use the `transfer` method instead.
*/
this.buildTx = ({ amount, recipient, memo, feeRate, sender, }) => __awaiter(this, void 0, void 0, function* () {
// Validate the recipient address
if (!this.validateAddress(recipient))
throw new Error('Invalid address');
// Scan unspent transaction outputs (UTXOs) for the sender's address
const utxos = yield this.scanUTXOs(sender, false);
// Throw an error if no UTXOs are found
if (utxos.length === 0)
throw new Error('No UTXOs to send');
// Round the fee rate to the nearest whole number
const feeRateWhole = Number(feeRate.toFixed(0));
// Compile the memo if provided
const compiledMemo = memo ? this.compileMemo(memo) : null;
const targetOutputs = [];
//1. Add output for the recipient
targetOutputs.push({
address: recipient,
value: amount.amount().toNumber(),
});
//2. Add output for the memo (if provided)
if (compiledMemo) {
targetOutputs.push({ script: compiledMemo, value: 0 });
}
// Calculate the inputs and outputs for the transaction
const { inputs, outputs } = accumulative(utxos, targetOutputs, feeRateWhole);
// Throw an error if no solution was found for inputs and outputs
if (!inputs || !outputs)
throw new Error('Balance insufficient for transaction');
// Create a new PSBT for building the transaction
const psbt = new Dogecoin.Psbt({ network: dogeNetwork(this.network) });
// Set the maximum fee rate for the PSBT
psbt.setMaximumFeeRate(7500000);
// Add inputs to the PSBT
for (const utxo of inputs) {
psbt.addInput({
hash: utxo.hash,
index: utxo.index,
nonWitnessUtxo: Buffer.from(utxo.txHex, 'hex'),
});
}
// 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 };
});
}
/**
* Get Dogecoin asset information.
*
* @returns {AssetInfo} Dogecoin asset information.
*/
getAssetInfo() {
const assetInfo = {
asset: AssetDOGE,
decimal: DOGE_DECIMAL,
};
return assetInfo;
}
/**
* Validate the given address.
*
* @param {Address} address The Dogecoin address to validate.
* @returns {boolean} `true` if the address is valid, otherwise `false`.
*/
validateAddress(address) {
return validateAddress(address, this.network);
}
/**
* Asynchronously creates transaction information for ledger sign.
*
* Builds a transaction (PSBT) and prepares necessary information for ledger signing.
*
* @param {LedgerTxInfoParams} params The parameters for creating transaction information.
* @returns {LedgerTxInfo} A promise that resolves to the transaction information used for ledger sign.
*/
createTxInfo(params) {
return __awaiter(this, void 0, void 0, function* () {
// Build the transaction (PSBT) and obtain the unspent transaction outputs (UTXOs)
const { psbt, utxos } = yield this.buildTx(params);
// Construct the ledger transaction information object
const ledgerTxInfo = {
utxos,
newTxHex: psbt.data.globalMap.unsignedTx.toBuffer().toString('hex'), // Convert unsigned transaction to hexadecimal string
};
return ledgerTxInfo;
});
}
/**
* Asynchronously prepares a transaction for transfer.
*
* Builds a transaction (PSBT) with the specified transfer options.
* @param {TxParams & { sender: Address; feeRate: FeeRate; spendPendingUTXO?: boolean }} params The transfer options including sender address, fee rate, and optional flag for spending pending UTXOs.
* @returns {Promise<PreparedTx>} A promise that resolves to the raw unsigned transaction (PSBT).
*/
prepareTx({ sender, memo, amount, recipient, feeRate, }) {
return __awaiter(this, void 0, void 0, function* () {
// Build the transaction (PSBT) with the specified transfer options
const { psbt, utxos, inputs } = yield this.buildTx({
sender,
recipient,
amount,
feeRate,
memo,
});
// Return the raw unsigned transaction (PSBT)
return { rawUnsignedTx: psbt.toBase64(), utxos, inputs };
});
}
/**
* Compiles the memo into a buffer for inclusion in a Dogecoin transaction.
*
* @param {string} memo The memo to be compiled.
* @returns {Buffer} The compiled memo as a buffer.
*/
compileMemo(memo) {
// Convert the memo to a buffer
const data = Buffer.from(memo, 'utf8');
// Compile the OP_RETURN script with the memo data
return Dogecoin.script.compile([Dogecoin.opcodes.OP_RETURN, data]);
}
/**
* Calculates the transaction fee based on the provided UTXOs, fee rate, and optional data.
*
* @param {UTXO[]} inputs The unspent transaction outputs (UTXOs) used as inputs.
* @param {FeeRate} feeRate The fee rate for the transaction.
* @param {Buffer | null} data The compiled memo (optional).
* @returns {number} The calculated transaction fee.
*/
getFeeFromUtxos(inputs, feeRate, data = null) {
// Calculate the size of the transaction
const inputSizeBasedOnInputs = inputs.length > 0
? inputs.reduce((a) => a + inputBytes(), 0) + inputs.length // +1 byte for each input signature
: 0;
// Calculate the sum of transaction size
let sum = TX_EMPTY_SIZE +
inputSizeBasedOnInputs +
TX_OUTPUT_BASE +
TX_OUTPUT_PUBKEYHASH +
TX_OUTPUT_BASE +
TX_OUTPUT_PUBKEYHASH;
// Add additional output size if data is provided
if (data) {
sum += TX_OUTPUT_BASE + data.length;
}
// Calculate the fee based on the sum of transaction size and the fee rate
const fee = sum * feeRate;
// Ensure the fee is not less than the minimum transaction fee
return fee > MIN_TX_FEE ? fee : MIN_TX_FEE;
}
}
/**
* Custom Doge client extended to support keystore functionality
*/
class ClientKeystore extends Client {
/**
* Get the Dogecoin address.
*
* Generates a Dogecoin address using the provided phrase and index.
* @param {number} index The index of the address to retrieve. Default is 0.
* @returns {Address} The Dogecoin address.
* @throws {"index must be greater than zero"} Thrown if the index is less than zero.
* @throws {"Phrase must be provided"} Thrown if the phrase is not provided.
* @throws {"Address not defined"} Thrown if failed to create the address from the phrase.
*/
getAddress(index = 0) {
if (index < 0) {
throw new Error('index must be greater than zero');
}
if (this.phrase) {
// Get Dogecoin network and keys
const dogeNetwork$1 = dogeNetwork(this.network);
const dogeKeys = this.getDogeKeys(this.phrase, index);
// Generate Dogecoin address
const { address } = Dogecoin.payments.p2pkh({
pubkey: dogeKeys.publicKey,
network: dogeNetwork$1,
});
if (!address) {
throw new Error('Address not defined');
}
return address;
}
throw new Error('Phrase must be provided');
}
/**
* @private
* Get private key.
*
* Private function to get keyPair from the this.phrase
*
* @param {string} phrase The phrase to be used for generating privkey
* @returns {ECPairInterface} The privkey generated from the given phrase
*
* @throws {"Could not get private key from phrase"} Throws an error if failed creating Doge keys from the given phrase
* */
getDogeKeys(phrase, index = 0) {
const dogeNetwork$1 = dogeNetwork(this.network);
const seed = getSeed(phrase);
const master = Dogecoin.bip32.fromSeed(seed, dogeNetwork$1).derivePath(this.getFullDerivationPath(index));
if (!master.privateKey) {
throw new Error('Could not get private key from phrase');
}
return Dogecoin.ECPair.fromPrivateKey(master.privateKey, { network: dogeNetwork$1 });
}
/**
* Get the current address.
* Asynchronous version of getAddress method.
* 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} 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(index = 0) {
return __awaiter(this, void 0, void 0, function* () {
return this.getAddress(index);
});
}
/**
* Asynchronously transfers Dogecoin.
*
* Builds, signs, and broadcasts a Dogecoin transaction with the specified parameters.
* @param {TxParams & { feeRate?: FeeRate }} params The transfer parameters including transaction details and optional fee rate.
* @returns {TxHash} A promise that resolves to the transaction hash once the transfer is completed.
*/
transfer(params) {
return __awaiter(this, void 0, void 0, function* () {
// Determine the fee rate for the transaction, using provided fee rate or fetching it from the network
const feeRate = params.feeRate || (yield this.getFeeRates())[FeeOption.Fast];
// Check if the fee rate is within the specified fee bounds
checkFeeBounds(this.feeBounds, feeRate);
// Get the index of the sender's address or use the default index (0)
const fromAddressIndex = (params === null || params === void 0 ? void 0 : params.walletIndex) || 0;
// Prepare the transaction by building it with the specified parameters
const { rawUnsignedTx } = yield this.prepareTx(Object.assign(Object.assign({}, params), { feeRate, sender: yield this.getAddressAsync(fromAddressIndex) }));
// Get the Dogecoin keys for signing the transaction
const dogeKeys = this.getDogeKeys(this.phrase, fromAddressIndex);
// Create a Partially Signed Bitcoin Transaction (PSBT) from the raw unsigned transaction
const psbt = Dogecoin.Psbt.fromBase64(rawUnsignedTx, { maximumFeeRate: 7500000 });
// Sign all inputs of the transaction with the Dogecoin keys
psbt.signAllInputs(dogeKeys);
// Finalize all inputs of the transaction
psbt.finalizeAllInputs();
// Extract the signed transaction and format it to hexadecimal
const txHex = psbt.extractTransaction().toHex();
// Broadcast the signed transaction to the Dogecoin network and return the transaction hash
return yield this.roundRobinBroadcastTx(txHex);
});
}
}
/**
* Custom Ledger Bitcoin 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 Doge application instance
getApp() {
return __awaiter(this, void 0, void 0, function* () {
if (this.app) {
return this.app;
}
this.app = new AppBtc({ transport: this.transport, currency: 'dogecoin' });
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: 'legacy',
verify,
});
return result.bitcoinAddress;
});
}
// Transfer Doge 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())[FeeOption.Fast];
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 = Dogecoin.Psbt.fromBase64(rawUnsignedTx);
// Prepare Ledger inputs
const ledgerInputs = inputs.map(({ txHex, hash, index }) => {
if (!txHex) {
throw Error(`Missing 'txHex' for UTXO (txHash ${hash})`);
}
const splittedTx = app.splitTransaction(txHex, false /* no segwit support */);
return [splittedTx, index, null, null];
});
// Prepare associated keysets
const associatedKeysets = ledgerInputs.map(() => this.getFullDerivationPath(fromAddressIndex));
// Convert the raw unsigned transaction to a Transaction object
// 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');
const txHex = yield app.createPaymentTransaction({
inputs: ledgerInputs,
associatedKeysets,
outputScriptHex,
// no additionals - similar to https://github.com/shapeshift/hdwallet/blob/a61234eb83081a4de54750b8965b873b15803a03/packages/hdwallet-ledger/src/bitcoin.ts#L222
additionals: [],
});
const txHash = yield this.broadcastTx(txHex);
// Throw error if no transaction hash is received
if (!txHash) {
throw Error('No Tx hash');
}
return txHash;
});
}
}
/**
* Function to get the URL for sending a transaction based on the network and Blockcypher URL.
* Throws an error if the network is 'testnet' since the testnet URL is not available for Blockcypher.
* @param {object} params Object containing the Blockcypher URL and network type.
* @param {string} params.blockcypherUrl The Blockcypher URL.
* @param {Network} params.network The network type (Mainnet, Testnet, or Stagenet).
* @returns {string} The URL for sending a transaction.
*/
const getSendTxUrl = ({ blockcypherUrl, network }) => {
if (network === 'testnet') {
// Check if the network is testnet
throw new Error('Testnet URL is not available for Blockcypher'); // Throw an error if testnet URL is requested
}
else {
return `${blockcypherUrl}/doge/main/txs/push`; // Return the mainnet URL for sending a transaction
}
};
export { AssetDOGE, BitgoProviders, ClientKeystore as Client, ClientLedger, DOGEChain, DOGE_DECIMAL, LOWER_FEE_BOUND, MIN_TX_FEE, UPPER_FEE_BOUND, blockcypherDataProviders, blockstreamExplorerProviders, defaultDogeParams, getPrefix, getSendTxUrl, sochainDataProviders, validateAddress };