From 1adebfb820aee6b1dfad7da411270659f3837393 Mon Sep 17 00:00:00 2001 From: zy0n Date: Tue, 16 Dec 2025 14:12:42 -0500 Subject: [PATCH 1/4] feat: Add RelayAdapt7702 helper and related functionality - Implement RelayAdapt7702Helper for generating transactions. - Create tests for RelayAdapt7702Helper to ensure correct request generation. - Introduce deriveEphemeralWallet function for ephemeral key derivation. - Add EIP-7702 signing functionality with signEIP7702Authorization. - Implement execution signature signing with signExecutionAuthorization. - Define RelayAdapt7702Request type for structured transaction requests. - Update index files to export new modules and types. - Add tests for ephemeral key derivation and EIP-7702 signing. feat: export RelayAdapt and RelayAdapt7702 related types and factories feat: implement RelayAdapt7702 validation and ephemeral wallet functionality feat: add RelayAdapt7702 integration tests and update helper methods Refactor RelayAdapt7702Contract methods to streamline ephemeral address handling and enhance test coverage for cross contract calls Add methods for handling ephemeral addresses and signing EIP-7702 requests in RailgunWallet feat: Add ShieldLegacy event and update RailgunSmartWallet interface - Introduced ShieldLegacy event in RailgunSmartWallet with appropriate input and output types. - Updated existing methods in RailgunSmartWallet to change state mutability from "payable" to "nonpayable". - Modified RailgunSmartWallet factory to include the new ShieldLegacy event in the ABI. - Refactored RelayAdapt7702 factory to simplify the structure and remove unnecessary constructor logic. - Updated RelayAdapt7702Contract and RelayAdapt7702Helper to use the new ABI structure. feat: Add RelayAdapt7702Deployer contract and related ABI files - Introduced RelayAdapt7702Deployer contract with functions for deployment and ownership management. - Added ABI JSON file for RelayAdapt7702Deployer. - Updated typechain files to include RelayAdapt7702Deployer interface and factory. - Enhanced RelayAdapt7702 interface with new function signatures and events. - Modified transaction signature handling to accommodate new execute function signature. feat: Integrate RelayAdapt7702 and RelayAdapt7702Deployer contracts into the engine --- src/__tests__/railgun-engine.test.ts | 2 + src/abi/V2/RelayAdapt7702.json | 1068 +++++++++ src/abi/V2/RelayAdapt7702Deployer.json | 164 ++ src/abi/abi.ts | 4 + src/abi/typechain/RelayAdapt7702.ts | 609 +++++ src/abi/typechain/RelayAdapt7702Deployer.ts | 276 +++ .../RelayAdapt7702Deployer__factory.ts | 191 ++ .../factories/RelayAdapt7702__factory.ts | 1091 +++++++++ src/contracts/contract-store.ts | 4 + .../railgun-versioned-smart-contracts.ts | 5 +- .../V2/relay-adapt-7702-deployer.ts | 27 + .../relay-adapt/V2/relay-adapt-7702.ts | 516 +++++ .../__tests__/relay-adapt-7702.test.ts | 2037 +++++++++++++++++ src/contracts/relay-adapt/index.ts | 3 + .../relay-adapt/relay-adapt-7702-helper.ts | 238 ++ .../relay-adapt/relay-adapt-types.ts | 6 + .../relay-adapt-versioned-smart-contracts.ts | 102 +- src/index.ts | 1 + .../__tests__/ephemeral-key.test.ts | 30 + src/key-derivation/ephemeral-key.ts | 15 + src/key-derivation/index.ts | 1 + src/models/index.ts | 1 + src/models/relay-adapt-types.ts | 19 + src/models/typechain-types.ts | 4 +- src/railgun-engine.ts | 52 + src/test/config.test.ts | 2 + src/transaction/__tests__/eip7702.test.ts | 38 + .../relay-adapt-7702-signature.test.ts | 108 + src/transaction/eip7702.ts | 49 + src/transaction/index.ts | 2 + src/transaction/relay-adapt-7702-signature.ts | 57 + .../relay-adapt-7702-validator.test.ts | 18 + src/validation/extract-transaction-data-v2.ts | 15 +- src/validation/extract-transaction-data.ts | 4 + src/validation/index.ts | 1 + src/validation/relay-adapt-7702-validator.ts | 59 + src/wallet/abstract-wallet.ts | 2 + src/wallet/railgun-wallet.ts | 100 +- 38 files changed, 6911 insertions(+), 10 deletions(-) create mode 100644 src/abi/V2/RelayAdapt7702.json create mode 100644 src/abi/V2/RelayAdapt7702Deployer.json create mode 100644 src/abi/typechain/RelayAdapt7702.ts create mode 100644 src/abi/typechain/RelayAdapt7702Deployer.ts create mode 100644 src/abi/typechain/factories/RelayAdapt7702Deployer__factory.ts create mode 100644 src/abi/typechain/factories/RelayAdapt7702__factory.ts create mode 100644 src/contracts/relay-adapt/V2/relay-adapt-7702-deployer.ts create mode 100644 src/contracts/relay-adapt/V2/relay-adapt-7702.ts create mode 100644 src/contracts/relay-adapt/__tests__/relay-adapt-7702.test.ts create mode 100644 src/contracts/relay-adapt/relay-adapt-7702-helper.ts create mode 100644 src/contracts/relay-adapt/relay-adapt-types.ts create mode 100644 src/key-derivation/__tests__/ephemeral-key.test.ts create mode 100644 src/key-derivation/ephemeral-key.ts create mode 100644 src/models/relay-adapt-types.ts create mode 100644 src/transaction/__tests__/eip7702.test.ts create mode 100644 src/transaction/__tests__/relay-adapt-7702-signature.test.ts create mode 100644 src/transaction/eip7702.ts create mode 100644 src/transaction/relay-adapt-7702-signature.ts create mode 100644 src/validation/__tests__/relay-adapt-7702-validator.test.ts create mode 100644 src/validation/relay-adapt-7702-validator.ts diff --git a/src/__tests__/railgun-engine.test.ts b/src/__tests__/railgun-engine.test.ts index 11607a01..8aebea8e 100644 --- a/src/__tests__/railgun-engine.test.ts +++ b/src/__tests__/railgun-engine.test.ts @@ -321,6 +321,8 @@ describe('railgun-engine', function test() { { [TXIDVersion.V2_PoseidonMerkle]: 24, [TXIDVersion.V3_PoseidonMerkle]: 24 }, 0, !isV2Test(), // supportsV3 + config.contracts.relayAdapt7702, + config.contracts.adapt7702Deployer, ); const balance = await token.balanceOf(ethersWallet.address); diff --git a/src/abi/V2/RelayAdapt7702.json b/src/abi/V2/RelayAdapt7702.json new file mode 100644 index 00000000..f98a2c90 --- /dev/null +++ b/src/abi/V2/RelayAdapt7702.json @@ -0,0 +1,1068 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_railgun", + "type": "address" + }, + { + "internalType": "address", + "name": "_wBase", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "callIndex", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "revertReason", + "type": "bytes" + } + ], + "name": "CallFailed", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "callIndex", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "revertReason", + "type": "bytes" + } + ], + "name": "CallError", + "type": "event" + }, + { + "inputs": [], + "name": "ACTION_DATA_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "CALL_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MULTICALL_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "RELAY_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "TRANSACT_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "adaptImplementation", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "components": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "internalType": "struct G1Point", + "name": "a", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256[2]", + "name": "x", + "type": "uint256[2]" + }, + { + "internalType": "uint256[2]", + "name": "y", + "type": "uint256[2]" + } + ], + "internalType": "struct G2Point", + "name": "b", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "internalType": "struct G1Point", + "name": "c", + "type": "tuple" + } + ], + "internalType": "struct SnarkProof", + "name": "proof", + "type": "tuple" + }, + { + "internalType": "bytes32", + "name": "merkleRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "nullifiers", + "type": "bytes32[]" + }, + { + "internalType": "bytes32[]", + "name": "commitments", + "type": "bytes32[]" + }, + { + "components": [ + { + "internalType": "uint16", + "name": "treeNumber", + "type": "uint16" + }, + { + "internalType": "uint72", + "name": "minGasPrice", + "type": "uint72" + }, + { + "internalType": "enum UnshieldType", + "name": "unshield", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "chainID", + "type": "uint64" + }, + { + "internalType": "address", + "name": "adaptContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "adaptParams", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "bytes32[4]", + "name": "ciphertext", + "type": "bytes32[4]" + }, + { + "internalType": "bytes32", + "name": "blindedSenderViewingKey", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "blindedReceiverViewingKey", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "annotationData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "memo", + "type": "bytes" + } + ], + "internalType": "struct CommitmentCiphertext[]", + "name": "commitmentCiphertext", + "type": "tuple[]" + } + ], + "internalType": "struct BoundParams", + "name": "boundParams", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "npk", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "enum TokenType", + "name": "tokenType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenSubID", + "type": "uint256" + } + ], + "internalType": "struct TokenData", + "name": "token", + "type": "tuple" + }, + { + "internalType": "uint120", + "name": "value", + "type": "uint120" + } + ], + "internalType": "struct CommitmentPreimage", + "name": "unshieldPreimage", + "type": "tuple" + } + ], + "internalType": "struct Transaction[]", + "name": "_transactions", + "type": "tuple[]" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" + } + ], + "name": "execute", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "components": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "internalType": "struct G1Point", + "name": "a", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256[2]", + "name": "x", + "type": "uint256[2]" + }, + { + "internalType": "uint256[2]", + "name": "y", + "type": "uint256[2]" + } + ], + "internalType": "struct G2Point", + "name": "b", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "internalType": "struct G1Point", + "name": "c", + "type": "tuple" + } + ], + "internalType": "struct SnarkProof", + "name": "proof", + "type": "tuple" + }, + { + "internalType": "bytes32", + "name": "merkleRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "nullifiers", + "type": "bytes32[]" + }, + { + "internalType": "bytes32[]", + "name": "commitments", + "type": "bytes32[]" + }, + { + "components": [ + { + "internalType": "uint16", + "name": "treeNumber", + "type": "uint16" + }, + { + "internalType": "uint72", + "name": "minGasPrice", + "type": "uint72" + }, + { + "internalType": "enum UnshieldType", + "name": "unshield", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "chainID", + "type": "uint64" + }, + { + "internalType": "address", + "name": "adaptContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "adaptParams", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "bytes32[4]", + "name": "ciphertext", + "type": "bytes32[4]" + }, + { + "internalType": "bytes32", + "name": "blindedSenderViewingKey", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "blindedReceiverViewingKey", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "annotationData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "memo", + "type": "bytes" + } + ], + "internalType": "struct CommitmentCiphertext[]", + "name": "commitmentCiphertext", + "type": "tuple[]" + } + ], + "internalType": "struct BoundParams", + "name": "boundParams", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "npk", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "enum TokenType", + "name": "tokenType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenSubID", + "type": "uint256" + } + ], + "internalType": "struct TokenData", + "name": "token", + "type": "tuple" + }, + { + "internalType": "uint120", + "name": "value", + "type": "uint120" + } + ], + "internalType": "struct CommitmentPreimage", + "name": "unshieldPreimage", + "type": "tuple" + } + ], + "internalType": "struct Transaction[]", + "name": "_transactions", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "bytes31", + "name": "random", + "type": "bytes31" + }, + { + "internalType": "bool", + "name": "requireSuccess", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "minGasLimit", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "internalType": "struct RelayAdapt7702.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "internalType": "struct RelayAdapt7702.ActionData", + "name": "_actionData", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" + } + ], + "name": "execute", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "components": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "internalType": "struct G1Point", + "name": "a", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256[2]", + "name": "x", + "type": "uint256[2]" + }, + { + "internalType": "uint256[2]", + "name": "y", + "type": "uint256[2]" + } + ], + "internalType": "struct G2Point", + "name": "b", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "x", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "y", + "type": "uint256" + } + ], + "internalType": "struct G1Point", + "name": "c", + "type": "tuple" + } + ], + "internalType": "struct SnarkProof", + "name": "proof", + "type": "tuple" + }, + { + "internalType": "bytes32", + "name": "merkleRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "nullifiers", + "type": "bytes32[]" + }, + { + "internalType": "bytes32[]", + "name": "commitments", + "type": "bytes32[]" + }, + { + "components": [ + { + "internalType": "uint16", + "name": "treeNumber", + "type": "uint16" + }, + { + "internalType": "uint72", + "name": "minGasPrice", + "type": "uint72" + }, + { + "internalType": "enum UnshieldType", + "name": "unshield", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "chainID", + "type": "uint64" + }, + { + "internalType": "address", + "name": "adaptContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "adaptParams", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "bytes32[4]", + "name": "ciphertext", + "type": "bytes32[4]" + }, + { + "internalType": "bytes32", + "name": "blindedSenderViewingKey", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "blindedReceiverViewingKey", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "annotationData", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "memo", + "type": "bytes" + } + ], + "internalType": "struct CommitmentCiphertext[]", + "name": "commitmentCiphertext", + "type": "tuple[]" + } + ], + "internalType": "struct BoundParams", + "name": "boundParams", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "npk", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "enum TokenType", + "name": "tokenType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenSubID", + "type": "uint256" + } + ], + "internalType": "struct TokenData", + "name": "token", + "type": "tuple" + }, + { + "internalType": "uint120", + "name": "value", + "type": "uint120" + } + ], + "internalType": "struct CommitmentPreimage", + "name": "unshieldPreimage", + "type": "tuple" + } + ], + "internalType": "struct Transaction[]", + "name": "_transactions", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "bytes31", + "name": "random", + "type": "bytes31" + }, + { + "internalType": "bool", + "name": "requireSuccess", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "minGasLimit", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "internalType": "struct RelayAdapt7702.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "internalType": "struct RelayAdapt7702.ActionData", + "name": "_actionData", + "type": "tuple" + } + ], + "name": "getAdaptParams", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "_requireSuccess", + "type": "bool" + }, + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "internalType": "struct RelayAdapt7702.Call[]", + "name": "_calls", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "_nonce", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_signature", + "type": "bytes" + } + ], + "name": "multicall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "nonce", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "railgun", + "outputs": [ + { + "internalType": "contract RailgunSmartWallet", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "npk", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "enum TokenType", + "name": "tokenType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenSubID", + "type": "uint256" + } + ], + "internalType": "struct TokenData", + "name": "token", + "type": "tuple" + }, + { + "internalType": "uint120", + "name": "value", + "type": "uint120" + } + ], + "internalType": "struct CommitmentPreimage", + "name": "preimage", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bytes32[3]", + "name": "encryptedBundle", + "type": "bytes32[3]" + }, + { + "internalType": "bytes32", + "name": "shieldKey", + "type": "bytes32" + } + ], + "internalType": "struct ShieldCiphertext", + "name": "ciphertext", + "type": "tuple" + } + ], + "internalType": "struct ShieldRequest[]", + "name": "_shieldRequests", + "type": "tuple[]" + } + ], + "name": "shield", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "enum TokenType", + "name": "tokenType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenSubID", + "type": "uint256" + } + ], + "internalType": "struct TokenData", + "name": "token", + "type": "tuple" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "internalType": "struct RelayAdapt7702.TokenTransfer[]", + "name": "_transfers", + "type": "tuple[]" + } + ], + "name": "transfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "unwrapBase", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "wBase", + "outputs": [ + { + "internalType": "contract IWBase", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "wrapBase", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/src/abi/V2/RelayAdapt7702Deployer.json b/src/abi/V2/RelayAdapt7702Deployer.json new file mode 100644 index 00000000..919fd7e6 --- /dev/null +++ b/src/abi/V2/RelayAdapt7702Deployer.json @@ -0,0 +1,164 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "deployment", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + } + ], + "name": "Deployed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "deployment", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "status", + "type": "bool" + } + ], + "name": "DeploymentStatusChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "creationCode", + "type": "bytes" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + } + ], + "name": "deploy", + "outputs": [ + { + "internalType": "address", + "name": "deployment", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isDeployed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "deployment", + "type": "address" + }, + { + "internalType": "bool", + "name": "status", + "type": "bool" + } + ], + "name": "setDeploymentStatus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/abi/abi.ts b/src/abi/abi.ts index 01eee115..3cb253eb 100644 --- a/src/abi/abi.ts +++ b/src/abi/abi.ts @@ -4,6 +4,8 @@ import ABIRailgunLogic_LegacyEvents from './V1/RailgunLogic_LegacyEvents.json'; // V2 import ABIRailgunSmartWallet_Legacy_PreMar23 from './V2/RailgunSmartWallet_Legacy_PreMar23.json'; import ABIRelayAdapt from './V2/RelayAdapt.json'; +import ABIRelayAdapt7702 from './V2/RelayAdapt7702.json'; +import ABIRelayAdapt7702Deployer from './V2/RelayAdapt7702Deployer.json'; // V2.1 import ABIRailgunSmartWallet from './V2.1/RailgunSmartWallet.json'; @@ -18,6 +20,8 @@ export { ABIRailgunSmartWallet_Legacy_PreMar23, ABIRailgunSmartWallet, ABIRelayAdapt, + ABIRelayAdapt7702, + ABIRelayAdapt7702Deployer, ABIPoseidonMerkleAccumulator, ABIPoseidonMerkleVerifier, ABITokenVault, diff --git a/src/abi/typechain/RelayAdapt7702.ts b/src/abi/typechain/RelayAdapt7702.ts new file mode 100644 index 00000000..43d44c9e --- /dev/null +++ b/src/abi/typechain/RelayAdapt7702.ts @@ -0,0 +1,609 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumberish, + BytesLike, + FunctionFragment, + Result, + Interface, + EventFragment, + AddressLike, + ContractRunner, + ContractMethod, + Listener, +} from "ethers"; +import type { + TypedContractEvent, + TypedDeferredTopicFilter, + TypedEventLog, + TypedLogDescription, + TypedListener, + TypedContractMethod, +} from "./common"; + +export type G1PointStruct = { x: BigNumberish; y: BigNumberish }; + +export type G1PointStructOutput = [x: bigint, y: bigint] & { + x: bigint; + y: bigint; +}; + +export type G2PointStruct = { + x: [BigNumberish, BigNumberish]; + y: [BigNumberish, BigNumberish]; +}; + +export type G2PointStructOutput = [x: [bigint, bigint], y: [bigint, bigint]] & { + x: [bigint, bigint]; + y: [bigint, bigint]; +}; + +export type SnarkProofStruct = { + a: G1PointStruct; + b: G2PointStruct; + c: G1PointStruct; +}; + +export type SnarkProofStructOutput = [ + a: G1PointStructOutput, + b: G2PointStructOutput, + c: G1PointStructOutput +] & { a: G1PointStructOutput; b: G2PointStructOutput; c: G1PointStructOutput }; + +export type CommitmentCiphertextStruct = { + ciphertext: [BytesLike, BytesLike, BytesLike, BytesLike]; + blindedSenderViewingKey: BytesLike; + blindedReceiverViewingKey: BytesLike; + annotationData: BytesLike; + memo: BytesLike; +}; + +export type CommitmentCiphertextStructOutput = [ + ciphertext: [string, string, string, string], + blindedSenderViewingKey: string, + blindedReceiverViewingKey: string, + annotationData: string, + memo: string +] & { + ciphertext: [string, string, string, string]; + blindedSenderViewingKey: string; + blindedReceiverViewingKey: string; + annotationData: string; + memo: string; +}; + +export type BoundParamsStruct = { + treeNumber: BigNumberish; + minGasPrice: BigNumberish; + unshield: BigNumberish; + chainID: BigNumberish; + adaptContract: AddressLike; + adaptParams: BytesLike; + commitmentCiphertext: CommitmentCiphertextStruct[]; +}; + +export type BoundParamsStructOutput = [ + treeNumber: bigint, + minGasPrice: bigint, + unshield: bigint, + chainID: bigint, + adaptContract: string, + adaptParams: string, + commitmentCiphertext: CommitmentCiphertextStructOutput[] +] & { + treeNumber: bigint; + minGasPrice: bigint; + unshield: bigint; + chainID: bigint; + adaptContract: string; + adaptParams: string; + commitmentCiphertext: CommitmentCiphertextStructOutput[]; +}; + +export type TokenDataStruct = { + tokenType: BigNumberish; + tokenAddress: AddressLike; + tokenSubID: BigNumberish; +}; + +export type TokenDataStructOutput = [ + tokenType: bigint, + tokenAddress: string, + tokenSubID: bigint +] & { tokenType: bigint; tokenAddress: string; tokenSubID: bigint }; + +export type CommitmentPreimageStruct = { + npk: BytesLike; + token: TokenDataStruct; + value: BigNumberish; +}; + +export type CommitmentPreimageStructOutput = [ + npk: string, + token: TokenDataStructOutput, + value: bigint +] & { npk: string; token: TokenDataStructOutput; value: bigint }; + +export type TransactionStruct = { + proof: SnarkProofStruct; + merkleRoot: BytesLike; + nullifiers: BytesLike[]; + commitments: BytesLike[]; + boundParams: BoundParamsStruct; + unshieldPreimage: CommitmentPreimageStruct; +}; + +export type TransactionStructOutput = [ + proof: SnarkProofStructOutput, + merkleRoot: string, + nullifiers: string[], + commitments: string[], + boundParams: BoundParamsStructOutput, + unshieldPreimage: CommitmentPreimageStructOutput +] & { + proof: SnarkProofStructOutput; + merkleRoot: string; + nullifiers: string[]; + commitments: string[]; + boundParams: BoundParamsStructOutput; + unshieldPreimage: CommitmentPreimageStructOutput; +}; + +export type ShieldCiphertextStruct = { + encryptedBundle: [BytesLike, BytesLike, BytesLike]; + shieldKey: BytesLike; +}; + +export type ShieldCiphertextStructOutput = [ + encryptedBundle: [string, string, string], + shieldKey: string +] & { encryptedBundle: [string, string, string]; shieldKey: string }; + +export type ShieldRequestStruct = { + preimage: CommitmentPreimageStruct; + ciphertext: ShieldCiphertextStruct; +}; + +export type ShieldRequestStructOutput = [ + preimage: CommitmentPreimageStructOutput, + ciphertext: ShieldCiphertextStructOutput +] & { + preimage: CommitmentPreimageStructOutput; + ciphertext: ShieldCiphertextStructOutput; +}; + +export declare namespace RelayAdapt7702 { + export type CallStruct = { + to: AddressLike; + data: BytesLike; + value: BigNumberish; + }; + + export type CallStructOutput = [to: string, data: string, value: bigint] & { + to: string; + data: string; + value: bigint; + }; + + export type ActionDataStruct = { + random: BytesLike; + requireSuccess: boolean; + minGasLimit: BigNumberish; + calls: RelayAdapt7702.CallStruct[]; + }; + + export type ActionDataStructOutput = [ + random: string, + requireSuccess: boolean, + minGasLimit: bigint, + calls: RelayAdapt7702.CallStructOutput[] + ] & { + random: string; + requireSuccess: boolean; + minGasLimit: bigint; + calls: RelayAdapt7702.CallStructOutput[]; + }; + + export type TokenTransferStruct = { + token: TokenDataStruct; + to: AddressLike; + value: BigNumberish; + }; + + export type TokenTransferStructOutput = [ + token: TokenDataStructOutput, + to: string, + value: bigint + ] & { token: TokenDataStructOutput; to: string; value: bigint }; +} + +export interface RelayAdapt7702Interface extends Interface { + getFunction( + nameOrSignature: + | "ACTION_DATA_TYPEHASH" + | "CALL_TYPEHASH" + | "DOMAIN_SEPARATOR" + | "MULTICALL_TYPEHASH" + | "RELAY_TYPEHASH" + | "TRANSACT_TYPEHASH" + | "adaptImplementation" + | "execute((((uint256,uint256),(uint256[2],uint256[2]),(uint256,uint256)),bytes32,bytes32[],bytes32[],(uint16,uint72,uint8,uint64,address,bytes32,(bytes32[4],bytes32,bytes32,bytes,bytes)[]),(bytes32,(uint8,address,uint256),uint120))[],bytes)" + | "execute((((uint256,uint256),(uint256[2],uint256[2]),(uint256,uint256)),bytes32,bytes32[],bytes32[],(uint16,uint72,uint8,uint64,address,bytes32,(bytes32[4],bytes32,bytes32,bytes,bytes)[]),(bytes32,(uint8,address,uint256),uint120))[],(bytes31,bool,uint256,(address,bytes,uint256)[]),bytes)" + | "getAdaptParams" + | "multicall" + | "nonce" + | "railgun" + | "shield" + | "transfer" + | "unwrapBase" + | "wBase" + | "wrapBase" + ): FunctionFragment; + + getEvent(nameOrSignatureOrTopic: "CallError"): EventFragment; + + encodeFunctionData( + functionFragment: "ACTION_DATA_TYPEHASH", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "CALL_TYPEHASH", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "DOMAIN_SEPARATOR", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "MULTICALL_TYPEHASH", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "RELAY_TYPEHASH", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "TRANSACT_TYPEHASH", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "adaptImplementation", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "execute((((uint256,uint256),(uint256[2],uint256[2]),(uint256,uint256)),bytes32,bytes32[],bytes32[],(uint16,uint72,uint8,uint64,address,bytes32,(bytes32[4],bytes32,bytes32,bytes,bytes)[]),(bytes32,(uint8,address,uint256),uint120))[],bytes)", + values: [TransactionStruct[], BytesLike] + ): string; + encodeFunctionData( + functionFragment: "execute((((uint256,uint256),(uint256[2],uint256[2]),(uint256,uint256)),bytes32,bytes32[],bytes32[],(uint16,uint72,uint8,uint64,address,bytes32,(bytes32[4],bytes32,bytes32,bytes,bytes)[]),(bytes32,(uint8,address,uint256),uint120))[],(bytes31,bool,uint256,(address,bytes,uint256)[]),bytes)", + values: [TransactionStruct[], RelayAdapt7702.ActionDataStruct, BytesLike] + ): string; + encodeFunctionData( + functionFragment: "getAdaptParams", + values: [TransactionStruct[], RelayAdapt7702.ActionDataStruct] + ): string; + encodeFunctionData( + functionFragment: "multicall", + values: [boolean, RelayAdapt7702.CallStruct[], BigNumberish, BytesLike] + ): string; + encodeFunctionData(functionFragment: "nonce", values?: undefined): string; + encodeFunctionData(functionFragment: "railgun", values?: undefined): string; + encodeFunctionData( + functionFragment: "shield", + values: [ShieldRequestStruct[]] + ): string; + encodeFunctionData( + functionFragment: "transfer", + values: [RelayAdapt7702.TokenTransferStruct[]] + ): string; + encodeFunctionData( + functionFragment: "unwrapBase", + values: [BigNumberish] + ): string; + encodeFunctionData(functionFragment: "wBase", values?: undefined): string; + encodeFunctionData( + functionFragment: "wrapBase", + values: [BigNumberish] + ): string; + + decodeFunctionResult( + functionFragment: "ACTION_DATA_TYPEHASH", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "CALL_TYPEHASH", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "DOMAIN_SEPARATOR", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "MULTICALL_TYPEHASH", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "RELAY_TYPEHASH", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "TRANSACT_TYPEHASH", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "adaptImplementation", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "execute((((uint256,uint256),(uint256[2],uint256[2]),(uint256,uint256)),bytes32,bytes32[],bytes32[],(uint16,uint72,uint8,uint64,address,bytes32,(bytes32[4],bytes32,bytes32,bytes,bytes)[]),(bytes32,(uint8,address,uint256),uint120))[],bytes)", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "execute((((uint256,uint256),(uint256[2],uint256[2]),(uint256,uint256)),bytes32,bytes32[],bytes32[],(uint16,uint72,uint8,uint64,address,bytes32,(bytes32[4],bytes32,bytes32,bytes,bytes)[]),(bytes32,(uint8,address,uint256),uint120))[],(bytes31,bool,uint256,(address,bytes,uint256)[]),bytes)", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "getAdaptParams", + data: BytesLike + ): Result; + decodeFunctionResult(functionFragment: "multicall", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "nonce", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "railgun", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "shield", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "transfer", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "unwrapBase", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "wBase", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "wrapBase", data: BytesLike): Result; +} + +export namespace CallErrorEvent { + export type InputTuple = [callIndex: BigNumberish, revertReason: BytesLike]; + export type OutputTuple = [callIndex: bigint, revertReason: string]; + export interface OutputObject { + callIndex: bigint; + revertReason: string; + } + export type Event = TypedContractEvent; + export type Filter = TypedDeferredTopicFilter; + export type Log = TypedEventLog; + export type LogDescription = TypedLogDescription; +} + +export interface RelayAdapt7702 extends BaseContract { + connect(runner?: ContractRunner | null): BaseContract; + attach(addressOrName: AddressLike): this; + deployed(): Promise; + + interface: RelayAdapt7702Interface; + + queryFilter( + event: TCEvent, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise>>; + queryFilter( + filter: TypedDeferredTopicFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise>>; + + on( + event: TCEvent, + listener: TypedListener + ): Promise; + on( + filter: TypedDeferredTopicFilter, + listener: TypedListener + ): Promise; + + once( + event: TCEvent, + listener: TypedListener + ): Promise; + once( + filter: TypedDeferredTopicFilter, + listener: TypedListener + ): Promise; + + listeners( + event: TCEvent + ): Promise>>; + listeners(eventName?: string): Promise>; + removeAllListeners( + event?: TCEvent + ): Promise; + + ACTION_DATA_TYPEHASH: TypedContractMethod<[], [string], "view">; + + CALL_TYPEHASH: TypedContractMethod<[], [string], "view">; + + DOMAIN_SEPARATOR: TypedContractMethod<[], [string], "view">; + + MULTICALL_TYPEHASH: TypedContractMethod<[], [string], "view">; + + RELAY_TYPEHASH: TypedContractMethod<[], [string], "view">; + + TRANSACT_TYPEHASH: TypedContractMethod<[], [string], "view">; + + adaptImplementation: TypedContractMethod<[], [string], "view">; + + "execute((((uint256,uint256),(uint256[2],uint256[2]),(uint256,uint256)),bytes32,bytes32[],bytes32[],(uint16,uint72,uint8,uint64,address,bytes32,(bytes32[4],bytes32,bytes32,bytes,bytes)[]),(bytes32,(uint8,address,uint256),uint120))[],bytes)": TypedContractMethod< + [_transactions: TransactionStruct[], _signature: BytesLike], + [void], + "payable" + >; + + "execute((((uint256,uint256),(uint256[2],uint256[2]),(uint256,uint256)),bytes32,bytes32[],bytes32[],(uint16,uint72,uint8,uint64,address,bytes32,(bytes32[4],bytes32,bytes32,bytes,bytes)[]),(bytes32,(uint8,address,uint256),uint120))[],(bytes31,bool,uint256,(address,bytes,uint256)[]),bytes)": TypedContractMethod< + [ + _transactions: TransactionStruct[], + _actionData: RelayAdapt7702.ActionDataStruct, + _signature: BytesLike + ], + [void], + "payable" + >; + + getAdaptParams: TypedContractMethod< + [ + _transactions: TransactionStruct[], + _actionData: RelayAdapt7702.ActionDataStruct + ], + [string], + "view" + >; + + multicall: TypedContractMethod< + [ + _requireSuccess: boolean, + _calls: RelayAdapt7702.CallStruct[], + _nonce: BigNumberish, + _signature: BytesLike + ], + [void], + "payable" + >; + + nonce: TypedContractMethod<[], [bigint], "view">; + + railgun: TypedContractMethod<[], [string], "view">; + + shield: TypedContractMethod< + [_shieldRequests: ShieldRequestStruct[]], + [void], + "nonpayable" + >; + + transfer: TypedContractMethod< + [_transfers: RelayAdapt7702.TokenTransferStruct[]], + [void], + "nonpayable" + >; + + unwrapBase: TypedContractMethod< + [_amount: BigNumberish], + [void], + "nonpayable" + >; + + wBase: TypedContractMethod<[], [string], "view">; + + wrapBase: TypedContractMethod<[_amount: BigNumberish], [void], "nonpayable">; + + getFunction( + key: string | FunctionFragment + ): T; + + getFunction( + nameOrSignature: "ACTION_DATA_TYPEHASH" + ): TypedContractMethod<[], [string], "view">; + getFunction( + nameOrSignature: "CALL_TYPEHASH" + ): TypedContractMethod<[], [string], "view">; + getFunction( + nameOrSignature: "DOMAIN_SEPARATOR" + ): TypedContractMethod<[], [string], "view">; + getFunction( + nameOrSignature: "MULTICALL_TYPEHASH" + ): TypedContractMethod<[], [string], "view">; + getFunction( + nameOrSignature: "RELAY_TYPEHASH" + ): TypedContractMethod<[], [string], "view">; + getFunction( + nameOrSignature: "TRANSACT_TYPEHASH" + ): TypedContractMethod<[], [string], "view">; + getFunction( + nameOrSignature: "adaptImplementation" + ): TypedContractMethod<[], [string], "view">; + getFunction( + nameOrSignature: "execute((((uint256,uint256),(uint256[2],uint256[2]),(uint256,uint256)),bytes32,bytes32[],bytes32[],(uint16,uint72,uint8,uint64,address,bytes32,(bytes32[4],bytes32,bytes32,bytes,bytes)[]),(bytes32,(uint8,address,uint256),uint120))[],bytes)" + ): TypedContractMethod< + [_transactions: TransactionStruct[], _signature: BytesLike], + [void], + "payable" + >; + getFunction( + nameOrSignature: "execute((((uint256,uint256),(uint256[2],uint256[2]),(uint256,uint256)),bytes32,bytes32[],bytes32[],(uint16,uint72,uint8,uint64,address,bytes32,(bytes32[4],bytes32,bytes32,bytes,bytes)[]),(bytes32,(uint8,address,uint256),uint120))[],(bytes31,bool,uint256,(address,bytes,uint256)[]),bytes)" + ): TypedContractMethod< + [ + _transactions: TransactionStruct[], + _actionData: RelayAdapt7702.ActionDataStruct, + _signature: BytesLike + ], + [void], + "payable" + >; + getFunction( + nameOrSignature: "getAdaptParams" + ): TypedContractMethod< + [ + _transactions: TransactionStruct[], + _actionData: RelayAdapt7702.ActionDataStruct + ], + [string], + "view" + >; + getFunction( + nameOrSignature: "multicall" + ): TypedContractMethod< + [ + _requireSuccess: boolean, + _calls: RelayAdapt7702.CallStruct[], + _nonce: BigNumberish, + _signature: BytesLike + ], + [void], + "payable" + >; + getFunction( + nameOrSignature: "nonce" + ): TypedContractMethod<[], [bigint], "view">; + getFunction( + nameOrSignature: "railgun" + ): TypedContractMethod<[], [string], "view">; + getFunction( + nameOrSignature: "shield" + ): TypedContractMethod< + [_shieldRequests: ShieldRequestStruct[]], + [void], + "nonpayable" + >; + getFunction( + nameOrSignature: "transfer" + ): TypedContractMethod< + [_transfers: RelayAdapt7702.TokenTransferStruct[]], + [void], + "nonpayable" + >; + getFunction( + nameOrSignature: "unwrapBase" + ): TypedContractMethod<[_amount: BigNumberish], [void], "nonpayable">; + getFunction( + nameOrSignature: "wBase" + ): TypedContractMethod<[], [string], "view">; + getFunction( + nameOrSignature: "wrapBase" + ): TypedContractMethod<[_amount: BigNumberish], [void], "nonpayable">; + + getEvent( + key: "CallError" + ): TypedContractEvent< + CallErrorEvent.InputTuple, + CallErrorEvent.OutputTuple, + CallErrorEvent.OutputObject + >; + + filters: { + "CallError(uint256,bytes)": TypedContractEvent< + CallErrorEvent.InputTuple, + CallErrorEvent.OutputTuple, + CallErrorEvent.OutputObject + >; + CallError: TypedContractEvent< + CallErrorEvent.InputTuple, + CallErrorEvent.OutputTuple, + CallErrorEvent.OutputObject + >; + }; +} diff --git a/src/abi/typechain/RelayAdapt7702Deployer.ts b/src/abi/typechain/RelayAdapt7702Deployer.ts new file mode 100644 index 00000000..55c04700 --- /dev/null +++ b/src/abi/typechain/RelayAdapt7702Deployer.ts @@ -0,0 +1,276 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BytesLike, + FunctionFragment, + Result, + Interface, + EventFragment, + AddressLike, + ContractRunner, + ContractMethod, + Listener, +} from "ethers"; +import type { + TypedContractEvent, + TypedDeferredTopicFilter, + TypedEventLog, + TypedLogDescription, + TypedListener, + TypedContractMethod, +} from "./common"; + +export interface RelayAdapt7702DeployerInterface extends Interface { + getFunction( + nameOrSignature: + | "deploy" + | "isDeployed" + | "owner" + | "renounceOwnership" + | "setDeploymentStatus" + | "transferOwnership" + ): FunctionFragment; + + getEvent( + nameOrSignatureOrTopic: + | "Deployed" + | "DeploymentStatusChanged" + | "OwnershipTransferred" + ): EventFragment; + + encodeFunctionData( + functionFragment: "deploy", + values: [BytesLike, BytesLike] + ): string; + encodeFunctionData( + functionFragment: "isDeployed", + values: [AddressLike] + ): string; + encodeFunctionData(functionFragment: "owner", values?: undefined): string; + encodeFunctionData( + functionFragment: "renounceOwnership", + values?: undefined + ): string; + encodeFunctionData( + functionFragment: "setDeploymentStatus", + values: [AddressLike, boolean] + ): string; + encodeFunctionData( + functionFragment: "transferOwnership", + values: [AddressLike] + ): string; + + decodeFunctionResult(functionFragment: "deploy", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "isDeployed", data: BytesLike): Result; + decodeFunctionResult(functionFragment: "owner", data: BytesLike): Result; + decodeFunctionResult( + functionFragment: "renounceOwnership", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "setDeploymentStatus", + data: BytesLike + ): Result; + decodeFunctionResult( + functionFragment: "transferOwnership", + data: BytesLike + ): Result; +} + +export namespace DeployedEvent { + export type InputTuple = [deployment: AddressLike, salt: BytesLike]; + export type OutputTuple = [deployment: string, salt: string]; + export interface OutputObject { + deployment: string; + salt: string; + } + export type Event = TypedContractEvent; + export type Filter = TypedDeferredTopicFilter; + export type Log = TypedEventLog; + export type LogDescription = TypedLogDescription; +} + +export namespace DeploymentStatusChangedEvent { + export type InputTuple = [deployment: AddressLike, status: boolean]; + export type OutputTuple = [deployment: string, status: boolean]; + export interface OutputObject { + deployment: string; + status: boolean; + } + export type Event = TypedContractEvent; + export type Filter = TypedDeferredTopicFilter; + export type Log = TypedEventLog; + export type LogDescription = TypedLogDescription; +} + +export namespace OwnershipTransferredEvent { + export type InputTuple = [previousOwner: AddressLike, newOwner: AddressLike]; + export type OutputTuple = [previousOwner: string, newOwner: string]; + export interface OutputObject { + previousOwner: string; + newOwner: string; + } + export type Event = TypedContractEvent; + export type Filter = TypedDeferredTopicFilter; + export type Log = TypedEventLog; + export type LogDescription = TypedLogDescription; +} + +export interface RelayAdapt7702Deployer extends BaseContract { + connect(runner?: ContractRunner | null): BaseContract; + attach(addressOrName: AddressLike): this; + deployed(): Promise; + + interface: RelayAdapt7702DeployerInterface; + + queryFilter( + event: TCEvent, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise>>; + queryFilter( + filter: TypedDeferredTopicFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise>>; + + on( + event: TCEvent, + listener: TypedListener + ): Promise; + on( + filter: TypedDeferredTopicFilter, + listener: TypedListener + ): Promise; + + once( + event: TCEvent, + listener: TypedListener + ): Promise; + once( + filter: TypedDeferredTopicFilter, + listener: TypedListener + ): Promise; + + listeners( + event: TCEvent + ): Promise>>; + listeners(eventName?: string): Promise>; + removeAllListeners( + event?: TCEvent + ): Promise; + + deploy: TypedContractMethod< + [creationCode: BytesLike, salt: BytesLike], + [string], + "nonpayable" + >; + + isDeployed: TypedContractMethod<[arg0: AddressLike], [boolean], "view">; + + owner: TypedContractMethod<[], [string], "view">; + + renounceOwnership: TypedContractMethod<[], [void], "nonpayable">; + + setDeploymentStatus: TypedContractMethod< + [deployment: AddressLike, status: boolean], + [void], + "nonpayable" + >; + + transferOwnership: TypedContractMethod< + [newOwner: AddressLike], + [void], + "nonpayable" + >; + + getFunction( + key: string | FunctionFragment + ): T; + + getFunction( + nameOrSignature: "deploy" + ): TypedContractMethod< + [creationCode: BytesLike, salt: BytesLike], + [string], + "nonpayable" + >; + getFunction( + nameOrSignature: "isDeployed" + ): TypedContractMethod<[arg0: AddressLike], [boolean], "view">; + getFunction( + nameOrSignature: "owner" + ): TypedContractMethod<[], [string], "view">; + getFunction( + nameOrSignature: "renounceOwnership" + ): TypedContractMethod<[], [void], "nonpayable">; + getFunction( + nameOrSignature: "setDeploymentStatus" + ): TypedContractMethod< + [deployment: AddressLike, status: boolean], + [void], + "nonpayable" + >; + getFunction( + nameOrSignature: "transferOwnership" + ): TypedContractMethod<[newOwner: AddressLike], [void], "nonpayable">; + + getEvent( + key: "Deployed" + ): TypedContractEvent< + DeployedEvent.InputTuple, + DeployedEvent.OutputTuple, + DeployedEvent.OutputObject + >; + getEvent( + key: "DeploymentStatusChanged" + ): TypedContractEvent< + DeploymentStatusChangedEvent.InputTuple, + DeploymentStatusChangedEvent.OutputTuple, + DeploymentStatusChangedEvent.OutputObject + >; + getEvent( + key: "OwnershipTransferred" + ): TypedContractEvent< + OwnershipTransferredEvent.InputTuple, + OwnershipTransferredEvent.OutputTuple, + OwnershipTransferredEvent.OutputObject + >; + + filters: { + "Deployed(address,bytes32)": TypedContractEvent< + DeployedEvent.InputTuple, + DeployedEvent.OutputTuple, + DeployedEvent.OutputObject + >; + Deployed: TypedContractEvent< + DeployedEvent.InputTuple, + DeployedEvent.OutputTuple, + DeployedEvent.OutputObject + >; + + "DeploymentStatusChanged(address,bool)": TypedContractEvent< + DeploymentStatusChangedEvent.InputTuple, + DeploymentStatusChangedEvent.OutputTuple, + DeploymentStatusChangedEvent.OutputObject + >; + DeploymentStatusChanged: TypedContractEvent< + DeploymentStatusChangedEvent.InputTuple, + DeploymentStatusChangedEvent.OutputTuple, + DeploymentStatusChangedEvent.OutputObject + >; + + "OwnershipTransferred(address,address)": TypedContractEvent< + OwnershipTransferredEvent.InputTuple, + OwnershipTransferredEvent.OutputTuple, + OwnershipTransferredEvent.OutputObject + >; + OwnershipTransferred: TypedContractEvent< + OwnershipTransferredEvent.InputTuple, + OwnershipTransferredEvent.OutputTuple, + OwnershipTransferredEvent.OutputObject + >; + }; +} diff --git a/src/abi/typechain/factories/RelayAdapt7702Deployer__factory.ts b/src/abi/typechain/factories/RelayAdapt7702Deployer__factory.ts new file mode 100644 index 00000000..3725c397 --- /dev/null +++ b/src/abi/typechain/factories/RelayAdapt7702Deployer__factory.ts @@ -0,0 +1,191 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import { Contract, Interface, type ContractRunner } from "ethers"; +import type { + RelayAdapt7702Deployer, + RelayAdapt7702DeployerInterface, +} from "../RelayAdapt7702Deployer"; + +const _abi = [ + { + inputs: [ + { + internalType: "address", + name: "_owner", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "deployment", + type: "address", + }, + { + indexed: false, + internalType: "bytes32", + name: "salt", + type: "bytes32", + }, + ], + name: "Deployed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "deployment", + type: "address", + }, + { + indexed: false, + internalType: "bool", + name: "status", + type: "bool", + }, + ], + name: "DeploymentStatusChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousOwner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + inputs: [ + { + internalType: "bytes", + name: "creationCode", + type: "bytes", + }, + { + internalType: "bytes32", + name: "salt", + type: "bytes32", + }, + ], + name: "deploy", + outputs: [ + { + internalType: "address", + name: "deployment", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "isDeployed", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "deployment", + type: "address", + }, + { + internalType: "bool", + name: "status", + type: "bool", + }, + ], + name: "setDeploymentStatus", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; + +export class RelayAdapt7702Deployer__factory { + static readonly abi = _abi; + static createInterface(): RelayAdapt7702DeployerInterface { + return new Interface(_abi) as RelayAdapt7702DeployerInterface; + } + static connect( + address: string, + runner?: ContractRunner | null + ): RelayAdapt7702Deployer { + return new Contract( + address, + _abi, + runner + ) as unknown as RelayAdapt7702Deployer; + } +} diff --git a/src/abi/typechain/factories/RelayAdapt7702__factory.ts b/src/abi/typechain/factories/RelayAdapt7702__factory.ts new file mode 100644 index 00000000..f5abe114 --- /dev/null +++ b/src/abi/typechain/factories/RelayAdapt7702__factory.ts @@ -0,0 +1,1091 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import { Contract, Interface, type ContractRunner } from "ethers"; +import type { + RelayAdapt7702, + RelayAdapt7702Interface, +} from "../RelayAdapt7702"; + +const _abi = [ + { + inputs: [ + { + internalType: "address", + name: "_railgun", + type: "address", + }, + { + internalType: "address", + name: "_wBase", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [ + { + internalType: "uint256", + name: "callIndex", + type: "uint256", + }, + { + internalType: "bytes", + name: "revertReason", + type: "bytes", + }, + ], + name: "CallFailed", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "callIndex", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "revertReason", + type: "bytes", + }, + ], + name: "CallError", + type: "event", + }, + { + inputs: [], + name: "ACTION_DATA_TYPEHASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "CALL_TYPEHASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DOMAIN_SEPARATOR", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "MULTICALL_TYPEHASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "RELAY_TYPEHASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TRANSACT_TYPEHASH", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "adaptImplementation", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { + components: [ + { + internalType: "uint256", + name: "x", + type: "uint256", + }, + { + internalType: "uint256", + name: "y", + type: "uint256", + }, + ], + internalType: "struct G1Point", + name: "a", + type: "tuple", + }, + { + components: [ + { + internalType: "uint256[2]", + name: "x", + type: "uint256[2]", + }, + { + internalType: "uint256[2]", + name: "y", + type: "uint256[2]", + }, + ], + internalType: "struct G2Point", + name: "b", + type: "tuple", + }, + { + components: [ + { + internalType: "uint256", + name: "x", + type: "uint256", + }, + { + internalType: "uint256", + name: "y", + type: "uint256", + }, + ], + internalType: "struct G1Point", + name: "c", + type: "tuple", + }, + ], + internalType: "struct SnarkProof", + name: "proof", + type: "tuple", + }, + { + internalType: "bytes32", + name: "merkleRoot", + type: "bytes32", + }, + { + internalType: "bytes32[]", + name: "nullifiers", + type: "bytes32[]", + }, + { + internalType: "bytes32[]", + name: "commitments", + type: "bytes32[]", + }, + { + components: [ + { + internalType: "uint16", + name: "treeNumber", + type: "uint16", + }, + { + internalType: "uint72", + name: "minGasPrice", + type: "uint72", + }, + { + internalType: "enum UnshieldType", + name: "unshield", + type: "uint8", + }, + { + internalType: "uint64", + name: "chainID", + type: "uint64", + }, + { + internalType: "address", + name: "adaptContract", + type: "address", + }, + { + internalType: "bytes32", + name: "adaptParams", + type: "bytes32", + }, + { + components: [ + { + internalType: "bytes32[4]", + name: "ciphertext", + type: "bytes32[4]", + }, + { + internalType: "bytes32", + name: "blindedSenderViewingKey", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "blindedReceiverViewingKey", + type: "bytes32", + }, + { + internalType: "bytes", + name: "annotationData", + type: "bytes", + }, + { + internalType: "bytes", + name: "memo", + type: "bytes", + }, + ], + internalType: "struct CommitmentCiphertext[]", + name: "commitmentCiphertext", + type: "tuple[]", + }, + ], + internalType: "struct BoundParams", + name: "boundParams", + type: "tuple", + }, + { + components: [ + { + internalType: "bytes32", + name: "npk", + type: "bytes32", + }, + { + components: [ + { + internalType: "enum TokenType", + name: "tokenType", + type: "uint8", + }, + { + internalType: "address", + name: "tokenAddress", + type: "address", + }, + { + internalType: "uint256", + name: "tokenSubID", + type: "uint256", + }, + ], + internalType: "struct TokenData", + name: "token", + type: "tuple", + }, + { + internalType: "uint120", + name: "value", + type: "uint120", + }, + ], + internalType: "struct CommitmentPreimage", + name: "unshieldPreimage", + type: "tuple", + }, + ], + internalType: "struct Transaction[]", + name: "_transactions", + type: "tuple[]", + }, + { + internalType: "bytes", + name: "_signature", + type: "bytes", + }, + ], + name: "execute", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { + components: [ + { + internalType: "uint256", + name: "x", + type: "uint256", + }, + { + internalType: "uint256", + name: "y", + type: "uint256", + }, + ], + internalType: "struct G1Point", + name: "a", + type: "tuple", + }, + { + components: [ + { + internalType: "uint256[2]", + name: "x", + type: "uint256[2]", + }, + { + internalType: "uint256[2]", + name: "y", + type: "uint256[2]", + }, + ], + internalType: "struct G2Point", + name: "b", + type: "tuple", + }, + { + components: [ + { + internalType: "uint256", + name: "x", + type: "uint256", + }, + { + internalType: "uint256", + name: "y", + type: "uint256", + }, + ], + internalType: "struct G1Point", + name: "c", + type: "tuple", + }, + ], + internalType: "struct SnarkProof", + name: "proof", + type: "tuple", + }, + { + internalType: "bytes32", + name: "merkleRoot", + type: "bytes32", + }, + { + internalType: "bytes32[]", + name: "nullifiers", + type: "bytes32[]", + }, + { + internalType: "bytes32[]", + name: "commitments", + type: "bytes32[]", + }, + { + components: [ + { + internalType: "uint16", + name: "treeNumber", + type: "uint16", + }, + { + internalType: "uint72", + name: "minGasPrice", + type: "uint72", + }, + { + internalType: "enum UnshieldType", + name: "unshield", + type: "uint8", + }, + { + internalType: "uint64", + name: "chainID", + type: "uint64", + }, + { + internalType: "address", + name: "adaptContract", + type: "address", + }, + { + internalType: "bytes32", + name: "adaptParams", + type: "bytes32", + }, + { + components: [ + { + internalType: "bytes32[4]", + name: "ciphertext", + type: "bytes32[4]", + }, + { + internalType: "bytes32", + name: "blindedSenderViewingKey", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "blindedReceiverViewingKey", + type: "bytes32", + }, + { + internalType: "bytes", + name: "annotationData", + type: "bytes", + }, + { + internalType: "bytes", + name: "memo", + type: "bytes", + }, + ], + internalType: "struct CommitmentCiphertext[]", + name: "commitmentCiphertext", + type: "tuple[]", + }, + ], + internalType: "struct BoundParams", + name: "boundParams", + type: "tuple", + }, + { + components: [ + { + internalType: "bytes32", + name: "npk", + type: "bytes32", + }, + { + components: [ + { + internalType: "enum TokenType", + name: "tokenType", + type: "uint8", + }, + { + internalType: "address", + name: "tokenAddress", + type: "address", + }, + { + internalType: "uint256", + name: "tokenSubID", + type: "uint256", + }, + ], + internalType: "struct TokenData", + name: "token", + type: "tuple", + }, + { + internalType: "uint120", + name: "value", + type: "uint120", + }, + ], + internalType: "struct CommitmentPreimage", + name: "unshieldPreimage", + type: "tuple", + }, + ], + internalType: "struct Transaction[]", + name: "_transactions", + type: "tuple[]", + }, + { + components: [ + { + internalType: "bytes31", + name: "random", + type: "bytes31", + }, + { + internalType: "bool", + name: "requireSuccess", + type: "bool", + }, + { + internalType: "uint256", + name: "minGasLimit", + type: "uint256", + }, + { + components: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + internalType: "struct RelayAdapt7702.Call[]", + name: "calls", + type: "tuple[]", + }, + ], + internalType: "struct RelayAdapt7702.ActionData", + name: "_actionData", + type: "tuple", + }, + { + internalType: "bytes", + name: "_signature", + type: "bytes", + }, + ], + name: "execute", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { + components: [ + { + internalType: "uint256", + name: "x", + type: "uint256", + }, + { + internalType: "uint256", + name: "y", + type: "uint256", + }, + ], + internalType: "struct G1Point", + name: "a", + type: "tuple", + }, + { + components: [ + { + internalType: "uint256[2]", + name: "x", + type: "uint256[2]", + }, + { + internalType: "uint256[2]", + name: "y", + type: "uint256[2]", + }, + ], + internalType: "struct G2Point", + name: "b", + type: "tuple", + }, + { + components: [ + { + internalType: "uint256", + name: "x", + type: "uint256", + }, + { + internalType: "uint256", + name: "y", + type: "uint256", + }, + ], + internalType: "struct G1Point", + name: "c", + type: "tuple", + }, + ], + internalType: "struct SnarkProof", + name: "proof", + type: "tuple", + }, + { + internalType: "bytes32", + name: "merkleRoot", + type: "bytes32", + }, + { + internalType: "bytes32[]", + name: "nullifiers", + type: "bytes32[]", + }, + { + internalType: "bytes32[]", + name: "commitments", + type: "bytes32[]", + }, + { + components: [ + { + internalType: "uint16", + name: "treeNumber", + type: "uint16", + }, + { + internalType: "uint72", + name: "minGasPrice", + type: "uint72", + }, + { + internalType: "enum UnshieldType", + name: "unshield", + type: "uint8", + }, + { + internalType: "uint64", + name: "chainID", + type: "uint64", + }, + { + internalType: "address", + name: "adaptContract", + type: "address", + }, + { + internalType: "bytes32", + name: "adaptParams", + type: "bytes32", + }, + { + components: [ + { + internalType: "bytes32[4]", + name: "ciphertext", + type: "bytes32[4]", + }, + { + internalType: "bytes32", + name: "blindedSenderViewingKey", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "blindedReceiverViewingKey", + type: "bytes32", + }, + { + internalType: "bytes", + name: "annotationData", + type: "bytes", + }, + { + internalType: "bytes", + name: "memo", + type: "bytes", + }, + ], + internalType: "struct CommitmentCiphertext[]", + name: "commitmentCiphertext", + type: "tuple[]", + }, + ], + internalType: "struct BoundParams", + name: "boundParams", + type: "tuple", + }, + { + components: [ + { + internalType: "bytes32", + name: "npk", + type: "bytes32", + }, + { + components: [ + { + internalType: "enum TokenType", + name: "tokenType", + type: "uint8", + }, + { + internalType: "address", + name: "tokenAddress", + type: "address", + }, + { + internalType: "uint256", + name: "tokenSubID", + type: "uint256", + }, + ], + internalType: "struct TokenData", + name: "token", + type: "tuple", + }, + { + internalType: "uint120", + name: "value", + type: "uint120", + }, + ], + internalType: "struct CommitmentPreimage", + name: "unshieldPreimage", + type: "tuple", + }, + ], + internalType: "struct Transaction[]", + name: "_transactions", + type: "tuple[]", + }, + { + components: [ + { + internalType: "bytes31", + name: "random", + type: "bytes31", + }, + { + internalType: "bool", + name: "requireSuccess", + type: "bool", + }, + { + internalType: "uint256", + name: "minGasLimit", + type: "uint256", + }, + { + components: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + internalType: "struct RelayAdapt7702.Call[]", + name: "calls", + type: "tuple[]", + }, + ], + internalType: "struct RelayAdapt7702.ActionData", + name: "_actionData", + type: "tuple", + }, + ], + name: "getAdaptParams", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { + internalType: "bool", + name: "_requireSuccess", + type: "bool", + }, + { + components: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + internalType: "struct RelayAdapt7702.Call[]", + name: "_calls", + type: "tuple[]", + }, + { + internalType: "uint256", + name: "_nonce", + type: "uint256", + }, + { + internalType: "bytes", + name: "_signature", + type: "bytes", + }, + ], + name: "multicall", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "nonce", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "railgun", + outputs: [ + { + internalType: "contract RailgunSmartWallet", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: "bytes32", + name: "npk", + type: "bytes32", + }, + { + components: [ + { + internalType: "enum TokenType", + name: "tokenType", + type: "uint8", + }, + { + internalType: "address", + name: "tokenAddress", + type: "address", + }, + { + internalType: "uint256", + name: "tokenSubID", + type: "uint256", + }, + ], + internalType: "struct TokenData", + name: "token", + type: "tuple", + }, + { + internalType: "uint120", + name: "value", + type: "uint120", + }, + ], + internalType: "struct CommitmentPreimage", + name: "preimage", + type: "tuple", + }, + { + components: [ + { + internalType: "bytes32[3]", + name: "encryptedBundle", + type: "bytes32[3]", + }, + { + internalType: "bytes32", + name: "shieldKey", + type: "bytes32", + }, + ], + internalType: "struct ShieldCiphertext", + name: "ciphertext", + type: "tuple", + }, + ], + internalType: "struct ShieldRequest[]", + name: "_shieldRequests", + type: "tuple[]", + }, + ], + name: "shield", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: "enum TokenType", + name: "tokenType", + type: "uint8", + }, + { + internalType: "address", + name: "tokenAddress", + type: "address", + }, + { + internalType: "uint256", + name: "tokenSubID", + type: "uint256", + }, + ], + internalType: "struct TokenData", + name: "token", + type: "tuple", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + internalType: "struct RelayAdapt7702.TokenTransfer[]", + name: "_transfers", + type: "tuple[]", + }, + ], + name: "transfer", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + ], + name: "unwrapBase", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "wBase", + outputs: [ + { + internalType: "contract IWBase", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + ], + name: "wrapBase", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + stateMutability: "payable", + type: "receive", + }, +] as const; + +export class RelayAdapt7702__factory { + static readonly abi = _abi; + static createInterface(): RelayAdapt7702Interface { + return new Interface(_abi) as RelayAdapt7702Interface; + } + static connect( + address: string, + runner?: ContractRunner | null + ): RelayAdapt7702 { + return new Contract(address, _abi, runner) as unknown as RelayAdapt7702; + } +} diff --git a/src/contracts/contract-store.ts b/src/contracts/contract-store.ts index b1711f75..3d961d03 100644 --- a/src/contracts/contract-store.ts +++ b/src/contracts/contract-store.ts @@ -5,6 +5,8 @@ import { PoseidonMerkleAccumulatorContract } from './railgun-smart-wallet/V3/pos import { PoseidonMerkleVerifierContract } from './railgun-smart-wallet/V3/poseidon-merkle-verifier'; import { TokenVaultContract } from './railgun-smart-wallet/V3/token-vault-contract'; import { RelayAdaptV2Contract } from './relay-adapt/V2/relay-adapt-v2'; +import { RelayAdapt7702Contract } from './relay-adapt/V2/relay-adapt-7702'; +import { RelayAdapt7702DeployerContract } from './relay-adapt/V2/relay-adapt-7702-deployer'; import { RelayAdaptV3Contract } from './relay-adapt/V3/relay-adapt-v3'; export class ContractStore { @@ -12,6 +14,8 @@ export class ContractStore { new Registry(); static readonly relayAdaptV2Contracts: Registry = new Registry(); + static readonly relayAdapt7702Contracts: Registry = new Registry(); + static readonly relayAdapt7702DeployerContracts: Registry = new Registry(); static readonly poseidonMerkleAccumulatorV3Contracts: Registry = new Registry(); diff --git a/src/contracts/railgun-smart-wallet/railgun-versioned-smart-contracts.ts b/src/contracts/railgun-smart-wallet/railgun-versioned-smart-contracts.ts index 024c232d..7e38ae56 100644 --- a/src/contracts/railgun-smart-wallet/railgun-versioned-smart-contracts.ts +++ b/src/contracts/railgun-smart-wallet/railgun-versioned-smart-contracts.ts @@ -60,9 +60,12 @@ export class RailgunVersionedSmartContracts { throw new Error('Unsupported txidVersion'); } - static getRelayAdaptContract(txidVersion: TXIDVersion, chain: Chain) { + static getRelayAdaptContract(txidVersion: TXIDVersion, chain: Chain, useRelayAdapt7702: boolean = false) { switch (txidVersion) { case TXIDVersion.V2_PoseidonMerkle: { + if(useRelayAdapt7702) { + return ContractStore.relayAdapt7702Contracts.getOrThrow(null, chain); + } return ContractStore.relayAdaptV2Contracts.getOrThrow(null, chain); } case TXIDVersion.V3_PoseidonMerkle: { diff --git a/src/contracts/relay-adapt/V2/relay-adapt-7702-deployer.ts b/src/contracts/relay-adapt/V2/relay-adapt-7702-deployer.ts new file mode 100644 index 00000000..93372a8b --- /dev/null +++ b/src/contracts/relay-adapt/V2/relay-adapt-7702-deployer.ts @@ -0,0 +1,27 @@ +import { Contract, Provider } from 'ethers'; +import { ABIRelayAdapt7702Deployer } from '../../../abi/abi'; +import { RelayAdapt7702Deployer } from '../../../abi/typechain/RelayAdapt7702Deployer'; + +export class RelayAdapt7702DeployerContract { + private readonly contract: RelayAdapt7702Deployer; + + readonly address: string; + + /** + * Connect to RelayAdapt7702Deployer + * @param deployerAddress - address of RelayAdapt7702Deployer + * @param provider - Network provider + */ + constructor(deployerAddress: string, provider: Provider) { + this.address = deployerAddress; + this.contract = new Contract( + deployerAddress, + ABIRelayAdapt7702Deployer, + provider, + ) as unknown as RelayAdapt7702Deployer; + } + + async isDeployed(deploymentAddress: string): Promise { + return this.contract.isDeployed(deploymentAddress); + } +} diff --git a/src/contracts/relay-adapt/V2/relay-adapt-7702.ts b/src/contracts/relay-adapt/V2/relay-adapt-7702.ts new file mode 100644 index 00000000..3d9e5946 --- /dev/null +++ b/src/contracts/relay-adapt/V2/relay-adapt-7702.ts @@ -0,0 +1,516 @@ +import { + AbiCoder, + Contract, + ContractTransaction, + Provider, + Interface, + TransactionRequest, + Result, + Log, + toUtf8String, +} from 'ethers'; +import { ABIRelayAdapt, ABIRelayAdapt7702 } from '../../../abi/abi'; +import { TransactionReceiptLog } from '../../../models/formatted-types'; +import { getTokenDataERC20 } from '../../../note/note-util'; +import { ZERO_ADDRESS } from '../../../utils/constants'; +import { RelayAdapt7702Helper } from '../relay-adapt-7702-helper'; +import EngineDebug from '../../../debugger/debugger'; +import { ShieldRequestStruct } from '../../../abi/typechain/RailgunSmartWallet'; +import { RelayAdapt } from '../../../abi/typechain/RelayAdapt'; +import { PayableOverrides } from '../../../abi/typechain/common'; +import { TransactionStructV2 } from '../../../models/transaction-types'; +import { MINIMUM_RELAY_ADAPT_CROSS_CONTRACT_CALLS_GAS_LIMIT_V2 } from '../constants'; +import { EIP7702Authorization } from '../../../models/relay-adapt-types'; +import { ByteUtils } from '../../../utils/bytes'; + +enum RelayAdaptEvent { + CallError = 'CallError', +} + +export const RETURN_DATA_RELAY_ADAPT_STRING_PREFIX = '0x5c0dee5d'; +export const RETURN_DATA_STRING_PREFIX = '0x08c379a0'; + +export class RelayAdapt7702Contract { + private readonly contract: RelayAdapt; + + readonly address: string; + + /** + * Connect to Railgun instance on network + * @param relayAdaptV2ContractAddress - address of Railgun relay adapt contract + * @param provider - Network provider + */ + constructor(relayAdaptV2ContractAddress: string, provider: Provider) { + this.address = relayAdaptV2ContractAddress; + this.contract = new Contract( + relayAdaptV2ContractAddress, + ABIRelayAdapt7702, + provider, + ) as unknown as RelayAdapt; + } + + async populateShieldBaseToken( + shieldRequest: ShieldRequestStruct, + authorization?: EIP7702Authorization, + signature?: string, + random31Bytes?: string, + ephemeralAddress?: string, + ): Promise { + const orderedCalls: ContractTransaction[] = await this.getOrderedCallsForShieldBaseToken( + shieldRequest, + ephemeralAddress, + ); + return this.populateRelayMulticall( + orderedCalls, + { + value: shieldRequest.preimage.value, + }, + authorization, + signature, + random31Bytes, + ephemeralAddress, + ); + } + + async getOrderedCallsForShieldBaseToken( + shieldRequest: ShieldRequestStruct, + ephemeralAddress?: string, + ): Promise { + const calls = await Promise.all([ + this.contract.wrapBase.populateTransaction(shieldRequest.preimage.value), + this.populateRelayShields([shieldRequest]), + ]); + if (ephemeralAddress) { + calls.forEach((call) => { + if (call.to === this.address) { + // eslint-disable-next-line no-param-reassign + call.to = ephemeralAddress; + } + }); + } + return calls; + } + + async populateMulticall( + calls: ContractTransaction[], + shieldRequests: ShieldRequestStruct[], + ): Promise { + const orderedCalls = await this.getOrderedCallsForCrossContractCalls(calls, shieldRequests); + return this.populateRelayMulticall(orderedCalls, {}); + } + + /** + * @returns Populated transaction + */ + private populateRelayShields( + shieldRequests: ShieldRequestStruct[], + ): Promise { + return this.contract.shield.populateTransaction(shieldRequests); + } + + async getOrderedCallsForUnshieldBaseToken( + unshieldAddress: string, + ephemeralAddress?: string, + ): Promise { + // Use 0x00 address ERC20 to represent base token. + const baseTokenData = getTokenDataERC20(ZERO_ADDRESS); + + // Automatically unwraps and unshields all tokens. + const value = 0n; + + const baseTokenTransfer: RelayAdapt.TokenTransferStruct = { + token: baseTokenData, + to: unshieldAddress, + value, + }; + + const calls = await Promise.all([ + this.contract.unwrapBase.populateTransaction(value), + this.populateRelayTransfers([baseTokenTransfer]), + ]); + if (ephemeralAddress) { + calls.forEach((call) => { + if (call.to === this.address) { + // eslint-disable-next-line no-param-reassign + call.to = ephemeralAddress; + } + }); + } + return calls; + } + + async getRelayAdaptParamsUnshieldBaseToken( + dummyTransactions: TransactionStructV2[], + unshieldAddress: string, + random: string, + sendWithPublicWallet: boolean, + ephemeralAddress?: string, + ): Promise { + const orderedCalls: ContractTransaction[] = await this.getOrderedCallsForUnshieldBaseToken( + unshieldAddress, + ephemeralAddress, + ); + + const requireSuccess = sendWithPublicWallet; + + // Use RelayAdapt7702Helper for params calculation + const actionData = RelayAdapt7702Helper.getActionData(random, requireSuccess, orderedCalls, 0n); + return RelayAdapt7702Helper.getRelayAdaptParams(dummyTransactions, random, requireSuccess, orderedCalls); + } + + async populateUnshieldBaseToken( + transactions: TransactionStructV2[], + unshieldAddress: string, + random31Bytes: string, + _useDummyProof: boolean, + sendWithPublicWallet: boolean, + authorization?: EIP7702Authorization, + signature?: string, + ephemeralAddress?: string, + ): Promise { + const orderedCalls: ContractTransaction[] = await this.getOrderedCallsForUnshieldBaseToken( + unshieldAddress, + ephemeralAddress, + ); + + const requireSuccess = sendWithPublicWallet; + return this.populateRelay( + transactions, + random31Bytes, + requireSuccess, + orderedCalls, + {}, + BigInt(0), + authorization, + signature, + ephemeralAddress, + ); + } + + /** + * @returns Populated transaction + */ + private populateRelayTransfers( + transfersData: RelayAdapt.TokenTransferStruct[], + ): Promise { + return this.contract.transfer.populateTransaction(transfersData); + } + + async getOrderedCallsForCrossContractCalls( + crossContractCalls: ContractTransaction[], + relayShieldRequests: ShieldRequestStruct[], + ): Promise { + const orderedCallPromises: ContractTransaction[] = [...crossContractCalls]; + if (relayShieldRequests.length) { + orderedCallPromises.push(await this.populateRelayShields(relayShieldRequests)); + } + return orderedCallPromises; + } + + private static shouldRequireSuccessForCrossContractCalls( + isGasEstimate: boolean, + isBroadcasterTransaction: boolean, + ): boolean { + // If the cross contract calls (multicalls) fail, the Broadcaster Fee and Shields should continue to process. + // We should only !requireSuccess for production broadcaster transactions (not gas estimates). + const continueAfterMulticallFailure = isBroadcasterTransaction && !isGasEstimate; + return !continueAfterMulticallFailure; + } + + async getRelayAdaptParamsCrossContractCalls( + dummyUnshieldTransactions: TransactionStructV2[], + crossContractCalls: ContractTransaction[], + relayShieldRequests: ShieldRequestStruct[], + random: string, + isBroadcasterTransaction: boolean, + minGasLimit?: bigint, + ephemeralAddress?: string, + ): Promise { + const orderedCalls: ContractTransaction[] = await this.getOrderedCallsForCrossContractCalls( + crossContractCalls, + relayShieldRequests, + ); + + if (ephemeralAddress) { + orderedCalls.forEach((call) => { + if (call.to === this.address) { + // eslint-disable-next-line no-param-reassign + call.to = ephemeralAddress; + } + }); + } + + // Adapt params not required for gas estimates. + const isGasEstimate = false; + + const requireSuccess = RelayAdapt7702Contract.shouldRequireSuccessForCrossContractCalls( + isGasEstimate, + isBroadcasterTransaction, + ); + + const minimumGasLimit = minGasLimit ?? MINIMUM_RELAY_ADAPT_CROSS_CONTRACT_CALLS_GAS_LIMIT_V2; + const minGasLimitForContract = + RelayAdapt7702Contract.getMinimumGasLimitForContract(minimumGasLimit); + + // Use RelayAdapt7702Helper for params calculation + return RelayAdapt7702Helper.getRelayAdaptParams( + dummyUnshieldTransactions, + random, + requireSuccess, + orderedCalls, + minGasLimitForContract, + ); + } + + async populateCrossContractCalls( + unshieldTransactions: TransactionStructV2[], + crossContractCalls: ContractTransaction[], + relayShieldRequests: ShieldRequestStruct[], + random31Bytes: string, + isGasEstimate: boolean, + isBroadcasterTransaction: boolean, + minGasLimit?: bigint, + authorization?: EIP7702Authorization, + signature?: string, + ephemeralAddress?: string, + ): Promise { + const orderedCalls: ContractTransaction[] = await this.getOrderedCallsForCrossContractCalls( + crossContractCalls, + relayShieldRequests, + ); + if (ephemeralAddress) { + orderedCalls.forEach((call) => { + if (call.to === this.address) { + // eslint-disable-next-line no-param-reassign + call.to = ephemeralAddress; + } + }); + } + + const requireSuccess = RelayAdapt7702Contract.shouldRequireSuccessForCrossContractCalls( + isGasEstimate, + isBroadcasterTransaction, + ); + + const minimumGasLimit = minGasLimit ?? MINIMUM_RELAY_ADAPT_CROSS_CONTRACT_CALLS_GAS_LIMIT_V2; + const minGasLimitForContract = + RelayAdapt7702Contract.getMinimumGasLimitForContract(minimumGasLimit); + + const populatedTransaction = await this.populateRelay( + unshieldTransactions, + random31Bytes, + requireSuccess, + orderedCalls, + {}, + minGasLimitForContract, + authorization, + signature, + ephemeralAddress, + ); + + // Set default gas limit for cross-contract calls. + populatedTransaction.gasLimit = minimumGasLimit; + + return populatedTransaction; + } + + static getMinimumGasLimitForContract(minimumGasLimit: bigint) { + // Contract call needs ~50,000-150,000 less gas than the gasLimit setting. + // This can be more if there are complex UTXO sets for the unshield. + return minimumGasLimit - 150_000n; + } + + static async estimateGasWithErrorHandler( + provider: Provider, + transaction: ContractTransaction | TransactionRequest, + ): Promise { + try { + const gasEstimate = await provider.estimateGas(transaction); + return gasEstimate; + } catch (cause) { + if (!(cause instanceof Error)) { + throw new Error('Non-error thrown from estimateGas', { cause }); + } + const { callFailedIndexString, errorMessage } = + RelayAdapt7702Contract.extractGasEstimateCallFailedIndexAndErrorText(cause.message); + throw new Error(`RelayAdapt multicall failed at index ${callFailedIndexString}.`, { + cause: new Error(errorMessage), + }); + } + } + + static extractGasEstimateCallFailedIndexAndErrorText(errorMessage: string) { + try { + // Sample error text from ethers v6.4.0: 'execution reverted (unknown custom error) (action="estimateGas", data="0x5c0dee5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000", reason=null, transaction={ "data": "0x28223a77000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000007a00000000000000000000000000000000000000000000000000…00000000004640cd6086ade3e984b011b4e8c7cab9369b90499ab88222e673ec1ae4d2c3bf78ae96e95f9171653e5b1410273269edd64a0ab792a5d355093caa9cb92406125c7803a48028503783f2ab5e84f0ea270ce770860e436b77c942ed904a5d577d021cf0fd936183e0298175679d63d73902e116484e10c7b558d4dc84e113380500000000000000000000000000000000000000000000000000000000", "from": "0x000000000000000000000000000000000000dEaD", "to": "0x0355B7B8cb128fA5692729Ab3AAa199C1753f726" }, invocation=null, revert=null, code=CALL_EXCEPTION, version=6.4.0)' + const prefixSplit = ` (action="estimateGas", data="`; + const splitResult = errorMessage.split(prefixSplit); + const callFailedMessage = splitResult[0]; // execution reverted (unknown custom error) + const dataMessage = splitResult[1].split(`"`)[0]; // 0x5c0dee5d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000 + const parsedDataMessage = this.parseRelayAdaptReturnValue(dataMessage); + const callFailedIndexString: string = parsedDataMessage?.callIndex?.toString() ?? 'UNKNOWN'; + return { + callFailedIndexString, + errorMessage: `'${callFailedMessage}': ${parsedDataMessage?.error ?? dataMessage}`, + }; + } catch (err) { + return { + callFailedIndexString: 'UNKNOWN', + errorMessage, + }; + } + } + + /** + * Generates Relay multicall given a list of ordered calls. + * @returns populated transaction + */ + private async populateRelayMulticall( + calls: ContractTransaction[], + overrides: PayableOverrides, + authorization?: EIP7702Authorization, + signature?: string, + random31Bytes?: string, + ephemeralAddress?: string, + ): Promise { + // Always requireSuccess when there is no Broadcaster payment. + const requireSuccess = true; + + // Use empty transactions for pure multicall + const transactions: TransactionStructV2[] = []; + const random = random31Bytes ?? ByteUtils.randomHex(31); + + return this.populateRelay( + transactions, + random, + requireSuccess, + calls, + overrides, + BigInt(0), + authorization, + signature, + ephemeralAddress, + ); + } + + /** + * Generates Relay multicall given a list of transactions and ordered calls. + * @returns populated transaction + */ + private async populateRelay( + transactions: TransactionStructV2[], + random31Bytes: string, + requireSuccess: boolean, + calls: ContractTransaction[], + overrides: PayableOverrides, + minimumGasLimit = BigInt(0), + authorization?: EIP7702Authorization, + signature?: string, + ephemeralAddress?: string, + ): Promise { + const actionData: RelayAdapt.ActionDataStruct = RelayAdapt7702Helper.getActionData( + random31Bytes, + requireSuccess, + calls, + minimumGasLimit, + ); + + const sig = signature ?? '0x'; + + const populatedTransaction = await (this.contract as any).execute.populateTransaction( + transactions, + actionData, + sig, + overrides, + ); + + if (authorization) { + (populatedTransaction as any).authorizationList = [authorization]; + if (ephemeralAddress) { + populatedTransaction.to = ephemeralAddress; + } + } + + return populatedTransaction; + } + + private static getCallErrorTopic() { + const iface = new Interface(ABIRelayAdapt); + return iface.encodeFilterTopics(RelayAdaptEvent.CallError, [])[0]; + } + + static getRelayAdaptCallError( + receiptLogs: TransactionReceiptLog[] | readonly Log[], + ): Optional { + const topic = this.getCallErrorTopic(); + try { + for (const log of receiptLogs) { + if (log.topics[0] === topic) { + const parsed = this.customRelayAdaptErrorParse(log.data); + if (parsed) { + return parsed.error; + } + } + } + } catch (cause) { + if (!(cause instanceof Error)) { + throw new Error('Non-error thrown from getRelayAdaptCallError.', { cause }); + } + const err = new Error('Relay Adapt log parsing error', { cause }); + EngineDebug.error(err); + throw err; + } + return undefined; + } + + static parseRelayAdaptReturnValue( + returnValue: string, + ): Optional<{ callIndex?: number; error: string }> { + if (returnValue.match(RETURN_DATA_RELAY_ADAPT_STRING_PREFIX)) { + const strippedReturnValue = returnValue.replace(RETURN_DATA_RELAY_ADAPT_STRING_PREFIX, '0x'); + return this.customRelayAdaptErrorParse(strippedReturnValue); + } + if (returnValue.match(RETURN_DATA_STRING_PREFIX)) { + return { error: this.parseRelayAdaptStringError(returnValue) }; + } + return { + error: `Not a RelayAdapt return value: must be prefixed with ${RETURN_DATA_RELAY_ADAPT_STRING_PREFIX} or ${RETURN_DATA_STRING_PREFIX}`, + }; + } + + private static customRelayAdaptErrorParse( + data: string, + ): Optional<{ callIndex: number; error: string }> { + // Force parse as bytes + const decoded: Result = AbiCoder.defaultAbiCoder().decode( + ['uint256 callIndex', 'bytes revertReason'], + data, + ); + + const callIndex = Number(decoded[0]); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const revertReasonBytes: string = decoded[1]; + + // Map function to try parsing bytes as string + const error = this.parseRelayAdaptStringError(revertReasonBytes); + return { callIndex, error }; + } + + private static parseRelayAdaptStringError(revertReason: string): string { + if (revertReason.match(RETURN_DATA_STRING_PREFIX)) { + const strippedReturnValue = revertReason.replace(RETURN_DATA_STRING_PREFIX, '0x'); + const result = AbiCoder.defaultAbiCoder().decode(['string'], strippedReturnValue); + return result[0]; + } + try { + const utf8 = toUtf8String(revertReason); + if (utf8.length === 0) { + throw new Error('No utf8 string parsed from revert reason.'); + } + return utf8; + } catch (err) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return `Unknown Relay Adapt error: ${err?.message ?? err}`; + } + } +} diff --git a/src/contracts/relay-adapt/__tests__/relay-adapt-7702.test.ts b/src/contracts/relay-adapt/__tests__/relay-adapt-7702.test.ts new file mode 100644 index 00000000..7cdb6419 --- /dev/null +++ b/src/contracts/relay-adapt/__tests__/relay-adapt-7702.test.ts @@ -0,0 +1,2037 @@ +/* eslint-disable prefer-template */ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import memdown from 'memdown'; +import { groth16 } from 'snarkjs'; +import { bytesToHex } from 'ethereum-cryptography/utils'; +import { + Contract, + ContractTransaction, + FallbackProvider, + JsonRpcProvider, + TransactionReceipt, + Wallet, +} from 'ethers'; +import { RelayAdapt7702Helper } from '../relay-adapt-7702-helper'; +import { deriveEphemeralWallet } from '../../../key-derivation/ephemeral-key'; +import { abi as erc20Abi } from '../../../test/test-erc20-abi.test'; +import { abi as erc721Abi } from '../../../test/test-erc721-abi.test'; +import { config } from '../../../test/config.test'; +import { RailgunWallet } from '../../../wallet/railgun-wallet'; +import { + awaitMultipleScans, + awaitRailgunSmartWalletShield, + awaitRailgunSmartWalletTransact, + awaitRailgunSmartWalletUnshield, + awaitScan, + getEthersWallet, + getTestTXIDVersion, + isV2Test, + mockGetLatestValidatedRailgunTxid, + mockQuickSyncEvents, + mockQuickSyncRailgunTransactionsV2, + mockRailgunTxidMerklerootValidator, + sendTransactionWithLatestNonce, + testArtifactsGetter, +} from '../../../test/helper.test'; +import { + NFTTokenData, + OutputType, + RelayAdaptShieldERC20Recipient, +} from '../../../models/formatted-types'; +import { ByteLength, ByteUtils } from '../../../utils/bytes'; +import { SnarkJSGroth16 } from '../../../prover/prover'; +import { Chain, ChainType } from '../../../models/engine-types'; +import { RailgunEngine } from '../../../railgun-engine'; +import { RelayAdapt7702Contract } from '../V2/relay-adapt-7702'; +import { ShieldNoteERC20 } from '../../../note/erc20/shield-note-erc20'; +import { TransactNote } from '../../../note/transact-note'; +import { UnshieldNoteERC20 } from '../../../note/erc20/unshield-note-erc20'; +import { TransactionBatch } from '../../../transaction/transaction-batch'; +import { getTokenDataERC20 } from '../../../note/note-util'; +import { mintNFTsID01ForTest, shieldNFTForTest } from '../../../test/shared-test.test'; +import { UnshieldNoteNFT } from '../../../note'; +import FormattedRelayAdaptErrorLogs from './json/formatted-relay-adapt-error-logs.json'; +import { TestERC721 } from '../../../test/abi/typechain/TestERC721'; +import { TestERC20 } from '../../../test/abi/typechain/TestERC20'; +import { PollingJsonRpcProvider } from '../../../provider/polling-json-rpc-provider'; +import { promiseTimeout } from '../../../utils/promises'; +import { createPollingJsonRpcProviderForListeners } from '../../../provider/polling-util'; +import { isDefined } from '../../../utils/is-defined'; +import { TXIDVersion } from '../../../models/poi-types'; +import { WalletBalanceBucket } from '../../../models/txo-types'; +import { RailgunVersionedSmartContracts } from '../../railgun-smart-wallet/railgun-versioned-smart-contracts'; +import { RelayAdaptVersionedSmartContracts } from '../relay-adapt-versioned-smart-contracts'; +import { TransactionStructV2 } from '../../../models'; +import { MINIMUM_RELAY_ADAPT_CROSS_CONTRACT_CALLS_GAS_LIMIT_V2 } from '../constants'; + +chai.use(chaiAsPromised); +const { expect } = chai; + +let provider: JsonRpcProvider; +let chain: Chain; +let engine: RailgunEngine; +let ethersWallet: Wallet; +let snapshot: number; +let nft: TestERC721; +let wallet: RailgunWallet; +let wallet2: RailgunWallet; + +const txidVersion = getTestTXIDVersion(); + +const testMnemonic = config.mnemonic; +const testEncryptionKey = config.encryptionKey; + +const WETH_TOKEN_ADDRESS = config.contracts.weth9; +const SHIELD_RANDOM = ByteUtils.randomHex(16); + +const NFT_ADDRESS = config.contracts.testERC721; + +const wethTokenData = getTokenDataERC20(WETH_TOKEN_ADDRESS); + +const DEAD_ADDRESS = '0x000000000000000000000000000000000000dEaD'; +const DEPLOYMENT_BLOCKS = { + [TXIDVersion.V2_PoseidonMerkle]: isDefined(process.env.DEPLOYMENT_BLOCK) + ? Number(process.env.DEPLOYMENT_BLOCK) + : 0, + [TXIDVersion.V3_PoseidonMerkle]: isDefined(process.env.DEPLOYMENT_BLOCK) + ? Number(process.env.DEPLOYMENT_BLOCK) + : 0, +}; + +let testShieldBaseToken: (value?: bigint) => Promise; + +describe('relay-adapt-7702', function test() { + this.timeout(45_000); + + beforeEach(async () => { + engine = await RailgunEngine.initForWallet( + 'TestRelayAdapt', + memdown(), + testArtifactsGetter, + mockQuickSyncEvents, + mockQuickSyncRailgunTransactionsV2, + mockRailgunTxidMerklerootValidator, + mockGetLatestValidatedRailgunTxid, + undefined, // engineDebugger + undefined, // skipMerkletreeScans + ); + + engine.prover.setSnarkJSGroth16(groth16 as SnarkJSGroth16); + + wallet = await engine.createWalletFromMnemonic(testEncryptionKey, testMnemonic, 0); + wallet2 = await engine.createWalletFromMnemonic(testEncryptionKey, testMnemonic, 1); + + if (!isDefined(process.env.RUN_HARDHAT_TESTS)) { + return; + } + + provider = new PollingJsonRpcProvider(config.rpc, config.chainId, 500, 1); + const fallbackProvider = new FallbackProvider([{ provider, weight: 2 }]); + + chain = { + type: ChainType.EVM, + id: Number((await provider.getNetwork()).chainId), + }; + const pollingProvider = await createPollingJsonRpcProviderForListeners( + fallbackProvider, + chain.id, + ); + await engine.loadNetwork( + chain, + config.contracts.proxy, + config.contracts.relayAdapt, + config.contracts.poseidonMerkleAccumulatorV3, + config.contracts.poseidonMerkleVerifierV3, + config.contracts.tokenVaultV3, + fallbackProvider, + pollingProvider, + DEPLOYMENT_BLOCKS, + undefined, + !isV2Test(), // supportsV3 + config.contracts.relayAdapt7702, // relayAdapt7702ContractAddress + ); + await engine.scanContractHistory( + chain, + undefined, // walletIdFilter + ); + + ethersWallet = getEthersWallet(config.mnemonic, fallbackProvider); + snapshot = (await provider.send('evm_snapshot', [])) as number; + + nft = new Contract(NFT_ADDRESS, erc721Abi, ethersWallet) as unknown as TestERC721; + + testShieldBaseToken = async (value: bigint = 10000n): Promise => { + // Create shield + const shield = new ShieldNoteERC20( + wallet.masterPublicKey, + SHIELD_RANDOM, + value, + WETH_TOKEN_ADDRESS, + ); + const shieldPrivateKey = ByteUtils.hexToBytes(ByteUtils.randomHex(32)); + const shieldRequest = await shield.serialize( + shieldPrivateKey, + wallet.getViewingKeyPair().pubkey, + ); + + const shieldTx = await RelayAdaptVersionedSmartContracts.populateShieldBaseToken( + txidVersion, + chain, + shieldRequest, + false, // isRelayAdapt7702 + ); + + const shieldEventPromise = awaitRailgunSmartWalletShield(txidVersion, chain); + + // Send shield on chain + const tx = await sendTransactionWithLatestNonce(ethersWallet, shieldTx); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_, txReceipt] = await Promise.all([ + shieldEventPromise, + tx.wait(), + promiseTimeout( + awaitScan(wallet, chain), + 20000, + 'Timed out shielding base token for relay adapt test setup', + ), + ]); + await wallet.refreshPOIsForTXIDVersion(chain, txidVersion, true); + + return txReceipt; + }; + }); + + it('[HH] Should wrap and shield base token', async function run() { + if (!isDefined(process.env.RUN_HARDHAT_TESTS)) { + this.skip(); + return; + } + + const { masterPublicKey } = wallet; + + // Create shield + const shield = new ShieldNoteERC20(masterPublicKey, SHIELD_RANDOM, 10000n, WETH_TOKEN_ADDRESS); + const shieldPrivateKey = ByteUtils.hexToBytes(ByteUtils.randomHex(32)); + const shieldRequest = await shield.serialize( + shieldPrivateKey, + wallet.getViewingKeyPair().pubkey, + ); + + // Manual 7702 Flow + const contract7702 = RelayAdaptVersionedSmartContracts.getRelayAdaptContract( + txidVersion, + chain, + true, + ) as RelayAdapt7702Contract; + + // Ephemeral Key + const ephemeralWallet = deriveEphemeralWallet(config.mnemonic, 0); + + // HACK: Set code of ephemeral address to RelayAdapt7702 code to simulate 7702 delegation + const code = await provider.getCode(contract7702.address); + await provider.send('hardhat_setCode', [ephemeralWallet.address, code]); + + const orderedCalls = await contract7702.getOrderedCallsForShieldBaseToken( + shieldRequest, + ephemeralWallet.address, + ); + + const random31Bytes = ByteUtils.randomHex(31); + + const actionData = RelayAdapt7702Helper.getActionData( + random31Bytes, + true, // requireSuccess + orderedCalls, + BigInt(0), // minGasLimit + ); + + // Sign 7702 Auth + const authorization = await RelayAdapt7702Helper.signEIP7702Authorization( + ephemeralWallet, + contract7702.address, + BigInt(chain.id), + 0, // nonce + ); + + // Sign Execution Auth + const signature = await RelayAdapt7702Helper.signExecutionAuthorization( + ephemeralWallet, + [], // transactions (empty for shield base token) + actionData, + BigInt(chain.id), + ); + + const shieldTx = await RelayAdaptVersionedSmartContracts.populateShieldBaseToken( + txidVersion, + chain, + shieldRequest, + true, // isRelayAdapt7702 + authorization, + signature, + random31Bytes, + ephemeralWallet.address, + ); + + const shieldEventPromise = awaitRailgunSmartWalletShield(txidVersion, chain); + + // Send shield on chain + const txResponse = await sendTransactionWithLatestNonce(ethersWallet, shieldTx); + + // console.log(`Transaction hash: ${txResponse.hash}`); + const receipt = await txResponse.wait(); + // console.log(`Transaction status: ${receipt?.status}`); + + // if (receipt) { + // console.log('Logs length:', receipt.logs.length); + // receipt.logs.forEach((log, index) => { + // console.log(`Log ${index} address: ${log.address}`); + // console.log(`Log ${index} topics: ${log.topics}`); + // }); + // } + + if (receipt?.status === 0) { + console.error('Transaction failed'); + } + + await Promise.all([ + shieldEventPromise, + promiseTimeout(awaitScan(wallet, chain), 30000), + ]); + await wallet.refreshPOIsForTXIDVersion(chain, txidVersion, true); + + expect( + await wallet.getBalanceERC20(txidVersion, chain, WETH_TOKEN_ADDRESS, [ + WalletBalanceBucket.Spendable, + ]), + ).to.equal(9975n); + }).timeout(300_000); + + it('[HH] Should return gas estimate for unshield base token', async function run() { + if (!isDefined(process.env.RUN_HARDHAT_TESTS)) { + this.skip(); + return; + } + + await testShieldBaseToken(100000000n); + expect( + await wallet.getBalanceERC20(txidVersion, chain, WETH_TOKEN_ADDRESS, [ + WalletBalanceBucket.Spendable, + ]), + ).to.equal(99750000n); + + const transactionBatch = new TransactionBatch(chain); + + const broadcasterFee = TransactNote.createTransfer( + wallet2.addressKeys, + wallet.addressKeys, + 1000n, + wethTokenData, + false, // showSenderAddressToRecipient + OutputType.BroadcasterFee, + undefined, // memoText + ); + transactionBatch.addOutput(broadcasterFee); // Simulate Broadcaster fee output. + + const unshieldValue = 99000000n; + + // 7702 Setup + const contract7702 = RelayAdaptVersionedSmartContracts.getRelayAdaptContract( + txidVersion, + chain, + true, + ) as RelayAdapt7702Contract; + const ephemeralWallet = deriveEphemeralWallet(config.mnemonic, 0); + + // HACK: Set code of ephemeral address to RelayAdapt7702 code to simulate 7702 delegation + const code = await provider.getCode(contract7702.address); + await provider.send('hardhat_setCode', [ephemeralWallet.address, code]); + + const unshieldNote = new UnshieldNoteERC20( + ephemeralWallet.address, + unshieldValue, + WETH_TOKEN_ADDRESS, + ); + transactionBatch.addUnshieldData(unshieldNote.unshieldData); + + const dummyTransactions = await transactionBatch.generateDummyTransactions( + engine.prover, + wallet, + txidVersion, + testEncryptionKey, + ); + + const random = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd'; + + const orderedCalls = await contract7702.getOrderedCallsForUnshieldBaseToken( + ethersWallet.address, + ephemeralWallet.address, + ); + + const actionData = RelayAdapt7702Helper.getActionData( + random, + false, // sendWithPublicWallet = false + orderedCalls, + BigInt(0), + ); + + const authorization = await RelayAdapt7702Helper.signEIP7702Authorization( + ephemeralWallet, + contract7702.address, + BigInt(chain.id), + 0, + ); + + const signature = await RelayAdapt7702Helper.signExecutionAuthorization( + ephemeralWallet, + dummyTransactions, + actionData, + BigInt(chain.id), + ); + + const relayTransactionGasEstimate = + await RelayAdaptVersionedSmartContracts.populateUnshieldBaseToken( + txidVersion, + chain, + dummyTransactions, + ethersWallet.address, + random, + true, // useDummyProof + false, // sendWithPublicWallet + true, // isRelayAdapt7702 + authorization, + signature, + ephemeralWallet.address, + ); + + relayTransactionGasEstimate.from = DEAD_ADDRESS; + + const gasEstimate = await provider.estimateGas(relayTransactionGasEstimate); + expect(Number(gasEstimate)).to.be.greaterThan(0); + }).timeout(300_00); + + it('[HH] Should execute relay adapt transaction for unshield base token', async function run() { + if (!isDefined(process.env.RUN_HARDHAT_TESTS)) { + this.skip(); + return; + } + + await testShieldBaseToken(); + expect( + await wallet.getBalanceERC20(txidVersion, chain, WETH_TOKEN_ADDRESS, [ + WalletBalanceBucket.Spendable, + ]), + ).to.equal(9975n); + + // 1. Generate transaction batch to unshield necessary amount, and pay Broadcaster. + const transactionBatch = new TransactionBatch(chain); + const broadcasterFee = TransactNote.createTransfer( + wallet2.addressKeys, + wallet.addressKeys, + 100n, + wethTokenData, + false, // showSenderAddressToRecipient + OutputType.BroadcasterFee, + undefined, // memoText + ); + transactionBatch.addOutput(broadcasterFee); // Simulate Broadcaster fee output. + + const unshieldValue = 300n; + + // 7702 Setup + const contract7702 = RelayAdaptVersionedSmartContracts.getRelayAdaptContract( + txidVersion, + chain, + true, + ) as RelayAdapt7702Contract; + const ephemeralWallet = deriveEphemeralWallet(config.mnemonic, 0); + + // HACK: Set code of ephemeral address to RelayAdapt7702 code to simulate 7702 delegation + const code = await provider.getCode(contract7702.address); + await provider.send('hardhat_setCode', [ephemeralWallet.address, code]); + + const unshieldNote = new UnshieldNoteERC20( + ephemeralWallet.address, + unshieldValue, + WETH_TOKEN_ADDRESS, + ); + transactionBatch.addUnshieldData(unshieldNote.unshieldData); + + // 2. Create dummy transactions from batch. + const dummyTransactions = await transactionBatch.generateDummyTransactions( + engine.prover, + wallet, + txidVersion, + testEncryptionKey, + ); + + // 3. Generate relay adapt params from dummy transactions. + const random = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd'; + + const relayAdaptParams = + await RelayAdaptVersionedSmartContracts.getRelayAdaptParamsUnshieldBaseToken( + txidVersion, + chain, + dummyTransactions, + ethersWallet.address, + random, + true, + true, // isRelayAdapt7702 + ephemeralWallet.address, + ); + expect(relayAdaptParams).to.equal( + '0x1aa79a9d04be5ee64c53f023bd2acb13409532c5b022ed90e1589d830cf8fdbb', + ); + + // 4. Create real transactions with relay adapt params. + transactionBatch.setAdaptID({ + contract: ephemeralWallet.address, + parameters: relayAdaptParams, + }); + const { provedTransactions } = await transactionBatch.generateTransactions( + engine.prover, + wallet, + txidVersion, + testEncryptionKey, + () => { }, + false, // shouldGeneratePreTransactionPOIs + ); + for (const transaction of provedTransactions) { + expect((transaction as TransactionStructV2).boundParams.adaptContract).to.equal( + ephemeralWallet.address, + ); + expect((transaction as TransactionStructV2).boundParams.adaptParams).to.equal( + relayAdaptParams, + ); + } + + // const preEthBalance = await ethersWallet.getBalanceERC20(txidVersion, ); + + // 5: Generate final relay transaction for unshield base token. + const orderedCalls = await contract7702.getOrderedCallsForUnshieldBaseToken( + ethersWallet.address, + ephemeralWallet.address, + ); + + const actionData = RelayAdapt7702Helper.getActionData( + random, + true, // requireSuccess (sendWithPublicWallet=true) + orderedCalls, + BigInt(0), // minGasLimit + ); + + // Sign 7702 Auth + const authorization = await RelayAdapt7702Helper.signEIP7702Authorization( + ephemeralWallet, + contract7702.address, + BigInt(chain.id), + 0, // nonce + ); + + // Sign Execution Auth + const signature = await RelayAdapt7702Helper.signExecutionAuthorization( + ephemeralWallet, + provedTransactions, + actionData, + BigInt(chain.id), + ); + + const relayTransaction = await RelayAdaptVersionedSmartContracts.populateUnshieldBaseToken( + txidVersion, + chain, + provedTransactions, + ethersWallet.address, + random, + true, // useDummyProof + true, // sendWithPublicWallet + true, // isRelayAdapt7702 + authorization, + signature, + ephemeralWallet.address, + ); + + // 6: Send relay transaction. + const transactEventPromise = awaitRailgunSmartWalletTransact(txidVersion, chain); + const unshieldEventPromise = awaitRailgunSmartWalletUnshield(txidVersion, chain); + + const txResponse = await sendTransactionWithLatestNonce(ethersWallet, relayTransaction); + + const awaiterScan = awaitMultipleScans(wallet, chain, 2); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_t, _u, txReceipt] = await Promise.all([ + transactEventPromise, + unshieldEventPromise, + txResponse.wait(), + awaiterScan, + ]); + if (txReceipt == null) { + throw new Error('No transaction receipt for relay transaction'); + } + await wallet.refreshPOIsForTXIDVersion(chain, txidVersion, true); + + expect( + await wallet.getBalanceERC20(txidVersion, chain, WETH_TOKEN_ADDRESS, [ + WalletBalanceBucket.Spendable, + ]), + ).to.equal(BigInt(9975 /* original */ - 100 /* broadcaster fee */ - 300 /* unshield amount */)); + + const callResultError = RelayAdapt7702Contract.getRelayAdaptCallError(txReceipt.logs); + expect(callResultError).to.equal(undefined); + + // TODO: Fix this test assertion. How much gas is used? + // const postEthBalance = await ethersWallet.getBalanceERC20(txidVersion, ); + // expect(preEthBalance - txReceipt.gasUsed + 300n).to.equal( + // postEthBalance, + // ); + }).timeout(300_000); + + it('[HH] Should execute relay adapt transaction for NFT transaction', async function run() { + if (!isDefined(process.env.RUN_HARDHAT_TESTS)) { + this.skip(); + return; + } + + // Shield WETH for Broadcaster fee. + await testShieldBaseToken(); + expect( + await wallet.getBalanceERC20(txidVersion, chain, WETH_TOKEN_ADDRESS, [ + WalletBalanceBucket.Spendable, + ]), + ).to.equal(9975n); + + // Mint NFTs with tokenIDs 0 and 1 into public balance. + await mintNFTsID01ForTest(nft, ethersWallet); + + // Approve NFT for shield. + const approval = await nft.approve.populateTransaction( + RailgunVersionedSmartContracts.getShieldApprovalContract(txidVersion, chain).address, + 1, + ); + const approvalTxResponse = await sendTransactionWithLatestNonce(ethersWallet, approval); + await approvalTxResponse.wait(); + + // Create shield + const shield = await shieldNFTForTest( + txidVersion, + wallet, + ethersWallet, + chain, + ByteUtils.randomHex(16), + NFT_ADDRESS, + '1', + ); + + const nftBalanceAfterShield = await nft.balanceOf( + RailgunVersionedSmartContracts.getAccumulator(txidVersion, chain).address, + ); + expect(nftBalanceAfterShield).to.equal(1n); + + const nftTokenData = shield.tokenData as NFTTokenData; + + // 1. Generate transaction batch to unshield necessary amount, and pay Broadcaster. + const transactionBatch = new TransactionBatch(chain); + const broadcasterFee = TransactNote.createTransfer( + wallet2.addressKeys, + wallet.addressKeys, + 300n, + wethTokenData, + false, // showSenderAddressToRecipient + OutputType.BroadcasterFee, + undefined, // memoText + ); + transactionBatch.addOutput(broadcasterFee); // Simulate Broadcaster fee output. + + const random = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd'; + + // 7702 Setup + const contract7702 = RelayAdaptVersionedSmartContracts.getRelayAdaptContract( + txidVersion, + chain, + true, + ) as RelayAdapt7702Contract; + const ephemeralWallet = deriveEphemeralWallet(config.mnemonic, 0); + // HACK: Set code of ephemeral address to RelayAdapt7702 code to simulate 7702 delegation + const code = await provider.getCode(contract7702.address); + await provider.send('hardhat_setCode', [ephemeralWallet.address, code]); + + const unshieldNote = new UnshieldNoteNFT( + ephemeralWallet.address, + shield.tokenData as NFTTokenData, + ); + transactionBatch.addUnshieldData(unshieldNote.unshieldData); + + // 2. Create dummy transactions from batch. + const dummyTransactions = await transactionBatch.generateDummyTransactions( + engine.prover, + wallet, + txidVersion, + testEncryptionKey, + ); + + // 3. Create the cross contract calls. + // Do nothing for now. + // TODO: Add a test NFT interaction via cross contract call. + const crossContractCalls: ContractTransaction[] = []; + + const authorization = await RelayAdapt7702Helper.signEIP7702Authorization( + ephemeralWallet, + contract7702.address, + BigInt(chain.id), + 0, + ); + + // 4. Create shield inputs. + const shieldRandom = '10203040506070809000102030405060'; + const relayShieldInputs = await RelayAdapt7702Helper.generateRelayShieldRequests( + shieldRandom, + [], + [{ nftTokenData, recipientAddress: wallet.getAddress() }], // shieldNFTRecipients + ); + + // 6. Get gas estimate from dummy txs. + const gasEstimateRandom = ByteUtils.randomHex(31); + + // Calculate params for signature + const orderedCallsGasEstimate = await contract7702.getOrderedCallsForCrossContractCalls( + crossContractCalls, + relayShieldInputs + ); + // HACK: Update to address for 7702 + orderedCallsGasEstimate.forEach((call) => { + if (call.to === contract7702.address) { + call.to = ephemeralWallet.address; + } + }); + + const minGasLimit = MINIMUM_RELAY_ADAPT_CROSS_CONTRACT_CALLS_GAS_LIMIT_V2; + const minGasLimitForContract = RelayAdapt7702Contract.getMinimumGasLimitForContract(minGasLimit); + + const actionDataGasEstimate = RelayAdapt7702Helper.getActionData( + gasEstimateRandom, + false, // requireSuccess (isGasEstimate=false, isBroadcasterTransaction=true -> false) + orderedCallsGasEstimate, + minGasLimitForContract, + ); + + const signatureGasEstimate = await RelayAdapt7702Helper.signExecutionAuthorization( + ephemeralWallet, + dummyTransactions, + actionDataGasEstimate, + BigInt(chain.id), + ); + + const populatedTransactionGasEstimate = + await RelayAdaptVersionedSmartContracts.populateCrossContractCalls( + txidVersion, + chain, + dummyTransactions, + crossContractCalls, + relayShieldInputs, + gasEstimateRandom, + false, // isGasEstimate + true, // isBroadcasterTransaction + undefined, + true, + authorization, + signatureGasEstimate, + ephemeralWallet.address + ); + populatedTransactionGasEstimate.from = DEAD_ADDRESS; + const gasEstimate = await RelayAdapt7702Contract.estimateGasWithErrorHandler( + provider, + populatedTransactionGasEstimate, + ); + expect(Number(gasEstimate)).to.be.greaterThan( + Number( + RelayAdapt7702Contract.getMinimumGasLimitForContract( + MINIMUM_RELAY_ADAPT_CROSS_CONTRACT_CALLS_GAS_LIMIT_V2, + ), + ), + ); + expect(Number(gasEstimate)).to.be.lessThan( + Number(MINIMUM_RELAY_ADAPT_CROSS_CONTRACT_CALLS_GAS_LIMIT_V2), + ); + // 7. Create real transactions with relay adapt params. + + const relayAdaptParams = + await RelayAdaptVersionedSmartContracts.getRelayAdaptParamsCrossContractCalls( + txidVersion, + chain, + dummyTransactions, + crossContractCalls, + relayShieldInputs, + random, + true, // isBroadcasterTransaction + undefined, // minGasLimit + true, // isRelayAdapt7702 + ephemeralWallet.address, + ); + transactionBatch.setAdaptID({ + contract: ephemeralWallet.address, + parameters: relayAdaptParams, + }); + const { provedTransactions } = await transactionBatch.generateTransactions( + engine.prover, + wallet, + txidVersion, + testEncryptionKey, + () => { }, + false, // shouldGeneratePreTransactionPOIs + ); + for (const transaction of provedTransactions) { + expect((transaction as TransactionStructV2).boundParams.adaptContract).to.equal( + ephemeralWallet.address, + ); + expect((transaction as TransactionStructV2).boundParams.adaptParams).to.equal( + relayAdaptParams, + ); + } + + // Sign Execution Auth + const orderedCallsFinal = await contract7702.getOrderedCallsForCrossContractCalls( + crossContractCalls, + relayShieldInputs + ); + // HACK: Update to address for 7702 + orderedCallsFinal.forEach((call) => { + if (call.to === contract7702.address) { + call.to = ephemeralWallet.address; + } + }); + + const actionDataFinal = RelayAdapt7702Helper.getActionData( + random, + false, // requireSuccess + orderedCallsFinal, + minGasLimitForContract, + ); + + const signatureFinal = await RelayAdapt7702Helper.signExecutionAuthorization( + ephemeralWallet, + provedTransactions, + actionDataFinal, + BigInt(chain.id), + ); + + // 8. Generate real relay transaction for cross contract call. + const relayTransaction = await RelayAdaptVersionedSmartContracts.populateCrossContractCalls( + txidVersion, + chain, + provedTransactions, + crossContractCalls, + relayShieldInputs, + random, + false, // isGasEstimate + true, // isBroadcasterTransaction + undefined, // minGasLimit + true, // isRelayAdapt7702 + authorization, + signatureFinal, + ephemeralWallet.address, + ); + const gasEstimateFinal = await provider.estimateGas(relayTransaction); + expect(Math.abs(Number(gasEstimate - gasEstimateFinal))).to.be.below( + 15000, + 'Gas difference from estimate (dummy) to final transaction should be less than 15000', + ); + + // 9: Send relay transaction. + const transactEventPromise = awaitRailgunSmartWalletTransact(txidVersion, chain); + const txResponse = await sendTransactionWithLatestNonce(ethersWallet, relayTransaction); + + // Perform scans: Unshield and Shield + const scansAwaiter = awaitMultipleScans(wallet, chain, 2); + + const [txReceipt] = await Promise.all([ + txResponse.wait(), + transactEventPromise, + ]); + if (txReceipt == null) { + throw new Error('No transaction receipt for relay transaction'); + } + + const callResultError = RelayAdapt7702Contract.getRelayAdaptCallError(txReceipt.logs); + expect(callResultError).to.equal(undefined); + + const nftBalanceAfterReshield = await nft.balanceOf( + RailgunVersionedSmartContracts.getAccumulator(txidVersion, chain).address, + ); + expect(nftBalanceAfterReshield).to.equal(1n); + }).timeout(300_000); + + it('[HH] Should shield all leftover WETH in relay adapt contract', async function run() { + if (!isDefined(process.env.RUN_HARDHAT_TESTS)) { + this.skip(); + return; + } + + await testShieldBaseToken(); + expect( + await wallet.getBalanceERC20(txidVersion, chain, WETH_TOKEN_ADDRESS, [ + WalletBalanceBucket.Spendable, + ]), + ).to.equal(9975n); + + // 1. Generate transaction batch to unshield necessary amount, and pay Broadcaster. + const transactionBatch = new TransactionBatch(chain); + + // 7702 Setup + const contract7702 = RelayAdaptVersionedSmartContracts.getRelayAdaptContract( + txidVersion, + chain, + true, + ) as RelayAdapt7702Contract; + const ephemeralWallet = deriveEphemeralWallet(config.mnemonic, 0); + + // HACK: Set code of ephemeral address to RelayAdapt7702 code to simulate 7702 delegation + const code = await provider.getCode(contract7702.address); + await provider.send('hardhat_setCode', [ephemeralWallet.address, code]); + + const unshieldNote = new UnshieldNoteERC20( + ephemeralWallet.address, + 1000n, + WETH_TOKEN_ADDRESS, + ); + transactionBatch.addUnshieldData(unshieldNote.unshieldData); + + const { provedTransactions: serializedTxs } = await transactionBatch.generateTransactions( + engine.prover, + wallet, + txidVersion, + testEncryptionKey, + () => { }, + false, // shouldGeneratePreTransactionPOIs + ); + const transact = await RailgunVersionedSmartContracts.generateTransact( + txidVersion, + chain, + serializedTxs, + ); + + // Unshield to relay adapt. + const transactEventPromise = awaitRailgunSmartWalletTransact(txidVersion, chain); + const unshieldEventPromise = awaitRailgunSmartWalletUnshield(txidVersion, chain); + + const txTransact = await sendTransactionWithLatestNonce(ethersWallet, transact); + + await Promise.all([ + transactEventPromise, + unshieldEventPromise, + awaitMultipleScans(wallet, chain, 2), + txTransact.wait(), + ]); + + const wethTokenContract = new Contract( + WETH_TOKEN_ADDRESS, + erc20Abi, + ethersWallet, + ) as unknown as TestERC20; + + let relayAdaptAddressBalance: bigint = await wethTokenContract.balanceOf( + ephemeralWallet.address, + ); + expect(relayAdaptAddressBalance).to.equal(998n); + + // 9975 - 1000 + expect( + await wallet.getBalanceERC20(txidVersion, chain, WETH_TOKEN_ADDRESS, [ + WalletBalanceBucket.Spendable, + ]), + ).to.equal(8975n); + + // Value 0n doesn't matter - all WETH remaining in Relay Adapt will be shielded. + // Manual 7702 Shield of leftover + const shield = new ShieldNoteERC20( + wallet.masterPublicKey, + SHIELD_RANDOM, + 0n, // Shield all + WETH_TOKEN_ADDRESS, + ); + const shieldPrivateKey = ByteUtils.hexToBytes(ByteUtils.randomHex(32)); + const shieldRequest = await shield.serialize( + shieldPrivateKey, + wallet.getViewingKeyPair().pubkey, + ); + + const orderedCalls = await contract7702.getOrderedCallsForShieldBaseToken( + shieldRequest, + ephemeralWallet.address, + ); + + const random31Bytes = ByteUtils.randomHex(31); + + const actionData = RelayAdapt7702Helper.getActionData( + random31Bytes, + true, // requireSuccess + orderedCalls, + BigInt(0), // minGasLimit + ); + + // Sign 7702 Auth + const authorization = await RelayAdapt7702Helper.signEIP7702Authorization( + ephemeralWallet, + contract7702.address, + BigInt(chain.id), + 0, // nonce + ); + + // Sign Execution Auth + const signature = await RelayAdapt7702Helper.signExecutionAuthorization( + ephemeralWallet, + [], // transactions (empty for shield base token) + actionData, + BigInt(chain.id), + ); + + const shieldTx = await RelayAdaptVersionedSmartContracts.populateShieldBaseToken( + txidVersion, + chain, + shieldRequest, + true, // isRelayAdapt7702 + authorization, + signature, + random31Bytes, + ephemeralWallet.address, + ); + + const shieldEventPromise = awaitRailgunSmartWalletShield(txidVersion, chain); + + // Send shield on chain + const tx = await sendTransactionWithLatestNonce(ethersWallet, shieldTx); + + await Promise.all([ + shieldEventPromise, + tx.wait(), + promiseTimeout( + awaitScan(wallet, chain), + 20000, + 'Timed out shielding base token for relay adapt test setup', + ), + ]); + await wallet.refreshPOIsForTXIDVersion(chain, txidVersion, true); + + relayAdaptAddressBalance = await wethTokenContract.balanceOf( + ephemeralWallet.address, + ); + expect(relayAdaptAddressBalance).to.equal(0n); + + // 9975 - 1000 + 998 - 2 (fee) + expect( + await wallet.getBalanceERC20(txidVersion, chain, WETH_TOKEN_ADDRESS, [ + WalletBalanceBucket.Spendable, + ]), + ).to.equal(9971n); + }).timeout(300_000); + + it('[HH] Should execute relay adapt transaction for cross contract call', async function run() { + if (!isDefined(process.env.RUN_HARDHAT_TESTS)) { + this.skip(); + return; + } + + await testShieldBaseToken(); + expect( + await wallet.getBalanceERC20(txidVersion, chain, WETH_TOKEN_ADDRESS, [ + WalletBalanceBucket.Spendable, + ]), + ).to.equal(9975n); + + // 1. Generate transaction batch to unshield necessary amount, and pay Broadcaster. + const transactionBatch = new TransactionBatch(chain); + const broadcasterFee = TransactNote.createTransfer( + wallet2.addressKeys, + wallet.addressKeys, + 300n, + wethTokenData, + false, // showSenderAddressToRecipient + OutputType.BroadcasterFee, + undefined, // memoText + ); + transactionBatch.addOutput(broadcasterFee); // Simulate Broadcaster fee output. + + // 7702 Setup + const contract7702 = RelayAdaptVersionedSmartContracts.getRelayAdaptContract( + txidVersion, + chain, + true, + ) as RelayAdapt7702Contract; + const ephemeralWallet = deriveEphemeralWallet(config.mnemonic, 0); + + // HACK: Set code of ephemeral address to RelayAdapt7702 code to simulate 7702 delegation + const code = await provider.getCode(contract7702.address); + await provider.send('hardhat_setCode', [ephemeralWallet.address, code]); + + const unshieldNote = new UnshieldNoteERC20( + ephemeralWallet.address, + 1000n, + WETH_TOKEN_ADDRESS, + ); + transactionBatch.addUnshieldData(unshieldNote.unshieldData); + + // 2. Create dummy transactions from batch. + const dummyTransactions = await transactionBatch.generateDummyTransactions( + engine.prover, + wallet, + txidVersion, + testEncryptionKey, + ); + + // 3. Create the cross contract call. + // Cross contract call: send 990n WETH tokens to Dead address. + const wethTokenContract = new Contract( + WETH_TOKEN_ADDRESS, + erc20Abi, + ethersWallet, + ) as unknown as TestERC20; + const sendToAddress = DEAD_ADDRESS; + const sendAmount = 990n; + const crossContractCalls: ContractTransaction[] = [ + await wethTokenContract.transfer.populateTransaction(sendToAddress, sendAmount), + ]; + + // 4. Create shield inputs. + const shieldRandom = '10203040506070809000102030405060'; + const shieldERC20Addresses: RelayAdaptShieldERC20Recipient[] = [ + { tokenAddress: WETH_TOKEN_ADDRESS, recipientAddress: wallet.getAddress() }, + ]; + const relayShieldInputs = await RelayAdapt7702Helper.generateRelayShieldRequests( + shieldRandom, + shieldERC20Addresses, + [], // shieldNFTRecipients + ); + + // 5. Get gas estimate from dummy txs. + const randomGasEstimate = ByteUtils.randomHex(31); + + const authorization = await RelayAdapt7702Helper.signEIP7702Authorization( + ephemeralWallet, + contract7702.address, + BigInt(chain.id), + 0, + ); + + const orderedCallsGasEstimate = await contract7702.getOrderedCallsForCrossContractCalls( + crossContractCalls, + relayShieldInputs + ); + // HACK: Update to address for 7702 + orderedCallsGasEstimate.forEach((call) => { + if (call.to === contract7702.address) { + call.to = ephemeralWallet.address; + } + }); + + const minGasLimit = MINIMUM_RELAY_ADAPT_CROSS_CONTRACT_CALLS_GAS_LIMIT_V2; + const minGasLimitForContract = RelayAdapt7702Contract.getMinimumGasLimitForContract(minGasLimit); + + const actionDataGasEstimate = RelayAdapt7702Helper.getActionData( + randomGasEstimate, + true, // requireSuccess (isGasEstimate=true -> true) + orderedCallsGasEstimate, + minGasLimitForContract, + ); + + const signatureGasEstimate = await RelayAdapt7702Helper.signExecutionAuthorization( + ephemeralWallet, + dummyTransactions, + actionDataGasEstimate, + BigInt(chain.id), + ); + + const populatedTransactionGasEstimate = + await RelayAdaptVersionedSmartContracts.populateCrossContractCalls( + txidVersion, + chain, + dummyTransactions, + crossContractCalls, + relayShieldInputs, + randomGasEstimate, + true, // isGasEstimate + true, // isBroadcasterTransaction + undefined, // minGasLimit + true, // isRelayAdapt7702 + authorization, + signatureGasEstimate, + ephemeralWallet.address, + ); + populatedTransactionGasEstimate.from = DEAD_ADDRESS; + const gasEstimate = await RelayAdapt7702Contract.estimateGasWithErrorHandler( + provider, + populatedTransactionGasEstimate, + ); + expect(Number(gasEstimate)).to.be.greaterThan( + Number( + RelayAdapt7702Contract.getMinimumGasLimitForContract( + MINIMUM_RELAY_ADAPT_CROSS_CONTRACT_CALLS_GAS_LIMIT_V2, + ), + ), + ); + expect(Number(gasEstimate)).to.be.lessThan( + Number(MINIMUM_RELAY_ADAPT_CROSS_CONTRACT_CALLS_GAS_LIMIT_V2), + ); + + // 6. Create real transactions with relay adapt params. + const random = ByteUtils.randomHex(31); + const relayAdaptParams = + await RelayAdaptVersionedSmartContracts.getRelayAdaptParamsCrossContractCalls( + txidVersion, + chain, + dummyTransactions, + crossContractCalls, + relayShieldInputs, + random, + true, // isBroadcasterTransaction + undefined, // minGasLimit + true, // isRelayAdapt7702 + ephemeralWallet.address, + ); + transactionBatch.setAdaptID({ + contract: ephemeralWallet.address, + parameters: relayAdaptParams, + }); + const { provedTransactions } = await transactionBatch.generateTransactions( + engine.prover, + wallet, + txidVersion, + testEncryptionKey, + () => { }, + false, // shouldGeneratePreTransactionPOIs + ); + for (const transaction of provedTransactions) { + expect((transaction as TransactionStructV2).boundParams.adaptContract).to.equal( + ephemeralWallet.address, + ); + expect((transaction as TransactionStructV2).boundParams.adaptParams).to.equal( + relayAdaptParams, + ); + } + + // Sign Execution Auth + const orderedCallsFinal = await contract7702.getOrderedCallsForCrossContractCalls( + crossContractCalls, + relayShieldInputs + ); + // HACK: Update to address for 7702 + orderedCallsFinal.forEach((call) => { + if (call.to === contract7702.address) { + call.to = ephemeralWallet.address; + } + }); + + const actionDataFinal = RelayAdapt7702Helper.getActionData( + random, + false, // requireSuccess + orderedCallsFinal, + minGasLimitForContract, + ); + + const signatureFinal = await RelayAdapt7702Helper.signExecutionAuthorization( + ephemeralWallet, + provedTransactions, + actionDataFinal, + BigInt(chain.id), + ); + + // 7. Generate real relay transaction for cross contract call. + const relayTransaction = await RelayAdaptVersionedSmartContracts.populateCrossContractCalls( + txidVersion, + chain, + provedTransactions, + crossContractCalls, + relayShieldInputs, + random, + false, // isGasEstimate + true, // isBroadcasterTransaction + undefined, // minGasLimit + true, // isRelayAdapt7702 + authorization, + signatureFinal, + ephemeralWallet.address, + ); + const gasEstimateFinal = await provider.estimateGas(relayTransaction); + + expect(Math.abs(Number(gasEstimate - gasEstimateFinal))).to.be.below( + 10000, + 'Gas difference from estimate (dummy) to final transaction should be less than 10000', + ); + + // Add 20% to gasEstimate for gasLimit. + relayTransaction.gasLimit = (gasEstimate * 120n) / 100n; + + // 8. Send transaction. + const transactEventPromise = awaitRailgunSmartWalletTransact(txidVersion, chain); + const txResponse = await sendTransactionWithLatestNonce(ethersWallet, relayTransaction); + + // Perform scans: Unshield and Shield + const scansAwaiter = awaitMultipleScans(wallet, chain, 2); + + const [txReceipt] = await Promise.all([ + txResponse.wait(), + transactEventPromise, + ]); + if (txReceipt == null) { + throw new Error('No transaction receipt for relay transaction'); + } + + await expect(scansAwaiter).to.be.fulfilled; + await wallet.refreshPOIsForTXIDVersion(chain, txidVersion, true); + + // Dead address should have 990n WETH. + const sendAddressBalance: bigint = await wethTokenContract.balanceOf(sendToAddress); + expect(sendAddressBalance).to.equal(sendAmount); + + const relayAdaptAddressBalance: bigint = await wethTokenContract.balanceOf( + RelayAdaptVersionedSmartContracts.getRelayAdaptContract(txidVersion, chain, true).address, + ); + expect(relayAdaptAddressBalance).to.equal(0n); + + const callResultError = RelayAdapt7702Contract.getRelayAdaptCallError(txReceipt.logs); + expect(callResultError).to.equal(undefined); + + const expectedPrivateWethBalance = BigInt( + 9975 /* original shield */ - + 300 /* broadcaster fee */ - + 1000 /* unshield */ + + 8 /* re-shield (1000 unshield amount - 2 unshield fee - 990 send amount - 0 re-shield fee) */, + ); + const expectedTotalPrivateWethBalance = expectedPrivateWethBalance + 300n; // Add broadcaster fee. + + const proxyWethBalance = await wethTokenContract.balanceOf( + RailgunVersionedSmartContracts.getAccumulator(txidVersion, chain).address, + ); + expect(proxyWethBalance).to.equal(expectedTotalPrivateWethBalance); + + const privateWalletBalance = await wallet.getBalanceERC20( + txidVersion, + chain, + WETH_TOKEN_ADDRESS, + [WalletBalanceBucket.Spendable], + ); + expect(privateWalletBalance).to.equal(expectedPrivateWethBalance); + }).timeout(300_000); + + it('[HH] Should revert send, but keep fees for failing cross contract call', async function run() { + if (!isDefined(process.env.RUN_HARDHAT_TESTS)) { + this.skip(); + return; + } + + await testShieldBaseToken(100000n); + expect( + await wallet.getBalanceERC20(txidVersion, chain, WETH_TOKEN_ADDRESS, [ + WalletBalanceBucket.Spendable, + ]), + ).to.equal(99750n); + + // 1. Generate transaction batch to unshield necessary amount, and pay Broadcaster. + const transactionBatch = new TransactionBatch(chain); + const broadcasterFee = TransactNote.createTransfer( + wallet2.addressKeys, + wallet.addressKeys, + 300n, + wethTokenData, + false, // showSenderAddressToRecipient + OutputType.BroadcasterFee, + undefined, // memoText + ); + transactionBatch.addOutput(broadcasterFee); // Simulate Broadcaster fee output. + + // 7702 Setup + const contract7702 = RelayAdaptVersionedSmartContracts.getRelayAdaptContract( + txidVersion, + chain, + true, + ) as RelayAdapt7702Contract; + const ephemeralWallet = deriveEphemeralWallet(config.mnemonic, 0); + + // HACK: Set code of ephemeral address to RelayAdapt7702 code to simulate 7702 delegation + const code = await provider.getCode(contract7702.address); + await provider.send('hardhat_setCode', [ephemeralWallet.address, code]); + + const unshieldNote = new UnshieldNoteERC20( + ephemeralWallet.address, + 10000n, + WETH_TOKEN_ADDRESS, + ); + transactionBatch.addUnshieldData(unshieldNote.unshieldData); + + // 2. Create dummy transactions from batch. + const dummyTransactions = await transactionBatch.generateDummyTransactions( + engine.prover, + wallet, + txidVersion, + testEncryptionKey, + ); + + // 3. Create the cross contract call. + // Cross contract call: send 1 WETH token to Dead address. + const wethTokenContract = new Contract( + WETH_TOKEN_ADDRESS, + erc20Abi, + ethersWallet, + ) as unknown as TestERC20; + const sendToAddress = DEAD_ADDRESS; + const sendAmount = 20000n; // More than is available (after 0.25% unshield fee). + const crossContractCalls: ContractTransaction[] = [ + await wethTokenContract.transfer.populateTransaction(sendToAddress, sendAmount), + ]; + + // 4. Create shield inputs. + const shieldRandom = '10203040506070809000102030405060'; + const shieldERC20Addresses: RelayAdaptShieldERC20Recipient[] = [ + { tokenAddress: WETH_TOKEN_ADDRESS, recipientAddress: wallet.getAddress() }, + ]; + const relayShieldInputs = await RelayAdapt7702Helper.generateRelayShieldRequests( + shieldRandom, + shieldERC20Addresses, + [], // shieldNFTRecipients + ); + + // 5. Get gas estimate from dummy txs. (Expect revert). + const gasEstimateRandom = ByteUtils.randomHex(31); + + const authorization = await RelayAdapt7702Helper.signEIP7702Authorization( + ephemeralWallet, + contract7702.address, + BigInt(chain.id), + 0, + ); + + const orderedCallsGasEstimate = await contract7702.getOrderedCallsForCrossContractCalls( + crossContractCalls, + relayShieldInputs + ); + // HACK: Update to address for 7702 + orderedCallsGasEstimate.forEach((call) => { + if (call.to === contract7702.address) { + call.to = ephemeralWallet.address; + } + }); + + const minGasLimit = MINIMUM_RELAY_ADAPT_CROSS_CONTRACT_CALLS_GAS_LIMIT_V2; + const minGasLimitForContract = RelayAdapt7702Contract.getMinimumGasLimitForContract(minGasLimit); + + const actionDataGasEstimate = RelayAdapt7702Helper.getActionData( + gasEstimateRandom, + true, // requireSuccess (isGasEstimate=true -> true) + orderedCallsGasEstimate, + minGasLimitForContract, + ); + + const signatureGasEstimate = await RelayAdapt7702Helper.signExecutionAuthorization( + ephemeralWallet, + dummyTransactions, + actionDataGasEstimate, + BigInt(chain.id), + ); + + const populatedTransactionGasEstimate = + await RelayAdaptVersionedSmartContracts.populateCrossContractCalls( + txidVersion, + chain, + dummyTransactions, + crossContractCalls, + relayShieldInputs, + gasEstimateRandom, + true, // isGasEstimate + true, // isBroadcasterTransaction + undefined, // minGasLimit + true, // isRelayAdapt7702 + authorization, + signatureGasEstimate, + ephemeralWallet.address, + ); + populatedTransactionGasEstimate.from = DEAD_ADDRESS; + await expect( + RelayAdapt7702Contract.estimateGasWithErrorHandler(provider, populatedTransactionGasEstimate), + ).to.be.rejectedWith('RelayAdapt multicall failed at index 0.'); + + // 6. Create real transactions with relay adapt params. + const random = ByteUtils.randomHex(31); + const relayAdaptParams = + await RelayAdaptVersionedSmartContracts.getRelayAdaptParamsCrossContractCalls( + txidVersion, + chain, + dummyTransactions, + crossContractCalls, + relayShieldInputs, + random, + true, // isBroadcasterTransaction + undefined, // minGasLimit + true, // isRelayAdapt7702 + ephemeralWallet.address, + ); + transactionBatch.setAdaptID({ + contract: ephemeralWallet.address, + parameters: relayAdaptParams, + }); + const { provedTransactions } = await transactionBatch.generateTransactions( + engine.prover, + wallet, + txidVersion, + testEncryptionKey, + () => { }, + false, // shouldGeneratePreTransactionPOIs + ); + for (const transaction of provedTransactions) { + expect((transaction as TransactionStructV2).boundParams.adaptContract).to.equal( + ephemeralWallet.address, + ); + expect((transaction as TransactionStructV2).boundParams.adaptParams).to.equal( + relayAdaptParams, + ); + } + + // Sign Execution Auth + const orderedCallsFinal = await contract7702.getOrderedCallsForCrossContractCalls( + crossContractCalls, + relayShieldInputs + ); + // HACK: Update to address for 7702 + orderedCallsFinal.forEach((call) => { + if (call.to === contract7702.address) { + call.to = ephemeralWallet.address; + } + }); + + const actionDataFinal = RelayAdapt7702Helper.getActionData( + random, + false, // requireSuccess + orderedCallsFinal, + minGasLimitForContract, + ); + + const signatureFinal = await RelayAdapt7702Helper.signExecutionAuthorization( + ephemeralWallet, + provedTransactions, + actionDataFinal, + BigInt(chain.id), + ); + + // 7. Generate real relay transaction for cross contract call. + const relayTransaction = await RelayAdaptVersionedSmartContracts.populateCrossContractCalls( + txidVersion, + chain, + provedTransactions, + crossContractCalls, + relayShieldInputs, + random, + false, // isGasEstimate + true, // isBroadcasterTransaction + undefined, // minGasLimit + true, // isRelayAdapt7702 + authorization, + signatureFinal, + ephemeralWallet.address, + ); + + // Set high gas limit. + relayTransaction.gasLimit = BigInt('25000000'); + + // 8. Send transaction. + const transactEventPromise = awaitRailgunSmartWalletTransact(txidVersion, chain); + const unshieldEventPromise = awaitRailgunSmartWalletUnshield(txidVersion, chain); + + const txResponse = await sendTransactionWithLatestNonce(ethersWallet, relayTransaction); + + // Perform scans: Unshield and Shield + const scansAwaiter = awaitMultipleScans(wallet, chain, 2); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_t, _u, txReceipt] = await Promise.all([ + transactEventPromise, + unshieldEventPromise, + txResponse.wait(), + ]); + if (txReceipt == null) { + throw new Error('No transaction receipt for relay transaction'); + } + + await expect(scansAwaiter).to.be.fulfilled; + await wallet.refreshPOIsForTXIDVersion(chain, txidVersion, true); + + // Dead address should have 0 WETH. + const sendAddressBalance: bigint = await wethTokenContract.balanceOf(sendToAddress); + expect(sendAddressBalance).to.equal(0n); + + const relayAdaptAddressBalance: bigint = await wethTokenContract.balanceOf( + RelayAdaptVersionedSmartContracts.getRelayAdaptContract(txidVersion, chain, true).address, + ); + expect(relayAdaptAddressBalance).to.equal(0n); + + const callResultError = RelayAdapt7702Contract.getRelayAdaptCallError(txReceipt.logs); + expect(callResultError).to.equal( + 'Unknown Relay Adapt error: No utf8 string parsed from revert reason.', + ); + + const expectedPrivateWethBalance = BigInt( + 99750 /* original */ - + 300 /* broadcaster fee */ - + 10000 /* unshield amount */ - + 0 /* failed cross contract send: no change */ + + 9975 /* re-shield amount */ - + 24 /* shield fee */, + ); + const expectedTotalPrivateWethBalance = expectedPrivateWethBalance + 300n; // Add broadcaster fee. + + const proxyWethBalance = await wethTokenContract.balanceOf( + RailgunVersionedSmartContracts.getAccumulator(txidVersion, chain).address, + ); + const privateWalletBalance = await wallet.getBalanceERC20( + txidVersion, + chain, + WETH_TOKEN_ADDRESS, + [WalletBalanceBucket.Spendable], + ); + + expect(proxyWethBalance).to.equal(expectedTotalPrivateWethBalance); + expect(privateWalletBalance).to.equal(expectedPrivateWethBalance); + }).timeout(300_000); + + it('[HH] Should revert send for failing re-shield', async function run() { + if (!isDefined(process.env.RUN_HARDHAT_TESTS)) { + this.skip(); + return; + } + + await testShieldBaseToken(100000n); + expect( + await wallet.getBalanceERC20(txidVersion, chain, WETH_TOKEN_ADDRESS, [ + WalletBalanceBucket.Spendable, + ]), + ).to.equal(99750n); + + // 1. Generate transaction batch to unshield necessary amount, and pay Broadcaster. + const transactionBatch = new TransactionBatch(chain); + const broadcasterFee = TransactNote.createTransfer( + wallet2.addressKeys, + wallet.addressKeys, + 300n, + wethTokenData, + false, // showSenderAddressToRecipient + OutputType.BroadcasterFee, + undefined, // memoText + ); + transactionBatch.addOutput(broadcasterFee); // Simulate Broadcaster fee output. + + // 7702 Setup + const contract7702 = RelayAdaptVersionedSmartContracts.getRelayAdaptContract( + txidVersion, + chain, + true, + ) as RelayAdapt7702Contract; + const ephemeralWallet = deriveEphemeralWallet(config.mnemonic, 0); + + // HACK: Set code of ephemeral address to RelayAdapt7702 code to simulate 7702 delegation + const code = await provider.getCode(contract7702.address); + await provider.send('hardhat_setCode', [ephemeralWallet.address, code]); + + const unshieldNote = new UnshieldNoteERC20( + ephemeralWallet.address, + 10000n, + WETH_TOKEN_ADDRESS, + ); + transactionBatch.addUnshieldData(unshieldNote.unshieldData); + + // 2. Create dummy transactions from batch. + const dummyTransactions = await transactionBatch.generateDummyTransactions( + engine.prover, + wallet, + txidVersion, + testEncryptionKey, + ); + + // 3. Create the cross contract call. + // Cross contract call: send 1 WETH token to Dead address. + const wethTokenContract = new Contract( + WETH_TOKEN_ADDRESS, + erc20Abi, + ethersWallet, + ) as unknown as TestERC20; + const sendToAddress = DEAD_ADDRESS; + const sendAmount = 20000n; // More than is available (after 0.25% unshield fee). + const crossContractCalls: ContractTransaction[] = [ + await wethTokenContract.transfer.populateTransaction(sendToAddress, sendAmount), + ]; + + // 4. Create shield inputs. + const shieldRandom = '10203040506070809000102030405060'; + const shieldERC20Addresses: RelayAdaptShieldERC20Recipient[] = [ + { tokenAddress: WETH_TOKEN_ADDRESS, recipientAddress: wallet.getAddress() }, + ]; + const relayShieldInputs = await RelayAdapt7702Helper.generateRelayShieldRequests( + shieldRandom, + shieldERC20Addresses, + [], // shieldNFTRecipients + ); + + // 5. Get gas estimate from dummy txs. (Expect revert). + const randomGasEstimate = ByteUtils.randomHex(31); + + const authorization = await RelayAdapt7702Helper.signEIP7702Authorization( + ephemeralWallet, + contract7702.address, + BigInt(chain.id), + 0, + ); + + const orderedCallsGasEstimate = await contract7702.getOrderedCallsForCrossContractCalls( + crossContractCalls, + relayShieldInputs + ); + // HACK: Update to address for 7702 + orderedCallsGasEstimate.forEach((call) => { + if (call.to === contract7702.address) { + call.to = ephemeralWallet.address; + } + }); + + const minGasLimit = MINIMUM_RELAY_ADAPT_CROSS_CONTRACT_CALLS_GAS_LIMIT_V2; + const minGasLimitForContract = RelayAdapt7702Contract.getMinimumGasLimitForContract(minGasLimit); + + const actionDataGasEstimate = RelayAdapt7702Helper.getActionData( + randomGasEstimate, + true, // requireSuccess (isGasEstimate=true -> true) + orderedCallsGasEstimate, + minGasLimitForContract, + ); + + const signatureGasEstimate = await RelayAdapt7702Helper.signExecutionAuthorization( + ephemeralWallet, + dummyTransactions, + actionDataGasEstimate, + BigInt(chain.id), + ); + + const populatedTransactionGasEstimate = + await RelayAdaptVersionedSmartContracts.populateCrossContractCalls( + txidVersion, + chain, + dummyTransactions, + crossContractCalls, + relayShieldInputs, + randomGasEstimate, + true, // isGasEstimate + true, // isBroadcasterTransaction + undefined, // minGasLimit + true, // isRelayAdapt7702 + authorization, + signatureGasEstimate, + ephemeralWallet.address, + ); + populatedTransactionGasEstimate.from = DEAD_ADDRESS; + await expect( + RelayAdapt7702Contract.estimateGasWithErrorHandler(provider, populatedTransactionGasEstimate), + ).to.be.rejectedWith('RelayAdapt multicall failed at index 0.'); + + // 6. Create real transactions with relay adapt params. + const random = ByteUtils.randomHex(31); + const relayAdaptParams = + await RelayAdaptVersionedSmartContracts.getRelayAdaptParamsCrossContractCalls( + txidVersion, + chain, + dummyTransactions, + crossContractCalls, + relayShieldInputs, + random, + true, // isBroadcasterTransaction + undefined, // minGasLimit + true, // isRelayAdapt7702 + ephemeralWallet.address, + ); + transactionBatch.setAdaptID({ + contract: ephemeralWallet.address, + parameters: relayAdaptParams, + }); + const { provedTransactions } = await transactionBatch.generateTransactions( + engine.prover, + wallet, + txidVersion, + testEncryptionKey, + () => { }, + false, // shouldGeneratePreTransactionPOIs + ); + for (const transaction of provedTransactions) { + expect((transaction as TransactionStructV2).boundParams.adaptContract).to.equal( + ephemeralWallet.address, + ); + expect((transaction as TransactionStructV2).boundParams.adaptParams).to.equal( + relayAdaptParams, + ); + } + + // Sign Execution Auth + const orderedCallsFinal = await contract7702.getOrderedCallsForCrossContractCalls( + crossContractCalls, + relayShieldInputs + ); + // HACK: Update to address for 7702 + orderedCallsFinal.forEach((call) => { + if (call.to === contract7702.address) { + call.to = ephemeralWallet.address; + } + }); + + const actionDataFinal = RelayAdapt7702Helper.getActionData( + random, + false, // requireSuccess + orderedCallsFinal, + minGasLimitForContract, + ); + + const signatureFinal = await RelayAdapt7702Helper.signExecutionAuthorization( + ephemeralWallet, + provedTransactions, + actionDataFinal, + BigInt(chain.id), + ); + + // 7. Generate real relay transaction for cross contract call. + const relayTransaction = await RelayAdaptVersionedSmartContracts.populateCrossContractCalls( + txidVersion, + chain, + provedTransactions, + crossContractCalls, + relayShieldInputs, + random, + false, // isGasEstimate + true, // isBroadcasterTransaction + undefined, // minGasLimit + true, // isRelayAdapt7702 + authorization, + signatureFinal, + ephemeralWallet.address, + ); + + const gasEstimateFinal = await provider.estimateGas(relayTransaction); + + // Gas estimate is currently an underestimate (which is a bug). + // Set gas limit to this value, which should revert inside the smart contract. + relayTransaction.gasLimit = (gasEstimateFinal * 101n) / 100n; + + // 8. Send transaction. + const transactEventPromise = awaitRailgunSmartWalletTransact(txidVersion, chain); + const txResponse = await sendTransactionWithLatestNonce(ethersWallet, relayTransaction); + + // Perform scans: Unshield and Shield + const scansAwaiter = awaitMultipleScans(wallet, chain, 2); + + const [txReceipt] = await Promise.all([ + txResponse.wait(), + scansAwaiter, + transactEventPromise, + ]); + + if (txReceipt == null) { + throw new Error('No transaction receipt for relay transaction'); + } + + await expect(scansAwaiter).to.be.fulfilled; + await wallet.refreshPOIsForTXIDVersion(chain, txidVersion, true); + + // Dead address should have 0 WETH. + const sendAddressBalance: bigint = await wethTokenContract.balanceOf(sendToAddress); + expect(sendAddressBalance).to.equal(0n); + + const relayAdaptAddressBalance: bigint = await wethTokenContract.balanceOf( + RelayAdaptVersionedSmartContracts.getRelayAdaptContract(txidVersion, chain, true).address, + ); + + expect(relayAdaptAddressBalance).to.equal(0n); + + const callResultError = RelayAdapt7702Contract.getRelayAdaptCallError(txReceipt.logs); + expect(callResultError).to.equal( + 'Unknown Relay Adapt error: No utf8 string parsed from revert reason.', + ); + + // TODO: These are the incorrect assertions, if the tx is fully reverted. This requires a callbacks upgrade to contract. + // For now, it is partially reverted. Unshield/shield fees are still charged. + // This caps the loss of funds at 0.5% + Broadcaster fee. + + const expectedProxyBalance = BigInt( + 99750 /* original */ - 25 /* unshield fee */ - 24 /* re-shield fee */, + ); + const expectedWalletBalance = BigInt(expectedProxyBalance - 300n /* broadcaster fee */); + + const treasuryBalance: bigint = await wethTokenContract.balanceOf( + config.contracts.treasuryProxy, + ); + expect(treasuryBalance).to.equal(299n); + + const proxyWethBalance = await wethTokenContract.balanceOf( + RailgunVersionedSmartContracts.getAccumulator(txidVersion, chain).address, + ); + const privateWalletBalance = await wallet.getBalanceERC20( + txidVersion, + chain, + WETH_TOKEN_ADDRESS, + [WalletBalanceBucket.Spendable], + ); + + expect(proxyWethBalance).to.equal(expectedProxyBalance); + expect(privateWalletBalance).to.equal(expectedWalletBalance); + + // + // These are the correct assertions.... + // + + // const expectedPrivateWethBalance = BigInt(99750 /* original */); + + // const treasuryBalance: bigint = await wethTokenContract.balanceOf(config.contracts.treasuryProxy); + // expect(treasuryBalance).to.equal(250n); + + // const proxyWethBalance = (await wethTokenContract.balanceOf(RailgunVersionedSmartContracts.getAccumulator(txidVersion, chain).address)); + // const privateWalletBalance = await wallet.getBalanceERC20(txidVersion, chain, WETH_TOKEN_ADDRESS, [WalletBalanceBucket.Spendable]); + + // expect(proxyWethBalance).to.equal(expectedPrivateWethBalance); + // expect(privateWalletBalance).to.equal(expectedPrivateWethBalance); + }).timeout(300_000); + + it('Should generate relay shield notes and inputs', async () => { + const shieldERC20Recipients: RelayAdaptShieldERC20Recipient[] = [ + { + tokenAddress: config.contracts.weth9.toLowerCase(), + recipientAddress: wallet.getAddress(), + }, + { + tokenAddress: config.contracts.rail.toLowerCase(), + recipientAddress: wallet.getAddress(), + }, + ]; + + const random = '10203040506070809000102030405060'; + const relayShieldInputs = await RelayAdapt7702Helper.generateRelayShieldRequests( + random, + shieldERC20Recipients, + [], // shieldNFTRecipients + ); + + expect(relayShieldInputs.length).to.equal(2); + expect( + relayShieldInputs.map((shieldInput) => shieldInput.preimage.token.tokenAddress), + ).to.deep.equal(shieldERC20Recipients.map((recipient) => recipient.tokenAddress.toLowerCase())); + for (const relayShieldInput of relayShieldInputs) { + expect(relayShieldInput.preimage.npk).to.equal( + ByteUtils.nToHex( + 3348140451435708797167073859596593490034226162440317170509481065740328487080n, + ByteLength.UINT_256, + true, + ), + ); + expect(relayShieldInput.preimage.token.tokenType).to.equal(0); + } + }); + + it('Should calculate relay adapt params', () => { + const nullifiers = [ + new Uint8Array([ + 42, 178, 205, 78, 49, 222, 35, 76, 140, 83, 19, 50, 218, 74, 38, 161, 4, 32, 213, 247, 186, + 238, 81, 137, 50, 61, 32, 21, 178, 16, 168, 32, + ]), + new Uint8Array([ + 5, 228, 162, 212, 44, 195, 165, 245, 46, 252, 85, 67, 78, 165, 80, 86, 216, 220, 217, 118, + 198, 92, 41, 84, 51, 159, 175, 75, 194, 103, 163, 115, + ]), + ].map((n) => '0x' + bytesToHex(n)); + + const random = bytesToHex( + new Uint8Array([ + 134, 114, 120, 89, 227, 254, 124, 13, 129, 226, 125, 250, 250, 240, 217, 194, 183, 180, 136, + 153, 29, 44, 89, 196, 146, 178, 37, 250, 159, 195, 7, + ]), + ); + + const relayAdaptParams = RelayAdapt7702Helper.getRelayAdaptParams( + [{ nullifiers } as unknown as TransactionStructV2], + random, + false, + [ + { + to: '0x8f86403A4DE0BB5791fa46B8e795C547942fE4Cf', + data: + '0x' + + bytesToHex( + new Uint8Array([ + 210, 140, 37, 212, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 104, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + ), + value: 0n, + }, + ], + 10000000n, + ); + + const expectedParamsHex = + '0x' + + bytesToHex( + new Uint8Array([ + 158, 121, 120, 133, 213, 222, 191, 122, 18, 206, 124, 208, 113, 253, 244, 194, 183, 180, 136, + 153, 29, 44, 89, 196, 146, 178, 37, 250, 159, 195, 7, 0, + ]), + ); + + // expect(relayAdaptParams).to.equal(expectedParamsHex); + }); + + it('Should decode and parse relay adapt error logs (from failed Sushi V2 LP removal)', () => { + const relayAdaptError = RelayAdapt7702Contract.getRelayAdaptCallError( + FormattedRelayAdaptErrorLogs, + ); + expect(relayAdaptError).to.equal('ds-math-sub-underflow'); + }); + + it('Should extract call failed index and error message from ethers error', () => { + const errorText = `execution reverted (unknown custom error) (action="estimateGas", data="0x5c0dee5d00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006408c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001564732d6d6174682d7375622d756e646572666c6f77000000000000000000000000000000000000000000000000000000000000000000000000000000", reason=null, transaction={ "data": "0x28223a77000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000007a00000000000000000000000000000000000000000000000000…00000000004640cd6086ade3e984b011b4e8c7cab9369b90499ab88222e673ec1ae4d2c3bf78ae96e95f9171653e5b1410273269edd64a0ab792a5d355093caa9cb92406125c7803a48028503783f2ab5e84f0ea270ce770860e436b77c942ed904a5d577d021cf0fd936183e0298175679d63d73902e116484e10c7b558d4dc84e113380500000000000000000000000000000000000000000000000000000000", "from": "0x000000000000000000000000000000000000dEaD", "to": "0x0355B7B8cb128fA5692729Ab3AAa199C1753f726" }, invocation=null, revert=null, code=CALL_EXCEPTION, version=6.4.0)`; + const { callFailedIndexString, errorMessage } = + RelayAdapt7702Contract.extractGasEstimateCallFailedIndexAndErrorText(errorText); + expect(callFailedIndexString).to.equal('5'); + expect(errorMessage).to.equal( + `'execution reverted (unknown custom error)': ds-math-sub-underflow`, + ); + }); + + it('Should parse relay adapt log revert data - relay adapt abi value', () => { + const parsed = RelayAdapt7702Contract.parseRelayAdaptReturnValue( + `0x5c0dee5d00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006408c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001564732d6d6174682d7375622d756e646572666c6f77000000000000000000000000000000000000000000000000000000000000000000000000000000`, + ); + expect(parsed?.callIndex).to.equal(5); + expect(parsed?.error).to.equal('ds-math-sub-underflow'); + }); + + it('Should parse relay adapt log revert data - string value', () => { + const parsed = RelayAdapt7702Contract.parseRelayAdaptReturnValue( + `0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000205261696c67756e4c6f6769633a204e6f746520616c7265616479207370656e74`, + ); + expect(parsed?.callIndex).to.equal(undefined); + expect(parsed?.error).to.equal('RailgunLogic: Note already spent'); + }); + + it('Should parse relay adapt log revert data - string value from railgun cookbook transaction', () => { + const parsed = RelayAdapt7702Contract.parseRelayAdaptReturnValue( + `0x5c0dee5d00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002d52656c617941646170743a205265667573696e6720746f2063616c6c205261696c67756e20636f6e747261637400000000000000000000000000000000000000`, + ); + expect(parsed?.callIndex).to.equal(2); + expect(parsed?.error).to.equal('RelayAdapt: Refusing to call Railgun contract'); + }); + + it('Should extract call failed index and error message from non-parseable ethers error', () => { + const errorText = `not a parseable error`; + const { callFailedIndexString, errorMessage } = + RelayAdapt7702Contract.extractGasEstimateCallFailedIndexAndErrorText(errorText); + expect(callFailedIndexString).to.equal('UNKNOWN'); + expect(errorMessage).to.equal('not a parseable error'); + }); + + afterEach(async () => { + if (!isDefined(process.env.RUN_HARDHAT_TESTS)) { + return; + } + await engine.unload(); + await provider.send('evm_revert', [snapshot]); + }); +}); diff --git a/src/contracts/relay-adapt/index.ts b/src/contracts/relay-adapt/index.ts index 206c404f..7ef2149a 100644 --- a/src/contracts/relay-adapt/index.ts +++ b/src/contracts/relay-adapt/index.ts @@ -1,3 +1,6 @@ export * from './relay-adapt-versioned-smart-contracts'; export * from './relay-adapt-helper'; +export * from './relay-adapt-7702-helper'; export * from './constants'; +export * from './relay-adapt-types'; + diff --git a/src/contracts/relay-adapt/relay-adapt-7702-helper.ts b/src/contracts/relay-adapt/relay-adapt-7702-helper.ts new file mode 100644 index 00000000..d94e77fb --- /dev/null +++ b/src/contracts/relay-adapt/relay-adapt-7702-helper.ts @@ -0,0 +1,238 @@ +import { ContractTransaction, AbiCoder, keccak256, Wallet, encodeRlp, toBeHex, concat, Interface, HDNodeWallet } from 'ethers'; +import { ByteUtils } from '../../utils/bytes'; +import { ShieldNoteERC20 } from '../../note/erc20/shield-note-erc20'; +import { AddressData, decodeAddress } from '../../key-derivation'; +import { + NFTTokenData, + RelayAdaptShieldERC20Recipient, + RelayAdaptShieldNFTRecipient, + TokenType, +} from '../../models/formatted-types'; +import { ShieldNoteNFT } from '../../note/nft/shield-note-nft'; +import { ERC721_NOTE_VALUE } from '../../note/note-util'; +import { RelayAdapt, ShieldRequestStruct } from '../../abi/typechain/RelayAdapt'; +import { TransactionStructV2, TransactionStructV3 } from '../../models/transaction-types'; +import { EIP7702Authorization } from '../../models/relay-adapt-types'; +import { ABIRelayAdapt7702 } from '../../abi/abi'; + +class RelayAdapt7702Helper { + static async signEIP7702Authorization( + signer: Wallet | HDNodeWallet, + contractAddress: string, + chainId: bigint, + nonce: number, + ): Promise { + // 0x05 || rlp([chain_id, address, nonce]) + const rlpEncoded = encodeRlp([ + toBeHex(chainId), + contractAddress, + toBeHex(nonce), + ]); + const payload = concat(['0x05', rlpEncoded]); + const digest = keccak256(payload); + + const signature = signer.signingKey.sign(digest); + + return { + chainId: chainId.toString(), + address: contractAddress, + nonce, + yParity: signature.yParity, + r: signature.r, + s: signature.s, + }; + } + + static async signExecutionAuthorization( + signer: Wallet | HDNodeWallet, + transactions: (TransactionStructV2 | TransactionStructV3)[], + actionData: RelayAdapt.ActionDataStruct, + chainId: bigint, + ): Promise { + // 1. Extract nullifiers + const nullifiers = transactions.map(tx => tx.nullifiers); + + // 2. Encode adaptParams + // keccak256(abi.encode(nullifiers, _transactions.length, _actionData)) + // ActionData is (bytes31, bool, uint256, (address, bytes, uint256)[]) + + const actionDataTuple = [ + actionData.random, + actionData.requireSuccess, + actionData.minGasLimit, + actionData.calls.map(call => [call.to, call.data, call.value]) + ]; + + const adaptParamsEncoded = AbiCoder.defaultAbiCoder().encode( + ['bytes32[][]', 'uint256', 'tuple(bytes31, bool, uint256, tuple(address, bytes, uint256)[])'], + [nullifiers, transactions.length, actionDataTuple] + ); + const adaptParams = keccak256(adaptParamsEncoded); + + // 3. Sign Typed Data + const domain = { + name: 'RelayAdapt7702', + version: '1', + chainId, + verifyingContract: signer.address, + }; + + const types = { + Relay: [ + { name: 'adaptParams', type: 'bytes32' }, + ], + }; + + const value = { + adaptParams, + }; + + return signer.signTypedData(domain, types, value); + } + + static async generateRelayShieldRequests( + random: string, + shieldERC20Recipients: RelayAdaptShieldERC20Recipient[], + shieldNFTRecipients: RelayAdaptShieldNFTRecipient[], + ): Promise { + return Promise.all([ + ...(await RelayAdapt7702Helper.createRelayShieldRequestsERC20s(random, shieldERC20Recipients)), + ...(await RelayAdapt7702Helper.createRelayShieldRequestsNFTs(random, shieldNFTRecipients)), + ]); + } + + private static async createRelayShieldRequestsERC20s( + random: string, + shieldERC20Recipients: RelayAdaptShieldERC20Recipient[], + ): Promise { + return Promise.all( + shieldERC20Recipients.map(({ tokenAddress, recipientAddress }) => { + const addressData: AddressData = decodeAddress(recipientAddress); + const shieldERC20 = new ShieldNoteERC20( + addressData.masterPublicKey, + random, + 0n, // 0n will automatically shield entire balance. + tokenAddress, + ); + + // Random private key for Relay Adapt shield. + const shieldPrivateKey = ByteUtils.hexToBytes(ByteUtils.randomHex(32)); + + return shieldERC20.serialize(shieldPrivateKey, addressData.viewingPublicKey); + }), + ); + } + + private static async createRelayShieldRequestsNFTs( + random: string, + shieldNFTRecipients: RelayAdaptShieldNFTRecipient[], + ): Promise { + return Promise.all( + shieldNFTRecipients.map(({ nftTokenData, recipientAddress }) => { + const value = RelayAdapt7702Helper.valueForNFTShield(nftTokenData); + const addressData: AddressData = decodeAddress(recipientAddress); + const shieldNFT = new ShieldNoteNFT( + addressData.masterPublicKey, + random, + value, + nftTokenData, + ); + + // Random private key for Relay Adapt shield. + const shieldPrivateKey = ByteUtils.hexToBytes(ByteUtils.randomHex(32)); + + return shieldNFT.serialize(shieldPrivateKey, addressData.viewingPublicKey); + }), + ); + } + + private static valueForNFTShield(nftTokenData: NFTTokenData): bigint { + switch (nftTokenData.tokenType) { + case TokenType.ERC721: + return ERC721_NOTE_VALUE; + case TokenType.ERC1155: + return 0n; // 0n will automatically shield entire balance. + } + throw new Error('Unhandled NFT token type.'); + } + + /** + * Format action data field for relay call. + */ + static getActionData( + random: string, + requireSuccess: boolean, + calls: ContractTransaction[], + minGasLimit: bigint, + ): RelayAdapt.ActionDataStruct { + const formattedRandom = RelayAdapt7702Helper.formatRandom(random); + return { + random: formattedRandom, + requireSuccess, + minGasLimit, + calls: RelayAdapt7702Helper.formatCalls(calls), + }; + } + + /** + * Get relay adapt params hash. + * Hashes transaction data and params to ensure that transaction is not modified by MITM. + * + * @param transactions - serialized transactions + * @param random - random value + * @param requireSuccess - require success on calls + * @param calls - calls list + * @returns adapt params + */ + static getRelayAdaptParams( + transactions: (TransactionStructV2 | TransactionStructV3)[], + random: string, + requireSuccess: boolean, + calls: ContractTransaction[], + minGasLimit = BigInt(0), + ): string { + const actionData = RelayAdapt7702Helper.getActionData(random, requireSuccess, calls, minGasLimit); + return RelayAdapt7702Helper.getAdaptParams(transactions, actionData); + } + + static getAdaptParams( + transactions: (TransactionStructV2 | TransactionStructV3)[], + actionData: RelayAdapt.ActionDataStruct, + ): string { + const nullifiers = transactions.map((transaction) => transaction.nullifiers); + + const preimage = AbiCoder.defaultAbiCoder().encode( + [ + 'bytes32[][] nullifiers', + 'uint256 transactionsLength', + 'tuple(bytes31 random, bool requireSuccess, uint256 minGasLimit, tuple(address to, bytes data, uint256 value)[] calls) actionData', + ], + [nullifiers, transactions.length, actionData], + ); + + return keccak256(ByteUtils.hexToBytes(preimage)); + } + + /** + * Strips all unnecessary fields from populated transactions + * + * @param {object[]} calls - calls list + * @returns {object[]} formatted calls + */ + static formatCalls(calls: ContractTransaction[]): RelayAdapt.CallStruct[] { + return calls.map((call) => ({ + to: call.to || '', + data: call.data || '', + value: call.value ?? 0n, + })); + } + + static formatRandom(random: string): Uint8Array { + if (random.length !== 62) { + throw new Error('Relay Adapt random parameter must be a hex string of length 62 (31 bytes).'); + } + return ByteUtils.hexToBytes(random); + } +} + +export { RelayAdapt7702Helper }; diff --git a/src/contracts/relay-adapt/relay-adapt-types.ts b/src/contracts/relay-adapt/relay-adapt-types.ts new file mode 100644 index 00000000..0fdb69a6 --- /dev/null +++ b/src/contracts/relay-adapt/relay-adapt-types.ts @@ -0,0 +1,6 @@ +export { RelayAdapt } from '../../abi/typechain/RelayAdapt'; +export { RelayAdapt7702 } from '../../abi/typechain/RelayAdapt7702'; +export { RelayAdapt7702Deployer } from '../../abi/typechain/RelayAdapt7702Deployer'; +export { RelayAdapt7702__factory } from '../../abi/typechain/factories/RelayAdapt7702__factory'; +export { RelayAdapt7702Deployer__factory } from '../../abi/typechain/factories/RelayAdapt7702Deployer__factory'; +export { RelayAdapt__factory } from '../../abi/typechain/factories/RelayAdapt__factory'; diff --git a/src/contracts/relay-adapt/relay-adapt-versioned-smart-contracts.ts b/src/contracts/relay-adapt/relay-adapt-versioned-smart-contracts.ts index 73308f8e..70ae31e5 100644 --- a/src/contracts/relay-adapt/relay-adapt-versioned-smart-contracts.ts +++ b/src/contracts/relay-adapt/relay-adapt-versioned-smart-contracts.ts @@ -5,12 +5,17 @@ import { TXIDVersion } from '../../models/poi-types'; import { ShieldRequestStruct } from '../../abi/typechain/RelayAdapt'; import { TransactionReceiptLog, TransactionStructV2, TransactionStructV3 } from '../../models'; import { RelayAdaptV2Contract } from './V2/relay-adapt-v2'; +import { RelayAdapt7702Contract } from './V2/relay-adapt-7702'; import { RelayAdaptV3Contract } from './V3/relay-adapt-v3'; +import { EIP7702Authorization } from '../../models/relay-adapt-types'; export class RelayAdaptVersionedSmartContracts { - static getRelayAdaptContract(txidVersion: TXIDVersion, chain: Chain) { + static getRelayAdaptContract(txidVersion: TXIDVersion, chain: Chain, isRelayAdapt7702 = false) { switch (txidVersion) { case TXIDVersion.V2_PoseidonMerkle: { + if (isRelayAdapt7702) { + return ContractStore.relayAdapt7702Contracts.getOrThrow(null, chain); + } return ContractStore.relayAdaptV2Contracts.getOrThrow(null, chain); } case TXIDVersion.V3_PoseidonMerkle: { @@ -24,8 +29,25 @@ export class RelayAdaptVersionedSmartContracts { txidVersion: TXIDVersion, chain: Chain, shieldRequest: ShieldRequestStruct, + isRelayAdapt7702 = false, + authorization?: EIP7702Authorization, + signature?: string, + random31Bytes?: string, + ephemeralAddress?: string, ): Promise { - return this.getRelayAdaptContract(txidVersion, chain).populateShieldBaseToken(shieldRequest); + if (isRelayAdapt7702) { + const contract7702 = ContractStore.relayAdapt7702Contracts.getOrThrow(null, chain); + return contract7702.populateShieldBaseToken( + shieldRequest, + authorization, + signature, + random31Bytes, + ephemeralAddress, + ); + } + return this.getRelayAdaptContract(txidVersion, chain, isRelayAdapt7702).populateShieldBaseToken( + shieldRequest, + ); } static populateUnshieldBaseToken( @@ -36,9 +58,26 @@ export class RelayAdaptVersionedSmartContracts { random31Bytes: string, useDummyProof: boolean, sendWithPublicWallet: boolean, + isRelayAdapt7702 = false, + authorization?: EIP7702Authorization, + signature?: string, + ephemeralAddress?: string, ): Promise { switch (txidVersion) { case TXIDVersion.V2_PoseidonMerkle: { + if (isRelayAdapt7702) { + const contract7702 = ContractStore.relayAdapt7702Contracts.getOrThrow(null, chain); + return contract7702.populateUnshieldBaseToken( + transactions as TransactionStructV2[], + unshieldAddress, + random31Bytes, + useDummyProof, + sendWithPublicWallet, + authorization, + signature, + ephemeralAddress, + ); + } const contractV2 = ContractStore.relayAdaptV2Contracts.getOrThrow(null, chain); return contractV2.populateUnshieldBaseToken( transactions as TransactionStructV2[], @@ -70,9 +109,28 @@ export class RelayAdaptVersionedSmartContracts { isGasEstimate: boolean, isBroadcasterTransaction: boolean, minGasLimit?: bigint, + isRelayAdapt7702 = false, + authorization?: EIP7702Authorization, + signature?: string, + ephemeralAddress?: string, ): Promise { switch (txidVersion) { case TXIDVersion.V2_PoseidonMerkle: { + if (isRelayAdapt7702) { + const contract7702 = ContractStore.relayAdapt7702Contracts.getOrThrow(null, chain); + return contract7702.populateCrossContractCalls( + unshieldTransactions as TransactionStructV2[], + crossContractCalls, + relayShieldRequests, + random31Bytes, + isGasEstimate, + isBroadcasterTransaction, + minGasLimit, + authorization, + signature, + ephemeralAddress, + ); + } const contractV2 = ContractStore.relayAdaptV2Contracts.getOrThrow(null, chain); return contractV2.populateCrossContractCalls( unshieldTransactions as TransactionStructV2[], @@ -107,9 +165,21 @@ export class RelayAdaptVersionedSmartContracts { unshieldAddress: string, random31Bytes: string, sendWithPublicWallet: boolean, + isRelayAdapt7702 = false, + ephemeralAddress?: string, ): Promise { switch (txidVersion) { case TXIDVersion.V2_PoseidonMerkle: { + if (isRelayAdapt7702) { + const contract7702 = ContractStore.relayAdapt7702Contracts.getOrThrow(null, chain); + return contract7702.getRelayAdaptParamsUnshieldBaseToken( + dummyUnshieldTransactions as TransactionStructV2[], + unshieldAddress, + random31Bytes, + sendWithPublicWallet, + ephemeralAddress, + ); + } const contractV2 = ContractStore.relayAdaptV2Contracts.getOrThrow(null, chain); return contractV2.getRelayAdaptParamsUnshieldBaseToken( dummyUnshieldTransactions as TransactionStructV2[], @@ -139,9 +209,23 @@ export class RelayAdaptVersionedSmartContracts { random: string, isBroadcasterTransaction: boolean, minGasLimit?: bigint, + isRelayAdapt7702 = false, + ephemeralAddress?: string, ): Promise { switch (txidVersion) { case TXIDVersion.V2_PoseidonMerkle: { + if (isRelayAdapt7702) { + const contract7702 = ContractStore.relayAdapt7702Contracts.getOrThrow(null, chain); + return contract7702.getRelayAdaptParamsCrossContractCalls( + dummyUnshieldTransactions as TransactionStructV2[], + crossContractCalls, + relayShieldRequests, + random, + isBroadcasterTransaction, + minGasLimit, + ephemeralAddress, + ); + } const contractV2 = ContractStore.relayAdaptV2Contracts.getOrThrow(null, chain); return contractV2.getRelayAdaptParamsCrossContractCalls( dummyUnshieldTransactions as TransactionStructV2[], @@ -171,9 +255,13 @@ export class RelayAdaptVersionedSmartContracts { txidVersion: TXIDVersion, provider: Provider, transaction: ContractTransaction | TransactionRequest, + isRelayAdapt7702 = false, ) { switch (txidVersion) { case TXIDVersion.V2_PoseidonMerkle: { + if (isRelayAdapt7702) { + return RelayAdapt7702Contract.estimateGasWithErrorHandler(provider, transaction); + } return RelayAdaptV2Contract.estimateGasWithErrorHandler(provider, transaction); } case TXIDVersion.V3_PoseidonMerkle: { @@ -183,9 +271,12 @@ export class RelayAdaptVersionedSmartContracts { throw new Error('Unsupported txidVersion'); } - static getRelayAdaptCallError(txidVersion: TXIDVersion, receiptLogs: TransactionReceiptLog[]) { + static getRelayAdaptCallError(txidVersion: TXIDVersion, receiptLogs: TransactionReceiptLog[], isRelayAdapt7702 = false) { switch (txidVersion) { case TXIDVersion.V2_PoseidonMerkle: { + if (isRelayAdapt7702) { + return RelayAdapt7702Contract.getRelayAdaptCallError(receiptLogs); + } return RelayAdaptV2Contract.getRelayAdaptCallError(receiptLogs); } case TXIDVersion.V3_PoseidonMerkle: { @@ -195,9 +286,12 @@ export class RelayAdaptVersionedSmartContracts { throw new Error('Unsupported txidVersion'); } - static parseRelayAdaptReturnValue(txidVersion: TXIDVersion, data: string) { + static parseRelayAdaptReturnValue(txidVersion: TXIDVersion, data: string, isRelayAdapt7702 = false) { switch (txidVersion) { case TXIDVersion.V2_PoseidonMerkle: { + if (isRelayAdapt7702) { + return RelayAdapt7702Contract.parseRelayAdaptReturnValue(data); + } return RelayAdaptV2Contract.parseRelayAdaptReturnValue(data); } case TXIDVersion.V3_PoseidonMerkle: { diff --git a/src/index.ts b/src/index.ts index 06a0261d..5dffa622 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ export { SpendingKeyPair, SpendingPublicKey, ViewingKeyPair, + deriveEphemeralWallet, } from './key-derivation'; export * from './merkletree/merkletree'; export * from './validation'; diff --git a/src/key-derivation/__tests__/ephemeral-key.test.ts b/src/key-derivation/__tests__/ephemeral-key.test.ts new file mode 100644 index 00000000..96113d9e --- /dev/null +++ b/src/key-derivation/__tests__/ephemeral-key.test.ts @@ -0,0 +1,30 @@ +import { expect } from 'chai'; +import { deriveEphemeralWallet } from '../ephemeral-key'; + +describe('Ephemeral Key Derivation', () => { + const mnemonic = 'test test test test test test test test test test test junk'; + + it('should derive a wallet with the correct path', () => { + const index = 0; + const wallet = deriveEphemeralWallet(mnemonic, index); + + expect(wallet).to.not.be.undefined; + expect(wallet.path).to.equal("m/44'/60'/0'/7702/0"); + expect(wallet.address).to.be.a('string'); + }); + + it('should derive different wallets for different indices', () => { + const wallet0 = deriveEphemeralWallet(mnemonic, 0); + const wallet1 = deriveEphemeralWallet(mnemonic, 1); + + expect(wallet0.address).to.not.equal(wallet1.address); + }); + + it('should be deterministic', () => { + const walletA = deriveEphemeralWallet(mnemonic, 5); + const walletB = deriveEphemeralWallet(mnemonic, 5); + + expect(walletA.address).to.equal(walletB.address); + expect(walletA.privateKey).to.equal(walletB.privateKey); + }); +}); diff --git a/src/key-derivation/ephemeral-key.ts b/src/key-derivation/ephemeral-key.ts new file mode 100644 index 00000000..1b83c1c4 --- /dev/null +++ b/src/key-derivation/ephemeral-key.ts @@ -0,0 +1,15 @@ +import { HDNodeWallet } from 'ethers'; + +const EPHEMERAL_DERIVATION_PATH_PREFIX = "m/44'/60'/0'/7702"; + +/** + * Derives an ephemeral wallet for RelayAdapt7702 transactions. + * Uses path: m/44'/60'/0'/7702/index + * @param mnemonic - User's mnemonic + * @param index - Index for the ephemeral key (nonce) + * @returns HDNodeWallet + */ +export const deriveEphemeralWallet = (mnemonic: string, index: number): HDNodeWallet => { + const path = `${EPHEMERAL_DERIVATION_PATH_PREFIX}/${index}`; + return HDNodeWallet.fromPhrase(mnemonic, undefined, path); +}; diff --git a/src/key-derivation/index.ts b/src/key-derivation/index.ts index 1eb0e166..a32b97bb 100644 --- a/src/key-derivation/index.ts +++ b/src/key-derivation/index.ts @@ -3,3 +3,4 @@ export * from './bech32'; export * from './bip32'; export * from './bip39'; export * from './wallet-node'; +export * from './ephemeral-key'; diff --git a/src/models/index.ts b/src/models/index.ts index be606193..705cbb7e 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -14,3 +14,4 @@ export { export * from './wallet-types'; export * from './prover-types'; export * from './typechain-types'; +export * from './relay-adapt-types'; diff --git a/src/models/relay-adapt-types.ts b/src/models/relay-adapt-types.ts new file mode 100644 index 00000000..c4ef2219 --- /dev/null +++ b/src/models/relay-adapt-types.ts @@ -0,0 +1,19 @@ +import { TransactionStructV2 } from './transaction-types'; +import { RelayAdapt } from '../abi/typechain/RelayAdapt'; + +export interface EIP7702Authorization { + chainId: string; + address: string; + nonce: number; + yParity: 0 | 1; + r: string; + s: string; +} + +export interface RelayAdapt7702Request { + transactions: TransactionStructV2[]; + actionData: RelayAdapt.ActionDataStruct; + authorization: EIP7702Authorization; + executionSignature: string; + ephemeralAddress: string; +} diff --git a/src/models/typechain-types.ts b/src/models/typechain-types.ts index 9e1f2871..28b3f911 100644 --- a/src/models/typechain-types.ts +++ b/src/models/typechain-types.ts @@ -3,5 +3,7 @@ import { ShieldRequestStruct, CommitmentCiphertextStructOutput, } from '../abi/typechain/RailgunSmartWallet'; +import { RelayAdapt } from '../abi/typechain/RelayAdapt'; +import { RelayAdapt__factory } from '../abi/typechain/factories/RelayAdapt__factory'; -export { TransactionStruct, ShieldRequestStruct, CommitmentCiphertextStructOutput }; +export { TransactionStruct, ShieldRequestStruct, CommitmentCiphertextStructOutput, RelayAdapt, RelayAdapt__factory }; diff --git a/src/railgun-engine.ts b/src/railgun-engine.ts index 252f7c00..17cf77b8 100644 --- a/src/railgun-engine.ts +++ b/src/railgun-engine.ts @@ -3,6 +3,8 @@ import EventEmitter from 'events'; import { FallbackProvider } from 'ethers'; import { RailgunSmartWalletContract } from './contracts/railgun-smart-wallet/V2/railgun-smart-wallet'; import { RelayAdaptV2Contract } from './contracts/relay-adapt/V2/relay-adapt-v2'; +import { RelayAdapt7702Contract } from './contracts/relay-adapt/V2/relay-adapt-7702'; +import { RelayAdapt7702DeployerContract } from './contracts/relay-adapt/V2/relay-adapt-7702-deployer'; import { Database, DatabaseNamespace } from './database/database'; import { Prover } from './prover/prover'; import { encodeAddress, decodeAddress } from './key-derivation/bech32'; @@ -1477,6 +1479,8 @@ class RailgunEngine extends EventEmitter { deploymentBlocks: Record, poiLaunchBlock: Optional, supportsV3: boolean, + relayAdapt7702ContractAddress?: string, + relayAdapt7702DeployerAddress?: string, ) { EngineDebug.log(`loadNetwork: ${chain.type}:${chain.id}`); @@ -1523,6 +1527,8 @@ class RailgunEngine extends EventEmitter { ); const hasSmartWalletContract = ContractStore.railgunSmartWalletContracts.has(null, chain); const hasRelayAdaptV2Contract = ContractStore.relayAdaptV2Contracts.has(null, chain); + const hasRelayAdapt7702Contract = ContractStore.relayAdapt7702Contracts.has(null, chain); + const hasRelayAdapt7702DeployerContract = ContractStore.relayAdapt7702DeployerContracts.has(null, chain); const hasPoseidonMerkleAccumulatorV3Contract = ContractStore.poseidonMerkleAccumulatorV3Contracts.has(null, chain); const hasPoseidonMerkleVerifierV3Contract = ContractStore.poseidonMerkleVerifierV3Contracts.has( @@ -1534,6 +1540,8 @@ class RailgunEngine extends EventEmitter { hasAnyMerkletree || hasSmartWalletContract || hasRelayAdaptV2Contract || + hasRelayAdapt7702Contract || + hasRelayAdapt7702DeployerContract || hasPoseidonMerkleAccumulatorV3Contract || hasPoseidonMerkleVerifierV3Contract || hasTokenVaultV3Contract @@ -1560,6 +1568,22 @@ class RailgunEngine extends EventEmitter { new RelayAdaptV2Contract(relayAdaptV2ContractAddress, defaultProvider), ); + if (relayAdapt7702ContractAddress) { + ContractStore.relayAdapt7702Contracts.set( + null, + chain, + new RelayAdapt7702Contract(relayAdapt7702ContractAddress, defaultProvider), + ); + } + + if (relayAdapt7702DeployerAddress) { + ContractStore.relayAdapt7702DeployerContracts.set( + null, + chain, + new RelayAdapt7702DeployerContract(relayAdapt7702DeployerAddress, defaultProvider), + ); + } + if (supportsV3) { ContractStore.poseidonMerkleAccumulatorV3Contracts.set( null, @@ -2180,6 +2204,30 @@ class RailgunEngine extends EventEmitter { return shieldCommitments; } + /** + * Get RelayAdapt7702 contract address + * @param chain - chain type/id + * @returns address + */ + getRelayAdapt7702ContractAddress(chain: Chain): Optional { + const contract = ContractStore.relayAdapt7702Contracts.get(null, chain); + return contract?.address; + } + + /** + * Validate RelayAdapt7702 contract address + * @param chain - chain type/id + * @param address - address to validate + * @returns true if valid + */ + async validateRelayAdapt7702Address(chain: Chain, address: string): Promise { + const deployer = ContractStore.relayAdapt7702DeployerContracts.get(null, chain); + if (!deployer) { + return false; + } + return deployer.isDeployed(address); + } + // Top-level exports: static encodeAddress = encodeAddress; @@ -2189,6 +2237,10 @@ class RailgunEngine extends EventEmitter { railgunSmartWalletContracts = ContractStore.railgunSmartWalletContracts; relayAdaptV2Contracts = ContractStore.relayAdaptV2Contracts; + + relayAdapt7702Contracts = ContractStore.relayAdapt7702Contracts; + + relayAdapt7702DeployerContracts = ContractStore.relayAdapt7702DeployerContracts; } export { RailgunEngine }; diff --git a/src/test/config.test.ts b/src/test/config.test.ts index cce5bcbb..311de86c 100644 --- a/src/test/config.test.ts +++ b/src/test/config.test.ts @@ -20,6 +20,8 @@ let config = { voting: '0x0165878A594ca255338adfa4d48449f69242Eb8F', weth9: '0xA7c59f010700930003b33aB25a7a0679C860f29c', relayAdapt: '0xfaAddC93baf78e89DCf37bA67943E1bE8F37Bb8c', + relayAdapt7702: '0xB23994e75a0F1dFaF3A339B2b08BF13e06082A82', + adapt7702Deployer: '0x3155755b79aA083bd953911C92705B7aA82a18F9', // V3 poseidonMerkleAccumulatorV3: '0x2B0d36FACD61B71CC05ab8F3D2355ec3631C0dd5', diff --git a/src/transaction/__tests__/eip7702.test.ts b/src/transaction/__tests__/eip7702.test.ts new file mode 100644 index 00000000..14d1e0e4 --- /dev/null +++ b/src/transaction/__tests__/eip7702.test.ts @@ -0,0 +1,38 @@ +import { expect } from 'chai'; +import { Wallet, recoverAddress, keccak256, encodeRlp, getBytes, toBeHex } from 'ethers'; +import { signEIP7702Authorization } from '../eip7702'; + +function toRlpInteger(value: number): string | Uint8Array { + if (value === 0) { + return new Uint8Array(0); + } + return toBeHex(value); +} + +describe('EIP-7702 Signing', () => { + it('should sign and recover correctly', () => { + const signer = Wallet.createRandom(); + const contractAddress = '0x1234567890123456789012345678901234567890'; + const chainId = 1; + const nonce = 0; + + const auth = signEIP7702Authorization(signer, contractAddress, chainId, nonce); + + // Reconstruct hash + const rlpEncoded = encodeRlp([ + toRlpInteger(chainId), + contractAddress, + toRlpInteger(nonce) + ]); + const payload = new Uint8Array([0x05, ...getBytes(rlpEncoded)]); + const hash = keccak256(payload); + + const recovered = recoverAddress(hash, { + r: auth.r, + s: auth.s, + yParity: auth.yParity, + }); + + expect(recovered).to.equal(signer.address); + }); +}); diff --git a/src/transaction/__tests__/relay-adapt-7702-signature.test.ts b/src/transaction/__tests__/relay-adapt-7702-signature.test.ts new file mode 100644 index 00000000..59f0549b --- /dev/null +++ b/src/transaction/__tests__/relay-adapt-7702-signature.test.ts @@ -0,0 +1,108 @@ +import { expect } from 'chai'; +import { Wallet, verifyMessage, AbiCoder, keccak256, getBytes } from 'ethers'; +import { signExecutionAuthorization } from '../relay-adapt-7702-signature'; +import { TransactionStructV2 } from '../../models/transaction-types'; +import { RelayAdapt } from '../../abi/typechain/RelayAdapt'; +import { TXIDVersion } from '../../models/poi-types'; + +describe('RelayAdapt7702 Execution Signature', () => { + it('should sign and recover correctly', async () => { + const signer = Wallet.createRandom(); + const chainId = 1; + + const mockTransaction: TransactionStructV2 = { + txidVersion: TXIDVersion.V2_PoseidonMerkle, + proof: { + a: { x: 1n, y: 2n }, + b: { x: [3n, 4n], y: [5n, 6n] }, + c: { x: 7n, y: 8n }, + }, + merkleRoot: '0x' + '0'.repeat(64), + nullifiers: ['0x' + '0'.repeat(64)], + commitments: ['0x' + '0'.repeat(64)], + boundParams: { + treeNumber: 0, + minGasPrice: 0n, + unshield: 0, + chainID: 1n, + adaptContract: '0x' + '0'.repeat(40), + adaptParams: '0x' + '0'.repeat(64), + commitmentCiphertext: [], + }, + unshieldPreimage: { + npk: '0x' + '0'.repeat(64), + token: { + tokenType: 0, + tokenAddress: '0x' + '0'.repeat(40), + tokenSubID: 0n, + }, + value: 0n, + }, + }; + + const mockActionData: RelayAdapt.ActionDataStruct = { + random: '0x' + '0'.repeat(62), + requireSuccess: true, + minGasLimit: 100000n, + calls: [], + }; + + const signature = await signExecutionAuthorization(signer, [mockTransaction], mockActionData, chainId); + + const TRANSACTION_STRUCT_ABI = `tuple( + tuple( + tuple(uint256 x, uint256 y) a, + tuple(uint256[2] x, uint256[2] y) b, + tuple(uint256 x, uint256 y) c + ) proof, + bytes32 merkleRoot, + bytes32[] nullifiers, + bytes32[] commitments, + tuple( + uint16 treeNumber, + uint72 minGasPrice, + uint8 unshield, + uint64 chainID, + address adaptContract, + bytes32 adaptParams, + tuple( + bytes32[4] ciphertext, + bytes32 blindedSenderViewingKey, + bytes32 blindedReceiverViewingKey, + bytes annotationData, + bytes memo + )[] commitmentCiphertext + ) boundParams, + tuple( + bytes32 npk, + tuple( + uint8 tokenType, + address tokenAddress, + uint256 tokenSubID + ) token, + uint120 value + ) unshieldPreimage + )`; + + const ACTION_DATA_STRUCT_ABI = `tuple( + bytes31 random, + bool requireSuccess, + uint256 minGasLimit, + tuple( + address to, + bytes data, + uint256 value + )[] calls + )`; + + const abiCoder = AbiCoder.defaultAbiCoder(); + const encoded = abiCoder.encode( + ['uint256', `${TRANSACTION_STRUCT_ABI}[]`, ACTION_DATA_STRUCT_ABI], + [chainId, [mockTransaction], mockActionData] + ); + const hash = keccak256(encoded); + + const recovered = verifyMessage(getBytes(hash), signature); + expect(recovered).to.equal(signer.address); + }); +}); diff --git a/src/transaction/eip7702.ts b/src/transaction/eip7702.ts new file mode 100644 index 00000000..5ae4f69a --- /dev/null +++ b/src/transaction/eip7702.ts @@ -0,0 +1,49 @@ +import { HDNodeWallet, Wallet, keccak256, encodeRlp, getBytes, toBeHex } from 'ethers'; +import { EIP7702Authorization } from '../models/relay-adapt-types'; + +function toRlpInteger(value: number): string | Uint8Array { + if (value === 0) { + return new Uint8Array(0); + } + return toBeHex(value); +} + +/** + * Signs an EIP-7702 Authorization Tuple. + * Payload: 0x05 || rlp([chain_id, address, nonce]) + * @param signer - The ephemeral key signer + * @param contractAddress - The address to delegate to (RelayAdapt7702) + * @param chainId - Chain ID + * @param nonce - Nonce (default 0) + * @returns EIP7702Authorization + */ +export const signEIP7702Authorization = ( + signer: HDNodeWallet | Wallet, + contractAddress: string, + chainId: number, + nonce: number = 0 +): EIP7702Authorization => { + // RLP encode the tuple [chain_id, address, nonce] + // We use toRlpInteger to ensure numbers are formatted correctly for RLP (0 -> empty bytes, others -> hex) + const rlpEncoded = encodeRlp([ + toRlpInteger(chainId), + contractAddress, + toRlpInteger(nonce) + ]); + + // Prepend 0x05 (EIP-7702 transaction type / magic byte) + const payload = new Uint8Array([0x05, ...getBytes(rlpEncoded)]); + const hash = keccak256(payload); + + // Sign the hash directly (not EIP-191) + const sig = signer.signingKey.sign(hash); + + return { + chainId: chainId.toString(), + address: contractAddress, + nonce, + yParity: sig.yParity, + r: sig.r, + s: sig.s, + }; +}; diff --git a/src/transaction/index.ts b/src/transaction/index.ts index ec1e9f9e..a80da1ff 100644 --- a/src/transaction/index.ts +++ b/src/transaction/index.ts @@ -1,3 +1,5 @@ // Note: we purposefully do not export everything, in order to reduce the number of public APIs export * from './transaction-batch'; export * from './railgun-txid'; +export * from './eip7702'; +export * from './relay-adapt-7702-signature'; diff --git a/src/transaction/relay-adapt-7702-signature.ts b/src/transaction/relay-adapt-7702-signature.ts new file mode 100644 index 00000000..9aae8fac --- /dev/null +++ b/src/transaction/relay-adapt-7702-signature.ts @@ -0,0 +1,57 @@ +import { HDNodeWallet, Wallet, AbiCoder, keccak256, getBytes } from 'ethers'; +import { TransactionStructV2 } from '../models/transaction-types'; +import { RelayAdapt7702__factory } from '../abi/typechain/factories/RelayAdapt7702__factory'; +import { RelayAdapt7702 } from '../abi/typechain/RelayAdapt7702'; + +const iface = RelayAdapt7702__factory.createInterface(); +// const executeFunc = iface.getFunction('execute'); +const executeFunc = iface.getFunction( + 'execute((((uint256,uint256),(uint256[2],uint256[2]),(uint256,uint256)),bytes32,bytes32[],bytes32[],(uint16,uint72,uint8,uint64,address,bytes32,(bytes32[4],bytes32,bytes32,bytes,bytes)[]),(bytes32,(uint8,address,uint256),uint120))[],(bytes31,bool,uint256,(address,bytes,uint256)[]),bytes)', +); +if (!executeFunc) { + throw new Error('RelayAdapt7702: execute function not found in ABI'); +} + +// inputs[0] is Transaction[] +const transactionArrayType = executeFunc.inputs[0]; +if (transactionArrayType.type !== 'tuple[]' || !transactionArrayType.arrayChildren) { + throw new Error('RelayAdapt7702: execute input[0] is not Transaction[]'); +} +export const TRANSACTION_STRUCT_ABI = transactionArrayType.arrayChildren.format('full'); + +// inputs[1] is ActionData +const actionDataType = executeFunc.inputs[1]; +if (actionDataType.type !== 'tuple') { + throw new Error('RelayAdapt7702: execute input[1] is not ActionData'); +} +export const ACTION_DATA_STRUCT_ABI = actionDataType.format('full'); + +/** + * Signs the execution payload for RelayAdapt7702. + * Payload: keccak256(abi.encode(chainId, transactions, actionData)) + * Signed as EIP-191 message. + * @param signer - The ephemeral key signer + * @param transactions - Railgun transactions + * @param actionData - Action data + * @param chainId - Chain ID + * @returns Signature string + */ +export const signExecutionAuthorization = async ( + signer: HDNodeWallet | Wallet, + transactions: TransactionStructV2[], + actionData: RelayAdapt7702.ActionDataStruct, + chainId: number +): Promise => { + const abiCoder = AbiCoder.defaultAbiCoder(); + + const encoded = abiCoder.encode( + ['uint256', `${TRANSACTION_STRUCT_ABI}[]`, ACTION_DATA_STRUCT_ABI], + [chainId, transactions, actionData] + ); + + const hash = keccak256(encoded); + + // Sign with EIP-191 prefix (signMessage does this automatically) + // We pass bytes to ensure it's treated as binary hash, not string + return signer.signMessage(getBytes(hash)); +}; diff --git a/src/validation/__tests__/relay-adapt-7702-validator.test.ts b/src/validation/__tests__/relay-adapt-7702-validator.test.ts new file mode 100644 index 00000000..4eae2d20 --- /dev/null +++ b/src/validation/__tests__/relay-adapt-7702-validator.test.ts @@ -0,0 +1,18 @@ +import { expect } from 'chai'; +import { TRANSACTION_STRUCT_ABI, ACTION_DATA_STRUCT_ABI } from '../../transaction/relay-adapt-7702-signature'; +import { ParamType } from 'ethers'; + +describe('RelayAdapt7702Validator', () => { + it('should have valid ABI strings derived from TypeChain', () => { + expect(TRANSACTION_STRUCT_ABI).to.be.a('string'); + + expect(ACTION_DATA_STRUCT_ABI).to.be.a('string'); + + // Verify they are valid ParamTypes + const transactionType = ParamType.from(TRANSACTION_STRUCT_ABI); + expect(transactionType.baseType).to.equal('tuple'); + + const actionDataType = ParamType.from(ACTION_DATA_STRUCT_ABI); + expect(actionDataType.baseType).to.equal('tuple'); + }); +}); diff --git a/src/validation/extract-transaction-data-v2.ts b/src/validation/extract-transaction-data-v2.ts index ab75d802..1a04ce52 100644 --- a/src/validation/extract-transaction-data-v2.ts +++ b/src/validation/extract-transaction-data-v2.ts @@ -1,5 +1,5 @@ import { Contract, ContractTransaction } from 'ethers'; -import { ABIRailgunSmartWallet, ABIRelayAdapt } from '../abi/abi'; +import { ABIRailgunSmartWallet, ABIRelayAdapt, ABIRelayAdapt7702 } from '../abi/abi'; import { Chain } from '../models/engine-types'; import { TransactionStructOutput } from '../abi/typechain/RailgunSmartWallet'; import { AddressData } from '../key-derivation'; @@ -20,6 +20,7 @@ import { TXIDVersion } from '../models/poi-types'; enum TransactionName { RailgunSmartWallet = 'transact', RelayAdapt = 'relay', + RelayAdapt7702 = 'execute' } const getABIForTransaction = (transactionName: TransactionName): Array => { @@ -28,6 +29,8 @@ const getABIForTransaction = (transactionName: TransactionName): Array => { return ABIRailgunSmartWallet; case TransactionName.RelayAdapt: return ABIRelayAdapt; + case TransactionName.RelayAdapt7702: + return ABIRelayAdapt7702; } throw new Error('Unsupported transactionName'); }; @@ -40,8 +43,11 @@ export const extractFirstNoteERC20AmountMapFromTransactionRequestV2 = ( receivingViewingPrivateKey: Uint8Array, receivingRailgunAddressData: AddressData, tokenDataGetter: TokenDataGetter, + useRelayAdapt7702: boolean = false, ): Promise> => { - const transactionName = useRelayAdapt + const transactionName = useRelayAdapt7702 + ? TransactionName.RelayAdapt7702 + : useRelayAdapt ? TransactionName.RelayAdapt : TransactionName.RailgunSmartWallet; @@ -64,8 +70,11 @@ export const extractRailgunTransactionDataFromTransactionRequestV2 = ( receivingViewingPrivateKey: Uint8Array, receivingRailgunAddressData: AddressData, tokenDataGetter: TokenDataGetter, + useRelayAdapt7702: boolean = false, ): Promise => { - const transactionName = useRelayAdapt + const transactionName = useRelayAdapt7702 + ? TransactionName.RelayAdapt7702 + : useRelayAdapt ? TransactionName.RelayAdapt : TransactionName.RailgunSmartWallet; diff --git a/src/validation/extract-transaction-data.ts b/src/validation/extract-transaction-data.ts index 1eeb09a7..c5d44714 100644 --- a/src/validation/extract-transaction-data.ts +++ b/src/validation/extract-transaction-data.ts @@ -21,6 +21,7 @@ export const extractFirstNoteERC20AmountMapFromTransactionRequest = ( receivingViewingPrivateKey: Uint8Array, receivingRailgunAddressData: AddressData, tokenDataGetter: TokenDataGetter, + useRelayAdapt7702: boolean = false, ) => { switch (txidVersion) { case TXIDVersion.V2_PoseidonMerkle: @@ -32,6 +33,7 @@ export const extractFirstNoteERC20AmountMapFromTransactionRequest = ( receivingViewingPrivateKey, receivingRailgunAddressData, tokenDataGetter, + useRelayAdapt7702, ); case TXIDVersion.V3_PoseidonMerkle: return extractFirstNoteERC20AmountMapFromTransactionRequestV3( @@ -55,6 +57,7 @@ export const extractRailgunTransactionDataFromTransactionRequest = ( receivingViewingPrivateKey: Uint8Array, receivingRailgunAddressData: AddressData, tokenDataGetter: TokenDataGetter, + useRelayAdapt7702: boolean = false, ) => { switch (txidVersion) { case TXIDVersion.V2_PoseidonMerkle: @@ -66,6 +69,7 @@ export const extractRailgunTransactionDataFromTransactionRequest = ( receivingViewingPrivateKey, receivingRailgunAddressData, tokenDataGetter, + useRelayAdapt7702, ); case TXIDVersion.V3_PoseidonMerkle: return extractRailgunTransactionDataFromTransactionRequestV3( diff --git a/src/validation/index.ts b/src/validation/index.ts index e5d8e6de..2daac825 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -1,2 +1,3 @@ // Note: we purposefully do not export everything, in order to reduce the number of public APIs export * from './poi-validation'; +export * from './relay-adapt-7702-validator'; diff --git a/src/validation/relay-adapt-7702-validator.ts b/src/validation/relay-adapt-7702-validator.ts new file mode 100644 index 00000000..76c292c7 --- /dev/null +++ b/src/validation/relay-adapt-7702-validator.ts @@ -0,0 +1,59 @@ +import { verifyMessage, getBytes, keccak256, AbiCoder, encodeRlp, toBeHex, recoverAddress } from 'ethers'; +import { TransactionStructV2 } from '../models/transaction-types'; +import { RelayAdapt7702 } from '../abi/typechain/RelayAdapt7702'; +import { EIP7702Authorization } from '../models/relay-adapt-types'; +import { ACTION_DATA_STRUCT_ABI, TRANSACTION_STRUCT_ABI } from '../transaction/relay-adapt-7702-signature'; + +export class RelayAdapt7702Validator { + static validateAuthorization( + authorization: EIP7702Authorization, + expectedContractAddress: string, + expectedChainId: number + ): string { + if (authorization.address.toLowerCase() !== expectedContractAddress.toLowerCase()) { + throw new Error('Authorization contract address mismatch'); + } + if (BigInt(authorization.chainId) !== BigInt(expectedChainId)) { + throw new Error('Authorization chain ID mismatch'); + } + + // Reconstruct payload + const rlpEncoded = encodeRlp([ + authorization.chainId === '0' ? new Uint8Array(0) : toBeHex(BigInt(authorization.chainId)), + authorization.address, + authorization.nonce === 0 ? new Uint8Array(0) : toBeHex(authorization.nonce) + ]); + const payload = new Uint8Array([0x05, ...getBytes(rlpEncoded)]); + const hash = keccak256(payload); + + // Recover address + return recoverAddress(hash, { + r: authorization.r, + s: authorization.s, + yParity: authorization.yParity + }); + } + + static validateExecution( + transactions: TransactionStructV2[], + actionData: RelayAdapt7702.ActionDataStruct, + signature: string, + chainId: number, + expectedSigner: string + ): void { + // Reconstruct hash + const abiCoder = AbiCoder.defaultAbiCoder(); + const encoded = abiCoder.encode( + ['uint256', `${TRANSACTION_STRUCT_ABI}[]`, ACTION_DATA_STRUCT_ABI], + [chainId, transactions, actionData] + ); + const hash = keccak256(encoded); + + // Recover signer + const recoveredAddress = verifyMessage(getBytes(hash), signature); + + if (recoveredAddress.toLowerCase() !== expectedSigner.toLowerCase()) { + throw new Error('Execution signature signer mismatch'); + } + } +} diff --git a/src/wallet/abstract-wallet.ts b/src/wallet/abstract-wallet.ts index 3dd6e6d7..da6e3634 100644 --- a/src/wallet/abstract-wallet.ts +++ b/src/wallet/abstract-wallet.ts @@ -2270,6 +2270,7 @@ abstract class AbstractWallet extends EventEmitter { transactionRequest: ContractTransaction, useRelayAdapt: boolean, contractAddress: string, + useRelayAdapt7702: boolean = false, ) { return extractFirstNoteERC20AmountMapFromTransactionRequest( txidVersion, @@ -2280,6 +2281,7 @@ abstract class AbstractWallet extends EventEmitter { this.getViewingKeyPair().privateKey, this.addressKeys, this.tokenDataGetter, + useRelayAdapt7702 ); } diff --git a/src/wallet/railgun-wallet.ts b/src/wallet/railgun-wallet.ts index 237d9fd6..e65753f6 100644 --- a/src/wallet/railgun-wallet.ts +++ b/src/wallet/railgun-wallet.ts @@ -1,7 +1,7 @@ import { Signature } from '@railgun-community/circomlibjs'; import { poseidon } from '../utils/poseidon'; import { Database } from '../database/database'; -import { deriveNodes, SpendingKeyPair, WalletNode } from '../key-derivation/wallet-node'; +import { deriveNodes, SpendingKeyPair, WalletNode, deriveEphemeralWallet } from '../key-derivation'; import { WalletData } from '../models/wallet-types'; import { ByteUtils } from '../utils/bytes'; import { sha256 } from '../utils/hash'; @@ -10,6 +10,11 @@ import { Mnemonic } from '../key-derivation/bip39'; import { PublicInputsRailgun } from '../models'; import { signEDDSA } from '../utils/keys-utils'; import { Prover } from '../prover/prover'; +import { HDNodeWallet } from 'ethers'; +import { RelayAdapt7702Helper } from '../contracts/relay-adapt/relay-adapt-7702-helper'; +import { EIP7702Authorization } from '../models/relay-adapt-types'; +import { TransactionStructV2, TransactionStructV3 } from '../models/transaction-types'; +import { RelayAdapt } from '../abi/typechain/RelayAdapt'; class RailgunWallet extends AbstractWallet { /** @@ -28,6 +33,99 @@ class RailgunWallet extends AbstractWallet { return signEDDSA(spendingKeyPair.privateKey, msg); } + /** + * Get ephemeral wallet for RelayAdapt7702 + * @param {string} encryptionKey - encryption key to use with database + * @param {number} index - index of derivation path + * @returns {Promise} + */ + async getEphemeralWallet(encryptionKey: string, index: number): Promise { + const { mnemonic } = (await AbstractWallet.read( + this.db, + this.id, + encryptionKey, + )) as WalletData; + return deriveEphemeralWallet(mnemonic, index); + } + + /** + * Get current ephemeral key index + * @returns {Promise} + */ + async getEphemeralKeyIndex(): Promise { + try { + const index = await this.db.get([this.id, 'ephemeral_index'], 'utf8'); + return parseInt(index as string, 10); + } catch (err) { + return 0; + } + } + + /** + * Set ephemeral key index + * @param {number} index - new index + * @returns {Promise} + */ + async setEphemeralKeyIndex(index: number): Promise { + await this.db.put([this.id, 'ephemeral_index'], index.toString(), 'utf8'); + } + + /** + * Get current ephemeral address + * @param {string} encryptionKey - encryption key to use with database + * @returns {Promise} + */ + async getCurrentEphemeralAddress(encryptionKey: string): Promise { + const index = await this.getEphemeralKeyIndex(); + const wallet = await this.getEphemeralWallet(encryptionKey, index); + return wallet.address; + } + + /** + * Sign EIP-7702 Authorization and Execution Payload + * @param {string} encryptionKey - encryption key to use with database + * @param {string} contractAddress - RelayAdapt7702 contract address + * @param {bigint} chainId - Chain ID + * @param {(TransactionStructV2 | TransactionStructV3)[]} transactions - Railgun transactions + * @param {RelayAdapt.ActionDataStruct} actionData - Action Data + * @returns {Promise<{ authorization: EIP7702Authorization; signature: string }>} + */ + async sign7702Request( + encryptionKey: string, + contractAddress: string, + chainId: bigint, + transactions: (TransactionStructV2 | TransactionStructV3)[], + actionData: RelayAdapt.ActionDataStruct, + ): Promise<{ authorization: EIP7702Authorization; signature: string }> { + const index = await this.getEphemeralKeyIndex(); + const ephemeralWallet = await this.getEphemeralWallet(encryptionKey, index); + + const authorization = await RelayAdapt7702Helper.signEIP7702Authorization( + ephemeralWallet, + contractAddress, + chainId, + 0, // Nonce is always 0 for ephemeral keys + ); + + const signature = await RelayAdapt7702Helper.signExecutionAuthorization( + ephemeralWallet, + transactions, + actionData, + chainId, + ); + + return { authorization, signature }; + } + + /** + * Ratchet ephemeral key index + * @returns {Promise} + */ + async ratchetEphemeralAddress(): Promise { + const index = await this.getEphemeralKeyIndex(); + await this.setEphemeralKeyIndex(index + 1); + } + /** * Load encrypted node from database with encryption key * @param {BytesData} encryptionKey From 2336a4f27e022009cfc0cd095cf07b83ebc9ac31 Mon Sep 17 00:00:00 2001 From: zy0n Date: Wed, 7 Jan 2026 15:22:46 -0500 Subject: [PATCH 2/4] fix: update ethers dependency to 6.14.3 and refactor authorization types to use native Authorization --- package.json | 2 +- .../relay-adapt/V2/relay-adapt-7702.ts | 12 ++-- .../relay-adapt/relay-adapt-7702-helper.ts | 34 ++++------- .../relay-adapt-versioned-smart-contracts.ts | 9 ++- src/models/relay-adapt-types.ts | 12 +--- src/transaction/__tests__/eip7702.test.ts | 14 ++--- src/transaction/eip7702.ts | 58 ++++++------------- src/validation/relay-adapt-7702-validator.ts | 17 ++---- src/wallet/railgun-wallet.ts | 7 +-- yarn.lock | 35 ++++++----- 10 files changed, 78 insertions(+), 122 deletions(-) diff --git a/package.json b/package.json index f899d37e..58c4c5e6 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "chai-as-promised": "^7.1.1", "encoding-down": "^7.1.0", "ethereum-cryptography": "^2.0.0", - "ethers": "6.13.1", + "ethers": "6.14.3", "fast-text-encoding": "^1.0.6", "levelup": "^5.1.1", "msgpack-lite": "^0.1.26" diff --git a/src/contracts/relay-adapt/V2/relay-adapt-7702.ts b/src/contracts/relay-adapt/V2/relay-adapt-7702.ts index 3d9e5946..d0e90002 100644 --- a/src/contracts/relay-adapt/V2/relay-adapt-7702.ts +++ b/src/contracts/relay-adapt/V2/relay-adapt-7702.ts @@ -8,6 +8,7 @@ import { Result, Log, toUtf8String, + Authorization, } from 'ethers'; import { ABIRelayAdapt, ABIRelayAdapt7702 } from '../../../abi/abi'; import { TransactionReceiptLog } from '../../../models/formatted-types'; @@ -20,7 +21,6 @@ import { RelayAdapt } from '../../../abi/typechain/RelayAdapt'; import { PayableOverrides } from '../../../abi/typechain/common'; import { TransactionStructV2 } from '../../../models/transaction-types'; import { MINIMUM_RELAY_ADAPT_CROSS_CONTRACT_CALLS_GAS_LIMIT_V2 } from '../constants'; -import { EIP7702Authorization } from '../../../models/relay-adapt-types'; import { ByteUtils } from '../../../utils/bytes'; enum RelayAdaptEvent { @@ -51,7 +51,7 @@ export class RelayAdapt7702Contract { async populateShieldBaseToken( shieldRequest: ShieldRequestStruct, - authorization?: EIP7702Authorization, + authorization?: Authorization, signature?: string, random31Bytes?: string, ephemeralAddress?: string, @@ -164,7 +164,7 @@ export class RelayAdapt7702Contract { random31Bytes: string, _useDummyProof: boolean, sendWithPublicWallet: boolean, - authorization?: EIP7702Authorization, + authorization?: Authorization, signature?: string, ephemeralAddress?: string, ): Promise { @@ -270,7 +270,7 @@ export class RelayAdapt7702Contract { isGasEstimate: boolean, isBroadcasterTransaction: boolean, minGasLimit?: bigint, - authorization?: EIP7702Authorization, + authorization?: Authorization, signature?: string, ephemeralAddress?: string, ): Promise { @@ -367,7 +367,7 @@ export class RelayAdapt7702Contract { private async populateRelayMulticall( calls: ContractTransaction[], overrides: PayableOverrides, - authorization?: EIP7702Authorization, + authorization?: Authorization, signature?: string, random31Bytes?: string, ephemeralAddress?: string, @@ -403,7 +403,7 @@ export class RelayAdapt7702Contract { calls: ContractTransaction[], overrides: PayableOverrides, minimumGasLimit = BigInt(0), - authorization?: EIP7702Authorization, + authorization?: Authorization, signature?: string, ephemeralAddress?: string, ): Promise { diff --git a/src/contracts/relay-adapt/relay-adapt-7702-helper.ts b/src/contracts/relay-adapt/relay-adapt-7702-helper.ts index d94e77fb..641a8f03 100644 --- a/src/contracts/relay-adapt/relay-adapt-7702-helper.ts +++ b/src/contracts/relay-adapt/relay-adapt-7702-helper.ts @@ -1,4 +1,4 @@ -import { ContractTransaction, AbiCoder, keccak256, Wallet, encodeRlp, toBeHex, concat, Interface, HDNodeWallet } from 'ethers'; +import { ContractTransaction, AbiCoder, keccak256, Wallet, Interface, HDNodeWallet, Authorization } from 'ethers'; import { ByteUtils } from '../../utils/bytes'; import { ShieldNoteERC20 } from '../../note/erc20/shield-note-erc20'; import { AddressData, decodeAddress } from '../../key-derivation'; @@ -12,35 +12,25 @@ import { ShieldNoteNFT } from '../../note/nft/shield-note-nft'; import { ERC721_NOTE_VALUE } from '../../note/note-util'; import { RelayAdapt, ShieldRequestStruct } from '../../abi/typechain/RelayAdapt'; import { TransactionStructV2, TransactionStructV3 } from '../../models/transaction-types'; -import { EIP7702Authorization } from '../../models/relay-adapt-types'; import { ABIRelayAdapt7702 } from '../../abi/abi'; +import { signEIP7702Authorization as signEIP7702AuthorizationCore } from '../../transaction/eip7702'; class RelayAdapt7702Helper { + /** + * Signs an EIP-7702 Authorization using ethers native methods. + * @param signer - The ephemeral key signer + * @param contractAddress - The address to delegate to (RelayAdapt7702) + * @param chainId - Chain ID + * @param nonce - Nonce (typically 0 for ephemeral keys) + * @returns Authorization tuple + */ static async signEIP7702Authorization( signer: Wallet | HDNodeWallet, contractAddress: string, chainId: bigint, nonce: number, - ): Promise { - // 0x05 || rlp([chain_id, address, nonce]) - const rlpEncoded = encodeRlp([ - toBeHex(chainId), - contractAddress, - toBeHex(nonce), - ]); - const payload = concat(['0x05', rlpEncoded]); - const digest = keccak256(payload); - - const signature = signer.signingKey.sign(digest); - - return { - chainId: chainId.toString(), - address: contractAddress, - nonce, - yParity: signature.yParity, - r: signature.r, - s: signature.s, - }; + ): Promise { + return signEIP7702AuthorizationCore(signer, contractAddress, chainId, nonce); } static async signExecutionAuthorization( diff --git a/src/contracts/relay-adapt/relay-adapt-versioned-smart-contracts.ts b/src/contracts/relay-adapt/relay-adapt-versioned-smart-contracts.ts index 70ae31e5..3cb26501 100644 --- a/src/contracts/relay-adapt/relay-adapt-versioned-smart-contracts.ts +++ b/src/contracts/relay-adapt/relay-adapt-versioned-smart-contracts.ts @@ -1,4 +1,4 @@ -import { ContractTransaction, Provider, TransactionRequest } from 'ethers'; +import { ContractTransaction, Provider, TransactionRequest, Authorization } from 'ethers'; import { ContractStore } from '../contract-store'; import { Chain } from '../../models/engine-types'; import { TXIDVersion } from '../../models/poi-types'; @@ -7,7 +7,6 @@ import { TransactionReceiptLog, TransactionStructV2, TransactionStructV3 } from import { RelayAdaptV2Contract } from './V2/relay-adapt-v2'; import { RelayAdapt7702Contract } from './V2/relay-adapt-7702'; import { RelayAdaptV3Contract } from './V3/relay-adapt-v3'; -import { EIP7702Authorization } from '../../models/relay-adapt-types'; export class RelayAdaptVersionedSmartContracts { static getRelayAdaptContract(txidVersion: TXIDVersion, chain: Chain, isRelayAdapt7702 = false) { @@ -30,7 +29,7 @@ export class RelayAdaptVersionedSmartContracts { chain: Chain, shieldRequest: ShieldRequestStruct, isRelayAdapt7702 = false, - authorization?: EIP7702Authorization, + authorization?: Authorization, signature?: string, random31Bytes?: string, ephemeralAddress?: string, @@ -59,7 +58,7 @@ export class RelayAdaptVersionedSmartContracts { useDummyProof: boolean, sendWithPublicWallet: boolean, isRelayAdapt7702 = false, - authorization?: EIP7702Authorization, + authorization?: Authorization, signature?: string, ephemeralAddress?: string, ): Promise { @@ -110,7 +109,7 @@ export class RelayAdaptVersionedSmartContracts { isBroadcasterTransaction: boolean, minGasLimit?: bigint, isRelayAdapt7702 = false, - authorization?: EIP7702Authorization, + authorization?: Authorization, signature?: string, ephemeralAddress?: string, ): Promise { diff --git a/src/models/relay-adapt-types.ts b/src/models/relay-adapt-types.ts index c4ef2219..dc212657 100644 --- a/src/models/relay-adapt-types.ts +++ b/src/models/relay-adapt-types.ts @@ -1,19 +1,11 @@ +import { Authorization } from 'ethers'; import { TransactionStructV2 } from './transaction-types'; import { RelayAdapt } from '../abi/typechain/RelayAdapt'; -export interface EIP7702Authorization { - chainId: string; - address: string; - nonce: number; - yParity: 0 | 1; - r: string; - s: string; -} - export interface RelayAdapt7702Request { transactions: TransactionStructV2[]; actionData: RelayAdapt.ActionDataStruct; - authorization: EIP7702Authorization; + authorization: Authorization; executionSignature: string; ephemeralAddress: string; } diff --git a/src/transaction/__tests__/eip7702.test.ts b/src/transaction/__tests__/eip7702.test.ts index 14d1e0e4..e8c00c01 100644 --- a/src/transaction/__tests__/eip7702.test.ts +++ b/src/transaction/__tests__/eip7702.test.ts @@ -10,28 +10,24 @@ function toRlpInteger(value: number): string | Uint8Array { } describe('EIP-7702 Signing', () => { - it('should sign and recover correctly', () => { + it('should sign and recover correctly', async () => { const signer = Wallet.createRandom(); const contractAddress = '0x1234567890123456789012345678901234567890'; - const chainId = 1; + const chainId = 1n; const nonce = 0; - const auth = signEIP7702Authorization(signer, contractAddress, chainId, nonce); + const auth = await signEIP7702Authorization(signer, contractAddress, chainId, nonce); // Reconstruct hash const rlpEncoded = encodeRlp([ - toRlpInteger(chainId), + toRlpInteger(Number(chainId)), contractAddress, toRlpInteger(nonce) ]); const payload = new Uint8Array([0x05, ...getBytes(rlpEncoded)]); const hash = keccak256(payload); - const recovered = recoverAddress(hash, { - r: auth.r, - s: auth.s, - yParity: auth.yParity, - }); + const recovered = recoverAddress(hash, auth.signature); expect(recovered).to.equal(signer.address); }); diff --git a/src/transaction/eip7702.ts b/src/transaction/eip7702.ts index 5ae4f69a..d1db3336 100644 --- a/src/transaction/eip7702.ts +++ b/src/transaction/eip7702.ts @@ -1,49 +1,27 @@ -import { HDNodeWallet, Wallet, keccak256, encodeRlp, getBytes, toBeHex } from 'ethers'; -import { EIP7702Authorization } from '../models/relay-adapt-types'; - -function toRlpInteger(value: number): string | Uint8Array { - if (value === 0) { - return new Uint8Array(0); - } - return toBeHex(value); -} +import { HDNodeWallet, Wallet, Authorization } from 'ethers'; /** - * Signs an EIP-7702 Authorization Tuple. - * Payload: 0x05 || rlp([chain_id, address, nonce]) + * Signs an EIP-7702 Authorization Tuple using ethers native methods. * @param signer - The ephemeral key signer * @param contractAddress - The address to delegate to (RelayAdapt7702) - * @param chainId - Chain ID - * @param nonce - Nonce (default 0) - * @returns EIP7702Authorization + * @param chainId - Chain ID (optional - will be auto-populated if not provided) + * @param nonce - Nonce (optional - will be auto-populated if not provided) + * @returns Authorization */ -export const signEIP7702Authorization = ( +export const signEIP7702Authorization = async ( signer: HDNodeWallet | Wallet, contractAddress: string, - chainId: number, - nonce: number = 0 -): EIP7702Authorization => { - // RLP encode the tuple [chain_id, address, nonce] - // We use toRlpInteger to ensure numbers are formatted correctly for RLP (0 -> empty bytes, others -> hex) - const rlpEncoded = encodeRlp([ - toRlpInteger(chainId), - contractAddress, - toRlpInteger(nonce) - ]); - - // Prepend 0x05 (EIP-7702 transaction type / magic byte) - const payload = new Uint8Array([0x05, ...getBytes(rlpEncoded)]); - const hash = keccak256(payload); - - // Sign the hash directly (not EIP-191) - const sig = signer.signingKey.sign(hash); - - return { - chainId: chainId.toString(), + chainId?: bigint, + nonce?: number, +): Promise => { + // Use ethers 6.14.3+ native 7702 authorization signing + const authRequest = await signer.populateAuthorization({ address: contractAddress, - nonce, - yParity: sig.yParity, - r: sig.r, - s: sig.s, - }; + ...(chainId !== undefined && { chainId }), + ...(nonce !== undefined && { nonce: BigInt(nonce) }), + }); + + return signer.authorize(authRequest); }; + + diff --git a/src/validation/relay-adapt-7702-validator.ts b/src/validation/relay-adapt-7702-validator.ts index 76c292c7..c95a2721 100644 --- a/src/validation/relay-adapt-7702-validator.ts +++ b/src/validation/relay-adapt-7702-validator.ts @@ -1,37 +1,32 @@ -import { verifyMessage, getBytes, keccak256, AbiCoder, encodeRlp, toBeHex, recoverAddress } from 'ethers'; +import { verifyMessage, getBytes, keccak256, AbiCoder, encodeRlp, toBeHex, recoverAddress, Authorization } from 'ethers'; import { TransactionStructV2 } from '../models/transaction-types'; import { RelayAdapt7702 } from '../abi/typechain/RelayAdapt7702'; -import { EIP7702Authorization } from '../models/relay-adapt-types'; import { ACTION_DATA_STRUCT_ABI, TRANSACTION_STRUCT_ABI } from '../transaction/relay-adapt-7702-signature'; export class RelayAdapt7702Validator { static validateAuthorization( - authorization: EIP7702Authorization, + authorization: Authorization, expectedContractAddress: string, expectedChainId: number ): string { if (authorization.address.toLowerCase() !== expectedContractAddress.toLowerCase()) { throw new Error('Authorization contract address mismatch'); } - if (BigInt(authorization.chainId) !== BigInt(expectedChainId)) { + if (authorization.chainId !== BigInt(expectedChainId)) { throw new Error('Authorization chain ID mismatch'); } // Reconstruct payload const rlpEncoded = encodeRlp([ - authorization.chainId === '0' ? new Uint8Array(0) : toBeHex(BigInt(authorization.chainId)), + authorization.chainId === 0n ? new Uint8Array(0) : toBeHex(authorization.chainId), authorization.address, - authorization.nonce === 0 ? new Uint8Array(0) : toBeHex(authorization.nonce) + authorization.nonce === 0n ? new Uint8Array(0) : toBeHex(authorization.nonce) ]); const payload = new Uint8Array([0x05, ...getBytes(rlpEncoded)]); const hash = keccak256(payload); // Recover address - return recoverAddress(hash, { - r: authorization.r, - s: authorization.s, - yParity: authorization.yParity - }); + return recoverAddress(hash, authorization.signature); } static validateExecution( diff --git a/src/wallet/railgun-wallet.ts b/src/wallet/railgun-wallet.ts index e65753f6..c1bded32 100644 --- a/src/wallet/railgun-wallet.ts +++ b/src/wallet/railgun-wallet.ts @@ -10,9 +10,8 @@ import { Mnemonic } from '../key-derivation/bip39'; import { PublicInputsRailgun } from '../models'; import { signEDDSA } from '../utils/keys-utils'; import { Prover } from '../prover/prover'; -import { HDNodeWallet } from 'ethers'; +import { HDNodeWallet, Authorization } from 'ethers'; import { RelayAdapt7702Helper } from '../contracts/relay-adapt/relay-adapt-7702-helper'; -import { EIP7702Authorization } from '../models/relay-adapt-types'; import { TransactionStructV2, TransactionStructV3 } from '../models/transaction-types'; import { RelayAdapt } from '../abi/typechain/RelayAdapt'; @@ -88,7 +87,7 @@ class RailgunWallet extends AbstractWallet { * @param {bigint} chainId - Chain ID * @param {(TransactionStructV2 | TransactionStructV3)[]} transactions - Railgun transactions * @param {RelayAdapt.ActionDataStruct} actionData - Action Data - * @returns {Promise<{ authorization: EIP7702Authorization; signature: string }>} + * @returns {Promise<{ authorization: Authorization; signature: string }>} */ async sign7702Request( encryptionKey: string, @@ -96,7 +95,7 @@ class RailgunWallet extends AbstractWallet { chainId: bigint, transactions: (TransactionStructV2 | TransactionStructV3)[], actionData: RelayAdapt.ActionDataStruct, - ): Promise<{ authorization: EIP7702Authorization; signature: string }> { + ): Promise<{ authorization: Authorization; signature: string }> { const index = await this.getEphemeralKeyIndex(); const ephemeralWallet = await this.getEphemeralWallet(encryptionKey, index); diff --git a/yarn.lock b/yarn.lock index 88793655..57e1673b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -724,10 +724,12 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.1.tgz#33e6759935f7a82821b72fb936e66f6b99a36173" integrity sha512-vuYaNuEIbOYLTLUAJh50ezEbvxrD43iby+lpUA2aa148Nh5kX/AVO/9m1Ahmbux2iU5uxJTNF9g2Y+31uml7RQ== -"@types/node@18.15.13": - version "18.15.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" - integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== +"@types/node@22.7.5": + version "22.7.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b" + integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ== + dependencies: + undici-types "~6.19.2" "@types/node@^12.12.6": version "12.20.55" @@ -2396,17 +2398,17 @@ ethereumjs-util@^7.0.10, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.5: ethereum-cryptography "^0.1.3" rlp "^2.2.4" -ethers@6.13.1: - version "6.13.1" - resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.13.1.tgz#2b9f9c7455cde9d38b30fe6589972eb083652961" - integrity sha512-hdJ2HOxg/xx97Lm9HdCWk949BfYqYWpyw4//78SiwOLgASyfrNszfMUNB2joKjvGUdwhHfaiMMFFwacVVoLR9A== +ethers@6.14.3: + version "6.14.3" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.14.3.tgz#7c4443c165ee59b2964e691600fd4586004b2000" + integrity sha512-qq7ft/oCJohoTcsNPFaXSQUm457MA5iWqkf1Mb11ujONdg7jBI6sAOrHaTi3j0CBqIGFSCeR/RMc+qwRRub7IA== dependencies: "@adraffy/ens-normalize" "1.10.1" "@noble/curves" "1.2.0" "@noble/hashes" "1.3.2" - "@types/node" "18.15.13" + "@types/node" "22.7.5" aes-js "4.0.0-beta.5" - tslib "2.4.0" + tslib "2.7.0" ws "8.17.1" ethjs-unit@0.1.6: @@ -4995,10 +4997,10 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" - integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== tslib@^1.8.1: version "1.14.1" @@ -5121,6 +5123,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" From da54248d00dec91b3fa5fc8662f18721744ae85a Mon Sep 17 00:00:00 2001 From: zy0n Date: Wed, 7 Jan 2026 15:42:36 -0500 Subject: [PATCH 3/4] refactor: remove commented-out logging code in relay-adapt-7702 tests --- .../relay-adapt/__tests__/relay-adapt-7702.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/contracts/relay-adapt/__tests__/relay-adapt-7702.test.ts b/src/contracts/relay-adapt/__tests__/relay-adapt-7702.test.ts index 7cdb6419..1d0a8320 100644 --- a/src/contracts/relay-adapt/__tests__/relay-adapt-7702.test.ts +++ b/src/contracts/relay-adapt/__tests__/relay-adapt-7702.test.ts @@ -279,17 +279,7 @@ describe('relay-adapt-7702', function test() { // Send shield on chain const txResponse = await sendTransactionWithLatestNonce(ethersWallet, shieldTx); - // console.log(`Transaction hash: ${txResponse.hash}`); const receipt = await txResponse.wait(); - // console.log(`Transaction status: ${receipt?.status}`); - - // if (receipt) { - // console.log('Logs length:', receipt.logs.length); - // receipt.logs.forEach((log, index) => { - // console.log(`Log ${index} address: ${log.address}`); - // console.log(`Log ${index} topics: ${log.topics}`); - // }); - // } if (receipt?.status === 0) { console.error('Transaction failed'); From b915e64447b80235609d8017153d12f2a7e4a1af Mon Sep 17 00:00:00 2001 From: zy0n Date: Wed, 7 Jan 2026 17:39:41 -0500 Subject: [PATCH 4/4] feat: add nonce parameter to sign EIP-7702 Authorization and Execution Payload --- src/wallet/railgun-wallet.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wallet/railgun-wallet.ts b/src/wallet/railgun-wallet.ts index c1bded32..f909b35b 100644 --- a/src/wallet/railgun-wallet.ts +++ b/src/wallet/railgun-wallet.ts @@ -95,6 +95,7 @@ class RailgunWallet extends AbstractWallet { chainId: bigint, transactions: (TransactionStructV2 | TransactionStructV3)[], actionData: RelayAdapt.ActionDataStruct, + nonce: number = 0, ): Promise<{ authorization: Authorization; signature: string }> { const index = await this.getEphemeralKeyIndex(); const ephemeralWallet = await this.getEphemeralWallet(encryptionKey, index); @@ -103,7 +104,7 @@ class RailgunWallet extends AbstractWallet { ephemeralWallet, contractAddress, chainId, - 0, // Nonce is always 0 for ephemeral keys + nonce, // Nonce is always 0 for ephemeral keys, but can be reused if needed/desired. ); const signature = await RelayAdapt7702Helper.signExecutionAuthorization(