@xchainjs/xchain-zcash
Version:
Custom Zcash client and utilities used by XChainJS clients
509 lines (496 loc) • 21.9 kB
JavaScript
import * as ecc from '@bitcoin-js/tiny-secp256k1-asmjs';
import { isValidAddr, mainnetPrefix, testnetPrefix, memoToScript, getFee, buildTx, buildMaxTx, skToAddr, signAndFinalize } from '@xchainjs/zcash-js';
import { ExplorerProvider, Network, FeeType, checkFeeBounds } from '@xchainjs/xchain-client';
import { getSeed } from '@xchainjs/xchain-crypto';
import { Client as Client$1, UtxoError, UtxoTransactionValidator } from '@xchainjs/xchain-utxo';
import { HDKey } from '@scure/bip32';
import { ECPairFactory } from 'ecpair';
import { AssetType, baseAmount } from '@xchainjs/xchain-util';
import { NownodesProvider } from '@xchainjs/xchain-utxo-providers';
/******************************************************************************
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;
};
const MIN_TX_FEE = 10000;
const ZEC_DECIMAL = 8;
const LOWER_FEE_BOUND = 10000;
const UPPER_FEE_BOUND = 100000;
/**
* Chain identifier for Zcash mainnet
*/
const ZECChain = 'ZEC';
/**
* Base "chain" asset on Zcash main net.
*/
const AssetZEC = { chain: ZECChain, symbol: 'ZEC', ticker: 'ZEC', type: AssetType.NATIVE };
// Explorer providers for Zcash
const ZEC_MAINNET_EXPLORER = new ExplorerProvider('https://blockchair.com/zcash/', 'https://blockchair.com/zcash/address/%%ADDRESS%%', 'https://blockchair.com/zcash/transaction/%%TX_ID%%');
const ZEC_TESTNET_EXPLORER = new ExplorerProvider('https://testnet.zcashexplorer.app/', 'https://testnet.zcashexplorer.app/address/%%ADDRESS%%', 'https://testnet.zcashexplorer.app/transactions/%%TX_ID%%');
const zcashExplorerProviders = {
[Network.Testnet]: ZEC_TESTNET_EXPLORER,
[Network.Stagenet]: ZEC_MAINNET_EXPLORER,
[Network.Mainnet]: ZEC_MAINNET_EXPLORER,
};
const mainnetNownodesProvider = new NownodesProvider('https://zecbook.nownodes.io/api/v2', ZECChain, AssetZEC, ZEC_DECIMAL, process.env.NOWNODES_API_KEY || '');
const NownodesProviders = {
[Network.Testnet]: undefined,
[Network.Stagenet]: mainnetNownodesProvider,
[Network.Mainnet]: mainnetNownodesProvider,
};
/**
* Function to get the Zcash prefix depending on network
*
* @param {Network} network - The network type (Mainnet, Testnet, or Stagenet).
* @returns {utxolib.Network} The Zcash network.
*/
const zecNetworkPrefix = (network) => {
switch (network) {
case Network.Mainnet:
case Network.Stagenet:
return mainnetPrefix;
case Network.Testnet:
return testnetPrefix;
default:
return mainnetPrefix;
}
};
/**
* Function to validate a Zcash address.
* @param {Address} address - The Zcash address to validate.
* @param {Network} network - The network type (Mainnet, Testnet, or Stagenet).
* @returns {boolean} `true` if the address is valid, `false` otherwise.
*/
const validateAddress = (address, network) => {
const prefix = zecNetworkPrefix(network);
return isValidAddr(address, new Uint8Array(prefix));
};
/**
* Function to get the address prefix based on the network.
* @param {Network} network - The network type (Mainnet, Testnet, or Stagenet).
* @returns {string} The address prefix based on the network.
*/
const getPrefix = (network) => {
switch (network) {
case Network.Mainnet:
case Network.Stagenet:
return 't1';
case Network.Testnet:
return 'tm';
}
};
// Default parameters for the Zcash client
const defaultZECParams = {
network: Network.Mainnet,
phrase: '',
explorerProviders: zcashExplorerProviders,
dataProviders: [NownodesProviders],
rootDerivationPaths: {
[Network.Mainnet]: `m/44'/133'/0'/0/`,
[Network.Testnet]: `m/44'/1'/0'/0/`,
[Network.Stagenet]: `m/44'/133'/0'/0/`,
},
feeBounds: {
lower: LOWER_FEE_BOUND,
upper: UPPER_FEE_BOUND,
},
};
/**
* Custom Zcash client (only support t-addresses)
*/
class Client extends Client$1 {
/**
* Constructor
* Initializes the client with network type and other parameters.
* @param {UtxoClientParams} params
*/
constructor(params = Object.assign({}, defaultZECParams)) {
super(ZECChain, {
network: params.network,
rootDerivationPaths: params.rootDerivationPaths,
phrase: params.phrase,
feeBounds: params.feeBounds,
explorerProviders: params.explorerProviders,
dataProviders: params.dataProviders,
});
}
/**
* Get ZEC asset info.
* @returns {AssetInfo} ZEC asset information.
*/
getAssetInfo() {
const assetInfo = {
asset: AssetZEC,
decimal: ZEC_DECIMAL,
};
return assetInfo;
}
/**
* Validate the given Zcash address.
* @param {string} address Zcash address to validate (only t-addresses).
* @returns {boolean} `true` if the address is valid, `false` otherwise.
*/
validateAddress(address) {
return validateAddress(address, this.network);
}
/**
* Compile memo into a buffer.
* @param {string} memo Memo to compile.
* @returns {Buffer} Compiled memo.
*/
compileMemo(memo) {
return memoToScript(memo);
}
/**
* Get transaction fee from UTXOs.
* @param {UTXO[]} inputs UTXOs to calculate fee from.
* @param {FeeRate} feeRate Fee rate.
* @param {Buffer | null} data Compiled memo (Optional).
* @returns {number} Transaction fee.
*/
getFeeFromUtxos(inputs, feeRate, _data = null) {
if (feeRate) {
throw 'No feerate supported for this client';
}
return getFee(inputs.length, 2, _data ? 'memo' : undefined);
}
/**
* Prepare transfer.
*
* @deprecated Use `prepareMaxTx` for sweep transactions. Zcash uses flat fees.
* @param {TxParams&Address&FeeRate&boolean} params The transfer options.
* @returns {PreparedTx} The raw unsigned transaction.
*/
prepareTx(_a) {
return __awaiter(this, arguments, void 0, function* ({ sender, memo, amount, recipient, feeRate: _feeRate, // Ignored: Zcash uses flat fees
spendPendingUTXO = true, }) {
// Validate recipient address
if (!this.validateAddress(recipient)) {
throw new Error('Invalid recipient address');
}
// Get UTXOs for sender
const utxos = yield this.scanUTXOs(sender, spendPendingUTXO);
if (utxos.length === 0) {
throw new Error('No UTXOs available for transaction');
}
// Convert UTXOs to zcash-js format
const zcashUtxos = utxos.map((utxo) => ({
address: sender,
txid: utxo.hash,
outputIndex: utxo.index,
satoshis: utxo.value,
}));
// Build unsigned transaction
const unsignedTx = yield buildTx(0, // height - can be 0 for prepared tx
sender, recipient, amount.amount().toNumber(), zcashUtxos, this.network === Network.Testnet ? false : true, memo);
// For Zcash, we return the transaction data as JSON string
// since zcash-js doesn't produce a standard raw transaction format
const rawUnsignedTx = JSON.stringify({
height: 0,
from: sender,
to: recipient,
amount: amount.amount().toNumber(),
utxos: zcashUtxos,
isMainnet: this.network === Network.Testnet ? false : true,
memo,
fee: unsignedTx.fee,
});
return {
rawUnsignedTx,
utxos,
inputs: utxos, // All UTXOs are potential inputs
};
});
}
getFeesWithRates() {
return __awaiter(this, void 0, void 0, function* () {
throw Error('Error Zcash has flat fee. Fee rates not supported');
});
}
getFeeRates() {
return __awaiter(this, void 0, void 0, function* () {
throw Error('Error Zcash has flat fee. Fee rates not supported');
});
}
getFees(options) {
return __awaiter(this, void 0, void 0, function* () {
let utxoNumber = 2; // By default pro rought estimation to display on interface
if (options === null || options === void 0 ? void 0 : options.sender) {
const utxo = yield this.scanUTXOs(options === null || options === void 0 ? void 0 : options.sender, false);
utxoNumber = utxo.length; // Max possible fee for interface display
}
const flatFee = getFee(utxoNumber, 2, options === null || options === void 0 ? void 0 : options.memo);
return {
average: baseAmount(flatFee, ZEC_DECIMAL),
fast: baseAmount(flatFee, ZEC_DECIMAL),
fastest: baseAmount(flatFee, ZEC_DECIMAL),
type: FeeType.FlatFee,
};
});
}
// ==================== Enhanced Transaction Methods ====================
/**
* Prepare max send transaction (sweep) for Zcash.
* Since Zcash uses flat fees, this calculates the maximum sendable amount
* after deducting the required fee.
*/
prepareMaxTx(_a) {
return __awaiter(this, arguments, void 0, function* ({ sender, recipient, memo, spendPendingUTXO = true, selectedUtxos, }) {
try {
// Validate addresses
if (!this.validateAddress(recipient)) {
throw UtxoError.invalidAddress(recipient, this.network);
}
if (!this.validateAddress(sender)) {
throw UtxoError.invalidAddress(sender, this.network);
}
// Use provided UTXOs (coin control) or fetch from chain
let utxos;
if (selectedUtxos && selectedUtxos.length > 0) {
UtxoTransactionValidator.validateUtxoSet(selectedUtxos);
utxos = selectedUtxos;
}
else {
utxos = yield this.scanUTXOs(sender, spendPendingUTXO);
}
if (utxos.length === 0) {
throw UtxoError.insufficientBalance('1', '0', this.network);
}
// Calculate total value of all UTXOs
const totalValue = utxos.reduce((sum, utxo) => sum + utxo.value, 0);
// Calculate flat fee for this transaction (1 = recipient only; getFee adds memo slots)
const fee = getFee(utxos.length, 1, memo);
// Calculate max sendable amount
const maxAmount = totalValue - fee;
if (maxAmount <= 0) {
throw UtxoError.insufficientBalance(String(fee), String(totalValue), this.network);
}
// Convert UTXOs to zcash-js format
const zcashUtxos = utxos.map((utxo) => ({
address: sender,
txid: utxo.hash,
outputIndex: utxo.index,
satoshis: utxo.value,
}));
// Build max transaction (no change output) using buildMaxTx
const maxTx = yield buildMaxTx(0, sender, recipient, zcashUtxos, this.network !== Network.Testnet, memo);
// For Zcash, we return the transaction data as JSON string
const rawUnsignedTx = JSON.stringify({
height: 0,
from: sender,
to: recipient,
amount: maxTx.maxAmount,
utxos: zcashUtxos,
isMainnet: this.network !== Network.Testnet,
memo,
fee: maxTx.fee,
isMaxTx: true, // Flag to indicate this is a sweep transaction
});
return {
rawUnsignedTx,
utxos,
inputs: utxos,
maxAmount: maxTx.maxAmount,
fee: maxTx.fee,
};
}
catch (error) {
if (UtxoError.isUtxoError(error)) {
throw error;
}
throw UtxoError.fromUnknown(error, 'prepareMaxTx');
}
});
}
}
const ECPair = ECPairFactory(ecc);
/**
* Custom Bitcoin client extended to support keystore functionality
*/
class ClientKeystore extends Client {
constructor(params = Object.assign({}, defaultZECParams)) {
super(params);
}
/**
* @deprecated This function eventually will be removed. Use getAddressAsync instead.
* Get the address associated with the given index.
* @param {number} index The index of the address.
* @returns {Address} The Bitcoin 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 has not been set before.
*/
getAddress(index = 0) {
// Check if the index is valid
if (index < 0) {
throw new Error('index must be greater than zero');
}
// Check if the phrase has been set
if (this.phrase) {
const zecKeys = this.getZecKeys(this.phrase, index);
if (!zecKeys.privateKey) {
throw Error('Error getting private key');
}
const prefix = zecNetworkPrefix(this.network);
const prefixUint8Array = new Uint8Array(prefix);
return skToAddr(zecKeys.privateKey, prefixUint8Array);
}
throw new Error('Phrase must be provided');
}
/**
* Get the current address asynchronously.
* @param {number} index The index of the address.
* @returns {Promise<Address>} A promise that resolves to the Bitcoin address.
* @throws {"Phrase must be provided"} Thrown if the phrase has not been set before.
*/
getAddressAsync() {
return __awaiter(this, arguments, void 0, function* (index = 0) {
return this.getAddress(index);
});
}
/**
* @private
* Get the keys derived from the given phrase.
*
* @param {string} phrase The phrase to be used for generating the keys.
* @param {number} index The index of the address.
* @returns {Bitcoin.ECPair.ECPairInterface} The Bitcoin key pair.
* @throws {"Could not get private key from phrase"} Thrown if failed to create BTC keys from the given phrase.
*/
getZecKeys(phrase, index = 0) {
const seed = getSeed(phrase);
const master = HDKey.fromMasterSeed(seed).derive(this.getFullDerivationPath(index));
if (!master.privateKey) {
throw new Error('Could not get private key from phrase');
}
return ECPair.fromPrivateKey(Buffer.from(master.privateKey)); // Be carefull missing zcash network due to this error: https://github.com/iancoleman/bip39/issues/94
}
/**
* Transfer ZEC.
*
* @param {TxParams&FeeRate} params The transfer options including the fee rate.
* @returns {Promise<TxHash|string>} A promise that resolves to the transaction hash or an error message.
* @throws {"memo too long"} Thrown if the memo is longer than 80 characters.
*/
transfer(params) {
return __awaiter(this, void 0, void 0, function* () {
// Get the address index from the parameters or use the default value
const fromAddressIndex = (params === null || params === void 0 ? void 0 : params.walletIndex) || 0;
const zecKeys = this.getZecKeys(this.phrase, fromAddressIndex);
const sender = yield this.getAddressAsync(fromAddressIndex);
let utxos;
if (params.selectedUtxos && params.selectedUtxos.length > 0) {
UtxoTransactionValidator.validateUtxoSet(params.selectedUtxos);
utxos = params.selectedUtxos;
}
else {
utxos = yield this.scanUTXOs(sender, true);
}
if (utxos.length === 0)
throw new Error('Insufficient Balance for transaction');
const zcashUtxos = utxos.map((utxo) => ({
address: sender,
txid: utxo.hash,
outputIndex: utxo.index,
satoshis: utxo.value,
}));
const tx = yield buildTx(0, sender, params.recipient, params.amount.amount().toNumber(), zcashUtxos, this.network === Network.Testnet ? false : true, params.memo);
checkFeeBounds(this.feeBounds, tx.fee);
if (!zecKeys.privateKey) {
throw Error('Error getting private key');
}
const signedBuffer = yield signAndFinalize(0, zecKeys.privateKey.toString('hex'), tx.inputs, tx.outputs);
const txId = yield this.roundRobinBroadcastTx(signedBuffer.toString('hex'));
return txId;
});
}
/**
* Transfer the maximum amount of ZEC (sweep).
*
* Calculates the maximum sendable amount after fees, signs, and broadcasts the transaction.
* Note: Zcash uses flat fees, so feeRate is ignored.
* @param {Object} params The transfer parameters.
* @param {string} params.recipient The recipient address.
* @param {string} [params.memo] Optional memo for the transaction.
* @param {number} [params.walletIndex] Optional wallet index. Defaults to 0.
* @returns {Promise<{ hash: TxHash; maxAmount: number; fee: number }>} The transaction hash, amount sent, and fee.
*/
transferMax(params) {
return __awaiter(this, void 0, void 0, function* () {
const fromAddressIndex = params.walletIndex || 0;
const sender = yield this.getAddressAsync(fromAddressIndex);
const zecKeys = this.getZecKeys(this.phrase, fromAddressIndex);
if (!zecKeys.privateKey) {
throw Error('Error getting private key');
}
let utxos;
if (params.selectedUtxos && params.selectedUtxos.length > 0) {
UtxoTransactionValidator.validateUtxoSet(params.selectedUtxos);
utxos = params.selectedUtxos;
}
else {
utxos = yield this.scanUTXOs(sender, true);
}
if (utxos.length === 0)
throw new Error('Insufficient Balance for transaction');
const zcashUtxos = utxos.map((utxo) => ({
address: sender,
txid: utxo.hash,
outputIndex: utxo.index,
satoshis: utxo.value,
}));
// Use buildMaxTx which creates NO change output (sweep transaction)
const tx = yield buildMaxTx(0, sender, params.recipient, zcashUtxos, this.network !== Network.Testnet, params.memo);
checkFeeBounds(this.feeBounds, tx.fee);
const signedBuffer = yield signAndFinalize(0, zecKeys.privateKey.toString('hex'), tx.inputs, tx.outputs);
const hash = yield this.roundRobinBroadcastTx(signedBuffer.toString('hex'));
return { hash, maxAmount: tx.maxAmount, fee: tx.fee };
});
}
}
class ClientLedger extends Client {
constructor(params) {
super(params);
throw Error('Ledger client not supported for Zcash.');
}
getApp() {
return __awaiter(this, void 0, void 0, function* () {
throw Error('Not implemented.');
});
}
getAddress() {
throw Error('Not implemented.');
}
getAddressAsync() {
return __awaiter(this, void 0, void 0, function* () {
throw Error('Not implemented.');
});
}
transfer() {
return __awaiter(this, void 0, void 0, function* () {
throw Error('Not implemented.');
});
}
}
export { AssetZEC, ClientKeystore as Client, ClientLedger, LOWER_FEE_BOUND, MIN_TX_FEE, NownodesProviders, UPPER_FEE_BOUND, ZECChain, ZEC_DECIMAL, defaultZECParams, getPrefix, validateAddress, zcashExplorerProviders };
//# sourceMappingURL=index.esm.js.map