Ethereum API

EthereumApp クラスは、Ethereum のアドレス取得、メッセージ署名、トランザクション署名、EIP-712 Typed Data 署名、EIP-7702 Authorization 署名を提供します。

インポート

import {
  EthereumApp,
  DEFAULT_ETH_PATH,
  type SignatureResult,
} from '@openloop/sdk-core'

初期化

const eth = new EthereumApp(transport)

transport は ITransport インターフェースを実装する任意の Transport インスタンスです。

メソッド一覧

メソッド 説明
getAddress(path) アドレスと公開鍵を取得
signPersonalMessage(path, message, chainId?) EIP-191 personal_sign
signTransaction(path, rawTx) RLP エンコード済みトランザクションに署名
signTypedData(path, domainHash, msgHash, chainId?) EIP-712 Typed Data に署名
signAuthorization(path, authorizationRlp) EIP-7702 Authorization に署名
signTypedDataStreaming(path, typedData, chainId?) EIP-712 Typed Data を中身のまま送って署名(デバイスが内容を表示できる)
isEip3009(typedData) Typed Data が EIP-3009 かどうか(送り方の判定に使う)

getAddress

デバイスから Ethereum アドレスと公開鍵を取得します。

async getAddress(path: string): Promise<{
  publicKey: string  // 非圧縮公開鍵 (hex, 65 bytes)
  address: string    // チェックサム付きアドレス ("0x...")
}>

パラメータ

名前 型 説明
path string BIP44 パス(例: "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"


signPersonalMessage

EIP-191 personal_sign でメッセージに署名します。チェーン ID を認識する Openloop 独自の拡張プロトコルを使用します。

async signPersonalMessage(
  path: string,
  message: string,
  chainId?: number
): Promise<SignatureResult>

パラメータ

名前 型 デフォルト 説明
path string — BIP44 パス
message string — メッセージ(UTF-8 文字列または 0x プレフィックス付き hex)
chainId number 1 EVM チェーン ID

返り値: SignatureResult

フィールド 型 説明
v number リカバリー ID (27 or 28)
r string R 値 (32 bytes hex, 0x なし)
s string S 値 (32 bytes hex, 0x なし)

使用例

const sig = await eth.signPersonalMessage(
  DEFAULT_ETH_PATH,
  'Hello, Openloop!',
  1
)
// sig.v = 27
// sig.r = "a1b2c3..."
// sig.s = "d4e5f6..."

注意: 長いメッセージは自動的にチャンク分割されてデバイスに送信されます(最初のチャンク 150 バイト、以降 255 バイトずつ)。


signTransaction

RLP エンコード済みの Ethereum トランザクションに署名します。

async signTransaction(
  path: string,
  rawTx: string
): Promise<SignatureResult>

パラメータ

名前 型 説明
path string BIP44 パス
rawTx string RLP エンコード済みトランザクション (hex, 0x プレフィックスあり/なし)

使用例

const sig = await eth.signTransaction(
  DEFAULT_ETH_PATH,
  'f86c0a8502540be400825208...'
)

signTypedData

EIP-712 Typed Data に署名します。事前にハッシュ化された domainSeparator と message を渡します。

async signTypedData(
  path: string,
  domainSeparatorHash: string,
  messageHash: string,
  chainId?: number
): Promise<SignatureResult>

パラメータ

名前 型 デフォルト 説明
path string — BIP44 パス
domainSeparatorHash string — EIP-712 domain separator ハッシュ (32 bytes hex)
messageHash string — EIP-712 message ハッシュ (32 bytes hex)
chainId number 1 EVM チェーン ID

使用例

const sig = await eth.signTypedData(
  DEFAULT_ETH_PATH,
  'aabbccddee...', // domainSeparatorHash
  '1122334455...', // messageHash
  1
)

signTypedDataStreaming

EIP-712 Typed Data を ハッシュではなく中身のままデバイスへ送って署名します。 デバイスが「何に署名するのか」を画面に出せるようになります。

async signTypedDataStreaming(
  path: string,
  typedData: TypedDataLike,
  chainId?: number
): Promise<SignatureResult>

なぜ 2 つあるのか

デバイスに届くもの デバイスの画面
signTypedData ハッシュ 2 つ ハッシュのみ
signTypedDataStreaming Typed Data そのもの 中身(対応する型のみ)

signTypedData で足りるのは、dApp 側も同じハッシュを表示する場合です(Safe など)。 利用者は 2 つを見比べれば検証できます。

一方、ステーブルコインのガスレス送金(EIP-3009)では、支払うのは一般の利用者で、 64 文字の 16 進を照合させるのは現実的ではありません。金額も送金先も確認できない まま署名することになります。そこでこちらを使います。

パラメータ

名前 型 既定 説明
path string — BIP44 パス
typedData TypedDataLike — { domain, types, primaryType, message }
chainId number 1 チェーン ID

注意

使用例

import { isEip3009 } from '@openloop/sdk-core'

// 中身を見せる価値がある型だけをこちらに流す
if (isEip3009(typedData)) {
  const sig = await eth.signTypedDataStreaming(DEFAULT_ETH_PATH, typedData, 137)
} else {
  const sig = await eth.signTypedData(DEFAULT_ETH_PATH, domainHash, messageHash, 137)
}

isEip3009

Typed Data が EIP-3009(TransferWithAuthorization / ReceiveWithAuthorization) かどうかを判定します。

function isEip3009(typedData: TypedDataLike): boolean

型名・フィールドの並び・message の中身をすべて確認し、完全に一致したときだけ true を返します。1 つでも違えば false です。

判定はデバイス側とまったく同じ規則です。両者がずれると、アプリが「読める画面が 出る」と判断したのにデバイスが出さない(またはその逆)ことになるためです。

この判定が決めるのはどちらの経路で送るかだけで、署名の可否ではありません。 EIP-3009 でない Typed Data も、これまでどおり署名できます。


signAuthorization

EIP-7702 Authorization(EOA に対する Set Code)に署名します。

async signAuthorization(
  path: string,
  authorizationRlp: string
): Promise<SignatureResult>

パラメータ

名前 型 説明
path string BIP44 パス
authorizationRlp string RLP([chain_id, address, nonce]) の hex

使用例

const sig = await eth.signAuthorization(
  DEFAULT_ETH_PATH,
  'c3010a...' // RLP([1, "0x...", 0])
)

ethers.js 連携

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)

// アドレス取得
const { address } = await eth.getAddress(DEFAULT_ETH_PATH)

// プロバイダー(Ethereum ノード接続)
const provider = new ethers.JsonRpcProvider('https://eth.llamarpc.com')

// トランザクション作成
const tx = {
  to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
  value: ethers.parseEther('0.01'),
  gasLimit: 21000n,
  gasPrice: (await provider.getFeeData()).gasPrice,
  nonce: await provider.getTransactionCount(address),
  chainId: 1n,
}

// RLP エンコードして署名
const unsignedTx = ethers.Transaction.from(tx).unsignedSerialized
const sig = await eth.signTransaction(DEFAULT_ETH_PATH, unsignedTx)

// 署名済みトランザクションを構築
const signedTx = ethers.Transaction.from({
  ...tx,
  signature: {
    v: sig.v,
    r: '0x' + sig.r,
    s: '0x' + sig.s,
  },
})

// ブロードキャスト
const txResponse = await provider.broadcastTransaction(signedTx.serialized)

viem 連携

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
)

// viem の署名フォーマットに変換
const signature = `0x${sig.r}${sig.s}${sig.v.toString(16)}` as `0x${string}`

次のステップ