diff --git a/lib/services/config/config.dart b/lib/services/config/config.dart index 82e36416..5c263d7e 100644 --- a/lib/services/config/config.dart +++ b/lib/services/config/config.dart @@ -715,6 +715,7 @@ class Config { return primaryToken; } +// TODO: remove use of getPrimaryAccountAbstractionConfig ERC4337Config getPrimaryAccountAbstractionConfig() { final primaryAccountAbstraction = accounts[community.primaryAccountFactory.fullAddress]; @@ -726,6 +727,8 @@ class Config { return primaryAccountAbstraction; } + // TODO: force required accountFactoryAddress + // TODO: remove use of getPrimaryAccountAbstractionConfig ERC4337Config getAccountAbstractionConfig({String? accountFactoryAddress}) { // If no accountFactoryAddress is provided, return the primary config if (accountFactoryAddress == null || accountFactoryAddress.isEmpty) { @@ -762,6 +765,7 @@ class Config { return chain.node.url; } + // TODO: force required accountFactoryAddress String getRpcUrl(String chainId, {String? accountFactoryAddress}) { final chain = chains[chainId]; diff --git a/lib/services/config/utils.dart b/lib/services/config/utils.dart index 7c8a3829..667df7d5 100644 --- a/lib/services/config/utils.dart +++ b/lib/services/config/utils.dart @@ -15,3 +15,87 @@ String fixLegacyAliases(String alias) { return alias == 'localhost' || alias == '' ? defaultAlias : alias; } + +/// migrate the accounts from the accounts migration db (when migrating from old app and you want to put a value in the account secret) +/// hard coded values for these communities (gratitude, bread, wallet.commonshub.brussels, wallet.sfluv.org) +/// if account factory address is '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2', return '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185' +/// the others just take the primary account factory +const Map configV4PrimaryAccountFactoryMap = { + /****cw-safe (old)*****/ + 'ctzn': '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2', + 'txirrin': '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2', + 'boliviapay': '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2', + 'seldesalm': '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2', + 'my.techi.be': '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2', + 'wallet.kingfishersmedia.io': '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2', + /*********/ + 'gratitude': '0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD', + 'bread': '0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9', + 'wallet.commonshub.brussels': '0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87', + 'wallet.sfluv.org': '0x5e987a6c4bb4239d498E78c34e986acf29c81E8e', + /****cw-safe (new)*****/ + 'wallet.berachain.sfluv.org': '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185', + 'laborhour': '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185', + 'rooted': '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185', + /*********/ + 'wallet.pay.brussels': '0xBABCf159c4e3186cf48e4a48bC0AeC17CF9d90FE', + 'wallet.regensunite.earth': '0x9406Cc6185a346906296840746125a0E44976454', + 'gt.celo': '0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD', + 'ceur.celo': '0xdA529eBEd3D459dac9d9D3D45b8Cae2D5796c098', + 'eure.polygon': '0x5bA08d9fC7b90f79B2b856bdB09FC9EB32e83616', + 'app': '0x270758454C012A1f51428b68aE473D728CCdFe88', + 'usdc.base': '0x05e2Fb34b4548990F96B3ba422eA3EF49D5dAa99', + 'wallet.oak.community': '0x9406Cc6185a346906296840746125a0E44976454', + 'sbc.polygon': '0x3Be13D9325C8C9174C3819d3d868D5D3aB8Fc8a5', + 'zinne': '0x11af2639817692D2b805BcE0e1e405E530B20006', + 'timebank.regensunite.earth': '0x39b77d77f7677997871b304094a05295eb71e240', + 'moos': '0x671f0662de72268d0f3966Fb62dFc6ee6389e244', + 'selcoupdepouce': '0x4Cc883b7E8E0BCB2e293703EF06426F9b4A5A284', + 'cit.celo': '0x0a9f4B7e7Ec393fF25dc9267289Be259Ec3FB970', + 'wallet.wolugo.be': '0x8474153A00C959f2cB64852949954DBC68415Bb3', + 'wtc.celo': '0xE79E19594A749330036280c685E2719d58d99052', + 'testnet-ethldn': '0xc1654087C580f868F08E34cd1c01eDB1d3673b82', + 'celo-c.citizenwallet.xyz': '0xcd8b1B9E760148c5026Bc5B0D56a5374e301FDcA', +}; + +const String oldSafeFactory = '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2'; +const String newSafeFactory = '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185'; + +/// Returns the correct account factory address for a given community alias during database migration. +/// +/// Priority logic: +/// 1. Specific hardcoded overrides for: gratitude, bread, wallet.commonshub.brussels, wallet.sfluv.org +/// 2. Safe factory redirection: if the mapped address is '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2', +/// return '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185' instead +/// 3. General fallback: return the address from the map +/// 4. Safety: if alias not found, return '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185' +String getAccountFactoryAddressByAlias(String alias) { + // List of specific aliases that should keep their original addresses + const Set hardcodedOverrides = { + 'gratitude', + 'bread', + 'wallet.commonshub.brussels', + 'wallet.sfluv.org', + }; + + // 1. Check if this is a hardcoded override + if (hardcodedOverrides.contains(alias)) { + return configV4PrimaryAccountFactoryMap[alias]!; + } + + // Get the address from the map + final String? mappedAddress = configV4PrimaryAccountFactoryMap[alias]; + + // 4. Safety: if alias not found, return new safe factory + if (mappedAddress == null) { + return newSafeFactory; + } + + // 2. Safe factory redirection: if old safe factory, return new safe factory + if (mappedAddress == oldSafeFactory) { + return newSafeFactory; + } + + // 3. General fallback: return the mapped address + return mappedAddress; +} diff --git a/lib/services/db/backup/accounts.dart b/lib/services/db/backup/accounts.dart index 13404623..0a8bb868 100644 --- a/lib/services/db/backup/accounts.dart +++ b/lib/services/db/backup/accounts.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:citizenwallet/services/config/utils.dart'; import 'package:citizenwallet/services/db/db.dart'; import 'package:citizenwallet/services/wallet/contracts/profile.dart'; import 'package:citizenwallet/services/wallet/wallet.dart'; @@ -63,6 +64,13 @@ String getAccountID(EthereumAddress address, String alias) { return '${address.hexEip55}@$alias'; } +String getAccountIDNew( + {required EthereumAddress address, + required String alias, + required String accountFactoryAddress}) { + return '${address.hexEip55}@$accountFactoryAddress@$alias'; +} + class UserHandle { final String username; final String communityAlias; @@ -96,7 +104,8 @@ class AccountsTable extends DBTable { name TEXT NOT NULL, username TEXT, privateKey TEXT, - profile TEXT + profile TEXT, + accountFactoryAddress TEXT NOT NULL ) '''; @@ -113,6 +122,12 @@ class AccountsTable extends DBTable { ], 3: [ 'ALTER TABLE $name ADD COLUMN username TEXT DEFAULT NULL', + ], + 4: [ + 'ALTER TABLE $name ADD COLUMN accountFactoryAddress TEXT DEFAULT ""', + 'PopulateAccountFactoryAddressMigration', + 'InsertRowsInNewIdFormatMigration', // Insert the rows in the new format $address@$accountFactoryAddress@$alias + // TODO: delete the rows in the old format $address@$alias ] }; @@ -122,6 +137,16 @@ class AccountsTable extends DBTable { if (queries != null) { for (final query in queries) { try { + switch (query) { + case 'PopulateAccountFactoryAddressMigration': + await _populateAccountFactoryAddressMigration(db, name); + continue; + + case 'InsertRowsInNewIdFormatMigration': + await _insertRowsInNewIdFormatMigration(db, name); + continue; + } + await db.execute(query); } catch (e, s) { debugPrint('Migration error: $e'); @@ -132,6 +157,58 @@ class AccountsTable extends DBTable { } } + Future _populateAccountFactoryAddressMigration( + Database db, String name) async { + // Work directly with raw DB data, not DBAccount objects + List> accounts = await db.query(name); + + for (final Map account in accounts) { + final alias = account['alias'] as String; + final oldId = account['id'] as String; + + final accountFactoryAddress = getAccountFactoryAddressByAlias(alias); + + // Update the accountFactoryAddress column (ID still in old format $address@$alias) + await db.update( + name, + {'accountFactoryAddress': accountFactoryAddress}, + where: 'id = ?', + whereArgs: [oldId], + ); + } + } + + Future _insertRowsInNewIdFormatMigration( + Database db, String name) async { + List> accounts = await db.query(name); // + + for (final Map account in accounts) { + final address = account['address'] as String; + final accountFactoryAddress = account['accountFactoryAddress'] as String; + final alias = account['alias'] as String; + + final newId = getAccountIDNew( + address: EthereumAddress.fromHex(address), + alias: alias, + accountFactoryAddress: accountFactoryAddress); + + await db.insert( + name, + { + 'id': newId, + 'alias': account['alias'], + 'address': account['address'], + 'accountFactoryAddress': account['accountFactoryAddress'], + 'name': account['name'], + 'username': account['username'], + 'privateKey': account['privateKey'], + 'profile': account['profile'], + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + } + // get account by id Future get(EthereumAddress address, String alias) async { final List> maps = await db.query( diff --git a/lib/services/db/backup/db.dart b/lib/services/db/backup/db.dart index 71ae81af..ea620ab1 100644 --- a/lib/services/db/backup/db.dart +++ b/lib/services/db/backup/db.dart @@ -40,7 +40,7 @@ class AccountBackupDBService extends DBService { return; }, - version: 3, + version: 4, ); final db = await databaseFactory.openDatabase( diff --git a/lib/services/wallet/contracts/profile.dart b/lib/services/wallet/contracts/profile.dart index e2296980..d5c1ee71 100644 --- a/lib/services/wallet/contracts/profile.dart +++ b/lib/services/wallet/contracts/profile.dart @@ -167,10 +167,12 @@ class ProfileContract { rcontract = DeployedContract(cabi, EthereumAddress.fromHex(addr)); } +// TODO: await return Future getURL(String addr) async { return contract.get(EthereumAddress.fromHex(addr)); } +// TODO: await return Future getURLFromUsername(String username) async { return contract.getFromUsername( convertStringToUint8List(username, forcePadLength: 32)); diff --git a/test/services/config/utils_test.dart b/test/services/config/utils_test.dart new file mode 100644 index 00000000..5fd3701e --- /dev/null +++ b/test/services/config/utils_test.dart @@ -0,0 +1,146 @@ +import 'package:citizenwallet/services/config/utils.dart'; +import 'package:test/test.dart'; + +/// Expected outcomes map for testing getAccountFactoryAddressByAlias +/// Covers all four logic branches: +/// 1. Hardcoded overrides (gratitude, bread, wallet.commonshub.brussels, wallet.sfluv.org) +/// 2. Safe factory redirection (old safe factory -> new safe factory) +/// 3. General fallback (return mapped address as-is) +/// 4. Unknown alias (return new safe factory as safety default) +const Map expectedOutcomes = { + // 1. Hardcoded Overrides - these should return their ORIGINAL addresses + 'gratitude': '0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD', + 'bread': '0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9', + 'wallet.commonshub.brussels': '0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87', + 'wallet.sfluv.org': '0x5e987a6c4bb4239d498E78c34e986acf29c81E8e', + + // 2. Safe Factory Redirection - old safe factory should redirect to new safe factory + 'ctzn': newSafeFactory, + 'txirrin': newSafeFactory, + 'boliviapay': newSafeFactory, + 'seldesalm': newSafeFactory, + 'my.techi.be': newSafeFactory, + 'wallet.kingfishersmedia.io': newSafeFactory, + + // 3. General Fallback - return mapped addresses as-is + 'wallet.berachain.sfluv.org': newSafeFactory, + 'laborhour': newSafeFactory, + 'rooted': newSafeFactory, + 'wallet.pay.brussels': '0xBABCf159c4e3186cf48e4a48bC0AeC17CF9d90FE', + 'wallet.regensunite.earth': '0x9406Cc6185a346906296840746125a0E44976454', + 'gt.celo': '0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD', + 'ceur.celo': '0xdA529eBEd3D459dac9d9D3D45b8Cae2D5796c098', + 'eure.polygon': '0x5bA08d9fC7b90f79B2b856bdB09FC9EB32e83616', + 'app': '0x270758454C012A1f51428b68aE473D728CCdFe88', + 'usdc.base': '0x05e2Fb34b4548990F96B3ba422eA3EF49D5dAa99', + 'wallet.oak.community': '0x9406Cc6185a346906296840746125a0E44976454', + 'sbc.polygon': '0x3Be13D9325C8C9174C3819d3d868D5D3aB8Fc8a5', + 'zinne': '0x11af2639817692D2b805BcE0e1e405E530B20006', + 'timebank.regensunite.earth': '0x39b77d77f7677997871b304094a05295eb71e240', + 'moos': '0x671f0662de72268d0f3966Fb62dFc6ee6389e244', + 'selcoupdepouce': '0x4Cc883b7E8E0BCB2e293703EF06426F9b4A5A284', + 'cit.celo': '0x0a9f4B7e7Ec393fF25dc9267289Be259Ec3FB970', + 'wallet.wolugo.be': '0x8474153A00C959f2cB64852949954DBC68415Bb3', + 'wtc.celo': '0xE79E19594A749330036280c685E2719d58d99052', + 'testnet-ethldn': '0xc1654087C580f868F08E34cd1c01eDB1d3673b82', + 'celo-c.citizenwallet.xyz': '0xcd8b1B9E760148c5026Bc5B0D56a5374e301FDcA', + + // 4. Unknown Alias - should return new safe factory as safety default + 'non-existent-alias': newSafeFactory, + 'unknown-community': newSafeFactory, + 'test-alias-not-in-map': newSafeFactory, +}; + +void main() { + group('getAccountFactoryAddressByAlias', () { + test('returns correct addresses for all test cases', () { + expectedOutcomes.forEach((alias, expectedAddress) { + final result = getAccountFactoryAddressByAlias(alias); + expect( + result, + expectedAddress, + reason: 'Failed for alias: $alias', + ); + }); + }); + + group('specific logic branch tests', () { + test('hardcoded overrides return original addresses', () { + // These four should return their original addresses, not redirected + expect( + getAccountFactoryAddressByAlias('gratitude'), + '0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD', + reason: 'gratitude should return its original address', + ); + expect( + getAccountFactoryAddressByAlias('bread'), + '0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9', + reason: 'bread should return its original address', + ); + expect( + getAccountFactoryAddressByAlias('wallet.commonshub.brussels'), + '0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87', + reason: + 'wallet.commonshub.brussels should return its original address', + ); + expect( + getAccountFactoryAddressByAlias('wallet.sfluv.org'), + '0x5e987a6c4bb4239d498E78c34e986acf29c81E8e', + reason: 'wallet.sfluv.org should return its original address', + ); + }); + + test('old safe factory addresses are redirected to new safe factory', () { + // All these aliases map to old safe factory and should be redirected + final oldSafeFactoryAliases = [ + 'ctzn', + 'txirrin', + 'boliviapay', + 'seldesalm', + 'my.techi.be', + 'wallet.kingfishersmedia.io', + ]; + + for (final alias in oldSafeFactoryAliases) { + expect( + getAccountFactoryAddressByAlias(alias), + newSafeFactory, + reason: '$alias should be redirected to new safe factory', + ); + } + }); + + test('general fallback returns mapped addresses as-is', () { + // Sample of aliases that should return their mapped addresses unchanged + expect( + getAccountFactoryAddressByAlias('wallet.pay.brussels'), + '0xBABCf159c4e3186cf48e4a48bC0AeC17CF9d90FE', + ); + expect( + getAccountFactoryAddressByAlias('app'), + '0x270758454C012A1f51428b68aE473D728CCdFe88', + ); + expect( + getAccountFactoryAddressByAlias('usdc.base'), + '0x05e2Fb34b4548990F96B3ba422eA3EF49D5dAa99', + ); + }); + + test('unknown aliases return new safe factory as default', () { + // Test various unknown aliases + expect( + getAccountFactoryAddressByAlias('non-existent-alias'), + newSafeFactory, + ); + expect( + getAccountFactoryAddressByAlias('unknown-community'), + newSafeFactory, + ); + expect( + getAccountFactoryAddressByAlias('random-test-123'), + newSafeFactory, + ); + }); + }); + }); +}