Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion lib/packages/wallet/eip712_signer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,20 @@ class Eip712Signer {
required bool swissTaxResidence,
required String registrationDate,
}) {
// The BitBox02 firmware refuses to sign typed data whose EIP712Domain has
// no chainId ("typed data has no chain ID" shown on the device), so
// hardware wallets sign with the chainId-extended domain. Software wallets
// keep the legacy chainId-less domain until Aktionariat has confirmed its
// signature re-verification accepts the extended variant — the API
// accepts both (pair PR DFXswiss/api#4542).
final includeChainIdInDomain = credentials is BitboxCredentials;

final Map<String, dynamic> typedDataMap = {
'types': {
'EIP712Domain': [
{'name': 'name', 'type': 'string'},
{'name': 'version', 'type': 'string'},
if (includeChainIdInDomain) {'name': 'chainId', 'type': 'uint256'},
],
'RealUnitUser': [
{'name': 'email', 'type': 'string'},
Expand All @@ -47,7 +56,11 @@ class Eip712Signer {
],
},
'primaryType': 'RealUnitUser',
'domain': {'name': 'RealUnitUser', 'version': '1'},
'domain': {
'name': 'RealUnitUser',
'version': '1',
if (includeChainIdInDomain) 'chainId': chainId,
},
'message': {
'email': email,
'name': name,
Expand Down
39 changes: 37 additions & 2 deletions test/packages/wallet/eip712_signer_bitbox_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:bitbox_flutter/bitbox_manager.dart';
Expand All @@ -23,9 +24,9 @@ void main() {
BitboxCredentials connected() =>
BitboxCredentials('0x000000000000000000000000000000000000dead')..setBitbox(manager);

Future<String> signRegistration() => Eip712Signer.signRegistration(
Future<String> signRegistration({int chainId = 1}) => Eip712Signer.signRegistration(
credentials: connected(),
chainId: 1,
chainId: chainId,
email: 'jk@dfx.swiss',
name: 'Joshua',
type: 'human',
Expand All @@ -48,6 +49,40 @@ void main() {
expect(await signRegistration(), '0xcafebabe');
});

// The BitBox02 firmware rejects typed data whose EIP712Domain has no
// chainId ("typed data has no chain ID" on the device) — hardware-wallet
// registrations must sign the chainId-extended domain. The software-wallet
// path keeps the legacy domain (pinned by the golden signature in
// eip712_signer_test.dart).
// Signs with a non-default chainId so a hardcoded value cannot pass, and asserts the
// EIP712Domain member list exactly: the domain typehash covers the names, their types
// and their order, so any of those drifting changes the digest and the API recovers a
// foreign address. Mirrors the chainId-wiring pin in
// test/integration/eip7702_delegation_bitbox_test.dart.
for (final chainId in const [1, 11155111]) {
test('signs with the chainId-extended EIP-712 domain (chainId $chainId)', () async {
when(
() => manager.signETHTypedMessage(any(), any(), any()),
).thenAnswer((_) async => Uint8List.fromList([0x01]));

await signRegistration(chainId: chainId);

// called(1): a second sign would mean a second on-device confirmation prompt.
final captured = (verify(
() => manager.signETHTypedMessage(captureAny(), any(), captureAny()),
)..called(1)).captured;
final typedData = jsonDecode(utf8.decode(captured[1] as Uint8List)) as Map<String, dynamic>;

expect(captured[0], chainId);
expect(typedData['domain']['chainId'], chainId);
expect(typedData['types']['EIP712Domain'], [
{'name': 'name', 'type': 'string'},
{'name': 'version', 'type': 'string'},
{'name': 'chainId', 'type': 'uint256'},
]);
});
}

test('throws SigningCancelledException on empty signature', () async {
when(
() => manager.signETHTypedMessage(any(), any(), any()),
Expand Down
Loading