GenClient by implementing the EddsaSigner trait. The worked example uses placeholders for the KMS call so you can drop in your provider’s SDK; the structure is the same for AWS CloudHSM, GCP KMS, Azure Key Vault Managed HSM, or HashiCorp Vault transit.
Prerequisites
- Rust 1.75+ and a project that depends on the Grid
client-sdkcrate. - An external key store that can sign raw Ed25519 (RFC 8032) without adding any extra hashing or domain separation.
- The 32-byte Ed25519 public key for that KMS-resident key, fetched once at boot.
AWS KMS specifically does not support Ed25519 as of this writing. Its asymmetric key specs are RSA, NIST P-curves, secp256k1, and SM2. If you need Ed25519 on AWS, use AWS CloudHSM (which exposes Ed25519 via PKCS#11) or AWS KMS External Key Store (XKS) backed by an HSM that does. Providers with native Ed25519 signing include GCP KMS (EC_SIGN_ED25519), Azure Key Vault Managed HSM (EdDSA), and HashiCorp Vault transit (ed25519).
How the SDK splits the work
EddsaSigner is the SDK’s signing-oracle interface. The trait has two required methods:
public_key()returns a cachedGenPublicKey. It must not perform I/O; fetch the key once at construction and store it.sign_bytes()signsmessagewith raw Ed25519. The SDK supplies a canonical 82-byte activation pre-image; do not hash it again or add domain separation.- The default
sign_activation()extracts that pre-image and forwards tosign_bytes(); you almost never override it.
Step 1: Construct the public key handle
Fetch the public key from your KMS once at boot, then wrap it in aGenPublicKey. The wire format is the raw 32-byte Ed25519 public key.
GenPublicKey::from_bytes does the curve check; if the KMS returned a key that isn’t a valid Ed25519 point, you find out here rather than at signing time.
Step 2: Implement EddsaSigner
A KMS adapter is a struct that holds your KMS client handle, the key identifier, and the cached public key. The mock signer in client-sdk/src/test_utils/mock_eddsa_signer.rs shows the same shape backed by ed25519-dalek.
?Send to keep the SDK WASM-compatible. If your KMS client is Send + Sync (it usually will be in a server), you can add those bounds at your own use sites without touching the trait.
Step 3: Submit a transfer
GenClient::sign_and_submit_activation is generic over EddsaSigner; pass your KMS adapter and the SDK does the rest:
- Builds the canonical unsigned activation against
signer.public_key(). - Hands the 82-byte pre-image to
signer.sign_bytes(). - Attaches the returned signature and submits to the RPC endpoint.
KmsSignerError::Sign(...), it surfaces as SdkError::EddsaSigner(_) with the original error preserved as the source.
For payload construction, see Send a transfer programmatically (Rust).
Operational notes
- Latency: A KMS round-trip per transaction is the dominant cost. Budget for it in any timeout you set (
SubmitActivationRequest,waitstrategy). For high-throughput agents, prefer regional endpoints and warm connections. - Public-key caching: Fetch the public key once at boot. The trait contract forbids I/O in
public_key(), and re-fetching on every call would double KMS load. - Retries:
sign_bytesruns after the activation pre-image is committed. If the KMS call fails transiently, it is safe to retry; the pre-image is byte-identical. Retries beyond the activation’svalid_until_blockwill be rejected by the mempool, so retry quickly or rebuild. - Authorization: Lock the KMS key policy down to the IAM role / service identity that runs your Grid client. Anyone who can call
signon that key can move funds from the corresponding account. - Audit: KMS providers log every
signcall. Pair the KMS audit log with the resultingActivationId(logged by the SDK) to get an end-to-end audit trail.
Bring-your-own without the trait
If your signing oracle can’t be wrapped in anasync fn (offline air-gapped flow, hardware wallet with a confirm-on-device prompt, etc.): drive the steps manually:
GenPublicKey::from_bytes: wrap the 32-byte public key.GenClient::build_signable_activation_with_options: produce the canonical unsigned activation.SignableGenExternalActivation::signable_buffer: extract the 82-byte pre-image.- Sign those bytes externally with raw Ed25519 (RFC 8032).
EddsaSignature::new(sig_bytes): wrap the 64-byte signature.SignableGenExternalActivation::into_signed: attach the signature.GenClient::submit_activation: submit.
See also
- Send a transfer programmatically (Rust): the in-process counterpart using
GenSigner. gen wallet: the local wallet store the CLI uses; not relevant once you’ve moved keys to a KMS.

