Skip to content

Conversation

@rohan-agarwal-coinbase
Copy link
Contributor

@rohan-agarwal-coinbase rohan-agarwal-coinbase commented Feb 5, 2025

APIs for review:
Everything in the new SmartWallet class, smart_wallet.ts

  • create()

  • connect() - alternatives considered were fetch to align with existing Wallet. We like connect cause it follows what Gnosis SAFE does: https://docs.safe.global/reference-sdk-protocol-kit/initialization/connect

  • use() - alternatives considered were switch() or making it part of the connect() interface. Functionally it's different than connect which 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 provider like in ethers, or client like in viem. We could also consider specifying network as part of the Coinbase initialization. That step involves API initialization and doesn't exactly feel appropriate either, but it's closer to a viem DevEx.

  • getAddress() - alternatives considered were getSmartWalletAddress() or getContractAddress()
  • sendUserOperation() - alternatives considered were 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 toLocalAccount utility, to_local_account.ts

  • toLocalAccount() - alternatives we're considering are createViemAccount (that's what Privy calls it), and createAccount (that's what Turnkey calls it). I kind of like createViemAccount the most

See test_smart_wallet.ts and test_viem_account.ts to see how these new APIs are used in practice.

@cb-heimdall
Copy link

cb-heimdall commented Feb 5, 2025

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

@rohan-agarwal-coinbase rohan-agarwal-coinbase changed the title WIP SmartWallet support WIP Feb 5, 2025

export class SmartWallet {
private model: SmartWalletModel;
private account: LocalAccount;
Copy link
Contributor

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?

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

Copy link
Contributor Author

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> {
Copy link
Contributor

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

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}) {
Copy link
Contributor

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).

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.
Copy link
Contributor

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<
Copy link
Contributor

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 {

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";

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;

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> {

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> {

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}) {

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[]>(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsdoc

Comment on lines +264 to +279
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>;

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 = {

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.

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>;

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.

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>;

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 {

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.

@rohan-agarwal-coinbase rohan-agarwal-coinbase changed the title SmartWallet support WIP SmartWallet support (object-oriented structure) Feb 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

5 participants