Skip to content

Commit ecf217a

Browse files
joalavedraclaude
andcommitted
feat: add signTypedData helper for EIP-712 typed-data signing
Contract SDKs (ethers, viem, Safe/Zodiac, GnosisPay account-kit) produce typed data without an EIP712Domain entry in `types`, but eth_signTypedData_v4 requires one. `signTypedData(provider, address, typedData)` re-adds it in canonical field order, serializes bigint values, and signs via the embedded wallet's EIP-1193 provider — so signatures recover correctly on-chain without hand-rolling the payload in app code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9b67e10 commit ecf217a

4 files changed

Lines changed: 129 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@openfort/react-native': minor
3+
---
4+
5+
Add `signTypedData` helper for EIP-712 typed-data signing.
6+
7+
Signs ethers/viem-style typed data (`{ domain, types, primaryType, message }`) via
8+
`eth_signTypedData_v4`, re-adding the required `EIP712Domain` type in canonical field
9+
order and serializing `bigint` values so the signature recovers on-chain. This unblocks
10+
signing typed data produced by contract SDKs (Safe/Zodiac, GnosisPay account-kit, ...)
11+
from React Native without hand-rolling the payload.

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,25 @@
1818

1919
Quickstart sample:
2020
https://github.com/openfort-xyz/react-native-auth-sample
21+
22+
## Signing EIP-712 typed data
23+
24+
Contract SDKs (ethers, viem, Safe/Zodiac, GnosisPay account-kit, ...) produce typed
25+
data without an `EIP712Domain` entry in `types`, but `eth_signTypedData_v4` requires
26+
one. `signTypedData` re-adds it (in canonical order) and serializes `bigint` values,
27+
so the signature recovers correctly on-chain. Use an EOA wallet for plain ECDSA
28+
signatures.
29+
30+
```ts
31+
import { signTypedData, useEmbeddedEthereumWallet } from '@openfort/react-native'
32+
33+
const wallet = useEmbeddedEthereumWallet()
34+
if (wallet.status === 'connected') {
35+
const signature = await signTypedData(wallet.provider, wallet.activeWallet.address, {
36+
domain: { name: 'MyApp', version: '1', chainId: 1, verifyingContract: '0xCcCC...' },
37+
primaryType: 'Mail',
38+
types: { Mail: [{ name: 'contents', type: 'string' }] },
39+
message: { contents: 'Hello, Openfort' },
40+
})
41+
}
42+
```

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ export * from './constants'
4646
export * from './core'
4747
// Re-export all hooks
4848
export * from './hooks'
49+
export type { TypedDataInput } from './lib/signTypedData'
50+
// Re-export the EIP-712 typed-data signing helper
51+
export { signTypedData } from './lib/signTypedData'
4952
// Re-export native functionality
5053
export * from './native'
5154
// Re-export all types from the main types module

src/lib/signTypedData.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { OpenfortEmbeddedEthereumWalletProvider } from '../types/wallet'
2+
3+
/**
4+
* EIP-712 domain fields in canonical order.
5+
*
6+
* `eth_signTypedData_v4` requires an explicit `EIP712Domain` entry in `types`, but
7+
* the typed data produced by most contract libraries (ethers, viem, Safe/Zodiac,
8+
* GnosisPay account-kit, ...) omits it. We re-add only the fields that are present,
9+
* in canonical order, so the digest matches what the verifying contract computes.
10+
*/
11+
const EIP712_DOMAIN_FIELDS: { name: string; type: string }[] = [
12+
{ name: 'name', type: 'string' },
13+
{ name: 'version', type: 'string' },
14+
{ name: 'chainId', type: 'uint256' },
15+
{ name: 'verifyingContract', type: 'address' },
16+
{ name: 'salt', type: 'bytes32' },
17+
]
18+
19+
/**
20+
* Ethers-style EIP-712 typed data (i.e. without an `EIP712Domain` entry in `types`).
21+
*/
22+
export interface TypedDataInput {
23+
/** EIP-712 domain separator fields (e.g. `chainId`, `verifyingContract`). */
24+
domain: Record<string, unknown>
25+
/** Struct definitions, keyed by type name (no `EIP712Domain` needed). */
26+
types: Record<string, { name: string; type: string }[]>
27+
/** The primary struct being signed. */
28+
primaryType: string
29+
/** The values for the primary struct. */
30+
message: Record<string, unknown>
31+
}
32+
33+
function domainTypes(domain: Record<string, unknown>): { name: string; type: string }[] {
34+
return EIP712_DOMAIN_FIELDS.filter((field) => domain[field.name] !== undefined)
35+
}
36+
37+
function bigintReplacer(_key: string, value: unknown): unknown {
38+
return typeof value === 'bigint' ? value.toString() : value
39+
}
40+
41+
/**
42+
* Sign EIP-712 typed data with an embedded wallet via `eth_signTypedData_v4`.
43+
*
44+
* Pass typed data in the ethers/viem shape (`{ domain, types, primaryType, message }`)
45+
* exactly as produced by contract SDKs. This helper re-adds the required `EIP712Domain`
46+
* type (in canonical field order) and serializes any `bigint` values, so the resulting
47+
* signature recovers correctly on-chain.
48+
*
49+
* The wallet should be an EOA (`AccountTypeEnum.EOA`) so the signature is plain ECDSA;
50+
* smart-account owners require ERC-1271 contract signatures, which this does not produce.
51+
*
52+
* @param provider - EIP-1193 provider from a connected embedded Ethereum wallet
53+
* @param address - The signer address (the active embedded wallet)
54+
* @param typedData - Ethers-style typed data: `{ domain, types, primaryType, message }`
55+
* @returns The hex signature
56+
*
57+
* @example
58+
* ```ts
59+
* import { signTypedData, useEmbeddedEthereumWallet } from '@openfort/react-native'
60+
*
61+
* const wallet = useEmbeddedEthereumWallet()
62+
* if (wallet.status === 'connected') {
63+
* const signature = await signTypedData(wallet.provider, wallet.activeWallet.address, {
64+
* domain: { name: 'MyApp', version: '1', chainId: 1, verifyingContract: '0xCcCC...' },
65+
* primaryType: 'Mail',
66+
* types: { Mail: [{ name: 'contents', type: 'string' }] },
67+
* message: { contents: 'Hello, Openfort' },
68+
* })
69+
* }
70+
* ```
71+
*/
72+
export async function signTypedData(
73+
provider: OpenfortEmbeddedEthereumWalletProvider,
74+
address: string,
75+
typedData: TypedDataInput
76+
): Promise<string> {
77+
const payload = JSON.stringify(
78+
{
79+
domain: typedData.domain,
80+
primaryType: typedData.primaryType,
81+
types: { EIP712Domain: domainTypes(typedData.domain), ...typedData.types },
82+
message: typedData.message,
83+
},
84+
bigintReplacer
85+
)
86+
87+
const signature = await provider.request({
88+
method: 'eth_signTypedData_v4',
89+
params: [address.toLowerCase(), payload],
90+
})
91+
92+
return signature as string
93+
}

0 commit comments

Comments
 (0)