diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 00000000..ae0d10c1 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,277 @@ +# Citizen Wallet Flutter Project - Coding Patterns & Conventions + +## Project Overview + +This is a Flutter-based mobile wallet application for community tokens, built with a focus on blockchain integration, state management, and cross-platform compatibility. + +## Architecture Patterns + +### 1. State Management + +- **Provider Pattern**: Uses `provider` package for state management +- **State/Logic Separation**: Each feature has separate `state.dart` and `logic.dart` files +- **ChangeNotifier**: All state classes extend `ChangeNotifier` for reactive updates +- **MultiProvider**: Centralized state provisioning in `lib/state/state.dart` + +### 2. Directory Structure + +``` +lib/ +├── models/ # Data models and DTOs +├── screens/ # UI screens organized by feature +├── widgets/ # Reusable UI components +├── services/ # Business logic and external integrations +├── state/ # State management (state + logic files) +├── router/ # Navigation and routing +├── theme/ # UI theming and styling +├── utils/ # Utility functions and helpers +├── modals/ # Modal dialogs and sheets +└── l10n/ # Localization files +``` + +### 3. File Naming Conventions + +- **snake_case**: All file names use snake_case +- **Feature-based**: Files organized by feature/domain +- **Platform suffixes**: `.android.dart`, `.apple.dart`, `.web.dart` for platform-specific code +- **State files**: `state.dart` for state classes, `logic.dart` for business logic + +## Code Style & Patterns + +### 1. Dart/Flutter Conventions + +- **Null Safety**: Full null safety implementation +- **Async/Await**: Prefer async/await over Future.then() +- **Const Constructors**: Use const constructors where possible +- **Final Variables**: Use final for immutable variables +- **Type Annotations**: Explicit type annotations for clarity + +### 2. State Management Patterns + +```dart +// State class pattern +class WalletState with ChangeNotifier { + bool loading = false; + String? error; + + void setLoading(bool value) { + loading = value; + notifyListeners(); + } +} + +// Logic class pattern +class WalletLogic { + final WalletState _state; + + Future loadWallet() async { + _state.setLoading(true); + try { + // business logic + } catch (e) { + _state.setError(e.toString()); + } finally { + _state.setLoading(false); + } + } +} +``` + +### 3. Service Layer Patterns + +- **Singleton Services**: Services like `ConfigService`, `AccountDBService` +- **Interface-based**: Use interfaces for service contracts +- **Error Handling**: Comprehensive try-catch blocks with proper error propagation +- **Async Operations**: All external operations are async + +### 4. UI Patterns + +- **Cupertino Design**: Uses Cupertino widgets for iOS-style design +- **Responsive Design**: Platform-aware UI components +- **Modal Sheets**: Heavy use of modal bottom sheets for user interactions +- **Loading States**: Consistent loading indicators and skeleton screens +- **Error Handling**: Toast notifications and error banners + +### 5. Navigation Patterns + +- **GoRouter**: Uses go_router for navigation +- **Deep Linking**: Comprehensive deep link support +- **Route Guards**: Conditional navigation based on app state +- **Platform-specific**: Different navigation patterns for web/mobile + +## Blockchain Integration + +### 1. Web3 Integration + +- **web3dart**: Primary Web3 library +- **Smart Contracts**: Custom contract interactions +- **Account Abstraction**: ERC-4337 account abstraction support +- **Gas Management**: Custom gas estimation and management + +### 2. Wallet Patterns + +- **Multi-wallet Support**: Multiple wallet accounts per user +- **Secure Storage**: Encrypted key storage +- **Transaction Management**: Queue-based transaction handling +- **Event Listening**: Real-time blockchain event monitoring + +## Database & Storage + +### 1. Local Storage + +- **SQLite**: Primary local database (sqflite) +- **Shared Preferences**: Simple key-value storage +- **Secure Storage**: Encrypted sensitive data storage +- **File System**: Asset and backup file management + +### 2. Data Models + +- **JSON Serialization**: All models support JSON serialization +- **Copy Methods**: Immutable data with copyWith methods +- **Validation**: Input validation and sanitization + +## Error Handling + +### 1. Exception Patterns + +- **Custom Exceptions**: Domain-specific exception classes +- **Graceful Degradation**: Fallback mechanisms for failures +- **User Feedback**: Clear error messages to users +- **Logging**: Comprehensive error logging and monitoring + +### 2. Error Recovery + +- **Retry Logic**: Exponential backoff for network operations +- **Offline Support**: Offline-first architecture +- **State Recovery**: Automatic state recovery mechanisms + +## Testing Patterns + +### 1. Test Organization + +- **Unit Tests**: Service and utility function tests +- **Widget Tests**: UI component testing +- **Integration Tests**: End-to-end workflow testing +- **Mock Services**: Comprehensive mocking for external dependencies + +### 2. Test Utilities + +- **Mock HTTP**: Network request mocking +- **Test Helpers**: Reusable test utilities +- **Test Data**: Consistent test data fixtures + +## Security Patterns + +### 1. Cryptography + +- **Encryption**: AES encryption for sensitive data +- **Key Management**: Secure key generation and storage +- **Digital Signatures**: Transaction signing and verification +- **Secure Communication**: HTTPS and secure WebSocket connections + +### 2. Authentication + +- **Biometric Auth**: Touch ID/Face ID integration +- **OAuth**: Google Sign-In integration +- **Session Management**: Secure session handling + +## Performance Patterns + +### 1. Optimization + +- **Lazy Loading**: On-demand resource loading +- **Caching**: Multi-level caching strategies +- **Image Optimization**: Efficient image handling and caching +- **Memory Management**: Proper disposal of resources + +### 2. Background Processing + +- **Event Listeners**: Efficient event handling +- **Background Tasks**: Non-blocking background operations +- **State Synchronization**: Efficient state updates + +## Internationalization + +### 1. Localization + +- **ARB Files**: Flutter's localization format +- **Multi-language**: English, French, Dutch support +- **RTL Support**: Right-to-left language support +- **Currency Formatting**: Localized currency display + +## Platform-Specific Code + +### 1. Platform Detection + +- **kIsWeb**: Web platform detection +- **Platform-specific Files**: Separate implementations for different platforms +- **Conditional Compilation**: Platform-specific code blocks + +### 2. Native Integrations + +- **iOS**: Apple-specific features (Face ID, iCloud) +- **Android**: Android-specific features (Google Sign-In) +- **Web**: Web-specific optimizations and features + +## Dependencies & Packages + +### 1. Core Dependencies + +- **Flutter**: Latest stable version +- **Provider**: State management +- **GoRouter**: Navigation +- **web3dart**: Blockchain integration +- **sqflite**: Local database + +### 2. UI Dependencies + +- **flutter_svg**: SVG support +- **lottie**: Animation support +- **cached_network_image**: Image caching +- **modal_bottom_sheet**: Modal dialogs + +### 3. Blockchain Dependencies + +- **smartcontracts**: Custom smart contract library +- **contractforge**: Contract interaction utilities +- **reown_walletkit**: Wallet functionality + +## Development Workflow + +### 1. Code Organization + +- **Feature-based**: Organize code by business features +- **Separation of Concerns**: Clear separation between UI, business logic, and data +- **Dependency Injection**: Service locator pattern for dependencies + +### 2. Code Quality + +- **Linting**: Flutter lints for code quality +- **Analysis**: Static analysis with custom rules +- **Documentation**: Comprehensive code documentation +- **Type Safety**: Strong typing throughout the codebase + +## Best Practices + +### 1. General + +- **Immutability**: Prefer immutable data structures +- **Composition**: Use composition over inheritance +- **Single Responsibility**: Each class has a single responsibility +- **Dependency Inversion**: Depend on abstractions, not concretions + +### 2. Flutter-specific + +- **Stateless Widgets**: Prefer stateless widgets when possible +- **Const Constructors**: Use const constructors for performance +- **Keys**: Use keys for widget identification +- **Dispose**: Properly dispose of controllers and listeners + +### 3. State Management + +- **Minimal State**: Keep state as minimal as possible +- **Predictable Updates**: State updates should be predictable +- **Performance**: Avoid unnecessary rebuilds +- **Testing**: Make state easily testable + +This project follows a well-structured, maintainable architecture with clear separation of concerns, comprehensive error handling, and strong typing throughout the codebase. diff --git a/.example.env b/.example.env index b0537ed0..8fdec574 100644 --- a/.example.env +++ b/.example.env @@ -6,6 +6,7 @@ ENCRYPTED_STORAGE_GROUP_ID='x' ORIGIN_HEADER='https://app.citizenwallet.xyz' MAIN_APP_SCHEME='citizenwallet://app.citizenwallet.xyz' APP_LINK_SUFFIX='.citizenwallet.xyz' +DASHBOARD_API='' WALLET_CONFIG_URL='https://config.internal.citizenwallet.xyz' SENTRY_URL='x' WC_PROJECT_ID='x' diff --git a/.metadata b/.metadata index 0d9855ef..38038067 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "be698c48a6750c8cb8e61c740ca9991bb947aba2" + revision: "a402d9a4376add5bc2d6b1e33e53edaae58c07f8" channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 - base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 - - platform: ios - create_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 - base_revision: be698c48a6750c8cb8e61c740ca9991bb947aba2 + create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 + base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 + - platform: android + create_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 + base_revision: a402d9a4376add5bc2d6b1e33e53edaae58c07f8 # User provided section diff --git a/android/app/build.gradle b/android/app/build.gradle index 3f68756f..c9ebe859 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -31,7 +31,7 @@ if (keystorePropertiesFile.exists()) { android { // compileSdkVersion flutter.compileSdkVersion - compileSdkVersion 35 + compileSdkVersion 36 // ndkVersion flutter.ndkVersion ndkVersion = "27.0.12077973" diff --git a/android/app/src/main/kotlin/xyz/citizenwallet/citizenwallet/MainActivity.kt b/android/app/src/main/kotlin/xyz/citizenwallet/citizenwallet/MainActivity.kt new file mode 100644 index 00000000..e0db280a --- /dev/null +++ b/android/app/src/main/kotlin/xyz/citizenwallet/citizenwallet/MainActivity.kt @@ -0,0 +1,5 @@ +package xyz.citizenwallet.citizenwallet + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/assets/config/v4/communities.json b/assets/config/v4/communities.json index 7c6b8ae6..29c53470 100644 --- a/assets/config/v4/communities.json +++ b/assets/config/v4/communities.json @@ -2139,4 +2139,4 @@ "config_location": "https://config.internal.citizenwallet.xyz/v4/wallet.kingfishersmedia.io.json", "version": 4 } -] \ No newline at end of file +] diff --git a/assets/config/v4/communities.test.json b/assets/config/v4/communities.test.json index ea16d3e7..f9d980ac 100644 --- a/assets/config/v4/communities.test.json +++ b/assets/config/v4/communities.test.json @@ -2139,4 +2139,4 @@ "config_location": "https://config.internal.citizenwallet.xyz/v4/wallet.kingfishersmedia.io.json", "version": 4 } -] \ No newline at end of file +] diff --git a/assets/config/v5/communities.json b/assets/config/v5/communities.json new file mode 100644 index 00000000..cf9be999 --- /dev/null +++ b/assets/config/v5/communities.json @@ -0,0 +1,2443 @@ +[ + { + "community": { + "name": "Citizen Wallet (CTZN)", + "description": "The token powering the Citizen Wallet economy.", + "url": "https://citizenwallet.xyz", + "alias": "ctzn", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/ctzn.svg", + "theme": { + "primary": "#9563D3" + }, + "profile": { + "address": "0x8dA817724Eb6A2aA47c0F8d8b8A98b9B3C2Ddb68", + "chain_id": 137 + }, + "primary_token": { + "address": "0x0D9B0790E97e3426C161580dF4Ee853E4A7C4607", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 137 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 137 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 137 + } + }, + "tokens": { + "137:0x0D9B0790E97e3426C161580dF4Ee853E4A7C4607": { + "standard": "erc20", + "name": "Citizen Wallet", + "address": "0x0D9B0790E97e3426C161580dF4Ee853E4A7C4607", + "symbol": "CTZN", + "decimals": 18, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 137, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x3A3E25871c5C6C84D5f397829FF316a37F7FD596", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "137:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 137, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "cards": { + "137:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "type": "safe", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 137, + "instance_id": "cw-discord-1" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [ + { + "name": "About", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/ctzn.svg", + "url": "https://citizenwallet.xyz/pay-with-ctzn", + "launch_mode": "browser" + }, + { + "name": "Top Up", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/ctzn.svg", + "url": "https://my.citizenwallet.xyz/onramp", + "action": "topup", + "signature": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/ctzn", + "version": 5 + }, + { + "community": { + "name": "Brussels Pay", + "description": "A community for the city of Brussels", + "url": "https://pay.brussels", + "alias": "wallet.pay.brussels", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/wallet.pay.brussels.png", + "custom_domain": "wallet.pay.brussels", + "hidden": false, + "theme": { + "primary": "#4a90e2" + }, + "profile": { + "address": "0x56Cc38bDa01bE6eC6D854513C995f6621Ee71229", + "chain_id": 100 + }, + "primary_token": { + "address": "0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1", + "chain_id": 100 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 100 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 100 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 100 + } + }, + "tokens": { + "100:0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1": { + "standard": "erc20", + "name": "pay.brussels", + "address": "0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1", + "symbol": "EURb", + "decimals": 6, + "chain_id": 100 + } + }, + "scan": { + "url": "https://gnosisscan.io", + "name": "Gnosis Explorer" + }, + "accounts": { + "100:0xBABCf159c4e3186cf48e4a48bC0AeC17CF9d90FE": { + "chain_id": 100, + "entrypoint_address": "0xAAEb9DC18aDadae9b3aE7ec2b47842565A81113f", + "paymaster_address": "0xcA1B9EC1117340818C1c1fdd1B48Ea79E57C140F", + "account_factory_address": "0xBABCf159c4e3186cf48e4a48bC0AeC17CF9d90FE", + "paymaster_type": "cw" + }, + "100:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 100, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x8fc2e97671C691e7Ff7B42e5c7cCbDD38fC8B729", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "cards": { + "100:0x1EaF6B6A6967608aF6c77224f087b042095891EB": { + "chain_id": 100, + "address": "0x1EaF6B6A6967608aF6c77224f087b042095891EB", + "type": "classic" + }, + "100:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "chain_id": 100, + "instance_id": "brussels-pay", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "type": "safe" + } + }, + "sessions": { + "100:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 100, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "100": { + "id": 100, + "node": { + "url": "https://engine.pay.brussels", + "ws_url": "wss://engine.pay.brussels" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [ + { + "name": "Top Up", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/wallet.pay.brussels.png", + "url": "https://checkout.pay.brussels/topup", + "action": "topup", + "signature": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.pay.brussels", + "version": 5 + }, + { + "community": { + "name": "Monerium EURe on Gnosis", + "description": "A community for EURe on Gnosis", + "url": "https://monerium.com/", + "alias": "eure.gnosis", + "logo": "https://eure.gnosis.citizenwallet.xyz/wallet-config/_images/eure.svg", + "custom_domain": "eure.gnosis.citizenwallet.xyz", + "hidden": false, + "theme": { + "primary": "#4a90e2" + }, + "profile": { + "address": "0x2e297B9ef4df73c8da5070E91EF0570FA312A698", + "chain_id": 100 + }, + "primary_token": { + "address": "0x420CA0f9B9b604cE0fd9C18EF134C705e5Fa3430", + "chain_id": 100 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 100 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 100 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 100 + } + }, + "tokens": { + "100:0x420CA0f9B9b604cE0fd9C18EF134C705e5Fa3430": { + "standard": "erc20", + "name": "EURe [Gnosis]", + "address": "0x420CA0f9B9b604cE0fd9C18EF134C705e5Fa3430", + "symbol": "EURe", + "decimals": 18, + "chain_id": 100 + } + }, + "scan": { + "url": "https://gnosisscan.io", + "name": "Gnosis Explorer" + }, + "accounts": { + "100:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 100, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xeBDd6ee8DB7055f62D14A5eAAB7c18ae36Cd2216", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "100:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 100, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "cards": { + "100:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "chain_id": 100, + "instance_id": "cw-discord-1", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "type": "safe" + } + }, + "chains": { + "100": { + "id": 100, + "node": { + "url": "https://100.engine.citizenwallet.xyz", + "ws_url": "wss://100.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/eure.gnosis", + "version": 5 + }, + { + "community": { + "name": "Gratitude Token", + "description": "Express your gratitude towards someone by sending them a token of gratitude.", + "url": "https://citizenwallet.xyz/gratitude", + "alias": "gratitude", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/gt.svg", + "theme": { + "primary": "#4EC19D" + }, + "profile": { + "address": "0xEEc0F3257369c6bCD2Fd8755CbEf8A95b12Bc4c9", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 42220 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1": { + "standard": "erc20", + "name": "Gratitude Token", + "address": "0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1", + "symbol": "GT", + "decimals": 0, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD": { + "chain_id": 42220, + "entrypoint_address": "0x985ec7d08D9d15Ea79876E35FAdEFD58A627187E", + "paymaster_address": "0x8dd43eE72f6A816b8eB0411B712D96cDd95246d8", + "account_factory_address": "0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xF05ba2641b31AF70c2678e3324eD8b9C53093FbE", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "cards": { + "42220:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "chain_id": 42220, + "instance_id": "cw-discord-1", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "type": "safe" + } + }, + "sessions": { + "42220:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 42220, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/gratitude", + "version": 5 + }, + { + "community": { + "url": "https://sfluv.org", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/sfluv.svg", + "name": "SFLUV Community", + "alias": "wallet.berachain.sfluv.org", + "theme": { + "primary": "#eb6c6c" + }, + "profile": { + "address": "0x05e2Fb34b4548990F96B3ba422eA3EF49D5dAa99", + "chain_id": 80094 + }, + "description": "A community currency for the city of San Francisco.", + "custom_domain": "wallet.sfluv.org", + "primary_token": { + "address": "0x881cad4f885c6701d8481c0ed347f6d35444ea7e", + "chain_id": 80094 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 80094 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 80094 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 80094 + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "cards": { + "80094:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "type": "safe", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 80094, + "instance_id": "cw-discord-1" + } + }, + "chains": { + "80094": { + "id": 80094, + "node": { + "url": "https://80094.engine.citizenwallet.xyz", + "ws_url": "wss://80094.engine.citizenwallet.xyz" + } + } + }, + "tokens": { + "80094:0x881cad4f885c6701d8481c0ed347f6d35444ea7e": { + "name": "SFLUV V1.1", + "symbol": "SFLUV", + "address": "0x881cad4f885c6701d8481c0ed347f6d35444ea7e", + "chain_id": 80094, + "decimals": 18, + "standard": "erc20" + } + }, + "plugins": [ + { + "url": "https://app.sfluv.org", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/sfluv.svg", + "name": "About", + "hidden": true, + "signature": true, + "launch_mode": "webview" + } + ], + "version": 5, + "accounts": { + "80094:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 80094, + "paymaster_type": "cw-safe", + "paymaster_address": "0x9A5be02B65f9Aa00060cB8c951dAFaBAB9B860cd", + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185" + } + }, + "sessions": { + "80094:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 80094, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.berachain.sfluv.org" + }, + { + "community": { + "url": "https://sfluv.org", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/sfluv.svg", + "name": "SFLUV Community (Polygon)", + "alias": "wallet.sfluv.org", + "theme": { + "primary": "#eb6c6c" + }, + "description": "A community currency for the city of San Francisco.", + "custom_domain": "wallet.polygon.sfluv.org", + "profile": { + "address": "0x05e2Fb34b4548990F96B3ba422eA3EF49D5dAa99", + "chain_id": 137 + }, + "primary_token": { + "address": "0x58a2993A618Afee681DE23dECBCF535A58A080BA", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x5e987a6c4bb4239d498E78c34e986acf29c81E8e", + "chain_id": 137 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 137 + } + }, + "tokens": { + "137:0x58a2993A618Afee681DE23dECBCF535A58A080BA": { + "standard": "erc20", + "name": "SFLUV V1.1", + "address": "0x58a2993A618Afee681DE23dECBCF535A58A080BA", + "symbol": "SFLUV", + "decimals": 6, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x5e987a6c4bb4239d498E78c34e986acf29c81E8e": { + "chain_id": 137, + "entrypoint_address": "0x2d01C5E40Aa6a8478eD0FFbF2784EBb9bf67C46A", + "paymaster_address": "0x7FC98D0a2bd7f766bAca37388eB0F6Db37666B33", + "account_factory_address": "0x5e987a6c4bb4239d498E78c34e986acf29c81E8e", + "paymaster_type": "cw" + } + }, + "sessions": { + "137:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 137, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [ + { + "name": "About", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/sfluv.svg", + "url": "https://app.sfluv.org", + "launch_mode": "webview", + "signature": true, + "hidden": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.sfluv.org", + "version": 5 + }, + { + "community": { + "name": "Txirrin", + "description": "A community for Txirrin", + "url": "https://citizenwallet.xyz/txirrin", + "alias": "txirrin", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/txirrin.png", + "hidden": false, + "theme": { + "primary": "#FB7502" + }, + "profile": { + "address": "0xd47f7198bf335bfe66dD29C0f3EeEf0cFE9D05D8", + "chain_id": 100 + }, + "primary_token": { + "address": "0x6c6611244547a6E9AaCfBA8744115ca1076756fc", + "chain_id": 100 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 100 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 100 + } + }, + "tokens": { + "100:0x6c6611244547a6E9AaCfBA8744115ca1076756fc": { + "standard": "erc20", + "name": "Txirrin", + "address": "0x6c6611244547a6E9AaCfBA8744115ca1076756fc", + "symbol": "TXI", + "decimals": 6, + "chain_id": 100 + } + }, + "scan": { + "url": "https://gnosisscan.io", + "name": "Gnosis Explorer" + }, + "accounts": { + "100:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 100, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x33500E7Eb3452421e56c2f4117530B1C4C85E0A5", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "100:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 100, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "100": { + "id": 100, + "node": { + "url": "https://100.engine.citizenwallet.xyz", + "ws_url": "wss://100.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/txirrin", + "version": 5 + }, + { + "community": { + "name": "Bolivia Pay", + "description": "A community for Ethereum Bolivia.", + "url": "https://www.ethereumbolivia.org", + "alias": "boliviapay", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/boliviapay.png", + "theme": { + "primary": "#009393" + }, + "profile": { + "address": "0x898C2737f2Cb52622711A89D85A1D5E0B881BDeA", + "chain_id": 137 + }, + "primary_token": { + "address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 137 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 137 + } + }, + "tokens": { + "137:0xc2132D05D31c914a87C6611C10748AEb04B58e8F": { + "standard": "erc20", + "name": "(PoS) Tether USD", + "address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", + "symbol": "USDT", + "decimals": 6, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 137, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x9a81Bd50D56485Cc863Ecb169812c7a821996C8c", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "137:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 137, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/boliviapay", + "version": 5 + }, + { + "community": { + "url": "https://breadchain.xyz/", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/bread.svg", + "name": "Breadchain Community Token", + "alias": "bread", + "profile": { + "address": "0x6b3a1f4277391526413F583c23D5B9EF4d2fE986", + "chain_id": 100 + }, + "description": "BREAD is a digital community token and solidarity primitive developed by Breadchain Cooperative that generates yield for post-capitalist organizations", + "primary_token": { + "address": "0xa555d5344f6fb6c65da19e403cb4c1ec4a1a5ee3", + "chain_id": 100 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 100 + }, + "primary_account_factory": { + "address": "0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2", + "chain_id": 100 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 100 + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "scan": { + "url": "https://gnosisscan.io", + "name": "Gnosis Explorer" + }, + "cards": { + "100:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "type": "safe", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 100, + "instance_id": "cw-discord-1" + } + }, + "chains": { + "100": { + "id": 100, + "node": { + "url": "https://100.engine.citizenwallet.xyz", + "ws_url": "wss://100.engine.citizenwallet.xyz" + } + } + }, + "tokens": { + "100:0xa555d5344f6fb6c65da19e403cb4c1ec4a1a5ee3": { + "name": "Breadchain Community Token", + "symbol": "BREAD", + "address": "0xa555d5344f6fb6c65da19e403cb4c1ec4a1a5ee3", + "chain_id": 100, + "decimals": 18, + "standard": "erc20" + } + }, + "plugins": [ + { + "url": "https://topup.citizenspring.earth/bread", + "icon": "https://bread.citizenwallet.xyz/uploads/logo.svg", + "name": "Top Up", + "action": "topup" + }, + { + "url": "https://marketplace.citizenwallet.xyz/bread", + "icon": "https://bread.citizenwallet.xyz/uploads/logo.svg", + "name": "Market", + "launch_mode": "webview" + } + ], + "version": 5, + "accounts": { + "100:0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2": { + "chain_id": 100, + "paymaster_type": "cw-safe", + "paymaster_address": "0x5987e57e85014B5A56C880313580346c20a5d1c1", + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "account_factory_address": "0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2" + }, + "100:0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9": { + "chain_id": 100, + "paymaster_type": "cw", + "paymaster_address": "0xbE2Cb3358aa14621134e923B68b8429315368E32", + "entrypoint_address": "0xcA0a75EF803a364C83c5EAE7Eb889aE7419c9dF2", + "account_factory_address": "0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9" + } + }, + "sessions": { + "100:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 100, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/bread" + }, + { + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "scan": { + "url": "https://gnosisscan.io", + "name": "Gnosis Explorer" + }, + "cards": { + "100:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "type": "safe", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 100, + "instance_id": "cw-discord-1" + } + }, + "chains": { + "100": { + "id": 100, + "node": { + "url": "https://100.engine.citizenwallet.xyz", + "ws_url": "wss://100.engine.citizenwallet.xyz" + } + } + }, + "tokens": { + "100:0x3d36ddFfa4666Ef12a176CaA8C3e67C1047bC007": { + "name": "Labor Hour Token", + "symbol": "HOUR", + "address": "0x3d36ddFfa4666Ef12a176CaA8C3e67C1047bC007", + "chain_id": 100, + "decimals": 6, + "standard": "erc20" + } + }, + "plugins": [], + "version": 5, + "accounts": { + "100:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 100, + "paymaster_type": "cw-safe", + "paymaster_address": "0xa7fa16C933f51d8623f39FA0dF34D3065B99Bd1c", + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185" + } + }, + "sessions": { + "100:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 100, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "community": { + "url": "https://breadchain.xyz/", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/laborhour.png", + "name": "Labor Hour Token", + "alias": "laborhour", + "profile": { + "address": "0x673601Eb36820bC9718214AC041E96f79383351B", + "chain_id": 100 + }, + "description": "Labor Hour Token aims to reward contributors for hours of labor, particularly targeting non-blockchain native users", + "primary_token": { + "address": "0x3d36ddFfa4666Ef12a176CaA8C3e67C1047bC007", + "chain_id": 100 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 100 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 100 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 100 + } + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/laborhour" + }, + { + "community": { + "name": "Commons Hub Brussels", + "description": "Community Token for the Commons Hub Brussels community", + "url": "https://commonshub.brussels", + "alias": "wallet.commonshub.brussels", + "custom_domain": "wallet.commonshub.brussels", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/chb.png", + "theme": { + "primary": "#ff4c02" + }, + "profile": { + "address": "0xc06bE1BbbeEAF2f34F3d5b76069D2560aee184Ae", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x65DD32834927de9E57E72a3E2130a19f81C6371D", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x65DD32834927de9E57E72a3E2130a19f81C6371D": { + "standard": "erc20", + "name": "Commons Hub Token", + "address": "0x65DD32834927de9E57E72a3E2130a19f81C6371D", + "symbol": "CHT", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "CELO Explorer" + }, + "accounts": { + "42220:0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87": { + "chain_id": 42220, + "entrypoint_address": "0xb7608dDA592d319687C89c4479e320b5a7740117", + "paymaster_address": "0x4E127A1DAa66568B4a91E8c5615120a6Ea5442E3", + "account_factory_address": "0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0xb7608dDA592d319687C89c4479e320b5a7740117", + "paymaster_address": "0x4860C0f127500F0cbF4a5Bd797cBb5aA50Eb0FbA", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "42220:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 42220, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "cards": { + "42220:0xc0F9e0907C8de79fd5902b61e463dFEdc5dc8570": { + "chain_id": 42220, + "address": "0xc0F9e0907C8de79fd5902b61e463dFEdc5dc8570", + "type": "classic" + } + }, + "plugins": [ + { + "name": "Market", + "icon": "https://marketplace.citizenwallet.xyz/marketplace.svg", + "url": "https://marketplace.citizenwallet.xyz/wallet.commonshub.brussels", + "launch_mode": "webview", + "signature": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.commonshub.brussels", + "version": 5 + }, + { + "community": { + "name": "Sel de Salm", + "description": "La communauté de Sel de Salm", + "url": "https://citizenwallet.xyz/community-currency-documentation/sel-de-salm", + "alias": "seldesalm", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/myrt.png", + "theme": { + "primary": "#6B5CA4" + }, + "profile": { + "address": "0x4083724953cC1cC13e76b436149B2b1e1a3E5970", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x83DfEB42347a7Ce46F1497F307a5c156D1f19CB2", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 42220 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x83DfEB42347a7Ce46F1497F307a5c156D1f19CB2": { + "standard": "erc20", + "name": "Myrtille", + "address": "0x83DfEB42347a7Ce46F1497F307a5c156D1f19CB2", + "symbol": "MYRT", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xd07412020dA5054c3b49f47Ca61224637F1703af", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "42220:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 42220, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "cards": { + "42220:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "chain_id": 42220, + "instance_id": "cw-seldesalm", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "type": "safe" + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [ + { + "name": "Informations Générales", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/myrt.png", + "url": "https://citizenwallet.xyz/community-currency-documentation/sel-de-salm", + "launch_mode": "webview" + }, + { + "name": "Échanges", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/myrt.png", + "url": "https://marketplace.citizenwallet.xyz/seldesalm", + "launch_mode": "webview", + "signature": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/seldesalm", + "version": 5 + }, + { + "community": { + "name": "TECHI", + "description": "A community for TECHI users", + "url": "https://my.techi.be", + "alias": "my.techi.be", + "logo": "https://my.techi.be/assets/token.svg", + "hidden": false, + "theme": { + "primary": "#617FF8" + }, + "profile": { + "address": "0x80C141861607b8FEfD53C9E71a9c7D2D3e2e76dc", + "chain_id": 100 + }, + "primary_token": { + "address": "0x01D0E7117510b371Ac38f52Cc6689ff8875280FA", + "chain_id": 100 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 100 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 100 + } + }, + "tokens": { + "100:0x01D0E7117510b371Ac38f52Cc6689ff8875280FA": { + "standard": "erc20", + "name": "TECHI", + "address": "0x01D0E7117510b371Ac38f52Cc6689ff8875280FA", + "symbol": "TECHI", + "decimals": 6, + "chain_id": 100 + } + }, + "scan": { + "url": "https://gnosisscan.io", + "name": "Gnosis Explorer" + }, + "accounts": { + "100:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 100, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x68c5a20f233264DB124a3c95a200bbD20b3b9762", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "100:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 100, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "100": { + "id": 100, + "node": { + "url": "https://engine.my.techi.be", + "ws_url": "wss://engine.my.techi.be" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/my.techi.be", + "version": 5 + }, + { + "community": { + "name": "Regens Unite", + "description": "A community currency for the Regens Unite community.", + "url": "https://www.regensunite.earth/", + "alias": "wallet.regensunite.earth", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/rgn.svg", + "custom_domain": "wallet.regensunite.earth", + "hidden": true, + "profile": { + "address": "0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9", + "chain_id": 137 + }, + "primary_token": { + "address": "0x9b1a0D2951b11Ac26A6cBbd5aEf2c4cb014b3B6e", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 137 + } + }, + "tokens": { + "137:0x9b1a0D2951b11Ac26A6cBbd5aEf2c4cb014b3B6e": { + "standard": "erc20", + "name": "Regens Unite Token", + "address": "0x9b1a0D2951b11Ac26A6cBbd5aEf2c4cb014b3B6e", + "symbol": "RGN", + "decimals": 6, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x9406Cc6185a346906296840746125a0E44976454": { + "chain_id": 137, + "entrypoint_address": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + "paymaster_address": "0x250711045d58b6310f0635C7D110BFe663cE1da5", + "account_factory_address": "0x9406Cc6185a346906296840746125a0E44976454", + "paymaster_type": "payg", + "gas_extra_percentage": 50 + }, + "137:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 137, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x250711045d58b6310f0635C7D110BFe663cE1da5", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.regensunite.earth", + "version": 5 + }, + { + "community": { + "name": "Gratitude Token", + "description": "Express your gratitude towards someone by sending them a token.", + "url": "https://citizenwallet.xyz/gratitude", + "alias": "gt.celo", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/gt.svg", + "custom_domain": null, + "hidden": true, + "theme": { + "primary": "#a256ff" + }, + "profile": { + "address": "0xEEc0F3257369c6bCD2Fd8755CbEf8A95b12Bc4c9", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1": { + "standard": "erc20", + "name": "Gratitude Token", + "address": "0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1", + "symbol": "GT", + "decimals": 0, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD": { + "chain_id": 42220, + "entrypoint_address": "0x985ec7d08D9d15Ea79876E35FAdEFD58A627187E", + "paymaster_address": "0x8dd43eE72f6A816b8eB0411B712D96cDd95246d8", + "account_factory_address": "0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x985ec7d08D9d15Ea79876E35FAdEFD58A627187E", + "paymaster_address": "0x8dd43eE72f6A816b8eB0411B712D96cDd95246d8", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/gt.celo", + "version": 5 + }, + { + "community": { + "name": "Celo Euro", + "description": "Celo Euro is a stablecoin for the Celo Community.", + "url": "https://celo.org/", + "alias": "ceur.celo", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/ceur.svg", + "hidden": true, + "theme": { + "primary": "#a256ff" + }, + "profile": { + "address": "0x0334C579E61aF6922D5deFEF02A361FBb2D6f406", + "chain_id": 42220 + }, + "primary_token": { + "address": "0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73": { + "standard": "erc20", + "name": "Celo Euro", + "address": "0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73", + "symbol": "cEUR", + "decimals": 18, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0xdA529eBEd3D459dac9d9D3D45b8Cae2D5796c098": { + "chain_id": 42220, + "entrypoint_address": "0xc3142BCBA2285d0a48A38e7Ea9Cbf28a12B235bB", + "paymaster_address": "0xedbEA8c0F25B34510149EaD4f72867B0d3D2264F", + "account_factory_address": "0xdA529eBEd3D459dac9d9D3D45b8Cae2D5796c098", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0xc3142BCBA2285d0a48A38e7Ea9Cbf28a12B235bB", + "paymaster_address": "0xedbEA8c0F25B34510149EaD4f72867B0d3D2264F", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/ceur.celo", + "version": 5 + }, + { + "community": { + "name": "EUR e-money", + "description": "Token by Monerium EMI, a regulated entity, licensed in the EEA.", + "url": "https://monerium.com/tokens/", + "alias": "eure.polygon", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/eure.svg", + "hidden": true, + "profile": { + "address": "0xF5F7317EDb8E88CaE09071B0C4F0fd6EA20B21f9", + "chain_id": 137 + }, + "primary_token": { + "address": "0x18ec0A6E18E5bc3784fDd3a3634b31245ab704F6", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 137 + } + }, + "tokens": { + "137:0x18ec0A6E18E5bc3784fDd3a3634b31245ab704F6": { + "standard": "erc20", + "name": "EUR emoney", + "address": "0x18ec0A6E18E5bc3784fDd3a3634b31245ab704F6", + "symbol": "EURe", + "decimals": 18, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x5bA08d9fC7b90f79B2b856bdB09FC9EB32e83616": { + "chain_id": 137, + "entrypoint_address": "0x2027Bde7C276D5F128587E3107c68A488ee31c72", + "paymaster_address": "0xB2cb6b75C2357Ca94dBdF58897E468E45fAC83Ec", + "account_factory_address": "0x5bA08d9fC7b90f79B2b856bdB09FC9EB32e83616", + "paymaster_type": "cw", + "gas_extra_percentage": 50 + }, + "137:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 137, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xB2cb6b75C2357Ca94dBdF58897E468E45fAC83Ec", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/eure.polygon", + "version": 5 + }, + { + "community": { + "name": "USDC on Polygon", + "description": "The community of people using USDC on Polygon.", + "url": "https://en.wikipedia.org/wiki/USD_Coin", + "alias": "app", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/usdc.svg", + "hidden": true, + "theme": { + "primary": "#0052ff" + }, + "profile": { + "address": "0xA63DFccB8a39a3DFE4479b33190b12019Ee594E7", + "chain_id": 137 + }, + "primary_token": { + "address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 137 + } + }, + "tokens": { + "137:0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174": { + "standard": "erc20", + "name": "USD Coin", + "address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + "symbol": "USDC", + "decimals": 6, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x270758454C012A1f51428b68aE473D728CCdFe88": { + "chain_id": 137, + "entrypoint_address": "0x466AA6ed2B7Bb829841F5aAEA9e82B840eC0feF9", + "paymaster_address": "0xB5D1C0167E6325466E2918e9fda8cc41384C0291", + "account_factory_address": "0x270758454C012A1f51428b68aE473D728CCdFe88", + "paymaster_type": "cw", + "gas_extra_percentage": 50 + }, + "137:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 137, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xB5D1C0167E6325466E2918e9fda8cc41384C0291", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/app", + "version": 5 + }, + { + "community": { + "name": "USDC on Base", + "description": "The community of people using USDC on Base.", + "url": "https://en.wikipedia.org/wiki/USD_Coin", + "alias": "usdc.base", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/usdc.svg", + "hidden": true, + "theme": { + "primary": "#0052ff" + }, + "profile": { + "address": "0x51Ef5Add405CCF63c206A80AF8c2B3cEE0282830", + "chain_id": 8453 + }, + "primary_token": { + "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "chain_id": 8453 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 8453 + } + }, + "tokens": { + "8453:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913": { + "standard": "erc20", + "name": "USD Coin", + "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "symbol": "USDC", + "decimals": 6, + "chain_id": 8453 + } + }, + "scan": { + "url": "https://basescan.org", + "name": "Base Explorer" + }, + "accounts": { + "8453:0x05e2Fb34b4548990F96B3ba422eA3EF49D5dAa99": { + "chain_id": 8453, + "entrypoint_address": "0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9", + "paymaster_address": "0xA63DFccB8a39a3DFE4479b33190b12019Ee594E7", + "account_factory_address": "0x05e2Fb34b4548990F96B3ba422eA3EF49D5dAa99", + "paymaster_type": "cw", + "gas_extra_percentage": 50 + }, + "8453:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 8453, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xA63DFccB8a39a3DFE4479b33190b12019Ee594E7", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "8453": { + "id": 8453, + "node": { + "url": "https://8453.engine.citizenwallet.xyz", + "ws_url": "wss://8453.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/usdc.base", + "version": 5 + }, + { + "community": { + "name": "OAK Community", + "description": "A community currency for the city of Oakland.", + "url": "https://www.oak.community/", + "alias": "wallet.oak.community", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/oak.svg", + "custom_domain": "wallet.oak.community", + "hidden": true, + "profile": { + "address": "0xFE213c74e25505B232CE4C7f89647408bE6f71d2", + "chain_id": 8453 + }, + "primary_token": { + "address": "0x845598Da418890a674cbaBA26b70807aF0c61dFE", + "chain_id": 8453 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 8453 + } + }, + "tokens": { + "8453:0x845598Da418890a674cbaBA26b70807aF0c61dFE": { + "standard": "erc20", + "name": "OAK Community Currency", + "address": "0x845598Da418890a674cbaBA26b70807aF0c61dFE", + "symbol": "OAK", + "decimals": 6, + "chain_id": 8453 + } + }, + "scan": { + "url": "https://basescan.org", + "name": "Base Explorer" + }, + "accounts": { + "8453:0x9406Cc6185a346906296840746125a0E44976454": { + "chain_id": 8453, + "entrypoint_address": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + "paymaster_address": "0x123", + "account_factory_address": "0x9406Cc6185a346906296840746125a0E44976454", + "paymaster_type": "payg" + }, + "8453:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 8453, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x123", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "8453": { + "id": 8453, + "node": { + "url": "https://8453.engine.citizenwallet.xyz", + "ws_url": "wss://8453.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.oak.community", + "version": 5 + }, + { + "community": { + "name": "Stable Coin", + "description": "SBC is a digital dollar stablecoin issued by Brale", + "url": "https://brale.xyz/", + "alias": "sbc.polygon", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/sbc.svg", + "hidden": true, + "profile": { + "address": "0xcA0a75EF803a364C83c5EAE7Eb889aE7419c9dF2", + "chain_id": 137 + }, + "primary_token": { + "address": "0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 137 + } + }, + "tokens": { + "137:0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798": { + "standard": "erc20", + "name": "Stable Coin", + "address": "0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798", + "symbol": "SBC", + "decimals": 18, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x3Be13D9325C8C9174C3819d3d868D5D3aB8Fc8a5": { + "chain_id": 137, + "entrypoint_address": "0xe84423Ba1A3f3535B09237245e22dBda5E27eB88", + "paymaster_address": "0x123", + "account_factory_address": "0x3Be13D9325C8C9174C3819d3d868D5D3aB8Fc8a5", + "paymaster_type": "cw" + }, + "137:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 137, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x123", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/sbc.polygon", + "version": 5 + }, + { + "community": { + "name": "Zinne.brussels", + "description": "A community currency for the city of Brussels", + "url": "https://zinne.brussels", + "alias": "zinne", + "logo": "https://citizenwallet.xyz/zinne/zinne-coin.svg", + "hidden": true, + "profile": { + "address": "0x23DB3D3Da510e60aF40902A04850E1F3a744905c", + "chain_id": 137 + }, + "primary_token": { + "address": "0x5491a3d35F148a44F0af4D718B9636A6e55eBc2D", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 137 + } + }, + "tokens": { + "137:0x5491a3d35F148a44F0af4D718B9636A6e55eBc2D": { + "standard": "erc20", + "name": "Zinne.brussels Token", + "address": "0x5491a3d35F148a44F0af4D718B9636A6e55eBc2D", + "symbol": "ZINNE", + "decimals": 6, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x11af2639817692D2b805BcE0e1e405E530B20006": { + "chain_id": 137, + "entrypoint_address": "0xF5507B3042f1C63625D856a2ABFF046243A5D74e", + "paymaster_address": "0xBb796D122Ec1aBDeD081D50B06a072f981c7E62b", + "account_factory_address": "0x11af2639817692D2b805BcE0e1e405E530B20006", + "paymaster_type": "cw" + }, + "137:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 137, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xBb796D122Ec1aBDeD081D50B06a072f981c7E62b", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/zinne", + "version": 5 + }, + { + "community": { + "name": "Regens Unite Time Bank", + "description": "Make time to regen", + "url": "https://regensunite.earth", + "alias": "timebank.regensunite.earth", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/rgn.svg", + "hidden": true, + "profile": { + "address": "0x605A827DF8C405D16Ec70AAb8d9a47D21db45c09", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x186DaBD027e228C988777907465807FDab270894", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x186DaBD027e228C988777907465807FDab270894": { + "standard": "erc20", + "name": "Regen Hour", + "address": "0x186DaBD027e228C988777907465807FDab270894", + "symbol": "rHour", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "CELO Explorer" + }, + "accounts": { + "42220:0x39b77d77f7677997871b304094a05295eb71e240": { + "chain_id": 42220, + "entrypoint_address": "0x41176F0C9b8f795Cb99e2DD5Db16017978eeFa4d", + "paymaster_address": "0xe45858bf63176595c2920822581917c7C705a12f", + "account_factory_address": "0x39b77d77f7677997871b304094a05295eb71e240", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xe45858bf63176595c2920822581917c7C705a12f", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [ + { + "name": "Market", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/rgn.svg", + "url": "https://marketplace.citizenwallet.xyz/timebank.regensunite.earth", + "launch_mode": "webview", + "signature": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/timebank.regensunite.earth", + "version": 5 + }, + { + "community": { + "name": "MOOS Token", + "description": "A community currency for MOOS.", + "url": "https://www.moos.garden/", + "alias": "moos", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/moos.svg", + "hidden": true, + "profile": { + "address": "0x2e4542Be47408d05F41703386eFaf4338Ee1D341", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x230542eda83346929e4E54f4a98e1ca1A4BFc0c3", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x230542eda83346929e4E54f4a98e1ca1A4BFc0c3": { + "standard": "erc20", + "name": "MOOS Token", + "address": "0x230542eda83346929e4E54f4a98e1ca1A4BFc0c3", + "symbol": "MOOS", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0x671f0662de72268d0f3966Fb62dFc6ee6389e244": { + "chain_id": 42220, + "entrypoint_address": "0x45a8e6AaDCc48D1Ce19eCbE07Ccd3a536EF712ed", + "paymaster_address": "0x55E519bfD63c7152D9F7B88Acd712A37F0BEC482", + "account_factory_address": "0x671f0662de72268d0f3966Fb62dFc6ee6389e244", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x55E519bfD63c7152D9F7B88Acd712A37F0BEC482", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [ + { + "name": "Market", + "icon": "https://moos.citizenwallet.xyz/wallet-config/_images/moos.svg", + "url": "https://marketplace.citizenwallet.xyz/moos", + "launch_mode": "webview", + "signature": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/moos", + "version": 5 + }, + { + "community": { + "name": "Bonne Heure", + "description": "Système d'Échange Local de Villers-la-Ville", + "url": "https://selcoupdepouce.be", + "alias": "selcoupdepouce", + "logo": "https://topup.citizenwallet.xyz/communities/selcoupdepouce/sel-coin.svg", + "hidden": true, + "profile": { + "address": "0xfB8F1e7ED42599638B3c509679E2F43937002C56", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x5Cdbc862BF4E20D98456D4c41D4A5239aDd496d3", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x5Cdbc862BF4E20D98456D4c41D4A5239aDd496d3": { + "standard": "erc20", + "name": "Bonne Heure", + "address": "0x5Cdbc862BF4E20D98456D4c41D4A5239aDd496d3", + "symbol": "BHR", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0x4Cc883b7E8E0BCB2e293703EF06426F9b4A5A284": { + "chain_id": 42220, + "entrypoint_address": "0xA90904F33df36899d810d040b8d5b3b77265Bb05", + "paymaster_address": "0x635032605337aB36A46D767905108e67EE687a72", + "account_factory_address": "0x4Cc883b7E8E0BCB2e293703EF06426F9b4A5A284", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x635032605337aB36A46D767905108e67EE687a72", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [ + { + "name": "Market", + "icon": "https://marketplace.citizenwallet.xyz/marketplace.svg", + "url": "https://marketplace.citizenwallet.xyz/selcoupdepouce", + "launch_mode": "browser", + "signature": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/selcoupdepouce", + "version": 5 + }, + { + "community": { + "name": "CI token", + "description": "Monnaie locale du Cercle Informatique de l’ULB", + "url": "https://citizenwallet.xyz/cit", + "alias": "cit.celo", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/cit.celo.svg", + "hidden": true, + "profile": { + "address": "0x4cB296BEc9FAd0B5e1E4FF1A2F307B425724AC82", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x12e26FAED228c425BceA8a8dd7658a9CeD944dd9", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x12e26FAED228c425BceA8a8dd7658a9CeD944dd9": { + "standard": "erc20", + "name": "CI token", + "address": "0x12e26FAED228c425BceA8a8dd7658a9CeD944dd9", + "symbol": "CIT", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0x0a9f4B7e7Ec393fF25dc9267289Be259Ec3FB970": { + "chain_id": 42220, + "entrypoint_address": "0xB8d9412f3A91A00ca762B5c35cd0863E9b716D68", + "paymaster_address": "0x452F7ff3e55fe29f481841985dE7f4939FD645fa", + "account_factory_address": "0x0a9f4B7e7Ec393fF25dc9267289Be259Ec3FB970", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x452F7ff3e55fe29f481841985dE7f4939FD645fa", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/cit.celo", + "version": 5 + }, + { + "community": { + "name": "Wolugo", + "description": "A community for the Woluwe-Saint-Pierre civic engagement platform", + "url": "https://wolugo.be", + "alias": "wallet.wolugo.be", + "custom_domain": "wallet.wolugo.be", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/wolugo.svg", + "theme": { + "primary": "#81e2c1" + }, + "hidden": true, + "profile": { + "address": "0x07e7b95B35866302b3A089feF4CFA3061061a51d", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x13Dd4B3cD2f2Be3eb41cD0C3E2ce9F8d8C93A451", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + }, + "card_factory": { + "address": "0xA3E1446E332a098A1f3b0555c5d149b4784A095F", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x13Dd4B3cD2f2Be3eb41cD0C3E2ce9F8d8C93A451": { + "standard": "erc20", + "name": "Wolu", + "address": "0x13Dd4B3cD2f2Be3eb41cD0C3E2ce9F8d8C93A451", + "symbol": "WOLU", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "CELO Explorer" + }, + "accounts": { + "42220:0x8474153A00C959f2cB64852949954DBC68415Bb3": { + "chain_id": 42220, + "entrypoint_address": "0x0F805BC1ED718FB9C7C18439cB11E1C17C6538C4", + "paymaster_address": "0xF2EFEC3cBFaDE0bB6108620cbF7Cc608d27DCF3c", + "account_factory_address": "0x8474153A00C959f2cB64852949954DBC68415Bb3", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xF2EFEC3cBFaDE0bB6108620cbF7Cc608d27DCF3c", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "cards": { + "42220:0xA3E1446E332a098A1f3b0555c5d149b4784A095F": { + "chain_id": 42220, + "address": "0xA3E1446E332a098A1f3b0555c5d149b4784A095F", + "type": "classic" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.wolugo.be", + "version": 5 + }, + { + "community": { + "name": "Woluwe Test", + "description": "Local currency for the Woluwe Test community.", + "url": "https://wollet-v2.citizenwallet.net/token", + "alias": "wtc.celo", + "logo": "https://wtc.celo.citizenwallet.xyz/wallet-config/_images/wtc.celo.svg", + "hidden": true, + "profile": { + "address": "0xB99a7B1574f051020EB4cb2fce5d48EE07592AfF", + "chain_id": 42220 + }, + "primary_token": { + "address": "0xc53Cb35591959cA62471dA9fF6AC16629A89874a", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0xc53Cb35591959cA62471dA9fF6AC16629A89874a": { + "standard": "erc20", + "name": "Woluwe Test Coin", + "address": "0xc53Cb35591959cA62471dA9fF6AC16629A89874a", + "symbol": "WTC", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0xE79E19594A749330036280c685E2719d58d99052": { + "chain_id": 42220, + "entrypoint_address": "0x1EaF6B6A6967608aF6c77224f087b042095891EB", + "paymaster_address": "0x3fefC19674f3F6E43B1dFf1861E07c303B9eAAc9", + "account_factory_address": "0xE79E19594A749330036280c685E2719d58d99052", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x3fefC19674f3F6E43B1dFf1861E07c303B9eAAc9", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/wtc.celo", + "version": 5 + }, + { + "community": { + "name": "ETHGlobal London Token", + "description": "The community of people using ETHLDN on Base.", + "url": "https://en.wikipedia.org/wiki/USD_Coin", + "alias": "testnet-ethldn", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/testnet-ethldn.svg", + "hidden": true, + "profile": { + "address": "0x0785D720279f42326846D5396b5F44b97d0BfECd", + "chain_id": 84532 + }, + "primary_token": { + "address": "0x9b1a0D2951b11Ac26A6cBbd5aEf2c4cb014b3B6e", + "chain_id": 84532 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 84532 + } + }, + "tokens": { + "84532:0x9b1a0D2951b11Ac26A6cBbd5aEf2c4cb014b3B6e": { + "standard": "erc20", + "name": "ETHGlobal London Token", + "address": "0x9b1a0D2951b11Ac26A6cBbd5aEf2c4cb014b3B6e", + "symbol": "ETHLDN", + "decimals": 6, + "chain_id": 84532 + } + }, + "scan": { + "url": "https://sepolia.basescan.org", + "name": "Base Sepolia Explorer" + }, + "accounts": { + "84532:0xc1654087C580f868F08E34cd1c01eDB1d3673b82": { + "chain_id": 84532, + "entrypoint_address": "0xBABCf159c4e3186cf48e4a48bC0AeC17CF9d90FE", + "paymaster_address": "0x389182aCCeE26D953d5188BF4b92c49339DcC9FC", + "account_factory_address": "0xc1654087C580f868F08E34cd1c01eDB1d3673b82", + "paymaster_type": "cw" + }, + "84532:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 84532, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x389182aCCeE26D953d5188BF4b92c49339DcC9FC", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "84532": { + "id": 84532, + "node": { + "url": "https://84532.engine.citizenwallet.xyz", + "ws_url": "wss://84532.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/testnet-ethldn", + "version": 5 + }, + { + "community": { + "name": "Celo Community Point", + "description": "This is a community for the Celo Point", + "url": "https://citizenwallet.xyz", + "alias": "celo-c.citizenwallet.xyz", + "logo": "https://celo-c.citizenwallet.xyz/uploads/logo.svg", + "hidden": true, + "profile": { + "address": "0x14004E13907282cFaD05f742022E56926eE92dAd", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x3C960E72BBbD837293e75080E1d0Fee6a4640357", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x3C960E72BBbD837293e75080E1d0Fee6a4640357": { + "standard": "erc20", + "name": "Celo Community Point", + "address": "0x3C960E72BBbD837293e75080E1d0Fee6a4640357", + "symbol": "CeloC", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "CELO Explorer" + }, + "accounts": { + "42220:0xcd8b1B9E760148c5026Bc5B0D56a5374e301FDcA": { + "chain_id": 42220, + "entrypoint_address": "0x66fE9c22CcA49B257dd4F00508AC90198d99Bf27", + "paymaster_address": "0x7f4011845Ea914b6cefc60629e1e00600c972c75", + "account_factory_address": "0xcd8b1B9E760148c5026Bc5B0D56a5374e301FDcA", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x7f4011845Ea914b6cefc60629e1e00600c972c75", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/celo-c.citizenwallet.xyz", + "version": 5 + }, + { + "community": { + "name": "KFMEDIA℠", + "description": "Certified Education Organization. Solving systemic educational disparity using Web3 solutions, removing barriers of entry for underdeveloped economies.", + "url": "https://kingfishersmedia.io", + "alias": "wallet.kingfishersmedia.io", + "custom_domain": "wallet.kingfishersmedia.io", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/kfmpfl.png", + "theme": { + "primary": "#88292c" + }, + "profile": { + "address": "0x5f6FEb03ad8EfeCdD2a837FAA1a29DEA2bAcfd55", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x56744910f7dEcD48c1a7FA61B4C317b15E99F156", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x56744910f7dEcD48c1a7FA61B4C317b15E99F156": { + "standard": "erc1155", + "name": "KFMEDIA℠ Pathways for LATAM™", + "address": "0x56744910f7dEcD48c1a7FA61B4C317b15E99F156", + "symbol": "KFMPFL", + "decimals": 0, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x02BDA8370d9497A5C808B2db237cfaA8f0733F36", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "42220:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 42220, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.kingfishersmedia.io", + "version": 5 + } +] \ No newline at end of file diff --git a/assets/config/v5/communities.test.json b/assets/config/v5/communities.test.json new file mode 100644 index 00000000..cf9be999 --- /dev/null +++ b/assets/config/v5/communities.test.json @@ -0,0 +1,2443 @@ +[ + { + "community": { + "name": "Citizen Wallet (CTZN)", + "description": "The token powering the Citizen Wallet economy.", + "url": "https://citizenwallet.xyz", + "alias": "ctzn", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/ctzn.svg", + "theme": { + "primary": "#9563D3" + }, + "profile": { + "address": "0x8dA817724Eb6A2aA47c0F8d8b8A98b9B3C2Ddb68", + "chain_id": 137 + }, + "primary_token": { + "address": "0x0D9B0790E97e3426C161580dF4Ee853E4A7C4607", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 137 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 137 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 137 + } + }, + "tokens": { + "137:0x0D9B0790E97e3426C161580dF4Ee853E4A7C4607": { + "standard": "erc20", + "name": "Citizen Wallet", + "address": "0x0D9B0790E97e3426C161580dF4Ee853E4A7C4607", + "symbol": "CTZN", + "decimals": 18, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 137, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x3A3E25871c5C6C84D5f397829FF316a37F7FD596", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "137:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 137, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "cards": { + "137:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "type": "safe", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 137, + "instance_id": "cw-discord-1" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [ + { + "name": "About", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/ctzn.svg", + "url": "https://citizenwallet.xyz/pay-with-ctzn", + "launch_mode": "browser" + }, + { + "name": "Top Up", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/ctzn.svg", + "url": "https://my.citizenwallet.xyz/onramp", + "action": "topup", + "signature": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/ctzn", + "version": 5 + }, + { + "community": { + "name": "Brussels Pay", + "description": "A community for the city of Brussels", + "url": "https://pay.brussels", + "alias": "wallet.pay.brussels", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/wallet.pay.brussels.png", + "custom_domain": "wallet.pay.brussels", + "hidden": false, + "theme": { + "primary": "#4a90e2" + }, + "profile": { + "address": "0x56Cc38bDa01bE6eC6D854513C995f6621Ee71229", + "chain_id": 100 + }, + "primary_token": { + "address": "0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1", + "chain_id": 100 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 100 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 100 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 100 + } + }, + "tokens": { + "100:0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1": { + "standard": "erc20", + "name": "pay.brussels", + "address": "0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1", + "symbol": "EURb", + "decimals": 6, + "chain_id": 100 + } + }, + "scan": { + "url": "https://gnosisscan.io", + "name": "Gnosis Explorer" + }, + "accounts": { + "100:0xBABCf159c4e3186cf48e4a48bC0AeC17CF9d90FE": { + "chain_id": 100, + "entrypoint_address": "0xAAEb9DC18aDadae9b3aE7ec2b47842565A81113f", + "paymaster_address": "0xcA1B9EC1117340818C1c1fdd1B48Ea79E57C140F", + "account_factory_address": "0xBABCf159c4e3186cf48e4a48bC0AeC17CF9d90FE", + "paymaster_type": "cw" + }, + "100:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 100, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x8fc2e97671C691e7Ff7B42e5c7cCbDD38fC8B729", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "cards": { + "100:0x1EaF6B6A6967608aF6c77224f087b042095891EB": { + "chain_id": 100, + "address": "0x1EaF6B6A6967608aF6c77224f087b042095891EB", + "type": "classic" + }, + "100:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "chain_id": 100, + "instance_id": "brussels-pay", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "type": "safe" + } + }, + "sessions": { + "100:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 100, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "100": { + "id": 100, + "node": { + "url": "https://engine.pay.brussels", + "ws_url": "wss://engine.pay.brussels" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [ + { + "name": "Top Up", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/wallet.pay.brussels.png", + "url": "https://checkout.pay.brussels/topup", + "action": "topup", + "signature": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.pay.brussels", + "version": 5 + }, + { + "community": { + "name": "Monerium EURe on Gnosis", + "description": "A community for EURe on Gnosis", + "url": "https://monerium.com/", + "alias": "eure.gnosis", + "logo": "https://eure.gnosis.citizenwallet.xyz/wallet-config/_images/eure.svg", + "custom_domain": "eure.gnosis.citizenwallet.xyz", + "hidden": false, + "theme": { + "primary": "#4a90e2" + }, + "profile": { + "address": "0x2e297B9ef4df73c8da5070E91EF0570FA312A698", + "chain_id": 100 + }, + "primary_token": { + "address": "0x420CA0f9B9b604cE0fd9C18EF134C705e5Fa3430", + "chain_id": 100 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 100 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 100 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 100 + } + }, + "tokens": { + "100:0x420CA0f9B9b604cE0fd9C18EF134C705e5Fa3430": { + "standard": "erc20", + "name": "EURe [Gnosis]", + "address": "0x420CA0f9B9b604cE0fd9C18EF134C705e5Fa3430", + "symbol": "EURe", + "decimals": 18, + "chain_id": 100 + } + }, + "scan": { + "url": "https://gnosisscan.io", + "name": "Gnosis Explorer" + }, + "accounts": { + "100:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 100, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xeBDd6ee8DB7055f62D14A5eAAB7c18ae36Cd2216", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "100:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 100, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "cards": { + "100:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "chain_id": 100, + "instance_id": "cw-discord-1", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "type": "safe" + } + }, + "chains": { + "100": { + "id": 100, + "node": { + "url": "https://100.engine.citizenwallet.xyz", + "ws_url": "wss://100.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/eure.gnosis", + "version": 5 + }, + { + "community": { + "name": "Gratitude Token", + "description": "Express your gratitude towards someone by sending them a token of gratitude.", + "url": "https://citizenwallet.xyz/gratitude", + "alias": "gratitude", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/gt.svg", + "theme": { + "primary": "#4EC19D" + }, + "profile": { + "address": "0xEEc0F3257369c6bCD2Fd8755CbEf8A95b12Bc4c9", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 42220 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1": { + "standard": "erc20", + "name": "Gratitude Token", + "address": "0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1", + "symbol": "GT", + "decimals": 0, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD": { + "chain_id": 42220, + "entrypoint_address": "0x985ec7d08D9d15Ea79876E35FAdEFD58A627187E", + "paymaster_address": "0x8dd43eE72f6A816b8eB0411B712D96cDd95246d8", + "account_factory_address": "0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xF05ba2641b31AF70c2678e3324eD8b9C53093FbE", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "cards": { + "42220:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "chain_id": 42220, + "instance_id": "cw-discord-1", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "type": "safe" + } + }, + "sessions": { + "42220:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 42220, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/gratitude", + "version": 5 + }, + { + "community": { + "url": "https://sfluv.org", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/sfluv.svg", + "name": "SFLUV Community", + "alias": "wallet.berachain.sfluv.org", + "theme": { + "primary": "#eb6c6c" + }, + "profile": { + "address": "0x05e2Fb34b4548990F96B3ba422eA3EF49D5dAa99", + "chain_id": 80094 + }, + "description": "A community currency for the city of San Francisco.", + "custom_domain": "wallet.sfluv.org", + "primary_token": { + "address": "0x881cad4f885c6701d8481c0ed347f6d35444ea7e", + "chain_id": 80094 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 80094 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 80094 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 80094 + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "cards": { + "80094:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "type": "safe", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 80094, + "instance_id": "cw-discord-1" + } + }, + "chains": { + "80094": { + "id": 80094, + "node": { + "url": "https://80094.engine.citizenwallet.xyz", + "ws_url": "wss://80094.engine.citizenwallet.xyz" + } + } + }, + "tokens": { + "80094:0x881cad4f885c6701d8481c0ed347f6d35444ea7e": { + "name": "SFLUV V1.1", + "symbol": "SFLUV", + "address": "0x881cad4f885c6701d8481c0ed347f6d35444ea7e", + "chain_id": 80094, + "decimals": 18, + "standard": "erc20" + } + }, + "plugins": [ + { + "url": "https://app.sfluv.org", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/sfluv.svg", + "name": "About", + "hidden": true, + "signature": true, + "launch_mode": "webview" + } + ], + "version": 5, + "accounts": { + "80094:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 80094, + "paymaster_type": "cw-safe", + "paymaster_address": "0x9A5be02B65f9Aa00060cB8c951dAFaBAB9B860cd", + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185" + } + }, + "sessions": { + "80094:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 80094, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.berachain.sfluv.org" + }, + { + "community": { + "url": "https://sfluv.org", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/sfluv.svg", + "name": "SFLUV Community (Polygon)", + "alias": "wallet.sfluv.org", + "theme": { + "primary": "#eb6c6c" + }, + "description": "A community currency for the city of San Francisco.", + "custom_domain": "wallet.polygon.sfluv.org", + "profile": { + "address": "0x05e2Fb34b4548990F96B3ba422eA3EF49D5dAa99", + "chain_id": 137 + }, + "primary_token": { + "address": "0x58a2993A618Afee681DE23dECBCF535A58A080BA", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x5e987a6c4bb4239d498E78c34e986acf29c81E8e", + "chain_id": 137 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 137 + } + }, + "tokens": { + "137:0x58a2993A618Afee681DE23dECBCF535A58A080BA": { + "standard": "erc20", + "name": "SFLUV V1.1", + "address": "0x58a2993A618Afee681DE23dECBCF535A58A080BA", + "symbol": "SFLUV", + "decimals": 6, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x5e987a6c4bb4239d498E78c34e986acf29c81E8e": { + "chain_id": 137, + "entrypoint_address": "0x2d01C5E40Aa6a8478eD0FFbF2784EBb9bf67C46A", + "paymaster_address": "0x7FC98D0a2bd7f766bAca37388eB0F6Db37666B33", + "account_factory_address": "0x5e987a6c4bb4239d498E78c34e986acf29c81E8e", + "paymaster_type": "cw" + } + }, + "sessions": { + "137:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 137, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [ + { + "name": "About", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/sfluv.svg", + "url": "https://app.sfluv.org", + "launch_mode": "webview", + "signature": true, + "hidden": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.sfluv.org", + "version": 5 + }, + { + "community": { + "name": "Txirrin", + "description": "A community for Txirrin", + "url": "https://citizenwallet.xyz/txirrin", + "alias": "txirrin", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/txirrin.png", + "hidden": false, + "theme": { + "primary": "#FB7502" + }, + "profile": { + "address": "0xd47f7198bf335bfe66dD29C0f3EeEf0cFE9D05D8", + "chain_id": 100 + }, + "primary_token": { + "address": "0x6c6611244547a6E9AaCfBA8744115ca1076756fc", + "chain_id": 100 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 100 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 100 + } + }, + "tokens": { + "100:0x6c6611244547a6E9AaCfBA8744115ca1076756fc": { + "standard": "erc20", + "name": "Txirrin", + "address": "0x6c6611244547a6E9AaCfBA8744115ca1076756fc", + "symbol": "TXI", + "decimals": 6, + "chain_id": 100 + } + }, + "scan": { + "url": "https://gnosisscan.io", + "name": "Gnosis Explorer" + }, + "accounts": { + "100:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 100, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x33500E7Eb3452421e56c2f4117530B1C4C85E0A5", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "100:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 100, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "100": { + "id": 100, + "node": { + "url": "https://100.engine.citizenwallet.xyz", + "ws_url": "wss://100.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/txirrin", + "version": 5 + }, + { + "community": { + "name": "Bolivia Pay", + "description": "A community for Ethereum Bolivia.", + "url": "https://www.ethereumbolivia.org", + "alias": "boliviapay", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/boliviapay.png", + "theme": { + "primary": "#009393" + }, + "profile": { + "address": "0x898C2737f2Cb52622711A89D85A1D5E0B881BDeA", + "chain_id": 137 + }, + "primary_token": { + "address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 137 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 137 + } + }, + "tokens": { + "137:0xc2132D05D31c914a87C6611C10748AEb04B58e8F": { + "standard": "erc20", + "name": "(PoS) Tether USD", + "address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", + "symbol": "USDT", + "decimals": 6, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 137, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x9a81Bd50D56485Cc863Ecb169812c7a821996C8c", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "137:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 137, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/boliviapay", + "version": 5 + }, + { + "community": { + "url": "https://breadchain.xyz/", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/bread.svg", + "name": "Breadchain Community Token", + "alias": "bread", + "profile": { + "address": "0x6b3a1f4277391526413F583c23D5B9EF4d2fE986", + "chain_id": 100 + }, + "description": "BREAD is a digital community token and solidarity primitive developed by Breadchain Cooperative that generates yield for post-capitalist organizations", + "primary_token": { + "address": "0xa555d5344f6fb6c65da19e403cb4c1ec4a1a5ee3", + "chain_id": 100 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 100 + }, + "primary_account_factory": { + "address": "0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2", + "chain_id": 100 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 100 + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "scan": { + "url": "https://gnosisscan.io", + "name": "Gnosis Explorer" + }, + "cards": { + "100:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "type": "safe", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 100, + "instance_id": "cw-discord-1" + } + }, + "chains": { + "100": { + "id": 100, + "node": { + "url": "https://100.engine.citizenwallet.xyz", + "ws_url": "wss://100.engine.citizenwallet.xyz" + } + } + }, + "tokens": { + "100:0xa555d5344f6fb6c65da19e403cb4c1ec4a1a5ee3": { + "name": "Breadchain Community Token", + "symbol": "BREAD", + "address": "0xa555d5344f6fb6c65da19e403cb4c1ec4a1a5ee3", + "chain_id": 100, + "decimals": 18, + "standard": "erc20" + } + }, + "plugins": [ + { + "url": "https://topup.citizenspring.earth/bread", + "icon": "https://bread.citizenwallet.xyz/uploads/logo.svg", + "name": "Top Up", + "action": "topup" + }, + { + "url": "https://marketplace.citizenwallet.xyz/bread", + "icon": "https://bread.citizenwallet.xyz/uploads/logo.svg", + "name": "Market", + "launch_mode": "webview" + } + ], + "version": 5, + "accounts": { + "100:0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2": { + "chain_id": 100, + "paymaster_type": "cw-safe", + "paymaster_address": "0x5987e57e85014B5A56C880313580346c20a5d1c1", + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "account_factory_address": "0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2" + }, + "100:0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9": { + "chain_id": 100, + "paymaster_type": "cw", + "paymaster_address": "0xbE2Cb3358aa14621134e923B68b8429315368E32", + "entrypoint_address": "0xcA0a75EF803a364C83c5EAE7Eb889aE7419c9dF2", + "account_factory_address": "0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9" + } + }, + "sessions": { + "100:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 100, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/bread" + }, + { + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "scan": { + "url": "https://gnosisscan.io", + "name": "Gnosis Explorer" + }, + "cards": { + "100:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "type": "safe", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 100, + "instance_id": "cw-discord-1" + } + }, + "chains": { + "100": { + "id": 100, + "node": { + "url": "https://100.engine.citizenwallet.xyz", + "ws_url": "wss://100.engine.citizenwallet.xyz" + } + } + }, + "tokens": { + "100:0x3d36ddFfa4666Ef12a176CaA8C3e67C1047bC007": { + "name": "Labor Hour Token", + "symbol": "HOUR", + "address": "0x3d36ddFfa4666Ef12a176CaA8C3e67C1047bC007", + "chain_id": 100, + "decimals": 6, + "standard": "erc20" + } + }, + "plugins": [], + "version": 5, + "accounts": { + "100:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 100, + "paymaster_type": "cw-safe", + "paymaster_address": "0xa7fa16C933f51d8623f39FA0dF34D3065B99Bd1c", + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185" + } + }, + "sessions": { + "100:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 100, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "community": { + "url": "https://breadchain.xyz/", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/laborhour.png", + "name": "Labor Hour Token", + "alias": "laborhour", + "profile": { + "address": "0x673601Eb36820bC9718214AC041E96f79383351B", + "chain_id": 100 + }, + "description": "Labor Hour Token aims to reward contributors for hours of labor, particularly targeting non-blockchain native users", + "primary_token": { + "address": "0x3d36ddFfa4666Ef12a176CaA8C3e67C1047bC007", + "chain_id": 100 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 100 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 100 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 100 + } + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/laborhour" + }, + { + "community": { + "name": "Commons Hub Brussels", + "description": "Community Token for the Commons Hub Brussels community", + "url": "https://commonshub.brussels", + "alias": "wallet.commonshub.brussels", + "custom_domain": "wallet.commonshub.brussels", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/chb.png", + "theme": { + "primary": "#ff4c02" + }, + "profile": { + "address": "0xc06bE1BbbeEAF2f34F3d5b76069D2560aee184Ae", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x65DD32834927de9E57E72a3E2130a19f81C6371D", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x65DD32834927de9E57E72a3E2130a19f81C6371D": { + "standard": "erc20", + "name": "Commons Hub Token", + "address": "0x65DD32834927de9E57E72a3E2130a19f81C6371D", + "symbol": "CHT", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "CELO Explorer" + }, + "accounts": { + "42220:0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87": { + "chain_id": 42220, + "entrypoint_address": "0xb7608dDA592d319687C89c4479e320b5a7740117", + "paymaster_address": "0x4E127A1DAa66568B4a91E8c5615120a6Ea5442E3", + "account_factory_address": "0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0xb7608dDA592d319687C89c4479e320b5a7740117", + "paymaster_address": "0x4860C0f127500F0cbF4a5Bd797cBb5aA50Eb0FbA", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "42220:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 42220, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "cards": { + "42220:0xc0F9e0907C8de79fd5902b61e463dFEdc5dc8570": { + "chain_id": 42220, + "address": "0xc0F9e0907C8de79fd5902b61e463dFEdc5dc8570", + "type": "classic" + } + }, + "plugins": [ + { + "name": "Market", + "icon": "https://marketplace.citizenwallet.xyz/marketplace.svg", + "url": "https://marketplace.citizenwallet.xyz/wallet.commonshub.brussels", + "launch_mode": "webview", + "signature": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.commonshub.brussels", + "version": 5 + }, + { + "community": { + "name": "Sel de Salm", + "description": "La communauté de Sel de Salm", + "url": "https://citizenwallet.xyz/community-currency-documentation/sel-de-salm", + "alias": "seldesalm", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/myrt.png", + "theme": { + "primary": "#6B5CA4" + }, + "profile": { + "address": "0x4083724953cC1cC13e76b436149B2b1e1a3E5970", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x83DfEB42347a7Ce46F1497F307a5c156D1f19CB2", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + }, + "primary_card_manager": { + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "chain_id": 42220 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x83DfEB42347a7Ce46F1497F307a5c156D1f19CB2": { + "standard": "erc20", + "name": "Myrtille", + "address": "0x83DfEB42347a7Ce46F1497F307a5c156D1f19CB2", + "symbol": "MYRT", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xd07412020dA5054c3b49f47Ca61224637F1703af", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "42220:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 42220, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "cards": { + "42220:0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28": { + "chain_id": 42220, + "instance_id": "cw-seldesalm", + "address": "0xBA861e2DABd8316cf11Ae7CdA101d110CF581f28", + "type": "safe" + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [ + { + "name": "Informations Générales", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/myrt.png", + "url": "https://citizenwallet.xyz/community-currency-documentation/sel-de-salm", + "launch_mode": "webview" + }, + { + "name": "Échanges", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/myrt.png", + "url": "https://marketplace.citizenwallet.xyz/seldesalm", + "launch_mode": "webview", + "signature": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/seldesalm", + "version": 5 + }, + { + "community": { + "name": "TECHI", + "description": "A community for TECHI users", + "url": "https://my.techi.be", + "alias": "my.techi.be", + "logo": "https://my.techi.be/assets/token.svg", + "hidden": false, + "theme": { + "primary": "#617FF8" + }, + "profile": { + "address": "0x80C141861607b8FEfD53C9E71a9c7D2D3e2e76dc", + "chain_id": 100 + }, + "primary_token": { + "address": "0x01D0E7117510b371Ac38f52Cc6689ff8875280FA", + "chain_id": 100 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 100 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 100 + } + }, + "tokens": { + "100:0x01D0E7117510b371Ac38f52Cc6689ff8875280FA": { + "standard": "erc20", + "name": "TECHI", + "address": "0x01D0E7117510b371Ac38f52Cc6689ff8875280FA", + "symbol": "TECHI", + "decimals": 6, + "chain_id": 100 + } + }, + "scan": { + "url": "https://gnosisscan.io", + "name": "Gnosis Explorer" + }, + "accounts": { + "100:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 100, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x68c5a20f233264DB124a3c95a200bbD20b3b9762", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "100:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 100, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "100": { + "id": 100, + "node": { + "url": "https://engine.my.techi.be", + "ws_url": "wss://engine.my.techi.be" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/my.techi.be", + "version": 5 + }, + { + "community": { + "name": "Regens Unite", + "description": "A community currency for the Regens Unite community.", + "url": "https://www.regensunite.earth/", + "alias": "wallet.regensunite.earth", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/rgn.svg", + "custom_domain": "wallet.regensunite.earth", + "hidden": true, + "profile": { + "address": "0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9", + "chain_id": 137 + }, + "primary_token": { + "address": "0x9b1a0D2951b11Ac26A6cBbd5aEf2c4cb014b3B6e", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 137 + } + }, + "tokens": { + "137:0x9b1a0D2951b11Ac26A6cBbd5aEf2c4cb014b3B6e": { + "standard": "erc20", + "name": "Regens Unite Token", + "address": "0x9b1a0D2951b11Ac26A6cBbd5aEf2c4cb014b3B6e", + "symbol": "RGN", + "decimals": 6, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x9406Cc6185a346906296840746125a0E44976454": { + "chain_id": 137, + "entrypoint_address": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + "paymaster_address": "0x250711045d58b6310f0635C7D110BFe663cE1da5", + "account_factory_address": "0x9406Cc6185a346906296840746125a0E44976454", + "paymaster_type": "payg", + "gas_extra_percentage": 50 + }, + "137:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 137, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x250711045d58b6310f0635C7D110BFe663cE1da5", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.regensunite.earth", + "version": 5 + }, + { + "community": { + "name": "Gratitude Token", + "description": "Express your gratitude towards someone by sending them a token.", + "url": "https://citizenwallet.xyz/gratitude", + "alias": "gt.celo", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/gt.svg", + "custom_domain": null, + "hidden": true, + "theme": { + "primary": "#a256ff" + }, + "profile": { + "address": "0xEEc0F3257369c6bCD2Fd8755CbEf8A95b12Bc4c9", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1": { + "standard": "erc20", + "name": "Gratitude Token", + "address": "0x5815E61eF72c9E6107b5c5A05FD121F334f7a7f1", + "symbol": "GT", + "decimals": 0, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD": { + "chain_id": 42220, + "entrypoint_address": "0x985ec7d08D9d15Ea79876E35FAdEFD58A627187E", + "paymaster_address": "0x8dd43eE72f6A816b8eB0411B712D96cDd95246d8", + "account_factory_address": "0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x985ec7d08D9d15Ea79876E35FAdEFD58A627187E", + "paymaster_address": "0x8dd43eE72f6A816b8eB0411B712D96cDd95246d8", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/gt.celo", + "version": 5 + }, + { + "community": { + "name": "Celo Euro", + "description": "Celo Euro is a stablecoin for the Celo Community.", + "url": "https://celo.org/", + "alias": "ceur.celo", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/ceur.svg", + "hidden": true, + "theme": { + "primary": "#a256ff" + }, + "profile": { + "address": "0x0334C579E61aF6922D5deFEF02A361FBb2D6f406", + "chain_id": 42220 + }, + "primary_token": { + "address": "0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73": { + "standard": "erc20", + "name": "Celo Euro", + "address": "0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73", + "symbol": "cEUR", + "decimals": 18, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0xdA529eBEd3D459dac9d9D3D45b8Cae2D5796c098": { + "chain_id": 42220, + "entrypoint_address": "0xc3142BCBA2285d0a48A38e7Ea9Cbf28a12B235bB", + "paymaster_address": "0xedbEA8c0F25B34510149EaD4f72867B0d3D2264F", + "account_factory_address": "0xdA529eBEd3D459dac9d9D3D45b8Cae2D5796c098", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0xc3142BCBA2285d0a48A38e7Ea9Cbf28a12B235bB", + "paymaster_address": "0xedbEA8c0F25B34510149EaD4f72867B0d3D2264F", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/ceur.celo", + "version": 5 + }, + { + "community": { + "name": "EUR e-money", + "description": "Token by Monerium EMI, a regulated entity, licensed in the EEA.", + "url": "https://monerium.com/tokens/", + "alias": "eure.polygon", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/eure.svg", + "hidden": true, + "profile": { + "address": "0xF5F7317EDb8E88CaE09071B0C4F0fd6EA20B21f9", + "chain_id": 137 + }, + "primary_token": { + "address": "0x18ec0A6E18E5bc3784fDd3a3634b31245ab704F6", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 137 + } + }, + "tokens": { + "137:0x18ec0A6E18E5bc3784fDd3a3634b31245ab704F6": { + "standard": "erc20", + "name": "EUR emoney", + "address": "0x18ec0A6E18E5bc3784fDd3a3634b31245ab704F6", + "symbol": "EURe", + "decimals": 18, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x5bA08d9fC7b90f79B2b856bdB09FC9EB32e83616": { + "chain_id": 137, + "entrypoint_address": "0x2027Bde7C276D5F128587E3107c68A488ee31c72", + "paymaster_address": "0xB2cb6b75C2357Ca94dBdF58897E468E45fAC83Ec", + "account_factory_address": "0x5bA08d9fC7b90f79B2b856bdB09FC9EB32e83616", + "paymaster_type": "cw", + "gas_extra_percentage": 50 + }, + "137:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 137, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xB2cb6b75C2357Ca94dBdF58897E468E45fAC83Ec", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/eure.polygon", + "version": 5 + }, + { + "community": { + "name": "USDC on Polygon", + "description": "The community of people using USDC on Polygon.", + "url": "https://en.wikipedia.org/wiki/USD_Coin", + "alias": "app", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/usdc.svg", + "hidden": true, + "theme": { + "primary": "#0052ff" + }, + "profile": { + "address": "0xA63DFccB8a39a3DFE4479b33190b12019Ee594E7", + "chain_id": 137 + }, + "primary_token": { + "address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 137 + } + }, + "tokens": { + "137:0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174": { + "standard": "erc20", + "name": "USD Coin", + "address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + "symbol": "USDC", + "decimals": 6, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x270758454C012A1f51428b68aE473D728CCdFe88": { + "chain_id": 137, + "entrypoint_address": "0x466AA6ed2B7Bb829841F5aAEA9e82B840eC0feF9", + "paymaster_address": "0xB5D1C0167E6325466E2918e9fda8cc41384C0291", + "account_factory_address": "0x270758454C012A1f51428b68aE473D728CCdFe88", + "paymaster_type": "cw", + "gas_extra_percentage": 50 + }, + "137:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 137, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xB5D1C0167E6325466E2918e9fda8cc41384C0291", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/app", + "version": 5 + }, + { + "community": { + "name": "USDC on Base", + "description": "The community of people using USDC on Base.", + "url": "https://en.wikipedia.org/wiki/USD_Coin", + "alias": "usdc.base", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/usdc.svg", + "hidden": true, + "theme": { + "primary": "#0052ff" + }, + "profile": { + "address": "0x51Ef5Add405CCF63c206A80AF8c2B3cEE0282830", + "chain_id": 8453 + }, + "primary_token": { + "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "chain_id": 8453 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 8453 + } + }, + "tokens": { + "8453:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913": { + "standard": "erc20", + "name": "USD Coin", + "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "symbol": "USDC", + "decimals": 6, + "chain_id": 8453 + } + }, + "scan": { + "url": "https://basescan.org", + "name": "Base Explorer" + }, + "accounts": { + "8453:0x05e2Fb34b4548990F96B3ba422eA3EF49D5dAa99": { + "chain_id": 8453, + "entrypoint_address": "0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9", + "paymaster_address": "0xA63DFccB8a39a3DFE4479b33190b12019Ee594E7", + "account_factory_address": "0x05e2Fb34b4548990F96B3ba422eA3EF49D5dAa99", + "paymaster_type": "cw", + "gas_extra_percentage": 50 + }, + "8453:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 8453, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xA63DFccB8a39a3DFE4479b33190b12019Ee594E7", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "8453": { + "id": 8453, + "node": { + "url": "https://8453.engine.citizenwallet.xyz", + "ws_url": "wss://8453.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/usdc.base", + "version": 5 + }, + { + "community": { + "name": "OAK Community", + "description": "A community currency for the city of Oakland.", + "url": "https://www.oak.community/", + "alias": "wallet.oak.community", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/oak.svg", + "custom_domain": "wallet.oak.community", + "hidden": true, + "profile": { + "address": "0xFE213c74e25505B232CE4C7f89647408bE6f71d2", + "chain_id": 8453 + }, + "primary_token": { + "address": "0x845598Da418890a674cbaBA26b70807aF0c61dFE", + "chain_id": 8453 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 8453 + } + }, + "tokens": { + "8453:0x845598Da418890a674cbaBA26b70807aF0c61dFE": { + "standard": "erc20", + "name": "OAK Community Currency", + "address": "0x845598Da418890a674cbaBA26b70807aF0c61dFE", + "symbol": "OAK", + "decimals": 6, + "chain_id": 8453 + } + }, + "scan": { + "url": "https://basescan.org", + "name": "Base Explorer" + }, + "accounts": { + "8453:0x9406Cc6185a346906296840746125a0E44976454": { + "chain_id": 8453, + "entrypoint_address": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + "paymaster_address": "0x123", + "account_factory_address": "0x9406Cc6185a346906296840746125a0E44976454", + "paymaster_type": "payg" + }, + "8453:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 8453, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x123", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "8453": { + "id": 8453, + "node": { + "url": "https://8453.engine.citizenwallet.xyz", + "ws_url": "wss://8453.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.oak.community", + "version": 5 + }, + { + "community": { + "name": "Stable Coin", + "description": "SBC is a digital dollar stablecoin issued by Brale", + "url": "https://brale.xyz/", + "alias": "sbc.polygon", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/sbc.svg", + "hidden": true, + "profile": { + "address": "0xcA0a75EF803a364C83c5EAE7Eb889aE7419c9dF2", + "chain_id": 137 + }, + "primary_token": { + "address": "0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 137 + } + }, + "tokens": { + "137:0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798": { + "standard": "erc20", + "name": "Stable Coin", + "address": "0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798", + "symbol": "SBC", + "decimals": 18, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x3Be13D9325C8C9174C3819d3d868D5D3aB8Fc8a5": { + "chain_id": 137, + "entrypoint_address": "0xe84423Ba1A3f3535B09237245e22dBda5E27eB88", + "paymaster_address": "0x123", + "account_factory_address": "0x3Be13D9325C8C9174C3819d3d868D5D3aB8Fc8a5", + "paymaster_type": "cw" + }, + "137:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 137, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x123", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/sbc.polygon", + "version": 5 + }, + { + "community": { + "name": "Zinne.brussels", + "description": "A community currency for the city of Brussels", + "url": "https://zinne.brussels", + "alias": "zinne", + "logo": "https://citizenwallet.xyz/zinne/zinne-coin.svg", + "hidden": true, + "profile": { + "address": "0x23DB3D3Da510e60aF40902A04850E1F3a744905c", + "chain_id": 137 + }, + "primary_token": { + "address": "0x5491a3d35F148a44F0af4D718B9636A6e55eBc2D", + "chain_id": 137 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 137 + } + }, + "tokens": { + "137:0x5491a3d35F148a44F0af4D718B9636A6e55eBc2D": { + "standard": "erc20", + "name": "Zinne.brussels Token", + "address": "0x5491a3d35F148a44F0af4D718B9636A6e55eBc2D", + "symbol": "ZINNE", + "decimals": 6, + "chain_id": 137 + } + }, + "scan": { + "url": "https://polygonscan.com", + "name": "Polygon Explorer" + }, + "accounts": { + "137:0x11af2639817692D2b805BcE0e1e405E530B20006": { + "chain_id": 137, + "entrypoint_address": "0xF5507B3042f1C63625D856a2ABFF046243A5D74e", + "paymaster_address": "0xBb796D122Ec1aBDeD081D50B06a072f981c7E62b", + "account_factory_address": "0x11af2639817692D2b805BcE0e1e405E530B20006", + "paymaster_type": "cw" + }, + "137:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 137, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xBb796D122Ec1aBDeD081D50B06a072f981c7E62b", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "137": { + "id": 137, + "node": { + "url": "https://137.engine.citizenwallet.xyz", + "ws_url": "wss://137.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [], + "config_location": "https://my.citizenwallet.xyz/api/communities/zinne", + "version": 5 + }, + { + "community": { + "name": "Regens Unite Time Bank", + "description": "Make time to regen", + "url": "https://regensunite.earth", + "alias": "timebank.regensunite.earth", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/rgn.svg", + "hidden": true, + "profile": { + "address": "0x605A827DF8C405D16Ec70AAb8d9a47D21db45c09", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x186DaBD027e228C988777907465807FDab270894", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x186DaBD027e228C988777907465807FDab270894": { + "standard": "erc20", + "name": "Regen Hour", + "address": "0x186DaBD027e228C988777907465807FDab270894", + "symbol": "rHour", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "CELO Explorer" + }, + "accounts": { + "42220:0x39b77d77f7677997871b304094a05295eb71e240": { + "chain_id": 42220, + "entrypoint_address": "0x41176F0C9b8f795Cb99e2DD5Db16017978eeFa4d", + "paymaster_address": "0xe45858bf63176595c2920822581917c7C705a12f", + "account_factory_address": "0x39b77d77f7677997871b304094a05295eb71e240", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xe45858bf63176595c2920822581917c7C705a12f", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [ + { + "name": "Market", + "icon": "https://assets.citizenwallet.xyz/wallet-config/_images/rgn.svg", + "url": "https://marketplace.citizenwallet.xyz/timebank.regensunite.earth", + "launch_mode": "webview", + "signature": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/timebank.regensunite.earth", + "version": 5 + }, + { + "community": { + "name": "MOOS Token", + "description": "A community currency for MOOS.", + "url": "https://www.moos.garden/", + "alias": "moos", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/moos.svg", + "hidden": true, + "profile": { + "address": "0x2e4542Be47408d05F41703386eFaf4338Ee1D341", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x230542eda83346929e4E54f4a98e1ca1A4BFc0c3", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x230542eda83346929e4E54f4a98e1ca1A4BFc0c3": { + "standard": "erc20", + "name": "MOOS Token", + "address": "0x230542eda83346929e4E54f4a98e1ca1A4BFc0c3", + "symbol": "MOOS", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0x671f0662de72268d0f3966Fb62dFc6ee6389e244": { + "chain_id": 42220, + "entrypoint_address": "0x45a8e6AaDCc48D1Ce19eCbE07Ccd3a536EF712ed", + "paymaster_address": "0x55E519bfD63c7152D9F7B88Acd712A37F0BEC482", + "account_factory_address": "0x671f0662de72268d0f3966Fb62dFc6ee6389e244", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x55E519bfD63c7152D9F7B88Acd712A37F0BEC482", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [ + { + "name": "Market", + "icon": "https://moos.citizenwallet.xyz/wallet-config/_images/moos.svg", + "url": "https://marketplace.citizenwallet.xyz/moos", + "launch_mode": "webview", + "signature": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/moos", + "version": 5 + }, + { + "community": { + "name": "Bonne Heure", + "description": "Système d'Échange Local de Villers-la-Ville", + "url": "https://selcoupdepouce.be", + "alias": "selcoupdepouce", + "logo": "https://topup.citizenwallet.xyz/communities/selcoupdepouce/sel-coin.svg", + "hidden": true, + "profile": { + "address": "0xfB8F1e7ED42599638B3c509679E2F43937002C56", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x5Cdbc862BF4E20D98456D4c41D4A5239aDd496d3", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x5Cdbc862BF4E20D98456D4c41D4A5239aDd496d3": { + "standard": "erc20", + "name": "Bonne Heure", + "address": "0x5Cdbc862BF4E20D98456D4c41D4A5239aDd496d3", + "symbol": "BHR", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0x4Cc883b7E8E0BCB2e293703EF06426F9b4A5A284": { + "chain_id": 42220, + "entrypoint_address": "0xA90904F33df36899d810d040b8d5b3b77265Bb05", + "paymaster_address": "0x635032605337aB36A46D767905108e67EE687a72", + "account_factory_address": "0x4Cc883b7E8E0BCB2e293703EF06426F9b4A5A284", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x635032605337aB36A46D767905108e67EE687a72", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "plugins": [ + { + "name": "Market", + "icon": "https://marketplace.citizenwallet.xyz/marketplace.svg", + "url": "https://marketplace.citizenwallet.xyz/selcoupdepouce", + "launch_mode": "browser", + "signature": true + } + ], + "config_location": "https://my.citizenwallet.xyz/api/communities/selcoupdepouce", + "version": 5 + }, + { + "community": { + "name": "CI token", + "description": "Monnaie locale du Cercle Informatique de l’ULB", + "url": "https://citizenwallet.xyz/cit", + "alias": "cit.celo", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/cit.celo.svg", + "hidden": true, + "profile": { + "address": "0x4cB296BEc9FAd0B5e1E4FF1A2F307B425724AC82", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x12e26FAED228c425BceA8a8dd7658a9CeD944dd9", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x12e26FAED228c425BceA8a8dd7658a9CeD944dd9": { + "standard": "erc20", + "name": "CI token", + "address": "0x12e26FAED228c425BceA8a8dd7658a9CeD944dd9", + "symbol": "CIT", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0x0a9f4B7e7Ec393fF25dc9267289Be259Ec3FB970": { + "chain_id": 42220, + "entrypoint_address": "0xB8d9412f3A91A00ca762B5c35cd0863E9b716D68", + "paymaster_address": "0x452F7ff3e55fe29f481841985dE7f4939FD645fa", + "account_factory_address": "0x0a9f4B7e7Ec393fF25dc9267289Be259Ec3FB970", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x452F7ff3e55fe29f481841985dE7f4939FD645fa", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/cit.celo", + "version": 5 + }, + { + "community": { + "name": "Wolugo", + "description": "A community for the Woluwe-Saint-Pierre civic engagement platform", + "url": "https://wolugo.be", + "alias": "wallet.wolugo.be", + "custom_domain": "wallet.wolugo.be", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/wolugo.svg", + "theme": { + "primary": "#81e2c1" + }, + "hidden": true, + "profile": { + "address": "0x07e7b95B35866302b3A089feF4CFA3061061a51d", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x13Dd4B3cD2f2Be3eb41cD0C3E2ce9F8d8C93A451", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + }, + "card_factory": { + "address": "0xA3E1446E332a098A1f3b0555c5d149b4784A095F", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x13Dd4B3cD2f2Be3eb41cD0C3E2ce9F8d8C93A451": { + "standard": "erc20", + "name": "Wolu", + "address": "0x13Dd4B3cD2f2Be3eb41cD0C3E2ce9F8d8C93A451", + "symbol": "WOLU", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "CELO Explorer" + }, + "accounts": { + "42220:0x8474153A00C959f2cB64852949954DBC68415Bb3": { + "chain_id": 42220, + "entrypoint_address": "0x0F805BC1ED718FB9C7C18439cB11E1C17C6538C4", + "paymaster_address": "0xF2EFEC3cBFaDE0bB6108620cbF7Cc608d27DCF3c", + "account_factory_address": "0x8474153A00C959f2cB64852949954DBC68415Bb3", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0xF2EFEC3cBFaDE0bB6108620cbF7Cc608d27DCF3c", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "cards": { + "42220:0xA3E1446E332a098A1f3b0555c5d149b4784A095F": { + "chain_id": 42220, + "address": "0xA3E1446E332a098A1f3b0555c5d149b4784A095F", + "type": "classic" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.wolugo.be", + "version": 5 + }, + { + "community": { + "name": "Woluwe Test", + "description": "Local currency for the Woluwe Test community.", + "url": "https://wollet-v2.citizenwallet.net/token", + "alias": "wtc.celo", + "logo": "https://wtc.celo.citizenwallet.xyz/wallet-config/_images/wtc.celo.svg", + "hidden": true, + "profile": { + "address": "0xB99a7B1574f051020EB4cb2fce5d48EE07592AfF", + "chain_id": 42220 + }, + "primary_token": { + "address": "0xc53Cb35591959cA62471dA9fF6AC16629A89874a", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0xc53Cb35591959cA62471dA9fF6AC16629A89874a": { + "standard": "erc20", + "name": "Woluwe Test Coin", + "address": "0xc53Cb35591959cA62471dA9fF6AC16629A89874a", + "symbol": "WTC", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0xE79E19594A749330036280c685E2719d58d99052": { + "chain_id": 42220, + "entrypoint_address": "0x1EaF6B6A6967608aF6c77224f087b042095891EB", + "paymaster_address": "0x3fefC19674f3F6E43B1dFf1861E07c303B9eAAc9", + "account_factory_address": "0xE79E19594A749330036280c685E2719d58d99052", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x3fefC19674f3F6E43B1dFf1861E07c303B9eAAc9", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/wtc.celo", + "version": 5 + }, + { + "community": { + "name": "ETHGlobal London Token", + "description": "The community of people using ETHLDN on Base.", + "url": "https://en.wikipedia.org/wiki/USD_Coin", + "alias": "testnet-ethldn", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/testnet-ethldn.svg", + "hidden": true, + "profile": { + "address": "0x0785D720279f42326846D5396b5F44b97d0BfECd", + "chain_id": 84532 + }, + "primary_token": { + "address": "0x9b1a0D2951b11Ac26A6cBbd5aEf2c4cb014b3B6e", + "chain_id": 84532 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 84532 + } + }, + "tokens": { + "84532:0x9b1a0D2951b11Ac26A6cBbd5aEf2c4cb014b3B6e": { + "standard": "erc20", + "name": "ETHGlobal London Token", + "address": "0x9b1a0D2951b11Ac26A6cBbd5aEf2c4cb014b3B6e", + "symbol": "ETHLDN", + "decimals": 6, + "chain_id": 84532 + } + }, + "scan": { + "url": "https://sepolia.basescan.org", + "name": "Base Sepolia Explorer" + }, + "accounts": { + "84532:0xc1654087C580f868F08E34cd1c01eDB1d3673b82": { + "chain_id": 84532, + "entrypoint_address": "0xBABCf159c4e3186cf48e4a48bC0AeC17CF9d90FE", + "paymaster_address": "0x389182aCCeE26D953d5188BF4b92c49339DcC9FC", + "account_factory_address": "0xc1654087C580f868F08E34cd1c01eDB1d3673b82", + "paymaster_type": "cw" + }, + "84532:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 84532, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x389182aCCeE26D953d5188BF4b92c49339DcC9FC", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "84532": { + "id": 84532, + "node": { + "url": "https://84532.engine.citizenwallet.xyz", + "ws_url": "wss://84532.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/testnet-ethldn", + "version": 5 + }, + { + "community": { + "name": "Celo Community Point", + "description": "This is a community for the Celo Point", + "url": "https://citizenwallet.xyz", + "alias": "celo-c.citizenwallet.xyz", + "logo": "https://celo-c.citizenwallet.xyz/uploads/logo.svg", + "hidden": true, + "profile": { + "address": "0x14004E13907282cFaD05f742022E56926eE92dAd", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x3C960E72BBbD837293e75080E1d0Fee6a4640357", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x3C960E72BBbD837293e75080E1d0Fee6a4640357": { + "standard": "erc20", + "name": "Celo Community Point", + "address": "0x3C960E72BBbD837293e75080E1d0Fee6a4640357", + "symbol": "CeloC", + "decimals": 6, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "CELO Explorer" + }, + "accounts": { + "42220:0xcd8b1B9E760148c5026Bc5B0D56a5374e301FDcA": { + "chain_id": 42220, + "entrypoint_address": "0x66fE9c22CcA49B257dd4F00508AC90198d99Bf27", + "paymaster_address": "0x7f4011845Ea914b6cefc60629e1e00600c972c75", + "account_factory_address": "0xcd8b1B9E760148c5026Bc5B0D56a5374e301FDcA", + "paymaster_type": "cw" + }, + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x7f4011845Ea914b6cefc60629e1e00600c972c75", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/celo-c.citizenwallet.xyz", + "version": 5 + }, + { + "community": { + "name": "KFMEDIA℠", + "description": "Certified Education Organization. Solving systemic educational disparity using Web3 solutions, removing barriers of entry for underdeveloped economies.", + "url": "https://kingfishersmedia.io", + "alias": "wallet.kingfishersmedia.io", + "custom_domain": "wallet.kingfishersmedia.io", + "logo": "https://assets.citizenwallet.xyz/wallet-config/_images/kfmpfl.png", + "theme": { + "primary": "#88292c" + }, + "profile": { + "address": "0x5f6FEb03ad8EfeCdD2a837FAA1a29DEA2bAcfd55", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x56744910f7dEcD48c1a7FA61B4C317b15E99F156", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + }, + "primary_session_manager": { + "address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x56744910f7dEcD48c1a7FA61B4C317b15E99F156": { + "standard": "erc1155", + "name": "KFMEDIA℠ Pathways for LATAM™", + "address": "0x56744910f7dEcD48c1a7FA61B4C317b15E99F156", + "symbol": "KFMPFL", + "decimals": 0, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x02BDA8370d9497A5C808B2db237cfaA8f0733F36", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "sessions": { + "42220:0xE2F3DC3E638113b9496060349e5332963d9C1152": { + "chain_id": 42220, + "module_address": "0xE2F3DC3E638113b9496060349e5332963d9C1152", + "factory_address": "0xEd0cD3886b84369A0e29Db9a4480ADF5051c76C9", + "provider_address": "0xF3004A1690f97Cf5d307eDc5958a7F76b62f9FC9" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "config_location": "https://my.citizenwallet.xyz/api/communities/wallet.kingfishersmedia.io", + "version": 5 + } +] \ No newline at end of file diff --git a/assets/config/v5/debug.json b/assets/config/v5/debug.json new file mode 100644 index 00000000..47501c0f --- /dev/null +++ b/assets/config/v5/debug.json @@ -0,0 +1,66 @@ +{ + "community": { + "name": "KFMEDIA℠", + "description": "Certified Education Organization. Solving systemic educational disparity using Web3 solutions, removing barriers of entry for underdeveloped economies.", + "url": "https://kingfishersmedia.io", + "alias": "wallet.kingfishersmedia.io", + "custom_domain": "wallet.kingfishersmedia.io", + "logo": "https://config.internal.citizenwallet.xyz/_images/kfmpfl.png", + "theme": { + "primary": "#88292c" + }, + "profile": { + "address": "0x5f6FEb03ad8EfeCdD2a837FAA1a29DEA2bAcfd55", + "chain_id": 42220 + }, + "primary_token": { + "address": "0x56744910f7dEcD48c1a7FA61B4C317b15E99F156", + "chain_id": 42220 + }, + "primary_account_factory": { + "address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "chain_id": 42220 + }, + "primary_card_manager": { + "address": "0x1EaF6B6A6967608aF6c77224f087b042095891EB", + "chain_id": 42220 + } + }, + "tokens": { + "42220:0x56744910f7dEcD48c1a7FA61B4C317b15E99F156": { + "standard": "erc1155", + "name": "KFMEDIA℠ Pathways for LATAM™", + "address": "0x56744910f7dEcD48c1a7FA61B4C317b15E99F156", + "symbol": "KFMPFL", + "decimals": 0, + "chain_id": 42220 + } + }, + "scan": { + "url": "https://celoscan.io", + "name": "Celo Explorer" + }, + "accounts": { + "42220:0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185": { + "chain_id": 42220, + "entrypoint_address": "0x7079253c0358eF9Fd87E16488299Ef6e06F403B6", + "paymaster_address": "0x250711045d58b6310f0635C7D110BFe663cE1da5", + "account_factory_address": "0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185", + "paymaster_type": "cw-safe" + } + }, + "chains": { + "42220": { + "id": 42220, + "node": { + "url": "https://42220.engine.citizenwallet.xyz", + "ws_url": "wss://42220.engine.citizenwallet.xyz" + } + } + }, + "ipfs": { + "url": "https://ipfs.internal.citizenwallet.xyz" + }, + "config_location": "https://config.internal.citizenwallet.xyz/v4/wallet.kingfishersmedia.io.json", + "version": 5 +} \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cf296501..c30fca89 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -159,9 +159,9 @@ PODS: - reown_yttrium (0.0.1): - Flutter - YttriumWrapper (= 0.8.35) - - SDWebImage (5.21.0): - - SDWebImage/Core (= 5.21.0) - - SDWebImage/Core (5.21.0) + - SDWebImage (5.21.2): + - SDWebImage/Core (= 5.21.2) + - SDWebImage/Core (5.21.2) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -265,43 +265,43 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f - audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40 - connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d - credential_manager: feb21034894e469e3686461dc96fb24bb7d350e4 + audioplayers_darwin: ccf9c770ee768abb07e26d90af093f7bab1c12ab + connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd + credential_manager: e32c96e222e95368067810f7529eea06d9c5c171 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - file_picker: be9a674155d9f334323856cb266e0d145d75d5c0 + file_picker: 6d4bf27b7318804adf4ddc1827dbc5cd4b78042d Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2 - firebase_core: 3c2f323cae65c97a636a05a23b17730ef93df2cf - firebase_messaging: 456e01ff29a451c90097d0b45925551d5be0c143 + firebase_core: ba71b44041571da878cb624ce0d80250bcbe58ad + firebase_messaging: 13129fe2ca166d1ed2d095062d76cee88943d067 FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7 FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679 FirebaseInstallations: 9980995bdd06ec8081dfb6ab364162bdd64245c3 FirebaseMessaging: 2b9f56aa4ed286e1f0ce2ee1d413aabb8f9f5cb9 - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 - flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 - google_sign_in_ios: 7411fab6948df90490dc4620ecbcabdc3ca04017 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 + flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 + google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 - icloud_storage: d9ac7a33ced81df08ba7ea1bf3099cc0ee58f60a - mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e + icloud_storage: e55639f0c0d7cb2b0ba9c0b3d5968ccca9cd9aa2 + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - nfc_manager: d7da7cb781f7744b94df5fe9dbca904ac4a0939e + nfc_manager: 9c40fe22528ab871ca11e52ea8b95790e9d92ca2 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - reown_yttrium: c0e87e5965fa60a3559564cc35cffbba22976089 - SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 - share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d + reown_yttrium: cee334ade64725b1d83f7b34c706a6aae2696d58 + SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966 PODFILE CHECKSUM: f90b7b7d52ec0d905039aa6f51266424548151c7 diff --git a/lib/main.dart b/lib/main.dart index f40375a6..f942def2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,7 +7,6 @@ import 'package:citizenwallet/services/config/service.dart'; import 'package:citizenwallet/services/db/account/db.dart'; import 'package:citizenwallet/services/db/app/db.dart'; import 'package:citizenwallet/services/preferences/preferences.dart'; -import 'package:citizenwallet/services/wallet/wallet.dart'; import 'package:citizenwallet/state/app/logic.dart'; import 'package:citizenwallet/state/app/state.dart'; import 'package:citizenwallet/state/communities/logic.dart'; @@ -61,7 +60,6 @@ FutureOr appRunner() async { AccountDBService(); - WalletService(); final config = ConfigService(); if (kIsWeb) { @@ -76,7 +74,7 @@ FutureOr appRunner() async { } final AppDBService appDBService = AppDBService(); - await appDBService.init('appv4'); + await appDBService.init('appv5'); final numConfigs = (await appDBService.communities.getAll()).length; config.singleCommunityMode = numConfigs < 2; diff --git a/lib/modals/account/switch_account.dart b/lib/modals/account/switch_account.dart index a91a5840..e23e4a99 100644 --- a/lib/modals/account/switch_account.dart +++ b/lib/modals/account/switch_account.dart @@ -1,6 +1,7 @@ // import 'package:citizenwallet/l10n/app_localizations.dart'; import 'package:citizenwallet/modals/wallet/community_picker.dart'; import 'package:citizenwallet/screens/wallet/wallet_row.dart'; +import 'package:citizenwallet/state/app/state.dart'; import 'package:citizenwallet/state/communities/logic.dart'; import 'package:citizenwallet/state/communities/selectors.dart'; import 'package:citizenwallet/state/profiles/logic.dart'; @@ -16,6 +17,7 @@ import 'package:citizenwallet/widgets/export_wallet_modal.dart'; import 'package:citizenwallet/widgets/header.dart'; import 'package:citizenwallet/widgets/scanner/scanner_modal.dart'; import 'package:citizenwallet/widgets/text_input_modal.dart'; +import 'package:citizenwallet/utils/migration_modal.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; @@ -83,6 +85,14 @@ class SwitchAccountModalState extends State { GoRouter.of(context).pop(); } + void handleMigration(BuildContext context) async { + GoRouter.of(context).pop(); + + await Future.delayed(const Duration(milliseconds: 300)); + + await MigrationModalUtils.showMigrationModal(context); + } + void handleCreate(BuildContext context) async { final navigator = GoRouter.of(context); @@ -402,69 +412,120 @@ class SwitchAccountModalState extends State { bottom: 20, left: 20, right: 20, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CupertinoButton( - padding: const EdgeInsets.fromLTRB(15, 5, 15, 5), - onPressed: () => handleImport(context), - borderRadius: BorderRadius.circular(25), - color: Theme.of(context) - .colors - .uiBackground - .resolveFrom(context), - child: Row( + child: Consumer( + builder: (context, appState, child) { + // Show migrate button if migration is required + if (appState.migrationRequired) { + return Row( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - AppLocalizations.of(context)!.importText, - style: TextStyle( - color: Theme.of(context) - .colors - .text - .resolveFrom(context), - ), - ), - const SizedBox(width: 5), - Icon( - CupertinoIcons.down_arrow, + CupertinoButton( + padding: + const EdgeInsets.fromLTRB(15, 5, 15, 5), + onPressed: () => handleMigration(context), + borderRadius: BorderRadius.circular(25), color: Theme.of(context) .colors - .text + .uiBackground .resolveFrom(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Text( + "Migrate Accounts", + style: TextStyle( + color: Theme.of(context) + .colors + .text + .resolveFrom(context), + ), + ), + const SizedBox(width: 5), + Icon( + CupertinoIcons.up_arrow, + color: Theme.of(context) + .colors + .text + .resolveFrom(context), + ), + ], + ), ), ], - ), - ), - const SizedBox(width: 10), - CupertinoButton( - padding: const EdgeInsets.fromLTRB(15, 5, 15, 5), - onPressed: () => handleCreate(context), - borderRadius: BorderRadius.circular(25), - color: Theme.of(context) - .colors - .surfacePrimary - .resolveFrom(context), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - AppLocalizations.of(context)!.joinCommunity, - style: TextStyle( - color: Theme.of(context).colors.black, - ), + ); + } + + // Show original import and join community buttons if migration is not required + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CupertinoButton( + padding: + const EdgeInsets.fromLTRB(15, 5, 15, 5), + onPressed: () => handleImport(context), + borderRadius: BorderRadius.circular(25), + color: Theme.of(context) + .colors + .uiBackground + .resolveFrom(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + AppLocalizations.of(context)!.importText, + style: TextStyle( + color: Theme.of(context) + .colors + .text + .resolveFrom(context), + ), + ), + const SizedBox(width: 5), + Icon( + CupertinoIcons.down_arrow, + color: Theme.of(context) + .colors + .text + .resolveFrom(context), + ), + ], ), - const SizedBox(width: 5), - Icon( - CupertinoIcons.plus, - color: Theme.of(context).colors.black, + ), + const SizedBox(width: 10), + CupertinoButton( + padding: + const EdgeInsets.fromLTRB(15, 5, 15, 5), + onPressed: () => handleCreate(context), + borderRadius: BorderRadius.circular(25), + color: Theme.of(context) + .colors + .surfacePrimary + .resolveFrom(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + AppLocalizations.of(context)! + .joinCommunity, + style: TextStyle( + color: Theme.of(context).colors.black, + ), + ), + const SizedBox(width: 5), + Icon( + CupertinoIcons.plus, + color: Theme.of(context).colors.black, + ), + ], ), - ], - ), - ), - ], + ), + ], + ); + }, ), ), ], diff --git a/lib/modals/landing/migration_modal.dart b/lib/modals/landing/migration_modal.dart new file mode 100644 index 00000000..81ee5279 --- /dev/null +++ b/lib/modals/landing/migration_modal.dart @@ -0,0 +1,181 @@ +import 'package:citizenwallet/theme/provider.dart' as theme_provider; +import 'package:citizenwallet/utils/platform.dart'; +import 'package:citizenwallet/widgets/button.dart'; +import 'package:citizenwallet/services/preferences/preferences.dart'; +import 'package:citizenwallet/services/migration/service.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:go_router/go_router.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class MigrationModal extends StatefulWidget { + final bool isWalletScreen; + + const MigrationModal({ + super.key, + this.isWalletScreen = false, + }); + + @override + MigrationModalState createState() => MigrationModalState(); +} + +class MigrationModalState extends State { + final PreferencesService _preferences = PreferencesService(); + final MigrationService _migrationService = MigrationService(); + bool _isMigrating = false; + + void handleDismiss() async { + await _preferences.incrementMigrationModalDismissalCount(); + + if (mounted) { + GoRouter.of(context).pop(); + } + } + + void handleMigrate() async { + if (widget.isWalletScreen) { + // Wallet screen: perform actual migration + await _performMigration(); + } else { + // Landing screen: launch app store + await _launchAppStore(); + } + + if (mounted) { + GoRouter.of(context).pop(); + } + } + + Future _performMigration() async { + setState(() { + _isMigrating = true; + }); + + try { + await _migrationService.performMigration(); + } catch (e) { + if (mounted) { + debugPrint('Migration failed: $e'); + } + } finally { + if (mounted) { + setState(() { + _isMigrating = false; + }); + } + } + } + + Future _launchAppStore() async { + String url; + if (isPlatformApple()) { + url = ''; + } else if (isPlatformAndroid()) { + url = ''; + } else { + url = ''; + } + + try { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } catch (e) { + // URL launch failed + } + } + + @override + Widget build(BuildContext context) { + final theme = theme_provider.Theme.of(context); + + return Container( + height: MediaQuery.of(context).size.height * 0.5, + decoration: BoxDecoration( + color: CupertinoColors.systemBackground, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Icon or Logo + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: theme.colors.surfacePrimary + .resolveFrom(context) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Icon( + CupertinoIcons.arrow_up_circle_fill, + size: 40, + color: theme.colors.surfacePrimary.resolveFrom(context), + ), + ), + + const SizedBox(height: 24), + + Text( + widget.isWalletScreen ? 'Migrate Your Data' : 'We\'ve Moved!', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 16), + + Text( + widget.isWalletScreen + ? 'Migrate your wallet data to the new app. Your accounts and settings will be securely transferred.' + : 'We\'ve migrated to a new and improved application. Download the new app to continue enjoying all the features.', + style: TextStyle( + fontSize: 16, + color: + CupertinoColors.label.resolveFrom(context).withOpacity(0.8), + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 32), + + SizedBox( + width: double.infinity, + child: Button( + onPressed: _isMigrating ? null : handleMigrate, + text: _isMigrating + ? 'Migrating...' + : widget.isWalletScreen + ? 'Migrate' + : 'Download New App', + color: theme.colors.surfacePrimary.resolveFrom(context), + ), + ), + + const SizedBox(height: 16), + + SizedBox( + width: double.infinity, + child: Button( + onPressed: handleDismiss, + text: 'Dismiss', + color: CupertinoColors.systemGrey4, + labelColor: CupertinoColors.label, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modals/profile/edit.dart b/lib/modals/profile/edit.dart index ea6066a2..cc94824d 100644 --- a/lib/modals/profile/edit.dart +++ b/lib/modals/profile/edit.dart @@ -1,8 +1,6 @@ import 'package:citizenwallet/services/wallet/contracts/profile.dart'; -import 'package:citizenwallet/state/notifications/logic.dart'; import 'package:citizenwallet/state/profile/logic.dart'; import 'package:citizenwallet/state/profile/state.dart'; -import 'package:citizenwallet/state/wallet/logic.dart'; import 'package:citizenwallet/state/wallet/state.dart'; import 'package:citizenwallet/theme/provider.dart'; import 'package:citizenwallet/utils/delay.dart'; @@ -36,9 +34,7 @@ class EditProfileModalState extends State { final FocusNode nameFocusNode = FocusNode(); final FocusNode descriptionFocusNode = FocusNode(); - late ProfileLogic _logic; - late WalletLogic _walletLogic; - late NotificationsLogic _notificationsLogic; + late final ProfileLogic _logic = ProfileLogic(context); late Debounce debouncedHandleUsernameUpdate; late Debounce debouncedHandleNameUpdate; @@ -48,10 +44,6 @@ class EditProfileModalState extends State { void initState() { super.initState(); - _logic = ProfileLogic(context); - _notificationsLogic = NotificationsLogic(context); - _walletLogic = WalletLogic(context, _notificationsLogic); - debouncedHandleUsernameUpdate = debounce( (String username) { _logic.checkUsername(username); @@ -123,7 +115,6 @@ class EditProfileModalState extends State { HapticFeedback.lightImpact(); final wallet = context.read().wallet; - final newName = context.read().nameController.value.text; if (wallet == null) { return; @@ -140,10 +131,6 @@ class EditProfileModalState extends State { return; } - if (newName.isNotEmpty) { - await _walletLogic.editWallet(wallet.account, wallet.alias, newName); - } - HapticFeedback.heavyImpact(); navigator.pop(); } @@ -154,8 +141,8 @@ class EditProfileModalState extends State { FocusManager.instance.primaryFocus?.unfocus(); HapticFeedback.lightImpact(); - final wallet = context.read().wallet; - final newName = context.read().nameController.value.text; + final walletState = context.read(); + final wallet = walletState.wallet; if (wallet == null) { return; @@ -171,10 +158,6 @@ class EditProfileModalState extends State { return; } - if (newName.isNotEmpty) { - await _walletLogic.editWallet(wallet.account, wallet.alias, newName); - } - HapticFeedback.heavyImpact(); navigator.pop(); } @@ -219,9 +202,14 @@ class EditProfileModalState extends State { context.select((ProfileState state) => state.descriptionEdit); final username = context.select((ProfileState state) => state.username); + + final usernameErrorMessage = + context.select((ProfileState state) => state.usernameErrorMessage); + final hasProfile = username.isNotEmpty; - final isInvalid = usernameError || usernameController.value.text == ''; + final isInvalid = + usernameError || usernameController.value.text == '' || usernameLoading; final disableSave = config?.online == false || isInvalid; @@ -257,7 +245,8 @@ class EditProfileModalState extends State { child: ListView( controller: ModalScrollController.of(context), physics: const ScrollPhysics( - parent: BouncingScrollPhysics()), + parent: BouncingScrollPhysics(), + ), children: [ Stack( alignment: Alignment.center, @@ -386,11 +375,13 @@ class EditProfileModalState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - usernameController.value.text == '' - ? AppLocalizations.of(context)! - .pleasePickAUsername - : AppLocalizations.of(context)! - .thisUsernameIsAlreadyTaken, + usernameErrorMessage.isNotEmpty + ? usernameErrorMessage + : usernameController.value.text.isEmpty + ? AppLocalizations.of(context)! + .pleasePickAUsername + : AppLocalizations.of(context)! + .thisUsernameIsAlreadyTaken, style: TextStyle( color: Theme.of(context) .colors diff --git a/lib/modals/profile/profile.dart b/lib/modals/profile/profile.dart index 806e45f5..9104dc35 100644 --- a/lib/modals/profile/profile.dart +++ b/lib/modals/profile/profile.dart @@ -1,6 +1,7 @@ import 'package:citizenwallet/modals/profile/edit.dart'; import 'package:citizenwallet/state/profile/logic.dart'; import 'package:citizenwallet/state/profile/state.dart'; +import 'package:citizenwallet/state/wallet/logic.dart'; import 'package:citizenwallet/theme/provider.dart'; import 'package:citizenwallet/utils/delay.dart'; import 'package:citizenwallet/widgets/profile/profile_qr_badge.dart'; @@ -15,12 +16,14 @@ class ProfileModal extends StatefulWidget { final String account; final bool readonly; final bool keepLink; + final WalletLogic? walletLogic; const ProfileModal({ super.key, required this.account, this.readonly = false, this.keepLink = false, + this.walletLogic, }); @override diff --git a/lib/modals/wallet/deep_link.dart b/lib/modals/wallet/deep_link.dart index 30a7a3e6..b38f6729 100644 --- a/lib/modals/wallet/deep_link.dart +++ b/lib/modals/wallet/deep_link.dart @@ -1,7 +1,5 @@ -import 'dart:async'; - +import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/wallet/utils.dart'; -import 'package:citizenwallet/services/wallet/wallet.dart'; import 'package:citizenwallet/state/deep_link/logic.dart'; import 'package:citizenwallet/state/deep_link/state.dart'; import 'package:citizenwallet/state/wallet/state.dart'; @@ -15,16 +13,21 @@ import 'package:go_router/go_router.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:provider/provider.dart'; import 'package:citizenwallet/l10n/app_localizations.dart'; +import 'package:web3dart/web3dart.dart'; class DeepLinkModal extends StatefulWidget { - final WalletService wallet; + final Config config; + final EthPrivateKey credentials; + final EthereumAddress account; final String deepLink; final String deepLinkParams; const DeepLinkModal({ super.key, - required this.wallet, + required this.config, + required this.credentials, + required this.account, required this.deepLink, required this.deepLinkParams, }); @@ -42,7 +45,7 @@ class DeepLinkModalState extends State { void initState() { super.initState(); - _logic = DeepLinkLogic(context, widget.wallet); + _logic = DeepLinkLogic(context, widget.config, widget.credentials, widget.account); // post frame callback WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/modals/wallet/voucher_read.dart b/lib/modals/wallet/voucher_read.dart index 20aa15a0..7fe8dbca 100644 --- a/lib/modals/wallet/voucher_read.dart +++ b/lib/modals/wallet/voucher_read.dart @@ -67,6 +67,17 @@ class VoucherReadModalState extends State } void onLoad() async { + final walletLogic = widget.logic; + if (walletLogic.config != null && + walletLogic.credentials != null && + walletLogic.accountAddress != null) { + _logic.setWalletState( + walletLogic.config!, + walletLogic.credentials!, + walletLogic.accountAddress!, + ); + } + final voucher = await _logic.openVoucher(widget.address); if (voucher == null) { @@ -81,6 +92,17 @@ class VoucherReadModalState extends State } void handleRedeem() async { + final walletLogic = widget.logic; + if (walletLogic.config != null && + walletLogic.credentials != null && + walletLogic.accountAddress != null) { + _logic.setWalletState( + walletLogic.config!, + walletLogic.credentials!, + walletLogic.accountAddress!, + ); + } + final navigator = GoRouter.of(context); _logic.returnVoucher( diff --git a/lib/router/router.dart b/lib/router/router.dart index a2883c19..9ccdd9a9 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -188,15 +188,22 @@ GoRouter createRouter( } final extra = state.extra as Map; + final logic = extra['logic'] as WalletLogic; return ChangeNotifierProvider( key: Key('transaction-$transactionHash'), - create: (_) => TransactionState( - transactionHash: transactionHash, - ), + create: (_) { + final transactionState = TransactionState( + transactionHash: transactionHash, + ); + if (logic.config != null) { + transactionState.setConfig(logic.config!); + } + return transactionState; + }, child: TransactionScreen( transactionId: transactionHash, - logic: extra['logic'], + logic: logic, profilesLogic: extra['profilesLogic'], ), ); @@ -393,6 +400,7 @@ GoRouter createRouter( address: state.pathParameters['voucher'] ?? '', amount: extra['amount'], logo: extra['logo'], + walletLogic: extra['walletLogic'], ); }, ), @@ -441,7 +449,6 @@ GoRouter createRouter( return ChangeNotifierProvider( create: (_) => DeepLinkState(extra['deepLink']), child: DeepLinkScreen( - wallet: extra['wallet'], deepLink: extra['deepLink'], deepLinkParams: extra['deepLinkParams'], ), @@ -570,14 +577,29 @@ GoRouter createWebRouter( return const SizedBox(); } + final transactionHash = + state.pathParameters['transactionId']; + if (transactionHash == null) { + return const SizedBox(); + } + final extra = state.extra as Map; - return PopScope( - canPop: false, - child: TransactionScreen( - transactionId: state.pathParameters['transactionId'], - logic: extra['logic'], - profilesLogic: extra['profilesLogic'], + return ChangeNotifierProvider( + key: Key('transaction-$transactionHash'), + create: (_) { + final transactionState = TransactionState( + transactionHash: transactionHash, + ); + return transactionState; + }, + child: PopScope( + canPop: false, + child: TransactionScreen( + transactionId: transactionHash, + logic: extra['logic'], + profilesLogic: extra['profilesLogic'], + ), ), ); }, diff --git a/lib/screens/account/screen.dart b/lib/screens/account/screen.dart index 754c1031..df568816 100644 --- a/lib/screens/account/screen.dart +++ b/lib/screens/account/screen.dart @@ -75,7 +75,10 @@ class AccountScreenState extends State { await _logic.loadProfileLink(); if (hasChanged) { - _logic.resetAll(); + final profileState = context.read(); + if (profileState.username.isEmpty) { + _logic.resetAll(); + } final online = _walletLogic.isOnline; _logic.loadProfile(online: online); } diff --git a/lib/screens/accounts/screen.dart b/lib/screens/accounts/screen.dart index a790ac5e..b04cad9d 100644 --- a/lib/screens/accounts/screen.dart +++ b/lib/screens/accounts/screen.dart @@ -12,9 +12,9 @@ import 'package:citizenwallet/state/wallet/state.dart'; import 'package:citizenwallet/theme/provider.dart'; import 'package:citizenwallet/utils/delay.dart'; import 'package:citizenwallet/utils/formatters.dart'; +import 'package:citizenwallet/utils/migration_modal.dart'; import 'package:citizenwallet/utils/ratio.dart'; import 'package:citizenwallet/widgets/confirm_modal.dart'; -import 'package:citizenwallet/widgets/expansion_panel/expansion_panel.dart'; import 'package:citizenwallet/widgets/export_wallet_modal.dart'; import 'package:citizenwallet/widgets/header.dart'; import 'package:citizenwallet/widgets/persistent_header_delegate.dart'; @@ -91,6 +91,20 @@ class AccountsScreenState extends State { GoRouter.of(context).pop(); } + Future handleMigration(BuildContext context) async { + final rootContext = + GoRouter.of(context).routerDelegate.navigatorKey.currentContext; + + GoRouter.of(context).pop(); + + await Future.delayed(const Duration(milliseconds: 300)); + + if (rootContext != null) { + await MigrationModalUtils.showMigrationModal(rootContext, + isWalletScreen: true); + } + } + void handleJoin(BuildContext context) async { final navigator = GoRouter.of(context); @@ -672,76 +686,124 @@ class AccountsScreenState extends State { bottom: 20, left: 20, right: 20, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CupertinoButton( - padding: const EdgeInsets.fromLTRB(15, 5, 15, 5), - onPressed: () => handleImport(context), - borderRadius: BorderRadius.circular(25), - color: Theme.of(context) - .colors - .uiBackground - .resolveFrom(context), - child: Row( + child: Consumer( + builder: (context, appState, child) { + if (appState.migrationRequired) { + return Row( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - AppLocalizations.of(context)!.importText, - style: TextStyle( - color: Theme.of(context) - .colors - .text - .resolveFrom(context), - ), - ), - const SizedBox(width: 5), - Icon( - CupertinoIcons.down_arrow, + CupertinoButton( + padding: + const EdgeInsets.fromLTRB(15, 5, 15, 5), + onPressed: () => handleMigration(context), + borderRadius: BorderRadius.circular(25), color: Theme.of(context) .colors - .text + .uiBackground .resolveFrom(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Text( + "Migrate Accounts", + style: TextStyle( + color: Theme.of(context) + .colors + .text + .resolveFrom(context), + ), + ), + const SizedBox(width: 5), + Icon( + CupertinoIcons.up_arrow, + color: Theme.of(context) + .colors + .text + .resolveFrom(context), + ), + ], + ), ), ], - ), - ), - const SizedBox(width: 10), - CupertinoButton( - padding: const EdgeInsets.fromLTRB(15, 5, 15, 5), - onPressed: singleCommunityMode - ? () => - handleCreate(context, currentWallet?.alias) - : () => handleJoin(context), - borderRadius: BorderRadius.circular(25), - color: Theme.of(context) - .colors - .surfacePrimary - .resolveFrom(context), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - singleCommunityMode - ? AppLocalizations.of(context)! - .createNewAccount - : AppLocalizations.of(context)! - .joinCommunity, - style: TextStyle( - color: Theme.of(context).colors.black, - ), + ); + } + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CupertinoButton( + padding: + const EdgeInsets.fromLTRB(15, 5, 15, 5), + onPressed: () => handleImport(context), + borderRadius: BorderRadius.circular(25), + color: Theme.of(context) + .colors + .uiBackground + .resolveFrom(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + AppLocalizations.of(context)!.importText, + style: TextStyle( + color: Theme.of(context) + .colors + .text + .resolveFrom(context), + ), + ), + const SizedBox(width: 5), + Icon( + CupertinoIcons.down_arrow, + color: Theme.of(context) + .colors + .text + .resolveFrom(context), + ), + ], ), - const SizedBox(width: 5), - Icon( - CupertinoIcons.plus, - color: Theme.of(context).colors.black, + ), + const SizedBox(width: 10), + CupertinoButton( + padding: + const EdgeInsets.fromLTRB(15, 5, 15, 5), + onPressed: singleCommunityMode + ? () => handleCreate( + context, currentWallet?.alias) + : () => handleJoin(context), + borderRadius: BorderRadius.circular(25), + color: Theme.of(context) + .colors + .surfacePrimary + .resolveFrom(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + singleCommunityMode + ? AppLocalizations.of(context)! + .createNewAccount + : AppLocalizations.of(context)! + .joinCommunity, + style: TextStyle( + color: Theme.of(context).colors.black, + ), + ), + const SizedBox(width: 5), + Icon( + CupertinoIcons.plus, + color: Theme.of(context).colors.black, + ), + ], ), - ], - ), - ), - ], + ), + ], + ); + }, ), ), ], diff --git a/lib/screens/cards/screen.dart b/lib/screens/cards/screen.dart index 80ef7c0b..484e4781 100644 --- a/lib/screens/cards/screen.dart +++ b/lib/screens/cards/screen.dart @@ -45,11 +45,6 @@ class CardsScreenState extends State { return; } - print( - '${AppLocalizations.of(context)!.initialAddress} ${widget.walletLogic.privateKey.address.hexEip55}'); - - // _logic.configure( - // widget.walletLogic.privateKey, wallet.account, wallet.alias); _logic.read(); } diff --git a/lib/screens/deeplink/deep_link.dart b/lib/screens/deeplink/deep_link.dart index 10acc764..6bae78f2 100644 --- a/lib/screens/deeplink/deep_link.dart +++ b/lib/screens/deeplink/deep_link.dart @@ -1,7 +1,4 @@ -import 'dart:async'; - import 'package:citizenwallet/services/wallet/utils.dart'; -import 'package:citizenwallet/services/wallet/wallet.dart'; import 'package:citizenwallet/state/deep_link/logic.dart'; import 'package:citizenwallet/state/deep_link/state.dart'; import 'package:citizenwallet/state/wallet/state.dart'; @@ -17,14 +14,11 @@ import 'package:provider/provider.dart'; import 'package:citizenwallet/l10n/app_localizations.dart'; class DeepLinkScreen extends StatefulWidget { - final WalletService wallet; - final String deepLink; final String deepLinkParams; const DeepLinkScreen({ super.key, - required this.wallet, required this.deepLink, required this.deepLinkParams, }); @@ -42,24 +36,23 @@ class DeepLinkScreenState extends State { void initState() { super.initState(); - _logic = DeepLinkLogic(context, widget.wallet); - // post frame callback WidgetsBinding.instance.addPostFrameCallback((_) { // initial requests go here - onLoad(); }); } @override void dispose() { - // - super.dispose(); } void onLoad() async { + final wallet = context.read().wallet; + if (wallet == null) { + return; + } switch (widget.deepLink) { case 'faucet-v1': // handle loading of metadata diff --git a/lib/screens/landing/account_recovery.dart b/lib/screens/landing/account_recovery.dart index 57305efb..82096562 100644 --- a/lib/screens/landing/account_recovery.dart +++ b/lib/screens/landing/account_recovery.dart @@ -5,7 +5,6 @@ import 'package:citizenwallet/state/backup/state.dart'; import 'package:citizenwallet/theme/provider.dart'; import 'package:citizenwallet/widgets/layouts/info_action.dart'; import 'package:citizenwallet/widgets/scanner/scanner_modal.dart'; -import 'package:citizenwallet/widgets/text_input_modal.dart'; import 'package:flutter/cupertino.dart'; import 'package:go_router/go_router.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; diff --git a/lib/screens/landing/screen.dart b/lib/screens/landing/screen.dart index b8c14204..25c13e77 100644 --- a/lib/screens/landing/screen.dart +++ b/lib/screens/landing/screen.dart @@ -1,5 +1,6 @@ // import 'package:citizenwallet/l10n/app_localizations.dart'; import 'package:citizenwallet/modals/account/select_account.dart'; + import 'package:citizenwallet/modals/wallet/community_picker.dart'; import 'package:citizenwallet/router/utils.dart'; import 'package:citizenwallet/services/wallet/utils.dart'; @@ -11,6 +12,7 @@ import 'package:citizenwallet/state/communities/logic.dart'; import 'package:citizenwallet/state/vouchers/logic.dart'; import 'package:citizenwallet/theme/provider.dart'; import 'package:citizenwallet/utils/platform.dart'; +import 'package:citizenwallet/utils/migration_modal.dart'; import 'package:citizenwallet/widgets/scanner/scanner_modal.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -126,6 +128,12 @@ class LandingScreenState extends State _appLogic.loadApp(); + // Feature flag for migration check - set to false to disable + const bool enableMigrationCheck = false; + if (enableMigrationCheck) { + await _appLogic.checkMigrationRequired(); + } + // set up recovery await handleAppleRecover(); await handleAndroidRecover(); @@ -167,7 +175,7 @@ class LandingScreenState extends State // pick an appropriate wallet to load if (widget.deepLink != null) { (address, alias) = await handleLoadFromParams(widget.deepLinkParams, - overrideAlias: alias); + overrideAlias: alias); } // handle send to params @@ -191,6 +199,9 @@ class LandingScreenState extends State if (address == null) { _appLogic.appLoaded(); + + await MigrationModalUtils.showMigrationModalIfNeeded(context); + return; } @@ -386,6 +397,7 @@ class LandingScreenState extends State navigator.go('/wallet/$address$params'); } + @override Widget build(BuildContext context) { final height = MediaQuery.of(context).size.height; @@ -670,6 +682,7 @@ class LandingScreenState extends State ), ), ), + const SizedBox(height: 20), ], if (isPlatformApple()) ...[ Container( diff --git a/lib/screens/send/send_details.dart b/lib/screens/send/send_details.dart index 6b3f8f9e..4b0ad510 100644 --- a/lib/screens/send/send_details.dart +++ b/lib/screens/send/send_details.dart @@ -123,16 +123,37 @@ class _SendDetailsScreenState extends State { final voucherLogic = widget.voucherLogic; if (voucherLogic == null) { + setState(() { + _isSending = false; + }); return; } final walletLogic = widget.walletLogic; - voucherLogic.createVoucher( - balance: widget.walletLogic.amountController.value.text, - symbol: symbol, - mint: mint, - ); + if (walletLogic.config != null && + walletLogic.credentials != null && + walletLogic.accountAddress != null) { + voucherLogic.setWalletState( + walletLogic.config!, + walletLogic.credentials!, + walletLogic.accountAddress!, + ); + } + + try { + voucherLogic.createVoucher( + balance: widget.walletLogic.amountController.value.text, + symbol: symbol, + mint: mint, + ); + } catch (e) { + debugPrint('Error creating voucher: $e'); + setState(() { + _isSending = false; + }); + return; + } voucherLogic.shareReady(); diff --git a/lib/screens/send/send_link_progress.dart b/lib/screens/send/send_link_progress.dart index 490ddad3..22173282 100644 --- a/lib/screens/send/send_link_progress.dart +++ b/lib/screens/send/send_link_progress.dart @@ -1,4 +1,3 @@ -import 'package:citizenwallet/models/transaction.dart'; import 'package:citizenwallet/services/wallet/utils.dart'; import 'package:citizenwallet/state/vouchers/logic.dart'; import 'package:citizenwallet/state/vouchers/state.dart'; diff --git a/lib/screens/send/send_progress.dart b/lib/screens/send/send_progress.dart index 136c97f7..9ef2e7fa 100644 --- a/lib/screens/send/send_progress.dart +++ b/lib/screens/send/send_progress.dart @@ -58,6 +58,16 @@ class _SendProgressState extends State { navigator.pop(); } + void handleDismiss(BuildContext context) { + final navigator = GoRouter.of(context); + + if (navigator.canPop()) { + navigator.pop(); + } else { + navigator.go('/wallet/${widget.walletLogic?.account}'); + } + } + void handleStartCloseScreenTimer(BuildContext context) { if (_isClosing) { return; @@ -153,11 +163,12 @@ class _SendProgressState extends State { final formattedAmount = inProgressTransaction.amount; - final profilesState = Provider.of(context, listen: true); - final selectedProfile = profilesState.selectedProfile; + final selectedProfile = context.watch().selectedProfile; final date = DateFormat.yMMMd().add_Hm().format(inProgressTransaction.date); + final hasTip = context.select((WalletState state) => state.hasTip); + final statusMessage = inProgressTransactionError ? widget.isMinting ? AppLocalizations.of(context)!.failedMint(wallet.symbol) @@ -359,7 +370,7 @@ class _SendProgressState extends State { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - context.select((WalletState state) => state.hasTip) + hasTip && widget.sendTransaction?.tipTo == null ? Column( children: [ Button( @@ -381,12 +392,7 @@ class _SendProgressState extends State { height: 10, ), CupertinoButton( - onPressed: () { - final navigator = GoRouter.of(context); - - navigator.go( - '/wallet/${widget.walletLogic?.account}'); - }, + onPressed: () => handleDismiss(context), child: ConstrainedBox( constraints: BoxConstraints( minWidth: 200, diff --git a/lib/screens/send/send_to.dart b/lib/screens/send/send_to.dart index 2774855d..2bd4c9cf 100644 --- a/lib/screens/send/send_to.dart +++ b/lib/screens/send/send_to.dart @@ -118,7 +118,18 @@ class _SendToScreenState extends State { final walletLogic = widget.walletLogic; final profilesLogic = widget.profilesLogic; - profilesLogic.allProfiles(); + if (walletLogic.config != null && + walletLogic.credentials != null && + walletLogic.accountAddress != null) { + profilesLogic.setWalletState( + walletLogic.config!, + walletLogic.credentials!, + walletLogic.accountAddress!, + ); + } + + profilesLogic.clearSearch(); + await profilesLogic.allProfiles(); walletLogic.updateAddress(); nameFocusNode.requestFocus(); @@ -135,9 +146,15 @@ class _SendToScreenState extends State { void handleThrottledUpdateAddress(String value) { final profilesLogic = widget.profilesLogic; + final walletLogic = widget.walletLogic; debouncedAddressUpdate(); - profilesLogic.searchProfile(value); + + if (walletLogic.config != null && + walletLogic.credentials != null && + walletLogic.accountAddress != null) { + profilesLogic.searchProfile(value); + } } void handleAddressFieldSubmitted(String value) { @@ -249,7 +266,7 @@ class _SendToScreenState extends State { return; } - widget.profilesLogic.getProfile(hex); + widget.profilesLogic.getLocalProfile(hex); if (!context.mounted) { return; diff --git a/lib/screens/send/tip_details.dart b/lib/screens/send/tip_details.dart index b6d380c8..421d34fd 100644 --- a/lib/screens/send/tip_details.dart +++ b/lib/screens/send/tip_details.dart @@ -67,17 +67,8 @@ class _TipDetailsScreenState extends State { super.initState(); _sendTransaction = widget.sendTransaction ?? SendTransaction(); - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) async { final walletLogic = widget.walletLogic; - final tipTo = context.read().tipTo; - - if (tipTo != null) { - widget.profilesLogic.getProfile(tipTo).then((profile) { - if (profile != null) { - widget.profilesLogic.selectProfile(profile); - } - }); - } onLoad(); @@ -115,6 +106,19 @@ class _TipDetailsScreenState extends State { void onLoad() async { await delay(const Duration(milliseconds: 250)); + final tipTo = context.read().tipTo; + + if (tipTo != null) { + try { + final profile = await widget.profilesLogic.getLocalProfile(tipTo); + if (profile != null) { + widget.profilesLogic.selectProfile(profile); + } + } catch (e) { + debugPrint('Error fetching profile: $e'); + } + } + amountFocusNode.requestFocus(); messageFocusNode.addListener(_onMessageFocusChange); } @@ -144,16 +148,37 @@ class _TipDetailsScreenState extends State { final voucherLogic = widget.voucherLogic; if (voucherLogic == null) { + setState(() { + _isSending = false; + }); return; } final walletLogic = widget.walletLogic; - voucherLogic.createVoucher( - balance: widget.walletLogic.amountController.value.text, - symbol: symbol, - mint: mint, - ); + if (walletLogic.config != null && + walletLogic.credentials != null && + walletLogic.accountAddress != null) { + voucherLogic.setWalletState( + walletLogic.config!, + walletLogic.credentials!, + walletLogic.accountAddress!, + ); + } + + try { + voucherLogic.createVoucher( + balance: widget.walletLogic.amountController.value.text, + symbol: symbol, + mint: mint, + ); + } catch (e) { + debugPrint('Error creating voucher: $e'); + setState(() { + _isSending = false; + }); + return; + } voucherLogic.shareReady(); @@ -243,7 +268,7 @@ class _TipDetailsScreenState extends State { final isValid = walletLogic.validateSendFields( walletLogic.amountController.value.text, - selectedAddress ?? walletLogic.addressController.value.text, + tipTo, ); if (!isValid) { @@ -282,7 +307,7 @@ class _TipDetailsScreenState extends State { HapticFeedback.heavyImpact(); final sent = await navigator.push( - '/wallet/${walletLogic.account}/send/$toAccount/progress', + '/wallet/${walletLogic.account}/send/$tipTo/progress', extra: { 'isMinting': widget.isMinting, 'walletLogic': walletLogic, @@ -291,16 +316,23 @@ class _TipDetailsScreenState extends State { }); if (sent == true) { + walletLogic.clearInProgressTransaction(); walletLogic.clearInputControllers(); walletLogic.resetInputErrorState(); widget.profilesLogic.clearSearch(); await Future.delayed(const Duration(milliseconds: 50)); - if (navigator.canPop()) { - navigator.go('/wallet/${walletLogic.account}'); + final walletAddress = walletLogic.address.isNotEmpty + ? walletLogic.address + : walletLogic.account.isNotEmpty + ? walletLogic.account + : context.read().wallet?.address ?? ''; + + if (walletAddress.isNotEmpty) { + navigator.go('/wallet/$walletAddress'); } else { - navigator.go('/wallet/${walletLogic.account}'); + navigator.go('/'); } return; } diff --git a/lib/screens/transaction/screen.dart b/lib/screens/transaction/screen.dart index 225f66b4..96f8217e 100644 --- a/lib/screens/transaction/screen.dart +++ b/lib/screens/transaction/screen.dart @@ -88,7 +88,7 @@ class TransactionScreenState extends State walletLogic.addressController.text = address; - final profile = await profilesLogic.getProfile(address); + final profile = await profilesLogic.getLocalProfile(address); walletLogic.updateAddress(override: profile != null); @@ -139,7 +139,7 @@ class TransactionScreenState extends State walletLogic.updateMessage(); walletLogic.updateListenerAmount(); - final profile = await profilesLogic.getProfile(address); + final profile = await profilesLogic.getLocalProfile(address); walletLogic.updateAddress(override: profile != null); walletLogic.updateAmount(); @@ -171,6 +171,7 @@ class TransactionScreenState extends State builder: (_) => ProfileModal( account: account, readonly: true, + walletLogic: widget.logic, ), ); } @@ -181,14 +182,151 @@ class TransactionScreenState extends State final wallet = context.select((WalletState state) => state.wallet); - final transaction = - context.watch().transaction; + final transactionState = + context.watch(); + final transaction = transactionState.transaction; + final transactionLoading = transactionState.loading; final loading = context.select((WalletState state) => state.loading); final blockSending = context.select(selectShouldBlockSending); - if (wallet == null || transaction == null) { + if (wallet == null) { + return const SizedBox(); + } + + if (transaction == null && transactionLoading) { + return CupertinoScaffold( + topRadius: const Radius.circular(40), + transitionBackgroundColor: Theme.of(context).colors.transparent, + body: CupertinoPageScaffold( + backgroundColor: + Theme.of(context).colors.uiBackgroundAlt.resolveFrom(context), + child: SafeArea( + top: false, + minimum: const EdgeInsets.only(left: 10, right: 10), + child: Flex( + direction: Axis.vertical, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: Stack( + alignment: Alignment.center, + children: [ + const Center( + child: CupertinoActivityIndicator(), + ), + Positioned( + top: MediaQuery.of(context).padding.top, + left: 0, + child: CupertinoButton( + padding: const EdgeInsets.all(5), + onPressed: () => handleDismiss(context), + child: Icon( + CupertinoIcons.back, + color: Theme.of(context) + .colors + .primary + .resolveFrom(context), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + if (transaction == null && !transactionLoading) { + return CupertinoScaffold( + topRadius: const Radius.circular(40), + transitionBackgroundColor: Theme.of(context).colors.transparent, + body: CupertinoPageScaffold( + backgroundColor: + Theme.of(context).colors.uiBackgroundAlt.resolveFrom(context), + child: SafeArea( + top: false, + minimum: const EdgeInsets.only(left: 10, right: 10), + child: Flex( + direction: Axis.vertical, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: Stack( + alignment: Alignment.center, + children: [ + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + CupertinoIcons.exclamationmark_triangle, + size: 48, + color: Theme.of(context) + .colors + .text + .resolveFrom(context), + ), + const SizedBox(height: 16), + Text( + 'Transaction not found', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .colors + .text + .resolveFrom(context), + ), + ), + const SizedBox(height: 8), + Text( + 'The transaction could not be loaded.', + style: TextStyle( + fontSize: 14, + color: Theme.of(context) + .colors + .text + .resolveFrom(context), + ), + ), + ], + ), + ), + Positioned( + top: MediaQuery.of(context).padding.top, + left: 0, + child: CupertinoButton( + padding: const EdgeInsets.all(5), + onPressed: () => handleDismiss(context), + child: Icon( + CupertinoIcons.back, + color: Theme.of(context) + .colors + .primary + .resolveFrom(context), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + if (transaction == null) { return const SizedBox(); } diff --git a/lib/screens/voucher/screen.dart b/lib/screens/voucher/screen.dart index a8cff67e..1c09f2d6 100644 --- a/lib/screens/voucher/screen.dart +++ b/lib/screens/voucher/screen.dart @@ -1,5 +1,6 @@ import 'package:citizenwallet/state/vouchers/logic.dart'; import 'package:citizenwallet/state/vouchers/state.dart'; +import 'package:citizenwallet/state/wallet/logic.dart'; import 'package:citizenwallet/state/wallet/state.dart'; import 'package:citizenwallet/theme/provider.dart'; import 'package:citizenwallet/widgets/blurry_child.dart'; @@ -18,12 +19,14 @@ class VoucherScreen extends StatefulWidget { final String address; final String amount; final String? logo; + final WalletLogic walletLogic; const VoucherScreen({ super.key, required this.address, required this.amount, this.logo = 'assets/icons/voucher.svg', + required this.walletLogic, }); @override @@ -61,6 +64,16 @@ class VoucherViewModalState extends State } void onLoad() async { + final walletLogic = widget.walletLogic; + if (walletLogic.config != null && + walletLogic.credentials != null && + walletLogic.accountAddress != null) { + _logic.setWalletState( + walletLogic.config!, + walletLogic.credentials!, + walletLogic.accountAddress!, + ); + } _logic.openVoucher(widget.address); } diff --git a/lib/screens/vouchers/screen.dart b/lib/screens/vouchers/screen.dart index adaa7929..9e6e8888 100644 --- a/lib/screens/vouchers/screen.dart +++ b/lib/screens/vouchers/screen.dart @@ -61,6 +61,17 @@ class VouchersScreenState extends State { } void onLoad() async { + final walletLogic = widget.walletLogic; + if (walletLogic.config != null && + walletLogic.credentials != null && + walletLogic.accountAddress != null) { + _logic.setWalletState( + walletLogic.config!, + walletLogic.credentials!, + walletLogic.accountAddress!, + ); + } + await _logic.fetchVouchers(); } @@ -87,6 +98,7 @@ class VouchersScreenState extends State { await navigator.push('/wallet/${wallet.account}/vouchers/$address', extra: { 'amount': amount, 'logo': logo, + 'walletLogic': widget.walletLogic, }); _logic.resume(address: address); } @@ -167,6 +179,7 @@ class VouchersScreenState extends State { .push('/wallet/${wallet.account}/vouchers/$address', extra: { 'amount': amount, 'logo': logo, + 'walletLogic': widget.walletLogic, }); } @@ -187,7 +200,21 @@ class VouchersScreenState extends State { ), ); - if (confirm == true) await _logic.returnVoucher(address); + if (confirm == true) { + + final walletLogic = widget.walletLogic; + if (walletLogic.config != null && + walletLogic.credentials != null && + walletLogic.accountAddress != null) { + _logic.setWalletState( + walletLogic.config!, + walletLogic.credentials!, + walletLogic.accountAddress!, + ); + } + + await _logic.returnVoucher(address); + } } if (!super.mounted) { diff --git a/lib/screens/vouchers/voucher_read.dart b/lib/screens/vouchers/voucher_read.dart index c66ace42..c08a1f61 100644 --- a/lib/screens/vouchers/voucher_read.dart +++ b/lib/screens/vouchers/voucher_read.dart @@ -67,6 +67,16 @@ class VoucherReadScreenState extends State } void onLoad() async { + final walletLogic = widget.logic; + if (walletLogic.config != null && + walletLogic.credentials != null && + walletLogic.accountAddress != null) { + _logic.setWalletState( + walletLogic.config!, + walletLogic.credentials!, + walletLogic.accountAddress!, + ); + } final voucher = await _logic.openVoucher(widget.address); if (voucher == null) { diff --git a/lib/screens/wallet/more_actions_sheet.dart b/lib/screens/wallet/more_actions_sheet.dart index 64302947..4d42efa3 100644 --- a/lib/screens/wallet/more_actions_sheet.dart +++ b/lib/screens/wallet/more_actions_sheet.dart @@ -4,11 +4,12 @@ import 'package:citizenwallet/state/wallet/state.dart'; import 'package:citizenwallet/theme/provider.dart'; import 'package:citizenwallet/widgets/coin_logo.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:provider/provider.dart'; import 'package:citizenwallet/l10n/app_localizations.dart'; + class MoreActionsSheet extends StatelessWidget { final void Function()? handleSendScreen; final void Function(PluginConfig pluginConfig)? handlePlugin; @@ -102,6 +103,7 @@ class MoreActionsSheet extends StatelessWidget { }), ); }).toList(); + default: return [ Container() diff --git a/lib/screens/wallet/screen.dart b/lib/screens/wallet/screen.dart index 7e695fcc..4d9d5954 100644 --- a/lib/screens/wallet/screen.dart +++ b/lib/screens/wallet/screen.dart @@ -9,6 +9,7 @@ import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/engine/events.dart'; import 'package:citizenwallet/services/wallet/utils.dart'; import 'package:citizenwallet/state/app/logic.dart'; +import 'package:citizenwallet/state/communities/logic.dart'; import 'package:citizenwallet/state/notifications/logic.dart'; import 'package:citizenwallet/state/notifications/state.dart'; import 'package:citizenwallet/state/profile/logic.dart'; @@ -19,6 +20,7 @@ import 'package:citizenwallet/state/wallet/state.dart'; import 'package:citizenwallet/state/wallet_connect/logic.dart'; import 'package:citizenwallet/state/wallet_connect/state.dart'; import 'package:citizenwallet/theme/provider.dart'; +import 'package:citizenwallet/utils/migration_modal.dart'; import 'package:citizenwallet/utils/qr.dart'; import 'package:citizenwallet/widgets/header.dart'; import 'package:citizenwallet/widgets/scanner/scanner_modal.dart'; @@ -74,6 +76,7 @@ class WalletScreenState extends State late ProfileLogic _profileLogic; late ProfilesLogic _profilesLogic; late VoucherLogic _voucherLogic; + late CommunitiesLogic _communitiesLogic; final WalletKitLogic _walletKitLogic = WalletKitLogic(); String? _address; @@ -84,7 +87,6 @@ class WalletScreenState extends State String? _deepLink; String? _deepLinkParams; String? _sendToURL; - Config? _config; @override void initState() { @@ -105,6 +107,7 @@ class WalletScreenState extends State _profileLogic = ProfileLogic(context); _profilesLogic = ProfilesLogic(context); _voucherLogic = VoucherLogic(context); + _communitiesLogic = CommunitiesLogic(context); WidgetsBinding.instance.addObserver(_profilesLogic); WidgetsBinding.instance.addObserver(_voucherLogic); @@ -183,6 +186,12 @@ class WalletScreenState extends State _address!, _alias!, (bool hasChanged) async { + _voucherLogic.setWalletState( + _logic.config!, + _logic.credentials!, + _logic.accountAddress!, + ); + _logic.requestWalletActions(); await _logic.loadTransactions(); @@ -210,6 +219,10 @@ class WalletScreenState extends State _notificationsLogic.init(); + if (_alias != null) { + await _communitiesLogic.fetchAndUpdateSingleCommunity(_alias!); + } + if (_voucher != null && _voucherParams != null) { await handleLoadFromVoucher(); } @@ -225,6 +238,8 @@ class WalletScreenState extends State if (_deepLink != null && _deepLinkParams != null) { await handleLoadDeepLink(); } + + await MigrationModalUtils.showMigrationModalIfNeeded(context); } Future handleDisconnect() async { @@ -308,7 +323,6 @@ class WalletScreenState extends State final navigator = GoRouter.of(context); await navigator.push('/wallet/$_address/deeplink', extra: { - 'wallet': _logic.wallet, 'deepLink': deepLink, 'deepLinkParams': deepLinkParams, }); @@ -445,7 +459,7 @@ class WalletScreenState extends State } _logic.updateAddress(override: true); - _profilesLogic.getProfile(addr); + _profilesLogic.getLocalProfile(addr); await navigator.push('/wallet/$_address/send/$addr', extra: { 'walletLogic': _logic, @@ -495,6 +509,7 @@ class WalletScreenState extends State account: wallet.account, readonly: true, keepLink: true, + walletLogic: _logic, ), ); @@ -514,8 +529,12 @@ class WalletScreenState extends State _voucherLogic.pause(); if (receiveParams != null) { + final wallet = context.read().wallet; + if (wallet == null) { + return; + } final hex = await _logic.updateFromCapture( - '/#/?alias=${_logic.wallet.alias}&receiveParams=$receiveParams', + '/#/?alias=${wallet.alias}&receiveParams=$receiveParams', ); if (hex == null) { @@ -532,7 +551,7 @@ class WalletScreenState extends State return; } - _profilesLogic.getProfile(hex); + _profilesLogic.getLocalProfile(hex); final navigator = GoRouter.of(context); @@ -808,7 +827,7 @@ class WalletScreenState extends State if (alias == null && params != null) { alias = paramsAlias(params); } - + if (alias == null) { return (null, null); } @@ -1048,13 +1067,14 @@ class WalletScreenState extends State final uriAlias = aliasFromUri(result); final receiveAlias = aliasFromReceiveUri(result); final sendAlias = aliasFromSendUri(result); - + if (voucherParams != null || deepLinkParams != null || sendToParams != null) { final (address, alias) = await handleLoadFromParams( voucherParams ?? sendToParams ?? deepLinkParams ?? parsedQRData.alias, - overrideAlias: uriAlias ?? receiveAlias ?? sendAlias ?? parsedQRData.alias, + overrideAlias: + uriAlias ?? receiveAlias ?? sendAlias ?? parsedQRData.alias, ); loadedAddress = address; loadedAlias = alias; @@ -1365,7 +1385,7 @@ class WalletScreenState extends State ), OfflineBanner( communityUrl: config?.community.url ?? '', - display: isOffline, + display: showOfflineBanner, loading: eventServiceState == EventServiceState.connecting, ), ], diff --git a/lib/screens/wallet/screen.web.dart b/lib/screens/wallet/screen.web.dart index 17f889d7..a95e0333 100644 --- a/lib/screens/wallet/screen.web.dart +++ b/lib/screens/wallet/screen.web.dart @@ -1,8 +1,6 @@ import 'package:citizenwallet/modals/profile/edit.dart'; -import 'package:citizenwallet/modals/wallet/deep_link.dart'; import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/wallet/utils.dart'; -import 'package:citizenwallet/state/deep_link/state.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:universal_html/html.dart' as html; @@ -194,8 +192,7 @@ class BurnerWalletScreenState extends State { switch (deepLink) { case 'plugin': - final pluginConfig = - await _logic.getPluginConfig(widget.alias!, params); + final pluginConfig = await _logic.getPluginConfig(widget.alias, params); if (pluginConfig == null) { return; } @@ -206,19 +203,12 @@ class BurnerWalletScreenState extends State { _profilesLogic.pause(); _voucherLogic.pause(); - await CupertinoScaffold.showCupertinoModalBottomSheet( - context: context, - expand: true, - useRootNavigator: true, - builder: (modalContext) => ChangeNotifierProvider( - create: (_) => DeepLinkState(deepLink), - child: DeepLinkModal( - wallet: _logic.wallet, - deepLink: deepLink, - deepLinkParams: params, - ), - ), - ); + final navigator = GoRouter.of(context); + + await navigator.push('/wallet/${widget.encoded}/deeplink', extra: { + 'deepLink': deepLink, + 'deepLinkParams': params, + }); _logic.resumeFetching(); _profilesLogic.resume(); diff --git a/lib/screens/wallet/tip_to.dart b/lib/screens/wallet/tip_to.dart index 7cb040f6..4c8842ee 100644 --- a/lib/screens/wallet/tip_to.dart +++ b/lib/screens/wallet/tip_to.dart @@ -7,13 +7,13 @@ import 'package:citizenwallet/state/profiles/state.dart'; import 'package:citizenwallet/state/wallet/logic.dart'; import 'package:citizenwallet/state/wallet/state.dart'; import 'package:citizenwallet/theme/provider.dart'; +import 'package:citizenwallet/utils/delay.dart'; import 'package:citizenwallet/widgets/button.dart'; import 'package:citizenwallet/widgets/header.dart'; import 'package:citizenwallet/widgets/persistent_header_delegate.dart'; import 'package:citizenwallet/widgets/profile/profile_chip.dart'; import 'package:citizenwallet/widgets/profile/profile_row.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; import 'package:citizenwallet/l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; @@ -73,10 +73,22 @@ class _TipToScreenState extends State { } void onLoad() async { + await delay(const Duration(milliseconds: 250)); + final walletLogic = widget.walletLogic; final profilesLogic = widget.profilesLogic; - profilesLogic?.allProfiles(); + if (walletLogic.config != null && + walletLogic.credentials != null && + walletLogic.accountAddress != null) { + profilesLogic?.setWalletState( + walletLogic.config!, + walletLogic.credentials!, + walletLogic.accountAddress!, + ); + } + + profilesLogic?.clearSearch(); walletLogic.updateAddress(); nameFocusNode.requestFocus(); @@ -84,9 +96,15 @@ class _TipToScreenState extends State { void handleThrottledUpdateAddress(String value) { final profilesLogic = widget.profilesLogic; + final walletLogic = widget.walletLogic; debouncedAddressUpdate(); - profilesLogic?.searchProfile(value); + + if (walletLogic.config != null && + walletLogic.credentials != null && + walletLogic.accountAddress != null) { + profilesLogic?.searchProfile(value); + } } void handleAddressFieldSubmitted(String value) { diff --git a/lib/screens/webview/screen.dart b/lib/screens/webview/screen.dart index 9384b095..e8492632 100644 --- a/lib/screens/webview/screen.dart +++ b/lib/screens/webview/screen.dart @@ -114,8 +114,6 @@ class _WebViewScreenState extends State { void handleBack() async { bool canGoBack = await webViewController?.canGoBack() ?? false; - print('can go back $canGoBack'); - if (canGoBack) { await webViewController?.goBack(); } diff --git a/lib/services/accounts/accounts.dart b/lib/services/accounts/accounts.dart index 3edeaf64..bec9387e 100644 --- a/lib/services/accounts/accounts.dart +++ b/lib/services/accounts/accounts.dart @@ -14,7 +14,7 @@ abstract class AccountsOptionsInterface {} /// /// This is used to store wallet backups and the implementation is platform specific. abstract class AccountsServiceInterface { - final int _version = 4; + final int _version = 6; int get version => _version; @@ -37,7 +37,7 @@ abstract class AccountsServiceInterface { Future setAccount(DBAccount account); // get account - Future getAccount(String address, String alias); + Future getAccount(String address, String alias, String accountFactoryAddress); // get accounts for alias Future> getAccountsForAlias(String alias); @@ -51,4 +51,6 @@ abstract class AccountsServiceInterface { Future populatePrivateKeysFromEncryptedStorage() async {} Future purgePrivateKeysAndAddToEncryptedStorage() async {} + + Future fixSafeAccounts() async {} } diff --git a/lib/services/accounts/native/android.dart b/lib/services/accounts/native/android.dart index 83a074a1..2a486506 100644 --- a/lib/services/accounts/native/android.dart +++ b/lib/services/accounts/native/android.dart @@ -3,6 +3,10 @@ import 'package:citizenwallet/services/db/backup/db.dart'; import 'package:citizenwallet/utils/encrypt.dart'; import 'package:citizenwallet/services/accounts/options.dart'; import 'package:citizenwallet/services/db/backup/accounts.dart'; +import 'package:citizenwallet/services/db/app/db.dart'; +import 'package:citizenwallet/services/config/config.dart'; +import 'package:citizenwallet/services/wallet/wallet.dart'; +import 'package:citizenwallet/services/wallet/contracts/safe_account.dart'; import 'package:citizenwallet/services/accounts/backup.dart'; import 'package:citizenwallet/services/accounts/accounts.dart'; @@ -10,6 +14,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:web3dart/crypto.dart'; import 'package:web3dart/web3dart.dart'; +import 'package:flutter/foundation.dart'; const pinCodeCheckKey = 'cw__pinCodeCheck__'; const pinCodeKey = 'cw__pinCode__'; @@ -27,6 +32,51 @@ class AndroidAccountsService extends AccountsServiceInterface { late SharedPreferences _sharedPreferences; late AccountBackupDBService _accountsDB; + Future _fixSafeAccount(DBAccount account, Config config) async { + try { + if (account.accountFactoryAddress != + '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2') { + return; + } + + final safeAccount = SafeAccount( + config.chains.values.first.node.chainId, + config.ethClient, + account.address.hexEip55, + ); + await safeAccount.init(); + + final calldata = safeAccount.fixFallbackHandlerCallData(); + + final (hash, userop) = await prepareUserop( + config, + account.address, + account.privateKey!, + [account.address.hexEip55], + [calldata], + deploy: false, + accountFactoryAddress: '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185', + ); + + final txHash = await submitUserop(config, userop, migrationSafe: true); + + if (txHash != null) { + debugPrint('fixed cw-safe account ${account.address.hexEip55}'); + } else { + debugPrint( + 'Failed to submit cw-safe account ${account.address.hexEip55}'); + } + } catch (e, stackTrace) { + debugPrint('Error: cw-safe account ${account.address.hexEip55}: $e'); + debugPrint('Stack trace: $stackTrace'); + + if (e.toString().contains('contract not whitelisted')) { + debugPrint( + 'Contract not whitelisted error for account ${account.address.hexEip55}'); + } + } + } + @override Future init(AccountsOptionsInterface options) async { final AndroidAccountsOptions androidOptions = @@ -38,6 +88,8 @@ class AndroidAccountsService extends AccountsServiceInterface { await _credentials.init(); await migrate(super.version); + + await migratePrivateKeysFromOldFormat(); } @override @@ -66,6 +118,7 @@ class AndroidAccountsService extends AccountsServiceInterface { alias: legacyBackup.alias, address: EthereumAddress.fromHex(legacyBackup.address), name: legacyBackup.name, + accountFactoryAddress: '', ); await _accountsDB.accounts.insert(account); @@ -90,16 +143,168 @@ class AndroidAccountsService extends AccountsServiceInterface { } } }, + 5: () async { + final allAccounts = await _accountsDB.accounts.all(); + + for (final account in allAccounts) { + if (account.accountFactoryAddress.isNotEmpty && + account.accountFactoryAddress != + '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2') { + continue; + } + + final community = await AppDBService().communities.get(account.alias); + if (community == null) { + continue; + } + + final config = Config.fromJson(community.config); + String accountFactoryAddress = + config.community.primaryAccountFactory.address; + + switch (account.alias) { + case 'gratitude': + accountFactoryAddress = + '0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD'; + break; + case 'bread': + accountFactoryAddress = + '0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9'; + break; + case 'wallet.commonshub.brussels': + accountFactoryAddress = + '0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87'; + break; + case 'wallet.sfluv.org': + accountFactoryAddress = + '0x5e987a6c4bb4239d498E78c34e986acf29c81E8e'; + break; + default: + if (account.accountFactoryAddress == + '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2') { + accountFactoryAddress = + '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185'; + } + break; + } + + final oldAccountId = account.id; + final oldPrivateKey = await _credentials.read(oldAccountId); + + final updatedAccount = DBAccount( + alias: account.alias, + address: account.address, + name: account.name, + username: account.username, + accountFactoryAddress: accountFactoryAddress, + profile: account.profile, + ); + + final newAccountId = updatedAccount.id; + + // Check if the account actually exists in the database with the expected ID + final existingAccount = await _accountsDB.accounts.db.query( + 't_accounts', + where: 'id = ?', + whereArgs: [oldAccountId], + ); + + if (existingAccount.isNotEmpty) { + // Delete using the expected ID + await _accountsDB.accounts.delete( + account.address, account.alias, account.accountFactoryAddress); + } else { + // Try to find by address and alias with empty accountFactoryAddress + final foundByAddress = await _accountsDB.accounts.db.query( + 't_accounts', + where: 'address = ? AND alias = ? AND accountFactoryAddress = ""', + whereArgs: [account.address.hexEip55, account.alias], + ); + + if (foundByAddress.isNotEmpty) { + final actualId = foundByAddress.first['id'] as String; + + // Delete using the actual ID + await _accountsDB.accounts.db.delete( + 't_accounts', + where: 'id = ?', + whereArgs: [actualId], + ); + } + } + + if (account.accountFactoryAddress == + '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2') { + final accountId = account.id; + await _sharedPreferences.setBool('needs_fixing_$accountId', true); + } + + // Insert the new record + await _accountsDB.accounts.insert(updatedAccount); + + if (oldPrivateKey != null) { + await _credentials.write(newAccountId, oldPrivateKey); + await _credentials.delete(oldAccountId); + } + } + }, + 6: () async { + final allAccounts = await _accountsDB.accounts.all(); + final toDelete = []; + + for (final account in allAccounts) { + if (account.accountFactoryAddress.isEmpty) { + toDelete.add(account); + } + } + + for (final account in toDelete) { + // Check if the account actually exists in the database + final existingAccount = await _accountsDB.accounts.db.query( + 't_accounts', + where: 'id = ?', + whereArgs: [account.id], + ); + + if (existingAccount.isNotEmpty) { + // Delete the account from database using the actual ID + await _accountsDB.accounts.db.delete( + 't_accounts', + where: 'id = ?', + whereArgs: [account.id], + ); + } else { + // Try to find by address and alias instead + final foundByAddress = await _accountsDB.accounts.db.query( + 't_accounts', + where: 'address = ? AND alias = ? AND accountFactoryAddress = ""', + whereArgs: [account.address.hexEip55, account.alias], + ); + + if (foundByAddress.isNotEmpty) { + final actualId = foundByAddress.first['id'] as String; + + // Delete using the actual ID + await _accountsDB.accounts.db.delete( + 't_accounts', + where: 'id = ?', + whereArgs: [actualId], + ); + } + } + + // Delete the private key from credentials + await _credentials.delete(account.id); + } + }, }; - // run all migrations - for (var i = oldVersion + 1; i <= version; i++) { + for (int i = oldVersion + 1; i <= version; i++) { if (migrations.containsKey(i)) { await migrations[i]!(); } } - // after success, we can update the version await _sharedPreferences.setString(versionPrefix, version.toString()); } @@ -143,10 +348,12 @@ class AndroidAccountsService extends AccountsServiceInterface { // get wallet backup @override - Future getAccount(String address, String alias) async { + Future getAccount(String address, String alias, + [String accountFactoryAddress = '']) async { final account = await _accountsDB.accounts.get( EthereumAddress.fromHex(address), alias, + accountFactoryAddress, ); if (account == null) { @@ -172,17 +379,25 @@ class AndroidAccountsService extends AccountsServiceInterface { // delete wallet backup @override Future deleteAccount(String address, String alias) async { - await _accountsDB.accounts.delete( - EthereumAddress.fromHex(address), - alias, - ); + final accounts = await _accountsDB.accounts.allForAlias(alias); + final account = + accounts.where((acc) => acc.address.hexEip55 == address).firstOrNull; - await _credentials.delete( - getAccountID( + if (account != null) { + await _accountsDB.accounts.delete( EthereumAddress.fromHex(address), alias, - ), - ); + account.accountFactoryAddress, + ); + + await _credentials.delete( + getAccountID( + EthereumAddress.fromHex(address), + alias, + account.accountFactoryAddress, + ), + ); + } } // delete all wallet backups @@ -276,17 +491,87 @@ class AndroidAccountsService extends AccountsServiceInterface { @override Future purgePrivateKeysAndAddToEncryptedStorage() async { - final allAccounts = await getAllAccounts(); // accounts with private keys + final accounts = await _accountsDB.accounts.all(); + + for (final account in accounts) { + final privateKey = await _credentials.read(account.id); + if (privateKey == null) { + continue; + } + + account.privateKey = EthPrivateKey.fromHex(privateKey); + } + + await _credentials.deleteCredentials(); + + for (final account in accounts) { + if (account.privateKey == null) { + continue; + } - for (final account in allAccounts) { await _credentials.write( account.id, bytesToHex(account.privateKey!.privateKey), ); + } + } + + @override + Future fixSafeAccounts() async { + try { + final allAccounts = await _accountsDB.accounts.all(); + + for (final account in allAccounts) { + final needsFixing = + _sharedPreferences.getBool('needs_fixing_${account.id}') ?? false; + if (!needsFixing) { + continue; + } + + final privateKey = await _credentials.read(account.id); + if (privateKey == null) { + continue; + } - // null private key before updating in DB - account.privateKey = null; - await _accountsDB.accounts.update(account); + account.privateKey = EthPrivateKey.fromHex(privateKey); + + final community = await AppDBService().communities.get(account.alias); + if (community == null) { + continue; + } + + final config = Config.fromJson(community.config); + + try { + await _fixSafeAccount(account, config); + + await _sharedPreferences.setBool('needs_fixing_${account.id}', false); + } catch (e, stackTrace) { + debugPrint('Stack trace: $stackTrace'); + } + } + } catch (e, stackTrace) { + debugPrint('Stack trace: $stackTrace'); + } + } + + Future migratePrivateKeysFromOldFormat() async { + final allAccounts = await _accountsDB.accounts.all(); + + for (final account in allAccounts) { + final currentPrivateKey = await _credentials.read(account.id); + if (currentPrivateKey != null) { + continue; + } + + final oldFormatKey = '${account.address.hexEip55}@${account.alias}'; + + final oldPrivateKey = await _credentials.read(oldFormatKey); + if (oldPrivateKey != null) { + await _credentials.write(account.id, oldPrivateKey); + + await _credentials.delete(oldFormatKey); + } } } } diff --git a/lib/services/accounts/native/apple.dart b/lib/services/accounts/native/apple.dart index 849f9f1f..9e5bc39c 100644 --- a/lib/services/accounts/native/apple.dart +++ b/lib/services/accounts/native/apple.dart @@ -1,16 +1,21 @@ -import 'package:citizenwallet/services/accounts/backup.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:web3dart/credentials.dart'; +import 'package:web3dart/crypto.dart'; +import 'package:web3dart/web3dart.dart'; + import 'package:citizenwallet/services/accounts/accounts.dart'; +import 'package:citizenwallet/services/accounts/backup.dart'; import 'package:citizenwallet/services/accounts/options.dart'; -import 'package:citizenwallet/services/accounts/utils.dart'; +import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/credentials/credentials.dart'; import 'package:citizenwallet/services/credentials/native/apple.dart'; +import 'package:citizenwallet/services/db/app/db.dart'; import 'package:citizenwallet/services/db/backup/accounts.dart'; import 'package:citizenwallet/services/db/backup/db.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:web3dart/credentials.dart'; -import 'package:web3dart/crypto.dart'; +import 'package:citizenwallet/services/wallet/contracts/safe_account.dart'; +import 'package:citizenwallet/services/wallet/wallet.dart'; -/// AppleAccountsService implements an AccountsServiceInterface for iOS and macOS class AppleAccountsService extends AccountsServiceInterface { static final AppleAccountsService _instance = AppleAccountsService._internal(); @@ -22,6 +27,51 @@ class AppleAccountsService extends AccountsServiceInterface { final CredentialsServiceInterface _credentials = getCredentialsService(); late AccountBackupDBService _accountsDB; + Future _fixSafeAccount(DBAccount account, Config config) async { + try { + if (account.accountFactoryAddress != + '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2') { + return; + } + + final safeAccount = SafeAccount( + config.chains.values.first.node.chainId, + config.ethClient, + account.address.hexEip55, + ); + await safeAccount.init(); + + final calldata = safeAccount.fixFallbackHandlerCallData(); + + final (hash, userop) = await prepareUserop( + config, + account.address, + account.privateKey!, + [account.address.hexEip55], + [calldata], + deploy: false, + accountFactoryAddress: '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185', + ); + + final txHash = await submitUserop(config, userop, migrationSafe: true); + + if (txHash != null) { + debugPrint('fixed cw-safe account ${account.address.hexEip55}'); + } else { + debugPrint( + 'Failed to submit for cw-safe account ${account.address.hexEip55}'); + } + } catch (e, stackTrace) { + debugPrint('Error: cw-safe account ${account.address.hexEip55}: $e'); + debugPrint('Stack trace: $stackTrace'); + + if (e.toString().contains('contract not whitelisted')) { + debugPrint( + 'Contract not whitelisted error for account ${account.address.hexEip55}'); + } + } + } + @override Future init(AccountsOptionsInterface options) async { final appleOptions = options as AppleAccountsOptions; @@ -34,7 +84,12 @@ class AppleAccountsService extends AccountsServiceInterface { _accountsDB = appleOptions.accountsDB; + await restoreAccountsFromKeychain(); + + // Run migrations on the complete dataset (existing + restored accounts) await migrate(super.version); + + await migratePrivateKeysFromOldFormat(); } @override @@ -47,172 +102,221 @@ class AppleAccountsService extends AccountsServiceInterface { } final migrations = { - 1: () async { - // coming from the old version, migrate all keys and delete the old ones - // all or nothing, first write all the new ones, then delete all the old ones - final allBackups = await getAllLegacyWalletBackups(); - - for (final backup in allBackups) { - // await setAccount(backup); - final saved = await _credentials.containsKey(backup.legacyKey2); - if (saved) { - await _credentials.delete(backup.legacyKey2); + 4: () async { + final allLegacyBackups = await getAllLegacyWalletBackups(); + + final toDelete = []; + + for (final legacyBackup in allLegacyBackups) { + final saved = await _credentials.containsKey(legacyBackup.key); + if (!saved) { + continue; } - await _credentials.write( - backup.legacyKey2, - backup.value, + // write the account data in the accounts table + final DBAccount account = DBAccount( + alias: legacyBackup.alias, + address: EthereumAddress.fromHex(legacyBackup.address), + name: legacyBackup.name, + accountFactoryAddress: '', ); - } - // delete all old keys - for (final backup in allBackups) { - // legacy delete - final saved = await _credentials.containsKey( - backup.legacyKey, - ); - if (saved) { - await _credentials.delete(backup.legacyKey); - } - } - }, - 2: () async { - final allBackups = await getAllLegacyWalletBackups(); + await _accountsDB.accounts.insert(account); - for (final backup in allBackups) { - final saved = await _credentials.containsKey(backup.key); - if (saved) { - await _credentials.delete(backup.key); - } + // write credentials into Keychain Services + final backup = BackupWallet( + address: legacyBackup.address, + alias: legacyBackup.alias, + privateKey: legacyBackup.privateKey, + ); await _credentials.write( backup.key, backup.value, ); + + toDelete.add(legacyBackup.key); } // delete all old keys - for (final backup in allBackups) { - // delete legacy keys + for (final key in toDelete) { final saved = await _credentials.containsKey( - backup.legacyKey2, + key, ); if (saved) { await _credentials.delete( - backup.legacyKey2, + key, ); } } }, - 3: () async { - final allBackups = await getAllLegacyWalletBackups(); + 5: () async { + final allAccounts = await _accountsDB.accounts.all(); - final toDelete = []; - - for (final backup in allBackups) { - bool saved = await _credentials.containsKey(backup.key); - if (!saved) { + for (final account in allAccounts) { + if (account.accountFactoryAddress.isNotEmpty && + account.accountFactoryAddress != + '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2') { continue; } - final account = await getLegacyAccountAddress(backup); - if (account == null) { + final community = await AppDBService().communities.get(account.alias); + if (community == null) { continue; } - final newBackup = LegacyBackupWallet( - address: account.hexEip55, - privateKey: backup.privateKey, - name: backup.name, - alias: backup.alias, - ); + final config = Config.fromJson(community.config); + String accountFactoryAddress = + config.community.primaryAccountFactory.address; + + switch (account.alias) { + case 'gratitude': + accountFactoryAddress = + '0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD'; + break; + case 'bread': + accountFactoryAddress = + '0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9'; + break; + case 'wallet.commonshub.brussels': + accountFactoryAddress = + '0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87'; + break; + case 'wallet.sfluv.org': + accountFactoryAddress = + '0x5e987a6c4bb4239d498E78c34e986acf29c81E8e'; + break; + default: + if (account.accountFactoryAddress == + '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2') { + accountFactoryAddress = + '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185'; + } + break; + } - await _credentials.write( - newBackup.key, - newBackup.value, - ); + final oldAccountId = account.id; + final oldPrivateKey = await _credentials.read(oldAccountId); - toDelete.add(backup.key); - } + final updatedAccount = DBAccount( + alias: account.alias, + address: account.address, + name: account.name, + username: account.username, + accountFactoryAddress: accountFactoryAddress, + profile: account.profile, + ); - // delete all old keys - for (final backup in allBackups) { - if (!toDelete.contains(backup.key)) { - continue; - } + final newAccountId = updatedAccount.id; - // delete legacy keys - final saved = await _credentials.containsKey( - backup.key, + // Check if the account actually exists in the database with the expected ID + final existingAccount = await _accountsDB.accounts.db.query( + 't_accounts', + where: 'id = ?', + whereArgs: [oldAccountId], ); - if (saved) { - await _credentials.delete( - backup.key, + if (existingAccount.isNotEmpty) { + // Delete using the expected ID + await _accountsDB.accounts.delete( + account.address, account.alias, account.accountFactoryAddress); + } else { + // Try to find by address and alias with empty accountFactoryAddress + final foundByAddress = await _accountsDB.accounts.db.query( + 't_accounts', + where: 'address = ? AND alias = ? AND accountFactoryAddress = ""', + whereArgs: [account.address.hexEip55, account.alias], ); - } - } - }, - 4: () async { - final allLegacyBackups = await getAllLegacyWalletBackups(); - final toDelete = []; + if (foundByAddress.isNotEmpty) { + final actualId = foundByAddress.first['id'] as String; - for (final legacyBackup in allLegacyBackups) { - final saved = await _credentials.containsKey(legacyBackup.key); - if (!saved) { - continue; + // Delete using the actual ID + await _accountsDB.accounts.db.delete( + 't_accounts', + where: 'id = ?', + whereArgs: [actualId], + ); + } + } + if (account.accountFactoryAddress == + '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2') { + final accountId = account.id; + await _credentials.write('needs_fixing_$accountId', 'true'); } - // write the account data in the accounts table - final DBAccount account = DBAccount( - alias: legacyBackup.alias, - address: EthereumAddress.fromHex(legacyBackup.address), - name: legacyBackup.name, - ); - - await _accountsDB.accounts.insert(account); + // Insert the new record + await _accountsDB.accounts.insert(updatedAccount); - // write credentials into Keychain Services - final backup = BackupWallet( - address: legacyBackup.address, - alias: legacyBackup.alias, - privateKey: legacyBackup.privateKey, - ); + if (oldPrivateKey != null) { + await _credentials.write(newAccountId, oldPrivateKey); + await _credentials.delete(oldAccountId); - await _credentials.write( - backup.key, - backup.value, - ); + final legacyKeychainKey = + '${account.address.hexEip55}@${account.alias}'; + if (await _credentials.containsKey(legacyKeychainKey)) { + await _credentials.delete(legacyKeychainKey); + } + } + } + }, + 6: () async { + final allAccounts = await _accountsDB.accounts.all(); + final toDelete = []; - toDelete.add(legacyBackup.key); + for (final account in allAccounts) { + if (account.accountFactoryAddress.isEmpty) { + toDelete.add(account); + } } - // delete all old keys - for (final key in toDelete) { - // delete legacy keys - final saved = await _credentials.containsKey( - key, + for (final account in toDelete) { + final existingAccount = await _accountsDB.accounts.db.query( + 't_accounts', + where: 'id = ?', + whereArgs: [account.id], ); - if (saved) { - await _credentials.delete( - key, + if (existingAccount.isNotEmpty) { + // Delete the account from database using the actual ID + await _accountsDB.accounts.db.delete( + 't_accounts', + where: 'id = ?', + whereArgs: [account.id], ); + } else { + // Try to find by address and alias instead + final foundByAddress = await _accountsDB.accounts.db.query( + 't_accounts', + where: 'address = ? AND alias = ? AND accountFactoryAddress = ""', + whereArgs: [account.address.hexEip55, account.alias], + ); + + if (foundByAddress.isNotEmpty) { + final actualId = foundByAddress.first['id'] as String; + + // Delete using the actual ID + await _accountsDB.accounts.db.delete( + 't_accounts', + where: 'id = ?', + whereArgs: [actualId], + ); + } } + + // Delete the private key from credentials + await _credentials.delete(account.id); } }, }; - // run all migrations - for (var i = oldVersion + 1; i <= version; i++) { + for (int i = oldVersion + 1; i <= version; i++) { if (migrations.containsKey(i)) { await migrations[i]!(); } } - // after success, we can update the version await _credentials.write(versionPrefix, version.toString()); } @@ -239,6 +343,145 @@ class AppleAccountsService extends AccountsServiceInterface { return accounts; } + Future restoreAccountsFromKeychain() async { + int restoredCount = 0; + try { + final allKeychainData = await _credentials.readAll(); + + if (allKeychainData.isEmpty) { + return 0; + } + + final existingAccounts = await _accountsDB.accounts.all(); + final existingAddresses = existingAccounts + .map((acc) => acc.address.hexEip55.toLowerCase()) + .toSet(); + + final existingAccountIds = existingAccounts.map((acc) => acc.id).toSet(); + + if (existingAccounts.length >= (allKeychainData.length - 1)) { + return 0; + } + + final allLegacyBackups = await getAllLegacyWalletBackups(); + + for (final legacyBackup in allLegacyBackups) { + final saved = await _credentials.containsKey(legacyBackup.key); + if (!saved) { + continue; + } + + final addressLower = legacyBackup.address.toLowerCase(); + if (existingAddresses.contains(addressLower)) { + continue; + } + + try { + final account = DBAccount( + alias: legacyBackup.alias, + address: EthereumAddress.fromHex(legacyBackup.address), + name: legacyBackup.name, + accountFactoryAddress: '', + privateKey: null, + ); + + await _accountsDB.accounts.insert(account); + + final backup = BackupWallet( + address: legacyBackup.address, + alias: legacyBackup.alias, + privateKey: legacyBackup.privateKey, + ); + + await _credentials.write(backup.key, backup.value); + restoredCount++; + } catch (e, stackTrace) { + if (kDebugMode) { + debugPrint('Error restoring legacy account: $e'); + debugPrintStack(stackTrace: stackTrace); + } + } + } + + for (final entry in allKeychainData.entries) { + try { + final key = entry.key; + + if (_isSystemKey(key) || key.startsWith(backupPrefix)) { + continue; + } + + if (!key.contains('@')) { + continue; + } + + final parts = key.split('@'); + if (parts.length != 2 && parts.length != 3) { + continue; + } + + String accountAddress; + String factoryAddress; + String alias; + + if (parts.length == 2) { + accountAddress = parts[0]; + factoryAddress = ''; + alias = parts[1]; + } else { + accountAddress = parts[0]; + factoryAddress = parts[1]; + alias = parts[2]; + } + + final potentialAccount = DBAccount( + alias: alias, + address: EthereumAddress.fromHex(accountAddress), + name: alias.toUpperCase(), + accountFactoryAddress: factoryAddress.isEmpty ? '' : factoryAddress, + ); + + if (existingAddresses.contains(accountAddress.toLowerCase()) || + existingAccountIds.contains(potentialAccount.id)) { + continue; + } + + final account = DBAccount( + alias: alias, + address: EthereumAddress.fromHex(accountAddress), + name: alias.toUpperCase(), + accountFactoryAddress: factoryAddress.isEmpty ? '' : factoryAddress, + privateKey: null, + ); + + await _accountsDB.accounts.insert(account); + + await _credentials.write(account.id, entry.value); + restoredCount++; + } catch (e, stackTrace) { + if (kDebugMode) { + debugPrint('Error processing keychain entry ${entry.key}: $e'); + debugPrintStack(stackTrace: stackTrace); + } + } + } + } catch (e, stackTrace) { + if (kDebugMode) { + debugPrint('Error restoring accounts from keychain: $e'); + debugPrintStack(stackTrace: stackTrace); + } + } + + return restoredCount; + } + + bool _isSystemKey(String key) { + return key.startsWith('credential_storage_key') || + key.startsWith('version_') || + key.startsWith(versionPrefix) || + key.length < 10; + } + // set wallet backup @override Future setAccount(DBAccount account) async { @@ -256,10 +499,12 @@ class AppleAccountsService extends AccountsServiceInterface { // get wallet backup @override - Future getAccount(String address, String alias) async { + Future getAccount(String address, String alias, + [String accountFactoryAddress = '']) async { final account = await _accountsDB.accounts.get( EthereumAddress.fromHex(address), alias, + accountFactoryAddress, ); if (account == null) { @@ -285,17 +530,25 @@ class AppleAccountsService extends AccountsServiceInterface { // delete wallet backup @override Future deleteAccount(String address, String alias) async { - await _accountsDB.accounts.delete( - EthereumAddress.fromHex(address), - alias, - ); + final accounts = await _accountsDB.accounts.allForAlias(alias); + final account = + accounts.where((acc) => acc.address.hexEip55 == address).firstOrNull; - await _credentials.delete( - getAccountID( + if (account != null) { + await _accountsDB.accounts.delete( EthereumAddress.fromHex(address), alias, - ), - ); + account.accountFactoryAddress, + ); + + await _credentials.delete( + getAccountID( + EthereumAddress.fromHex(address), + alias, + account.accountFactoryAddress, + ), + ); + } } // delete all wallet backups @@ -352,4 +605,98 @@ class AppleAccountsService extends AccountsServiceInterface { return backups; } + + Future migratePrivateKeysFromOldFormat() async { + try { + final allAccounts = await _accountsDB.accounts.all(); + + for (final account in allAccounts) { + try { + final currentPrivateKey = await _credentials.read(account.id); + if (currentPrivateKey != null) { + continue; + } + + final oldFormatKey = '${account.address.hexEip55}@${account.alias}'; + + final oldPrivateKey = await _credentials.read(oldFormatKey); + if (oldPrivateKey != null) { + await _credentials.write(account.id, oldPrivateKey); + await _credentials.delete(oldFormatKey); + } + } catch (e) { + debugPrint( + 'Error migrating private key for account ${account.id}: $e'); + } + } + } catch (e) { + debugPrint('Error during private key migration: $e'); + } + } + + Future purgePrivateKeysAndAddToEncryptedStorage() async { + final accounts = await _accountsDB.accounts.all(); + + for (final account in accounts) { + final privateKey = await _credentials.read(account.id); + if (privateKey == null) { + continue; + } + + account.privateKey = EthPrivateKey.fromHex(privateKey); + } + + await _credentials.deleteCredentials(); + + for (final account in accounts) { + if (account.privateKey == null) { + continue; + } + + await _credentials.write( + account.id, + bytesToHex(account.privateKey!.privateKey), + ); + } + } + + @override + Future fixSafeAccounts() async { + try { + final allAccounts = await _accountsDB.accounts.all(); + + for (final account in allAccounts) { + final needsFixingStr = + await _credentials.read('needs_fixing_${account.id}'); + final needsFixing = needsFixingStr == 'true'; + if (!needsFixing) { + continue; + } + + final privateKey = await _credentials.read(account.id); + if (privateKey == null) { + continue; + } + + account.privateKey = EthPrivateKey.fromHex(privateKey); + + final community = await AppDBService().communities.get(account.alias); + if (community == null) { + continue; + } + + final config = Config.fromJson(community.config); + + try { + await _fixSafeAccount(account, config); + + await _credentials.delete('needs_fixing_${account.id}'); + } catch (e, stackTrace) { + debugPrint('Stack trace: $stackTrace'); + } + } + } catch (e, stackTrace) { + debugPrint('Stack trace: $stackTrace'); + } + } } diff --git a/lib/services/accounts/web.dart b/lib/services/accounts/web.dart index 4b31f568..e11498cc 100644 --- a/lib/services/accounts/web.dart +++ b/lib/services/accounts/web.dart @@ -42,7 +42,7 @@ class WebAccountsService extends AccountsServiceInterface { // get wallet backup @override - Future getAccount(String address, String alias) async { + Future getAccount(String address, String alias, [String accountFactoryAddress = '']) async { return null; } diff --git a/lib/services/api/api.dart b/lib/services/api/api.dart index eef4a92c..f50d6bb1 100644 --- a/lib/services/api/api.dart +++ b/lib/services/api/api.dart @@ -339,3 +339,18 @@ RPCException parseRPCErrorText(String errorText) { ); } } + +extension MigrationAPI on APIService { + Future checkMigrationRequired() async { + try { + final response = await get(url: '/migration'); + + return false; + } on NotFoundException { + return true; + } catch (e) { + return false; + } + } +} + diff --git a/lib/services/config/config.dart b/lib/services/config/config.dart index a31a9ca8..93d5d77a 100644 --- a/lib/services/config/config.dart +++ b/lib/services/config/config.dart @@ -1,5 +1,21 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:citizenwallet/services/api/api.dart'; import 'package:citizenwallet/services/config/legacy.dart'; import 'package:collection/collection.dart'; +import 'package:citizenwallet/services/wallet/contracts/account_factory.dart'; +import 'package:citizenwallet/services/wallet/contracts/cards/interface.dart'; +import 'package:citizenwallet/services/wallet/contracts/cards/safe_card_manager.dart'; +import 'package:citizenwallet/services/wallet/contracts/communityModule.dart'; +import 'package:citizenwallet/services/wallet/contracts/entrypoint.dart'; +import 'package:citizenwallet/services/wallet/contracts/erc1155.dart'; +import 'package:citizenwallet/services/wallet/contracts/erc20.dart'; +import 'package:citizenwallet/services/wallet/contracts/profile.dart'; +import 'package:citizenwallet/services/wallet/contracts/safe_account.dart'; +import 'package:citizenwallet/services/wallet/contracts/simple_account.dart'; +import 'package:web3dart/crypto.dart'; +import 'package:web3dart/web3dart.dart'; const String defaultPrimary = '#A256FF'; @@ -66,6 +82,7 @@ class CommunityConfig { final ContractLocation profile; final ContractLocation primaryToken; final ContractLocation primaryAccountFactory; + final ContractLocation? primarySessionManager; final ContractLocation? primaryCardManager; CommunityConfig({ @@ -80,6 +97,7 @@ class CommunityConfig { required this.profile, required this.primaryToken, required this.primaryAccountFactory, + this.primarySessionManager, this.primaryCardManager, }); @@ -101,6 +119,9 @@ class CommunityConfig { primaryToken: ContractLocation.fromJson(json['primary_token']), primaryAccountFactory: ContractLocation.fromJson(json['primary_account_factory']), + primarySessionManager: json['primary_session_manager'] != null + ? ContractLocation.fromJson(json['primary_session_manager']) + : null, primaryCardManager: json['primary_card_manager'] != null ? ContractLocation.fromJson(json['primary_card_manager']) : null, @@ -121,6 +142,8 @@ class CommunityConfig { 'profile': profile.toJson(), 'primary_token': primaryToken.toJson(), 'primary_account_factory': primaryAccountFactory.toJson(), + if (primarySessionManager != null) + 'primary_session_manager': primarySessionManager!.toJson(), if (primaryCardManager != null) 'primary_card_manager': primaryCardManager!.toJson(), }; @@ -364,7 +387,7 @@ enum PluginLaunchMode { class PluginConfig { final String name; final String? icon; - String url; + final String url; final PluginLaunchMode launchMode; final String? action; final bool hidden; @@ -394,10 +417,6 @@ class PluginConfig { ); } - void updateUrl(String url) { - this.url = url; - } - // to json Map toJson() { return { @@ -414,7 +433,7 @@ class PluginConfig { // to string @override String toString() { - return 'PluginConfig{name: $name, icon: $icon, url: $url, launchMode: $launchMode, action: $action, hidden: $hidden, signature: $signature}'; + return 'PluginConfig{name: $name, icon: $icon, url: $url}'; } } @@ -489,15 +508,54 @@ class CardsConfig extends ContractLocation { return { 'chain_id': chainId, 'address': address, + 'instance_id': instanceId, 'type': type.name, - if (instanceId != null) 'instance_id': instanceId, }; } // to string @override String toString() { - return 'CardsConfig{chainId: $chainId, address: $address, type: $type}'; + return 'CardsConfig{chainId: $chainId, address: $address, instanceId: $instanceId, type: $type}'; + } +} + +class SessionsConfig { + final int chainId; + final String moduleAddress; + final String factoryAddress; + final String providerAddress; + + SessionsConfig({ + required this.chainId, + required this.moduleAddress, + required this.factoryAddress, + required this.providerAddress, + }); + + factory SessionsConfig.fromJson(Map json) { + return SessionsConfig( + chainId: json['chain_id'], + moduleAddress: json['module_address'], + factoryAddress: json['factory_address'], + providerAddress: json['provider_address'], + ); + } + + // to json + Map toJson() { + return { + 'chain_id': chainId, + 'module_address': moduleAddress, + 'factory_address': factoryAddress, + 'provider_address': providerAddress, + }; + } + + // to string + @override + String toString() { + return 'SessionsConfig{chainId: $chainId, moduleAddress: $moduleAddress, factoryAddress: $factoryAddress, providerAddress: $providerAddress}'; } } @@ -530,6 +588,7 @@ class Config { final Map tokens; final ScanConfig scan; final Map accounts; + final Map? sessions; final Map? cards; final Map chains; final IPFSConfig ipfs; @@ -538,11 +597,33 @@ class Config { final int version; bool online; + Web3Client? _ethClient; + + Web3Client get ethClient { + _initializeServices(); + return _ethClient!; + } + + late APIService ipfsService; + late APIService engine; + late APIService engineRPC; + late APIService engineIPFSService; + + late StackupEntryPoint entryPointContract; + late CommunityModule communityModuleContract; + late AccountFactoryService accountFactoryContract; + late ProfileContract profileContract; + AbstractCardManagerContract? cardManagerContract; + + late ERC20Contract token20Contract; + late ERC1155Contract token1155Contract; + Config({ required this.community, required this.tokens, required this.scan, required this.accounts, + required this.sessions, required this.cards, required this.chains, required this.ipfs, @@ -550,7 +631,116 @@ class Config { required this.configLocation, this.version = 0, this.online = true, - }); + }) { + // Defer initialization to avoid errors during config loading + } + + void _initializeServices() { + if (_ethClient != null) return; + + final chain = chains.values.first; + final rpcUrl = getRpcUrl(chain.id.toString()); + final nodeUrl = getNodeUrl(chain.id.toString()); + + _ethClient = Web3Client(rpcUrl, Client()); + ipfsService = APIService(baseURL: ipfs.url); + engine = APIService(baseURL: nodeUrl); + engineRPC = APIService(baseURL: rpcUrl); + engineIPFSService = APIService(baseURL: nodeUrl); + } + + Future initContracts([String accountFactoryAddress = '']) async { + final chain = chains.values.first; + + final erc4337Config = accountFactoryAddress.isNotEmpty + ? getAccountAbstractionConfig(accountFactoryAddress, chain.id) + : getPrimaryAccountAbstractionConfig(); + + entryPointContract = StackupEntryPoint( + chain.id, + ethClient, + erc4337Config.entrypointAddress, + ); + await entryPointContract.init(); + + communityModuleContract = CommunityModule( + chain.id, + ethClient, + erc4337Config.entrypointAddress, + ); + await communityModuleContract.init(); + + accountFactoryContract = AccountFactoryService( + chain.id, + ethClient, + erc4337Config.accountFactoryAddress, + ); + await accountFactoryContract.init(); + + token20Contract = ERC20Contract( + chain.id, + ethClient, + getPrimaryToken().address, + ); + await token20Contract.init(); + + token1155Contract = ERC1155Contract( + chain.id, + ethClient, + getPrimaryToken().address, + ); + await token1155Contract.init(); + + profileContract = ProfileContract( + chain.id, + ethClient, + community.profile.address, + ); + await profileContract.init(); + + final primaryCardManager = getPrimaryCardManager(); + + if (primaryCardManager != null && + primaryCardManager.type == CardManagerType.safe) { + cardManagerContract = SafeCardManagerContract( + keccak256(utf8.encode(primaryCardManager.instanceId!)), + chain.id, + ethClient, + primaryCardManager.address, + ); + await cardManagerContract!.init(); + } + } + + Future getSimpleAccount(String address) async { + final chain = chains.values.first; + + final account = SimpleAccount( + chain.id, + ethClient, + address, + ); + await account.init(); + + return account; + } + + Future getSafeAccount(String address) async { + final chain = chains.values.first; + + final account = SafeAccount( + chain.id, + ethClient, + address, + ); + await account.init(); + + return account; + } + + Future getNonce(String address) async { + return await entryPointContract.getNonce(address); + } factory Config.fromLegacy(LegacyConfig legacy) { final community = CommunityConfig( @@ -636,6 +826,7 @@ class Config { tokens: tokens, scan: ScanConfig(name: legacy.scan.name, url: legacy.scan.url), accounts: accounts, + sessions: null, cards: cards, chains: chains, ipfs: IPFSConfig(url: legacy.ipfs.url), @@ -658,6 +849,8 @@ class Config { scan: ScanConfig.fromJson(json['scan']), accounts: (json['accounts'] as Map) .map((key, value) => MapEntry(key, ERC4337Config.fromJson(value))), + sessions: (json['sessions'] as Map?) + ?.map((key, value) => MapEntry(key, SessionsConfig.fromJson(value))), cards: (json['cards'] as Map?) ?.map((key, value) => MapEntry(key, CardsConfig.fromJson(value))), chains: (json['chains'] as Map) @@ -678,6 +871,9 @@ class Config { 'tokens': tokens.map((key, value) => MapEntry(key, value.toJson())), 'scan': scan.toJson(), 'accounts': accounts.map((key, value) => MapEntry(key, value.toJson())), + if (sessions != null) + 'sessions': + sessions!.map((key, value) => MapEntry(key, value.toJson())), if (cards != null) 'cards': cards!.map((key, value) => MapEntry(key, value.toJson())), 'chains': chains.map((key, value) => MapEntry(key, value.toJson())), @@ -722,6 +918,41 @@ class Config { return primaryAccountAbstraction; } + SessionsConfig getPrimarySessionManager() { + if (sessions == null) { + throw Exception('Sessions not found'); + } + + final primarySessionManager = + sessions![community.primarySessionManager!.fullAddress]; + + if (primarySessionManager == null) { + throw Exception('Primary Session Manager Config not found'); + } + + return primarySessionManager; + } + + String getPaymasterType() { + final erc4337Config = getPrimaryAccountAbstractionConfig(); + + return erc4337Config.paymasterType; + } + + ERC4337Config getAccountAbstractionConfig(String accountFactoryAddress, + [int? chainId]) { + final targetChainId = chainId ?? community.primaryAccountFactory.chainId; + final accountAbstraction = + accounts['$targetChainId:$accountFactoryAddress']; + + if (accountAbstraction == null) { + throw Exception( + 'Account Abstraction Config not found for factory: $accountFactoryAddress on chain: $targetChainId'); + } + + return accountAbstraction; + } + CardsConfig? getPrimaryCardManager() { return cards?[community.primaryCardManager?.fullAddress]; } @@ -736,13 +967,17 @@ class Config { return chain.node.url; } - String getRpcUrl(String chainId) { + String getRpcUrl(String chainId, [String? accountFactory]) { final chain = chains[chainId]; if (chain == null) { throw Exception('Chain not found'); } - return '${chain.node.url}/v1/rpc/${getPrimaryAccountAbstractionConfig().paymasterAddress}'; + final accountAbstractionConfig = accountFactory != null + ? getAccountAbstractionConfig(accountFactory, int.parse(chainId)) + : getPrimaryAccountAbstractionConfig(); + + return '${chain.node.url}/v1/rpc/${accountAbstractionConfig.paymasterAddress}'; } } diff --git a/lib/services/config/service.dart b/lib/services/config/service.dart index 89b998ec..3875d1be 100644 --- a/lib/services/config/service.dart +++ b/lib/services/config/service.dart @@ -1,4 +1,6 @@ +import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:citizenwallet/services/api/api.dart'; import 'package:citizenwallet/services/config/config.dart'; @@ -30,12 +32,13 @@ class ConfigService { static const String communityConfigListS3FileName = 'communities'; static const String communityDebugFileName = 'debug'; - static const int version = 4; + static const int version = 5; final PreferencesService _pref = PreferencesService(); late APIService _api; late APIService _communityServer; bool singleCommunityMode = false; + bool _isWebInitialized = false; List _configs = []; @@ -54,14 +57,22 @@ class ConfigService { return _configs.first; } + if (!kIsWeb) { + throw Exception('getWebConfig should only be called on web platform'); + } + + if (!_isWebInitialized) { + initWeb(); + } + if (_configs.isNotEmpty && _configs.length == 1) { _communityServer.get(url: '/config/community.json').then((response) { final config = Config.fromJson(response); _configs = [config]; }).catchError((e, s) { - print('Error fetching config: $e'); - print('Stacktrace: $s'); + debugPrint('Error fetching config: $e'); + debugPrint('Stacktrace: $s'); }); return _configs.first; @@ -76,8 +87,8 @@ class ConfigService { return config; } catch (e, s) { - print('Error fetching config: $e'); - print('Stacktrace: $s'); + debugPrint('Error fetching config: $e'); + debugPrint('Stacktrace: $s'); } String alias = Uri.base.host.endsWith(appLinkSuffix) @@ -116,11 +127,12 @@ class ConfigService { void initWeb() { final scheme = Uri.base.scheme.isNotEmpty ? Uri.base.scheme : 'http'; final url = kDebugMode || Uri.base.host.contains('localhost') - ? 'https://config.internal.citizenwallet.xyz' + ? 'https://dashboard-orpin-xi.vercel.app' : '$scheme://${Uri.base.host}:${Uri.base.port}/wallet-config'; _api = APIService(baseURL: url); _communityServer = APIService(baseURL: '$scheme://${Uri.base.host}'); + _isWebInitialized = true; } void init(String endpoint) { @@ -128,32 +140,67 @@ class ConfigService { } Future> getConfigs({String? location}) async { - if (kDebugMode) { - final localConfigs = jsonDecode(await rootBundle.loadString( - 'assets/config/v$version/$communityConfigListFileName.json')); + try { + if (location != null) { + final response = await _api.get(url: location); + return [Config.fromJson(response)]; + } - final configs = - (localConfigs as List).map((e) => Config.fromJson(e)).toList(); + final response = await _api.get(url: '/api/communities'); - return configs; - } + try { + _pref.setConfigs(response); + } catch (e) { + debugPrint('Error saving configs to preferences: $e'); + } - if (location != null) { - // we only need a single file for the web - final response = await _api.get(url: location); + final configs = (response as List) + .map((e) { + try { + final configData = e['json']; + return configData != null ? Config.fromJson(configData) : null; + } catch (e) { + debugPrint('Error parsing config item: $e'); + return null; + } + }) + .where((config) => config != null) + .cast() + .toList(); - return [Config.fromJson(response)]; + return configs; + } on TimeoutException catch (e) { + debugPrint('Timeout fetching configs from API: $e'); + return _handleConfigAPIFailure(); + } on SocketException catch (e) { + debugPrint('Network error fetching configs from API: $e'); + return _handleConfigAPIFailure(); + } on FormatException catch (e) { + debugPrint('Invalid JSON response from configs API: $e'); + return _handleConfigAPIFailure(); + } catch (e, s) { + debugPrint('Error fetching configs from API: $e'); + debugPrintStack(stackTrace: s); + return _handleConfigAPIFailure(); } + } - final response = await _api.get( - url: - '/v$version/$communityConfigListFileName.json?cachebuster=${generateCacheBusterValue()}'); - - _pref.setConfigs(response); - - final configs = (response as List).map((e) => Config.fromJson(e)).toList(); + Future> _handleConfigAPIFailure() async { + if (kDebugMode) { + debugPrint('Falling back to local configs in debug mode'); + try { + final localConfigs = jsonDecode(await rootBundle.loadString( + 'assets/config/v$version/$communityConfigListFileName.json')); + + return (localConfigs as List).map((e) => Config.fromJson(e)).toList(); + } catch (e) { + debugPrint('Error loading local configs: $e'); + return []; + } + } - return configs; + debugPrint('Config API failed in production mode, returning empty list'); + return []; } Future> getLocalConfigs() async { @@ -189,44 +236,114 @@ class ConfigService { return config; } catch (e, s) { debugPrint('Error fetching remote config: $e'); - debugPrint('Stacktrace: $s'); + debugPrintStack(stackTrace: s); return null; } } - Future> getCommunitiesFromRemote() async { - if (kDebugMode) { - final localConfigs = jsonDecode(await rootBundle.loadString( - 'assets/config/v$version/$communityConfigListFileName.json')); + Future getSingleCommunityConfig(String configLocation) async { + try { + String alias = configLocation; + if (configLocation.contains('/')) { + alias = configLocation.split('/').last; + } - final configs = - (localConfigs as List).map((e) => Config.fromJson(e)).toList(); + final response = await _api.get(url: '/api/communities/$alias'); - return configs; + if (response == null) { + debugPrint('No response received for community alias: $alias'); + return null; + } + + final configData = response['json']; + if (configData == null) { + debugPrint('No config data found in response for alias: $alias'); + return null; + } + + final config = Config.fromJson(configData); + return config; + } catch (e, s) { + debugPrint('Error fetching single community config: $e'); + debugPrintStack(stackTrace: s); + return null; } + } + + Future> getCommunitiesFromRemote() async { + try { + final List response = await _api.get(url: '/api/communities'); - final List response = await _api.get( - url: - '/v$version/$communityConfigListS3FileName.json?cachebuster=${generateCacheBusterValue()}'); + if (response.isEmpty) { + debugPrint('Empty response from communities API'); + throw Exception('Empty response from communities API'); + } + + final List communities = response + .map((item) { + try { + final configData = item['json']; + return configData != null ? Config.fromJson(configData) : null; + } catch (e) { + debugPrint('Error parsing community config: $e'); + return null; + } + }) + .where((config) => config != null) + .cast() + .toList(); + + return communities; + } on TimeoutException catch (e) { + debugPrint('Timeout fetching communities from API: $e'); + return _handleCommunityAPIFailure(); + } on SocketException catch (e) { + debugPrint('Network error fetching communities from API: $e'); + return _handleCommunityAPIFailure(); + } on FormatException catch (e) { + debugPrint('Invalid JSON response from communities API: $e'); + return _handleCommunityAPIFailure(); + } catch (e, s) { + debugPrint('Error fetching communities from API: $e'); + debugPrintStack(stackTrace: s); + return _handleCommunityAPIFailure(); + } + } - final List communities = - response.map((item) => Config.fromJson(item)).toList(); + Future> _handleCommunityAPIFailure() async { + if (kDebugMode) { + debugPrint('Falling back to local configs in debug mode'); + try { + final localConfigs = jsonDecode(await rootBundle.loadString( + 'assets/config/v$version/$communityConfigListFileName.json')); + + return (localConfigs as List).map((e) => Config.fromJson(e)).toList(); + } catch (e) { + debugPrint('Error loading local configs: $e'); + return []; + } + } - return communities; + debugPrint('API failed in production mode, returning empty community list'); + return []; } Future isCommunityOnline(String indexerUrl) async { - final indexer = APIService(baseURL: indexerUrl, netTimeoutSeconds: 20); + final indexer = APIService(baseURL: indexerUrl, netTimeoutSeconds: 10); try { await indexer.get(url: '/health'); return true; - } catch (e, s) { - debugPrint('indexerUrl: $indexerUrl'); - debugPrint('Error checking if community is online: $e, $indexerUrl'); - debugPrint('Stacktrace: $s, $indexerUrl'); - + } on TimeoutException catch (e) { + debugPrint('Timeout checking if community is online: $indexerUrl - $e'); + return false; + } on SocketException catch (e) { + debugPrint( + 'Network error checking if community is online: $indexerUrl - $e'); + return false; + } catch (e) { + debugPrint('Error checking if community is online: $indexerUrl - $e'); return false; } } diff --git a/lib/services/credentials/native/android.dart b/lib/services/credentials/native/android.dart index 6bd11386..86dbcb89 100644 --- a/lib/services/credentials/native/android.dart +++ b/lib/services/credentials/native/android.dart @@ -2,7 +2,6 @@ import 'dart:typed_data'; import 'package:citizenwallet/services/credentials/credentials.dart'; import 'package:citizenwallet/utils/encrypt.dart'; -import 'package:convert/convert.dart'; import 'package:credential_manager/credential_manager.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:web3dart/crypto.dart'; diff --git a/lib/services/db/app/communities.dart b/lib/services/db/app/communities.dart index 46eefac1..1d02fa3e 100644 --- a/lib/services/db/app/communities.dart +++ b/lib/services/db/app/communities.dart @@ -5,6 +5,7 @@ import 'package:citizenwallet/services/config/service.dart'; import 'package:citizenwallet/services/db/db.dart'; import 'package:flutter/foundation.dart'; import 'package:sqflite/sqflite.dart'; +import 'package:collection/collection.dart'; Future> legacyToV4(Database db, String name) async { final List> maps = await db.query(name); @@ -28,6 +29,40 @@ Future> legacyToV4(Database db, String name) async { return v4Configs; } +Future> V5Migration(Database db, String name) async { + final ConfigService config = ConfigService(); + + final localConfigs = await config.getLocalConfigs(); + + final List> maps = await db.query(name); + final existingCommunities = List.generate(maps.length, (i) { + return DBCommunity.fromMap(maps[i]); + }); + + final List updatedConfigs = []; + + for (final localConfig in localConfigs) { + final existingCommunity = existingCommunities.firstWhereOrNull( + (c) => c.alias == localConfig.community.alias, + ); + + if (existingCommunity != null) { + final updatedCommunity = DBCommunity( + alias: localConfig.community.alias, + config: localConfig.toJson(), + hidden: localConfig.community.hidden, + version: localConfig.version, + online: existingCommunity.online, + ); + updatedConfigs.add(updatedCommunity); + } else { + updatedConfigs.add(DBCommunity.fromConfig(localConfig)); + } + } + + return updatedConfigs; +} + class DBCommunity { final String alias; // index final bool hidden; @@ -118,6 +153,9 @@ class CommunityTable extends DBTable { 2: [ 'V4Migration', ], + 3: [ + 'V5Migration', + ], }; for (var i = oldVersion + 1; i <= newVersion; i++) { @@ -131,6 +169,10 @@ class CommunityTable extends DBTable { final updatedConfigs = await legacyToV4(db, name); await upsert(updatedConfigs); continue; + case 'V5Migration': + final updatedConfigs = await V5Migration(db, name); + await upsert(updatedConfigs); + continue; } await db.execute(query); diff --git a/lib/services/db/app/db.dart b/lib/services/db/app/db.dart index f73c927d..65e25b60 100644 --- a/lib/services/db/app/db.dart +++ b/lib/services/db/app/db.dart @@ -27,7 +27,7 @@ class AppDBService extends DBService { await communities.migrate(db, oldVersion, newVersion); return; }, - version: 2, + version: 3, ); final db = await databaseFactory.openDatabase( diff --git a/lib/services/db/backup/accounts.dart b/lib/services/db/backup/accounts.dart index 13404623..2b8dcc8b 100644 --- a/lib/services/db/backup/accounts.dart +++ b/lib/services/db/backup/accounts.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:citizenwallet/services/db/db.dart'; +import 'package:citizenwallet/services/db/backup/legacy.dart'; import 'package:citizenwallet/services/wallet/contracts/profile.dart'; -import 'package:citizenwallet/services/wallet/wallet.dart'; import 'package:flutter/foundation.dart'; import 'package:sqflite/sqlite_api.dart'; import 'package:web3dart/crypto.dart'; @@ -15,6 +15,7 @@ class DBAccount { final String name; final UserHandle? userHandle; final String? username; + final String accountFactoryAddress; EthPrivateKey? privateKey; final ProfileV1? profile; @@ -22,10 +23,11 @@ class DBAccount { required this.alias, required this.address, required this.name, + required this.accountFactoryAddress, this.username, this.privateKey, this.profile, - }) : id = getAccountID(address, alias), + }) : id = getAccountID(address, alias, accountFactoryAddress), userHandle = username != null ? UserHandle(username, alias) : null; // toMap @@ -36,6 +38,7 @@ class DBAccount { 'address': address.hexEip55, if (name.isNotEmpty) 'name': name, 'username': username, + 'accountFactoryAddress': accountFactoryAddress, 'privateKey': privateKey != null ? bytesToHex(privateKey!.privateKey) : null, if (profile != null) 'profile': jsonEncode(profile!.toJson()), @@ -49,6 +52,7 @@ class DBAccount { address: EthereumAddress.fromHex(map['address']), name: map['name'], username: map['username'], + accountFactoryAddress: map['accountFactoryAddress'] ?? '', privateKey: map['privateKey'] != null ? EthPrivateKey.fromHex(map['privateKey']) : null, @@ -59,8 +63,9 @@ class DBAccount { } } -String getAccountID(EthereumAddress address, String alias) { - return '${address.hexEip55}@$alias'; +String getAccountID( + EthereumAddress address, String alias, String accountFactoryAddress) { + return '${address.hexEip55}@$accountFactoryAddress@$alias'; } class UserHandle { @@ -95,6 +100,7 @@ class AccountsTable extends DBTable { address TEXT NOT NULL, name TEXT NOT NULL, username TEXT, + accountFactoryAddress TEXT NOT NULL, privateKey TEXT, profile TEXT ) @@ -113,6 +119,10 @@ class AccountsTable extends DBTable { ], 3: [ 'ALTER TABLE $name ADD COLUMN username TEXT DEFAULT NULL', + ], + 4: [ + 'ALTER TABLE $name ADD COLUMN accountFactoryAddress TEXT', + 'UPDATE $name SET accountFactoryAddress = "" WHERE accountFactoryAddress IS NULL', ] }; @@ -132,19 +142,86 @@ class AccountsTable extends DBTable { } } + Future> getAllLegacyDBAccounts() async { + final List> maps = await db.query( + name, + where: 'accountFactoryAddress IS NULL OR accountFactoryAddress = ""', + ); + + return List.generate(maps.length, (i) { + return LegacyDBAccount.fromMap(maps[i]); + }); + } + + /// Converts a LegacyDBAccount to a DBAccount + /// For legacy accounts, we use a default account factory address + DBAccount convertLegacyToDBAccount(LegacyDBAccount legacyAccount) { + return DBAccount( + alias: legacyAccount.alias, + address: legacyAccount.address, + name: legacyAccount.name, + username: legacyAccount.username, + accountFactoryAddress: '', + privateKey: legacyAccount.privateKey, + profile: legacyAccount.profile, + ); + } + // get account by id - Future get(EthereumAddress address, String alias) async { + Future get(EthereumAddress address, String alias, + String accountFactoryAddress) async { + final accountId = getAccountID(address, alias, accountFactoryAddress); + + if (accountFactoryAddress.isEmpty) { + var maps = await db.query( + name, + where: 'id = ?', + whereArgs: [accountId], + ); + + if (maps.isNotEmpty) { + final account = DBAccount.fromMap(maps.first); + return account; + } + + final oldFormatId = '${address.hexEip55}@$alias'; + maps = await db.query( + name, + where: 'id = ?', + whereArgs: [oldFormatId], + ); + + if (maps.isNotEmpty) { + final account = DBAccount.fromMap(maps.first); + return account; + } + + maps = await db.query( + name, + where: 'address = ? AND alias = ?', + whereArgs: [address.hexEip55, alias], + ); + + if (maps.isNotEmpty) { + final account = DBAccount.fromMap(maps.first); + return account; + } + + return null; + } + final List> maps = await db.query( name, where: 'id = ?', - whereArgs: [getAccountID(address, alias)], + whereArgs: [accountId], ); if (maps.isEmpty) { return null; } - return DBAccount.fromMap(maps.first); + final account = DBAccount.fromMap(maps.first); + return account; } Future insert(DBAccount account) async { @@ -164,11 +241,12 @@ class AccountsTable extends DBTable { ); } - Future delete(EthereumAddress address, String alias) async { + Future delete(EthereumAddress address, String alias, + String accountFactoryAddress) async { await db.delete( name, where: 'id = ?', - whereArgs: [getAccountID(address, alias)], + whereArgs: [getAccountID(address, alias, accountFactoryAddress)], ); } diff --git a/lib/services/db/backup/db.dart b/lib/services/db/backup/db.dart index 71ae81af..b2e2d2a4 100644 --- a/lib/services/db/backup/db.dart +++ b/lib/services/db/backup/db.dart @@ -1,6 +1,5 @@ import 'package:citizenwallet/services/db/backup/accounts.dart'; import 'package:citizenwallet/services/db/db.dart'; -import 'package:sqflite/sqlite_api.dart'; import 'package:sqflite_common/sqflite.dart'; class AccountBackupDBService extends DBService { @@ -40,7 +39,7 @@ class AccountBackupDBService extends DBService { return; }, - version: 3, + version: 4, ); final db = await databaseFactory.openDatabase( diff --git a/lib/services/db/backup/legacy.dart b/lib/services/db/backup/legacy.dart new file mode 100644 index 00000000..ee18630c --- /dev/null +++ b/lib/services/db/backup/legacy.dart @@ -0,0 +1,199 @@ +import 'dart:convert'; + +import 'package:citizenwallet/services/db/db.dart'; +import 'package:citizenwallet/services/wallet/contracts/profile.dart'; +import 'package:flutter/foundation.dart'; +import 'package:sqflite/sqlite_api.dart'; +import 'package:web3dart/crypto.dart'; +import 'package:web3dart/web3dart.dart'; + +class LegacyDBAccount { + final String id; + final String alias; + final EthereumAddress address; + final String name; + final UserHandle? userHandle; + final String? username; + EthPrivateKey? privateKey; + final ProfileV1? profile; + + LegacyDBAccount({ + required this.alias, + required this.address, + required this.name, + this.username, + this.privateKey, + this.profile, + }) : id = getAccountID(address, alias), + userHandle = username != null ? UserHandle(username, alias) : null; + + // toMap + Map toMap() { + return { + 'id': id, + 'alias': alias, + 'address': address.hexEip55, + if (name.isNotEmpty) 'name': name, + 'username': username, + 'privateKey': + privateKey != null ? bytesToHex(privateKey!.privateKey) : null, + if (profile != null) 'profile': jsonEncode(profile!.toJson()), + }; + } + + // fromMap + factory LegacyDBAccount.fromMap(Map map) { + return LegacyDBAccount( + alias: map['alias'], + address: EthereumAddress.fromHex(map['address']), + name: map['name'], + username: map['username'], + privateKey: map['privateKey'] != null + ? EthPrivateKey.fromHex(map['privateKey']) + : null, + profile: map['profile'] != null + ? ProfileV1.fromJson(jsonDecode(map['profile'])) + : null, + ); + } +} + +String getAccountID(EthereumAddress address, String alias) { + return '${address.hexEip55}@$alias'; +} + +class UserHandle { + final String username; + final String communityAlias; + + const UserHandle(this.username, this.communityAlias); + + factory UserHandle.fromUserHandle(String userHandle) { + final parts = userHandle.split('@'); + if (parts.length != 2) { + throw FormatException('Invalid user handle format: $userHandle'); + } + return UserHandle(parts[0], parts[1]); + } + + @override + String toString() => '$username@$communityAlias'; +} + +class AccountsTable extends DBTable { + AccountsTable(super.db); + + @override + String get name => 't_accounts'; + + @override + String get createQuery => ''' + CREATE TABLE $name ( + id TEXT PRIMARY KEY, + alias TEXT NOT NULL, + address TEXT NOT NULL, + name TEXT NOT NULL, + username TEXT, + privateKey TEXT, + profile TEXT + ) + '''; + + @override + Future create(Database db) async { + await db.execute(createQuery); + } + + @override + Future migrate(Database db, int oldVersion, int newVersion) async { + final migrations = { + 2: [ + 'UPDATE $name SET privateKey = NULL', + ], + 3: [ + 'ALTER TABLE $name ADD COLUMN username TEXT DEFAULT NULL', + ] + }; + + for (var i = oldVersion + 1; i <= newVersion; i++) { + final queries = migrations[i]; + + if (queries != null) { + for (final query in queries) { + try { + await db.execute(query); + } catch (e, s) { + debugPrint('Migration error: $e'); + debugPrintStack(stackTrace: s); + } + } + } + } + } + + // get account by id + Future get(EthereumAddress address, String alias) async { + final List> maps = await db.query( + name, + where: 'id = ?', + whereArgs: [getAccountID(address, alias)], + ); + + if (maps.isEmpty) { + return null; + } + + return LegacyDBAccount.fromMap(maps.first); + } + + Future insert(LegacyDBAccount account) async { + await db.insert( + name, + account.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + Future update(LegacyDBAccount account) async { + await db.update( + name, + account.toMap(), + where: 'id = ?', + whereArgs: [account.id], + ); + } + + Future delete(EthereumAddress address, String alias) async { + await db.delete( + name, + where: 'id = ?', + whereArgs: [getAccountID(address, alias)], + ); + } + + // delete all + Future deleteAll() async { + await db.delete(name); + } + + Future> all() async { + final List> maps = await db.query(name); + + return List.generate(maps.length, (i) { + return LegacyDBAccount.fromMap(maps[i]); + }); + } + + // get all accounts for alias + Future> allForAlias(String alias) async { + final List> maps = await db.query( + name, + where: 'alias = ?', + whereArgs: [alias], + ); + + return List.generate(maps.length, (i) { + return LegacyDBAccount.fromMap(maps[i]); + }); + } +} \ No newline at end of file diff --git a/lib/services/db/db.dart b/lib/services/db/db.dart index dc134075..8ded1a75 100644 --- a/lib/services/db/db.dart +++ b/lib/services/db/db.dart @@ -56,7 +56,6 @@ abstract class DBService { final dbPath = kIsWeb ? this.name : join(await getDatabasesPath(), this.name); - print('dbPath: $dbPath'); _db = await openDB(dbPath); } diff --git a/lib/services/engine/events.dart b/lib/services/engine/events.dart index 076faf1e..6eed5d1c 100644 --- a/lib/services/engine/events.dart +++ b/lib/services/engine/events.dart @@ -41,7 +41,6 @@ class WebSocketEvent { final jsonData = json.decode(message); return WebSocketEvent.fromJson(jsonData); } catch (e) { - print('Error parsing WebSocket message: $e'); return null; } } @@ -66,7 +65,6 @@ class EventService { bool get isOffline => _isConnected == false; Future connect({Duration? reconnectDelay}) async { - print('Connecting to $_url/v1/events/$_contractAddress/$_topic'); if (_isConnected) return; @@ -89,7 +87,6 @@ class EventService { onDone: _onDone, ); } catch (e) { - print('Connection error: $e'); _isConnected = false; _onStateChange(EventServiceState.error); Duration delay = Duration(seconds: _reconnectDelay.inSeconds); @@ -118,36 +115,26 @@ class EventService { } void _onMessage(dynamic message) { - print('Received message: $message'); if (message is String) { final event = WebSocketEvent.tryParse(message); if (event != null) { // Handle the parsed event - print('Parsed WebSocketEvent: ${event.type} - ${event.id}'); _messageHandler?.call(event); - } else { - print('Failed to parse WebSocket message'); } - } else { - print('Received non-string message'); } } void _onError(error) { - print('WebSocket error: $error'); _isConnected = false; _onStateChange(EventServiceState.error); if (!_intentionalDisconnect) { _scheduleReconnect(); - } else { - print('Skipping reconnect due to intentional disconnect'); } } void _onDone() { - print('WebSocket connection closed'); _isConnected = false; if (!_intentionalDisconnect) { _scheduleReconnect(); @@ -164,7 +151,6 @@ class EventService { _reconnectTimer?.cancel(); _reconnectTimer = Timer(reconnectDelay ?? _reconnectDelay, () async { - print('Attempting to reconnect...'); _onStateChange(EventServiceState.connecting); @@ -175,7 +161,6 @@ class EventService { } Future disconnect() async { - print('Disconnecting from $_url/v1/events/$_contractAddress/$_topic'); _reconnectTimer?.cancel(); _isConnected = false; _intentionalDisconnect = true; diff --git a/lib/services/migration/service.dart b/lib/services/migration/service.dart new file mode 100644 index 00000000..cf3f194f --- /dev/null +++ b/lib/services/migration/service.dart @@ -0,0 +1,115 @@ +import 'dart:io'; +import 'dart:math'; +import 'package:citizenwallet/services/backup/backup.dart'; +import 'package:citizenwallet/services/db/backup/db.dart'; +import 'package:citizenwallet/utils/encrypt.dart'; +import 'package:flutter/foundation.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:web3dart/crypto.dart'; +import 'package:web3dart/web3dart.dart'; + +class MigrationService { + static final MigrationService _instance = MigrationService._internal(); + factory MigrationService() => _instance; + MigrationService._internal(); + + EthPrivateKey generateMigrationKey() { + final random = Random.secure(); + final privateKey = EthPrivateKey.createRandom(random); + return privateKey; + } + + // Encrypts the database file using the provided private key + Future encryptDatabase(String dbPath, EthPrivateKey privateKey) async { + try { + final dbFile = File(dbPath); + if (!dbFile.existsSync()) { + throw Exception('Database file not found: $dbPath'); + } + + final dbBytes = await dbFile.readAsBytes(); + + final keyBytes = privateKey.privateKey; + final encryptKey = keyBytes.length == 33 ? keyBytes.sublist(1) : keyBytes; + + final encrypt = Encrypt(encryptKey); + + final encryptedBytes = await encrypt.encrypt(dbBytes); + + final encryptedFile = File('${dbPath}.encrypted'); + await encryptedFile.writeAsBytes(encryptedBytes); + + return encryptedFile; + } catch (e) { + rethrow; + } + } + + //Uploads the encrypted database using the existing backup service + Future uploadEncryptedDatabase( + File encryptedFile, + String fileName, + ) async { + try { + final backupService = getBackupService(); + + final username = await backupService.init(); + + await backupService.upload(encryptedFile.path, fileName); + + await encryptedFile.delete(); + } catch (e) { + debugPrint('Error uploading database: $e'); + rethrow; + } + } + + // Launches the new app with the migration deep link + Future launchNewApp(String privateKeyHex) async { + try { + final deepLink = 'citizenwallet2://migrate?key=$privateKeyHex'; + final uri = Uri.parse(deepLink); + final canLaunch = await canLaunchUrl(uri); + if (canLaunch) { + // Use url_launcher to open the external app + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + throw Exception('Cannot launch new app: $deepLink'); + } + } catch (e) { + debugPrint('Error launching new app: $e'); + rethrow; + } + } + + /// Performs the complete migration process + Future performMigration() async { + try { + final privateKey = generateMigrationKey(); + final privateKeyHex = bytesToHex(privateKey.privateKey); + final address = privateKey.address.hexEip55; + + final accountsDB = AccountBackupDBService(); + await accountsDB.init('accounts'); + final dbPath = accountsDB.path; + + final dbFile = File(dbPath); + if (dbFile.existsSync()) { + final dbSize = await dbFile.length(); + } else {} + + final encryptedFile = await encryptDatabase(dbPath, privateKey); + final encryptedSize = await encryptedFile.length(); + + final fileName = '${address}.db'; + + await uploadEncryptedDatabase(encryptedFile, fileName); + + await launchNewApp(privateKeyHex); + } catch (e, stackTrace) { + debugPrint('Error: $e'); + debugPrint('Stack Trace: $stackTrace'); + rethrow; + } + } +} diff --git a/lib/services/preferences/preferences.dart b/lib/services/preferences/preferences.dart index 14d8a7d6..3c4b3ece 100644 --- a/lib/services/preferences/preferences.dart +++ b/lib/services/preferences/preferences.dart @@ -1,7 +1,5 @@ import 'dart:convert'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/scheduler.dart'; import 'package:shared_preferences/shared_preferences.dart'; class PreferencesService { @@ -81,6 +79,13 @@ class PreferencesService { String? get lastAlias => _preferences.getString('lastAlias'); + // save the last account factory address that was opened + Future setLastAccountFactoryAddress(String accountFactoryAddress) async { + await _preferences.setString('lastAccountFactoryAddress', accountFactoryAddress); + } + + String? get lastAccountFactoryAddress => _preferences.getString('lastAccountFactoryAddress'); + // save the last link that was opened on web Future setLastWalletLink(String link) async { await _preferences.setString('lastWalletLink', link); @@ -161,4 +166,26 @@ class PreferencesService { return _preferences.getString('languageCode'); } + Future setMigrationModalShown(bool shown) async { + await _preferences.setBool('migrationModalShown', shown); + } + + bool get migrationModalShown => + _preferences.getBool('migrationModalShown') ?? false; + + Future setMigrationModalDismissalCount(int count) async { + await _preferences.setInt('migrationModalDismissalCount', count); + } + + int get migrationModalDismissalCount => + _preferences.getInt('migrationModalDismissalCount') ?? 0; + + Future incrementMigrationModalDismissalCount() async { + final currentCount = migrationModalDismissalCount; + await setMigrationModalDismissalCount(currentCount + 1); + } + + Future resetMigrationModalDismissalCount() async { + await setMigrationModalDismissalCount(0); + } } diff --git a/lib/services/sentry/sentry.dart b/lib/services/sentry/sentry.dart index 030e1a0b..4c47988b 100644 --- a/lib/services/sentry/sentry.dart +++ b/lib/services/sentry/sentry.dart @@ -1,7 +1,3 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; - // remove the sensitive part of the fragment String? scrubFragment(String? fragment) { if (fragment == null) { diff --git a/lib/services/wallet/contracts/account_factory.dart b/lib/services/wallet/contracts/account_factory.dart index cf27bffc..3bd6a085 100644 --- a/lib/services/wallet/contracts/account_factory.dart +++ b/lib/services/wallet/contracts/account_factory.dart @@ -8,16 +8,14 @@ import 'package:http/http.dart'; import 'package:smartcontracts/accounts.dart'; import 'package:web3dart/crypto.dart'; import 'package:web3dart/web3dart.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; Future accountFactoryServiceFromConfig(Config config, {String? customAccountFactory}) async { final primaryAccountFactory = config.community.primaryAccountFactory; - final url = config.getRpcUrl(primaryAccountFactory.chainId.toString()); + final url = config.getRpcUrl(primaryAccountFactory.chainId.toString(), customAccountFactory); // final wsurl = // config.chains[primaryAccountFactory.chainId.toString()]!.node.wsUrl; - print('url: $url'); final client = Client(); diff --git a/lib/services/wallet/contracts/cards/card_manager.dart b/lib/services/wallet/contracts/cards/card_manager.dart index 9f5305ea..1f335ec5 100644 --- a/lib/services/wallet/contracts/cards/card_manager.dart +++ b/lib/services/wallet/contracts/cards/card_manager.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/services.dart'; diff --git a/lib/services/wallet/contracts/cards/safe_card_manager.dart b/lib/services/wallet/contracts/cards/safe_card_manager.dart index 74053007..81ef0824 100644 --- a/lib/services/wallet/contracts/cards/safe_card_manager.dart +++ b/lib/services/wallet/contracts/cards/safe_card_manager.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/services.dart'; diff --git a/lib/services/wallet/contracts/profile.dart b/lib/services/wallet/contracts/profile.dart index e2296980..788d9653 100644 --- a/lib/services/wallet/contracts/profile.dart +++ b/lib/services/wallet/contracts/profile.dart @@ -168,12 +168,15 @@ class ProfileContract { } Future getURL(String addr) async { - return contract.get(EthereumAddress.fromHex(addr)); + final result = await contract.get(EthereumAddress.fromHex(addr)); + return result; } Future getURLFromUsername(String username) async { - return contract.getFromUsername( + final result = await contract.getFromUsername( convertStringToUint8List(username, forcePadLength: 32)); + + return result; } Uint8List setCallData(String addr, String username, String url) { diff --git a/lib/services/wallet/models/userop.dart b/lib/services/wallet/models/userop.dart index 8bbb473d..05ada3f4 100644 --- a/lib/services/wallet/models/userop.dart +++ b/lib/services/wallet/models/userop.dart @@ -6,6 +6,9 @@ import 'package:web3dart/web3dart.dart'; const String gasFeeErrorMessage = 'pending ops: replacement op must increase maxFeePerGas and MaxPriorityFeePerGas'; const String invalidBalanceErrorMessage = 'transfer amount exceeds balance'; +const String unauthorizedErrorMessage = 'Unauthorized'; +const String accessDeniedErrorMessage = 'Access Denied'; +const String forbiddenErrorMessage = 'Forbidden'; class NetworkCongestedException implements Exception { final String message = 'network congestion'; @@ -25,6 +28,12 @@ class NetworkUnknownException implements Exception { NetworkUnknownException(); } +class NetworkUnauthorizedException implements Exception { + final String message = 'unauthorized access'; + + NetworkUnauthorizedException(); +} + const String zeroAddress = '0x0000000000000000000000000000000000000000'; final BigInt defaultCallGasLimit = BigInt.from(35000); final BigInt defaultVerificationGasLimit = BigInt.from(70000); diff --git a/lib/services/wallet/utils.dart b/lib/services/wallet/utils.dart index 6d79cf14..d89fa20c 100644 --- a/lib/services/wallet/utils.dart +++ b/lib/services/wallet/utils.dart @@ -127,12 +127,12 @@ bool isValidPrivateKey(String privateKey) { String compress(String data) { final enCodedData = utf8.encode(data); final gZipData = GZipEncoder().encode(enCodedData, level: 6); - return base64Url.encode(gZipData!); + return base64Url.encode(gZipData); } Uint8List compressBytes(Uint8List data) { final gZipData = GZipEncoder().encode(data, level: 6); - return convertBytesToUint8List(gZipData!); + return convertBytesToUint8List(gZipData); } String decompress(String data) { diff --git a/lib/services/wallet/wallet.dart b/lib/services/wallet/wallet.dart index afbe4beb..2fcca7eb 100644 --- a/lib/services/wallet/wallet.dart +++ b/lib/services/wallet/wallet.dart @@ -5,24 +5,17 @@ import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/indexer/pagination.dart'; import 'package:citizenwallet/services/indexer/push_update_request.dart'; import 'package:citizenwallet/services/indexer/signed_request.dart'; -import 'package:citizenwallet/services/preferences/preferences.dart'; +import 'package:citizenwallet/services/engine/utils.dart'; import 'package:citizenwallet/services/sigauth/sigauth.dart'; -import 'package:citizenwallet/services/wallet/contracts/accessControl.dart'; -import 'package:citizenwallet/services/wallet/contracts/cards/card_manager.dart'; -import 'package:citizenwallet/services/wallet/contracts/cards/safe_card_manager.dart'; -import 'package:citizenwallet/services/wallet/contracts/communityModule.dart'; -import 'package:citizenwallet/services/wallet/contracts/entrypoint.dart'; -import 'package:citizenwallet/services/wallet/contracts/erc1155.dart'; import 'package:citizenwallet/services/wallet/contracts/erc20.dart'; +import 'package:citizenwallet/services/wallet/contracts/entrypoint.dart'; import 'package:citizenwallet/services/wallet/contracts/profile.dart'; -import 'package:citizenwallet/services/wallet/contracts/safe_account.dart'; import 'package:citizenwallet/services/wallet/contracts/simpleFaucet.dart'; -import 'package:citizenwallet/services/wallet/contracts/simple_account.dart'; -import 'package:citizenwallet/services/wallet/contracts/account_factory.dart'; +import 'package:citizenwallet/services/wallet/contracts/cards/card_manager.dart'; +import 'package:citizenwallet/services/wallet/contracts/cards/safe_card_manager.dart'; import 'package:citizenwallet/services/wallet/contracts/cards/interface.dart'; -import 'package:citizenwallet/services/engine/utils.dart'; +import 'package:citizenwallet/services/wallet/contracts/accessControl.dart'; import 'package:citizenwallet/services/wallet/gas.dart'; -import 'package:citizenwallet/services/wallet/models/chain.dart'; import 'package:citizenwallet/services/wallet/models/json_rpc.dart'; import 'package:citizenwallet/services/wallet/models/paymaster_data.dart'; import 'package:citizenwallet/services/wallet/models/userop.dart'; @@ -30,991 +23,1162 @@ import 'package:citizenwallet/services/wallet/utils.dart'; import 'package:citizenwallet/utils/delay.dart'; import 'package:citizenwallet/utils/uint8.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:http/http.dart'; import 'package:web3dart/crypto.dart'; import 'package:web3dart/web3dart.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; -class WalletService { - static final WalletService _instance = WalletService._internal(); +int _prepareUseropCallCount = 0; +int _submitUseropCallCount = 0; +bool _migrationInProgress = false; - factory WalletService() { - return _instance; - } - - final PreferencesService _pref = PreferencesService(); - - BigInt? _chainId; - late NativeCurrency currency; +void setMigrationInProgress(bool inProgress) { + _migrationInProgress = inProgress; +} - late Client _client; +bool get isMigrationInProgress => _migrationInProgress; - late String _alias; - late String _url; - late String _wsurl; - late Web3Client _ethClient; +/// given a tx hash, waits for the tx to be mined +Future waitForTxSuccess( + Config config, + String txHash, { + int retryCount = 0, + int maxRetries = 20, +}) async { + if (retryCount >= maxRetries) { + return false; + } - late String ipfsUrl; - late APIService _ipfs; - late APIService _indexer; - late APIService _indexerIPFS; + final ethClient = config.ethClient; - late APIService _rpc; - late APIService _bundlerRPC; - late APIService _paymasterRPC; - late String _paymasterType; + final receipt = await ethClient.getTransactionReceipt(txHash); + if (receipt?.status != true) { + // there is either no receipt or the tx is still not confirmed - late EIP1559GasPriceEstimator _gasPriceEstimator; + // increment the retry count + final nextRetryCount = retryCount + 1; - late Map erc4337Headers; + // wait for a bit before retrying + await delay(Duration(milliseconds: 250 * (nextRetryCount))); - WalletService._internal() { - _client = Client(); + // retry + return waitForTxSuccess( + config, + txHash, + retryCount: nextRetryCount, + maxRetries: maxRetries, + ); } - // Declare variables using the `late` keyword, which means they will be initialized at a later time. -// The variables are related to Ethereum blockchain development. - late EthPrivateKey - _credentials; // Represents a private key for an Ethereum account. - late EthereumAddress _account; // Represents an Ethereum address. - late SigAuthService _sigAuth; - - late StackupEntryPoint - _contractEntryPoint; // Represents the entry point for a smart contract on the Ethereum blockchain. - late CommunityModule _contractCommunityModule; - late AccountFactoryService - _contractAccountFactory; // Represents a factory for creating Ethereum accounts. - String _tokenStandard = 'erc20'; - ERC20Contract? - _contractToken; // Represents a smart contract for an ERC20 token on the Ethereum blockchain. - ERC1155Contract? - _contract1155Token; // Represents a smart contract for an ERC1155 token on the Ethereum blockchain. - late AccessControlUpgradeableContract _contractAccessControl; - late SimpleAccount _contractAccount; // Represents a simple Ethereum account. - late SafeAccount _contractSafeAccount; - late ProfileContract - _contractProfile; // Represents a smart contract for a user profile on the Ethereum blockchain. - AbstractCardManagerContract? _cardManager; - - EthPrivateKey get credentials => _credentials; - EthereumAddress get address => _credentials.address; - EthereumAddress get account => _account; - - /// retrieves the current balance of the address - Future getBalance({String? addr, BigInt? tokenId}) async { - try { - BigInt b = BigInt.zero; - if (_tokenStandard == 'erc20') { - b = await _contractToken!.getBalance(addr ?? _account.hexEip55).timeout( - const Duration(seconds: 10), - ); - } else if (_tokenStandard == 'erc1155') { - b = await _contract1155Token! - .getBalance(addr ?? _account.hexEip55, tokenId ?? BigInt.zero) - .timeout( - const Duration(seconds: 10), - ); - } - - // TODO: figure out why the returned balance is sometimes out of bounds - _pref.setBalance(_account.hexEip55, b.toString()); - - return b.toString(); - } catch (_) {} + return true; +} - return _pref.getBalance(_account.hexEip55) ?? '0'; +/// construct transfer call data +Uint8List tokenTransferCallData( + Config config, + EthereumAddress from, + String to, + BigInt amount, { + BigInt? tokenId, +}) { + if (config.getPrimaryToken().standard == 'erc20') { + return config.token20Contract.transferCallData(to, amount); + } else if (config.getPrimaryToken().standard == 'erc1155') { + return config.token1155Contract + .transferCallData(from.hexEip55, to, tokenId ?? BigInt.zero, amount); + } else { + return Uint8List.fromList([]); } +} - String get standard => _tokenStandard; - - Future get minter async { - return _contractAccessControl.isMinter(_account.hexEip55); +String transferEventStringSignature(Config config) { + if (config.getPrimaryToken().standard == 'erc20') { + return config.token20Contract.transferEventStringSignature; + } else if (config.getPrimaryToken().standard == 'erc1155') { + return config.token1155Contract.transferEventStringSignature; } - /// retrieve chain id - int get chainId => _chainId != null ? _chainId!.toInt() : 0; - String? get alias { - try { - return _alias; - } catch (_) {} + return ''; +} - return null; +String transferEventSignature(Config config) { + if (config.getPrimaryToken().standard == 'erc20') { + return config.token20Contract.transferEventSignature; + } else if (config.getPrimaryToken().standard == 'erc1155') { + return config.token1155Contract.transferEventSignature; } - String get tokenAddress => _tokenStandard == 'erc20' - ? _contractToken!.addr - : _contract1155Token!.addr; - String get profileAddress => _contractProfile.addr; - - SigAuthConnection get connection => _sigAuth.connect(); - - Future get accountNonce async => - getEntryPointContract().getNonce(_account.hexEip55); - - Future init( - EthereumAddress account, - EthPrivateKey privateKey, - NativeCurrency currency, - Config config, { - void Function(String)? onNotify, - void Function(bool)? onFinished, - }) async { - _alias = config.community.alias; - - final token = config.getPrimaryToken(); - final accountAbstractionConfig = - config.getPrimaryAccountAbstractionConfig(); - final chain = config.chains[token.chainId.toString()]; - - _url = chain!.node.url; - _wsurl = chain.node.wsUrl; - - final rpcUrl = config.getRpcUrl(token.chainId.toString()); - - _ethClient = Web3Client( - rpcUrl, - _client, - // socketConnector: () => - // WebSocketChannel.connect(Uri.parse(_wsurl)).cast(), - ); + return ''; +} - ipfsUrl = config.ipfs.url; - _ipfs = APIService(baseURL: ipfsUrl); - _indexer = APIService(baseURL: _url); - _indexerIPFS = APIService(baseURL: _url); +/// retrieves the current balance of the address +Future getBalance( + Config config, + EthereumAddress addr, { + BigInt? tokenId, +}) async { + try { + final tokenStandard = config.getPrimaryToken().standard; + + BigInt balance = BigInt.zero; + switch (tokenStandard) { + case 'erc20': + balance = + await config.token20Contract.getBalance(addr.hexEip55).timeout( + const Duration(seconds: 4), + ); - _rpc = APIService(baseURL: rpcUrl); + break; + case 'erc1155': + balance = await config.token1155Contract + .getBalance(addr.hexEip55, tokenId ?? BigInt.zero) + .timeout( + const Duration(seconds: 4), + ); + break; + } - _bundlerRPC = APIService(baseURL: rpcUrl); - _paymasterRPC = APIService(baseURL: rpcUrl); - _paymasterType = accountAbstractionConfig.paymasterType; + return balance.toString(); + } catch (e, s) { + debugPrint('error: $e'); + debugPrint('stack trace: $s'); + } - _gasPriceEstimator = EIP1559GasPriceEstimator( - _rpc, - _ethClient, - gasExtraPercentage: accountAbstractionConfig.gasExtraPercentage, - ); + return '0'; +} + +/// get profile data +Future getProfile(Config config, String addr) async { + try { + final url = await config.profileContract.getURL(addr); - erc4337Headers = {}; - if (!kIsWeb || kDebugMode) { - // on native, we need to set the origin header - erc4337Headers['Origin'] = dotenv.get('ORIGIN_HEADER'); + if (url.isEmpty) { + return null; } + final profileData = await config.ipfsService.get(url: '/$url'); - _credentials = privateKey; - _sigAuth = SigAuthService( - credentials: privateKey, - address: account, - redirect: dotenv.get('ORIGIN_HEADER'), - ); + final profile = ProfileV1.fromJson(profileData); - final cachedChainId = _pref.getChainIdForAlias(config.community.alias); - _chainId = cachedChainId != null - ? BigInt.parse(cachedChainId) - : await _ethClient.getChainId(); - await _pref.setChainIdForAlias( - config.community.alias, _chainId!.toString()); + profile.parseIPFSImageURLs(config.ipfs.url); - this.currency = currency; + return profile; + } catch (exception) { + // + } - final primaryCardManager = config.getPrimaryCardManager(); + return null; +} - if (primaryCardManager != null && - primaryCardManager.type == CardManagerType.classic) { - _cardManager = CardManagerContract( - _chainId!.toInt(), - _ethClient, - primaryCardManager.address, - ); - } +/// get profile data by username +Future getProfileByUsername(Config config, String username) async { + try { + final url = await config.profileContract.getURLFromUsername(username); - if (primaryCardManager != null && - primaryCardManager.type == CardManagerType.safe && - primaryCardManager.instanceId != null) { - final instanceId = primaryCardManager.instanceId!; - _cardManager = SafeCardManagerContract( - keccak256(convertStringToUint8List(instanceId)), - _chainId!.toInt(), - _ethClient, - primaryCardManager.address, - ); + if (url.isEmpty) { + return null; } + final profileData = await config.ipfsService.get(url: '/$url'); - await _cardManager?.init(); + final profile = ProfileV1.fromJson(profileData); - await _initContracts( - account, - config.community.alias, - accountAbstractionConfig.entrypointAddress, - accountAbstractionConfig.accountFactoryAddress, - token, - config.community.profile.address, - ); + profile.parseIPFSImageURLs(config.ipfs.url); - onFinished?.call(true); + return profile; + } catch (exception) { + // } - Future getCardHash(String serial, {bool local = true}) async { - if (_cardManager == null) { - throw Exception('Card manager not initialized'); - } + return null; +} - return _cardManager!.getCardHash(serial, local: local); - } +/// profileExists checks whether there is a profile for this username +Future profileExists(Config config, String username) async { + try { + final url = await config.profileContract + .getURLFromUsername(username) + .timeout(const Duration(seconds: 10)); - Future getCardAddress(Uint8List hash) async { - if (_cardManager == null) { - throw Exception('Card manager not initialized'); - } - return _cardManager!.getCardAddress(hash); + return url != ''; + } catch (exception) { + // } - Future initWeb( - EthereumAddress account, - EthPrivateKey privateKey, - NativeCurrency currency, - Config config, { - bool legacy = false, - }) async { - // _useLegacyBundlers = legacy; - - // _alias = config.community.alias; - // _url = config.node.url; - // _wsurl = config.node.wsUrl; - - // _ethClient = Web3Client( - // _url, - // _client, - // socketConnector: () => - // WebSocketChannel.connect(Uri.parse(_wsurl)).cast(), - // ); - - // ipfsUrl = config.ipfs.url; - // _ipfs = APIService(baseURL: ipfsUrl); - // _indexer = APIService(baseURL: config.indexer.url); - // _indexerIPFS = APIService(baseURL: config.indexer.ipfsUrl); - - // _rpc = APIService(baseURL: config.node.url); - // _bundlerRPC = APIService( - // baseURL: config.erc4337.paymasterAddress != null - // ? '${config.erc4337.rpcUrl}/${config.erc4337.paymasterAddress}' - // : config.erc4337.rpcUrl); - // _paymasterRPC = APIService( - // baseURL: config.erc4337.paymasterAddress != null - // ? '${config.erc4337.paymasterRPCUrl}/${config.erc4337.paymasterAddress}' - // : config.erc4337.paymasterRPCUrl); - // _paymasterType = config.erc4337.paymasterType; - - // _gasPriceEstimator = EIP1559GasPriceEstimator( - // _rpc, - // _ethClient, - // gasExtraPercentage: config.erc4337.gasExtraPercentage, - // ); - - // erc4337Headers = {}; - // if (!kIsWeb || kDebugMode) { - // // on native, we need to set the origin header - // erc4337Headers['Origin'] = dotenv.get('ORIGIN_HEADER'); - // } - - // _credentials = privateKey; - - // final cachedChainId = _pref.getChainIdForAlias(config.community.alias); - // _chainId = cachedChainId != null - // ? BigInt.parse(cachedChainId) - // : await _ethClient.getChainId(); - // await _pref.setChainIdForAlias( - // config.community.alias, _chainId!.toString()); - - // this.currency = currency; - - // _legacy4337Bundlers = await getLegacy4337Bundlers(); - - // await _initContracts( - // account, - // config.community.alias, - // config.erc4337.entrypointAddress, - // config.erc4337.accountFactoryAddress, - // config.token.address, - // config.profile.address, - // ); - - // await _initLegacyContracts(); - // await _initLegacyRPCs(); + return false; +} + +/// get profile data +Future getProfileFromUrl(Config config, String url) async { + try { + final profileData = await config.ipfsService.get(url: '/$url'); + final profile = ProfileV1.fromJson(profileData); + profile.parseIPFSImageURLs(config.ipfs.url); + return profile; + } catch (exception) { + debugPrint('Error: $exception'); } - /// Initializes the Ethereum smart contracts used by the wallet. - /// - /// [account] The account address - /// [alias] The community alias - /// [eaddr] The Ethereum address of the entry point for the smart contract. - /// [afaddr] The Ethereum address of the account factory smart contract. - /// [taddr] The Ethereum address of the ERC20 token smart contract. - /// [prfaddr] The Ethereum address of the user profile smart contract. - Future _initContracts( - EthereumAddress account, - String alias, - String eaddr, - String afaddr, - TokenConfig token, - String prfaddr, - ) async { - // Get the Ethereum address for the current account. - // _account = EthereumAddress.fromHex(account); - _account = account; - await _pref.setAccountAddress( - _credentials.address.hexEip55, - address.hexEip55, + return null; +} + +/// set profile data +Future setProfile( + Config config, + EthereumAddress account, + EthPrivateKey credentials, + ProfileRequest profile, { + required List image, + required String fileType, + String? accountFactoryAddress, +}) async { + try { + final url = + '/v1/profiles/${config.profileContract.addr}/${account.hexEip55}'; + + final json = jsonEncode( + profile.toJson(), ); - // Create a new simple account instance and initialize it. - _contractAccount = SimpleAccount(chainId, _ethClient, _account.hexEip55); - await _contractAccount.init(); + final body = SignedRequest(convertBytesToUint8List(utf8.encode(json))); - // Create a new safe account instance and initialize it. - _contractSafeAccount = SafeAccount(chainId, _ethClient, _account.hexEip55); - await _contractSafeAccount.init(); + final sig = await compute( + generateSignature, (jsonEncode(body.toJson()), credentials)); - // Create a new entry point instance and initialize it. - _contractEntryPoint = StackupEntryPoint(chainId, _ethClient, eaddr); - await _contractEntryPoint.init(); + final resp = await config.engineIPFSService.filePut( + url: url, + file: image, + fileType: fileType, + headers: { + 'X-Signature': sig, + 'X-Address': account.hexEip55, + }, + body: body.toJson(), + ); - // Create a new community module instance and initialize it. - _contractCommunityModule = CommunityModule(chainId, _ethClient, eaddr); - await _contractCommunityModule.init(); + final String profileUrl = resp['object']['ipfs_url']; - // Create a new account factory instance and initialize it. - _contractAccountFactory = - AccountFactoryService(chainId, _ethClient, afaddr); - await _contractAccountFactory.init(); + final calldata = config.profileContract + .setCallData(account.hexEip55, profile.username, profileUrl); - _tokenStandard = token.standard; + final factoryAddress = + accountFactoryAddress ?? config.community.primaryAccountFactory.address; + final (_, userop) = await prepareUserop( + config, + account, + credentials, + [config.profileContract.addr], + [calldata], + accountFactoryAddress: factoryAddress, + ); - switch (token.standard) { - case 'erc20': - // Create a new ERC20 token contract instance and initialize it. - _contractToken = ERC20Contract(chainId, _ethClient, token.address); - await _contractToken?.init(); - break; - case 'erc1155': - _contract1155Token = - ERC1155Contract(chainId, _ethClient, token.address); - await _contract1155Token?.init(); - break; + final paymasterType = config.getPaymasterType(); + final isCWSafeAccount = paymasterType == 'cw-safe'; + + final txHash = await submitUserop( + config, + userop, + migrationSafe: isCWSafeAccount, + ); + if (txHash == null) { + throw Exception('profile update failed'); } - _contractAccessControl = - AccessControlUpgradeableContract(chainId, _ethClient, token.address); - await _contractAccessControl.init(); + final success = await waitForTxSuccess(config, txHash); + if (!success) { + throw Exception('transaction failed'); + } - // Create a new user profile contract instance and initialize it. - _contractProfile = ProfileContract(chainId, _ethClient, prfaddr); - await _contractProfile.init(); + return profileUrl; + } catch (e, s) { + debugPrint('error: $e'); + debugPrint('stack trace: $s'); } - /// given a tx hash, waits for the tx to be mined - Future waitForTxSuccess( - String txHash, { - int retryCount = 0, - int maxRetries = 20, - }) async { - if (retryCount >= maxRetries) { - return false; - } + return null; +} - final receipt = await _ethClient.getTransactionReceipt(txHash); - if (receipt?.status != true) { - // there is either no receipt or the tx is still not confirmed +/// update profile data +Future updateProfile( + Config config, + EthereumAddress account, + EthPrivateKey credentials, + ProfileV1 profile, { + String? accountFactoryAddress, +}) async { + try { + final url = + '/v1/profiles/${config.profileContract.addr}/${account.hexEip55}'; + + final json = jsonEncode( + profile.toJson(), + ); - // increment the retry count - final nextRetryCount = retryCount + 1; + final body = SignedRequest(convertBytesToUint8List(utf8.encode(json))); - // wait for a bit before retrying - await delay(Duration(milliseconds: 250 * (nextRetryCount))); + final sig = await compute( + generateSignature, (jsonEncode(body.toJson()), credentials)); - // retry - return waitForTxSuccess( - txHash, - retryCount: nextRetryCount, - maxRetries: maxRetries, - ); - } + final resp = await config.engineIPFSService.patch( + url: url, + headers: { + 'X-Signature': sig, + 'X-Address': account.hexEip55, + }, + body: body.toJson(), + ); - return true; - } + final String profileUrl = resp['object']['ipfs_url']; - StackupEntryPoint getEntryPointContract({bool legacy = false}) { - return _contractEntryPoint; - } + final calldata = config.profileContract + .setCallData(account.hexEip55, profile.username, profileUrl); - AccountFactoryService getAccounFactoryContract({bool legacy = false}) { - return _contractAccountFactory; - } + final factoryAddress = + accountFactoryAddress ?? config.community.primaryAccountFactory.address; - APIService getBundlerRPC({bool legacy = false}) { - return _bundlerRPC; - } + final (_, userop) = await prepareUserop( + config, + account, + credentials, + [config.profileContract.addr], + [calldata], + accountFactoryAddress: factoryAddress, + ); - APIService getPaymasterRPC({bool legacy = false}) { - return _paymasterRPC; - } + final paymasterType = config.getPaymasterType(); + final isCWSafeAccount = paymasterType == 'cw-safe'; - String getPaymasterType({bool legacy = false}) { - return _paymasterType; - } + final txHash = await submitUserop( + config, + userop, + migrationSafe: isCWSafeAccount, + ); - /// fetches the balance of a given address - // Future getBalance(String addr, {BigInt? tokenId}) async { - // BigInt b = BigInt.zero; - // if (_tokenStandard == 'erc20') { - // b = await _contractToken!.getBalance(addr); - // } else if (_tokenStandard == 'erc1155') { - // b = await _contract1155Token!.getBalance(addr, tokenId!); - // } - - // return fromDoubleUnit( - // b.toString(), - // decimals: currency.decimals, - // ); - // } - - String get transferEventStringSignature { - if (_tokenStandard == 'erc20') { - return _contractToken!.transferEventStringSignature; - } else if (_tokenStandard == 'erc1155') { - return _contract1155Token!.transferEventStringSignature; + if (txHash == null) { + throw Exception('profile update failed'); } - return ''; - } - - String get transferEventSignature { - if (_tokenStandard == 'erc20') { - return _contractToken!.transferEventSignature; - } else if (_tokenStandard == 'erc1155') { - return _contract1155Token!.transferEventSignature; + final success = await waitForTxSuccess(config, txHash); + if (!success) { + throw Exception('transaction failed'); } - return ''; + return profileUrl; + } catch (e, stackTrace) { + debugPrint('Stack trace: $stackTrace'); } - Future isMinter(String addr) async { - return _contractAccessControl.isMinter(addr); - } - - /// set profile data - Future setProfile( - ProfileRequest profile, { - required List image, - required String fileType, - }) async { - try { - final url = '/v1/profiles/$profileAddress/${_account.hexEip55}'; - - final json = jsonEncode( - profile.toJson(), - ); - - final body = SignedRequest(convertBytesToUint8List(utf8.encode(json))); + return null; +} - final sig = await compute( - generateSignature, (jsonEncode(body.toJson()), _credentials)); +/// set profile data +Future deleteCurrentProfile( + Config config, + EthereumAddress account, + EthPrivateKey credentials, +) async { + try { + final url = + '/v1/profiles/${config.profileContract.addr}/${account.hexEip55}'; + + final encoded = jsonEncode( + { + 'account': account.hexEip55, + 'date': DateTime.now().toUtc().toIso8601String(), + }, + ); - final resp = await _indexerIPFS.filePut( - url: url, - file: image, - fileType: fileType, - headers: { - 'X-Signature': sig, - 'X-Address': _account.hexEip55, - }, - body: body.toJson(), - ); + final body = SignedRequest(convertStringToUint8List(encoded)); - final String profileUrl = resp['object']['ipfs_url']; + final sig = await compute( + generateSignature, (jsonEncode(body.toJson()), credentials)); - final calldata = _contractProfile.setCallData( - _account.hexEip55, profile.username, profileUrl); + await config.engineIPFSService.delete( + url: url, + headers: { + 'X-Signature': sig, + 'X-Address': account.hexEip55, + }, + body: body.toJson(), + ); - final (_, userop) = await prepareUserop([profileAddress], [calldata]); + return true; + } catch (e, s) { + debugPrint('error: $e'); + debugPrint('stack trace: $s'); + } - final txHash = await submitUserop(userop); - if (txHash == null) { - throw Exception('profile update failed'); - } + return false; +} - final success = await waitForTxSuccess(txHash); - if (!success) { - throw Exception('transaction failed'); - } +Future accountExists( + Config config, + EthereumAddress account, +) async { + try { + final url = '/v1/accounts/${account.hexEip55}/exists'; - return profileUrl; - } catch (_) {} + await config.engine.get( + url: url, + ); - return null; + return true; + } catch (e) { + return false; } +} - /// update profile data - Future updateProfile(ProfileV1 profile) async { - try { - final url = '/v1/profiles/$profileAddress/${_account.hexEip55}'; - - final json = jsonEncode( - profile.toJson(), - ); +Future accountExistsWithFallback( + Config config, + EthereumAddress account, +) async { + try { + final exists = await accountExists(config, account); + if (exists) { + return true; + } + } catch (e) {} - final body = SignedRequest(convertBytesToUint8List(utf8.encode(json))); + try { + final nonce = await config.entryPointContract.getNonce(account.hexEip55); + final exists = nonce > BigInt.zero; + if (exists) { + return true; + } + } catch (e) {} - final sig = await compute( - generateSignature, (jsonEncode(body.toJson()), _credentials)); + try { + final balance = await getBalance(config, account); + final exists = + double.tryParse(balance) != null && double.parse(balance) > 0; + if (exists) { + return true; + } + } catch (e) {} - final resp = await _indexerIPFS.patch( - url: url, - headers: { - 'X-Signature': sig, - 'X-Address': _account.hexEip55, - }, - body: body.toJson(), - ); + return false; +} - final String profileUrl = resp['object']['ipfs_url']; +/// create an account +Future createAccount( + Config config, + EthereumAddress account, + EthPrivateKey credentials, +) async { + try { + final exists = await accountExists(config, account); + if (exists) { + return true; + } - final calldata = _contractProfile.setCallData( - _account.hexEip55, profile.username, profileUrl); + final simpleAccount = await config.getSimpleAccount(account.hexEip55); - final (_, userop) = await prepareUserop([profileAddress], [calldata]); + Uint8List calldata = simpleAccount.transferOwnershipCallData( + credentials.address.hexEip55, + ); + if (config.getPaymasterType() == 'cw-safe') { + calldata = config.communityModuleContract.getChainIdCallData(); + } - final txHash = await submitUserop(userop); - if (txHash == null) { - throw Exception('profile update failed'); - } + final (_, userop) = await prepareUserop( + config, + account, + credentials, + [account.hexEip55], + [calldata], + accountFactoryAddress: config.community.primaryAccountFactory.address, + ); - final success = await waitForTxSuccess(txHash); - if (!success) { - throw Exception('transaction failed'); - } + final txHash = await submitUserop( + config, + userop, + ); + if (txHash == null) { + throw Exception('failed to submit user op'); + } - return profileUrl; - } catch (_) {} + final success = await waitForTxSuccess(config, txHash); + if (!success) { + throw Exception('transaction failed'); + } - return null; + return true; + } catch (e, s) { + debugPrint('error: $e'); + debugPrint('stack trace: $s'); } - /// set profile data - Future unpinCurrentProfile() async { - try { - final url = '/v1/profiles/$profileAddress/${_account.hexEip55}'; - - final encoded = jsonEncode( - { - 'account': _account.hexEip55, - 'date': DateTime.now().toUtc().toIso8601String(), - }, - ); - - final body = SignedRequest(convertStringToUint8List(encoded)); - - final sig = await compute( - generateSignature, (jsonEncode(body.toJson()), _credentials)); - - await _indexerIPFS.delete( - url: url, - headers: { - 'X-Signature': sig, - 'X-Address': _account.hexEip55, - }, - body: body.toJson(), - ); - - return true; - } catch (_) {} + return false; +} - return false; - } +/// makes a jsonrpc request from this wallet +Future requestPaymaster( + Config config, + SUJSONRPCRequest body, { + bool legacy = false, +}) async { + final rawResponse = await config.engineRPC.post( + body: body, + ); - /// get profile data - Future getProfile(String addr) async { - try { - final url = await _contractProfile.getURL(addr); - - final profileData = await _ipfs.get(url: '/$url'); - - final profile = ProfileV1.fromJson(profileData); - - profile.parseIPFSImageURLs(ipfsUrl); - - return profile; - } on UnauthorizedException { - // - } on NotFoundException { - // - } on ConflictException { - // - } catch (exception) { - // - final rpcError = parseRPCErrorText(exception.toString()); - if (rpcError.code == -1) { - throw NetworkException(); - } - } + final response = SUJSONRPCResponse.fromJson(rawResponse); - return null; + if (response.error != null) { + throw Exception(response.error!.message); } - /// get profile data - Future getProfileFromUrl(String url) async { - try { - final profileData = await _ipfs.get(url: '/$url'); - - final profile = ProfileV1.fromJson(profileData); - - profile.parseIPFSImageURLs(ipfsUrl); + return response; +} - return profile; - } catch (exception) { - // +/// return paymaster data for constructing a user op +Future<(PaymasterData?, Exception?)> getPaymasterData( + Config config, + UserOp userop, + String eaddr, + String ptype, { + bool legacy = false, +}) async { + final body = SUJSONRPCRequest( + method: 'pm_sponsorUserOperation', + params: [ + userop.toJson(), + eaddr, + {'type': ptype}, + ], + ); + + try { + final response = await requestPaymaster(config, body, legacy: legacy); + final result = PaymasterData.fromJson(response.result); + return (result, null); + } catch (exception) { + final strerr = exception.toString(); + + if (strerr.contains(gasFeeErrorMessage)) { + return (null, NetworkCongestedException()); } - return null; + if (strerr.contains(invalidBalanceErrorMessage)) { + return (null, NetworkInvalidBalanceException()); + } } - /// get profile data by username - Future getProfileByUsername(String username) async { - try { - final url = await _contractProfile.getURLFromUsername(username); + return (null, NetworkUnknownException()); +} - final profileData = await _ipfs.get(url: '/$url'); +/// return paymaster data for constructing a user op +Future<(List, Exception?)> getPaymasterOOData( + Config config, + UserOp userop, + String eaddr, + String ptype, { + bool legacy = false, + int count = 1, +}) async { + final body = SUJSONRPCRequest( + method: 'pm_ooSponsorUserOperation', + params: [ + userop.toJson(), + eaddr, + {'type': ptype}, + count, + ], + ); + + try { + final response = await requestPaymaster(config, body, legacy: legacy); + + final List data = response.result; + if (data.isEmpty) { + throw Exception('empty paymaster data'); + } - final profile = ProfileV1.fromJson(profileData); + if (data.length != count) { + throw Exception('invalid paymaster data'); + } - profile.parseIPFSImageURLs(ipfsUrl); + return (data.map((item) => PaymasterData.fromJson(item)).toList(), null); + } catch (exception) { + final strerr = exception.toString(); - return profile; - } catch (exception) { - // + if (strerr.contains(gasFeeErrorMessage)) { + return ([], NetworkCongestedException()); } - return null; + if (strerr.contains(invalidBalanceErrorMessage)) { + return ([], NetworkInvalidBalanceException()); + } } - /// profileExists checks whether there is a profile for this username - Future profileExists(String username) async { - try { - final url = await _contractProfile.getURLFromUsername(username); + return ([], NetworkUnknownException()); +} - return url != ''; - } catch (exception) { - // +Future<(PaymasterData?, Exception?)> getPaymasterDataWithFallback( + Config config, + UserOp userop, + String eaddr, + String ptype, { + bool legacy = false, +}) async { + try { + final (data, error) = + await getPaymasterData(config, userop, eaddr, ptype, legacy: legacy); + if (data != null) { + return (data, null); } + } catch (e) {} - return false; + if (!legacy) { + try { + final (data, error) = + await getPaymasterData(config, userop, eaddr, ptype, legacy: true); + if (data != null) { + return (data, null); + } + } catch (e) {} } - /// Accounts + final alternativeTypes = ['payg', 'cw', 'cw-safe']; + for (final altType in alternativeTypes) { + if (altType != ptype) { + try { + final (data, error) = await getPaymasterData( + config, userop, eaddr, altType, + legacy: legacy); + if (data != null) { + return (data, null); + } + } catch (e) {} + } + } - /// check if an account exists - Future accountExists({ - String? account, - }) async { - try { - final url = '/v1/accounts/${account ?? _account.hexEip55}/exists'; + return (null, NetworkUnknownException()); +} - await _indexer.get( - url: url, - ); +Future<(String, UserOp)> prepareUserop( + Config config, + EthereumAddress account, + EthPrivateKey credentials, + List dest, + List calldata, { + EthPrivateKey? customCredentials, + BigInt? customNonce, + bool deploy = true, + BigInt? value, + String? accountFactoryAddress, + bool migrationSafe = false, + bool useFallback = true, +}) async { + _prepareUseropCallCount++; + + if (migrationSafe) { + _prepareUseropCallCount--; + return await _prepareUseropOriginal( + config, + account, + credentials, + dest, + calldata, + customCredentials: customCredentials, + customNonce: customNonce, + deploy: deploy, + value: value, + accountFactoryAddress: accountFactoryAddress, + ); + } - return true; - } catch (_) {} + if (!useFallback || _prepareUseropCallCount > 1) { + _prepareUseropCallCount--; + return await _prepareUseropOriginal( + config, + account, + credentials, + dest, + calldata, + customCredentials: customCredentials, + customNonce: customNonce, + deploy: deploy, + value: value, + accountFactoryAddress: accountFactoryAddress, + ); + } - return false; + if (_migrationInProgress) { + _prepareUseropCallCount--; + return await _prepareUseropOriginal( + config, + account, + credentials, + dest, + calldata, + customCredentials: customCredentials, + customNonce: customNonce, + deploy: deploy, + value: value, + accountFactoryAddress: accountFactoryAddress, + ); } - // TODO: create an account using a user op + final result = await prepareUseropWithFallback( + config, + account, + credentials, + dest, + calldata, + customCredentials: customCredentials, + customNonce: customNonce, + deploy: deploy, + value: value, + accountFactoryAddress: accountFactoryAddress, + ); + + _prepareUseropCallCount--; + return result; +} - /// create an account - Future createAccount({ - EthPrivateKey? customCredentials, - }) async { - try { - final exists = await accountExists(); - if (exists) { - return true; - } +Future<(String, UserOp)> _prepareUseropOriginal( + Config config, + EthereumAddress account, + EthPrivateKey credentials, + List dest, + List calldata, { + EthPrivateKey? customCredentials, + BigInt? customNonce, + bool deploy = true, + BigInt? value, + String? accountFactoryAddress, +}) async { + try { + final cred = customCredentials ?? credentials; + + EthereumAddress acc = account; + if (customCredentials != null) { + acc = await getAccountAddress(config, customCredentials.address.hexEip55); + } - Uint8List calldata = _contractAccount.transferOwnershipCallData( - _credentials.address.hexEip55, - ); - if (_paymasterType == 'cw-safe') { - calldata = _contractCommunityModule.getChainIdCallData(); - } + // instantiate user op with default values + final userop = UserOp.defaultUserOp(); - final (_, userop) = await prepareUserop( - [_account.hexEip55], - [calldata], - ); + // use the account hex as the sender + userop.sender = acc.hexEip55; - final txHash = await submitUserop( - userop, - ); - if (txHash == null) { - throw Exception('failed to submit user op'); - } + // determine the appropriate nonce + BigInt nonce = customNonce ?? await config.getNonce(acc.hexEip55); + + var paymasterType = config.getPaymasterType(); - final success = await waitForTxSuccess(txHash); - if (!success) { - throw Exception('transaction failed'); + // if it's the first user op from this account, we need to deploy the account contract + if (nonce == BigInt.zero && deploy) { + bool exists = false; + if (paymasterType == 'payg') { + // solves edge case with legacy account migration + exists = await accountExists(config, acc); } - return true; - } catch (_) {} + if (!exists) { + final accountFactory = config.accountFactoryContract; - return false; - } + // construct the init code to deploy the account + userop.initCode = await accountFactory.createAccountInitCode( + cred.address.hexEip55, + BigInt.zero, + ); + } else { + // try again in case the account was created in the meantime + nonce = customNonce ?? + await config.entryPointContract.getNonce(acc.hexEip55); + } + } - /// upgrade an account - Future upgradeAccount() async { - try { - final accountFactory = _contractAccountFactory; + userop.nonce = nonce; - final url = - '/accounts/factory/${accountFactory.addr}/sca/${_account.hexEip55}'; + // set the appropriate call data for the transfer + // we need to call account.execute which will call token.transfer + switch (paymasterType) { + case 'payg': + case 'cw': + { + final simpleAccount = await config.getSimpleAccount(acc.hexEip55); - final encoded = jsonEncode( + userop.callData = dest.length > 1 && calldata.length > 1 + ? simpleAccount.executeBatchCallData( + dest, + calldata, + ) + : simpleAccount.executeCallData( + dest[0], + value ?? BigInt.zero, + calldata[0], + ); + break; + } + case 'cw-safe': { - 'owner': _credentials.address.hexEip55, - 'salt': BigInt.zero.toInt(), - }, - ); + // Get account-specific configuration if available + ERC4337Config? accountConfig; + if (accountFactoryAddress != null && + accountFactoryAddress.isNotEmpty) { + try { + accountConfig = + config.getAccountAbstractionConfig(accountFactoryAddress); + } catch (e) {} + } + + final oldFactoryAddresses = [ + '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2', // Old Safe factory + '0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9', // Old Bread factory + '0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD', // Old Gratitude factory + '0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87', // Old Brussels factory + '0x5e987a6c4bb4239d498E78c34e986acf29c81E8e', // Old SFLUV factory + '0xBABCf159c4e3186cf48e4a48bC0AeC17CF9d90FE', // Brussels Pay old factory + ]; + + // Determine if this account should use legacy SimpleAccount execution + bool needsLegacyExecution = false; + + try { + // Check if using an old factory address - these definitely need legacy execution + final isUsingOldFactory = accountFactoryAddress != null && + oldFactoryAddresses.contains(accountFactoryAddress); + + if (isUsingOldFactory) { + needsLegacyExecution = true; + } else { + final paymasterTypeFromConfig = + accountConfig?.paymasterType ?? paymasterType; + + if (paymasterTypeFromConfig == 'cw-safe') { + if (nonce > BigInt.zero && _needsLegacyExecution(config)) { + needsLegacyExecution = true; + } else { + needsLegacyExecution = false; + } + } else { + // For other paymaster types, existing accounts might need legacy execution + needsLegacyExecution = nonce > BigInt.zero; + } + } + } catch (e) { + // Default to legacy for safety + needsLegacyExecution = true; + } + + final isOldAccount = needsLegacyExecution; + + if (isOldAccount) { + // Use SimpleAccount execution for legacy accounts + final simpleAccount = await config.getSimpleAccount(acc.hexEip55); + userop.callData = simpleAccount.executeCallData( + dest[0], + value ?? BigInt.zero, + calldata[0], + ); - final body = SignedRequest(convertStringToUint8List(encoded)); + // Apply account-specific configuration overrides + // For legacy accounts, use old configuration + if (needsLegacyExecution && _needsLegacyExecution(config)) { + final oldAccountConfig = _getOldAccountConfig(config); + if (oldAccountConfig != null) { + try { + // Override with old configuration + if (config.entryPointContract.addr != + oldAccountConfig.entrypointAddress) { + config.entryPointContract = StackupEntryPoint( + config.chains.values.first.id, + config.ethClient, + oldAccountConfig.entrypointAddress, + ); + await config.entryPointContract.init(); + } + + if (paymasterType != oldAccountConfig.paymasterType) { + paymasterType = oldAccountConfig.paymasterType; + } + + final paymasterUrl = + '${config.chains.values.first.node.url}/v1/rpc/${oldAccountConfig.paymasterAddress}'; + config.engineRPC = APIService(baseURL: paymasterUrl); + } catch (e) { + // Ignore config errors, will use defaults + } + } + } else if (accountConfig != null) { + // Use the new cw-safe configuration for SafeAccount execution + if (config.entryPointContract.addr != + accountConfig.entrypointAddress) { + config.entryPointContract = StackupEntryPoint( + config.chains.values.first.id, + config.ethClient, + accountConfig.entrypointAddress, + ); + await config.entryPointContract.init(); + } + + if (paymasterType != accountConfig.paymasterType) { + paymasterType = accountConfig.paymasterType; + } + + if (accountConfig.paymasterAddress != null) { + final paymasterUrl = + '${config.chains.values.first.node.url}/v1/rpc/${accountConfig.paymasterAddress!}'; + config.engineRPC = APIService(baseURL: paymasterUrl); + } + } + } else { + // Use SafeAccount execution for new accounts + final safeAccount = await config.getSafeAccount(acc.hexEip55); + userop.callData = safeAccount.executeCallData( + dest[0], + value ?? BigInt.zero, + calldata[0], + ); + } + break; + } + } - final sig = await compute( - generateSignature, (jsonEncode(body.toJson()), _credentials)); + List paymasterOOData = []; + Exception? paymasterErr; + final useAccountNonce = + (nonce == BigInt.zero || paymasterType == 'payg') && deploy; - final response = await _indexer.patch( - url: url, - headers: { - 'X-Signature': sig, - 'X-Address': _account.hexEip55, - }, - body: body.toJson(), + if (useAccountNonce) { + PaymasterData? paymasterData; + (paymasterData, paymasterErr) = await getPaymasterData( + config, + userop, + config.entryPointContract.addr, + paymasterType, ); - final implementation = response['object']['account_implementation']; - if (implementation == "0x0000000000000000000000000000000000000000") { - throw Exception('invalid implementation'); + if (paymasterData != null) { + paymasterOOData.add(paymasterData); } + } else { + (paymasterOOData, paymasterErr) = await getPaymasterOOData( + config, + userop, + config.entryPointContract.addr, + paymasterType, + ); + } - return implementation; - } on ConflictException { - // account is already up to date - return null; - } catch (_) {} - - return null; - } - - /// Transactions - - /// fetch erc20 transfer events - /// - /// [offset] number of transfers to skip - /// - /// [limit] number of transferst to fetch - /// - /// [maxDate] fetch transfers up to this date - Future<(List, Pagination)> fetchErc20Transfers({ - required int offset, - required int limit, - required DateTime maxDate, - }) async { - try { - final List tx = []; - - const path = '/v1/logs'; + if (paymasterErr != null) { + throw paymasterErr; + } - final eventSignature = _tokenStandard == 'erc20' - ? _contractToken!.transferEventSignature - : _contract1155Token!.transferEventSignature; + if (paymasterOOData.isEmpty) { + throw Exception('unable to get paymaster data'); + } - final dataQueryParams = buildQueryParams([ - { - 'key': 'from', - 'value': _account.hexEip55, - }, - ], or: [ - { - 'key': 'to', - 'value': _account.hexEip55, - }, - ]); + final paymasterData = paymasterOOData.first; - final addr = _tokenStandard == 'erc20' - ? _contractToken!.addr - : _contract1155Token!.addr; + if (!useAccountNonce) { + userop.nonce = paymasterData.nonce; + } - final url = - '$path/$addr/$eventSignature?offset=$offset&limit=$limit&maxDate=${Uri.encodeComponent(maxDate.toUtc().toIso8601String())}&$dataQueryParams'; + userop.paymasterAndData = paymasterData.paymasterAndData; + userop.preVerificationGas = paymasterData.preVerificationGas; + userop.verificationGasLimit = paymasterData.verificationGasLimit; + userop.callGasLimit = paymasterData.callGasLimit; - final response = await _indexer.get(url: url); + final hash = await config.entryPointContract.getUserOpHash(userop); - // convert response array into TransferEvent list - for (final item in response['array']) { - final log = Log.fromJson(item); + userop.generateSignature(cred, hash); - tx.add(TransferEvent.fromLog(log, standard: _tokenStandard)); - } + return (bytesToHex(hash, include0x: true), userop); + } catch (e) { + rethrow; + } +} - return (tx, Pagination.fromJson(response['meta'])); - } catch (_) {} +Future<(String, UserOp)> prepareUseropWithFallback( + Config config, + EthereumAddress account, + EthPrivateKey credentials, + List dest, + List calldata, { + EthPrivateKey? customCredentials, + BigInt? customNonce, + bool deploy = true, + BigInt? value, + String? accountFactoryAddress, +}) async { + try { + final (hash, userop) = await prepareUserop( + config, + account, + credentials, + dest, + calldata, + customCredentials: customCredentials, + customNonce: customNonce, + deploy: deploy, + value: value, + accountFactoryAddress: accountFactoryAddress, + ); + return (hash, userop); + } catch (e) {} - return ([], Pagination.empty()); + if (!deploy) { + try { + final (hash, userop) = await prepareUserop( + config, + account, + credentials, + dest, + calldata, + customCredentials: customCredentials, + customNonce: customNonce, + deploy: true, + value: value, + accountFactoryAddress: accountFactoryAddress, + ); + return (hash, userop); + } catch (e) {} } - /// fetch new erc20 transfer events - /// - /// [fromDate] fetches transfers from this date - Future?> fetchNewErc20Transfers(DateTime fromDate) async { + if (accountFactoryAddress != null) { try { - final List tx = []; + final (hash, userop) = await prepareUserop( + config, + account, + credentials, + dest, + calldata, + customCredentials: customCredentials, + customNonce: customNonce, + deploy: deploy, + value: value, + accountFactoryAddress: null, + ); + return (hash, userop); + } catch (e) {} + } - const path = 'logs/v2/transfers'; + try { + final cred = customCredentials ?? credentials; + final acc = account; - final addr = _tokenStandard == 'erc20' - ? _contractToken!.addr - : _contract1155Token!.addr; + final userop = UserOp.defaultUserOp(); + userop.sender = acc.hexEip55; - final url = - '/$path/$addr/${_account.hexEip55}/new?limit=10&fromDate=${Uri.encodeComponent(fromDate.toUtc().toIso8601String())}'; + final exists = await accountExistsWithFallback(config, acc); - final response = await _indexer.get(url: url); + BigInt nonce = customNonce ?? await config.getNonce(acc.hexEip55); - // convert response array into TransferEvent list - for (final item in response['array']) { - tx.add(TransferEvent.fromJson(item)); - } + if (nonce == BigInt.zero && deploy && !exists) { + final accountFactory = config.accountFactoryContract; + userop.initCode = await accountFactory.createAccountInitCode( + cred.address.hexEip55, + BigInt.zero, + ); + } - return tx; - } catch (_) {} + userop.nonce = nonce; - return null; - } + final simpleAccount = await config.getSimpleAccount(acc.hexEip55); + userop.callData = dest.length > 1 && calldata.length > 1 + ? simpleAccount.executeBatchCallData(dest, calldata) + : simpleAccount.executeCallData( + dest[0], value ?? BigInt.zero, calldata[0]); - /// construct transfer call data - Uint8List tokenTransferCallData( - String to, - BigInt amount, { - String? from, - BigInt? tokenId, - }) { - if (_tokenStandard == 'erc20') { - return _contractToken!.transferCallData(to, amount); - } else if (_tokenStandard == 'erc1155') { - return _contract1155Token!.transferCallData( - from ?? _account.hexEip55, to, tokenId ?? BigInt.zero, amount); - } - - return Uint8List.fromList([]); - } + final (paymasterData, paymasterErr) = await getPaymasterDataWithFallback( + config, + userop, + config.entryPointContract.addr, + config.getPaymasterType(), + ); - /// construct erc20 transfer call data - Uint8List tokenMintCallData( - String to, - BigInt amount, { - BigInt? tokenId, - }) { - if (_tokenStandard == 'erc20') { - return _contractToken!.mintCallData(to, amount); - } else if (_tokenStandard == 'erc1155') { - return _contract1155Token! - .mintCallData(to, amount, tokenId ?? BigInt.zero); + if (paymasterErr != null) { + throw paymasterErr; } - return Uint8List.fromList([]); - } + if (paymasterData != null) { + userop.paymasterAndData = paymasterData.paymasterAndData; + userop.preVerificationGas = paymasterData.preVerificationGas; + userop.verificationGasLimit = paymasterData.verificationGasLimit; + userop.callGasLimit = paymasterData.callGasLimit; - /// construct simple faucet redeem call data - Future simpleFaucetRedeemCallData( - String address, - ) async { - final contract = SimpleFaucetContract(chainId, _ethClient, address); + if (nonce == BigInt.zero && deploy) { + userop.nonce = paymasterData.nonce; + } + } else { + throw Exception('No paymaster data available'); + } - await contract.init(); + final hash = await config.entryPointContract.getUserOpHash(userop); + userop.generateSignature(cred, hash); - return contract.redeemCallData(); + return (bytesToHex(hash, include0x: true), userop); + } catch (e) { + rethrow; } +} - /// fetch simple faucet redeem amount - Future getFaucetRedeemAmount(String address) async { - final contract = SimpleFaucetContract(chainId, _ethClient, address); - - await contract.init(); - - return contract.getAmount(); +Future submitUserop( + Config config, + UserOp userop, { + EthPrivateKey? customCredentials, + Map? data, + TransferData? extraData, + bool migrationSafe = false, + bool useRetry = true, +}) async { + _submitUseropCallCount++; + + if (migrationSafe) { + _submitUseropCallCount--; + return await _submitUseropOriginal( + config, + userop, + customCredentials: customCredentials, + data: data, + extraData: extraData, + ); } - /// Account Abstraction - - // get account address - Future getAccountAddress( - String addr, { - bool legacy = false, - bool cache = true, - }) async { - final prefKey = addr; - final cachedAccAddress = _pref.getAccountAddress(prefKey); - - EthereumAddress address; - if (!cache || cachedAccAddress == null) { - final accountFactory = getAccounFactoryContract(legacy: legacy); - - address = await accountFactory.getAddress(addr); - } else { - address = EthereumAddress.fromHex(cachedAccAddress); - } - await _pref.setAccountAddress( - prefKey, - address.hexEip55, + if (!useRetry || _submitUseropCallCount > 1) { + _submitUseropCallCount--; + return await _submitUseropOriginal( + config, + userop, + customCredentials: customCredentials, + data: data, + extraData: extraData, ); - return address; } - setAccountAddress(EthereumAddress address) async { - final prefKey = _credentials.address.hexEip55; - await _pref.setAccountAddress( - prefKey, - address.hexEip55, + if (_migrationInProgress) { + _submitUseropCallCount--; + return await _submitUseropOriginal( + config, + userop, + customCredentials: customCredentials, + data: data, + extraData: extraData, ); } - /// Submits a user operation to the Ethereum network. - /// - /// This function sends a JSON-RPC request to the ERC4337 bundler. The entrypoint is specified by the - /// [eaddr] parameter, with the [eth_sendUserOperation] method and the given - /// [userop] parameter. If the request is successful, the function returns a - /// tuple containing the transaction hash as a string and `null`. If the request - /// fails, the function returns a tuple containing `null` and an exception - /// object representing the type of error that occurred. - /// - /// If the request fails due to a network congestion error, the function returns - /// a [NetworkCongestedException] object. If the request fails due to an invalid - /// balance error, the function returns a [NetworkInvalidBalanceException] - /// object. If the request fails for any other reason, the function returns a - /// [NetworkUnknownException] object. - /// - /// [userop] The user operation to submit to the Ethereum network. - /// [eaddr] The Ethereum address of the node to send the request to. - /// A tuple containing the transaction hash as a string and [null] if - /// the request was successful, or [null] and an exception object if the - /// request failed. - Future<(String?, Exception?)> _submitUserOp( - UserOp userop, - String eaddr, { - Map? data, - TransferData? extraData, - }) async { - final params = [userop.toJson(), eaddr]; + final result = await submitUseropWithRetry( + config, + userop, + customCredentials: customCredentials, + data: data, + extraData: extraData, + ); + + _submitUseropCallCount--; + return result; +} + +Future _submitUseropOriginal( + Config config, + UserOp userop, { + EthPrivateKey? customCredentials, + Map? data, + TransferData? extraData, +}) async { + try { + final entryPoint = config.entryPointContract; + final params = [userop.toJson(), entryPoint.addr]; + if (data != null) { params.add(data); } @@ -1027,418 +1191,546 @@ class WalletService { params: params, ); - try { - final response = await _requestBundler(body); + final response = await requestBundler(config, body); + return response.result as String; + } catch (exception) { + final strerr = exception.toString(); - return (response.result as String, null); - } catch (exception) { - final strerr = exception.toString(); + if (strerr.contains(gasFeeErrorMessage)) { + throw NetworkCongestedException(); + } - if (strerr.contains(gasFeeErrorMessage)) { - return (null, NetworkCongestedException()); - } + if (strerr.contains(invalidBalanceErrorMessage)) { + throw NetworkInvalidBalanceException(); + } - if (strerr.contains(invalidBalanceErrorMessage)) { - return (null, NetworkInvalidBalanceException()); - } + if (strerr.contains(unauthorizedErrorMessage) || + strerr.contains(accessDeniedErrorMessage) || + strerr.contains(forbiddenErrorMessage)) { + throw NetworkUnauthorizedException(); } - return (null, NetworkUnknownException()); + throw NetworkUnknownException(); } +} - /// makes a jsonrpc request from this wallet - Future _requestPaymaster( - SUJSONRPCRequest body, { - bool legacy = false, - }) async { - final paymasterRPC = getPaymasterRPC(legacy: legacy); +Future submitUseropWithRetry( + Config config, + UserOp userop, { + EthPrivateKey? customCredentials, + Map? data, + TransferData? extraData, + int maxRetries = 3, +}) async { + int attempt = 0; + Duration delay = const Duration(seconds: 1); - final rawResponse = await paymasterRPC.post( - body: body, - headers: erc4337Headers, - ); + while (attempt < maxRetries) { + attempt++; - final response = SUJSONRPCResponse.fromJson(rawResponse); + try { + final result = await submitUserop( + config, + userop, + customCredentials: customCredentials, + data: data, + extraData: extraData, + ); - if (response.error != null) { - throw Exception(response.error!.message); - } + if (result != null) { + return result; + } + } catch (e) { + if (attempt >= maxRetries) { + rethrow; + } - return response; + if (e is NetworkInvalidBalanceException) { + rethrow; + } + + await Future.delayed(delay); + delay = Duration(seconds: delay.inSeconds * 2); + } } - /// makes a jsonrpc request from this wallet - Future _requestBundler(SUJSONRPCRequest body) async { - final rawResponse = await getBundlerRPC().post( - body: body, - headers: erc4337Headers, - ); + return null; +} + +Future requestBundler( + Config config, SUJSONRPCRequest body) async { + final rawResponse = await config.engineRPC.post( + body: body, + ); + + final response = SUJSONRPCResponse.fromJson(rawResponse); + + if (response.error != null) { + throw Exception(response.error!.message); + } - final response = SUJSONRPCResponse.fromJson(rawResponse); + return response; +} - if (response.error != null) { - throw Exception(response.error!.message); +/// fetch erc20 transfer events +Future<(List, Pagination)> fetchErc20Transfers( + Config config, + String addr, { + required int offset, + required int limit, + required DateTime maxDate, +}) async { + try { + final List tx = []; + + const path = '/v1/logs'; + + final eventSignature = config.getPrimaryToken().standard == 'erc20' + ? config.token20Contract.transferEventSignature + : config.token1155Contract.transferEventSignature; + + final dataQueryParams = buildQueryParams([ + { + 'key': 'from', + 'value': addr, + }, + ], or: [ + { + 'key': 'to', + 'value': addr, + }, + ]); + + final tokenAddr = config.getPrimaryToken().standard == 'erc20' + ? config.token20Contract.addr + : config.token1155Contract.addr; + + final url = + '$path/$tokenAddr/$eventSignature?offset=$offset&limit=$limit&maxDate=${Uri.encodeComponent(maxDate.toUtc().toIso8601String())}&$dataQueryParams'; + + final response = await config.engine.get(url: url); + + // convert response array into TransferEvent list + for (final item in response['array']) { + final log = Log.fromJson(item); + + tx.add(TransferEvent.fromLog(log, + standard: config.getPrimaryToken().standard)); } - return response; + return (tx, Pagination.fromJson(response['meta'])); + } catch (e, s) { + debugPrint('error: $e'); + debugPrint('stack trace: $s'); } - /// return paymaster data for constructing a user op - Future<(PaymasterData?, Exception?)> _getPaymasterData( - UserOp userop, - String eaddr, - String ptype, { - bool legacy = false, - }) async { - final body = SUJSONRPCRequest( - method: 'pm_sponsorUserOperation', - params: [ - userop.toJson(), - eaddr, - {'type': ptype}, - ], - ); + return ([], Pagination.empty()); +} - try { - final response = await _requestPaymaster(body, legacy: legacy); +/// fetch new erc20 transfer events +Future?> fetchNewErc20Transfers( + Config config, + String addr, + DateTime fromDate, +) async { + try { + final List tx = []; - return (PaymasterData.fromJson(response.result), null); - } catch (exception) { - final strerr = exception.toString(); + const path = 'logs/v2/transfers'; - if (strerr.contains(gasFeeErrorMessage)) { - return (null, NetworkCongestedException()); - } + final tokenAddr = config.getPrimaryToken().standard == 'erc20' + ? config.token20Contract.addr + : config.token1155Contract.addr; - if (strerr.contains(invalidBalanceErrorMessage)) { - return (null, NetworkInvalidBalanceException()); - } + final url = + '/$path/$tokenAddr/$addr/new?limit=10&fromDate=${Uri.encodeComponent(fromDate.toUtc().toIso8601String())}'; + + final response = await config.engine.get(url: url); + + // convert response array into TransferEvent list + for (final item in response['array']) { + tx.add(TransferEvent.fromJson(item)); } - return (null, NetworkUnknownException()); + return tx; + } catch (e, s) { + debugPrint('error: $e'); + debugPrint('stack trace: $s'); } - /// return paymaster data for constructing a user op - Future<(List, Exception?)> _getPaymasterOOData( - UserOp userop, - String eaddr, - String ptype, { - bool legacy = false, - int count = 1, - }) async { - final body = SUJSONRPCRequest( - method: 'pm_ooSponsorUserOperation', - params: [ - userop.toJson(), - eaddr, - {'type': ptype}, - count, - ], - ); + return null; +} - try { - final response = await _requestPaymaster(body, legacy: legacy); +/// construct erc20 transfer call data +Uint8List tokenMintCallData( + Config config, + String to, + BigInt amount, { + BigInt? tokenId, +}) { + if (config.getPrimaryToken().standard == 'erc20') { + return config.token20Contract.mintCallData(to, amount); + } else if (config.getPrimaryToken().standard == 'erc1155') { + return config.token1155Contract + .mintCallData(to, amount, tokenId ?? BigInt.zero); + } - final List data = response.result; - if (data.isEmpty) { - throw Exception('empty paymaster data'); - } + return Uint8List.fromList([]); +} - if (data.length != count) { - throw Exception('invalid paymaster data'); - } +bool _needsLegacyExecution(Config config) { + const problematicCommunities = { + 'wallet.pay.brussels', + }; - return (data.map((item) => PaymasterData.fromJson(item)).toList(), null); - } catch (exception) { - final strerr = exception.toString(); + return problematicCommunities.contains(config.community.alias); +} - if (strerr.contains(gasFeeErrorMessage)) { - return ([], NetworkCongestedException()); - } +ERC4337Config? _getOldAccountConfig(Config config) { + const oldFactoryMap = { + 'wallet.pay.brussels': '0xBABCf159c4e3186cf48e4a48bC0AeC17CF9d90FE', + }; - if (strerr.contains(invalidBalanceErrorMessage)) { - return ([], NetworkInvalidBalanceException()); - } - } + final oldFactoryAddress = oldFactoryMap[config.community.alias]; + if (oldFactoryAddress == null) return null; - return ([], NetworkUnknownException()); + try { + return config.getAccountAbstractionConfig(oldFactoryAddress); + } catch (e) { + return null; } +} - /// prepare a userop for with calldata - Future<(String, UserOp)> prepareUserop( - List dest, - List calldata, { - EthPrivateKey? customCredentials, - BigInt? customNonce, - bool deploy = true, - BigInt? value, - }) async { - try { - final cred = customCredentials ?? _credentials; +/// construct simple faucet redeem call data +Future simpleFaucetRedeemCallData( + Config config, + String address, +) async { + final chain = config.chains.values.first; + final contract = SimpleFaucetContract(chain.id, config.ethClient, address); - EthereumAddress acc = _account; - if (customCredentials != null) { - acc = await getAccountAddress( - customCredentials.address.hexEip55, - ); - } - final StackupEntryPoint entryPoint = getEntryPointContract(); + await contract.init(); - // instantiate user op with default values - final userop = UserOp.defaultUserOp(); + return contract.redeemCallData(); +} - // use the account hex as the sender - userop.sender = acc.hexEip55; +/// fetch simple faucet redeem amount +Future getFaucetRedeemAmount(Config config, String address) async { + final chain = config.chains.values.first; + final contract = SimpleFaucetContract(chain.id, config.ethClient, address); - // determine the appropriate nonce - BigInt nonce = customNonce ?? await entryPoint.getNonce(acc.hexEip55); + await contract.init(); - // if it's the first user op from this account, we need to deploy the account contract - if (nonce == BigInt.zero && deploy) { - bool exists = false; - if (getPaymasterType() == 'payg') { - // solves edge case with legacy account migration - exists = await accountExists(account: acc.hexEip55); - } + return contract.getAmount(); +} - if (!exists) { - final accountFactory = getAccounFactoryContract(); - - // construct the init code to deploy the account - userop.initCode = await accountFactory.createAccountInitCode( - cred.address.hexEip55, - BigInt.zero, - ); - } else { - // try again in case the account was created in the meantime - nonce = customNonce ?? await entryPoint.getNonce(acc.hexEip55); - } - } +/// updates the push token for the current account +Future updatePushToken( + Config config, + EthereumAddress account, + EthPrivateKey credentials, + String token, { + EthPrivateKey? customCredentials, +}) async { + try { + final cred = customCredentials ?? credentials; + + EthereumAddress acc = account; + if (customCredentials != null) { + acc = await getAccountAddress(config, customCredentials.address.hexEip55); + } - userop.nonce = nonce; + final tokenAddr = config.getPrimaryToken().standard == 'erc20' + ? config.token20Contract.addr + : config.token1155Contract.addr; - // set the appropriate call data for the transfer - // we need to call account.execute which will call token.transfer - switch (getPaymasterType()) { - case 'payg': - case 'cw': - userop.callData = dest.length > 1 && calldata.length > 1 - ? _contractAccount.executeBatchCallData( - dest, - calldata, - ) - : _contractAccount.executeCallData( - dest[0], - value ?? BigInt.zero, - calldata[0], - ); - break; - case 'cw-safe': - userop.callData = _contractSafeAccount.executeCallData( - dest[0], - value ?? BigInt.zero, - calldata[0], - ); - break; - } + final url = '/v1/push/$tokenAddr/${acc.hexEip55}'; - // set the appropriate gas fees based on network - final fees = await _gasPriceEstimator.estimate; - if (fees == null) { - throw Exception('unable to estimate fees'); - } + final encoded = jsonEncode( + PushUpdateRequest(token, acc.hexEip55).toJson(), + ); - userop.maxPriorityFeePerGas = - fees.maxPriorityFeePerGas * BigInt.from(calldata.length); - userop.maxFeePerGas = fees.maxFeePerGas * BigInt.from(calldata.length); - - // submit the user op to the paymaster in order to receive information to complete the user op - List paymasterOOData = []; - Exception? paymasterErr; - final useAccountNonce = - (nonce == BigInt.zero || getPaymasterType() == 'payg') && deploy; - - if (useAccountNonce) { - // if it's the first user op, we should use a normal paymaster signature - PaymasterData? paymasterData; - (paymasterData, paymasterErr) = await _getPaymasterData( - userop, - entryPoint.addr, - getPaymasterType(), - ); + final body = SignedRequest(convertStringToUint8List(encoded)); - if (paymasterData != null) { - paymasterOOData.add(paymasterData); - } - } else { - // if it's not the first user op, we should use an out of order paymaster signature - (paymasterOOData, paymasterErr) = await _getPaymasterOOData( - userop, - entryPoint.addr, - getPaymasterType(), - ); - } + final sig = + await compute(generateSignature, (jsonEncode(body.toJson()), cred)); - if (paymasterErr != null) { - throw paymasterErr; - } + await config.engine.put( + url: url, + headers: { + 'X-Signature': sig, + 'X-Address': acc.hexEip55, + }, + body: body.toJson(), + ); - if (paymasterOOData.isEmpty) { - throw Exception('unable to get paymaster data'); - } + return true; + } catch (e, s) { + debugPrint('error: $e'); + debugPrint('stack trace: $s'); + } - final paymasterData = paymasterOOData.first; - if (!useAccountNonce) { - // use the nonce received from the paymaster - userop.nonce = paymasterData.nonce; - } + return false; +} - // add the received data to the user op - userop.paymasterAndData = paymasterData.paymasterAndData; - userop.preVerificationGas = paymasterData.preVerificationGas; - userop.verificationGasLimit = paymasterData.verificationGasLimit; - userop.callGasLimit = paymasterData.callGasLimit; +/// removes the push token for the current account +Future removePushToken( + Config config, + EthereumAddress account, + EthPrivateKey credentials, + String token, { + EthPrivateKey? customCredentials, +}) async { + try { + final cred = customCredentials ?? credentials; + + EthereumAddress acc = account; + if (customCredentials != null) { + acc = await getAccountAddress(config, customCredentials.address.hexEip55); + } - // get the hash of the user op - final hash = await entryPoint.getUserOpHash(userop); + final tokenAddr = config.getPrimaryToken().standard == 'erc20' + ? config.token20Contract.addr + : config.token1155Contract.addr; - // now we can sign the user op - userop.generateSignature(cred, hash); + final url = '/v1/push/$tokenAddr/${acc.hexEip55}/$token'; - return (bytesToHex(hash, include0x: true), userop); - } catch (_) { - rethrow; - } + final encoded = jsonEncode( + { + 'account': acc.hexEip55, + 'date': DateTime.now().toUtc().toIso8601String(), + }, + ); + + final body = SignedRequest(convertStringToUint8List(encoded)); + + final sig = + await compute(generateSignature, (jsonEncode(body.toJson()), cred)); + + await config.engine.delete( + url: url, + headers: { + 'X-Signature': sig, + 'X-Address': acc.hexEip55, + }, + body: body.toJson(), + ); + + return true; + } catch (e, s) { + debugPrint('error: $e'); + debugPrint('stack trace: $s'); } - /// submit a user op - Future submitUserop( - UserOp userop, { - EthPrivateKey? customCredentials, - Map? data, - TransferData? extraData, - }) async { - try { - final entryPoint = getEntryPointContract(); + return false; +} - // send the user op - final (txHash, useropErr) = await _submitUserOp( - userop, - entryPoint.addr, - data: data, - extraData: extraData, - ); - if (useropErr != null) { - throw useropErr; - } +/// check if an account is a minter +Future isMinter(Config config, EthereumAddress account) async { + try { + final chain = config.chains.values.first; + final tokenAddress = config.getPrimaryToken().address; - return txHash; - } catch (_) { - rethrow; - } + final accessControl = AccessControlUpgradeableContract( + chain.id, + config.ethClient, + tokenAddress, + ); + await accessControl.init(); + + return await accessControl.isMinter(account.hexEip55); + } catch (e, s) { + debugPrint('error: $e'); + debugPrint('stack trace: $s'); } - /// updates the push token for the current account - /// - /// [token] the push token - /// [customCredentials] optional credentials to use - Future updatePushToken( - String token, { - EthPrivateKey? customCredentials, - }) async { - try { - final cred = customCredentials ?? _credentials; + return false; +} - EthereumAddress acc = _account; - if (customCredentials != null) { - acc = await getAccountAddress( - customCredentials.address.hexEip55, - ); - } +/// dispose of resources +void disposeWallet(Config config) { + config.ethClient.dispose(); +} - final addr = _tokenStandard == 'erc20' - ? _contractToken!.addr - : _contract1155Token!.addr; +/// get sigauth connection +SigAuthConnection getSigAuthConnection( + Config config, + EthereumAddress account, + EthPrivateKey credentials, + String redirect, +) { + final _sigAuth = SigAuthService( + credentials: credentials, + address: account, + redirect: redirect, + ); + + return _sigAuth.connect(); +} - final url = '/v1/push/$addr/${acc.hexEip55}'; +/// get account address +Future getAccountAddress( + Config config, + String addr, { + bool legacy = false, + bool cache = true, +}) async { + final accountFactory = config.accountFactoryContract; + return await accountFactory.getAddress(addr); +} - final encoded = jsonEncode( - PushUpdateRequest(token, acc.hexEip55).toJson(), - ); +/// upgrade an account +Future upgradeAccount( + Config config, + EthereumAddress account, + EthPrivateKey credentials, +) async { + try { + final accountFactory = config.accountFactoryContract; + + final url = + '/accounts/factory/${accountFactory.addr}/sca/${account.hexEip55}'; + + final encoded = jsonEncode( + { + 'owner': credentials.address.hexEip55, + 'salt': BigInt.zero.toInt(), + }, + ); - final body = SignedRequest(convertStringToUint8List(encoded)); + final body = SignedRequest(convertStringToUint8List(encoded)); - final sig = - await compute(generateSignature, (jsonEncode(body.toJson()), cred)); + final sig = await compute( + generateSignature, (jsonEncode(body.toJson()), credentials)); - await _indexer.put( - url: url, - headers: { - 'X-Signature': sig, - 'X-Address': acc.hexEip55, - }, - body: body.toJson(), - ); + final response = await config.engine.patch( + url: url, + headers: { + 'X-Signature': sig, + 'X-Address': account.hexEip55, + }, + body: body.toJson(), + ); - return true; - } catch (_) {} + final implementation = response['object']['account_implementation']; + if (implementation == "0x0000000000000000000000000000000000000000") { + throw Exception('invalid implementation'); + } - return false; + return implementation; + } catch (e, s) { + debugPrint('error: $e'); + debugPrint('stack trace: $s'); } - /// removes the push token for the current account - /// - /// [token] the push token - /// [customCredentials] optional credentials to use - Future removePushToken( - String token, { - EthPrivateKey? customCredentials, - }) async { - try { - final cred = customCredentials ?? _credentials; + return null; +} - EthereumAddress acc = _account; - if (customCredentials != null) { - acc = await getAccountAddress( - customCredentials.address.hexEip55, - ); - } +/// get card hash +Future getCardHash( + Config config, + String serial, { + bool local = true, +}) async { + final primaryCardManager = config.getPrimaryCardManager(); - final addr = _tokenStandard == 'erc20' - ? _contractToken!.addr - : _contract1155Token!.addr; + if (primaryCardManager == null) { + throw Exception('Card manager not initialized'); + } - final url = '/v1/push/$addr/${acc.hexEip55}/$token'; + AbstractCardManagerContract cardManager; - final encoded = jsonEncode( - { - 'account': acc.hexEip55, - 'date': DateTime.now().toUtc().toIso8601String(), - }, - ); + if (primaryCardManager.type == CardManagerType.classic) { + cardManager = CardManagerContract( + primaryCardManager.chainId, + config.ethClient, + primaryCardManager.address, + ); + } else if (primaryCardManager.type == CardManagerType.safe && + primaryCardManager.instanceId != null) { + final instanceId = primaryCardManager.instanceId!; + cardManager = SafeCardManagerContract( + keccak256(convertStringToUint8List(instanceId)), + primaryCardManager.chainId, + config.ethClient, + primaryCardManager.address, + ); + } else { + throw Exception('Invalid card manager configuration'); + } - final body = SignedRequest(convertStringToUint8List(encoded)); + await cardManager.init(); + return await cardManager.getCardHash(serial, local: local); +} - final sig = - await compute(generateSignature, (jsonEncode(body.toJson()), cred)); +/// get card address +Future getCardAddress( + Config config, + Uint8List hash, +) async { + final primaryCardManager = config.getPrimaryCardManager(); - await _indexer.delete( - url: url, - headers: { - 'X-Signature': sig, - 'X-Address': acc.hexEip55, - }, - body: body.toJson(), - ); + if (primaryCardManager == null) { + throw Exception('Card manager not initialized'); + } - return true; - } catch (_) {} + AbstractCardManagerContract cardManager; - return false; + if (primaryCardManager.type == CardManagerType.classic) { + cardManager = CardManagerContract( + primaryCardManager.chainId, + config.ethClient, + primaryCardManager.address, + ); + } else if (primaryCardManager.type == CardManagerType.safe && + primaryCardManager.instanceId != null) { + final instanceId = primaryCardManager.instanceId!; + cardManager = SafeCardManagerContract( + keccak256(convertStringToUint8List(instanceId)), + primaryCardManager.chainId, + config.ethClient, + primaryCardManager.address, + ); + } else { + throw Exception('Invalid card manager configuration'); } - /// dispose of resources - void dispose() { - _ethClient.dispose(); + await cardManager.init(); + return await cardManager.getCardAddress(hash); +} + +/// estimate gas prices using EIP1559 +Future estimateGasPrice( + Config config, +) async { + try { + final chain = config.chains.values.first; + final rpcUrl = config.getRpcUrl(chain.id.toString()); + final rpc = APIService(baseURL: rpcUrl); + + final estimator = EIP1559GasPriceEstimator( + rpc, + config.ethClient, + gasExtraPercentage: + config.getPrimaryAccountAbstractionConfig().gasExtraPercentage, + ); + + return await estimator.estimate; + } catch (e, s) { + debugPrint('error: $e'); + debugPrint('stack trace: $s'); } + + return null; } + +// Future getTwoFAAddress( +// Config config, +// String source, +// String type, +// ) async { +// final provider = EthereumAddress.fromHex( +// config.getPrimarySessionManager().providerAddress); +// final salt = generateSessionSalt(source, type); +// return await config.twoFAFactoryContract.getAddress(provider, salt); +// } diff --git a/lib/services/wallet_connect/wallet_kit.dart b/lib/services/wallet_connect/wallet_kit.dart index 4e4381ee..bc050777 100644 --- a/lib/services/wallet_connect/wallet_kit.dart +++ b/lib/services/wallet_connect/wallet_kit.dart @@ -11,6 +11,7 @@ import 'package:http/http.dart' as http; import 'package:reown_walletkit/reown_walletkit.dart'; import 'package:web3dart/crypto.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:citizenwallet/services/config/config.dart'; final List supportedChains = [ 'eip155:100', @@ -254,8 +255,8 @@ class WalletKitService { _connectClient!.onSessionRequest.subscribe(callback); } - Future personalSignHandler( - String topic, dynamic params, bool? approve) async { + Future personalSignHandler(String topic, dynamic params, bool? approve, + EthPrivateKey? credentials) async { if (_connectClient == null) throw Exception('WalletKit not initialized'); final SessionRequest request = @@ -265,11 +266,7 @@ class WalletKitService { final decoded = hex.decode(params.first.substring(2)); final message = utf8.decode(decoded); - if (approve == true) { - final walletService = WalletService(); - - final credentials = walletService.credentials; - + if (approve == true && credentials != null) { final signature = bytesToHex( credentials.signPersonalMessageToUint8List( keccak256(utf8.encode(message)), @@ -299,7 +296,12 @@ class WalletKitService { } Future ethSendTransactionHandler( - String topic, dynamic params, bool approve) async { + String topic, + dynamic params, + bool approve, + Config? config, + EthereumAddress? account, + EthPrivateKey? credentials) async { if (_connectClient == null) throw Exception('WalletKit not initialized'); final SessionRequest request = @@ -309,9 +311,10 @@ class WalletKitService { final transaction = (params as List).first as Map; - if (approve == true) { - final walletService = WalletService(); - + if (approve == true && + config != null && + account != null && + credentials != null) { final data = transaction['data'] != null ? hexToBytes(transaction['data']) : Uint8List(0); @@ -328,19 +331,23 @@ class WalletKitService { value = BigInt.zero; } - final (hash, userop) = await walletService.prepareUserop( + final (hash, userop) = await prepareUserop( + config, + account, + credentials, [transaction['to']], [data], value: value, + accountFactoryAddress: config.community.primaryAccountFactory.address, ); - final txHash = await walletService.submitUserop( + final txHash = await submitUserop( + config, userop, - data: {'data': transaction['data']}, ); - if (userop.isFirst()) { - await walletService.waitForTxSuccess(txHash!).then((value) {}); + if (txHash != null && userop.isFirst()) { + await waitForTxSuccess(config, txHash).then((value) {}); } return _connectClient!.respondSessionRequest( diff --git a/lib/state/app/logic.dart b/lib/state/app/logic.dart index 7ece074d..d19a129a 100644 --- a/lib/state/app/logic.dart +++ b/lib/state/app/logic.dart @@ -1,11 +1,18 @@ import 'dart:convert'; import 'dart:math'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:provider/provider.dart'; +import 'package:web3dart/web3dart.dart'; import 'package:citizenwallet/models/wallet.dart'; +import 'package:citizenwallet/services/accounts/accounts.dart'; +import 'package:citizenwallet/services/accounts/native/apple.dart'; +import 'package:citizenwallet/services/api/api.dart'; import 'package:citizenwallet/services/audio/audio.dart'; import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/config/service.dart'; -import 'package:citizenwallet/services/accounts/accounts.dart'; import 'package:citizenwallet/services/db/app/db.dart'; import 'package:citizenwallet/services/db/backup/accounts.dart'; import 'package:citizenwallet/services/preferences/preferences.dart'; @@ -15,11 +22,6 @@ import 'package:citizenwallet/state/app/state.dart'; import 'package:citizenwallet/state/theme/logic.dart'; import 'package:citizenwallet/utils/delay.dart'; import 'package:citizenwallet/utils/uint8.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:provider/provider.dart'; -import 'package:web3dart/web3dart.dart'; class AppLogic { final ThemeLogic _theme = ThemeLogic(); @@ -55,6 +57,20 @@ class AppLogic { _appState.appLoaded(); } + Future checkMigrationRequired() async { + try { + _appState.migrationCheckRequest(); + + final apiService = APIService(baseURL: dotenv.get('DASHBOARD_API')); + final migrationRequired = await apiService.checkMigrationRequired(); + + _appState.migrationCheckSuccess(migrationRequired); + } catch (e) { + debugPrint('Migration check error: $e'); + _appState.migrationCheckFailed(); + } + } + void setFirstLaunch(bool firstLaunch) { try { _preferences.setFirstLaunch(firstLaunch); @@ -64,62 +80,94 @@ class AppLogic { } Future<(String?, String?)> loadLastWallet() async { - try { - _appState.importLoadingReq(); - final String? lastWallet = _preferences.lastWallet; - final String? lastAlias = _preferences.lastAlias; + return _loadLastWalletWithRetry(); + } - DBAccount? dbWallet; - if (lastWallet != null && lastAlias != null) { - dbWallet = await _accounts.getAccount(lastWallet, lastAlias); - } + Future<(String?, String?)> _loadLastWalletWithRetry( + {int maxRetries = 3}) async { + for (int attempt = 0; attempt < maxRetries; attempt++) { + try { + _appState.importLoadingReq(); + final String? lastWallet = _preferences.lastWallet; + final String? lastAlias = _preferences.lastAlias; + final String? lastAccountFactoryAddress = + _preferences.lastAccountFactoryAddress; + + DBAccount? dbWallet; + if (lastWallet != null && + lastAlias != null && + lastAccountFactoryAddress != null) { + dbWallet = await _accounts.getAccount( + lastWallet, lastAlias, lastAccountFactoryAddress); + } - if (dbWallet == null) { - // attempt to see if there are any other wallets backed up - final dbWallets = await _accounts.getAllAccounts(); + if (dbWallet == null) { + final dbWallets = await _accounts.getAllAccounts(); - if (dbWallets.isNotEmpty) { - final dbWallet = dbWallets[0]; + if (dbWallets.isNotEmpty) { + final dbWallet = dbWallets[0]; + final address = dbWallet.address.hexEip55; - final address = dbWallet.address.hexEip55; + final community = + await _appDBService.communities.get(dbWallet.alias); - // final config = await _config.getConfig(dbWallet.alias); + if (community == null) { + throw Exception('community not found'); + } - final community = await _appDBService.communities.get(dbWallet.alias); + Config communityConfig = Config.fromJson(community.config); - if (community == null) { - throw Exception('community not found'); - } + _theme.changeTheme(communityConfig.community.theme); - Config communityConfig = Config.fromJson(community.config); + await _preferences.setLastWallet(address); + await _preferences.setLastAlias(dbWallet.alias); + await _preferences + .setLastAccountFactoryAddress(dbWallet.accountFactoryAddress); - _theme.changeTheme(communityConfig.community.theme); + _appState.importLoadingSuccess(); - await _preferences.setLastWallet(address); - await _preferences.setLastAlias(dbWallet.alias); + return (address, dbWallet.alias); + } - _appState.importLoadingSuccess(); + // If this is not the last attempt and no accounts found, try recovery and retry + if (attempt < maxRetries - 1) { + // Try to restore missing accounts from keychain on each retry + try { + final accountsService = getAccountsService(); + if (accountsService is AppleAccountsService) { + await accountsService.restoreAccountsFromKeychain(); + } + } catch (e) { + debugPrint('Error during account restoration: $e'); + } + + await delay(Duration( + milliseconds: 1000 + (attempt * 500))); // Progressive delay + continue; + } - return (address, dbWallet.alias); + _appState.importLoadingError(); + return (null, null); } - _appState.importLoadingError(); - - return (null, null); - } + await delay( + const Duration(milliseconds: 1500)); // smoother launch experience - await delay( - const Duration(milliseconds: 1500)); // smoother launch experience + _appState.importLoadingSuccess(); - _appState.importLoadingSuccess(); + final address = dbWallet.address.hexEip55; - final address = dbWallet.address.hexEip55; - - return (address, dbWallet.alias); - } catch (_) {} + return (address, dbWallet.alias); + } catch (e) { + if (attempt < maxRetries - 1) { + await delay(Duration( + milliseconds: 1000 + (attempt * 500))); // Progressive delay + continue; + } + } + } _appState.importLoadingError(); - return (null, null); } @@ -193,12 +241,16 @@ class AppLogic { privateKey: credentials, name: token.name, alias: communityConfig.community.alias, + accountFactoryAddress: + communityConfig.community.primaryAccountFactory.address, )); _theme.changeTheme(communityConfig.community.theme); await _preferences.setLastWallet(address.hexEip55); await _preferences.setLastAlias(communityConfig.community.alias); + await _preferences.setLastAccountFactoryAddress( + communityConfig.community.primaryAccountFactory.address); _appState.importLoadingSuccess(); @@ -308,6 +360,8 @@ class AppLogic { privateKey: credentials, name: name, alias: communityConfig.community.alias, + accountFactoryAddress: + communityConfig.community.primaryAccountFactory.address, ), ); @@ -363,7 +417,7 @@ class AppLogic { final address = EthereumAddress.fromHex(decodedSplit[0]); final existing = await _accounts.getAccount( - address.hexEip55, communityConfig.community.alias); + address.hexEip55, communityConfig.community.alias, ''); if (existing != null) { return (existing.address.hexEip55, alias); } @@ -374,6 +428,8 @@ class AppLogic { privateKey: credentials, name: '${token.symbol} Web Account', alias: communityConfig.community.alias, + accountFactoryAddress: + communityConfig.community.primaryAccountFactory.address, ), ); @@ -381,6 +437,8 @@ class AppLogic { await _preferences.setLastWallet(address.hexEip55); await _preferences.setLastAlias(communityConfig.community.alias); + await _preferences.setLastAccountFactoryAddress( + communityConfig.community.primaryAccountFactory.address); _appState.importLoadingSuccess(); diff --git a/lib/state/app/state.dart b/lib/state/app/state.dart index 2ec8c65e..733a9def 100644 --- a/lib/state/app/state.dart +++ b/lib/state/app/state.dart @@ -37,6 +37,10 @@ class AppState with ChangeNotifier { bool muted = false; + bool migrationRequired = false; + bool migrationCheckLoading = false; + bool migrationCheckError = false; + AppState() { muted = PreferencesService().muted; @@ -174,4 +178,24 @@ class AppState with ChangeNotifier { singleCommunityMode = ConfigService().singleCommunityMode; notifyListeners(); } + + void migrationCheckRequest() { + migrationCheckLoading = true; + migrationCheckError = false; + notifyListeners(); + } + + void migrationCheckSuccess(bool required) { + migrationRequired = required; + migrationCheckLoading = false; + migrationCheckError = false; + notifyListeners(); + } + + void migrationCheckFailed() { + migrationRequired = false; + migrationCheckLoading = false; + migrationCheckError = true; + notifyListeners(); + } } diff --git a/lib/state/backup/logic.dart b/lib/state/backup/logic.dart index b10ab213..1c69c6c5 100644 --- a/lib/state/backup/logic.dart +++ b/lib/state/backup/logic.dart @@ -54,7 +54,11 @@ class BackupLogic { accountsDB: AccountBackupDBService(), ), ); - } catch (_) {} + await delay(const Duration(milliseconds: 500)); + + } catch (e) { + debugPrint('Error setting up Apple keychain: $e'); + } } Future hasAccounts() async { @@ -201,6 +205,8 @@ class BackupLogic { // set up the first wallet as the default, this will allow the app to start normally _preferences.setLastAlias(accounts.first.alias); _preferences.setLastWallet(accounts.first.address.hexEip55); + _preferences + .setLastAccountFactoryAddress(accounts.first.accountFactoryAddress); _state.decryptSuccess(backupTime, username); @@ -290,6 +296,8 @@ class BackupLogic { // set up the first wallet as the default, this will allow the app to start normally _preferences.setLastAlias(accounts.first.alias); _preferences.setLastWallet(accounts.first.address.hexEip55); + _preferences + .setLastAccountFactoryAddress(accounts.first.accountFactoryAddress); } on BackupNotFoundException { _state.setStatus(BackupStatus.nobackup); _state.backupError(); diff --git a/lib/state/communities/logic.dart b/lib/state/communities/logic.dart index 502b677b..5cea1ebe 100644 --- a/lib/state/communities/logic.dart +++ b/lib/state/communities/logic.dart @@ -101,13 +101,13 @@ class CommunitiesLogic { _state.fetchCommunitiesRequest(); final communities = await _db.communities.getAll(); - List communityConfigs = + final communityConfigs = communities.map((c) => Config.fromJson(c.config)).toList(); _state.fetchCommunitiesSuccess(communityConfigs); return; } catch (e) { - // + debugPrint('Error fetching communities: $e'); } _state.fetchCommunitiesFailure(); @@ -123,19 +123,58 @@ class CommunitiesLogic { void _fetchAllCommunitiesFromRemote() async { try { - final List communities = await config.getCommunitiesFromRemote(); - await _db.communities - .upsert(communities.map((c) => DBCommunity.fromConfig(c)).toList()); - return; + final remoteCommunities = await config.getCommunitiesFromRemote(); + + if (remoteCommunities.isNotEmpty) { + await _db.communities.upsert(remoteCommunities + .map((config) => DBCommunity.fromConfig(config)) + .toList()); + _state.upsertCommunities(remoteCommunities); + } } catch (e) { - // + debugPrint('Error fetching communities from remote API: $e'); + } + + try { + final communities = await _db.communities.getAll(); + for (final community in communities) { + try { + final config = Config.fromJson(community.config); + if (config.community.hidden) { + continue; + } + + final token = config.getPrimaryToken(); + final chain = config.chains[token.chainId.toString()]; + + if (chain != null) { + try { + final isOnline = + await this.config.isCommunityOnline(chain.node.url); + await _db.communities + .updateOnlineStatus(config.community.alias, isOnline); + _state.setCommunityOnline(config.community.alias, isOnline); + } catch (e) { + debugPrint( + 'Error checking online status for ${config.community.alias}: $e'); + await _db.communities + .updateOnlineStatus(config.community.alias, false); + _state.setCommunityOnline(config.community.alias, false); + } + } + } catch (e) { + debugPrint('Error processing community ${community.alias}: $e'); + } + } + } catch (e) { + debugPrint('Error checking community online status: $e'); } } void _fetchSingleCommunityFromRemote() async { try { final communities = await _db.communities.getAll(); - List communityConfigs = + final communityConfigs = communities.map((c) => Config.fromJson(c.config)).toList(); if (communityConfigs.isEmpty) { @@ -144,59 +183,61 @@ class CommunitiesLogic { final first = communityConfigs.first; - final remoteCommunity = - await config.getRemoteConfig(first.configLocation); + try { + final remoteCommunity = + await config.getRemoteConfig(first.configLocation); - if (remoteCommunity == null) { - return; + if (remoteCommunity != null) { + await _db.communities + .upsert([DBCommunity.fromConfig(remoteCommunity)]); + _state.upsertCommunities([remoteCommunity]); + } + } catch (e) { + debugPrint( + 'Error fetching remote config for ${first.community.alias}: $e'); } - - await _db.communities.upsert([DBCommunity.fromConfig(remoteCommunity)]); - return; } catch (e) { - // + debugPrint('Error in single community remote fetch: $e'); } } Future isAliasFromDeeplinkExist(String alias) async { - bool communityExists = await _db.communities.exists(alias); - if (communityExists) { - return true; - } + return await _db.communities.exists(alias); + } - for (int attempt = 0; attempt < 2; attempt++) { - final List communities = await config.getCommunitiesFromRemote(); + Future initializeAppDB() async { + try { + await _db.init('app'); + } catch (e) { + // + } + } - for (final community in communities) { - if (community.community.alias != alias) { - continue; - } + Future fetchAndUpdateSingleCommunity(String alias) async { + try { + final community = await _db.communities.get(alias); + if (community == null) { + return; + } - final token = community.getPrimaryToken(); - final chain = community.chains[token.chainId.toString()]; + final config = Config.fromJson(community.config); + final remoteConfig = + await this.config.getSingleCommunityConfig(config.configLocation); - final isOnline = await config.isCommunityOnline(chain!.node.url); + if (remoteConfig != null) { + await _db.communities.upsert([DBCommunity.fromConfig(remoteConfig)]); - await _db.communities.upsert([DBCommunity.fromConfig(community)]); - await _db.communities - .updateOnlineStatus(community.community.alias, isOnline); - } + final existingIndex = _state.communities.indexWhere( + (c) => c.community.alias == alias, + ); - // Check again if the community exists after the update - communityExists = await _db.communities.exists(alias); - if (communityExists) { - return true; + if (existingIndex != -1) { + remoteConfig.online = _state.communities[existingIndex].online; + _state.upsertCommunities([remoteConfig]); + } } - } - - return communityExists; - } - - Future initializeAppDB() async { - try { - await _db.init('app'); } catch (e) { - // + debugPrint('Error fetching single community: $e'); } } } diff --git a/lib/state/deep_link/logic.dart b/lib/state/deep_link/logic.dart index 9233834c..6ab6ad44 100644 --- a/lib/state/deep_link/logic.dart +++ b/lib/state/deep_link/logic.dart @@ -1,21 +1,38 @@ +import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/wallet/wallet.dart'; import 'package:citizenwallet/state/deep_link/state.dart'; import 'package:citizenwallet/state/notifications/logic.dart'; import 'package:flutter/cupertino.dart'; import 'package:provider/provider.dart'; +import 'package:web3dart/web3dart.dart'; class DeepLinkLogic { final DeepLinkState _state; - final WalletService _wallet; final NotificationsLogic _notifications; + late Config _config; + late EthPrivateKey _credentials; + late EthereumAddress _account; - DeepLinkLogic(BuildContext context, WalletService wallet) + DeepLinkLogic(BuildContext context, Config config, EthPrivateKey credentials, EthereumAddress account) : _state = context.read(), - _wallet = wallet, - _notifications = NotificationsLogic(context); + _notifications = NotificationsLogic(context) { + _config = config; + _credentials = credentials; + _account = account; + } + + void setWalletState(Config config, EthPrivateKey credentials, EthereumAddress account) { + _config = config; + _credentials = credentials; + _account = account; + } Future faucetV1Redeem(String params) async { try { + if (_config == null || _credentials == null || _account == null) { + throw Exception('Wallet not initialized'); + } + _state.request(); final uri = Uri(query: params); @@ -25,16 +42,23 @@ class DeepLinkLogic { throw Exception('Address is required'); } - final calldata = await _wallet.simpleFaucetRedeemCallData(address); + final calldata = await simpleFaucetRedeemCallData(_config, address); - final (_, userop) = await _wallet.prepareUserop([address], [calldata]); + final (_, userop) = await prepareUserop( + _config, + _account, + _credentials, + [address], + [calldata], + accountFactoryAddress: _config.community.primaryAccountFactory.address, + ); - final txHash = await _wallet.submitUserop(userop); + final txHash = await submitUserop(_config, userop); if (txHash == null) { throw Exception('transaction failed'); } - final success = await _wallet.waitForTxSuccess(txHash); + final success = await waitForTxSuccess(_config, txHash); if (!success) { throw Exception('transaction failed'); } @@ -49,6 +73,10 @@ class DeepLinkLogic { Future faucetV1Metadata(String params) async { try { + if (_config == null) { + throw Exception('Wallet not initialized'); + } + _state.request(); final uri = Uri(query: params); @@ -58,7 +86,7 @@ class DeepLinkLogic { throw Exception('Address is required'); } - final amount = await _wallet.getFaucetRedeemAmount(address); + final amount = await getFaucetRedeemAmount(_config, address); _state.setFaucetAmount(amount); _state.success(); diff --git a/lib/state/notifications/logic.dart b/lib/state/notifications/logic.dart index f0c2041b..b14d82fd 100644 --- a/lib/state/notifications/logic.dart +++ b/lib/state/notifications/logic.dart @@ -1,4 +1,5 @@ import 'package:citizenwallet/services/audio/audio.dart'; +import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/preferences/preferences.dart'; import 'package:citizenwallet/services/push/push.dart'; import 'package:citizenwallet/services/wallet/wallet.dart'; @@ -6,6 +7,7 @@ import 'package:citizenwallet/state/notifications/state.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; import 'package:provider/provider.dart'; +import 'package:web3dart/web3dart.dart'; class NotificationsLogic { final NotificationsState _state; @@ -13,15 +15,25 @@ class NotificationsLogic { final PreferencesService _prefs = PreferencesService(); final PushService _push = PushService(); final AudioService _audio = AudioService(); - final WalletService _wallet = WalletService(); + late Config _config; + late EthPrivateKey _credentials; + late EthereumAddress _account; NotificationsLogic(BuildContext context) : _state = context.read(); + void setWalletState(Config config, EthPrivateKey credentials, EthereumAddress account) { + _config = config; + _credentials = credentials; + _account = account; + } + void init() async { try { + if (_account == null) return; + final systemEnabled = await _push.isEnabled(); - bool enabled = _prefs.pushNotifications(_wallet.account.hexEip55); + bool enabled = _prefs.pushNotifications(_account.hexEip55); if (!systemEnabled) { final allowed = await _push.requestPermissions(); @@ -39,7 +51,7 @@ class NotificationsLogic { // enable push _state.setPush(true); await _push.start(onToken, onMessage); - _prefs.setPushNotifications(_wallet.account.hexEip55, true); + _prefs.setPushNotifications(_account.hexEip55, true); } catch (e) { // } @@ -47,8 +59,10 @@ class NotificationsLogic { void checkPushPermissions() async { try { + if (_account == null) return; + final systemEnabled = await _push.isEnabled(); - final enabled = _prefs.pushNotifications(_wallet.account.hexEip55); + final enabled = _prefs.pushNotifications(_account.hexEip55); _state.setPush(systemEnabled && enabled); } catch (e) { @@ -90,7 +104,14 @@ class NotificationsLogic { Future onToken(String token) async { try { - final updated = await _wallet.updatePushToken(token); + if (_config == null || _credentials == null || _account == null) return; + + final updated = await updatePushToken( + _config, + _account, + _credentials, + token, + ); if (!updated) { throw Exception('Failed to update push token'); } @@ -99,14 +120,21 @@ class NotificationsLogic { } } - Future updatePushToken() async { + Future refreshPushToken() async { try { + if (_config == null || _credentials == null || _account == null) return; + final token = await _push.token; if (token == null) { return; } - final updated = await _wallet.updatePushToken(token); + final updated = await updatePushToken( + _config, + _account, + _credentials, + token, + ); if (!updated) { throw Exception('Failed to update push token'); } @@ -117,21 +145,30 @@ class NotificationsLogic { Future togglePushNotifications() async { try { + if (_account == null) return; + final systemEnabled = await _push.isEnabled(); - final enabled = _prefs.pushNotifications(_wallet.account.hexEip55); + final enabled = _prefs.pushNotifications(_account.hexEip55); if (systemEnabled && enabled) { // disable push _state.setPush(false); await _push.stop(); - _prefs.setPushNotifications(_wallet.account.hexEip55, false); + _prefs.setPushNotifications(_account.hexEip55, false); final token = await _push.token; if (token == null) { return; } - final updated = await _wallet.removePushToken(token); + if (_config == null || _credentials == null) return; + + final updated = await removePushToken( + _config, + _account, + _credentials, + token, + ); if (!updated) { throw Exception('Failed to update push token'); } @@ -148,7 +185,7 @@ class NotificationsLogic { // enable push _state.setPush(true); await _push.start(onToken, onMessage); - _prefs.setPushNotifications(_wallet.account.hexEip55, true); + _prefs.setPushNotifications(_account.hexEip55, true); } catch (e) { // } diff --git a/lib/state/profile/logic.dart b/lib/state/profile/logic.dart index 6c53f64a..5e1ff11d 100644 --- a/lib/state/profile/logic.dart +++ b/lib/state/profile/logic.dart @@ -1,3 +1,4 @@ +import 'package:citizenwallet/services/accounts/accounts.dart'; import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/db/account/contacts.dart'; import 'package:citizenwallet/services/db/account/db.dart'; @@ -10,6 +11,7 @@ import 'package:citizenwallet/services/wallet/utils.dart'; import 'package:citizenwallet/services/wallet/wallet.dart'; import 'package:citizenwallet/state/profile/state.dart'; import 'package:citizenwallet/state/profiles/state.dart'; +import 'package:citizenwallet/state/wallet/state.dart'; import 'package:citizenwallet/utils/delay.dart'; import 'package:citizenwallet/utils/formatters.dart'; import 'package:citizenwallet/utils/random.dart'; @@ -30,17 +32,53 @@ class ProfileLogic { late ProfileState _state; late ProfilesState _profiles; final PhotosService _photos = PhotosService(); + final AccountsServiceInterface _accountsService = getAccountsService(); final AccountDBService _db = AccountDBService(); - final WalletService _wallet = WalletService(); - + late BuildContext _context; bool _pauseProfileCreation = false; ProfileLogic(BuildContext context) { + _context = context; _state = context.read(); _profiles = context.read(); } + Config? get _config => _context.read().config; + String? get _walletAccount => _context.read().wallet?.account; + String? get _walletAlias => _context.read().wallet?.alias; + + Future get _credentials async { + final account = _walletAccount; + final alias = _walletAlias; + if (account == null || alias == null) return null; + + final dbAccount = await _accountsService.getAccount(account, alias, ''); + return dbAccount?.privateKey; + } + + EthereumAddress? get _account { + final account = _walletAccount; + return account != null ? EthereumAddress.fromHex(account) : null; + } + + Future< + ({ + Config? config, + EthPrivateKey? credentials, + EthereumAddress? account + })?> get _walletData async { + final config = _config; + final credentials = await _credentials; + final account = _account; + + if (config == null || credentials == null || account == null) { + return null; + } + + return (config: config, credentials: credentials, account: account); + } + void resetAll() { _state.resetAll(); } @@ -77,11 +115,11 @@ class ProfileLogic { try { _state.setProfileLinkRequest(); - if (_wallet.alias == null) { - throw Exception('alias not found'); - } + final walletData = await _walletData; + if (walletData == null) return; - final community = await _appDBService.communities.get(_wallet.alias!); + final community = await _appDBService.communities + .get(walletData.config!.community.alias); if (community == null) { throw Exception('community not found'); @@ -92,12 +130,13 @@ class ProfileLogic { final url = communityConfig.community.walletUrl(deepLinkURL); final compressedParams = compress( - '?address=${_wallet.account.hexEip55}&alias=${communityConfig.community.alias}'); + '?address=${walletData.account!.hexEip55}&alias=${communityConfig.community.alias}'); _state.setProfileLinkSuccess('$url&receiveParams=$compressedParams'); return; } catch (e) { - // + // Add logging to help debug future issues + debugPrint('Error loading profile link: $e'); } _state.setProfileLinkError(); @@ -122,34 +161,70 @@ class ProfileLogic { } Future checkUsername(String username) async { + final walletData = await _walletData; + if (walletData == null) { + _state.setUsernameError(); + return; + } + if (username == '') { _state.setUsernameError(); } - if (username == _state.username) { + if (username.length < 3) { + _state.setUsernameError( + message: 'Username must be at least 3 characters long.'); + return; + } + + final usernameRegex = RegExp(r'^[a-zA-Z0-9_-]+$'); + if (!usernameRegex.hasMatch(username)) { + _state.setUsernameError( + message: + 'Username can only contain letters, numbers, underscores, and hyphens.'); + return; + } + + if (username.toLowerCase() == _state.username.toLowerCase()) { + _state.setUsernameSuccess(); return; } try { _state.setUsernameRequest(); - final exists = await _wallet.profileExists(username); + final exists = + await profileExists(walletData.config!, username.toLowerCase()); if (exists) { + final existingProfile = await getProfileByUsername( + walletData.config!, username.toLowerCase()); + if (existingProfile != null && + existingProfile.account == walletData.account!.hexEip55) { + _state.setUsernameSuccess(); + return; + } throw Exception('Already exists'); } _state.setUsernameSuccess(); return; } catch (exception) { - // + debugPrint('Username check error: $exception'); + if (exception.toString().contains('Already exists')) { + _state.setUsernameError(message: 'This username is already taken.'); + } else { + _state.setUsernameError( + message: 'Unable to check username availability.'); + } } - - _state.setUsernameError(); } Future loadProfile({String? account, bool online = false}) async { - final ethAccount = _wallet.account; - final alias = _wallet.alias ?? ''; + final walletData = await _walletData; + if (walletData == null) return; + + final ethAccount = walletData.account!; + final alias = walletData.config!.community.alias; final acc = account ?? ethAccount.hexEip55; resume(); @@ -158,7 +233,7 @@ class ProfileLogic { _state.setProfileRequest(); final account = - await _accountBackupDBService.accounts.get(ethAccount, alias); + await _accountBackupDBService.accounts.get(ethAccount, alias, ''); if (account != null && account.profile != null) { final profile = account.profile!; @@ -182,10 +257,13 @@ class ProfileLogic { throw Exception('community is offline'); } - final profile = await _wallet.getProfile(acc); + final profile = await getProfile(walletData.config!, acc); if (profile == null) { _state.setProfileNoChangeSuccess(); - giveProfileUsername(); + + if (_state.username.isEmpty) { + giveProfileUsername(); + } return; } @@ -207,11 +285,16 @@ class ProfileLogic { profile, ); + // Get the existing account to preserve the account factory address + final existingAccount = + await _accountBackupDBService.accounts.get(ethAccount, alias, ''); + _accountBackupDBService.accounts.update(DBAccount( alias: alias, address: ethAccount, name: profile.name, username: profile.username, + accountFactoryAddress: existingAccount?.accountFactoryAddress ?? '', privateKey: null, profile: profile, )); @@ -236,7 +319,10 @@ class ProfileLogic { _state.viewProfileSuccess(cachedProfile!.profile); } - final profile = await _wallet.getProfile(account); + final config = _config; + if (config == null) return; + + final profile = await getProfile(config, account); if (profile == null) { await delay(const Duration(milliseconds: 500)); _state.setViewProfileNoChangeSuccess(); @@ -260,7 +346,16 @@ class ProfileLogic { _state.viewProfileError(); } + /// Save a new profile with optional image Future save(ProfileV1 profile, Uint8List? image) async { + final config = _config; + final credentials = await _credentials; + final account = _account; + + if (config == null || credentials == null || account == null) { + return false; + } + try { _state.setProfileRequest(); @@ -270,7 +365,7 @@ class ProfileLogic { profile.name = _state.nameController.value.text; profile.description = _state.descriptionController.value.text; - final exists = await _wallet.createAccount(); + final exists = await createAccount(config, account, credentials); if (!exists) { throw Exception('Failed to create account'); } @@ -281,10 +376,17 @@ class ProfileLogic { ? convertBytesToUint8List(image) : await _photos.photoFromBundle('assets/icons/profile.jpg'); - final url = await _wallet.setProfile( + final accountForFactory = await _accountBackupDBService.accounts + .get(account, config.community.alias, ''); + + final url = await setProfile( + config, + account, + credentials, ProfileRequest.fromProfileV1(profile), image: newImage, fileType: '.jpg', + accountFactoryAddress: accountForFactory?.accountFactoryAddress, ); if (url == null) { throw Exception('Failed to save profile'); @@ -292,7 +394,7 @@ class ProfileLogic { _state.setProfileFetching(); - final newProfile = await _wallet.getProfileFromUrl(url); + final newProfile = await getProfileFromUrl(config, url); if (newProfile == null) { throw Exception('Failed to load profile'); } @@ -321,12 +423,18 @@ class ProfileLogic { ), ); + final existingAccount = await _accountBackupDBService.accounts.get( + EthereumAddress.fromHex(newProfile.account), + config.community.alias, + ''); + _accountBackupDBService.accounts.update( DBAccount( - alias: _wallet.alias!, + alias: config.community.alias, address: EthereumAddress.fromHex(newProfile.account), name: newProfile.name, username: newProfile.username, + accountFactoryAddress: existingAccount?.accountFactoryAddress ?? '', privateKey: null, profile: newProfile, ), @@ -344,7 +452,16 @@ class ProfileLogic { return false; } + /// Update an existing profile Future update(ProfileV1 profile) async { + final config = _config; + final credentials = await _credentials; + final account = _account; + + if (config == null || credentials == null || account == null) { + return false; + } + try { _state.setProfileRequest(); @@ -357,7 +474,7 @@ class ProfileLogic { _state.setProfileExisting(); - final existing = await _wallet.getProfile(profile.account); + final existing = await getProfile(config, profile.account); if (existing == null) { throw Exception('Failed to load profile'); } @@ -369,14 +486,27 @@ class ProfileLogic { _state.setProfileUploading(); - final url = await _wallet.updateProfile(profile); + final accountForFactory = await _accountBackupDBService.accounts + .get(account, config.community.alias, ''); + + if (accountForFactory == null) {} + + final factoryAddress = accountForFactory?.accountFactoryAddress; + + final url = await updateProfile( + config, + account, + credentials, + profile, + accountFactoryAddress: factoryAddress, + ); if (url == null) { throw Exception('Failed to save profile'); } _state.setProfileFetching(); - final newProfile = await _wallet.getProfileFromUrl(url); + final newProfile = await getProfileFromUrl(config, url); if (newProfile == null) { throw Exception('Failed to load profile'); } @@ -403,12 +533,18 @@ class ProfileLogic { imageSmall: newProfile.imageSmall, )); + final existingAccount = await _accountBackupDBService.accounts.get( + EthereumAddress.fromHex(newProfile.account), + config.community.alias, + ''); + _accountBackupDBService.accounts.update( DBAccount( - alias: _wallet.alias!, + alias: config.community.alias, address: EthereumAddress.fromHex(newProfile.account), name: newProfile.name, username: newProfile.username, + accountFactoryAddress: existingAccount?.accountFactoryAddress ?? '', privateKey: null, profile: newProfile, ), @@ -420,7 +556,10 @@ class ProfileLogic { ); return true; - } catch (_) {} + } catch (e, stackTrace) { + debugPrint('ProfileLogic.update() - Error during profile update: $e'); + debugPrint('Stack trace: $stackTrace'); + } _state.setProfileError(); return false; @@ -435,6 +574,9 @@ class ProfileLogic { } Future generateProfileUsername() async { + final walletData = await _walletData; + if (walletData == null) return null; + String username = await getRandomUsername(); _state.setUsernameSuccess(username: username); @@ -442,7 +584,7 @@ class ProfileLogic { const baseDelay = Duration(milliseconds: 100); for (int tries = 1; tries <= maxTries; tries++) { - final exists = await _wallet.profileExists(username); + final exists = await profileExists(walletData.config!, username); if (!exists) { return username; @@ -460,6 +602,9 @@ class ProfileLogic { Future giveProfileUsername() async { debugPrint('handleNewProfile'); + final walletData = await _walletData; + if (walletData == null) return; + try { final username = await generateProfileUsername(); if (username == null) { @@ -467,13 +612,11 @@ class ProfileLogic { return; } - _state.setUsernameSuccess(username: username); - - final address = _wallet.account.hexEip55; - final alias = _wallet.alias ?? ''; + final address = walletData.account!.hexEip55; + final alias = walletData.config!.community.alias; final account = await _accountBackupDBService.accounts - .get(EthereumAddress.fromHex(address), alias); + .get(EthereumAddress.fromHex(address), alias, ''); if (account == null) { throw Exception( @@ -489,16 +632,12 @@ class ProfileLogic { name: account.name, ); - _profiles.isLoaded( - profile.account, - profile, - ); - if (_pauseProfileCreation) { return; } - final exists = await _wallet.createAccount(); + final exists = await createAccount( + walletData.config!, walletData.account!, walletData.credentials!); if (!exists) { throw Exception('Failed to create account'); } @@ -507,10 +646,14 @@ class ProfileLogic { return; } - final url = await _wallet.setProfile( + final url = await setProfile( + walletData.config!, + walletData.account!, + walletData.credentials!, ProfileRequest.fromProfileV1(profile), image: await _photos.photoFromBundle('assets/icons/profile.jpg'), fileType: '.jpg', + accountFactoryAddress: account.accountFactoryAddress, ); if (url == null) { throw Exception('Failed to create profile url'); @@ -520,11 +663,12 @@ class ProfileLogic { return; } - final newProfile = await _wallet.getProfileFromUrl(url); + final newProfile = await getProfileFromUrl(walletData.config!, url); if (newProfile == null) { throw Exception('Failed to get profile from url $url'); } + _state.setUsernameSuccess(username: newProfile.username); _profiles.isLoaded( newProfile.account, newProfile, @@ -556,6 +700,7 @@ class ProfileLogic { address: EthereumAddress.fromHex(address), name: newProfile.name, username: newProfile.username, + accountFactoryAddress: account.accountFactoryAddress, profile: newProfile, ), ); diff --git a/lib/state/profile/state.dart b/lib/state/profile/state.dart index 6cc7ac11..b4e30aa5 100644 --- a/lib/state/profile/state.dart +++ b/lib/state/profile/state.dart @@ -40,6 +40,7 @@ class ProfileState with ChangeNotifier { final TextEditingController usernameController = TextEditingController(); bool usernameLoading = false; bool usernameError = false; + String usernameErrorMessage = ''; final TextEditingController nameController = TextEditingController(); bool nameError = false; @@ -67,6 +68,7 @@ class ProfileState with ChangeNotifier { usernameController.text = ''; usernameLoading = false; usernameError = false; + usernameErrorMessage = ''; nameController.text = ''; nameError = false; @@ -83,6 +85,7 @@ class ProfileState with ChangeNotifier { usernameController.text = ''; usernameLoading = false; usernameError = false; + usernameErrorMessage = ''; nameController.text = ''; nameError = false; @@ -225,9 +228,18 @@ class ProfileState with ChangeNotifier { notifyListeners(); } + void setUsernameError({String message = ''}) { + usernameLoading = false; + usernameError = true; + usernameErrorMessage = message; + + notifyListeners(); + } + void setUsernameSuccess({String? username}) { usernameLoading = false; usernameError = false; + usernameErrorMessage = ''; if (username != null && username.isNotEmpty) { this.username = username; @@ -236,13 +248,6 @@ class ProfileState with ChangeNotifier { notifyListeners(); } - void setUsernameError() { - usernameLoading = false; - usernameError = true; - - notifyListeners(); - } - void setNameError(bool err) { nameError = err; diff --git a/lib/state/profiles/logic.dart b/lib/state/profiles/logic.dart index febe6767..705590ab 100644 --- a/lib/state/profiles/logic.dart +++ b/lib/state/profiles/logic.dart @@ -1,3 +1,4 @@ +import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/db/account/contacts.dart'; import 'package:citizenwallet/services/db/account/db.dart'; import 'package:citizenwallet/services/db/backup/accounts.dart'; @@ -10,19 +11,24 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:rate_limiter/rate_limiter.dart'; import 'package:citizenwallet/services/cache/contacts.dart'; +import 'package:web3dart/web3dart.dart'; class ProfilesLogic extends WidgetsBindingObserver { final AccountDBService _db = AccountDBService(); final AccountBackupDBService _accountBackupDBService = AccountBackupDBService(); late ProfilesState _state; - final WalletService _wallet = WalletService(); + + late EthPrivateKey _currentCredentials; + late EthereumAddress _currentAccount; + late Config _currentConfig; late Debounce debouncedSearchProfile; late Debounce debouncedLoad; List toLoad = []; bool stopLoading = false; + bool _isInitialized = false; ProfilesLogic(BuildContext context) { _state = context.read(); @@ -41,10 +47,22 @@ class ProfilesLogic extends WidgetsBindingObserver { ); } + void setWalletState( + Config config, EthPrivateKey credentials, EthereumAddress account) { + _currentConfig = config; + _currentCredentials = credentials; + _currentAccount = account; + _isInitialized = true; + } + Future _loadCachedProfile(String addr) async { try { + if (_currentConfig == null || !_isInitialized) { + return null; + } + final cachedProfile = await ContactsCache().get(addr, () async { - final fetchedProfile = await _wallet.getProfile(addr); + final fetchedProfile = await getProfile(_currentConfig, addr); if (fetchedProfile == null) { return null; } @@ -77,6 +95,10 @@ class ProfilesLogic extends WidgetsBindingObserver { return; } + if (!_isInitialized) { + return; + } + final toLoadCopy = [...toLoad]; toLoad = []; @@ -106,6 +128,10 @@ class ProfilesLogic extends WidgetsBindingObserver { Future loadProfile(String addr) async { try { + if (!_isInitialized) { + return; + } + if (!toLoad.contains(addr) && !_state.exists(addr)) { toLoad.add(addr); debouncedLoad(); @@ -117,23 +143,22 @@ class ProfilesLogic extends WidgetsBindingObserver { Future _searchProfile(String value) async { try { + if (_currentConfig == null || !_isInitialized) { + _state.isSearchingError(); + return; + } + final cleanValue = value.replaceFirst('@', ''); _state.isSearching(); final profile = cleanValue.startsWith('0x') - ? await _wallet.getProfile(cleanValue) - : await _wallet.getProfileByUsername(cleanValue); - - final results = await _db.contacts.search(cleanValue.toLowerCase()); - - _state.isSearchingSuccess( - profile, - results.map((e) => ProfileV1.fromMap(e.toMap())).toList(), - ); + ? await getProfile(_currentConfig, cleanValue) + : await getProfileByUsername(_currentConfig, cleanValue); if (profile != null) { - _db.contacts.upsert(DBContact( + _state.isSearchingSuccess(profile, []); + await _db.contacts.upsert(DBContact( account: profile.account, username: profile.username, name: profile.name, @@ -142,6 +167,14 @@ class ProfilesLogic extends WidgetsBindingObserver { imageMedium: profile.imageMedium, imageSmall: profile.imageSmall, )); + } else { + if (cleanValue.length >= 3) { + final results = await _db.contacts.search(cleanValue.toLowerCase()); + _state.isSearchingSuccess( + null, results.map((e) => ProfileV1.fromMap(e.toMap())).toList()); + } else { + _state.isSearchingSuccess(null, []); + } } return; } catch (e) { @@ -151,8 +184,13 @@ class ProfilesLogic extends WidgetsBindingObserver { _state.isSearchingError(); } - Future getProfile(String addr) async { + Future getLocalProfile(String addr) async { try { + if (!_isInitialized) { + _state.isSearchingError(); + return null; + } + _state.isSearching(); final profile = await _loadCachedProfile(addr); @@ -171,12 +209,29 @@ class ProfilesLogic extends WidgetsBindingObserver { } Future searchProfile(String username) async { + if (username.trim().isEmpty) { + debouncedSearchProfile.cancel(); + _state.clearSearch(); + await allProfiles(); + return; + } + + if (!_isInitialized) { + _state.isSearchingError(); + return; + } + _state.isSearching(); debouncedSearchProfile([username]); } Future allProfiles() async { try { + if (!_isInitialized) { + _state.isSearchingError(); + return; + } + _state.isSearching(); final results = await _db.contacts.getAll(); @@ -195,6 +250,11 @@ class ProfilesLogic extends WidgetsBindingObserver { Future loadProfiles() async { try { + if (!_isInitialized) { + _state.profileListFail(); + return; + } + _state.profileListRequest(); final results = await _db.contacts.getAll(); @@ -212,6 +272,10 @@ class ProfilesLogic extends WidgetsBindingObserver { Future loadProfilesFromAllAccounts() async { try { + if (!_isInitialized) { + return; + } + final accounts = await _accountBackupDBService.accounts.all(); final profilesMap = {}; @@ -222,8 +286,7 @@ class ProfilesLogic extends WidgetsBindingObserver { } // Try to get updated profile from wallet - final updatedProfile = - await _wallet.getProfile(account.address.hexEip55); + final updatedProfile = await getLocalProfile(account.address.hexEip55); if (updatedProfile != null) { profilesMap[account.address.hexEip55] = updatedProfile; @@ -234,6 +297,7 @@ class ProfilesLogic extends WidgetsBindingObserver { address: account.address, name: updatedProfile.name, username: updatedProfile.username, + accountFactoryAddress: account.accountFactoryAddress, profile: updatedProfile, ), ); @@ -247,24 +311,51 @@ class ProfilesLogic extends WidgetsBindingObserver { } void selectProfile(ProfileV1? profile) { + if (!_isInitialized) { + return; + } + _state.isSelected(profile); } Future getAccountAddressWithAlias(String alias) async { - final accounts = await _accountBackupDBService.accounts.allForAlias(alias); - return accounts.first.address.hex; + if (!_isInitialized) { + return null; + } + + try { + final accounts = + await _accountBackupDBService.accounts.allForAlias(alias); + return accounts.first.address.hex; + } catch (e) { + return null; + } } Future getSendToProfile(String address) async { - final profile = await _wallet.getProfile(address); + if (!_isInitialized) { + return null; + } + + final profile = await getLocalProfile(address); return profile; } void deSelectProfile() { + if (!_isInitialized) { + return; + } + _state.isDeSelected(); } void clearSearch({bool notify = true}) { + if (!_isInitialized) { + return; + } + + debouncedSearchProfile.cancel(); + _state.clearSearch(notify: notify); } @@ -275,6 +366,11 @@ class ProfilesLogic extends WidgetsBindingObserver { void resume() { stopLoading = false; + + if (!_isInitialized) { + return; + } + debouncedLoad(); } diff --git a/lib/state/profiles/selectors.dart b/lib/state/profiles/selectors.dart index 12cc1037..22d5c85e 100644 --- a/lib/state/profiles/selectors.dart +++ b/lib/state/profiles/selectors.dart @@ -2,6 +2,12 @@ import 'package:citizenwallet/services/wallet/contracts/profile.dart'; import 'package:citizenwallet/state/profiles/state.dart'; List selectProfileSuggestions(ProfilesState state) { + if (!state.searchLoading && + state.searchedProfile == null && + state.searchResults.isEmpty) { + return []; + } + Map profiles = {}; if (state.searchedProfile != null) { diff --git a/lib/state/scan/logic.dart b/lib/state/scan/logic.dart index f7b3382e..e05b374e 100644 --- a/lib/state/scan/logic.dart +++ b/lib/state/scan/logic.dart @@ -2,10 +2,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/nfc/default.dart'; import 'package:citizenwallet/services/nfc/service.dart'; import 'package:citizenwallet/services/wallet/wallet.dart'; import 'package:citizenwallet/state/scan/state.dart'; +import 'package:web3dart/web3dart.dart'; class ScanLogic extends WidgetsBindingObserver { static final ScanLogic _instance = ScanLogic._internal(); @@ -18,12 +20,24 @@ class ScanLogic extends WidgetsBindingObserver { late ScanState _state; final NFCService _nfc = DefaultNFCService(); - final WalletService _wallet = WalletService(); + late EthPrivateKey _currentCredentials; + late EthereumAddress _currentAccount; + late Config _currentConfig; + + static void setGlobalWalletState(Config config, EthPrivateKey credentials, EthereumAddress account) { + _instance.setWalletState(config, credentials, account); + } void init(BuildContext context) { _state = context.read(); } + void setWalletState(Config config, EthPrivateKey credentials, EthereumAddress account) { + _currentConfig = config; + _currentCredentials = credentials; + _currentAccount = account; + } + void load() async { try { _state.loadScanner(); @@ -48,6 +62,10 @@ class ScanLogic extends WidgetsBindingObserver { Future read({String? message, String? successMessage}) async { try { + if (_currentConfig == null || _currentCredentials == null || _currentAccount == null) { + throw Exception('Wallet not initialized'); + } + _state.setNfcAddressRequest(); _state.setNfcReading(true); @@ -59,8 +77,8 @@ class ScanLogic extends WidgetsBindingObserver { _state.setNfcReading(false); // - final cardHash = await _wallet.getCardHash(serialNumber); - final address = await _wallet.getCardAddress(cardHash); + final cardHash = await getCardHash(_currentConfig, serialNumber); + final address = await getCardAddress(_currentConfig, cardHash); _state.setNfcAddressSuccess(address.hexEip55); diff --git a/lib/state/transaction.dart b/lib/state/transaction.dart index 40896b35..2db1bf52 100644 --- a/lib/state/transaction.dart +++ b/lib/state/transaction.dart @@ -1,11 +1,10 @@ import 'dart:convert'; import 'package:citizenwallet/models/transaction.dart' as transaction_model; +import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/db/account/db.dart'; -import 'package:citizenwallet/services/db/account/transactions.dart'; import 'package:citizenwallet/services/wallet/contracts/erc20.dart'; import 'package:citizenwallet/services/wallet/utils.dart'; -import 'package:citizenwallet/services/wallet/wallet.dart'; import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; import 'package:web3dart/web3dart.dart'; @@ -14,7 +13,7 @@ class TransactionState with ChangeNotifier { final String _transactionHash; transaction_model.CWTransaction? transaction; final AccountDBService _accountDBService = AccountDBService(); - final WalletService _wallet = WalletService(); + Config? _config; TransactionState({required String transactionHash}) : _transactionHash = transactionHash; @@ -28,6 +27,10 @@ class TransactionState with ChangeNotifier { } } + void setConfig(Config config) { + _config = config; + } + @override void dispose() { _mounted = false; @@ -39,6 +42,13 @@ class TransactionState with ChangeNotifier { loading = true; safeNotifyListeners(); + if (_config == null) { + loading = false; + transaction = null; + safeNotifyListeners(); + return; + } + final dbTransaction = await _accountDBService.transactions .getTransactionByHash(_transactionHash); @@ -52,11 +62,11 @@ class TransactionState with ChangeNotifier { transaction = transaction_model.CWTransaction( fromDoubleUnit( dbTransaction.value.toString(), - decimals: _wallet.currency.decimals, + decimals: _config!.getPrimaryToken().decimals, ), id: dbTransaction.hash, hash: dbTransaction.txHash, - chainId: _wallet.chainId, + chainId: _config!.chains.values.first.id, from: EthereumAddress.fromHex(dbTransaction.from).hexEip55, to: EthereumAddress.fromHex(dbTransaction.to).hexEip55, description: dbTransaction.data != '' diff --git a/lib/state/vouchers/logic.dart b/lib/state/vouchers/logic.dart index f74d7099..0a238f30 100644 --- a/lib/state/vouchers/logic.dart +++ b/lib/state/vouchers/logic.dart @@ -5,6 +5,8 @@ import 'package:citizenwallet/services/config/config.dart'; import 'package:citizenwallet/services/db/account/db.dart'; import 'package:citizenwallet/services/db/account/vouchers.dart'; import 'package:citizenwallet/services/db/app/db.dart'; +import 'package:citizenwallet/services/accounts/accounts.dart'; +import 'package:citizenwallet/services/db/backup/accounts.dart'; import 'package:citizenwallet/services/share/share.dart'; import 'package:citizenwallet/services/wallet/contracts/erc20.dart'; import 'package:citizenwallet/services/engine/utils.dart'; @@ -27,14 +29,19 @@ class VoucherLogic extends WidgetsBindingObserver { final AppDBService _appDBService = AppDBService(); final AccountDBService _accountDBService = AccountDBService(); - final WalletService _wallet = WalletService(); final SharingService _sharing = SharingService(); + final AccountsServiceInterface _encPrefs = getAccountsService(); + + late EthPrivateKey _currentCredentials; + late EthereumAddress _currentAccount; + late Config _currentConfig; late VoucherState _state; late Debounce debouncedLoad; List toLoad = []; bool stopLoading = false; + bool _isInitialized = false; VoucherLogic(BuildContext context) { _state = context.read(); @@ -46,10 +53,178 @@ class VoucherLogic extends WidgetsBindingObserver { ); } + void setWalletState( + Config config, EthPrivateKey credentials, EthereumAddress account) { + _currentConfig = config; + _currentCredentials = credentials; + _currentAccount = account; + _isInitialized = true; + } + void resetCreate() { _state.resetCreate(notify: false); } + Future resolveAccountFactoryAddress() async { + try { + if (_currentConfig == null) { + throw Exception('Current config is null'); + } + + if (_currentConfig.community.primaryAccountFactory.address.isEmpty) { + throw Exception('Primary account factory address is empty'); + } + + if (_currentAccount == null) { + throw Exception('Current account is null'); + } + + try { + final factoryAddress = + await getAccountFactoryAddressWithHiddenCommunityFallback(); + if (factoryAddress.isNotEmpty) { + return factoryAddress; + } + } catch (e) {} + + try { + final possibleFactories = [ + '', + _currentConfig.community.primaryAccountFactory.address, + '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2', + '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185', + '0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9', + '0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD', + '0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87', + '0x5e987a6c4bb4239d498E78c34e986acf29c81E8e', + ]; + + for (final factory in possibleFactories) { + try { + final dbAccount = await _encPrefs.getAccount( + _currentAccount.hexEip55, + _currentConfig.community.alias, + factory); + if (dbAccount != null && + dbAccount.accountFactoryAddress.isNotEmpty) { + return dbAccount.accountFactoryAddress; + } + } catch (e) {} + } + } catch (e) {} + + try { + final communityMappings = { + 'bread': '0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9', + 'gratitude': '0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD', + 'wallet.commonshub.brussels': + '0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87', + 'wallet.sfluv.org': '0x5e987a6c4bb4239d498E78c34e986acf29c81E8e', + }; + + final mappedFactory = communityMappings[_currentConfig.community.alias]; + if (mappedFactory != null) { + return mappedFactory; + } + } catch (e) {} + + try { + final allAccounts = await _encPrefs.getAllAccounts(); + final matchingAccounts = allAccounts + .where((acc) => acc.address.hexEip55 == _currentAccount.hexEip55) + .toList(); + + for (final account in matchingAccounts) { + if (account.accountFactoryAddress.isNotEmpty) { + return account.accountFactoryAddress; + } + } + } catch (e) {} + + try { + final primaryFactory = + _currentConfig.community.primaryAccountFactory.address; + if (primaryFactory.isNotEmpty) { + return primaryFactory; + } + } catch (e) {} + + return '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185'; + } catch (e) { + return '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185'; + } + } + + Future getAccountFactoryAddressWithHiddenCommunityFallback() async { + try { + final dbAccount = await _encPrefs.getAccount( + _currentAccount.hexEip55, _currentConfig.community.alias, ''); + + if (dbAccount?.accountFactoryAddress != null && + dbAccount!.accountFactoryAddress.isNotEmpty) { + return dbAccount.accountFactoryAddress; + } + + final oldAccountFactories = { + 'bread': '0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9', + 'gratitude': '0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD', + 'wallet.commonshub.brussels': + '0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87', + 'wallet.sfluv.org': '0x5e987a6c4bb4239d498E78c34e986acf29c81E8e', + }; + + final oldFactory = oldAccountFactories[_currentConfig.community.alias]; + if (oldFactory != null) { + if (dbAccount != null) { + final updatedAccount = DBAccount( + alias: dbAccount.alias, + address: dbAccount.address, + name: dbAccount.name, + username: dbAccount.username, + accountFactoryAddress: oldFactory, + privateKey: dbAccount.privateKey, + profile: dbAccount.profile, + ); + await _encPrefs.setAccount(updatedAccount); + } + return oldFactory; + } + final allAccounts = await _encPrefs.getAllAccounts(); + final matchingAccount = allAccounts + .where((acc) => acc.address.hexEip55 == _currentAccount.hexEip55) + .firstOrNull; + + if (matchingAccount != null && + matchingAccount.accountFactoryAddress.isNotEmpty) { + return matchingAccount.accountFactoryAddress; + } + final legacyAccount = await _encPrefs.getAccount( + _currentAccount.hexEip55, + _currentConfig.community.alias, + '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2'); + if (legacyAccount != null) { + const newFactoryAddress = '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185'; + + final updatedAccount = DBAccount( + alias: legacyAccount.alias, + address: legacyAccount.address, + name: legacyAccount.name, + username: legacyAccount.username, + accountFactoryAddress: newFactoryAddress, + privateKey: legacyAccount.privateKey, + profile: legacyAccount.profile, + ); + await _encPrefs.setAccount(updatedAccount); + + return newFactoryAddress; + } + + return _currentConfig.community.primaryAccountFactory.address; + } catch (e) { + return _currentConfig.community.primaryAccountFactory.address; + } + } + _loadVoucher() async { if (stopLoading) { return; @@ -63,7 +238,8 @@ class VoucherLogic extends WidgetsBindingObserver { return; } try { - final balance = await _wallet.getBalance(addr: addr); + final balance = + await getBalance(_currentConfig, EthereumAddress.fromHex(addr)); await _accountDBService.vouchers .updateBalance(addr, balance.toString()); @@ -91,14 +267,13 @@ class VoucherLogic extends WidgetsBindingObserver { Future fetchVouchers() async { try { - _state.vouchersRequest(); - - if (_wallet.alias == null) { - throw Exception('alias not found'); + if (!_isInitialized) { + throw Exception('wallet not initialized'); } - final vouchers = - await _accountDBService.vouchers.getAllByAlias(_wallet.alias!); + _state.vouchersRequest(); + final vouchers = await _accountDBService.vouchers + .getAllByAlias(_currentConfig.community.alias); _state.vouchersSuccess(vouchers .map( @@ -129,6 +304,10 @@ class VoucherLogic extends WidgetsBindingObserver { String salt = '', }) async { try { + if (!_isInitialized) { + throw Exception('wallet not initialized'); + } + _state.readVoucherRequest(); final jsonVoucher = decompress(compressedVoucher); @@ -151,13 +330,14 @@ class VoucherLogic extends WidgetsBindingObserver { EthereumAddress account = uri.queryParameters['account'] != null ? EthereumAddress.fromHex(uri.queryParameters['account']!) - : await _wallet.getAccountAddress( + : await getAccountAddress( + _currentConfig, credentials.address.hexEip55, legacy: true, cache: false, ); - final balance = await _wallet.getBalance(addr: account.hexEip55); + final balance = await getBalance(_currentConfig, account); final voucher = Voucher( address: account.hexEip55, @@ -196,6 +376,10 @@ class VoucherLogic extends WidgetsBindingObserver { Future openVoucher(String address) async { try { + if (!_isInitialized) { + throw Exception('wallet not initialized'); + } + _state.openVoucherRequest(); final dbvoucher = await _accountDBService.vouchers.get(address); @@ -203,7 +387,8 @@ class VoucherLogic extends WidgetsBindingObserver { throw Exception('voucher not found'); } - final balance = await _wallet.getBalance(addr: address); + final balance = + await getBalance(_currentConfig, EthereumAddress.fromHex(address)); await _accountDBService.vouchers.updateBalance(address, balance); @@ -218,11 +403,12 @@ class VoucherLogic extends WidgetsBindingObserver { legacy: dbvoucher.legacy, ); - if (_wallet.alias == null) { + if (_currentConfig.community.alias.isEmpty) { throw Exception('alias not found'); } - final community = await _appDBService.communities.get(_wallet.alias!); + final community = + await _appDBService.communities.get(_currentConfig.community.alias); if (community == null) { throw Exception('community not found'); @@ -236,7 +422,7 @@ class VoucherLogic extends WidgetsBindingObserver { voucher, voucher.getLink( appLink, - _wallet.currency.symbol, + _currentConfig.getPrimaryToken().symbol, dbvoucher.voucher, ), ); @@ -262,19 +448,24 @@ class VoucherLogic extends WidgetsBindingObserver { String salt = '', }) async { try { + if (!_isInitialized) { + throw Exception('wallet not initialized'); + } + _state.createVoucherRequest(); final doubleAmount = balance.replaceAll(',', '.'); final parsedAmount = toUnit( doubleAmount, - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ); - if (_wallet.alias == null) { + if (_currentConfig.community.alias.isEmpty) { throw Exception('alias not found'); } - final community = await _appDBService.communities.get(_wallet.alias!); + final community = + await _appDBService.communities.get(_currentConfig.community.alias); if (community == null) { throw Exception('community not found'); @@ -291,7 +482,7 @@ class VoucherLogic extends WidgetsBindingObserver { final List vouchers = []; for (int i = 0; i < quantity; i++) { - addresses.add(_wallet.tokenAddress); + addresses.add(_currentConfig.getPrimaryToken().address); final credentials = EthPrivateKey.createRandom(Random.secure()); @@ -302,8 +493,8 @@ class VoucherLogic extends WidgetsBindingObserver { scryptN: 2, ); - final account = - await _wallet.getAccountAddress(credentials.address.hexEip55); + final account = await getAccountAddress( + _currentConfig, credentials.address.hexEip55); final dbvoucher = DBVoucher( address: account.hexEip55, @@ -312,14 +503,16 @@ class VoucherLogic extends WidgetsBindingObserver { balance: parsedAmount.toString(), voucher: wallet.toJson(), salt: salt, - creator: _wallet.account.hexEip55, + creator: _currentAccount.hexEip55, legacy: false, ); dbvouchers.add(dbvoucher); // TODO: token id should be set - calldata.add(_wallet.tokenTransferCallData( + calldata.add(tokenTransferCallData( + _currentConfig, + _currentAccount, account.hexEip55, parsedAmount, )); @@ -338,17 +531,26 @@ class VoucherLogic extends WidgetsBindingObserver { vouchers.add(voucher); } - final (_, userop) = await _wallet.prepareUserop( + final accountFactoryAddress = await resolveAccountFactoryAddress(); + + final (_, userop) = await prepareUserop( + _currentConfig, + _currentAccount, + _currentCredentials, addresses, calldata, + accountFactoryAddress: accountFactoryAddress, ); - final txHash = await _wallet.submitUserop(userop); + final txHash = await submitUserop( + _currentConfig, + userop, + ); if (txHash == null) { throw Exception('transaction failed'); } - final success = await _wallet.waitForTxSuccess(txHash); + final success = await waitForTxSuccess(_currentConfig, txHash); if (!success) { throw Exception('transaction failed'); } @@ -362,8 +564,8 @@ class VoucherLogic extends WidgetsBindingObserver { ); return; - } catch (_) { - // + } catch (e) { + debugPrint('Error creating voucher: $e'); } _state.createVoucherError(); @@ -377,6 +579,9 @@ class VoucherLogic extends WidgetsBindingObserver { bool mint = false, }) async { try { + if (!_isInitialized) { + throw Exception('wallet not initialized'); + } _state.createVoucherRequest(); final credentials = EthPrivateKey.createRandom(Random.secure()); @@ -384,17 +589,18 @@ class VoucherLogic extends WidgetsBindingObserver { final doubleAmount = balance.replaceAll(',', '.'); final parsedAmount = toUnit( doubleAmount, - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ); final account = - await _wallet.getAccountAddress(credentials.address.hexEip55); + await getAccountAddress(_currentConfig, credentials.address.hexEip55); - if (_wallet.alias == null) { + if (_currentConfig.community.alias.isEmpty) { throw Exception('alias not found'); } - final community = await _appDBService.communities.get(_wallet.alias!); + final community = + await _appDBService.communities.get(_currentConfig.community.alias); if (community == null) { throw Exception('community not found'); @@ -409,7 +615,7 @@ class VoucherLogic extends WidgetsBindingObserver { balance: parsedAmount.toString(), voucher: 'v2-${bytesToHex(credentials.privateKey)}', salt: salt, - creator: _wallet.account.hexEip55, + creator: _currentAccount.hexEip55, legacy: false, ); @@ -417,26 +623,34 @@ class VoucherLogic extends WidgetsBindingObserver { // TODO: token id should be set final calldata = mint - ? _wallet.tokenMintCallData( + ? tokenMintCallData( + _currentConfig, account.hexEip55, parsedAmount, ) - : _wallet.tokenTransferCallData( + : tokenTransferCallData( + _currentConfig, + _currentAccount, account.hexEip55, parsedAmount, ); + final accountFactoryAddress = await resolveAccountFactoryAddress(); - final (_, userop) = await _wallet.prepareUserop( - [_wallet.tokenAddress], + final (_, userop) = await prepareUserop( + _currentConfig, + _currentAccount, + _currentCredentials, + [_currentConfig.getPrimaryToken().address], [calldata], + accountFactoryAddress: accountFactoryAddress, ); final args = { - 'from': _wallet.account.hexEip55, + 'from': _currentAccount.hexEip55, 'to': account.hexEip55, }; - if (_wallet.standard == 'erc1155') { - args['operator'] = _wallet.account.hexEip55; + if (_currentConfig.getPrimaryToken().standard == 'erc1155') { + args['operator'] = _currentAccount.hexEip55; args['id'] = '0'; args['amount'] = parsedAmount.toString(); } else { @@ -444,12 +658,13 @@ class VoucherLogic extends WidgetsBindingObserver { } final eventData = createEventData( - stringSignature: _wallet.transferEventStringSignature, - topic: _wallet.transferEventSignature, + stringSignature: transferEventStringSignature(_currentConfig), + topic: transferEventSignature(_currentConfig), args: args, ); - final txHash = await _wallet.submitUserop( + final txHash = await submitUserop( + _currentConfig, userop, data: eventData, extraData: TransferData(dbvoucher.name), @@ -480,7 +695,7 @@ class VoucherLogic extends WidgetsBindingObserver { ), ); - final success = await _wallet.waitForTxSuccess(txHash); + final success = await waitForTxSuccess(_currentConfig, txHash); if (!success) { throw Exception('transaction failed'); } @@ -538,6 +753,9 @@ class VoucherLogic extends WidgetsBindingObserver { })? sendingTransaction, }) async { try { + if (!_isInitialized) { + throw Exception('wallet not initialized'); + } _state.returnVoucherRequest(); final voucher = await _accountDBService.vouchers.get(address); @@ -566,31 +784,38 @@ class VoucherLogic extends WidgetsBindingObserver { if (preSendingTransaction != null) { preSendingTransaction( - amount, tempId, _wallet.account.hexEip55, voucher.address); + amount, tempId, _currentAccount.hexEip55, voucher.address); } - final calldata = _wallet.tokenTransferCallData( - _wallet.account.hexEip55, + final calldata = tokenTransferCallData( + _currentConfig, + _currentAccount, + voucher.address, amount, - from: voucher.address, ); - final (hash, userop) = await _wallet.prepareUserop( - [_wallet.tokenAddress], + final accountFactoryAddress = await resolveAccountFactoryAddress(); + + final (hash, userop) = await prepareUserop( + _currentConfig, + _currentAccount, + _currentCredentials, + [_currentConfig.getPrimaryToken().address], [calldata], customCredentials: credentials, + accountFactoryAddress: accountFactoryAddress, ); if (sendingTransaction != null) { sendingTransaction( - amount, hash, _wallet.account.hexEip55, voucher.address); + amount, hash, _currentAccount.hexEip55, voucher.address); } final args = { 'from': voucher.address, - 'to': _wallet.account.hexEip55, + 'to': _currentAccount.hexEip55, }; - if (_wallet.standard == 'erc1155') { + if (_currentConfig.getPrimaryToken().standard == 'erc1155') { args['operator'] = voucher.address; args['id'] = '0'; args['amount'] = amount.toString(); @@ -599,12 +824,13 @@ class VoucherLogic extends WidgetsBindingObserver { } final eventData = createEventData( - stringSignature: _wallet.transferEventStringSignature, - topic: _wallet.transferEventSignature, + stringSignature: transferEventStringSignature(_currentConfig), + topic: transferEventSignature(_currentConfig), args: args, ); - final txHash = await _wallet.submitUserop( + final txHash = await submitUserop( + _currentConfig, userop, customCredentials: credentials, data: eventData, @@ -614,7 +840,7 @@ class VoucherLogic extends WidgetsBindingObserver { throw Exception('transaction failed'); } - final success = await _wallet.waitForTxSuccess(txHash); + final success = await waitForTxSuccess(_currentConfig, txHash); if (!success) { throw Exception('transaction failed'); } @@ -657,6 +883,10 @@ class VoucherLogic extends WidgetsBindingObserver { void resume({String? address}) { stopLoading = false; + if (!_isInitialized) { + return; + } + if (address != null) { updateVoucher(address); return; diff --git a/lib/state/wallet/logic.dart b/lib/state/wallet/logic.dart index 43eb0c46..1decf9a0 100644 --- a/lib/state/wallet/logic.dart +++ b/lib/state/wallet/logic.dart @@ -10,6 +10,7 @@ import 'package:citizenwallet/services/config/service.dart'; import 'package:citizenwallet/services/db/account/db.dart'; import 'package:citizenwallet/services/db/backup/accounts.dart'; import 'package:citizenwallet/services/db/app/db.dart'; +import 'package:citizenwallet/services/db/app/communities.dart'; import 'package:citizenwallet/services/db/account/transactions.dart'; import 'package:citizenwallet/services/accounts/accounts.dart'; import 'package:citizenwallet/services/engine/events.dart'; @@ -24,7 +25,10 @@ import 'package:citizenwallet/services/wallet/models/userop.dart'; import 'package:citizenwallet/services/wallet/utils.dart'; import 'package:citizenwallet/services/wallet/wallet.dart'; import 'package:citizenwallet/state/notifications/logic.dart'; +import 'package:citizenwallet/state/profiles/logic.dart'; +import 'package:citizenwallet/state/scan/logic.dart'; import 'package:citizenwallet/state/theme/logic.dart'; +import 'package:citizenwallet/state/vouchers/logic.dart'; import 'package:citizenwallet/state/wallet/state.dart'; import 'package:citizenwallet/utils/delay.dart'; import 'package:citizenwallet/utils/qr.dart'; @@ -79,7 +83,6 @@ class WalletLogic extends WidgetsBindingObserver { final String appUniversalURL = dotenv.get('ORIGIN_HEADER'); final ConfigService _config = ConfigService(); - final WalletService _wallet = WalletService(); final AccountDBService _accountDBService = AccountDBService(); final AppDBService _appDBService = AppDBService(); @@ -88,9 +91,21 @@ class WalletLogic extends WidgetsBindingObserver { bool cancelLoadAccounts = false; - WalletService get wallet => _wallet; EventService? _eventService; - SigAuthConnection get connection => _wallet.connection; + + late EthPrivateKey _currentCredentials; + late EthereumAddress _currentAccount; + late Config _currentConfig; + bool _isInitialized = false; + + SigAuthConnection get connection { + return getSigAuthConnection( + _currentConfig, + _currentAccount, + _currentCredentials, + dotenv.get('ORIGIN_HEADER'), + ); + } final TextEditingController _addressController = TextEditingController(); final TextEditingController _amountController = TextEditingController(); @@ -101,9 +116,9 @@ class WalletLogic extends WidgetsBindingObserver { TextEditingController get messageController => _messageController; String? get lastWallet => _preferences.lastWallet; - String get address => _wallet.address.hexEip55; - String get account => _wallet.account.hexEip55; - String get token => _wallet.tokenAddress; + String get address => _currentCredentials.address.hexEip55; + String get account => _currentAccount.hexEip55; + String get token => _currentConfig.getPrimaryToken().address; WalletLogic(BuildContext context, NotificationsLogic notificationsLogic) : _state = context.read(), @@ -111,8 +126,33 @@ class WalletLogic extends WidgetsBindingObserver { _walletKitLogic.setContext(context); } - EthPrivateKey get privateKey { - return _wallet.credentials; + // References to other logic classes that need wallet state + ProfilesLogic? _profilesLogic; + VoucherLogic? _voucherLogic; + + void setLogicReferences( + ProfilesLogic? profilesLogic, VoucherLogic? voucherLogic) { + _profilesLogic = profilesLogic; + _voucherLogic = voucherLogic; + } + + EthPrivateKey? get privateKey => _isInitialized ? _currentCredentials : null; + Config? get config => _isInitialized ? _currentConfig : null; + EthPrivateKey? get credentials => _isInitialized ? _currentCredentials : null; + EthereumAddress? get accountAddress => + _isInitialized ? _currentAccount : null; + + void setWalletStateInOtherLogic() { + if (_isInitialized) { + _notificationsLogic.setWalletState( + _currentConfig, _currentCredentials, _currentAccount); + _profilesLogic?.setWalletState( + _currentConfig, _currentCredentials, _currentAccount); + _voucherLogic?.setWalletState( + _currentConfig, _currentCredentials, _currentAccount); + ScanLogic.setGlobalWalletState( + _currentConfig, _currentCredentials, _currentAccount); + } } void updateMessage() { @@ -149,10 +189,12 @@ class WalletLogic extends WidgetsBindingObserver { Future fetchWalletConfig() async { try { - final config = - await _config.getWebConfig(dotenv.get('APP_LINK_SUFFIX'), null); + if (kIsWeb) { + final config = + await _config.getWebConfig(dotenv.get('APP_LINK_SUFFIX'), null); - _state.setWalletConfig(config); + _state.setWalletConfig(config); + } return; } catch (_) {} @@ -243,25 +285,30 @@ class WalletLogic extends WidgetsBindingObserver { final token = config.getPrimaryToken(); - await _wallet.initWeb( - EthereumAddress.fromHex(decodedSplit[0]), - cred.privateKey, - legacy: fromLegacy, - NativeCurrency( - name: token.name, - symbol: token.symbol, - decimals: token.decimals, - ), - config, - ); + // await _wallet.initWeb( + // EthereumAddress.fromHex(decodedSplit[0]), + // cred.privateKey, + // legacy: fromLegacy, + // NativeCurrency( + // name: token.name, + // symbol: token.symbol, + // decimals: token.decimals, + // ), + // config, + // ); + + await config.initContracts(); + + _currentCredentials = cred.privateKey; + _currentAccount = EthereumAddress.fromHex(decodedSplit[0]); + _currentConfig = config; + _isInitialized = true; await _accountDBService.init( - 'wallet_${_wallet.address.hexEip55}'); // TODO: migrate to account address instead + 'wallet_${_currentCredentials.address.hexEip55}'); // TODO: migrate to account address instead ContactsCache().init(_accountDBService); - final currency = _wallet.currency; - _state.setWalletConfig(config); _state.setWallet( @@ -269,26 +316,30 @@ class WalletLogic extends WidgetsBindingObserver { '0', name: 'Citizen Wallet', // on web, acts as a page's title, wallet is fitting here - address: _wallet.address.hexEip55, + address: _currentCredentials.address.hexEip55, alias: config.community.alias, - account: _wallet.account.hexEip55, + account: _currentAccount.hexEip55, currencyName: token.name, symbol: token.symbol, currencyLogo: config.community.logo, - decimalDigits: currency.decimals, + decimalDigits: token.decimals, locked: false, minter: false, ), ); - _wallet.getBalance().then((v) => _state.setWalletBalance(v)); - _wallet.minter.then((v) => _state.setWalletMinter(v)); + getBalance(config, _currentAccount) + .then((v) => _state.setWalletBalance(v)); + isMinter(config, _currentAccount).then((v) => _state.setWalletMinter(v)); + + // Set wallet state in other logic classes + setWalletStateInOtherLogic(); if (loadAdditionalData != null) await loadAdditionalData(); _theme.changeTheme(config.community.theme); - await _preferences.setLastWallet(_wallet.address.hexEip55); + await _preferences.setLastWallet(_currentCredentials.address.hexEip55); await _preferences.setLastAlias(config.community.alias); await _preferences.setLastWalletLink(encoded); @@ -330,7 +381,84 @@ class WalletLogic extends WidgetsBindingObserver { Config communityConfig = Config.fromJson(community.config); _theme.changeTheme(communityConfig.community.theme); - final dbWallet = await _encPrefs.getAccount(accAddress, alias); + var dbWallet = await _encPrefs.getAccount(accAddress, alias, ''); + + if (dbWallet != null && dbWallet.privateKey == null) { + final allAccounts = await _encPrefs.getAllAccounts(); + final accountWithKey = allAccounts + .where((acc) => + acc.address.hexEip55 == accAddress && + acc.alias == alias && + acc.privateKey != null) + .firstOrNull; + + if (accountWithKey != null) { + dbWallet = accountWithKey; + } + } + + if (dbWallet != null && dbWallet.accountFactoryAddress.isNotEmpty) { + var factoryWallet = await _encPrefs.getAccount( + accAddress, alias, dbWallet.accountFactoryAddress); + + if (factoryWallet != null && factoryWallet.privateKey == null) { + final allAccounts = await _encPrefs.getAllAccounts(); + final accountWithKey = allAccounts + .where((acc) => + acc.address?.hexEip55 == accAddress && + acc.alias == alias && + acc.privateKey != null) + .firstOrNull; + + if (accountWithKey != null) { + factoryWallet = accountWithKey; + } + } + + dbWallet = factoryWallet; + } else if (dbWallet != null && dbWallet.accountFactoryAddress.isEmpty) { + String defaultAccountFactoryAddress = + communityConfig.community.primaryAccountFactory.address; + + switch (alias) { + case 'gratitude': + defaultAccountFactoryAddress = + '0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD'; + break; + case 'bread': + defaultAccountFactoryAddress = + '0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9'; + break; + case 'wallet.commonshub.brussels': + defaultAccountFactoryAddress = + '0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87'; + break; + case 'wallet.sfluv.org': + defaultAccountFactoryAddress = + '0x5e987a6c4bb4239d498E78c34e986acf29c81E8e'; + break; + default: + if (defaultAccountFactoryAddress == + '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2') { + defaultAccountFactoryAddress = + '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185'; + } + break; + } + + final updatedAccount = DBAccount( + alias: dbWallet.alias, + address: dbWallet.address, + name: dbWallet.name, + username: dbWallet.username, + accountFactoryAddress: defaultAccountFactoryAddress, + privateKey: dbWallet.privateKey, + profile: dbWallet.profile, + ); + + await _encPrefs.setAccount(updatedAccount); + dbWallet = updatedAccount; + } if (dbWallet == null || dbWallet.privateKey == null) { throw NotFoundException(); @@ -345,9 +473,9 @@ class WalletLogic extends WidgetsBindingObserver { ); if (isWalletLoaded && - accAddress == _wallet.account.hexEip55 && - alias == _wallet.alias) { - _wallet.getBalance().then((v) { + accAddress == _currentAccount.hexEip55 && + alias == communityConfig.community.alias) { + getBalance(communityConfig, _currentAccount).then((v) { _state.updateWalletBalanceSuccess(v); }); @@ -371,36 +499,36 @@ class WalletLogic extends WidgetsBindingObserver { _state.setChainId(chainId); - await _wallet.init( - dbWallet.address, - dbWallet.privateKey!, - nativeCurrency, - communityConfig, - onNotify: (String message) { - _notificationsLogic.show(message); - }, - onFinished: (bool ok) { - _state.setWalletReady(ok); - _state.setWalletReadyLoading(false); - }, - ); + await communityConfig.initContracts(dbWallet.accountFactoryAddress); + + // Set current wallet state + _currentCredentials = dbWallet.privateKey!; + _currentAccount = dbWallet.address; + _currentConfig = communityConfig; + _isInitialized = true; await _accountDBService.init( - 'wallet_${_wallet.address.hexEip55}'); // TODO: migrate to account address instead + 'wallet_${_currentCredentials.address.hexEip55}'); // TODO: migrate to account address instead ContactsCache().init(_accountDBService); - _config - .isCommunityOnline( - communityConfig.chains[token.chainId.toString()]!.node.url) - .then((isOnline) { - communityConfig.online = isOnline; + _state.setWalletReady(true); + _state.setWalletReadyLoading(false); - _state.setWalletConfig(communityConfig); + final isOnline = await _config.isCommunityOnline( + communityConfig.chains[token.chainId.toString()]!.node.url); - _appDBService.communities - .updateOnlineStatus(communityConfig.community.alias, isOnline); - }); + communityConfig.online = isOnline; + _state.setWalletConfig(communityConfig); + await _appDBService.communities + .updateOnlineStatus(communityConfig.community.alias, isOnline); + + try { + final accountsService = getAccountsService(); + await accountsService.fixSafeAccounts(); + } catch (e) { + debugPrint('Error fixing accounts: $e'); + } _state.setWallet( CWWallet( @@ -408,7 +536,7 @@ class WalletLogic extends WidgetsBindingObserver { name: dbWallet.name, address: dbWallet.address.hexEip55, alias: dbWallet.alias, - account: _wallet.account.hexEip55, + account: _currentAccount.hexEip55, currencyName: token.name, symbol: token.symbol, currencyLogo: communityConfig.community.logo, @@ -419,7 +547,11 @@ class WalletLogic extends WidgetsBindingObserver { ), ); - _wallet.getBalance().then((v) => _state.setWalletBalance(v)); + getBalance(communityConfig, _currentAccount) + .then((v) => _state.setWalletBalance(v)); + + // Set wallet state in other logic classes + setWalletStateInOtherLogic(); loadAdditionalData(true); @@ -427,6 +559,8 @@ class WalletLogic extends WidgetsBindingObserver { await _preferences.setLastWallet(accAddress); await _preferences.setLastAlias(communityConfig.community.alias); + await _preferences + .setLastAccountFactoryAddress(dbWallet.accountFactoryAddress); return accAddress; } on NotFoundException { @@ -486,12 +620,16 @@ class WalletLogic extends WidgetsBindingObserver { privateKey: credentials, name: 'New ${token.symbol} Account', alias: communityConfig.community.alias, + accountFactoryAddress: + communityConfig.community.primaryAccountFactory.address, )); _theme.changeTheme(communityConfig.community.theme); await _preferences.setLastWallet(address.hexEip55); await _preferences.setLastAlias(communityConfig.community.alias); + await _preferences.setLastAccountFactoryAddress( + communityConfig.community.primaryAccountFactory.address); _state.createWalletSuccess( cwwallet, @@ -553,12 +691,16 @@ class WalletLogic extends WidgetsBindingObserver { privateKey: credentials, name: name, alias: communityConfig.community.alias, + accountFactoryAddress: + communityConfig.community.primaryAccountFactory.address, )); _theme.changeTheme(communityConfig.community.theme); await _preferences.setLastWallet(address.hexEip55); await _preferences.setLastAlias(communityConfig.community.alias); + await _preferences.setLastAccountFactoryAddress( + communityConfig.community.primaryAccountFactory.address); _state.createWalletSuccess(cwwallet); @@ -572,7 +714,7 @@ class WalletLogic extends WidgetsBindingObserver { Future editWallet(String address, String alias, String name) async { try { - final dbWallet = await _encPrefs.getAccount(address, alias); + final dbWallet = await _encPrefs.getAccount(address, alias, ''); if (dbWallet == null) { throw NotFoundException(); } @@ -582,6 +724,7 @@ class WalletLogic extends WidgetsBindingObserver { privateKey: dbWallet.privateKey, name: name, alias: dbWallet.alias, + accountFactoryAddress: dbWallet.accountFactoryAddress, )); loadDBWallets(); @@ -601,8 +744,8 @@ class WalletLogic extends WidgetsBindingObserver { Future transferEventSubscribe() async { try { - final alias = _wallet.alias; - if (alias == null) { + final alias = _currentConfig.community.alias; + if (alias.isEmpty) { return; } @@ -625,7 +768,7 @@ class WalletLogic extends WidgetsBindingObserver { _eventService = EventService( communityConfig.chains[token.chainId.toString()]!.node.wsUrl, token.address, - _wallet.transferEventSignature, + transferEventSignature(_currentConfig), ); _eventService!.setMessageHandler(handleTransferEvent); @@ -662,10 +805,11 @@ class WalletLogic extends WidgetsBindingObserver { case 'update': final log = Log.fromJson(event.data); - final tx = TransferEvent.fromLog(log, standard: _wallet.standard); + final tx = TransferEvent.fromLog(log, + standard: _currentConfig.getPrimaryToken().standard); // TODO: fix this on the websocket side - final myAccount = _wallet.account.hexEip55; + final myAccount = _currentAccount.hexEip55; if (tx.from.hexEip55 != myAccount && tx.to.hexEip55 != myAccount) { return; } @@ -682,18 +826,18 @@ class WalletLogic extends WidgetsBindingObserver { value: tx.value.toString(), data: tx.data != null ? jsonEncode(tx.data?.toJson()) : '', status: tx.status, - contract: _wallet.tokenAddress, + contract: _currentConfig.getPrimaryToken().address, )); final txList = [ CWTransaction( fromDoubleUnit( tx.value.toString(), - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ), id: tx.hash, hash: tx.txhash, - chainId: _wallet.chainId, + chainId: _currentConfig.chains.values.first.id, from: tx.from.hexEip55, to: tx.to.hexEip55, description: tx.data?.description ?? '', @@ -720,7 +864,7 @@ class WalletLogic extends WidgetsBindingObserver { } incomingTxNotification(txList.where((element) => - element.to == _wallet.account.hexEip55 && + element.to == _currentAccount.hexEip55 && element.state != TransactionState.success)); _state.incomingTransactionsRequestSuccess(txList); @@ -742,7 +886,7 @@ class WalletLogic extends WidgetsBindingObserver { if (incomingTxCount > 0 && incomingTxCount > _incomingTxCount) { // _lastIncomingTx = incomingTx.first; _notificationsLogic.show( - 'Receiving ${incomingTx.first.amount} ${_wallet.currency.symbol}...', + 'Receiving ${incomingTx.first.amount} ${_currentConfig.getPrimaryToken().symbol}...', ); } @@ -760,7 +904,7 @@ class WalletLogic extends WidgetsBindingObserver { // takes a password and returns a wallet Future returnWallet(String address, String alias) async { try { - final dbWallet = await _encPrefs.getAccount(address, alias); + final dbWallet = await _encPrefs.getAccount(address, alias, ''); if (dbWallet == null || dbWallet.privateKey == null) { throw NotFoundException(); } @@ -827,20 +971,20 @@ class WalletLogic extends WidgetsBindingObserver { final List txs = (await _accountDBService.transactions.getPreviousTransactions( maxDate, - _wallet.tokenAddress, + _currentConfig.getPrimaryToken().address, "0", // TODO: remove tokenId hardcode - _wallet.account.hexEip55, + _currentAccount.hexEip55, offset: 0, limit: limit, )) .map((dbtx) => CWTransaction( fromDoubleUnit( dbtx.value.toString(), - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ), id: dbtx.hash, hash: dbtx.txHash, - chainId: _wallet.chainId, + chainId: _currentConfig.chains.values.first.id, from: EthereumAddress.fromHex(dbtx.from).hexEip55, to: EthereumAddress.fromHex(dbtx.to).hexEip55, description: dbtx.data != '' @@ -857,7 +1001,9 @@ class WalletLogic extends WidgetsBindingObserver { if (txs.isEmpty || (txs.isNotEmpty && txs.first.date.isBefore(maxDate))) { // nothing in the db or slightly less than there could be, check remote - final (remoteTxs, _) = await _wallet.fetchErc20Transfers( + final (remoteTxs, _) = await fetchErc20Transfers( + _currentConfig, + _currentAccount.hexEip55, offset: 0, limit: limit, maxDate: maxDate, @@ -876,7 +1022,7 @@ class WalletLogic extends WidgetsBindingObserver { value: tx.value.toString(), data: tx.data != null ? jsonEncode(tx.data?.toJson()) : '', status: tx.status, - contract: _wallet.tokenAddress, + contract: _currentConfig.getPrimaryToken().address, ), ); @@ -888,11 +1034,11 @@ class WalletLogic extends WidgetsBindingObserver { txs.addAll(iterableRemoteTxs.map((dbtx) => CWTransaction( fromDoubleUnit( dbtx.value.toString(), - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ), id: dbtx.hash, hash: dbtx.txHash, - chainId: _wallet.chainId, + chainId: _currentConfig.chains.values.first.id, from: EthereumAddress.fromHex(dbtx.from).hexEip55, to: EthereumAddress.fromHex(dbtx.to).hexEip55, description: dbtx.data != '' @@ -914,7 +1060,7 @@ class WalletLogic extends WidgetsBindingObserver { maxDate: maxDate, ); - _wallet.getBalance().then((v) { + getBalance(_currentConfig, _currentAccount).then((v) { _state.updateWalletBalanceSuccess(v); }); @@ -936,20 +1082,20 @@ class WalletLogic extends WidgetsBindingObserver { final List txs = (await _accountDBService.transactions.getPreviousTransactions( maxDate, - _wallet.tokenAddress, + _currentConfig.getPrimaryToken().address, "0", // TODO: remove tokenId hardcode - _wallet.account.hexEip55, + _currentAccount.hexEip55, offset: offset, limit: limit, )) .map((dbtx) => CWTransaction( fromDoubleUnit( dbtx.value.toString(), - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ), id: dbtx.hash, hash: dbtx.txHash, - chainId: _wallet.chainId, + chainId: _currentConfig.chains.values.first.id, from: EthereumAddress.fromHex(dbtx.from).hexEip55, to: EthereumAddress.fromHex(dbtx.to).hexEip55, description: dbtx.data != '' @@ -966,7 +1112,9 @@ class WalletLogic extends WidgetsBindingObserver { if (txs.isEmpty || txs.length < limit) { // nothing in the db or slightly less than there could be, check remote - final (remoteTxs, _) = await _wallet.fetchErc20Transfers( + final (remoteTxs, _) = await fetchErc20Transfers( + _currentConfig, + _currentAccount.hexEip55, offset: offset, limit: limit, maxDate: maxDate, @@ -985,7 +1133,7 @@ class WalletLogic extends WidgetsBindingObserver { value: tx.value.toString(), data: tx.data != null ? jsonEncode(tx.data?.toJson()) : '', status: tx.status, - contract: _wallet.tokenAddress, + contract: _currentConfig.getPrimaryToken().address, ), ); @@ -997,11 +1145,11 @@ class WalletLogic extends WidgetsBindingObserver { txs.addAll(iterableRemoteTxs.map((dbtx) => CWTransaction( fromDoubleUnit( dbtx.value.toString(), - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ), id: dbtx.hash, hash: dbtx.txHash, - chainId: _wallet.chainId, + chainId: _currentConfig.chains.values.first.id, from: EthereumAddress.fromHex(dbtx.from).hexEip55, to: EthereumAddress.fromHex(dbtx.to).hexEip55, description: dbtx.data != '' @@ -1029,11 +1177,11 @@ class WalletLogic extends WidgetsBindingObserver { Future updateBalance() async { try { - if (_wallet.alias == null) { + if (_currentConfig.community.alias.isEmpty) { throw Exception('alias not found'); } - final balance = await _wallet.getBalance(); + final balance = await getBalance(_currentConfig, _currentAccount); final currentDoubleBalance = double.tryParse(_state.wallet?.balance ?? '0.0') ?? 0.0; @@ -1077,7 +1225,18 @@ class WalletLogic extends WidgetsBindingObserver { String message = '', String? id, bool clearInProgress = false, + bool useFallbackStrategy = false, }) async { + if (useFallbackStrategy) { + return sendTransactionWithFallback( + amount, + to, + message: message, + id: id, + clearInProgress: clearInProgress, + ); + } + return kIsWeb ? sendTransactionFromUnlocked( amount, @@ -1090,6 +1249,172 @@ class WalletLogic extends WidgetsBindingObserver { message: message, id: id, clearInProgress: clearInProgress); } + Future sendTransactionWithFallback( + String amount, + String to, { + String message = '', + String? id, + bool clearInProgress = false, + }) async { + final doubleAmount = amount.replaceAll(',', '.'); + final parsedAmount = toUnit( + doubleAmount, + decimals: _currentConfig.getPrimaryToken().decimals, + ); + + var tempId = id ?? '${pendingTransactionId}_${generateRandomId()}'; + + try { + _state.sendTransaction(id: id); + + if (to.isEmpty) { + _state.setInvalidAddress(true); + throw Exception('invalid address'); + } + + preSendingTransaction( + parsedAmount, + tempId, + to, + _currentAccount.hexEip55, + message: message, + ); + + final calldata = tokenTransferCallData( + _currentConfig, + _currentAccount, + to, + parsedAmount, + ); + + final accountFactoryAddress = await resolveAccountFactoryAddress(); + + final (hash, userop) = await prepareUseropWithFallback( + _currentConfig, + _currentAccount, + _currentCredentials, + [_currentConfig.getPrimaryToken().address], + [calldata], + accountFactoryAddress: accountFactoryAddress, + ); + + final args = { + 'from': _currentAccount.hexEip55, + 'to': to, + }; + if (_currentConfig.getPrimaryToken().standard == 'erc1155') { + args['operator'] = _currentAccount.hexEip55; + args['id'] = '0'; + args['amount'] = parsedAmount.toString(); + } else { + args['value'] = parsedAmount.toString(); + } + + final eventData = createEventData( + stringSignature: transferEventStringSignature(_currentConfig), + topic: transferEventSignature(_currentConfig), + args: args, + ); + + final txHash = await submitUseropWithRetry( + _currentConfig, + userop, + data: eventData, + extraData: message != '' ? TransferData(message) : null, + ); + + if (txHash == null) { + throw Exception('transaction failed'); + } + + sendingTransaction( + parsedAmount, + tempId, + to, + _currentAccount.hexEip55, + message: message, + ); + + if (userop.isFirst()) { + waitForTxSuccess(_currentConfig, txHash).then((value) { + if (!value) { + return; + } + _notificationsLogic.refreshPushToken(); + }); + } + + clearInputControllers(); + + _state.sendTransactionSuccess(null); + if (clearInProgress) { + _state.clearInProgressTransaction(notify: true); + } + + return txHash; + } on NetworkCongestedException { + _state.sendQueueAddTransaction( + CWTransaction.failed( + fromDoubleUnit( + parsedAmount.toString(), + decimals: _currentConfig.getPrimaryToken().decimals, + ), + id: tempId, + hash: '', + to: to, + description: message, + date: DateTime.now(), + error: NetworkCongestedException().message), + ); + } on NetworkInvalidBalanceException { + _state.sendQueueAddTransaction( + CWTransaction.failed( + fromDoubleUnit( + parsedAmount.toString(), + decimals: _currentConfig.getPrimaryToken().decimals, + ), + id: tempId, + hash: '', + to: to, + description: message, + date: DateTime.now(), + error: NetworkInvalidBalanceException().message), + ); + } on NetworkUnauthorizedException { + _state.sendQueueAddTransaction( + CWTransaction.failed( + fromDoubleUnit( + parsedAmount.toString(), + decimals: _currentConfig.getPrimaryToken().decimals, + ), + id: tempId, + hash: '', + to: to, + description: message, + date: DateTime.now(), + error: NetworkUnauthorizedException().message), + ); + } catch (e, s) { + _state.sendQueueAddTransaction( + CWTransaction.failed( + fromDoubleUnit( + parsedAmount.toString(), + decimals: _currentConfig.getPrimaryToken().decimals, + ), + id: tempId, + hash: '', + to: to, + description: message, + date: DateTime.now(), + error: NetworkUnknownException().message), + ); + } + + _state.sendTransactionError(); + + return null; + } + bool isInvalidAmount(String amount, {unlimited = false}) { if (unlimited) { return false; @@ -1098,7 +1423,7 @@ class WalletLogic extends WidgetsBindingObserver { final balance = double.tryParse(_state.wallet?.balance ?? '0.0') ?? 0.0; final doubleAmount = double.parse(toUnit( amount.replaceAll(',', '.'), - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ).toString()); return doubleAmount == 0 || doubleAmount > balance; @@ -1125,11 +1450,11 @@ class WalletLogic extends WidgetsBindingObserver { CWTransaction.sending( fromDoubleUnit( amount.toString(), - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ), id: tempId, hash: '', - chainId: _wallet.chainId, + chainId: _currentConfig.chains.values.first.id, from: from, to: to, description: message, @@ -1149,11 +1474,11 @@ class WalletLogic extends WidgetsBindingObserver { CWTransaction.pending( fromDoubleUnit( amount.toString(), - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ), id: hash, hash: '', - chainId: _wallet.chainId, + chainId: _currentConfig.chains.values.first.id, to: to, from: from, description: message, @@ -1172,7 +1497,7 @@ class WalletLogic extends WidgetsBindingObserver { final doubleAmount = amount.replaceAll(',', '.'); final parsedAmount = toUnit( doubleAmount, - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ); var tempId = id ?? '${pendingTransactionId}_${generateRandomId()}'; @@ -1189,27 +1514,34 @@ class WalletLogic extends WidgetsBindingObserver { parsedAmount, tempId, to, - _wallet.account.hexEip55, + _currentAccount.hexEip55, message: message, ); // TODO: token id should be set - final calldata = _wallet.tokenTransferCallData( + final calldata = tokenTransferCallData( + _currentConfig, + _currentAccount, to, parsedAmount, ); - final (hash, userop) = await _wallet.prepareUserop( - [_wallet.tokenAddress], + final accountFactoryAddress = await resolveAccountFactoryAddress(); + final (hash, userop) = await prepareUserop( + _currentConfig, + _currentAccount, + _currentCredentials, + [_currentConfig.getPrimaryToken().address], [calldata], + accountFactoryAddress: accountFactoryAddress, ); final args = { - 'from': _wallet.account.hexEip55, + 'from': _currentAccount.hexEip55, 'to': to, }; - if (_wallet.standard == 'erc1155') { - args['operator'] = _wallet.account.hexEip55; + if (_currentConfig.getPrimaryToken().standard == 'erc1155') { + args['operator'] = _currentAccount.hexEip55; args['id'] = '0'; args['amount'] = parsedAmount.toString(); } else { @@ -1217,12 +1549,13 @@ class WalletLogic extends WidgetsBindingObserver { } final eventData = createEventData( - stringSignature: _wallet.transferEventStringSignature, - topic: _wallet.transferEventSignature, + stringSignature: transferEventStringSignature(_currentConfig), + topic: transferEventSignature(_currentConfig), args: args, ); - final txHash = await _wallet.submitUserop( + final txHash = await submitUserop( + _currentConfig, userop, data: eventData, extraData: message != '' ? TransferData(message) : null, @@ -1236,19 +1569,18 @@ class WalletLogic extends WidgetsBindingObserver { parsedAmount, tempId, to, - _wallet.account.hexEip55, + _currentAccount.hexEip55, message: message, ); if (userop.isFirst()) { - // an account was created, update push token in the background - _wallet.waitForTxSuccess(txHash).then((value) { + waitForTxSuccess(_currentConfig, txHash).then((value) { if (!value) { return; } // the account exists, enable push notifications - _notificationsLogic.updatePushToken(); + _notificationsLogic.refreshPushToken(); }); } @@ -1280,7 +1612,7 @@ class WalletLogic extends WidgetsBindingObserver { CWTransaction.failed( fromDoubleUnit( parsedAmount.toString(), - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ), id: tempId, hash: '', @@ -1294,7 +1626,7 @@ class WalletLogic extends WidgetsBindingObserver { CWTransaction.failed( fromDoubleUnit( parsedAmount.toString(), - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ), id: tempId, hash: '', @@ -1304,13 +1636,13 @@ class WalletLogic extends WidgetsBindingObserver { error: NetworkInvalidBalanceException().message), ); } catch (e, s) { - print('error: $e'); - print('stack: $s'); + debugPrint('error: $e'); + debugPrint('stack: $s'); _state.sendQueueAddTransaction( CWTransaction.failed( fromDoubleUnit( parsedAmount.toString(), - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ), id: tempId, hash: '', @@ -1341,13 +1673,20 @@ class WalletLogic extends WidgetsBindingObserver { final calldata = hexToBytes(data); - final (hash, userop) = await _wallet.prepareUserop( + final accountFactoryAddress = await resolveAccountFactoryAddress(); + + final (hash, userop) = await prepareUserop( + _currentConfig, + _currentAccount, + _currentCredentials, [to], [calldata], value: BigInt.parse(value.isEmpty ? '0' : value), + accountFactoryAddress: accountFactoryAddress, ); - final txHash = await _wallet.submitUserop( + final txHash = await submitUserop( + _currentConfig, userop, ); if (txHash == null) { @@ -1356,14 +1695,13 @@ class WalletLogic extends WidgetsBindingObserver { } if (userop.isFirst()) { - // an account was created, update push token in the background - _wallet.waitForTxSuccess(txHash).then((value) { + waitForTxSuccess(_currentConfig, txHash).then((value) { if (!value) { return; } // the account exists, enable push notifications - _notificationsLogic.updatePushToken(); + _notificationsLogic.refreshPushToken(); }); } @@ -1377,8 +1715,8 @@ class WalletLogic extends WidgetsBindingObserver { } on NetworkInvalidBalanceException { // } catch (e, s) { - print('error: $e'); - print('stack: $s'); + debugPrint('error: $e'); + debugPrint('stack: $s'); } _state.sendCallDataTransactionError(); @@ -1396,7 +1734,7 @@ class WalletLogic extends WidgetsBindingObserver { final doubleAmount = amount.replaceAll(',', '.'); final parsedAmount = toUnit( doubleAmount, - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ); var tempId = id ?? '${pendingTransactionId}_${generateRandomId()}'; @@ -1413,26 +1751,33 @@ class WalletLogic extends WidgetsBindingObserver { parsedAmount, tempId, to, - _wallet.account.hexEip55, + _currentAccount.hexEip55, ); - // TODO: token id should be set - final calldata = _wallet.tokenTransferCallData( + final calldata = tokenTransferCallData( + _currentConfig, + _currentAccount, to, parsedAmount, ); - final (hash, userop) = await _wallet.prepareUserop( - [_wallet.tokenAddress], + final accountFactoryAddress = await resolveAccountFactoryAddress(); + + final (hash, userop) = await prepareUserop( + _currentConfig, + _currentAccount, + _currentCredentials, + [_currentConfig.getPrimaryToken().address], [calldata], + accountFactoryAddress: accountFactoryAddress, ); final args = { - 'from': _wallet.account.hexEip55, + 'from': _currentAccount.hexEip55, 'to': to, }; - if (_wallet.standard == 'erc1155') { - args['operator'] = _wallet.account.hexEip55; + if (_currentConfig.getPrimaryToken().standard == 'erc1155') { + args['operator'] = _currentAccount.hexEip55; args['id'] = '0'; args['amount'] = parsedAmount.toString(); } else { @@ -1440,12 +1785,13 @@ class WalletLogic extends WidgetsBindingObserver { } final eventData = createEventData( - stringSignature: _wallet.transferEventStringSignature, - topic: _wallet.transferEventSignature, + stringSignature: transferEventStringSignature(_currentConfig), + topic: transferEventSignature(_currentConfig), args: args, ); - final txHash = await _wallet.submitUserop( + final txHash = await submitUserop( + _currentConfig, userop, data: eventData, extraData: message != '' ? TransferData(message) : null, @@ -1459,19 +1805,19 @@ class WalletLogic extends WidgetsBindingObserver { parsedAmount, tempId, to, - _wallet.account.hexEip55, + _currentAccount.hexEip55, message: message, ); if (userop.isFirst()) { // an account was created, update push token in the background - _wallet.waitForTxSuccess(txHash).then((value) { + waitForTxSuccess(_currentConfig, txHash).then((value) { if (!value) { return; } // the account exists, enable push notifications - _notificationsLogic.updatePushToken(); + _notificationsLogic.refreshPushToken(); }); } @@ -1488,7 +1834,7 @@ class WalletLogic extends WidgetsBindingObserver { CWTransaction.failed( fromDoubleUnit( parsedAmount.toString(), - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ), id: tempId, hash: '', @@ -1502,7 +1848,7 @@ class WalletLogic extends WidgetsBindingObserver { CWTransaction.failed( fromDoubleUnit( parsedAmount.toString(), - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ), id: tempId, hash: '', @@ -1516,7 +1862,7 @@ class WalletLogic extends WidgetsBindingObserver { CWTransaction.failed( fromDoubleUnit( parsedAmount.toString(), - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ), id: tempId, hash: '', @@ -1541,7 +1887,7 @@ class WalletLogic extends WidgetsBindingObserver { final doubleAmount = amount.replaceAll(',', '.'); final parsedAmount = toUnit( doubleAmount, - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ); var tempId = id ?? '${pendingTransactionId}_${generateRandomId()}'; @@ -1563,22 +1909,29 @@ class WalletLogic extends WidgetsBindingObserver { ); // TODO: token id should be set - final calldata = _wallet.tokenMintCallData( + final calldata = tokenMintCallData( + _currentConfig, to, parsedAmount, ); - final (_, userop) = await _wallet.prepareUserop( - [_wallet.tokenAddress], + final accountFactoryAddress = await resolveAccountFactoryAddress(); + + final (_, userop) = await prepareUserop( + _currentConfig, + _currentAccount, + _currentCredentials, + [_currentConfig.getPrimaryToken().address], [calldata], + accountFactoryAddress: accountFactoryAddress, ); final args = { 'from': zeroAddress, 'to': to, }; - if (_wallet.standard == 'erc1155') { - args['operator'] = _wallet.account.hexEip55; + if (_currentConfig.getPrimaryToken().standard == 'erc1155') { + args['operator'] = _currentAccount.hexEip55; args['id'] = '0'; args['amount'] = parsedAmount.toString(); } else { @@ -1586,12 +1939,13 @@ class WalletLogic extends WidgetsBindingObserver { } final eventData = createEventData( - stringSignature: _wallet.transferEventStringSignature, - topic: _wallet.transferEventSignature, + stringSignature: transferEventStringSignature(_currentConfig), + topic: transferEventSignature(_currentConfig), args: args, ); - final txHash = await _wallet.submitUserop( + final txHash = await submitUserop( + _currentConfig, userop, data: eventData, extraData: message != '' ? TransferData(message) : null, @@ -1611,13 +1965,13 @@ class WalletLogic extends WidgetsBindingObserver { if (userop.isFirst()) { // an account was created, update push token in the background - _wallet.waitForTxSuccess(txHash).then((value) { + waitForTxSuccess(_currentConfig, txHash).then((value) { if (!value) { return; } // the account exists, enable push notifications - _notificationsLogic.updatePushToken(); + _notificationsLogic.refreshPushToken(); }); } @@ -1636,11 +1990,11 @@ class WalletLogic extends WidgetsBindingObserver { CWTransaction.failed( fromDoubleUnit( amount.toString(), - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ), id: tempId, hash: '', - chainId: _wallet.chainId, + chainId: _currentConfig.chains.values.first.id, to: to, from: zeroAddress, description: message.isNotEmpty ? message : 'Failed to mint token', @@ -1693,7 +2047,7 @@ class WalletLogic extends WidgetsBindingObserver { void setMaxAmount() { _amountController.text = fromDoubleUnit( _state.wallet?.balance ?? '0.0', - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ); updateAmount(); } @@ -1778,7 +2132,7 @@ class WalletLogic extends WidgetsBindingObserver { if (format == QRFormat.eip681Transfer) { final amount = fromDoubleUnit( parsedData.amount!, - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, ); _amountController.text = amount; } else { @@ -1793,7 +2147,8 @@ class WalletLogic extends WidgetsBindingObserver { addressToUse = parsedData.address; } catch (_) { String username = parsedData.address; - ProfileV1? profile = await _wallet.getProfileByUsername(username); + ProfileV1? profile = + await getProfileByUsername(_currentConfig, username); if (profile != null) { addressToUse = profile.account; } else { @@ -1835,11 +2190,12 @@ class WalletLogic extends WidgetsBindingObserver { try { updateListenerAmount(); - if (_wallet.alias == null) { + if (_currentConfig.community.alias.isEmpty) { throw Exception('alias not found'); } - final community = await _appDBService.communities.get(_wallet.alias!); + final community = + await _appDBService.communities.get(_currentConfig.community.alias); if (community == null) { throw Exception('community not found'); @@ -1851,12 +2207,12 @@ class WalletLogic extends WidgetsBindingObserver { if (onlyHex != null && onlyHex) { _state.updateReceiveQR( - '$url?sendto=${_wallet.account.hexEip55}@${communityConfig.community.alias}'); + '$url?sendto=${_currentAccount.hexEip55}@${communityConfig.community.alias}'); return; } String params = - 'sendto=${_wallet.account.hexEip55}@${communityConfig.community.alias}'; + 'sendto=${_currentAccount.hexEip55}@${communityConfig.community.alias}'; if (_amountController.value.text.isNotEmpty) { final double amount = _amountController.value.text.isEmpty @@ -1891,7 +2247,7 @@ class WalletLogic extends WidgetsBindingObserver { void updateWalletQR() async { try { - _state.updateWalletQR(_wallet.account.hexEip55); + _state.updateWalletQR(_currentAccount.hexEip55); return; } catch (_) {} @@ -1904,7 +2260,7 @@ class WalletLogic extends WidgetsBindingObserver { void copyWalletAccount() { try { - Clipboard.setData(ClipboardData(text: _wallet.account.hexEip55)); + Clipboard.setData(ClipboardData(text: _currentAccount.hexEip55)); } catch (_) {} } @@ -1956,6 +2312,169 @@ class WalletLogic extends WidgetsBindingObserver { _state.loadWalletsError(); } + Future getAccountFactoryAddressWithHiddenCommunityFallback() async { + try { + final dbAccount = await _encPrefs.getAccount( + _currentAccount.hexEip55, _currentConfig.community.alias, ''); + + if (dbAccount?.accountFactoryAddress != null && + dbAccount!.accountFactoryAddress.isNotEmpty) { + return dbAccount.accountFactoryAddress; + } + + final oldAccountFactories = { + 'bread': '0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9', + 'gratitude': '0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD', + 'wallet.commonshub.brussels': + '0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87', + 'wallet.sfluv.org': '0x5e987a6c4bb4239d498E78c34e986acf29c81E8e', + 'wallet.pay.brussels': '0xBABCf159c4e3186cf48e4a48bC0AeC17CF9d90FE', + }; + + final oldFactory = oldAccountFactories[_currentConfig.community.alias]; + if (oldFactory != null) { + if (dbAccount != null) { + final updatedAccount = DBAccount( + alias: dbAccount.alias, + address: dbAccount.address, + name: dbAccount.name, + username: dbAccount.username, + accountFactoryAddress: oldFactory, + privateKey: dbAccount.privateKey, + profile: dbAccount.profile, + ); + await _encPrefs.setAccount(updatedAccount); + } + return oldFactory; + } + + final allAccounts = await _encPrefs.getAllAccounts(); + final matchingAccount = allAccounts + .where((acc) => acc.address.hexEip55 == _currentAccount.hexEip55) + .firstOrNull; + + if (matchingAccount != null && + matchingAccount.accountFactoryAddress.isNotEmpty) { + return matchingAccount.accountFactoryAddress; + } + + final legacyAccount = await _encPrefs.getAccount( + _currentAccount.hexEip55, + _currentConfig.community.alias, + '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2'); + if (legacyAccount != null) { + const newFactoryAddress = '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185'; + + final updatedAccount = DBAccount( + alias: legacyAccount.alias, + address: legacyAccount.address, + name: legacyAccount.name, + username: legacyAccount.username, + accountFactoryAddress: newFactoryAddress, + privateKey: legacyAccount.privateKey, + profile: legacyAccount.profile, + ); + await _encPrefs.setAccount(updatedAccount); + + return newFactoryAddress; + } + + return _currentConfig.community.primaryAccountFactory.address; + } catch (e) { + return _currentConfig.community.primaryAccountFactory.address; + } + } + + Future resolveAccountFactoryAddress() async { + try { + if (_currentConfig == null) { + throw Exception('Current config is null'); + } + + if (_currentConfig.community.primaryAccountFactory.address.isEmpty) { + throw Exception('Primary account factory address is empty'); + } + + if (_currentAccount == null) { + throw Exception('Current account is null'); + } + + try { + final factoryAddress = + await getAccountFactoryAddressWithHiddenCommunityFallback(); + if (factoryAddress.isNotEmpty) { + return factoryAddress; + } + } catch (e) {} + + try { + final possibleFactories = [ + '', + _currentConfig.community.primaryAccountFactory.address, + '0x940Cbb155161dc0C4aade27a4826a16Ed8ca0cb2', + '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185', + '0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9', + '0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD', + '0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87', + '0x5e987a6c4bb4239d498E78c34e986acf29c81E8e', + ]; + + for (final factory in possibleFactories) { + try { + final dbAccount = await _encPrefs.getAccount( + _currentAccount.hexEip55, + _currentConfig.community.alias, + factory); + if (dbAccount != null && + dbAccount.accountFactoryAddress.isNotEmpty) { + return dbAccount.accountFactoryAddress; + } + } catch (e) {} + } + } catch (e) {} + + try { + final communityMappings = { + 'bread': '0xAE76B1C6818c1DD81E20ccefD3e72B773068ABc9', + 'gratitude': '0xAE6E18a9Cd26de5C8f89B886283Fc3f0bE5f04DD', + 'wallet.commonshub.brussels': + '0x307A9456C4057F7C7438a174EFf3f25fc0eA6e87', + 'wallet.sfluv.org': '0x5e987a6c4bb4239d498E78c34e986acf29c81E8e', + }; + + final mappedFactory = communityMappings[_currentConfig.community.alias]; + if (mappedFactory != null) { + return mappedFactory; + } + } catch (e) {} + + try { + final allAccounts = await _encPrefs.getAllAccounts(); + final matchingAccounts = allAccounts + .where((acc) => acc.address.hexEip55 == _currentAccount.hexEip55) + .toList(); + + for (final account in matchingAccounts) { + if (account.accountFactoryAddress.isNotEmpty) { + return account.accountFactoryAddress; + } + } + } catch (e) {} + + try { + final primaryFactory = + _currentConfig.community.primaryAccountFactory.address; + if (primaryFactory.isNotEmpty) { + return primaryFactory; + } + } catch (e) {} + + return '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185'; + } catch (e) { + return '0x7cC54D54bBFc65d1f0af7ACee5e4042654AF8185'; + } + } + void prepareReplyTransaction(String address) { try { _addressController.text = address; @@ -1983,7 +2502,7 @@ class WalletLogic extends WidgetsBindingObserver { _amountController.text = double.parse('${toUnit( amount, - decimals: _wallet.currency.decimals, + decimals: _currentConfig.getPrimaryToken().decimals, )}') .toStringAsFixed(2); @@ -2000,7 +2519,8 @@ class WalletLogic extends WidgetsBindingObserver { try { final now = DateTime.now().toUtc().add(const Duration(seconds: 30)); - final redirectUrl = '$appUniversalURL/?alias=${_wallet.alias}'; + final redirectUrl = + '$appUniversalURL/?alias=${_currentConfig.community.alias}'; final encodedRedirectUrl = Uri.encodeComponent(redirectUrl); final parsedURL = Uri.parse(appUniversalURL); @@ -2014,7 +2534,7 @@ class WalletLogic extends WidgetsBindingObserver { } return ( - '${pluginConfig.url}?account=${_wallet.account.hexEip55}&expiry=${now.millisecondsSinceEpoch}&redirectUrl=$encodedRedirectUrl&signature=0x123', + '${pluginConfig.url}?account=${_currentAccount.hexEip55}&expiry=${now.millisecondsSinceEpoch}&redirectUrl=$encodedRedirectUrl&signature=0x123', parsedURL.scheme != 'https' ? parsedURL.scheme : null, redirectUrl, ); @@ -2056,7 +2576,7 @@ class WalletLogic extends WidgetsBindingObserver { } if (extraParams != null) { - plugin.updateUrl('${plugin.url}?$extraParams'); + // plugin.updateUrl('${plugin.url}?$extraParams'); } return plugin; @@ -2075,7 +2595,7 @@ class WalletLogic extends WidgetsBindingObserver { void cleanupWalletService() { try { - _wallet.dispose(); + // Cleanup any remaining resources } catch (_) {} transferEventUnsubscribe(); } @@ -2096,16 +2616,21 @@ class WalletLogic extends WidgetsBindingObserver { Future didChangeAppLifecycleState(AppLifecycleState state) async { switch (state) { case AppLifecycleState.resumed: + if (!_isInitialized) { + return; + } + resumeFetching(); loadTransactions(); - if (_wallet.alias == null) { + if (_currentConfig.community.alias.isEmpty) { return; } await updateBalance(); - final community = await _appDBService.communities.get(_wallet.alias!); + final community = + await _appDBService.communities.get(_currentConfig.community.alias); if (community == null) { return; @@ -2113,6 +2638,18 @@ class WalletLogic extends WidgetsBindingObserver { Config communityConfig = Config.fromJson(community.config); + try { + final remoteConfig = await _config + .getSingleCommunityConfig(communityConfig.configLocation); + if (remoteConfig != null) { + await _appDBService.communities + .upsert([DBCommunity.fromConfig(remoteConfig)]); + communityConfig = remoteConfig; + } + } catch (e) { + debugPrint('Error fetching single community config: $e'); + } + final token = communityConfig.getPrimaryToken(); communityConfig.online = await _config.isCommunityOnline( @@ -2123,6 +2660,13 @@ class WalletLogic extends WidgetsBindingObserver { _state.setWalletConfig(communityConfig); + try { + final accountsService = getAccountsService(); + await accountsService.fixSafeAccounts(); + } catch (e) { + debugPrint('Error fixing accounts: $e'); + } + break; default: pauseFetching(); @@ -2152,10 +2696,10 @@ class WalletLogic extends WidgetsBindingObserver { )); try { - final isMinter = await _wallet.minter; - _state.setWalletMinter(isMinter); + final isMinterResult = await isMinter(_currentConfig, _currentAccount); + _state.setWalletMinter(isMinterResult); - if (isMinter) { + if (isMinterResult) { actionsToAdd.add(ActionButton( label: 'Minter', buttonType: ActionButtonType.minter, @@ -2164,7 +2708,7 @@ class WalletLogic extends WidgetsBindingObserver { } catch (_) {} try { - final alias = _wallet.alias ?? ""; + final alias = _currentConfig.community.alias; final community = await _appDBService.communities.get(alias); if (community != null) { diff --git a/lib/state/wallet_connect/logic.dart b/lib/state/wallet_connect/logic.dart index 059752dd..84c332d2 100644 --- a/lib/state/wallet_connect/logic.dart +++ b/lib/state/wallet_connect/logic.dart @@ -7,6 +7,7 @@ import 'package:citizenwallet/state/notifications/state.dart'; import 'package:citizenwallet/state/wallet_connect/state.dart'; import 'package:provider/provider.dart'; import 'package:citizenwallet/widgets/wallet_session_approval.dart'; +import 'package:citizenwallet/services/config/config.dart'; import 'dart:async'; class WalletKitLogic with WidgetsBindingObserver { @@ -17,6 +18,11 @@ class WalletKitLogic with WidgetsBindingObserver { NotificationsLogic? _notificationsLogic; WalletConnectState? _state; + // Wallet state for handlers + late Config _config; + late EthereumAddress _account; + late EthPrivateKey _credentials; + ReownWalletKit? get connectClient => _service.client; SessionProposalEvent? get currentProposal => _currentProposal; @@ -35,6 +41,13 @@ class WalletKitLogic with WidgetsBindingObserver { _state = context.read(); } + void setWalletState( + Config config, EthPrivateKey credentials, EthereumAddress account) { + _config = config; + _credentials = credentials; + _account = account; + } + Future initialize() async { try { await _service.initialize(); @@ -356,7 +369,8 @@ class WalletKitLogic with WidgetsBindingObserver { } Future _personalSignHandler(String topic, dynamic params) async { - return await _service.personalSignHandler(topic, params, true); + return await _service.personalSignHandler( + topic, params, true, _credentials); } Future _ethSendTransactionHandler(String topic, dynamic params) async { @@ -369,7 +383,8 @@ class WalletKitLogic with WidgetsBindingObserver { if (currentSession == null) { debugPrint('No active session found for topic: $topic'); - return await _service.ethSendTransactionHandler(topic, params, false); + return await _service.ethSendTransactionHandler( + topic, params, false, _config, _account, _credentials); } final List paramsList = params as List; @@ -381,7 +396,7 @@ class WalletKitLogic with WidgetsBindingObserver { String? transactionType; try { final contractData = await _service.getContractDetails(transaction['to']); - if (contractData != null && contractData.abi != null) { + if (contractData != null && contractData.abi.isNotEmpty) { transactionType = _service.getTransactionTypeFromAbi( contractData.abi, transaction['data'] ?? '', @@ -400,11 +415,13 @@ class WalletKitLogic with WidgetsBindingObserver { event: currentSession, transactionType: transactionType ?? '', onConfirm: () async { - await _service.ethSendTransactionHandler(topic, params, true); + await _service.ethSendTransactionHandler( + topic, params, true, _config, _account, _credentials); Navigator.of(_context!).pop(true); }, onCancel: () async { - await _service.ethSendTransactionHandler(topic, params, false); + await _service.ethSendTransactionHandler( + topic, params, false, _config, _account, _credentials); Navigator.of(_context!).pop(false); }, ), diff --git a/lib/utils/migration_modal.dart b/lib/utils/migration_modal.dart new file mode 100644 index 00000000..a64d93d0 --- /dev/null +++ b/lib/utils/migration_modal.dart @@ -0,0 +1,44 @@ +import 'package:citizenwallet/modals/landing/migration_modal.dart'; +import 'package:citizenwallet/services/preferences/preferences.dart'; +import 'package:citizenwallet/state/app/state.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:provider/provider.dart'; + +class MigrationModalUtils { + static Future showMigrationModalIfNeeded(BuildContext context) async { + final preferences = PreferencesService(); + final appState = context.read(); + + if (!appState.migrationRequired) { + return; + } + + final dismissalCount = preferences.migrationModalDismissalCount; + if (dismissalCount >= 3) { + return; + } + + await showCupertinoModalBottomSheet( + context: context, + topRadius: const Radius.circular(40), + useRootNavigator: true, + builder: (context) => const MigrationModal(), + isDismissible: false, + enableDrag: false, + ); + } + + static Future showMigrationModal(BuildContext context, + {bool isWalletScreen = false}) async { + // Always show the modal when manually triggered from the migrate button + await showCupertinoModalBottomSheet( + context: context, + topRadius: const Radius.circular(40), + useRootNavigator: true, + builder: (context) => MigrationModal(isWalletScreen: isWalletScreen), + isDismissible: false, + enableDrag: false, + ); + } +} diff --git a/lib/widgets/communities/community_row.dart b/lib/widgets/communities/community_row.dart index 78111821..5ed71fa7 100644 --- a/lib/widgets/communities/community_row.dart +++ b/lib/widgets/communities/community_row.dart @@ -3,7 +3,6 @@ import 'package:citizenwallet/theme/provider.dart'; import 'package:citizenwallet/widgets/coin_logo.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_svg/svg.dart'; class CommunityRow extends StatelessWidget { final Config config; diff --git a/lib/widgets/scanner/scanner.dart b/lib/widgets/scanner/scanner.dart index b9fc94f1..1f964538 100644 --- a/lib/widgets/scanner/scanner.dart +++ b/lib/widgets/scanner/scanner.dart @@ -4,7 +4,6 @@ import 'package:citizenwallet/theme/provider.dart'; import 'package:citizenwallet/utils/delay.dart'; import 'package:citizenwallet/widgets/borders/border_painter.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:lottie/lottie.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; diff --git a/lib/widgets/wallet_transaction_modal.dart b/lib/widgets/wallet_transaction_modal.dart index 1bea87ea..4e78edca 100644 --- a/lib/widgets/wallet_transaction_modal.dart +++ b/lib/widgets/wallet_transaction_modal.dart @@ -32,7 +32,6 @@ class _WalletTransactionModalState extends State { initState() { super.initState(); - print(widget.event); } void _handleConfirm() { diff --git a/lib/widgets/webview/connected_webview_modal.dart b/lib/widgets/webview/connected_webview_modal.dart index a6b3dfb7..99c9e8a3 100644 --- a/lib/widgets/webview/connected_webview_modal.dart +++ b/lib/widgets/webview/connected_webview_modal.dart @@ -11,7 +11,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:go_router/go_router.dart'; import 'package:citizenwallet/widgets/webview/webview_navigation.dart'; -import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; class ConnectedWebViewModal extends StatefulWidget { final String? modalKey; @@ -79,7 +78,6 @@ class _WebViewModalState extends State { void handleConsoleMessage( InAppWebViewController controller, ConsoleMessage message) { - print('>>>> ${message.message}'); } Future shouldOverrideUrlLoading( @@ -139,7 +137,7 @@ class _WebViewModalState extends State { HapticFeedback.heavyImpact(); - widget.profilesLogic.getProfile(parsedData.address); + widget.profilesLogic.getLocalProfile(parsedData.address); final dismiss = await showCupertinoModalPopup( context: context, @@ -169,7 +167,7 @@ class _WebViewModalState extends State { HapticFeedback.heavyImpact(); - widget.profilesLogic.getProfile(parsedData.address); + widget.profilesLogic.getLocalProfile(parsedData.address); final dismiss = await showCupertinoModalPopup( context: context, diff --git a/lib/widgets/webview/webview_navigation.dart b/lib/widgets/webview/webview_navigation.dart index d132b192..c5247cef 100644 --- a/lib/widgets/webview/webview_navigation.dart +++ b/lib/widgets/webview/webview_navigation.dart @@ -1,6 +1,5 @@ import 'package:flutter/cupertino.dart'; import 'package:citizenwallet/theme/provider.dart'; -import 'package:citizenwallet/l10n/app_localizations.dart'; class WebViewNavigation extends StatelessWidget { final String? url; diff --git a/pubspec.lock b/pubspec.lock index 827cd02d..1c61ba3d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -302,10 +302,10 @@ packages: dependency: "direct main" description: name: credential_manager - sha256: "91d54ed2eade14880e381edc604722d400966b6f2e50e87c38eb02ff988c5238" + sha256: "4dbd38e17fb9a8fd30d591826246b7d327d69d7c33bd15e8a9192aa40c178374" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" cross_file: dependency: transitive description: @@ -914,26 +914,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -1592,26 +1592,26 @@ packages: dependency: transitive description: name: test - sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" url: "https://pub.dev" source: hosted - version: "1.25.15" + version: "1.26.2" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" test_core: dependency: transitive description: name: test_core - sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" url: "https://pub.dev" source: hosted - version: "0.6.8" + version: "0.6.11" timeago: dependency: "direct main" description: @@ -1752,10 +1752,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -1861,5 +1861,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.2 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 845f00e8..6a73850e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: citizenwallet -version: 2.0.25+272 +version: 2.0.26+274 publish_to: none description: A mobile wallet for your community. environment: @@ -61,7 +61,7 @@ dependencies: firebase_messaging: ^15.1.3 firebase_core: ^3.6.0 flutter_inappwebview: ^6.0.0 - credential_manager: ^2.0.1 + credential_manager: ^2.0.4 googleapis: ^14.0.0 path_provider: ^2.1.2 icloud_storage: ^2.2.0 @@ -128,10 +128,13 @@ flutter: - assets/icons/citizenbank.svg - assets/icons/language-svgrepo-com.svg - assets/icons/contactless.svg + - assets/config/v5/communities.json + - assets/config/v5/communities.test.json + - assets/config/v5/debug.json + - assets/icons/switch_accounts.svg - assets/config/v4/communities.json - assets/config/v4/communities.test.json - assets/config/v4/debug.json - - assets/icons/switch_accounts.svg - assets/config/v3/communities.json - assets/config/v3/communities.test.json - assets/config/v3/legacy_4337_bundlers.json diff --git a/test/router_extension.dart b/test/router_extension.dart index 4bcbd02b..453068c1 100644 --- a/test/router_extension.dart +++ b/test/router_extension.dart @@ -1,7 +1,6 @@ import 'package:citizenwallet/l10n/app_localizations.dart'; import 'package:citizenwallet/state/theme/state.dart'; import 'package:citizenwallet/theme/provider.dart'; -import 'package:citizenwallet/l10n/app_localizations.dart'; import 'package:citizenwallet/state/state.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/scheduler.dart';