-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: account abstraction and paymaster syntax #1
Conversation
export function encodeData(data: any[]): string { | ||
const types = data.map(item => { | ||
if (typeof item === 'number') { | ||
return 'uint256'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if I need uint128
or int64
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good question. I thought the same while setting up this function too (another reason why I left this PR as draft). I would think now then that it is not possible to deduce these lower levels types without the developer specifying them either through an ABI or some form of input. This changes the syntax for constructing both the Paymaster
and abstractWallet
classes, considering they both can potentially have methods that serialise data.
@@ -124,3 +125,52 @@ export class Wallet extends AdapterL2(AdapterL1(ethers.Wallet)) { | |||
return await this.provider.broadcastTransaction(await this.signTransaction(populatedTx)); | |||
} | |||
} | |||
|
|||
interface SigningFunction { | |||
(this: AbstractWallet, transaction: TransactionRequest): Promise<string>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does it need access tothis (AbstractWallet)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could otherwise accept the wallet as a parameter as:
(abstractWallet: AbstractWallet, transaction: TransactionRequest): Promise<string>;
Both approaches are fine by me
paymasterParams: this.paymaster.getPaymasterParams(), | ||
} : {}; | ||
transaction.customData = this._fillCustomData(transaction.customData); | ||
transaction.customData.customSignature = await this.eip712.sign(transaction); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how does it work? What key is used to sign it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will use the privateKey
parameter passed to the constructor here. Ultimately, a SigningKey
function is added at ethers.BaseWallet
, the class that ethers.Wallet
extends (and this class is extended by zks.Wallet
), using this passed in private key that is used for signatures.
I find it also tricky to know what exactly is going on with the class as it extends another class about 5 times.. But this approach adds the least lines of code too.
providerL2?: Provider, | ||
providerL1?: ethers.Provider, | ||
paymaster: Paymaster | null = null, | ||
customSigningFunction: SigningFunction = signingFunction, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need a default here? Or should we just make it required?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find it makes the syntax for declaration simpler by adding a default here. I prefer it because of that. I can imagine some developers might not be interested in overriding the signing function and only seek to use AA for the custom logic you can add to accounts on-chain. In these cases I can imagine they'd be happy not having to be concerned with the signing logic, or paymaster logic either (if they do not wish to use a paymaster).
) { | ||
super(privateKey, providerL2); | ||
this.accountAddress = accountAddress; | ||
this.paymaster = paymaster; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this.paymaster
here? How I can I control it's usage if it's here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.paymaster
is called in the signingFunction
logic inside of signTransaction
which is called for signing any transaction. The goal here was to provide a way for the developer to specify using a paymaster by default on any transaction. Currently the syntax in signingFunction
is:
...
transaction.customData = this.paymaster ? {
gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT,
paymasterParams: this.paymaster.getPaymasterParams(),
} : {};
...
But I think it would be better to change it actually to:
...
transaction.customData ??= this.paymaster ? {
gasPerPubdata: DEFAULT_GAS_PER_PUBDATA_LIMIT,
paymasterParams: this.paymaster.getPaymasterParams(),
} : {};
...
Considering the developer might want to specify some transactions on the frontend where a paymaster is applied, and some others not. But if the developer wants a paymaster to be used in all transactions, then they can simply declare the paymaster in this constructor here.
expect(difference / BigInt(10 ** 18) < 10.1).to.be.true; | ||
}).timeout(25_000); | ||
|
||
it.skip("should transfer DAI", async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
skip
expect(balanceAfterTransfer - balanceBeforeTransfer).to.be.equal(BigInt(amount)); | ||
}).timeout(25_000); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we please add a test for just calling some smart contract?
}; | ||
} | ||
|
||
getPaymasterParams() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this class if we simply want to build paymaster params? Shouldn't it be a util instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I also notice this method will require adding parameters as those are required for the ApprovalBased
paymasters that accept a quantity of tokens to cover the transaction cost.
As for making this a class, this class will also include a method to enrich a transaction with the data for paymasterParams
which is useful when developers only want to sponsor some transactions. Something like .addParamsToTransaction(trx)
that calls getPaymasterParams
to fill in the transaction object. This removes the need for the developer to manipulate the transaction object manually.
Not precious about this either though, so if you think strongly of keeping these as functions then we could go that way but I find this:
let tx = contract.methodWrite(...);
tx = utils.addPaymasterParamsToTransaction(utils.getPaymasterParams(...));
Less appealing than:
const paymaster = new Paymaster(...);
let tx = contract.methodWrite(...);
tx = paymaster.addParamsToTransaction(tx);
What 💻
This PR introduces both an
AbstractWallet
class and aPaymaster
class to simplify sending initialising AA accounts and sending paymaster-endorsed transactions. Developers will also be able to customise their own signature methods for the account abstraction by importing thesigningFunction
and redefining it.Why ✋
Sending transactions currently involves either (i) creating custom classes to override signature functions or (ii) constructing raw transaction objects from scratch and manually signing and serialising.
Evidence 📷
Testing 🧪
yarn
local-setup
yarn test:aa
TODOs ✔️
console.log