Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
462 changes: 439 additions & 23 deletions README.md

Large diffs are not rendered by default.

150 changes: 150 additions & 0 deletions src/providers/http.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
const std = @import("std");
const Provider = @import("./provider.zig").Provider;

/// HTTP provider for Ethereum
pub const HttpProvider = struct {
provider: Provider,

/// Create a new HTTP provider
pub fn init(allocator: std.mem.Allocator, url: []const u8) !HttpProvider {
const provider = try Provider.init(allocator, url);
return .{ .provider = provider };
}

/// Free allocated memory
pub fn deinit(self: HttpProvider) void {
self.provider.deinit();
}

/// Get the underlying provider
pub fn getProvider(self: *HttpProvider) *Provider {
return @constCast(&self.provider);
}

/// Common provider methods (convenience wrappers)
pub fn getBlockNumber(self: HttpProvider) !u64 {
return try self.provider.getBlockNumber();
}

pub fn getBalance(self: HttpProvider, address: @import("../primitives/address.zig").Address) !@import("../primitives/uint.zig").U256 {
return try self.provider.getBalance(address);
}

pub fn getChainId(self: HttpProvider) !u64 {
return try self.provider.getChainId();
}

pub fn getGasPrice(self: HttpProvider) !@import("../primitives/uint.zig").U256 {
return try self.provider.getGasPrice();
}

pub fn getLatestBlock(self: HttpProvider) !@import("../types/block.zig").Block {
return try self.provider.getLatestBlock();
}

pub fn getTransaction(self: HttpProvider, hash: @import("../primitives/hash.zig").Hash) !@import("../types/transaction.zig").Transaction {
return try self.provider.getTransaction(hash);
}

pub fn getTransactionReceipt(self: HttpProvider, hash: @import("../primitives/hash.zig").Hash) !@import("../types/receipt.zig").Receipt {
return try self.provider.getTransactionReceipt(hash);
}

pub fn sendTransaction(self: HttpProvider, signed_tx: []const u8) !@import("../primitives/hash.zig").Hash {
return try self.provider.sendTransaction(signed_tx);
}

pub fn waitForTransaction(
self: HttpProvider,
tx_hash: @import("../primitives/hash.zig").Hash,
timeout_ms: u64,
) !@import("../types/receipt.zig").Receipt {
return try self.provider.waitForTransaction(tx_hash, timeout_ms, 1000);
}

pub fn isContract(self: HttpProvider, address: @import("../primitives/address.zig").Address) !bool {
return try self.provider.isContract(address);
}
};

/// Create HTTP provider for common networks
pub const Networks = struct {
/// Ethereum mainnet (Etherspot RPC v2 - Chain ID: 1)
pub fn mainnet(allocator: std.mem.Allocator) !HttpProvider {
return try HttpProvider.init(allocator, "https://rpc.etherspot.io/v2/1?api-key=etherspot_3ZSiRBeAjmYnJu1bCsaRXjeD");
}

/// Ethereum Sepolia testnet (Etherspot RPC v2 - Chain ID: 11155111)
pub fn sepolia(allocator: std.mem.Allocator) !HttpProvider {
return try HttpProvider.init(allocator, "https://rpc.etherspot.io/v2/11155111?api-key=etherspot_3ZSiRBeAjmYnJu1bCsaRXjeD");
}

/// Polygon mainnet (Etherspot RPC v2 - Chain ID: 137)
pub fn polygon(allocator: std.mem.Allocator) !HttpProvider {
return try HttpProvider.init(allocator, "https://rpc.etherspot.io/v2/137?api-key=etherspot_3ZSiRBeAjmYnJu1bCsaRXjeD");
}

/// Arbitrum mainnet (Etherspot RPC v2 - Chain ID: 42161)
pub fn arbitrum(allocator: std.mem.Allocator) !HttpProvider {
return try HttpProvider.init(allocator, "https://rpc.etherspot.io/v2/42161?api-key=etherspot_3ZSiRBeAjmYnJu1bCsaRXjeD");
}

/// Optimism mainnet (Etherspot RPC v2 - Chain ID: 10)
pub fn optimism(allocator: std.mem.Allocator) !HttpProvider {
return try HttpProvider.init(allocator, "https://rpc.etherspot.io/v2/10?api-key=etherspot_3ZSiRBeAjmYnJu1bCsaRXjeD");
}

/// Base mainnet (Etherspot RPC v2 - Chain ID: 8453)
pub fn base(allocator: std.mem.Allocator) !HttpProvider {
return try HttpProvider.init(allocator, "https://rpc.etherspot.io/v2/8453?api-key=etherspot_3ZSiRBeAjmYnJu1bCsaRXjeD");
}

/// Local development node
pub fn localhost(allocator: std.mem.Allocator) !HttpProvider {
return try HttpProvider.init(allocator, "http://localhost:8545");
}

/// Custom endpoint
pub fn custom(allocator: std.mem.Allocator, url: []const u8) !HttpProvider {
return try HttpProvider.init(allocator, url);
}
};

test "http provider creation" {
const allocator = std.testing.allocator;

const provider = try HttpProvider.init(allocator, "http://localhost:8545");
defer provider.deinit();

try std.testing.expectEqualStrings("http://localhost:8545", provider.provider.getEndpoint());
}

test "http provider networks" {
const allocator = std.testing.allocator;

const mainnet = try Networks.mainnet(allocator);
defer mainnet.deinit();
try std.testing.expect(std.mem.indexOf(u8, mainnet.provider.getEndpoint(), "etherspot.io/v2/1") != null);

const sepolia = try Networks.sepolia(allocator);
defer sepolia.deinit();
try std.testing.expect(std.mem.indexOf(u8, sepolia.provider.getEndpoint(), "etherspot.io/v2/11155111") != null);

const polygon = try Networks.polygon(allocator);
defer polygon.deinit();
try std.testing.expect(std.mem.indexOf(u8, polygon.provider.getEndpoint(), "etherspot.io/v2/137") != null);

const localhost = try Networks.localhost(allocator);
defer localhost.deinit();
try std.testing.expectEqualStrings("http://localhost:8545", localhost.provider.getEndpoint());
}

test "http provider get provider" {
const allocator = std.testing.allocator;

var http_provider = try HttpProvider.init(allocator, "http://localhost:8545");
defer http_provider.deinit();

const provider = http_provider.getProvider();
try std.testing.expect(provider.rpc_client.endpoint.len > 0);
}
235 changes: 235 additions & 0 deletions src/providers/ipc.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
const std = @import("std");
const Provider = @import("./provider.zig").Provider;

/// IPC provider for local Ethereum nodes (Unix socket communication)
pub const IpcProvider = struct {
provider: Provider,
socket_path: []const u8,
allocator: std.mem.Allocator,
stream: ?std.net.Stream,
connected: bool,

/// Create a new IPC provider
pub fn init(allocator: std.mem.Allocator, socket_path: []const u8) !IpcProvider {
// For IPC, we use a dummy HTTP endpoint since actual communication
// would happen through Unix sockets
const provider = try Provider.init(allocator, "ipc://local");
const path_copy = try allocator.dupe(u8, socket_path);

return .{
.provider = provider,
.socket_path = path_copy,
.allocator = allocator,
.stream = null,
.connected = false,
};
}

/// Free allocated memory
pub fn deinit(self: *IpcProvider) void {
if (self.connected) {
self.disconnect();
}
self.allocator.free(self.socket_path);
self.provider.deinit();
}

/// Get the underlying provider
pub fn getProvider(self: *IpcProvider) *Provider {
return @constCast(&self.provider);
}

/// Connect to IPC socket
pub fn connect(self: *IpcProvider) !void {
if (self.connected) {
return error.AlreadyConnected;
}

// Check OS support for Unix sockets
if (std.builtin.os.tag == .windows) {
// Windows uses named pipes, not Unix sockets
return error.WindowsNamedPipesNotSupported;
}

// Connect to Unix socket
const stream = try std.net.connectUnixSocket(self.socket_path);
self.stream = stream;
self.connected = true;
}

/// Disconnect from IPC socket
pub fn disconnect(self: *IpcProvider) void {
if (self.stream) |stream| {
stream.close();
self.stream = null;
}
self.connected = false;
}

/// Check if connected
pub fn isConnected(self: IpcProvider) bool {
return self.connected and self.stream != null;
}

/// Get socket path
pub fn getSocketPath(self: IpcProvider) []const u8 {
return self.socket_path;
}

/// Send JSON-RPC request over IPC
pub fn sendRequest(self: *IpcProvider, request: []const u8) ![]u8 {
if (!self.connected or self.stream == null) {
return error.NotConnected;
}

const stream = self.stream.?;

// Write request to socket
_ = try stream.write(request);

// Read response from socket
var response_buf = std.ArrayList(u8).init(self.allocator);
errdefer response_buf.deinit();

var read_buf: [4096]u8 = undefined;
while (true) {
const bytes_read = try stream.read(&read_buf);
if (bytes_read == 0) break;

try response_buf.appendSlice(read_buf[0..bytes_read]);

// Check if we have a complete JSON response
// (basic check for matching braces)
const response = response_buf.items;
if (response.len > 0 and response[response.len - 1] == '}') {
var open_braces: i32 = 0;
for (response) |char| {
if (char == '{') open_braces += 1;
if (char == '}') open_braces -= 1;
}
if (open_braces == 0) break;
}
}

return response_buf.toOwnedSlice();
}

/// Get stream for direct access
pub fn getStream(self: *IpcProvider) ?std.net.Stream {
return self.stream;
}
};

/// Common IPC socket paths
pub const SocketPaths = struct {
/// Default Geth IPC path (Unix)
pub const GETH_UNIX = "/tmp/geth.ipc";

/// Default Geth IPC path (macOS)
pub const GETH_MACOS = "~/Library/Ethereum/geth.ipc";

/// Default Geth IPC path (Windows)
pub const GETH_WINDOWS = "\\\\.\\pipe\\geth.ipc";

/// Get default path for current OS
pub fn getDefault() []const u8 {
return switch (std.builtin.os.tag) {
.linux => GETH_UNIX,
.macos => GETH_MACOS,
.windows => GETH_WINDOWS,
else => GETH_UNIX,
};
}
};

test "ipc provider creation" {
const allocator = std.testing.allocator;

const provider = try IpcProvider.init(allocator, "/tmp/geth.ipc");
defer provider.deinit();

try std.testing.expectEqualStrings("/tmp/geth.ipc", provider.getSocketPath());
}

test "ipc socket paths" {
const default_path = SocketPaths.getDefault();
try std.testing.expect(default_path.len > 0);

// Check that it contains expected path components
try std.testing.expect(
std.mem.indexOf(u8, SocketPaths.GETH_UNIX, "geth.ipc") != null or
std.mem.indexOf(u8, SocketPaths.GETH_UNIX, "tmp") != null,
);
}

test "ipc provider get provider" {
const allocator = std.testing.allocator;

var ipc_provider = try IpcProvider.init(allocator, "/tmp/geth.ipc");
defer ipc_provider.deinit();

const provider = ipc_provider.getProvider();
try std.testing.expect(provider.rpc_client.endpoint.len > 0);
}

test "ipc provider connection state" {
const allocator = std.testing.allocator;

var ipc_provider = try IpcProvider.init(allocator, "/tmp/test-geth.ipc");
defer ipc_provider.deinit();

// Initially not connected
try std.testing.expect(!ipc_provider.isConnected());
try std.testing.expect(ipc_provider.stream == null);

// Note: We can't actually connect without a running node,
// so we just verify the connection state logic
try std.testing.expect(!ipc_provider.connected);
}

test "ipc provider disconnect when not connected" {
const allocator = std.testing.allocator;

var ipc_provider = try IpcProvider.init(allocator, "/tmp/test-geth.ipc");
defer ipc_provider.deinit();

// Should be safe to call disconnect when not connected
ipc_provider.disconnect();
try std.testing.expect(!ipc_provider.isConnected());
}

test "ipc provider stream access" {
const allocator = std.testing.allocator;

var ipc_provider = try IpcProvider.init(allocator, "/tmp/test-geth.ipc");
defer ipc_provider.deinit();

// Stream should be null initially
try std.testing.expect(ipc_provider.getStream() == null);
}

test "ipc provider sendRequest when not connected" {
const allocator = std.testing.allocator;

var ipc_provider = try IpcProvider.init(allocator, "/tmp/test-geth.ipc");
defer ipc_provider.deinit();

const request = "{\"jsonrpc\":\"2.0\",\"method\":\"eth_blockNumber\",\"params\":[],\"id\":1}";

// Should return error when not connected
const result = ipc_provider.sendRequest(request);
try std.testing.expectError(error.NotConnected, result);
}

test "ipc provider windows named pipes not supported" {
if (std.builtin.os.tag != .windows) return error.SkipZigTest;

const allocator = std.testing.allocator;

var ipc_provider = try IpcProvider.init(allocator, "\\\\.\\pipe\\geth.ipc");
defer ipc_provider.deinit();

// Windows named pipes should return error
const result = ipc_provider.connect();
try std.testing.expectError(error.WindowsNamedPipesNotSupported, result);
}
Loading
Loading