diff --git a/bindings/node-adapters/__test__/viem.spec.mjs b/bindings/node-adapters/__test__/viem.spec.mjs index 8748047fb..2668edeed 100644 --- a/bindings/node-adapters/__test__/viem.spec.mjs +++ b/bindings/node-adapters/__test__/viem.spec.mjs @@ -3,7 +3,8 @@ import assert from 'node:assert/strict'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { createWallet, getWallet, signMessage as owsSignMessage } from '@open-wallet-standard/core'; +import { createWallet, getWallet, signMessage as owsSignMessage, importWalletPrivateKey } from '@open-wallet-standard/core'; +import { privateKeyToAccount } from 'viem/accounts'; import { owsToViemAccount } from '../src/viem.js'; describe('@open-wallet-standard/adapters — viem', () => { @@ -59,6 +60,27 @@ describe('@open-wallet-standard/adapters — viem', () => { const td = { domain: { name: 'T', version: '1', chainId: '1', verifyingContract: '0x0000000000000000000000000000000000000001' }, types: { EIP712Domain: [{ name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' }], M: [{ name: 'c', type: 'string' }] }, primaryType: 'M', message: { c: 'D' } }; assert.equal(await account.signTypedData(td), await account.signTypedData(td)); }); + it('signTypedData matches viem for a uint256-bearing message', async () => { + // A wallet imported from a known key so the signature can be compared to + // viem's own privateKeyToAccount. The message carries uint256 bigints and + // omits EIP712Domain, matching how a real viem walletClient signs. + const pkVault = mkdtempSync(join(tmpdir(), 'ows-viem-pk-')); + const pk = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; + importWalletPrivateKey('viem-pk-test', pk.slice(2), undefined, pkVault, 'evm'); + const account = owsToViemAccount('viem-pk-test', { chain: 'eip155:1', vaultPath: pkVault }); + const reference = privateKeyToAccount(pk); + const typedData = { + domain: { name: 'Test', version: '1', chainId: 1, verifyingContract: '0x0000000000000000000000000000000000000001' }, + types: { Msg: [{ name: 'id', type: 'uint256' }, { name: 'ids', type: 'uint256[]' }] }, + primaryType: 'Msg', + message: { id: 2n ** 200n, ids: [0n, 1n, 2n ** 256n - 1n] }, + }; + try { + assert.equal(await account.signTypedData(typedData), await reference.signTypedData(typedData)); + } finally { + rmSync(pkVault, { recursive: true, force: true }); + } + }); it('signTransaction returns RLP-encoded signed transaction', async () => { const account = owsToViemAccount(walletName, { vaultPath: vaultDir }); const tx = { to: '0x0000000000000000000000000000000000000001', value: 0n, chainId: 1, type: 'eip1559', maxFeePerGas: 1000000000n, maxPriorityFeePerGas: 1000000000n }; diff --git a/bindings/node-adapters/src/viem.js b/bindings/node-adapters/src/viem.js index 4fbc51218..da73150bf 100644 --- a/bindings/node-adapters/src/viem.js +++ b/bindings/node-adapters/src/viem.js @@ -1,6 +1,15 @@ const { getWallet, signMessage, signTypedData, signTransaction } = require("@open-wallet-standard/core"); const { toAccount } = require("viem/accounts"); +// Encode a bigint as the core's EIP-712 parser expects a uint value: even-length +// hex. Decimal uints above 2^128 are rejected, and hex must have an even number +// of digits. Negative values fall back to decimal (int types). +function bigintToOwsHex(value) { + if (value < 0n) return value.toString(); + const hex = value.toString(16); + return `0x${hex.length % 2 === 1 ? `0${hex}` : hex}`; +} + function owsToViemAccount(walletNameOrId, options = {}) { const chain = options.chain ?? "eip155:1"; const wallet = getWallet(walletNameOrId, options.vaultPath); @@ -33,7 +42,23 @@ function owsToViemAccount(walletNameOrId, options = {}) { return serializeTransaction(transaction, { r, s, yParity }); }, async signTypedData(typedData) { - const result = signTypedData(walletNameOrId, chain, JSON.stringify(typedData), options.passphrase, options.index, options.vaultPath); + const { getTypesForEIP712Domain } = require("viem"); + // viem's signTypedData action adds EIP712Domain to `types` before calling an + // account; a direct account.signTypedData() call does not. Add it when absent + // so the core resolves the domain type. A caller-supplied EIP712Domain wins. + const payload = { + ...typedData, + types: { + EIP712Domain: getTypesForEIP712Domain({ domain: typedData.domain }), + ...typedData.types, + }, + }; + // The core parses the JSON payload and expects uint values as even-length hex. + // JSON.stringify cannot serialize bigints, so encode them here. + const json = JSON.stringify(payload, (_key, value) => + typeof value === "bigint" ? bigintToOwsHex(value) : value + ); + const result = signTypedData(walletNameOrId, chain, json, options.passphrase, options.index, options.vaultPath); return result.signature.startsWith("0x") ? result.signature : `0x${result.signature}`; }, });