Skip to content

Conversation

@guilleasz-crossmint
Copy link
Contributor

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

Description

This PR implements automatic "shadow" delegated signers for wallets created via getOrCreateWallet. Shadow signers are device-bound keypairs (stored using Web Crypto API's non-extractable keys) that act as delegated signers, intended to allow transactions without requiring OTP authentication on the same device.

Shadow signers are only added to Stellar and Solana wallets, a follow up PR will add it for EVM.

Important Notes:

  • Shadow signers are enabled by default - users must explicitly set shadowSigner: { enabled: false } to opt out

Changes Made:

  1. New shadow-signer.ts utility:

    • generateShadowSigner(): Creates device-bound keypairs using Web Crypto API
    • Ed25519 for Solana/Stellar (as external-wallet delegated signers)
    • localStorage functions for storing/retrieving shadow signer metadata
  2. WalletFactory modifications:

    • Automatically generates shadow signer during wallet creation (client-side only)
    • Adds shadow signer to delegated signers array
    • Stores metadata after successful wallet creation
    • Adds shadow signer as signer of the wallet instance and handles the signing of transactions
  3. React Provider updates:

    • Added shadowSigner prop to CrossmintWalletBaseProviderProps
    • Passes configuration through to wallet creation

Test plan

Manual tested in quickstart with solana and stellar wallets, no email otp was necessary for fulfilling transactions

Package updates

This are part of the delegated signers breaking changes, so it goes to the wallets-v1 branch

Session Info:

devin-ai-integration bot and others added 26 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 <[email protected]>
- Export OnCreateConfig from wallets SDK index
- Inline chainType logic in CrossmintWalletBaseProvider instead of accessing private method

Co-Authored-By: Guille <[email protected]>
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 <[email protected]>
- 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 <[email protected]>
- 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 <[email protected]>
- 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 <[email protected]>
- Create tempArgs to capture mutation from mutateSignerFromCustomAuth
- Reassign adminSignerConfig from mutated tempArgs.signer
- Ensures external wallet signer reassignment works correctly

Co-Authored-By: Guille <[email protected]>
- 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 <[email protected]>
@devin-ai-integration
Copy link
Contributor

Original prompt from Guille
Create a new branch basing yourself first in guillea/wal-4751-allowing-delegated-signers-to-use-the-sdk


## Implementation Plan for Shadow Signers

You're asking for a comprehensive implementation plan to add automatic "shadow" delegated signers to wallets created via `getOrCreateWallet` in the Crossmint SDK. This feature would create a device-bound keypair using Web Crypto API that acts as a delegated signer, allowing transactions without requiring OTP authentication on the same device.

### High-Level Architecture

The implementation needs to modify the wallet creation flow in `packages/wallets/src/wallets/wallet-factory.ts` and `packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx` to automatically generate and add shadow signers. [6-cite-0](#6-cite-0) 

### Detailed Implementation Steps

#### 1. Add Shadow Signer Configuration to WalletOptions

**File: `packages/wallets/src/wallets/types.ts`**

Add a new option to control shadow signer behavior:

```typescript
export type WalletOptions = {
  clientTEEConnection?: () => HandshakeParent<typeof signerOutboundEvents, typeof signerInboundEvents>;
  experimental_callbacks?: {
    onWalletCreationStart?: () => Promise<void>;
    onTransactionStart?: () => Promise<void>;
  };
  // NEW: Add shadow signer configuration
  shadowSigner?: {
    enabled?: boolean; // defaults to true
  };
};

2. Create Shadow Signer Generation Utility

File: packages/wallets/src/utils/shadow-signer.ts (new file)

Create utilities for generating and storing shadow signers:

import { WebAuthnP256 } from "ox";
import type { Chain } from "../chains/chains";
import type { DelegatedSigner } from "../wallets/types";

const SHADOW_SIGNER_STORAGE_KEY = "crossmint_shadow_signer";

export type ShadowSignerData = {
  chain: Chain;
  walletAddress: string;
  publicKey: string;
  createdAt: number;
};

export async function generateShadowSigner(
  chain: Chain
): Promise<{ delegatedSigner: DelegatedSigner; ... (17104 chars truncated...)

@changeset-bot
Copy link

changeset-bot bot commented Oct 16, 2025

⚠️ No Changeset found

Latest commit: 4d2b684

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

@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@vercel
Copy link

vercel bot commented Oct 16, 2025

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

Project Deployment Preview Comments Updated (UTC)
crossmint-sdk-quickstart-devkit Error Error Oct 23, 2025 11:58pm
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
smart-wallet-auth-demo Ignored Ignored Oct 23, 2025 11:58pm

Base automatically changed from guillea/wal-4751-allowing-delegated-signers-to-use-the-sdk to wallets-v1 October 20, 2025 22:36
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.

Left some comments suggesting a refactor so the code lives closer to ncs-signer

@devin-ai-integration
Copy link
Contributor

Thanks for the review @AlbertoElias! I understand the proposed refactor to move this into the NCS signer layer. Just to make sure I understand the architecture correctly:

  1. Shadow signer config location: The config would move from WalletOptions.shadowSigner to inside the email/phone signer config (e.g., { type: "email", email: "...", options: { sessions: { duration: "" } } })

  2. Shadow signer generation: Instead of generating in WalletFactory.createWallet, we'd call a method like ncsSigner.getShadowSigner() that generates and stores the shadow signer

  3. Shadow signer usage: The signTransaction method in each chain-specific NCS signer would:

    • Check if a shadow signer exists for this device/wallet
    • If yes, use it to sign (bypassing OTP)
    • If no, fall back to the current OTP flow

A few questions before I proceed with the refactor:

  • Should the shadow signer still be registered as a delegated signer during wallet creation, or should that be handled differently?
  • For the signer config, should it look like: { type: "email", email: "...", shadowSigner: { enabled: true, duration: "..." } }?
  • The shadow signer would still be device-specific (stored in IndexedDB + localStorage), correct?

Let me know if my understanding is correct and I'll proceed with the refactor!

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.

Looking really good! left some small comments, but leaving approved. remember to add changeset

shadowSignerPrivateKey: CryptoKey | null;
}> {
const {
delegatedSigners: updatedDelegatedSigners,
Copy link
Collaborator

Choose a reason for hiding this comment

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

i would keep this called as delegatedSigners and the one below can have a more specific name for the changes/enhancements we do to signers


const delegatedSigners = await Promise.all(
updatedDelegatedSigners?.map(
async (signer): Promise<DelegatedSigner | RegisterSignerParams | { signer: PasskeySignerConfig }> => {
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 we should separate this to a separate function with a nice name to make it more declarative

- Replace all @ imports with relative imports
- Change != null to == null for style consistency
- Rename updatedDelegatedSigners to delegatedSigners for clarity
- Extract delegated signer registration logic to registerDelegatedSigners method

Co-Authored-By: Guille <[email protected]>
@devin-ai-integration
Copy link
Contributor

@AlbertoElias Thanks for the approval! I've addressed all your feedback:

✅ Fixed != null to == null for style consistency
✅ Replaced all @ imports with relative imports
✅ Renamed updatedDelegatedSignersdelegatedSigners for clarity
✅ Extracted delegated signer registration to registerDelegatedSigners() method
✅ Changeset already added by Guille

The refactoring makes the code more declarative and easier to maintain. All changes pushed!

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.

2 participants