├── .gitignore ├── build.zig ├── build.zig.zon └── src ├── main.zig ├── provider.zig └── types ├── block.zig ├── main.zig ├── primitives.zig └── transaction.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ 3 | 4 | .env 5 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | // Although this function looks imperative, note that its job is to 4 | // declaratively construct a build graph that will be executed by an external 5 | // runner. 6 | pub fn build(b: *std.Build) void { 7 | const target = b.standardTargetOptions(.{}); 8 | const optimize = b.standardOptimizeOption(.{}); 9 | 10 | // Import libraries 11 | const opts = .{ .target = target, .optimize = optimize }; 12 | const getty = b.dependency("getty", opts); 13 | const json = b.dependency("json", opts); 14 | const dotenv = b.dependency("dotenv", opts); 15 | 16 | const lib = b.addStaticLibrary(.{ 17 | .name = "ethers", 18 | .root_source_file = .{ .path = "src/main.zig" }, 19 | .target = target, 20 | .optimize = optimize, 21 | }); 22 | 23 | lib.addModule("getty", getty.module("getty")); 24 | lib.addModule("json", json.module("json")); 25 | lib.addModule("dotenv", dotenv.module("dotenv")); 26 | 27 | // This declares intent for the library to be installed into the standard 28 | // location when the user invokes the "install" step (the default step when 29 | // running `zig build`). 30 | b.installArtifact(lib); 31 | 32 | // Creates a step for unit testing. This only builds the test executable 33 | // but does not run it. 34 | const main_tests = b.addTest(.{ 35 | .root_source_file = .{ .path = "src/main.zig" }, 36 | .target = target, 37 | .optimize = optimize, 38 | }); 39 | 40 | main_tests.addModule("getty", getty.module("getty")); 41 | main_tests.addModule("json", json.module("json")); 42 | main_tests.addModule("dotenv", dotenv.module("dotenv")); 43 | 44 | const run_main_tests = b.addRunArtifact(main_tests); 45 | 46 | // This creates a build step. It will be visible in the `zig build --help` menu, 47 | // and can be selected like this: `zig build test` 48 | // This will evaluate the `test` step rather than the default, which is "install". 49 | const test_step = b.step("test", "Run library tests"); 50 | test_step.dependOn(&run_main_tests.step); 51 | } 52 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "ethers-zig", 3 | .version = "0.1.0", 4 | .dependencies = .{ 5 | .getty = .{ 6 | .url = "https://github.com/getty-zig/getty/archive/main.tar.gz", 7 | .hash = "12205aae2ea6dcaa3efcf3751c40dbd3946e539794688bad62e79c86635762204a3d", 8 | }, 9 | .json = .{ 10 | .url = "https://github.com/getty-zig/json/archive/07595d9323568873ac07aebc616683855c2e7d56.tar.gz", 11 | .hash = "1220465f43808ceff290a3473a63fe623d2bf9c9968de242af188930ec5301a17de4", 12 | }, 13 | .dotenv = .{ 14 | .url = "https://github.com/ncitron/dotenv-zig/archive/6c188eec63bed8794bb8b9e35493c60f1722e9d9.tar.gz", 15 | .hash = "12201c77e103385bbad61a67280f48b95e6182bc7fb8c2755dc63e0fe0e71a17261f", 16 | }, 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | pub const types = @import("types/main.zig"); 2 | pub const provider = @import("provider.zig"); 3 | 4 | test { 5 | @import("std").testing.refAllDeclsRecursive(@This()); 6 | } 7 | -------------------------------------------------------------------------------- /src/provider.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const getty = @import("getty"); 3 | const json = @import("json"); 4 | const dotenv = @import("dotenv"); 5 | 6 | const types = @import("types/main.zig"); 7 | const U256 = types.U256; 8 | const U64 = types.U64; 9 | const H256 = types.H256; 10 | const Bytes = types.Bytes; 11 | const Address = types.Address; 12 | const Block = types.Block; 13 | const Transaction = types.Transaction; 14 | const TransactionRequest = types.TransactionRequest; 15 | 16 | pub const Provider = struct { 17 | allocator: std.mem.Allocator, 18 | rpc: []const u8, 19 | 20 | pub fn blockNumber(self: Provider) !U64 { 21 | const req_body = try buildRpcRequest("eth_blockNumber", .{}); 22 | return try self.sendRpcRequest(req_body, U64); 23 | } 24 | 25 | pub fn getBalance(self: Provider, addr: Address) !U256 { 26 | const req_body = try buildRpcRequest("eth_getBalance", .{addr}); 27 | return try self.sendRpcRequest(req_body, U256); 28 | } 29 | 30 | pub fn chainId(self: Provider) !U64 { 31 | const req_body = try buildRpcRequest("eth_chainId", .{}); 32 | return try self.sendRpcRequest(req_body, U64); 33 | } 34 | 35 | pub fn getBlockByNumber(self: Provider, comptime TxType: type, number: U64) !Block(TxType) { 36 | const full_tx = TxType == Transaction; 37 | const req_body = try buildRpcRequest("eth_getBlockByNumber", .{ number, full_tx }); 38 | return try self.sendRpcRequest(req_body, Block(TxType)); 39 | } 40 | 41 | pub fn getTransactionByHash(self: Provider, hash: H256) !Transaction { 42 | const req_body = try buildRpcRequest("eth_getTransactionByHash", .{hash}); 43 | return try self.sendRpcRequest(req_body, Transaction); 44 | } 45 | 46 | pub fn call(self: Provider, tx: TransactionRequest) !Bytes { 47 | const req_body = try buildRpcRequest("eth_call", .{ tx, "latest" }); 48 | return try self.sendRpcRequest(req_body, Bytes); 49 | } 50 | 51 | fn sendRpcRequest(self: Provider, rpc_req_body: anytype, comptime R: type) !R { 52 | var provider = std.http.Client{ .allocator = self.allocator }; 53 | defer provider.deinit(); 54 | 55 | const uri = try std.Uri.parse(self.rpc); 56 | 57 | var headers = std.http.Headers{ .allocator = self.allocator }; 58 | defer headers.deinit(); 59 | try headers.append("accept", "*/*"); 60 | 61 | var req = try provider.request(.POST, uri, headers, .{}); 62 | defer req.deinit(); 63 | 64 | req.transfer_encoding = .chunked; 65 | try req.start(); 66 | try json.toWriter(self.allocator, rpc_req_body, req.writer()); 67 | 68 | try req.finish(); 69 | try req.wait(); 70 | 71 | const resp = try json.fromReader(self.allocator, RpcResponse(R), req.reader()); 72 | return resp.result; 73 | } 74 | }; 75 | 76 | fn RpcResponse(comptime R: type) type { 77 | return struct { 78 | id: u32, 79 | result: R, 80 | 81 | pub const @"getty.db" = struct { 82 | pub const attributes = .{ 83 | .Container = .{ .ignore_unknown_fields = true }, 84 | }; 85 | }; 86 | }; 87 | } 88 | 89 | fn RpcRequest(comptime P: type) type { 90 | return struct { 91 | jsonrpc: []const u8, 92 | id: u32, 93 | method: []const u8, 94 | params: P, 95 | }; 96 | } 97 | 98 | fn buildRpcRequest(method: []const u8, params: anytype) !RpcRequest(@TypeOf(params)) { 99 | return RpcRequest(@TypeOf(params)){ 100 | .jsonrpc = "2.0", 101 | .id = 0, 102 | .method = method, 103 | .params = params, 104 | }; 105 | } 106 | 107 | test "serialize rpc request" { 108 | const allocator = std.testing.allocator; 109 | 110 | const addr = try Address.fromString("0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5"); 111 | const req_body = try buildRpcRequest("eth_getBalance", .{addr}); 112 | 113 | const req_json = try json.toSlice(allocator, req_body); 114 | defer allocator.free(req_json); 115 | 116 | const expected = "{\"jsonrpc\":\"2.0\",\"id\":0,\"method\":\"eth_getBalance\",\"params\":[\"0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5\"]}"; 117 | try std.testing.expect(std.mem.eql(u8, req_json, expected)); 118 | } 119 | 120 | test "deserialize rpc response" { 121 | const allocator = std.testing.allocator; 122 | 123 | const resp = "{\"jsonrpc\":\"2.0\",\"id\":0,\"result\":\"0x4d2\"}"; 124 | const parsed = try json.fromSlice(allocator, RpcResponse(U64), resp); 125 | 126 | try std.testing.expectEqual(parsed.result.value, 1234); 127 | } 128 | 129 | test "block number" { 130 | const allocator = std.testing.allocator; 131 | 132 | const rpc = try dotenv.getEnvVar(allocator, ".env", "mainnet_rpc"); 133 | defer allocator.free(rpc); 134 | 135 | const provider = Provider{ .allocator = allocator, .rpc = rpc }; 136 | 137 | const block_number = try provider.blockNumber(); 138 | try std.testing.expect(block_number.value > 17_000_000); 139 | } 140 | 141 | test "get balance" { 142 | const allocator = std.testing.allocator; 143 | 144 | const rpc = try dotenv.getEnvVar(allocator, ".env", "mainnet_rpc"); 145 | defer allocator.free(rpc); 146 | 147 | const provider = Provider{ .allocator = allocator, .rpc = rpc }; 148 | 149 | const addr = try Address.fromString("0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5"); 150 | const balance = try provider.getBalance(addr); 151 | 152 | try std.testing.expect(balance.value > 0); 153 | } 154 | 155 | test "chain id" { 156 | const allocator = std.testing.allocator; 157 | 158 | const rpc = try dotenv.getEnvVar(allocator, ".env", "mainnet_rpc"); 159 | defer allocator.free(rpc); 160 | 161 | const provider = Provider{ .allocator = allocator, .rpc = rpc }; 162 | 163 | const chain_id = try provider.chainId(); 164 | try std.testing.expectEqual(chain_id.value, 1); 165 | } 166 | 167 | test "get block by number" { 168 | const allocator = std.testing.allocator; 169 | 170 | const rpc = try dotenv.getEnvVar(allocator, ".env", "mainnet_rpc"); 171 | defer allocator.free(rpc); 172 | 173 | const provider = Provider{ .allocator = allocator, .rpc = rpc }; 174 | 175 | const block = try provider.getBlockByNumber(H256, U64.from(17728594)); 176 | defer block.deinit(); 177 | 178 | try std.testing.expectEqual(block.number, U64.from(17728594)); 179 | } 180 | 181 | test "get block by full tx" { 182 | const allocator = std.testing.allocator; 183 | 184 | const rpc = try dotenv.getEnvVar(allocator, ".env", "mainnet_rpc"); 185 | defer allocator.free(rpc); 186 | 187 | const provider = Provider{ .allocator = allocator, .rpc = rpc }; 188 | 189 | const block = try provider.getBlockByNumber(Transaction, U64.from(17728594)); 190 | defer block.deinit(); 191 | 192 | try std.testing.expectEqual(block.number, U64.from(17728594)); 193 | } 194 | 195 | test "get transaction by hash" { 196 | const allocator = std.testing.allocator; 197 | 198 | const rpc = try dotenv.getEnvVar(allocator, ".env", "mainnet_rpc"); 199 | defer allocator.free(rpc); 200 | 201 | const provider = Provider{ .allocator = allocator, .rpc = rpc }; 202 | 203 | const hash = try H256.fromString("0xb87a0074d06fcb53389401b137a5cc741e50d7eabe18a4248edf2a1910b99719"); 204 | const tx = try provider.getTransactionByHash(hash); 205 | defer tx.deinit(); 206 | 207 | try std.testing.expectEqual(tx.hash.?.value, hash.value); 208 | } 209 | 210 | test "call" { 211 | const allocator = std.testing.allocator; 212 | 213 | const rpc = try dotenv.getEnvVar(allocator, ".env", "mainnet_rpc"); 214 | defer allocator.free(rpc); 215 | 216 | const provider = Provider{ .allocator = allocator, .rpc = rpc }; 217 | 218 | const tx: TransactionRequest = .{ 219 | .to = try Address.fromString("0x6B175474E89094C44Da98b954EedeAC495271d0F"), 220 | .input = try Bytes.fromString(std.testing.allocator, "0x95d89b41"), 221 | }; 222 | 223 | defer tx.input.?.deinit(); 224 | 225 | const result = try provider.call(tx); 226 | defer result.deinit(); 227 | 228 | const expected = try Bytes.fromString(allocator, "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000034441490000000000000000000000000000000000000000000000000000000000"); 229 | defer expected.deinit(); 230 | 231 | try std.testing.expect(std.mem.eql(u8, result.value.items, expected.value.items)); 232 | } 233 | -------------------------------------------------------------------------------- /src/types/block.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const getty = @import("getty"); 3 | const json = @import("json"); 4 | 5 | const types = @import("main.zig"); 6 | const U256 = types.U256; 7 | const U64 = types.U64; 8 | const H2048 = types.H2048; 9 | const H256 = types.H256; 10 | const H64 = types.H64; 11 | const Address = types.Address; 12 | 13 | pub fn Block(comptime TxType: type) type { 14 | return struct { 15 | hash: H256, 16 | parent_hash: H256, 17 | number: U64, 18 | timestamp: U64, 19 | base_fee_per_gas: U256, 20 | gas_limit: U256, 21 | gas_used: U256, 22 | miner: Address, 23 | receipts_root: H256, 24 | state_root: H256, 25 | transactions_root: H256, 26 | uncles_root: H256, 27 | transactions: std.ArrayList(TxType), 28 | difficulty: U256, 29 | extra_data: H256, 30 | mix_hash: H256, 31 | nonce: H64, 32 | size: U256, 33 | total_difficulty: U256, 34 | logs_bloom: H2048, 35 | 36 | pub fn deinit(self: @This()) void { 37 | if (TxType == types.Transaction) { 38 | for (self.transactions.items) |tx| { 39 | tx.deinit(); 40 | } 41 | } 42 | 43 | self.transactions.deinit(); 44 | } 45 | 46 | pub const @"getty.db" = struct { 47 | pub const attributes = .{ 48 | .Container = .{ .ignore_unknown_fields = true }, 49 | .parent_hash = .{ .rename = "parentHash" }, 50 | .base_fee_per_gas = .{ .rename = "baseFeePerGas" }, 51 | .gas_limit = .{ .rename = "gasLimit" }, 52 | .gas_used = .{ .rename = "gasUsed" }, 53 | .receipts_root = .{ .rename = "receiptsRoot" }, 54 | .state_root = .{ .rename = "stateRoot" }, 55 | .transactions_root = .{ .rename = "transactionsRoot" }, 56 | .uncles_root = .{ .rename = "sha3Uncles" }, 57 | .extra_data = .{ .rename = "extraData" }, 58 | .mix_hash = .{ .rename = "mixHash" }, 59 | .total_difficulty = .{ .rename = "totalDifficulty" }, 60 | .logs_bloom = .{ .rename = "logsBloom" }, 61 | }; 62 | }; 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/types/main.zig: -------------------------------------------------------------------------------- 1 | pub usingnamespace @import("primitives.zig"); 2 | pub const Block = @import("block.zig").Block; 3 | pub const Transaction = @import("transaction.zig").Transaction; 4 | pub const TransactionRequest = @import("transaction.zig").TransactionRequest; 5 | -------------------------------------------------------------------------------- /src/types/primitives.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const getty = @import("getty"); 3 | const json = @import("json"); 4 | 5 | pub const U256 = U(256); 6 | pub const U64 = U(64); 7 | 8 | pub const H2048 = H(2048); 9 | pub const H256 = H(256); 10 | pub const H64 = H(64); 11 | pub const Address = H(160); 12 | 13 | pub const Bytes = struct { 14 | value: std.ArrayList(u8), 15 | 16 | pub fn deinit(self: Bytes) void { 17 | self.value.deinit(); 18 | } 19 | 20 | pub fn fromString(allocator: std.mem.Allocator, string: []const u8) !Bytes { 21 | const stripped = if (std.mem.startsWith(u8, string, "0x")) string[2..] else string; 22 | 23 | const value = try allocator.alloc(u8, stripped.len / 2); 24 | _ = try std.fmt.hexToBytes(value, stripped); 25 | return .{ .value = std.ArrayList(u8).fromOwnedSlice(allocator, value) }; 26 | } 27 | 28 | pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { 29 | try writer.print("0x{}", .{std.fmt.fmtSliceHexLower(self.value.items)}); 30 | } 31 | 32 | pub const @"getty.sb" = struct { 33 | pub fn serialize(allocator: ?std.mem.Allocator, value: anytype, serializer: anytype) !@TypeOf(serializer).Ok { 34 | const str = try std.fmt.allocPrint(allocator.?, "0x{}", .{std.fmt.fmtSliceHexLower(value.value.items)}); 35 | defer allocator.?.free(str); 36 | return try serializer.serializeString(str); 37 | } 38 | }; 39 | 40 | pub const @"getty.db" = struct { 41 | pub fn deserialize(allocator: ?std.mem.Allocator, comptime T: type, deserializer: anytype, value: anytype) !T { 42 | return deserializer.deserializeAny(allocator, value); 43 | } 44 | 45 | pub fn Visitor(comptime T: type) type { 46 | return struct { 47 | pub usingnamespace getty.de.Visitor( 48 | @This(), 49 | T, 50 | .{ .visitString = visitString }, 51 | ); 52 | 53 | pub fn visitString(_: @This(), allocator: ?std.mem.Allocator, comptime De: type, string: anytype) De.Error!T { 54 | return Bytes.fromString(allocator.?, string) catch return De.Error.InvalidValue; 55 | } 56 | }; 57 | } 58 | }; 59 | }; 60 | 61 | fn U(comptime bits: u16) type { 62 | const Uint = std.meta.Int(.unsigned, bits); 63 | 64 | return struct { 65 | value: Uint, 66 | 67 | pub fn from(value: Uint) @This() { 68 | return .{ .value = value }; 69 | } 70 | 71 | pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { 72 | try writer.print("{}", .{self.value}); 73 | } 74 | 75 | pub const @"getty.sb" = struct { 76 | pub fn serialize(allocator: ?std.mem.Allocator, value: anytype, serializer: anytype) !@TypeOf(serializer).Ok { 77 | const uint_str = try std.fmt.allocPrint(allocator.?, "0x{x}", .{value.value}); 78 | defer allocator.?.free(uint_str); 79 | return try serializer.serializeString(uint_str); 80 | } 81 | }; 82 | 83 | pub const @"getty.db" = struct { 84 | pub fn deserialize(allocator: ?std.mem.Allocator, comptime T: type, deserializer: anytype, value: anytype) !T { 85 | return deserializer.deserializeAny(allocator, value); 86 | } 87 | 88 | pub fn Visitor(comptime T: type) type { 89 | return struct { 90 | pub usingnamespace getty.de.Visitor( 91 | @This(), 92 | T, 93 | .{ .visitString = visitString }, 94 | ); 95 | 96 | pub fn visitString(_: @This(), _: ?std.mem.Allocator, comptime De: type, string: anytype) De.Error!T { 97 | const value = try std.fmt.parseInt(Uint, string, 0); 98 | return .{ .value = value }; 99 | } 100 | }; 101 | } 102 | }; 103 | }; 104 | } 105 | 106 | fn H(comptime bits: u16) type { 107 | const Uint = std.meta.Int(.unsigned, bits); 108 | 109 | return struct { 110 | value: Uint, 111 | 112 | pub fn from(value: Uint) @This() { 113 | return .{ .value = value }; 114 | } 115 | 116 | pub fn fromString(hash_str: []const u8) !@This() { 117 | const value = try std.fmt.parseInt(Uint, hash_str, 0); 118 | return .{ .value = value }; 119 | } 120 | 121 | pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { 122 | const fmt_str = std.fmt.comptimePrint("0x{{x:0>{}}}", .{bits / 8 * 2}); 123 | try writer.print(fmt_str, .{self.value}); 124 | } 125 | 126 | pub const @"getty.sb" = struct { 127 | pub fn serialize(allocator: ?std.mem.Allocator, value: anytype, serializer: anytype) !@TypeOf(serializer).Ok { 128 | const fmt_str = std.fmt.comptimePrint("0x{{x:0>{}}}", .{bits / 8 * 2}); 129 | const hash_str = try std.fmt.allocPrint(allocator.?, fmt_str, .{value.value}); 130 | defer allocator.?.free(hash_str); 131 | return try serializer.serializeString(hash_str); 132 | } 133 | }; 134 | 135 | pub const @"getty.db" = struct { 136 | pub fn deserialize(allocator: ?std.mem.Allocator, comptime T: type, deserializer: anytype, value: anytype) !T { 137 | return deserializer.deserializeAny(allocator, value); 138 | } 139 | 140 | pub fn Visitor(comptime T: type) type { 141 | return struct { 142 | pub usingnamespace getty.de.Visitor( 143 | @This(), 144 | T, 145 | .{ .visitString = visitString }, 146 | ); 147 | 148 | pub fn visitString(_: @This(), _: ?std.mem.Allocator, comptime De: type, string: anytype) De.Error!T { 149 | const value = try std.fmt.parseInt(Uint, string, 0); 150 | return .{ .value = value }; 151 | } 152 | }; 153 | } 154 | }; 155 | }; 156 | } 157 | 158 | test "create address" { 159 | const addr = try Address.fromString("0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5"); 160 | try std.testing.expectEqual(addr.value, 1250238713705615060704406741895064647274915793861); 161 | } 162 | 163 | test "serialize address" { 164 | const allocator = std.testing.allocator; 165 | 166 | const addr = try Address.fromString("0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5"); 167 | const addr_json = try json.toSlice(allocator, addr); 168 | defer allocator.free(addr_json); 169 | 170 | try std.testing.expect(std.mem.eql(u8, addr_json, "\"0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5\"")); 171 | } 172 | 173 | test "deserialize address" { 174 | const allocator = std.testing.allocator; 175 | 176 | const addr_json = "\"0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5\""; 177 | const addr = try json.fromSlice(allocator, Address, addr_json); 178 | 179 | try std.testing.expectEqual(addr.value, 1250238713705615060704406741895064647274915793861); 180 | } 181 | 182 | test "create uint" { 183 | const uint = U(256).from(1234); 184 | try std.testing.expectEqual(uint.value, 1234); 185 | } 186 | 187 | test "serialize uint" { 188 | const allocator = std.testing.allocator; 189 | 190 | const uint = U(256).from(1234); 191 | const uint_json = try json.toSlice(allocator, uint); 192 | defer allocator.free(uint_json); 193 | 194 | try std.testing.expect(std.mem.eql(u8, uint_json, "\"0x4d2\"")); 195 | } 196 | 197 | test "deserialize uint" { 198 | const allocator = std.testing.allocator; 199 | 200 | const uint_json = "\"0x4d2\""; 201 | const uint = try json.fromSlice(allocator, U(256), uint_json); 202 | 203 | try std.testing.expectEqual(uint.value, 1234); 204 | } 205 | 206 | test "create hash" { 207 | const hash = try H(256).fromString("0x4d2"); 208 | try std.testing.expectEqual(hash.value, 1234); 209 | } 210 | 211 | test "serialize hash" { 212 | const allocator = std.testing.allocator; 213 | 214 | const hash = try H(256).fromString("0x1234"); 215 | const hash_json = try json.toSlice(allocator, hash); 216 | defer allocator.free(hash_json); 217 | 218 | const expected = "\"0x0000000000000000000000000000000000000000000000000000000000001234\""; 219 | try std.testing.expect(std.mem.eql(u8, hash_json, expected)); 220 | } 221 | 222 | test "deserialize hash" { 223 | const allocator = std.testing.allocator; 224 | 225 | const hash_json = "\"0x4d2\""; 226 | const hash = try json.fromSlice(allocator, H(256), hash_json); 227 | 228 | try std.testing.expectEqual(hash.value, 1234); 229 | } 230 | 231 | test "deserialize bytes" { 232 | const bytes_json = "\"0x1234\""; 233 | const bytes = try json.fromSlice(std.testing.allocator, Bytes, bytes_json); 234 | defer bytes.deinit(); 235 | 236 | try std.testing.expect(std.mem.eql(u8, bytes.value.items, &[2]u8{ 18, 52 })); 237 | } 238 | -------------------------------------------------------------------------------- /src/types/transaction.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const getty = @import("getty"); 3 | const json = @import("json"); 4 | 5 | const types = @import("main.zig"); 6 | const U256 = types.U256; 7 | const U64 = types.U64; 8 | const H256 = types.H256; 9 | const Address = types.Address; 10 | const Bytes = types.Bytes; 11 | 12 | pub const Transaction = struct { 13 | from: Address, 14 | to: ?Address, 15 | value: U256, 16 | input: Bytes, 17 | hash: ?H256, 18 | 19 | nonce: U256, 20 | block_hash: ?H256, 21 | block_number: ?U64, 22 | transaction_index: ?U64, 23 | chain_id: U64, 24 | 25 | gas: U256, 26 | gas_price: ?U256, 27 | max_fee_per_gas: ?U256, 28 | max_priority_fee_per_gas: ?U256, 29 | 30 | v: U64, 31 | r: U256, 32 | s: U256, 33 | 34 | pub fn deinit(self: Transaction) void { 35 | self.input.deinit(); 36 | } 37 | 38 | pub const @"getty.db" = struct { 39 | pub const attributes = .{ 40 | .Container = .{ .ignore_unknown_fields = true }, 41 | .to = .{ .default = null }, 42 | .hash = .{ .default = null }, 43 | .block_hash = .{ .default = null, .rename = "blockHash" }, 44 | .block_number = .{ .default = null, .rename = "blockNumber" }, 45 | .chain_id = .{ .rename = "chainId" }, 46 | .transaction_index = .{ .default = null, .rename = "transactionIndex" }, 47 | .gas_price = .{ .default = null, .rename = "gasPrice" }, 48 | .max_fee_per_gas = .{ .default = null, .rename = "maxFeePerGas" }, 49 | .max_priority_fee_per_gas = .{ .default = null, .rename = "maxPriorityFeePerGas" }, 50 | }; 51 | }; 52 | }; 53 | 54 | pub const TransactionRequest = struct { 55 | from: ?Address = null, 56 | to: ?Address = null, 57 | value: ?U256 = null, 58 | input: ?Bytes = null, 59 | chain_id: ?U64 = null, 60 | 61 | gas: ?U256 = null, 62 | gas_price: ?U256 = null, 63 | max_fee_per_gas: ?U256 = null, 64 | max_priority_fee_per_gas: ?U256 = null, 65 | 66 | pub const @"getty.sb" = struct { 67 | pub fn serialize(allocator: ?std.mem.Allocator, value: anytype, serializer: anytype) !@TypeOf(serializer).Ok { 68 | var m = try serializer.serializeMap(std.meta.fields(@TypeOf(value)).len); 69 | const map = m.map(); 70 | 71 | inline for (std.meta.fields(TransactionRequest)) |field| { 72 | const field_value = @field(value, field.name); 73 | 74 | const name = try toCamelCase(allocator.?, field.name); 75 | defer allocator.?.free(name); 76 | 77 | switch (@typeInfo(@TypeOf(field_value))) { 78 | .Optional => { 79 | if (field_value) |v| { 80 | try map.serializeEntry(name, v); 81 | } 82 | }, 83 | else => { 84 | try map.serializeEntry(name, field_value); 85 | }, 86 | } 87 | } 88 | 89 | return try map.end(); 90 | } 91 | }; 92 | }; 93 | 94 | fn toCamelCase(allocator: std.mem.Allocator, value: []const u8) ![]const u8 { 95 | var list = std.ArrayList(u8).init(allocator); 96 | var split = std.mem.splitAny(u8, value, "_"); 97 | try list.appendSlice(split.next() orelse return list.items); 98 | 99 | while (split.next()) |word| { 100 | var word_copy = try allocator.dupe(u8, word); 101 | defer allocator.free(word_copy); 102 | 103 | word_copy[0] = std.ascii.toUpper(word_copy[0]); 104 | try list.appendSlice(word_copy); 105 | } 106 | 107 | return try list.toOwnedSlice(); 108 | } 109 | 110 | test "deserialize transaction" { 111 | const tx_json = 112 | \\{ 113 | \\"blockHash": "0xde97ef27d682f1d0c4f6ca2496e63101dee63c54bfa25da57e9282c84e3310ed", 114 | \\"blockNumber": "0x10ea0f1", 115 | \\"from": "0x5111a967b4e1a2598ca152d9aee008a2f1dec321", 116 | \\"gas": "0x16822", 117 | \\"gasPrice":"0x130f3e6524", 118 | \\"maxFeePerGas": "0x106d8d272c", 119 | \\"maxPriorityFeePerGas": "0xc5229dd61", 120 | \\"hash": "0xdfc60e0f73927acd90380910e4a8b3164c39719ce42ac8f810a4f8e91df18e3d", 121 | \\"input": "0xa9059cbb0000000000000000000000000ed98203db63ef0089f9c0f8171255bc092da39900000000000000000000000000000000000000000000000000000000034d7fa8", 122 | \\"nonce": "0x90e", 123 | \\"to": "0xdac17f958d2ee523a2206206994597c13d831ec7", 124 | \\"transactionIndex": "0x1", 125 | \\"value": "0x0", 126 | \\"type": "0x2", 127 | \\"accessList": [], 128 | \\"chainId": "0x1", 129 | \\"v": "0x1", 130 | \\"r": "0x42b2d8707ae29063061db4f6ea7352870fde91f1ee434c624e1c62793236bce2", 131 | \\"s": "0x98f93884c2280e18601a8a7d98dfaf8004ad6669f31a6cc84206e698bda150" 132 | \\} 133 | ; 134 | 135 | const tx = try json.fromSlice(std.testing.allocator, Transaction, tx_json); 136 | defer tx.deinit(); 137 | 138 | const hash = try H256.fromString("0xdfc60e0f73927acd90380910e4a8b3164c39719ce42ac8f810a4f8e91df18e3d"); 139 | try std.testing.expectEqual(tx.hash.?.value, hash.value); 140 | } 141 | 142 | test "serialize transaction request" { 143 | const tx: TransactionRequest = .{ 144 | .to = try Address.fromString("0x6B175474E89094C44Da98b954EedeAC495271d0F"), 145 | .input = try Bytes.fromString(std.testing.allocator, "0x18160ddd"), 146 | .chain_id = U64.from(1), 147 | }; 148 | 149 | defer tx.input.?.deinit(); 150 | 151 | const tx_json = try json.toSlice(std.testing.allocator, tx); 152 | defer std.testing.allocator.free(tx_json); 153 | 154 | const expected = "{\"to\":\"0x6b175474e89094c44da98b954eedeac495271d0f\",\"input\":\"0x18160ddd\",\"chainId\":\"0x1\"}"; 155 | try std.testing.expect(std.mem.eql(u8, tx_json, expected)); 156 | } 157 | --------------------------------------------------------------------------------