-
Notifications
You must be signed in to change notification settings - Fork 88
SmartWallet support (object-oriented structure) #374
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
base: master
Are you sure you want to change the base?
Conversation
🟡 Heimdall Review Status
|
This stops wrapping the signature client-side as this is handled server-side during user operation orchestration.
|
|
||
| export class SmartWallet { | ||
| private model: SmartWalletModel; | ||
| private account: LocalAccount; |
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.
Should we call this "signer" instead, to make it clearer what it does?
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.
imo because this i private account is fine but not a strong opinion
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.
yeah I went with account based on this Viem FAQ: https://viem.sh/docs/faq#why-use-the-terms-wallet--account-instead-of-signer
| return smartWallet; | ||
| } | ||
|
|
||
| public static async connect({smartWalletAddress, account}: {smartWalletAddress: string, account: LocalAccount}): Promise<SmartWallet> { |
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's an interesting experience reading these APIs without documentation - because without the JSDoc, I can't immediately grok what they would do.
I understand there are schelling points with Gnosis Safe, but I personally haven't used it, so I don't have a point of comparison.
Seems to me that, in a vacuum, this method should be called fetch. Just my 2c
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.
imo connect makes sense, you're not really fetching anything (well you are literally calling an api...), the job to be done here is get a SmartWallet object from the users perspective, fetching is an implementation detail. Would vote connect for convention
| return smartWallet; | ||
| } | ||
|
|
||
| public async use({networkId}: {networkId: NetworkIdentifier}) { |
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.
Similar comment here, although it is more parseable. Weirdly, I would probably name this method connect in a vacuum (i.e. the SmartWallet is connecting to a network).
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.
Imo explicit is better, useNetwork. We're not actually connecting to anything locally, we're setting the network of operation
| * Private constructor to prevent direct instantiation outside of the factory methods. | ||
| * | ||
| * @ignore | ||
| * @param userOperationModel - The UserOperation model. |
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.
Missing param for smartWalletAddress
| return `${payloadSignature.getSignature()}` as Hex; | ||
| } | ||
|
|
||
| export async function signTransaction< |
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.
Export these too in index?
| import { WalletAddress } from "../address/wallet_address"; | ||
| import { hashMessage } from "viem"; | ||
|
|
||
| export function toLocalAccount(address: WalletAddress): LocalAccount { |
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.
JSDoc on all exported functions please!
| @@ -0,0 +1,83 @@ | |||
| import { hashTypedData, keccak256, serializeTransaction } from "viem"; | |||
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.
lets start file names using proper TS conventions in this PR
|
|
||
| export class SmartWallet { | ||
| private model: SmartWalletModel; | ||
| private account: LocalAccount; |
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.
imo because this i private account is fine but not a strong opinion
| this.account = account; | ||
| } | ||
|
|
||
| public static async create({account}: {account: LocalAccount}): Promise<SmartWallet> { |
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 agree with the lint, JSDoc on all public and exported function
| return smartWallet; | ||
| } | ||
|
|
||
| public static async connect({smartWalletAddress, account}: {smartWalletAddress: string, account: LocalAccount}): Promise<SmartWallet> { |
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.
imo connect makes sense, you're not really fetching anything (well you are literally calling an api...), the job to be done here is get a SmartWallet object from the users perspective, fetching is an implementation detail. Would vote connect for convention
| return smartWallet; | ||
| } | ||
|
|
||
| public async use({networkId}: {networkId: NetworkIdentifier}) { |
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.
Imo explicit is better, useNetwork. We're not actually connecting to anything locally, we're setting the network of operation
| return this.model.address; | ||
| } | ||
|
|
||
| public async sendUserOperation<T extends readonly unknown[]>( |
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.
jsdoc
| createSmartWallet: ( | ||
| createSmartWalletRequest?: CreateSmartWalletRequest, | ||
| options?: RawAxiosRequestConfig, | ||
| ) => AxiosPromise<SmartWalletModel>; | ||
|
|
||
| /* | ||
| Get the smart wallet by address | ||
| @param smartWalletAddress - The address of the smart wallet to fetch. | ||
| @param options - Override http request option. | ||
| @throws {APIError} If the request fails. | ||
| */ | ||
| getSmartWallet: ( | ||
| smartWalletAddress: string, | ||
| options?: RawAxiosRequestConfig, | ||
| ) => AxiosPromise<SmartWalletModel>; |
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 these stutter when constructed?
const smartWalletClient = SmartWalletAPIClient()
smartWalletClient.createSmartWallet() I guess it depends on the construction
| /** | ||
| * SmartWalletAPI client type definition. | ||
| */ | ||
| export type SmartWalletAPIClient = { |
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.
SmartWalletAPIClient feels a little long to me, maybe just SmartWalletClient or SmartWallet if this is the entrypoint for devs to broadcast transactions might make sense
| }; | ||
|
|
||
| /** | ||
| * SmartWalletAPI client type definition. |
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.
should match the type name
| createSmartWallet: ( | ||
| createSmartWalletRequest?: CreateSmartWalletRequest, | ||
| options?: RawAxiosRequestConfig, | ||
| ) => AxiosPromise<SmartWalletModel>; |
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.
Should this be typed as AxiosPromise or Promise
| * | ||
| * @class | ||
| * @param createdSmartWalletRequest - The smart wallet creation request. | ||
| * @param options - Axios request options. |
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 don't love exposing dependency libs in our docs / api
| networkId: string, | ||
| createUserOperationRequest: CreateUserOperationRequest, | ||
| options?: RawAxiosRequestConfig, | ||
| ) => AxiosPromise<UserOperationModel>; |
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.
same as above on Axios, and same below
| * A representation of a UserOperation, which calls a smart contract method | ||
| * onchain. | ||
| */ | ||
| export class UserOperation { |
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 feels very ruby to me, all functions other than wait and reload are just a property on model or address. Might be worth just using functions.
APIs for review:
Everything in the new
SmartWalletclass,smart_wallet.tscreate()
connect() - alternatives considered were
fetchto align with existingWallet. We likeconnectcause it follows what Gnosis SAFE does: https://docs.safe.global/reference-sdk-protocol-kit/initialization/connectuse() - alternatives considered were
switch()or making it part of theconnect()interface. Functionally it's different thanconnectwhich is responsible for fetching the smart wallet and validating the local account is the owner, which is why a separate function felt more appropriate.We have to invent something because we have no concept of a
providerlike in ethers, orclientlike in viem. We could also consider specifying network as part of theCoinbaseinitialization. That step involves API initialization and doesn't exactly feel appropriate either, but it's closer to a viem DevEx.getSmartWalletAddress()orgetContractAddress()execute(),invoke(),executeBatch(). My issue with sendUserOperation is that while it is low-level function that's similar to viem's sendUserOperation, it's not exactly the same thing since we take a simpler request type as input and orchestrate through our backend.The new
toLocalAccountutility,to_local_account.tscreateViemAccount(that's what Privy calls it), andcreateAccount(that's what Turnkey calls it). I kind of likecreateViemAccountthe mostSee test_smart_wallet.ts and test_viem_account.ts to see how these new APIs are used in practice.