From ff1d2e27551b7c5257a3c18d1f172c2419306fd3 Mon Sep 17 00:00:00 2001 From: Blume1977 Date: Fri, 24 Jul 2026 11:24:03 +0200 Subject: [PATCH] fix(bitbox): sign the registration with a chainId-extended EIP-712 domain The BitBox02 firmware refuses to sign typed data whose EIP712Domain has no chainId and aborts with 'typed data has no chain ID' on the device, so BitBox users could not complete the registration at all. Hardware wallets now sign with the chainId-extended domain; software wallets keep the legacy domain (pinned by the golden signature test) until Aktionariat confirms its re-verification accepts the extended variant. Pair PR: DFXswiss/api#4354 (accepts both domain variants, must deploy first). --- lib/packages/wallet/eip712_signer.dart | 15 +++++++++++- .../wallet/eip712_signer_bitbox_test.dart | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/packages/wallet/eip712_signer.dart b/lib/packages/wallet/eip712_signer.dart index f19f0e789..42a2591a1 100644 --- a/lib/packages/wallet/eip712_signer.dart +++ b/lib/packages/wallet/eip712_signer.dart @@ -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#4354). + final includeChainIdInDomain = credentials is BitboxCredentials; + final Map typedDataMap = { 'types': { 'EIP712Domain': [ {'name': 'name', 'type': 'string'}, {'name': 'version', 'type': 'string'}, + if (includeChainIdInDomain) {'name': 'chainId', 'type': 'uint256'}, ], 'RealUnitUser': [ {'name': 'email', 'type': 'string'}, @@ -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, diff --git a/test/packages/wallet/eip712_signer_bitbox_test.dart b/test/packages/wallet/eip712_signer_bitbox_test.dart index 438ccc787..c44cc1000 100644 --- a/test/packages/wallet/eip712_signer_bitbox_test.dart +++ b/test/packages/wallet/eip712_signer_bitbox_test.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:typed_data'; import 'package:bitbox_flutter/bitbox_manager.dart'; @@ -48,6 +49,28 @@ 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). + test('signs with the chainId-extended EIP-712 domain', () async { + when( + () => manager.signETHTypedMessage(any(), any(), any()), + ).thenAnswer((_) async => Uint8List.fromList([0x01])); + + await signRegistration(); + + final jsonMessage = + verify(() => manager.signETHTypedMessage(any(), any(), captureAny())).captured.single as Uint8List; + final typedData = jsonDecode(utf8.decode(jsonMessage)) as Map; + expect(typedData['domain']['chainId'], 1); + expect( + (typedData['types']['EIP712Domain'] as List).map((e) => e['name']), + containsAll(['name', 'version', 'chainId']), + ); + }); + test('throws SigningCancelledException on empty signature', () async { when( () => manager.signETHTypedMessage(any(), any(), any()),