Skip to content

Conversation

@guilleasz-crossmint
Copy link
Contributor

@guilleasz-crossmint guilleasz-crossmint commented Oct 29, 2025

Description

Support EVM Shadow Signers in SDK

How it works

  1. We generate a P-256 keypair and return a delegated signer: evm-p256-keypair with publicKey as base64.
  2. Signing uses a storage callback that produces Stubbed WebAuthn-compatible signatures.

Test plan

Manual tested shadow signers with EVM

Package updates

devin-ai-integration bot and others added 30 commits October 14, 2025 09:10
- Add OnCreateConfig<C> type to wallets SDK types
- Update WalletArgsFor<C> to include optional onCreateConfig field
- Modify WalletFactory.createWallet() to use onCreateConfig admin signer when provided
- Update validateExistingWalletConfig() to validate onCreateConfig parameters
- Add validateSignerCanUseWallet() helper to ensure signer can use wallet
- Add getSignerLocator() helper for determining signer locators
- Remove server-side restriction from getWallet() method
- Add comprehensive tests for onCreateConfig functionality
- Update React Base SDK to export OnCreateConfig and support getWallet
- Implement getWallet() in CrossmintWalletBaseProvider
- Update getOrCreateWallet() to pass through onCreateConfig
- Export OnCreateConfig type from React UI SDK

This allows delegated signers to use the SDK by separating the concept of
admin signer (who creates/owns the wallet) from usage signer (who interacts
with it).

Co-Authored-By: Guille <guille.a@paella.dev>
- Export OnCreateConfig from wallets SDK index
- Inline chainType logic in CrossmintWalletBaseProvider instead of accessing private method

Co-Authored-By: Guille <guille.a@paella.dev>
Breaking changes:
- Remove delegatedSigners field from WalletArgsFor type
- Remove delegatedSigners field from CreateOnLogin type
- delegatedSigners now ONLY exist within onCreateConfig

When onCreateConfig is provided:
- args.signer = the signer that will USE the wallet (can be admin or delegated)
- onCreateConfig.adminSigner = the admin who OWNS the wallet (only for creation)
- onCreateConfig.delegatedSigners = delegated signers (only for creation)

When onCreateConfig is NOT provided (backward compat):
- args.signer acts as BOTH the admin and usage signer

Updated:
- WalletFactory validation logic to handle both cases
- React providers to not pass delegatedSigners
- Tests to only use onCreateConfig for delegated signers

Co-Authored-By: Guille <guille.a@paella.dev>
- Created WalletCreateArgs type that extends WalletArgsFor with required onCreateConfig
- Updated getOrCreateWallet and createWallet to use WalletCreateArgs
- Updated CreateOnLogin type to use WalletCreateArgs
- Made onCreateConfig optional in WalletArgsFor for getWallet use cases
- args.signer is now always the usage signer, not the admin signer

Co-Authored-By: Guille <guille.a@paella.dev>
Co-Authored-By: Guille <guille.a@paella.dev>
- Updated smart-wallet/next, quickstart-devkit, and expo apps
- All createOnLogin usages now include onCreateConfig with adminSigner
- Delegated signers moved from top-level to onCreateConfig

Co-Authored-By: Guille <guille.a@paella.dev>
- Create tempArgs to capture mutation from mutateSignerFromCustomAuth
- Reassign expectedAdminSigner from mutated tempArgs.signer
- Ensures external wallet signer reassignment is captured correctly

Co-Authored-By: Guille <guille.a@paella.dev>
- Create tempArgs to capture mutation from mutateSignerFromCustomAuth
- Reassign adminSignerConfig from mutated tempArgs.signer
- Ensures external wallet signer reassignment works correctly

Co-Authored-By: Guille <guille.a@paella.dev>
- Add shadowSigner option to WalletOptions type
- Create shadow-signer.ts utility for generating device-bound keypairs
- Automatically generate shadow signers during wallet creation (client-side only)
- Store shadow signer metadata in localStorage
- Pass shadow signer configuration through React providers
- Support ed25519 for Solana/Stellar and p256 passkeys for EVM chains
- Gracefully handle shadow signer creation failures

Co-Authored-By: Guille <guille.a@paella.dev>
Co-Authored-By: Guille <guille.a@paella.dev>
- Create SmartWalletConfig type to properly type wallet.config
- Replace all 'as any' casts with SmartWalletConfig type
- Improve error message clarity by avoiding 'args' in developer-facing text
- Addresses PR feedback from Alberto

Co-Authored-By: Guille <guille.a@paella.dev>
Co-Authored-By: Guille <guille.a@paella.dev>
@changeset-bot
Copy link

changeset-bot bot commented Oct 29, 2025

🦋 Changeset detected

Latest commit: 7e6c42e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
Name Type
@crossmint/wallets-sdk Minor
@crossmint/client-sdk-react-native-ui Patch
@crossmint/common-sdk-base Patch
expo-demo Patch
@crossmint/client-sdk-react-base Patch
@crossmint/client-sdk-react-ui Patch
@crossmint/client-sdk-auth Patch
@crossmint/client-sdk-base Patch
@crossmint/client-sdk-verifiable-credentials Patch
@crossmint/client-sdk-smart-wallet Patch
@crossmint/client-sdk-walletconnect Patch
@crossmint/common-sdk-auth Patch
@crossmint/server-sdk Patch
@crossmint/auth-ssr-nextjs-demo Patch
@crossmint/client-sdk-nextjs-starter Patch
@crossmint/wallets-quickstart-devkit Patch
@crossmint/client-sdk-smart-wallet-next-starter Patch
crossmint-auth-node Patch

Not sure what this means? Click here to learn what changesets are.

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

@vercel
Copy link

vercel bot commented Oct 29, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
smart-wallet-auth-demo Ignored Ignored Nov 26, 2025 5:23pm

@guilleasz-crossmint guilleasz-crossmint force-pushed the guillea/wal-7240-add-support-for-evm-shadow-signers-in-sdk branch from e867852 to 5faf63b Compare October 30, 2025 15:27
…guillea/wal-7240-add-support-for-evm-shadow-signers-in-sdk
@guilleasz-crossmint guilleasz-crossmint force-pushed the guillea/wal-7240-add-support-for-evm-shadow-signers-in-sdk branch from 5faf63b to 2af0201 Compare October 30, 2025 15:28
}

hasShadowSigner(): this is { signer: ExternalWalletSigner<C> } {
hasShadowSigner(): this is { signer: S } {
Copy link
Contributor

@alberto-crossmint alberto-crossmint Nov 26, 2025

Choose a reason for hiding this comment

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

I feel this is a bit confusing! A Shadow Signer gets casted to a subtype so there's an effective isomorphism between this and this.signer. What are the situations in which ShadowSigner.hasShadowSigner() is false. Wouldn't it fit better in an <ncs>.shadowSigner: ShadowSigner | null schema and if it exists we assume it "hasShadowSigner"?

Not saying this is wrong! Just to understand the motivation

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The ShadowSigner instance is the one that initializes the signer when the wallet has a shadow signer. if we move that logic up to the ncs wallet we would need to move the shadow signer retrieval logic to each of this signers, this keeps it simpler by making all the other signers to have a shadowSigner instance which confirms if the wallet has or has not a shadow signer attached to it

type = "p256-keypair" as const;
private _address: string;
private _locator: string;
private onSignTransaction: (publicKeyBase64: string, data: Uint8Array) => Promise<Uint8Array>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need the publicKey as an input in this callback! Isn't it kept as an attribute of the class? Isn't it the value in _address?

Copy link
Contributor Author

@guilleasz-crossmint guilleasz-crossmint Nov 26, 2025

Choose a reason for hiding this comment

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

Yes but onSignTransaction is defined as a config passed in the constructor, when defined it doesn't have access to this this. Check packages/wallets/src/signers/shadow-signer/evm-shadow-signer.ts:22

constructor(
config: ExternalWalletInternalSignerConfig<EVMChain>,
walletAddress?: string,
shadowSignerEnabled?: boolean,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we know this by checking if this.shadowSigner is not nullish?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

shadowSignerEnabled comes from the wallets config from the args. It could happen that a wallet was created with a shadow signer, but now the client is disabling its use through the config

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.

3 participants