chaingate
Version:
Multi-chain cryptocurrency SDK for TypeScript — unified API for Bitcoin, Ethereum, Litecoin, Dogecoin, Bitcoin Cash, Polygon, Arbitrum, and any EVM-compatible chain. Create wallets, query balances, send transactions, and manage tokens and NFTs across UTXO
361 lines (360 loc) • 14.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 () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__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.CustomUtxoTransaction = void 0;
exports.signCustomUtxoTransaction = signCustomUtxoTransaction;
const decimal_js_1 = __importDefault(require("decimal.js"));
const btc = __importStar(require("@scure/btc-signer"));
const btc_signer_1 = require("@scure/btc-signer");
const errors_1 = require("../../errors");
const BroadcastedUtxoTransaction_1 = require("./BroadcastedUtxoTransaction");
const utils_1 = require("../../utils");
// ---------------------------------------------------------------------------
// Custom transaction class
// ---------------------------------------------------------------------------
/**
* A UTXO transaction with caller-defined inputs and outputs.
*
* The fee is implicitly the difference between the sum of input amounts and
* the sum of output amounts.
*/
class CustomUtxoTransaction {
/** @internal */
constructor(params) {
this.sent = false;
this.explorer = params.explorer;
this.networkParams = params.networkParams;
this.inputList = [...params.inputs];
this.outputList = [...params.outputs];
this.getPrivateKey = params.getPrivateKey;
this._signTransaction = params.signTransaction;
}
// -------------------------------------------------------------------------
// Accessors
// -------------------------------------------------------------------------
/** Returns the inputs that will be spent. */
inputs() {
return this.inputList;
}
/** Returns the outputs that will be created. */
outputs() {
return this.outputList;
}
/**
* Returns the implied fee (sum of inputs minus sum of outputs).
*
* A negative value means the outputs exceed the inputs — the transaction
* would be invalid.
*/
fee() {
const totalIn = this.inputList.reduce((sum, i) => sum + i.amount.min(), 0n);
const totalOut = this.outputList.reduce((sum, o) => sum + o.amount.min(), 0n);
return this.explorer.amountFromSat(totalIn - totalOut);
}
/**
* Returns the estimated virtual size (vbytes) of this transaction.
*
* Returns `null` if the transaction has no inputs or no outputs.
*/
estimatedSizeBytes() {
if (this.inputList.length === 0 || this.outputList.length === 0)
return null;
const result = this.runSelection(1n);
if (!result)
return null;
return Math.ceil(result.weight / 4);
}
// -------------------------------------------------------------------------
// Mutation
// -------------------------------------------------------------------------
/**
* Appends an input to the transaction.
*
* @throws {@link TransactionAlreadySentError} if already broadcast.
*/
addInput(input) {
this.guardNotSent();
this.inputList.push(input);
}
/**
* Removes an input identified by its transaction ID and output index.
*
* @returns `true` if the input was found and removed.
* @throws {@link TransactionAlreadySentError} if already broadcast.
*/
removeInput(txid, index) {
this.guardNotSent();
const idx = this.inputList.findIndex((i) => i.txid === txid && i.index === index);
if (idx === -1)
return false;
this.inputList.splice(idx, 1);
return true;
}
/**
* Appends an output to the transaction.
*
* @throws {@link TransactionAlreadySentError} if already broadcast.
*/
addOutput(output) {
this.guardNotSent();
this.outputList.push(output);
}
/**
* Removes an output by its position in the outputs list.
*
* @returns `true` if the index was valid and the output was removed.
* @throws {@link TransactionAlreadySentError} if already broadcast.
*/
removeOutput(index) {
this.guardNotSent();
if (index < 0 || index >= this.outputList.length)
return false;
this.outputList.splice(index, 1);
return true;
}
// -------------------------------------------------------------------------
// Change address & fee estimation
// -------------------------------------------------------------------------
/**
* Sets a change address and computes the change output automatically.
*
* Appends a change output whose amount equals the remaining value after
* subtracting all other outputs and the estimated fee. If the change
* amount is below the dust threshold, no change output is added and
* the remainder goes entirely to fees.
*
* @param address - The address to receive the change.
* @param fee - A tier object from {@link recommendedFees} or an explicit fee rate.
* @throws {@link TransactionAlreadySentError} if already broadcast.
* @throws {Error} if inputs or outputs are empty, or if outputs exceed inputs.
*/
setChangeAddress(address, fee) {
this.guardNotSent();
const feePerByte = fee.feePerKbSat / 1000n || 1n;
if (this.inputList.length === 0) {
throw new Error('Cannot set change address: no inputs.');
}
const result = this.runSelection(feePerByte, address);
if (!result) {
throw new Error('Cannot compute change: outputs exceed inputs at the given fee rate.');
}
// If selectUTXO added a change output, append it to our output list.
if (result.change) {
// The change output is the last one appended by selectUTXO.
const changeOutput = result.outputs[result.outputs.length - 1];
if ('address' in changeOutput) {
this.outputList.push({
address: changeOutput.address,
amount: this.explorer.amountFromSat(changeOutput.amount),
});
}
}
}
/**
* Returns recommended fees for each priority tier.
*
* Fee rates are fetched from the network. For each tier, the estimated fee
* is computed from the current inputs and outputs.
*
* Returns `null` if the transaction has no inputs or no outputs.
*/
async recommendedFees() {
if (this.inputList.length === 0 || this.outputList.length === 0)
return null;
const totalIn = this.inputList.reduce((sum, i) => sum + i.amount.min(), 0n);
const feeRateResult = await this.explorer.getFeeRate();
const tiers = ['low', 'normal', 'high', 'maximum'];
const results = {};
for (const tier of tiers) {
const entry = feeRateResult[tier];
const feePerKbSat = BigInt(new decimal_js_1.default(entry.feePerKb).toFixed(0));
const feePerByte = feePerKbSat / 1000n || 1n;
const selected = this.runSelection(feePerByte);
const estimatedFeeSat = selected ? (selected.fee ?? null) : null;
const enoughFunds = estimatedFeeSat !== null && totalIn >= (estimatedFeeSat ?? 0n);
const result = {
feePerKbSat,
estimatedFeeSat,
estimatedConfirmationSecs: entry.confirmationTimeSecs,
enoughFunds,
};
if (!enoughFunds) {
result.missingFunds = this.estimateMissingFunds(feePerByte, totalIn);
}
results[tier] = result;
}
return results;
}
/**
* Estimates the satoshis missing for the transaction to succeed at a given
* fee rate by re-running selection with a synthetic high-value input. Returns
* `null` when the gap cannot be estimated.
*/
estimateMissingFunds(feePerByte, totalIn) {
const totalOut = this.outputList.reduce((sum, o) => sum + o.amount.min(), 0n);
const syntheticScript = (0, utils_1.hexToBytes)(this.inputList[0].script);
const dummyAmount = totalOut + 100000000000n;
const vins = this.inputList.map((vin) => ({
txid: (0, utils_1.hexToBytes)(vin.txid),
index: vin.index,
witnessUtxo: { script: (0, utils_1.hexToBytes)(vin.script), amount: vin.amount.min() },
}));
vins.push({
txid: new Uint8Array(32),
index: 0,
witnessUtxo: { script: syntheticScript, amount: dummyAmount },
});
const vouts = this.outputList.map((vout) => ({
address: vout.address,
amount: vout.amount.min(),
}));
const selected = btc.selectUTXO(vins, vouts, 'all', {
changeAddress: this.outputList[0].address,
feePerByte,
bip69: false,
createTx: false,
allowLegacyWitnessUtxo: true,
network: this.networkParams,
});
if (!selected)
return null;
const estimatedFee = selected.fee ?? 0n;
const needed = totalOut + estimatedFee;
return needed > totalIn ? needed - totalIn : null;
}
// -------------------------------------------------------------------------
// Sign & broadcast
// -------------------------------------------------------------------------
/**
* Signs the transaction and broadcasts it to the network.
*
* @throws {@link TransactionAlreadySentError} if already broadcast.
* @throws {Error} if outputs exceed inputs.
*/
async signAndBroadcast() {
if (this.sent) {
throw new errors_1.TransactionAlreadySentError();
}
const impliedFee = this.fee().min();
if (impliedFee < 0n) {
throw new Error(`Transaction outputs exceed inputs. ` + `Reduce output amounts or add more inputs.`);
}
const privateKey = await this.getPrivateKey();
const rawTx = this._signTransaction(this.inputList, this.outputList, privateKey, this.networkParams);
privateKey.fill(0);
const { txId } = await this.explorer.broadcastTransaction((0, utils_1.bytesToHex)(rawTx));
this.sent = true;
// Update local UTXO cache: mark inputs as spent.
const cache = this.explorer.global.utxoCache;
for (const input of this.inputList) {
cache.markSpent(input.txid, input.index, txId);
}
return new BroadcastedUtxoTransaction_1.BroadcastedUtxoTransaction(txId, this.explorer);
}
// -------------------------------------------------------------------------
// Internal helpers
// -------------------------------------------------------------------------
guardNotSent() {
if (this.sent)
throw new errors_1.TransactionAlreadySentError();
}
/**
* Runs selectUTXO with the 'all' strategy so every input is included.
* Uses the current outputs as-is. Returns null if selection fails.
*/
runSelection(feePerByte, changeAddress) {
if (this.inputList.length === 0 || this.outputList.length === 0)
return null;
const vins = this.inputList.map((vin) => ({
txid: (0, utils_1.hexToBytes)(vin.txid),
index: vin.index,
witnessUtxo: {
script: (0, utils_1.hexToBytes)(vin.script),
amount: vin.amount.min(),
},
}));
const vouts = this.outputList.map((vout) => ({
address: vout.address,
amount: vout.amount.min(),
}));
// Use the first output address as a dummy change address when none is provided.
// The change address only affects the output script size estimation, which is
// negligible for size calculations.
const resolvedChangeAddress = changeAddress ?? this.outputList[0].address;
return (btc.selectUTXO(vins, vouts, 'all', {
changeAddress: resolvedChangeAddress,
feePerByte,
bip69: false,
createTx: false,
allowLegacyWitnessUtxo: true,
network: this.networkParams,
}) ?? null);
}
}
exports.CustomUtxoTransaction = CustomUtxoTransaction;
// ---------------------------------------------------------------------------
// Signing implementations (one per UTXO family)
// ---------------------------------------------------------------------------
/**
* Standard UTXO signing (BTC, LTC, DOGE, tBTC).
* @internal
*/
function signCustomUtxoTransaction(inputs, outputs, privateKey, networkParams) {
const txVins = inputs.map((vin) => ({
txid: (0, utils_1.hexToBytes)(vin.txid),
index: vin.index,
witnessUtxo: {
script: (0, utils_1.hexToBytes)(vin.script),
amount: vin.amount.min(),
},
}));
const txVouts = outputs.map((vout) => ({
script: btc_signer_1.OutScript.encode(btc.Address(networkParams).decode(vout.address)),
amount: vout.amount.min(),
}));
const transaction = new btc.Transaction({ allowLegacyWitnessUtxo: true });
for (const vin of txVins)
transaction.addInput(vin);
for (const vout of txVouts)
transaction.addOutput(vout);
for (let i = 0; i < transaction.inputsLength; i++) {
transaction.signIdx(privateKey, i);
}
transaction.finalize();
return (0, utils_1.hexToBytes)(transaction.hex);
}