A unified, type-safe library that provides a single interface for AWS KMS, Google Cloud KMS, Ledger, Trezor, Turnkey, and Local Keys — all compatible with Viem v2.
Write your blockchain signing logic once. Switch providers through configuration.
- Unified API — All providers return a Viem-compatible
LocalAccount - Full Signing Support — Transactions, messages (
signMessage), and typed data (signTypedData/ EIP-712) - Cloud KMS Ready — Automatic ASN.1 DER decoding and EIP-2 signature normalization
- Hardware Wallet Support — Ledger (USB HID) and Trezor (Connect) with proper resource management
- Type-Safe — Full TypeScript support with exported interfaces for all configurations
- Modern Runtime — Built for Bun and Node.js
npm install @universal-signer/core viemThen install only the provider(s) you need:
| Provider | Dependencies |
|---|---|
| AWS KMS | npm install @aws-sdk/client-kms |
| GCP KMS | npm install @google-cloud/kms |
| Ledger | npm install @ledgerhq/hw-app-eth @ledgerhq/hw-transport-node-hid |
| Trezor | npm install @trezor/connect |
| Turnkey | npm install @turnkey/viem @turnkey/http @turnkey/api-key-stamper |
| Local | No additional dependencies |
Example: AWS KMS setup
npm install @universal-signer/core viem @aws-sdk/client-kmsExample: Install all providers
npm install @universal-signer/core viem \
@aws-sdk/client-kms \
@google-cloud/kms \
@ledgerhq/hw-app-eth @ledgerhq/hw-transport-node-hid \
@trezor/connect \
@turnkey/viem @turnkey/http @turnkey/api-key-stamperimport {
createUniversalClient,
createAwsAccount,
} from "@universal-signer/core";
import { mainnet } from "viem/chains";
import { http } from "viem";
// 1. Create an account (using AWS KMS as example)
const account = await createAwsAccount({
keyId: "alias/my-eth-key",
region: "us-east-1",
});
// 2. Create a wallet client
const client = createUniversalClient(account, mainnet, http());
// 3. Use standard Viem methods
const hash = await client.sendTransaction({
to: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
value: 1_000_000_000_000_000n, // 0.001 ETH
});
// Sign a message
const signature = await account.signMessage({
message: "Hello, Ethereum!",
});
// Sign typed data (EIP-712)
const typedSignature = await account.signTypedData({
domain: {
name: "My App",
version: "1",
chainId: 1,
},
types: {
Message: [{ name: "content", type: "string" }],
},
primaryType: "Message",
message: { content: "Hello" },
});Uses @aws-sdk/client-kms for signing with AWS Key Management Service.
npm install @aws-sdk/client-kms- Key Spec:
ECC_SECG_P256K1 - Key Usage:
SIGN_VERIFY - IAM Permissions:
kms:GetPublicKey,kms:Sign
import { createAwsAccount, type AwsKmsConfig } from "@universal-signer/core";
const config: AwsKmsConfig = {
// Key ID, ARN, or alias
keyId:
"arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012",
// AWS region (default: "us-east-1")
region: "us-east-1",
// Optional: explicit credentials (defaults to AWS SDK credential chain)
credentials: {
accessKeyId: "AKIA...",
secretAccessKey: "...",
},
};
const account = await createAwsAccount(config);
console.log("Address:", account.address);| Property | Type | Required | Default | Description |
|---|---|---|---|---|
keyId |
string |
Yes | — | KMS key ID, ARN, or alias |
region |
string |
No | "us-east-1" |
AWS region |
credentials |
object |
No | SDK default | AWS credentials |
Uses @google-cloud/kms for signing with Google Cloud Key Management Service.
npm install @google-cloud/kms- Algorithm:
EC_SIGN_SECP256K1_SHA256 - Purpose:
ASYMMETRIC_SIGN - IAM Role:
roles/cloudkms.signerVerifier
import { createGcpAccount, type GcpKmsConfig } from "@universal-signer/core";
const config: GcpKmsConfig = {
// Full resource name of the CryptoKeyVersion
name: "projects/my-project/locations/us-central1/keyRings/my-ring/cryptoKeys/eth-key/cryptoKeyVersions/1",
// Optional: GCP client options
clientOptions: {
projectId: "my-project",
// credentials: require("./service-account.json"),
},
};
const account = await createGcpAccount(config);
console.log("Address:", account.address);| Property | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Full CryptoKeyVersion resource name |
clientOptions |
ClientOptions |
No | GCP client configuration (from google-gax) |
Uses @ledgerhq/hw-app-eth over USB HID for hardware wallet signing.
npm install @ledgerhq/hw-app-eth @ledgerhq/hw-transport-node-hid- Ledger device connected via USB
- Ethereum app installed and open
- Blind signing enabled (Settings > Blind signing)
- Linux: Configure udev rules for USB access
import {
createLedgerAccount,
type LedgerConfig,
type LedgerAccount,
} from "@universal-signer/core";
const config: LedgerConfig = {
// BIP-44 derivation path (default: "44'/60'/0'/0/0")
derivationPath: "44'/60'/0'/0/0",
};
const account: LedgerAccount = await createLedgerAccount(config);
console.log("Address:", account.address);
// Sign transactions, messages, or typed data
const signature = await account.signMessage({ message: "Hello" });
// Important: Close the transport when done
await account.close();| Property | Type | Required | Default | Description |
|---|---|---|---|---|
derivationPath |
string |
No | "44'/60'/0'/0/0" |
BIP-44 derivation path |
transport |
Transport |
No | Auto-created | Custom HID transport instance |
LedgerAccount extends LocalAccount with:
| Method | Description |
|---|---|
close() |
Closes the USB HID transport connection |
Uses @trezor/connect for hardware wallet signing via Trezor Connect.
npm install @trezor/connect- Trezor device connected
- Trezor Bridge installed (or using WebUSB)
- Valid manifest configuration (required by Trezor)
import { createTrezorAccount, type TrezorConfig } from "@universal-signer/core";
const config: TrezorConfig = {
// Required: Trezor Connect manifest
email: "developer@myapp.com",
appUrl: "https://myapp.com",
appName: "My Application",
// Optional: BIP-44 derivation path
derivationPath: "m/44'/60'/0'/0/0",
};
const account = await createTrezorAccount(config);
console.log("Address:", account.address);| Property | Type | Required | Default | Description |
|---|---|---|---|---|
email |
string |
Yes | — | Contact email for manifest |
appUrl |
string |
Yes | — | Application URL for manifest |
appName |
string |
Yes | — | Application name for manifest |
derivationPath |
string |
No | "m/44'/60'/0'/0/0" |
BIP-44 derivation path |
Uses @turnkey/viem for signing with Turnkey's key management infrastructure.
npm install @turnkey/viem @turnkey/http @turnkey/api-key-stamperimport { createTurnkeyAccount } from "@universal-signer/core";
const account = await createTurnkeyAccount({
baseUrl: "https://api.turnkey.com",
apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY!,
apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY!,
organizationId: process.env.TURNKEY_ORG_ID!,
privateKeyId: process.env.TURNKEY_PRIVATE_KEY_ID!,
});
console.log("Address:", account.address);| Property | Type | Required | Description |
|---|---|---|---|
baseUrl |
string |
Yes | Turnkey API base URL |
apiPublicKey |
string |
Yes | Turnkey API public key |
apiPrivateKey |
string |
Yes | Turnkey API private key |
organizationId |
string |
Yes | Turnkey organization ID |
privateKeyId |
string |
Yes | Private key or wallet ID in Turnkey |
Wraps Viem's native account functions for development and testing.
Warning: Never use local accounts with real private keys in production.
import { createLocalAccount } from "@universal-signer/core";
// From private key
const account = createLocalAccount({
privateKey:
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
});
// Or from mnemonic
const accountFromMnemonic = createLocalAccount({
mnemonic: "test test test test test test test test test test test junk",
});
console.log("Address:", account.address);| Property | Type | Required | Description |
|---|---|---|---|
privateKey |
Hex |
One of | 32-byte private key with 0x prefix |
mnemonic |
string |
One of | BIP-39 mnemonic phrase |
// Provider functions
export { createAwsAccount } from "./providers/aws";
export { createGcpAccount } from "./providers/gcp";
export { createLedgerAccount } from "./providers/ledger";
export { createLocalAccount } from "./providers/local";
export { createTrezorAccount } from "./providers/trezor";
export { createTurnkeyAccount } from "./providers/turnkey";
// Configuration types
export type { AwsKmsConfig } from "./providers/aws";
export type { GcpKmsConfig } from "./providers/gcp";
export type { LedgerConfig, LedgerAccount } from "./providers/ledger";
export type { TrezorConfig } from "./providers/trezor";
// Utilities
export { normalizeKmsSignature } from "./utils/kms";
export { createUniversalClient } from "./index";Helper function to create a Viem WalletClient from any account.
function createUniversalClient(
account: Account,
chain?: Chain, // Default: mainnet
transport?: Transport, // Default: http()
): WalletClient;Low-level utility for converting KMS DER signatures to Ethereum format.
function normalizeKmsSignature(
derSignature: Uint8Array | Buffer,
digest: Hash,
expectedAddress: string,
): Promise<{ r: Hex; s: Hex; v: bigint }>;Cloud KMS providers return ECDSA signatures in ASN.1 DER format. Ethereum requires raw (r, s, v) signatures. This library handles the conversion:
- DER Parsing — Extracts
randsintegers from the ASN.1 structure - EIP-2 Normalization — Ensures
s <= secp256k1.n / 2to prevent signature malleability - Recovery ID — Determines
v(27 or 28) by trial recovery against the known public key
| Provider | signTransaction |
signMessage |
signTypedData |
|---|---|---|---|
| AWS KMS | Yes | Yes | Yes |
| GCP KMS | Yes | Yes | Yes |
| Ledger | Yes | Yes | Yes |
| Trezor | Yes | Yes | Yes |
| Turnkey | Yes | Yes | Yes |
| Local | Yes | Yes | Yes |
All providers support:
- Legacy transactions
- EIP-2930 (Type 1)
- EIP-1559 (Type 2)
- Contract deployments (no
toaddress)
The KMS returned a malformed signature. This can happen due to:
- Network issues truncating the response
- Incorrect key configuration
Solution: Retry the operation. If persistent, verify your KMS key configuration.
Causes:
- Incorrect key ID or ARN
- Missing IAM permissions
- Key is disabled or pending deletion
Solution: Verify the key exists and your credentials have kms:GetPublicKey permission.
Causes:
- Incorrect resource name format
- Missing IAM permissions
- Key version is disabled
Solution: Verify the full resource path and cloudkms.cryptoKeyVersions.viewPublicKey permission.
Causes:
- Another application has the device open (Ledger Live, browser wallet)
- Ethereum app not open on device
- USB permissions (Linux)
Solutions:
- Close Ledger Live and any browser wallets
- Open the Ethereum app on your Ledger
- On Linux, add udev rules:
# /etc/udev/rules.d/20-hw1.rules SUBSYSTEM=="usb", ATTR{idVendor}=="2c97", MODE="0666"
Trezor Connect requires a valid manifest with email, appUrl, and appName.
Solution: Ensure all three manifest fields are provided in your configuration.
Trezor requires an explicit chainId for transaction signing.
Solution: Include chainId in your transaction object.
MIT