UNPKG

@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
"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); }