Skip to main content

EVM Sign

This EVM signing guide covers address confirmation, transaction signing, message signing, and EIP-712 Typed Data with UKey Wallet. The main goal is to keep the app's payload, the device display, and the final broadcast or verification result aligned.

Table of contents

  1. Signing Logic
  2. Package Setup
  3. Start Session
  4. Common Cases
  5. User Flow
  6. Code Samples
  7. Verification And Troubleshooting

Signing Logic

EVM related methods are exposed through the SDK entry used in the examples. After the application prepares the path, chain ID, transaction, or message data, the SDK hands the request to the device for review, and the signature result is returned after the user confirms. Interactions such as PIN, Passphrase, opening an App, confirming an address, transaction, or message are all notified to the application through the UI_REQUEST event.

Basic link for EVM signature:

  1. The application side prepares the path, chainId, transaction fields or message content.
  2. The SDK establishes a session and sends the data to be signed to UKey Wallet.
  3. The device displays the Address, Path, Amount, Gas, ChainId, Message Digest or Typed Data fields.
  4. After the user confirms on the device, the SDK returns { r, s, v } or the full hex signature.
  5. The application side uses tools such as ethers, viem to complete serialization, signature verification or broadcasting.

When accessing, please ensure three things first: the inputs can be correctly parsed by the chain tool library, the device display content is consistent with the front-end expectations, and the return signature can be independently verified.

Package Setup

EVM signatures rely on the connectivity layer and core event definitions. Please complete the transmission access first before processing the signature request.

npm install @ukeyfe/hardware-core @ukeyfe/hardware-common-connect-sdk

Start Session

import ukeySdk from "@ukeyfe/hardware-common-connect-sdk";
// Note: Or use connect-sdk packaged ukeySdk

await ukeySdk.init({ env: "webusb", debug: false, fetchConfig: true });

const [{ connectId }] = await ukeySdk.searchDevices();
const deviceId = (await ukeySdk.getFeatures(connectId)).payload?.device_id;

Before signing, use evmGetAddress(showOnUKey: true) to let the user check the address on the device to avoid path or account selection errors.

Common Cases

Common EVM methods include evmGetAddress, evmSignTransaction, evmSignMessage, and evmSignTypedData. All methods return a Promise, with the result represented by { success, payload }.


Case 1: Read Address

Read the EVM address under the specified path. When it comes to payment collection, account binding or first-time use, enable device-side display.

const sampleAccountPath = ["m", "44'", "60'", "3'", "0", "1"].join("/");
const opResult = await ukeySdk.evmGetAddress(connectId, deviceId, {
path: sampleAccountPath,
showOnUKey: true,
chainId: 1,
});
// Example data: opResult.payload.address, publicKey?, chainCode?

Input Fields

  • path (string | number[]): Derivation route used to locate the EVM account, for instance "44'/60'/2'/0/0".
  • showOnUKey? (boolean): Set this when the address should be shown on UKey Wallet for user verification.
  • chainId? (number): Network ID rendered on the device; prefer the actual target chain.

return

Promise<{ success; payload: { address; path; publicKey?; chainCode? } }>;
type GetAddressResult = {
address: `0x${string}`;
path: string;
publicKey?: string;
chainCode?: string;
};

Case 2: Sign Transaction

Supports EIP‑1559 (type: 2) and classic transactions, the fields before serialization need to be passed in. Except for chainId, numeric fields must be 0x hexadecimal strings (the SDK will strip off leading 0s).

const { success, payload } = await ukeySdk.evmSignTransaction(
connectId,
deviceId,
{
path: sampleAccountPath,
transaction: tx,
keepSession: true,
},
);
// 返回对象形如 { v, r, s, authorizationSignatures? }

Input Fields

  • path (string | number[]): Account derivation path used as the signing source.
  • transaction (object): Unsigned transaction payload. Provide one of the following field sets:
    • EIP-1559: to value data nonce gasLimit maxFeePerGas maxPriorityFeePerGas chainId type: 2
    • Legacy gas price: to value data nonce gasLimit gasPrice chainId
  • keepSession? (boolean): Keep the current device session open for a series of signatures.
  • domain? (string): Human-readable transaction label, such as an ENS name, shown on the device when available.

return

Promise<{ success; payload: { v; r; s } }>;

Can be used with ethers/viem to serialize to raw tx.


Case 3: Sign Message (personal_sign)

const opResult = await ukeySdk.evmSignMessage(connectId, deviceId, {
path: sampleAccountPath,
messageHex,
chainId: 1,
});

Input Fields

  • path: Account path that should produce the message signature.
  • messageHex: Message body encoded as hex; both 0x-prefixed and plain hex forms are accepted.
  • chainId?: Chain identifier used only for the device-side context display.

return

The result may come back as a full hex signature or as split { r, s, v } fields; pass it through ethers.verifyMessage when verifying.


Case 4: Sign Typed Data (EIP-712)

const opResult = await ukeySdk.evmSignTypedData(connectId, deviceId, {
path: sampleAccountPath,
data: typedData,
chainId: 1,
});

Input Fields

  • path: Account path selected for the Typed Data signature.
  • data: EIP-712 v4 payload, including domain, types, primaryType, and message.
  • chainId: Network identifier that must match the typed-data domain.

Typed Data can follow this object shape:

interface TypedData {
domain: {
name?: string;
version?: string;
chainId?: number;
verifyingContract?: string;
salt?: string;
};
types: Record<string, Array<{ name: string; type: string }>>;
primaryType: string;
message: Record<string, unknown>;
}

return

Promise<{ success; payload: { signature } }>;

For verification, pass the returned signature to ethers.verifyTypedData and recover the signer.

User Flow

FocusIntegration note
Request resultSDK actions still resolve through Promises; intermediate prompts such as unlocking, opening the EVM App, and confirming addresses, transactions, messages, Typed Data, or authorization arrive through UI_REQUEST.
Call orderKeep requests to the same device serial. For a signing sequence, use keepSession to reduce repeated PIN or passphrase prompts.
UI handlingSubscribe to UI_REQUEST, Reference the user through the current device action, and keep visible cancel/retry paths available.

Code Samples

Sample: 1559 Tx

import ukeySdk from "@ukeyfe/hardware-common-connect-sdk";
import { UI_REQUEST, UI_RESPONSE } from "@ukeyfe/hardware-core";
import { serialize, TransactionTypes } from "@ethersproject/transactions";

const [{ connectId }] = await ukeySdk.searchDevices();
const deviceId = (await ukeySdk.getFeatures(connectId)).payload?.device_id;

ukeySdk.on(UI_REQUEST.REQUEST_PIN, () => {
// Ask the user to enter PIN on-device, or collect it in custom UI before calling uiResponse
});

await ukeySdk.evmGetAddress(connectId, deviceId, {
path: sampleAccountPath,
showOnUKey: true,
});

const tx = {
to: "0x2b4d6f8091a3c5e7f9b1d3f507192b4d6f8091a3",
value: "0x2386f26fc10000",
data: "0x",
nonce: "0x2",
gasLimit: "0x5dc0",
maxFeePerGas: "0x4a817c800",
maxPriorityFeePerGas: "0x77359400",
chainId: 1,
};

const { success, payload: sig } = await ukeySdk.evmSignTransaction(
connectId,
deviceId,
{
path: sampleAccountPath,
transaction: tx,
keepSession: true,
},
);

const rawTx = serialize(
{ ...tx, type: TransactionTypes.eip1559 },
{
r: sig.r,
s: sig.s,
v: Number(sig.v),
},
);

// Broadcast it through RPC, for instance with ethers.js provider.sendTransaction(rawTx)

Sample: Classic Tx

const legacyTx = {
to: "0xc4d2e1f0a9876543210fedcba9876543210abcde",
value: "0x2386f26fc10000", // 金额参考值:0.01 ETH
data: "0x",
nonce: "0x1",
gasLimit: "0x5208",
gasPrice: "0x3b9aca00", // 参考 gas 单价:1 gwei
chainId: 1,
};

const signatureData = await ukeySdk.evmSignTransaction(connectId, deviceId, {
path: sampleAccountPath,
transaction: legacyTx,
});

Sample: Sign Msg

const message = "Sample EVM message for signing";
const messageHex = Buffer.from(message).toString("hex");

const opResult = await ukeySdk.evmSignMessage(connectId, deviceId, {
path: sampleAccountPath,
messageHex,
chainId: 1,
});
// Example data: opResult.payload.signature -> Verify with ethers.verifyMessage

Sample: Sign Typed

const typedData = {
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
],
Mail: [
{ name: "from", type: "Person" },
{ name: "to", type: "Person" },
{ name: "contents", type: "string" },
],
Person: [
{ name: "name", type: "string" },
{ name: "wallet", type: "address" },
],
},
primaryType: "Mail",
domain: {
name: "UKey Wallet Sample Mail",
version: "1",
chainId: 1,
verifyingContract: "0x2b4d6f8091a3c5e7f9b1d3f507192b4d6f8091a3",
},
message: {
from: { name: "Ava", wallet: "0x4b6d8f0123456789abcdef0123456789abcdef01" },
to: { name: "Liam", wallet: "0xc4d2e1f0a9876543210fedcba9876543210abcde" },
contents: "Sample typed data payload",
},
};

const opResult = await ukeySdk.evmSignTypedData(connectId, deviceId, {
path: sampleAccountPath,
data: typedData,
chainId: 1,
});
// Example data: opResult.payload.signature -> Use ethers.verifyTypedData(...) to restore the signer

Verification And Troubleshooting

ScenarioHow to verify
Transaction signatureRe-serialize the original transaction fields with ethers or viem, then compare the signature hash with what the device showed.
personal_signRun ethers.verifyMessage(message, sig.signature) and compare the recovered address with the expected account.
EIP-712Use ethers.verifyTypedData(domain, types, message, sig.signature) to validate the structured-data signature.
IssueSuggested handling
Large data or unusual fieldsThe device may reject oversized data or unexpected fields; validate length and format in the app first.
Network mismatchKeep chainId aligned with the Network that will receive the broadcast.
Wrong pathThe default path is m/44'/60'/6'/0/2, with later accounts or addresses increasing by index; confirm the address before signing.
Incomplete gas fieldsEIP-1559 transactions need both maxFeePerGas and maxPriorityFeePerGas; classic transactions only provide gasPrice.
Device prompt stallsDo not send concurrent requests to the same device; reuse keepSession during a signing sequence to reduce repeated PIN or passphrase prompts.
User rejection or timeoutExpose retry and cancel actions, but do not silently replay the same request.

Continue with the method-level pages: evmSignTransaction · evmSignMessage · evmSignTypedData.