diff --git a/CHANGELOG.md b/CHANGELOG.md index 68e9240..57161c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 0.1.4 + +* add surpport for web3_signers v0.1+ +* add create safe account with passkeys method in accounts factory +* modify getSafeSignature to support hybrid signatures (private key + passkey) +* update default singleton to safeL2 +* refactor safe inititializer +* fix issue that causes create2salt to result in a different safe address +* fetch the smart account balance from the entrypoint +* fix issue where empty amounts array throws a light account ArrayLengthMismatch() error during batch transactions + ## 0.1.3 * replace Simple account with Alchemy light account diff --git a/example/android/build.gradle b/example/android/build.gradle index e83fb5d..d56ded6 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '2.0.20' repositories { google() mavenCentral() diff --git a/example/devtools_options.yaml b/example/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/example/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/example/lib/providers/wallet_provider.dart b/example/lib/providers/wallet_provider.dart index fe2358f..cdc65f1 100644 --- a/example/lib/providers/wallet_provider.dart +++ b/example/lib/providers/wallet_provider.dart @@ -1,13 +1,10 @@ -import 'dart:convert'; import 'dart:developer'; -import 'dart:io'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:web3_signers/web3_signers.dart'; import 'package:variance_dart/variance_dart.dart'; import 'package:web3dart/credentials.dart'; import 'package:web3dart/web3dart.dart' as w3d; -import 'package:web3dart/crypto.dart' as w3d; class WalletProvider extends ChangeNotifier { final Chain _chain; @@ -23,67 +20,56 @@ class WalletProvider extends ChangeNotifier { final EthereumAddress erc20 = EthereumAddress.fromHex("0xAEaF19097D8a8da728438D6B57edd9Bc5DAc4795"); final EthereumAddress deployer = - EthereumAddress.fromHex("0x218F6Bbc32Ef28F547A67c70AbCF8c2ea3b468BA"); + EthereumAddress.fromHex("0xf5bb7f874d8e3f41821175c0aa9910d30d10e193"); + + final salt = Uint256.zero; + static const rpc = + "https://api.pimlico.io/v2/84532/rpc?apikey=pim_NuuL4a9tBdyfoogF5LtP5A"; WalletProvider() - : _chain = Chain( - chainId: 31337, - explorer: "https://sepolia.etherscan.io/", - entrypoint: EntryPointAddress( - 0.6, - EthereumAddress.fromHex( - "0x5165c9e79213e2208947589c6e1dcc80ee8d3d00"))) - ..accountFactory = EthereumAddress.fromHex( - "0x0ce83Bf5d20c539E77e1E607B8349E26c6b20133") // v07 p256 factory address - ..jsonRpcUrl = "http://127.0.0.1:8545" - ..bundlerUrl = "http://localhost:3000/rpc"; - // ..paymasterUrl = - // "https://api.pimlico.io/v2/11155111/rpc?apikey=875f3458-a37c-4187-8ac5-d08bbfa0d501"; - - // "0x402A266e92993EbF04a5B3fd6F0e2b21bFC83070" v06 p256 factory address + : _chain = Chains.getChain(Network.baseTestnet) + ..accountFactory = Constants.lightAccountFactoryAddressv07 + ..bundlerUrl = rpc + ..paymasterUrl = rpc; + Future registerWithPassKey(String name, {bool? requiresUserVerification}) async { - final pkpSigner = - PassKeySigner("webauthn.io", "webauthn", "https://webauthn.io"); - final hwdSigner = HardwareSigner.withTag(name); + _chain.accountFactory = Constants.safeProxyFactoryAddress; - final salt = Uint256.zero; - Uint256.fromHex( - hexlify(w3d.keccak256(Uint8List.fromList(utf8.encode(name))))); + final options = PassKeysOptions( + name: "variance", + namespace: "variance.space", + origin: "https://variance.space", + userVerification: "required", + requireResidentKey: true, + sharedWebauthnSigner: EthereumAddress.fromHex( + "0xfD90FAd33ee8b58f32c00aceEad1358e4AFC23f9")); + final pkpSigner = PassKeySigner(options: options); try { - // uses passkeys on android, secure enclave on iOS - if (Platform.isAndroid) { - final SmartWalletFactory walletFactory = - SmartWalletFactory(_chain, pkpSigner); - final keypair = await pkpSigner.register(name, name); - _wallet = - await walletFactory.createP256Account(keypair, salt); - } else if (Platform.isIOS) { - final SmartWalletFactory walletFactory = - SmartWalletFactory(_chain, hwdSigner); - final keypair = await hwdSigner.generateKeyPair(); - _wallet = await walletFactory.createP256Account( - keypair, salt); - } + final SmartWalletFactory walletFactory = + SmartWalletFactory(_chain, pkpSigner); + final keypair = await pkpSigner.register( + "${DateTime.timestamp().millisecondsSinceEpoch}@variance.space", + name); + _wallet = await walletFactory.createSafeAccountWithPasskey( + keypair, salt, options.sharedWebauthnSigner); log("wallet created ${_wallet?.address.hex} "); } catch (e) { _errorMessage = e.toString(); notifyListeners(); log("something happened: $e"); + rethrow; } } Future createEOAWallet() async { - _chain.accountFactory = Constants.lightAccountFactoryAddressv06; - - final signer = EOAWallet.createWallet(); + final signer = EOAWallet.createWallet( + WordLength.word_12, const SignatureOptions(prefix: [0])); log("signer: ${signer.getAddress()}"); final SmartWalletFactory walletFactory = SmartWalletFactory(_chain, signer); - final salt = Uint256.fromHex(hexlify(w3d - .keccak256(EthereumAddress.fromHex(signer.getAddress()).addressBytes))); try { _wallet = await walletFactory.createAlchemyLightAccount(salt); @@ -99,17 +85,13 @@ class WalletProvider extends ChangeNotifier { final random = math.Random.secure(); final privateKey = EthPrivateKey.createRandom(random); - final signer = PrivateKeySigner.create(privateKey, "123456", random); + final signer = PrivateKeySigner.create(privateKey, "123456", random, + options: const SignatureOptions(prefix: [0])); log("signer: ${signer.getAddress()}"); log("pk: ${hexlify(privateKey.privateKey)}"); final SmartWalletFactory walletFactory = SmartWalletFactory(_chain, signer); - final salt = Uint256.fromHex(hexlify(w3d - .keccak256(EthereumAddress.fromHex(signer.getAddress()).addressBytes))); - - log("pk salt: ${salt.toHex()}"); - try { _wallet = await walletFactory.createAlchemyLightAccount(salt); log("pk wallet created ${_wallet?.address.hex} "); @@ -128,11 +110,6 @@ class WalletProvider extends ChangeNotifier { final SmartWalletFactory walletFactory = SmartWalletFactory(_chain, signer); - final salt = Uint256.fromHex(hexlify(w3d - .keccak256(EthereumAddress.fromHex(signer.getAddress()).addressBytes))); - - log("salt: ${salt.toHex()}"); - try { _wallet = await walletFactory.createSafeAccount(salt); log("safe created ${_wallet?.address.hex} "); @@ -180,16 +157,11 @@ class WalletProvider extends ChangeNotifier { Future sendTransaction(String recipient, String amount) async { if (_wallet != null) { - final response = await transferToken( - EthereumAddress.fromHex(recipient), - w3d.EtherAmount.fromBigInt( - w3d.EtherUnit.wei, BigInt.from(20 * math.pow(10, 6)))); - - // final etherAmount = w3d.EtherAmount.fromBigInt(w3d.EtherUnit.wei, - // BigInt.from(double.parse(amount) * math.pow(10, 18))); + final etherAmount = w3d.EtherAmount.fromBigInt(w3d.EtherUnit.wei, + BigInt.from(double.parse(amount) * math.pow(10, 18))); - // final response = - // await _wallet?.send(EthereumAddress.fromHex(recipient), etherAmount); + final response = + await _wallet?.send(EthereumAddress.fromHex(recipient), etherAmount); final receipt = await response?.wait(); log("Transaction receipt Hash: ${receipt?.userOpHash}"); @@ -197,13 +169,4 @@ class WalletProvider extends ChangeNotifier { log("No wallet available to send transaction"); } } - - Future transferToken( - EthereumAddress recipient, w3d.EtherAmount amount) async { - final erc20 = - EthereumAddress.fromHex("0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9"); - - return await _wallet?.sendTransaction( - erc20, Contract.encodeERC20TransferCall(erc20, recipient, amount)); - } } diff --git a/example/lib/screens/create_account.dart b/example/lib/screens/create_account.dart index 05517fc..195e524 100644 --- a/example/lib/screens/create_account.dart +++ b/example/lib/screens/create_account.dart @@ -134,8 +134,48 @@ class _CreateAccountScreenState extends State { } }, icon: const Icon(Icons.key), - label: const Text('Create Safe Smart Account')), - ) + label: const Text( + 'Create Alchemy Light Account with private key')), + ), + 18.verticalSpace, + Container( + margin: const EdgeInsets.only(left: 135), + child: Text('OR', style: TextStyle(fontSize: 18.sp))), + 24.verticalSpace, + Container( + margin: const EdgeInsets.only(left: 30), + child: TextButton.icon( + onPressed: () { + try { + context.read().createEOAWallet(); + Navigator.pushNamed(context, '/home'); + } catch (e) { + 'Something went wrong: $e'; + } + }, + icon: const Icon(Icons.key), + label: const Text( + 'Create Alchemy Light Account with seed phrase')), + ), + 18.verticalSpace, + Container( + margin: const EdgeInsets.only(left: 135), + child: Text('OR', style: TextStyle(fontSize: 18.sp))), + 24.verticalSpace, + Container( + margin: const EdgeInsets.only(left: 30), + child: TextButton.icon( + onPressed: () { + try { + context.read().createSafeWallet(); + Navigator.pushNamed(context, '/home'); + } catch (e) { + 'Something went wrong: $e'; + } + }, + icon: const Icon(Icons.key), + label: const Text('Create default Safe Account')), + ), ], ), ); diff --git a/example/lib/screens/home/home_screen.dart b/example/lib/screens/home/home_screen.dart index e8b32c9..1fe28a3 100644 --- a/example/lib/screens/home/home_screen.dart +++ b/example/lib/screens/home/home_screen.dart @@ -1,8 +1,6 @@ import 'dart:developer'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provider/provider.dart'; import 'package:variancedemo/providers/wallet_provider.dart'; @@ -167,9 +165,6 @@ class NFT extends StatelessWidget { @override Widget build(BuildContext context) { - final wallet = context.select( - (WalletProvider provider) => provider.wallet, - ); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ diff --git a/example/pubspec.lock b/example/pubspec.lock index 664766e..196b83b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -300,18 +300,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -340,18 +340,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" nested: dependency: transitive description: @@ -641,10 +641,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" typed_data: dependency: transitive description: @@ -683,7 +683,7 @@ packages: path: ".." relative: true source: path - version: "0.1.3" + version: "0.1.4" vector_math: dependency: transitive description: @@ -696,10 +696,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" wallet: dependency: transitive description: @@ -720,10 +720,10 @@ packages: dependency: "direct main" description: name: web3_signers - sha256: "455acf0207ef6edc5937a73edd57ce94f028b41856c07193be23e282d542d625" + sha256: cb07808a4add7119f07c5c696db242a78df0c6c45a414ff8f1333b747b811b21 url: "https://pub.dev" source: hosted - version: "0.0.14-alpha-01" + version: "0.1.6" web3dart: dependency: "direct main" description: @@ -757,5 +757,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.3" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 4a202f8..cb3206c 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -17,13 +17,13 @@ dependencies: google_fonts: 6.1.0 flutter_screenutil: ^5.9.0 qr_flutter: ^4.1.0 - web3dart: ^2.7.2 + web3dart: ^2.7.3 shared_preferences: ^2.2.2 path_provider: ^2.1.1 fluttertoast: ^8.2.4 variance_dart: path: ../ - web3_signers: ^0.0.14-alpha-01 + web3_signers: ^0.1.6 dev_dependencies: flutter_test: diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart deleted file mode 100644 index 339fefa..0000000 --- a/example/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:variancedemo/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/lib/src/4337/chains.dart b/lib/src/4337/chains.dart index d26cebe..bb95f38 100644 --- a/lib/src/4337/chains.dart +++ b/lib/src/4337/chains.dart @@ -26,6 +26,9 @@ class Chain { /// is not known or needed. String? paymasterUrl; + /// Specify whether it is testnet + bool testnet; + /// Creates a new instance of the [Chain] class. /// /// [chainId] is the unique identifier of the chain. @@ -59,7 +62,8 @@ class Chain { this.accountFactory, this.jsonRpcUrl, this.bundlerUrl, - this.paymasterUrl}); + this.paymasterUrl, + this.testnet = false}); } //predefined Chains you can use @@ -70,71 +74,61 @@ class Chains { explorer: "https://etherscan.io/", jsonRpcUrl: "https://rpc.ankr.com/eth", entrypoint: EntryPointAddress.v07, - accountFactory: Constants.safeProxyFactoryAddress, ), Network.polygon: Chain( chainId: 137, explorer: "https://polygonscan.com/", jsonRpcUrl: "https://rpc.ankr.com/polygon", entrypoint: EntryPointAddress.v07, - accountFactory: Constants.safeProxyFactoryAddress, ), Network.optimism: Chain( chainId: 10, explorer: "https://explorer.optimism.io", jsonRpcUrl: "https://rpc.ankr.com/optimism", entrypoint: EntryPointAddress.v07, - accountFactory: Constants.safeProxyFactoryAddress, ), Network.base: Chain( chainId: 8453, explorer: "https://basescan.org", jsonRpcUrl: "https://rpc.ankr.com/base", entrypoint: EntryPointAddress.v07, - accountFactory: Constants.safeProxyFactoryAddress, ), Network.arbitrum: Chain( chainId: 42161, explorer: "https://arbiscan.io/", jsonRpcUrl: "https://rpc.ankr.com/arbitrum", entrypoint: EntryPointAddress.v07, - accountFactory: Constants.safeProxyFactoryAddress, ), Network.linea: Chain( chainId: 59144, explorer: "https://lineascan.build/", jsonRpcUrl: "https://rpc.linea.build", entrypoint: EntryPointAddress.v07, - accountFactory: Constants.safeProxyFactoryAddress, ), Network.scroll: Chain( chainId: 534352, explorer: "https://scrollscan.com/", jsonRpcUrl: "https://rpc.ankr.com/scroll", entrypoint: EntryPointAddress.v07, - accountFactory: Constants.safeProxyFactoryAddress, ), Network.fuse: Chain( chainId: 122, explorer: "https://explorer.fuse.io", jsonRpcUrl: "https://rpc.fuse.io", entrypoint: EntryPointAddress.v07, - accountFactory: Constants.safeProxyFactoryAddress, ), Network.sepolia: Chain( - chainId: 11155111, - explorer: "https://sepolia.etherscan.io/", - jsonRpcUrl: "https://rpc.ankr.com/eth_sepolia", - entrypoint: EntryPointAddress.v07, - accountFactory: Constants.safeProxyFactoryAddress, - ), + chainId: 11155111, + explorer: "https://sepolia.etherscan.io/", + jsonRpcUrl: "https://rpc.ankr.com/eth_sepolia", + entrypoint: EntryPointAddress.v07, + testnet: true), Network.baseTestnet: Chain( - chainId: 84532, - explorer: "https://sepolia.basescan.org/", - jsonRpcUrl: "https://rpc.ankr.com/base_sepolia", - entrypoint: EntryPointAddress.v07, - accountFactory: Constants.safeProxyFactoryAddress, - ) + chainId: 84532, + explorer: "https://sepolia.basescan.org/", + jsonRpcUrl: "https://rpc.ankr.com/base_sepolia", + entrypoint: EntryPointAddress.v07, + testnet: true) }; const Chains._(); diff --git a/lib/src/4337/factory.dart b/lib/src/4337/factory.dart index 0acac55..2a7c135 100644 --- a/lib/src/4337/factory.dart +++ b/lib/src/4337/factory.dart @@ -24,12 +24,6 @@ class SmartWalletFactory implements SmartWalletFactoryBase { _contract = Contract(_jsonRpc.rpc); } - /// A getter for the P256AccountFactory contract instance. - _P256AccountFactory get _p256Accountfactory => _P256AccountFactory( - address: _chain.accountFactory!, - chainId: _chain.chainId, - rpc: _jsonRpc.rpc); - /// A getter for the SafePlugin instance. _SafePlugin get _safePlugin => _SafePlugin( address: @@ -50,48 +44,41 @@ class SmartWalletFactory implements SmartWalletFactoryBase { rpc: _jsonRpc.rpc); @override - Future createP256Account(T keyPair, Uint256 salt, - [EthereumAddress? recoveryAddress]) { - switch (keyPair.runtimeType) { - case const (PassKeyPair): - return _createPasskeyAccount( - keyPair as PassKeyPair, salt, recoveryAddress); - case const (P256Credential): - return _createSecureEnclaveAccount( - keyPair as P256Credential, salt, recoveryAddress); - default: - throw ArgumentError.value(keyPair, 'keyPair', - 'createP256Account: An instance of `PassKeyPair` or `P256Credential` is expected, but got: ${keyPair.runtimeType}'); + Future createSafeAccountWithPasskey(PassKeyPair keyPair, + Uint256 salt, EthereumAddress safeWebauthnSharedSigner, + [EthereumAddress? p256Verifier]) { + final module = Safe4337ModuleAddress.fromVersion(_chain.entrypoint.version); + final verifier = p256Verifier ?? Constants.p256VerifierAddress; + + encodeWebAuthnConfigure() { + return Contract.encodeFunctionCall("configure", safeWebauthnSharedSigner, + ContractAbis.get("enableWebauthn"), [ + [ + keyPair.authData.publicKey.item1.value, + keyPair.authData.publicKey.item2.value, + hexToInt(verifier.hexNo0x.padLeft(44, '0')), + ] + ]); } + + encodeWebauthnSetup(Uint8List Function() encodeModuleSetup) { + return _safePlugin.getSafeMultisendCallData( + [module.setup, safeWebauthnSharedSigner], + null, + [encodeModuleSetup(), encodeWebAuthnConfigure()], + [intToBytes(BigInt.one), intToBytes(BigInt.one)]); + } + + return _createSafeAccount( + salt, safeWebauthnSharedSigner, module, encodeWebauthnSetup); } @override - Future createSafeAccount(Uint256 salt) async { + Future createSafeAccount(Uint256 salt) { final signer = EthereumAddress.fromHex(_signer.getAddress()); + final module = Safe4337ModuleAddress.fromVersion(_chain.entrypoint.version); - // Get the initializer data for the Safe account - final initializer = _safeProxyFactory.getInitializer([signer], 1, - Safe4337ModuleAddress.fromVersion(_chain.entrypoint.version)); - - // Get the proxy creation code for the Safe account - final creation = await _safeProxyFactory.proxyCreationCode(); - - // Predict the address of the Safe account - final address = - _safeProxyFactory.getPredictedSafe(initializer, salt, creation); - - // Encode the call data for the `createProxyWithNonce` function - // This function is used to create the Safe account with the given initializer data and salt - final initCallData = _safeProxyFactory.self - .function("createProxyWithNonce") - .encodeCall([Constants.safeSingletonAddress, initializer, salt.value]); - - // Generate the initialization code by combining the account factory address and the encoded call data - final initCode = _getInitCode(initCallData); - - // Create the SmartWallet instance for the Safe account - return _createAccount(_chain, address, initCode) - ..addPlugin<_SafePlugin>('safe', _safePlugin); + return _createSafeAccount(salt, signer, module); } @override @@ -114,8 +101,7 @@ class SmartWalletFactory implements SmartWalletFactoryBase { final initCode = _getInitCode(initCalldata); // Create the SmartWallet instance for the light account - return _createAccount( - _chain, address, initCode, Constants.defaultBytePrefix); + return _createAccount(_chain, address, initCode); } @override @@ -126,6 +112,38 @@ class SmartWalletFactory implements SmartWalletFactoryBase { return _createAccount(_chain, address, initCode); } + Future _createSafeAccount( + Uint256 salt, EthereumAddress signer, Safe4337ModuleAddress module, + [Uint8List Function(Uint8List Function())? setup]) async { + final singleton = _chain.chainId == 1 + ? Constants.safeSingletonAddress + : Constants.safeL2SingletonAddress; + + // Get the initializer data for the Safe account + final initializer = + _safeProxyFactory.getInitializer([signer], 1, module, setup); + + // Get the proxy creation code for the Safe account + final creation = await _safeProxyFactory.proxyCreationCode(); + + // Predict the address of the Safe account + final address = _safeProxyFactory.getPredictedSafe( + initializer, salt, creation, singleton); + + // Encode the call data for the `createProxyWithNonce` function + // This function is used to create the Safe account with the given initializer data and salt + final initCallData = _safeProxyFactory.self + .function("createProxyWithNonce") + .encodeCall([singleton, initializer, salt.value]); + + // Generate the initialization code by combining the account factory address and the encoded call data + final initCode = _getInitCode(initCallData); + + // Create the SmartWallet instance for the Safe account + return _createAccount(_chain, address, initCode) + ..addPlugin<_SafePlugin>('safe', _safePlugin); + } + /// Creates a new [SmartWallet] instance with the provided chain, address, and initialization code. /// /// [chain] is the Ethereum chain configuration. @@ -140,9 +158,11 @@ class SmartWalletFactory implements SmartWalletFactoryBase { /// /// Returns a [SmartWallet] instance representing the created account. SmartWallet _createAccount( - Chain chain, EthereumAddress address, Uint8List initCalldata, - [Uint8List? prefix]) { - final wallet = SmartWallet(chain, address, initCalldata, prefix) + Chain chain, + EthereumAddress address, + Uint8List initCalldata, + ) { + final wallet = SmartWallet(chain, address, initCalldata) ..addPlugin('signer', _signer) ..addPlugin('bundler', _bundler) ..addPlugin('jsonRpc', _jsonRpc) @@ -155,88 +175,6 @@ class SmartWalletFactory implements SmartWalletFactoryBase { return wallet; } - /// Creates a new passkey account with the provided [PassKeyPair], salt, and optional recovery address. - /// - /// [pkp] is the [PassKeyPair] instance used to create the account. - /// [salt] is the salt value used in the account creation process. - /// [recoveryAddress] is an optional recovery address for the account. - /// - /// Returns a [Future] that resolves to a [SmartWallet] instance representing - /// the created passkey account. - /// - /// The process involves: - /// 1. Encoding the account creation data with the provided [PassKeyPair], [salt], and [recoveryAddress]. - /// - The encoded data includes the recovery address, credential hex, and public key components. - /// 2. Calling the `createP256Account` function on the `_p256AccountFactory` contract with the encoded data and [salt]. - /// - This function initiates the account creation process on the Ethereum blockchain. - /// 3. Getting the initialization code by combining the account factory address and the encoded call data. - /// - The initialization code is required to create the account on the client-side. - /// 4. Predicting the account address using the `_p256AccountFactory` contract's `getP256AccountAddress` function. - /// - This function predicts the address of the account based on the creation data and salt. - /// 5. Creating a new [SmartWallet] instance with the predicted address and initialization code. - Future _createPasskeyAccount(PassKeyPair pkp, Uint256 salt, - [EthereumAddress? recoveryAddress]) async { - final Uint8List creation = abi.encode([ - 'address', - 'bytes32', - 'uint256', - 'uint256' - ], [ - recoveryAddress ?? Constants.zeroAddress, - hexToBytes(pkp.authData.hexCredential), - pkp.authData.publicKey.item1.value, - pkp.authData.publicKey.item2.value, - ]); - - final initCalldata = _p256Accountfactory.self - .function('createP256Account') - .encodeCall([salt.value, creation]); - final initCode = _getInitCode(initCalldata); - final address = await _p256Accountfactory - .getP256AccountAddress((salt: salt.value, creation: creation)); - return _createAccount(_chain, address, initCode); - } - - /// Creates a new secure enclave account with the provided [P256Credential], salt, and optional recovery address. - /// - /// [p256] is the [P256Credential] instance used to create the account. - /// [salt] is the salt value used in the account creation process. - /// [recoveryAddress] is an optional recovery address for the account. - /// - /// Returns a [Future] that resolves to a [SmartWallet] instance representing - /// the created secure enclave account. - /// - /// The process is similar to [_createPasskeyAccount] but with a different encoding for the account creation data. - /// 1. Encoding the account creation data with the provided [P256Credential], [salt], and [recoveryAddress]. - /// - The encoded data includes the recovery address, an empty bytes32 value, and the public key components. - /// 2. Calling the `createP256Account` function on the `_p256AccountFactory` contract with the encoded data and [salt]. - /// 3. Getting the initialization code by combining the account factory address and the encoded call data. - /// 4. Predicting the account address using the `_p256AccountFactory` contract's `getP256AccountAddress` function. - /// 5. Creating a new [SmartWallet] instance with the predicted address and initialization code. - Future _createSecureEnclaveAccount( - P256Credential p256, Uint256 salt, - [EthereumAddress? recoveryAddress]) async { - final Uint8List creation = abi.encode([ - 'address', - 'bytes32', - 'uint256', - 'uint256' - ], [ - recoveryAddress ?? Constants.zeroAddress, - Uint8List(32), - p256.publicKey.item1.value, - p256.publicKey.item2.value, - ]); - - final initCalldata = _p256Accountfactory.self - .function('createP256Account') - .encodeCall([salt.value, creation]); - final initCode = _getInitCode(initCalldata); - final address = await _p256Accountfactory - .getP256AccountAddress((salt: salt.value, creation: creation)); - return _createAccount(_chain, address, initCode); - } - /// Returns the initialization code for the account by concatenating the account factory address with the provided initialization call data. /// /// [initCalldata] is the initialization call data for the account. diff --git a/lib/src/4337/safe.dart b/lib/src/4337/safe.dart index f0aa190..8e055d4 100644 --- a/lib/src/4337/safe.dart +++ b/lib/src/4337/safe.dart @@ -19,30 +19,27 @@ class _SafePlugin extends Safe4337Module implements Safe4337ModuleBase { /// [signature] is the signature of the user operation. /// [blockInfo] is the current blockInformation including the timestamp and baseFee. /// + /// > Note: The validity period is set to 1 hour. + /// > Hence varinace_sdk does not generate long-time spaning signatures. + /// /// Returns a HexString representing the encoded signature with a validity period. - String getSafeSignature(T signature, BlockInformation blockInfo) { - late Uint8List signatureBytes; - - if (signature is Uint8List) { - signatureBytes = signature; - } else if (signature is String && signature.startsWith('0x')) { - signatureBytes = hexToBytes(signature); - } else { - throw ArgumentError('Unsupported signature type'); - } + String getSafeSignature(String signature, BlockInformation blockInfo) { final timestamp = blockInfo.timestamp.millisecondsSinceEpoch ~/ 1000; - String validAfter = (timestamp - 3600).toRadixString(16).padLeft(12, '0'); String validUntil = (timestamp + 3600).toRadixString(16).padLeft(12, '0'); - int v = signatureBytes[64]; - if (v >= 27 && v <= 30) { - v += 4; - } + if (signature.length == 132) { + int v = int.parse(signature.substring(130, 132), radix: 16); + if (v >= 27 && v <= 30) { + v += 4; + } - String modifiedV = v.toRadixString(16).padLeft(2, '0'); - String hexSignature = hexlify(signatureBytes.sublist(0, 64)); - return '0x$validAfter$validUntil$hexSignature$modifiedV'; + String modifiedV = v.toRadixString(16).padLeft(2, '0'); + return '0x$validAfter$validUntil${signature.substring(2, 130)}$modifiedV'; + } else { + // passkey signatures - does not contain v + return '0x$validAfter$validUntil${signature.substring(2)}'; + } } /// Computes the hash of a Safe UserOperation. @@ -86,11 +83,12 @@ class _SafePlugin extends Safe4337Module implements Safe4337ModuleBase { } Uint8List getSafeMultisendCallData(List recipients, - List? amounts, List? innerCalls) { + List? amounts, List? innerCalls, + [List? operations]) { Uint8List packedCallData = Uint8List(0); for (int i = 0; i < recipients.length; i++) { - Uint8List operation = Uint8List.fromList([0]); + Uint8List operation = operations?[i] ?? intToBytes(BigInt.zero); Uint8List to = recipients[i].addressBytes; Uint8List value = amounts != null ? padTo32Bytes(amounts[i].getInWei) @@ -100,9 +98,12 @@ class _SafePlugin extends Safe4337Module implements Safe4337ModuleBase { : padTo32Bytes(BigInt.zero); Uint8List data = innerCalls != null ? innerCalls[i] : Uint8List.fromList([]); - Uint8List encodedCall = Uint8List.fromList( - [...operation, ...to, ...value, ...dataLength, ...data]); - packedCallData = Uint8List.fromList([...packedCallData, ...encodedCall]); + packedCallData = packedCallData + .concat(operation) + .concat(to) + .concat(value) + .concat(dataLength) + .concat(data); } return Contract.encodeFunctionCall( @@ -113,9 +114,6 @@ class _SafePlugin extends Safe4337Module implements Safe4337ModuleBase { } Uint8List padTo32Bytes(BigInt number) { - final bytes = intToBytes(number); - return bytes.length < 32 - ? Uint8List.fromList([...Uint8List(32 - bytes.length), ...bytes]) - : bytes; + return intToBytes(number).padLeftTo32Bytes(); } } diff --git a/lib/src/4337/wallet.dart b/lib/src/4337/wallet.dart index bca90e4..5360114 100644 --- a/lib/src/4337/wallet.dart +++ b/lib/src/4337/wallet.dart @@ -34,25 +34,18 @@ class SmartWallet with _PluginManager, _GasSettings implements SmartWalletBase { /// The initialization code for deploying the Smart Wallet contract. Uint8List _initCode; - /// Defines the signer type for Alchemy Light Accounts. - final Uint8List _prefix; - /// Creates a new instance of the [SmartWallet] class. /// /// [_chain] is an object representing the blockchain chain configuration. /// [_walletAddress] is the address of the Smart Wallet. /// [_initCode] is the initialization code for deploying the Smart Wallet contract. - /// [prefix] is the signature prefix for signing light account transactions. - SmartWallet(this._chain, this._walletAddress, this._initCode, - [Uint8List? signaturePrefix]) - : _prefix = signaturePrefix ?? Uint8List(0); + SmartWallet(this._chain, this._walletAddress, this._initCode); @override EthereumAddress get address => _walletAddress; @override - Future get balance => - plugin("contract").getBalance(_walletAddress); + Future get balance => _getBalance(); @override Future get isDeployed => @@ -71,8 +64,9 @@ class SmartWallet with _PluginManager, _GasSettings implements SmartWalletBase { String? get toHex => _walletAddress.hexEip55; @override - String get dummySignature => - plugin('signer').getDummySignature(prefix: hexlify(_prefix)); + String get dummySignature => plugin('signer').getDummySignature(); + + bool get isSafe => hasPlugin("safe"); /// Returns the estimated gas required for deploying the Smart Wallet contract. /// @@ -103,7 +97,7 @@ class SmartWallet with _PluginManager, _GasSettings implements SmartWalletBase { EthereumAddress recipient, EtherAmount amount) => sendUserOperation(buildUserOperation( callData: Contract.execute(_walletAddress, - to: recipient, amount: amount, isSafe: hasPlugin("safe")))); + to: recipient, amount: amount, isSafe: isSafe))); @override Future sendTransaction( @@ -114,13 +108,12 @@ class SmartWallet with _PluginManager, _GasSettings implements SmartWalletBase { to: to, amount: amount, innerCallData: encodedFunctionData, - isSafe: hasPlugin("safe")))); + isSafe: isSafe))); @override Future sendBatchedTransaction( List recipients, List calls, {List? amounts}) { - final isSafe = hasPlugin("safe"); if (isSafe) { final innerCall = plugin<_SafePlugin>('safe') .getSafeMultisendCallData(recipients, amounts, calls); @@ -174,24 +167,30 @@ class SmartWallet with _PluginManager, _GasSettings implements SmartWalletBase { @override Future signUserOperation(UserOperation op, {int? index}) async { - final isSafe = hasPlugin('safe'); final blockInfo = await plugin('jsonRpc').getBlockInformation(); - // Calculate the operation hash - final opHash = isSafe - ? await plugin<_SafePlugin>('safe').getSafeOperationHash(op, blockInfo) - : op.hash(_chain); - - // Sign the operation hash using the 'signer' plugin - final signature = - await plugin('signer').personalSign(opHash, index: index); + calculateOperationHash(UserOperation op, BlockInformation blockInfo) async { + if (isSafe) { + return plugin<_SafePlugin>('safe').getSafeOperationHash(op, blockInfo); + } else { + return op.hash(_chain); + } + } - // Append the signature validity period if the 'safe' plugin is enabled - op.signature = isSafe - ? plugin<_SafePlugin>('safe').getSafeSignature(signature, blockInfo) - : hexlify(_prefix + signature); + signOperationHash(Uint8List opHash, int? index) async { + final signature = + await plugin('signer').personalSign(opHash, index: index); + final signatureHex = hexlify(signature); + if (isSafe) { + return plugin<_SafePlugin>('safe') + .getSafeSignature(signatureHex, blockInfo); + } + return signatureHex; + } + final opHash = await calculateOperationHash(op, blockInfo); + op.signature = await signOperationHash(opHash, index); return op; } @@ -210,6 +209,16 @@ class SmartWallet with _PluginManager, _GasSettings implements SmartWalletBase { .then((value) => Uint256(value[0])) .catchError((e) => throw NonceError(e.toString(), _walletAddress))); + /// Returns the balance for the Smart Wallet address from the entrypoint. + /// + /// If an error occurs during the balance retrieval process, a [FetchBalanceError] exception is thrown. + Future _getBalance() => plugin("contract") + .read(_chain.entrypoint.address, ContractAbis.get('getBalance'), + "balanceOf", + params: [_walletAddress]) + .then((value) => EtherAmount.fromBigInt(EtherUnit.wei, value[0])) + .catchError((e) => throw FetchBalanceError(e.toString(), _walletAddress)); + /// Updates the user operation with the latest nonce and gas prices. /// /// [op] is the user operation to update. diff --git a/lib/src/abis/abis.dart b/lib/src/abis/abis.dart index e916e00..f16540f 100644 --- a/lib/src/abis/abis.dart +++ b/lib/src/abis/abis.dart @@ -1,5 +1,4 @@ export 'lightAccountFactory.g.dart'; -export 'p256AccountFactory.g.dart'; export 'safeProxyFactory.g.dart'; export 'safe4337Module.g.dart'; export 'contract_abis.dart'; diff --git a/lib/src/abis/contract_abis.dart b/lib/src/abis/contract_abis.dart index 05f429d..ae1e889 100644 --- a/lib/src/abis/contract_abis.dart +++ b/lib/src/abis/contract_abis.dart @@ -60,7 +60,11 @@ class ContractAbis { break; case 'getNonce': abi = - '[{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint192","name":"key","type":"uint192"}],"name":"getNonce","outputs":[{"internalType":"uint256","name":"nonce","type":"uint256"}],"stateMutability":"view","type":"function" }]'; + '[{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint192","name":"key","type":"uint192"}],"name":"getNonce","outputs":[{"internalType":"uint256","name":"nonce","type":"uint256"}],"stateMutability":"view","type":"function"}]'; + break; + case 'getBalance': + abi = + '[{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"}]'; break; case 'execute': abi = @@ -78,6 +82,10 @@ class ContractAbis { abi = '[{"type":"function","name":"enableModules","inputs":[{"name":"modules","type":"address[]","internalType":"address[]"}],"outputs":[],"stateMutability":"nonpayable"}]'; break; + case 'enableWebauthn': + abi = + '[{"inputs":[{"components":[{"internalType":"uint256","name":"x","type":"uint256"},{"internalType":"uint256","name":"y","type":"uint256"},{"internalType":"P256.Verifiers","name":"verifiers","type":"uint176"}],"internalType":"struct SafeWebAuthnSharedSigner.Signer","name":"signer","type":"tuple"}],"name":"configure","outputs":[],"stateMutability":"nonpayable","type":"function"}]'; + break; case 'setup': abi = '[{"type":"function","name":"setup","inputs":[{"name":"_owners","type":"address[]","internalType":"address[]"},{"name":"_threshold","type":"uint256","internalType":"uint256"},{"name":"to","type":"address","internalType":"address"},{"name":"data","type":"bytes","internalType":"bytes"},{"name":"fallbackHandler","type":"address","internalType":"address"},{"name":"paymentToken","type":"address","internalType":"address"},{"name":"payment","type":"uint256","internalType":"uint256"},{"name":"paymentReceiver","type":"address","internalType":"address payable"}],"outputs":[],"stateMutability":"nonpayable"}]'; diff --git a/lib/src/abis/p256AccountFactory.abi.json b/lib/src/abis/p256AccountFactory.abi.json deleted file mode 100644 index 17df5bc..0000000 --- a/lib/src/abis/p256AccountFactory.abi.json +++ /dev/null @@ -1,48 +0,0 @@ -[ - { - "type": "constructor", - "inputs": [ - { - "name": "_entryPoint", - "type": "address", - "internalType": "contract IEntryPoint" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "createP256Account", - "inputs": [ - { "name": "salt", "type": "uint256", "internalType": "uint256" }, - { "name": "creation", "type": "bytes", "internalType": "bytes" } - ], - "outputs": [ - { - "name": "ret", - "type": "address", - "internalType": "contract P256Account" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "getP256AccountAddress", - "inputs": [ - { "name": "salt", "type": "uint256", "internalType": "uint256" }, - { "name": "creation", "type": "bytes", "internalType": "bytes" } - ], - "outputs": [{ "name": "", "type": "address", "internalType": "address" }], - "stateMutability": "view" - }, - { - "type": "function", - "name": "p256Account", - "inputs": [], - "outputs": [ - { "name": "", "type": "address", "internalType": "contract P256Account" } - ], - "stateMutability": "view" - } -] diff --git a/lib/src/abis/p256AccountFactory.g.dart b/lib/src/abis/p256AccountFactory.g.dart deleted file mode 100644 index 1befcb7..0000000 --- a/lib/src/abis/p256AccountFactory.g.dart +++ /dev/null @@ -1,86 +0,0 @@ -// @dart=3.0 -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_local_variable, unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:web3dart/web3dart.dart' as _i1; -import 'dart:typed_data' as _i2; - -final _contractAbi = _i1.ContractAbi.fromJson( - '[{"type":"constructor","inputs":[{"name":"_entryPoint","type":"address","internalType":"contract IEntryPoint"}],"stateMutability":"nonpayable"},{"type":"function","name":"createP256Account","inputs":[{"name":"salt","type":"uint256","internalType":"uint256"},{"name":"creation","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"ret","type":"address","internalType":"contract P256Account"}],"stateMutability":"nonpayable"},{"type":"function","name":"getP256AccountAddress","inputs":[{"name":"salt","type":"uint256","internalType":"uint256"},{"name":"creation","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"p256Account","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract P256Account"}],"stateMutability":"view"}]', - 'P256AccountFactory', -); - -class P256AccountFactory extends _i1.GeneratedContract { - P256AccountFactory({ - required _i1.EthereumAddress address, - required _i1.Web3Client client, - int? chainId, - }) : super( - _i1.DeployedContract( - _contractAbi, - address, - ), - client, - chainId, - ); - - /// The optional [transaction] parameter can be used to override parameters - /// like the gas price, nonce and max gas. The `data` and `to` fields will be - /// set by the contract. - Future createP256Account( - ({BigInt salt, _i2.Uint8List creation}) args, { - required _i1.Credentials credentials, - _i1.Transaction? transaction, - }) async { - final function = self.abi.functions[1]; - assert(checkSignature(function, '8bb4387f')); - final params = [ - args.salt, - args.creation, - ]; - return write( - credentials, - transaction, - function, - params, - ); - } - - /// The optional [atBlock] parameter can be used to view historical data. When - /// set, the function will be evaluated in the specified block. By default, the - /// latest on-chain block will be used. - Future<_i1.EthereumAddress> getP256AccountAddress( - ({BigInt salt, _i2.Uint8List creation}) args, { - _i1.BlockNum? atBlock, - }) async { - final function = self.abi.functions[2]; - assert(checkSignature(function, '28ef50f0')); - final params = [ - args.salt, - args.creation, - ]; - final response = await read( - function, - params, - atBlock, - ); - return (response[0] as _i1.EthereumAddress); - } - - /// The optional [atBlock] parameter can be used to view historical data. When - /// set, the function will be evaluated in the specified block. By default, the - /// latest on-chain block will be used. - Future<_i1.EthereumAddress> p256Account({_i1.BlockNum? atBlock}) async { - final function = self.abi.functions[3]; - assert(checkSignature(function, 'e8eb9e23')); - final params = []; - final response = await read( - function, - params, - atBlock, - ); - return (response[0] as _i1.EthereumAddress); - } -} diff --git a/lib/src/common/constants.dart b/lib/src/common/constants.dart index 1b16477..f68f892 100644 --- a/lib/src/common/constants.dart +++ b/lib/src/common/constants.dart @@ -19,13 +19,16 @@ class Constants { EthereumAddress.fromHex("0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226"); static final EthereumAddress safeSingletonAddress = EthereumAddress.fromHex("0x41675C099F32341bf84BFc5382aF534df5C7461a"); + static final EthereumAddress safeL2SingletonAddress = + EthereumAddress.fromHex("0x29fcB43b46531BcA003ddC8FCB67FFE91900C762"); static final EthereumAddress safeModuleSetupAddressv06 = EthereumAddress.fromHex("0x8EcD4ec46D4D2a6B64fE960B3D64e8B94B2234eb"); static final EthereumAddress safeModuleSetupAddressv07 = EthereumAddress.fromHex("0x2dd68b007B46fBe91B9A7c3EDa5A7a1063cB5b47"); static final EthereumAddress safeMultiSendaddress = EthereumAddress.fromHex("0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526"); - static final defaultBytePrefix = Uint8List(1); + static final EthereumAddress p256VerifierAddress = + EthereumAddress.fromHex("0x445a0683e494ea0c5AF3E83c5159fBE47Cf9e765"); Constants._(); } diff --git a/lib/src/common/contract.dart b/lib/src/common/contract.dart index fda0bef..e02b85d 100644 --- a/lib/src/common/contract.dart +++ b/lib/src/common/contract.dart @@ -337,7 +337,8 @@ class Contract { bool isSafe = false}) { List params = [ recipients, - amounts?.map((e) => e.getInWei) ?? [], + amounts?.map((e) => e.getInWei).toList() ?? + recipients.map((e) => BigInt.zero).toList(), innerCalls ?? Uint8List.fromList([]), ]; diff --git a/lib/src/common/factories.dart b/lib/src/common/factories.dart index 84f70dc..ec70cad 100644 --- a/lib/src/common/factories.dart +++ b/lib/src/common/factories.dart @@ -1,22 +1,5 @@ part of '../../variance_dart.dart'; -/// A class that extends [P256AccountFactory] and implements [P256AccountFactoryBase]. -/// It creates an instance of [P256AccountFactory] with a custom [RPCBase] client. -/// Used to create instances of [SmartWallet] for P256 accounts. -class _P256AccountFactory extends P256AccountFactory - implements P256AccountFactoryBase { - /// Creates a new instance of [_P256AccountFactory]. - /// - /// [address] is the address of the account factory. - /// [chainId] is the ID of the blockchain chain. - /// [rpc] is the [RPCBase] client used for communication with the blockchain. - _P256AccountFactory({ - required super.address, - super.chainId, - required RPCBase rpc, - }) : super(client: Web3Client.custom(rpc)); -} - /// A class that extends [SafeProxyFactory] and implements [SafeProxyFactoryBase]. /// It creates an instance of [SafeProxyFactory] with a custom [RPCBase] client. /// Used to create instances of [SmartWallet] for Safe accounts. @@ -49,17 +32,35 @@ class _SafeProxyFactory extends SafeProxyFactory /// /// Returns a [Uint8List] containing the encoded initializer data. Uint8List getInitializer(Iterable owners, int threshold, - Safe4337ModuleAddress module) { - return Contract.encodeFunctionCall( - "setup", Constants.safeSingletonAddress, ContractAbis.get("setup"), [ - owners.toList(), - BigInt.from(threshold), - module.setup, - Contract.encodeFunctionCall( + Safe4337ModuleAddress module, + [Uint8List Function(Uint8List Function())? encodeWebauthnSetup]) { + encodeModuleSetup() { + return Contract.encodeFunctionCall( "enableModules", module.setup, ContractAbis.get("enableModules"), [ [module.address] - ]), - module.address, + ]); + } + + final setup = { + "owners": owners.toList(), + "threshold": BigInt.from(threshold), + "to": module.setup, + "data": encodeModuleSetup(), + "fallbackHandler": module.address, + }; + + if (encodeWebauthnSetup != null) { + setup["to"] = Constants.safeMultiSendaddress; + setup["data"] = encodeWebauthnSetup(encodeModuleSetup); + } + + return Contract.encodeFunctionCall( + "setup", Constants.safeL2SingletonAddress, ContractAbis.get("setup"), [ + setup["owners"], + setup["threshold"], + setup["to"], + setup["data"], + setup["fallbackHandler"], Constants.zeroAddress, BigInt.zero, Constants.zeroAddress, @@ -71,37 +72,27 @@ class _SafeProxyFactory extends SafeProxyFactory /// [initializer] is the initializer data for deploying the Safe contract. /// [salt] is the salt value used for address calculation. /// [creationCode] is the creation code for deploying the Safe contract. + /// [singleton] is the address of the Safe singleton. /// /// Returns the predicted [EthereumAddress] of the Safe contract. - EthereumAddress getPredictedSafe( - Uint8List initializer, Uint256 salt, Uint8List creationCode) { - paddedAddressBytes(Uint8List addressBytes) { - return [...Uint8List(32 - addressBytes.length), ...addressBytes]; - } + EthereumAddress getPredictedSafe(Uint8List initializer, Uint256 salt, + Uint8List creationCode, EthereumAddress singleton) { + final deploymentData = + creationCode.concat(singleton.addressBytes.padLeftTo32Bytes()); - final deploymentData = Uint8List.fromList( - [ - ...creationCode, - ...paddedAddressBytes(Constants.safeSingletonAddress.addressBytes) - ], - ); + // toHex pads to 64 then tobytes ensures its always 32 bytes salt + final create2Salt = + keccak256(keccak256(initializer).concat(hexToBytes(salt.toHex()))); final hash = keccak256( - Uint8List.fromList([ - 0xff, - ...self.address.addressBytes, - ...keccak256(Uint8List.fromList([ - ...keccak256(initializer), - ...intToBytes(salt.value), - ])), - ...keccak256(deploymentData), - ]), + Uint8List.fromList([0xff]) + .concat(self.address.addressBytes) + .concat(create2Salt) + .concat(keccak256(deploymentData)), ); - final predictedAddress = - EthereumAddress(Uint8List.fromList(hash.skip(12).take(20).toList())); - return predictedAddress; + return EthereumAddress(Uint8List.fromList(hash.skip(12).take(20).toList())); } } diff --git a/lib/src/common/pack.dart b/lib/src/common/pack.dart index cec7cab..7cfd17b 100644 --- a/lib/src/common/pack.dart +++ b/lib/src/common/pack.dart @@ -32,7 +32,7 @@ Uint8List packUints(BigInt high128, BigInt low128) { /// final unpacked = unpackUints("0x...32byteshex"); /// print(unpacked); /// ``` -List unpackUints(Hex hex) { +List unpackUints(String hex) { final value = BigInt.parse(hex.substring(2), radix: 16); return [value >> 128, value.toUnsigned(128)]; } diff --git a/lib/src/errors/wallet_errors.dart b/lib/src/errors/wallet_errors.dart index cab5efc..64121f8 100644 --- a/lib/src/errors/wallet_errors.dart +++ b/lib/src/errors/wallet_errors.dart @@ -144,3 +144,19 @@ class SendError extends Error { '''; } } + +class FetchBalanceError extends Error { + final String message; + final EthereumAddress? address; + + FetchBalanceError(this.message, this.address); + + @override + String toString() { + return ''' + Error fetching user account balance for address ${address?.hex}! + -------------------------------------------------- + Failed with error: $message + '''; + } +} diff --git a/lib/src/interfaces/smart_wallet_factory.dart b/lib/src/interfaces/smart_wallet_factory.dart index 49ea6d3..223d420 100644 --- a/lib/src/interfaces/smart_wallet_factory.dart +++ b/lib/src/interfaces/smart_wallet_factory.dart @@ -3,18 +3,17 @@ part of 'interfaces.dart'; abstract class SmartWalletFactoryBase { /// Creates a new P256 account using the provided key pair and salt. /// - /// [keyPair] is the key pair used to create the account. It can be either a - /// [PassKeyPair] or a [P256Credential] instance. + /// [PassKeyPair] is the key pair used to create the account. /// [salt] is the salt value used in the account creation process. - /// [recoveryAddress] is an optional recovery address for the account. + /// [safeWebauthnSharedSigner] is the address of the Safe Webauthn shared signer. /// /// Returns a [Future] that resolves to a [SmartWallet] instance representing /// the created account. /// /// Throws an [ArgumentError] if the provided [keyPair] is not a - /// [PassKeyPair] or [P256Credential] instance. - Future createP256Account(T keyPair, Uint256 salt, - [EthereumAddress? recoveryAddress]); + /// [PassKeyPair] instance. + Future createSafeAccountWithPasskey(PassKeyPair keyPair, + Uint256 salt, EthereumAddress safeWebauthnSharedSigner); /// Creates a new Safe account with the provided salt and optional owners and threshold. /// diff --git a/lib/variance_dart.dart b/lib/variance_dart.dart index ea2bb58..55598df 100644 --- a/lib/variance_dart.dart +++ b/lib/variance_dart.dart @@ -23,7 +23,7 @@ part 'src/4337/providers.dart'; part 'src/4337/safe.dart'; part 'src/4337/userop.dart'; part 'src/4337/wallet.dart'; -part 'src/common//constants.dart'; +part 'src/common/constants.dart'; part 'src/common/contract.dart'; part 'src/common/factories.dart'; part 'src/common/mixins.dart'; diff --git a/pubspec.lock b/pubspec.lock index d0a0948..310ef24 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -210,7 +210,7 @@ packages: source: hosted version: "1.1.0" flutter: - dependency: "direct main" + dependency: transitive description: flutter source: sdk version: "0.0.0" @@ -335,18 +335,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -628,10 +628,10 @@ packages: dependency: "direct main" description: name: web3_signers - sha256: "455acf0207ef6edc5937a73edd57ce94f028b41856c07193be23e282d542d625" + sha256: cb07808a4add7119f07c5c696db242a78df0c6c45a414ff8f1333b747b811b21 url: "https://pub.dev" source: hosted - version: "0.0.14-alpha-01" + version: "0.1.6" web3dart: dependency: "direct main" description: @@ -673,5 +673,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.16.9" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.3" diff --git a/pubspec.yaml b/pubspec.yaml index 443daaf..2883632 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,26 +1,24 @@ name: variance_dart description: An Account Abstraction (4337) Development kit, for quickly building mobile web3 apps and smart wallets. -version: 0.1.3 +version: 0.1.4 documentation: https://docs.variance.space homepage: https://variance.space repository: https://github.com/vaariance/variance-dart issue_tracker: https://github.com/vaariance/variance-dart/issues environment: - sdk: ">=3.0.6 <4.0.0" - flutter: ">=3.13.0" + sdk: ^3.5.0 + flutter: ">=3.24.3" platforms: android: ios: - windows: + web: dependencies: - flutter: - sdk: flutter web3dart: ^2.7.3 http: ^1.2.0 - web3_signers: ^0.0.14-alpha-01 + web3_signers: ^0.1.6 dev_dependencies: web3dart_builders: ^0.1.2