diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 55ebd86..eddad47 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.27.3' + flutter-version: '3.32.4' - name: Install dependencies run: flutter pub get diff --git a/.vscode/launch.json b/.vscode/launch.json index 987f302..8b1deac 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,9 +7,10 @@ "type": "dart", "program": "lib/main.dart", "toolArgs": [ - "run" + "run", + "--dart-define=API_KEY=your_api_key_here" ], "args": [] } ] -} \ No newline at end of file +} \ No newline at end of file diff --git a/README.md b/README.md index d00bba3..5569217 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,17 @@ flutter run ``` --- +## 🛠 Environment Variables + +This project requires the following environment variable for proper operation: + +- `API_KEY`: Your API key for the Avnu provider. This is used by `AvnuRepositoryImpl` to connect to the StarkNet node. Set this value in your environment or pass it at build time. + +Example (for development): +```bash +flutter run --dart-define=API_KEY=your_api_key_here +``` + ## Contribute to StarkWager We welcome contributions to StarkWager! Check out our [Contributor's Guide](https://github.com/stakepoint/stark-wager-mobile/blob/dev/CONTRIBUTING.md) to get started. diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart index b0095bb..107f0af 100644 --- a/lib/core/network/api_client.dart +++ b/lib/core/network/api_client.dart @@ -7,7 +7,9 @@ class ApiClient { ApiClient(this._dio) { _dio.options = BaseOptions( - baseUrl: AppConfigs.baseUrl, + baseUrl: _dio.options.baseUrl.isNotEmpty + ? _dio.options.baseUrl + : AppConfigs.baseUrl, connectTimeout: const Duration(milliseconds: 30000), receiveTimeout: const Duration(milliseconds: 30000), responseType: ResponseType.json, diff --git a/lib/data/datasources/avnu_remote_datasource.dart b/lib/data/datasources/avnu_remote_datasource.dart new file mode 100644 index 0000000..4a6347b --- /dev/null +++ b/lib/data/datasources/avnu_remote_datasource.dart @@ -0,0 +1,39 @@ +import 'package:starkwager/core/network/api_client.dart'; + +abstract class AvnuRemoteDataSource { + Future> deployAccount( + {required String userAddress, + required String classHash, + required String salt, + required String unique, + required List calldata, + required List sigdata}); +} + +class AvnuRemoteDataSourceImpl implements AvnuRemoteDataSource { + final ApiClient apiClient; + AvnuRemoteDataSourceImpl(this.apiClient); + @override + Future> deployAccount( + {required String userAddress, + required String classHash, + required String salt, + required String unique, + required List calldata, + required List sigdata}) async { + final ref = await apiClient.post( + '/paymaster/v1/deploy-account', + data: { + "userAddress": userAddress, + "deploymentData": { + "class_hash": classHash, + "salt": salt, + "unique": unique, + "calldata": calldata, + "sigdata": sigdata + } + }, + ); + return ref.data; + } +} diff --git a/lib/data/repositories/avnu_repository_impl.dart b/lib/data/repositories/avnu_repository_impl.dart new file mode 100644 index 0000000..a76fb5a --- /dev/null +++ b/lib/data/repositories/avnu_repository_impl.dart @@ -0,0 +1,94 @@ +import 'package:avnu_provider/avnu_provider.dart'; +import 'package:starkwager/data/datasources/avnu_remote_datasource.dart'; +import 'package:starkwager/domain/repositories/avnu_repository.dart'; + +class AvnuRepositoryImpl implements AvnuRepository { + late AvnuProvider _avnuProvider; + late AvnuReadProvider _avnuReadProvider; + final AvnuRemoteDataSource _avnuRemoteDataSource; + + AvnuRepositoryImpl({required AvnuRemoteDataSource avnuRemoteDataSource}) + : _avnuRemoteDataSource = avnuRemoteDataSource { + _initialize(); + } + + void _initialize() { + _avnuProvider = AvnuJsonRpcProvider( + nodeUri: Uri.parse("https://starknet-sepolia.public.blastapi.io"), + apiKey: String.fromEnvironment('API_KEY'), + ); + _avnuReadProvider = AvnuJsonRpcReadProvider( + nodeUri: Uri.parse("https://starknet-sepolia.public.blastapi.io"), + apiKey: String.fromEnvironment('API_KEY'), + ); + } + + @override + Future deployAccount({ + required String userAddress, + required String classHash, + required String salt, + required String unique, + required List calldata, + required List sigdata, + }) async { + await _avnuRemoteDataSource.deployAccount( + userAddress: userAddress, + classHash: classHash, + salt: salt, + unique: unique, + calldata: calldata, + sigdata: sigdata, + ); + } + + @override + Future setUserLimit( + {required String address, + required String campaign, + required String protocol, + required int freeTx, + required DateTime expirationDate, + required List> whitelistedCalls}) async { + return await _avnuProvider.setAccountRewards( + address, + campaign, + protocol, + freeTx, + expirationDate.toIso8601String(), + whitelistedCalls, + ); + } + + @override + Future registerTypedData( + {required String userAddress, + required List> calls, + required String gasTokenAddress, + required String maxGasTokenAmount, + required String accountClassHash}) async { + return await _avnuProvider.buildTypedData(userAddress, calls, + gasTokenAddress, maxGasTokenAmount, accountClassHash); + } + + @override + Future executeContract( + {required String userAddress, + required String typedData, + required List signature, + Map? deploymentData}) async { + return await _avnuProvider.execute( + userAddress, typedData, signature, deploymentData); + } + + @override + Future getGasTokenPrices() async { + return await _avnuReadProvider.getGasTokenPrices(); + } + + @override + Future checkAccountCompatibility({required String userAddress}) async { + final ref = await _avnuReadProvider.checkAccountCompatible(userAddress); + return ref is AvnuAccountCompatibleIsCompatible ? true : false; + } +} diff --git a/lib/domain/repositories/avnu_repository.dart b/lib/domain/repositories/avnu_repository.dart new file mode 100644 index 0000000..760046d --- /dev/null +++ b/lib/domain/repositories/avnu_repository.dart @@ -0,0 +1,37 @@ +import 'package:avnu_provider/avnu_provider.dart'; + +abstract class AvnuRepository { + Future deployAccount({ + required String userAddress, + required String classHash, + required String salt, + required String unique, + required List calldata, + required List sigdata, + }); + + Future setUserLimit( + {required String address, + required String campaign, + required String protocol, + required int freeTx, + required DateTime expirationDate, + required List> whitelistedCalls}); + + Future registerTypedData( + {required String userAddress, + required List> calls, + required String gasTokenAddress, + required String maxGasTokenAmount, + required String accountClassHash}); + + Future executeContract( + {required String userAddress, + required String typedData, + required List signature, + Map? deploymentData}); + + Future getGasTokenPrices(); + + Future checkAccountCompatibility({required String userAddress}); +} diff --git a/lib/features/connect_wallet/connect_wallet_screen.dart b/lib/features/connect_wallet/connect_wallet_screen.dart index 746e674..d2fc005 100644 --- a/lib/features/connect_wallet/connect_wallet_screen.dart +++ b/lib/features/connect_wallet/connect_wallet_screen.dart @@ -26,7 +26,8 @@ class _ConnectWalletScreen extends ConsumerState { final namespace = ReownAppKitModalNetworks.getNamespaceForChainId( chainId, ); - final address = next.service.session!.getAddress(namespace)!; + final address = next.service.session?.getAddress(namespace); + if (address == null) return; // Call create login endpoint with this address and trigger AuthLoading state await ref diff --git a/lib/features/connect_wallet/provider/avnu_repository_provider.dart b/lib/features/connect_wallet/provider/avnu_repository_provider.dart new file mode 100644 index 0000000..e4f4520 --- /dev/null +++ b/lib/features/connect_wallet/provider/avnu_repository_provider.dart @@ -0,0 +1,16 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:starkwager/core/network/api_client.dart'; +import 'package:starkwager/data/datasources/avnu_remote_datasource.dart'; +import 'package:starkwager/data/repositories/avnu_repository_impl.dart'; +import 'package:starkwager/domain/repositories/avnu_repository.dart'; + +final avnuRemoteDataSourceProvider = Provider((ref) { + return AvnuRemoteDataSourceImpl( + ApiClient(Dio(BaseOptions(baseUrl: "https://sepolia.api.avnu.fi/")))); +}); + +final avnuRepositoryProvider = Provider((ref) { + final remoteDataSource = ref.watch(avnuRemoteDataSourceProvider); + return AvnuRepositoryImpl(avnuRemoteDataSource: remoteDataSource); +}); diff --git a/lib/features/connect_wallet/provider/wallet_connection_provider.dart b/lib/features/connect_wallet/provider/wallet_connection_provider.dart index ba2ae0f..e0fd175 100644 --- a/lib/features/connect_wallet/provider/wallet_connection_provider.dart +++ b/lib/features/connect_wallet/provider/wallet_connection_provider.dart @@ -9,25 +9,29 @@ import 'package:starkwager/features/connect_wallet/provider/wallet_connection_st final argentCheckProvider = FutureProvider((ref) async { final appCheck = AppCheck(); late bool isArgentInstalled; - if (Platform.isAndroid) { - const package = "im.argent.contractwalletclient"; - await appCheck.checkAvailability(package).then( - (app) { - if (app?.appName.toString() != null) { - isArgentInstalled = true; - } - }, - ); - return isArgentInstalled; - } else if (Platform.isIOS) { - await appCheck.checkAvailability("argent://").then( - (app) { - if (app?.appName.toString() != null) { - isArgentInstalled = true; - } - }, - ); - return isArgentInstalled; + try { + if (Platform.isAndroid) { + const package = "im.argent.contractwalletclient"; + await appCheck.checkAvailability(package).then( + (app) { + if (app?.appName.toString() != null) { + isArgentInstalled = true; + } + }, + ); + return isArgentInstalled; + } else if (Platform.isIOS) { + await appCheck.checkAvailability("argent://").then( + (app) { + if (app?.appName.toString() != null) { + isArgentInstalled = true; + } + }, + ); + return isArgentInstalled; + } + } catch (e) { + debugPrint("$e"); } return false; }); @@ -35,25 +39,29 @@ final argentCheckProvider = FutureProvider((ref) async { final braavosCheckProvider = FutureProvider((ref) async { final appCheck = AppCheck(); late bool isBraavosInstalled; - if (Platform.isAndroid) { - const package = "app.braavos.wallet"; - await appCheck.checkAvailability(package).then( - (app) { - if (app?.appName.toString() != null) { - isBraavosInstalled = true; - } - }, - ); - return isBraavosInstalled; - } else if (Platform.isIOS) { - await appCheck.checkAvailability("braavos://").then( - (app) { - if (app?.appName.toString() != null) { - isBraavosInstalled = true; - } - }, - ); - return isBraavosInstalled; + try { + if (Platform.isAndroid) { + const package = "app.braavos.wallet"; + await appCheck.checkAvailability(package).then( + (app) { + if (app?.appName.toString() != null) { + isBraavosInstalled = true; + } + }, + ); + return isBraavosInstalled; + } else if (Platform.isIOS) { + await appCheck.checkAvailability("braavos://").then( + (app) { + if (app?.appName.toString() != null) { + isBraavosInstalled = true; + } + }, + ); + return isBraavosInstalled; + } + } catch (e) { + debugPrint("$e"); } return false; }); @@ -61,25 +69,29 @@ final braavosCheckProvider = FutureProvider((ref) async { final metamaskCheckProvider = FutureProvider((ref) async { final appCheck = AppCheck(); late bool isMetaMaskInstalled; - if (Platform.isAndroid) { - const package = "app.metamask.wallet"; - await appCheck.checkAvailability(package).then( - (app) { - if (app?.appName.toString() != null) { - isMetaMaskInstalled = true; - } - }, - ); - return isMetaMaskInstalled; - } else if (Platform.isIOS) { - await appCheck.checkAvailability("metamask://").then( - (app) { - if (app?.appName.toString() != null) { - isMetaMaskInstalled = true; - } - }, - ); - return isMetaMaskInstalled; + try { + if (Platform.isAndroid) { + const package = "io.metamask"; + await appCheck.checkAvailability(package).then( + (app) { + if (app?.appName.toString() != null) { + isMetaMaskInstalled = true; + } + }, + ); + return isMetaMaskInstalled; + } else if (Platform.isIOS) { + await appCheck.checkAvailability("metamask://").then( + (app) { + if (app?.appName.toString() != null) { + isMetaMaskInstalled = true; + } + }, + ); + return isMetaMaskInstalled; + } + } catch (e) { + debugPrint("$e"); } return false; }); @@ -130,7 +142,7 @@ class WalletConnectionNotifier extends StateNotifier { ); if (!w3mService.isConnected) { - listenToWalletConnection(); + await listenToWalletConnection(); } state = WalletConnectionState.connected(service: w3mService); } catch (e) { diff --git a/pubspec.lock b/pubspec.lock index 0e90e36..c48aa1a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -70,6 +70,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.5" + avnu_provider: + dependency: "direct main" + description: + name: avnu_provider + sha256: "261c9b5a67b480ce2918de293374544b1d60063de04a19cd29ec47c53b2a1b4d" + url: "https://pub.dev" + source: hosted + version: "0.0.2" base_x: dependency: transitive description: @@ -686,10 +694,10 @@ packages: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.4.0" http_multi_server: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 04fe077..c9453a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,7 @@ dependencies: shimmer: ^3.0.0 starknet: ^0.1.2+1 flutter_dotenv: ^5.2.1 + avnu_provider: ^0.0.2 dev_dependencies: flutter_test: