Skip to content
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

Init Wallet SDK #956

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open

Init Wallet SDK #956

wants to merge 19 commits into from

Conversation

Destiner
Copy link
Contributor

@Destiner Destiner commented Feb 18, 2025

Description

Initiates a new Wallet SDK (@crossmint/wallets-sdk) and defines the high-level project structure.

Also introduces code generation based on the OpenAPI schema using HeyAPI and a high-level API client.

Example (to be used internally):

const apiClient = new ApiClient(apiKey);

const walletCreationResponse = await apiClient.createWallet({
  type: "evm-smart-wallet",
  config: {
    adminSigner: {
      type: "evm-keypair",
      address: account.address,
    },
  },
});
const address = walletCreationResponse.address;

Test plan

  • Build passes
  • Stub definitions correspond to the PRD

Copy link

changeset-bot bot commented Feb 18, 2025

⚠️ No Changeset found

Latest commit: bc005e1

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link

vercel bot commented Feb 18, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
crossmint-sdk-remix-ssr ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 25, 2025 2:32pm
1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
smart-wallet-auth-demo ⬜️ Skipped (Inspect) Feb 25, 2025 2:32pm

@vercel vercel bot temporarily deployed to Preview – smart-wallet-auth-demo February 18, 2025 19:17 Inactive
@Destiner Destiner marked this pull request as ready for review February 18, 2025 19:19
...args: WalletTypeToArgs["solana-smart-wallet"]
): Promise<SolanaSmartWallet>;
public async getOrCreateWallet(
type: "solana-mpc-wallet",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks awkward, and a bit harder to maintain, but this will give the consumers great DX:

const wallet = await sdk.getOrCreateWallet("evm-smart-wallet", adminSigner: );
//      ^ this will be typed as `EVMSmartWallet`               ^ this will have full autocomplete

Copy link
Collaborator

Choose a reason for hiding this comment

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

i think u can just make another type map like WalletTypeToWallet to use as the return type on the actual implementation func, and it should do the same thing without needing to define all these

Copy link
Contributor Author

Choose a reason for hiding this comment

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

addressed below

Comment on lines 62 to 65
public async getOrCreateWallet<WalletType extends keyof WalletTypeToArgs>(
_type: WalletType,
..._args: WalletTypeToArgs[WalletType]
): Promise<EVMSmartWallet | EVMMPCWallet> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

like this

Suggested change
public async getOrCreateWallet<WalletType extends keyof WalletTypeToArgs>(
_type: WalletType,
..._args: WalletTypeToArgs[WalletType]
): Promise<EVMSmartWallet | EVMMPCWallet> {
public async getOrCreateWallet<WalletType extends keyof WalletTypeToArgs>(
_type: WalletType,
..._args: WalletTypeToArgs[WalletType]
): Promise<WalletTypeToWallet[WalletType]> {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

nice, this simplifies things a lot! fixed in 9abda4d

}) => Promise<Hex>;
}

export class EVMSmartWallet implements ViemWallet {
Copy link
Collaborator

Choose a reason for hiding this comment

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

you should check the goat repo to see if there is overlap/ can we use this wallet client within goat

we have too many wallet client definitions already...

https://github.com/goat-sdk/goat/tree/main/typescript/packages/wallets

Copy link
Contributor Author

@Destiner Destiner Feb 19, 2025

Choose a reason for hiding this comment

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

Looked into it. Some things are similar, others are different (e.g. we don't need resolveAddress here).

Probably best to keep separate to not have a tight dependency between two different projects.

}) => Promise<string>; // Returns transaction signature
}

export class SolanaSmartWallet implements SolanaWallet {
Copy link
Collaborator

Choose a reason for hiding this comment

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

same about checking goat repo

Copy link
Contributor Author

Choose a reason for hiding this comment

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

addressed above

Comment on lines 3 to 19
const SmartWalletTestnet = {
BASE_SEPOLIA: Blockchain.BASE_SEPOLIA,
POLYGON_AMOY: Blockchain.POLYGON_AMOY,
OPTIMISM_SEPOLIA: Blockchain.OPTIMISM_SEPOLIA,
ARBITRUM_SEPOLIA: Blockchain.ARBITRUM_SEPOLIA,
} as const;
type SmartWalletTestnet = ObjectValues<typeof SmartWalletTestnet>;
const SMART_WALLET_TESTNETS: readonly SmartWalletChain[] = objectValues(SmartWalletTestnet);

const SmartWalletMainnet = {
BASE: Blockchain.BASE,
POLYGON: Blockchain.POLYGON,
OPTIMISM: Blockchain.OPTIMISM,
ARBITRUM: Blockchain.ARBITRUM,
} as const;
type SmartWalletMainnet = ObjectValues<typeof SmartWalletMainnet>;
const SMART_WALLET_MAINNETS: readonly SmartWalletChain[] = objectValues(SmartWalletMainnet);
Copy link
Collaborator

Choose a reason for hiding this comment

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

i think i would just define this as like SMART_WALLET_SUPPORTED_TESTNETS = [Blockchain.BASE_SEPOLIA, ...]

rather than an object and having to re-define all the names of the blockchains

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agree, fixed in 4e3de34

Copy link
Contributor Author

@Destiner Destiner Feb 25, 2025

Choose a reason for hiding this comment

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

There are plenty of tooling to generate the code based on OpenAPI spec. Went with HeyAPI:

  • low-level API (easy to build a high-level SDK around it)
  • can set base URL dynamically
  • has a plugin for zod (not used currently)
  • fetch-based (no runtime dependencies like axios)
  • generates both runtime code and the typings

Comment on lines +33 to +51
type CreateWalletParams = CreateWalletDto;
type CreateWalletResponse = WalletV1Alpha2ResponseDto;
type GetWalletResponse = WalletV1Alpha2ResponseDto;

type CreateTransactionParams = CreateTransactionDto;
type CreateTransactionResponse = WalletsV1Alpha2TransactionResponseDto;
type ApproveTransactionParams = SubmitApprovalDto;
type ApproveTransactionResponse = WalletsV1ControllerGetTransaction4Response;
type GetTransactionResponse = WalletsV1Alpha2TransactionResponseDto;

type CreateSignatureParams = CreateSignatureRequestDto;
type CreateSignatureResponse = WalletsV1Alpha2SignatureResponseDto;
type ApproveSignatureParams = SubmitApprovalDto;
type ApproveSignatureResponse = WalletsV1Alpha2SignatureResponseDto;
type GetSignatureResponse = WalletsV1Alpha2SignatureResponseDto;

type GetTransactionsResponse = WalletsV1Alpha2TransactionsResponseDto;
type GetNftsResponse = Nftevm | Nftsol;
type GetBalanceResponse = WalletBalanceResponseDto;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renaming the types before the export to keep them aligned with ApiClient methods

Comment on lines +80 to +82
headers: {
"X-API-KEY": this.apiKey,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

there seems to be some redundancy here, as we will need to pass this on every call

Any way to abstract that out s e.g. the client takes care of adding this header?


constructor(
private readonly apiKey: string,
private readonly jwt?: string
Copy link
Contributor

Choose a reason for hiding this comment

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

the jwt can change over time (e.g. when the user refreshes the token). im not sure this ap client supports jwt mutations?

type GetNftsResponse = Nftevm | Nftsol;
type GetBalanceResponse = WalletBalanceResponseDto;

class ApiClient {
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought we had already something like a api client class in base SDK. Did you check? Would it be possible to reuse parts of that?

throw new Error("Invalid API key");
}
if (validationResult.usageOrigin === APIKeyUsageOrigin.CLIENT && !jwt) {
throw new Error("JWT is required for client API key");
Copy link
Contributor

Choose a reason for hiding this comment

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

Im nt sure jwt is always needed - why did you reach that conclusion?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it depends on the specific API you;re calling

Copy link
Collaborator

@AlbertoElias AlbertoElias left a comment

Choose a reason for hiding this comment

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

Beyond the discussions happening on Slack around using CrossmintApiClient, looking good!

import { defineConfig } from "@hey-api/openapi-ts";

export default defineConfig({
input: "src/openapi.json",
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we not fetch this from a url so it's always up to date?

Blockchain.ARBITRUM_SEPOLIA,
];

const SMART_WALLET_MAINNET_CHAINS = [Blockchain.BASE, Blockchain.POLYGON, Blockchain.OPTIMISM, Blockchain.ARBITRUM];
Copy link
Collaborator

Choose a reason for hiding this comment

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

don't we support more of them?


import type { SmartWalletChain } from "./chains";

export interface ViemWallet {
Copy link
Collaborator

Choose a reason for hiding this comment

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

interesting that viem doesnt export smth like this haha

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants