From 4f1dba4a5ac4e049f13a6b5c3d98392e8d290d51 Mon Sep 17 00:00:00 2001 From: maxiggle Date: Sat, 6 Jan 2024 09:13:40 +0100 Subject: [PATCH 1/4] wip: send transaction --- lib/src/4337/chains.dart | 1 + lib/src/4337/userop.dart | 69 ++++++++-------- lib/src/4337/wallet.dart | 66 +++++++++++---- lib/src/abis/encoder.abi.json | 82 +++++++++++++++++++ lib/src/abis/encoder.g.dart | 65 +++++++++++++++ lib/src/utils/models/ens.freezed.dart | 2 +- lib/src/utils/models/metadata.freezed.dart | 2 +- lib/src/utils/models/nft.freezed.dart | 2 +- lib/src/utils/models/price.freezed.dart | 2 +- lib/src/utils/models/token.freezed.dart | 2 +- lib/src/utils/models/transaction.freezed.dart | 2 +- lib/src/utils/models/transfer.freezed.dart | 2 +- lib/variance.dart | 1 + pubspec.yaml | 2 +- 14 files changed, 244 insertions(+), 56 deletions(-) create mode 100644 lib/src/abis/encoder.abi.json create mode 100644 lib/src/abis/encoder.g.dart diff --git a/lib/src/4337/chains.dart b/lib/src/4337/chains.dart index f6054a0..2e6c295 100644 --- a/lib/src/4337/chains.dart +++ b/lib/src/4337/chains.dart @@ -26,6 +26,7 @@ class Chain { } } +//predefined Chains you can use class Chains { static Map chains = { // Ethereum Mainnet diff --git a/lib/src/4337/userop.dart b/lib/src/4337/userop.dart index 9498a9d..9abd719 100644 --- a/lib/src/4337/userop.dart +++ b/lib/src/4337/userop.dart @@ -13,6 +13,8 @@ class UserOperation implements UserOperationBase { @override final String callData; + final Uint8List? callDataBytes; + @override final BigInt callGasLimit; @@ -37,9 +39,8 @@ class UserOperation implements UserOperationBase { final dummySig = "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; - Uint8List _hash; - UserOperation({ + this.callDataBytes, required this.sender, required this.nonce, required this.initCode, @@ -51,29 +52,7 @@ class UserOperation implements UserOperationBase { required this.maxPriorityFeePerGas, required this.signature, required this.paymasterAndData, - }) : _hash = keccak256(abi.encode([ - 'address', - 'uint256', - 'bytes32', - 'bytes32', - 'uint256', - 'uint256', - 'uint256', - 'uint256', - 'uint256', - 'bytes32', - ], [ - sender, - nonce, - keccak256(hexToBytes(initCode)), - keccak256(hexToBytes(callData)), - callGasLimit, - verificationGasLimit, - preVerificationGas, - maxFeePerGas, - maxPriorityFeePerGas, - keccak256(hexToBytes(paymasterAndData)), - ])); + }); factory UserOperation.fromJson(String source) => UserOperation.fromMap(json.decode(source) as Map); @@ -95,6 +74,7 @@ class UserOperation implements UserOperationBase { } factory UserOperation.partial({ + Uint8List? calldataBytes, required String callData, EthereumAddress? sender, BigInt? nonce, @@ -106,15 +86,16 @@ class UserOperation implements UserOperationBase { BigInt? maxPriorityFeePerGas, }) => UserOperation( + callDataBytes: calldataBytes, sender: sender ?? Constants.zeroAddress, nonce: nonce ?? BigInt.zero, initCode: initCode ?? "0x", callData: callData, - callGasLimit: callGasLimit ?? BigInt.from(35000), - verificationGasLimit: verificationGasLimit ?? BigInt.from(70000), + callGasLimit: callGasLimit ?? BigInt.from(21000000), + verificationGasLimit: verificationGasLimit ?? BigInt.from(21000000), preVerificationGas: preVerificationGas ?? BigInt.from(21000), - maxFeePerGas: maxFeePerGas ?? BigInt.zero, - maxPriorityFeePerGas: maxPriorityFeePerGas ?? BigInt.zero, + maxFeePerGas: maxFeePerGas ?? BigInt.one, + maxPriorityFeePerGas: maxPriorityFeePerGas ?? BigInt.one, signature: "0x", paymasterAndData: '0x', ); @@ -140,9 +121,33 @@ class UserOperation implements UserOperationBase { } @override - Uint8List hash(Chain chain) => keccak256(abi.encode( - ['bytes32', 'address', 'uint256'], - [keccak256(_hash), chain.entrypoint, chain.chainId])); + Uint8List hash(Chain chain) { + final encoded = keccak256(abi.encode([ + 'address', + 'uint256', + 'bytes32', + 'bytes32', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'uint256', + 'bytes32', + ], [ + sender, + nonce, + keccak256(hexToBytes(initCode)), + keccak256(hexToBytes(callData)), + callGasLimit, + verificationGasLimit, + preVerificationGas, + maxFeePerGas, + maxPriorityFeePerGas, + keccak256(hexToBytes(paymasterAndData)), + ])); + return keccak256(abi.encode(['bytes32', 'address', 'uint256'], + [encoded, chain.entrypoint, chain.chainId])); + } @override String toJson() => json.encode(toMap()); diff --git a/lib/src/4337/wallet.dart b/lib/src/4337/wallet.dart index 54d946e..dffff52 100644 --- a/lib/src/4337/wallet.dart +++ b/lib/src/4337/wallet.dart @@ -5,6 +5,7 @@ class SmartWallet with _PluginManager implements SmartWalletBase { EthereumAddress? _walletAddress; String? _initCode; + Uint8List? _initCodeBytes; /// [Entrypoint] is not initialized /// to initialize with entrypoint, you have to call [SmartWallet.init] instead @@ -102,8 +103,8 @@ class SmartWallet with _PluginManager implements SmartWalletBase { BigInt? maxFeePerGas, BigInt? maxPriorityFeePerGas, }) { - dev.log("building useroperation: $callData"); return UserOperation.partial( + calldataBytes: callData, callData: hexlify(callData), sender: _walletAddress, nonce: customNonce, @@ -129,14 +130,16 @@ class SmartWallet with _PluginManager implements SmartWalletBase { .then((value) => {_walletAddress = value}); } +//create account signed with passkey @override Future createSimplePasskeyAccount(PassKeyPair pkp, Uint256 salt) async { - _initCode = hexlify(_getInitCode('createPasskeyAccount', [ + _initCodeBytes = _getInitCode('createPasskeyAccount', [ pkp.credentialHexBytes, pkp.publicKey[0].value, pkp.publicKey[1].value, salt.value - ])); + ]); + _initCode = hexlify(_initCodeBytes!); getSimplePassKeyAccountAddress(pkp, salt) .then((addr) => {_walletAddress = addr}); } @@ -206,9 +209,26 @@ class SmartWallet with _PluginManager implements SmartWalletBase { @override Future signUserOperation(UserOperation userOp, {bool update = true, String? id, int? index}) async { - if (update) await _updateUserOperation(userOp); + if (update) userOp = await _updateUserOperation(userOp); dev.log("updated useroperation: ${userOp.toMap()}"); - final opHash = userOp.hash(_chain); + final opHash = await Encoder( + address: EthereumAddress.fromHex( + "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"), + client: Web3Client.custom(plugin('ethRpc'))) + .encodeUserOpEntriesToHash( + userOp.sender, + userOp.nonce, + _initCodeBytes!, + userOp.callDataBytes!, + userOp.callGasLimit, + userOp.verificationGasLimit, + userOp.preVerificationGas, + userOp.maxFeePerGas, + userOp.maxPriorityFeePerGas, + Uint8List.fromList([]), + BigInt.from(_chain.chainId)); + + dev.log("$opHash"); Uint8List signature = await plugin('signer') .personalSign(opHash, index: index, @@ -222,6 +242,7 @@ class SmartWallet with _PluginManager implements SmartWalletBase { Uint8List _getInitCode(String functionName, List params) { final factory = plugin<_AccountFactory>('factory'); final data = factory.self.function(functionName).encodeCall(params); + dev.log(bytesToHex(data)); final initCode = abi.encode(['address', 'bytes'], [factory.self.address, data]); return initCode; @@ -243,22 +264,35 @@ class SmartWallet with _PluginManager implements SmartWalletBase { .catchError((e) => Uint256.zero); } - Future _updateUserOperation(UserOperation op) async { - List reponses = await Future.wait( + Future _updateUserOperation(UserOperation op) async { + List responses = await Future.wait( [plugin('ethRpc').getGasPrice(), nonce, deployed]); - dev.log("responses from update: $reponses"); + dev.log("responses from update: $responses"); + + String extractInitCode(String code) { + // making sure the first 20 bytes is the factory address. + String extractedString = code.substring(0, 66); + String modifiedString = + extractedString.replaceRange(2, extractedString.length - 40, ""); + return modifiedString + code.substring(66); + } + op = UserOperation.update(op.toMap(), sender: _walletAddress, - nonce: reponses[1].value, - initCode: !(reponses[2]) ? _initCode! : null); - op.maxFeePerGas = reponses[0]["maxFeePerGas"] as BigInt; - op.maxPriorityFeePerGas = reponses[0]["maxPriorityFeePerGas"] as BigInt; + nonce: responses[1].value, + initCode: !(responses[2]) ? extractInitCode(_initCode!) : null); + op.maxFeePerGas = + (responses[0] as Map)["maxFeePerGas"]!.getInWei; + op.maxPriorityFeePerGas = + (responses[0] as Map)["maxPriorityFeePerGas"]! + .getInWei; op.signature = op.dummySig; dev.log("mock user op = ${op.toMap()}"); - UserOperationGas opGas = await plugin('bundler') - .estimateUserOperationGas(op.toMap(), _chain.entrypoint); - dev.log("opGas = ${opGas.toString()}"); - op = UserOperation.update(op.toMap(), opGas: opGas); + // UserOperationGas opGas = await plugin('bundler') + // .estimateUserOperationGas(op.toMap(), _chain.entrypoint); + // dev.log("opGas = ${opGas.toString()}"); + // op = UserOperation.update(op.toMap(), opGas: opGas); + return op; } Future _validateUserOperation(UserOperation op) async { diff --git a/lib/src/abis/encoder.abi.json b/lib/src/abis/encoder.abi.json new file mode 100644 index 0000000..ed36b1c --- /dev/null +++ b/lib/src/abis/encoder.abi.json @@ -0,0 +1,82 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "entrypoint", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "initCode", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "callGasLimit", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "verificationGasLimit", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "preVerificationGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxFeePerGas", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxPriorityFeePerGas", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "paymasterAndData", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "chainid", + "type": "uint256" + } + ], + "name": "encodeUserOpEntriesToHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + } + ] \ No newline at end of file diff --git a/lib/src/abis/encoder.g.dart b/lib/src/abis/encoder.g.dart new file mode 100644 index 0000000..8a91119 --- /dev/null +++ b/lib/src/abis/encoder.g.dart @@ -0,0 +1,65 @@ +// Generated code, do not modify. Run `build_runner build` to re-generate! +// @dart=2.12 +// 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( + '[{"inputs":[{"internalType":"address","name":"entrypoint","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"initCode","type":"bytes"},{"internalType":"bytes","name":"callData","type":"bytes"},{"internalType":"uint256","name":"callGasLimit","type":"uint256"},{"internalType":"uint256","name":"verificationGasLimit","type":"uint256"},{"internalType":"uint256","name":"preVerificationGas","type":"uint256"},{"internalType":"uint256","name":"maxFeePerGas","type":"uint256"},{"internalType":"uint256","name":"maxPriorityFeePerGas","type":"uint256"},{"internalType":"bytes","name":"paymasterAndData","type":"bytes"},{"internalType":"uint256","name":"chainid","type":"uint256"}],"name":"encodeUserOpEntriesToHash","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"}]', + 'Encoder', +); + +class Encoder extends _i1.GeneratedContract { + Encoder({ + required _i1.EthereumAddress address, + required _i1.Web3Client client, + int? chainId, + }) : super( + _i1.DeployedContract( + _contractAbi, + address, + ), + client, + chainId, + ); + + /// 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<_i2.Uint8List> encodeUserOpEntriesToHash( + _i1.EthereumAddress sender, + BigInt nonce, + _i2.Uint8List initCode, + _i2.Uint8List callData, + BigInt callGasLimit, + BigInt verificationGasLimit, + BigInt preVerificationGas, + BigInt maxFeePerGas, + BigInt maxPriorityFeePerGas, + _i2.Uint8List paymasterAndData, + BigInt chainid, { + _i1.BlockNum? atBlock, + }) async { + final function = self.abi.functions[1]; + assert(checkSignature(function, '281ac952')); + final params = [ + sender, + nonce, + initCode, + callData, + callGasLimit, + verificationGasLimit, + preVerificationGas, + maxFeePerGas, + maxPriorityFeePerGas, + paymasterAndData, + chainid, + ]; + final response = await read( + function, + params, + atBlock, + ); + return (response[0] as _i2.Uint8List); + } +} diff --git a/lib/src/utils/models/ens.freezed.dart b/lib/src/utils/models/ens.freezed.dart index 52e9b77..b061334 100644 --- a/lib/src/utils/models/ens.freezed.dart +++ b/lib/src/utils/models/ens.freezed.dart @@ -243,7 +243,7 @@ class _$ENSImpl implements _ENS { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$ENSImpl && diff --git a/lib/src/utils/models/metadata.freezed.dart b/lib/src/utils/models/metadata.freezed.dart index a40c06c..160e5c3 100644 --- a/lib/src/utils/models/metadata.freezed.dart +++ b/lib/src/utils/models/metadata.freezed.dart @@ -254,7 +254,7 @@ class _$TokenMetadataImpl implements _TokenMetadata { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$TokenMetadataImpl && diff --git a/lib/src/utils/models/nft.freezed.dart b/lib/src/utils/models/nft.freezed.dart index 9fc24a4..4163272 100644 --- a/lib/src/utils/models/nft.freezed.dart +++ b/lib/src/utils/models/nft.freezed.dart @@ -403,7 +403,7 @@ class _$NFTImpl implements _NFT { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$NFTImpl && diff --git a/lib/src/utils/models/price.freezed.dart b/lib/src/utils/models/price.freezed.dart index e498f51..a0a10d8 100644 --- a/lib/src/utils/models/price.freezed.dart +++ b/lib/src/utils/models/price.freezed.dart @@ -164,7 +164,7 @@ class _$TokenPriceImpl implements _TokenPrice { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$TokenPriceImpl && diff --git a/lib/src/utils/models/token.freezed.dart b/lib/src/utils/models/token.freezed.dart index 3df9e4c..f3b0f4e 100644 --- a/lib/src/utils/models/token.freezed.dart +++ b/lib/src/utils/models/token.freezed.dart @@ -268,7 +268,7 @@ class _$TokenImpl implements _Token { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$TokenImpl && diff --git a/lib/src/utils/models/transaction.freezed.dart b/lib/src/utils/models/transaction.freezed.dart index f72fd74..44702a7 100644 --- a/lib/src/utils/models/transaction.freezed.dart +++ b/lib/src/utils/models/transaction.freezed.dart @@ -522,7 +522,7 @@ class _$TransactionImpl implements _Transaction { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$TransactionImpl && diff --git a/lib/src/utils/models/transfer.freezed.dart b/lib/src/utils/models/transfer.freezed.dart index cfe74b9..841b010 100644 --- a/lib/src/utils/models/transfer.freezed.dart +++ b/lib/src/utils/models/transfer.freezed.dart @@ -312,7 +312,7 @@ class _$TokenTransferImpl implements _TokenTransfer { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$TokenTransferImpl && diff --git a/lib/variance.dart b/lib/variance.dart index fc9505c..1adae1d 100644 --- a/lib/variance.dart +++ b/lib/variance.dart @@ -20,6 +20,7 @@ import 'package:webauthn/webauthn.dart'; import 'interfaces.dart'; import 'src/abis/abis.dart'; +import 'src/abis/encoder.g.dart'; import 'src/common/common.dart'; import 'utils.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index fc6966a..c4202d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,5 +48,5 @@ dev_dependencies: topics: - wallets - ethereum - - 4337 + - "4337" - account-abstraction From ded6a5cf4cad897ad8dc58d25317438f2da0cb61 Mon Sep 17 00:00:00 2001 From: Peter Anyaogu Date: Sun, 7 Jan 2024 00:11:05 +0100 Subject: [PATCH 2/4] feat: v0.0.4 --- CHANGELOG.md | 4 +- lib/interfaces.dart | 8 +- lib/src/4337/userop.dart | 92 ++++--- lib/src/4337/wallet.dart | 234 ++++++++++-------- lib/src/abis/contract_abis.dart | 2 +- lib/src/abis/encoder.abi.json | 82 ------ lib/src/abis/encoder.g.dart | 65 ----- lib/src/common/contract.dart | 8 +- .../interfaces/multi_signer_interface.dart | 5 + lib/src/interfaces/smart_wallet.dart | 5 +- lib/src/interfaces/user_operations.dart | 6 +- lib/src/signers/hd_wallet_signer.dart | 4 + lib/src/signers/passkey_signer.dart | 4 + lib/src/signers/private_key_signer.dart | 12 +- lib/variance.dart | 3 +- 15 files changed, 209 insertions(+), 325 deletions(-) delete mode 100644 lib/src/abis/encoder.abi.json delete mode 100644 lib/src/abis/encoder.g.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 30d6c70..394ebdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ * Biometric Auth layer over the secure storage repository -* Deprecate the ChainbaseAPI helpers for ENS resolution to use the new ensdart_v3 +* Retrieve account nonce from Entrypoint instead of account contract using zeroth key. + +* fixed userOp hashing and added compulsory dummySignature field to account signer interface. * Bug Fixes diff --git a/lib/interfaces.dart b/lib/interfaces.dart index 237205e..2c18694 100644 --- a/lib/interfaces.dart +++ b/lib/interfaces.dart @@ -19,15 +19,15 @@ import 'src/abis/abis.dart' show Entrypoint; import 'variance.dart' show Chain, + PassKeyPair, + PassKeySignature, + PassKeysOptions, Uint256, UserOperation, UserOperationByHash, UserOperationGas, UserOperationReceipt, - UserOperationResponse, - PassKeySignature, - PassKeysOptions, - PassKeyPair; + UserOperationResponse; part 'src/interfaces/account_factory.dart'; part 'src/interfaces/bundler_provider.dart'; diff --git a/lib/src/4337/userop.dart b/lib/src/4337/userop.dart index 9abd719..1dc0693 100644 --- a/lib/src/4337/userop.dart +++ b/lib/src/4337/userop.dart @@ -8,12 +8,10 @@ class UserOperation implements UserOperationBase { final BigInt nonce; @override - final String initCode; + final Uint8List initCode; @override - final String callData; - - final Uint8List? callDataBytes; + final Uint8List callData; @override final BigInt callGasLimit; @@ -34,13 +32,9 @@ class UserOperation implements UserOperationBase { String signature; @override - String paymasterAndData; - - final dummySig = - "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; + Uint8List paymasterAndData; UserOperation({ - this.callDataBytes, required this.sender, required this.nonce, required this.initCode, @@ -55,30 +49,29 @@ class UserOperation implements UserOperationBase { }); factory UserOperation.fromJson(String source) => - UserOperation.fromMap(json.decode(source) as Map); + UserOperation.fromMap(json.decode(source) as Map); - factory UserOperation.fromMap(Map map) { + factory UserOperation.fromMap(Map map) { return UserOperation( - sender: EthereumAddress.fromHex(map['sender']), - nonce: BigInt.parse(map['nonce']), - initCode: map['initCode'], - callData: map['callData'], - callGasLimit: BigInt.parse(map['callGasLimit']), - verificationGasLimit: BigInt.parse(map['verificationGasLimit']), - preVerificationGas: BigInt.parse(map['preVerificationGas']), - maxFeePerGas: BigInt.parse(map['maxFeePerGas']), - maxPriorityFeePerGas: BigInt.parse(map['maxPriorityFeePerGas']), - signature: map['signature'], - paymasterAndData: map['paymasterAndData'], + sender: EthereumAddress.fromHex(map['sender'] as String), + nonce: BigInt.parse(map['nonce'] as String), + initCode: hexToBytes(map['initCode'] as String), + callData: hexToBytes(map['callData'] as String), + callGasLimit: BigInt.parse(map['callGasLimit'] as String), + verificationGasLimit: BigInt.parse(map['verificationGasLimit'] as String), + preVerificationGas: BigInt.parse(map['preVerificationGas'] as String), + maxFeePerGas: BigInt.parse(map['maxFeePerGas'] as String), + maxPriorityFeePerGas: BigInt.parse(map['maxPriorityFeePerGas'] as String), + signature: map['signature'] as String, + paymasterAndData: hexToBytes(map['paymasterAndData'] as String), ); } factory UserOperation.partial({ - Uint8List? calldataBytes, - required String callData, + required Uint8List callData, EthereumAddress? sender, BigInt? nonce, - String? initCode, + Uint8List? initCode, BigInt? callGasLimit, BigInt? verificationGasLimit, BigInt? preVerificationGas, @@ -86,32 +79,33 @@ class UserOperation implements UserOperationBase { BigInt? maxPriorityFeePerGas, }) => UserOperation( - callDataBytes: calldataBytes, sender: sender ?? Constants.zeroAddress, nonce: nonce ?? BigInt.zero, - initCode: initCode ?? "0x", + initCode: initCode ?? Uint8List(0), callData: callData, - callGasLimit: callGasLimit ?? BigInt.from(21000000), - verificationGasLimit: verificationGasLimit ?? BigInt.from(21000000), + callGasLimit: callGasLimit ?? BigInt.from(10000000), + verificationGasLimit: verificationGasLimit ?? BigInt.from(10000000), preVerificationGas: preVerificationGas ?? BigInt.from(21000), maxFeePerGas: maxFeePerGas ?? BigInt.one, maxPriorityFeePerGas: maxPriorityFeePerGas ?? BigInt.one, signature: "0x", - paymasterAndData: '0x', + paymasterAndData: Uint8List(0), ); factory UserOperation.update( - Map map, { + Map map, { UserOperationGas? opGas, EthereumAddress? sender, BigInt? nonce, String? initCode, }) { - map['callGasLimit'] = opGas?.callGasLimit ?? map['callGasLimit']; - map['verificationGasLimit'] = - opGas?.verificationGasLimit ?? map['verificationGasLimit']; - map['preVerificationGas'] = - opGas?.preVerificationGas ?? map['preVerificationGas']; + if (opGas != null) { + map['callGasLimit'] = '0x${opGas.callGasLimit.toRadixString(16)}'; + map['verificationGasLimit'] = + '0x${opGas.verificationGasLimit.toRadixString(16)}'; + map['preVerificationGas'] = + '0x${(opGas.preVerificationGas + BigInt.from(35000)).toRadixString(16)}'; + } if (sender != null) map['sender'] = sender.hex; if (nonce != null) map['nonce'] = '0x${nonce.toRadixString(16)}'; @@ -136,38 +130,40 @@ class UserOperation implements UserOperationBase { ], [ sender, nonce, - keccak256(hexToBytes(initCode)), - keccak256(hexToBytes(callData)), + keccak256(initCode), + keccak256(callData), callGasLimit, verificationGasLimit, preVerificationGas, maxFeePerGas, maxPriorityFeePerGas, - keccak256(hexToBytes(paymasterAndData)), + keccak256(paymasterAndData), ])); return keccak256(abi.encode(['bytes32', 'address', 'uint256'], - [encoded, chain.entrypoint, chain.chainId])); + [encoded, chain.entrypoint, BigInt.from(chain.chainId)])); } @override - String toJson() => json.encode(toMap()); - - @override - Map toMap() { - return { + Map toMap() { + return { 'sender': sender.hexEip55, 'nonce': '0x${nonce.toRadixString(16)}', - 'initCode': initCode, - 'callData': callData, + 'initCode': hexlify(initCode), + 'callData': hexlify(callData), 'callGasLimit': '0x${callGasLimit.toRadixString(16)}', 'verificationGasLimit': '0x${verificationGasLimit.toRadixString(16)}', 'preVerificationGas': '0x${preVerificationGas.toRadixString(16)}', 'maxFeePerGas': '0x${maxFeePerGas.toRadixString(16)}', 'maxPriorityFeePerGas': '0x${maxPriorityFeePerGas.toRadixString(16)}', 'signature': signature, - 'paymasterAndData': paymasterAndData, + 'paymasterAndData': hexlify(paymasterAndData), }; } + + @override + String toJson() { + return jsonEncode(toMap()); + } } class UserOperationByHash { diff --git a/lib/src/4337/wallet.dart b/lib/src/4337/wallet.dart index dffff52..e6e4171 100644 --- a/lib/src/4337/wallet.dart +++ b/lib/src/4337/wallet.dart @@ -4,8 +4,10 @@ class SmartWallet with _PluginManager implements SmartWalletBase { final Chain _chain; EthereumAddress? _walletAddress; - String? _initCode; - Uint8List? _initCodeBytes; + + Uint8List? _initCalldata; + + bool? _deployed; /// [Entrypoint] is not initialized /// to initialize with entrypoint, you have to call [SmartWallet.init] instead @@ -43,14 +45,14 @@ class SmartWallet with _PluginManager implements SmartWalletBase { /// - [bundler]: The bundler provider. /// - [jsonRpcProvider]: The Ethereum JSON RPC provider (optional). /// - [address]: The Ethereum address (optional). - /// - [initCode]: The init code (optional). + /// - [initCallData]: The callData of the factory create method `WITHOUT the factory address`. (optional). factory SmartWallet.init( {required Chain chain, required MultiSignerInterface signer, required BundlerProviderBase bundler, RPCProviderBase? jsonRpcProvider, EthereumAddress? address, - String? initCode}) { + Uint8List? initCallData}) { final instance = SmartWallet( chain: chain, signer: signer, @@ -59,7 +61,7 @@ class SmartWallet with _PluginManager implements SmartWalletBase { address: address); instance - ..dangerouslySetInitCode(initCode) + ..dangerouslySetInitCallData(initCallData) ..plugin('bundler') .initializeWithEntrypoint(Entrypoint( address: chain.entrypoint, @@ -81,18 +83,36 @@ class SmartWallet with _PluginManager implements SmartWalletBase { await plugin("contract").deployed(_walletAddress); @override - String? get initCode => _initCode; + String get initCode => _initCode; @override - Future get initCodeGas async => - await _getInitCodeGas().then((value) => value); + Future get initCodeGas => _initCodeGas; @override Future get nonce async => await _getNonce(); + @override + @Deprecated( + "pass the wallet address alongside the constructor if known beforehand") + set setWalletAddress(EthereumAddress address) => _walletAddress = address; + @override String? get toHex => _walletAddress?.hexEip55; + String get _initCode => _initCalldata != null + ? _chain.accountFactory.hexEip55 + hexlify(_initCalldata!).substring(2) + : "0x"; + + Uint8List get _initCodeBytes { + if (_initCalldata == null) return Uint8List(0); + List extended = _chain.accountFactory.addressBytes.toList(); + extended.addAll(_initCalldata!); + return Uint8List.fromList(extended); + } + + Future get _initCodeGas => plugin('ethRpc') + .estimateGas(_chain.entrypoint, _initCode); + @override UserOperation buildUserOperation({ required Uint8List callData, @@ -104,8 +124,8 @@ class SmartWallet with _PluginManager implements SmartWalletBase { BigInt? maxPriorityFeePerGas, }) { return UserOperation.partial( - calldataBytes: callData, - callData: hexlify(callData), + callData: callData, + initCode: _initCodeBytes, sender: _walletAddress, nonce: customNonce, callGasLimit: callGasLimit, @@ -117,74 +137,74 @@ class SmartWallet with _PluginManager implements SmartWalletBase { } @override - void dangerouslySetInitCode(String? code) { - _initCode = code; - } - - @override - Future createSimpleAccount(Uint256 salt, {int? index}) async { + Future createSimpleAccount(Uint256 salt, {int? index}) async { EthereumAddress signer = EthereumAddress.fromHex( plugin('signer').getAddress()); - _initCode = hexlify(_getInitCode('createAccount', [signer, salt.value])); - getSimpleAccountAddress(signer, salt) + _initCalldata = _getInitCallData('createAccount', [signer, salt.value]); + await getSimpleAccountAddress(signer, salt) .then((value) => {_walletAddress = value}); + return this; } -//create account signed with passkey + //create account signed with passkey @override - Future createSimplePasskeyAccount(PassKeyPair pkp, Uint256 salt) async { - _initCodeBytes = _getInitCode('createPasskeyAccount', [ + Future createSimplePasskeyAccount( + PassKeyPair pkp, Uint256 salt) async { + _initCalldata = _getInitCallData('createPasskeyAccount', [ pkp.credentialHexBytes, pkp.publicKey[0].value, pkp.publicKey[1].value, salt.value ]); - _initCode = hexlify(_initCodeBytes!); - getSimplePassKeyAccountAddress(pkp, salt) + + await getSimplePassKeyAccountAddress(pkp, salt) .then((addr) => {_walletAddress = addr}); + return this; + } + + @override + void dangerouslySetInitCallData(Uint8List? code) { + _initCalldata = code; } @override Future getSimpleAccountAddress( - EthereumAddress signer, Uint256 salt) async { - return await plugin<_AccountFactory>('factory') - .getAddress(signer, salt.value); + EthereumAddress signer, Uint256 salt) { + try { + return plugin<_AccountFactory>('factory').getAddress(signer, salt.value); + } catch (e) { + throw SmartWalletError("Simple acount creation error: $e"); + } } @override Future getSimplePassKeyAccountAddress( - PassKeyPair pkp, Uint256 salt) async { - return await plugin<_AccountFactory>('factory').getPasskeyAccountAddress( - pkp.credentialHexBytes, - pkp.publicKey[0].value, - pkp.publicKey[1].value, - salt.value); + PassKeyPair pkp, Uint256 salt) { + try { + return plugin<_AccountFactory>('factory').getPasskeyAccountAddress( + pkp.credentialHexBytes, + pkp.publicKey[0].value, + pkp.publicKey[1].value, + salt.value); + } catch (e) { + throw SmartWalletError("Passkey acount creation error: $e"); + } } @override Future send( EthereumAddress recipient, EtherAmount amount) async { - require(_walletAddress != null, 'Wallet not deployed'); + _validateAddressAndThrow(); return sendUserOperation(buildUserOperation( callData: Contract.execute(_walletAddress!, to: recipient, amount: amount))); } - @override - Future sendTransaction( - EthereumAddress to, Uint8List encodedFunctionData, - {EtherAmount? amount}) async { - require(_walletAddress != null, 'Wallet not deployed'); - return sendUserOperation(buildUserOperation( - callData: Contract.execute(_walletAddress!, - to: to, amount: amount, innerCallData: encodedFunctionData))); - } - @override Future sendBatchedTransaction( List recipients, List calls, {List? amounts}) async { - require(_walletAddress != null, 'Wallet not deployed'); + _validateAddressAndThrow(); return sendUserOperation(buildUserOperation( callData: Contract.executeBatch( walletAddress: _walletAddress!, @@ -200,6 +220,16 @@ class SmartWallet with _PluginManager implements SmartWalletBase { .sendUserOperation(op.toMap(), _chain.entrypoint); } + @override + Future sendTransaction( + EthereumAddress to, Uint8List encodedFunctionData, + {EtherAmount? amount}) async { + _validateAddressAndThrow(); + return sendUserOperation(buildUserOperation( + callData: Contract.execute(_walletAddress!, + to: to, amount: amount, innerCallData: encodedFunctionData))); + } + @override Future sendUserOperation(UserOperation op, {String? id}) async { @@ -210,103 +240,87 @@ class SmartWallet with _PluginManager implements SmartWalletBase { Future signUserOperation(UserOperation userOp, {bool update = true, String? id, int? index}) async { if (update) userOp = await _updateUserOperation(userOp); - dev.log("updated useroperation: ${userOp.toMap()}"); - final opHash = await Encoder( - address: EthereumAddress.fromHex( - "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"), - client: Web3Client.custom(plugin('ethRpc'))) - .encodeUserOpEntriesToHash( - userOp.sender, - userOp.nonce, - _initCodeBytes!, - userOp.callDataBytes!, - userOp.callGasLimit, - userOp.verificationGasLimit, - userOp.preVerificationGas, - userOp.maxFeePerGas, - userOp.maxPriorityFeePerGas, - Uint8List.fromList([]), - BigInt.from(_chain.chainId)); - - dev.log("$opHash"); + + final opHash = userOp.hash(_chain); + Uint8List signature = await plugin('signer') .personalSign(opHash, index: index, id: id ?? plugin('signer').defaultId); userOp.signature = hexlify(signature); - dev.log("signed useroperation: ${userOp.signature}"); + await _validateUserOperation(userOp); return userOp; } - Uint8List _getInitCode(String functionName, List params) { + Uint8List _getInitCallData(String functionName, List params) { final factory = plugin<_AccountFactory>('factory'); - final data = factory.self.function(functionName).encodeCall(params); - dev.log(bytesToHex(data)); - final initCode = - abi.encode(['address', 'bytes'], [factory.self.address, data]); - return initCode; - } - - Future _getInitCodeGas() { - require(_initCode != null, "No init code"); - return plugin('ethRpc') - .estimateGas(_chain.entrypoint, _initCode!); + return factory.self.function(functionName).encodeCall(params); } Future _getNonce() async { - if (_walletAddress == null) { - return Uint256.zero; - } - return plugin("contract") - .call(_walletAddress!, ContractAbis.get('getNonce'), "getNonce") - .then((value) => Uint256(value[0])) - .catchError((e) => Uint256.zero); + _validateAddressAndThrow(); + return plugin("contract").call( + _chain.entrypoint, ContractAbis.get('getNonce'), "getNonce", params: [ + _walletAddress, + BigInt.zero + ]).then((value) => Uint256(value[0])); } Future _updateUserOperation(UserOperation op) async { - List responses = await Future.wait( - [plugin('ethRpc').getGasPrice(), nonce, deployed]); - dev.log("responses from update: $responses"); - - String extractInitCode(String code) { - // making sure the first 20 bytes is the factory address. - String extractedString = code.substring(0, 66); - String modifiedString = - extractedString.replaceRange(2, extractedString.length - 40, ""); - return modifiedString + code.substring(66); - } + List responses = await Future.wait([ + plugin('ethRpc').getGasPrice(), + _getNonce(), + Future.microtask(() async => _deployed = await deployed) + ]); op = UserOperation.update(op.toMap(), sender: _walletAddress, nonce: responses[1].value, - initCode: !(responses[2]) ? extractInitCode(_initCode!) : null); + initCode: responses[2] ? "0x" : null); op.maxFeePerGas = (responses[0] as Map)["maxFeePerGas"]!.getInWei; op.maxPriorityFeePerGas = (responses[0] as Map)["maxPriorityFeePerGas"]! .getInWei; - op.signature = op.dummySig; - dev.log("mock user op = ${op.toMap()}"); - // UserOperationGas opGas = await plugin('bundler') - // .estimateUserOperationGas(op.toMap(), _chain.entrypoint); - // dev.log("opGas = ${opGas.toString()}"); - // op = UserOperation.update(op.toMap(), opGas: opGas); - return op; + op.signature = plugin('signer').dummySignature; + + return plugin('bundler') + .estimateUserOperationGas(op.toMap(), _chain.entrypoint) + .then((opGas) => UserOperation.update(op.toMap(), opGas: opGas)); + } + + void _validateAddressAndThrow() { + if (_walletAddress == null) { + throw SmartWalletError( + 'Wallet address must be set: DID you call create?', + ); + } } Future _validateUserOperation(UserOperation op) async { - require(op.sender.hex == _walletAddress?.hex && _walletAddress != null, + _validateAddressAndThrow(); + + require(op.sender.hex == _walletAddress!.hex, "Operation sender error. ${op.sender} provided."); - require(op.initCode == ((await deployed) ? "0x" : _initCode!), + require( + (_deployed ?? (await deployed)) + ? hexlify(op.initCode).toLowerCase() == "0x" + : hexlify(op.initCode).toLowerCase() == initCode.toLowerCase(), "Init code mismatch"); - require(op.callGasLimit >= BigInt.from(35000), - "Call gas limit too small expected value greater than 35000"); - require(op.verificationGasLimit >= BigInt.from(70000), - "Verification gas limit too small expected value greater than 70000"); - require(op.preVerificationGas >= BigInt.from(21000), - "Pre verification gas too small expected value greater than 21000"); + require(op.callGasLimit >= BigInt.from(21000), + "Call gas limit too small expected value greater than 21000"); + require(op.verificationGasLimit >= BigInt.from(39000), + "Verification gas limit too small expected value greater than 39000"); + require(op.preVerificationGas >= BigInt.from(5000), + "Pre verification gas too small expected value greater than 5000"); require(op.callData.length >= 4, "Call data too short, min is 4 bytes"); require(op.signature.length >= 64, "Signature too short, min is 32 bytes"); } } + +class SmartWalletError extends Error { + final String message; + + SmartWalletError(this.message); +} diff --git a/lib/src/abis/contract_abis.dart b/lib/src/abis/contract_abis.dart index 6b719ba..602054e 100644 --- a/lib/src/abis/contract_abis.dart +++ b/lib/src/abis/contract_abis.dart @@ -14,7 +14,7 @@ class ContractAbis { break; case 'getNonce': abi = - '[{"inputs":[],"name":"getNonce","outputs":[{"internalType":"uint256","name":"","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" }]'; case 'execute': abi = '[{"inputs":[{"internalType":"address","name":"dest","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"func","type":"bytes"}],"name":"execute","outputs":[],"stateMutability":"nonpayable","type":"function"}]'; diff --git a/lib/src/abis/encoder.abi.json b/lib/src/abis/encoder.abi.json deleted file mode 100644 index ed36b1c..0000000 --- a/lib/src/abis/encoder.abi.json +++ /dev/null @@ -1,82 +0,0 @@ -[ - { - "inputs": [ - { - "internalType": "address", - "name": "entrypoint", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "nonce", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "initCode", - "type": "bytes" - }, - { - "internalType": "bytes", - "name": "callData", - "type": "bytes" - }, - { - "internalType": "uint256", - "name": "callGasLimit", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "verificationGasLimit", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "preVerificationGas", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "maxFeePerGas", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "maxPriorityFeePerGas", - "type": "uint256" - }, - { - "internalType": "bytes", - "name": "paymasterAndData", - "type": "bytes" - }, - { - "internalType": "uint256", - "name": "chainid", - "type": "uint256" - } - ], - "name": "encodeUserOpEntriesToHash", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - } - ] \ No newline at end of file diff --git a/lib/src/abis/encoder.g.dart b/lib/src/abis/encoder.g.dart deleted file mode 100644 index 8a91119..0000000 --- a/lib/src/abis/encoder.g.dart +++ /dev/null @@ -1,65 +0,0 @@ -// Generated code, do not modify. Run `build_runner build` to re-generate! -// @dart=2.12 -// 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( - '[{"inputs":[{"internalType":"address","name":"entrypoint","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"initCode","type":"bytes"},{"internalType":"bytes","name":"callData","type":"bytes"},{"internalType":"uint256","name":"callGasLimit","type":"uint256"},{"internalType":"uint256","name":"verificationGasLimit","type":"uint256"},{"internalType":"uint256","name":"preVerificationGas","type":"uint256"},{"internalType":"uint256","name":"maxFeePerGas","type":"uint256"},{"internalType":"uint256","name":"maxPriorityFeePerGas","type":"uint256"},{"internalType":"bytes","name":"paymasterAndData","type":"bytes"},{"internalType":"uint256","name":"chainid","type":"uint256"}],"name":"encodeUserOpEntriesToHash","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"}]', - 'Encoder', -); - -class Encoder extends _i1.GeneratedContract { - Encoder({ - required _i1.EthereumAddress address, - required _i1.Web3Client client, - int? chainId, - }) : super( - _i1.DeployedContract( - _contractAbi, - address, - ), - client, - chainId, - ); - - /// 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<_i2.Uint8List> encodeUserOpEntriesToHash( - _i1.EthereumAddress sender, - BigInt nonce, - _i2.Uint8List initCode, - _i2.Uint8List callData, - BigInt callGasLimit, - BigInt verificationGasLimit, - BigInt preVerificationGas, - BigInt maxFeePerGas, - BigInt maxPriorityFeePerGas, - _i2.Uint8List paymasterAndData, - BigInt chainid, { - _i1.BlockNum? atBlock, - }) async { - final function = self.abi.functions[1]; - assert(checkSignature(function, '281ac952')); - final params = [ - sender, - nonce, - initCode, - callData, - callGasLimit, - verificationGasLimit, - preVerificationGas, - maxFeePerGas, - maxPriorityFeePerGas, - paymasterAndData, - chainid, - ]; - final response = await read( - function, - params, - atBlock, - ); - return (response[0] as _i2.Uint8List); - } -} diff --git a/lib/src/common/contract.dart b/lib/src/common/contract.dart index 86c3b03..8494af1 100644 --- a/lib/src/common/contract.dart +++ b/lib/src/common/contract.dart @@ -238,7 +238,7 @@ class Contract { spender, tokenId, )); - return UserOperation.partial(callData: hexlify(innerCallData)); + return UserOperation.partial(callData: innerCallData); } /// Returns a UserOperation to transfer an NFT. @@ -256,7 +256,7 @@ class Contract { recipient, tokenId, )); - return UserOperation.partial(callData: hexlify(innerCallData)); + return UserOperation.partial(callData: innerCallData); } /// Returns the UserOperation for an ERC20 approval. @@ -277,7 +277,7 @@ class Contract { to: contractAddress, innerCallData: encodeERC20ApproveCall(contractAddress, spender, amount)); - return UserOperation.partial(callData: hexlify(callData)); + return UserOperation.partial(callData: callData); } /// Returns the UserOperation for an ERC20 transfer. @@ -294,6 +294,6 @@ class Contract { to: contractAddress, innerCallData: encodeERC20TransferCall(contractAddress, recipient, amount)); - return UserOperation.partial(callData: hexlify(callData)); + return UserOperation.partial(callData: callData); } } diff --git a/lib/src/interfaces/multi_signer_interface.dart b/lib/src/interfaces/multi_signer_interface.dart index 7c1c8d3..8d52979 100644 --- a/lib/src/interfaces/multi_signer_interface.dart +++ b/lib/src/interfaces/multi_signer_interface.dart @@ -7,6 +7,11 @@ part of 'package:variance_dart/interfaces.dart'; /// of multi-signers while adhering to a common interface. /// interfaces include: [PrivateKeySigner], [PassKeySigner] and [HDWalletSigner] abstract class MultiSignerInterface { + /// The dummy signature is a valid signature that can be used for testing purposes. + /// specifically, this will be used to simulate user operation on the entrypoint. + /// You must specify a dummy signature that matches your transaction signature standard. + String dummySignature = "0x"; + /// Returns the Hex address associated with the signer. /// /// Optional parameters: diff --git a/lib/src/interfaces/smart_wallet.dart b/lib/src/interfaces/smart_wallet.dart index a9ed18a..1a21b8e 100644 --- a/lib/src/interfaces/smart_wallet.dart +++ b/lib/src/interfaces/smart_wallet.dart @@ -28,6 +28,9 @@ abstract class SmartWalletBase { /// Converts the Smart Wallet address to its hexadecimal representation. String? get toHex; + /// Sets the smart wallet address for this account; + set setWalletAddress(EthereumAddress address); + /// Builds a [UserOperation] based on provided parameters. /// /// This method creates a [UserOperation] with the given call data and optional parameters. @@ -43,7 +46,7 @@ abstract class SmartWalletBase { }); /// manually Sets the init code of the Smart Wallet and overrides the default. - void dangerouslySetInitCode(String? code); + void dangerouslySetInitCallData(Uint8List? code); /// Creates a new wallet address using counterfactual deployment. /// diff --git a/lib/src/interfaces/user_operations.dart b/lib/src/interfaces/user_operations.dart index 2509670..d2dccb4 100644 --- a/lib/src/interfaces/user_operations.dart +++ b/lib/src/interfaces/user_operations.dart @@ -9,9 +9,9 @@ abstract class UserOperationBase { BigInt get nonce; - String get initCode; + Uint8List get initCode; - String get callData; + Uint8List get callData; BigInt get callGasLimit; @@ -25,7 +25,7 @@ abstract class UserOperationBase { String get signature; - String get paymasterAndData; + Uint8List get paymasterAndData; /// Hashes the user operation for the given chain. /// diff --git a/lib/src/signers/hd_wallet_signer.dart b/lib/src/signers/hd_wallet_signer.dart index 58ce0e5..6528163 100644 --- a/lib/src/signers/hd_wallet_signer.dart +++ b/lib/src/signers/hd_wallet_signer.dart @@ -134,4 +134,8 @@ class HDWalletSigner with SecureStorageMixin implements HDInterface { authMiddleware: authMiddleware, credential: _getMnemonic()); } + + @override + String dummySignature = + "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; } diff --git a/lib/src/signers/passkey_signer.dart b/lib/src/signers/passkey_signer.dart index 9893f7e..15135b1 100644 --- a/lib/src/signers/passkey_signer.dart +++ b/lib/src/signers/passkey_signer.dart @@ -146,6 +146,10 @@ class PassKeySigner implements PasskeyInterface { @override PassKeysOptions get opts => _opts; + @override + String dummySignature = + "0xe017c9b829f0d550c9a0f1d791d460485b774c5e157d2eaabdf690cba2a62726b3e3a3c5022dc5301d272a752c05053941b1ca608bf6bc8ec7c71dfe15d5305900000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000025205f5f63c4a6cebdc67844b75186367e6d2e4f19b976ab0affefb4e981c22435050000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d222c226f726967696e223a226170692e776562617574686e2e696f227d000000"; + @override Uint8List clientDataHash(PassKeysOptions options, {String? challenge}) { options.challenge = challenge ?? _randomChallenge(options); diff --git a/lib/src/signers/private_key_signer.dart b/lib/src/signers/private_key_signer.dart index 58923f0..3ff95aa 100644 --- a/lib/src/signers/private_key_signer.dart +++ b/lib/src/signers/private_key_signer.dart @@ -17,7 +17,9 @@ class PrivateKeySigner with SecureStorageMixin implements CredentialInterface { } factory PrivateKeySigner.fromJson(String source, String password) => - PrivateKeySigner._internal(Wallet.fromJson(source, password)); + PrivateKeySigner._internal( + Wallet.fromJson(source, password), + ); static Future loadFromSecureStorage( {required SecureStorageRepository storageMiddleware, @@ -29,9 +31,7 @@ class PrivateKeySigner with SecureStorageMixin implements CredentialInterface { value != null ? PrivateKeySigner.fromJson(value, password) : null); } - const PrivateKeySigner._internal( - this._credential, - ); + PrivateKeySigner._internal(this._credential); @override EthereumAddress get address => _credential.privateKey.address; @@ -67,4 +67,8 @@ class PrivateKeySigner with SecureStorageMixin implements CredentialInterface { authMiddleware: authMiddleware, credential: toJson()); } + + @override + String dummySignature = + "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; } diff --git a/lib/variance.dart b/lib/variance.dart index 1adae1d..d73561e 100644 --- a/lib/variance.dart +++ b/lib/variance.dart @@ -5,12 +5,12 @@ import 'dart:convert'; import 'dart:isolate'; import 'dart:math'; import 'dart:typed_data'; -import 'dart:developer' as dev; import 'package:asn1lib/asn1lib.dart'; import 'package:bip32_bip44/dart_bip32_bip44.dart' as bip44; import "package:bip39/bip39.dart" as bip39; import 'package:cbor/cbor.dart'; +import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:uuid/uuid.dart'; import 'package:web3dart/crypto.dart'; @@ -20,7 +20,6 @@ import 'package:webauthn/webauthn.dart'; import 'interfaces.dart'; import 'src/abis/abis.dart'; -import 'src/abis/encoder.g.dart'; import 'src/common/common.dart'; import 'utils.dart'; From 52987029cbc4a51d2f0ee1751edabe697bc35094 Mon Sep 17 00:00:00 2001 From: Peter Anyaogu Date: Sun, 7 Jan 2024 23:45:55 +0100 Subject: [PATCH 3/4] feat: fix hd wallet signatures --- lib/src/4337/wallet.dart | 33 +++++++++++------------ lib/src/common/uint256.dart | 7 ++++- lib/src/interfaces/uint256_interface.dart | 25 ++++++++++++++--- lib/src/signers/hd_wallet_signer.dart | 17 ++++++------ 4 files changed, 52 insertions(+), 30 deletions(-) diff --git a/lib/src/4337/wallet.dart b/lib/src/4337/wallet.dart index e6e4171..048a219 100644 --- a/lib/src/4337/wallet.dart +++ b/lib/src/4337/wallet.dart @@ -139,10 +139,10 @@ class SmartWallet with _PluginManager implements SmartWalletBase { @override Future createSimpleAccount(Uint256 salt, {int? index}) async { EthereumAddress signer = EthereumAddress.fromHex( - plugin('signer').getAddress()); + plugin('signer').getAddress(index: index ?? 0)); _initCalldata = _getInitCallData('createAccount', [signer, salt.value]); await getSimpleAccountAddress(signer, salt) - .then((value) => {_walletAddress = value}); + .then((addr) => {_walletAddress = addr}); return this; } @@ -170,25 +170,17 @@ class SmartWallet with _PluginManager implements SmartWalletBase { @override Future getSimpleAccountAddress( EthereumAddress signer, Uint256 salt) { - try { - return plugin<_AccountFactory>('factory').getAddress(signer, salt.value); - } catch (e) { - throw SmartWalletError("Simple acount creation error: $e"); - } + return plugin<_AccountFactory>('factory').getAddress(signer, salt.value); } @override Future getSimplePassKeyAccountAddress( PassKeyPair pkp, Uint256 salt) { - try { - return plugin<_AccountFactory>('factory').getPasskeyAccountAddress( - pkp.credentialHexBytes, - pkp.publicKey[0].value, - pkp.publicKey[1].value, - salt.value); - } catch (e) { - throw SmartWalletError("Passkey acount creation error: $e"); - } + return plugin<_AccountFactory>('factory').getPasskeyAccountAddress( + pkp.credentialHexBytes, + pkp.publicKey[0].value, + pkp.publicKey[1].value, + salt.value); } @override @@ -244,9 +236,14 @@ class SmartWallet with _PluginManager implements SmartWalletBase { final opHash = userOp.hash(_chain); Uint8List signature = await plugin('signer') - .personalSign(opHash, + .personalSign( + opHash, index: index, - id: id ?? plugin('signer').defaultId); + id: id ?? + (plugin('signer') is PasskeyInterface + ? plugin('signer').defaultId + : id)); + userOp.signature = hexlify(signature); await _validateUserOperation(userOp); diff --git a/lib/src/common/uint256.dart b/lib/src/common/uint256.dart index 08501c2..9d26097 100644 --- a/lib/src/common/uint256.dart +++ b/lib/src/common/uint256.dart @@ -65,7 +65,12 @@ class Uint256 implements Uint256Base { } @override - double toUnit(int decimals) { + BigInt toUnit(int decimals) { + return _value * BigInt.from(pow(10, decimals)); + } + + @override + double fromUnit(int decimals) { return _value / BigInt.from(pow(10, decimals)); } diff --git a/lib/src/interfaces/uint256_interface.dart b/lib/src/interfaces/uint256_interface.dart index 0fce3d7..38c5b52 100644 --- a/lib/src/interfaces/uint256_interface.dart +++ b/lib/src/interfaces/uint256_interface.dart @@ -62,10 +62,29 @@ abstract class Uint256Base { @override String toString(); - /// Converts this Uint256 to a [double] giving the [decimals]. + /// The `toUnit` method is used to convert a Uint256 value from its base unit to a specific + /// unit. It takes an `int` parameter `decimals` which represents the number of decimal places + /// to consider. The method returns a `BigInt` representing the converted value. + /// example: /// - /// Returns an [double] - double toUnit(int decimals); + /// ```dart + /// Uint256(BigInt.from(1)).toUnit(18); + /// + /// // 1 * 10^18 = 1000000000000000000 + /// ``` + BigInt toUnit(int decimals); + + /// The `fromUnit` method is used to convert a Uint256 value from a specific unit to its base + /// unit. It takes an `int` parameter `decimals` which represents the number of decimal places + /// to consider. The method returns a `double` representing the converted value. + /// example: + /// + /// ```dart + /// Uint256(BigInt.from(1000000000000000000)).fromUnit(18); + /// + /// // 1000000000000000000 / 10^18 = 1.0 + /// ``` + double fromUnit(int decimals); /// Converts this Uint256 to an [EtherAmount] in wei. /// diff --git a/lib/src/signers/hd_wallet_signer.dart b/lib/src/signers/hd_wallet_signer.dart index 6528163..ca87626 100644 --- a/lib/src/signers/hd_wallet_signer.dart +++ b/lib/src/signers/hd_wallet_signer.dart @@ -1,12 +1,15 @@ part of 'package:variance_dart/variance.dart'; class HDWalletSigner with SecureStorageMixin implements HDInterface { - String? _mnemonic; + final String _mnemonic; + final String _seed; late final EthereumAddress zerothAddress; - HDWalletSigner({required String seed}) : _seed = seed { + HDWalletSigner._internal({required String seed, required String mnemonic}) + : _seed = seed, + _mnemonic = mnemonic { assert(seed.isNotEmpty, "seed cannot be empty"); } @@ -14,8 +17,7 @@ class HDWalletSigner with SecureStorageMixin implements HDInterface { /// /// Returns the HD signer instance. factory HDWalletSigner.createWallet() { - final mnemonic = bip39.generateMnemonic(); - return HDWalletSigner.recoverAccount(mnemonic); + return HDWalletSigner.recoverAccount(bip39.generateMnemonic()); } /// Recovers an account from a mnemonic phrase and stores it in the HD wallet as zeroth. @@ -25,7 +27,7 @@ class HDWalletSigner with SecureStorageMixin implements HDInterface { /// Returns the HD signer instance. factory HDWalletSigner.recoverAccount(String mnemonic) { final seed = bip39.mnemonicToSeedHex(mnemonic); - final signer = HDWalletSigner(seed: seed); + final signer = HDWalletSigner._internal(seed: seed, mnemonic: mnemonic); signer.zerothAddress = signer._add(seed, 0); return signer; } @@ -52,7 +54,7 @@ class HDWalletSigner with SecureStorageMixin implements HDInterface { } @override - String? exportMnemonic() { + String exportMnemonic() { return _getMnemonic(); } @@ -115,8 +117,7 @@ class HDWalletSigner with SecureStorageMixin implements HDInterface { return _deriveHdKey(_seed, index); } - String? _getMnemonic() { - require(_mnemonic != null, "exportMnemonic: Not a Valid Wallet"); + String _getMnemonic() { return _mnemonic; } From f1a783dec4c27c07c8d06ae43a9089e152edd1fc Mon Sep 17 00:00:00 2001 From: Peter Anyaogu Date: Sat, 13 Jan 2024 13:37:15 +0100 Subject: [PATCH 4/4] feat: improved docs and code, bug fixes --- .github/workflows/ci.yml | 39 ++ .github/workflows/publish.yml | 12 + README.md | 187 ++++++++- analysis_options.yaml | 6 +- example/lib/main.dart | 103 +++++ example/pubspec.yml | 16 + lib/interfaces.dart | 44 +- lib/src/4337/chains.dart | 8 +- lib/src/4337/paymaster.dart | 2 +- lib/src/4337/providers.dart | 15 +- lib/src/4337/userop.dart | 46 ++- lib/src/4337/wallet.dart | 239 +++++------ lib/src/common/abi_coder.dart | 34 +- lib/src/common/address.dart | 78 ++-- lib/src/common/common.dart | 9 +- lib/src/common/contract.dart | 389 ++++++++++++++---- lib/src/common/plugins.dart | 29 +- lib/src/common/uint256.dart | 37 +- lib/src/interfaces/account_factory.dart | 2 +- lib/src/interfaces/bundler_provider.dart | 94 ++++- lib/src/interfaces/credential_interface.dart | 34 -- lib/src/interfaces/ens_resolver.dart | 2 +- lib/src/interfaces/hd_interface.dart | 45 +- lib/src/interfaces/interfaces.dart | 43 ++ lib/src/interfaces/local_authentication.dart | 2 +- .../interfaces/multi_signer_interface.dart | 69 ++-- lib/src/interfaces/passkey_interface.dart | 94 +++-- lib/src/interfaces/rpc_provider.dart | 84 +++- .../interfaces/secure_storage_repository.dart | 145 +++++-- lib/src/interfaces/smart_wallet.dart | 235 +++++++++-- lib/src/interfaces/uint256_interface.dart | 141 ++++--- lib/src/interfaces/user_operations.dart | 2 +- lib/src/signers/hd_wallet_signer.dart | 104 +++-- lib/src/signers/passkey_signer.dart | 31 +- lib/src/signers/private_key_signer.dart | 92 ++++- lib/src/utils/crypto.dart | 103 +++-- lib/src/utils/dio_client.dart | 2 +- lib/src/utils/local_authentication.dart | 2 +- lib/src/utils/secure_storage_repository.dart | 34 +- lib/utils.dart | 4 +- lib/variance.dart | 4 +- pubspec.lock | 44 +- pubspec.yaml | 1 + 43 files changed, 1954 insertions(+), 752 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish.yml create mode 100644 example/lib/main.dart create mode 100644 example/pubspec.yml delete mode 100644 lib/src/interfaces/credential_interface.dart create mode 100644 lib/src/interfaces/interfaces.dart diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..55d468a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + get_dependencies: + name: "Get dependencies" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: dart-lang/setup-dart@v1 + - name: "Print Dart SDK Version" + run: dart --version + - uses: actions/cache@v2 + with: + path: .dart_tool + key: dart-dependencies-${{ hashFiles('pubspec.yaml') }} + - name: "Get dependencies" + env: + PUB_CACHE: ".dart_tool/pub_cache" + run: dart pub upgrade + + analyze: + name: "Analysis" + needs: get_dependencies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: .dart_tool + key: dart-dependencies-${{ hashFiles('pubspec.yaml') }} + - uses: dart-lang/setup-dart@v1 + - run: "dart format --output=none --set-exit-if-changed ." + - run: dart analyze --fatal-infos diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..a2be2df --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,12 @@ +name: Publish to pub.dev + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+*" + +jobs: + publish: + permissions: + id-token: write # Required for authentication using OIDC + uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 diff --git a/README.md b/README.md index 985d9b3..51352e4 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,181 @@ -# 4337 Dart +# Variance SDK -Account Abstraction SDK written in dart. Build Cross Platform Native Ethereum dApps easily. +Variance is a Dart SDK designed to simplify interaction with Ethereum-based blockchains and enables flutter developers to implement account abstraction with minimal efforts. It provides functionalities such as encoding and decoding ABI data, handling Ethereum transactions, working with ERC20 and ERC721 tokens, and managing Ethereum smart accounts. -supports: +## Features -- [x] multi-signatures - - - [x] passkeys (secp256r1) - - - [x] default (secp256k1) - - - [x] DIY (custom schemes) -- [] safe core protocol -- [x] SDK plugins +- **ABI Encoding/Decoding:** Easily encode and decode ABI data for Ethereum smart contract and Entrypoint interactions. +- **Transaction Handling:** Simplify the process of creating and sending UserOperations. +- **Token Operations:** Work with ERC20 and ERC721 tokens, including transfer and approval functionalities. +- **Secure Storage:** Securely store and manage sensitive data such as private keys and credentials. +- **Web3 Functionality:** Interact with Ethereum nodes and bundlers using web3 functions like `eth_call`, `eth_sendTransaction`, `eth_sendUserOperation`, etc. +- **PassKeyPair and HDWalletSigner:** Manage smart accounts signers using Passkeys or Seed Phrases. + +## Getting Started + +### Installation + +```yml +// Add this line to your pubspec.yaml file + +dependencies: + variance_dart: ^0.0.4 +``` + +Then run: + +```sh +flutter pub get +``` + +### Usage + +```dart +// Import the package +import 'package:variance_dart/utils.dart'; +import 'package:variance_dart/variance.dart'; + +// optionally +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:web3dart/web3dart.dart'; +``` + +configure your chains: there are 2 ways to get the chain configuration. either manually or using the already supported configurations. + +```dart +Chain chain; + +// manually +const String rpcUrl = 'http://localhost:8545'; +const String bundlerUrl = 'http://localhost:3000/rpc'; + +chain = Chain( + ethRpcUrl: rpcUrl, + bundlerUrl: bundlerUrl, + entrypoint: Constants.entrypoint, + accountFactory: Constants.accountFactory, + chainId: 1337, + explorer: ""); + +// using pre configured chain +chain = Chains.getChain(Network.localhost) + ..ethRpcUrl = rpcUrl + ..bundlerUrl = bundlerUrl; +``` + +In order to create a smart wallet client you need to set up a signer, which will sign useroperation hashes to be verified onchain. +there are 3 available signers: + +- passkeys +- hd wallet +- simple credential (privatekey) + +> Variance SDK can be used to create both EOA and Smart Wallets. the `HD wallet signer` itself is a fully featured EOA wallet that can be used to build any EOA wallet like metamask. it can also be used as an account signer for a smart wallet. + +```dart +// create smart wallet signer based of seed phrase +final HDWalletSigner hd = HDWalletSigner.createWallet(); +print("mnemonic: ${hd.exportMnemonic()}"); + +// create a smart wallet signer based on passkeys +// this operation requires biometrics verification from the user +final PassKeyPair pkp = + await PassKeySigner("myapp.xyz", "myapp", "https://myapp.xyz") + .register("", true); +print("pkp: ${pkp.toJson()}"); +``` + +Optionally the credentials returned from the signer instances can be securely saved on device android encrypted shared preferences or ios keychain using the `SecureStorageMiddleware`. + +```dart +// save a signer credential to device +await hd + .withSecureStorage(FlutterSecureStorage()) + .saveCredential(CredentialType.hdwallet); + +await pkp + .withSecureStorage(FlutterSecureStorage()) + .saveCredential(CredentialType.passkeypair); + +// load a credential from the device +final ss = SecureStorageMiddleware(secureStorage: FlutterSecureStorage()); +final hdInstance = + await HDWalletSigner.loadFromSecureStorage(storageMiddleware: ss); +print("pkp: ${hdInstance?.exportMnemonic()}"); + +// NOTE: interactions with securestorage can be authenticated when using `SecureStorageMiddleware` + +final ss = SecureStorageMiddleware(secureStorage: FlutterSecureStorage(), authMiddleware: AuthenticationMiddleware()); +// then used with `SecureStorageMiddleware` in the following way + +ss.save("key", "value", options: SSAuthOperationOptions(requiresAuth: true, authReason: "reason")); +ss.read("key"); // default options are used i.e requiresAuth: false +ss.delete("key", options: SSAuthOperationOptions(requiresAuth: false)); // explicitly reject authentication +``` + +Interacting with the smart wallet: + +```dart +// create a smart wallet client +final walletClient = SmartWallet( + chain: chain, + signer: hd, + bundler: BundlerProvider(chain, RPCProvider(chain.bundlerUrl!)), +); + +// create a simple account based on hd +final SmartWallet simpleSmartAccount = + await walletClient.createSimpleAccount(salt); +print("simple account address: ${simpleSmartAccount.address}"); + +// create a simple account based on pkp +final SmartWallet simplePkpAccount = + await walletClient.createSimplePasskeyAccount(pkp, salt); +print("simple pkp account address: ${simplePkpAccount.address}"); + +// retrieve the balance of a smart wallet +final EtherAmount balance = await simpleSmartAccount.balance; +print("account balance: ${balance.getInWei}"); + +// retrive the account nonce +final Uint256 nonce = await simpleSmartAccount.nonce; +print("account nonce: ${nonce.toInt()}"); + +// check if a smart wallet has been deployed +final bool deployed = await simpleSmartAccount.deployed; +print("account deployed: $deployed"); + +// get the init code of the smart wallet +final String initCode = simpleSmartAccount.initCode; +print("account init code: $initCode"); + +// perform a simple transaction (send ether to another account) +// account must be prefunded with native token. paymaster is not yet implemented +await simpleSmartAccount.send( + EthereumAddress.fromHex( + "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"), // receive address + getConversion("0.7142"), // 0.7142 ether +); + + +// utility function to convert eth amount from string to wei +EtherAmount getConversion(String amount) { + final amtToDb = double.parse(amount); + return EtherAmount.fromBigInt( + EtherUnit.wei, BigInt.from(amtToDb * pow(10, 18))); +} +``` + +For detailed usage and examples, refer to the [documentation](https://docs.variance.space). Additional refer to the [demo](https://github.com/vaariance/variancedemo) for use in a flutter app. + +## API Reference + +Detailed API reference and examples can be found in the [API reference](https://pub.dev/documentation/variance_dart/latest/variance/variance-library.html). + +## Contributing + +We are committed to maintaining variance as an open source sdk, take a look at existing issues, open a pull request etc. + +## License + +This project is licensed under the **BSD-3-Clause** - see the [LICENSE](./LICENSE) file for details. diff --git a/analysis_options.yaml b/analysis_options.yaml index 5ab0a6d..4b7fc40 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -3,8 +3,9 @@ # analyzer: exclude: - - "**/*.g.dart" - - "**/*.freezed.dart" + - "lib/src/abis/*.g.dart" + - "lib/src/utils/models/*.dart" + errors: invalid_annotation_target: ignore # The issues identified by the analyzer are surfaced in the UI of Dart-enabled @@ -31,6 +32,5 @@ linter: constant_identifier_names: false # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..c43724e --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,103 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:math'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:variance_dart/utils.dart'; +import 'package:variance_dart/variance.dart'; +import 'package:web3dart/web3dart.dart'; + +const String rpcUrl = 'http://localhost:8545'; +const String bundlerUrl = 'http://localhost:3000/rpc'; + +Future main() async { + final Uint256 salt = Uint256.zero; + + // configure your chain + final Chain chain = Chain( + ethRpcUrl: rpcUrl, + bundlerUrl: bundlerUrl, + entrypoint: Constants.entrypoint, + accountFactory: Constants.accountFactory, + chainId: 1337, + explorer: ""); + + // create smart wallet signer based of seed phrase + final HDWalletSigner hd = HDWalletSigner.createWallet(); + print("mnemonic: ${hd.exportMnemonic()}"); + + // create a smart wallet signer based on passkeys + // this operation requires biometrics verification from the user + final PassKeyPair pkp = + await PassKeySigner("myapp.xyz", "myapp", "https://myapp.xyz") + .register("", true); + print("pkp: ${pkp.toJson()}"); + + // save a signer credential to device + await hd + .withSecureStorage(FlutterSecureStorage()) + .saveCredential(CredentialType.hdwallet); + + // load a credential from the device + final ss = SecureStorageMiddleware(secureStorage: FlutterSecureStorage()); + final hdInstance = + await HDWalletSigner.loadFromSecureStorage(storageMiddleware: ss); + print("pkp: ${hdInstance?.exportMnemonic()}"); + + // NOTE: interactions with securestorage can be authenticated when using `SecureStorageMiddleware` + // + // final ss = SecureStorageMiddleware(secureStorage: FlutterSecureStorage(), authMiddleware: AuthenticationMiddleware()); + // then used with `SecureStorageMiddleware` in the following way + // + // ss.save("key", "value", options: SSAuthOperationOptions(requiresAuth: true, authReason: "reason")) + // ss.read("key") // default options are used i.e requiresAuth: false + // ss.delete("key", options: SSAuthOperationOptions(requiresAuth: false)) // explicitly reject authentication + //; + + // create a smart wallet client + final walletClient = SmartWallet( + chain: chain, + signer: hd, + bundler: BundlerProvider(chain, RPCProvider(chain.bundlerUrl!)), + ); + + // create a simple account based on hd + final SmartWallet simpleSmartAccount = + await walletClient.createSimpleAccount(salt); + print("simple account address: ${simpleSmartAccount.address}"); + + // create a simple account based on pkp + final SmartWallet simplePkpAccount = + await walletClient.createSimplePasskeyAccount(pkp, salt); + print("simple pkp account address: ${simplePkpAccount.address}"); + + // retrieve the balance of a smart wallet + final EtherAmount balance = await simpleSmartAccount.balance; + print("account balance: ${balance.getInWei}"); + + // retrive the account nonce + final Uint256 nonce = await simpleSmartAccount.nonce; + print("account nonce: ${nonce.toInt()}"); + + // check if a smart wallet has been deployed + final bool deployed = await simpleSmartAccount.deployed; + print("account deployed: $deployed"); + + // get the init code of the smart wallet + final String initCode = simpleSmartAccount.initCode; + print("account init code: $initCode"); + + // perform a simple transaction (send ether to another account) + // account must be prefunded with native token. paymaster is not yet implemented + await simpleSmartAccount.send( + EthereumAddress.fromHex( + "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"), // receive address + getConversion("0.7142"), // 0.7142 ether + ); +} + +EtherAmount getConversion(String amount) { + final amtToDb = double.parse(amount); + return EtherAmount.fromBigInt( + EtherUnit.wei, BigInt.from(amtToDb * pow(10, 18))); +} diff --git a/example/pubspec.yml b/example/pubspec.yml new file mode 100644 index 0000000..b1879ff --- /dev/null +++ b/example/pubspec.yml @@ -0,0 +1,16 @@ +name: example +publish_to: none +version: 0.0.1 +homepage: https://variance.space +repository: https://github.com/vaariance/variance-dart + +environment: + sdk: ">=2.12.0 <4.0.0" + +dependencies: + variance_dart: + path: ../ + +dev_dependencies: + coverage: ^1.1.0 + lints: ^2.0.0 diff --git a/lib/interfaces.dart b/lib/interfaces.dart index 2c18694..b804c71 100644 --- a/lib/interfaces.dart +++ b/lib/interfaces.dart @@ -1,44 +1,4 @@ library interfaces; -import 'dart:typed_data'; - -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:local_auth_android/local_auth_android.dart'; -import 'package:local_auth_ios/local_auth_ios.dart'; -import 'package:variance_dart/utils.dart' - show - SSAuthOperationOptions, - ChainBaseApiBase, - CredentialType, - SecureStorageMiddleware; -import 'package:web3dart/crypto.dart'; -import 'package:web3dart/json_rpc.dart' show RpcService; -import 'package:web3dart/web3dart.dart'; - -import 'src/abis/abis.dart' show Entrypoint; -import 'variance.dart' - show - Chain, - PassKeyPair, - PassKeySignature, - PassKeysOptions, - Uint256, - UserOperation, - UserOperationByHash, - UserOperationGas, - UserOperationReceipt, - UserOperationResponse; - -part 'src/interfaces/account_factory.dart'; -part 'src/interfaces/bundler_provider.dart'; -part 'src/interfaces/credential_interface.dart'; -part 'src/interfaces/ens_resolver.dart'; -part 'src/interfaces/hd_interface.dart'; -part 'src/interfaces/local_authentication.dart'; -part 'src/interfaces/multi_signer_interface.dart'; -part 'src/interfaces/passkey_interface.dart'; -part 'src/interfaces/rpc_provider.dart'; -part 'src/interfaces/secure_storage_repository.dart'; -part 'src/interfaces/smart_wallet.dart'; -part 'src/interfaces/uint256_interface.dart'; -part 'src/interfaces/user_operations.dart'; +export 'src/interfaces/interfaces.dart' + show MultiSignerInterface, HDInterface, PasskeyInterface; diff --git a/lib/src/4337/chains.dart b/lib/src/4337/chains.dart index 2e6c295..d1d3649 100644 --- a/lib/src/4337/chains.dart +++ b/lib/src/4337/chains.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/variance.dart'; +part of '../../variance.dart'; class Chain { final int chainId; @@ -18,10 +18,8 @@ class Chain { /// asserts that [ethRpcUrl] and [bundlerUrl] is provided Chain validate() { - require(ethRpcUrl != null && ethRpcUrl!.isNotEmpty, - "Chain: please provide a valid eth rpc url"); - require(bundlerUrl != null && bundlerUrl!.isNotEmpty, - "Chain: please provide a valid bundler url"); + require(isURL(ethRpcUrl), "Chain: please provide a valid eth rpc url"); + require(isURL(bundlerUrl), "Chain: please provide a valid bundler url"); return this; } } diff --git a/lib/src/4337/paymaster.dart b/lib/src/4337/paymaster.dart index 5cfe7f0..bbf17e6 100644 --- a/lib/src/4337/paymaster.dart +++ b/lib/src/4337/paymaster.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/variance.dart'; +part of '../../variance.dart'; abstract class PaymasterBase {} diff --git a/lib/src/4337/providers.dart b/lib/src/4337/providers.dart index df780b0..fa8e9bc 100644 --- a/lib/src/4337/providers.dart +++ b/lib/src/4337/providers.dart @@ -1,6 +1,7 @@ -part of 'package:variance_dart/variance.dart'; +part of '../../variance.dart'; class BundlerProvider implements BundlerProviderBase { + /// Set of Ethereum RPC methods supported by the SmartWallet SDK. static final Set methods = { 'eth_chainId', 'eth_sendUserOperation', @@ -111,6 +112,18 @@ class BundlerProvider implements BundlerProviderBase { _initialized = true; } + /// Validates if the provided method is a supported RPC method. + /// + /// Parameters: + /// - `method`: The Ethereum RPC method to validate. + /// + /// Throws: + /// - A [Exception] if the method is not a valid supported method. + /// + /// Example: + /// ```dart + /// validateBundlerMethod('eth_sendUserOperation'); + /// ``` static validateBundlerMethod(String method) { require(methods.contains(method), "validateMethod: method ::'$method':: is not a valid method"); diff --git a/lib/src/4337/userop.dart b/lib/src/4337/userop.dart index 1dc0693..0121441 100644 --- a/lib/src/4337/userop.dart +++ b/lib/src/4337/userop.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/variance.dart'; +part of '../../variance.dart'; class UserOperation implements UserOperationBase { @override @@ -67,6 +67,29 @@ class UserOperation implements UserOperationBase { ); } + /// Creates a partial [UserOperation] with specified parameters. + /// + /// Parameters: + /// - `callData` (required): The call data as a [Uint8List]. + /// - `sender`: The Ethereum address of the sender. Defaults to the smartwallet address. + /// - `nonce`: The nonce value. Defaults to [BigInt.zero]. + /// - `initCode`: The initialization code as a [Uint8List]. Defaults to an empty [Uint8List]. + /// - `callGasLimit`: The call gas limit as a [BigInt]. Defaults to [BigInt.from(10000000)]. + /// - `verificationGasLimit`: The verification gas limit as a [BigInt]. Defaults to [BigInt.from(10000000)]. + /// - `preVerificationGas`: The pre-verification gas as a [BigInt]. Defaults to [BigInt.from(21000)]. + /// - `maxFeePerGas`: The maximum fee per gas as a [BigInt]. Defaults to [BigInt.one]. + /// - `maxPriorityFeePerGas`: The maximum priority fee per gas as a [BigInt]. Defaults to [BigInt.one]. + /// + /// Returns: + /// A [UserOperation] instance. + /// + /// Example: + /// ```dart + /// var partialUserOperation = UserOperation.partial( + /// callData: Uint8List(0xabcdef), + /// // Other parameters can be set as needed. + /// ); + /// ``` factory UserOperation.partial({ required Uint8List callData, EthereumAddress? sender, @@ -92,6 +115,27 @@ class UserOperation implements UserOperationBase { paymasterAndData: Uint8List(0), ); + /// Creates a [UserOperation] by updating an existing operation using a map. + /// + /// Parameters: + /// - `map`: A map containing key-value pairs representing the user operation data. + /// - `opGas`: Optional parameter of type [UserOperationGas] for specifying gas-related information. + /// - `sender`: Optional Ethereum address of the sender. + /// - `nonce`: Optional nonce value. + /// - `initCode`: Optional initialization code. + /// + /// Returns: + /// A [UserOperation] instance created from the provided map. + /// + /// Example: + /// ```dart + /// var map = UserOperation.partial(callData: Uint8List(0xabcdef)).toMap(); + /// var updatedUserOperation = UserOperation.update( + /// map, + /// opGas: UserOperationGas(callGasLimit: BigInt.from(20000000), ...), + /// // Other parameters can be updated as needed. + /// ); + /// ``` factory UserOperation.update( Map map, { UserOperationGas? opGas, diff --git a/lib/src/4337/wallet.dart b/lib/src/4337/wallet.dart index 048a219..1dd8052 100644 --- a/lib/src/4337/wallet.dart +++ b/lib/src/4337/wallet.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/variance.dart'; +part of '../../variance.dart'; class SmartWallet with _PluginManager implements SmartWalletBase { final Chain _chain; @@ -9,63 +9,61 @@ class SmartWallet with _PluginManager implements SmartWalletBase { bool? _deployed; - /// [Entrypoint] is not initialized - /// to initialize with entrypoint, you have to call [SmartWallet.init] instead - /// - /// - [bundler]: Is the bundler provider e.g voltaire, alto, stackup ... - /// - [jsonRpcProvider]: (optional) The Ethereum JSON RPC provider. e.g infura, alchemy, quicknode - /// - /// Creates an instance of [SmartWallet] SmartWallet( {required Chain chain, required MultiSignerInterface signer, required BundlerProviderBase bundler, - RPCProviderBase? jsonRpcProvider, EthereumAddress? address}) : _chain = chain.validate(), _walletAddress = address { - final rpc = jsonRpcProvider ?? RPCProvider(chain.ethRpcUrl!); - addPlugin('signer', signer); - addPlugin('bundler', bundler); - addPlugin('ethRpc', rpc); - addPlugin('contract', Contract(rpc)); - addPlugin( - 'factory', - _AccountFactory( - address: chain.accountFactory, chainId: chain.chainId, rpc: rpc)); + final rpc = RPCProvider(chain.ethRpcUrl!); + final fact = _AccountFactory( + address: chain.accountFactory, chainId: chain.chainId, rpc: rpc); + + addPlugin('signer', signer); + addPlugin('bundler', bundler); + addPlugin('ethRpc', rpc); + addPlugin('contract', Contract(rpc)); + addPlugin('factory', fact); } - /// Initializes the [SmartWallet] instance and the associated [Entrypoint] contract. + /// Initializes a [SmartWallet] instance for a specific chain with the provided parameters. + /// + /// Parameters: + /// - `chain`: The blockchain [Chain] associated with the smart wallet. + /// - `signer`: The [MultiSignerInterface] responsible for signing transactions. + /// - `bundler`: The [BundlerProviderBase] that provides bundling services. + /// - `address`: Optional Ethereum address associated with the smart wallet. + /// - `initCallData`: Optional initialization calldata of the factory create method as a [Uint8List]. /// - /// Use this method directly when you need to interact with the entrypoint, - /// wait for user Operation Events, or recovering an account. + /// Returns: + /// A fully initialized [SmartWallet] instance. /// - /// - [chain]: The blockchain chain. - /// - [signer]: required multi-signer interface - /// - [bundler]: The bundler provider. - /// - [jsonRpcProvider]: The Ethereum JSON RPC provider (optional). - /// - [address]: The Ethereum address (optional). - /// - [initCallData]: The callData of the factory create method `WITHOUT the factory address`. (optional). + /// Example: + /// ```dart + /// var smartWallet = SmartWallet.init( + /// chain: Chain.ethereum, + /// signer: myMultiSigner, + /// bundler: myBundler, + /// address: myWalletAddress, + /// initCallData: Uint8List.fromList([0x01, 0x02, 0x03]), + /// ); + /// ``` + /// additionally initializes the associated Entrypoint contract for `tx.wait(userOpHash)` calls factory SmartWallet.init( {required Chain chain, required MultiSignerInterface signer, required BundlerProviderBase bundler, - RPCProviderBase? jsonRpcProvider, EthereumAddress? address, Uint8List? initCallData}) { final instance = SmartWallet( - chain: chain, - signer: signer, - bundler: bundler, - jsonRpcProvider: jsonRpcProvider, - address: address); + chain: chain, signer: signer, bundler: bundler, address: address); instance ..dangerouslySetInitCallData(initCallData) - ..plugin('bundler') - .initializeWithEntrypoint(Entrypoint( + ..plugin('bundler').initializeWithEntrypoint(Entrypoint( address: chain.entrypoint, - client: instance.plugin<_AccountFactory>('factory').client, + client: instance.plugin('factory').client, )); return instance; @@ -75,12 +73,12 @@ class SmartWallet with _PluginManager implements SmartWalletBase { EthereumAddress? get address => _walletAddress; @override - Future get balance async => - await plugin("contract").getBalance(_walletAddress); + Future get balance => + plugin("contract").getBalance(_walletAddress); @override - Future get deployed async => - await plugin("contract").deployed(_walletAddress); + Future get deployed => + plugin("contract").deployed(_walletAddress); @override String get initCode => _initCode; @@ -89,7 +87,7 @@ class SmartWallet with _PluginManager implements SmartWalletBase { Future get initCodeGas => _initCodeGas; @override - Future get nonce async => await _getNonce(); + Future get nonce => _getNonce(); @override @Deprecated( @@ -113,29 +111,6 @@ class SmartWallet with _PluginManager implements SmartWalletBase { Future get _initCodeGas => plugin('ethRpc') .estimateGas(_chain.entrypoint, _initCode); - @override - UserOperation buildUserOperation({ - required Uint8List callData, - BigInt? customNonce, - BigInt? callGasLimit, - BigInt? verificationGasLimit, - BigInt? preVerificationGas, - BigInt? maxFeePerGas, - BigInt? maxPriorityFeePerGas, - }) { - return UserOperation.partial( - callData: callData, - initCode: _initCodeBytes, - sender: _walletAddress, - nonce: customNonce, - callGasLimit: callGasLimit, - verificationGasLimit: verificationGasLimit, - preVerificationGas: preVerificationGas, - maxFeePerGas: maxFeePerGas, - maxPriorityFeePerGas: maxPriorityFeePerGas, - ); - } - @override Future createSimpleAccount(Uint256 salt, {int? index}) async { EthereumAddress signer = EthereumAddress.fromHex( @@ -146,7 +121,6 @@ class SmartWallet with _PluginManager implements SmartWalletBase { return this; } - //create account signed with passkey @override Future createSimplePasskeyAccount( PassKeyPair pkp, Uint256 salt) async { @@ -169,64 +143,75 @@ class SmartWallet with _PluginManager implements SmartWalletBase { @override Future getSimpleAccountAddress( - EthereumAddress signer, Uint256 salt) { - return plugin<_AccountFactory>('factory').getAddress(signer, salt.value); - } + EthereumAddress signer, Uint256 salt) => + plugin<_AccountFactory>('factory').getAddress(signer, salt.value); @override Future getSimplePassKeyAccountAddress( - PassKeyPair pkp, Uint256 salt) { - return plugin<_AccountFactory>('factory').getPasskeyAccountAddress( - pkp.credentialHexBytes, - pkp.publicKey[0].value, - pkp.publicKey[1].value, - salt.value); - } + PassKeyPair pkp, Uint256 salt) => + plugin<_AccountFactory>('factory').getPasskeyAccountAddress( + pkp.credentialHexBytes, + pkp.publicKey[0].value, + pkp.publicKey[1].value, + salt.value); + + @override + UserOperation buildUserOperation({ + required Uint8List callData, + BigInt? customNonce, + BigInt? callGasLimit, + BigInt? verificationGasLimit, + BigInt? preVerificationGas, + BigInt? maxFeePerGas, + BigInt? maxPriorityFeePerGas, + }) => + UserOperation.partial( + callData: callData, + initCode: _initCodeBytes, + sender: _walletAddress, + nonce: customNonce, + callGasLimit: callGasLimit, + verificationGasLimit: verificationGasLimit, + preVerificationGas: preVerificationGas, + maxFeePerGas: maxFeePerGas, + maxPriorityFeePerGas: maxPriorityFeePerGas, + ); @override Future send( - EthereumAddress recipient, EtherAmount amount) async { - _validateAddressAndThrow(); - return sendUserOperation(buildUserOperation( - callData: - Contract.execute(_walletAddress!, to: recipient, amount: amount))); - } + EthereumAddress recipient, EtherAmount amount) => + sendUserOperation(buildUserOperation( + callData: + Contract.execute(_walletAddress, to: recipient, amount: amount))); @override - Future sendBatchedTransaction( - List recipients, List calls, - {List? amounts}) async { - _validateAddressAndThrow(); - return sendUserOperation(buildUserOperation( - callData: Contract.executeBatch( - walletAddress: _walletAddress!, - recipients: recipients, - amounts: amounts, - innerCalls: calls))); - } + Future sendTransaction( + EthereumAddress to, Uint8List encodedFunctionData, + {EtherAmount? amount}) => + sendUserOperation(buildUserOperation( + callData: Contract.execute(_walletAddress, + to: to, amount: amount, innerCallData: encodedFunctionData))); @override - Future sendSignedUserOperation( - UserOperation op) async { - return plugin('bundler') - .sendUserOperation(op.toMap(), _chain.entrypoint); - } + Future sendBatchedTransaction( + List recipients, List calls, + {List? amounts}) => + sendUserOperation(buildUserOperation( + callData: Contract.executeBatch( + walletAddress: _walletAddress, + recipients: recipients, + amounts: amounts, + innerCalls: calls))); @override - Future sendTransaction( - EthereumAddress to, Uint8List encodedFunctionData, - {EtherAmount? amount}) async { - _validateAddressAndThrow(); - return sendUserOperation(buildUserOperation( - callData: Contract.execute(_walletAddress!, - to: to, amount: amount, innerCallData: encodedFunctionData))); - } + Future sendSignedUserOperation(UserOperation op) => + plugin('bundler') + .sendUserOperation(op.toMap(), _chain.entrypoint); @override Future sendUserOperation(UserOperation op, - {String? id}) async { - return signUserOperation(op, id: id).then(sendSignedUserOperation); - } + {String? id}) => + signUserOperation(op, id: id).then(sendSignedUserOperation); @override Future signUserOperation(UserOperation userOp, @@ -250,19 +235,18 @@ class SmartWallet with _PluginManager implements SmartWalletBase { return userOp; } - Uint8List _getInitCallData(String functionName, List params) { - final factory = plugin<_AccountFactory>('factory'); - return factory.self.function(functionName).encodeCall(params); - } + Uint8List _getInitCallData(String functionName, List params) => + plugin<_AccountFactory>('factory') + .self + .function(functionName) + .encodeCall(params); - Future _getNonce() async { - _validateAddressAndThrow(); - return plugin("contract").call( - _chain.entrypoint, ContractAbis.get('getNonce'), "getNonce", params: [ - _walletAddress, - BigInt.zero - ]).then((value) => Uint256(value[0])); - } + Future _getNonce() => plugin("contract") + .call(_chain.entrypoint, ContractAbis.get('getNonce'), "getNonce", + params: [_walletAddress, BigInt.zero]) + .then((value) => Uint256(value[0])) + .catchError((e) => throw SmartWalletError( + "Error getting nonce for address: $_walletAddress. ${e.toString()}")); Future _updateUserOperation(UserOperation op) async { List responses = await Future.wait([ @@ -287,18 +271,14 @@ class SmartWallet with _PluginManager implements SmartWalletBase { .then((opGas) => UserOperation.update(op.toMap(), opGas: opGas)); } - void _validateAddressAndThrow() { + Future _validateUserOperation(UserOperation op) async { if (_walletAddress == null) { throw SmartWalletError( - 'Wallet address must be set: DID you call create?', + 'Wallet address must be set: Did you call create?', ); } - } - - Future _validateUserOperation(UserOperation op) async { - _validateAddressAndThrow(); - require(op.sender.hex == _walletAddress!.hex, + require(op.sender.hex == _walletAddress?.hex, "Operation sender error. ${op.sender} provided."); require( (_deployed ?? (await deployed)) @@ -320,4 +300,9 @@ class SmartWalletError extends Error { final String message; SmartWalletError(this.message); + + @override + String toString() { + return message; + } } diff --git a/lib/src/common/abi_coder.dart b/lib/src/common/abi_coder.dart index af3c8ac..408da54 100644 --- a/lib/src/common/abi_coder.dart +++ b/lib/src/common/abi_coder.dart @@ -8,28 +8,42 @@ part of 'common.dart'; class abi { abi._(); - /// Decodes a list of types and values. + /// Decodes a list of ABI-encoded types and values. /// - /// - [types]: A list of string types. - /// - [value]: A [Uint8List] containing the ABI-encoded data. + /// Parameters: + /// - `types`: A list of string types describing the ABI types to decode. + /// - `value`: A [Uint8List] containing the ABI-encoded data to be decoded. /// - /// Returns a list of decoded values. - static List decode(List types, Uint8List value) { + /// Returns: + /// A list of decoded values with the specified type. + /// + /// Example: + /// ```dart + /// var decodedValues = abi.decode(['uint256', 'string'], encodedData); + /// ``` + static List decode(List types, Uint8List value) { List abiTypes = []; for (String type in types) { var abiType = parseAbiType(type); abiTypes.add(abiType); } final parsedData = TupleType(abiTypes).decode(value.buffer, 0); - return parsedData.data as List; + return parsedData.data; } - /// Encodes a list of types and values. + /// Encodes a list of types and values into ABI-encoded data. + /// + /// Parameters: + /// - `types`: A list of string types describing the ABI types. + /// - `values`: A list of dynamic values to be ABI-encoded. /// - /// - [types]: A list of string types. - /// - [values]: A list of dynamic values to be encoded. + /// Returns: + /// A [Uint8List] containing the ABI-encoded types and values. /// - /// Returns a [Uint8List] containing the ABI-encoded types and values. + /// Example: + /// ```dart + /// var encodedData = abi.encode(['uint256', 'string'], [BigInt.from(123), 'Hello']); + /// ``` static Uint8List encode(List types, List values) { List abiTypes = []; LengthTrackingByteSink result = LengthTrackingByteSink(); diff --git a/lib/src/common/address.dart b/lib/src/common/address.dart index 6623f0a..a6cb4fe 100644 --- a/lib/src/common/address.dart +++ b/lib/src/common/address.dart @@ -1,60 +1,20 @@ part of 'common.dart'; -class Address extends ENSResolver { - Address(super.addressBytes, {super.ens}); - - factory Address.fromEthAddress(EthereumAddress ethAddress) { - return Address(ethAddress.addressBytes); - } - - static Future? fromEns(String name, {ChainBaseApiBase? client}) { - return ENSResolver.fromEns(name, client: client) - ?.then((value) => value == null ? null : Address(value.addressBytes)); - } -} - -class AddressFormatter extends EthereumAddress { - AddressFormatter(super.addressBytes); - - factory AddressFormatter.fromEthAddress(EthereumAddress ethAddress) { - return AddressFormatter(ethAddress.addressBytes); - } - - String avatarUrl() { - return 'https://effigy.im/a/$hex.svg'; - } - - String diceAvatar() { - return "https://api.dicebear.com/7.x/pixel-art/svg"; - } - - String formattedAddress({int length = 6}) { - final prefix = hex.substring(0, 2 + length); - final suffix = hex.substring(hex.length - length); - return '$prefix...$suffix'; - } - - EthereumAddress toEthAddress() { - return EthereumAddress(addressBytes); - } -} - -class ENSResolver extends AddressFormatter implements ENSResolverBase { +class Address extends EthereumAddress implements ENSResolverBase { String? _ens; ChainBaseApiBase? client; - ENSResolver(super.addressBytes, {bool ens = false, this.client}) { + Address(super.addressBytes, {bool ens = false, this.client}) { _setEnsName(); } - @override - String? get ens => _ens; + factory Address.fromEthAddress(EthereumAddress ethAddress) { + return Address(ethAddress.addressBytes); + } @override - ENSResolverBase withClient(ChainBaseApiBase client) { - return ENSResolver(addressBytes, client: client); - } + String? get ens => _ens; @override Future? getEnsName() async { @@ -66,6 +26,11 @@ class ENSResolver extends AddressFormatter implements ENSResolverBase { return _getEnsName(address); } + @override + Address withClient(ChainBaseApiBase client) { + return Address(addressBytes, client: client); + } + Future? _getEnsName(EthereumAddress address) { return client?.reverseENSAddress(address).then((value) => value.data?.name); } @@ -78,18 +43,29 @@ class ENSResolver extends AddressFormatter implements ENSResolverBase { }); } - /// Creates an instance of ENSResolver from an ENS name. + /// Creates an instance of Address from an ENS name. /// /// - [name]: The ENS name. /// - /// Returns a [Future] that completes with an instance of ENSResolver. - static Future? fromEns(String name, - {ChainBaseApiBase? client}) { + /// Returns a [Future] that completes with an instance of Address. + static Future? fromEns(String name, {ChainBaseApiBase? client}) { return client ?.resolveENSName(name) .then((value) => value.data?.address) .then((address) => address == null ? null - : ENSResolver(EthereumAddress.fromHex(address).addressBytes)); + : Address(EthereumAddress.fromHex(address).addressBytes)); + } +} + +extension AddressExtension on EthereumAddress { + String avatarUrl() { + return 'https://effigy.im/a/$hex.svg'; + } + + String formattedAddress({int length = 6}) { + final prefix = hex.substring(0, 2 + length); + final suffix = hex.substring(hex.length - length); + return '$prefix...$suffix'; } } diff --git a/lib/src/common/common.dart b/lib/src/common/common.dart index 3086e65..041793f 100644 --- a/lib/src/common/common.dart +++ b/lib/src/common/common.dart @@ -3,13 +3,14 @@ library common; import 'dart:math'; import 'dart:typed_data'; -import 'package:variance_dart/interfaces.dart'; -import 'package:variance_dart/variance.dart'; -import 'package:variance_dart/utils.dart'; -import 'package:variance_dart/src/abis/abis.dart' show ContractAbis; import 'package:web3dart/crypto.dart'; import 'package:web3dart/web3dart.dart'; +import '../../utils.dart'; +import '../../variance.dart'; +import '../abis/abis.dart' show ContractAbis; +import '../interfaces/interfaces.dart'; + part 'abi_coder.dart'; part 'address.dart'; part 'contract.dart'; diff --git a/lib/src/common/contract.dart b/lib/src/common/contract.dart index 8494af1..1132688 100644 --- a/lib/src/common/contract.dart +++ b/lib/src/common/contract.dart @@ -1,10 +1,6 @@ part of 'common.dart'; -/// A wrapper for interacting with deployed Ethereum contracts through RPCProviderBase. -/// -/// The Contract class provides methods to perform various operations on Ethereum smart contracts, -/// including making static calls, checking deployment status, getting contract balance, -/// and encoding data for common operations like ERC20 approvals and transfers. +/// A wrapper for interacting with deployed Ethereum contracts through [RPCProvider]. class Contract { RPCProviderBase _provider; @@ -18,15 +14,29 @@ class Contract { _provider = provider; } - /// Performs a static call to a contract method. + /// Asynchronously calls a function on a smart contract with the provided parameters. /// - /// - [contractAddress]: The address of the contract. - /// - [abi]: The ABI (Application Binary Interface) of the contract. - /// - [methodName]: The name of the method in the contract. - /// - [params]: Additional parameters for the method. - /// - [sender]: Additional sender for the transaction. + /// Parameters: + /// - `contractAddress`: The [EthereumAddress] of the smart contract. + /// - `abi`: The [ContractAbi] representing the smart contract's ABI. + /// - `methodName`: The name of the method to call on the smart contract. + /// - `params`: Optional parameters for the function call. + /// - `sender`: The [EthereumAddress] of the sender, if applicable. /// - /// Returns a list of dynamic values representing the result of the static call. + /// Returns: + /// A [Future] that completes with a list of dynamic values representing the result of the function call. + /// + /// Example: + /// ```dart + /// var result = await call( + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// myErc20ContractAbi, + /// 'balanceOf', + /// params: [ EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432')], + /// ); + /// ``` + /// This method uses the an Ethereum jsonRPC to `staticcall` a function on the specified smart contract. + /// **Note:** This method does not support contract calls with state changes. Future> call( EthereumAddress contractAddress, ContractAbi abi, String methodName, {List? params, EthereumAddress? sender}) { @@ -43,12 +53,23 @@ class Contract { (value) => function.decodeReturnValues(value)); } - /// Checks if a contract is deployed. + /// Asynchronously checks whether a smart contract is deployed at the specified address. + /// + /// Parameters: + /// - `address`: The [EthereumAddress] of the smart contract. + /// - `atBlock`: The [BlockNum] specifying the block to check for deployment. Defaults to the current block. /// - /// - [address]: The address of the contract. - /// - optional [atBlock]: The block number to check. defaults to the current block + /// Returns: + /// A [Future] that completes with a [bool] indicating whether the smart contract is deployed. /// - /// Returns a Future indicating whether the contract is deployed or not. + /// Example: + /// ```dart + /// var isDeployed = await deployed( + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// atBlock: BlockNum.exact(123456), // optional + /// ); + /// ``` + /// This method uses an ethereum jsonRPC to check if a smart contract is deployed at the specified address. Future deployed(EthereumAddress? address, {BlockNum atBlock = const BlockNum.current()}) { if (address == null) { @@ -61,11 +82,23 @@ class Contract { return isDeployed; } - /// Gets the amount of Ether held by a contract. + /// Asynchronously retrieves the balance of an Ethereum address. + /// + /// Parameters: + /// - `address`: The [EthereumAddress] for which to retrieve the balance. + /// - `atBlock`: The [BlockNum] specifying the block at which to check the balance. Defaults to the current block. /// - /// - [address]: The address to get the balance of. + /// Returns: + /// A [Future] that completes with an [EtherAmount] representing the balance. /// - /// Returns a Future representing the balance. + /// Example: + /// ```dart + /// var balance = await getBalance( + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// atBlock: BlockNum.exact(123456), // optional + /// ); + /// ``` + /// This method uses an ethereum jsonRPC to fetch the balance of the specified Ethereum address. Future getBalance(EthereumAddress? address, {BlockNum atBlock = const BlockNum.current()}) { if (address == null) { @@ -77,13 +110,25 @@ class Contract { .then((value) => EtherAmount.fromBigInt(EtherUnit.wei, value)); } - /// Encodes the calldata for ERC20 approval. + /// Encodes an ERC-20 token approval function call. /// - /// - [address]: The 4337 wallet address. - /// - [spender]: The address of the approved spender. - /// - [amount]: The amount to approve for the spender. + /// Parameters: + /// - `address`: The [EthereumAddress] of the ERC-20 token contract. + /// - `spender`: The [EthereumAddress] of the spender to approve. + /// - `amount`: The [EtherAmount] representing the amount to approve in the token's base unit. /// - /// Returns a Uint8List representing the calldata. + /// Returns: + /// A [Uint8List] containing the ABI-encoded data for the 'approve' function call. + /// + /// Example: + /// ```dart + /// var encodedCall = encodeERC20ApproveCall( + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// EthereumAddress.fromHex('0x1234567890abcdef1234567890abcdef12345678'), + /// EtherAmount.inWei(BigInt.from(1000000000000000000)), + /// ); + /// ``` + /// This method uses the ERC-20 contract ABI to return a `calldata` for 'approve' function call. static Uint8List encodeERC20ApproveCall( EthereumAddress address, EthereumAddress spender, @@ -97,13 +142,25 @@ class Contract { ); } - /// Encodes the calldata for ERC20 transfer. + /// Encodes an ERC-20 token transfer function call. + /// + /// Parameters: + /// - `address`: The [EthereumAddress] of the ERC-20 token contract. + /// - `recipient`: The [EthereumAddress] of the recipient to receive the tokens. + /// - `amount`: The [EtherAmount] representing the amount of tokens to transfer in the token's base unit. /// - /// - [address]: The 4337 wallet address. - /// - [recipient]: The address of the recipient. - /// - [amount]: The amount to transfer. + /// Returns: + /// A [Uint8List] containing the ABI-encoded data for the 'transfer' function call. /// - /// Returns a Uint8List representing the calldata. + /// Example: + /// ```dart + /// var encodedCall = encodeERC20TransferCall( + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// EthereumAddress.fromHex('0x1234567890abcdef1234567890abcdef12345678'), + /// EtherAmount.inWei(BigInt.from(1000000000000000000)), + /// ); + /// ``` + /// This method uses the ERC-20 contract ABI to return a `calldata` for'transfer' function call. static Uint8List encodeERC20TransferCall( EthereumAddress address, EthereumAddress recipient, @@ -117,54 +174,107 @@ class Contract { ); } - /// Encodes the calldata for ERC721 approval. + /// Encodes an ERC-721 token approval function call. + /// + /// Parameters: + /// - `contractAddress`: The [EthereumAddress] of the ERC-721 token contract. + /// - `to`: The [EthereumAddress] of the address to grant approval. + /// - `tokenId`: The [BigInt] representing the ID of the token to approve. /// - /// - [contractAddress]: The address of the contract. - /// - [to]: The address to approve. - /// - [tokenId]: The tokenId to approve. + /// Returns: + /// A [Uint8List] containing the ABI-encoded data for the 'approve' function call. /// - /// Returns a Uint8List representing the calldata. + /// Example: + /// ```dart + /// var encodedCall = encodeERC721ApproveCall( + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// EthereumAddress.fromHex('0x1234567890abcdef1234567890abcdef12345678'), + /// BigInt.from(123), + /// ); + /// ``` + /// This method uses the ERC-721 contract ABI to return a `calldata` for 'approve' function call. static Uint8List encodeERC721ApproveCall( EthereumAddress contractAddress, EthereumAddress to, BigInt tokenId) { return encodeFunctionCall("approve", contractAddress, ContractAbis.get("ERC721"), [to.hex, tokenId]); } - /// Encodes the calldata for ERC721 safe transfer. + /// Encodes an ERC-721 token safe transfer function call. /// - /// - [contractAddress]: The address of the contract. - /// - [from]: The address to transfer from. + /// Parameters: + /// - `contractAddress`: The [EthereumAddress] of the ERC-721 token contract. + /// - `from`: The [EthereumAddress] of the current owner of the token. + /// - `to`: The [EthereumAddress] of the recipient to receive the token. + /// - `tokenId`: The [BigInt] representing the ID of the token to transfer. /// - /// Returns a Uint8List representing the calldata. + /// Returns: + /// A [Uint8List] containing the ABI-encoded data for the 'safeTransferFrom' function call. + /// + /// Example: + /// ```dart + /// var encodedCall = encodeERC721SafeTransferCall( + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// EthereumAddress.fromHex('0x1234567890abcdef1234567890abcdef12345678'), + /// EthereumAddress.fromHex('0xabcdef1234567890abcdef1234567890abcdef12'), + /// BigInt.from(123), + /// ); + /// ``` + /// This method uses the ERC-721 contract ABI to return a `calldata` for 'safeTransferFrom' function call. static Uint8List encodeERC721SafeTransferCall(EthereumAddress contractAddress, EthereumAddress from, EthereumAddress to, BigInt tokenId) { return encodeFunctionCall("safeTransferFrom", contractAddress, ContractAbis.get("ERC721"), [from.hex, to.hex, tokenId]); } - /// Encodes the calldata for a function call. + /// Encodes a function call for a smart contract. + /// + /// Parameters: + /// - `methodName`: The name of the method to call. + /// - `contractAddress`: The [EthereumAddress] of the smart contract. + /// - `abi`: The [ContractAbi] representing the smart contract's ABI. + /// - `params`: The list of dynamic parameters for the function call. /// - /// - [methodName]: The name of the method in the contract. - /// - [contractAddress]: The address of the contract. - /// - [abi]: The ABI of the contract. - /// - [params]: The parameters for the method. + /// Returns: + /// A [Uint8List] containing the ABI-encoded calldata for the specified function call. /// - /// Returns a Uint8List representing the calldata. + /// Example: + /// ```dart + /// var encodedCall = encodeFunctionCall( + /// 'transfer', + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// ContractAbis.get('ERC20'), + /// [EthereumAddress.fromHex('0x1234567890abcdef1234567890abcdef12345678'), BigInt.from(100)], + /// ); + /// ``` + /// This method uses the specified ABI to encode the function call for the smart contract. static Uint8List encodeFunctionCall(String methodName, EthereumAddress contractAddress, ContractAbi abi, List params) { final func = getContractFunction(methodName, contractAddress, abi); return func.encodeCall(params); } - /// Generates the calldata for a user operation. + /// Encodes a function call to execute a user operation in a smart wallet. + /// + /// Parameters: + /// - `walletAddress`: The [EthereumAddress] of the smart wallet. + /// - `to`: The [EthereumAddress] of the target recipient for the operation. + /// - `amount`: The [EtherAmount] representing the amount to transfer, if applicable. + /// - `innerCallData`: The [Uint8List] containing inner call data, if applicable. /// - /// - [walletAddress]: The address of the wallet. - /// - [to]: The address or contract to send the transaction to. - /// - [amount]: The amount to send. - /// - [innerCallData]: The calldata of the inner call. + /// Returns: + /// A [Uint8List] containing the ABI-encoded data for the 'execute' function call. /// - /// Returns the Uint8List of the calldata. - static Uint8List execute(EthereumAddress walletAddress, + /// Example: + /// ```dart + /// var encodedCall = execute( + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// to: EthereumAddress.fromHex('0x1234567890abcdef1234567890abcdef12345678'), + /// amount: EtherAmount.inWei(BigInt.from(1000000000000000000)), + /// innerCallData: Uint8List.fromList([]), + /// ); // transfer to 0x1234567890abcdef1234567890abcdef12345678 with 1000000000000000000 wei + /// ``` + /// This method uses the 'execute' function ABI to encode the smart wallet operation. + static Uint8List execute(EthereumAddress? walletAddress, {required EthereumAddress to, EtherAmount? amount, Uint8List? innerCallData}) { @@ -174,6 +284,11 @@ class Contract { innerCallData ?? Uint8List.fromList([]) ]; + if (walletAddress == null) { + throw SmartWalletError( + "Invlaid Operation, SmartWallet Address is undefined! (contract.execute)"); + } + return encodeFunctionCall( 'execute', walletAddress, @@ -182,16 +297,35 @@ class Contract { ); } - /// Generates the calldata for a batched user operation. + /// Encodes a function call to execute a batch of operations in a smart wallet. /// - /// - [walletAddress]: The address of the wallet. - /// - [recipients]: A list of addresses to send the transaction. - /// - [amounts]: A list of amounts to send alongside. - /// - [innerCalls]: A list of calldata of the inner calls. + /// Parameters: + /// - `walletAddress`: The [EthereumAddress] of the smart wallet. + /// - `recipients`: A list of [EthereumAddress] instances representing the recipients for each operation. + /// - `amounts`: Optional list of [EtherAmount] instances representing the amounts to transfer for each operation. + /// - `innerCalls`: Optional list of [Uint8List] instances containing inner call data for each operation. /// - /// Returns the Uint8List of the calldata. + /// Returns: + /// A [Uint8List] containing the ABI-encoded data for the 'executeBatch' function call. + /// + /// Example: + /// ```dart + /// var encodedCall = executeBatch( + /// walletAddress: EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// recipients: [ + /// EthereumAddress.fromHex('0x1234567890abcdef1234567890abcdef12345678'), + /// EthereumAddress.fromHex('0xabcdef1234567890abcdef1234567890abcdef12'), + /// ], + /// amounts: [EtherAmount.zero(), EtherAmount.inWei(BigInt.from(1000000000000000000))], + /// innerCalls: [ + /// Uint8List.fromList([...]), + /// Uint8List.fromList([]), + /// ], + /// ); // first call contains call to 0x1234567890abcdef1234567890abcdef12345678 with 0 wei, second call contains call to 0xabcdef1234567890abcdef1234567890abcdef12 with 1000000000000000000 wei + /// ``` + /// This method uses the 'executeBatch' function ABI to encode the smart wallet batch operation. static Uint8List executeBatch( - {required EthereumAddress walletAddress, + {required EthereumAddress? walletAddress, required List recipients, List? amounts, List? innerCalls}) { @@ -203,6 +337,12 @@ class Contract { if (innerCalls == null || innerCalls.isEmpty) { require(amounts != null && amounts.isNotEmpty, "malformed batch request"); } + + if (walletAddress == null) { + throw SmartWalletError( + "Invlaid Operation, SmartWallet Address is undefined! (contract.executeBatch)"); + } + return encodeFunctionCall( 'executeBatch', walletAddress, @@ -211,25 +351,53 @@ class Contract { ); } - /// Returns a ContractFunction instance for a given method. + /// Retrieves a smart contract function by name from its ABI. + /// + /// Parameters: + /// - `methodName`: The name of the method to retrieve. + /// - `contractAddress`: The [EthereumAddress] of the smart contract. + /// - `abi`: The [ContractAbi] representing the smart contract's ABI. /// - /// - [methodName]: The name of the method in the contract. - /// - [contractAddress]: The address of the contract. - /// - [abi]: The ABI of the contract. + /// Returns: + /// A [ContractFunction] representing the specified function from the smart contract's ABI. /// - /// Returns a ContractFunction. + /// Example: + /// ```dart + /// var function = getContractFunction( + /// 'transfer', + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// ContractAbis.get('ERC20'), + /// ); + /// ``` + /// This method uses the 'function' method of the DeployedContract instance. static ContractFunction getContractFunction( String methodName, EthereumAddress contractAddress, ContractAbi abi) { final instance = DeployedContract(abi, contractAddress); return instance.function(methodName); } - /// Returns a UserOperation to approve the spender of the NFT. + /// Generates a user operation for approving the transfer of an ERC-721 token. + /// + /// Parameters: + /// - `contractAddress`: The [EthereumAddress] of the ERC-721 token contract. + /// - `owner`: The [EthereumAddress] of the token owner. + /// - `spender`: The [EthereumAddress] of the address to grant approval. + /// - `tokenId`: The [BigInt] representing the ID of the token to approve. /// - /// - [contractAddress]: The address of the contract. - /// - [owner]: The address of the owner of the NFT. - /// - [spender]: The address of the spender of the NFT. - static UserOperation nftApproveOperation(EthereumAddress contractAddress, + /// Returns: + /// A [UserOperation] instance for the ERC-721 token approval operation. + /// + /// Example: + /// ```dart + /// var userOperation = nftApproveUserOperation( + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// EthereumAddress.fromHex('0x1234567890abcdef1234567890abcdef12345678'), + /// EthereumAddress.fromHex('0xabcdef1234567890abcdef1234567890abcdef12'), + /// BigInt.from(123), + /// ); + /// ``` + /// This method combines the 'execute' function and 'encodeERC721ApproveCall' to create a user operation. + static UserOperation nftApproveUserOperation(EthereumAddress contractAddress, EthereumAddress owner, EthereumAddress spender, BigInt tokenId) { final innerCallData = execute(owner, to: contractAddress, @@ -241,12 +409,28 @@ class Contract { return UserOperation.partial(callData: innerCallData); } - /// Returns a UserOperation to transfer an NFT. + /// Generates a user operation for transferring an ERC-721 token. + /// + /// Parameters: + /// - `contractAddress`: The [EthereumAddress] of the ERC-721 token contract. + /// - `owner`: The [EthereumAddress] of the current owner of the token. + /// - `recipient`: The [EthereumAddress] of the recipient to receive the token. + /// - `tokenId`: The [BigInt] representing the ID of the token to transfer. + /// + /// Returns: + /// A [UserOperation] instance for the ERC-721 token transfer operation. /// - /// - [contractAddress]: The address of the contract. - /// - [owner]: The address of the owner of the NFT. - /// - [recipient]: The address of the recipient of the NFT. - static UserOperation nftTransferOperation(EthereumAddress contractAddress, + /// Example: + /// ```dart + /// var userOperation = nftTransferUserOperation( + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// EthereumAddress.fromHex('0x1234567890abcdef1234567890abcdef12345678'), + /// EthereumAddress.fromHex('0xabcdef1234567890abcdef1234567890abcdef12'), + /// BigInt.from(123), + /// ); + /// ``` + /// This method combines the 'execute' function and 'encodeERC721SafeTransferCall' to create a user operation. + static UserOperation nftTransferUserOperation(EthereumAddress contractAddress, EthereumAddress owner, EthereumAddress recipient, BigInt tokenId) { final innerCallData = execute(owner, to: contractAddress, @@ -259,15 +443,28 @@ class Contract { return UserOperation.partial(callData: innerCallData); } - /// Returns the UserOperation for an ERC20 approval. + /// Generates a user operation for approving the transfer of ERC-20 tokens. /// - /// - [contractAddress]: The 4337 wallet address. - /// - [owner]: The address of the approved owner. - /// - [spender]: The address of the approved spender. - /// - [amount]: The amount to approve for the spender. + /// Parameters: + /// - `contractAddress`: The [EthereumAddress] of the ERC-20 token contract. + /// - `owner`: The [EthereumAddress] of the token owner. + /// - `spender`: The [EthereumAddress] of the address to grant approval. + /// - `amount`: The [EtherAmount] representing the amount to approve. /// - /// Returns the UserOperation. - static UserOperation tokenApproveOperation( + /// Returns: + /// A [UserOperation] instance for the ERC-20 token approval operation. + /// + /// Example: + /// ```dart + /// var userOperation = tokenApproveUserOperation( + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// EthereumAddress.fromHex('0x1234567890abcdef1234567890abcdef12345678'), + /// EthereumAddress.fromHex('0xabcdef1234567890abcdef1234567890abcdef12'), + /// EtherAmount.inWei(BigInt.from(1000000000000000000)), + /// ); + /// ``` + /// This method combines the 'execute' function and 'encodeERC20ApproveCall' to create a user operation. + static UserOperation tokenApproveUserOperation( EthereumAddress contractAddress, EthereumAddress owner, EthereumAddress spender, @@ -280,16 +477,32 @@ class Contract { return UserOperation.partial(callData: callData); } - /// Returns the UserOperation for an ERC20 transfer. + /// Generates a user operation for transferring ERC-20 tokens. + /// + /// Parameters: + /// - `contractAddress`: The [EthereumAddress] of the ERC-20 token contract. + /// - `owner`: The [EthereumAddress] of the current owner of the tokens. + /// - `recipient`: The [EthereumAddress] of the recipient to receive the tokens. + /// - `amount`: The [EtherAmount] representing the amount of tokens to transfer. /// - /// - [contractAddress]: The 4337 wallet address. - /// - [owner]: The address of the sender. - /// - [recipient]: The address of the recipient. - /// - [amount]: The amount to transfer. + /// Returns: + /// A [UserOperation] instance for the ERC-20 token transfer operation. /// - /// Returns the UserOperation. - static UserOperation tokenTransferOperation(EthereumAddress contractAddress, - EthereumAddress owner, EthereumAddress recipient, EtherAmount amount) { + /// Example: + /// ```dart + /// var userOperation = tokenTransferUserOperation( + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// EthereumAddress.fromHex('0x1234567890abcdef1234567890abcdef12345678'), + /// EthereumAddress.fromHex('0xabcdef1234567890abcdef1234567890abcdef12'), + /// EtherAmount.inWei(BigInt.from(1000000000000000000)), + /// ); + /// ``` + /// This method combines the 'execute' function and 'encodeERC20TransferCall' to create a user operation. + static UserOperation tokenTransferUserOperation( + EthereumAddress contractAddress, + EthereumAddress owner, + EthereumAddress recipient, + EtherAmount amount) { final callData = execute(owner, to: contractAddress, innerCallData: diff --git a/lib/src/common/plugins.dart b/lib/src/common/plugins.dart index 8897efa..c184bb7 100644 --- a/lib/src/common/plugins.dart +++ b/lib/src/common/plugins.dart @@ -8,17 +8,40 @@ mixin _PluginManager { return _plugins.keys.toList(growable: false); } - ///gets a [plugin] by name + /// Gets a plugin by name. + /// + /// Parameters: + /// - `name`: The name of the plugin to retrieve. + /// + /// Returns: + /// The plugin with the specified name. T plugin(String name) { return _plugins[name] as T; } - /// removes an unwanted plugin by [name] + /// Removes an unwanted plugin by name. + /// + /// Parameters: + /// - `name`: The name of the plugin to remove. + /// + /// Example: + /// ```dart + /// removePlugin('logger'); + /// ``` void removePlugin(String name) { _plugins.remove(name); } - ///adds a [plugin] by name + /// Adds a plugin by name. + /// + /// Parameters: + /// - `name`: The name of the plugin to add. + /// - `module`: The instance of the plugin. + /// + /// Example: + /// ```dart + /// addPlugin('logger', Logger()); + /// ``` void addPlugin(String name, T module) { _plugins[name] = module; } diff --git a/lib/src/common/uint256.dart b/lib/src/common/uint256.dart index 9d26097..521582e 100644 --- a/lib/src/common/uint256.dart +++ b/lib/src/common/uint256.dart @@ -5,16 +5,49 @@ class Uint256 implements Uint256Base { final BigInt _value; - Uint256(this._value); - + const Uint256(this._value); + + /// Creates a [Uint256] instance from a hexadecimal string [hex]. + /// + /// Example: + /// ```dart + /// final value = Uint256.fromHex('0x1a'); // Creates Uint256 with value 26 + /// ``` factory Uint256.fromHex(String hex) { return Uint256(hexToInt(hex)); } + /// Creates a [Uint256] instance from an [EtherAmount] value [inWei]. + /// + /// Example: + /// ```dart + /// final amount = EtherAmount.inWei(BigInt.from(5)).getInWei; + /// final value = Uint256.fromWei(amount); // Creates Uint256 with value 5 + /// ``` factory Uint256.fromWei(EtherAmount inWei) { return Uint256(inWei.getInWei); } + /// Creates a [Uint256] instance from an integer value [value]. + /// + /// Example: + /// ```dart + /// final value = Uint256.fromInt(42); // Creates Uint256 with value 42 + /// ``` + factory Uint256.fromInt(int value) { + return Uint256(BigInt.from(value)); + } + + /// Creates a [Uint256] instance from a string representation of a number [value]. + /// + /// Example: + /// ```dart + /// final value = Uint256.fromString('123'); // Creates Uint256 with value 123 + /// ``` + factory Uint256.fromString(String value) { + return Uint256(BigInt.parse(value)); + } + @override BigInt get value => _value; diff --git a/lib/src/interfaces/account_factory.dart b/lib/src/interfaces/account_factory.dart index eb6b6f3..1e65397 100644 --- a/lib/src/interfaces/account_factory.dart +++ b/lib/src/interfaces/account_factory.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/interfaces.dart'; +part of 'interfaces.dart'; /// An abstract class representing the contract interface for an Ethereum Account Factory. /// diff --git a/lib/src/interfaces/bundler_provider.dart b/lib/src/interfaces/bundler_provider.dart index f2bb2c8..c691401 100644 --- a/lib/src/interfaces/bundler_provider.dart +++ b/lib/src/interfaces/bundler_provider.dart @@ -1,31 +1,58 @@ -part of 'package:variance_dart/interfaces.dart'; +part of 'interfaces.dart'; /// Abstract base class representing a provider for interacting with an entrypoint. /// /// Implementations of this class are expected to provide functionality for interacting specifically /// with bundlers and provides methods for sending user operations to an entrypoint. abstract class BundlerProviderBase { - /// Estimates the gas cost for a user operation. + /// Asynchronously estimates the gas cost for a user operation using the provided data and entrypoint. /// - /// - [userOp]: The user operation. - /// - [entrypoint]: The entrypoint address through which the operation should pass. + /// Parameters: + /// - `userOp`: A map containing the user operation data. + /// - `entrypoint`: The [EthereumAddress] representing the entrypoint for the operation. /// - /// Returns a [Future] that completes with a [UserOperationGas] object. + /// Returns: + /// A [Future] that completes with a [UserOperationGas] instance representing the estimated gas values. + /// + /// Example: + /// ```dart + /// var gasEstimation = await estimateUserOperationGas( + /// myUserOp, // Map + /// EthereumAddress.fromHex('0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789'), + /// ); + /// ``` + /// This method uses the bundled RPC to estimate the gas cost for the provided user operation data. Future estimateUserOperationGas( Map userOp, EthereumAddress entrypoint); - /// Retrieves a user operation object associated with a userOpHash. + /// Asynchronously retrieves information about a user operation using its hash. + /// + /// Parameters: + /// - `userOpHash`: The hash of the user operation to retrieve information for. /// - /// - [userOpHash]: The hashed string of the user operation. + /// Returns: + /// A [Future] that completes with a [UserOperationByHash] instance representing the details of the user operation. /// - /// Returns a [Future] that completes with a [UserOperationByHash] object. + /// Example: + /// ```dart + /// var userOpDetails = await getUserOperationByHash('0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'); + /// ``` + /// This method uses the bundled RPC to fetch information about the specified user operation using its hash. Future getUserOperationByHash(String userOpHash); - /// Retrieves a user operation receipt associated with a userOpHash. + /// Asynchronously retrieves the receipt of a user operation using its hash. /// - /// - [userOpHash]: The hashed string of the user operation. + /// Parameters: + /// - `userOpHash`: The hash of the user operation to retrieve the receipt for. /// - /// Returns a [Future] that completes with a [UserOperationReceipt] object. + /// Returns: + /// A [Future] that completes with a [UserOperationReceipt] instance representing the receipt of the user operation. + /// + /// Example: + /// ```dart + /// var userOpReceipt = await getUserOpReceipt('0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'); + /// ``` + /// This method uses the bundled RPC to fetch the receipt of the specified user operation using its hash. Future getUserOpReceipt(String userOpHash); /// Initializes the provider with an entrypoint. @@ -33,24 +60,51 @@ abstract class BundlerProviderBase { /// - [ep]: The entrypoint to initialize with. void initializeWithEntrypoint(Entrypoint ep); - /// Sends a user operation to the given network. + /// Asynchronously sends a user operation to the bundler for execution. + /// + /// Parameters: + /// - `userOp`: A map containing the user operation data. + /// - `entrypoint`: The [EthereumAddress] representing the entrypoint for the operation. /// - /// - [userOp]: The user operation. - /// - [entrypoint]: The entrypoint address through which the operation should pass. + /// Returns: + /// A [Future] that completes with a [UserOperationResponse] containing information about the executed operation. /// - /// Returns a [Future] that completes with a [UserOperationResponse] object. + /// Example: + /// ```dart + /// var response = await sendUserOperation( + /// myUserOp, // Map + /// EthereumAddress.fromHex('0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789'), + /// ); + /// ``` + /// This method uses the bundled RPC to send the specified user operation for execution and returns the response. Future sendUserOperation( Map userOp, EthereumAddress entrypoint); - /// Returns a list of supported entrypoints for the bundler. + /// Asynchronously retrieves a list of supported entrypoints from the bundler. /// - /// Returns a [Future] that completes with a list of supported entrypoints. + /// Returns: + /// A [Future] that completes with a list of supported entrypoints as strings. + /// + /// Example: + /// ```dart + /// var entrypoints = await supportedEntryPoints(); + /// ``` Future> supportedEntryPoints(); - /// Waits for a specified duration and returns a [FilterEvent] based on an event emitted by the smart contract. + /// Asynchronously waits for a FilterEvent within a specified time duration based on an event emmitted by entrypoint. + /// Used to wait for [UserOperation] to complete. + /// + /// Parameters: + /// - `millisecond`: The time duration, in milliseconds, to wait for a FilterEvent. Defaults to `0`. /// - /// - [millisecond]: The duration to wait in milliseconds. + /// Returns: + /// A [Future] that completes with a [FilterEvent] if one is found within the specified duration, otherwise, returns `null`. /// - /// Returns a [Future] that completes with a [FilterEvent] or `null`. + /// Example: + /// ```dart + /// var filterEvent = await wait(millisecond: 5000); + /// ``` + /// This method waits for a FilterEvent related to the 'UserOperationEvent' within the given time duration. + Future wait({int millisecond}); } diff --git a/lib/src/interfaces/credential_interface.dart b/lib/src/interfaces/credential_interface.dart deleted file mode 100644 index 8d0c72e..0000000 --- a/lib/src/interfaces/credential_interface.dart +++ /dev/null @@ -1,34 +0,0 @@ -part of 'package:variance_dart/interfaces.dart'; - -/// An interface for basic credentials that can sign messages. -/// -/// This interface defines the basic interface for wallet credential type that can -/// be used to sign messages in the Ethereum context. -abstract class CredentialInterface extends MultiSignerInterface { - /// The Ethereum address associated with this credential. - EthereumAddress get address; - - /// The public key associated with this credential. - Uint8List get publicKey; - - /// Signs a message hash using the credential. - /// - /// The [index] and [id] parameters are ignored in this implementation. - /// - /// Returns a [Uint8List] representing the signature of the provided [hash]. - @override - Future personalSign(Uint8List hash, {int? index, String? id}); - - /// Signs a message hash and returns the ECDSA signature. - /// - /// The [index] and [id] parameters are ignored in this implementation. - /// - /// Returns a [MsgSignature] representing the ECDSA signature of the provided [hash]. - @override - Future signToEc(Uint8List hash, {int? index, String? id}); - - /// Converts the credential to a JSON representation. - /// - /// Returns a JSON-encoded string representing the credential. - String toJson(); -} diff --git a/lib/src/interfaces/ens_resolver.dart b/lib/src/interfaces/ens_resolver.dart index 6cec77e..ac757e6 100644 --- a/lib/src/interfaces/ens_resolver.dart +++ b/lib/src/interfaces/ens_resolver.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/interfaces.dart'; +part of 'interfaces.dart'; /// Abstract base class for handling Ethereum Name Service (ENS) resolution. /// diff --git a/lib/src/interfaces/hd_interface.dart b/lib/src/interfaces/hd_interface.dart index 4774724..d8dedba 100644 --- a/lib/src/interfaces/hd_interface.dart +++ b/lib/src/interfaces/hd_interface.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/interfaces.dart'; +part of 'interfaces.dart'; /// An interface for hierarchical deterministic (HD) wallets. /// @@ -6,29 +6,42 @@ part of 'package:variance_dart/interfaces.dart'; /// allowing the creation of accounts, exporting mnemonic phrases, exporting /// private keys, signing messages, and more. abstract class HDInterface extends MultiSignerInterface { - /// Adds a new account to the HD wallet. + /// Adds an Ethereum account derived from the HD wallet using the specified [index]. /// - /// - [index]: The index of the account. + /// Parameters: + /// - [index]: The index used to derive the Ethereum account. /// - /// Returns the Ethereum address of the new account. + /// Returns the Ethereum address associated with the specified index. + /// + /// Example: + /// ```dart + /// final walletSigner = HDWalletSigner.recoverAccount('mnemonic phrase'); + /// final newAccount = walletSigner.addAccount(1); + /// ``` EthereumAddress addAccount(int index); - /// Exports the mnemonic phrase associated with the HD wallet. + /// Exports the mnemonic phrase associated with the HD wallet signer. + /// + /// Returns the mnemonic phrase. /// - /// Returns the mnemonic phrase as a [String]. + /// Example: + /// ```dart + /// final walletSigner = HDWalletSigner.recoverAccount('mnemonic phrase'); + /// final exportedMnemonic = walletSigner.exportMnemonic(); + /// ``` String? exportMnemonic(); - /// Exports the private key of an account from the HD wallet. - /// - /// - [index]: The index of the account. + /// Exports the private key associated with the Ethereum account derived from the HD wallet using the specified [index]. /// - /// Returns the private key as a [String]. - String exportPrivateKey(int index); - - /// Retrieves the Ethereum address of an account from the HD wallet. + /// Parameters: + /// - [index]: The index used to derive the Ethereum account. /// - /// - [index]: The index of the account. + /// Returns the exported private key as a hexadecimal string. /// - /// Returns the Ethereum address. - EthereumAddress getEthereumAddress({int index = 0}); + /// Example: + /// ```dart + /// final walletSigner = HDWalletSigner.recoverAccount('mnemonic phrase'); + /// final exportedPrivateKey = walletSigner.exportPrivateKey(1); + /// ``` + String exportPrivateKey(int index); } diff --git a/lib/src/interfaces/interfaces.dart b/lib/src/interfaces/interfaces.dart new file mode 100644 index 0000000..2e4fb50 --- /dev/null +++ b/lib/src/interfaces/interfaces.dart @@ -0,0 +1,43 @@ +library interfaces; + +import 'dart:typed_data'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:local_auth_android/local_auth_android.dart'; +import 'package:local_auth_ios/local_auth_ios.dart'; +import 'package:web3dart/crypto.dart'; +import 'package:web3dart/json_rpc.dart' show RpcService; +import 'package:web3dart/web3dart.dart'; + +import '../../utils.dart' + show + SSAuthOperationOptions, + ChainBaseApiBase, + CredentialType, + SecureStorageMiddleware; +import '../../variance.dart' + show + Chain, + PassKeyPair, + PassKeySignature, + PassKeysOptions, + Uint256, + UserOperation, + UserOperationByHash, + UserOperationGas, + UserOperationReceipt, + UserOperationResponse; +import '../abis/abis.dart' show Entrypoint; + +part 'account_factory.dart'; +part 'bundler_provider.dart'; +part 'ens_resolver.dart'; +part 'hd_interface.dart'; +part 'local_authentication.dart'; +part 'multi_signer_interface.dart'; +part 'passkey_interface.dart'; +part 'rpc_provider.dart'; +part 'secure_storage_repository.dart'; +part 'smart_wallet.dart'; +part 'uint256_interface.dart'; +part 'user_operations.dart'; diff --git a/lib/src/interfaces/local_authentication.dart b/lib/src/interfaces/local_authentication.dart index 1fca363..5bd8a22 100644 --- a/lib/src/interfaces/local_authentication.dart +++ b/lib/src/interfaces/local_authentication.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/interfaces.dart'; +part of 'interfaces.dart'; /// An abstract class representing authentication operations. /// diff --git a/lib/src/interfaces/multi_signer_interface.dart b/lib/src/interfaces/multi_signer_interface.dart index 8d52979..8b0edce 100644 --- a/lib/src/interfaces/multi_signer_interface.dart +++ b/lib/src/interfaces/multi_signer_interface.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/interfaces.dart'; +part of 'interfaces.dart'; /// An interface for a multi-signer, allowing signing of data and returning the result. /// @@ -12,46 +12,63 @@ abstract class MultiSignerInterface { /// You must specify a dummy signature that matches your transaction signature standard. String dummySignature = "0x"; - /// Returns the Hex address associated with the signer. + /// Generates an Ethereum address from the key at the specified [index]. /// - /// Optional parameters: - /// - [index]: The index or position of the signer. Used for multi-signature scenarios. - /// - [id]: An optional identifier associated with the signing process. + /// Parameters: + /// - [index]: The index to determine which key to use for address generation. Defaults to 0. + /// - [bytes]: Optional bytes for key generation. If not provided, it defaults to `null`. /// - /// hex string length is 40+2 for Ethereum addresses and 64+2 Passkey compliant addresses - /// - /// Returns a [String] representing a valid hex string. + /// Example: + /// ```dart + /// final address = getAddress(); + /// ``` String getAddress({int index = 0, bytes}); - /// Signs the provided [hash] using a multi-signature process. - /// - /// - [Uint8List]: The data type of signature expected. + /// Signs the provided [hash] using the personal sign method. /// - /// Optional parameters: - /// - [index]: The index or position of the signer. Used for multi-signature scenarios. - /// - [id]: An optional identifier associated with the signing process. + /// Parameters: + /// - [hash]: The hash to be signed. + /// - [index]: The optional index to specify which privatekey to use for signing (required for HD wallets). If not provided, it defaults to `null`. + /// - [id]: The optional identifier for the signing key. If not provided, it defaults to `null`. Required for passkey signers. /// - /// Returns a Future representing the signed data. + /// Example: + /// ```dart + /// final hashToSign = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); + /// final signature = await personalSign(hashToSign, index: 0, id: 'credentialId'); // credentialId is only required for passkey signers + /// ``` + Future personalSign(Uint8List hash, {int? index, String? id}); - /// Signs the provided [hash] and returns the result as a [MsgSignature]. + /// Signs the provided [hash] using elliptic curve (EC) signatures and returns the r and s values. /// - /// Optional parameters: - /// - [index]: The index or position of the signer. Used for multi-signature scenarios. - /// - [id]: An optional identifier associated with the signing process. + /// Parameters: + /// - [hash]: The hash to be signed. + /// - [index]: The optional index to specify which key to use for signing. If not provided, it defaults to `null`. + /// - [id]: The optional identifier for the signing key. If not provided, it defaults to `null`. Required for passkey signers. /// - /// Returns a `Future` representing the r and s values. + /// Example: + /// ```dart + /// final hashToSign = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); + /// final signature = await signToEc(hashToSign, index: 0, id: 'credentialId'); + /// ``` Future signToEc(Uint8List hash, {int? index, String? id}); } mixin SecureStorageMixin { - /// Wraps a [SecureStorage] instance with additional middleware. + /// Creates a `SecureStorageMiddleware` instance with the provided [FlutterSecureStorage]. /// - /// The [secureStorage] parameter represents the underlying secure storage - /// implementation to be enhanced. The [authMiddleware] parameter, when provided, - /// allows incorporating authentication features into secure storage operations. + /// Parameters: + /// - [secureStorage]: The FlutterSecureStorage instance to be used for secure storage. + /// - [authMiddleware]: Optional authentication middleware. Defaults to `null`. /// - /// Returns a new instance of [SecureStorage] with the specified middleware. - SecureStorageMiddleware withSecureStorage(SecureStorage secureStorage, + /// Example: + /// ```dart + /// final flutterSecureStorage = FlutterSecureStorage(); + /// final secureStorageMiddleware = this.withSecureStorage( + /// flutterSecureStorage, + /// authMiddleware: myAuthMiddleware, + /// ); + /// ``` + SecureStorageMiddleware withSecureStorage(FlutterSecureStorage secureStorage, {Authentication? authMiddleware}); } diff --git a/lib/src/interfaces/passkey_interface.dart b/lib/src/interfaces/passkey_interface.dart index 979ed27..7f8d700 100644 --- a/lib/src/interfaces/passkey_interface.dart +++ b/lib/src/interfaces/passkey_interface.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/interfaces.dart'; +part of 'interfaces.dart'; abstract class PasskeyInterface extends MultiSignerInterface { /// Gets the PassKeysOptions used by the PasskeyInterface. @@ -7,49 +7,95 @@ abstract class PasskeyInterface extends MultiSignerInterface { /// Gets the default credential ID used by the Passkey. String? get defaultId; - /// Creates a client data hash for PassKeys authentication. + /// Generates the client data hash for the given [PassKeysOptions] and optional challenge. /// - /// - [options]: The PassKeysOptions for the client data hash. - /// - [challenge]: A random challenge for the client data hash. + /// Parameters: + /// - [options]: PassKeysOptions containing the authentication options. + /// - [challenge]: Optional challenge value. Defaults to a randomly generated challenge if not provided. /// - /// Returns a [Uint8List] representing the client data hash. + /// Returns the Uint8List representation of the client data hash. + /// + /// Example: + /// ```dart + /// final passKeysOptions = PassKeysOptions(type: 'webauthn', origin: 'https://example.com'); + /// final clientDataHash = clientDataHash(passKeysOptions); + /// ``` Uint8List clientDataHash(PassKeysOptions options, {String? challenge}); - /// Creates a 32-byte client data hash for PassKeys authentication. + /// Generates the 32-byte client data hash for the given [PassKeysOptions] and optional challenge. + /// + /// Parameters: + /// - [options]: PassKeysOptions containing the authentication options. + /// - [challenge]: Optional challenge value. Defaults to a randomly generated challenge if not provided. /// - /// - [options]: The PassKeysOptions for the client data hash. - /// - [challenge]: A random challenge for the client data hash. + /// Returns the Uint8List representation of the 32-byte client data hash. /// - /// Returns a [Uint8List] representing the 32-byte client data hash. + /// Example: + /// ```dart + /// final passKeysOptions = PassKeysOptions(type: 'webauthn', origin: 'https://example.com'); + /// final clientDataHash32 = clientDataHash32(passKeysOptions); + /// ``` Uint8List clientDataHash32(PassKeysOptions options, {String? challenge}); - /// Converts a credentialId to a 32-byte hex string. + /// Converts a List credentialId to a hex string representation with a length of 32 bytes. /// - /// - [credentialId]: The credentialId to convert. + /// Parameters: + /// - [credentialId]: List of integers representing the credentialId. /// - /// Returns a 32-byte hex string. + /// Returns the hex string representation of the credentialId padded to 32 bytes. + /// + /// Example: + /// ```dart + /// final credentialId = [1, 2, 3]; + /// final hexString = credentialIdToBytes32Hex(credentialId); + /// ``` String credentialIdToBytes32Hex(List credentialId); - /// Gets the messaging signature from the PassKeys authentication response. + /// Parses ASN1-encoded signature bytes and returns a List of two hex strings representing the `r` and `s` values. + /// + /// Parameters: + /// - [signatureBytes]: Uint8List containing the ASN1-encoded signature bytes. /// - /// - [signatureBytes]: The base64 encoded signature. + /// Returns a Future> containing hex strings for `r` and `s` values. /// - /// Returns a [List] containing [String] values representing 'r' and 's'. + /// Example: + /// ```dart + /// final signatureBytes = Uint8List.fromList([48, 68, 2, 32, ...]); + /// final signatureHexValues = await getMessagingSignature(signatureBytes); + /// ``` Future> getMessagingSignature(Uint8List signatureBytes); - /// Registers a user and returns a PassKeyPair key pair. + /// Registers a new PassKeyPair. /// - /// - [name]: The user name. - /// - [requiresUserVerification]: True if user verification is required. + /// Parameters: + /// - [name]: The name associated with the PassKeyPair. + /// - [requiresUserVerification]: A boolean indicating whether user verification is required. /// - /// Returns a [PassKeyPair]. + /// Returns a Future representing the registered PassKeyPair. + /// + /// Example: + /// ```dart + /// final pkps = PassKeySigner("example", "example.com", "https://example.com"); + /// final passKeyPair = await pkps.register('geffy', true); + /// ``` Future register(String name, bool requiresUserVerification); - /// Signs the intended request and returns the signedMessage. + /// Signs a hash using the PassKeyPair associated with the given credentialId. + /// + /// Parameters: + /// - [hash]: The hash to be signed. + /// - [credentialId]: The credentialId associated with the PassKeyPair. + /// + /// Returns a Future representing the PassKeySignature of the signed hash. /// - /// - [hash]: The hash of the intended request. - /// - [credentialId]: The credential id. + /// Example: + /// ```dart + /// final hash = Uint8List.fromList([/* your hash bytes here */]); + /// final credentialId = 'your_credential_id'; /// - /// Returns a [PassKeySignature]. - Future signToPasskeySignature(Uint8List hash, String credentialId); + /// final pkps = PassKeySigner("example", "example.com", "https://example.com"); + /// final passKeySignature = await pkps.signToPasskeySignature(hash, credentialId); + /// ``` + Future signToPasskeySignature( + Uint8List hash, String credentialId); } diff --git a/lib/src/interfaces/rpc_provider.dart b/lib/src/interfaces/rpc_provider.dart index 88f2a1a..3aff221 100644 --- a/lib/src/interfaces/rpc_provider.dart +++ b/lib/src/interfaces/rpc_provider.dart @@ -1,52 +1,92 @@ -part of 'package:variance_dart/interfaces.dart'; +part of 'interfaces.dart'; /// Abstract base class for interacting with an Bundler RPC provider. /// -/// Implementations of this class are expected to provide functionality for specifically interacting +/// Implementations of this class are expected to provide functionality for specifically interacting /// with bundlers only. abstract class RPCProviderBase implements RpcService { - /// Estimates the gas cost of a transaction. + /// Asynchronously estimates the gas cost for a transaction to the specified address with the given calldata. /// - /// - [to]: The address or contract to which the transaction is to be sent. - /// - [calldata]: The calldata of the transaction. + /// Parameters: + /// - `to`: The [EthereumAddress] of the transaction recipient. + /// - `calldata`: The ABI-encoded data for the transaction. /// - /// Returns the estimated gas cost in wei. + /// Returns: + /// A [Future] that completes with a [BigInt] representing the estimated gas cost. + /// + /// Example: + /// ```dart + /// var gasEstimation = await estimateGas( + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// '0x0123456789abcdef', + /// ); + /// ``` + /// This method uses an ethereum jsonRPC to estimate the gas cost for the specified transaction. Future estimateGas( EthereumAddress to, String calldata, ); - /// Returns the current block number. + /// Asynchronously retrieves the current block number from the Ethereum node. + /// + /// Returns: + /// A [Future] that completes with an [int] representing the current block number. /// - /// Returns a [Future] that completes with the current block number. + /// Example: + /// ```dart + /// var blockNumber = await getBlockNumber(); + /// ``` + /// This method uses an ethereum jsonRPC to fetch the current block number from the Ethereum node. Future getBlockNumber(); - /// Returns the EIP1559 gas price in wei for a network. + /// Asynchronously retrieves the EIP-1559 gas prices, including `maxFeePerGas` and `maxPriorityFeePerGas`. /// - /// Returns a [Future] that completes with a [Map] containing the following keys: + /// Returns: + /// A [Future] that completes with a [Map] containing the gas prices in [EtherAmount]. /// - /// - `'maxFeePerGas'`: An [EtherAmount] representing the maximum fee per gas. - /// - `'maxPriorityFeePerGas'`: An [EtherAmount] representing the maximum priority fee per gas. + /// Example: + /// ```dart + /// var gasPrices = await getEip1559GasPrice(); + /// ``` + /// This method uses an ethereum jsonRPC to fetch EIP-1559 gas prices from the Ethereum node. Future> getEip1559GasPrice(); - /// Returns the gas price in wei for a network. + /// Asynchronously retrieves the gas prices, supporting both EIP-1559 and legacy gas models. /// - /// Returns a [Future] that completes with a [Map] containing the following keys: + /// Returns: + /// A [Future] that completes with a [Map] containing the gas prices in [EtherAmount]. /// - /// - `'maxFeePerGas'`: An [EtherAmount] representing the maximum fee per gas. - /// - `'maxPriorityFeePerGas'`: An [EtherAmount] representing the maximum priority fee per gas. + /// Example: + /// ```dart + /// var gasPrices = await getGasPrice(); + /// ``` + /// This method first attempts to fetch EIP-1559 gas prices and falls back to legacy gas prices if it fails. Future> getGasPrice(); - /// Returns the legacy gas price in wei for a network. + /// Asynchronously retrieves the legacy gas price from the Ethereum node. /// - /// Returns a [Future] that completes with an [EtherAmount] representing the legacy gas price. + /// Returns: + /// A [Future] that completes with an [EtherAmount] representing the legacy gas price in [Wei]. + /// + /// Example: + /// ```dart + /// var legacyGasPrice = await getLegacyGasPrice(); + /// ``` + /// This method uses an ethereum jsonRPC to fetch the legacy gas price from the Ethereum node. Future getLegacyGasPrice(); - /// Sends a transaction to the bundler RPC. + /// Asynchronously sends an RPC call to the Ethereum node for the specified function and parameters. + /// + /// Parameters: + /// - `function`: The Ethereum RPC function to call. eg: `eth_getBalance` + /// - `params`: Optional parameters for the RPC call. /// - /// - [function]: The method to call. - /// - [params]: The parameters for the request (optional). + /// Returns: + /// A [Future] that completes with the result of the RPC call. /// - /// Returns a [Future] that completes with an RPC response of a generic type [T]. + /// Example: + /// ```dart + /// var result = await send('eth_getBalance', ['0x9876543210abcdef9876543210abcdef98765432']); + /// ``` Future send(String function, [List? params]); } diff --git a/lib/src/interfaces/secure_storage_repository.dart b/lib/src/interfaces/secure_storage_repository.dart index 59ee259..ad7d881 100644 --- a/lib/src/interfaces/secure_storage_repository.dart +++ b/lib/src/interfaces/secure_storage_repository.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/interfaces.dart'; +part of 'interfaces.dart'; /// A repository for secure storage operations. /// @@ -10,69 +10,140 @@ part of 'package:variance_dart/interfaces.dart'; /// for these methods based on the specific secure storage mechanism they intend /// to use. abstract class SecureStorageRepository { - /// Saves a key-value pair in the secure storage. + /// Saves a key-value pair to the secure storage. /// - /// The [key] parameter represents the identifier for the stored value, and - /// the [value] parameter is the data to be stored. The [options] parameter - /// encapsulates authentication and other operation-specific options. + /// Parameters: + /// - [key]: The key under which to store the value. + /// - [value]: The value to be stored. + /// - [options]: Options for the secure storage operation, including authentication requirements. /// - /// Throws an exception if the operation fails. + /// Throws a [SecureStorageAuthMiddlewareError] if authentication is required but no authentication middleware is provided. + /// + /// Example: + /// ```dart + /// final saveKey = 'myKey'; + /// final saveValue = 'myValue'; + /// final saveOptions = SSAuthOperationOptions( + /// requiresAuth: true, + /// authReason: 'Authenticate to save the key-value pair.', + /// ssNameSpace: 'myNamespace', + /// ); + /// await save(saveKey, saveValue, options: saveOptions); + /// print('Key-value pair saved successfully.'); + /// ``` Future save(String key, String value, {SSAuthOperationOptions? options}); - /// Saves a credential of the specified type. + /// Saves a credential to the secure storage for a specified [CredentialType]. + /// + /// Parameters: + /// - [type]: The type of credential to be saved. + /// - [options]: Options for the secure storage operation, including authentication requirements. /// - /// The [type] parameter represents the type of credential to be saved. - /// The [type] is an enum that defines three types of credentials: `mnemonic`, - /// `privateKey`, and `passkeypair`. The [options] parameter encapsulates - /// authentication and other operation-specific options. + /// Throws a [SecureStorageAuthMiddlewareError] if authentication is required but no authentication middleware is provided. /// - /// Throws an exception if the operation fails. + /// Example: + /// ```dart + /// final saveCredentialType = CredentialType.exampleCredential; + /// final saveOptions = SSAuthOperationOptions( + /// requiresAuth: true, + /// authReason: 'Authenticate to save the credential.', + /// ssNameSpace: 'myNamespace', + /// ); + /// await saveCredential(saveCredentialType, options: saveOptions); + /// print('Credential saved successfully.'); + /// ``` Future saveCredential(CredentialType type, {SSAuthOperationOptions? options}); - /// Reads the value associated with the given key from secure storage. + /// Reads a value from the secure storage. /// - /// The [key] parameter represents the identifier for the value to be read. - /// The [options] parameter encapsulates authentication and other - /// operation-specific options. + /// Parameters: + /// - [key]: The key for the value to be read. + /// - [options]: Options for the secure storage operation, including authentication requirements. /// - /// Returns the stored value if found, otherwise returns `null`. + /// Throws a [SecureStorageAuthMiddlewareError] if authentication is required but no authentication middleware is provided. /// - /// Throws an exception if the operation fails. + /// Returns the value associated with the provided key, or `null` if the key is not found. + /// + /// Example: + /// ```dart + /// final keyToRead = 'exampleKey'; + /// final readOptions = SSAuthOperationOptions( + /// requiresAuth: true, + /// authReason: 'Authenticate to read the key.', + /// ssNameSpace: 'myNamespace', + /// ); + /// final storedValue = await read(keyToRead, options: readOptions); + /// print('Stored value: $storedValue'); + /// ``` Future read(String key, {SSAuthOperationOptions? options}); - /// Reads a credential of the specified type. + /// Reads a credential from the secure storage. + /// + /// Parameters: + /// - [type]: The type of credential to be read. + /// - [options]: Options for the secure storage operation, including authentication requirements. /// - /// The [type] parameter represents the type of credential to be read. - /// The [type] is an enum that defines three types of credentials: `mnemonic`, - /// `privateKey`, and `passkeypair`. The [options] parameter encapsulates - /// authentication and other operation-specific options. + /// Throws a [SecureStorageAuthMiddlewareError] if authentication is required but no authentication middleware is provided. /// - /// Returns the stored credential if found, otherwise returns `null`. + /// Returns the credential associated with the provided type, or `null` if the credential is not found. /// - /// Throws an exception if the operation fails. + /// Example: + /// ```dart + /// final credentialType = CredentialType.hdwallet; + /// final readOptions = SSAuthOperationOptions( + /// requiresAuth: true, + /// authReason: 'Authenticate to read the credential.', + /// ssNameSpace: 'myNamespace', + /// ); + /// final storedCredential = await readCredential(credentialType, options: readOptions); + /// print('Stored credential: $storedCredential'); + /// ``` Future readCredential(CredentialType type, {SSAuthOperationOptions? options}); - /// Updates the value associated with the given key in secure storage. + /// Updates the value of an existing key in the secure storage. /// - /// The [key] parameter represents the identifier for the value to be updated, - /// and the [value] parameter is the new data to be stored. The [options] - /// parameter encapsulates authentication and other operation-specific options. + /// Parameters: + /// - [key]: The key for which to update the value. + /// - [value]: The new value to be stored. + /// - [options]: Options for the secure storage operation, including authentication requirements. /// - /// Throws an exception if the operation fails. + /// Throws a [SecureStorageAuthMiddlewareError] if authentication is required but no authentication middleware is provided. + /// + /// Example: + /// ```dart + /// final updateKey = 'myKey'; + /// final updateValue = 'newValue'; + /// final updateOptions = SSAuthOperationOptions( + /// requiresAuth: true, + /// authReason: 'Authenticate to update the key value.', + /// ssNameSpace: 'myNamespace', + /// ); + /// await update(updateKey, updateValue, options: updateOptions); + /// print('Key updated successfully.'); + /// ``` Future update(String key, String value, {SSAuthOperationOptions? options}); - /// Deletes the key-value pair associated with the given key from secure storage. + /// Deletes a key from the secure storage. + /// + /// Parameters: + /// - [key]: The key to be deleted. + /// - [options]: Options for the secure storage operation, including authentication requirements. /// - /// The [key] parameter represents the identifier for the value to be deleted. - /// The [options] parameter encapsulates authentication and other - /// operation-specific options. + /// Throws a [SecureStorageAuthMiddlewareError] if authentication is required but no authentication middleware is provided. /// - /// Throws an exception if the operation fails. + /// Example: + /// ```dart + /// final keyToDelete = 'exampleKey'; + /// final deleteOptions = SSAuthOperationOptions( + /// requiresAuth: true, + /// authReason: 'Authenticate to delete the key.', + /// ssNameSpace: 'myNamespace', + /// ); + /// await delete(keyToDelete, options: deleteOptions); + /// ``` Future delete(String key, {SSAuthOperationOptions? options}); } - -abstract class SecureStorage implements FlutterSecureStorage {} diff --git a/lib/src/interfaces/smart_wallet.dart b/lib/src/interfaces/smart_wallet.dart index 1a21b8e..cd96b91 100644 --- a/lib/src/interfaces/smart_wallet.dart +++ b/lib/src/interfaces/smart_wallet.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/interfaces.dart'; +part of 'interfaces.dart'; /// An abstract class representing the base structure of a Smart Wallet. /// @@ -31,10 +31,29 @@ abstract class SmartWalletBase { /// Sets the smart wallet address for this account; set setWalletAddress(EthereumAddress address); - /// Builds a [UserOperation] based on provided parameters. + /// Builds a [UserOperation] instance with the specified parameters. /// - /// This method creates a [UserOperation] with the given call data and optional parameters. - /// The resulting [UserOperation] can be used for various operations on the Smart Wallet. + /// Parameters: + /// - `callData` (required): The call data as a [Uint8List]. + /// - `customNonce`: An optional custom nonce value. + /// - `callGasLimit`: An optional custom call gas limit as a [BigInt]. + /// - `verificationGasLimit`: An optional custom verification gas limit as a [BigInt]. + /// - `preVerificationGas`: An optional custom pre-verification gas as a [BigInt]. + /// - `maxFeePerGas`: An optional custom maximum fee per gas as a [BigInt]. + /// - `maxPriorityFeePerGas`: An optional custom maximum priority fee per gas as a [BigInt]. + /// + /// Returns: + /// A [UserOperation] instance with the specified parameters. + /// + /// Example: + /// ```dart + /// var userOperation = buildUserOperation( + /// callData: Uint8List(0xabcdef), + /// customNonce: BigInt.from(42), + /// callGasLimit: BigInt.from(20000000), + /// // Other optional parameters can be provided as needed. + /// ); + /// ``` UserOperation buildUserOperation({ required Uint8List callData, BigInt? customNonce, @@ -45,95 +64,223 @@ abstract class SmartWalletBase { BigInt? maxPriorityFeePerGas, }); - /// manually Sets the init code of the Smart Wallet and overrides the default. + /// Sets the account initialization calldata for a [SmartWalletBase] in a potentially unsafe manner. + /// + /// **Warning:** + /// This method allows setting the initialization calldata directly, which may lead to unexpected behavior + /// if used improperly. It is intended for advanced use cases where the caller is aware of the potential risks. + /// + /// Parameters: + /// - `code`: The initialization calldata as a [Uint8List]. Set to `null` to clear the existing data. + /// + /// Example: + /// ```dart + /// dangerouslySetInitCallData(Uint8List.fromList([0x01, 0x02, 0x03])); + /// ``` void dangerouslySetInitCallData(Uint8List? code); - /// Creates a new wallet address using counterfactual deployment. - /// - /// This method generates a new wallet address based on the provided salt value. - /// The wallet may not actually be deployed, and [deployed] should be used to check deployment status. + /// Asynchronously creates a simple Ethereum smart account using the provided salt value. + /// Uses counterfactactual deployment to create the account and [deployed] should be used to check deployment status. /// An `initCode` will be attached on the first transaction. /// - /// - [salt]: The salt for the wallet. - /// - [index]: The index of the wallet (optional). + /// Parameters: + /// - `salt`: A [Uint256] representing the salt value for account creation. + /// - `index`: Optional parameter specifying the index for selecting a signer. Defaults to `null`. + /// + /// Returns: + /// A [Future] that completes with the created [SmartWallet] instance. + /// + /// Example: + /// ```dart + /// var smartWallet = await createSimpleAccount(Uint256.zero, index: 1); + /// ``` + /// This method generates initialization calldata using the 'createAccount' method and the provided signer and salt. + /// It then retrieves the Ethereum address for the simple account and sets it to the wallet instance. Future createSimpleAccount(Uint256 salt, {int? index}); - /// Creates a new Passkey wallet address using counterfactual deployment. + /// Asynchronously creates a simple Ethereum smart account using a passkey pair and the provided salt value. /// - /// This method generates a new Passkey wallet address based on the provided parameters. - /// The wallet may not actually be deployed, and [deployed] should be used to check deployment status. - /// An `initCode` will be attached on the first transaction. + /// Parameters: + /// - `pkp`: A [PassKeyPair] representing the passkey pair for account creation. + /// - `salt`: A [Uint256] representing the salt value for account creation. /// - /// - [pkp]: The PasskeyPair for the wallet passkey signer. - /// - [salt]: The salt for create2. + /// Returns: + /// A [Future] that completes with the created [SmartWallet] instance. + /// + /// Example: + /// ```dart + /// var smartWallet = await createSimplePasskeyAccount(myPassKeyPair, Uint256.zero); + /// ``` + /// This method generates initialization calldata using the 'createPasskeyAccount' method and the provided + /// passkey pair and salt. The passkey pair includes the credential and public key values. Future createSimplePasskeyAccount(PassKeyPair pkp, Uint256 salt); - /// Retrieves the counterfactual address of a wallet created with [createSimpleAccount]. + /// Asynchronously retrieves the Ethereum address for a simple account created with the specified signer and salt. + /// + /// Parameters: + /// - `signer`: The [EthereumAddress] of the signer associated with the account. + /// - `salt`: A [Uint256] representing the salt value used in the account creation. + /// + /// Returns: + /// A [Future] that completes with the Ethereum address of the simple account. + /// + /// Example: + /// ```dart + /// var address = await getSimpleAccountAddress( + /// EthereumAddress.fromHex('0x1234567890abcdef1234567890abcdef12345678'), + /// Uint256.zero, + /// ); + /// ``` Future getSimpleAccountAddress( EthereumAddress signer, Uint256 salt); - /// Retrieves the counterfactual address of a Passkey wallet created with [createSimplePasskeyAccount]. + /// Asynchronously retrieves the Ethereum address for a simple account created with the specified passkey pair and salt. + /// + /// Parameters: + /// - `pkp`: The [PassKeyPair] used for creating the account. + /// - `salt`: A [Uint256] representing the salt value used in the account creation. + /// + /// Returns: + /// A [Future] that completes with the Ethereum address of the simple account. + /// + /// Example: + /// ```dart + /// var address = await getSimplePassKeyAccountAddress( + /// myPassKeyPair, + /// Uint256.zero, + /// ); + /// ``` Future getSimplePassKeyAccountAddress( PassKeyPair pkp, Uint256 salt); - /// Transfers native tokens to another recipient. + /// Asynchronously transfers native Token (ETH) to the specified recipient with the given amount. + /// + /// Parameters: + /// - `recipient`: The [EthereumAddress] of the transaction recipient. + /// - `amount`: The [EtherAmount] representing the amount to be sent in the transaction. /// - /// - [recipient]: The address of the recipient. - /// - [amount]: The amount to send. + /// Returns: + /// A [Future] that completes with a [UserOperationResponse] containing information about the transaction. /// - /// Returns the [UserOperationResponse] of the transaction. + /// Example: + /// ```dart + /// var response = await send( + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// EtherAmount.inWei(BigInt.from(1000000000000000000)), + /// ); + /// ``` + /// This method internally builds a [UserOperation] using the provided parameters and sends the user operation + /// using [sendUserOperation], returning the response. Future send( EthereumAddress recipient, EtherAmount amount, ); - /// Sends a batched transaction to the wallet. + /// Asynchronously sends a batched Ethereum transaction to multiple recipients with the given calls and optional amounts. /// - /// - [recipients]: The addresses of the recipients. - /// - [calls]: The calldata to send. - /// - [amounts]: The amounts to send (optional). + /// Parameters: + /// - `recipients`: A list of [EthereumAddress] representing the recipients of the batched transaction. + /// - `calls`: A list of [Uint8List] representing the calldata for each transaction in the batch. + /// - `amounts`: Optional list of [EtherAmount] representing the amounts for each transaction. Defaults to `null`. /// - /// Returns the [UserOperationResponse] of the transaction. + /// Returns: + /// A [Future] that completes with a [UserOperationResponse] containing information about the batched transaction. + /// + /// Example: + /// ```dart + /// var response = await sendBatchedTransaction( + /// [ + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// EthereumAddress.fromHex('0xabcdef1234567890abcdef1234567890abcdef12'), + /// ], + /// [ + /// Contract.execute(_walletAddress, to: recipient1, amount: amount1), + /// Contract.execute(_walletAddress, to: recipient2, amount: amount2), + /// ], + /// amounts: [EtherAmount.inWei(BigInt.from(1000000000000000000)), EtherAmount.inWei(BigInt.from(500000000000000000))], + /// ); + /// ``` + /// This method internally builds a [UserOperation] using the provided parameters and sends the user operation + /// using [sendUserOperation], returning the response. Future sendBatchedTransaction( List recipients, List calls, { List? amounts, }); - /// Sends a signed user operation to the bundler. + /// Asynchronously sends a signed user operation to the bundler for execution. + /// + /// Parameters: + /// - `op`: The signed [UserOperation] to be sent for execution. /// - /// - [op]: The [UserOperation]. + /// Returns: + /// A [Future] that completes with a [UserOperationResponse] containing information about the executed operation. /// - /// Returns the [UserOperationResponse] of the transaction. + /// Example: + /// ```dart + /// var response = await sendSignedUserOperation(mySignedUserOperation); + /// ``` Future sendSignedUserOperation(UserOperation op); - /// Sends a transaction to the wallet contract. + /// Asynchronously sends an Ethereum transaction to the specified address with the provided encoded function data and optional amount. + /// + /// Parameters: + /// - `to`: The [EthereumAddress] of the transaction recipient. + /// - `encodedFunctionData`: The [Uint8List] containing the encoded function data for the transaction. + /// - `amount`: Optional [EtherAmount] representing the amount to be sent in the transaction. Defaults to `null`. /// - /// - [to]: The address of the recipient. - /// - [encodedFunctionData]: The calldata to send. - /// - [amount]: The amount to send (optional). + /// Returns: + /// A [Future] that completes with a [UserOperationResponse] containing information about the transaction. /// - /// Returns the [UserOperationResponse] of the transaction. + /// Example: + /// ```dart + /// var response = await sendTransaction( + /// EthereumAddress.fromHex('0x9876543210abcdef9876543210abcdef98765432'), + /// Uint8List.fromList([]), + /// amount: EtherAmount.inWei(BigInt.from(1000000000000000000)), + /// ); // tranfers ether to 0x9876543210abcdef9876543210abcdef98765432 + /// ``` + /// This method internally builds a [UserOperation] using the provided parameters and sends the user operation + /// using [sendUserOperation], returning the response. Future sendTransaction( EthereumAddress to, Uint8List encodedFunctionData, { EtherAmount? amount, }); - /// Signs and sends a user operation to the bundler. + /// Asynchronously sends a user operation after signing it and obtaining the required signatures. /// - /// - [op]: The [UserOperation]. + /// Parameters: + /// - `op`: The [UserOperation] to be signed and sent. + /// - `id`: Optional identifier (credential Id) when using a passkey signer Defaults to `null`. /// - /// Returns the [UserOperationResponse] of the transaction. + /// Returns: + /// A [Future] that completes with a [UserOperationResponse] containing information about the executed operation. + /// + /// Example: + /// ```dart + /// // when using passkey signer, the credentialId idenfies the credential that is associated with the account. + /// var response = await sendUserOperation(myUserOperation, id: 'credentialId'); // index is effectively ignored even if provided + /// ``` Future sendUserOperation(UserOperation op); - /// Signs a user operation using the provided key. + /// Asynchronously signs a user operation with the required signatures. + /// + /// Parameters: + /// - `userOp`: The [UserOperation] to be signed. + /// - `update`: Optional parameter indicating whether to update the user operation before signing. Defaults to `true`. + /// - `id`: Optional identifier (credential Id) when using a passkey signer Defaults to `null`. + /// - `index`: Optional index parameter for selecting a signer. Defaults to `null`. /// - /// - [userOp]: The [UserOperation]. - /// - [update]: True if you want to update the user operation (optional). - /// - [id]: The id of the transaction (optional). + /// Returns: + /// A [Future] that completes with the signed [UserOperation]. /// - /// Returns a signed [UserOperation]. + /// Example: + /// ```dart + /// // when using HD wallet, index can be used to specify which privatekey to use + /// var signedOperation = await signUserOperation(myUserOperation, index: 0); // signer 0 + /// var signedOperation = await signUserOperation(myUserOperation, index: 1); // signer 1 + /// ``` Future signUserOperation( UserOperation userOp, { bool update = true, diff --git a/lib/src/interfaces/uint256_interface.dart b/lib/src/interfaces/uint256_interface.dart index 38c5b52..64a315e 100644 --- a/lib/src/interfaces/uint256_interface.dart +++ b/lib/src/interfaces/uint256_interface.dart @@ -1,93 +1,138 @@ -part of '../../interfaces.dart'; +part of 'interfaces.dart'; /// Abstract base class representing a 64-bit length big number, similar to Solidity. /// /// This interface defines methods and properties for working with 64-bit length big numbers, /// with operations such as multiplication, addition, subtraction, division, and various conversions. abstract class Uint256Base { - /// The value of the Uint256. BigInt get value; - /// Multiplies this Uint256 by [other]. - /// - /// - [other]: The Uint256 to multiply with. - /// - /// Returns a new Uint256 representing the result of the multiplication. Uint256Base operator *(covariant Uint256Base other); - /// Adds [other] to this Uint256. - /// - /// - [other]: The Uint256 to add. - /// - /// Returns a new Uint256 representing the result of the addition. Uint256Base operator +(covariant Uint256Base other); - /// Subtracts [other] from this Uint256. - /// - /// - [other]: The Uint256 to subtract. - /// - /// Returns a new Uint256 representing the result of the subtraction. Uint256Base operator -(covariant Uint256Base other); - /// Divides this Uint256 by [other]. - /// - /// - [other]: The Uint256 to divide by. - /// - /// Returns a new Uint256 representing the result of the division. Uint256Base operator /(covariant Uint256Base other); - /// Converts this Uint256 to an [EtherAmount] in ether. + /// Converts the value of this [Uint256] instance to a [BigInt] representing the equivalent amount in ether. /// - /// Returns an [BigInt] representing the Uint256 value in ether. + /// Example 1: + /// ```dart + /// final value = Uint256(BigInt.from(5000000000)); + /// final etherValue = value.toEther(); // Converts the value to ether (0.000000000000000005) + /// ``` + + /// Example 2: + /// ```dart + /// final value = Uint256(BigInt.from(1000000000000000000)); + /// final etherValue = value.toEther(); // Converts the value to ether (1.0) + /// ``` BigInt toEther(); - /// Converts this Uint256 to an [EtherAmount] in wei. + /// Converts the value of this [Uint256] instance to an [EtherAmount] with the equivalent amount in wei. /// - /// Returns an [EtherAmount] + /// Example 1: + /// ```dart + /// final value = Uint256(BigInt.from(5000000000)); + /// final etherAmount = value.toEtherAmount(); // Converts the value to EtherAmount (5 wei) + /// ``` + + /// Example 2: + /// ```dart + /// final value = Uint256(BigInt.from(1000000000000000000)); + /// final etherAmount = value.toEtherAmount(); // Converts the value to EtherAmount (1 ether) + /// ``` EtherAmount toEtherAmount(); - /// Converts this Uint256 to a hexadecimal string. + /// Converts the value of this [Uint256] instance to a hexadecimal string with a length of 64 characters, padded with leading zeros. /// - /// Returns a string representation of the Uint256 value in hexadecimal format. + /// Example 1: + /// ```dart + /// final value = Uint256(BigInt.from(42)); + /// final hexString = value.toHex(); // Converts the value to hex (0x000000000000000000000000000000000000000000000000000000000000002a) + /// ``` + + /// Example 2: + /// ```dart + /// final value = Uint256(BigInt.from(255)); + /// final hexString = value.toHex(); // Converts the value to hex (0x00000000000000000000000000000000000000000000000000000000000000ff) + /// ``` String toHex(); - /// Converts this Uint256 to an integer. + /// Converts the value of this [Uint256] instance to an integer. /// - /// Returns an integer representation of the Uint256 value. + /// Example 1: + /// ```dart + /// final value = Uint256(BigInt.from(42)); + /// final intValue = value.toInt(); // Converts the value to an integer (42) + /// ``` + + /// Example 2: + /// ```dart + /// final value = Uint256(BigInt.from(123456789)); + /// final intValue = value.toInt(); // Converts the value to an integer (123456789) + /// ``` int toInt(); - /// Converts this Uint256 to a string. + /// Returns the hexadecimal representation of this [Uint256] instance as a string. /// - /// Returns a string representation of the Uint256 value. + /// Example 1: + /// ```dart + /// final value = Uint256(BigInt.from(42)); + /// final stringValue = value.toString(); // Converts the value to a string (0x000000000000000000000000000000000000000000000000000000000000002a) + /// ``` + + /// Example 2: + /// ```dart + /// final value = Uint256(BigInt.from(255)); + /// final stringValue = value.toString(); // Converts the value to a string (0x00000000000000000000000000000000000000000000000000000000000000ff) + /// ``` @override String toString(); - /// The `toUnit` method is used to convert a Uint256 value from its base unit to a specific - /// unit. It takes an `int` parameter `decimals` which represents the number of decimal places - /// to consider. The method returns a `BigInt` representing the converted value. - /// example: + /// Converts the value of this [Uint256] instance to a [BigInt] with a scale defined by [decimals]. /// + /// Example 1: /// ```dart - /// Uint256(BigInt.from(1)).toUnit(18); - /// - /// // 1 * 10^18 = 1000000000000000000 + /// final value = Uint256(BigInt.from(42)); + /// final unitValue = value.toUnit(3); // Converts the value to a unit with 3 decimals (42000) + /// ``` + + /// Example 2: + /// ```dart + /// final value = Uint256(BigInt.from(123456789)); + /// final unitValue = value.toUnit(6); // Converts the value to a unit with 6 decimals (123456789000000) /// ``` BigInt toUnit(int decimals); - /// The `fromUnit` method is used to convert a Uint256 value from a specific unit to its base - /// unit. It takes an `int` parameter `decimals` which represents the number of decimal places - /// to consider. The method returns a `double` representing the converted value. - /// example: + /// Converts the value of this [Uint256] instance from a unit with [decimals] to a double. /// + /// Example 1: /// ```dart - /// Uint256(BigInt.from(1000000000000000000)).fromUnit(18); - /// - /// // 1000000000000000000 / 10^18 = 1.0 + /// final value = Uint256(BigInt.from(42000)); + /// final doubleValue = value.fromUnit(3); // Converts the value from a unit with 3 decimals to a double (42.0) + /// ``` + + /// Example 2: + /// ```dart + /// final value = Uint256(BigInt.from(123456789000000)); + /// final doubleValue = value.fromUnit(6); // Converts the value from a unit with 6 decimals to a double (123.456789) /// ``` double fromUnit(int decimals); - /// Converts this Uint256 to an [EtherAmount] in wei. + /// Converts the value of this [Uint256] instance to a [BigInt] representing the equivalent amount in wei. /// - /// Returns an [BigInt] representing the Uint256 value in wei. + /// Example 1: + /// ```dart + /// final value = Uint256(BigInt.from(5000000000)); + /// final weiValue = value.toWei(); // Converts the value to wei (5000000000) + /// ``` + + /// Example 2: + /// ```dart + /// final value = Uint256(BigInt.from(1000000000000000000)); + /// final weiValue = value.toWei(); // Converts the value to wei (1000000000000000000) + /// ``` BigInt toWei(); } diff --git a/lib/src/interfaces/user_operations.dart b/lib/src/interfaces/user_operations.dart index d2dccb4..10564d3 100644 --- a/lib/src/interfaces/user_operations.dart +++ b/lib/src/interfaces/user_operations.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/interfaces.dart'; +part of 'interfaces.dart'; /// Abstract base class representing a user operation. /// diff --git a/lib/src/signers/hd_wallet_signer.dart b/lib/src/signers/hd_wallet_signer.dart index ca87626..9bef3ca 100644 --- a/lib/src/signers/hd_wallet_signer.dart +++ b/lib/src/signers/hd_wallet_signer.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/variance.dart'; +part of '../../variance.dart'; class HDWalletSigner with SecureStorageMixin implements HDInterface { final String _mnemonic; @@ -7,24 +7,31 @@ class HDWalletSigner with SecureStorageMixin implements HDInterface { late final EthereumAddress zerothAddress; - HDWalletSigner._internal({required String seed, required String mnemonic}) - : _seed = seed, - _mnemonic = mnemonic { - assert(seed.isNotEmpty, "seed cannot be empty"); - } + @override + String dummySignature = + "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; - /// Generates a new account in the HD wallet and stores it as zeroth. + /// Creates a new HD wallet signer instance by generating a random mnemonic phrase. /// - /// Returns the HD signer instance. + /// Example: + /// ```dart + /// final walletSigner = HDWalletSigner.createWallet(); + /// ``` factory HDWalletSigner.createWallet() { return HDWalletSigner.recoverAccount(bip39.generateMnemonic()); } - /// Recovers an account from a mnemonic phrase and stores it in the HD wallet as zeroth. + /// Recovers an HD wallet signer instance from a given mnemonic phrase. /// - /// - [mnemonic]: The mnemonic phrase. + /// Parameters: + /// - [mnemonic]: The mnemonic phrase used for recovering the HD wallet signer. /// - /// Returns the HD signer instance. + /// Example: + /// ```dart + /// final mnemonicPhrase = 'word1 word2 word3 ...'; // Replace with an actual mnemonic phrase + /// final recoveredSigner = HDWalletSigner.recoverAccount(mnemonicPhrase); + /// ``` + factory HDWalletSigner.recoverAccount(String mnemonic) { final seed = bip39.mnemonicToSeedHex(mnemonic); final signer = HDWalletSigner._internal(seed: seed, mnemonic: mnemonic); @@ -32,20 +39,10 @@ class HDWalletSigner with SecureStorageMixin implements HDInterface { return signer; } - /// Loads an account from secure storage and stores it in the HD wallet as zeroth. - /// if no account is found, it returns `null`. - /// - /// - [storageMiddleware]: The secure storage middleware. - /// - [options]: The authentication options. - /// - /// Returns a `Future` completing with the HD signer instance. - static Future loadFromSecureStorage( - {required SecureStorageRepository storageMiddleware, - SSAuthOperationOptions? options}) { - return storageMiddleware - .readCredential(CredentialType.hdwallet, options: options) - .then((value) => - value != null ? HDWalletSigner.recoverAccount(value) : null); + HDWalletSigner._internal({required String seed, required String mnemonic}) + : _seed = seed, + _mnemonic = mnemonic { + assert(seed.isNotEmpty, "seed cannot be empty"); } @override @@ -71,14 +68,7 @@ class HDWalletSigner with SecureStorageMixin implements HDInterface { @override String getAddress({int index = 0, bytes}) { - return getEthereumAddress(index: index).hex; - } - - @override - EthereumAddress getEthereumAddress({int index = 0}) { - bip44.ExtendedPrivateKey hdKey = _getHdKey(index); - final privKey = _deriveEthPrivKey(hdKey.privateKeyHex()); - return privKey.address; + return _getEthereumAddress(index: index).hex; } @override @@ -95,6 +85,15 @@ class HDWalletSigner with SecureStorageMixin implements HDInterface { return privKey.signToEcSignature(hash); } + @override + SecureStorageMiddleware withSecureStorage(FlutterSecureStorage secureStorage, + {Authentication? authMiddleware}) { + return SecureStorageMiddleware( + secureStorage: secureStorage, + authMiddleware: authMiddleware, + credential: _getMnemonic()); + } + EthereumAddress _add(String seed, int index) { final hdKey = _deriveHdKey(seed, index); final privKey = _deriveEthPrivKey(hdKey.privateKeyHex()); @@ -113,6 +112,12 @@ class HDWalletSigner with SecureStorageMixin implements HDInterface { return hdKey; } + EthereumAddress _getEthereumAddress({int index = 0}) { + bip44.ExtendedPrivateKey hdKey = _getHdKey(index); + final privKey = _deriveEthPrivKey(hdKey.privateKeyHex()); + return privKey.address; + } + bip44.ExtendedPrivateKey _getHdKey(int index) { return _deriveHdKey(_seed, index); } @@ -127,16 +132,27 @@ class HDWalletSigner with SecureStorageMixin implements HDInterface { return privateKey; } - @override - SecureStorageMiddleware withSecureStorage(SecureStorage secureStorage, - {Authentication? authMiddleware}) { - return SecureStorageMiddleware( - secureStorage: secureStorage, - authMiddleware: authMiddleware, - credential: _getMnemonic()); + /// Loads an HD wallet signer instance from secure storage using the provided [SecureStorageRepository]. + /// + /// Parameters: + /// - [storageMiddleware]: The secure storage repository used to retrieve the HD wallet credentials. + /// - [options]: Optional authentication operation options. Defaults to `null`. + /// + /// Returns a `Future` that resolves to a `HDWalletSigner` instance if successfully loaded, or `null` otherwise. + /// + /// Example: + /// ```dart + /// final secureStorageRepo = SecureStorageRepository(); // Replace with an actual instance + /// final loadedSigner = await HDWalletSigner.loadFromSecureStorage( + /// storageMiddleware: secureStorageRepo, + /// ); + /// ``` + static Future loadFromSecureStorage( + {required SecureStorageRepository storageMiddleware, + SSAuthOperationOptions? options}) { + return storageMiddleware + .readCredential(CredentialType.hdwallet, options: options) + .then((value) => + value != null ? HDWalletSigner.recoverAccount(value) : null); } - - @override - String dummySignature = - "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; } diff --git a/lib/src/signers/passkey_signer.dart b/lib/src/signers/passkey_signer.dart index 15135b1..103d25a 100644 --- a/lib/src/signers/passkey_signer.dart +++ b/lib/src/signers/passkey_signer.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/variance.dart'; +part of '../../variance.dart'; class AuthData { final String credentialHex; @@ -50,7 +50,7 @@ class PassKeyPair with SecureStorageMixin { } @override - SecureStorageMiddleware withSecureStorage(SecureStorage secureStorage, + SecureStorageMiddleware withSecureStorage(FlutterSecureStorage secureStorage, {Authentication? authMiddleware}) { return SecureStorageMiddleware( secureStorage: secureStorage, @@ -58,6 +58,21 @@ class PassKeyPair with SecureStorageMixin { credential: toJson()); } + /// Loads a passkey pair from secure storage using the provided [SecureStorageRepository]. + /// + /// Parameters: + /// - [storageMiddleware]: The secure storage repository used to retrieve the passkey pair credentials. + /// - [options]: Optional authentication operation options. Defaults to `null`. + /// + /// Returns a `Future` that resolves to a `PassKeyPair` instance if successfully loaded, or `null` otherwise. + /// + /// Example: + /// ```dart + /// final secureStorageRepo = SecureStorageRepository(); // Replace with an actual instance + /// final loadedPassKeyPair = await PassKeyPair.loadFromSecureStorage( + /// storageMiddleware: secureStorageRepo, + /// ); + /// ``` static Future loadFromSecureStorage( {required SecureStorageRepository storageMiddleware, SSAuthOperationOptions? options}) { @@ -76,7 +91,15 @@ class PassKeySignature { PassKeySignature(this.credentialId, this.rs, this.authData, this.clientDataPrefix, this.clientDataSuffix); - Uint8List toList() { + /// Converts the `PassKeySignature` to a `Uint8List` using the specified ABI encoding. + /// + /// Returns the encoded Uint8List. + /// + /// Example: + /// ```dart + /// final Uint8List encodedSig = pkpSig.toUint8List(); + /// ``` + Uint8List toUint8List() { return abi.encode([ 'uint256', 'uint256', @@ -209,7 +232,7 @@ class PassKeySigner implements PasskeyInterface { {int? index, String? id}) async { require(id != null, "credential id expected"); final signature = await signToPasskeySignature(hash, id!); - return signature.toList(); + return signature.toUint8List(); } @override diff --git a/lib/src/signers/private_key_signer.dart b/lib/src/signers/private_key_signer.dart index 3ff95aa..bd20676 100644 --- a/lib/src/signers/private_key_signer.dart +++ b/lib/src/signers/private_key_signer.dart @@ -1,14 +1,44 @@ -part of 'package:variance_dart/variance.dart'; +part of '../../variance.dart'; -class PrivateKeySigner with SecureStorageMixin implements CredentialInterface { +class PrivateKeySigner with SecureStorageMixin implements MultiSignerInterface { final Wallet _credential; + @override + String dummySignature = + "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; + + /// Creates a PrivateKeySigner instance using the provided EthPrivateKey. + /// + /// Parameters: + /// - [privateKey]: The EthPrivateKey used to create the PrivateKeySigner. + /// - [password]: The password for encrypting the private key. + /// - [random]: The Random instance for generating random values. + /// - [scryptN]: Scrypt parameter N (CPU/memory cost) for key derivation. Defaults to 8192. + /// - [p]: Scrypt parameter p (parallelization factor) for key derivation. Defaults to 1. + /// + /// Example: + /// ```dart + /// final ethPrivateKey = EthPrivateKey.fromHex('your_private_key_hex'); + /// final password = 'your_password'; + /// final random = Random.secure(); + /// final privateKeySigner = PrivateKeySigner.create(ethPrivateKey, password, random); + /// ``` PrivateKeySigner.create( EthPrivateKey privateKey, String password, Random random, {int scryptN = 8192, int p = 1}) : _credential = Wallet.createNew(privateKey, password, random, scryptN: scryptN, p: p); + /// Creates a PrivateKeySigner instance with a randomly generated EthPrivateKey. + /// + /// Parameters: + /// - [password]: The password for encrypting the private key. + /// + /// Example: + /// ```dart + /// final password = 'your_password'; + /// final privateKeySigner = PrivateKeySigner.createRandom(password); + /// ``` factory PrivateKeySigner.createRandom(String password) { final random = Random.secure(); final privateKey = EthPrivateKey.createRandom(random); @@ -16,27 +46,29 @@ class PrivateKeySigner with SecureStorageMixin implements CredentialInterface { return PrivateKeySigner._internal(credential); } + /// Creates a PrivateKeySigner instance from JSON representation. + /// + /// Parameters: + /// - [source]: The JSON representation of the wallet. + /// - [password]: The password for decrypting the private key. + /// + /// Example: + /// ```dart + /// final sourceJson = '{"privateKey": "your_private_key_encrypted", ...}'; + /// final password = 'your_password'; + /// final privateKeySigner = PrivateKeySigner.fromJson(sourceJson, password); + /// ``` factory PrivateKeySigner.fromJson(String source, String password) => PrivateKeySigner._internal( Wallet.fromJson(source, password), ); - static Future loadFromSecureStorage( - {required SecureStorageRepository storageMiddleware, - required String password, - SSAuthOperationOptions? options}) { - return storageMiddleware - .readCredential(CredentialType.hdwallet, options: options) - .then((value) => - value != null ? PrivateKeySigner.fromJson(value, password) : null); - } - PrivateKeySigner._internal(this._credential); - @override + /// Returns the Ethereum address associated with the PrivateKeySigner. EthereumAddress get address => _credential.privateKey.address; - @override + /// Returns the public key associated with the PrivateKeySigner. Uint8List get publicKey => _credential.privateKey.encodedPublicKey; @override @@ -56,11 +88,10 @@ class PrivateKeySigner with SecureStorageMixin implements CredentialInterface { return _credential.privateKey.signToEcSignature(hash); } - @override String toJson() => _credential.toJson(); @override - SecureStorageMiddleware withSecureStorage(SecureStorage secureStorage, + SecureStorageMiddleware withSecureStorage(FlutterSecureStorage secureStorage, {Authentication? authMiddleware}) { return SecureStorageMiddleware( secureStorage: secureStorage, @@ -68,7 +99,30 @@ class PrivateKeySigner with SecureStorageMixin implements CredentialInterface { credential: toJson()); } - @override - String dummySignature = - "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; + /// Loads a PrivateKeySigner encrypted credentialJson from secure storage. + /// + /// Parameters: + /// - [storageMiddleware]: The repository for secure storage. + /// - [password]: The password for decrypting the private key. + /// - [options]: Additional options for the authentication operation. + /// + /// Example: + /// ```dart + /// final storageMiddleware = SecureStorageRepository(); // Initialize your storage middleware + /// final password = 'your_password'; + /// final privateKeySigner = await PrivateKeySigner.loadFromSecureStorage( + /// storageMiddleware: storageMiddleware, + /// password: password, + /// options: yourSSAuthOperationOptions, + /// ); + /// ``` + static Future loadFromSecureStorage( + {required SecureStorageRepository storageMiddleware, + required String password, + SSAuthOperationOptions? options}) { + return storageMiddleware + .readCredential(CredentialType.hdwallet, options: options) + .then((value) => + value != null ? PrivateKeySigner.fromJson(value, password) : null); + } } diff --git a/lib/src/utils/crypto.dart b/lib/src/utils/crypto.dart index 9675146..cc9b559 100644 --- a/lib/src/utils/crypto.dart +++ b/lib/src/utils/crypto.dart @@ -1,10 +1,17 @@ -part of 'package:variance_dart/utils.dart'; +part of '../../utils.dart'; -/// Converts the given hex string [hexString] to its corresponding 32 bytes [Uint8List] representation. +/// Converts a hex string to a 32bytes `Uint8List`. /// -/// - [hexString]: The hex string to convert. +/// Parameters: +/// - [hexString]: The input hex string. /// -/// Returns a [Uint8List] representing the converted bytes. +/// Returns a Uint8List containing the converted bytes. +/// +/// Example: +/// ```dart +/// final hexString = '0x1a2b3c'; +/// final resultBytes = arrayify(hexString); +/// ``` Uint8List arrayify(String hexString) { hexString = hexString.replaceAll(RegExp(r'\s+'), ''); List bytes = []; @@ -16,12 +23,19 @@ Uint8List arrayify(String hexString) { return Uint8List.fromList(bytes); } -/// Encrypts the provided public key bytes [publicKeyBytes] with EcdsaPublicKey. +/// Retrieves the X and Y components of an ECDSA public key from its bytes. +/// +/// Parameters: +/// - [publicKeyBytes]: The bytes of the ECDSA public key. /// -/// - [publicKeyBytes]: The bytes of the public key. +/// Returns a Future containing a List of two strings representing the X and Y components of the public key. /// -/// Returns a [Future] that completes with a list of Uint8List representing the JSON Web Key x and y. -/// Throws an exception if the public key is invalid. +/// Example: +/// ```dart +/// final publicKeyBytes = Uint8List.fromList([4, 1, 2, 3]); // Replace with actual public key bytes +/// final components = await getPublicKeyFromBytes(publicKeyBytes); +/// print(components); // Output: ['01', '02'] +/// ``` Future?> getPublicKeyFromBytes(Uint8List publicKeyBytes) async { final pKey = await EcdsaPublicKey.importSpkiKey(publicKeyBytes, EllipticCurve.p256); @@ -39,11 +53,19 @@ Future?> getPublicKeyFromBytes(Uint8List publicKeyBytes) async { } } -/// Converts the list of integer values [intArray] to a hexadecimal string. +/// Converts a list of integers to a hexadecimal string. /// -/// - [intArray]: The list of integer values to convert. +/// Parameters: +/// - [intArray]: The list of integers to be converted. /// -/// Returns a hexadecimal string representation of the input list. +/// Returns a string representing the hexadecimal value. +/// +/// Example: +/// ```dart +/// final intArray = [1, 15, 255]; +/// final hexString = hexlify(intArray); +/// print(hexString); // Output: '0x01ff' +/// ``` String hexlify(List intArray) { var ss = []; for (int value in intArray) { @@ -52,39 +74,74 @@ String hexlify(List intArray) { return "0x${ss.join('')}"; } -/// Throws an exception with the provided [exception] message if the given [requirement] is not met. +/// Throws an exception if the specified requirement is not met. +/// +/// Parameters: +/// - [requirement]: The boolean requirement to be checked. +/// - [exception]: The exception message to be thrown if the requirement is not met. /// -/// - [requirement]: The condition to check. -/// - [exception]: The exception message to throw if the requirement is not met. +/// Throws an exception with the specified message if the requirement is not met. +/// +/// Example: +/// ```dart +/// final value = 42; +/// require(value > 0, "Value must be greater than 0"); +/// print("Value is valid: $value"); +/// ``` require(bool requirement, String exception) { if (!requirement) { throw Exception(exception); } } -/// Computes the SHA-256 hash of the given input [input]. +/// Computes the SHA-256 hash of the specified input. +/// +/// Parameters: +/// - [input]: The list of integers representing the input data. /// -/// - [input]: The input bytes to hash. +/// Returns a [Digest] object representing the SHA-256 hash. /// -/// Returns a [Digest] representing the SHA-256 hash. +/// Example: +/// ```dart +/// final data = utf8.encode("Hello, World!"); +/// final hash = sha256Hash(data); +/// print("SHA-256 Hash: ${hash.toString()}"); +/// ``` Digest sha256Hash(List input) { return sha256.convert(input); } -/// Checks if the first byte in the provided [bytes] is 0x0 and the second byte's most significant bit is set. +/// Checks whether the leading zero should be removed from the byte array. /// -/// - [bytes]: The list of bytes to check. +/// Parameters: +/// - [bytes]: The list of integers representing the byte array. /// -/// Returns true if the first byte is 0x0 and the second byte's most significant bit is set; otherwise, false. +/// Returns `true` if the leading zero should be removed, otherwise `false`. +/// +/// Example: +/// ```dart +/// final byteData = Uint8List.fromList([0x00, 0x01, 0x02, 0x03]); +/// final removeZero = shouldRemoveLeadingZero(byteData); +/// print("Remove Leading Zero: $removeZero"); +/// ``` bool shouldRemoveLeadingZero(Uint8List bytes) { return bytes[0] == 0x0 && (bytes[1] & (1 << 7)) != 0; } -/// Concatenates a list of lists of integer values [buff] into a single list of integers. +/// Combines multiple lists of integers into a single list. +/// +/// Parameters: +/// - [buff]: List of lists of integers to be combined. /// -/// - [buff]: The list of lists of integer values to concatenate. +/// Returns a new list containing all the integers from the input lists. /// -/// Returns a list of integers representing the concatenated values. +/// Example: +/// ```dart +/// final list1 = [1, 2, 3]; +/// final list2 = [4, 5, 6]; +/// final combinedList = toBuffer([list1, list2]); +/// print("Combined List: $combinedList"); +/// ``` List toBuffer(List> buff) { return List.from(buff.expand((element) => element).toList()); } diff --git a/lib/src/utils/dio_client.dart b/lib/src/utils/dio_client.dart index c5f8c34..3ff2c78 100644 --- a/lib/src/utils/dio_client.dart +++ b/lib/src/utils/dio_client.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/utils.dart'; +part of '../../utils.dart'; class DioClient implements RestClient { final Dio _dio = Dio() diff --git a/lib/src/utils/local_authentication.dart b/lib/src/utils/local_authentication.dart index c84dff3..211a396 100644 --- a/lib/src/utils/local_authentication.dart +++ b/lib/src/utils/local_authentication.dart @@ -1,4 +1,4 @@ -part of 'package:variance_dart/utils.dart'; +part of '../../utils.dart'; class AuthenticationError extends Error { final String message; diff --git a/lib/src/utils/secure_storage_repository.dart b/lib/src/utils/secure_storage_repository.dart index 84b1cc9..a3fb354 100644 --- a/lib/src/utils/secure_storage_repository.dart +++ b/lib/src/utils/secure_storage_repository.dart @@ -1,18 +1,4 @@ -part of 'package:variance_dart/utils.dart'; - -class SSAuthOperationOptions { - final bool requiresAuth; - final String authReason; - // Namespace for uniquely addressing the secure storage keys. - // if provided the secure storage keys will be prefixed with this value, defaults to "vaariance" - // namespace ?? "vaariance" + "_" + identifier - final String? ssNameSpace; - - const SSAuthOperationOptions( - {bool? requiresAuth, String? authReason, this.ssNameSpace}) - : authReason = authReason ?? "unlock to access secure storage", - requiresAuth = requiresAuth ?? false; -} +part of '../../utils.dart'; enum CredentialType { hdwallet, privateKey, passkeypair } @@ -24,7 +10,7 @@ class SecureStorageAuthMiddlewareError extends Error { @override String toString() { - return 'SecureStorageError: $message'; + return 'SecureStorageAuthMiddlewareError: $message'; } } @@ -32,7 +18,7 @@ class SecureStorageMiddleware implements SecureStorageRepository { final AndroidOptions androidOptions; final IOSOptions iosOptions; - final SecureStorage secureStorage; + final FlutterSecureStorage secureStorage; final Authentication? authMiddleware; final String? _credential; @@ -134,3 +120,17 @@ class SecureStorageMiddleware implements SecureStorageRepository { key: "${options.ssNameSpace ?? "vaariance"}_$key", value: value); } } + +class SSAuthOperationOptions { + final bool requiresAuth; + final String authReason; + // Namespace for uniquely addressing the secure storage keys. + // if provided the secure storage keys will be prefixed with this value, defaults to "vaariance" + // namespace ?? "vaariance" + "_" + identifier + final String? ssNameSpace; + + const SSAuthOperationOptions( + {bool? requiresAuth, String? authReason, this.ssNameSpace}) + : authReason = authReason ?? "unlock to access secure storage", + requiresAuth = requiresAuth ?? false; +} diff --git a/lib/utils.dart b/lib/utils.dart index 1a99014..8e750ff 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -12,11 +12,10 @@ import 'package:local_auth/error_codes.dart' as auth_error; import 'package:local_auth/local_auth.dart'; import 'package:local_auth_android/local_auth_android.dart'; import 'package:local_auth_ios/types/auth_messages_ios.dart'; -import 'package:variance_dart/interfaces.dart'; -import 'package:variance_dart/variance.dart' show Chain; import 'package:web3dart/web3dart.dart' show BlockNum, EthereumAddress; import 'package:webcrypto/webcrypto.dart'; +import 'src/interfaces/interfaces.dart'; import 'src/utils/models/ens.dart'; import 'src/utils/models/metadata.dart'; import 'src/utils/models/nft.dart'; @@ -24,6 +23,7 @@ import 'src/utils/models/price.dart'; import 'src/utils/models/token.dart'; import 'src/utils/models/transaction.dart'; import 'src/utils/models/transfer.dart'; +import 'variance.dart' show Chain; export 'package:dio/dio.dart' show BaseOptions; diff --git a/lib/variance.dart b/lib/variance.dart index d73561e..ad77e2c 100644 --- a/lib/variance.dart +++ b/lib/variance.dart @@ -11,14 +11,16 @@ import 'package:bip32_bip44/dart_bip32_bip44.dart' as bip44; import "package:bip39/bip39.dart" as bip39; import 'package:cbor/cbor.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart' as http; +import 'package:string_validator/string_validator.dart'; import 'package:uuid/uuid.dart'; import 'package:web3dart/crypto.dart'; import 'package:web3dart/json_rpc.dart'; import 'package:web3dart/web3dart.dart'; import 'package:webauthn/webauthn.dart'; -import 'interfaces.dart'; +import 'src/interfaces/interfaces.dart'; import 'src/abis/abis.dart'; import 'src/common/common.dart'; import 'utils.dart'; diff --git a/pubspec.lock b/pubspec.lock index 00e4ff0..05d0ed0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -109,10 +109,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.4.8" build_runner_core: dependency: transitive description: @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: code_builder - sha256: feee43a5c05e7b3199bb375a86430b8ada1b04104f2923d0e03cc01ca87b6d84 + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.10.0" collection: dependency: transitive description: @@ -503,10 +503,10 @@ packages: dependency: "direct main" description: name: local_auth - sha256: "7e6c63082e399b61e4af71266b012e767a5d4525dd6e9ba41e174fd42d76e115" + sha256: "27679ed8e0d7daab2357db6bb7076359e083a56b295c0c59723845301da6aed9" url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.1.8" local_auth_android: dependency: "direct main" description: @@ -527,10 +527,10 @@ packages: dependency: transitive description: name: local_auth_platform_interface - sha256: fc5bd537970a324260fda506cfb61b33ad7426f37a8ea5c461cf612161ebba54 + sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a" url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.0.10" local_auth_windows: dependency: transitive description: @@ -615,10 +615,10 @@ packages: dependency: transitive description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_android: dependency: transitive description: @@ -647,10 +647,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -663,18 +663,18 @@ packages: dependency: transitive description: name: platform - sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.1.8" pointycastle: dependency: transitive description: @@ -832,6 +832,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + string_validator: + dependency: "direct main" + description: + name: string_validator + sha256: "54d4f42cd6878ae72793a58a529d9a18ebfdfbfebd9793bbe55c9b28935e8543" + url: "https://pub.dev" + source: hosted + version: "1.0.2" synchronized: dependency: transitive description: @@ -972,10 +980,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c4202d8..1b45a3c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: local_auth_ios: ^1.1.5 local_auth_android: ^1.0.36 flutter_secure_storage: ^9.0.0 + string_validator: ^1.0.2 dev_dependencies: web3dart_builders: ^0.0.7