@xchainjs/xchain-zcash
Version:
Custom Zcash client and utilities used by XChainJS clients
543 lines (527 loc) • 23.2 kB
JavaScript
;
var ecc = require('@bitcoin-js/tiny-secp256k1-asmjs');
var zcashJs = require('@xchainjs/zcash-js');
var xchainClient = require('@xchainjs/xchain-client');
var xchainCrypto = require('@xchainjs/xchain-crypto');
var xchainUtxo = require('@xchainjs/xchain-utxo');
var bip32 = require('@scure/bip32');
var ecpair = require('ecpair');
var xchainUtil = require('@xchainjs/xchain-util');
var xchainUtxoProviders = require('@xchainjs/xchain-utxo-providers');
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 ecc__namespace = /*#__PURE__*/_interopNamespace(ecc);
/******************************************************************************
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: xchainUtil.AssetType.NATIVE };
// Explorer providers for Zcash
const ZEC_MAINNET_EXPLORER = new xchainClient.ExplorerProvider('https://blockchair.com/zcash/', 'https://blockchair.com/zcash/address/%%ADDRESS%%', 'https://blockchair.com/zcash/transaction/%%TX_ID%%');
const ZEC_TESTNET_EXPLORER = new xchainClient.ExplorerProvider('https://testnet.zcashexplorer.app/', 'https://testnet.zcashexplorer.app/address/%%ADDRESS%%', 'https://testnet.zcashexplorer.app/transactions/%%TX_ID%%');
const zcashExplorerProviders = {
[xchainClient.Network.Testnet]: ZEC_TESTNET_EXPLORER,
[xchainClient.Network.Stagenet]: ZEC_MAINNET_EXPLORER,
[xchainClient.Network.Mainnet]: ZEC_MAINNET_EXPLORER,
};
const mainnetNownodesProvider = new xchainUtxoProviders.NownodesProvider('https://zecbook.nownodes.io/api/v2', ZECChain, AssetZEC, ZEC_DECIMAL, process.env.NOWNODES_API_KEY || '');
const NownodesProviders = {
[xchainClient.Network.Testnet]: undefined,
[xchainClient.Network.Stagenet]: mainnetNownodesProvider,
[xchainClient.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 xchainClient.Network.Mainnet:
case xchainClient.Network.Stagenet:
return zcashJs.mainnetPrefix;
case xchainClient.Network.Testnet:
return zcashJs.testnetPrefix;
default:
return zcashJs.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 zcashJs.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 xchainClient.Network.Mainnet:
case xchainClient.Network.Stagenet:
return 't1';
case xchainClient.Network.Testnet:
return 'tm';
}
};
// Default parameters for the Zcash client
const defaultZECParams = {
network: xchainClient.Network.Mainnet,
phrase: '',
explorerProviders: zcashExplorerProviders,
dataProviders: [NownodesProviders],
rootDerivationPaths: {
[xchainClient.Network.Mainnet]: `m/44'/133'/0'/0/`,
[xchainClient.Network.Testnet]: `m/44'/1'/0'/0/`,
[xchainClient.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 xchainUtxo.Client {
/**
* 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 zcashJs.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 zcashJs.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 zcashJs.buildTx(0, // height - can be 0 for prepared tx
sender, recipient, amount.amount().toNumber(), zcashUtxos, this.network === xchainClient.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 === xchainClient.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 = zcashJs.getFee(utxoNumber, 2, options === null || options === void 0 ? void 0 : options.memo);
return {
average: xchainUtil.baseAmount(flatFee, ZEC_DECIMAL),
fast: xchainUtil.baseAmount(flatFee, ZEC_DECIMAL),
fastest: xchainUtil.baseAmount(flatFee, ZEC_DECIMAL),
type: xchainClient.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 xchainUtxo.UtxoError.invalidAddress(recipient, this.network);
}
if (!this.validateAddress(sender)) {
throw xchainUtxo.UtxoError.invalidAddress(sender, this.network);
}
// Use provided UTXOs (coin control) or fetch from chain
let utxos;
if (selectedUtxos && selectedUtxos.length > 0) {
xchainUtxo.UtxoTransactionValidator.validateUtxoSet(selectedUtxos);
utxos = selectedUtxos;
}
else {
utxos = yield this.scanUTXOs(sender, spendPendingUTXO);
}
if (utxos.length === 0) {
throw xchainUtxo.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 = zcashJs.getFee(utxos.length, 1, memo);
// Calculate max sendable amount
const maxAmount = totalValue - fee;
if (maxAmount <= 0) {
throw xchainUtxo.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 zcashJs.buildMaxTx(0, sender, recipient, zcashUtxos, this.network !== xchainClient.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 !== xchainClient.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 (xchainUtxo.UtxoError.isUtxoError(error)) {
throw error;
}
throw xchainUtxo.UtxoError.fromUnknown(error, 'prepareMaxTx');
}
});
}
}
const ECPair = ecpair.ECPairFactory(ecc__namespace);
/**
* 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 zcashJs.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 = xchainCrypto.getSeed(phrase);
const master = bip32.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) {
xchainUtxo.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 zcashJs.buildTx(0, sender, params.recipient, params.amount.amount().toNumber(), zcashUtxos, this.network === xchainClient.Network.Testnet ? false : true, params.memo);
xchainClient.checkFeeBounds(this.feeBounds, tx.fee);
if (!zecKeys.privateKey) {
throw Error('Error getting private key');
}
const signedBuffer = yield zcashJs.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) {
xchainUtxo.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 zcashJs.buildMaxTx(0, sender, params.recipient, zcashUtxos, this.network !== xchainClient.Network.Testnet, params.memo);
xchainClient.checkFeeBounds(this.feeBounds, tx.fee);
const signedBuffer = yield zcashJs.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.');
});
}
}
exports.AssetZEC = AssetZEC;
exports.Client = ClientKeystore;
exports.ClientLedger = ClientLedger;
exports.LOWER_FEE_BOUND = LOWER_FEE_BOUND;
exports.MIN_TX_FEE = MIN_TX_FEE;
exports.NownodesProviders = NownodesProviders;
exports.UPPER_FEE_BOUND = UPPER_FEE_BOUND;
exports.ZECChain = ZECChain;
exports.ZEC_DECIMAL = ZEC_DECIMAL;
exports.defaultZECParams = defaultZECParams;
exports.getPrefix = getPrefix;
exports.validateAddress = validateAddress;
exports.zcashExplorerProviders = zcashExplorerProviders;
//# sourceMappingURL=index.js.map