From fa23389fea8ce0faed99a0d85a0ca48c653e2766 Mon Sep 17 00:00:00 2001 From: brock smedley <2791467+zeroXbrock@users.noreply.github.com> Date: Wed, 22 Nov 2023 15:29:36 -0800 Subject: [PATCH] suave (#4) * remove CHANGELOG.md symlink * add transactionRequest test, update SuaveTransactionRequest definition * add tx formatter * disable anvil calls in vitest setup (run with SKIP_GLOBAL_SETUP=true) * add test:e2e in playgrounds/bun/ * update chain id, add local devnet config * working SuaveTransactionRequest impl * infer isConfidential from confidentialInputs * infer tx type in SuaveTransactionRequest * add getTransaction[Receipt] to playground test * revise SuaveTransaction formatter/type * harden SuaveTransaction[Request] formatter/type * add SuaveTransaction serialization constructs * refactor type defs; make rpc types generic, deserialized types restrictive * experimental serializer * fix rlp signature bugs; working CCR transactionRequests * push lint fixes * cleanup, remove debug console logs * compartmentalize wallet action overrides with new fn: getSuaveWallet * support all tx types * remove bad 'from' addrs * lint * fix bad imports * make privatekey optional in getSuaveWallet * lint * fix some type-related bugs + formatter tests * remove junk comments * add block formatter tests * remove unused fields in block formatter * add tx receipt formatter test * clean up duplicate logic in sendTransaction override, add examples in playground * wait for fund tx to land in playground example * lint * re-enable anvil backend for tests * cleanup junk comments, remove unused arg * stricter input requirement for serializeConfidentialComputeRequest * add links to spec in doc-comments * use idiomatic errors in tx serializers * add parsers in parsers.ts, tests * add parseTransactionSuave; covers all supported tx types; +test * chore: forge init * forge install: forge-std v1.7.1 * fix pre-signature ccRecord serialization scheme * replace 'executionNode' with 'kettleAddress', remove invalid instances from SuaveTx * add deployment example in playground * clean up & fix bugs in serializer/signer - working ccRequest & contract deployment - made example useless so it will compile - will fix this next * cleanup * add deploy script for mev-share contract, clean up - deploy mev-share suave contract w/ forge - convert index-style imports to tiny mod imports - fix build errors from new type changes * tighten up types * clean up deploy script * add working mev-share bid example in playgrounds/bun * comment out old examples; saving for later isolated examples * make chainId optional in TransactionRequestSuave * move getSuaveWallet to suaveRigil.newWallet * move playground code to examples/suave/ * lint * delete old code comments * insert suave chainId if missing * add 'suaveRigil.newPublicClient' * chore: format --------- Co-authored-by: zeroXbrock --- .gitmodules | 6 +- CHANGELOG.md | 1 - biome.json | 3 +- bun.lockb | Bin 331367 -> 333171 bytes examples/suave/.env.example | 4 + examples/suave/README.md | 42 ++ examples/suave/bids/index.ts | 76 +++ .../contracts/.github/workflows/test.yml | 34 ++ examples/suave/contracts/.gitignore | 15 + examples/suave/contracts/README.md | 66 +++ examples/suave/contracts/foundry.toml | 9 + examples/suave/contracts/lib/suave-geth | 1 + examples/suave/contracts/script/Counter.s.sol | 12 + examples/suave/contracts/script/Deploy.s.sol | 15 + .../contracts/src/ConfidentialWithLogs.sol | 53 ++ examples/suave/index.ts | 140 +++++ examples/suave/package.json | 13 + examples/suave/tsconfig.json | 23 + package.json | 2 + playgrounds/bun/.env.example | 0 playgrounds/bun/README.md | 15 - playgrounds/bun/index.ts | 40 -- src/chains/definitions/suaveRigil.ts | 10 +- src/chains/suave/errors/transaction.ts | 28 + src/chains/suave/formatters.test.ts | 554 +++++++++++++++--- src/chains/suave/formatters.ts | 183 +++--- src/chains/suave/parsers.test.ts | 137 +++-- src/chains/suave/parsers.ts | 264 ++++++--- src/chains/suave/precompiles.ts | 24 + src/chains/suave/serializers.ts | 249 ++++++-- src/chains/suave/types.ts | 249 +++++--- src/chains/suave/wallet.ts | 106 ++++ 32 files changed, 1848 insertions(+), 526 deletions(-) delete mode 120000 CHANGELOG.md create mode 100644 examples/suave/.env.example create mode 100644 examples/suave/README.md create mode 100644 examples/suave/bids/index.ts create mode 100644 examples/suave/contracts/.github/workflows/test.yml create mode 100644 examples/suave/contracts/.gitignore create mode 100644 examples/suave/contracts/README.md create mode 100644 examples/suave/contracts/foundry.toml create mode 160000 examples/suave/contracts/lib/suave-geth create mode 100644 examples/suave/contracts/script/Counter.s.sol create mode 100644 examples/suave/contracts/script/Deploy.s.sol create mode 100644 examples/suave/contracts/src/ConfidentialWithLogs.sol create mode 100644 examples/suave/index.ts create mode 100644 examples/suave/package.json create mode 100644 examples/suave/tsconfig.json delete mode 100644 playgrounds/bun/.env.example delete mode 100644 playgrounds/bun/README.md delete mode 100644 playgrounds/bun/index.ts create mode 100644 src/chains/suave/errors/transaction.ts create mode 100644 src/chains/suave/precompiles.ts create mode 100644 src/chains/suave/wallet.ts 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 235c1a926356ccf6922f7bd14cff4f7d0f701db9..02c39a22abdb347c8e2d4164ba11dd12ca38fa2a 100755 GIT binary patch delta 57824 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 TooFj-C^toRh~HBz$2t8M#X!%m delta 70832 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 JOSL;){{=B)4=Mlv 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) + } + }, + })) +}