@mayaprotocol/zcash-js
Version:
Zcash JavaScript library for Maya Protocol - Build and sign Zcash transparent transactions with memo support
283 lines (282 loc) • 10.7 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getFee = getFee;
exports.buildTx = buildTx;
exports.signAndFinalize = signAndFinalize;
const secp256k1_1 = require("@noble/curves/secp256k1");
const blake2b_wasm_1 = __importDefault(require("blake2b-wasm"));
const lodash_1 = require("lodash");
const addr_1 = require("./addr");
const script_1 = require("./script");
const writer_1 = require("./writer");
const PKH_OUTPUT_SIZE = 34;
const MARGINAL_FEE = 5000;
const GRACE_ACTIONS = 2;
function calculateFee(inCount, outCount) {
const logicalActions = inCount + outCount;
return MARGINAL_FEE * Math.max(GRACE_ACTIONS, logicalActions);
}
function getFee(inCount, outCount, memo) {
if (memo && memo.length > 0) {
const memoLenWithOverhead = memo.length + 2;
const memoOutputSlots = Math.floor((memoLenWithOverhead + PKH_OUTPUT_SIZE - 1) / PKH_OUTPUT_SIZE);
outCount += memoOutputSlots;
}
return calculateFee(inCount, outCount);
}
function selectUTXOS(utxos, amount, memo) {
let currentFee = 0;
const selected = [];
let remaining = amount;
for (const utxo of utxos) {
if (remaining == 0)
break;
selected.push(utxo);
const fee = getFee(selected.length, 2, memo);
const deltaFee = fee - currentFee;
currentFee = fee;
remaining += deltaFee;
const used = (0, lodash_1.min)([utxo.satoshis, remaining]);
remaining -= used;
}
return selected;
}
// @ts-ignore
async function buildTx(height, from, to, amount, utxos, isMainnet, memo) {
const prefixb = isMainnet ? addr_1.mainnetPrefix : addr_1.testnetPrefix;
const prefix = Buffer.from(prefixb);
if (!(0, addr_1.isValidAddr)(from, prefix))
throw new Error('Invalid "from" address');
if (!(0, addr_1.isValidAddr)(to, prefix))
throw new Error('Invalid "to" address');
if (amount > 1e14)
throw new Error('Amount too large');
if (memo && memo.length > 80)
throw new Error('Memo too long');
const inputs = selectUTXOS(utxos, amount, memo);
const outputCount = memo ? 3 : 2; // change + to + memo (if exists)
const fee = getFee(inputs.length, outputCount, memo);
const change = (0, lodash_1.sumBy)(inputs, (u) => u.satoshis) - amount - fee;
if (change < 0)
throw new Error('Not enough funds');
const outputs = [];
outputs.push({
type: 'pkh',
address: from,
amount: change
});
outputs.push({
type: 'pkh',
address: to,
amount: amount
});
if (memo) {
outputs.push({
type: 'op_return',
memo: memo
});
}
return {
height: height,
inputs: inputs,
outputs: outputs,
fee: fee
};
}
async function signAndFinalize(height, skb, utxos, outputs) {
const sk = new Uint8Array(Buffer.from(skb, 'hex'));
const pk = secp256k1_1.secp256k1.getPublicKey(sk, true);
let offset = 0;
// HEADER
let buf = Buffer.alloc(20);
buf.writeUInt32LE(0x80000005, 0);
buf.writeUInt32LE(0x26a7270a, 4);
buf.writeUInt32LE(0xc8e71055, 8);
buf.writeUInt32LE(0x00000000, 12);
buf.writeUInt32LE(height, 16);
let h = (0, blake2b_wasm_1.default)(32, undefined, undefined, new TextEncoder().encode('ZTxIdHeadersHash'));
h.update(buf);
const headerHash = h.digest('hex');
buf = Buffer.alloc(36 * utxos.length);
for (const [i, utxo] of utxos.entries()) {
const txid = Buffer.from(utxo.txid, 'hex');
txid.reverse();
txid.copy(buf, 36 * i);
buf.writeUInt32LE(utxo.outputIndex, 36 * i + 32);
}
h = (0, blake2b_wasm_1.default)(32, undefined, undefined, new TextEncoder().encode('ZTxIdPrevoutHash'));
h.update(buf);
const prevoutputsHash = h.digest('hex');
buf = Buffer.alloc(4 * utxos.length);
for (const [i, _] of utxos.entries()) {
buf.writeInt32LE(-1, 4 * i);
}
h = (0, blake2b_wasm_1.default)(32, undefined, undefined, new TextEncoder().encode('ZTxIdSequencHash'));
h.update(buf);
const sequencesHash = h.digest('hex');
// Calculate the actual buffer size needed
let outputsBufferSize = 0;
for (const output of outputs) {
if (output.type === 'pkh') {
outputsBufferSize += 34; // 8 bytes amount + 26 bytes script
}
else if (output.type === 'op_return') {
outputsBufferSize += 8 + (0, script_1.memoToScript)(output.memo).length;
}
}
buf = Buffer.alloc(outputsBufferSize);
offset = 0;
for (const [, output] of outputs.entries()) {
switch (output.type) {
case 'pkh':
buf.writeUIntLE(output.amount, offset, 6); // 6 is the max
offset += 8;
const pkhscript = (0, script_1.addressToScript)(output.address);
pkhscript.copy(buf, offset);
offset += 26;
break;
case 'op_return':
offset += 8;
const oprscript = (0, script_1.memoToScript)(output.memo);
oprscript.copy(buf, offset);
offset += oprscript.length;
break;
}
}
h = (0, blake2b_wasm_1.default)(32, undefined, undefined, new TextEncoder().encode('ZTxIdOutputsHash'));
h.update(buf.subarray(0, offset));
const outputsHash = h.digest('hex');
buf = Buffer.alloc(8 * utxos.length);
for (const [i, utxo] of utxos.entries()) {
buf.writeUIntLE(utxo.satoshis, 8 * i, 6);
}
h = (0, blake2b_wasm_1.default)(32, undefined, undefined, new TextEncoder().encode('ZTxTrAmountsHash'));
h.update(buf);
const amountsHash = h.digest('hex');
buf = Buffer.alloc(26 * utxos.length);
for (const [i, utxo] of utxos.entries()) {
const script = (0, script_1.addressToScript)(utxo.address);
script.copy(buf, 26 * i);
}
h = (0, blake2b_wasm_1.default)(32, undefined, undefined, new TextEncoder().encode('ZTxTrScriptsHash'));
h.update(buf);
const scriptsHash = h.digest('hex');
const signatures = [];
for (const [, utxo] of utxos.entries()) {
buf = Buffer.alloc(32 + 4 + 8 + 26 + 4);
offset = 0;
const txid = Buffer.from(utxo.txid, 'hex');
txid.reverse();
txid.copy(buf, offset);
offset += 32;
buf.writeUInt32LE(utxo.outputIndex, offset);
offset += 4;
buf.writeUIntLE(utxo.satoshis, offset, 6);
offset += 8;
const script = (0, script_1.addressToScript)(utxo.address);
script.copy(buf, offset);
offset += 26;
buf.writeInt32LE(-1, offset);
h = (0, blake2b_wasm_1.default)(32, undefined, undefined, new TextEncoder().encode('Zcash___TxInHash'));
h.update(buf);
const txInHash = h.digest('hex');
buf = Buffer.alloc(1 + 32 * 6);
offset = 1;
buf[0] = 1;
Buffer.from(prevoutputsHash, 'hex').copy(buf, offset);
offset += 32;
Buffer.from(amountsHash, 'hex').copy(buf, offset);
offset += 32;
Buffer.from(scriptsHash, 'hex').copy(buf, offset);
offset += 32;
Buffer.from(sequencesHash, 'hex').copy(buf, offset);
offset += 32;
Buffer.from(outputsHash, 'hex').copy(buf, offset);
offset += 32;
Buffer.from(txInHash, 'hex').copy(buf, offset);
offset += 32;
h = (0, blake2b_wasm_1.default)(32, undefined, undefined, new TextEncoder().encode('ZTxIdTranspaHash'));
h.update(buf);
const transparentHash = h.digest('hex');
buf = Buffer.alloc(32 * 4);
offset = 0;
Buffer.from(headerHash, 'hex').copy(buf, offset);
offset += 32;
Buffer.from(transparentHash, 'hex').copy(buf, offset);
offset += 32;
Buffer.from('6f2fc8f98feafd94e74a0df4bed74391ee0b5a69945e4ced8ca8a095206f00ae', 'hex').copy(buf, offset);
offset += 32;
Buffer.from('9fbe4ed13b0c08e671c11a3407d84e1117cd45028a2eee1b9feae78b48a6e2c1', 'hex').copy(buf, offset);
offset += 32;
const personal = Buffer.alloc(16);
Buffer.from('ZcashTxHash_').copy(personal);
personal.writeUInt32LE(0xc8e71055, 12);
h = (0, blake2b_wasm_1.default)(32, undefined, undefined, personal);
h.update(buf);
const sigHash = h.digest();
const signature = await secp256k1_1.secp256k1.sign(sigHash, sk, { lowS: true, prehash: false });
const signatureDER = signature.toDERRawBytes();
signatures.push(signatureDER);
}
buf = Buffer.alloc(2000);
offset = 0;
buf.writeUInt32LE(0x80000005, offset);
offset += 4;
buf.writeUInt32LE(0x26a7270a, offset);
offset += 4;
buf.writeUInt32LE(0xc8e71055, offset);
offset += 4;
buf.writeUInt32LE(0x00000000, offset);
offset += 4;
buf.writeUInt32LE(height, offset);
offset += 4;
const txinc = (0, writer_1.writeCompactInt)(utxos.length);
txinc.copy(buf, offset);
offset += txinc.length;
for (const [i, utxo] of utxos.entries()) {
const txid = Buffer.from(utxo.txid, 'hex');
txid.reverse();
txid.copy(buf, offset);
offset += 32;
buf.writeUInt32LE(utxo.outputIndex, offset);
offset += 4;
const ss = (0, script_1.writeSigScript)(signatures[i], pk);
const ssl = (0, writer_1.writeCompactInt)(ss.length);
ssl.copy(buf, offset);
offset += ssl.length;
ss.copy(buf, offset);
offset += ss.length;
buf.writeInt32LE(-1, offset);
offset += 4;
}
const txoutc = (0, writer_1.writeCompactInt)(outputs.length);
txoutc.copy(buf, offset);
offset += txoutc.length;
for (const [, out] of outputs.entries()) {
switch (out.type) {
case 'pkh':
{
buf.writeBigInt64LE(BigInt(out.amount), offset);
offset += 8;
const pkhscript = (0, script_1.addressToScript)(out.address);
pkhscript.copy(buf, offset);
offset += pkhscript.length;
}
break;
case 'op_return':
{
offset += 8;
const memoscript = (0, script_1.memoToScript)(out.memo);
memoscript.copy(buf, offset);
offset += memoscript.length;
}
break;
}
}
// Add 000000
offset += 3;
return buf.subarray(0, offset);
}