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
- Signing Logic
- Package Setup
- Start Session
- Common Cases
- User Flow
- Code Samples
- 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:
- The application side prepares the path,
chainId, transaction fields or message content. - The SDK establishes a session and sends the data to be signed to UKey Wallet.
- The device displays the Address, Path, Amount, Gas, ChainId, Message Digest or Typed Data fields.
- After the user confirms on the device, the SDK returns
{ r, s, v }or the full hex signature. - The application side uses tools such as
ethers,viemto 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:
tovaluedatanoncegasLimitmaxFeePerGasmaxPriorityFeePerGaschainIdtype: 2 - Legacy gas price:
tovaluedatanoncegasLimitgasPricechainId
- EIP-1559:
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; both0x-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, includingdomain,types,primaryType, andmessage.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
| Focus | Integration note |
|---|---|
| Request result | SDK 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 order | Keep requests to the same device serial. For a signing sequence, use keepSession to reduce repeated PIN or passphrase prompts. |
| UI handling | Subscribe 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
| Scenario | How to verify |
|---|---|
| Transaction signature | Re-serialize the original transaction fields with ethers or viem, then compare the signature hash with what the device showed. |
personal_sign | Run ethers.verifyMessage(message, sig.signature) and compare the recovered address with the expected account. |
| EIP-712 | Use ethers.verifyTypedData(domain, types, message, sig.signature) to validate the structured-data signature. |
| Issue | Suggested handling |
|---|---|
| Large data or unusual fields | The device may reject oversized data or unexpected fields; validate length and format in the app first. |
| Network mismatch | Keep chainId aligned with the Network that will receive the broadcast. |
| Wrong path | The default path is m/44'/60'/6'/0/2, with later accounts or addresses increasing by index; confirm the address before signing. |
| Incomplete gas fields | EIP-1559 transactions need both maxFeePerGas and maxPriorityFeePerGas; classic transactions only provide gasPrice. |
| Device prompt stalls | Do not send concurrent requests to the same device; reuse keepSession during a signing sequence to reduce repeated PIN or passphrase prompts. |
| User rejection or timeout | Expose retry and cancel actions, but do not silently replay the same request. |
Continue with the method-level pages: evmSignTransaction · evmSignMessage · evmSignTypedData.