import "crypto" import { BarretenbergBackend } from '@noir-lang/backend_barretenberg'; import { Noir, InputMap } from '@noir-lang/noir_js'; import { zeroPadValue, TypedDataEncoder } from 'ethers'; import json2toml from "json2toml" import * as types from './types.js'; import { TransactionInputs, create_transaction } from '../circuits/helpers/codegen/create_transaction'; import { UTXO_New } from '../circuits/helpers/codegen/utxo_to_commitment'; import { MerkleTree } from './MerkleTree'; import { keccak_tx_no_deposit } from '../circuits/helpers/codegen/keccak_tx_no_deposit'; import { tx_as_hash } from '../circuits/helpers/codegen/tx_as_hash'; import { Momiji__factory, XFTMock__factory, UltraVerifier__factory, IUniswapV3Pool__factory } from './typechain-types'; import { transaction, transaction_wrapper } from './transaction_circuits'; import { Fr, Barretenberg } from "@aztec/bb.js" import { Buffer } from "buffer" import { rollup_transaction, RecursionInputs, Verifier } from "../circuits/helpers/codegen/rollup_transaction"; import './database'; const MAX_FIELD_VALUE: string = (new types.NoirFr(types.NoirFr.MAX_VALUE)).toString(); export class TransactionBuilder { initialized: boolean = false; circuits: types.Circuits; backends: types.Backends; noirs: types.Noirs; contracts!: types.Contracts; sendRpc: string | undefined; verbose: boolean = false; config: types.GlobalConfig; constructor(config: types.GlobalConfig) { this.config = config; this.verbose = (config.verbose) ? config.verbose : false; const circuits: types.Circuits = { transaction: transaction, transaction_wrapper: transaction_wrapper, }; let backendOptions: types.NoirBackendOptions | undefined = (this.config.threads) ? { threads: this.config.threads } : undefined; const backends: types.Backends = { transaction: new BarretenbergBackend(circuits.transaction, backendOptions), transaction_wrapper: new BarretenbergBackend(circuits.transaction_wrapper, backendOptions), }; const noirs: types.Noirs = { transaction: new Noir(circuits.transaction, backends.transaction), transaction_wrapper: new Noir(circuits.transaction_wrapper, backends.transaction_wrapper), }; this.circuits = circuits; this.backends = backends; this.noirs = noirs; } async initializeTransactionBuilder(): Promise<void> { let _runner = this.config.signer ? this.config.signer : this.config.provider let _state = Momiji__factory.connect("0xd66E7B1B008a560ad8be69494ce101DE188FbCd3", _runner) this.contracts = { state: _state, verifier: UltraVerifier__factory.connect(await _state.verifier(), _runner), token: XFTMock__factory.connect(await _state.xft(), _runner), pool: IUniswapV3Pool__factory.connect(await _state.xftPool(), _runner) } this.initialized = true; } emptyTransaction = async (): Promise<types.Transaction> => { return { proof_artifacts: { proofData: { proof: Uint8Array.from(new Array(2144).fill(255)), publicInputs: [MAX_FIELD_VALUE] }, proofAsFields: new Array(93).fill(MAX_FIELD_VALUE), vkAsFields: new Array(114).fill(MAX_FIELD_VALUE), vkHash: await this.contracts.state.txKeyHash() }, contract_inputs: { current_root: await this.contracts.state.merkleRoot(), utxo_root: MAX_FIELD_VALUE, price_limit: MAX_FIELD_VALUE, timestamp: await this.config.provider.getBlock("latest") .then(res => types.toFixedHex(res!.timestamp - (res!.timestamp % 60), true)), deadline: await this.config.provider!.getBlock("latest") .then(res => types.toFixedHex(res!.timestamp - (res!.timestamp % 60) + (60 * 60 * 24 * 7), true)), amount: MAX_FIELD_VALUE, encrypted_utxo: new Array(16).fill({ secret: MAX_FIELD_VALUE, amount: MAX_FIELD_VALUE, data: MAX_FIELD_VALUE }), withdrawals: new Array(16).fill(types.ZERO_HEX), commitments_in: new Array(16).fill(types.ZERO_VALUE), commitments: new Array(16).fill(types.NoirFr.random().toString()), recipients: new Array(16).fill(types.ZERO_HEX), nullifier_hashes: new Array(16).fill(types.ZERO_VALUE), uids: new Array(16).fill(types.ZERO_HEX), swap_amounts: new Array(16).fill(types.ZERO_HEX), deposit: { signature: "0x00", pi_hash: MAX_FIELD_VALUE } }, public_inputs: { current_root: await this.contracts.state.merkleRoot(), utxo_root: MAX_FIELD_VALUE, deposit_amount: types.ZERO_HEX, withdrawals: types.ZERO_HEX, commitment_in: new Array(16).fill(types.ZERO_VALUE), commitment_out: new Array(16).fill(types.ZERO_VALUE), nullifier_hashes: new Array(16).fill(types.ZERO_VALUE), contract_only_inputs: MAX_FIELD_VALUE } } }; async _padBatch(tx: types.Transaction[]): Promise<types.Batch> { const txContractInputs = tx.map(_tx => _tx.contract_inputs) const _fill = new Array(16 - tx.length).fill((await this.emptyTransaction()).contract_inputs) const txFill = txContractInputs.concat(_fill) let _batch: types.Batch = { tx_key_hash: await this.contracts.state.txKeyHash(), recursive_key_hash: await this.contracts.state.recursiveKeyHash(), new_root: MAX_FIELD_VALUE, old_hist_root: await this.contracts.state.histRoot(), new_hist_root: MAX_FIELD_VALUE, oracle: MAX_FIELD_VALUE, historic_path: new Array(20).fill(MAX_FIELD_VALUE), aggregation_object: new Array(16).fill(MAX_FIELD_VALUE), transactions: txFill } console.log(_batch.transactions) return _batch } async getTransactionsGas(tx: types.Transaction[] | types.Transaction): Promise<bigint> { const SUBSIDY_FACTOR = 16n const _verifyCost = 430_000n; if (!Array.isArray(tx)) tx = [tx] console.log("pad now") const _batch = await this._padBatch(tx) console.log(_batch) let _batchSim = _batch; let _emptySim = _batch; try { _batchSim.transactions = _batch.transactions.slice(tx.length); _emptySim.transactions = _batch.transactions = []; let _fullGas = (await this.contracts.state.simulatePublish.estimateGas(_batch)); let _stubGas = (await this.contracts.state.simulatePublish.estimateGas(_batchSim)); let _emptyGas = (await this.contracts.state.simulatePublish.estimateGas(_emptySim)); console.log(_fullGas, _stubGas, _emptyGas); return (_fullGas - _stubGas) + ((_verifyCost + _emptyGas) / SUBSIDY_FACTOR); } catch (e) { console.log(e); return 0n; } } async _generateUTXOEncrypted(utxo_input: types.UTXO_Input[]): Promise<types.UTXO_Encrypted[]> { const utxos: types.UTXO_Encrypted[] = utxo_input.map((utxo) => { return { secret: utxo.secret ? utxo.secret : Fr.random().toString(), amount: utxo.amount, asset_type: utxo.asset_type ? utxo.asset_type : types.ZERO_VALUE, secret_encrypted: types.ZERO_VALUE, amount_encrypted: types.ZERO_VALUE, asset_type_encrypted: types.ZERO_VALUE, uid: types.ZERO_VALUE } }) return utxos } async utxo_to_commitment(utxo: UTXO_New): Promise<string> { const barretenberg: types._Barretenberg = await Barretenberg.new() const hash: string = await barretenberg.pedersenHash([Fr.fromString(utxo.secret)], 0) .then((owner: types._NoirFr) => barretenberg.pedersenHash([owner, Fr.fromString(utxo.amount), Fr.fromString(utxo.asset_type)], 0)) .then((hashFr: types._NoirFr) => hashFr.toString()) await barretenberg.destroy() return hash } async pedersen_left_right(left: string, right: string): Promise<string> { const barretenberg: types._Barretenberg = await Barretenberg.new() const hash: string = await barretenberg.pedersenHash([Fr.fromString(left), Fr.fromString(right)], 0) .then(hashFr => hashFr.toString()) await barretenberg.destroy() return hash } _calculateDepositAmount(utxo_commitments?: types.UTXO_Commitment[], utxo_new?: types.UTXO_Encrypted[], withdrawals?: types.WithdrawalSwap[]): string { const utxo_in_total: bigint = utxo_commitments ? utxo_commitments.map(utxo => (utxo.amount === types.ZERO_VALUE ? BigInt(0) : BigInt(utxo.amount))).reduce((prev, curr) => BigInt(prev) + BigInt(curr)) : BigInt(0) const utxo_out_total: bigint = utxo_new ? utxo_new.map(utxo => (utxo.amount === types.ZERO_VALUE ? BigInt(0) : BigInt(utxo.amount))).reduce((prev, curr) => BigInt(prev) + BigInt(curr)) : BigInt(0) const withdrawal_total: bigint = withdrawals ? withdrawals.map(w => BigInt(w.amount)).reduce((prev, curr) => BigInt(prev) + BigInt(curr)) : BigInt(0) let total: string = ((utxo_out_total + withdrawal_total) > utxo_in_total) ? (new Fr(utxo_out_total + withdrawal_total - utxo_in_total)).toString() : (new Fr(BigInt(0))).toString(); return total } async _getPriceLimitX96(slippage_percentage: number): Promise<string> { if (slippage_percentage > 24) return Promise.reject("Slippage too high"); if (slippage_percentage < 0) return Promise.reject("Slippage cannot be less than zero") const _sqrtPriceX96: bigint = await this.contracts.pool.slot0().then(slot0 => slot0.sqrtPriceX96) const _sqrtPriceLimitX96: bigint = _sqrtPriceX96 * BigInt(100_000 - Math.floor(slippage_percentage * 1_000)) / BigInt(100_000) return (new Fr(_sqrtPriceLimitX96)).toString() } async _getTransactionFeeInXFT(gasUsed: bigint): Promise<bigint> { const _sqrtPriceX96: bigint = await this.contracts.pool.slot0().then(slot0 => slot0.sqrtPriceX96) const _xftPerEth: bigint = BigInt(2**192 * 1e18) / (_sqrtPriceX96 * _sqrtPriceX96) const _feeData = await this.config.provider.getFeeData() if (!_feeData.maxFeePerGas || !_feeData.maxPriorityFeePerGas) return Promise.reject("Fee data cannot be fetched") const _transactionFeeInXft: bigint = BigInt(gasUsed) * (_feeData.maxFeePerGas! + _feeData.maxPriorityFeePerGas!) * _xftPerEth / BigInt(1e18) return _transactionFeeInXft } async _getEtherFromXFT(xft: bigint): Promise<bigint> { const sqrtPriceX96: bigint = await this.contracts.pool.slot0().then(slot0 => slot0.sqrtPriceX96) const xftToEther: bigint = xft * sqrtPriceX96 * sqrtPriceX96 / BigInt(2**192) return xftToEther } async _generateWithdrawals(_withdrawals: types.Withdrawal[], slippage_percentage: number): Promise<types.WithdrawalSwap[]> { const priceLimitX96: string = await this._getPriceLimitX96(slippage_percentage) const withdrawals: types.WithdrawalSwap[] = _withdrawals.map( (withdrawal) => { const _amount: string = (withdrawal.amount === types.ZERO_VALUE) ? types.ZERO_HEX : withdrawal.amount; return { amount: _amount, recipient: zeroPadValue(withdrawal.recipient, 32), swap_amount: (new Fr(BigInt(_amount) * BigInt(Math.floor(withdrawal.swap_percentage * 1_000)) / BigInt(100_000))).toString(), price_limit: priceLimitX96 } as types.WithdrawalSwap } ) return withdrawals } async _generateRawTransactionInputs(utxo_commitments?: types.UTXO_Commitment[], utxo_encrypted?: types.UTXO_Encrypted[], withdrawals?: types.WithdrawalSwap[]): Promise<types.TransactionInputsRaw> { const inputs: types.TransactionInputsRaw = { current_root: await this.contracts.state.histRoot(), deposit_amount: this._calculateDepositAmount(utxo_commitments, utxo_encrypted, withdrawals), withdrawals: (withdrawals) ? new Array(16).fill("0x").map((_, i, __) => (withdrawals[i]) ? withdrawals[i].amount : types.ZERO_HEX) : new Array(16).fill(types.ZERO_HEX), utxo_spendable: await this._generateUTXOSpendable(utxo_commitments), utxo_new: (utxo_encrypted) ? new Array(16).fill({ secret: types.ZERO_VALUE, amount: types.ZERO_VALUE, asset_type: types.ZERO_VALUE }) .map((_, i, __) => { if (utxo_encrypted[i]) { return { secret: utxo_encrypted[i].secret, amount: utxo_encrypted[i].amount, asset_type: utxo_encrypted[i].asset_type } } else { return { secret: types.ZERO_HEX, amount: types.ZERO_VALUE, asset_type: types.ZERO_VALUE } } }) : new Array(16).fill({ secret: types.ZERO_VALUE, amount: types.ZERO_VALUE, asset_type: types.ZERO_VALUE }), contract_only_inputs: { timestamp: await this.config.provider.getBlock("latest") .then(res => types.toFixedHex(res!.timestamp - (res!.timestamp % 60), true)), deadline: await this.config.provider!.getBlock("latest") .then(res => types.toFixedHex(res!.timestamp - (res!.timestamp % 60) + (60 * 60 * 24 * 7), true)), signature_hash: types.ZERO_HEX, price_limit: (withdrawals) ? withdrawals.reduce((prev, curr) => ((BigInt(curr.price_limit) > BigInt(prev.price_limit)) ? curr : prev)).price_limit : types.ZERO_HEX, recipients: (withdrawals) ? new Array(16).fill("0x").map((_, i, __) => ((withdrawals[i]) ? withdrawals[i].recipient : types.ZERO_HEX)) : new Array(16).fill(types.ZERO_HEX), swap_amounts: (withdrawals) ? new Array(16).fill("0x").map((_, i, __) => ((withdrawals[i]) ? withdrawals[i].swap_amount : types.ZERO_HEX)) : new Array(16).fill(types.ZERO_HEX), uids: (utxo_encrypted) ? new Array(16).fill("0x").map((_, i, __) => ((utxo_encrypted[i]) ? utxo_encrypted[i].uid : types.ZERO_VALUE)) : new Array(16).fill(types.ZERO_VALUE), encrypted_utxo: (utxo_encrypted) ? new Array(16).fill("0x") .map((_, i, __) => { if (utxo_encrypted[i]) { return { secret: utxo_encrypted[i].secret_encrypted, amount: utxo_encrypted[i].amount_encrypted, data: utxo_encrypted[i].asset_type_encrypted } } else { return { secret: types.ZERO_VALUE, amount: types.ZERO_VALUE, data: types.ZERO_VALUE } } }) : new Array(16).fill({ secret: types.ZERO_VALUE, amount: types.ZERO_VALUE, data: types.ZERO_VALUE }) } } const _domain: types.TypedDataDomain = { name: "Momiji", version: "1", chainId: BigInt(1), verifyingContract: await this.contracts.state.getAddress() } const _types: types.TypedDataTypes = { DepositHash: [ { name: "pi_hash", type: "bytes32" } ] } const _values: types.TypedDataValues = { pi_hash: types.ZERO_HEX } inputs.contract_only_inputs.signature_hash = await keccak_tx_no_deposit( inputs.current_root, inputs.deposit_amount, inputs.withdrawals, inputs.utxo_spendable, inputs.utxo_new, inputs.contract_only_inputs ).then(hash => { _values.pi_hash = zeroPadValue(hash, 32) if (inputs.deposit_amount === types.toFixedHex(0, true)) return types.toFixedHex(0, true); else return zeroPadValue( Fr.fromBufferReduce( Buffer.from( TypedDataEncoder.hash(_domain, _types, _values).slice(2), "hex" ) ).toString(), 32 ); }) return inputs } async _generateRawTransactionInputsExpert(deposit?: bigint, utxo_commitments?: types.UTXO_Commitment[], utxo_encrypted?: types.UTXO_Encrypted[], withdrawals?: types.WithdrawalSwap[]): Promise<types.TransactionInputsRaw> { const inputs: types.TransactionInputsRaw = { current_root: await this.contracts.state.histRoot(), deposit_amount: (deposit) ? (new types.NoirFr(deposit)).toString() : types.toFixedHex(0, true), withdrawals: (withdrawals) ? new Array(16).fill("0x").map((_, i, __) => (withdrawals[i]) ? withdrawals[i].amount : types.ZERO_HEX) : new Array(16).fill(types.ZERO_HEX), utxo_spendable: await this._generateUTXOSpendable(utxo_commitments), utxo_new: (utxo_encrypted) ? new Array(16).fill({ secret: types.ZERO_VALUE, amount: types.ZERO_VALUE, asset_type: types.ZERO_VALUE }) .map((_, i, __) => { if (utxo_encrypted[i]) { return { secret: utxo_encrypted[i].secret, amount: utxo_encrypted[i].amount, asset_type: utxo_encrypted[i].asset_type } } else { return { secret: types.ZERO_HEX, amount: types.ZERO_VALUE, asset_type: types.ZERO_VALUE } } }) : new Array(16).fill({ secret: types.ZERO_VALUE, amount: types.ZERO_VALUE, asset_type: types.ZERO_VALUE }), contract_only_inputs: { timestamp: await this.config.provider.getBlock("latest") .then(res => types.toFixedHex(res!.timestamp - (res!.timestamp % 60), true)), deadline: await this.config.provider!.getBlock("latest") .then(res => types.toFixedHex(res!.timestamp - (res!.timestamp % 60) + (60 * 60 * 24 * 7), true)), signature_hash: types.ZERO_HEX, price_limit: (withdrawals) ? withdrawals.reduce((prev, curr) => ((BigInt(curr.price_limit) > BigInt(prev.price_limit)) ? curr : prev)).price_limit : types.ZERO_HEX, recipients: (withdrawals) ? new Array(16).fill("0x").map((_, i, __) => ((withdrawals[i]) ? withdrawals[i].recipient : types.ZERO_HEX)) : new Array(16).fill(types.ZERO_HEX), swap_amounts: (withdrawals) ? new Array(16).fill("0x").map((_, i, __) => ((withdrawals[i]) ? withdrawals[i].swap_amount : types.ZERO_HEX)) : new Array(16).fill(types.ZERO_HEX), uids: (utxo_encrypted) ? new Array(16).fill("0x").map((_, i, __) => ((utxo_encrypted[i]) ? utxo_encrypted[i].uid : types.ZERO_VALUE)) : new Array(16).fill(types.ZERO_VALUE), encrypted_utxo: (utxo_encrypted) ? new Array(16).fill("0x") .map((_, i, __) => { if (utxo_encrypted[i]) { return { secret: utxo_encrypted[i].secret_encrypted, amount: utxo_encrypted[i].amount_encrypted, data: utxo_encrypted[i].asset_type_encrypted } } else { return { secret: types.ZERO_VALUE, amount: types.ZERO_VALUE, data: types.ZERO_VALUE } } }) : new Array(16).fill({ secret: types.ZERO_VALUE, amount: types.ZERO_VALUE, data: types.ZERO_VALUE }) } } const _domain: types.TypedDataDomain = { name: "Momiji", version: "1", chainId: BigInt(1), verifyingContract: await this.contracts.state.getAddress() } const _types: types.TypedDataTypes = { DepositHash: [ { name: "pi_hash", type: "bytes32" } ] } const _values: types.TypedDataValues = { pi_hash: types.ZERO_HEX } console.log(inputs) inputs.contract_only_inputs.signature_hash = await keccak_tx_no_deposit( inputs.current_root, inputs.deposit_amount, inputs.withdrawals, inputs.utxo_spendable, inputs.utxo_new, inputs.contract_only_inputs ).then(hash => { _values.pi_hash = zeroPadValue(hash, 32) if (inputs.deposit_amount === types.toFixedHex(0, true)) return types.toFixedHex(0, true); else return zeroPadValue( Fr.fromBufferReduce( Buffer.from( TypedDataEncoder.hash(_domain, _types, _values).slice(2), "hex" ) ).toString(), 32 ); }) return inputs } async getSiblingsFromChain(utxo: UTXO_New): Promise<string[]> { const commitment: string = await this.utxo_to_commitment(utxo).then(c => zeroPadValue(c, 32)) const old_root: string = await this.contracts.state.utxoPrevRoots(commitment) const siblings: string[] = await this.contracts.state.queryFilter( this.contracts.state.filters.TransactionPublish(undefined, old_root, undefined) ) .then((events: any) => events.map((event: any) => event.args[0].commitments)) .then((commitments: any) => commitments.find((c: any) => c.includes(commitment))) if (!siblings) return Promise.reject("Siblings not found.") return siblings } async getUtxoCommitment(utxo: UTXO_New): Promise<string> { const commitment: string = await this.utxo_to_commitment(utxo) return commitment } async getTxHashFromUtxo(utxo: UTXO_New): Promise<string> { const commitment: string = await this.getUtxoCommitment(utxo) const oldRoot: string = await this.contracts.state.utxoPrevRoots(commitment) const txHash: string = await this.contracts.state.queryFilter( this.contracts.state.filters.TransactionPublish(undefined, oldRoot, undefined) ) .then(events => events.find(event => event.args[0].commitments.includes(commitment))) .then(event => (event ? event.transactionHash : types.toFixedHex(0, true))) return txHash } async _generateUTXOCommitments(inputs: types.TransactionInputsRaw, spend_in_same_batch: boolean[]): Promise<types.UTXO_Commitment[]> { const formattedInputs: TransactionInputs = await create_transaction( inputs.current_root, inputs.deposit_amount, inputs.withdrawals, inputs.utxo_spendable, inputs.utxo_new, inputs.contract_only_inputs ) const utxo_commitments: types.UTXO_Commitment[] = inputs.utxo_new.map((utxo, i, _) => { return { commitment: formattedInputs.public_inputs.commitment_out[i], secret: utxo.secret, amount: utxo.amount, asset_type: utxo.asset_type, siblings: formattedInputs.public_inputs.commitment_out, spend_in_same_batch: spend_in_same_batch[i] ? true : false } as types.UTXO_Commitment }) return utxo_commitments } async _generateFormattedTransactionInputs(inputs: types.TransactionInputsRaw): Promise<TransactionInputs> { const formattedInputs: TransactionInputs = await create_transaction( inputs.current_root, inputs.deposit_amount, inputs.withdrawals, inputs.utxo_spendable, inputs.utxo_new, inputs.contract_only_inputs ) return formattedInputs } async _generateMerkleProof( utxo_input: types.MerkleProofInput, tx_input: types.MerkleProofInput, state_input: types.MerkleProofInput ): Promise<types.Merkle_Proof_Struct> { const merkleProof: types.Merkle_Proof_Struct = { path_utxo: await utxo_input.tree.proof(utxo_input.tree.getIndex(utxo_input.leaf)) .then(proof => proof.pathElements), path_tx: await tx_input.tree.proof(tx_input.tree.getIndex(tx_input.leaf)) .then(proof => proof.pathElements), path_historic: await state_input.tree.proof(state_input.tree.getIndex(state_input.leaf)) .then(proof => proof.pathElements), index_utxo: types.toFixedHex(utxo_input.tree.getIndex(utxo_input.leaf), true), index_tx: types.toFixedHex(tx_input.tree.getIndex(tx_input.leaf), true), index_historic: types.toFixedHex(state_input.tree.getIndex(state_input.leaf), true) } return merkleProof } async getNewStateRoot(old_state_root: string): Promise<string> { const validRoots: string[] = await this.contracts.state.getValidRoots() const oldRootIndex: number = validRoots.indexOf(old_state_root) if (oldRootIndex === undefined) return Promise.reject("Root not found") const newStateRoot: string = validRoots[oldRootIndex + 1] return newStateRoot } async _generateUTXOSpendableSingle(utxo: types.UTXO_Commitment, hist_tree: MerkleTree): Promise<types.UTXO_Spendable_Struct> { const empty_utxo_spendable: types.UTXO_Spendable_Struct = { secret: types.ZERO_HEX, amount: types.ZERO_VALUE, asset_type: types.ZERO_VALUE, oracle: types.ZERO_VALUE, old_root_proof: types.ZERO_VALUE, merkle_proof: { path_utxo: new Array(types.treeConfig.utxoDepth).fill(types.ZERO_HEX), path_tx: new Array(types.treeConfig.txDepth).fill(types.ZERO_HEX), path_historic: new Array(types.treeConfig.stateDepth).fill(types.ZERO_HEX), index_utxo: types.ZERO_HEX, index_tx: types.ZERO_HEX, index_historic: types.ZERO_HEX }, spend_in_same_batch: false } if (!utxo.spend_in_same_batch) utxo.spend_in_same_batch = false; if (!utxo.commitment) utxo.commitment = await this.utxo_to_commitment(utxo); if (!utxo.siblings && !utxo.spend_in_same_batch) utxo.siblings = await this.getSiblingsFromChain(utxo); if (utxo.commitment === types.ZERO_VALUE) return empty_utxo_spendable if (utxo.spend_in_same_batch) return { secret: utxo.secret, amount: utxo.amount, asset_type: utxo.asset_type, oracle: types.ZERO_VALUE, old_root_proof: types.ZERO_HEX, merkle_proof: { path_utxo: new Array(types.treeConfig.utxoDepth).fill(types.ZERO_HEX), path_tx: new Array(types.treeConfig.txDepth).fill(types.ZERO_HEX), path_historic: new Array(types.treeConfig.stateDepth).fill(types.ZERO_HEX), index_utxo: types.ZERO_HEX, index_tx: types.ZERO_HEX, index_historic: types.ZERO_HEX }, spend_in_same_batch: true } const utxo_tree: MerkleTree = new MerkleTree( types.treeConfig.utxoDepth, utxo.siblings ) await utxo_tree.init() const old_root: string = (utxo.commitment !== types.ZERO_VALUE && utxo.spend_in_same_batch === false) ? await this.contracts.state.utxoPrevRoots(zeroPadValue(utxo.commitment, 32)) : types.ZERO_HEX const new_root: string = (old_root === types.ZERO_HEX) ? types.ZERO_HEX : await this.getNewStateRoot(old_root) const tx_tree: MerkleTree = await this.getBatchTransactions(old_root) .then(transactions => new MerkleTree(types.treeConfig.txDepth, transactions)) await tx_tree.init() const utxo_input: types.MerkleProofInput = { leaf: utxo.commitment, tree: utxo_tree } const tx_input: types.MerkleProofInput = { leaf: utxo_tree.root(), tree: tx_tree } const state_input: types.MerkleProofInput = { leaf: new_root, tree: hist_tree } const utxo_spendable: types.UTXO_Spendable_Struct = { secret: utxo.secret, amount: utxo.amount, asset_type: utxo.asset_type, oracle: await this.getBatchOracle(old_root), old_root_proof: old_root, merkle_proof: await this._generateMerkleProof(utxo_input, tx_input, state_input), spend_in_same_batch: utxo.spend_in_same_batch } return utxo_spendable } async _generateUTXOSpendable(utxos?: types.UTXO_Commitment[]): Promise<types.UTXO_Spendable_Struct[]> { const empty_utxo_spendable: types.UTXO_Spendable_Struct = { secret: types.ZERO_HEX, amount: types.ZERO_VALUE, asset_type: types.ZERO_VALUE, oracle: types.ZERO_VALUE, old_root_proof: types.ZERO_VALUE, merkle_proof: { path_utxo: new Array(types.treeConfig.utxoDepth).fill(types.ZERO_HEX), path_tx: new Array(types.treeConfig.txDepth).fill(types.ZERO_HEX), path_historic: new Array(types.treeConfig.stateDepth).fill(types.ZERO_HEX), index_utxo: types.ZERO_HEX, index_tx: types.ZERO_HEX, index_historic: types.ZERO_HEX }, spend_in_same_batch: false } const utxo_spendable_empty_array: types.UTXO_Spendable_Struct[] = new Array(16).fill(empty_utxo_spendable) if (!utxos) return utxo_spendable_empty_array; const hist_tree: MerkleTree = await this.contracts.state.getValidRoots() .then((roots: any) => new MerkleTree(types.treeConfig.stateDepth, roots)) await hist_tree.init() let utxo_spendable_array: types.UTXO_Spendable_Struct[] = await Promise.all( utxos.map(utxo => this._generateUTXOSpendableSingle(utxo, hist_tree)) ) utxo_spendable_array = utxo_spendable_array.concat(new Array(16 - utxos.length).fill(empty_utxo_spendable)) return utxo_spendable_array } async getBatchTransactions(old_root: string): Promise<string[]> { const transactions: string[] = await this.contracts.state.queryFilter( this.contracts.state.filters.TransactionPublish(undefined, old_root, undefined) ) .then((events: any) => events.map((event: any) => event.args[0].utxo_root)) return transactions } async getBatchOracle(root: string): Promise<string> { const index = await this.contracts.state.getValidRoots() .then((roots: any) => roots.indexOf(root)) const oracle: string = await this.contracts.state.queryFilter( this.contracts.state.filters.BatchPublish(index, undefined, undefined, undefined, undefined) ) .then((events: any) => { if (events.length === 0) return Promise.reject("No batch for this root"); else return events[0].args[3]; }) return oracle } async _generateTransactionDepositSignature(inputs: types.TransactionInputsRaw): Promise<types.Deposit> { if (!this.config.signer) throw new Error("Signer not initialized"); const _domain: types.TypedDataDomain = { name: "Momiji", version: "1", chainId: BigInt(1), verifyingContract: await this.contracts.state.getAddress() } const _types: types.TypedDataTypes = { DepositHash: [ { name: "pi_hash", type: "bytes32" } ] } const _values: types.TypedDataValues = { pi_hash: await keccak_tx_no_deposit( inputs.current_root, inputs.deposit_amount, inputs.withdrawals, inputs.utxo_spendable, inputs.utxo_new, inputs.contract_only_inputs ).then(hash => zeroPadValue(hash, 32)) } const deposit: types.Deposit = { signature: (BigInt(inputs.deposit_amount) === BigInt(types.ZERO_HEX)) ? "0x00" : await this.config.signer.signTypedData(_domain, _types, _values), pi_hash: inputs.contract_only_inputs.signature_hash } return deposit } async _generateTransactionContractInputs(inputs: types.TransactionInputsRaw): Promise<types.ContractTransaction> { const formattedInputs = await this._generateFormattedTransactionInputs(inputs) const deposit = await this._generateTransactionDepositSignature(inputs) const contract_inputs: types.ContractTransaction = { current_root: await this.contracts.state.histRoot(), utxo_root: zeroPadValue(formattedInputs.public_inputs.utxo_root, 32), price_limit: zeroPadValue(inputs.contract_only_inputs.price_limit, 32), timestamp: zeroPadValue(inputs.contract_only_inputs.timestamp, 32), deadline: zeroPadValue(inputs.contract_only_inputs.deadline, 32), amount: zeroPadValue(formattedInputs.public_inputs.deposit_amount, 32), encrypted_utxo: inputs.contract_only_inputs.encrypted_utxo .map(utxo => { return { secret: zeroPadValue(utxo.secret, 32), amount: zeroPadValue(utxo.amount, 32), data: zeroPadValue(utxo.data, 32) } as types.EncryptedUTXO }), withdrawals: inputs.withdrawals, commitments_in: formattedInputs.public_inputs.commitment_in.map(c => zeroPadValue(c, 32)), commitments: formattedInputs.public_inputs.commitment_out.map(c => zeroPadValue(c, 32)), recipients: inputs.contract_only_inputs.recipients.map(r => zeroPadValue(r, 32)), nullifier_hashes: formattedInputs.public_inputs.nullifier_hashes.map(hash => zeroPadValue(hash, 32)), uids: inputs.contract_only_inputs.uids.map(uid => zeroPadValue(uid, 32)), swap_amounts: inputs.contract_only_inputs.swap_amounts.map(swap => zeroPadValue(swap, 32)), deposit: deposit } return contract_inputs } async _generateTransactionProofArtifacts(inputs: types.TransactionInputsRaw): Promise<types.ProofArtifacts> { const formattedInputs: TransactionInputs = await create_transaction( inputs.current_root, inputs.deposit_amount, inputs.withdrawals, inputs.utxo_spendable, inputs.utxo_new, inputs.contract_only_inputs ) const toml = json2toml(formattedInputs) let proof_artifacts: types.ProofArtifacts; let tx_proof = await this.noirs.transaction.execute(formattedInputs) .then(w => this.backends.transaction.generateProof(w.witness)) let artifacts = await this.backends.transaction.generateRecursiveProofArtifacts(tx_proof, 1) proof_artifacts = { proofData: tx_proof, proofAsFields: artifacts.proofAsFields, vkAsFields: artifacts.vkAsFields, vkHash: artifacts.vkHash } return proof_artifacts } async _generateTransactionProof( utxo_commitments?: types.UTXO_Commitment[], utxo_encrypted?: types.UTXO_Encrypted[], withdrawals?: types.WithdrawalSwap[] ): Promise<types.Transaction> { const inputs: types.TransactionInputsRaw = await this._generateRawTransactionInputs( utxo_commitments, utxo_encrypted, withdrawals ) return { public_inputs: await this._generateFormattedTransactionInputs(inputs).then(formatted => formatted.public_inputs), contract_inputs: await this._generateTransactionContractInputs(inputs), proof_artifacts: await this._generateTransactionProofArtifacts(inputs) } } async _generateTransactionProofExpert( deposit?: bigint, utxo_commitments?: types.UTXO_Commitment[], utxo_encrypted?: types.UTXO_Encrypted[], withdrawals?: types.WithdrawalSwap[] ): Promise<types.Transaction> { const inputs: types.TransactionInputsRaw = await this._generateRawTransactionInputsExpert( deposit, utxo_commitments, utxo_encrypted, withdrawals ) return { public_inputs: await this._generateFormattedTransactionInputs(inputs).then(formatted => formatted.public_inputs), contract_inputs: await this._generateTransactionContractInputs(inputs), proof_artifacts: await this._generateTransactionProofArtifacts(inputs) } } async generateTransactions(inputs: types.TransactionIO[]): Promise<types.Transaction[]> { const transactions: types.Transaction[] = new Array(inputs.length) for (let i = 0; i < inputs.length; i++) { console.log(`Transaction proof ${i}`) transactions[i] = await this._generateTransactionProof( inputs[i].utxo_commitment, inputs[i].utxo_encrypted, inputs[i].withdrawal ) } return transactions } async wrapTransaction(transaction: types.Transaction, txProofArtifacts: types.ProofArtifacts): Promise<Verifier> { const public_inputs_hash: string = await tx_as_hash(transaction.public_inputs) const wrapperInputs: InputMap = { public_inputs_hash: public_inputs_hash, transaction_verifier: { key_hash: txProofArtifacts.vkHash, verification_key: txProofArtifacts.vkAsFields, proof: txProofArtifacts.proofAsFields, } } let tx_aggregation_object: string[] let wrapperArtifacts: types.ProofArtifacts; let wrapper_proof = await this.noirs.transaction_wrapper.execute(wrapperInputs) .then((wrapper_witness: any) => this.backends.transaction_wrapper.generateProof(wrapper_witness.witness)); tx_aggregation_object = wrapper_proof.publicInputs.slice(1); let artifacts = await this.backends.transaction_wrapper.generateRecursiveProofArtifacts(wrapper_proof, 17); wrapperArtifacts = { proofData: wrapper_proof, proofAsFields: artifacts.proofAsFields, vkAsFields: artifacts.vkAsFields, vkHash: artifacts.vkHash } const txWrapperVerifier: Verifier = { key_hash: wrapperArtifacts.vkHash, proof: wrapperArtifacts.proofAsFields, verification_key: wrapperArtifacts.vkAsFields, aggregation_object: tx_aggregation_object } txWrapperVerifier.proof = tx_aggregation_object.concat(txWrapperVerifier.proof) return txWrapperVerifier } }