Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
62 changes: 61 additions & 1 deletion src/chain_client.zig
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,12 @@ pub const EthChainClient = struct {
transport: *eth.http_transport.HttpTransport,
provider: *eth.provider.Provider,
wallet: *eth.wallet.Wallet,
/// The heap-owned KMS signer when this client signs via AWS KMS (see
/// `createWithKms`); null for the raw-key path. Kept stable because the
/// wallet's `Signer` holds a borrowed pointer to it.
kms_signer: ?*eth.signer.KmsSigner = null,
/// Owned copy of the KMS key id (the signer borrows it), freed on `destroy`.
kms_key_id: ?[]u8 = null,

/// Allocate and wire the eth.zig objects on the heap. Returns the heap
/// pointer -- keep it and hand out `ChainClient`s via `client()`.
Expand Down Expand Up @@ -284,11 +290,65 @@ pub const EthChainClient = struct {
return self;
}

/// Like `create`, but signs via AWS KMS: the private key never leaves KMS.
/// `region` is e.g. "us-west-2" and `key_id` is a KMS key id, ARN, or
/// `alias/...` (must be an `ECC_SECG_P256K1` key). Credentials are resolved
/// from the environment / container role at call time. The signer derives
/// and caches the wallet address from KMS during construction (one
/// `kms:GetPublicKey`), so this makes a network call.
pub fn createWithKms(
allocator: std.mem.Allocator,
rpc_url: []const u8,
region: []const u8,
key_id: []const u8,
) !*EthChainClient {
const self = try allocator.create(EthChainClient);
errdefer allocator.destroy(self);

const transport = try allocator.create(eth.http_transport.HttpTransport);
errdefer allocator.destroy(transport);
transport.* = eth.http_transport.HttpTransport.init(allocator, rpc_url, eth.runtime.blockingIo());
errdefer transport.deinit();

const provider = try allocator.create(eth.provider.Provider);
errdefer allocator.destroy(provider);
provider.* = eth.provider.Provider.init(allocator, transport);

// The signer borrows key_id for its lifetime, so own a copy here.
const key_id_owned = try allocator.dupe(u8, key_id);
errdefer allocator.free(key_id_owned);

// Heap-owned + stable: the wallet's Signer holds a borrowed pointer.
const kms_signer = try allocator.create(eth.signer.KmsSigner);
errdefer allocator.destroy(kms_signer);
kms_signer.* = try eth.signer.KmsSigner.init(allocator, eth.runtime.blockingIo(), region, key_id_owned);
errdefer kms_signer.deinit();

const wallet = try allocator.create(eth.wallet.Wallet);
errdefer allocator.destroy(wallet);
wallet.* = eth.wallet.Wallet.init(allocator, eth.signer.Signer.fromKms(kms_signer), provider);

self.* = .{
.allocator = allocator,
.transport = transport,
.provider = provider,
.wallet = wallet,
.kms_signer = kms_signer,
.kms_key_id = key_id_owned,
};
return self;
}

/// Tear down the wallet/transport and free every heap allocation, including
/// `self`.
/// `self` and (when signing via KMS) the KMS signer.
pub fn destroy(self: *EthChainClient) void {
self.wallet.deinit();
self.transport.deinit();
if (self.kms_signer) |ks| {
ks.deinit();
self.allocator.destroy(ks);
}
if (self.kms_key_id) |kid| self.allocator.free(kid);
self.allocator.destroy(self.wallet);
self.allocator.destroy(self.provider);
self.allocator.destroy(self.transport);
Expand Down
25 changes: 25 additions & 0 deletions src/context.zig
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,31 @@ pub const PerpCityContext = struct {
};
}

/// Like `init`, but signs the write path via AWS KMS -- the private key
/// never leaves KMS. `region` is e.g. "us-west-2"; `key_id` is a KMS key id,
/// ARN, or `alias/...` (an `ECC_SECG_P256K1` key). Credentials resolve from
/// the environment / container role. Derives the wallet address from KMS at
/// construction, so this makes a network call.
pub fn initWithKms(
allocator: std.mem.Allocator,
rpc_url: []const u8,
region: []const u8,
key_id: []const u8,
deployments: types.PerpCityDeployments,
) !Self {
const ec = try EthChainClient.createWithKms(allocator, rpc_url, region, key_id);
return Self{
.allocator = allocator,
.client = ec.client(),
.eth_client = ec,
.deployments = deployments,
.approved_perps = std.AutoHashMap(types.Address, void).init(allocator),
.config_cache = std.AutoHashMap(types.Address, CacheEntry).init(allocator),
.state_cache = state_cache_mod.StateCache.init(allocator, .{}),
.rpc_url = rpc_url,
};
}

/// Build a context around an already-constructed `ChainClient` (for tests
/// with an in-memory mock). The context does not own the client, so
/// `deinit` leaves it alone (`eth_client` is null).
Expand Down
42 changes: 42 additions & 0 deletions tests/contract/chain_client_test.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const std = @import("std");
const sdk = @import("perpcity_sdk");

const EthChainClient = sdk.chain_client.EthChainClient;

// The raw-key path constructs and tears down without a network call (the
// address derives locally from the key), so this regression-guards the
// `destroy` changes made for the KMS variant: the `kms_signer == null` branch
// must free cleanly under the testing allocator.
test "EthChainClient raw-key create/destroy is leak-clean and has no KMS signer" {
const alloc = std.testing.allocator;
const private_key = [_]u8{0x11} ** 32;

const ec = try EthChainClient.create(alloc, "http://localhost:8545", private_key);
defer ec.destroy();

try std.testing.expect(ec.kms_signer == null);
try std.testing.expect(ec.kms_key_id == null);

// Address derivation is local (secp256k1), so this needs no node.
var cc = ec.client();
const addr = try cc.address();
var all_zero = true;
for (addr) |b| {
if (b != 0) {
all_zero = false;
break;
}
}
try std.testing.expect(!all_zero);
}

// The KMS constructors are referenced here so a signature change breaks the
// build. They are not invoked: KmsSigner.init calls kms:GetPublicKey (a network
// call needing AWS credentials), so the KMS signing path is exercised only by
// integration tests, not CI.
test "KMS constructors are wired (compile-time reference only)" {
const ctx_kms = @TypeOf(sdk.context.PerpCityContext.initWithKms);
const ec_kms = @TypeOf(EthChainClient.createWithKms);
try std.testing.expect(@typeInfo(ctx_kms) == .@"fn");
try std.testing.expect(@typeInfo(ec_kms) == .@"fn");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
1 change: 1 addition & 0 deletions tests/contract_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ comptime {
_ = @import("contract/revert_test.zig");
_ = @import("contract/context_simulate_override_test.zig");
_ = @import("contract/context_multicall_test.zig");
_ = @import("contract/chain_client_test.zig");
}
Loading