@universal-signer/core
v0.0.4
Published
Universal blockchain signer for Hardware (Ledger, Trezor), Cloud KMS (AWS, GCP), Turnkey, and Local environments using Viem v2.
Maintainers
Readme
@universal-signer/core
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.
Table of Contents
Features
- 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
Installation
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 |
npm install @universal-signer/core viem @aws-sdk/client-kmsnpm 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-stamperQuick Start
import {
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" },
});Providers
AWS KMS
Uses @aws-sdk/client-kms for signing with AWS Key Management Service.
npm install @aws-sdk/client-kmsPrerequisites
- Key Spec:
ECC_SECG_P256K1 - Key Usage:
SIGN_VERIFY - IAM Permissions:
kms:GetPublicKey,kms:Sign
Usage
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);Configuration
| 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 |
Google Cloud KMS
Uses @google-cloud/kms for signing with Google Cloud Key Management Service.
npm install @google-cloud/kmsPrerequisites
- Algorithm:
EC_SIGN_SECP256K1_SHA256 - Purpose:
ASYMMETRIC_SIGN - IAM Role:
roles/cloudkms.signerVerifier
Usage
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);Configuration
| Property | Type | Required | Description |
| --------------- | --------------- | -------- | -------------------------------------------- |
| name | string | Yes | Full CryptoKeyVersion resource name |
| clientOptions | ClientOptions | No | GCP client configuration (from google-gax) |
Ledger
Uses @ledgerhq/hw-app-eth over USB HID for hardware wallet signing.
npm install @ledgerhq/hw-app-eth @ledgerhq/hw-transport-node-hidPrerequisites
- Ledger device connected via USB
- Ethereum app installed and open
- Blind signing enabled (Settings > Blind signing)
- Linux: Configure udev rules for USB access
Usage
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();Configuration
| 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 |
Extended Account
LedgerAccount extends LocalAccount with:
| Method | Description |
| --------- | --------------------------------------- |
| close() | Closes the USB HID transport connection |
Trezor
Uses @trezor/connect for hardware wallet signing via Trezor Connect.
npm install @trezor/connectPrerequisites
- Trezor device connected
- Trezor Bridge installed (or using WebUSB)
- Valid manifest configuration (required by Trezor)
Usage
import { createTrezorAccount, type TrezorConfig } from "@universal-signer/core";
const config: TrezorConfig = {
// Required: Trezor Connect manifest
email: "[email protected]",
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);Configuration
| 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 |
Turnkey
Uses @turnkey/viem for signing with Turnkey's key management infrastructure.
npm install @turnkey/viem @turnkey/http @turnkey/api-key-stamperUsage
import { 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);Configuration
| 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 |
Local
Wraps Viem's native account functions for development and testing.
Warning: Never use local accounts with real private keys in production.
Usage
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);Configuration
| Property | Type | Required | Description |
| ------------ | -------- | -------- | ---------------------------------- |
| privateKey | Hex | One of | 32-byte private key with 0x prefix |
| mnemonic | string | One of | BIP-39 mnemonic phrase |
API Reference
Exports
// 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";createUniversalClient
Helper function to create a Viem WalletClient from any account.
function createUniversalClient(
account: Account,
chain?: Chain, // Default: mainnet
transport?: Transport, // Default: http()
): WalletClient;normalizeKmsSignature
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 }>;Technical Details
KMS Signature Normalization
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
Supported Operations
| 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 |
Transaction Types
All providers support:
- Legacy transactions
- EIP-2930 (Type 1)
- EIP-1559 (Type 2)
- Contract deployments (no
toaddress)
Troubleshooting
"Invalid DER: Unexpected end of data"
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.
"AWS KMS: Unable to retrieve Public Key"
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.
"GCP KMS: Public Key not found"
Causes:
- Incorrect resource name format
- Missing IAM permissions
- Key version is disabled
Solution: Verify the full resource path and cloudkms.cryptoKeyVersions.viewPublicKey permission.
Ledger Connection Issues
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 "Manifest not set"
Trezor Connect requires a valid manifest with email, appUrl, and appName.
Solution: Ensure all three manifest fields are provided in your configuration.
"Trezor: chainId is required"
Trezor requires an explicit chainId for transaction signing.
Solution: Include chainId in your transaction object.
License
MIT
