From b60f048e3d2bd91bd484f4e4575d7c7b42665883 Mon Sep 17 00:00:00 2001 From: Alfreedom <00tango.bromine@icloud.com> Date: Wed, 31 Jan 2024 21:00:13 +0100 Subject: [PATCH] first approach: added custom credentials as extension and enhanced examples --- example/dapp/android/app/build.gradle | 4 +- .../dapp/ios/Runner.xcodeproj/project.pbxproj | 9 +- example/dapp/ios/Runner/Info.plist | 50 +- example/dapp/lib/main.dart | 31 +- example/dapp/lib/pages/connect_page.dart | 41 +- example/dapp/lib/utils/crypto/eip155.dart | 153 +- example/dapp/lib/utils/smart_contracts.dart | 279 +++ example/dapp/lib/widgets/session_widget.dart | 184 +- .../macos/Runner/Configs/AppInfo.xcconfig | 2 +- .../lib/dependencies/bip32/bip32_base.dart | 269 +++ .../lib/dependencies/bip32/utils/crypto.dart | 21 + .../lib/dependencies/bip32/utils/ecurve.dart | 263 +++ .../lib/dependencies/bip32/utils/wif.dart | 56 + .../lib/dependencies/bip39/bip39_base.dart | 153 ++ .../lib/dependencies/bip39/utils/pbkdf2.dart | 31 + .../dependencies/bip39/wordlists/english.dart | 2052 +++++++++++++++++ .../lib/dependencies/chains/evm_service.dart | 218 +- .../dependencies/key_service/chain_key.dart | 4 +- .../key_service/i_key_service.dart | 15 +- .../dependencies/key_service/key_service.dart | 133 +- .../lib/dependencies/web3wallet_service.dart | 14 +- example/wallet/lib/main.dart | 8 +- example/wallet/lib/models/chain_data.dart | 72 + example/wallet/lib/pages/app_detail_page.dart | 3 +- example/wallet/lib/pages/apps_page.dart | 1 + example/wallet/lib/pages/settings_page.dart | 235 ++ example/wallet/lib/utils/constants.dart | 2 +- example/wallet/lib/utils/dart_defines.dart | 6 +- .../lib/utils/namespace_model_builder.dart | 22 +- example/wallet/lib/widgets/custom_button.dart | 8 +- .../wallet/lib/widgets/recover_from_seed.dart | 88 + .../wc_connection_widget.dart | 2 +- .../wc_connection_widget_info.dart | 2 +- example/wallet/pubspec.yaml | 1 - lib/apis/utils/extensions.dart | 163 ++ lib/walletconnect_flutter_v2.dart | 2 +- 36 files changed, 4354 insertions(+), 243 deletions(-) create mode 100644 example/dapp/lib/utils/smart_contracts.dart create mode 100644 example/wallet/lib/dependencies/bip32/bip32_base.dart create mode 100644 example/wallet/lib/dependencies/bip32/utils/crypto.dart create mode 100644 example/wallet/lib/dependencies/bip32/utils/ecurve.dart create mode 100644 example/wallet/lib/dependencies/bip32/utils/wif.dart create mode 100644 example/wallet/lib/dependencies/bip39/bip39_base.dart create mode 100644 example/wallet/lib/dependencies/bip39/utils/pbkdf2.dart create mode 100644 example/wallet/lib/dependencies/bip39/wordlists/english.dart create mode 100644 example/wallet/lib/models/chain_data.dart create mode 100644 example/wallet/lib/pages/settings_page.dart create mode 100644 example/wallet/lib/widgets/recover_from_seed.dart create mode 100644 lib/apis/utils/extensions.dart diff --git a/example/dapp/android/app/build.gradle b/example/dapp/android/app/build.gradle index db2ca1aa..c3c083b9 100644 --- a/example/dapp/android/app/build.gradle +++ b/example/dapp/android/app/build.gradle @@ -44,7 +44,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.example.dapp" + applicationId "com.walletconnect.flutterdapp" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. minSdkVersion flutter.minSdkVersion @@ -60,7 +60,7 @@ android { signingConfig signingConfigs.debug } } - namespace 'com.example.dapp' + namespace 'com.walletconnect.flutterdapp' } flutter { diff --git a/example/dapp/ios/Runner.xcodeproj/project.pbxproj b/example/dapp/ios/Runner.xcodeproj/project.pbxproj index 36a22b63..a65a62da 100644 --- a/example/dapp/ios/Runner.xcodeproj/project.pbxproj +++ b/example/dapp/ios/Runner.xcodeproj/project.pbxproj @@ -361,11 +361,12 @@ DEVELOPMENT_TEAM = W5R8AG9K22; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Flutter Dapp"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.dapp; + PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.flutterdapp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -490,11 +491,12 @@ DEVELOPMENT_TEAM = W5R8AG9K22; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Flutter Dapp"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.dapp; + PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.flutterdapp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -513,11 +515,12 @@ DEVELOPMENT_TEAM = W5R8AG9K22; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Flutter Dapp"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.dapp; + PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.flutterdapp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/example/dapp/ios/Runner/Info.plist b/example/dapp/ios/Runner/Info.plist index 21b9a0ff..0627d218 100644 --- a/example/dapp/ios/Runner/Info.plist +++ b/example/dapp/ios/Runner/Info.plist @@ -2,10 +2,12 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Dapp + Flutter Dapp CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -13,17 +15,38 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - dapp + Flutter Dapp CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleURLSchemes + + wcflutterdapp + + + CFBundleVersion $(FLUTTER_BUILD_NUMBER) + LSApplicationCategoryType + + LSApplicationQueriesSchemes + + wcflutterwallet + LSRequiresIPhoneOS + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -43,28 +66,5 @@ UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - LSApplicationCategoryType - - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleURLSchemes - - wcflutterdapp - - - - LSApplicationQueriesSchemes - - wcflutterwallet - diff --git a/example/dapp/lib/main.dart b/example/dapp/lib/main.dart index ce1706c2..3d3a7b1e 100644 --- a/example/dapp/lib/main.dart +++ b/example/dapp/lib/main.dart @@ -63,7 +63,7 @@ class _MyHomePageState extends State { projectId: DartDefines.projectId, logLevel: LogLevel.info, metadata: const PairingMetadata( - name: 'Sample dApp Fllutter', + name: 'Sample dApp Flutter', description: 'WalletConnect\'s sample dapp with Flutter', url: 'https://walletconnect.com/', icons: [ @@ -86,12 +86,17 @@ class _MyHomePageState extends State { } // Register event handlers + _web3App!.onSessionConnect.subscribe(_onSessionConnect); _web3App!.onSessionPing.subscribe(_onSessionPing); _web3App!.onSessionEvent.subscribe(_onSessionEvent); _web3App!.onSessionUpdate.subscribe(_onSessionUpdate); + _web3App!.core.relayClient.onRelayClientConnect.subscribe(_setState); _web3App!.core.relayClient.onRelayClientDisconnect.subscribe(_setState); - _web3App!.onSessionConnect.subscribe(_onSessionConnect); + _web3App!.core.relayClient.onRelayClientMessage.subscribe(_onRelayMessage); + + _web3App!.signEngine.onSessionEvent.subscribe(_onSessionEvent); + _web3App!.signEngine.onSessionUpdate.subscribe(_onSessionUpdate); setState(() { _pageDatas = [ @@ -128,12 +133,19 @@ class _MyHomePageState extends State { @override void dispose() { + // Unregister event handlers _web3App!.onSessionConnect.unsubscribe(_onSessionConnect); - _web3App!.core.relayClient.onRelayClientConnect.unsubscribe(_setState); - _web3App!.core.relayClient.onRelayClientDisconnect.unsubscribe(_setState); _web3App!.onSessionPing.unsubscribe(_onSessionPing); _web3App!.onSessionEvent.unsubscribe(_onSessionEvent); _web3App!.onSessionUpdate.unsubscribe(_onSessionUpdate); + + _web3App!.core.relayClient.onRelayClientConnect.unsubscribe(_setState); + _web3App!.core.relayClient.onRelayClientDisconnect.unsubscribe(_setState); + _web3App!.core.relayClient.onRelayClientMessage + .unsubscribe(_onRelayMessage); + + _web3App!.signEngine.onSessionEvent.unsubscribe(_onSessionEvent); + _web3App!.signEngine.onSessionUpdate.unsubscribe(_onSessionUpdate); super.dispose(); } @@ -249,4 +261,15 @@ class _MyHomePageState extends State { void _onSessionUpdate(SessionUpdate? args) { debugPrint('[$runtimeType] _onSessionUpdate $args'); } + + void _onRelayMessage(MessageEvent? args) async { + if (args != null) { + final payloadString = await _web3App!.core.crypto.decode( + args.topic, + args.message, + ); + final data = jsonDecode(payloadString ?? '{}') as Map; + debugPrint(data.toString()); + } + } } diff --git a/example/dapp/lib/pages/connect_page.dart b/example/dapp/lib/pages/connect_page.dart index 843007fd..f6bf3cf6 100644 --- a/example/dapp/lib/pages/connect_page.dart +++ b/example/dapp/lib/pages/connect_page.dart @@ -107,7 +107,7 @@ class ConnectPageState extends State { children: [ const Text( StringConstants.appTitle, - style: StyleConstants.titleText, + style: StyleConstants.subtitleText, textAlign: TextAlign.center, ), const SizedBox( @@ -115,7 +115,7 @@ class ConnectPageState extends State { ), const Text( StringConstants.selectChains, - style: StyleConstants.subtitleText, + style: StyleConstants.paragraph, textAlign: TextAlign.center, ), const SizedBox( @@ -169,8 +169,32 @@ class ConnectPageState extends State { final encodedUri = Uri.encodeComponent(res.uri.toString()); final uri = 'wcflutterwallet://wc?uri=$encodedUri'; + // final uri = 'metamask://wc?uri=$encodedUri'; if (await canLaunchUrlString(uri)) { - launchUrlString(uri, mode: LaunchMode.externalApplication); + // ignore: use_build_context_synchronously + final openApp = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + content: const Text('Do you want to open with Web3Wallet Flutter'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Show QR'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Open'), + ), + ], + ); + }, + ); + if (openApp) { + launchUrlString(uri, mode: LaunchMode.externalApplication); + } else { + _showQrCode(res); + } } else { _showQrCode(res); } @@ -203,13 +227,14 @@ class ConnectPageState extends State { showToast?.call(StringConstants.authSucceeded); } - if (_shouldDismissQrCode) { + // ignore: use_build_context_synchronously + if (_shouldDismissQrCode && Navigator.canPop(context)) { // ignore: use_build_context_synchronously Navigator.pop(context); } } catch (e) { - // debugPrint(e.toString()); - if (_shouldDismissQrCode) { + // ignore: use_build_context_synchronously + if (_shouldDismissQrCode && Navigator.canPop(context)) { // ignore: use_build_context_synchronously Navigator.pop(context); } @@ -217,9 +242,7 @@ class ConnectPageState extends State { } } - Future _showQrCode( - ConnectResponse response, - ) async { + Future _showQrCode(ConnectResponse response) async { // Show the QR code debugPrint('Showing QR Code: ${response.uri}'); diff --git a/example/dapp/lib/utils/crypto/eip155.dart b/example/dapp/lib/utils/crypto/eip155.dart index 730c7677..cd6e6bcd 100644 --- a/example/dapp/lib/utils/crypto/eip155.dart +++ b/example/dapp/lib/utils/crypto/eip155.dart @@ -1,5 +1,10 @@ +import 'dart:convert'; + +import 'package:intl/intl.dart'; import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; -import 'package:walletconnect_flutter_v2_dapp/models/eth/ethereum_transaction.dart'; +import 'package:walletconnect_flutter_v2_dapp/models/chain_metadata.dart'; +import 'package:walletconnect_flutter_v2_dapp/utils/crypto/chain_data.dart'; +import 'package:walletconnect_flutter_v2_dapp/utils/smart_contracts.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/test_data.dart'; enum EIP155Methods { @@ -15,10 +20,6 @@ enum EIP155Events { accountsChanged, } -extension EIP155MethodsX on EIP155Methods { - String? get value => EIP155.methods[this]; -} - extension EIP155MethodsStringX on String { EIP155Methods? toEip155Method() { final entries = EIP155.methods.entries.where( @@ -28,10 +29,6 @@ extension EIP155MethodsStringX on String { } } -extension EIP155EventsX on EIP155Events { - String? get value => EIP155.events[this]; -} - extension EIP155EventsStringX on String { EIP155Events? toEip155Event() { final entries = EIP155.events.entries.where( @@ -59,7 +56,7 @@ class EIP155 { required Web3App web3App, required String topic, required EIP155Methods method, - required String chainId, + required ChainMetadata chainData, required String address, }) { switch (method) { @@ -67,7 +64,7 @@ class EIP155 { return personalSign( web3App: web3App, topic: topic, - chainId: chainId, + chainId: chainData.chainId, address: address, data: testSignData, ); @@ -75,7 +72,7 @@ class EIP155 { return ethSign( web3App: web3App, topic: topic, - chainId: chainId, + chainId: chainData.chainId, address: address, data: testSignData, ); @@ -83,7 +80,7 @@ class EIP155 { return ethSignTypedData( web3App: web3App, topic: topic, - chainId: chainId, + chainId: chainData.chainId, address: address, data: typedData, ); @@ -91,24 +88,69 @@ class EIP155 { return ethSignTransaction( web3App: web3App, topic: topic, - chainId: chainId, - transaction: EthereumTransaction( - from: address, - to: address, - value: '0x01', + chainId: chainData.chainId, + transaction: Transaction( + from: EthereumAddress.fromHex(address), + to: EthereumAddress.fromHex( + '0x59e2f66C0E96803206B6486cDb39029abAE834c0'), + value: EtherAmount.fromInt(EtherUnit.finney, 12), // == 0.012 ), ); case EIP155Methods.ethSendTransaction: return ethSendTransaction( web3App: web3App, topic: topic, - chainId: chainId, - transaction: EthereumTransaction( - from: address, - to: address, - value: '0x01', + chainId: chainData.chainId, + transaction: Transaction( + from: EthereumAddress.fromHex(address), + to: EthereumAddress.fromHex( + '0x59e2f66C0E96803206B6486cDb39029abAE834c0'), + value: EtherAmount.fromInt(EtherUnit.finney, 11), // == 0.011 + ), + ); + } + } + + static Future callSmartContract({ + required Web3App web3App, + required String topic, + required String address, + required String action, + }) { + // Create DeployedContract object using contract's ABI and address + final deployedContract = DeployedContract( + ContractAbi.fromJson( + jsonEncode(SepoliaTestContract.readContractAbi), + 'Alfreedoms', + ), + EthereumAddress.fromHex(SepoliaTestContract.contractAddress), + ); + + switch (action) { + case 'read': + return readSmartContract( + web3App: web3App, + rpcUrl: ChainData.testChains.first.rpc.first, + contract: deployedContract, + address: address, + ); + case 'write': + return writeToSmartContract( + web3App: web3App, + rpcUrl: ChainData.testChains.first.rpc.first, + address: address, + topic: topic, + chainId: ChainData.testChains.first.chainId, + contract: deployedContract, + transaction: Transaction( + from: EthereumAddress.fromHex(address), + to: EthereumAddress.fromHex( + '0x59e2f66C0E96803206B6486cDb39029abAE834c0'), + value: EtherAmount.fromInt(EtherUnit.finney, 10), // == 0.010 ), ); + default: + return Future.value(); } } @@ -167,7 +209,7 @@ class EIP155 { required Web3App web3App, required String topic, required String chainId, - required EthereumTransaction transaction, + required Transaction transaction, }) async { return await web3App.request( topic: topic, @@ -183,7 +225,7 @@ class EIP155 { required Web3App web3App, required String topic, required String chainId, - required EthereumTransaction transaction, + required Transaction transaction, }) async { return await web3App.request( topic: topic, @@ -194,4 +236,65 @@ class EIP155 { ), ); } + + static Future readSmartContract({ + required Web3App web3App, + required String rpcUrl, + required String address, + required DeployedContract contract, + }) async { + final results = await Future.wait([ + // results[0] + web3App.readContractCall( + deployedContract: contract, + functionName: 'name', + rpcUrl: rpcUrl, + ), + // results[1] + web3App.readContractCall( + deployedContract: contract, + functionName: 'totalSupply', + rpcUrl: rpcUrl, + ), + // results[2] + web3App.readContractCall( + deployedContract: contract, + functionName: 'balanceOf', + rpcUrl: rpcUrl, + parameters: [ + EthereumAddress.fromHex(address), + ], + ), + ]); + + final oCcy = NumberFormat("#,##0.00", "en_US"); + final name = results[0].toString(); + final total = results[1] / BigInt.from(1000000000000000000); + final balance = results[2] / BigInt.from(1000000000000000000); + + return { + 'name': name, + 'totalSupply': oCcy.format(total), + 'balance': oCcy.format(balance), + }; + } + + static Future writeToSmartContract({ + required Web3App web3App, + required String rpcUrl, + required String topic, + required String chainId, + required String address, + required DeployedContract contract, + required Transaction transaction, + }) async { + return await web3App.writeContractCall( + topic: topic, + chainId: chainId, + rpcUrl: rpcUrl, + deployedContract: contract, + functionName: 'transfer', + transaction: transaction, + ); + } } diff --git a/example/dapp/lib/utils/smart_contracts.dart b/example/dapp/lib/utils/smart_contracts.dart new file mode 100644 index 00000000..ca128c39 --- /dev/null +++ b/example/dapp/lib/utils/smart_contracts.dart @@ -0,0 +1,279 @@ +class SepoliaTestContract { + // Alfreedoms2 ALF2 in Sepolia + // DEPLOY https://sepolia.etherscan.io/tx/0xebf287281abbc976b7cf6956a7f5f66338935d324c6453a350e3bb42ff7bd4e2 + // MINT https://sepolia.etherscan.io/tx/0x04a015504be7420a40a59936bfcca9302e55700fd00129059444539770fed5e7 + // CONTRACT https://sepolia.etherscan.io/address/0xBe60D05C11BD1C365849C824E0C2D880d2466eAF + // TRANSFERS https://sepolia.etherscan.io/token/0xbe60d05c11bd1c365849c824e0c2d880d2466eaf?a=0x59e2f66C0E96803206B6486cDb39029abAE834c0 + // SOURCIFY https://repo.sourcify.dev/contracts/full_match/11155111/0xBe60D05C11BD1C365849C824E0C2D880d2466eAF/ + static const contractAddress = '0xBe60D05C11BD1C365849C824E0C2D880d2466eAF'; + + static const readContractAbi = [ + { + "inputs": [ + {"internalType": "address", "name": "initialOwner", "type": "address"} + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "allowance", "type": "uint256"}, + {"internalType": "uint256", "name": "needed", "type": "uint256"} + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + {"internalType": "address", "name": "sender", "type": "address"}, + {"internalType": "uint256", "name": "balance", "type": "uint256"}, + {"internalType": "uint256", "name": "needed", "type": "uint256"} + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + {"internalType": "address", "name": "approver", "type": "address"} + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + {"internalType": "address", "name": "receiver", "type": "address"} + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + {"internalType": "address", "name": "sender", "type": "address"} + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"} + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "inputs": [ + {"internalType": "address", "name": "owner", "type": "address"} + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + {"internalType": "address", "name": "owner", "type": "address"}, + {"internalType": "address", "name": "spender", "type": "address"} + ], + "name": "allowance", + "outputs": [ + {"internalType": "uint256", "name": "", "type": "uint256"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "value", "type": "uint256"} + ], + "name": "approve", + "outputs": [ + {"internalType": "bool", "name": "", "type": "bool"} + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "balanceOf", + "outputs": [ + {"internalType": "uint256", "name": "", "type": "uint256"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + {"internalType": "uint8", "name": "", "type": "uint8"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "to", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + {"internalType": "string", "name": "", "type": "string"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + {"internalType": "address", "name": "", "type": "address"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + {"internalType": "string", "name": "", "type": "string"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + {"internalType": "uint256", "name": "", "type": "uint256"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "to", "type": "address"}, + {"internalType": "uint256", "name": "value", "type": "uint256"} + ], + "name": "transfer", + "outputs": [ + {"internalType": "bool", "name": "", "type": "bool"} + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "from", "type": "address"}, + {"internalType": "address", "name": "to", "type": "address"}, + {"internalType": "uint256", "name": "value", "type": "uint256"} + ], + "name": "transferFrom", + "outputs": [ + {"internalType": "bool", "name": "", "type": "bool"} + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "newOwner", "type": "address"} + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ]; +} diff --git a/example/dapp/lib/widgets/session_widget.dart b/example/dapp/lib/widgets/session_widget.dart index 9f60318f..7532033f 100644 --- a/example/dapp/lib/widgets/session_widget.dart +++ b/example/dapp/lib/widgets/session_widget.dart @@ -3,6 +3,7 @@ import 'package:url_launcher/url_launcher_string.dart'; import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; import 'package:walletconnect_flutter_v2_dapp/models/chain_metadata.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/constants.dart'; +import 'package:walletconnect_flutter_v2_dapp/utils/crypto/chain_data.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/crypto/eip155.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/crypto/helpers.dart'; import 'package:walletconnect_flutter_v2_dapp/utils/string_constants.dart'; @@ -26,14 +27,6 @@ class SessionWidgetState extends State { @override Widget build(BuildContext context) { final List children = [ - Text( - widget.session.peer.metadata.name, - style: StyleConstants.titleText, - textAlign: TextAlign.center, - ), - const SizedBox( - height: StyleConstants.linear16, - ), Text( '${StringConstants.sessionTopic}${widget.session.topic}', ), @@ -85,19 +78,16 @@ class SessionWidgetState extends State { ), ); + children.add(const SizedBox(height: 20.0)); return ListView( children: children, ); } Widget _buildAccountWidget(String namespaceAccount) { - final String chainId = NamespaceUtils.getChainFromAccount( - namespaceAccount, - ); - final String account = NamespaceUtils.getAccount( - namespaceAccount, - ); - final ChainMetadata chainMetadata = getChainMetadataFromChain(chainId); + final chainId = NamespaceUtils.getChainFromAccount(namespaceAccount); + final account = NamespaceUtils.getAccount(namespaceAccount); + final chainMetadata = getChainMetadataFromChain(chainId); final List children = [ Text( @@ -120,12 +110,13 @@ class SessionWidgetState extends State { ), ]; - children.addAll( - _buildChainMethodButtons( - chainMetadata, - account, - ), - ); + children.addAll(_buildChainMethodButtons(chainMetadata, account)); + + children.add(const Divider()); + if (chainId != ChainData.testChains.first.chainId) { + children.add(const Text('Connect to Sepolia to Test')); + } + children.addAll(_buildSepoliaButtons(account, chainId)); children.addAll([ const SizedBox( @@ -184,20 +175,15 @@ class SessionWidgetState extends State { ), child: ElevatedButton( onPressed: () async { - final walletUrl = widget.session.peer.metadata.redirect?.native; - if ((walletUrl ?? '').isNotEmpty) { - launchUrlString( - walletUrl!, - mode: LaunchMode.externalApplication, - ); - } - Future future = callChainMethod( - chainMetadata.type, - method, - chainMetadata, - address, + final future = EIP155.callMethod( + web3App: widget.web3App, + topic: widget.session.topic, + method: method.toEip155Method()!, + chainData: chainMetadata, + address: address.toLowerCase(), ); MethodDialog.show(context, method, future); + _launchWallet(); }, style: ButtonStyle( backgroundColor: MaterialStateProperty.all( @@ -224,9 +210,111 @@ class SessionWidgetState extends State { return buttons; } + void _launchWallet() { + final walletUrl = widget.session.peer.metadata.redirect?.native; + if ((walletUrl ?? '').isNotEmpty) { + launchUrlString( + walletUrl!, + mode: LaunchMode.externalApplication, + ); + } + } + + List _buildSepoliaButtons(String address, String chainId) { + final List buttons = []; + final enabled = chainId == ChainData.testChains.first.chainId; + buttons.add( + Container( + width: double.infinity, + height: StyleConstants.linear48, + margin: const EdgeInsets.symmetric( + vertical: StyleConstants.linear8, + ), + child: ElevatedButton( + onPressed: enabled + ? () async { + final future = EIP155.callSmartContract( + web3App: widget.web3App, + topic: widget.session.topic, + address: address, + action: 'read', + ); + MethodDialog.show(context, 'Test Contract (Read)', future); + } + : null, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return StyleConstants.grayColor; + } + return Colors.orange; + }), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + StyleConstants.linear8, + ), + ), + ), + ), + child: const Text( + 'Test Contract (Read)', + style: StyleConstants.buttonText, + textAlign: TextAlign.center, + ), + ), + ), + ); + buttons.add( + Container( + width: double.infinity, + height: StyleConstants.linear48, + margin: const EdgeInsets.symmetric( + vertical: StyleConstants.linear8, + ), + child: ElevatedButton( + onPressed: enabled + ? () async { + final future = EIP155.callSmartContract( + web3App: widget.web3App, + topic: widget.session.topic, + address: address, + action: 'write', + ); + MethodDialog.show(context, 'Test Contract (Write)', future); + _launchWallet(); + } + : null, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return StyleConstants.grayColor; + } + return Colors.orange; + }), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + StyleConstants.linear8, + ), + ), + ), + ), + child: const Text( + 'Test Contract (Write)', + style: StyleConstants.buttonText, + textAlign: TextAlign.center, + ), + ), + ), + ); + + return buttons; + } + List _buildChainEventsTiles(ChainMetadata chainMetadata) { final List values = []; - // Add Methods + for (final String event in getChainEvents(chainMetadata.type)) { values.add( Container( @@ -258,32 +346,4 @@ class SessionWidgetState extends State { return values; } - - Future callChainMethod( - ChainType type, - String method, - ChainMetadata chainMetadata, - String address, - ) { - switch (type) { - case ChainType.eip155: - return EIP155.callMethod( - web3App: widget.web3App, - topic: widget.session.topic, - method: method.toEip155Method()!, - chainId: chainMetadata.chainId, - address: address.toLowerCase(), - ); - // case ChainType.kadena: - // return Kadena.callMethod( - // web3App: widget.web3App, - // topic: widget.session.topic, - // method: method.toKadenaMethod()!, - // chainId: chainMetadata.chainId, - // address: address.toLowerCase(), - // ); - default: - return Future.value(); - } - } } diff --git a/example/dapp/macos/Runner/Configs/AppInfo.xcconfig b/example/dapp/macos/Runner/Configs/AppInfo.xcconfig index ef570649..e7eb0032 100644 --- a/example/dapp/macos/Runner/Configs/AppInfo.xcconfig +++ b/example/dapp/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = dapp // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.dapp +PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.flutterdapp // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. diff --git a/example/wallet/lib/dependencies/bip32/bip32_base.dart b/example/wallet/lib/dependencies/bip32/bip32_base.dart new file mode 100644 index 00000000..caabd0d5 --- /dev/null +++ b/example/wallet/lib/dependencies/bip32/bip32_base.dart @@ -0,0 +1,269 @@ +// ignore_for_file: non_constant_identifier_names, depend_on_referenced_packages, constant_identifier_names + +import 'dart:typed_data'; +import 'package:bs58/bs58.dart'; + +import 'utils/crypto.dart'; +import 'utils/ecurve.dart' as ecc; +import 'utils/wif.dart' as wif; +import 'dart:convert'; + +class Bip32Type { + int public; + int private; + Bip32Type({required this.public, required this.private}); +} + +class NetworkType { + int wif; + Bip32Type bip32; + NetworkType({required this.wif, required this.bip32}); +} + +final BITCOIN = NetworkType( + wif: 0x80, + bip32: Bip32Type( + public: 0x0488b21e, + private: 0x0488ade4, + ), +); +const HIGHEST_BIT = 0x80000000; +const UINT31_MAX = 2147483647; // 2^31 - 1 +const UINT32_MAX = 4294967295; // 2^32 - 1 + +/// Checks if you are awesome. Spoiler: you are. +class BIP32 { + Uint8List? _d; + Uint8List? _Q; + Uint8List chainCode; + int depth = 0; + int index = 0; + NetworkType network; + int parentFingerprint = 0x00000000; + BIP32(this._d, this._Q, this.chainCode, this.network); + + Uint8List get publicKey { + _Q ??= ecc.pointFromScalar(_d!, true)!; + return _Q!; + } + + Uint8List? get privateKey => _d; + Uint8List get identifier => hash160(publicKey); + Uint8List get fingerprint => identifier.sublist(0, 4); + + bool isNeutered() { + return _d == null; + } + + BIP32 neutered() { + final neutered = BIP32.fromPublicKey(publicKey, chainCode, network); + neutered.depth = depth; + neutered.index = index; + neutered.parentFingerprint = parentFingerprint; + return neutered; + } + + String toBase58() { + final version = + (!isNeutered()) ? network.bip32.private : network.bip32.public; + Uint8List buffer = Uint8List(78); + ByteData bytes = buffer.buffer.asByteData(); + bytes.setUint32(0, version); + bytes.setUint8(4, depth); + bytes.setUint32(5, parentFingerprint); + bytes.setUint32(9, index); + buffer.setRange(13, 45, chainCode); + if (!isNeutered()) { + bytes.setUint8(45, 0); + buffer.setRange(46, 78, privateKey!); + } else { + buffer.setRange(45, 78, publicKey); + } + return base58.encode(buffer); + } + + String toWIF() { + if (privateKey == null) { + throw ArgumentError('Missing private key'); + } + return wif.encode( + wif.WIF( + version: network.wif, + privateKey: privateKey!, + compressed: true, + ), + ); + } + + BIP32 derive(int index) { + if (index > UINT32_MAX || index < 0) { + throw ArgumentError('Expected UInt32'); + } + final isHardened = index >= HIGHEST_BIT; + Uint8List data = Uint8List(37); + if (isHardened) { + if (isNeutered()) { + throw ArgumentError('Missing private key for hardened child key'); + } + data[0] = 0x00; + data.setRange(1, 33, privateKey!); + data.buffer.asByteData().setUint32(33, index); + } else { + data.setRange(0, 33, publicKey); + data.buffer.asByteData().setUint32(33, index); + } + final I = hmacSHA512(chainCode, data); + final IL = I.sublist(0, 32); + final IR = I.sublist(32); + if (!ecc.isPrivate(IL)) { + return derive(index + 1); + } + BIP32 hd; + if (!isNeutered()) { + final ki = ecc.privateAdd(privateKey!, IL); + if (ki == null) return derive(index + 1); + hd = BIP32.fromPrivateKey(ki, IR, network); + } else { + final ki = ecc.pointAddScalar(publicKey, IL, true); + if (ki == null) return derive(index + 1); + hd = BIP32.fromPublicKey(ki, IR, network); + } + hd.depth = depth + 1; + hd.index = index; + hd.parentFingerprint = fingerprint.buffer.asByteData().getUint32(0); + return hd; + } + + BIP32 deriveHardened(int index) { + if (index > UINT31_MAX || index < 0) { + throw ArgumentError('Expected UInt31'); + } + return derive(index + HIGHEST_BIT); + } + + BIP32 derivePath(String path) { + final regex = RegExp(r"^(m\/)?(\d+'?\/)*\d+'?$"); + if (!regex.hasMatch(path)) { + throw ArgumentError('Expected BIP32 Path'); + } + List splitPath = path.split('/'); + if (splitPath[0] == 'm') { + if (parentFingerprint != 0) { + throw ArgumentError('Expected master, got child'); + } + splitPath = splitPath.sublist(1); + } + return splitPath.fold(this, (BIP32 prevHd, String indexStr) { + int index; + if (indexStr.substring(indexStr.length - 1) == "'") { + index = int.parse(indexStr.substring(0, indexStr.length - 1)); + return prevHd.deriveHardened(index); + } else { + index = int.parse(indexStr); + return prevHd.derive(index); + } + }); + } + + sign(Uint8List hash) { + return ecc.sign(hash, privateKey!); + } + + verify(Uint8List hash, Uint8List signature) { + return ecc.verify(hash, publicKey, signature); + } + + factory BIP32.fromBase58(String string, [NetworkType? nw]) { + Uint8List buffer = base58.decode(string); + if (buffer.length != 78) { + throw ArgumentError('Invalid buffer length'); + } + NetworkType network = nw ?? BITCOIN; + ByteData bytes = buffer.buffer.asByteData(); + // 4 bytes: version bytes + var version = bytes.getUint32(0); + if (version != network.bip32.private && version != network.bip32.public) { + throw ArgumentError('Invalid network version'); + } + // 1 byte: depth: 0x00 for master nodes, 0x01 for level-1 descendants, ... + var depth = buffer[4]; + + // 4 bytes: the fingerprint of the parent's key (0x00000000 if master key) + var parentFingerprint = bytes.getUint32(5); + if (depth == 0) { + if (parentFingerprint != 0x00000000) { + throw ArgumentError('Invalid parent fingerprint'); + } + } + + // 4 bytes: child number. This is the number i in xi = xpar/i, with xi the key being serialized. + // This is encoded in MSB order. (0x00000000 if master key) + var index = bytes.getUint32(9); + if (depth == 0 && index != 0) { + throw ArgumentError('Invalid index'); + } + + // 32 bytes: the chain code + Uint8List chainCode = buffer.sublist(13, 45); + BIP32 hd; + + // 33 bytes: private key data (0x00 + k) + if (version == network.bip32.private) { + if (bytes.getUint8(45) != 0x00) { + throw ArgumentError('Invalid private key'); + } + Uint8List k = buffer.sublist(46, 78); + hd = BIP32.fromPrivateKey(k, chainCode, network); + } else { + // 33 bytes: public key data (0x02 + X or 0x03 + X) + Uint8List X = buffer.sublist(45, 78); + hd = BIP32.fromPublicKey(X, chainCode, network); + } + hd.depth = depth; + hd.index = index; + hd.parentFingerprint = parentFingerprint; + return hd; + } + + factory BIP32.fromPublicKey( + Uint8List publicKey, + Uint8List chainCode, [ + NetworkType? nw, + ]) { + NetworkType network = nw ?? BITCOIN; + if (!ecc.isPoint(publicKey)) { + throw ArgumentError('Point is not on the curve'); + } + return BIP32(null, publicKey, chainCode, network); + } + + factory BIP32.fromPrivateKey( + Uint8List privateKey, + Uint8List chainCode, [ + NetworkType? nw, + ]) { + NetworkType network = nw ?? BITCOIN; + if (privateKey.length != 32) { + throw ArgumentError( + 'Expected property privateKey of type Buffer(Length: 32)'); + } + if (!ecc.isPrivate(privateKey)) { + throw ArgumentError('Private key not in range [1, n]'); + } + return BIP32(privateKey, null, chainCode, network); + } + + factory BIP32.fromSeed(Uint8List seed, [NetworkType? nw]) { + if (seed.length < 16) { + throw ArgumentError('Seed should be at least 128 bits'); + } + if (seed.length > 64) { + throw ArgumentError('Seed should be at most 512 bits'); + } + NetworkType network = nw ?? BITCOIN; + final I = hmacSHA512(utf8.encode('Bitcoin seed'), seed); + final IL = I.sublist(0, 32); + final IR = I.sublist(32); + return BIP32.fromPrivateKey(IL, IR, network); + } +} diff --git a/example/wallet/lib/dependencies/bip32/utils/crypto.dart b/example/wallet/lib/dependencies/bip32/utils/crypto.dart new file mode 100644 index 00000000..3df8a41d --- /dev/null +++ b/example/wallet/lib/dependencies/bip32/utils/crypto.dart @@ -0,0 +1,21 @@ +// ignore_for_file: depend_on_referenced_packages, non_constant_identifier_names + +import 'dart:typed_data'; +import 'package:pointycastle/digests/sha512.dart'; +import 'package:pointycastle/api.dart'; +import 'package:pointycastle/macs/hmac.dart'; +import 'package:pointycastle/digests/ripemd160.dart'; +import 'package:pointycastle/digests/sha256.dart'; + +final ONE1 = Uint8List.fromList([1]); +final ZERO1 = Uint8List.fromList([0]); + +Uint8List hash160(Uint8List buffer) { + Uint8List tmp = SHA256Digest().process(buffer); + return RIPEMD160Digest().process(tmp); +} + +Uint8List hmacSHA512(Uint8List key, Uint8List data) { + final tmp = HMac(SHA512Digest(), 128)..init(KeyParameter(key)); + return tmp.process(data); +} diff --git a/example/wallet/lib/dependencies/bip32/utils/ecurve.dart b/example/wallet/lib/dependencies/bip32/utils/ecurve.dart new file mode 100644 index 00000000..39b5cbad --- /dev/null +++ b/example/wallet/lib/dependencies/bip32/utils/ecurve.dart @@ -0,0 +1,263 @@ +// ignore_for_file: constant_identifier_names, non_constant_identifier_names, depend_on_referenced_packages, implementation_imports + +import 'dart:typed_data'; +import 'package:convert/convert.dart'; +import 'package:pointycastle/ecc/curves/secp256k1.dart'; +import 'package:pointycastle/api.dart'; +import 'package:pointycastle/ecc/api.dart'; +import 'package:pointycastle/signers/ecdsa_signer.dart'; +import 'package:pointycastle/macs/hmac.dart'; +import 'package:pointycastle/digests/sha256.dart'; +import 'package:pointycastle/src/utils.dart'; + +final ZERO32 = Uint8List.fromList(List.generate(32, (index) => 0)); +final EC_GROUP_ORDER = hex + .decode('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'); +final EC_P = hex + .decode('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'); +final secp256k1 = ECCurve_secp256k1(); +final n = secp256k1.n; +final G = secp256k1.G; +BigInt nDiv2 = n >> 1; +const THROW_BAD_PRIVATE = 'Expected Private'; +const THROW_BAD_POINT = 'Expected Point'; +const THROW_BAD_TWEAK = 'Expected Tweak'; +const THROW_BAD_HASH = 'Expected Hash'; +const THROW_BAD_SIGNATURE = 'Expected Signature'; + +bool isPrivate(Uint8List x) { + if (!isScalar(x)) return false; + return _compare(x, ZERO32) > 0 && // > 0 + _compare(x, EC_GROUP_ORDER as Uint8List) < 0; // < G +} + +bool isPoint(Uint8List p) { + if (p.length < 33) { + return false; + } + var t = p[0]; + var x = p.sublist(1, 33); + + if (_compare(x, ZERO32) == 0) { + return false; + } + if (_compare(x, EC_P as Uint8List) == 1) { + return false; + } + try { + decodeFrom(p); + } catch (err) { + return false; + } + if ((t == 0x02 || t == 0x03) && p.length == 33) { + return true; + } + var y = p.sublist(33); + if (_compare(y, ZERO32) == 0) { + return false; + } + if (_compare(y, EC_P as Uint8List) == 1) { + return false; + } + if (t == 0x04 && p.length == 65) { + return true; + } + return false; +} + +bool isScalar(Uint8List x) { + return x.length == 32; +} + +bool isOrderScalar(x) { + if (!isScalar(x)) return false; + return _compare(x, EC_GROUP_ORDER as Uint8List) < 0; // < G +} + +bool isSignature(Uint8List value) { + Uint8List r = value.sublist(0, 32); + Uint8List s = value.sublist(32, 64); + + return value.length == 64 && + _compare(r, EC_GROUP_ORDER as Uint8List) < 0 && + _compare(s, EC_GROUP_ORDER as Uint8List) < 0; +} + +bool _isPointCompressed(Uint8List p) { + return p[0] != 0x04; +} + +bool assumeCompression(bool? value, Uint8List? pubkey) { + if (value == null && pubkey != null) return _isPointCompressed(pubkey); + if (value == null) return true; + return value; +} + +Uint8List? pointFromScalar(Uint8List d, bool compressed) { + if (!isPrivate(d)) { + throw ArgumentError(THROW_BAD_PRIVATE); + } + BigInt dd = fromBuffer(d); + ECPoint pp = (G * dd) as ECPoint; + if (pp.isInfinity) return null; + return getEncoded(pp, compressed); +} + +Uint8List? pointAddScalar(Uint8List p, Uint8List tweak, bool comprsd) { + if (!isPoint(p)) throw ArgumentError(THROW_BAD_POINT); + if (!isOrderScalar(tweak)) throw ArgumentError(THROW_BAD_TWEAK); + bool compressed = assumeCompression(comprsd, p); + ECPoint? pp = decodeFrom(p); + if (_compare(tweak, ZERO32) == 0) return getEncoded(pp, compressed); + BigInt tt = fromBuffer(tweak); + ECPoint qq = (G * tt) as ECPoint; + ECPoint uu = (pp! + qq) as ECPoint; + if (uu.isInfinity) return null; + return getEncoded(uu, compressed); +} + +Uint8List? privateAdd(Uint8List d, Uint8List tweak) { + if (!isPrivate(d)) throw ArgumentError(THROW_BAD_PRIVATE); + if (!isOrderScalar(tweak)) throw ArgumentError(THROW_BAD_TWEAK); + BigInt dd = fromBuffer(d); + BigInt tt = fromBuffer(tweak); + Uint8List dt = toBuffer((dd + tt) % n); + + if (dt.length < 32) { + Uint8List padLeadingZero = Uint8List(32 - dt.length); + dt = Uint8List.fromList(padLeadingZero + dt); + } + + if (!isPrivate(dt)) return null; + return dt; +} + +Uint8List sign(Uint8List hash, Uint8List x) { + if (!isScalar(hash)) throw ArgumentError(THROW_BAD_HASH); + if (!isPrivate(x)) throw ArgumentError(THROW_BAD_PRIVATE); + ECSignature sig = deterministicGenerateK(hash, x); + Uint8List buffer = Uint8List(64); + buffer.setRange(0, 32, _encodeBigInt(sig.r)); + BigInt s; + if (sig.s.compareTo(nDiv2) > 0) { + s = n - sig.s; + } else { + s = sig.s; + } + buffer.setRange(32, 64, _encodeBigInt(s)); + return buffer; +} + +bool verify(Uint8List hash, Uint8List q, Uint8List signature) { + if (!isScalar(hash)) throw ArgumentError(THROW_BAD_HASH); + if (!isPoint(q)) throw ArgumentError(THROW_BAD_POINT); + // 1.4.1 Enforce r and s are both integers in the interval [1, n − 1] (1, isSignature enforces '< n - 1') + if (!isSignature(signature)) throw ArgumentError(THROW_BAD_SIGNATURE); + + ECPoint? Q = decodeFrom(q); + BigInt r = fromBuffer(signature.sublist(0, 32)); + BigInt s = fromBuffer(signature.sublist(32, 64)); + + final signer = ECDSASigner(null, HMac(SHA256Digest(), 64)); + signer.init(false, PublicKeyParameter(ECPublicKey(Q, secp256k1))); + return signer.verifySignature(hash, ECSignature(r, s)); + /* STEP BY STEP + // 1.4.1 Enforce r and s are both integers in the interval [1, n − 1] (2, enforces '> 0') + if (r.compareTo(n) >= 0) return false; + if (s.compareTo(n) >= 0) return false; + + // 1.4.2 H = Hash(M), already done by the user + // 1.4.3 e = H + BigInt e = fromBuffer(hash); + + BigInt sInv = s.modInverse(n); + BigInt u1 = (e * sInv) % n; + BigInt u2 = (r * sInv) % n; + + // 1.4.5 Compute R = (xR, yR) + // R = u1G + u2Q + ECPoint R = G * u1 + Q * u2; + + // 1.4.5 (cont.) Enforce R is not at infinity + if (R.isInfinity) return false; + + // 1.4.6 Convert the field element R.x to an integer + BigInt xR = R.x.toBigInteger(); + + // 1.4.7 Set v = xR mod n + BigInt v = xR % n; + + // 1.4.8 If v = r, output "valid", and if v != r, output "invalid" + return v.compareTo(r) == 0; + */ +} + +/// Decode a BigInt from bytes in big-endian encoding. +BigInt _decodeBigInt(List bytes) { + BigInt result = BigInt.from(0); + for (int i = 0; i < bytes.length; i++) { + result += BigInt.from(bytes[bytes.length - i - 1]) << (8 * i); + } + return result; +} + +var _byteMask = BigInt.from(0xff); + +/// Encode a BigInt into bytes using big-endian encoding. +Uint8List _encodeBigInt(BigInt number) { + int needsPaddingByte; + int rawSize; + + if (number > BigInt.zero) { + rawSize = (number.bitLength + 7) >> 3; + needsPaddingByte = + ((number >> (rawSize - 1) * 8) & negativeFlag) == negativeFlag ? 1 : 0; + + if (rawSize < 32) { + needsPaddingByte = 1; + } + } else { + needsPaddingByte = 0; + rawSize = (number.bitLength + 8) >> 3; + } + + final size = rawSize < 32 ? rawSize + needsPaddingByte : rawSize; + var result = Uint8List(size); + for (int i = 0; i < size; i++) { + result[size - i - 1] = (number & _byteMask).toInt(); + number = number >> 8; + } + return result; +} + +BigInt fromBuffer(Uint8List d) { + return _decodeBigInt(d); +} + +Uint8List toBuffer(BigInt d) { + return _encodeBigInt(d); +} + +ECPoint? decodeFrom(Uint8List P) { + return secp256k1.curve.decodePoint(P); +} + +Uint8List getEncoded(ECPoint? P, compressed) { + return P!.getEncoded(compressed); +} + +ECSignature deterministicGenerateK(Uint8List hash, Uint8List x) { + final signer = ECDSASigner(null, HMac(SHA256Digest(), 64)); + var pkp = PrivateKeyParameter(ECPrivateKey(_decodeBigInt(x), secp256k1)); + signer.init(true, pkp); +// signer.init(false, new PublicKeyParameter(new ECPublicKey(secp256k1.curve.decodePoint(x), secp256k1))); + return signer.generateSignature(hash) as ECSignature; +} + +int _compare(Uint8List a, Uint8List b) { + BigInt aa = fromBuffer(a); + BigInt bb = fromBuffer(b); + if (aa == bb) return 0; + if (aa > bb) return 1; + return -1; +} diff --git a/example/wallet/lib/dependencies/bip32/utils/wif.dart b/example/wallet/lib/dependencies/bip32/utils/wif.dart new file mode 100644 index 00000000..cba7cf50 --- /dev/null +++ b/example/wallet/lib/dependencies/bip32/utils/wif.dart @@ -0,0 +1,56 @@ +// ignore_for_file: depend_on_referenced_packages + +import 'dart:typed_data'; +import 'package:bs58/bs58.dart'; + +class WIF { + int version; + Uint8List privateKey; + bool compressed; + WIF( + {required this.version, + required this.privateKey, + required this.compressed}); +} + +WIF decodeRaw(Uint8List buffer, [int? version]) { + if (version != null && buffer[0] != version) { + throw ArgumentError('Invalid network version'); + } + if (buffer.length == 33) { + return WIF( + version: buffer[0], + privateKey: buffer.sublist(1, 33), + compressed: false); + } + if (buffer.length != 34) { + throw ArgumentError('Invalid WIF length'); + } + if (buffer[33] != 0x01) { + throw ArgumentError('Invalid compression flag'); + } + return WIF( + version: buffer[0], privateKey: buffer.sublist(1, 33), compressed: true); +} + +Uint8List encodeRaw(int version, Uint8List privateKey, bool compressed) { + if (privateKey.length != 32) { + throw ArgumentError('Invalid privateKey length'); + } + Uint8List result = Uint8List(compressed ? 34 : 33); + ByteData bytes = result.buffer.asByteData(); + bytes.setUint8(0, version); + result.setRange(1, 33, privateKey); + if (compressed) { + result[33] = 0x01; + } + return result; +} + +WIF decode(String string, [int? version]) { + return decodeRaw(base58.decode(string), version); +} + +String encode(WIF wif) { + return base58.encode(encodeRaw(wif.version, wif.privateKey, wif.compressed)); +} diff --git a/example/wallet/lib/dependencies/bip39/bip39_base.dart b/example/wallet/lib/dependencies/bip39/bip39_base.dart new file mode 100644 index 00000000..f446a987 --- /dev/null +++ b/example/wallet/lib/dependencies/bip39/bip39_base.dart @@ -0,0 +1,153 @@ +// ignore_for_file: depend_on_referenced_packages, constant_identifier_names, non_constant_identifier_names + +import 'dart:math'; +import 'dart:typed_data'; + +// import 'package:crypto/crypto.dart' show sha256; +// import 'package:hex/hex.dart'; + +import 'package:convert/convert.dart'; +import 'package:pointycastle/digests/sha256.dart'; + +import 'utils/pbkdf2.dart'; +import 'wordlists/english.dart'; + +const int _SIZE_BYTE = 255; +const _INVALID_MNEMONIC = 'Invalid mnemonic'; +const _INVALID_ENTROPY = 'Invalid entropy'; +const _INVALID_CHECKSUM = 'Invalid mnemonic checksum'; + +typedef RandomBytes = Uint8List Function(int size); + +int _binaryToByte(String binary) { + return int.parse(binary, radix: 2); +} + +String _bytesToBinary(Uint8List bytes) { + return bytes.map((byte) => byte.toRadixString(2).padLeft(8, '0')).join(''); +} + +//Uint8List _createUint8ListFromString( String s ) { +// var ret = new Uint8List(s.length); +// for( var i=0 ; i 32) { + throw ArgumentError(_INVALID_ENTROPY); + } + if (entropy.length % 4 != 0) { + throw ArgumentError(_INVALID_ENTROPY); + } + final entropyBits = _bytesToBinary(entropy); + final checksumBits = _deriveChecksumBits(entropy); + final bits = entropyBits + checksumBits; + final regex = RegExp(r'.{1,11}', caseSensitive: false, multiLine: false); + final chunks = regex + .allMatches(bits) + .map((match) => match.group(0)!) + .toList(growable: false); + List wordlist = WORDLIST; + String words = + chunks.map((binary) => wordlist[_binaryToByte(binary)]).join(' '); + return words; +} + +Uint8List mnemonicToSeed(String mnemonic, {String passphrase = ''}) { + final pbkdf2 = PBKDF2(); + return pbkdf2.process(mnemonic, passphrase: passphrase); +} + +String mnemonicToSeedHex(String mnemonic, {String passphrase = ''}) { + return mnemonicToSeed(mnemonic, passphrase: passphrase).map((byte) { + return byte.toRadixString(16).padLeft(2, '0'); + }).join(''); +} + +bool validateMnemonic(String mnemonic) { + try { + mnemonicToEntropy(mnemonic); + } catch (e) { + return false; + } + return true; +} + +String mnemonicToEntropy(mnemonic) { + var words = mnemonic.split(' '); + if (words.length % 3 != 0) { + throw ArgumentError(_INVALID_MNEMONIC); + } + // convert word indices to 11 bit binary strings + final bits = words.map((word) { + final index = WORDLIST.indexOf(word); + if (index == -1) { + throw ArgumentError(_INVALID_MNEMONIC); + } + return index.toRadixString(2).padLeft(11, '0'); + }).join(''); + // split the binary string into ENT/CS + final dividerIndex = (bits.length / 33).floor() * 32; + final entropyBits = bits.substring(0, dividerIndex); + final checksumBits = bits.substring(dividerIndex); + + // calculate the checksum and compare + final regex = RegExp(r'.{1,8}'); + final entropyBytes = Uint8List.fromList(regex + .allMatches(entropyBits) + .map((match) => _binaryToByte(match.group(0)!)) + .toList(growable: false)); + if (entropyBytes.length < 16) { + throw StateError(_INVALID_ENTROPY); + } + if (entropyBytes.length > 32) { + throw StateError(_INVALID_ENTROPY); + } + if (entropyBytes.length % 4 != 0) { + throw StateError(_INVALID_ENTROPY); + } + final newChecksum = _deriveChecksumBits(entropyBytes); + if (newChecksum != checksumBits) { + throw StateError(_INVALID_CHECKSUM); + } + return entropyBytes.map((byte) { + return byte.toRadixString(16).padLeft(2, '0'); + }).join(''); +} +// List> _loadWordList() { +// final res = new Resource('package:bip39/src/wordlists/english.json').readAsString(); +// List words = (json.decode(res) as List).map((e) => e.toString()).toList(); +// return words; +// } diff --git a/example/wallet/lib/dependencies/bip39/utils/pbkdf2.dart b/example/wallet/lib/dependencies/bip39/utils/pbkdf2.dart new file mode 100644 index 00000000..474c0fc1 --- /dev/null +++ b/example/wallet/lib/dependencies/bip39/utils/pbkdf2.dart @@ -0,0 +1,31 @@ +// ignore_for_file: depend_on_referenced_packages + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:pointycastle/digests/sha512.dart'; +import 'package:pointycastle/key_derivators/api.dart'; +import 'package:pointycastle/key_derivators/pbkdf2.dart'; +import 'package:pointycastle/macs/hmac.dart'; + +class PBKDF2 { + final int blockLength; + final int iterationCount; + final int desiredKeyLength; + final String saltPrefix = 'mnemonic'; + + final PBKDF2KeyDerivator _derivator; + + PBKDF2({ + this.blockLength = 128, + this.iterationCount = 2048, + this.desiredKeyLength = 64, + }) : _derivator = PBKDF2KeyDerivator(HMac(SHA512Digest(), blockLength)); + + Uint8List process(String mnemonic, {passphrase = ''}) { + final salt = Uint8List.fromList(utf8.encode(saltPrefix + passphrase)); + _derivator.reset(); + _derivator.init(Pbkdf2Parameters(salt, iterationCount, desiredKeyLength)); + return _derivator.process(Uint8List.fromList(mnemonic.codeUnits)); + } +} diff --git a/example/wallet/lib/dependencies/bip39/wordlists/english.dart b/example/wallet/lib/dependencies/bip39/wordlists/english.dart new file mode 100644 index 00000000..8be30b62 --- /dev/null +++ b/example/wallet/lib/dependencies/bip39/wordlists/english.dart @@ -0,0 +1,2052 @@ +// ignore_for_file: constant_identifier_names + +const WORDLIST = [ + 'abandon', + 'ability', + 'able', + 'about', + 'above', + 'absent', + 'absorb', + 'abstract', + 'absurd', + 'abuse', + 'access', + 'accident', + 'account', + 'accuse', + 'achieve', + 'acid', + 'acoustic', + 'acquire', + 'across', + 'act', + 'action', + 'actor', + 'actress', + 'actual', + 'adapt', + 'add', + 'addict', + 'address', + 'adjust', + 'admit', + 'adult', + 'advance', + 'advice', + 'aerobic', + 'affair', + 'afford', + 'afraid', + 'again', + 'age', + 'agent', + 'agree', + 'ahead', + 'aim', + 'air', + 'airport', + 'aisle', + 'alarm', + 'album', + 'alcohol', + 'alert', + 'alien', + 'all', + 'alley', + 'allow', + 'almost', + 'alone', + 'alpha', + 'already', + 'also', + 'alter', + 'always', + 'amateur', + 'amazing', + 'among', + 'amount', + 'amused', + 'analyst', + 'anchor', + 'ancient', + 'anger', + 'angle', + 'angry', + 'animal', + 'ankle', + 'announce', + 'annual', + 'another', + 'answer', + 'antenna', + 'antique', + 'anxiety', + 'any', + 'apart', + 'apology', + 'appear', + 'apple', + 'approve', + 'april', + 'arch', + 'arctic', + 'area', + 'arena', + 'argue', + 'arm', + 'armed', + 'armor', + 'army', + 'around', + 'arrange', + 'arrest', + 'arrive', + 'arrow', + 'art', + 'artefact', + 'artist', + 'artwork', + 'ask', + 'aspect', + 'assault', + 'asset', + 'assist', + 'assume', + 'asthma', + 'athlete', + 'atom', + 'attack', + 'attend', + 'attitude', + 'attract', + 'auction', + 'audit', + 'august', + 'aunt', + 'author', + 'auto', + 'autumn', + 'average', + 'avocado', + 'avoid', + 'awake', + 'aware', + 'away', + 'awesome', + 'awful', + 'awkward', + 'axis', + 'baby', + 'bachelor', + 'bacon', + 'badge', + 'bag', + 'balance', + 'balcony', + 'ball', + 'bamboo', + 'banana', + 'banner', + 'bar', + 'barely', + 'bargain', + 'barrel', + 'base', + 'basic', + 'basket', + 'battle', + 'beach', + 'bean', + 'beauty', + 'because', + 'become', + 'beef', + 'before', + 'begin', + 'behave', + 'behind', + 'believe', + 'below', + 'belt', + 'bench', + 'benefit', + 'best', + 'betray', + 'better', + 'between', + 'beyond', + 'bicycle', + 'bid', + 'bike', + 'bind', + 'biology', + 'bird', + 'birth', + 'bitter', + 'black', + 'blade', + 'blame', + 'blanket', + 'blast', + 'bleak', + 'bless', + 'blind', + 'blood', + 'blossom', + 'blouse', + 'blue', + 'blur', + 'blush', + 'board', + 'boat', + 'body', + 'boil', + 'bomb', + 'bone', + 'bonus', + 'book', + 'boost', + 'border', + 'boring', + 'borrow', + 'boss', + 'bottom', + 'bounce', + 'box', + 'boy', + 'bracket', + 'brain', + 'brand', + 'brass', + 'brave', + 'bread', + 'breeze', + 'brick', + 'bridge', + 'brief', + 'bright', + 'bring', + 'brisk', + 'broccoli', + 'broken', + 'bronze', + 'broom', + 'brother', + 'brown', + 'brush', + 'bubble', + 'buddy', + 'budget', + 'buffalo', + 'build', + 'bulb', + 'bulk', + 'bullet', + 'bundle', + 'bunker', + 'burden', + 'burger', + 'burst', + 'bus', + 'business', + 'busy', + 'butter', + 'buyer', + 'buzz', + 'cabbage', + 'cabin', + 'cable', + 'cactus', + 'cage', + 'cake', + 'call', + 'calm', + 'camera', + 'camp', + 'can', + 'canal', + 'cancel', + 'candy', + 'cannon', + 'canoe', + 'canvas', + 'canyon', + 'capable', + 'capital', + 'captain', + 'car', + 'carbon', + 'card', + 'cargo', + 'carpet', + 'carry', + 'cart', + 'case', + 'cash', + 'casino', + 'castle', + 'casual', + 'cat', + 'catalog', + 'catch', + 'category', + 'cattle', + 'caught', + 'cause', + 'caution', + 'cave', + 'ceiling', + 'celery', + 'cement', + 'census', + 'century', + 'cereal', + 'certain', + 'chair', + 'chalk', + 'champion', + 'change', + 'chaos', + 'chapter', + 'charge', + 'chase', + 'chat', + 'cheap', + 'check', + 'cheese', + 'chef', + 'cherry', + 'chest', + 'chicken', + 'chief', + 'child', + 'chimney', + 'choice', + 'choose', + 'chronic', + 'chuckle', + 'chunk', + 'churn', + 'cigar', + 'cinnamon', + 'circle', + 'citizen', + 'city', + 'civil', + 'claim', + 'clap', + 'clarify', + 'claw', + 'clay', + 'clean', + 'clerk', + 'clever', + 'click', + 'client', + 'cliff', + 'climb', + 'clinic', + 'clip', + 'clock', + 'clog', + 'close', + 'cloth', + 'cloud', + 'clown', + 'club', + 'clump', + 'cluster', + 'clutch', + 'coach', + 'coast', + 'coconut', + 'code', + 'coffee', + 'coil', + 'coin', + 'collect', + 'color', + 'column', + 'combine', + 'come', + 'comfort', + 'comic', + 'common', + 'company', + 'concert', + 'conduct', + 'confirm', + 'congress', + 'connect', + 'consider', + 'control', + 'convince', + 'cook', + 'cool', + 'copper', + 'copy', + 'coral', + 'core', + 'corn', + 'correct', + 'cost', + 'cotton', + 'couch', + 'country', + 'couple', + 'course', + 'cousin', + 'cover', + 'coyote', + 'crack', + 'cradle', + 'craft', + 'cram', + 'crane', + 'crash', + 'crater', + 'crawl', + 'crazy', + 'cream', + 'credit', + 'creek', + 'crew', + 'cricket', + 'crime', + 'crisp', + 'critic', + 'crop', + 'cross', + 'crouch', + 'crowd', + 'crucial', + 'cruel', + 'cruise', + 'crumble', + 'crunch', + 'crush', + 'cry', + 'crystal', + 'cube', + 'culture', + 'cup', + 'cupboard', + 'curious', + 'current', + 'curtain', + 'curve', + 'cushion', + 'custom', + 'cute', + 'cycle', + 'dad', + 'damage', + 'damp', + 'dance', + 'danger', + 'daring', + 'dash', + 'daughter', + 'dawn', + 'day', + 'deal', + 'debate', + 'debris', + 'decade', + 'december', + 'decide', + 'decline', + 'decorate', + 'decrease', + 'deer', + 'defense', + 'define', + 'defy', + 'degree', + 'delay', + 'deliver', + 'demand', + 'demise', + 'denial', + 'dentist', + 'deny', + 'depart', + 'depend', + 'deposit', + 'depth', + 'deputy', + 'derive', + 'describe', + 'desert', + 'design', + 'desk', + 'despair', + 'destroy', + 'detail', + 'detect', + 'develop', + 'device', + 'devote', + 'diagram', + 'dial', + 'diamond', + 'diary', + 'dice', + 'diesel', + 'diet', + 'differ', + 'digital', + 'dignity', + 'dilemma', + 'dinner', + 'dinosaur', + 'direct', + 'dirt', + 'disagree', + 'discover', + 'disease', + 'dish', + 'dismiss', + 'disorder', + 'display', + 'distance', + 'divert', + 'divide', + 'divorce', + 'dizzy', + 'doctor', + 'document', + 'dog', + 'doll', + 'dolphin', + 'domain', + 'donate', + 'donkey', + 'donor', + 'door', + 'dose', + 'double', + 'dove', + 'draft', + 'dragon', + 'drama', + 'drastic', + 'draw', + 'dream', + 'dress', + 'drift', + 'drill', + 'drink', + 'drip', + 'drive', + 'drop', + 'drum', + 'dry', + 'duck', + 'dumb', + 'dune', + 'during', + 'dust', + 'dutch', + 'duty', + 'dwarf', + 'dynamic', + 'eager', + 'eagle', + 'early', + 'earn', + 'earth', + 'easily', + 'east', + 'easy', + 'echo', + 'ecology', + 'economy', + 'edge', + 'edit', + 'educate', + 'effort', + 'egg', + 'eight', + 'either', + 'elbow', + 'elder', + 'electric', + 'elegant', + 'element', + 'elephant', + 'elevator', + 'elite', + 'else', + 'embark', + 'embody', + 'embrace', + 'emerge', + 'emotion', + 'employ', + 'empower', + 'empty', + 'enable', + 'enact', + 'end', + 'endless', + 'endorse', + 'enemy', + 'energy', + 'enforce', + 'engage', + 'engine', + 'enhance', + 'enjoy', + 'enlist', + 'enough', + 'enrich', + 'enroll', + 'ensure', + 'enter', + 'entire', + 'entry', + 'envelope', + 'episode', + 'equal', + 'equip', + 'era', + 'erase', + 'erode', + 'erosion', + 'error', + 'erupt', + 'escape', + 'essay', + 'essence', + 'estate', + 'eternal', + 'ethics', + 'evidence', + 'evil', + 'evoke', + 'evolve', + 'exact', + 'example', + 'excess', + 'exchange', + 'excite', + 'exclude', + 'excuse', + 'execute', + 'exercise', + 'exhaust', + 'exhibit', + 'exile', + 'exist', + 'exit', + 'exotic', + 'expand', + 'expect', + 'expire', + 'explain', + 'expose', + 'express', + 'extend', + 'extra', + 'eye', + 'eyebrow', + 'fabric', + 'face', + 'faculty', + 'fade', + 'faint', + 'faith', + 'fall', + 'false', + 'fame', + 'family', + 'famous', + 'fan', + 'fancy', + 'fantasy', + 'farm', + 'fashion', + 'fat', + 'fatal', + 'father', + 'fatigue', + 'fault', + 'favorite', + 'feature', + 'february', + 'federal', + 'fee', + 'feed', + 'feel', + 'female', + 'fence', + 'festival', + 'fetch', + 'fever', + 'few', + 'fiber', + 'fiction', + 'field', + 'figure', + 'file', + 'film', + 'filter', + 'final', + 'find', + 'fine', + 'finger', + 'finish', + 'fire', + 'firm', + 'first', + 'fiscal', + 'fish', + 'fit', + 'fitness', + 'fix', + 'flag', + 'flame', + 'flash', + 'flat', + 'flavor', + 'flee', + 'flight', + 'flip', + 'float', + 'flock', + 'floor', + 'flower', + 'fluid', + 'flush', + 'fly', + 'foam', + 'focus', + 'fog', + 'foil', + 'fold', + 'follow', + 'food', + 'foot', + 'force', + 'forest', + 'forget', + 'fork', + 'fortune', + 'forum', + 'forward', + 'fossil', + 'foster', + 'found', + 'fox', + 'fragile', + 'frame', + 'frequent', + 'fresh', + 'friend', + 'fringe', + 'frog', + 'front', + 'frost', + 'frown', + 'frozen', + 'fruit', + 'fuel', + 'fun', + 'funny', + 'furnace', + 'fury', + 'future', + 'gadget', + 'gain', + 'galaxy', + 'gallery', + 'game', + 'gap', + 'garage', + 'garbage', + 'garden', + 'garlic', + 'garment', + 'gas', + 'gasp', + 'gate', + 'gather', + 'gauge', + 'gaze', + 'general', + 'genius', + 'genre', + 'gentle', + 'genuine', + 'gesture', + 'ghost', + 'giant', + 'gift', + 'giggle', + 'ginger', + 'giraffe', + 'girl', + 'give', + 'glad', + 'glance', + 'glare', + 'glass', + 'glide', + 'glimpse', + 'globe', + 'gloom', + 'glory', + 'glove', + 'glow', + 'glue', + 'goat', + 'goddess', + 'gold', + 'good', + 'goose', + 'gorilla', + 'gospel', + 'gossip', + 'govern', + 'gown', + 'grab', + 'grace', + 'grain', + 'grant', + 'grape', + 'grass', + 'gravity', + 'great', + 'green', + 'grid', + 'grief', + 'grit', + 'grocery', + 'group', + 'grow', + 'grunt', + 'guard', + 'guess', + 'guide', + 'guilt', + 'guitar', + 'gun', + 'gym', + 'habit', + 'hair', + 'half', + 'hammer', + 'hamster', + 'hand', + 'happy', + 'harbor', + 'hard', + 'harsh', + 'harvest', + 'hat', + 'have', + 'hawk', + 'hazard', + 'head', + 'health', + 'heart', + 'heavy', + 'hedgehog', + 'height', + 'hello', + 'helmet', + 'help', + 'hen', + 'hero', + 'hidden', + 'high', + 'hill', + 'hint', + 'hip', + 'hire', + 'history', + 'hobby', + 'hockey', + 'hold', + 'hole', + 'holiday', + 'hollow', + 'home', + 'honey', + 'hood', + 'hope', + 'horn', + 'horror', + 'horse', + 'hospital', + 'host', + 'hotel', + 'hour', + 'hover', + 'hub', + 'huge', + 'human', + 'humble', + 'humor', + 'hundred', + 'hungry', + 'hunt', + 'hurdle', + 'hurry', + 'hurt', + 'husband', + 'hybrid', + 'ice', + 'icon', + 'idea', + 'identify', + 'idle', + 'ignore', + 'ill', + 'illegal', + 'illness', + 'image', + 'imitate', + 'immense', + 'immune', + 'impact', + 'impose', + 'improve', + 'impulse', + 'inch', + 'include', + 'income', + 'increase', + 'index', + 'indicate', + 'indoor', + 'industry', + 'infant', + 'inflict', + 'inform', + 'inhale', + 'inherit', + 'initial', + 'inject', + 'injury', + 'inmate', + 'inner', + 'innocent', + 'input', + 'inquiry', + 'insane', + 'insect', + 'inside', + 'inspire', + 'install', + 'intact', + 'interest', + 'into', + 'invest', + 'invite', + 'involve', + 'iron', + 'island', + 'isolate', + 'issue', + 'item', + 'ivory', + 'jacket', + 'jaguar', + 'jar', + 'jazz', + 'jealous', + 'jeans', + 'jelly', + 'jewel', + 'job', + 'join', + 'joke', + 'journey', + 'joy', + 'judge', + 'juice', + 'jump', + 'jungle', + 'junior', + 'junk', + 'just', + 'kangaroo', + 'keen', + 'keep', + 'ketchup', + 'key', + 'kick', + 'kid', + 'kidney', + 'kind', + 'kingdom', + 'kiss', + 'kit', + 'kitchen', + 'kite', + 'kitten', + 'kiwi', + 'knee', + 'knife', + 'knock', + 'know', + 'lab', + 'label', + 'labor', + 'ladder', + 'lady', + 'lake', + 'lamp', + 'language', + 'laptop', + 'large', + 'later', + 'latin', + 'laugh', + 'laundry', + 'lava', + 'law', + 'lawn', + 'lawsuit', + 'layer', + 'lazy', + 'leader', + 'leaf', + 'learn', + 'leave', + 'lecture', + 'left', + 'leg', + 'legal', + 'legend', + 'leisure', + 'lemon', + 'lend', + 'length', + 'lens', + 'leopard', + 'lesson', + 'letter', + 'level', + 'liar', + 'liberty', + 'library', + 'license', + 'life', + 'lift', + 'light', + 'like', + 'limb', + 'limit', + 'link', + 'lion', + 'liquid', + 'list', + 'little', + 'live', + 'lizard', + 'load', + 'loan', + 'lobster', + 'local', + 'lock', + 'logic', + 'lonely', + 'long', + 'loop', + 'lottery', + 'loud', + 'lounge', + 'love', + 'loyal', + 'lucky', + 'luggage', + 'lumber', + 'lunar', + 'lunch', + 'luxury', + 'lyrics', + 'machine', + 'mad', + 'magic', + 'magnet', + 'maid', + 'mail', + 'main', + 'major', + 'make', + 'mammal', + 'man', + 'manage', + 'mandate', + 'mango', + 'mansion', + 'manual', + 'maple', + 'marble', + 'march', + 'margin', + 'marine', + 'market', + 'marriage', + 'mask', + 'mass', + 'master', + 'match', + 'material', + 'math', + 'matrix', + 'matter', + 'maximum', + 'maze', + 'meadow', + 'mean', + 'measure', + 'meat', + 'mechanic', + 'medal', + 'media', + 'melody', + 'melt', + 'member', + 'memory', + 'mention', + 'menu', + 'mercy', + 'merge', + 'merit', + 'merry', + 'mesh', + 'message', + 'metal', + 'method', + 'middle', + 'midnight', + 'milk', + 'million', + 'mimic', + 'mind', + 'minimum', + 'minor', + 'minute', + 'miracle', + 'mirror', + 'misery', + 'miss', + 'mistake', + 'mix', + 'mixed', + 'mixture', + 'mobile', + 'model', + 'modify', + 'mom', + 'moment', + 'monitor', + 'monkey', + 'monster', + 'month', + 'moon', + 'moral', + 'more', + 'morning', + 'mosquito', + 'mother', + 'motion', + 'motor', + 'mountain', + 'mouse', + 'move', + 'movie', + 'much', + 'muffin', + 'mule', + 'multiply', + 'muscle', + 'museum', + 'mushroom', + 'music', + 'must', + 'mutual', + 'myself', + 'mystery', + 'myth', + 'naive', + 'name', + 'napkin', + 'narrow', + 'nasty', + 'nation', + 'nature', + 'near', + 'neck', + 'need', + 'negative', + 'neglect', + 'neither', + 'nephew', + 'nerve', + 'nest', + 'net', + 'network', + 'neutral', + 'never', + 'news', + 'next', + 'nice', + 'night', + 'noble', + 'noise', + 'nominee', + 'noodle', + 'normal', + 'north', + 'nose', + 'notable', + 'note', + 'nothing', + 'notice', + 'novel', + 'now', + 'nuclear', + 'number', + 'nurse', + 'nut', + 'oak', + 'obey', + 'object', + 'oblige', + 'obscure', + 'observe', + 'obtain', + 'obvious', + 'occur', + 'ocean', + 'october', + 'odor', + 'off', + 'offer', + 'office', + 'often', + 'oil', + 'okay', + 'old', + 'olive', + 'olympic', + 'omit', + 'once', + 'one', + 'onion', + 'online', + 'only', + 'open', + 'opera', + 'opinion', + 'oppose', + 'option', + 'orange', + 'orbit', + 'orchard', + 'order', + 'ordinary', + 'organ', + 'orient', + 'original', + 'orphan', + 'ostrich', + 'other', + 'outdoor', + 'outer', + 'output', + 'outside', + 'oval', + 'oven', + 'over', + 'own', + 'owner', + 'oxygen', + 'oyster', + 'ozone', + 'pact', + 'paddle', + 'page', + 'pair', + 'palace', + 'palm', + 'panda', + 'panel', + 'panic', + 'panther', + 'paper', + 'parade', + 'parent', + 'park', + 'parrot', + 'party', + 'pass', + 'patch', + 'path', + 'patient', + 'patrol', + 'pattern', + 'pause', + 'pave', + 'payment', + 'peace', + 'peanut', + 'pear', + 'peasant', + 'pelican', + 'pen', + 'penalty', + 'pencil', + 'people', + 'pepper', + 'perfect', + 'permit', + 'person', + 'pet', + 'phone', + 'photo', + 'phrase', + 'physical', + 'piano', + 'picnic', + 'picture', + 'piece', + 'pig', + 'pigeon', + 'pill', + 'pilot', + 'pink', + 'pioneer', + 'pipe', + 'pistol', + 'pitch', + 'pizza', + 'place', + 'planet', + 'plastic', + 'plate', + 'play', + 'please', + 'pledge', + 'pluck', + 'plug', + 'plunge', + 'poem', + 'poet', + 'point', + 'polar', + 'pole', + 'police', + 'pond', + 'pony', + 'pool', + 'popular', + 'portion', + 'position', + 'possible', + 'post', + 'potato', + 'pottery', + 'poverty', + 'powder', + 'power', + 'practice', + 'praise', + 'predict', + 'prefer', + 'prepare', + 'present', + 'pretty', + 'prevent', + 'price', + 'pride', + 'primary', + 'print', + 'priority', + 'prison', + 'private', + 'prize', + 'problem', + 'process', + 'produce', + 'profit', + 'program', + 'project', + 'promote', + 'proof', + 'property', + 'prosper', + 'protect', + 'proud', + 'provide', + 'public', + 'pudding', + 'pull', + 'pulp', + 'pulse', + 'pumpkin', + 'punch', + 'pupil', + 'puppy', + 'purchase', + 'purity', + 'purpose', + 'purse', + 'push', + 'put', + 'puzzle', + 'pyramid', + 'quality', + 'quantum', + 'quarter', + 'question', + 'quick', + 'quit', + 'quiz', + 'quote', + 'rabbit', + 'raccoon', + 'race', + 'rack', + 'radar', + 'radio', + 'rail', + 'rain', + 'raise', + 'rally', + 'ramp', + 'ranch', + 'random', + 'range', + 'rapid', + 'rare', + 'rate', + 'rather', + 'raven', + 'raw', + 'razor', + 'ready', + 'real', + 'reason', + 'rebel', + 'rebuild', + 'recall', + 'receive', + 'recipe', + 'record', + 'recycle', + 'reduce', + 'reflect', + 'reform', + 'refuse', + 'region', + 'regret', + 'regular', + 'reject', + 'relax', + 'release', + 'relief', + 'rely', + 'remain', + 'remember', + 'remind', + 'remove', + 'render', + 'renew', + 'rent', + 'reopen', + 'repair', + 'repeat', + 'replace', + 'report', + 'require', + 'rescue', + 'resemble', + 'resist', + 'resource', + 'response', + 'result', + 'retire', + 'retreat', + 'return', + 'reunion', + 'reveal', + 'review', + 'reward', + 'rhythm', + 'rib', + 'ribbon', + 'rice', + 'rich', + 'ride', + 'ridge', + 'rifle', + 'right', + 'rigid', + 'ring', + 'riot', + 'ripple', + 'risk', + 'ritual', + 'rival', + 'river', + 'road', + 'roast', + 'robot', + 'robust', + 'rocket', + 'romance', + 'roof', + 'rookie', + 'room', + 'rose', + 'rotate', + 'rough', + 'round', + 'route', + 'royal', + 'rubber', + 'rude', + 'rug', + 'rule', + 'run', + 'runway', + 'rural', + 'sad', + 'saddle', + 'sadness', + 'safe', + 'sail', + 'salad', + 'salmon', + 'salon', + 'salt', + 'salute', + 'same', + 'sample', + 'sand', + 'satisfy', + 'satoshi', + 'sauce', + 'sausage', + 'save', + 'say', + 'scale', + 'scan', + 'scare', + 'scatter', + 'scene', + 'scheme', + 'school', + 'science', + 'scissors', + 'scorpion', + 'scout', + 'scrap', + 'screen', + 'script', + 'scrub', + 'sea', + 'search', + 'season', + 'seat', + 'second', + 'secret', + 'section', + 'security', + 'seed', + 'seek', + 'segment', + 'select', + 'sell', + 'seminar', + 'senior', + 'sense', + 'sentence', + 'series', + 'service', + 'session', + 'settle', + 'setup', + 'seven', + 'shadow', + 'shaft', + 'shallow', + 'share', + 'shed', + 'shell', + 'sheriff', + 'shield', + 'shift', + 'shine', + 'ship', + 'shiver', + 'shock', + 'shoe', + 'shoot', + 'shop', + 'short', + 'shoulder', + 'shove', + 'shrimp', + 'shrug', + 'shuffle', + 'shy', + 'sibling', + 'sick', + 'side', + 'siege', + 'sight', + 'sign', + 'silent', + 'silk', + 'silly', + 'silver', + 'similar', + 'simple', + 'since', + 'sing', + 'siren', + 'sister', + 'situate', + 'six', + 'size', + 'skate', + 'sketch', + 'ski', + 'skill', + 'skin', + 'skirt', + 'skull', + 'slab', + 'slam', + 'sleep', + 'slender', + 'slice', + 'slide', + 'slight', + 'slim', + 'slogan', + 'slot', + 'slow', + 'slush', + 'small', + 'smart', + 'smile', + 'smoke', + 'smooth', + 'snack', + 'snake', + 'snap', + 'sniff', + 'snow', + 'soap', + 'soccer', + 'social', + 'sock', + 'soda', + 'soft', + 'solar', + 'soldier', + 'solid', + 'solution', + 'solve', + 'someone', + 'song', + 'soon', + 'sorry', + 'sort', + 'soul', + 'sound', + 'soup', + 'source', + 'south', + 'space', + 'spare', + 'spatial', + 'spawn', + 'speak', + 'special', + 'speed', + 'spell', + 'spend', + 'sphere', + 'spice', + 'spider', + 'spike', + 'spin', + 'spirit', + 'split', + 'spoil', + 'sponsor', + 'spoon', + 'sport', + 'spot', + 'spray', + 'spread', + 'spring', + 'spy', + 'square', + 'squeeze', + 'squirrel', + 'stable', + 'stadium', + 'staff', + 'stage', + 'stairs', + 'stamp', + 'stand', + 'start', + 'state', + 'stay', + 'steak', + 'steel', + 'stem', + 'step', + 'stereo', + 'stick', + 'still', + 'sting', + 'stock', + 'stomach', + 'stone', + 'stool', + 'story', + 'stove', + 'strategy', + 'street', + 'strike', + 'strong', + 'struggle', + 'student', + 'stuff', + 'stumble', + 'style', + 'subject', + 'submit', + 'subway', + 'success', + 'such', + 'sudden', + 'suffer', + 'sugar', + 'suggest', + 'suit', + 'summer', + 'sun', + 'sunny', + 'sunset', + 'super', + 'supply', + 'supreme', + 'sure', + 'surface', + 'surge', + 'surprise', + 'surround', + 'survey', + 'suspect', + 'sustain', + 'swallow', + 'swamp', + 'swap', + 'swarm', + 'swear', + 'sweet', + 'swift', + 'swim', + 'swing', + 'switch', + 'sword', + 'symbol', + 'symptom', + 'syrup', + 'system', + 'table', + 'tackle', + 'tag', + 'tail', + 'talent', + 'talk', + 'tank', + 'tape', + 'target', + 'task', + 'taste', + 'tattoo', + 'taxi', + 'teach', + 'team', + 'tell', + 'ten', + 'tenant', + 'tennis', + 'tent', + 'term', + 'test', + 'text', + 'thank', + 'that', + 'theme', + 'then', + 'theory', + 'there', + 'they', + 'thing', + 'this', + 'thought', + 'three', + 'thrive', + 'throw', + 'thumb', + 'thunder', + 'ticket', + 'tide', + 'tiger', + 'tilt', + 'timber', + 'time', + 'tiny', + 'tip', + 'tired', + 'tissue', + 'title', + 'toast', + 'tobacco', + 'today', + 'toddler', + 'toe', + 'together', + 'toilet', + 'token', + 'tomato', + 'tomorrow', + 'tone', + 'tongue', + 'tonight', + 'tool', + 'tooth', + 'top', + 'topic', + 'topple', + 'torch', + 'tornado', + 'tortoise', + 'toss', + 'total', + 'tourist', + 'toward', + 'tower', + 'town', + 'toy', + 'track', + 'trade', + 'traffic', + 'tragic', + 'train', + 'transfer', + 'trap', + 'trash', + 'travel', + 'tray', + 'treat', + 'tree', + 'trend', + 'trial', + 'tribe', + 'trick', + 'trigger', + 'trim', + 'trip', + 'trophy', + 'trouble', + 'truck', + 'true', + 'truly', + 'trumpet', + 'trust', + 'truth', + 'try', + 'tube', + 'tuition', + 'tumble', + 'tuna', + 'tunnel', + 'turkey', + 'turn', + 'turtle', + 'twelve', + 'twenty', + 'twice', + 'twin', + 'twist', + 'two', + 'type', + 'typical', + 'ugly', + 'umbrella', + 'unable', + 'unaware', + 'uncle', + 'uncover', + 'under', + 'undo', + 'unfair', + 'unfold', + 'unhappy', + 'uniform', + 'unique', + 'unit', + 'universe', + 'unknown', + 'unlock', + 'until', + 'unusual', + 'unveil', + 'update', + 'upgrade', + 'uphold', + 'upon', + 'upper', + 'upset', + 'urban', + 'urge', + 'usage', + 'use', + 'used', + 'useful', + 'useless', + 'usual', + 'utility', + 'vacant', + 'vacuum', + 'vague', + 'valid', + 'valley', + 'valve', + 'van', + 'vanish', + 'vapor', + 'various', + 'vast', + 'vault', + 'vehicle', + 'velvet', + 'vendor', + 'venture', + 'venue', + 'verb', + 'verify', + 'version', + 'very', + 'vessel', + 'veteran', + 'viable', + 'vibrant', + 'vicious', + 'victory', + 'video', + 'view', + 'village', + 'vintage', + 'violin', + 'virtual', + 'virus', + 'visa', + 'visit', + 'visual', + 'vital', + 'vivid', + 'vocal', + 'voice', + 'void', + 'volcano', + 'volume', + 'vote', + 'voyage', + 'wage', + 'wagon', + 'wait', + 'walk', + 'wall', + 'walnut', + 'want', + 'warfare', + 'warm', + 'warrior', + 'wash', + 'wasp', + 'waste', + 'water', + 'wave', + 'way', + 'wealth', + 'weapon', + 'wear', + 'weasel', + 'weather', + 'web', + 'wedding', + 'weekend', + 'weird', + 'welcome', + 'west', + 'wet', + 'whale', + 'what', + 'wheat', + 'wheel', + 'when', + 'where', + 'whip', + 'whisper', + 'wide', + 'width', + 'wife', + 'wild', + 'will', + 'win', + 'window', + 'wine', + 'wing', + 'wink', + 'winner', + 'winter', + 'wire', + 'wisdom', + 'wise', + 'wish', + 'witness', + 'wolf', + 'woman', + 'wonder', + 'wood', + 'wool', + 'word', + 'work', + 'world', + 'worry', + 'worth', + 'wrap', + 'wreck', + 'wrestle', + 'wrist', + 'write', + 'wrong', + 'yard', + 'year', + 'yellow', + 'you', + 'young', + 'youth', + 'zebra', + 'zero', + 'zone', + 'zoo' +]; diff --git a/example/wallet/lib/dependencies/chains/evm_service.dart b/example/wallet/lib/dependencies/chains/evm_service.dart index 3889b674..cfbbcca5 100644 --- a/example/wallet/lib/dependencies/chains/evm_service.dart +++ b/example/wallet/lib/dependencies/chains/evm_service.dart @@ -10,7 +10,7 @@ import 'package:walletconnect_flutter_v2_wallet/dependencies/bottom_sheet/i_bott import 'package:walletconnect_flutter_v2_wallet/dependencies/deep_link_handler.dart'; import 'package:walletconnect_flutter_v2_wallet/dependencies/i_web3wallet_service.dart'; import 'package:walletconnect_flutter_v2_wallet/dependencies/key_service/i_key_service.dart'; -import 'package:walletconnect_flutter_v2_wallet/models/eth/ethereum_transaction.dart'; +import 'package:walletconnect_flutter_v2_wallet/models/chain_data.dart'; import 'package:walletconnect_flutter_v2_wallet/utils/eth_utils.dart'; import 'package:walletconnect_flutter_v2_wallet/widgets/wc_connection_widget/wc_connection_model.dart'; import 'package:walletconnect_flutter_v2_wallet/widgets/wc_connection_widget/wc_connection_widget.dart'; @@ -21,7 +21,7 @@ enum EVMChainsSupported { polygon, arbitrum, sepolia, - bsc, + // bsc, mumbai; String chain() { @@ -40,9 +40,9 @@ enum EVMChainsSupported { case EVMChainsSupported.sepolia: id = '11155111'; break; - case EVMChainsSupported.bsc: - id = '56'; - break; + // case EVMChainsSupported.bsc: + // id = '56'; + // break; case EVMChainsSupported.mumbai: id = '80001'; break; @@ -58,14 +58,16 @@ class EVMService { final _web3Wallet = GetIt.I().getWeb3Wallet(); final EVMChainsSupported chainSupported; - final Web3Client ethClient; - - EVMService({required this.chainSupported, Web3Client? ethClient}) - : ethClient = ethClient ?? - Web3Client( - 'https://mainnet.infura.io/v3/51716d2096df4e73bec298680a51f0c5', - http.Client()) { - // Supported events + late final Web3Client ethClient; + + EVMService({required this.chainSupported}) { + final supportedId = chainSupported.chain(); + final chainMetadata = ChainData.allChains.firstWhere( + (c) => c.chainId == supportedId, + ); + debugPrint('supportedId $supportedId - ${chainMetadata.rpc.first}'); + ethClient = Web3Client(chainMetadata.rpc.first, http.Client()); + const supportedEvents = EventsConstants.requiredEvents; for (final String event in supportedEvents) { debugPrint('Supported event ${chainSupported.chain()} $event'); @@ -80,7 +82,7 @@ class EVMService { 'eth_sign': ethSign, 'eth_signTransaction': ethSignTransaction, 'eth_signTypedData': ethSignTypedData, - 'eth_sendTransaction': ethSignTransaction, + 'eth_sendTransaction': ethSendTransaction, 'eth_signTypedData_v4': ethSignTypedData, 'wallet_switchEthereumChain': switchChain, 'wallet_addEthereumChain': addChain, @@ -121,6 +123,7 @@ class EVMService { } catch (e) { debugPrint('[$runtimeType] personalSign error $e'); result = e.toString(); + // _web3Wallet.respondSessionRequest(topic: topic, response: response) } } @@ -170,54 +173,23 @@ class EVMService { debugPrint('[$runtimeType] ethSignTransaction request: $parameters'); String? result; - result = await requestApproval(jsonEncode(parameters[0])); - if (result == null) { - // Load the private key - final keys = GetIt.I().getKeysForChain( - chainSupported.chain(), - ); - final credentials = EthPrivateKey.fromHex('0x${keys[0].privateKey}'); - - final ethTransaction = EthereumTransaction.fromJson(parameters[0]); + final tJson = parameters[0] as Map; + final transaction = await approveTransaction(jsonEncode(tJson)); + if (transaction != null) { + try { + // Load the private key + final keys = GetIt.I().getKeysForChain( + chainSupported.chain(), + ); + final credentials = EthPrivateKey.fromHex('0x${keys[0].privateKey}'); - // Construct a transaction from the EthereumTransaction object - final transaction = Transaction( - from: EthereumAddress.fromHex(ethTransaction.from), - to: EthereumAddress.fromHex(ethTransaction.to), - value: EtherAmount.fromBigInt( - EtherUnit.wei, - BigInt.tryParse(ethTransaction.value) ?? BigInt.zero, - ), - gasPrice: ethTransaction.gasPrice != null - ? EtherAmount.fromBigInt( - EtherUnit.wei, - BigInt.tryParse(ethTransaction.gasPrice!) ?? BigInt.zero, - ) - : null, - maxFeePerGas: ethTransaction.maxFeePerGas != null - ? EtherAmount.fromBigInt( - EtherUnit.wei, - BigInt.tryParse(ethTransaction.maxFeePerGas!) ?? BigInt.zero, - ) - : null, - maxPriorityFeePerGas: ethTransaction.maxPriorityFeePerGas != null - ? EtherAmount.fromBigInt( - EtherUnit.wei, - BigInt.tryParse(ethTransaction.maxPriorityFeePerGas!) ?? - BigInt.zero, - ) - : null, - maxGas: int.tryParse(ethTransaction.gasLimit ?? ''), - nonce: int.tryParse(ethTransaction.nonce ?? ''), - data: (ethTransaction.data != null && ethTransaction.data != '0x') - ? Uint8List.fromList(hex.decode(ethTransaction.data!)) - : null, - ); + final chainId = chainSupported.chain().split(':').last; + debugPrint('[$runtimeType] ethSignTransaction chainId: $chainId'); - try { final signature = await ethClient.signTransaction( credentials, transaction, + chainId: int.parse(chainId), ); // Sign the transaction @@ -225,8 +197,47 @@ class EVMService { // Return the signed transaction as a hexadecimal string result = '0x$signedTx'; - } catch (e) { + } catch (e, s) { debugPrint('[$runtimeType] ethSignTransaction error $e'); + print(s); + result = e.toString(); + } + } + + final session = _web3Wallet.sessions.get(topic); + final scheme = session?.peer.metadata.redirect?.native ?? ''; + DeepLinkHandler.goTo(scheme, delay: 300, modalTitle: 'Success'); + + return result; + } + + Future ethSendTransaction(String topic, dynamic parameters) async { + debugPrint('[$runtimeType] ethSendTransaction request: $parameters'); + String? result; + + final tJson = parameters[0] as Map; + final transaction = await approveTransaction(jsonEncode(tJson)); + if (transaction != null) { + try { + // Load the private key + final keys = GetIt.I().getKeysForChain( + chainSupported.chain(), + ); + final credentials = EthPrivateKey.fromHex('0x${keys[0].privateKey}'); + + final chainId = chainSupported.chain().split(':').last; + debugPrint('[$runtimeType] ethSendTransaction chainId: $chainId'); + + final signedTx = await ethClient.sendTransaction( + credentials, + transaction, + chainId: int.parse(chainId), + ); + + result = '0x$signedTx'; + } catch (e, s) { + debugPrint('[$runtimeType] ethSendTransaction error $e'); + print(s); result = e.toString(); } } @@ -306,4 +317,95 @@ class EVMService { return null; } + + Future approveTransaction(String text) async { + final tJson = jsonDecode(text) as Map; + String? tValue = tJson['value']; + debugPrint('tValue $tValue'); + EtherAmount? value; + if (tValue != null) { + tValue = tValue.replaceFirst('0x', ''); + value = EtherAmount.fromBigInt( + EtherUnit.wei, + BigInt.from(int.parse(tValue, radix: 16)), + ); + } + Transaction transaction = Transaction( + from: EthereumAddress.fromHex(tJson['from']), + to: EthereumAddress.fromHex(tJson['to']), + value: value, + gasPrice: tJson['gasPrice'], + maxFeePerGas: tJson['maxFeePerGas'], + maxPriorityFeePerGas: tJson['maxPriorityFeePerGas'], + maxGas: tJson['maxGas'], + nonce: tJson['nonce'], + data: (tJson['data'] != null && tJson['data'] != '0x') + ? Uint8List.fromList(hex.decode(tJson['data']!)) + : null, + ); + + // Gas limit + final gasLimit = await ethClient.estimateGas( + sender: transaction.from, + to: transaction.to, + value: transaction.value, + data: transaction.data, + ); + print(gasLimit.toInt().toString()); + // BigInt estimateGas; + // BigInt maxFee; + + final gasPrice = await ethClient.getGasPrice(); + print(gasPrice.getInWei); + + transaction = transaction.copyWith( + maxGas: gasLimit.toInt(), + gasPrice: gasPrice, + ); + + final estimateGas = gasLimit * gasPrice.getInWei; + final maxFee = estimateGas; + final trxValue = transaction.value ?? EtherAmount.zero(); + + BigInt total = estimateGas + trxValue.getInWei; + BigInt maxAmount = maxFee + trxValue.getInWei; + + // Adjust the amount if it exceeds the balance + if (trxValue.getInWei > BigInt.zero) { + final chainId = await ethClient.getNetworkId(); + debugPrint(chainId.toString()); + final balance = await ethClient.getBalance(transaction.from!); + if (maxAmount > balance.getInWei) { + final amountLeft = balance.getInWei - maxFee; + if (amountLeft <= BigInt.zero) { + throw Exception( + 'Insufficient funds for transfer, maybe it needs gas fee.', + ); + } + + transaction = transaction.copyWith( + value: EtherAmount.inWei(amountLeft), + ); + total = estimateGas + trxValue.getInWei; + maxAmount = maxFee + trxValue.getInWei; + } + } + + final approved = await _bottomSheetService.queueBottomSheet( + widget: WCRequestWidget( + child: WCConnectionWidget( + title: 'Sign Transaction', + info: [ + WCConnectionModel(text: text), + ], + ), + ), + ); + + if (approved != null && approved == false) { + return null; + } + + return transaction; + } } diff --git a/example/wallet/lib/dependencies/key_service/chain_key.dart b/example/wallet/lib/dependencies/key_service/chain_key.dart index 8918a7ed..8db7da01 100644 --- a/example/wallet/lib/dependencies/key_service/chain_key.dart +++ b/example/wallet/lib/dependencies/key_service/chain_key.dart @@ -2,15 +2,17 @@ class ChainKey { final List chains; final String privateKey; final String publicKey; + final String address; ChainKey({ required this.chains, required this.privateKey, required this.publicKey, + required this.address, }); @override String toString() { - return 'ChainKey(chains: $chains, privateKey: $privateKey, publicKey: $publicKey)'; + return 'ChainKey(chains: $chains, privateKey: $privateKey, publicKey: $publicKey, address: $address)'; } } diff --git a/example/wallet/lib/dependencies/key_service/i_key_service.dart b/example/wallet/lib/dependencies/key_service/i_key_service.dart index 28406ccf..4c7aa19b 100644 --- a/example/wallet/lib/dependencies/key_service/i_key_service.dart +++ b/example/wallet/lib/dependencies/key_service/i_key_service.dart @@ -1,8 +1,9 @@ +import 'package:walletconnect_flutter_v2/apis/core/crypto/crypto_models.dart'; import 'package:walletconnect_flutter_v2_wallet/dependencies/key_service/chain_key.dart'; abstract class IKeyService { /// Returns a list of all the keys. - List getKeys(); + Future> setKeys(); /// Returns a list of all the chain ids. List getChains(); @@ -14,4 +15,16 @@ abstract class IKeyService { /// Returns a list of all the accounts in namespace:chainId:address format. List getAllAccounts(); + + String generateMnemonic(); + + CryptoKeyPair keyPairFromMnemonic(String mnemonic); + + String getAddressFromPrivateKey(String privateKey); + + Future createWallet(); + + Future restoreWallet({required String mnemonic}); + + Future deleteWallet(); } diff --git a/example/wallet/lib/dependencies/key_service/key_service.dart b/example/wallet/lib/dependencies/key_service/key_service.dart index 0bea000b..f831ffe4 100644 --- a/example/wallet/lib/dependencies/key_service/key_service.dart +++ b/example/wallet/lib/dependencies/key_service/key_service.dart @@ -1,27 +1,53 @@ +import 'package:convert/convert.dart'; +import 'package:flutter/foundation.dart'; +import 'package:walletconnect_flutter_v2/apis/core/crypto/crypto_models.dart'; +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; import 'package:walletconnect_flutter_v2_wallet/dependencies/chains/evm_service.dart'; import 'package:walletconnect_flutter_v2_wallet/dependencies/key_service/chain_key.dart'; import 'package:walletconnect_flutter_v2_wallet/dependencies/key_service/i_key_service.dart'; import 'package:walletconnect_flutter_v2_wallet/utils/dart_defines.dart'; +import 'package:walletconnect_flutter_v2_wallet/dependencies/bip39/bip39_base.dart' + as bip39; +import 'package:walletconnect_flutter_v2_wallet/dependencies/bip32/bip32_base.dart' + as bip32; class KeyService extends IKeyService { - final List keys = [ - ChainKey( + final List keys = []; + + @override + Future> setKeys() async { + // WARNING: SharedPreferences is not the best way to store your keys! + final prefs = await SharedPreferences.getInstance(); + final privateKey = prefs.getString('privateKey') ?? ''; + if (privateKey.isEmpty) { + return []; + } + final publicKey = prefs.getString('publicKey') ?? ''; + final address = getAddressFromPrivateKey(privateKey); + final evmChainKey = ChainKey( + chains: EVMChainsSupported.values.map((e) => e.chain()).toList(), + privateKey: privateKey, + publicKey: publicKey, + address: address, + ); + debugPrint(evmChainKey.toString()); + final kadenaChainKey = ChainKey( chains: [ 'kadena:mainnet01', 'kadena:testnet04', 'kadena:development', ], privateKey: DartDefines.kadenaPrivateKey, - publicKey: DartDefines.kadenaPublicKey, - ), - ChainKey( - chains: EVMChainsSupported.values.map((e) => e.chain()).toList(), - privateKey: - '300851edb635b2dbb2d4e70615444925afeb60bf95c19365aff88740e09d7345', - publicKey: - '0xeaa05f75445a4beacc73e8fbf07ddb3a76a80a0c', // Eth Address, not actual public key - ) - ]; + publicKey: '', + address: DartDefines.kadenaAddress, + ); + keys + ..clear() + ..addAll( + [kadenaChainKey, evmChainKey], + ); + return keys; + } @override List getChains() { @@ -32,11 +58,6 @@ class KeyService extends IKeyService { return chainIds; } - @override - List getKeys() { - return keys; - } - @override List getKeysForChain(String chain) { return keys.where((e) => e.chains.contains(chain)).toList(); @@ -47,9 +68,85 @@ class KeyService extends IKeyService { final List accounts = []; for (final ChainKey key in keys) { for (final String chain in key.chains) { - accounts.add('$chain:${key.publicKey}'); + accounts.add('$chain:${key.address}'); } } return accounts; } + + @override + String generateMnemonic() => bip39.generateMnemonic(); + + @override + CryptoKeyPair keyPairFromMnemonic(String mnemonic) { + final isValidMnemonic = bip39.validateMnemonic(mnemonic); + if (!isValidMnemonic) { + throw 'Invalid mnemonic'; + } + + final seed = bip39.mnemonicToSeed(mnemonic); + final root = bip32.BIP32.fromSeed(seed); + + final firstChild = root.derivePath("m/44'/60'/0'/0/0"); + final private = hex.encode(firstChild.privateKey as List); + final public = hex.encode(firstChild.publicKey); + return CryptoKeyPair(private, public); + } + + @override + String getAddressFromPrivateKey(String privateKey) { + final private = EthPrivateKey.fromHex(privateKey); + return private.address.hex; + } + + @override + Future createWallet() async { + final mnemonic = generateMnemonic(); + return await restoreWallet(mnemonic: mnemonic); + } + + @override + Future restoreWallet({required String mnemonic}) async { + print(mnemonic); + final keyPair = keyPairFromMnemonic(mnemonic); + final address = getAddressFromPrivateKey(keyPair.privateKey); + final evmChainKey = ChainKey( + chains: EVMChainsSupported.values.map((e) => e.chain()).toList(), + privateKey: keyPair.privateKey, + publicKey: keyPair.publicKey, + address: address, + ); + debugPrint(evmChainKey.toString()); + final kadenaChainKey = ChainKey( + chains: [ + 'kadena:mainnet01', + 'kadena:testnet04', + 'kadena:development', + ], + privateKey: DartDefines.kadenaPrivateKey, + publicKey: '', + address: DartDefines.kadenaAddress, + ); + + keys + ..clear() + ..addAll( + [kadenaChainKey, evmChainKey], + ); + + // WARNING: SharedPreferences is not the best way to store your keys! + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('privateKey', keyPair.privateKey); + await prefs.setString('publicKey', keyPair.publicKey); + await prefs.setString('mnemonic', mnemonic); + return; + } + + @override + Future deleteWallet() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.clear(); + keys.clear(); + return; + } } diff --git a/example/wallet/lib/dependencies/web3wallet_service.dart b/example/wallet/lib/dependencies/web3wallet_service.dart index 4a3e0258..e898b79d 100644 --- a/example/wallet/lib/dependencies/web3wallet_service.dart +++ b/example/wallet/lib/dependencies/web3wallet_service.dart @@ -34,7 +34,7 @@ class Web3WalletService extends IWeb3WalletService { ValueNotifier> auth = ValueNotifier>([]); @override - void create() { + void create() async { // Create the web3wallet _web3Wallet = Web3Wallet( core: Core( @@ -56,18 +56,22 @@ class Web3WalletService extends IWeb3WalletService { ); // Setup our accounts - List chainKeys = GetIt.I().getKeys(); + List chainKeys = await GetIt.I().setKeys(); + if (chainKeys.isEmpty) { + await GetIt.I().createWallet(); + chainKeys = await GetIt.I().setKeys(); + } for (final chainKey in chainKeys) { for (final chainId in chainKey.chains) { if (chainId.startsWith('kadena')) { _web3Wallet!.registerAccount( chainId: chainId, - accountAddress: 'k**${chainKey.publicKey}', + accountAddress: 'k**${chainKey.address}', ); } else { _web3Wallet!.registerAccount( chainId: chainId, - accountAddress: chainKey.publicKey, + accountAddress: chainKey.address, ); } } @@ -172,7 +176,7 @@ class Web3WalletService extends IWeb3WalletService { 'eip155:1', ); // Create the message to be signed - final String iss = 'did:pkh:eip155:1:${chainKeys.first.publicKey}'; + final String iss = 'did:pkh:eip155:1:${chainKeys.first.address}'; final bool? auth = await _bottomSheetHandler.queueBottomSheet( widget: WCRequestWidget( diff --git a/example/wallet/lib/main.dart b/example/wallet/lib/main.dart index decb1010..f6006faf 100644 --- a/example/wallet/lib/main.dart +++ b/example/wallet/lib/main.dart @@ -11,6 +11,7 @@ import 'package:walletconnect_flutter_v2_wallet/dependencies/key_service/key_ser import 'package:walletconnect_flutter_v2_wallet/dependencies/web3wallet_service.dart'; import 'package:walletconnect_flutter_v2_wallet/models/page_data.dart'; import 'package:walletconnect_flutter_v2_wallet/pages/apps_page.dart'; +import 'package:walletconnect_flutter_v2_wallet/pages/settings_page.dart'; import 'package:walletconnect_flutter_v2_wallet/utils/constants.dart'; import 'package:flutter/material.dart'; import 'package:walletconnect_flutter_v2_wallet/utils/string_constants.dart'; @@ -88,12 +89,7 @@ class _MyHomePageState extends State with GetItStateMixin { icon: Icons.inbox_rounded, ), PageData( - page: const Center( - child: Text( - 'Settings (Not Implemented)', - style: StyleConstants.bodyText, - ), - ), + page: const SettingsPage(), title: 'Settings', icon: Icons.settings_outlined, ), diff --git a/example/wallet/lib/models/chain_data.dart b/example/wallet/lib/models/chain_data.dart new file mode 100644 index 00000000..edaef5a4 --- /dev/null +++ b/example/wallet/lib/models/chain_data.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:walletconnect_flutter_v2_wallet/models/chain_metadata.dart'; + +class ChainData { + static final List mainChains = [ + ChainMetadata( + type: ChainType.eip155, + chainId: 'eip155:1', + name: 'Ethereum', + logo: '/chain-logos/eip155-1.png', + color: Colors.blue.shade300, + rpc: ['https://eth.drpc.org'], + ), + ChainMetadata( + type: ChainType.eip155, + chainId: 'eip155:137', + name: 'Polygon', + logo: '/chain-logos/eip155-137.png', + color: Colors.purple.shade300, + rpc: ['https://polygon-rpc.com/'], + ), + const ChainMetadata( + type: ChainType.eip155, + chainId: 'eip155:42161', + name: 'Arbitrum', + logo: '/chain-logos/eip155-42161.png', + color: Colors.blue, + rpc: ['https://arbitrum.blockpi.network/v1/rpc/public'], + ), + const ChainMetadata( + type: ChainType.eip155, + chainId: 'eip155:43114', + name: 'Avalanche', + logo: '/chain-logos/eip155-43114.png', + color: Colors.red, + rpc: ['https://api.avax.network/ext/bc/C/rpc'], + ), + ]; + + static final List testChains = [ + ChainMetadata( + type: ChainType.eip155, + chainId: 'eip155:11155111', + name: 'Sepolia', + logo: '/chain-logos/eip155-1.png', + color: Colors.blue.shade300, + isTestnet: true, + rpc: ['https://ethereum-sepolia.publicnode.com'], + ), + ChainMetadata( + type: ChainType.eip155, + chainId: 'eip155:80001', + name: 'Polygon Mumbai', + logo: '/chain-logos/eip155-137.png', + color: Colors.purple.shade300, + isTestnet: true, + rpc: ['https://matic-mumbai.chainstacklabs.com'], + ), + ChainMetadata( + type: ChainType.kadena, + chainId: 'kadena:testnet04', + name: 'Kadena', + logo: 'TODO', + color: Colors.purple.shade600, + rpc: [ + 'https://api.testnet.chainweb.com', + ], + ), + ]; + + static final List allChains = [...mainChains, ...testChains]; +} diff --git a/example/wallet/lib/pages/app_detail_page.dart b/example/wallet/lib/pages/app_detail_page.dart index 6453827b..f78393b5 100644 --- a/example/wallet/lib/pages/app_detail_page.dart +++ b/example/wallet/lib/pages/app_detail_page.dart @@ -42,8 +42,7 @@ class AppDetailPageState extends State { List sessionWidgets = []; for (final SessionData session in sessions) { - List namespaceWidget = - ConnectionWidgetBuilder.buildFromNamespaces( + final namespaceWidget = ConnectionWidgetBuilder.buildFromNamespaces( session.topic, session.namespaces, ); diff --git a/example/wallet/lib/pages/apps_page.dart b/example/wallet/lib/pages/apps_page.dart index 8a11db0f..5baacd59 100644 --- a/example/wallet/lib/pages/apps_page.dart +++ b/example/wallet/lib/pages/apps_page.dart @@ -31,6 +31,7 @@ class AppsPageState extends State with GetItStateMixin { _pairings = web3Wallet.pairings.getAll(); web3Wallet.core.pairing.onPairingDelete.subscribe(_onPairingDelete); web3Wallet.core.pairing.onPairingExpire.subscribe(_onPairingDelete); + // TODO web3Wallet.core.echo.register(firebaseAccessToken); DeepLinkHandler.onLink.listen(_deepLinkListener); DeepLinkHandler.checkInitialLink(); } diff --git a/example/wallet/lib/pages/settings_page.dart b/example/wallet/lib/pages/settings_page.dart new file mode 100644 index 00000000..466450e5 --- /dev/null +++ b/example/wallet/lib/pages/settings_page.dart @@ -0,0 +1,235 @@ +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:walletconnect_flutter_v2_wallet/dependencies/bottom_sheet/i_bottom_sheet_service.dart'; +import 'package:walletconnect_flutter_v2_wallet/dependencies/key_service/i_key_service.dart'; +import 'package:walletconnect_flutter_v2_wallet/utils/constants.dart'; +import 'package:walletconnect_flutter_v2_wallet/widgets/custom_button.dart'; +import 'package:walletconnect_flutter_v2_wallet/widgets/recover_from_seed.dart'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({super.key}); + + @override + State createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + @override + Widget build(BuildContext context) { + final keysService = GetIt.I(); + final chainKeys = keysService.getKeysForChain('eip155:1'); + return Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.only(left: 8.0, bottom: 8.0), + child: Text( + 'Account', + style: TextStyle( + color: Colors.black, + fontSize: 16.0, + fontWeight: FontWeight.w500, + ), + ), + ), + Container( + decoration: BoxDecoration( + color: StyleConstants.lightGray, + borderRadius: BorderRadius.circular( + StyleConstants.linear16, + ), + ), + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Text( + 'CAIP-10', + style: TextStyle( + color: Colors.black, + fontSize: 14.0, + fontWeight: FontWeight.bold, + ), + ), + SizedBox( + width: 8.0, + ), + Icon( + Icons.copy, + size: 14.0, + ), + ], + ), + Text( + 'eip155:1:${chainKeys.first.address}', + style: const TextStyle( + color: Colors.black87, + fontSize: 13.0, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + const SizedBox(height: 12.0), + Container( + decoration: BoxDecoration( + color: StyleConstants.lightGray, + borderRadius: BorderRadius.circular( + StyleConstants.linear16, + ), + ), + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Text( + 'Private key', + style: TextStyle( + color: Colors.black, + fontSize: 14.0, + fontWeight: FontWeight.bold, + ), + ), + SizedBox( + width: 8.0, + ), + Icon( + Icons.copy, + size: 14.0, + ), + ], + ), + Text( + chainKeys.first.privateKey, + style: const TextStyle( + color: Colors.black87, + fontSize: 13.0, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + const SizedBox(height: 12.0), + Container( + decoration: BoxDecoration( + color: StyleConstants.lightGray, + borderRadius: BorderRadius.circular( + StyleConstants.linear16, + ), + ), + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Text( + 'Public key', + style: TextStyle( + color: Colors.black, + fontSize: 14.0, + fontWeight: FontWeight.bold, + ), + ), + SizedBox( + width: 8.0, + ), + Icon( + Icons.copy, + size: 14.0, + ), + ], + ), + Text( + chainKeys.first.publicKey, + style: const TextStyle( + color: Colors.black87, + fontSize: 13.0, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + const Expanded(child: SizedBox()), + Row( + children: [ + CustomButton( + onTap: () async { + final mnemonic = + await GetIt.I().queueBottomSheet( + widget: RecoverFromSeed(), + ); + if (mnemonic is String) { + debugPrint(mnemonic); + await keysService.restoreWallet(mnemonic: mnemonic); + setState(() {}); + } + }, + child: const Center( + child: Text( + 'Import account', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + // const SizedBox(height: 12.0), + // Row( + // children: [ + // CustomButton( + // type: CustomButtonType.normal, + // onTap: () async { + // await keysService.createWallet(); + // setState(() {}); + // }, + // child: const Center( + // child: Text( + // 'Restart account', + // style: TextStyle( + // color: Colors.white, + // fontWeight: FontWeight.bold, + // ), + // ), + // ), + // ), + // ], + // ), + const SizedBox(height: 12.0), + Row( + children: [ + CustomButton( + type: CustomButtonType.invalid, + onTap: () async { + await keysService.createWallet(); + setState(() {}); + }, + child: const Center( + child: Text( + 'Delete and create new', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/example/wallet/lib/utils/constants.dart b/example/wallet/lib/utils/constants.dart index 1c67448c..8326aefe 100644 --- a/example/wallet/lib/utils/constants.dart +++ b/example/wallet/lib/utils/constants.dart @@ -12,7 +12,7 @@ class StyleConstants { static const Color primaryColor = Color(0xFF3396FF); static const Color darkGray = Color(0xFF141414); - static const Color lightGray = Color.fromARGB(255, 196, 196, 196); + static const Color lightGray = Color.fromARGB(255, 227, 227, 227); static const Color clear = Color.fromARGB(0, 0, 0, 0); static const Color layerColor0 = Color(0xFF000000); diff --git a/example/wallet/lib/utils/dart_defines.dart b/example/wallet/lib/utils/dart_defines.dart index e0acc1a1..1f2ecd21 100644 --- a/example/wallet/lib/utils/dart_defines.dart +++ b/example/wallet/lib/utils/dart_defines.dart @@ -2,13 +2,13 @@ class DartDefines { static const String projectId = String.fromEnvironment( 'PROJECT_ID', ); - static const String kadenaPrivateKey = String.fromEnvironment( + static const kadenaPrivateKey = String.fromEnvironment( 'KADENA_PRIVATE_KEY', defaultValue: '7d54a79feeb95ac4efdc6cfd4b702da5ee5dc1c31781b76ea092301c266e2451', ); - static const String kadenaPublicKey = String.fromEnvironment( - 'KADENA_PUBLIC_KEY', + static const kadenaAddress = String.fromEnvironment( + 'KADENA_ADDRESS', defaultValue: 'af242a8d963f184eca742271a5134ee3d3e006f0377d667510e15f6fc18e41d9', ); diff --git a/example/wallet/lib/utils/namespace_model_builder.dart b/example/wallet/lib/utils/namespace_model_builder.dart index 890b3860..b0b3728e 100644 --- a/example/wallet/lib/utils/namespace_model_builder.dart +++ b/example/wallet/lib/utils/namespace_model_builder.dart @@ -57,10 +57,12 @@ class ConnectionWidgetBuilder { elements: ns.accounts, ), ); - models.add(WCConnectionModel( - title: StringConstants.methods, - elements: ns.methods, - )); + models.add( + WCConnectionModel( + title: StringConstants.methods, + elements: ns.methods, + ), + ); Map actions = {}; for (final String event in ns.events) { @@ -78,11 +80,13 @@ class ConnectionWidgetBuilder { ); }; } - models.add(WCConnectionModel( - title: StringConstants.events, - elements: ns.events, - elementActions: actions, - )); + models.add( + WCConnectionModel( + title: StringConstants.events, + elements: ns.events, + elementActions: actions, + ), + ); views.add( WCConnectionWidget( diff --git a/example/wallet/lib/widgets/custom_button.dart b/example/wallet/lib/widgets/custom_button.dart index 7cc19284..d7c9cf94 100644 --- a/example/wallet/lib/widgets/custom_button.dart +++ b/example/wallet/lib/widgets/custom_button.dart @@ -5,17 +5,17 @@ enum CustomButtonType { normal, valid, invalid } class CustomButton extends StatelessWidget { final Widget child; - final CustomButtonType type; + final CustomButtonType? type; final VoidCallback onTap; const CustomButton({ super.key, required this.child, - required this.type, required this.onTap, + this.type, }); - Color _getBackgroundColor(CustomButtonType type) { + Color _getBackgroundColor(CustomButtonType? type) { switch (type) { case CustomButtonType.normal: return Colors.blue; @@ -24,7 +24,7 @@ class CustomButton extends StatelessWidget { case CustomButtonType.invalid: return StyleConstants.errorColor; default: - return Colors.blue; + return Colors.blue[200]!; } } diff --git a/example/wallet/lib/widgets/recover_from_seed.dart b/example/wallet/lib/widgets/recover_from_seed.dart new file mode 100644 index 00000000..0f959e3b --- /dev/null +++ b/example/wallet/lib/widgets/recover_from_seed.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:walletconnect_flutter_v2_wallet/utils/constants.dart'; + +class RecoverFromSeed extends StatelessWidget { + RecoverFromSeed({ + Key? key, + }) : super(key: key); + + final controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + final unfocusedBorder = OutlineInputBorder( + borderSide: BorderSide(color: Colors.grey.shade300, width: 1.0), + borderRadius: BorderRadius.circular(12.0), + ); + final focusedBorder = unfocusedBorder.copyWith( + borderSide: const BorderSide(color: Colors.blue, width: 1.0), + ); + return Container( + color: Colors.white, + height: 282.0, + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 20.0), + child: Column( + children: [ + Text( + 'Insert Seed Phrase', + style: StyleConstants.subtitleText.copyWith( + color: Colors.black, + fontSize: 18.0, + ), + ), + const SizedBox(height: StyleConstants.magic10), + SizedBox( + height: 90.0, + // padding: const EdgeInsets.all(3.0), + child: TextFormField( + controller: controller, + maxLines: 4, + textAlignVertical: TextAlignVertical.center, + cursorColor: Colors.blue, + enableSuggestions: false, + autocorrect: false, + cursorHeight: 16.0, + decoration: InputDecoration( + isDense: true, + hintText: 'your seed phrase here', + hintStyle: const TextStyle(color: Colors.grey), + border: unfocusedBorder, + errorBorder: unfocusedBorder, + enabledBorder: unfocusedBorder, + disabledBorder: unfocusedBorder, + focusedBorder: focusedBorder, + filled: true, + fillColor: Colors.grey[200], + contentPadding: const EdgeInsets.all(8.0), + ), + ), + ), + const SizedBox(height: StyleConstants.magic10), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.of(context).pop(controller.text), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(Colors.blue), + foregroundColor: MaterialStateProperty.all(Colors.white), + ), + child: const Text('Recover'), + ), + ), + const SizedBox(height: StyleConstants.magic10), + SizedBox( + width: double.infinity, + child: TextButton( + onPressed: () => Navigator.of(context).pop(null), + child: const Text( + 'Cancel', + style: TextStyle(color: Colors.grey), + ), + ), + ), + ], + ), + ); + } +} diff --git a/example/wallet/lib/widgets/wc_connection_widget/wc_connection_widget.dart b/example/wallet/lib/widgets/wc_connection_widget/wc_connection_widget.dart index f2ff8845..9d7a81e4 100644 --- a/example/wallet/lib/widgets/wc_connection_widget/wc_connection_widget.dart +++ b/example/wallet/lib/widgets/wc_connection_widget/wc_connection_widget.dart @@ -43,7 +43,7 @@ class WCConnectionWidget extends StatelessWidget { Widget _buildTitle(String text) { return Container( decoration: BoxDecoration( - color: Colors.grey, + color: Colors.black12, borderRadius: BorderRadius.circular( StyleConstants.linear16, ), diff --git a/example/wallet/lib/widgets/wc_connection_widget/wc_connection_widget_info.dart b/example/wallet/lib/widgets/wc_connection_widget/wc_connection_widget_info.dart index 237a2b55..da3060d2 100644 --- a/example/wallet/lib/widgets/wc_connection_widget/wc_connection_widget_info.dart +++ b/example/wallet/lib/widgets/wc_connection_widget/wc_connection_widget_info.dart @@ -15,7 +15,7 @@ class WCConnectionWidgetInfo extends StatelessWidget { return Container( width: double.infinity, decoration: BoxDecoration( - color: Colors.grey, + color: Colors.black12, borderRadius: BorderRadius.circular( StyleConstants.linear16, ), diff --git a/example/wallet/pubspec.yaml b/example/wallet/pubspec.yaml index 28823db2..7a0760a8 100644 --- a/example/wallet/pubspec.yaml +++ b/example/wallet/pubspec.yaml @@ -16,7 +16,6 @@ dependencies: qr_flutter: ^4.0.0 json_annotation: ^4.8.1 fl_toast: ^3.1.0 - mobile_scanner: ^3.0.0 get_it: ^7.2.0 eth_sig_util: ^0.0.9 diff --git a/lib/apis/utils/extensions.dart b/lib/apis/utils/extensions.dart new file mode 100644 index 00000000..2c2858f3 --- /dev/null +++ b/lib/apis/utils/extensions.dart @@ -0,0 +1,163 @@ +import 'dart:typed_data'; + +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; +import 'package:web3dart/crypto.dart' as crypto; +import 'package:http/http.dart' as http; + +extension TransactionExtension on Transaction { + // https://github.com/WalletConnect/WalletConnectFlutterV2/issues/246#issuecomment-1905455072 + Map toJson() { + return { + if (from != null) 'from': from!.hex, + if (to != null) 'to': to!.hex, + if (maxGas != null) 'gas': '0x${maxGas!.toRadixString(16)}', + if (gasPrice != null) + 'gasPrice': '0x${gasPrice!.getInWei.toRadixString(16)}', + if (value != null) 'value': '0x${value!.getInWei.toRadixString(16)}', + if (data != null) 'data': crypto.bytesToHex(data!), + if (nonce != null) 'nonce': nonce, + if (maxFeePerGas != null) + 'maxFeePerGas': '0x${maxFeePerGas!.getInWei.toRadixString(16)}', + if (maxPriorityFeePerGas != null) + 'maxPriorityFeePerGas': + '0x${maxPriorityFeePerGas!.getInWei.toRadixString(16)}', + }; + } +} + +extension ContractsInteractionExtension on Web3App { + Future readContractCall({ + required DeployedContract deployedContract, + required String functionName, + required String rpcUrl, + List parameters = const [], + }) async { + try { + core.logger.i('readContractCall: with function $functionName'); + final result = await Web3Client(rpcUrl, http.Client()).call( + contract: deployedContract, + function: deployedContract.function(functionName), + params: parameters, + ); + + core.logger.i( + 'readContractCall - function: $functionName - result: ${result.first}'); + return result.first; + } catch (e) { + rethrow; + } + } + + Future writeContractCall({ + required String topic, + required String chainId, + required String rpcUrl, + required DeployedContract deployedContract, + required String functionName, + required Transaction transaction, + String? method, + List parameters = const [], + }) async { + try { + final credentials = _CustomCredentialsSender( + topic: topic, + chainId: chainId, + signEngine: signEngine, + address: transaction.from!, + method: method, + ); + final trx = Transaction.callContract( + contract: deployedContract, + function: deployedContract.function(functionName), + from: credentials.address, + parameters: [ + if (transaction.to != null) transaction.to, + if (transaction.value != null) transaction.value!.getInWei, + ...parameters, + ], + ); + + if (chainId.contains(':')) { + chainId = chainId.split(':').last; + } + return await Web3Client(rpcUrl, http.Client()).sendTransaction( + credentials, + trx, + chainId: int.parse(chainId), + ); + } catch (e) { + rethrow; + } + } +} + +class _CustomCredentialsSender extends CustomTransactionSender { + _CustomCredentialsSender({ + required ISignEngine signEngine, + required String topic, + required String chainId, + required EthereumAddress address, + String? method, + }) : _signEngine = signEngine, + _topic = topic, + _chainId = chainId, + _address = address, + _method = method; + + final ISignEngine _signEngine; + final String _topic; + final String _chainId; + final EthereumAddress _address; + final String? _method; + + @override + EthereumAddress get address => _address; + + @override + Future sendTransaction(Transaction transaction) async { + try { + final sessionRequestParams = SessionRequestParams( + method: _method ?? MethodsConstants.ethSendTransaction, + params: [ + transaction.toJson(), + ], + ); + + final result = await _signEngine.request( + topic: _topic, + chainId: _chainId, + request: sessionRequestParams, + ); + return result; + } catch (e) { + rethrow; + } + } + + @override + Future extractAddress() => Future.value(_address); + + @override + Future signToSignature( + Uint8List payload, { + int? chainId, + bool isEIP1559 = false, + }) { + final signature = signToEcSignature( + payload, + chainId: chainId, + isEIP1559: isEIP1559, + ); + return Future.value(signature); + } + + @override + crypto.MsgSignature signToEcSignature( + Uint8List payload, { + int? chainId, + bool isEIP1559 = false, + }) { + // TODO: implement signToEcSignature + throw UnimplementedError(); + } +} diff --git a/lib/walletconnect_flutter_v2.dart b/lib/walletconnect_flutter_v2.dart index 7b1f133b..0c4d6767 100644 --- a/lib/walletconnect_flutter_v2.dart +++ b/lib/walletconnect_flutter_v2.dart @@ -19,6 +19,7 @@ export 'apis/models/uri_parse_result.dart'; export 'apis/utils/method_constants.dart'; export 'apis/utils/namespace_utils.dart'; export 'apis/utils/log_level.dart'; +export 'apis/utils/extensions.dart'; // Sign API export 'apis/sign_api/i_sign_client.dart'; @@ -57,4 +58,3 @@ export 'package:logger/logger.dart'; export 'package:shared_preferences/shared_preferences.dart'; export 'package:universal_io/io.dart'; export 'package:web3dart/web3dart.dart'; -export 'package:web3dart/crypto.dart';