The EthereumApp class provides Ethereum address
retrieval, message signing, transaction signing, EIP-712 typed data
signing, and EIP-7702 authorization signing.
import {
EthereumApp,
DEFAULT_ETH_PATH,
type SignatureResult,
} from '@openloop/sdk-core'const eth = new EthereumApp(transport)transport is any transport instance that implements the
ITransport interface.
| Method | Description |
|---|---|
getAddress(path) |
Retrieves the address and public key |
signPersonalMessage(path, message, chainId?) |
EIP-191 personal_sign |
signTransaction(path, rawTx) |
Signs an RLP-encoded transaction |
signTypedData(path, domainHash, msgHash, chainId?) |
Signs EIP-712 typed data |
signAuthorization(path, authorizationRlp) |
Signs an EIP-7702 authorization |
Retrieves the Ethereum address and public key from the device.
async getAddress(path: string): Promise<{
publicKey: string // uncompressed public key (hex, 65 bytes)
address: string // checksummed address ("0x...")
}>| Name | Type | Description |
|---|---|---|
path |
string |
BIP44 path (e.g. "44'/60'/0'/0/0") |
import { DEFAULT_ETH_PATH } from '@openloop/sdk-core'
const { address, publicKey } = await eth.getAddress(DEFAULT_ETH_PATH)
// address: "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18"
// publicKey: "04a1b2c3d4..."DEFAULT_ETH_PATH = "44'/60'/0'/0/0"
Signs a message with EIP-191 personal_sign. It uses the Openloop-specific extended protocol that is aware of the chain ID.
async signPersonalMessage(
path: string,
message: string,
chainId?: number
): Promise<SignatureResult>| Name | Type | Default | Description |
|---|---|---|---|
path |
string |
— | BIP44 path |
message |
string |
— | The message — a UTF-8 string or 0x-prefixed hex |
chainId |
number |
1 |
EVM chain ID |
SignatureResult| Field | Type | Description |
|---|---|---|
v |
number |
Recovery ID (27 or 28) |
r |
string |
R value (32 bytes hex, no 0x) |
s |
string |
S value (32 bytes hex, no 0x) |
const sig = await eth.signPersonalMessage(
DEFAULT_ETH_PATH,
'Hello, Openloop!',
1
)
// sig.v = 27
// sig.r = "a1b2c3..."
// sig.s = "d4e5f6..."Note: Long messages are split into chunks automatically before they are sent to the device — 150 bytes for the first chunk, then 255 bytes each.
Signs an RLP-encoded Ethereum transaction.
async signTransaction(
path: string,
rawTx: string
): Promise<SignatureResult>| Name | Type | Description |
|---|---|---|
path |
string |
BIP44 path |
rawTx |
string |
RLP-encoded transaction (hex, with or without the 0x
prefix) |
const sig = await eth.signTransaction(
DEFAULT_ETH_PATH,
'f86c0a8502540be400825208...'
)Signs EIP-712 typed data. Pass the pre-hashed domainSeparator and message.
async signTypedData(
path: string,
domainSeparatorHash: string,
messageHash: string,
chainId?: number
): Promise<SignatureResult>| Name | Type | Default | Description |
|---|---|---|---|
path |
string |
— | BIP44 path |
domainSeparatorHash |
string |
— | EIP-712 domain separator hash (32 bytes hex) |
messageHash |
string |
— | EIP-712 message hash (32 bytes hex) |
chainId |
number |
1 |
EVM chain ID |
const sig = await eth.signTypedData(
DEFAULT_ETH_PATH,
'aabbccddee...', // domainSeparatorHash
'1122334455...', // messageHash
1
)Signs an EIP-7702 authorization — Set Code for an EOA.
async signAuthorization(
path: string,
authorizationRlp: string
): Promise<SignatureResult>| Name | Type | Description |
|---|---|---|
path |
string |
BIP44 path |
authorizationRlp |
string |
Hex of RLP([chain_id, address, nonce]) |
const sig = await eth.signAuthorization(
DEFAULT_ETH_PATH,
'c3010a...' // RLP([1, "0x...", 0])
)import { ethers } from 'ethers'
import { EthereumApp, DEFAULT_ETH_PATH } from '@openloop/sdk-core'
import { WebHidTransport } from '@openloop/transport-webhid'
const transport = await WebHidTransport.connect()
const eth = new EthereumApp(transport)
// Get the address
const { address } = await eth.getAddress(DEFAULT_ETH_PATH)
// Provider (connection to an Ethereum node)
const provider = new ethers.JsonRpcProvider('https://eth.llamarpc.com')
// Build the transaction
const tx = {
to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
value: ethers.parseEther('0.01'),
gasLimit: 21000n,
gasPrice: (await provider.getFeeData()).gasPrice,
nonce: await provider.getTransactionCount(address),
chainId: 1n,
}
// RLP-encode and sign
const unsignedTx = ethers.Transaction.from(tx).unsignedSerialized
const sig = await eth.signTransaction(DEFAULT_ETH_PATH, unsignedTx)
// Assemble the signed transaction
const signedTx = ethers.Transaction.from({
...tx,
signature: {
v: sig.v,
r: '0x' + sig.r,
s: '0x' + sig.s,
},
})
// Broadcast
const txResponse = await provider.broadcastTransaction(signedTx.serialized)import { createPublicClient, http, parseEther } from 'viem'
import { mainnet } from 'viem/chains'
import { EthereumApp, DEFAULT_ETH_PATH } from '@openloop/sdk-core'
import { WebHidTransport } from '@openloop/transport-webhid'
const transport = await WebHidTransport.connect()
const eth = new EthereumApp(transport)
const { address } = await eth.getAddress(DEFAULT_ETH_PATH)
// personal_sign
const sig = await eth.signPersonalMessage(
DEFAULT_ETH_PATH,
'Hello from viem!',
1
)
// Convert to viem's signature format
const signature = `0x${sig.r}${sig.s}${sig.v.toString(16)}` as `0x${string}`