Skip to content

Commit

Permalink
feat: fully support v07 useroperations with pimlico
Browse files Browse the repository at this point in the history
  • Loading branch information
code-z2 committed Apr 7, 2024
1 parent d333e67 commit e2e3e11
Show file tree
Hide file tree
Showing 15 changed files with 261 additions and 123 deletions.
6 changes: 0 additions & 6 deletions example/lib/providers/send_crypto_provider.dart

This file was deleted.

26 changes: 20 additions & 6 deletions example/lib/providers/wallet_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,19 @@ class WalletProvider extends ChangeNotifier {
EthereumAddress.fromHex("0x218F6Bbc32Ef28F547A67c70AbCF8c2ea3b468BA");

WalletProvider()
: _chain = Chains.getChain(Network.baseTestnet)
: _chain = Chain(
chainId: 11155111,
explorer: "https://sepolia.etherscan.io/",
entrypoint: EntryPointAddress.v07)
..accountFactory = EthereumAddress.fromHex(
"0x402A266e92993EbF04a5B3fd6F0e2b21bFC83070")
"0xECA49857B32A12403F5a3A64ad291861EF4B63cb") // v07 p256 factory address
..jsonRpcUrl = "https://rpc.ankr.com/eth_sepolia"
..bundlerUrl =
"https://api.pimlico.io/v2/84532/rpc?apikey=YOUR_API_KEY"
..paymasterUrl = "https://paymaster.optimism.io/v1/84532/rpc";
"https://api.pimlico.io/v2/11155111/rpc?apikey=YOUR_API_KEY"
..paymasterUrl =
"https://api.pimlico.io/v2/11155111/rpc?apikey=YOUR_API_KEY";

// "0x402A266e92993EbF04a5B3fd6F0e2b21bFC83070" v06 p256 factory address
Future<void> registerWithPassKey(String name,
{bool? requiresUserVerification}) async {
final pkpSigner =
Expand Down Expand Up @@ -67,7 +73,7 @@ class WalletProvider extends ChangeNotifier {
}

Future<void> createEOAWallet() async {
_chain.accountFactory = Constants.simpleAccountFactoryAddress;
_chain.accountFactory = Constants.simpleAccountFactoryAddressv06;

final signer = EOAWallet.createWallet();
log("signer: ${signer.getAddress()}");
Expand All @@ -87,7 +93,7 @@ class WalletProvider extends ChangeNotifier {
}

Future<void> createPrivateKeyWallet() async {
_chain.accountFactory = Constants.simpleAccountFactoryAddress;
_chain.accountFactory = Constants.simpleAccountFactoryAddressv06;

final signer = PrivateKeySigner.createRandom("123456");
log("signer: ${signer.getAddress()}");
Expand Down Expand Up @@ -131,6 +137,14 @@ class WalletProvider extends ChangeNotifier {
}

Future<void> mintNFt() async {
// pimlico requires us to get the gasfees from their bundler.
// that cannot be built into the sdk so we modify the internal fees manually
if (_chain.entrypoint.version == 0.7) {
_wallet?.gasSettings = GasSettings(
gasMultiplierPercentage: 5,
userDefinedMaxFeePerGas: BigInt.parse("0x524e1909"),
userDefinedMaxPriorityFeePerGas: BigInt.parse("0x52412100"));
}
// mints nft
final tx1 = await _wallet?.sendTransaction(
nft,
Expand Down
2 changes: 1 addition & 1 deletion example/lib/screens/home/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ class NFT extends StatelessWidget {
style: TextStyle(color: VarianceColors.secondary),
),
]),
Spacer(),
const Spacer(),
ElevatedButton(
onPressed: () {
context.read<WalletProvider>().mintNFt();
Expand Down
30 changes: 25 additions & 5 deletions lib/src/4337/chains.dart
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,22 @@ class Constants {
EthereumAddress.fromHex("0x0000000071727De22E5E9d8BAf0edAc6f37da032");
static EthereumAddress zeroAddress =
EthereumAddress.fromHex("0x0000000000000000000000000000000000000000");
static final EthereumAddress simpleAccountFactoryAddress =
static final EthereumAddress simpleAccountFactoryAddressv06 =
EthereumAddress.fromHex("0x9406Cc6185a346906296840746125a0E44976454");
static final EthereumAddress simpleAccountFactoryAddressv07 =
EthereumAddress.fromHex("0x91E60e0613810449d098b0b5Ec8b51A0FE8c8985");
static final EthereumAddress safeProxyFactoryAddress =
EthereumAddress.fromHex("0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67");
static final EthereumAddress safe4337ModuleAddress =
static final EthereumAddress safe4337ModuleAddressv06 =
EthereumAddress.fromHex("0xa581c4A4DB7175302464fF3C06380BC3270b4037");
static final EthereumAddress safe4337ModuleAddressv07 =
EthereumAddress.fromHex("0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226");
static final EthereumAddress safeSingletonAddress =
EthereumAddress.fromHex("0x41675C099F32341bf84BFc5382aF534df5C7461a");
static final EthereumAddress safeModuleSetupAddress =
static final EthereumAddress safeModuleSetupAddressv06 =
EthereumAddress.fromHex("0x8EcD4ec46D4D2a6B64fE960B3D64e8B94B2234eb");
static final EthereumAddress safeModuleSetupAddressv07 =
EthereumAddress.fromHex("0x2dd68b007B46fBe91B9A7c3EDa5A7a1063cB5b47");
static final EthereumAddress safeMultiSendaddress =
EthereumAddress.fromHex("0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526");

Expand Down Expand Up @@ -234,7 +240,15 @@ class Safe4337ModuleAddress {
/// The address of the Safe4337Module contract for version 0.6.
static Safe4337ModuleAddress v06 = Safe4337ModuleAddress(
0.6,
Constants.safe4337ModuleAddress,
Constants.safe4337ModuleAddressv06,
Constants.safeModuleSetupAddressv06,
);

/// The address of the Safe4337Module contract for version 0.7.
static Safe4337ModuleAddress v07 = Safe4337ModuleAddress(
0.7,
Constants.safe4337ModuleAddressv07,
Constants.safeModuleSetupAddressv07,
);

/// The version of the Safe4337Module contract.
Expand All @@ -243,11 +257,15 @@ class Safe4337ModuleAddress {
/// The Ethereum address of the Safe4337Module contract.
final EthereumAddress address;

/// The address of the SafeModuleSetup contract.
final EthereumAddress setup;

/// Creates a new instance of the [Safe4337ModuleAddress] class.
///
/// [version] is the version of the Safe4337Module contract.
/// [address] is the Ethereum address of the Safe4337Module contract.
const Safe4337ModuleAddress(this.version, this.address);
/// [setup] is the address of the SafeModuleSetup contract.
const Safe4337ModuleAddress(this.version, this.address, this.setup);

/// Creates a new instance of the [Safe4337ModuleAddress] class from a given version.
///
Expand All @@ -264,6 +282,8 @@ class Safe4337ModuleAddress {
switch (version) {
case 0.6:
return Safe4337ModuleAddress.v06;
case 0.7:
return Safe4337ModuleAddress.v07;
default:
throw Exception("Unsupported version: $version");
}
Expand Down
64 changes: 42 additions & 22 deletions lib/src/4337/paymaster.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ class Paymaster implements PaymasterBase {
final RPCBase _rpc;
final Chain _chain;

/// The address of the Paymaster contract.
///
/// This is an optional parameter and can be left null if the paymaster address
/// is not known or needed.
EthereumAddress? _paymasterAddress;

/// The context data for the Paymaster.
///
/// This is an optional parameter and can be used to provide additional context
Expand All @@ -14,11 +20,12 @@ class Paymaster implements PaymasterBase {
/// Creates a new instance of the [Paymaster] class.
///
/// [_chain] is the Ethereum chain configuration.
/// [_paymasterAddress] is an optional address of the Paymaster contract.
/// [_context] is an optional map containing the context data for the Paymaster.
///
/// Throws an [InvalidPaymasterUrl] exception if the paymaster URL in the
/// provided chain configuration is not a valid URL.
Paymaster(this._chain, [this._context])
Paymaster(this._chain, [this._paymasterAddress, this._context])
: assert(_chain.paymasterUrl.isURL(),
InvalidPaymasterUrl(_chain.paymasterUrl)),
_rpc = RPCBase(_chain.paymasterUrl!);
Expand All @@ -29,19 +36,27 @@ class Paymaster implements PaymasterBase {
}

@override
Future<UserOperation> intercept(UserOperation operation) async {
set paymasterAddress(EthereumAddress? address) {
_paymasterAddress = address;
}

@override
Future<UserOperation> intercept(UserOperation op) async {
if (_paymasterAddress != null) {
op.paymasterAndData = Uint8List.fromList([
..._paymasterAddress!.addressBytes,
...op.paymasterAndData.sublist(20)
]);
}
final paymasterResponse = await sponsorUserOperation(
operation.toMap(), _chain.entrypoint, _context);
op.toMap(_chain.entrypoint.version), _chain.entrypoint, _context);

// Create a new UserOperation with the updated Paymaster data and gas limits
return operation.copyWith(
return op.copyWith(
paymasterAndData: paymasterResponse.paymasterAndData,
preVerificationGas: paymasterResponse.preVerificationGas,
verificationGasLimit: paymasterResponse.verificationGasLimit,
callGasLimit: paymasterResponse.callGasLimit,
maxFeePerGas: paymasterResponse.maxFeePerGas ?? operation.maxFeePerGas,
maxPriorityFeePerGas: paymasterResponse.maxPriorityFeePerGas ??
operation.maxPriorityFeePerGas,
);
}

Expand All @@ -65,30 +80,35 @@ class PaymasterResponse {
final BigInt preVerificationGas;
final BigInt verificationGasLimit;
final BigInt callGasLimit;
final BigInt? maxFeePerGas;
final BigInt? maxPriorityFeePerGas;

PaymasterResponse({
required this.paymasterAndData,
required this.preVerificationGas,
required this.verificationGasLimit,
required this.callGasLimit,
this.maxFeePerGas,
this.maxPriorityFeePerGas,
});

factory PaymasterResponse.fromMap(Map<String, dynamic> map) {
final List<BigInt> accountGasLimits = map['accountGasLimits'] != null
? unpackUints(map['accountGasLimits'])
: [
BigInt.parse(map['verificationGasLimit']),
BigInt.parse(map['callGasLimit'])
];

final paymasterAndData = map['paymasterAndData'] != null
? hexToBytes(map['paymasterAndData'])
: Uint8List.fromList([
...EthereumAddress.fromHex(map['paymaster']).addressBytes,
...packUints(BigInt.parse(map['paymasterVerificationGasLimit']),
BigInt.parse(map['paymasterPostOpGasLimit'])),
...hexToBytes(map["paymasterData"])
]);

return PaymasterResponse(
paymasterAndData: hexToBytes(map['paymasterAndData']),
preVerificationGas: BigInt.parse(map['preVerificationGas']),
verificationGasLimit: BigInt.parse(map['verificationGasLimit']),
callGasLimit: BigInt.parse(map['callGasLimit']),
maxFeePerGas: map['maxFeePerGas'] != null
? BigInt.parse(map['maxFeePerGas'])
: null,
maxPriorityFeePerGas: map['maxPriorityFeePerGas'] != null
? BigInt.parse(map['maxPriorityFeePerGas'])
: null,
);
paymasterAndData: paymasterAndData,
preVerificationGas: BigInt.parse(map['preVerificationGas']),
verificationGasLimit: accountGasLimits[0],
callGasLimit: accountGasLimits[1]);
}
}
30 changes: 20 additions & 10 deletions lib/src/4337/safe.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,38 +50,48 @@ class _SafePlugin extends Safe4337Module implements Safe4337ModuleBase {
///
/// Returns a Future that resolves to the hash of the user operation as a Uint8List.
Future<Uint8List> getSafeOperationHash(
UserOperation op, BlockInformation blockInfo) async =>
getOperationHash([
UserOperation op, BlockInformation blockInfo) async {
if (self.address == Safe4337ModuleAddress.v07.address) {
return getOperationHash$2([
op.sender,
op.nonce,
op.initCode,
op.callData,
op.callGasLimit,
op.verificationGasLimit,
packUints(op.verificationGasLimit, op.callGasLimit),
op.preVerificationGas,
op.maxFeePerGas,
op.maxPriorityFeePerGas,
packUints(op.maxPriorityFeePerGas, op.maxFeePerGas),
op.paymasterAndData,
hexToBytes(getSafeSignature(op.signature, blockInfo))
]);
}
return getOperationHash([
op.sender,
op.nonce,
op.initCode,
op.callData,
op.callGasLimit,
op.verificationGasLimit,
op.preVerificationGas,
op.maxFeePerGas,
op.maxPriorityFeePerGas,
op.paymasterAndData,
hexToBytes(getSafeSignature(op.signature, blockInfo))
]);
}

Uint8List getSafeMultisendCallData(List<EthereumAddress> recipients,
List<EtherAmount>? amounts, List<Uint8List>? innerCalls) {
Uint8List packedCallData = Uint8List(0);

for (int i = 0; i < recipients.length; i++) {
Uint8List operation = Uint8List.fromList([0]);
assert(operation.length == 1);
Uint8List to = recipients[i].addressBytes;
assert(to.length == 20);
Uint8List value = amounts != null
? padTo32Bytes(amounts[i].getInWei)
: padTo32Bytes(BigInt.zero);
assert(value.length == 32);
Uint8List dataLength = innerCalls != null
? padTo32Bytes(BigInt.from(innerCalls[i].length))
: padTo32Bytes(BigInt.zero);
assert(dataLength.length == 32);
Uint8List data =
innerCalls != null ? innerCalls[i] : Uint8List.fromList([]);
Uint8List encodedCall = Uint8List.fromList(
Expand Down
50 changes: 45 additions & 5 deletions lib/src/4337/userop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class UserOperation implements UserOperationBase {
@override
Uint8List hash(Chain chain) {
Uint8List encoded;
if (chain.entrypoint.version >= EntryPointAddress.v07.version) {
if (chain.entrypoint.version == EntryPointAddress.v07.version) {
encoded = keccak256(abi.encode([
'address',
'uint256',
Expand Down Expand Up @@ -196,13 +196,46 @@ class UserOperation implements UserOperationBase {
[encoded, chain.entrypoint.address, BigInt.from(chain.chainId)]));
}

/// uses pimlico v07 useroperation standard, which requires only pimlico bundlers
/// this is a training wheel and will revert to which ever schema bundlers accept in concensus
@override
Map<String, String> packUserOperation() {
Map<String, String> op = {
'sender': sender.hexEip55,
'nonce': '0x${nonce.toRadixString(16)}',
'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,
};
if (initCode.isNotEmpty) {
op['factory'] = hexlify(initCode.sublist(0, 20));
op['factoryData'] = hexlify(initCode.sublist(20));
}
if (paymasterAndData.isNotEmpty) {
op['paymaster'] = hexlify(paymasterAndData.sublist(0, 20));
final upackedPaymasterGasFields =
unpackUints(hexlify(paymasterAndData.sublist(20, 52)));
op['paymasterVerificationGasLimit'] =
'0x${upackedPaymasterGasFields[0].toRadixString(16)}';
op['paymasterPostOpGasLimit'] =
'0x${upackedPaymasterGasFields[1].toRadixString(16)}';
op['paymasterData'] = hexlify(paymasterAndData.sublist(52));
}
return op;
}

@override
String toJson() {
return jsonEncode(toMap());
}

@override
Map<String, String> toMap() {
Map<String, String> toMap([double version = 0.6]) {
if (version == 0.7) return packUserOperation();
return {
'sender': sender.hexEip55,
'nonce': '0x${nonce.toRadixString(16)}',
Expand All @@ -213,8 +246,8 @@ class UserOperation implements UserOperationBase {
'preVerificationGas': '0x${preVerificationGas.toRadixString(16)}',
'maxFeePerGas': '0x${maxFeePerGas.toRadixString(16)}',
'maxPriorityFeePerGas': '0x${maxPriorityFeePerGas.toRadixString(16)}',
'signature': signature,
'paymasterAndData': hexlify(paymasterAndData),
'signature': signature,
};
}

Expand Down Expand Up @@ -282,9 +315,16 @@ class UserOperationGas {
this.validUntil,
});
factory UserOperationGas.fromMap(Map<String, dynamic> map) {
final List<BigInt> accountGasLimits = map['accountGasLimits'] != null
? unpackUints(map['accountGasLimits'])
: [
BigInt.parse(map['verificationGasLimit']),
BigInt.parse(map['callGasLimit'])
];

return UserOperationGas(
callGasLimit: BigInt.parse(map['callGasLimit']),
verificationGasLimit: BigInt.parse(map['verificationGasLimit']),
verificationGasLimit: accountGasLimits[0],
callGasLimit: accountGasLimits[1],
preVerificationGas: BigInt.parse(map['preVerificationGas']),
validAfter:
map['validAfter'] != null ? BigInt.parse(map['validAfter']) : null,
Expand Down
Loading

0 comments on commit e2e3e11

Please sign in to comment.