diff --git a/.gitmodules b/.gitmodules index c65a5965..f5489919 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "contracts/lib/forge-std"] - path = contracts/lib/forge-std - url = https://github.com/foundry-rs/forge-std +[submodule "examples/suave/contracts/lib/suave-geth"] + path = examples/suave/contracts/lib/suave-geth + url = https://github.com/flashbots/suave-geth diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 120000 index f7374931..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1 +0,0 @@ -[src/CHANGELOG.md](https://github.com/wagmi-dev/viem/blob/main/src/CHANGELOG.md) diff --git a/biome.json b/biome.json index 3b749a7c..bb5ecfb8 100644 --- a/biome.json +++ b/biome.json @@ -13,7 +13,8 @@ "tsconfig.json", "tsconfig.*.json", "generated.ts", - "vectors/*.json" + "vectors/*.json", + "examples" ] }, "formatter": { diff --git a/bun.lockb b/bun.lockb index 235c1a92..02c39a22 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/examples/suave/.env.example b/examples/suave/.env.example new file mode 100644 index 00000000..44d8d84d --- /dev/null +++ b/examples/suave/.env.example @@ -0,0 +1,4 @@ +PRIVATE_KEY=0x91ab9a7e53c220e6210460b65a7a3bb2ca181412a8a7b43ff336b3df1737ce12 +KETTLE_ADDRESS=0xb5feafbdd752ad52afb7e1bd2e40432a485bbb7f +SUAVE_RPC_URL_HTTP=http://localhost:8545 +GOERLI_RPC_URL_HTTP= diff --git a/examples/suave/README.md b/examples/suave/README.md new file mode 100644 index 00000000..0a35ee68 --- /dev/null +++ b/examples/suave/README.md @@ -0,0 +1,42 @@ +# suave example + +## build contracts + +Forge will install the required solidity dependencies into `examples/suave/contracts/lib/`. + +```sh +# from examples/suave/contracts/ + +forge install +forge build +``` + +## deploy contracts + +We use a forge script to deploy our contracts. Normally we'd use `forge create` for this but because we rely on (deeply-nested) suave-geth contracts, this is a bit cleaner. + +```sh +# from examples/suave/contracts/ + +# do a dry run to see that your dependencies are set up correctly: +forge script DeployContracts + +# populate environment vars using this project's .env file +source ../.env + +# send real deployment transactions with the --broadcast flag +forge script --broadcast --rpc-url $RPC_URL_HTTP --private-key $PRIVATE_KEY DeployContracts +``` + +Then populate your .env file with the new bid contract address. + +```sh +# from examples/suave/contracts/ +echo "BID_CONTRACT_ADDRESS=$(cat broadcast/Deploy.s.sol/16813125/run-latest.json | jq -r '.receipts[0].contractAddress')" >> ../.env +``` + +## run example + +```bash +bun run index.ts +``` diff --git a/examples/suave/bids/index.ts b/examples/suave/bids/index.ts new file mode 100644 index 00000000..9500df0e --- /dev/null +++ b/examples/suave/bids/index.ts @@ -0,0 +1,76 @@ +import { + Address, + Hex, + encodeAbiParameters, + encodeFunctionData, + toHex, +} from 'viem' +import precompiles from 'viem/chains/suave/precompiles' +import { SuaveTxTypes, TransactionRequestSuave } from 'viem/chains/suave/types' +import MevShareBidContract from '../contracts/out/bids.sol/MevShareBidContract.json' + +export interface MevShareBid { + allowedPeekers: Address[] + allowedStores: Address[] + blockNumber: bigint + signedTx: Hex + mevShareContract: Address + kettle: Address + chainId: number +} + +/** Helper class to create MEV-Share bids on SUAVE. */ +export class MevShareBid { + constructor( + blockNumber: bigint, + signedTx: Hex, + kettle: Address, + mevShareContract: Address, + chainId: number, + ) { + this.blockNumber = blockNumber + this.signedTx = signedTx + this.kettle = kettle + this.mevShareContract = mevShareContract + this.chainId = chainId + this.allowedPeekers = [ + // no idea what I'm doing here + precompiles.ANYALLOWED, + ] + this.allowedStores = [] + } + + /** Encodes calldata to call the `newBid` function. */ + private newBidCalldata() { + return encodeFunctionData({ + abi: MevShareBidContract.abi, + functionName: 'newBid', + args: [this.blockNumber, this.allowedPeekers, this.allowedStores], + }) + } + + /** Wraps `signedTx` in a bundle, then ABI-encodes it as bytes for `confidentialInputs`. */ + private confidentialInputsBytes(): Hex { + const bundleBytes = toHex( + JSON.stringify({ + txs: [this.signedTx], + revertingHashes: [], + }), + ) + return encodeAbiParameters([{ type: 'bytes' }], [bundleBytes]) + } + + /** Encodes this bid as a ConfidentialComputeRequest, which can be sent to SUAVE. */ + toConfidentialRequest(): TransactionRequestSuave { + return { + to: this.mevShareContract, + data: this.newBidCalldata(), + type: SuaveTxTypes.ConfidentialRequest, + gas: 500000n, + gasPrice: 1000000000n, + chainId: this.chainId, + kettleAddress: this.kettle, + confidentialInputs: this.confidentialInputsBytes(), + } + } +} diff --git a/examples/suave/contracts/.github/workflows/test.yml b/examples/suave/contracts/.github/workflows/test.yml new file mode 100644 index 00000000..09880b1d --- /dev/null +++ b/examples/suave/contracts/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: test + +on: workflow_dispatch + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Forge build + run: | + forge --version + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/examples/suave/contracts/.gitignore b/examples/suave/contracts/.gitignore new file mode 100644 index 00000000..6501bdd5 --- /dev/null +++ b/examples/suave/contracts/.gitignore @@ -0,0 +1,15 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ +**/broadcast + +# Docs +docs/ + +# Dotenv file +.env diff --git a/examples/suave/contracts/README.md b/examples/suave/contracts/README.md new file mode 100644 index 00000000..9265b455 --- /dev/null +++ b/examples/suave/contracts/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/examples/suave/contracts/foundry.toml b/examples/suave/contracts/foundry.toml new file mode 100644 index 00000000..9c7e15b5 --- /dev/null +++ b/examples/suave/contracts/foundry.toml @@ -0,0 +1,9 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options +remappings = [ + "suave/=lib/suave-geth/suave/sol/" +] diff --git a/examples/suave/contracts/lib/suave-geth b/examples/suave/contracts/lib/suave-geth new file mode 160000 index 00000000..57cbeb46 --- /dev/null +++ b/examples/suave/contracts/lib/suave-geth @@ -0,0 +1 @@ +Subproject commit 57cbeb46daa4e783d1becb853f8fd8d5d4953ec1 diff --git a/examples/suave/contracts/script/Counter.s.sol b/examples/suave/contracts/script/Counter.s.sol new file mode 100644 index 00000000..1a47b40b --- /dev/null +++ b/examples/suave/contracts/script/Counter.s.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console2} from "forge-std/Script.sol"; + +contract CounterScript is Script { + function setUp() public {} + + function run() public { + vm.broadcast(); + } +} diff --git a/examples/suave/contracts/script/Deploy.s.sol b/examples/suave/contracts/script/Deploy.s.sol new file mode 100644 index 00000000..71708d0e --- /dev/null +++ b/examples/suave/contracts/script/Deploy.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console2} from "forge-std/Script.sol"; +import {MevShareBidContract} from "suave/standard_peekers/bids.sol"; + +contract DeployContracts is Script { + function setUp() public {} + + function run() public { + vm.broadcast(); + MevShareBidContract bidContract = new MevShareBidContract(); + console2.log("bid contract deployed", address(bidContract)); + } +} diff --git a/examples/suave/contracts/src/ConfidentialWithLogs.sol b/examples/suave/contracts/src/ConfidentialWithLogs.sol new file mode 100644 index 00000000..9ed6b5a8 --- /dev/null +++ b/examples/suave/contracts/src/ConfidentialWithLogs.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "suave/libraries/Suave.sol"; + +contract ConfidentialWithLogs { + event SimResultEvent( + uint64 egp + ); + + event Test( + uint64 num + ); + + constructor() { + emit Test(1); + } + + fallback() external { + emit Test(2); + } + + function fetchBidConfidentialBundleData() public returns (bytes memory x) { + emit Test(101); + // require(Suave.isConfidential(), "not confidential"); + + // bytes memory confidentialInputs = Suave.confidentialInputs(); + // return abi.decode(confidentialInputs, (bytes)); + x = hex"deadbeef"; + } + + // note: this enables the result of the confidential compute request (CCR) + // to be emitted on chain + function emitSimResultEvent(uint64 egp) public { + emit SimResultEvent(egp); + } + + // note: because of confidential execution, + // you will not see your input as input to the function + function helloWorld() external returns (bytes memory) { + // 0. ensure confidential execution + // require(Suave.isConfidential(), "not confidential"); + + // 1. fetch bundle data + bytes memory bundleData = this.fetchBidConfidentialBundleData(); + + // 2. sim bundle and get effective gas price + uint64 effectiveGasPrice = Suave.simulateBundle(bundleData); + + // note: this enables the computation result to be emitted on chain + return bytes.concat(this.emitSimResultEvent.selector, abi.encode(effectiveGasPrice)); + } +} diff --git a/examples/suave/index.ts b/examples/suave/index.ts new file mode 100644 index 00000000..f19dd028 --- /dev/null +++ b/examples/suave/index.ts @@ -0,0 +1,140 @@ +import { sleep } from 'bun' +import { http, Address, Hex, createPublicClient, formatEther } from 'viem' +import { goerli, suaveRigil } from 'viem/chains' +import { TransactionRequestSuave } from 'viem/chains/suave/types' +import { MevShareBid } from 'bids' + +const failEnv = (name: string) => { + throw new Error(`missing env var ${name}`) +} +if (!process.env.PRIVATE_KEY) { + failEnv('PRIVATE_KEY') +} +if (!process.env.KETTLE_ADDRESS) { + failEnv('KETTLE_ADDRESS') +} +if (!process.env.SUAVE_RPC_URL_HTTP) { + console.warn('SUAVE_RPC_URL_HTTP not set. Defaulting to localhost:8545') +} +if (!process.env.GOERLI_RPC_URL_HTTP) { + console.warn('GOERLI_RPC_URL_HTTP not set. Defaulting to localhost:8545') +} +const KETTLE_ADDRESS: Address = process.env.KETTLE_ADDRESS as Address +const PRIVATE_KEY: Hex = process.env.PRIVATE_KEY as Hex +const SUAVE_RPC_URL_HTTP: string = + process.env.SUAVE_RPC_URL_HTTP || 'http://localhost:8545' +const GOERLI_RPC_URL_HTTP: string = + process.env.GOERLI_RPC_URL_HTTP || 'http://localhost:8545' + +const suaveProvider = suaveRigil.newPublicClient(http(SUAVE_RPC_URL_HTTP)) +const goerliProvider = createPublicClient({ + chain: goerli, + transport: http(GOERLI_RPC_URL_HTTP), +}) +const adminWallet = suaveRigil.newWallet(http(SUAVE_RPC_URL_HTTP), PRIVATE_KEY) +const wallet = suaveRigil.newWallet( + http(SUAVE_RPC_URL_HTTP), + '0x01000070530220062104600650003002001814120800043ff33603df10300012', +) +console.log('admin', adminWallet.account.address) +console.log('wallet', wallet.account.address) + +const retryExceptionsWithTimeout = async ( + timeout_ms: number, + fn: () => Promise, +) => { + const startTime = new Date().getTime() + while (true) { + if (new Date().getTime() - startTime > timeout_ms) { + console.warn('timed out') + break + } + try { + const res = await fn() + return res + } catch (e) { + console.warn((e as Error).message) + await sleep(4000) + } + } +} + +/** Send `amount` to `wallet` from admin wallet. */ +const fundAccount = async (wallet: Address, amount: bigint) => { + const balance = await suaveProvider.getBalance({ address: wallet }) + if (balance < amount) { + const tx: TransactionRequestSuave = { + value: amount, + type: '0x0', + gasPrice: 10000000000n, + gas: 21000n, + to: wallet, + } + return await adminWallet.sendTransaction(tx) + } else { + console.log(`wallet balance: ${formatEther(balance)} ETH`) + } +} + +/** MEV-Share implementation on SUAVE. + * + * To run this, you'll need to deploy the contract first. + * See the [README](./README.md) for instructions. + */ +async function testSuaveBids() { + const BID_CONTRACT_ADDRESS = process.env.BID_CONTRACT_ADDRESS as Hex + if (!BID_CONTRACT_ADDRESS) { + console.error( + 'Need to run the DeployContracts script first. See ./README.md for instructions.', + ) + failEnv('BID_CONTRACT_ADDRESS') + } + + // fund our test wallet w/ 1 ETH + const fundRes = await fundAccount( + wallet.account.address, + 1000000000000000000n, + ) + fundRes && console.log('fundRes', fundRes) + + // a tx that should be landed on goerli + const testTx = { + to: '0x0000000000000000000000000000000000000000' as Address, + data: '0x686f776479' as Hex, + gas: 26000n, + gasPrice: 10000000000n, + chainId: 5, + type: '0x0' as '0x0', + } + const signedTx = await wallet.signTransaction(testTx) + + // create bid & send ccr + const block = await goerliProvider.getBlockNumber() + const bid = new MevShareBid( + block + 1n, + signedTx, + KETTLE_ADDRESS, + BID_CONTRACT_ADDRESS, + suaveRigil.id, + ) + const ccr = bid.toConfidentialRequest() + const ccrRes = await wallet.sendTransaction(ccr) + console.log('ccrRes', ccrRes) + + // wait for ccr to land and get tx receipt + const ccrReceipt = await retryExceptionsWithTimeout(10 * 1000, async () => { + const receipt = await suaveProvider.getTransactionReceipt({ + hash: ccrRes, + }) + return receipt + }) + console.log('ccrReceipt', ccrReceipt) +} + +async function main() { + await testSuaveBids() +} + +main().then(() => { + console.log('done') +}) diff --git a/examples/suave/package.json b/examples/suave/package.json new file mode 100644 index 00000000..e59be2d0 --- /dev/null +++ b/examples/suave/package.json @@ -0,0 +1,13 @@ +{ + "name": "suave-example", + "version": "0.0.0", + "private": true, + "module": "index.ts", + "type": "module", + "dependencies": { + "viem": "workspace:*" + }, + "devDependencies": { + "bun-types": "^0.5.0" + } +} diff --git a/examples/suave/tsconfig.json b/examples/suave/tsconfig.json new file mode 100644 index 00000000..b7198b04 --- /dev/null +++ b/examples/suave/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "strict": true, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "preserve", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "types": [ + "bun-types" // add Bun global + ], + "baseUrl": ".", + "paths": { + "viem": ["../../src"], + "viem/*": ["../../src/*"] + } + } +} diff --git a/package.json b/package.json index b2811053..87f882d6 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "prepare": "bun x simple-git-hooks", "size": "size-limit", "test": "vitest -c ./test/vitest.config.ts dev", + "test:formatter": "vitest -c ./test/vitest.config.ts ./src/chains/suave/formatters.test.ts", + "test:parser": "vitest -c ./test/vitest.config.ts ./src/chains/suave/parsers.test.ts", "test:cov": "vitest dev -c ./test/vitest.config.ts --coverage", "test:ci": "CI=true vitest -c ./test/vitest.config.ts --coverage --retry=3 --bail=1", "test:typecheck": "SKIP_GLOBAL_SETUP=true vitest typecheck -c ./test/vitest.config.ts", diff --git a/playgrounds/bun/.env.example b/playgrounds/bun/.env.example deleted file mode 100644 index e69de29b..00000000 diff --git a/playgrounds/bun/README.md b/playgrounds/bun/README.md deleted file mode 100644 index d7fa9ad3..00000000 --- a/playgrounds/bun/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# bun - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v0.5.6. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/playgrounds/bun/index.ts b/playgrounds/bun/index.ts deleted file mode 100644 index 7ac64d02..00000000 --- a/playgrounds/bun/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { http, createPublicClient } from 'viem' -import { mainnet, polygon } from 'viem/chains' - -//////////////////////////////////////////////////////////////////// -// Clients - -export const publicClients = { - mainnet: createPublicClient({ - chain: mainnet, - transport: http(), - }), - polygon: createPublicClient({ - chain: polygon, - transport: http(), - }), -} - -//////////////////////////////////////////////////////////////////// -// Blocks - -// const blockNumber = await publicClients.mainnet.getBlockNumber() -// const blockNumber = await publicClients.polygon.getBlockNumber() -// console.log('blockNumber', blockNumber) - -//////////////////////////////////////////////////////////////////// -// Events, Logs & Filters - -// const logs = await publicClients.mainnet.getLogs() -// console.log(logs) - -publicClients.mainnet.watchEvent({ - onError(error) { - console.log(error) - }, - onLogs(logs) { - console.log(logs) - }, -}) - -//////////////////////////////////////////////////////////////////// diff --git a/src/chains/definitions/suaveRigil.ts b/src/chains/definitions/suaveRigil.ts index 85fc4f14..a5a47eb6 100644 --- a/src/chains/definitions/suaveRigil.ts +++ b/src/chains/definitions/suaveRigil.ts @@ -1,9 +1,12 @@ +import { createPublicClient } from '../../clients/createPublicClient.js' +import { type Hex } from '../../types/misc.js' import { defineChain } from '../../utils/chain.js' import { formattersSuave } from '../suave/formatters.js' +import { getSuaveWallet } from '../suave/wallet.js' export const suaveRigil = /*#__PURE__*/ defineChain( { - id: 424242, + id: 16813125, name: 'Suave Rigil Testnet', network: 'rigil-testnet', nativeCurrency: { @@ -29,8 +32,13 @@ export const suaveRigil = /*#__PURE__*/ defineChain( }, contracts: {}, testnet: true, + newWallet: (transport: any, privateKey: Hex) => + getSuaveWallet({ transport, chain: suaveRigil }, privateKey), + newPublicClient: (transport: any) => + createPublicClient({ transport, chain: suaveRigil }), }, { formatters: formattersSuave, + // serializers: serializersSuave, }, ) diff --git a/src/chains/suave/errors/transaction.ts b/src/chains/suave/errors/transaction.ts new file mode 100644 index 00000000..a07d1b70 --- /dev/null +++ b/src/chains/suave/errors/transaction.ts @@ -0,0 +1,28 @@ +import { BaseError } from '../../../errors/base.js' + +export type MissingFieldErrorType = MissingFieldError & { + name: 'MissingFieldError' +} +export class MissingFieldError extends BaseError { + override name = 'MissingField' + + missingField: string + found: any + + constructor({ + missingField, + found, + }: { missingField: string; found?: any; message?: string }) { + super(`missing field: '${missingField}'${found ? `. found ${found}` : ''}`) + + this.missingField = missingField + } +} + +export class InvalidConfidentialRequestError extends MissingFieldError { + override name = 'InvalidConfidentialRequest' +} + +export class InvalidConfidentialRecordError extends MissingFieldError { + override name = 'InvalidConfidentialRecord' +} diff --git a/src/chains/suave/formatters.test.ts b/src/chains/suave/formatters.test.ts index 5ce6c902..e2c8c53a 100644 --- a/src/chains/suave/formatters.test.ts +++ b/src/chains/suave/formatters.test.ts @@ -1,104 +1,478 @@ -// import { describe, expect, test } from 'vitest' +import { describe, expect, test } from 'vitest' +import { type Hex, numberToHex, zeroAddress } from '~viem/index.js' +import { suaveRigil } from '../index.js' +import { + type ConfidentialComputeRecordRpc, + type RpcTransactionReceiptSuave, + type RpcTransactionSuave, + type SuaveRpcBlock, + SuaveTxTypes, + type TransactionRequestSuave, +} from './types.js' -// // Assuming you have similar actions for the Suave chain like the Celo ones provided. -// import { getBlock } from '../../actions/public/getBlock.js' -// import { getTransaction } from '../../actions/public/getTransaction.js' -// import { getTransactionReceipt } from '../../actions/public/getTransactionReceipt.js' +describe('block', () => { + const { block } = suaveRigil.formatters! -// import { suaveRigil } from '../index.js' + test('formatter (tx hashes)', () => { + const inputBlock: SuaveRpcBlock = { + baseFeePerGas: '0x235dbc28', + difficulty: '0x2', + extraData: + '0xd983010c00846765746889676f312e32302e3130856c696e757800000000000059ef44f64ed372a15256091c83b05f5baed1aa0e5bec25bdaa0429fcf32600884ed7c748ef6537a2b8d9cc4a99e8758ae1de406e18f522990381e47290a42e2100', + gasLimit: '0x1c9c380', + gasUsed: '0x6aeb', + hash: '0xbe3e3c4205915e175df10e39a69d8dcbd4ca5b3e7dff2549a71edbc891a39e63', + logsBloom: + '0x00400000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000', + miner: '0x0000000000000000000000000000000000000000', + mixHash: + '0x0000000000000000000000000000000000000000000000000000000000000000', + nonce: '0x0000000000000000', + number: '0x4', + parentHash: + '0xe96d8ed683827eb8e6405c7c04354d41b6878a7ab14f4a501d0966e6f8e96827', + receiptsRoot: + '0xe546cca7622a906d721d722c3682fa634126ac32d0a0d29045e573205ef941c5', + sha3Uncles: + '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', + size: '0x5f9', + stateRoot: + '0x38d581cf6f130e0f4f2ab517b22e4eeeb1a8bd40d3e39b4bf6b50bfd6e4857eb', + timestamp: numberToHex(1699046195), + totalDifficulty: '0x9', + transactions: [ + '0x53acb8d180079aa0cc37f5cf3143f71eaffbedde5047b7ed8abaf3c1e6d0d059', + ], + transactionsRoot: + '0x5b7fdbd60e4a948b077d615314474b83cad6b8a07b9272fadd85c807395a913a', + uncles: [], + } + const formattedBlock = block.format(inputBlock) + expect(formattedBlock).toMatchInlineSnapshot(` + { + "baseFeePerGas": 593345576n, + "extraData": "0xd983010c00846765746889676f312e32302e3130856c696e757800000000000059ef44f64ed372a15256091c83b05f5baed1aa0e5bec25bdaa0429fcf32600884ed7c748ef6537a2b8d9cc4a99e8758ae1de406e18f522990381e47290a42e2100", + "gasUsed": 27371n, + "hash": "0xbe3e3c4205915e175df10e39a69d8dcbd4ca5b3e7dff2549a71edbc891a39e63", + "logsBloom": "0x00400000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000", + "number": 4n, + "parentHash": "0xe96d8ed683827eb8e6405c7c04354d41b6878a7ab14f4a501d0966e6f8e96827", + "receiptsRoot": "0xe546cca7622a906d721d722c3682fa634126ac32d0a0d29045e573205ef941c5", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": 1529n, + "stateRoot": "0x38d581cf6f130e0f4f2ab517b22e4eeeb1a8bd40d3e39b4bf6b50bfd6e4857eb", + "timestamp": 1699046195n, + "totalDifficulty": 9n, + "transactions": [ + "0x53acb8d180079aa0cc37f5cf3143f71eaffbedde5047b7ed8abaf3c1e6d0d059", + ], + "transactionsRoot": "0x5b7fdbd60e4a948b077d615314474b83cad6b8a07b9272fadd85c807395a913a", + } + `) + }) -// describe('block', () => { -// test('formatter', () => { -// const { block } = suaveRigil.formatters! + test('formatter (full txs)', () => { + const inputBlock: SuaveRpcBlock = { + baseFeePerGas: '0x235dbc28', + difficulty: '0x2', + extraData: + '0xd983010c00846765746889676f312e32302e3130856c696e757800000000000059ef44f64ed372a15256091c83b05f5baed1aa0e5bec25bdaa0429fcf32600884ed7c748ef6537a2b8d9cc4a99e8758ae1de406e18f522990381e47290a42e2100', + gasLimit: '0x1c9c380', + gasUsed: '0x6aeb', + hash: '0xbe3e3c4205915e175df10e39a69d8dcbd4ca5b3e7dff2549a71edbc891a39e63', + logsBloom: + '0x00400000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000', + miner: '0x0000000000000000000000000000000000000000', + mixHash: + '0x0000000000000000000000000000000000000000000000000000000000000000', + nonce: '0x0000000000000000', + number: '0x4', + parentHash: + '0xe96d8ed683827eb8e6405c7c04354d41b6878a7ab14f4a501d0966e6f8e96827', + receiptsRoot: + '0xe546cca7622a906d721d722c3682fa634126ac32d0a0d29045e573205ef941c5', + sha3Uncles: + '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', + size: '0x5f9', + stateRoot: + '0x38d581cf6f130e0f4f2ab517b22e4eeeb1a8bd40d3e39b4bf6b50bfd6e4857eb', + timestamp: '0x65419984', + totalDifficulty: '0x9', + transactions: [ + { + blockHash: + '0xbe3e3c4205915e175df10e39a69d8dcbd4ca5b3e7dff2549a71edbc891a39e63', + blockNumber: '0x4', + from: '0xbe69d72ca5f88acba033a063df5dbe43a4148de0', + gas: '0xf4240', + gasPrice: '0x3518320e', + hash: '0x53acb8d180079aa0cc37f5cf3143f71eaffbedde5047b7ed8abaf3c1e6d0d059', + input: + '0xc0b9d28700000000000000000000000000000000000000000000000000000000000000201518a916067557098f425aad1b1614f10000000000000000000000000000000011176998e3484c2d95582c916403a54100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008f21fdd6b4f4cacd33151777a46c122797c8bf170000000000000000000000000000000000000000000000000000000000000001000000000000000000000000b5feafbdd752ad52afb7e1bd2e40432a485bbb7f00000000000000000000000000000000000000000000000000000000000000156d657673686172653a76303a6d61746368426964730000000000000000000000', + nonce: '0x3', + to: '0x8f21fdd6b4f4cacd33151777a46c122797c8bf17', + transactionIndex: '0x0', + value: '0x0', + type: '0x50', + typeHex: '0x50', + chainId: '0x1008c45', + requestRecord: { + type: '0x42' as any, + typeHex: '0x42', + chainId: '0x1008c45', + nonce: '0x3', + to: '0x8f21fdd6b4f4cacd33151777a46c122797c8bf17', + gas: '0xf4240', + gasPrice: '0x3518320e', + value: '0x0', + input: + '0xd8f55db90000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0c90db5a779ab544cb9105c6ec1118f290000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008f21fdd6b4f4cacd33151777a46c122797c8bf170000000000000000000000000000000000000000000000000000000000000000', + kettleAddress: '0xb5feafbdd752ad52afb7e1bd2e40432a485bbb7f', + confidentialInputsHash: + '0xd890046400e66fc2ae1841fd630f4c2eab51d8f238bf20f9a6785a73ff113741', + v: '0x1', + r: '0xd0c7f58b8c9b94f48fe4d606ed21d6fa8eb2a57f68b98a6365c97a44f16ad46', + s: '0x4762533922d26ba2435105191384710d322586bfdb2ee9f8c8f9b89234d68112', + hash: '0x800fab8954d4f1392030fa6b5dce0ccf4dcfdac30175201927d7be6dcb62a0a5', + }, + confidentialComputeResult: + '0xc0b9d28700000000000000000000000000000000000000000000000000000000000000201518a916067557098f425aad1b1614f10000000000000000000000000000000011176998e3484c2d95582c916403a54100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008f21fdd6b4f4cacd33151777a46c122797c8bf170000000000000000000000000000000000000000000000000000000000000001000000000000000000000000b5feafbdd752ad52afb7e1bd2e40432a485bbb7f00000000000000000000000000000000000000000000000000000000000000156d657673686172653a76303a6d61746368426964730000000000000000000000', + v: '0x0', + r: '0x38bda742051df0c9c3853f197533c3dbc7113c7ef1b91bcb7cc268228fad01c', + s: '0x32581db9b1ff062e66d9ecd8e3c6168418f01498d9aa4722febac0f1c7b70f', + }, + ], + transactionsRoot: + '0x5b7fdbd60e4a948b077d615314474b83cad6b8a07b9272fadd85c807395a913a', + uncles: [], + } + const formattedBlock = block.format(inputBlock) + expect(formattedBlock).toMatchInlineSnapshot(` + { + "baseFeePerGas": 593345576n, + "extraData": "0xd983010c00846765746889676f312e32302e3130856c696e757800000000000059ef44f64ed372a15256091c83b05f5baed1aa0e5bec25bdaa0429fcf32600884ed7c748ef6537a2b8d9cc4a99e8758ae1de406e18f522990381e47290a42e2100", + "gasUsed": 27371n, + "hash": "0xbe3e3c4205915e175df10e39a69d8dcbd4ca5b3e7dff2549a71edbc891a39e63", + "logsBloom": "0x00400000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000", + "number": 4n, + "parentHash": "0xe96d8ed683827eb8e6405c7c04354d41b6878a7ab14f4a501d0966e6f8e96827", + "receiptsRoot": "0xe546cca7622a906d721d722c3682fa634126ac32d0a0d29045e573205ef941c5", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": 1529n, + "stateRoot": "0x38d581cf6f130e0f4f2ab517b22e4eeeb1a8bd40d3e39b4bf6b50bfd6e4857eb", + "timestamp": 1698797956n, + "totalDifficulty": 9n, + "transactions": [ + { + "blockHash": "0xbe3e3c4205915e175df10e39a69d8dcbd4ca5b3e7dff2549a71edbc891a39e63", + "blockNumber": 4n, + "chainId": 16813125, + "confidentialComputeResult": "0xc0b9d28700000000000000000000000000000000000000000000000000000000000000201518a916067557098f425aad1b1614f10000000000000000000000000000000011176998e3484c2d95582c916403a54100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008f21fdd6b4f4cacd33151777a46c122797c8bf170000000000000000000000000000000000000000000000000000000000000001000000000000000000000000b5feafbdd752ad52afb7e1bd2e40432a485bbb7f00000000000000000000000000000000000000000000000000000000000000156d657673686172653a76303a6d61746368426964730000000000000000000000", + "from": "0xbe69d72ca5f88acba033a063df5dbe43a4148de0", + "gas": 1000000n, + "gasPrice": 890778126n, + "hash": "0x53acb8d180079aa0cc37f5cf3143f71eaffbedde5047b7ed8abaf3c1e6d0d059", + "input": "0xc0b9d28700000000000000000000000000000000000000000000000000000000000000201518a916067557098f425aad1b1614f10000000000000000000000000000000011176998e3484c2d95582c916403a54100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008f21fdd6b4f4cacd33151777a46c122797c8bf170000000000000000000000000000000000000000000000000000000000000001000000000000000000000000b5feafbdd752ad52afb7e1bd2e40432a485bbb7f00000000000000000000000000000000000000000000000000000000000000156d657673686172653a76303a6d61746368426964730000000000000000000000", + "nonce": 3, + "r": "0x38bda742051df0c9c3853f197533c3dbc7113c7ef1b91bcb7cc268228fad01c", + "requestRecord": { + "chainId": "0x1008c45", + "confidentialInputsHash": "0xd890046400e66fc2ae1841fd630f4c2eab51d8f238bf20f9a6785a73ff113741", + "gas": "0xf4240", + "gasPrice": "0x3518320e", + "hash": "0x800fab8954d4f1392030fa6b5dce0ccf4dcfdac30175201927d7be6dcb62a0a5", + "input": "0xd8f55db90000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0c90db5a779ab544cb9105c6ec1118f290000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008f21fdd6b4f4cacd33151777a46c122797c8bf170000000000000000000000000000000000000000000000000000000000000000", + "kettleAddress": "0xb5feafbdd752ad52afb7e1bd2e40432a485bbb7f", + "nonce": "0x3", + "r": "0xd0c7f58b8c9b94f48fe4d606ed21d6fa8eb2a57f68b98a6365c97a44f16ad46", + "s": "0x4762533922d26ba2435105191384710d322586bfdb2ee9f8c8f9b89234d68112", + "to": "0x8f21fdd6b4f4cacd33151777a46c122797c8bf17", + "type": "0x42", + "typeHex": "0x42", + "v": "0x1", + "value": "0x0", + }, + "s": "0x32581db9b1ff062e66d9ecd8e3c6168418f01498d9aa4722febac0f1c7b70f", + "to": "0x8f21fdd6b4f4cacd33151777a46c122797c8bf17", + "transactionIndex": 0, + "type": "0x50", + "typeHex": "0x50", + "v": 0n, + "value": 0n, + }, + ], + "transactionsRoot": "0x5b7fdbd60e4a948b077d615314474b83cad6b8a07b9272fadd85c807395a913a", + } + `) + }) +}) -// const formattedBlock = block.format({ -// randomness: 'sampleRandomValue', -// transactions: [ -// { -// ExecutionNode: 'sampleExecutionNode', -// ConfidentialComputeRequest: 'sampleRequest', -// ConfidentialComputeResult: 'sampleResult', -// // ... other RpcTransaction fields if present -// }, -// ], -// }) +describe('transaction', () => { + const { transaction } = suaveRigil.formatters! -// expect(formattedBlock).toMatchInlineSnapshot(` -// { -// "randomness": "sampleRandomValue", -// "transactions": [ -// { -// "ExecutionNode": "sampleExecutionNode", -// "ConfidentialComputeRequest": "sampleRequest", -// "ConfidentialComputeResult": "sampleResult", -// // ... Other expected fields here -// } -// ] -// } -// `) -// }) -// }) + test('formatter (RPC -> Transaction)', () => { + const requestRecord = { + from: zeroAddress, + to: '0x1300000000130000000013000000001300000000', + chainId: '0x1' as Hex, + gas: '0x13' as Hex, + gasPrice: '0x1000' as Hex, + kettleAddress: zeroAddress, + confidentialInputsHash: '0x0' as Hex, + hash: '0x3303d96ec5d3387da51f2fc815ea3e88c5b534383f86eef02a9200f0c6fd5713', + nonce: '0x0' as Hex, + input: '0x0' as Hex, + value: '0x0' as Hex, + r: '0x0' as Hex, + s: '0x0' as Hex, + v: '0x0' as Hex, + type: '0x42' as `0x42`, + typeHex: '0x42' as `0x42`, + } as ConfidentialComputeRecordRpc -// describe('transaction', () => { -// test('formatter', () => { -// const { transaction } = suaveRigil.formatters! + const inputTransactionRpc = { + blockHash: + '0x8756d7614991fafffd2c788d7213122a2145629860575fb52be80cbef128fbb6', + chainId: numberToHex(suaveRigil.id), + requestRecord, + confidentialComputeResult: '0x0' as Hex, + blockNumber: '0x10' as Hex, + gasPrice: '0x100' as Hex, + hash: '0xcd6a47804736bf27ec2a5845c560adcdfab305b4e80452354bcf96fb472fd364', + nonce: '0x0' as Hex, + transactionIndex: '0x0' as Hex, + r: '0x0' as Hex, + s: '0x0' as Hex, + v: '0x0' as Hex, + from: zeroAddress, + gas: '0x13' as Hex, + input: '0x0' as Hex, + to: '0x1300000000130000000013000000001300000000' as Hex, + value: '0x0' as Hex, + type: SuaveTxTypes.Suave, + typeHex: SuaveTxTypes.Suave, + } as RpcTransactionSuave -// const inputTransaction = { -// ExecutionNode: 'sampleExecutionNode', -// ConfidentialComputeRequest: 'sampleRequest', -// ConfidentialComputeResult: 'sampleResult', -// // ... other fields if present -// } + const formattedTransaction = transaction.format(inputTransactionRpc) + expect(formattedTransaction).toMatchInlineSnapshot(` + { + "accessList": undefined, + "blockHash": "0x8756d7614991fafffd2c788d7213122a2145629860575fb52be80cbef128fbb6", + "blockNumber": 16n, + "chainId": 16813125, + "confidentialComputeResult": "0x0", + "from": "0x0000000000000000000000000000000000000000", + "gas": 19n, + "gasPrice": 256n, + "hash": "0xcd6a47804736bf27ec2a5845c560adcdfab305b4e80452354bcf96fb472fd364", + "input": "0x0", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "nonce": 0, + "r": "0x0", + "requestRecord": { + "blockHash": null, + "blockNumber": null, + "chainId": 1, + "confidentialInputsHash": "0x0", + "from": "0x0000000000000000000000000000000000000000", + "gas": 19n, + "gasPrice": 4096n, + "hash": "0x3303d96ec5d3387da51f2fc815ea3e88c5b534383f86eef02a9200f0c6fd5713", + "input": "0x0", + "kettleAddress": "0x0000000000000000000000000000000000000000", + "nonce": 0, + "r": "0x0", + "s": "0x0", + "to": "0x1300000000130000000013000000001300000000", + "transactionIndex": null, + "type": "0x42", + "typeHex": "0x42", + "v": 0n, + "value": 0n, + }, + "s": "0x0", + "to": "0x1300000000130000000013000000001300000000", + "transactionIndex": 0, + "type": "0x50", + "typeHex": "0x50", + "v": 0n, + "value": 0n, + } + `) + }) +}) -// const formattedTransaction = transaction.format(inputTransaction) +describe('transactionReceipt', () => { + test('formatter', () => { + const { transactionReceipt } = suaveRigil.formatters! -// expect(formattedTransaction).toMatchInlineSnapshot(` -// { -// "ExecutionNode": "sampleExecutionNode", -// "ConfidentialComputeRequest": "sampleRequest", -// "ConfidentialComputeResult": "sampleResult", -// // ... Other expected fields here -// } -// `) -// }) -// }) + const inputReceipt: RpcTransactionReceiptSuave = { + blockHash: + '0x254eecc07b2c034bd9ea619c75992d4c491eb5d4576e98f3c8cbd5b4ad456a2a', + blockNumber: '0x3', + contractAddress: null, + cumulativeGasUsed: '0x7c14', + effectiveGasPrice: '0x3518320e', + from: '0xbe69d72ca5f88acba033a063df5dbe43a4148de0', + gasUsed: '0x7c14', + logs: [ + { + address: '0x8f21fdd6b4f4cacd33151777a46c122797c8bf17', + topics: [ + '0x83481d5b04dea534715acad673a8177a46fc93882760f36bdc16ccac439d504e', + ], + data: '0xbef01c5c5f3655619d1e24fcc9a5f37b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008f21fdd6b4f4cacd33151777a46c122797c8bf17', + blockNumber: '0x3', + transactionHash: + '0xa123928f4eadf4bccad09a143f7f549d21eff9e772ee9db90d11a0e65b125711', + transactionIndex: '0x0', + blockHash: + '0x254eecc07b2c034bd9ea619c75992d4c491eb5d4576e98f3c8cbd5b4ad456a2a', + logIndex: '0x0', + removed: false, + }, + { + address: '0x8f21fdd6b4f4cacd33151777a46c122797c8bf17', + topics: [ + '0xdab8306bad2ca820d05b9eff8da2e3016d372c15f00bb032f758718b9cda3950', + ], + data: '0xbef01c5c5f3655619d1e24fcc9a5f37b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003d7b22546f223a22307832316332306464346562303030663862343132613532353065386132373566353135353232326131222c2244617461223a22227d000000', + blockNumber: '0x3', + transactionHash: + '0xa123928f4eadf4bccad09a143f7f549d21eff9e772ee9db90d11a0e65b125711', + transactionIndex: '0x0', + blockHash: + '0x254eecc07b2c034bd9ea619c75992d4c491eb5d4576e98f3c8cbd5b4ad456a2a', + logIndex: '0x1', + removed: false, + }, + ], + logsBloom: + '0x00400000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000002000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000001000000000000010000000000000000000000000000000000000000000000000000', + status: '0x1', + to: '0x8f21fdd6b4f4cacd33151777a46c122797c8bf17', + transactionHash: + '0xa123928f4eadf4bccad09a143f7f549d21eff9e772ee9db90d11a0e65b125711', + transactionIndex: '0x0', + type: '0x50', + } -// describe('transactionReceipt', () => { -// test('formatter', () => { -// const { transactionReceipt } = suaveRigil.formatters! + const formattedReceipt = transactionReceipt.format(inputReceipt) -// const inputReceipt = { -// // ... input fields based on SuaveRpcTransactionReceiptOverrides -// } + expect(formattedReceipt).toMatchInlineSnapshot(` + { + "blockHash": "0x254eecc07b2c034bd9ea619c75992d4c491eb5d4576e98f3c8cbd5b4ad456a2a", + "blockNumber": 3n, + "contractAddress": null, + "cumulativeGasUsed": 31764n, + "effectiveGasPrice": 890778126n, + "from": "0xbe69d72ca5f88acba033a063df5dbe43a4148de0", + "gasUsed": 31764n, + "logs": [ + { + "address": "0x8f21fdd6b4f4cacd33151777a46c122797c8bf17", + "blockHash": "0x254eecc07b2c034bd9ea619c75992d4c491eb5d4576e98f3c8cbd5b4ad456a2a", + "blockNumber": 3n, + "data": "0xbef01c5c5f3655619d1e24fcc9a5f37b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008f21fdd6b4f4cacd33151777a46c122797c8bf17", + "logIndex": 0, + "removed": false, + "topics": [ + "0x83481d5b04dea534715acad673a8177a46fc93882760f36bdc16ccac439d504e", + ], + "transactionHash": "0xa123928f4eadf4bccad09a143f7f549d21eff9e772ee9db90d11a0e65b125711", + "transactionIndex": 0, + }, + { + "address": "0x8f21fdd6b4f4cacd33151777a46c122797c8bf17", + "blockHash": "0x254eecc07b2c034bd9ea619c75992d4c491eb5d4576e98f3c8cbd5b4ad456a2a", + "blockNumber": 3n, + "data": "0xbef01c5c5f3655619d1e24fcc9a5f37b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000003d7b22546f223a22307832316332306464346562303030663862343132613532353065386132373566353135353232326131222c2244617461223a22227d000000", + "logIndex": 1, + "removed": false, + "topics": [ + "0xdab8306bad2ca820d05b9eff8da2e3016d372c15f00bb032f758718b9cda3950", + ], + "transactionHash": "0xa123928f4eadf4bccad09a143f7f549d21eff9e772ee9db90d11a0e65b125711", + "transactionIndex": 0, + }, + ], + "logsBloom": "0x00400000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000002000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000001000000000000010000000000000000000000000000000000000000000000000000", + "status": "success", + "to": "0x8f21fdd6b4f4cacd33151777a46c122797c8bf17", + "transactionHash": "0xa123928f4eadf4bccad09a143f7f549d21eff9e772ee9db90d11a0e65b125711", + "transactionIndex": 0, + "type": "0x50", + } + `) + }) +}) -// const formattedReceipt = transactionReceipt.format(inputReceipt) +describe('transactionRequest', () => { + const { transactionRequest } = suaveRigil.formatters! -// expect(formattedReceipt).toMatchInlineSnapshot(` -// { -// // ... Expected fields here based on the SuaveRpcTransactionReceiptOverrides format -// } -// `) -// }) -// }) + test('formatter (confidential)', () => { + const inputRequest: TransactionRequestSuave = { + from: zeroAddress, + to: zeroAddress, + gas: 1n, + gasPrice: 0x10000000n, + value: 0n, + kettleAddress: zeroAddress, + confidentialInputs: '0x13131313', + chainId: suaveRigil.id, + nonce: 13, + data: '0x0', + type: SuaveTxTypes.ConfidentialRequest, + } + const formattedRequest = transactionRequest.format(inputRequest) + expect(formattedRequest).toMatchInlineSnapshot(` + { + "chainId": "0x1008c45", + "confidentialInputs": "0x13131313", + "data": "0x0", + "from": "0x0000000000000000000000000000000000000000", + "gas": "0x1", + "gasPrice": "0x10000000", + "isConfidential": true, + "kettleAddress": "0x0000000000000000000000000000000000000000", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "nonce": "0xd", + "to": "0x0000000000000000000000000000000000000000", + "type": "0x43", + "value": "0x0", + } + `) + }) -// describe('transactionRequest', () => { -// test('formatter', () => { -// const { transactionRequest } = suaveRigil.formatters! - -// const inputRequest = { -// ExecutionNode: 'sampleExecutionNode', -// ConfidentialComputeRequest: 'sampleRequest', -// // ... other fields if present -// } - -// const formattedRequest = transactionRequest.format(inputRequest) - -// expect(formattedRequest).toMatchInlineSnapshot(` -// { -// "ExecutionNode": "sampleExecutionNode", -// "ConfidentialComputeRequest": "sampleRequest", -// // ... Other expected fields here -// } -// `) -// }) -// }) + test('formatter (standard)', () => { + const inputRequest: TransactionRequestSuave = { + chainId: suaveRigil.id, + from: zeroAddress, + to: zeroAddress, + gas: 1n, + gasPrice: 0n, + value: 0n, + nonce: 13, + data: '0x0', + type: '0x0', + } + const formattedRequest = transactionRequest.format(inputRequest) + expect(formattedRequest).toMatchInlineSnapshot(` + { + "chainId": 16813125, + "data": "0x0", + "from": "0x0000000000000000000000000000000000000000", + "gas": "0x1", + "gasPrice": "0x0", + "maxFeePerGas": undefined, + "maxPriorityFeePerGas": undefined, + "nonce": "0xd", + "to": "0x0000000000000000000000000000000000000000", + "type": undefined, + "value": "0x0", + } + `) + }) +}) diff --git a/src/chains/suave/formatters.ts b/src/chains/suave/formatters.ts index 01dd69ed..a0f99b5b 100644 --- a/src/chains/suave/formatters.ts +++ b/src/chains/suave/formatters.ts @@ -1,114 +1,149 @@ +import { zeroAddress } from '../../constants/address.js' import { type ChainFormatters } from '../../types/chain.js' -import type { Hash } from '../../types/misc.js' +import type { Hash, Hex } from '../../types/misc.js' import type { RpcTransaction } from '../../types/rpc.js' +import type { + Transaction, + TransactionRequestBase, +} from '../../types/transaction.js' +import { hexToBigInt } from '../../utils/encoding/fromHex.js' +import { toHex } from '../../utils/encoding/toHex.js' import { defineBlock } from '../../utils/formatters/block.js' import { defineTransaction, formatTransaction, } from '../../utils/formatters/transaction.js' -import { defineTransactionReceipt } from '../../utils/formatters/transactionReceipt.js' -import { defineTransactionRequest } from '../../utils/formatters/transactionRequest.js' - -// Introduce the new types -export type ConfidentialComputeRequest = { - ExecutionNode: string // Assuming address is a string type - Wrapped: RpcTransaction // This might need to be adjusted to the actual Ethereum Transaction type -} - -export type SuaveTransaction = { - ExecutionNode: string - ConfidentialComputeRequest: ConfidentialComputeRequest - ConfidentialComputeResult: string // Assuming bytes are represented as hexadecimal strings - // TODO: signature fields -} - -import type { - SuaveBlockOverrides, - SuaveRpcTransaction, - SuaveRpcTransactionRequest, - SuaveTransactionReceipt, - SuaveTransactionReceiptOverrides, - SuaveTransactionRequest, +import { + defineTransactionReceipt, + formatTransactionReceipt, +} from '../../utils/formatters/transactionReceipt.js' +import { + defineTransactionRequest, + formatTransactionRequest, +} from '../../utils/formatters/transactionRequest.js' +import { suaveRigil } from '../index.js' +import { + type ConfidentialComputeRecord, + type RpcTransactionReceiptSuave, + type RpcTransactionRequestSuave, + type RpcTransactionSuave, + type SuaveBlockOverrides, + type SuaveTxType, + type TransactionReceiptSuave, + type TransactionRequestSuave, + type TransactionSuave, } from './types.js' export const formattersSuave = { block: /*#__PURE__*/ defineBlock({ - exclude: ['difficulty', 'gasLimit', 'mixHash', 'nonce', 'uncles'], + exclude: ['difficulty', 'gasLimit', 'miner', 'mixHash', 'nonce', 'uncles'], format( args: SuaveBlockOverrides & { - transactions: Hash[] | SuaveRpcTransaction[] + transactions: + | Hash[] + | (RpcTransactionSuave | RpcTransaction)[] }, ): SuaveBlockOverrides & { - transactions: Hash[] | SuaveTransaction[] + transactions: Hash[] | TransactionSuave[] } { const transactions = args.transactions?.map((transaction) => { if (typeof transaction === 'string') return transaction - return { - ...formatTransaction(transaction as RpcTransaction), - ExecutionNode: transaction.ExecutionNode, - ConfidentialComputeRequest: { - ExecutionNode: transaction.ExecutionNode, - Wrapped: transaction as RpcTransaction, - }, - ConfidentialComputeResult: transaction.ConfidentialComputeResult, - // TODO : Signature fields + else if (transaction.type === '0x50') { + return { + ...formatTransaction({ + ...transaction, + type: '0x0', + } as RpcTransaction), + gasPrice: hexToBigInt(transaction.gasPrice as Hex), + confidentialComputeResult: transaction.confidentialComputeResult, + type: transaction.type, + typeHex: transaction.typeHex, + } } - }) as Hash[] | SuaveTransaction[] + return formatTransaction(transaction as RpcTransaction) + }) as Hash[] | TransactionSuave[] return { transactions, } }, }), transaction: /*#__PURE__*/ defineTransaction({ - format(args: SuaveRpcTransaction): SuaveTransaction { - if (args.IsConfidential) { + format( + args: RpcTransactionSuave, + ): TransactionSuave | Transaction { + if (args.type === '0x50') { return { - ExecutionNode: args.ExecutionNode, - ConfidentialComputeRequest: { - ExecutionNode: args.ExecutionNode, - Wrapped: args.ConfidentialComputeRequest, // This assumes that args.ConfidentialComputeRequest is of type Transaction - }, - ConfidentialComputeResult: args.ConfidentialComputeResult, - // TODO : Signature fields - } as SuaveTransaction + // format original eth params as legacy tx + ...formatTransaction({ ...args, type: '0x0' } as RpcTransaction), + chainId: parseInt(args.chainId, 16), + accessList: args.accessList, + // ... then replace and add fields as needed + gasPrice: hexToBigInt(args.gasPrice as Hex), + requestRecord: { + // format confidential compute request as legacy tx + ...{ + ...formatTransaction({ + ...args.requestRecord, + type: '0x0', + blockHash: '0x0', // dummy fields to force type coercion + blockNumber: '0x0', + transactionIndex: '0x0', + from: zeroAddress, + } as RpcTransaction), + blockHash: null, + blockNumber: null, + transactionIndex: null, + }, + // ... then replace and add fields as needed + kettleAddress: args.requestRecord.kettleAddress, + confidentialInputsHash: args.requestRecord.confidentialInputsHash, + chainId: + args.requestRecord.chainId && + parseInt(args.requestRecord.chainId, 16), + type: args.requestRecord.type, + typeHex: args.requestRecord.typeHex, + } as ConfidentialComputeRecord, + confidentialComputeResult: args.confidentialComputeResult, + type: args.type, + typeHex: args.typeHex, + } as TransactionSuave } else { - return args as any // TODO : Handle as regular Ethereum transaction + console.log('formatting regular tx') + // Handle as regular Ethereum transaction + return formatTransaction(args as RpcTransaction) as Transaction } }, }), transactionReceipt: /*#__PURE__*/ defineTransactionReceipt({ - format(args: SuaveTransactionReceiptOverrides): SuaveTransactionReceipt { - const { - ExecutionNode, - ConfidentialComputeRequest, - ConfidentialComputeResult, - ...baseProps - } = args - + format(args: RpcTransactionReceiptSuave): TransactionReceiptSuave { + console.log('formatting tx receipt') return { - ...baseProps, - ExecutionNode, - ConfidentialComputeRequest: { - ...ConfidentialComputeRequest, - }, - ConfidentialComputeResult, - // signature fields - } as SuaveTransactionReceipt + ...formatTransactionReceipt(args), + } as TransactionReceiptSuave }, }), - transactionRequest: /*#__PURE__*/ defineTransactionRequest({ - format(args: SuaveTransactionRequest): SuaveRpcTransactionRequest { - if (args.IsConfidential) { - const { ExecutionNode, IsConfidential } = args + format(args: TransactionRequestSuave): RpcTransactionRequestSuave { + if ( + args.confidentialInputs && + !['0x', '0x0'].includes(args.confidentialInputs) + ) { + const { kettleAddress, confidentialInputs } = args return { - ...args, // Include other properties from args - ExecutionNode: ExecutionNode, - IsConfidential: IsConfidential, - // We omit the ConfidentialComputeRequest here - } as SuaveRpcTransactionRequest + ...formatTransactionRequest({ + ...args, + from: zeroAddress, + } as TransactionRequestBase), + kettleAddress, + // isConfidential: true, // TODO: where does this come from? where does it go? where does it come from, cotton-eyed joe? + confidentialInputs, + type: args.type, + gasPrice: toHex(args.gasPrice), + chainId: toHex(args.chainId || suaveRigil.id), + } as RpcTransactionRequestSuave } else { - return args as any // TODO : Handle as regular Ethereum transaction + // handle as regular ethereum transaction + return formatTransactionRequest(args as any) as any } }, }), diff --git a/src/chains/suave/parsers.test.ts b/src/chains/suave/parsers.test.ts index aca3cea4..2cf1722f 100644 --- a/src/chains/suave/parsers.test.ts +++ b/src/chains/suave/parsers.test.ts @@ -1,64 +1,78 @@ -// import { expect, test } from 'vitest' - -// import { accounts } from '~test/src/constants.js' -// import { -// parseEther, -// parseTransaction as parseTransaction_, -// serializeTransaction, -// toRlp, -// } from '../../index.js' -// import { parseTransactionSuave } from './parsers.js' -// import { serializeTransactionSuave } from './serializers.js' -// import type { TransactionSerializableSuave } from './types.js' - -// test('should be able to parse a standard Suave transaction', () => { -// const signedTransaction = /* Sample Suave signed transaction */; - -// expect(parseTransactionSuave(signedTransaction)).toMatchInlineSnapshot(` -// { -// "chainId": /* Some chain ID */, -// "gas": /* Some gas amount */, -// "to": /* Some address */, -// "value": /* Some value */, -// "ExecutionNode": /* Execution Node value */, -// "ConfidentialComputeRequest": /* Compute Request value */, -// "ConfidentialComputeResult": /* Compute Result value */ -// } -// `) -// }) - -// test('should parse a Suave transaction with data', () => { -// const transactionWithData = { -// ...transaction, -// data: '0x1234', // Example data for this test -// } - -// const serialized = serializeTransactionSuave(transactionWithData) - -// expect(parseTransactionSuave(serialized)).toMatchInlineSnapshot(` -// { -// ...otherTransactionDetails, -// "data": "0x1234" -// } -// `) -// }) - -// test('should parse a Suave transaction with Execution Node', () => { -// const transactionWithNode = { -// ...transaction, -// ExecutionNode: accounts[1].address, // Example address -// } - -// const serialized = serializeTransactionSuave(transactionWithNode) - -// expect(parseTransactionSuave(serialized)).toMatchInlineSnapshot(` -// { -// ...otherTransactionDetails, -// "ExecutionNode": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8" -// } -// `) -// }) - +import { describe, expect, test } from 'vitest' +import { accounts } from '~test/src/constants.js' +import { http } from '~viem/index.js' +import { suaveRigil } from '../index.js' +import { parseSignedComputeRequest, parseTransactionSuave } from './parsers.js' +import { + type SuaveTxType, + SuaveTxTypes, + type TransactionRequestSuave, +} from './types.js' +import { getSuaveWallet } from './wallet.js' + +describe('Suave Transaction Parsers', () => { + const wallet = getSuaveWallet( + { + transport: http(suaveRigil.rpcUrls.local.http[0]), + chain: suaveRigil, + }, + accounts[0].privateKey, + ) + const sampleTx = { + to: accounts[1].address, + data: '0x13', + gas: 100n, + gasPrice: 100n, + nonce: 0, + type: '0x43', + chainId: 0x33, + kettleAddress: accounts[1].address, + confidentialInputs: '0x42424242', + } as TransactionRequestSuave + + test('parses a signed ConfidentialComputeRequest', async () => { + const signedTransaction = await wallet.signTransaction(sampleTx) + console.log('signed confidentialRequest', signedTransaction) + expect(parseSignedComputeRequest(signedTransaction)).toMatchInlineSnapshot(` + { + "chainId": ${sampleTx.chainId}, + "confidentialInputs": "${sampleTx.confidentialInputs}", + "data": "${sampleTx.data}", + "gas": 100n, + "gasPrice": 100n, + "kettleAddress": "${sampleTx.kettleAddress}", + "nonce": 0, + "r": "0x502ec36261e4b88251ef11134fa6304a0e8ae65200efb9a606ee9eef4343346d", + "s": "0x0e01ac9093d1bbeee267ee6ac81838a7a4d6ebe62ab3b33753311496b083122f", + "to": "${sampleTx.to}", + "type": "0x43", + "v": 0n, + "value": 0n, + } + `) + }) + + test('parseTransactionSuave parses all SUAVE tx types', async () => { + const serializedTx2 = + '0x43f902aaf90184098412504db4830f4240949a151aa453329f3cdf04d8e4e81585a423f7fc2580b8e4d8f55db90000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c012e8eff6ead85d9d948631a18c41afb60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000009a151aa453329f3cdf04d8e4e81585a423f7fc25000000000000000000000000000000000000000000000000000000000000000094b5feafbdd752ad52afb7e1bd2e40432a485bbb7fa0249c92db3766bc250ffe17682d363e78dbd3aa1fff59a3b5ca242c872910effa8401008c4580a04a0e49a3711af960c5e76d10a21ae318912702b4cfdb37e6baf087edc84feedca02304c28a2a6cb07efa0643e4e2a78bdd2980ccc1d23b359c9cc67543461eb98ab90120000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000dc7b22747873223a5b2230786638363538303064383235336163393431633638353738353161333737633866613736343130396435353933383261393235376334393962383230336538383038343032303131386164613037613861313734613333643136353432363938616538353061303965303530333262373865353934616164613061343164313137376136383333636266633630613031633465663334313031626161363665376338393438376365353062343239653138623733663535323064366130656633396630366234386362343862373064225d7d00000000' + const parsedTx2 = parseTransactionSuave(serializedTx2) + expect(parsedTx2.type).toBe(SuaveTxTypes.ConfidentialRequest) + + const serializedTx3 = await wallet.signTransaction({ + to: accounts[1].address, + data: '0x13', + gas: 100n, + gasPrice: 100n, + nonce: 0, + type: '0x0', + chainId: 0x33, + }) + const parsedTx3 = parseTransactionSuave(serializedTx3 as SuaveTxType) + expect(parsedTx3.type).toBe('0x0') + }) +}) + +// TODO: Add tests for invalid transactions // test('invalid transaction (all missing)', () => { // expect(() => // parseTransactionSuave(`0xYourPrefix${toRlp([]).slice(2)}`), @@ -93,4 +107,3 @@ // }) // // ... Additional tests specific to your needs ... - diff --git a/src/chains/suave/parsers.ts b/src/chains/suave/parsers.ts index 25b9c03d..d9c27be3 100644 --- a/src/chains/suave/parsers.ts +++ b/src/chains/suave/parsers.ts @@ -1,95 +1,169 @@ -// import { InvalidSerializedTransactionError } from '../../errors/transaction.js' -// import type { Hex } from '../../types/misc.js' -// import { isHex } from '../../utils/data/isHex.js' -// import { sliceHex } from '../../utils/data/slice.js' -// import { hexToBigInt, hexToNumber } from '../../utils/encoding/fromHex.js' -// import type { RecursiveArray } from '../../utils/encoding/toRlp.js' -// import type { GetSerializedTransactionType } from '../../utils/transaction/getSerializedTransactionType.js' -// import { -// type ParseTransactionReturnType, -// parseAccessList, -// parseTransaction, -// toTransactionArray, -// } from '../../utils/transaction/parseTransaction.js' -// import { assertTransactionSuave } from './serializers.js' -// import type { -// SuaveTransactionSerialized, -// SuaveTransactionType, -// TransactionSerializableSuave, -// } from './types.js' - -// export type ParseTransactionSuaveReturnType< -// TSerialized extends SuaveTransactionSerialized = SuaveTransactionSerialized, -// TType extends SuaveTransactionType = GetSerializedTransactionType, -// > = ParseTransactionReturnType - -// export function parseTransactionSuave< -// TSerialized extends SuaveTransactionSerialized, -// >( -// serializedTransaction: TSerialized, -// ): ParseTransactionSuaveReturnType { -// return parseTransaction( -// serializedTransaction, -// ) as ParseTransactionSuaveReturnType -// } - -// function parseSuaveTransaction( -// serializedTransaction: SuaveTransactionSerialized, -// ): TransactionSerializableSuave { -// const transactionArray = toTransactionArray(serializedTransaction) - -// const [ -// chainId, -// nonce, -// gas, -// to, -// value, -// data, -// ExecutionNode, -// ConfidentialComputeRequest, -// ConfidentialComputeResult, -// v, -// r, -// s, -// ] = transactionArray - -// if (transactionArray.length !== 12) { -// throw new InvalidSerializedTransactionError({ -// attributes: { -// chainId, -// nonce, -// gas, -// to, -// value, -// data, -// ExecutionNode, -// ConfidentialComputeRequest, -// ConfidentialComputeResult, -// v, -// r, -// s, -// }, -// serializedTransaction, -// type: 'suave', -// }) -// } - -// const transaction: Partial = { -// chainId: hexToNumber(chainId as Hex), -// } - -// if (isHex(to) && to !== '0x') transaction.to = to -// if (isHex(gas) && gas !== '0x') transaction.gas = hexToBigInt(gas) -// if (isHex(data) && data !== '0x') transaction.data = data -// if (isHex(nonce) && nonce !== '0x') transaction.nonce = hexToNumber(nonce) -// if (isHex(value) && value !== '0x') transaction.value = hexToBigInt(value) -// if (isHex(ExecutionNode)) transaction.ExecutionNode = ExecutionNode -// if (isHex(ConfidentialComputeRequest)) -// transaction.ConfidentialComputeRequest = ConfidentialComputeRequest -// if (isHex(ConfidentialComputeResult)) -// transaction.ConfidentialComputeResult = ConfidentialComputeResult - -// assertTransactionSuave(transaction as TransactionSerializableSuave) - -// return transaction as TransactionSerializableSuave -// } +// import { isAddress, isHex } from '~viem/index.js' +import { + InvalidSerializedTransactionError, + InvalidSerializedTransactionTypeError, +} from '../../errors/transaction.js' +import type { Hex } from '../../types/misc.js' +import { isAddress } from '../../utils/address/isAddress.js' +import { isHex } from '../../utils/data/isHex.js' +import { hexToBigInt, hexToNumber } from '../../utils/encoding/fromHex.js' +import { parseTransaction } from '../../utils/transaction/parseTransaction.js' +import { toTransactionArray } from '../../utils/transaction/parseTransaction.js' +import { + type SuaveTxType, + SuaveTxTypes, + type TransactionSerializableSuave, + type TransactionSerializedSuave, +} from './types.js' + +const safeHexToBigInt = (hex: Hex) => { + if (hex === '0x') return 0n + return hexToBigInt(hex) +} + +const safeHexToNumber = (hex: Hex) => { + if (hex === '0x') return 0 + return hexToNumber(hex) +} + +export const parseSignedComputeRequest = (signedComputeRequest: Hex) => { + const serializedType = signedComputeRequest.slice(0, 4) + if (serializedType !== SuaveTxTypes.ConfidentialRequest) { + throw new InvalidSerializedTransactionTypeError({ + serializedType: serializedType as Hex, + }) + } + const txArray = toTransactionArray(signedComputeRequest) + const [ + [ + nonce, + gasPrice, + gas, + to, + value, + data, + kettleAddress, + confidentialInputsHash, + chainId, + v, + r, + s, + ], + confidentialInputs, + ] = txArray + if (txArray.length !== 2 || txArray[0].length !== 12) { + throw new InvalidSerializedTransactionError({ + attributes: { + nonce, + to, + data, + gas, + kettleAddress, + confidentialInputsHash, + value, + gasPrice, + chainId, + v, + r, + s, + confidentialInputs, + }, + type: '0x43' as SuaveTxType, + serializedTransaction: signedComputeRequest, + }) + } + const ccRequest: Partial = { + nonce: safeHexToNumber(nonce as Hex), + to: to as Hex, + data: data as Hex, + gas: hexToBigInt(gas as Hex), + kettleAddress: kettleAddress as Hex, + confidentialInputs: confidentialInputs as Hex, + value: safeHexToBigInt(value as Hex), + gasPrice: safeHexToBigInt(gasPrice as Hex), + chainId: hexToNumber(chainId as Hex), + v: safeHexToBigInt(v as Hex), + r: r as Hex, + s: s as Hex, + type: '0x43', + } + return ccRequest +} + +/** This type represents the return type of `parseTransactionSuave`. + * + * TType is used to inform the transaction type, which is only known after + * parsing. `SuaveTxType` can be used here type when the transaction + * type isn't yet known. + */ +export type ParseTransactionSuaveReturnType = + TransactionSerializableSuave + +/** Parse a serialized transaction into a SUAVE Transaction object. */ +export function parseTransactionSuave( + serializedTransaction: TransactionSerializedSuave, +): ParseTransactionSuaveReturnType { + const serializedType = serializedTransaction.slice(0, 4) + const parsedTx = + serializedType === SuaveTxTypes.ConfidentialRequest + ? (parseSignedComputeRequest( + serializedTransaction, + ) as ParseTransactionSuaveReturnType<'0x43'>) + : ({ + ...parseTransaction(serializedTransaction), + type: '0x0', + } as ParseTransactionSuaveReturnType<'0x0'>) + + assertTransactionSuave(parsedTx) + return parsedTx +} + +export function assertTransactionSuave( + transaction: TransactionSerializableSuave, +) { + const { + chainId, + gasPrice, + gas, + data, + value, + maxPriorityFeePerGas, + maxFeePerGas, + confidentialInputs, + confidentialInputsHash, + kettleAddress, + to, + r, + s, + v, + } = transaction + if (chainId && chainId <= 0) throw new Error('invalid chain ID') + if (to && !isAddress(to)) throw new Error('invalid to address') + if (!gasPrice) throw new Error('gasPrice is required') + + if (confidentialInputs && !isHex(confidentialInputs)) + throw new Error('invalid confidentialInputs') + + if (kettleAddress && !isHex(kettleAddress)) + throw new Error('invalid kettleAddress') + + if (confidentialInputsHash && !isHex(confidentialInputsHash)) + throw new Error('invalid confidentialInputsHash') + + if (gas && gas <= 0) throw new Error('invalid gas') + + if (data && !isHex(data)) throw new Error('invalid data') + + if (value && value < 0) throw new Error('invalid value') + + if (maxPriorityFeePerGas && maxPriorityFeePerGas < 0) + throw new Error('invalid maxPriorityFeePerGas') + + if (maxFeePerGas && maxFeePerGas < 0) throw new Error('invalid maxFeePerGas') + + if (r && !isHex(r)) throw new Error(`invalid r: ${r}`) + + if (s && !isHex(s)) throw new Error(`invalid s: ${s}`) + + if (v && v <= 0n) throw new Error(`invalid v: ${v}`) +} diff --git a/src/chains/suave/precompiles.ts b/src/chains/suave/precompiles.ts new file mode 100644 index 00000000..1a8d0550 --- /dev/null +++ b/src/chains/suave/precompiles.ts @@ -0,0 +1,24 @@ +import type { Address } from 'abitype' + +// TODO: is it possible to generate this file from the contracts? +export default { + ANYALLOWED: '0xc8df3686b4afb2bb53e60eae97ef043fe03fb829' as Address, + IS_CONFIDENTIAL_ADDR: '0x0000000000000000000000000000000042010000' as Address, + BUILD_ETH_BLOCK: '0x0000000000000000000000000000000042100001' as Address, + CONFIDENTIAL_INPUTS: '0x0000000000000000000000000000000042010001' as Address, + CONFIDENTIAL_RETRIEVE: + '0x0000000000000000000000000000000042020001' as Address, + CONFIDENTIAL_STORE: '0x0000000000000000000000000000000042020000' as Address, + ETHCALL: '0x0000000000000000000000000000000042100003' as Address, + EXTRACT_HINT: '0x0000000000000000000000000000000042100037' as Address, + FETCH_BIDS: '0x0000000000000000000000000000000042030001' as Address, + FILL_MEV_SHARE_BUNDLE: + '0x0000000000000000000000000000000043200001' as Address, + NEW_BID: '0x0000000000000000000000000000000042030000' as Address, + SIGN_ETH_TRANSACTION: '0x0000000000000000000000000000000040100001' as Address, + SIMULATE_BUNDLE: '0x0000000000000000000000000000000042100000' as Address, + SUBMIT_BUNDLE_JSON_RPC: + '0x0000000000000000000000000000000043000001' as Address, + SUBMIT_ETH_BLOCK_BID_TO_RELAY: + '0x0000000000000000000000000000000042100002' as Address, +} diff --git a/src/chains/suave/serializers.ts b/src/chains/suave/serializers.ts index 185ae07e..fd118499 100644 --- a/src/chains/suave/serializers.ts +++ b/src/chains/suave/serializers.ts @@ -1,63 +1,190 @@ -// // Import necessary utilities and types -// import { InvalidAddressError } from '../../errors/address.js' -// import { InvalidChainIdError } from '../../errors/chain.js' -// import type { ChainSerializers } from '../../types/chain.js' -// import type { Signature } from '../../types/misc.js' -// import { isAddress } from '../../utils/address/isAddress.js' -// import { toHex } from '../../utils/encoding/toHex.js' -// import { toRlp } from '../../utils/encoding/toRlp.js' -// import type { -// SuaveTransactionSerializable, -// TransactionSerializedSuave, -// } from './types.js' // Adjust the import path - -// // Define a type for the serialized Suave transaction -// export type TransactionSerializedSuave = string // Adjust the type definition as necessary - -// // Define a function to serialize Suave transactions -// export const serializeTransactionSuave = ( -// transaction: SuaveTransactionSerializable, -// signature?: Signature, -// ): TransactionSerializedSuave => { -// // Extract fields from the transaction -// const { -// chainId, -// nonce, -// gas, -// to, -// value, -// data, -// ExecutionNode, -// ConfidentialComputeRequest, -// ConfidentialComputeResult, -// } = transaction - -// // Serialize the transaction fields into an array -// const serializedTransaction = [ -// toHex(chainId), -// nonce ? toHex(nonce) : '0x', -// gas ? toHex(gas) : '0x', -// to ?? '0x', -// value ? toHex(value) : '0x', -// data ?? '0x', -// ExecutionNode ?? '0x', -// // ... serialize ConfidentialComputeRequest and ConfidentialComputeResult -// ] - -// // Append the signature to the serialized transaction if provided -// if (signature) { -// serializedTransaction.push( -// signature.v === 27n ? '0x' : toHex(1), // yParity -// toHex(signature.r), -// toHex(signature.s), -// ) -// } - -// // Concatenate the serialized transaction array into a single string using RLP encoding -// return toRlp(serializedTransaction) -// } - -// // Define the Suave serializers object +import { InvalidSerializedTransactionTypeError } from '../../index.js' +import type { Hex } from '../../types/misc.js' +import { concatHex } from '../../utils/data/concat.js' +import { numberToHex, toHex } from '../../utils/encoding/toHex.js' +import { toRlp } from '../../utils/encoding/toRlp.js' +import { + InvalidConfidentialRecordError, + InvalidConfidentialRequestError, +} from './errors/transaction.js' +import { + SuaveTxTypes, + type TransactionSerializableSuave, + type TransactionSerializedSuave, +} from './types.js' + +const safeHex = (hex: Hex): Hex => { + if (hex === '0x0' || hex === '0x00') { + return '0x' + } else if (hex.length % 2 !== 0) { + return `0x0${hex.slice(2)}` + } + return hex +} + +/** Serializes a ConfidentialComputeRecord transaction. Conforms to [ConfidentialComputeRequest Spec](https://github.com/flashbots/suave-specs/blob/main/specs/rigil/suave-chain.md?plain=1#L135-L158). +Satisfies `ChainSerializers.transaction` +*/ +export const serializeConfidentialComputeRecord = ( + transaction: TransactionSerializableSuave, +): TransactionSerializedSuave => { + if (transaction.type !== SuaveTxTypes.ConfidentialRecord) { + throw new InvalidSerializedTransactionTypeError({ + serializedType: transaction.type, + }) + } + if (!transaction.kettleAddress) { + throw new InvalidConfidentialRecordError({ missingField: 'kettleAddress' }) + } + + // Extract fields from the original request + const { + nonce, + gas, + gasPrice, + to, + value, + data, + kettleAddress, + confidentialInputsHash, + } = transaction + + if (!confidentialInputsHash) { + throw new InvalidConfidentialRecordError({ + missingField: 'confidentialInputsHash', + }) + } + if (nonce === undefined) { + throw new InvalidConfidentialRecordError({ missingField: 'nonce' }) + } + if (gas === undefined) { + throw new InvalidConfidentialRecordError({ missingField: 'gas' }) + } + if (gasPrice === undefined) { + throw new InvalidConfidentialRecordError({ missingField: 'gasPrice' }) + } + + // Serialize the transaction fields into an array + const preSerializedTransaction: Hex[] = [ + kettleAddress, + confidentialInputsHash, + numberToHex(nonce), + numberToHex(gasPrice), + numberToHex(gas), + to ?? '0x', + value ? numberToHex(value) : '0x', + data ?? '0x', + ].map(safeHex) + + // Concatenate the serialized transaction array into a single string using RLP encoding + return concatHex([ + SuaveTxTypes.ConfidentialRecord, + toRlp(preSerializedTransaction), + ]) as TransactionSerializedSuave +} + +/** RLP serialization for ConfidentialComputeRequest. + * Conforms to [ConfidentialComputeRequest Spec](https://github.com/flashbots/suave-specs/blob/main/specs/rigil/suave-chain.md?plain=1#L164-L180). + */ +export const serializeConfidentialComputeRequest = ( + transaction: TransactionSerializableSuave, +): TransactionSerializedSuave => { + if (transaction.type !== SuaveTxTypes.ConfidentialRequest) { + throw new InvalidSerializedTransactionTypeError({ + serializedType: transaction.type, + }) + } + if (!transaction.confidentialInputs) { + throw new InvalidConfidentialRequestError({ + missingField: 'confidentialInputs', + }) + } + if (!transaction.confidentialInputsHash) { + throw new InvalidConfidentialRequestError({ + missingField: 'confidentialInputsHash', + }) + } + if (!transaction.kettleAddress) { + throw new InvalidConfidentialRequestError({ + missingField: 'kettleAddress', + }) + } + if (transaction.v === undefined) { + throw new InvalidConfidentialRequestError({ + missingField: 'v', + found: transaction.v, + }) + } + if (transaction.r === undefined) { + throw new InvalidConfidentialRequestError({ + missingField: 'r', + found: transaction.r, + }) + } + if (transaction.s === undefined) { + throw new InvalidConfidentialRequestError({ + missingField: 's', + found: transaction.s, + }) + } + if (transaction.nonce === undefined) { + throw new InvalidConfidentialRequestError({ + missingField: 'nonce', + found: transaction.nonce, + }) + } + if (!transaction.gas) { + throw new InvalidConfidentialRequestError({ + missingField: 'gas', + found: transaction.gas, + }) + } + if (!transaction.to) { + throw new InvalidConfidentialRequestError({ + missingField: 'to', + found: transaction.to, + }) + } + + /* This is the final serialization step; what's sent to the JSON-RPC node. */ + const preSerializedTransaction: (Hex | Hex[])[] = [ + [ + numberToHex(transaction.nonce), + toHex(transaction.gasPrice), + toHex(transaction.gas), + transaction.to, + toHex(transaction.value || 0), + transaction.data || '0x', + + transaction.kettleAddress, + transaction.confidentialInputsHash, + + numberToHex(transaction.chainId), + toHex(transaction.v), + transaction.r, + transaction.s, + ].map(safeHex), + safeHex(transaction.confidentialInputs), + ] + return concatHex([ + SuaveTxTypes.ConfidentialRequest, + toRlp(preSerializedTransaction), + ]) as TransactionSerializedSuave +} + +/* The following does not work. It's left here as a reminder of how it typically should be written, + in case we change the signature scheme to match the standard implementation. + - viem has a fixed signature scheme + - ccRequest txs have to serialize as a ccRecord first, have the account sign it, then re-serialize as a ccRequest + - as an alternative to configuring the serializers here, we override sendTransaction and signTransaction in the wallet +*/ +// Define the Suave serializers object // export const serializersSuave = { -// transaction: serializeTransactionSuave, +// transaction: (tx: TransactionSerializableSuave, sig?: Signature) => { +// console.log(`tx: ${tx}`, `sig: ${sig}`) +// if (tx.type === SuaveTxTypes.ConfidentialRequest) { +// return serializeConfidentialComputeRequest(tx as TransactionSerializableSuave, , sig) +// } +// return serializeTransaction(tx, sig) +// }, // } as const satisfies ChainSerializers diff --git a/src/chains/suave/types.ts b/src/chains/suave/types.ts index 3ae224a6..b2e54ad2 100644 --- a/src/chains/suave/types.ts +++ b/src/chains/suave/types.ts @@ -1,110 +1,193 @@ import type { Address } from 'abitype' import type { Block, BlockTag } from '../../types/block.js' -import type { Hex } from '../../types/misc.js' +import type { FeeValuesLegacy } from '../../types/fee.js' +import type { Hash, Hex } from '../../types/misc.js' +import type { RpcTransactionReceipt } from '../../types/rpc.js' import type { - Index, - Quantity, - RpcBlock, - RpcTransaction as RpcTransaction_, - RpcTransactionReceipt, - RpcTransactionRequest as RpcTransactionRequest_, -} from '../../types/rpc.js' -import type { - Transaction as Transaction_, - TransactionBase, + AccessList, + TransactionBase as TransactionBase_, TransactionReceipt, - TransactionRequest as TransactionRequest_, + TransactionRequestBase as TransactionRequestBase_, + TransactionSerializableBase, } from '../../types/transaction.js' +/// CUSTOM OVERRIDES =========================================================== + +export enum SuaveTxTypes { + ConfidentialRecord = '0x42', + ConfidentialRequest = '0x43', + Suave = '0x50', +} + +export type SuaveTxType = '0x0' | `${SuaveTxTypes}` + +type ConfidentialOverrides = { + kettleAddress?: Address +} + +type ConfidentialComputeRequestOverrides = ConfidentialOverrides & { + confidentialInputs?: Hex +} + +type ConfidentialComputeRecordOverrides = ConfidentialOverrides & { + confidentialInputsHash?: Hash +} + export type SuaveBlockOverrides = {} // Add any specific block overrides if necessary for Suave +export type SuaveTransactionReceiptOverrides = + Partial & { + confidentialComputeRequest: ConfidentialComputeRecord | null + confidentialComputeResult: Hex | null + } + +/// BASE ETHEREUM TYPE EXTENSIONS ============================================== + +type FeeValues = FeeValuesLegacy + +type TransactionBase< + TQuantity, + TIndex, + TType, + TPending extends boolean, +> = TransactionBase_ & + FeeValues & { + accessList?: AccessList + chainId: TIndex + type: TType + } + +type TransactionRequestBase = Omit< + TransactionRequestBase_, + 'from' +> & + FeeValues & { + accessList?: AccessList + chainId?: TIndex + type: TType + } + export type SuaveBlock< TIncludeTransactions extends boolean = boolean, TBlockTag extends BlockTag = BlockTag, -> = Block< - bigint, - TIncludeTransactions, - TBlockTag, - SuaveTransaction + TQuantity = bigint, + TIndex = number, +> = Omit< + Block< + TQuantity, + TIncludeTransactions, + TBlockTag, + TransactionSuave< + TBlockTag extends 'pending' ? true : false, + SuaveTxType, + TQuantity, + TIndex + > + >, + 'sealFields' > & SuaveBlockOverrides export type SuaveRpcBlock< TBlockTag extends BlockTag = BlockTag, TIncludeTransactions extends boolean = boolean, -> = RpcBlock & SuaveBlockOverrides - -export type SuaveRpcTransaction = - | (RpcTransaction_ & { - ExecutionNode: Address - ConfidentialComputeRequest: RpcTransaction_ - ConfidentialComputeResult: Hex - IsConfidential?: boolean // Add this line - }) - | RpcTransactionSuave - -export type SuaveRpcTransactionRequest = RpcTransactionRequest_ & { - ExecutionNode?: Address - IsConfidential?: boolean -} +> = SuaveBlock -export type SuaveTransaction = Transaction_< - bigint, - number, - TPending -> & { - ExecutionNode: Address - ConfidentialComputeRequest: Transaction_ - ConfidentialComputeResult: Hex +export type TransactionSuave< + TPending extends boolean = boolean, + TType extends SuaveTxType = SuaveTxType, + TQuantity = bigint, + TIndex = number, +> = TransactionBase & { + requestRecord: ConfidentialComputeRecord + confidentialComputeResult: Hex + type: TType } -export type SuaveTransactionReceiptOverrides = { - ExecutionNode: Address | null - ConfidentialComputeRequest: Transaction_ | null // TODO : modify to regular transaction - ConfidentialComputeResult: Hex | null -} +/** The type that interfaces with RPC endpoints. + * Used for endpoints that returns transactions, such as `eth_getTransactionByHash`. + * Also used when parsing transactions from this client before sending to RPC endpoints + * and/or signing transactions. + */ +export type RpcTransactionSuave< + TType extends SuaveTxType, + TPending extends boolean = boolean, +> = TransactionSuave -export type SuaveTransactionReceipt = TransactionReceipt & - SuaveTransactionReceiptOverrides +export type ConfidentialComputeRecord< + TPending extends boolean = false, + TQuantity = bigint, + TIndex = number, +> = Omit< + Omit< + Omit< + Omit< + TransactionBase< + TQuantity, + TIndex, + SuaveTxTypes.ConfidentialRecord, + TPending + >, + 'blockHash' + >, + 'transactionIndex' + >, + 'blockNumber' + >, + 'from' +> & + ConfidentialComputeRecordOverrides -export type SuaveTransactionRequest = TransactionRequest_ & { - ExecutionNode?: Address - IsConfidential?: boolean -} +export type ConfidentialComputeRecordRpc = + ConfidentialComputeRecord -type RpcTransactionSuave = TransactionBase< - Quantity, - Index, - TPending -> & { - ExecutionNode: Address - ConfidentialComputeRequest: RpcTransaction_ - ConfidentialComputeResult: Hex - IsConfidential?: boolean -} +export type TransactionRequestSuave< + TQuantity = bigint, + TIndex = number, + TType extends SuaveTxType = SuaveTxTypes.ConfidentialRequest | '0x0', +> = TransactionRequestBase & + ConfidentialComputeRequestOverrides & { + accessList?: AccessList + type: TType + from?: Address + } -export type SuaveRpcTransactionReceipt = RpcTransactionReceipt & { - ExecutionNode: Address - ConfidentialComputeRequest: RpcTransaction_ - ConfidentialComputeResult: Hex -} +export type RpcTransactionRequestSuave = TransactionRequestSuave + +export type TransactionReceiptSuave = TransactionReceipt & + SuaveTransactionReceiptOverrides -// Define a type for serializable Suave transactions -export type SuaveTransactionSerializable = { - chainId: bigint - nonce: bigint - gas: bigint - to: Address - value: bigint - data: Hex - ExecutionNode: Address - ConfidentialComputeRequest: { - ExecutionNode: Address - Wrapped: { toHex: () => Hex } // Adjust this type as necessary - // ... other fields +export type RpcTransactionReceiptSuave = RpcTransactionReceipt & {} + +/// Original 2930 spec sans `type: 'eip2930'` +export type TransactionSerializableEIP2930< + TQuantity = bigint, + TIndex = number, +> = TransactionSerializableBase & + FeeValues & { + accessList?: AccessList + chainId: TIndex + yParity?: TIndex + } + +export type TransactionSerializableSuave< + TQuantity = bigint, + TIndex = number, + TType = SuaveTxType, +> = TransactionSerializableEIP2930 & + ConfidentialComputeRecordOverrides & + ConfidentialComputeRequestOverrides & { + signedComputeRecord?: Hex + type: TType } - ConfidentialComputeResult: Hex -} -// Define a type for serialized Suave transactions -export type TransactionSerializedSuave = string +/** Required format for a serialized SUAVE transaction. + * + * By default, this type can be used to represent any serialized SUAVE transaction. + * + * To restrict the type to a specific SUAVE transaction type, use your specific type as + * the generic TType argument, e.g. `TransactionSerializedSuave<'0x43'>`. + */ +export type TransactionSerializedSuave< + TType extends SuaveTxType = SuaveTxType, +> = `${TType}${string}` diff --git a/src/chains/suave/wallet.ts b/src/chains/suave/wallet.ts new file mode 100644 index 00000000..ce20f0e1 --- /dev/null +++ b/src/chains/suave/wallet.ts @@ -0,0 +1,106 @@ +import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' +import { sign } from '../../accounts/utils/sign.js' +import { + type Chain, + type PrivateKeyAccount, + type Transport, + type WalletClient, + createWalletClient, + keccak256, +} from '../../index.js' +import { type Hex } from '../../types/misc.js' +import { suaveRigil } from '../index.js' +import { + serializeConfidentialComputeRecord, + serializeConfidentialComputeRequest, +} from './serializers.js' +import { + SuaveTxTypes, + type TransactionRequestSuave, + type TransactionSerializableSuave, +} from './types.js' + +async function signConfidentialComputeRecord( + transaction: TransactionSerializableSuave, + privateKey: Hex, +): Promise { + if (transaction.type !== SuaveTxTypes.ConfidentialRecord) { + throw new Error( + `transaction.type must be ConfidentialRecord (${SuaveTxTypes.ConfidentialRecord})`, + ) + } + const serialized = serializeConfidentialComputeRecord(transaction) + const { r, s, v } = await sign({ hash: keccak256(serialized), privateKey }) + const signature = { + r, + s, + v: v === 27n ? 0n : 1n, + } + return { + ...transaction, + ...signature, + } +} + +export function getSuaveWallet< + TTransport extends Transport, + TChain extends Chain, +>( + params: { transport: TTransport; chain: TChain }, + privateKey: Hex, +): WalletClient< + TTransport, + TChain, + PrivateKeyAccount // TODO: generalize account types (required to make metamask transport work) +> { + return createWalletClient({ + account: privateKey ? privateKeyToAccount(privateKey) : undefined, + transport: params.transport, + chain: params.chain, + }).extend((client) => ({ + async sendTransaction(txRequest: TransactionRequestSuave) { + const preparedTx = await client.prepareTransactionRequest( + txRequest as any, + ) + const payload: TransactionRequestSuave = { + ...txRequest, + from: preparedTx.from, + nonce: preparedTx.nonce, + gas: txRequest.gas ?? preparedTx.gas, + gasPrice: txRequest.gasPrice ?? preparedTx.gasPrice, + chainId: txRequest.chainId ?? suaveRigil.id, + } + + const signedTx = await this.signTransaction(payload) + return client.request({ + method: 'eth_sendRawTransaction', + params: [signedTx], + }) + }, + async signTransaction(txRequest: TransactionRequestSuave) { + if (txRequest.type === SuaveTxTypes.ConfidentialRequest) { + const confidentialInputs = txRequest.confidentialInputs || '0x' + const presignTx = { + ...txRequest, + type: SuaveTxTypes.ConfidentialRecord, + confidentialInputsHash: keccak256(confidentialInputs), + chainId: txRequest.chainId ?? suaveRigil.id, + } + const { r, s, v } = await signConfidentialComputeRecord( + presignTx, + privateKey, + ) + return serializeConfidentialComputeRequest({ + ...presignTx, + confidentialInputs, + type: SuaveTxTypes.ConfidentialRequest, + r, + s, + v, + }) + } else { + return await client.account.signTransaction(txRequest as any) + } + }, + })) +}