diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..546787f --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ + +SUAVEX_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +SUAVEX_RPC_URL=http://localhost:8545 + +deploy-suavex-contract: + forge create \ + --rpc-url $(SUAVEX_RPC_URL) \ + --private-key $(SUAVEX_PRIVATE_KEY) \ + test/protocols/Builder/Session.t.sol:Example --json | jq -r ".deployedTo" diff --git a/foundry.toml b/foundry.toml index 530a8f9..a274bb9 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,6 +1,7 @@ [profile.default] runs = 10_000 solc_version = "0.8.23" +fs_permissions = [{ access = "read", path = "./test" }] ast = true [profile.suave] whitelist = ["*"] diff --git a/src/Transactions.sol b/src/Transactions.sol index 0e9ed77..810acd8 100644 --- a/src/Transactions.sol +++ b/src/Transactions.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.13; import "./utils/RLPWriter.sol"; import "./suavelib/Suave.sol"; import "Solidity-RLP/RLPReader.sol"; +import "solady/src/utils/LibString.sol"; /// @notice Transactions is a library with utilities to encode, decode and sign Ethereum transactions. library Transactions { @@ -433,4 +434,46 @@ library Transactions { v := byte(0, mload(add(signature, 0x60))) } } + + function encodeJSON(EIP155 memory txn) internal pure returns (bytes memory) { + // encode transaction in json + bytes memory txnEncoded; + + // dynamic fields + if (txn.data.length != 0) { + txnEncoded = abi.encodePacked(txnEncoded, '"input":"', LibString.toHexString(txn.data), '",'); + } else { + txnEncoded = abi.encodePacked(txnEncoded, '"input":"0x",'); + } + if (txn.to != address(0)) { + txnEncoded = abi.encodePacked(txnEncoded, '"to":"', LibString.toHexString(txn.to), '",'); + } else { + txnEncoded = abi.encodePacked(txnEncoded, '"to":null,'); + } + + // fixed fields + txnEncoded = abi.encodePacked( + txnEncoded, + '"gas":"', + LibString.toMinimalHexString(txn.gas), + '","gasPrice":"', + LibString.toMinimalHexString(txn.gasPrice), + '","nonce":"', + LibString.toMinimalHexString(txn.nonce), + '","value":"', + LibString.toMinimalHexString(txn.value), + '","chainId":"', + LibString.toMinimalHexString(txn.chainId), + '","r":"', + LibString.toHexString(abi.encodePacked(txn.r)), + '","s":"', + LibString.toHexString(abi.encodePacked(txn.s)), + '",' + ); + + txnEncoded = abi.encodePacked(txnEncoded, '"v":"', LibString.toMinimalHexString(txn.v), '"'); + txnEncoded = abi.encodePacked("{", txnEncoded, "}"); + + return txnEncoded; + } } diff --git a/src/protocols/Builder/Session.sol b/src/protocols/Builder/Session.sol new file mode 100644 index 0000000..9256b23 --- /dev/null +++ b/src/protocols/Builder/Session.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "../../suavelib/Suave.sol"; +import "../../Transactions.sol"; +import "solady/src/utils/LibString.sol"; +import {Types} from "./Types.sol"; +import "solady/src/utils/JSONParserLib.sol"; +import "../../utils/HexStrings.sol"; + +contract Session is Test { + using JSONParserLib for *; + + string url; + string session; + + constructor(string memory _url) { + url = _url; + } + + function start(Types.BuildBlockArgs memory args) public { + bytes memory encoded = Types.encodeBuildBlockArgs(args); + JSONParserLib.Item memory output = callImpl("newSession", encoded); + session = output.value(); + } + + function addTransaction(Transactions.EIP155 memory txn) public returns (Types.SimulateTransactionResult memory) { + bytes memory encoded = Transactions.encodeJSON(txn); + + bytes memory input = abi.encodePacked(session, ",", encoded); + JSONParserLib.Item memory output = callImpl("addTransaction", input); + + Types.SimulateTransactionResult memory result = Types.decodeSimulateTransactionResult(output.value()); + return result; + } + + function buildBlock() public { + bytes memory input = abi.encodePacked(session); + callImpl("buildBlock", input); + } + + function bid(string memory blsPubKey) public returns (JSONParserLib.Item memory) { + bytes memory input = abi.encodePacked(session, ',"0x', blsPubKey, '"'); + JSONParserLib.Item memory output = callImpl("bid", input); + + console.log("-- bid output --"); + console.log(output.value()); + + // retrieve the root to sign + bytes memory root = HexStrings.fromHexString(HexStrings.stripQuotesAndPrefix(output.at('"root"').value())); + } + + function callImpl(string memory method, bytes memory args) internal returns (JSONParserLib.Item memory) { + Suave.HttpRequest memory request; + request.method = "POST"; + request.url = url; + request.headers = new string[](1); + request.headers[0] = "Content-Type: application/json"; + + bytes memory body = + abi.encodePacked('{"jsonrpc":"2.0","method":"suavex_', method, '","params":[', args, '],"id":1}'); + request.body = body; + + console.log(string(body)); + + bytes memory output = Suave.doHTTPRequest(request); + + console.log(string(output)); + + JSONParserLib.Item memory item = string(output).parse(); + return item.at('"result"'); + } +} diff --git a/src/protocols/Builder/Types.sol b/src/protocols/Builder/Types.sol new file mode 100644 index 0000000..5a3c8b4 --- /dev/null +++ b/src/protocols/Builder/Types.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.13; + +import "../../suavelib/Suave.sol"; +import "../../Transactions.sol"; +import "solady/src/utils/LibString.sol"; +import "solady/src/utils/JSONParserLib.sol"; + +library Types { + using JSONParserLib for *; + + struct SimulateTransactionResult { + uint64 egp; + SimulatedLog[] logs; + bool success; + string error; + } + + struct SimulatedLog { + bytes data; + address addr; + bytes32[] topics; + } + + struct BuildBlockArgs { + uint64 slot; + bytes proposerPubkey; + bytes32 parent; + uint64 timestamp; + address feeRecipient; + uint64 gasLimit; + bytes32 random; + Withdrawal[] withdrawals; + bytes extra; + bytes32 beaconRoot; + bool fillPending; + } + + struct Withdrawal { + uint64 index; + uint64 validator; + address Address; + uint64 amount; + } + + // encodeBuildBlockArgs encodes BuildBlockArgs to json + function encodeBuildBlockArgs(BuildBlockArgs memory args) internal returns (bytes memory) { + bytes memory body = abi.encodePacked( + '{"slot":"', + LibString.toMinimalHexString(args.slot), + '","proposerPubkey":"', + LibString.toHexString(args.proposerPubkey), + '"', + ',"parent":"', + LibString.toHexString(abi.encodePacked(args.parent)), + '"', + ',"timestamp":"', + LibString.toHexString(args.timestamp) + ); + + body = abi.encodePacked( + body, + '","feeRecipient":"', + LibString.toHexStringChecksummed(args.feeRecipient), + '"', + ',"gasLimit":"', + LibString.toHexString(args.gasLimit), + '","random":"', + LibString.toHexString(abi.encodePacked(args.random)), + '"' + ); + + body = abi.encodePacked( + body, + ',"withdrawals":', + encodeWithdrawals(args.withdrawals), + ',"extra":"', + LibString.toHexString(args.extra), + '"', + ',"beaconRoot":"', + LibString.toHexString(abi.encodePacked(args.beaconRoot)), + '"', + ',"fillPending":', + args.fillPending ? "true" : "false", + "}" + ); + return body; + } + + // encodeWithdrawals encodes Withdrawal array to json + function encodeWithdrawals(Withdrawal[] memory withdrawals) internal returns (bytes memory) { + bytes memory result = abi.encodePacked("["); + for (uint64 i = 0; i < withdrawals.length; i++) { + result = abi.encodePacked(result, i > 0 ? "," : "", encodeWithdrawal(withdrawals[i])); + } + return abi.encodePacked(result, "]"); + } + + // encodeWithdrawal encodes Withdrawal to json + function encodeWithdrawal(Withdrawal memory withdrawal) internal returns (bytes memory) { + return abi.encodePacked( + '{"index":', + LibString.toHexString(withdrawal.index), + ',"validator":', + LibString.toHexString(withdrawal.validator), + ',"Address":"', + LibString.toHexStringChecksummed(withdrawal.Address), + '"', + ',"amount":', + LibString.toHexString(withdrawal.amount), + "}" + ); + } + + function decodeSimulateTransactionResult(string memory input) + internal + returns (SimulateTransactionResult memory result) + { + JSONParserLib.Item memory item = input.parse(); + return decodeSimulateTransactionResult(item); + } + + function decodeSimulatedLog(JSONParserLib.Item memory item) internal returns (SimulatedLog memory log) { + log.data = fromHexString(_stripQuotesAndPrefix(item.at('"data"').value())); + log.addr = bytesToAddress(fromHexString(_stripQuotesAndPrefix(item.at('"addr"').value()))); + + JSONParserLib.Item[] memory topics = item.at('"topics"').children(); + log.topics = new bytes32[](topics.length); + for (uint64 i = 0; i < topics.length; i++) { + log.topics[i] = bytesToBytes32(fromHexString(_stripQuotesAndPrefix(topics[i].value()))); + } + } + + function decodeSimulateTransactionResult(JSONParserLib.Item memory item) + internal + returns (SimulateTransactionResult memory result) + { + if (compareStrings(item.at('"success"').value(), "true")) { + result.success = true; + } else { + result.success = false; + result.error = trimQuotes(item.at('"error"').value()); + } + + // decode logs + JSONParserLib.Item[] memory logs = item.at('"logs"').children(); + result.logs = new SimulatedLog[](logs.length); + for (uint64 i = 0; i < logs.length; i++) { + result.logs[i] = decodeSimulatedLog(logs[i]); + } + } + + function compareStrings(string memory a, string memory b) internal pure returns (bool) { + // Check if the lengths of the strings are the same + if (bytes(a).length != bytes(b).length) { + return false; + } else { + // Compare each character of the strings + for (uint256 i = 0; i < bytes(a).length; i++) { + if (bytes(a)[i] != bytes(b)[i]) { + return false; + } + } + return true; + } + } + + function trimQuotes(string memory input) private pure returns (string memory) { + bytes memory inputBytes = bytes(input); + require( + inputBytes.length >= 2 && inputBytes[0] == '"' && inputBytes[inputBytes.length - 1] == '"', "Invalid input" + ); + + bytes memory result = new bytes(inputBytes.length - 2); + + for (uint256 i = 1; i < inputBytes.length - 1; i++) { + result[i - 1] = inputBytes[i]; + } + + return string(result); + } + + function _fromHexChar(uint8 c) internal pure returns (uint8) { + if (bytes1(c) >= bytes1("0") && bytes1(c) <= bytes1("9")) { + return c - uint8(bytes1("0")); + } + if (bytes1(c) >= bytes1("a") && bytes1(c) <= bytes1("f")) { + return 10 + c - uint8(bytes1("a")); + } + if (bytes1(c) >= bytes1("A") && bytes1(c) <= bytes1("F")) { + return 10 + c - uint8(bytes1("A")); + } + revert("fail"); + } + + function _stripQuotesAndPrefix(string memory s) internal pure returns (string memory) { + bytes memory strBytes = bytes(s); + bytes memory result = new bytes(strBytes.length - 4); + for (uint256 i = 3; i < strBytes.length - 1; i++) { + result[i - 3] = strBytes[i]; + } + return string(result); + } + + // Convert an hexadecimal string to raw bytes + function fromHexString(string memory s) internal pure returns (bytes memory) { + bytes memory ss = bytes(s); + require(ss.length % 2 == 0); // length must be even + bytes memory r = new bytes(ss.length / 2); + for (uint256 i = 0; i < ss.length / 2; ++i) { + r[i] = bytes1(_fromHexChar(uint8(ss[2 * i])) * 16 + _fromHexChar(uint8(ss[2 * i + 1]))); + } + return r; + } + + function bytesToAddress(bytes memory data) public pure returns (address) { + // Ensure data length is at least 20 bytes (address length) + require(data.length >= 20, "Invalid data length"); + + address addr; + // Convert bytes to address + assembly { + addr := mload(add(data, 20)) + } + return addr; + } + + function bytesToBytes32(bytes memory data) public pure returns (bytes32) { + require(data.length >= 32, "Data length must be at least 32 bytes"); + + bytes32 result; + assembly { + // Copy 32 bytes from data to result + result := mload(add(data, 32)) + } + return result; + } +} diff --git a/src/utils/HexStrings.sol b/src/utils/HexStrings.sol index 3ee9a1d..b14dc65 100644 --- a/src/utils/HexStrings.sol +++ b/src/utils/HexStrings.sol @@ -26,4 +26,13 @@ library HexStrings { } return r; } + + function stripQuotesAndPrefix(string memory s) internal pure returns (string memory) { + bytes memory strBytes = bytes(s); + bytes memory result = new bytes(strBytes.length - 4); + for (uint256 i = 3; i < strBytes.length - 1; i++) { + result[i - 3] = strBytes[i]; + } + return string(result); + } } diff --git a/test/Fixtures.sol b/test/Fixtures.sol new file mode 100644 index 0000000..beba10b --- /dev/null +++ b/test/Fixtures.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.13; + +interface vmSafe { + function readFile(string calldata path) external view returns (string memory data); +} + +library Fixtures { + vmSafe constant vm = vmSafe(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + function readFixture(string memory path) internal view returns (string memory) { + string memory fullPath = string.concat("./test/fixtures/", path); + return vm.readFile(fullPath); + } + + function validate(string memory path, string memory value) internal view { + string memory data = readFixture(path); + if (keccak256(abi.encodePacked(data)) != keccak256(abi.encodePacked(value))) { + string memory revertMsg = string.concat("Fixtures.validate: expected: ", data, " != ", value); + revert(revertMsg); + } + } +} diff --git a/test/Transactions.t.sol b/test/Transactions.t.sol index 8928b42..2dec288 100644 --- a/test/Transactions.t.sol +++ b/test/Transactions.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "src/Transactions.sol"; import "src/Test.sol"; +import "./Fixtures.sol"; contract TestTransactions is Test, SuaveEnabled { using Transactions for *; @@ -27,6 +28,9 @@ contract TestTransactions is Test, SuaveEnabled { ); _testEIP155Transaction(txnWithToAddress, expected); + bytes memory txnJSON = Transactions.encodeJSON(txnWithToAddress); + Fixtures.validate("txn_155_contract_call.json", string(txnJSON)); + Transactions.EIP155 memory txnWithoutToAddress = Transactions.EIP155({ to: address(0), gas: 50000, @@ -44,6 +48,9 @@ contract TestTransactions is Test, SuaveEnabled { hex"f84b010a82c350800a021ba0754a33a9c37cfcf61cd61939fd93f5fe194b7d1ee6ef07490e8c880f3bd0d87da0715bd50fa2c24e2ce0ea595025a44a39ac238558882f9f07dd885ddc51839419" ); _testEIP155Transaction(txnWithoutToAddress, expected); + + txnJSON = Transactions.encodeJSON(txnWithoutToAddress); + Fixtures.validate("txn_155_contract_creation.json", string(txnJSON)); } function testEIP1559TransactionRLPEncoding() public { @@ -56,7 +63,7 @@ contract TestTransactions is Test, SuaveEnabled { nonce: 38, data: abi.encodePacked( hex"a9059cbb00000000000000000000000061b7b515c1ec603cf21463bcac992b60fd610ca900000000000000000000000000000000000000000000002dbf877cf6ec677800" - ), + ), chainId: 1, accessList: bytes(""), v: 0, @@ -79,7 +86,7 @@ contract TestTransactions is Test, SuaveEnabled { nonce: 38, data: abi.encodePacked( hex"a9059cbb00000000000000000000000061b7b515c1ec603cf21463bcac992b60fd610ca900000000000000000000000000000000000000000000002dbf877cf6ec677800" - ), + ), chainId: 1, accessList: bytes(""), v: 0, @@ -128,7 +135,7 @@ contract TestTransactions is Test, SuaveEnabled { nonce: 38, data: abi.encodePacked( hex"a9059cbb00000000000000000000000061b7b515c1ec603cf21463bcac992b60fd610ca900000000000000000000000000000000000000000000002dbf877cf6ec677800" - ), + ), chainId: 1, accessList: bytes("") }); diff --git a/test/fixtures/suave_builder_builderArgs.json b/test/fixtures/suave_builder_builderArgs.json new file mode 100644 index 0000000..be56a6c --- /dev/null +++ b/test/fixtures/suave_builder_builderArgs.json @@ -0,0 +1 @@ +{"slot":"0x1","proposerPubkey":"0x1234","parent":"0x5678000000000000000000000000000000000000000000000000000000000000","timestamp":"0x7b","feeRecipient":"0x0000000000000000000000000000000000001234","gasLimit":"0x7b","random":"0x1234000000000000000000000000000000000000000000000000000000000000","withdrawals":[],"extra":"0x1234","beaconRoot":"0x1234000000000000000000000000000000000000000000000000000000000000","fillPending":true} \ No newline at end of file diff --git a/test/fixtures/suave_builder_simulateTransactionResult_failed.json b/test/fixtures/suave_builder_simulateTransactionResult_failed.json new file mode 100644 index 0000000..b0bb8a0 --- /dev/null +++ b/test/fixtures/suave_builder_simulateTransactionResult_failed.json @@ -0,0 +1 @@ +{"egp":0,"logs":[],"success":false,"error":"some error"} \ No newline at end of file diff --git a/test/fixtures/suave_builder_simulateTransactionResult_success.json b/test/fixtures/suave_builder_simulateTransactionResult_success.json new file mode 100644 index 0000000..8b7a77d --- /dev/null +++ b/test/fixtures/suave_builder_simulateTransactionResult_success.json @@ -0,0 +1 @@ +{"egp":"0x0","logs":[{"data":"0x00000000000000000000000000000000000000000000000000000000000004d2","addr":"0x5fbdb2315678afecb367f032d93f642f64180aa3","topics":["0x379340f64b65a8890c7ea4f6d86d2359beaf41080f36a7ea64b78a2c06eee3f0"]}],"success":true,"error":""} \ No newline at end of file diff --git a/test/fixtures/txn_155_contract_call.json b/test/fixtures/txn_155_contract_call.json new file mode 100644 index 0000000..9ec1a56 --- /dev/null +++ b/test/fixtures/txn_155_contract_call.json @@ -0,0 +1 @@ +{"input":"0x","to":"0x095e7baea6a6c7c4c2dfeb977efac326af552d87","gas":"0xc350","gasPrice":"0xa","nonce":"0x0","value":"0xa","chainId":"0x0","r":"0x9bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094f","s":"0x8a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b1","v":"0x1b"} \ No newline at end of file diff --git a/test/fixtures/txn_155_contract_creation.json b/test/fixtures/txn_155_contract_creation.json new file mode 100644 index 0000000..24e6c7b --- /dev/null +++ b/test/fixtures/txn_155_contract_creation.json @@ -0,0 +1 @@ +{"input":"0x02","to":null,"gas":"0xc350","gasPrice":"0xa","nonce":"0x1","value":"0xa","chainId":"0x0","r":"0x754a33a9c37cfcf61cd61939fd93f5fe194b7d1ee6ef07490e8c880f3bd0d87d","s":"0x715bd50fa2c24e2ce0ea595025a44a39ac238558882f9f07dd885ddc51839419","v":"0x1b"} \ No newline at end of file diff --git a/test/protocols/Builder/Session.t.sol b/test/protocols/Builder/Session.t.sol new file mode 100644 index 0000000..d15ed87 --- /dev/null +++ b/test/protocols/Builder/Session.t.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "src/protocols/Builder/Session.sol"; +import {Types} from "src/protocols/Builder/Types.sol"; +import "src/suavelib/Suave.sol"; +import "../../Fixtures.sol"; +import "src/Test.sol"; +import "src/Transactions.sol"; + +contract Example { + event SomeEvent(uint256 value); + + function get(uint256 value) public { + emit SomeEvent(value); + } +} + +contract SuaveBuilderSessionTest is Test, SuaveEnabled { + string constant signingKey = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + string constant execNodeEndpoint = "http://localhost:8545"; + + function getBlockBuildArgs() public returns (Types.BuildBlockArgs memory) { + Types.BuildBlockArgs memory args; + args.slot = 1; + args.proposerPubkey = hex"1234"; + args.parent = hex""; // root is empty, take the latest header + args.timestamp = 123; + args.feeRecipient = address(0x1234); + args.gasLimit = 123; + args.random = hex"1234"; + args.extra = hex"1234"; + args.beaconRoot = hex"1234"; + args.fillPending = true; + + return args; + } + + function getValidTxn() public returns (Transactions.EIP155 memory) { + bytes memory targetCall = abi.encodeWithSignature("get(uint256)", 1234); + + Transactions.EIP155Request memory txnrequest = Transactions.EIP155Request({ + to: address(0x5FbDB2315678afecb367f032d93F642f64180aa3), + gas: 1000000, + gasPrice: 875000000, + value: 0, + nonce: 1, + data: targetCall, + chainId: 1337 + }); + + Transactions.EIP155 memory response = Transactions.signTxn(txnrequest, signingKey); + return response; + } + + function testBuilderAPI_AddTransaction() public { + Types.BuildBlockArgs memory args = getBlockBuildArgs(); + + Session session = new Session(getBuilderSessionURL()); + session.start(args); + + // call the "Example" contract + Transactions.EIP155 memory response = getValidTxn(); + Types.SimulateTransactionResult memory result = session.addTransaction(response); + + assertEq(result.success, true); + assertEq(result.logs.length, 1); + assertEq(result.logs[0].addr, 0x5FbDB2315678afecb367f032d93F642f64180aa3); + assertEq(result.logs[0].topics[0], keccak256("SomeEvent(uint256)")); + assertEq(result.logs[0].data, hex"00000000000000000000000000000000000000000000000000000000000004d2"); + + // Try to send the same transaction again in the session should fail because + // the nonce is already used + result = session.addTransaction(response); + + assertEq(result.success, false); + assertNotEq(bytes(result.error).length, 0); + } + + function testBuilderAPI_BuildBlock() public { + Types.BuildBlockArgs memory args = getBlockBuildArgs(); + + Session session = new Session(getBuilderSessionURL()); + session.start(args); + + // send a valid transaction + Transactions.EIP155 memory response = getValidTxn(); + Types.SimulateTransactionResult memory result = session.addTransaction(response); + + // build the block + session.buildBlock(); + } + + function testBuilderAPI_BidBuiltBlock() public { + // pair of bls private and public keys + string memory blsPrivKey = "68a84428e388a5de81fa54f6f91a34d28f09692262c0ee4da81935a4e832ae19"; + string memory blsPubKey = + "b6b973370f9684a2bc0b89f873b772b01269277196e84b69fe8ebad8908e777c09cdfad9d4a2f849e12ecd12ba9dce20"; + + Types.BuildBlockArgs memory args = getBlockBuildArgs(); + + Session session = new Session(getBuilderSessionURL()); + session.start(args); + + // send a valid transaction + Transactions.EIP155 memory response = getValidTxn(); + Types.SimulateTransactionResult memory result = session.addTransaction(response); + + // build the block + session.buildBlock(); + session.bid(blsPubKey); + } + + function getBuilderSessionURL() public returns (string memory) { + try vm.envString("BUILDER_SESSION_URL") returns (string memory sessionURL) { + if (bytes(sessionURL).length == 0) { + vm.skip(true); + } + return sessionURL; + } catch { + vm.skip(true); + } + } +} diff --git a/test/protocols/Builder/Types.t.sol b/test/protocols/Builder/Types.t.sol new file mode 100644 index 0000000..191e60c --- /dev/null +++ b/test/protocols/Builder/Types.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import {Types} from "src/protocols/Builder/Types.sol"; +import "src/suavelib/Suave.sol"; +import "../../Fixtures.sol"; + +contract SuaveBuilderTypesTest is Test { + function testSuaveBuilderTypes_Encode_BuildblockArgs() public { + Types.BuildBlockArgs memory args; + args.slot = 1; + args.proposerPubkey = hex"1234"; + args.parent = hex"5678"; + args.timestamp = 123; + args.feeRecipient = address(0x1234); + args.gasLimit = 123; + args.random = hex"1234"; + args.extra = hex"1234"; + args.beaconRoot = hex"1234"; + args.fillPending = true; + bytes memory encode = Types.encodeBuildBlockArgs(args); + Fixtures.validate("suave_builder_builderArgs.json", string(encode)); + } + + function testSuaveBuilderTypes_Decode_SimulateTransactionResult() public { + string memory test = Fixtures.readFixture("suave_builder_simulateTransactionResult_failed.json"); + Types.SimulateTransactionResult memory result = Types.decodeSimulateTransactionResult(test); + assertEq(result.success, false); + assertEq(result.error, "some error"); + + test = Fixtures.readFixture("suave_builder_simulateTransactionResult_success.json"); + result = Types.decodeSimulateTransactionResult(test); + assertEq(result.success, true); + assertEq(result.logs.length, 1); + assertEq(result.logs[0].addr, address(0x5FbDB2315678afecb367f032d93F642f64180aa3)); + assertEq(result.logs[0].data, hex"00000000000000000000000000000000000000000000000000000000000004d2"); + assertEq(result.logs[0].topics.length, 1); + assertEq(result.logs[0].topics[0], bytes32(0x379340f64b65a8890c7ea4f6d86d2359beaf41080f36a7ea64b78a2c06eee3f0)); + } +}