Skip to content
Draft
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
24 changes: 23 additions & 1 deletion assets/languages/strings_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,28 @@
"location": "Ort",
"logout": "Abmelden",
"max": "Max",
"migrateBitbox": "Auf BitBox umziehen",
"migrateBitboxAlreadyLinkedError": "Diese BitBox-Adresse ist bereits mit einem anderen Konto verknüpft. Kontaktieren Sie den Support, falls Sie dies für einen Fehler halten.",
"migrateBitboxCompleting": "Umzug wird abgeschlossen…",
"migrateBitboxIntroBalance": "Aktueller Bestand",
"migrateBitboxIntroDescription": "Verbinden Sie Ihre BitBox, registrieren Sie sie im Aktienregister und übertragen Sie Ihr gesamtes REALU-Guthaben in einem Durchlauf. Ihre Software-Wallet bleibt danach als Backup erhalten.",
"migrateBitboxIntroTitle": "Umzug auf Ihre BitBox",
"migrateBitboxLinking": "Wallet wird verknüpft…",
"migrateBitboxPreparingTransfer": "Guthaben wird vorbereitet…",
"migrateBitboxRegisterConfirmHint": "Bestätigen Sie die Registrierung auf Ihrer BitBox, um fortzufahren.",
"migrateBitboxRegisterCta": "Wallet registrieren",
"migrateBitboxRegisterDescription": "Ihre BitBox wird Ihrem bestehenden Konto hinzugefügt und im Aktienregister registriert.",
"migrateBitboxRegisterTitle": "BitBox registrieren",
"migrateBitboxRegistrationMissingError": "Ihre Software-Wallet ist noch nicht registriert. Bitte schliessen Sie zuerst die normale Registrierung ab und versuchen Sie den Umzug danach erneut.",
"migrateBitboxRegistrationPendingInfo": "Ihre Registrierung wird geprüft. Ihr REALU-Guthaben bleibt auf Ihrer Software-Wallet — Sie können den Umzug fortsetzen, sobald die Prüfung abgeschlossen ist.",
"migrateBitboxSettling": "Transfer wird bestätigt…",
"migrateBitboxSettlingTimeoutInfo": "Die Übertragung läuft noch auf der Blockchain. Die App bleibt vorerst auf Ihrer Software-Wallet. Öffnen Sie den Umzug später erneut — er wird automatisch fortgesetzt.",
"migrateBitboxStart": "Loslegen",
"migrateBitboxSuccessDescription": "Ihre BitBox ist jetzt Ihre aktive Wallet und hält Ihr REALU-Guthaben. Ihre Software-Wallet bleibt als Backup erhalten.",
"migrateBitboxSuccessTitle": "Umzug abgeschlossen",
"migrateBitboxTransferCta": "Guthaben übertragen",
"migrateBitboxTransferDescription": "Ihr gesamtes REALU-Guthaben wird auf Ihre neue BitBox-Adresse übertragen. Dabei fallen keine Gasgebühren an.",
"migrateBitboxTransferTitle": "Guthaben übertragen",
"month": "Monat",
"name": "Name",
"networkMainnet": "Mainnet",
Expand Down Expand Up @@ -461,4 +483,4 @@
"youPay": "Sie bezahlen",
"youReceive": "Sie erhalten",
"youSell": "Sie verkaufen"
}
}
24 changes: 23 additions & 1 deletion assets/languages/strings_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,28 @@
"location": "Location",
"logout": "Logout",
"max": "Max",
"migrateBitbox": "Move to BitBox",
"migrateBitboxAlreadyLinkedError": "This BitBox address is already linked to a different account. Contact support if you believe this is a mistake.",
"migrateBitboxCompleting": "Finishing move…",
"migrateBitboxIntroBalance": "Current balance",
"migrateBitboxIntroDescription": "Connect your BitBox, register it in the share register, and transfer your full REALU balance in one flow. Your software wallet stays available as a backup afterwards.",
"migrateBitboxIntroTitle": "Move to your BitBox",
"migrateBitboxLinking": "Linking wallet…",
"migrateBitboxPreparingTransfer": "Preparing balance…",
"migrateBitboxRegisterConfirmHint": "Confirm the registration on your BitBox to continue.",
"migrateBitboxRegisterCta": "Register wallet",
"migrateBitboxRegisterDescription": "Your BitBox will be added to your existing account and registered in the share register.",
"migrateBitboxRegisterTitle": "Register your BitBox",
"migrateBitboxRegistrationMissingError": "Your software wallet is not registered yet. Please complete the normal registration first, then try the move again.",
"migrateBitboxRegistrationPendingInfo": "Your registration is being reviewed. Your REALU balance stays on your software wallet — you can continue the move once the review is complete.",
"migrateBitboxSettling": "Confirming transfer…",
"migrateBitboxSettlingTimeoutInfo": "The transfer is still processing on the blockchain. The app will remain on your software wallet for now. Open the move again later — it will continue automatically.",
"migrateBitboxStart": "Get started",
"migrateBitboxSuccessDescription": "Your BitBox is now your active wallet and holds your REALU balance. Your software wallet remains available as a backup.",
"migrateBitboxSuccessTitle": "Move complete",
"migrateBitboxTransferCta": "Transfer balance",
"migrateBitboxTransferDescription": "Your full REALU balance will be transferred to your new BitBox address. This does not cost any gas.",
"migrateBitboxTransferTitle": "Transfer your balance",
"month": "Month",
"name": "Name",
"networkMainnet": "Mainnet",
Expand Down Expand Up @@ -461,4 +483,4 @@
"youPay": "You pay",
"youReceive": "You receive",
"youSell": "You sell"
}
}
10 changes: 10 additions & 0 deletions lib/packages/repository/wallet_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ class WalletRepository {
Future<int> createViewWallet(String name, WalletType type, String address) =>
_appDatabase.insertWallet(name, '', address, type.index);

/// Returns the row id of an existing BitBox wallet with [address], or null.
/// Used by the migration wizard to make committing a paired device idempotent.
Future<int?> getBitboxWalletIdByAddress(String address) async {
final info = await _appDatabase.getWalletByTypeAndAddress(
WalletType.bitbox.index,
address,
);
return info?.id;
}

/// Returns the wallet row with the encrypted seed *still encrypted*. Use this
/// at app startup so we don't pay the mnemonic-decrypt / BIP32-derivation
/// cost just to render the dashboard — the cached address is enough.
Expand Down
58 changes: 41 additions & 17 deletions lib/packages/service/balance_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,23 +59,7 @@ class BalanceService {

if (response.statusCode == 200) {
if (generation == _syncGeneration) _accountMissing = false;

final json = jsonDecode(response.body);
final balanceString = json['balance'] as String?;

if (balanceString != null) {
final balanceValue = BigInt.parse(balanceString);

await _balanceRepository.saveBalance(
Balance(
chainId: _appStore.apiConfig.asset.chainId,
contractAddress: _appStore.apiConfig.asset.address,
walletAddress: address,
balance: balanceValue,
asset: _appStore.apiConfig.asset,
),
);
}
await _parseAndPersistBalance(address, response.body);
} else if (response.statusCode == 404) {
if (generation == _syncGeneration) _accountMissing = true;
}
Expand All @@ -84,6 +68,46 @@ class BalanceService {
}
}

/// Fresh, fail-loud balance read for money-moving flows (migration wizard):
/// performs the API fetch and THROWS on any transport error, non-200 status,
/// or unparseable body instead of falling back to the persisted cache. On
/// success the fresh value is also persisted (same shape as [updateBalance])
/// and returned. The polling [updateBalance] path stays log-and-continue —
/// this method exists precisely because that path may serve stale data.
Future<Balance> fetchBalance(String address) async {
final uri = buildUri(_host, '$_balancePath/$address');
final response = await _appStore.httpClient.get(uri);

if (response.statusCode == 404) {
throw Exception('RealUnit account not found (404) for address $address');
}
if (response.statusCode != 200) {
throw Exception(
'Failed to fetch RealUnit balance. Status: ${response.statusCode} ${response.body}',
);
}

return _parseAndPersistBalance(address, response.body);
}

Future<Balance> _parseAndPersistBalance(String address, String responseBody) async {
final json = jsonDecode(responseBody) as Map<String, dynamic>;
final balanceString = json['balance'];
if (balanceString is! String) {
throw const FormatException('Balance response has no parseable balance');
}

final balance = Balance(
chainId: _appStore.apiConfig.asset.chainId,
contractAddress: _appStore.apiConfig.asset.address,
walletAddress: address,
balance: BigInt.parse(balanceString),
asset: _appStore.apiConfig.asset,
);
await _balanceRepository.saveBalance(balance);
return balance;
}

Future<Balance?> getBalance(Asset asset, String address) =>
_balanceRepository.getBalance(asset, address);
}
20 changes: 17 additions & 3 deletions lib/packages/service/debug_auth_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const _signatureKey = 'debugAuthSignature';
class DebugAuthService {
final AppStore _appStore;
final SharedPreferences _prefs;
String? _fetchedSignMessage;
String? _fetchedSignMessageAddress;

DebugAuthService(this._appStore, this._prefs);

Expand All @@ -31,12 +33,20 @@ class DebugAuthService {

if (response.statusCode == 200) {
final body = jsonDecode(response.body);
return body['message'] as String;
final message = body['message'] as String;
_fetchedSignMessage = message;
_fetchedSignMessageAddress = address;
return message;
}
throw Exception('Failed to fetch sign message (${response.statusCode})');
}

Future<void> authenticate(String address, String signature) async {
final signedMessage = _fetchedSignMessageAddress == address ? _fetchedSignMessage : null;
if (signedMessage == null) {
throw StateError('No fetched sign message for address $address');
}

final uri = buildUri(_appStore.apiConfig.apiHost, '/v1/auth');
final response = await _appStore.httpClient.post(
uri,
Expand All @@ -51,8 +61,12 @@ class DebugAuthService {
if (response.statusCode == 201) {
final body = jsonDecode(response.body) as Map<String, dynamic>;
final checksumAddress = EthereumAddress.fromHex(address).hexEip55;
_appStore.sessionCache.setAuthToken(body['accessToken'] as String);
await _appStore.sessionCache.saveSignature(checksumAddress, signature);
_appStore.sessionCache.setAuthToken(body['accessToken'] as String, checksumAddress);
await _appStore.sessionCache.saveSignature(
checksumAddress,
signature,
signedMessage,
);
await _prefs.setString(_addressKey, address);
await _prefs.setString(_signatureKey, signature);
} else {
Expand Down
Loading