Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
2033ee9
feat: add onCreateConfig support to separate admin and usage signers
devin-ai-integration[bot] Oct 14, 2025
8a5417a
fix: export OnCreateConfig and fix getChainType access
devin-ai-integration[bot] Oct 14, 2025
a69a978
refactor: remove args.delegatedSigners field (BREAKING CHANGE)
devin-ai-integration[bot] Oct 14, 2025
8089157
feat: make onCreateConfig required via WalletCreateArgs type
devin-ai-integration[bot] Oct 14, 2025
c675932
chore: fix lint issues
devin-ai-integration[bot] Oct 14, 2025
b642c2c
chore: update demo apps to use onCreateConfig
devin-ai-integration[bot] Oct 14, 2025
41e35a2
fix: correctly mutate signer in validateExistingWalletConfig
devin-ai-integration[bot] Oct 14, 2025
eb5c28e
fix: correctly mutate adminSignerConfig in createWallet
devin-ai-integration[bot] Oct 14, 2025
1be9a9f
remove unnecesary type
guilleasz-crossmint Oct 14, 2025
cfde33f
reuse common functionality from get and create
guilleasz-crossmint Oct 14, 2025
d59062a
remove redundant comments
guilleasz-crossmint Oct 14, 2025
c1ea37e
fix tsc issue
guilleasz-crossmint Oct 14, 2025
6b8b3f3
fix linter
guilleasz-crossmint Oct 14, 2025
3e6d5c2
added client side specific get wallet
guilleasz-crossmint Oct 14, 2025
133dc92
fix signers and transactions
guilleasz-crossmint Oct 15, 2025
5bd3ce4
fix delegated signer
guilleasz-crossmint Oct 15, 2025
34f6f16
remove redundant check
guilleasz-crossmint Oct 15, 2025
e0397c1
fix unit test
guilleasz-crossmint Oct 15, 2025
9276bcf
make oncreateconfig optional
guilleasz-crossmint Oct 15, 2025
f890560
remove unnecesary format changes
guilleasz-crossmint Oct 15, 2025
b04e71d
unify getWallet functionality
guilleasz-crossmint Oct 16, 2025
43c64d8
remove admin from name
guilleasz-crossmint Oct 16, 2025
77faeca
fix react provider
guilleasz-crossmint Oct 16, 2025
6891aa5
remove unnecessary types
guilleasz-crossmint Oct 16, 2025
ee8195b
remove optional param
guilleasz-crossmint Oct 16, 2025
199d2ab
feat(wallets): add shadow signer support for automatic delegated signers
devin-ai-integration[bot] Oct 16, 2025
1b0eba3
fix: remove unused @ts-expect-error directive
devin-ai-integration[bot] Oct 16, 2025
a1afbb5
refactor: replace 'as any' with custom SmartWalletConfig type
devin-ai-integration[bot] Oct 17, 2025
05fbb32
fix: format error message to meet line length requirements
devin-ai-integration[bot] Oct 17, 2025
75a3e8f
refactor: remove Flow-specific check, let API handle incompatible chains
devin-ai-integration[bot] Oct 17, 2025
c23d2b6
feat: use shadow signer as active signer for wallet instances
devin-ai-integration[bot] Oct 17, 2025
3d92392
fix: apply biome lint formatting
devin-ai-integration[bot] Oct 17, 2025
dbaf09d
fix: add localStorage checks for Node.js test environment
devin-ai-integration[bot] Oct 17, 2025
1351429
Merge branch 'main' of github.com:Crossmint/crossmint-sdk into devin/…
guilleasz-crossmint Oct 17, 2025
050c6be
wip shadow
guilleasz-crossmint Oct 17, 2025
6cd46ee
remove support for evm
guilleasz-crossmint Oct 20, 2025
c66cb23
add passkey logic to delegate signer comparison
guilleasz-crossmint Oct 20, 2025
14914bc
Merge branch 'guillea/wal-4751-allowing-delegated-signers-to-use-the-…
guilleasz-crossmint Oct 20, 2025
b588a95
fix wallet with multiple passkey signers
guilleasz-crossmint Oct 20, 2025
e5516f0
remove console.log
guilleasz-crossmint Oct 20, 2025
8b024c7
remove all shadow passkey references
guilleasz-crossmint Oct 20, 2025
4da2a7f
appplied albertos comment
guilleasz-crossmint Oct 20, 2025
9c31e78
Merge branch 'guillea/wal-4751-allowing-delegated-signers-to-use-the-…
guilleasz-crossmint Oct 20, 2025
69a147a
differentiate wallet creation for stellar and solana
guilleasz-crossmint Oct 20, 2025
c34c4e2
Merge branch 'wallets-v1' of github.com:Crossmint/crossmint-sdk into …
guilleasz-crossmint Oct 20, 2025
8f2aca8
fix shadow signer for stellar
guilleasz-crossmint Oct 21, 2025
55e291f
move signing shado logic to signer
guilleasz-crossmint Oct 23, 2025
6aace1b
change shado signer option config
guilleasz-crossmint Oct 23, 2025
a550bb8
move delegated signer logic to a new function
guilleasz-crossmint Oct 23, 2025
16f5f1b
abstract signature
guilleasz-crossmint Oct 23, 2025
bc49080
remove unnecessary change
guilleasz-crossmint Oct 23, 2025
28682a3
restructure folder
guilleasz-crossmint Oct 23, 2025
af82106
improve has shadow signer condition
guilleasz-crossmint Oct 23, 2025
3b85db9
fix import
guilleasz-crossmint Oct 23, 2025
adaee17
fix import
guilleasz-crossmint Oct 23, 2025
ea04677
Merge branch 'wallets-v1' of github.com:Crossmint/crossmint-sdk into …
guilleasz-crossmint Oct 23, 2025
7a345c5
fix vitest test in wallets package
guilleasz-crossmint Oct 23, 2025
0df29c4
refactor: address PR feedback from Alberto
devin-ai-integration[bot] Oct 23, 2025
4d2b684
fix: apply biome formatting
devin-ai-integration[bot] Oct 23, 2025
6c4f679
Merge branch 'wallets-v1' into devin/wal-shadow-signers-1760630286
guilleasz-crossmint Oct 28, 2025
ee6c518
applied comments
guilleasz-crossmint Oct 28, 2025
ccacf80
Merge branch 'devin/wal-shadow-signers-1760630286' of github.com:Cros…
guilleasz-crossmint Oct 28, 2025
c42de5f
added changeset
guilleasz-crossmint Oct 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/sixty-ladybugs-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@crossmint/client-sdk-react-ui": minor
"@crossmint/client-sdk-react-base": minor
"@crossmint/wallets-sdk": minor
---

Added Shadow Signers for avoiding redundant OTP Verification
22 changes: 5 additions & 17 deletions packages/wallets/src/signers/evm-external-wallet.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,18 @@
import type { Account, EIP1193Provider as ViemEIP1193Provider } from "viem";
import type { GenericEIP1193Provider, Signer, ExternalWalletInternalSignerConfig } from "./types";
import type { GenericEIP1193Provider, ExternalWalletInternalSignerConfig } from "./types";
import type { EVMChain } from "@/chains/chains";
import { ExternalWalletSigner } from "./external-wallet-signer";

export class EVMExternalWalletSigner implements Signer {
type = "external-wallet" as const;
private _address: string;
export class EVMExternalWalletSigner extends ExternalWalletSigner<EVMChain> {
provider?: GenericEIP1193Provider | ViemEIP1193Provider;
viemAccount?: Account;

constructor(private config: ExternalWalletInternalSignerConfig<EVMChain>) {
if (config.address == null) {
throw new Error("Please provide an address for the External Wallet Signer");
}
this._address = config.address;
constructor(config: ExternalWalletInternalSignerConfig<EVMChain>) {
super(config);
this.provider = config.provider;
this.viemAccount = config.viemAccount;
}

address() {
return this._address;
}

locator() {
return this.config.locator;
}

async signMessage(message: string) {
if (this.provider != null) {
const signature = await this.provider.request({
Expand Down
25 changes: 25 additions & 0 deletions packages/wallets/src/signers/external-wallet-signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { ExternalWalletInternalSignerConfig, Signer } from "./types";
import type { Chain } from "../chains/chains";

export abstract class ExternalWalletSigner<C extends Chain> implements Signer {
type = "external-wallet" as const;
protected _address: string;

constructor(protected config: ExternalWalletInternalSignerConfig<C>) {
if (config.address == null) {
throw new Error("Please provide an address for the External Wallet Signer");
}
this._address = config.address;
}

address() {
return this._address;
}

locator() {
return this.config.locator;
}

abstract signMessage(message: string): Promise<{ signature: string }>;
abstract signTransaction(transaction: string): Promise<{ signature: string }>;
}
10 changes: 7 additions & 3 deletions packages/wallets/src/signers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ import type { Chain } from "../chains/chains";
import type { InternalSignerConfig, Signer } from "./types";
import { StellarExternalWalletSigner } from "./stellar-external-wallet";

export function assembleSigner<C extends Chain>(chain: C, config: InternalSignerConfig<C>): Signer {
export function assembleSigner<C extends Chain>(
chain: C,
config: InternalSignerConfig<C>,
walletAddress: string
): Signer {
switch (config.type) {
case "email":
case "phone":
if (chain === "solana") {
return new SolanaNonCustodialSigner(config);
return new SolanaNonCustodialSigner(config, walletAddress);
}
if (chain === "stellar") {
return new StellarNonCustodialSigner(config);
return new StellarNonCustodialSigner(config, walletAddress);
}
return new EVMNonCustodialSigner(config);
case "api-key":
Expand Down
11 changes: 10 additions & 1 deletion packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type { EmailInternalSignerConfig, PhoneInternalSignerConfig } from "../types";
import type {
EmailInternalSignerConfig,
ExternalWalletInternalSignerConfig,
PhoneInternalSignerConfig,
} from "../types";
import { NonCustodialSigner, DEFAULT_EVENT_OPTIONS } from "./ncs-signer";
import { PersonalMessage } from "ox";
import { isHex, toHex, type Hex } from "viem";
import type { EVMChain } from "../../chains/chains";

export class EVMNonCustodialSigner extends NonCustodialSigner {
constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig) {
Expand Down Expand Up @@ -64,4 +69,8 @@ export class EVMNonCustodialSigner extends NonCustodialSigner {
);
}
}

protected getShadowSignerConfig(): ExternalWalletInternalSignerConfig<EVMChain> {
throw new Error("Shadow signer not implemented for EVM chains");
}
}
41 changes: 40 additions & 1 deletion packages/wallets/src/signers/non-custodial/ncs-signer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import type { BaseSignResult, EmailInternalSignerConfig, PhoneInternalSignerConfig, Signer } from "../types";
import type {
BaseSignResult,
EmailInternalSignerConfig,
ExternalWalletInternalSignerConfig,
PhoneInternalSignerConfig,
Signer,
} from "../types";
import { AuthRejectedError } from "../types";
import { NcsIframeManager } from "./ncs-iframe-manager";
import { validateAPIKey } from "@crossmint/common-sdk-base";
import type { SignerOutputEvent } from "@crossmint/client-signers";
import { getShadowSigner, hasShadowSigner, type ShadowSignerData } from "../shadow-signer";
import type { Chain } from "../../chains/chains";
import type { ExternalWalletSigner } from "../external-wallet-signer";

export abstract class NonCustodialSigner implements Signer {
public readonly type: "email" | "phone";
Expand All @@ -13,17 +22,24 @@ export abstract class NonCustodialSigner implements Signer {
reject: (error: Error) => void;
} | null = null;
private _initializationPromise: Promise<void> | null = null;
protected shadowSigner: ExternalWalletSigner<Chain> | null = null;

constructor(protected config: EmailInternalSignerConfig | PhoneInternalSignerConfig) {
this.initialize();
this.type = this.config.type;
}

locator() {
if (this.shadowSigner != null) {
return this.shadowSigner.locator();
}
return this.config.locator;
}

address() {
if (this.shadowSigner != null) {
return this.shadowSigner.address();
}
return this.config.address;
}

Expand Down Expand Up @@ -83,6 +99,10 @@ export abstract class NonCustodialSigner implements Signer {
}

protected async handleAuthRequired() {
if (this.shadowSigner != null) {
return;
}

const clientTEEConnection = await this.getTEEConnection();

if (this.config.onAuthRequired == null) {
Expand Down Expand Up @@ -267,6 +287,25 @@ export abstract class NonCustodialSigner implements Signer {
this._authPromise?.reject(error);
throw error;
}

protected abstract getShadowSignerConfig(
shadowSigner: ShadowSignerData,
walletAddress: string
): ExternalWalletInternalSignerConfig<Chain>;

protected initializeShadowSigner<C extends Chain>(
walletAddress: string,
ExternalWalletSignerClass: new (config: ExternalWalletInternalSignerConfig<C>) => ExternalWalletSigner<C>
) {
if (hasShadowSigner(walletAddress)) {
const shadowSigner = getShadowSigner(walletAddress);
if (shadowSigner != null && this.config.shadowSigner?.enabled !== false) {
this.shadowSigner = new ExternalWalletSignerClass(
this.getShadowSignerConfig(shadowSigner, walletAddress) as ExternalWalletInternalSignerConfig<C>
);
}
}
}
}

export const DEFAULT_EVENT_OPTIONS = {
Expand Down
42 changes: 39 additions & 3 deletions packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import { VersionedTransaction } from "@solana/web3.js";
import { PublicKey, VersionedTransaction } from "@solana/web3.js";
import base58 from "bs58";
import type { EmailInternalSignerConfig, PhoneInternalSignerConfig } from "../types";
import type {
EmailInternalSignerConfig,
ExternalWalletInternalSignerConfig,
PhoneInternalSignerConfig,
} from "../types";
import { NonCustodialSigner, DEFAULT_EVENT_OPTIONS } from "./ncs-signer";
import { getShadowSignerPrivateKey, type ShadowSignerData } from "../shadow-signer";
import { SolanaExternalWalletSigner } from "../solana-external-wallet";
import type { SolanaChain } from "../../chains/chains";

export class SolanaNonCustodialSigner extends NonCustodialSigner {
constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig) {
constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig, walletAddress: string) {
super(config);
this.initializeShadowSigner(walletAddress, SolanaExternalWalletSigner);
}

async signMessage() {
return await Promise.reject(new Error("signMessage method not implemented for email signer"));
}

async signTransaction(transaction: string): Promise<{ signature: string }> {
if (this.shadowSigner != null) {
return await this.shadowSigner.signTransaction(transaction);
}
await this.handleAuthRequired();
const jwt = this.getJwtOrThrow();

Expand Down Expand Up @@ -60,4 +71,29 @@ export class SolanaNonCustodialSigner extends NonCustodialSigner {
);
}
}

protected getShadowSignerConfig(
shadowData: ShadowSignerData,
walletAddress: string
): ExternalWalletInternalSignerConfig<SolanaChain> {
return {
type: "external-wallet",
address: shadowData.publicKey,
locator: `external-wallet:${shadowData.publicKey}`,
onSignTransaction: async (transaction) => {
const privateKey = await getShadowSignerPrivateKey(walletAddress);
if (privateKey == null) {
throw new Error("Shadow signer private key not found");
}

const messageBytes = new Uint8Array(transaction.message.serialize());
const signatureBuffer = await window.crypto.subtle.sign({ name: "Ed25519" }, privateKey, messageBytes);

const signature = new Uint8Array(signatureBuffer);
transaction.addSignature(new PublicKey(shadowData.publicKey), signature);

return transaction;
},
};
}
}
42 changes: 40 additions & 2 deletions packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import type { EmailInternalSignerConfig, PhoneInternalSignerConfig } from "../types";
import { getShadowSignerPrivateKey, type ShadowSignerData } from "../shadow-signer";
import type {
EmailInternalSignerConfig,
ExternalWalletInternalSignerConfig,
PhoneInternalSignerConfig,
} from "../types";
import { DEFAULT_EVENT_OPTIONS, NonCustodialSigner } from "./ncs-signer";
import { StellarExternalWalletSigner } from "../stellar-external-wallet";
import type { StellarChain } from "../../chains/chains";

export class StellarNonCustodialSigner extends NonCustodialSigner {
constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig) {
constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig, walletAddress: string) {
super(config);
this.initializeShadowSigner(walletAddress, StellarExternalWalletSigner);
}

async signMessage() {
return await Promise.reject(new Error("signMessage method not implemented for stellar signer"));
}

async signTransaction(payload: string): Promise<{ signature: string }> {
if (this.shadowSigner != null) {
return await this.shadowSigner.signTransaction(payload);
}
await this.handleAuthRequired();
const jwt = this.getJwtOrThrow();

Expand Down Expand Up @@ -58,4 +69,31 @@ export class StellarNonCustodialSigner extends NonCustodialSigner {
);
}
}

protected getShadowSignerConfig(
shadowData: ShadowSignerData,
walletAddress: string
): ExternalWalletInternalSignerConfig<StellarChain> {
return {
type: "external-wallet",
address: shadowData.publicKey,
locator: `external-wallet:${shadowData.publicKey}`,
onSignStellarTransaction: async (payload) => {
const privateKey = await getShadowSignerPrivateKey(walletAddress);
if (privateKey == null) {
throw new Error("Shadow signer private key not found");
}

const transactionString = typeof payload === "string" ? payload : (payload as any).tx;

const messageBytes = Uint8Array.from(atob(transactionString), (c) => c.charCodeAt(0));

const signatureBuffer = await window.crypto.subtle.sign({ name: "Ed25519" }, privateKey, messageBytes);

const signature = new Uint8Array(signatureBuffer);
const signatureBase64 = btoa(String.fromCharCode(...signature));
return signatureBase64;
},
};
}
}
Loading