From 2033ee994d88a433570e590228489d97cb66b406 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:10:06 +0000 Subject: [PATCH 01/82] feat: add onCreateConfig support to separate admin and usage signers - Add OnCreateConfig type to wallets SDK types - Update WalletArgsFor 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 --- .../providers/CrossmintWalletBaseProvider.tsx | 83 +++++++- .../client/react-base/src/types/wallet.ts | 5 + .../client/ui/react-ui/src/types/wallet.ts | 2 +- packages/wallets/src/wallets/types.ts | 6 + .../src/wallets/wallet-factory.test.ts | 195 ++++++++++++++++++ .../wallets/src/wallets/wallet-factory.ts | 88 ++++++-- 6 files changed, 363 insertions(+), 16 deletions(-) diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index c046069af..4c26b1410 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -17,6 +17,9 @@ export type CrossmintWalletBaseContext = { wallet: Wallet | undefined; status: "not-loaded" | "in-progress" | "loaded" | "error"; getOrCreateWallet: (props: WalletArgsFor) => Promise | undefined>; + getWallet: ( + props: Pick, "chain" | "signer"> + ) => Promise | undefined>; onAuthRequired?: EmailSignerConfig["onAuthRequired"] | PhoneSignerConfig["onAuthRequired"]; clientTEEConnection?: () => HandshakeParent; }; @@ -25,6 +28,7 @@ export const CrossmintWalletBaseContext = createContext Promise.resolve(undefined), + getWallet: () => Promise.resolve(undefined), onAuthRequired: undefined, clientTEEConnection: undefined, }); @@ -122,6 +126,7 @@ export function CrossmintWalletBaseProvider({ owner: args.owner, plugins: args.plugins, delegatedSigners: args.delegatedSigners, + onCreateConfig: args.onCreateConfig, options: { clientTEEConnection: clientTEEConnection?.(), experimental_callbacks: { @@ -143,6 +148,81 @@ export function CrossmintWalletBaseProvider({ [crossmint, experimental_customAuth] ); + const getWallet = useCallback( + async (args: Pick, "chain" | "signer">) => { + if (experimental_customAuth?.jwt == null) { + return undefined; + } + + try { + const wallets = CrossmintWallets.from(crossmint); + + let signer = args.signer; + + if (signer.type === "email") { + const email = signer.email ?? experimental_customAuth?.email; + const _onAuthRequired = signer.onAuthRequired ?? onAuthRequired; + + if (email == null) { + throw new Error( + "Email not found in experimental_customAuth or signer. Please set email in experimental_customAuth or signer." + ); + } + signer = { + ...signer, + email, + onAuthRequired: _onAuthRequired, + }; + } + + if (signer.type === "phone") { + const phone = signer.phone ?? experimental_customAuth?.phone; + const _onAuthRequired = signer.onAuthRequired ?? onAuthRequired; + + if (phone == null) { + throw new Error("Phone not found in signer. Please set phone in signer."); + } + signer = { + ...signer, + phone, + onAuthRequired: _onAuthRequired, + }; + } + + if (signer.type === "external-wallet") { + const resolvedSigner = + signer.address != null ? signer : experimental_customAuth.externalWalletSigner; + + if (resolvedSigner == null) { + throw new Error( + "External wallet config not found in experimental_customAuth or signer. Please set it in experimental_customAuth or signer." + ); + } + signer = resolvedSigner as SignerConfigForChain; + } + + if (signer.type === "email" || signer.type === "phone") { + await initializeWebView?.(); + } + + const walletLocator = `me:${wallets["getChainType"](args.chain)}:smart`; + const wallet = await wallets.getWallet(walletLocator, { + chain: args.chain, + signer, + options: { + clientTEEConnection: clientTEEConnection?.(), + experimental_callbacks: callbacks, + }, + }); + return wallet; + } catch (error) { + console.error("Failed to get wallet:", error); + return undefined; + } + }, + [crossmint, experimental_customAuth, onAuthRequired, clientTEEConnection, callbacks, initializeWebView] + ); + useEffect(() => { if (createOnLogin != null) { if ( @@ -175,10 +255,11 @@ export function CrossmintWalletBaseProvider({ wallet, status: walletStatus, getOrCreateWallet, + getWallet, onAuthRequired, clientTEEConnection, }), - [getOrCreateWallet, wallet, walletStatus, onAuthRequired, clientTEEConnection] + [getOrCreateWallet, getWallet, wallet, walletStatus, onAuthRequired, clientTEEConnection] ); return {children}; diff --git a/packages/client/react-base/src/types/wallet.ts b/packages/client/react-base/src/types/wallet.ts index ba052f77b..487c339a3 100644 --- a/packages/client/react-base/src/types/wallet.ts +++ b/packages/client/react-base/src/types/wallet.ts @@ -2,6 +2,7 @@ import type { UIConfig } from "@crossmint/common-sdk-base"; import type { DelegatedSigner, EVMChain, + OnCreateConfig, SignerConfigForChain, SolanaChain, StellarChain, @@ -13,6 +14,7 @@ export type { Chain, EvmExternalWalletSignerConfig, DelegatedSigner, + OnCreateConfig, SolanaExternalWalletSignerConfig, Wallet, WalletPlugin, @@ -27,6 +29,7 @@ export type CreateOnLogin = owner?: string; plugins?: WalletPlugin[]; delegatedSigners?: Array; + onCreateConfig?: OnCreateConfig; } | { chain: EVMChain; @@ -34,6 +37,7 @@ export type CreateOnLogin = owner?: string; plugins?: WalletPlugin[]; delegatedSigners?: Array; + onCreateConfig?: OnCreateConfig; } | { chain: StellarChain; @@ -41,6 +45,7 @@ export type CreateOnLogin = owner?: string; plugins?: WalletPlugin[]; delegatedSigners?: Array; + onCreateConfig?: OnCreateConfig; }; export type BaseCrossmintWalletProviderProps = { diff --git a/packages/client/ui/react-ui/src/types/wallet.ts b/packages/client/ui/react-ui/src/types/wallet.ts index fcdcef23d..cc0ea356e 100644 --- a/packages/client/ui/react-ui/src/types/wallet.ts +++ b/packages/client/ui/react-ui/src/types/wallet.ts @@ -1,4 +1,4 @@ -export type { BaseCrossmintWalletProviderProps } from "@crossmint/client-sdk-react-base"; +export type { BaseCrossmintWalletProviderProps, OnCreateConfig } from "@crossmint/client-sdk-react-base"; export { type Activity, type Balances, diff --git a/packages/wallets/src/wallets/types.ts b/packages/wallets/src/wallets/types.ts index a522a810b..b19210375 100644 --- a/packages/wallets/src/wallets/types.ts +++ b/packages/wallets/src/wallets/types.ts @@ -95,6 +95,11 @@ export type DelegatedSigner = { signer: string; }; +export type OnCreateConfig = { + adminSigner: SignerConfigForChain; + delegatedSigners?: Array; +}; + // Approvals export type PendingApproval = NonNullable< NonNullable["pending"] @@ -121,6 +126,7 @@ export type WalletArgsFor = { plugins?: WalletPlugin[]; options?: WalletOptions; delegatedSigners?: Array; + onCreateConfig?: OnCreateConfig; }; type ChainExtras = { diff --git a/packages/wallets/src/wallets/wallet-factory.test.ts b/packages/wallets/src/wallets/wallet-factory.test.ts index 7dc3abff4..ad1be4486 100644 --- a/packages/wallets/src/wallets/wallet-factory.test.ts +++ b/packages/wallets/src/wallets/wallet-factory.test.ts @@ -624,3 +624,198 @@ describe("WalletFactory - Stellar Delegated Signers Validation", () => { }); }); }); +describe("WalletFactory - OnCreateConfig Support", () => { + let walletFactory: WalletFactory; + let mockApiClient: MockedApiClient; + + const mockWalletWithAdminAndDelegated = { + chainType: "solana" as const, + type: "smart" as const, + address: "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", + owner: "test-owner", + config: { + adminSigner: { + type: "external-wallet" as const, + address: "AdminSignerAddress123", + locator: "external-wallet:AdminSignerAddress123", + }, + delegatedSigners: [ + { + type: "external-wallet" as const, + address: "DelegatedSignerAddress456", + locator: "external-wallet:DelegatedSignerAddress456", + }, + ], + }, + createdAt: Date.now(), + } as GetWalletSuccessResponse; + + beforeEach(() => { + vi.resetAllMocks(); + + mockApiClient = { + isServerSide: false, + crossmint: { projectId: "test-project" }, + getWallet: vi.fn(), + createWallet: vi.fn(), + }; + + walletFactory = new WalletFactory(mockApiClient as unknown as ApiClient); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("getOrCreateWallet with onCreateConfig", () => { + it("should create wallet with onCreateConfig admin signer when wallet does not exist", async () => { + mockApiClient.getWallet.mockResolvedValue({ error: "not found" }); + mockApiClient.createWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); + + const args: WalletArgsFor<"solana"> = { + chain: "solana", + signer: { + type: "external-wallet", + address: "DelegatedSignerAddress456", + }, + onCreateConfig: { + adminSigner: { + type: "external-wallet", + address: "AdminSignerAddress123", + }, + delegatedSigners: [{ signer: "external-wallet:DelegatedSignerAddress456" }], + }, + }; + + await walletFactory.getOrCreateWallet(args); + + expect(mockApiClient.createWallet).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({ + adminSigner: expect.objectContaining({ + type: "external-wallet", + address: "AdminSignerAddress123", + }), + delegatedSigners: [{ signer: "external-wallet:DelegatedSignerAddress456" }], + }), + }) + ); + }); + + it("should validate existing wallet against onCreateConfig admin signer", async () => { + mockApiClient.getWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); + + const args: WalletArgsFor<"solana"> = { + chain: "solana", + signer: { + type: "external-wallet", + address: "DelegatedSignerAddress456", + }, + onCreateConfig: { + adminSigner: { + type: "external-wallet", + address: "AdminSignerAddress123", + }, + delegatedSigners: [{ signer: "external-wallet:DelegatedSignerAddress456" }], + }, + }; + + await expect(walletFactory.getOrCreateWallet(args)).resolves.toBeDefined(); + }); + + it("should throw error when onCreateConfig admin signer does not match existing wallet", async () => { + mockApiClient.getWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); + + const args: WalletArgsFor<"solana"> = { + chain: "solana", + signer: { + type: "external-wallet", + address: "DelegatedSignerAddress456", + }, + onCreateConfig: { + adminSigner: { + type: "external-wallet", + address: "WrongAdminAddress", + }, + }, + }; + + await expect(walletFactory.getOrCreateWallet(args)).rejects.toThrow(WalletCreationError); + }); + + it("should validate that signer can use the wallet when onCreateConfig is provided", async () => { + mockApiClient.getWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); + + const argsWithValidDelegatedSigner: WalletArgsFor<"solana"> = { + chain: "solana", + signer: { + type: "external-wallet", + address: "DelegatedSignerAddress456", + }, + onCreateConfig: { + adminSigner: { + type: "external-wallet", + address: "AdminSignerAddress123", + }, + delegatedSigners: [{ signer: "external-wallet:DelegatedSignerAddress456" }], + }, + }; + + await expect(walletFactory.getOrCreateWallet(argsWithValidDelegatedSigner)).resolves.toBeDefined(); + }); + + it("should throw error when signer cannot use wallet with onCreateConfig", async () => { + mockApiClient.getWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); + + const argsWithInvalidSigner: WalletArgsFor<"solana"> = { + chain: "solana", + signer: { + type: "external-wallet", + address: "UnauthorizedSignerAddress", + }, + onCreateConfig: { + adminSigner: { + type: "external-wallet", + address: "AdminSignerAddress123", + }, + delegatedSigners: [{ signer: "external-wallet:DelegatedSignerAddress456" }], + }, + }; + + await expect(walletFactory.getOrCreateWallet(argsWithInvalidSigner)).rejects.toThrow( + new WalletCreationError( + `Signer cannot use wallet "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM". The provided signer is neither the admin nor a delegated signer.` + ) + ); + }); + }); + + describe("Backward compatibility without onCreateConfig", () => { + it("should use signer as admin when onCreateConfig is not provided", async () => { + mockApiClient.getWallet.mockResolvedValue({ error: "not found" }); + mockApiClient.createWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); + + const args: WalletArgsFor<"solana"> = { + chain: "solana", + signer: { + type: "external-wallet", + address: "AdminSignerAddress123", + }, + delegatedSigners: [{ signer: "external-wallet:DelegatedSignerAddress456" }], + }; + + await walletFactory.getOrCreateWallet(args); + + expect(mockApiClient.createWallet).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({ + adminSigner: expect.objectContaining({ + type: "external-wallet", + address: "AdminSignerAddress123", + }), + }), + }) + ); + }); + }); +}); diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index bf7d7b7de..ad969d5c3 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -39,10 +39,6 @@ export class WalletFactory { } public async getWallet(walletLocator: string, args: WalletArgsFor): Promise> { - if (!this.apiClient.isServerSide) { - throw new WalletCreationError("getWallet is not supported on client side, use getOrCreateWallet instead"); - } - const existingWallet = await this.apiClient.getWallet(walletLocator); if ("error" in existingWallet) { throw new WalletNotAvailableError(JSON.stringify(existingWallet)); @@ -54,10 +50,23 @@ export class WalletFactory { public async createWallet(args: WalletArgsFor): Promise> { await args.options?.experimental_callbacks?.onWalletCreationStart?.(); - this.mutateSignerFromCustomAuth(args, true); + let adminSignerConfig: SignerConfigForChain; + let delegatedSigners: Array | undefined; + + if (args.onCreateConfig) { + adminSignerConfig = args.onCreateConfig.adminSigner; + delegatedSigners = args.onCreateConfig.delegatedSigners; + } else { + adminSignerConfig = args.signer; + delegatedSigners = args.delegatedSigners; + } + + this.mutateSignerFromCustomAuth({ ...args, signer: adminSignerConfig }, true); const adminSigner = - args.signer.type === "passkey" ? await this.createPasskeyAdminSigner(args.signer) : args.signer; + adminSignerConfig.type === "passkey" + ? await this.createPasskeyAdminSigner(adminSignerConfig) + : adminSignerConfig; const walletResponse = await this.apiClient.createWallet({ type: "smart", @@ -65,7 +74,7 @@ export class WalletFactory { config: { adminSigner, ...(args?.plugins ? { plugins: args.plugins } : {}), - ...(args.delegatedSigners != null ? { delegatedSigners: args.delegatedSigners } : {}), + ...(delegatedSigners != null ? { delegatedSigners } : {}), }, owner: args.owner ?? undefined, } as CreateWalletParams); @@ -232,7 +241,12 @@ export class WalletFactory { existingWallet: GetWalletSuccessResponse, args: WalletArgsFor ): void { - this.mutateSignerFromCustomAuth(args); + const expectedAdminSigner = args.onCreateConfig ? args.onCreateConfig.adminSigner : args.signer; + const expectedDelegatedSigners = args.onCreateConfig + ? args.onCreateConfig.delegatedSigners + : args.delegatedSigners; + + this.mutateSignerFromCustomAuth({ ...args, signer: expectedAdminSigner }); if (args.owner != null && existingWallet.owner != null && args.owner !== existingWallet.owner) { throw new WalletCreationError("Wallet owner does not match existing wallet's linked user"); @@ -253,21 +267,67 @@ export class WalletFactory { return; } - const adminSignerArgs = args.signer; const existingWalletSigner = (existingWallet?.config as any)?.adminSigner as AdminSignerConfig; - if (adminSignerArgs != null && existingWalletSigner != null) { - if (adminSignerArgs.type !== existingWalletSigner.type) { + if (expectedAdminSigner != null && existingWalletSigner != null) { + if (expectedAdminSigner.type !== existingWalletSigner.type) { throw new WalletCreationError( "The wallet signer type provided in the wallet config does not match the existing wallet's adminSigner type" ); } - compareSignerConfigs(adminSignerArgs, existingWalletSigner); + compareSignerConfigs(expectedAdminSigner, existingWalletSigner); } - if (args.delegatedSigners != null) { - this.validateDelegatedSigners(existingWallet, args.delegatedSigners); + if (expectedDelegatedSigners != null) { + this.validateDelegatedSigners(existingWallet, expectedDelegatedSigners); + } + + this.validateSignerCanUseWallet(existingWallet, args.signer); + } + + private validateSignerCanUseWallet( + wallet: GetWalletSuccessResponse, + signer: SignerConfigForChain + ): void { + const adminSigner = (wallet.config as any)?.adminSigner as AdminSignerConfig; + const delegatedSigners = ((wallet.config as any)?.delegatedSigners as DelegatedSignerResponse[]) || []; + + if (adminSigner != null && signer.type === adminSigner.type) { + try { + compareSignerConfigs(signer, adminSigner); + return; + } catch {} + } + + const signerLocator = this.getSignerLocator(signer); + const isDelegated = delegatedSigners.some((ds) => ds.locator === signerLocator); + + if (isDelegated) { + return; + } + + throw new WalletCreationError( + `Signer cannot use wallet "${wallet.address}". The provided signer is neither the admin nor a delegated signer.` + ); + } + + private getSignerLocator(signer: SignerConfigForChain): string { + if (signer.type === "external-wallet") { + return `external-wallet:${signer.address}`; + } + if (signer.type === "email" && signer.email) { + return `email:${signer.email}`; + } + if (signer.type === "phone" && signer.phone) { + return `phone:${signer.phone}`; + } + if (signer.type === "passkey" && signer.name) { + return `passkey:${signer.name}`; + } + if (signer.type === "api-key") { + return "api-key"; } + return signer.type; } private validateDelegatedSigners( From 8a5417a8455695cf797cb746b7f59c774c2b366e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:14:37 +0000 Subject: [PATCH 02/82] fix: export OnCreateConfig and fix getChainType access - Export OnCreateConfig from wallets SDK index - Inline chainType logic in CrossmintWalletBaseProvider instead of accessing private method Co-Authored-By: Guille --- .../react-base/src/providers/CrossmintWalletBaseProvider.tsx | 4 +++- packages/wallets/src/index.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index 4c26b1410..7ee8dc378 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -205,7 +205,9 @@ export function CrossmintWalletBaseProvider({ await initializeWebView?.(); } - const walletLocator = `me:${wallets["getChainType"](args.chain)}:smart`; + const chainType = + args.chain === "solana" ? "solana" : args.chain === "stellar" ? "stellar" : "evm"; + const walletLocator = `me:${chainType}:smart`; const wallet = await wallets.getWallet(walletLocator, { chain: args.chain, signer, diff --git a/packages/wallets/src/index.ts b/packages/wallets/src/index.ts index 5dcc7d610..68cc5be0a 100644 --- a/packages/wallets/src/index.ts +++ b/packages/wallets/src/index.ts @@ -16,6 +16,7 @@ export type { Balances, DelegatedSigner, EVMTransactionInput, + OnCreateConfig, Transaction, WalletArgsFor, WalletPlugin, From a69a97805ca0e586a8ea570ed8ac74e797374044 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:29:22 +0000 Subject: [PATCH 03/82] refactor: remove args.delegatedSigners field (BREAKING CHANGE) 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 --- .../providers/CrossmintWalletBaseProvider.tsx | 1 - .../client/react-base/src/types/wallet.ts | 3 - packages/wallets/src/wallets/types.ts | 1 - .../src/wallets/wallet-factory.test.ts | 614 ------------------ .../wallets/src/wallets/wallet-factory.ts | 57 +- 5 files changed, 30 insertions(+), 646 deletions(-) diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index 7ee8dc378..927b8efc1 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -125,7 +125,6 @@ export function CrossmintWalletBaseProvider({ signer: args.signer, owner: args.owner, plugins: args.plugins, - delegatedSigners: args.delegatedSigners, onCreateConfig: args.onCreateConfig, options: { clientTEEConnection: clientTEEConnection?.(), diff --git a/packages/client/react-base/src/types/wallet.ts b/packages/client/react-base/src/types/wallet.ts index 487c339a3..dd5b9b832 100644 --- a/packages/client/react-base/src/types/wallet.ts +++ b/packages/client/react-base/src/types/wallet.ts @@ -28,7 +28,6 @@ export type CreateOnLogin = signer: SignerConfigForChain; owner?: string; plugins?: WalletPlugin[]; - delegatedSigners?: Array; onCreateConfig?: OnCreateConfig; } | { @@ -36,7 +35,6 @@ export type CreateOnLogin = signer: SignerConfigForChain; owner?: string; plugins?: WalletPlugin[]; - delegatedSigners?: Array; onCreateConfig?: OnCreateConfig; } | { @@ -44,7 +42,6 @@ export type CreateOnLogin = signer: SignerConfigForChain; owner?: string; plugins?: WalletPlugin[]; - delegatedSigners?: Array; onCreateConfig?: OnCreateConfig; }; diff --git a/packages/wallets/src/wallets/types.ts b/packages/wallets/src/wallets/types.ts index b19210375..e0fa1cc08 100644 --- a/packages/wallets/src/wallets/types.ts +++ b/packages/wallets/src/wallets/types.ts @@ -125,7 +125,6 @@ export type WalletArgsFor = { owner?: string; plugins?: WalletPlugin[]; options?: WalletOptions; - delegatedSigners?: Array; onCreateConfig?: OnCreateConfig; }; diff --git a/packages/wallets/src/wallets/wallet-factory.test.ts b/packages/wallets/src/wallets/wallet-factory.test.ts index ad1be4486..cbee653e1 100644 --- a/packages/wallets/src/wallets/wallet-factory.test.ts +++ b/packages/wallets/src/wallets/wallet-factory.test.ts @@ -11,619 +11,6 @@ type MockedApiClient = { createWallet: MockedFunction; }; -describe("WalletFactory - Delegated Signers Validation", () => { - let walletFactory: WalletFactory; - let mockApiClient: MockedApiClient; - - // Mock wallet response with delegated signers - const mockWalletWithDelegatedSigners = { - chainType: "solana" as const, - type: "smart" as const, - address: "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", - owner: "test-owner", - config: { - adminSigner: { - type: "external-wallet" as const, - address: "AdminSignerAddress123", - locator: "external-wallet:AdminSignerAddress123", - }, - delegatedSigners: [ - { - type: "external-wallet" as const, - address: "EbXL4e6XgbcC7s33cD5EZtyn5nixRDsieBjPQB7zf448", - locator: "external-wallet:EbXL4e6XgbcC7s33cD5EZtyn5nixRDsieBjPQB7zf448", - }, - { - type: "external-wallet" as const, - address: "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", - locator: "external-wallet:9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", - }, - ], - }, - createdAt: Date.now(), - } as GetWalletSuccessResponse; - - // Mock wallet response without delegated signers - const mockWalletWithoutDelegatedSigners = { - chainType: "solana" as const, - type: "smart" as const, - address: "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", - owner: "test-owner", - config: { - adminSigner: { - type: "external-wallet" as const, - address: "AdminSignerAddress123", - locator: "external-wallet:AdminSignerAddress123", - }, - }, - createdAt: Date.now(), - } as GetWalletSuccessResponse; - - const mockValidSolanaArgs: WalletArgsFor<"solana"> = { - chain: "solana", - signer: { - type: "external-wallet", - address: "AdminSignerAddress123", - }, - delegatedSigners: [ - { signer: "external-wallet:EbXL4e6XgbcC7s33cD5EZtyn5nixRDsieBjPQB7zf448" }, - { signer: "external-wallet:9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM" }, - ], - }; - - beforeEach(() => { - vi.resetAllMocks(); - - mockApiClient = { - isServerSide: false, - crossmint: { projectId: "test-project" }, - getWallet: vi.fn(), - createWallet: vi.fn(), - }; - - walletFactory = new WalletFactory(mockApiClient as unknown as ApiClient); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe("Happy Path", () => { - it("should successfully validate matching delegated signers", async () => { - // Mock getWallet to return existing wallet with delegated signers - mockApiClient.getWallet.mockResolvedValue(mockWalletWithDelegatedSigners); - - // This should not throw an error - await expect(walletFactory.getOrCreateWallet(mockValidSolanaArgs)).resolves.toBeDefined(); - - expect(mockApiClient.getWallet).toHaveBeenCalledWith("me:solana:smart"); - }); - }); - - describe("Error Cases", () => { - it("should throw error when delegated signers are provided but wallet has none", async () => { - // Mock getWallet to return wallet without delegated signers - mockApiClient.getWallet.mockResolvedValue(mockWalletWithoutDelegatedSigners); - - await expect(walletFactory.getOrCreateWallet(mockValidSolanaArgs)).rejects.toThrow( - new WalletCreationError( - `2 delegated signer(s) specified, but wallet "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM" has no delegated signers. When 'delegatedSigners' is provided to a method that may fetch an existing wallet, each specified delegated signer must exist in that wallet's configuration.` - ) - ); - }); - - it("should allow subset of delegated signers (wallet can have more than specified)", async () => { - // Mock getWallet to return wallet with delegated signers - mockApiClient.getWallet.mockResolvedValue(mockWalletWithDelegatedSigners); - - const argsWithFewerSigners: WalletArgsFor<"solana"> = { - chain: "solana", - signer: { - type: "external-wallet", - address: "AdminSignerAddress123", - }, - delegatedSigners: [ - // Only providing 1 signer when wallet has 2 - this should now be allowed - { signer: "external-wallet:EbXL4e6XgbcC7s33cD5EZtyn5nixRDsieBjPQB7zf448" }, - ], - }; - - // This should not throw an error since the specified signer exists in the wallet - await expect(walletFactory.getOrCreateWallet(argsWithFewerSigners)).resolves.toBeDefined(); - }); - - it("should throw error when a delegated signer is not found in existing wallet", async () => { - // Mock getWallet to return wallet with delegated signers - mockApiClient.getWallet.mockResolvedValue(mockWalletWithDelegatedSigners); - - const argsWithNonMatchingSigner: WalletArgsFor<"solana"> = { - chain: "solana", - signer: { - type: "external-wallet", - address: "AdminSignerAddress123", - }, - delegatedSigners: [ - { signer: "external-wallet:EbXL4e6XgbcC7s33cD5EZtyn5nixRDsieBjPQB7zf448" }, // This exists - { signer: "external-wallet:NonExistentSignerAddress123" }, // This doesn't exist - ], - }; - - await expect(walletFactory.getOrCreateWallet(argsWithNonMatchingSigner)).rejects.toThrow( - new WalletCreationError( - `Delegated signer 'external-wallet:NonExistentSignerAddress123' does not exist in wallet "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM". Available delegated signers: external-wallet:EbXL4e6XgbcC7s33cD5EZtyn5nixRDsieBjPQB7zf448, external-wallet:9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM. When 'delegatedSigners' is provided to a method that may fetch an existing wallet, each specified delegated signer must exist in that wallet's configuration.` - ) - ); - }); - }); - - describe("Edge Cases", () => { - it("should handle empty delegated signers array in both args and wallet", async () => { - // Mock wallet with empty delegated signers array - const walletWithEmptyDelegatedSigners = { - chainType: "solana" as const, - type: "smart" as const, - address: mockWalletWithDelegatedSigners.address, - owner: mockWalletWithDelegatedSigners.owner, - config: { - adminSigner: (mockWalletWithDelegatedSigners.config as any)?.adminSigner, - delegatedSigners: [], - }, - createdAt: mockWalletWithDelegatedSigners.createdAt, - } as GetWalletSuccessResponse; - - mockApiClient.getWallet.mockResolvedValue(walletWithEmptyDelegatedSigners); - - const argsWithEmptyDelegatedSigners: WalletArgsFor<"solana"> = { - chain: "solana", - signer: { - type: "external-wallet", - address: "AdminSignerAddress123", - }, - delegatedSigners: [], // Empty array - }; - - // This should not throw an error (both are empty) - await expect(walletFactory.getOrCreateWallet(argsWithEmptyDelegatedSigners)).resolves.toBeDefined(); - }); - - it("should allow empty array when wallet has signers (no validation needed)", async () => { - // Mock getWallet to return wallet with delegated signers - mockApiClient.getWallet.mockResolvedValue(mockWalletWithDelegatedSigners); - - const argsWithEmptyDelegatedSigners: WalletArgsFor<"solana"> = { - chain: "solana", - signer: { - type: "external-wallet", - address: "AdminSignerAddress123", - }, - delegatedSigners: [], // Empty array - }; - - // This should not throw an error since no delegated signers were specified - await expect(walletFactory.getOrCreateWallet(argsWithEmptyDelegatedSigners)).resolves.toBeDefined(); - }); - - it("should maintain order independence when comparing delegated signers", async () => { - // Mock getWallet to return wallet with delegated signers - mockApiClient.getWallet.mockResolvedValue(mockWalletWithDelegatedSigners); - - // Provide delegated signers in different order - const argsWithDifferentOrder: WalletArgsFor<"solana"> = { - chain: "solana", - signer: { - type: "external-wallet", - address: "AdminSignerAddress123", - }, - delegatedSigners: [ - // Reversed order from wallet response - { signer: "external-wallet:9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM" }, - { signer: "external-wallet:EbXL4e6XgbcC7s33cD5EZtyn5nixRDsieBjPQB7zf448" }, - ], - }; - - // This should not throw an error (order shouldn't matter, only presence) - await expect(walletFactory.getOrCreateWallet(argsWithDifferentOrder)).resolves.toBeDefined(); - }); - }); -}); - -describe("WalletFactory - EVM Delegated Signers Validation", () => { - let walletFactory: WalletFactory; - let mockApiClient: MockedApiClient; - - // Mock EVM wallet response with delegated signers - const mockEvmWalletWithDelegatedSigners = { - chainType: "evm" as const, - type: "smart" as const, - address: "0x1234567890123456789012345678901234567890", - owner: "test-owner", - config: { - adminSigner: { - type: "external-wallet" as const, - address: "0xAdminSignerAddress123456789012345678901234", - locator: "external-wallet:0xAdminSignerAddress123456789012345678901234", - }, - delegatedSigners: [ - { - type: "external-wallet" as const, - address: "0xEbXL4e6XgbcC7s33cD5EZtyn5nixRDsieBjPQB7z", - locator: "external-wallet:0xEbXL4e6XgbcC7s33cD5EZtyn5nixRDsieBjPQB7z", - }, - { - type: "external-wallet" as const, - address: "0x9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYt", - locator: "external-wallet:0x9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYt", - }, - ], - }, - createdAt: Date.now(), - } as GetWalletSuccessResponse; - - // Mock EVM wallet response without delegated signers - const mockEvmWalletWithoutDelegatedSigners = { - chainType: "evm" as const, - type: "smart" as const, - address: "0x1234567890123456789012345678901234567890", - owner: "test-owner", - config: { - adminSigner: { - type: "external-wallet" as const, - address: "0xAdminSignerAddress123456789012345678901234", - locator: "external-wallet:0xAdminSignerAddress123456789012345678901234", - }, - }, - createdAt: Date.now(), - } as GetWalletSuccessResponse; - - const mockValidEvmArgs: WalletArgsFor<"base-sepolia"> = { - chain: "base-sepolia", - signer: { - type: "external-wallet", - address: "0xAdminSignerAddress123456789012345678901234", - }, - delegatedSigners: [ - { signer: "external-wallet:0xEbXL4e6XgbcC7s33cD5EZtyn5nixRDsieBjPQB7z" }, - { signer: "external-wallet:0x9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYt" }, - ], - }; - - beforeEach(() => { - vi.resetAllMocks(); - - mockApiClient = { - isServerSide: false, - crossmint: { projectId: "test-project" }, - getWallet: vi.fn(), - createWallet: vi.fn(), - }; - - walletFactory = new WalletFactory(mockApiClient as unknown as ApiClient); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe("Happy Path", () => { - it("should successfully validate matching delegated signers", async () => { - mockApiClient.getWallet.mockResolvedValue(mockEvmWalletWithDelegatedSigners); - - await expect(walletFactory.getOrCreateWallet(mockValidEvmArgs)).resolves.toBeDefined(); - - expect(mockApiClient.getWallet).toHaveBeenCalledWith("me:evm:smart"); - }); - }); - - describe("Error Cases", () => { - it("should throw error when delegated signers are provided but wallet has none", async () => { - mockApiClient.getWallet.mockResolvedValue(mockEvmWalletWithoutDelegatedSigners); - - await expect(walletFactory.getOrCreateWallet(mockValidEvmArgs)).rejects.toThrow( - new WalletCreationError( - `2 delegated signer(s) specified, but wallet "0x1234567890123456789012345678901234567890" has no delegated signers. When 'delegatedSigners' is provided to a method that may fetch an existing wallet, each specified delegated signer must exist in that wallet's configuration.` - ) - ); - }); - - it("should allow subset of delegated signers (wallet can have more than specified)", async () => { - mockApiClient.getWallet.mockResolvedValue(mockEvmWalletWithDelegatedSigners); - - const argsWithFewerSigners: WalletArgsFor<"base-sepolia"> = { - chain: "base-sepolia", - signer: { - type: "external-wallet", - address: "0xAdminSignerAddress123456789012345678901234", - }, - delegatedSigners: [{ signer: "external-wallet:0xEbXL4e6XgbcC7s33cD5EZtyn5nixRDsieBjPQB7z" }], - }; - - await expect(walletFactory.getOrCreateWallet(argsWithFewerSigners)).resolves.toBeDefined(); - }); - - it("should throw error when a delegated signer is not found in existing wallet", async () => { - mockApiClient.getWallet.mockResolvedValue(mockEvmWalletWithDelegatedSigners); - - const argsWithNonMatchingSigner: WalletArgsFor<"base-sepolia"> = { - chain: "base-sepolia", - signer: { - type: "external-wallet", - address: "0xAdminSignerAddress123456789012345678901234", - }, - delegatedSigners: [ - { signer: "external-wallet:0xEbXL4e6XgbcC7s33cD5EZtyn5nixRDsieBjPQB7z" }, - { signer: "external-wallet:0xNonExistentSignerAddress123456789012345678" }, - ], - }; - - await expect(walletFactory.getOrCreateWallet(argsWithNonMatchingSigner)).rejects.toThrow( - new WalletCreationError( - `Delegated signer 'external-wallet:0xNonExistentSignerAddress123456789012345678' does not exist in wallet "0x1234567890123456789012345678901234567890". Available delegated signers: external-wallet:0xEbXL4e6XgbcC7s33cD5EZtyn5nixRDsieBjPQB7z, external-wallet:0x9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYt. When 'delegatedSigners' is provided to a method that may fetch an existing wallet, each specified delegated signer must exist in that wallet's configuration.` - ) - ); - }); - }); - - describe("Edge Cases", () => { - it("should handle empty delegated signers array in both args and wallet", async () => { - const walletWithEmptyDelegatedSigners = { - chainType: "evm" as const, - type: "smart" as const, - address: mockEvmWalletWithDelegatedSigners.address, - owner: mockEvmWalletWithDelegatedSigners.owner, - config: { - adminSigner: (mockEvmWalletWithDelegatedSigners.config as any)?.adminSigner, - delegatedSigners: [], - }, - createdAt: mockEvmWalletWithDelegatedSigners.createdAt, - } as GetWalletSuccessResponse; - - mockApiClient.getWallet.mockResolvedValue(walletWithEmptyDelegatedSigners); - - const argsWithEmptyDelegatedSigners: WalletArgsFor<"base-sepolia"> = { - chain: "base-sepolia", - signer: { - type: "external-wallet", - address: "0xAdminSignerAddress123456789012345678901234", - }, - delegatedSigners: [], - }; - - await expect(walletFactory.getOrCreateWallet(argsWithEmptyDelegatedSigners)).resolves.toBeDefined(); - }); - - it("should allow empty array when wallet has signers (no validation needed)", async () => { - mockApiClient.getWallet.mockResolvedValue(mockEvmWalletWithDelegatedSigners); - - const argsWithEmptyDelegatedSigners: WalletArgsFor<"base-sepolia"> = { - chain: "base-sepolia", - signer: { - type: "external-wallet", - address: "0xAdminSignerAddress123456789012345678901234", - }, - delegatedSigners: [], - }; - - await expect(walletFactory.getOrCreateWallet(argsWithEmptyDelegatedSigners)).resolves.toBeDefined(); - }); - - it("should maintain order independence when comparing delegated signers", async () => { - mockApiClient.getWallet.mockResolvedValue(mockEvmWalletWithDelegatedSigners); - - const argsWithDifferentOrder: WalletArgsFor<"base-sepolia"> = { - chain: "base-sepolia", - signer: { - type: "external-wallet", - address: "0xAdminSignerAddress123456789012345678901234", - }, - delegatedSigners: [ - { signer: "external-wallet:0x9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYt" }, - { signer: "external-wallet:0xEbXL4e6XgbcC7s33cD5EZtyn5nixRDsieBjPQB7z" }, - ], - }; - - await expect(walletFactory.getOrCreateWallet(argsWithDifferentOrder)).resolves.toBeDefined(); - }); - }); -}); - -describe("WalletFactory - Stellar Delegated Signers Validation", () => { - let walletFactory: WalletFactory; - let mockApiClient: MockedApiClient; - - // Mock Stellar wallet response with delegated signers - const mockStellarWalletWithDelegatedSigners = { - chainType: "stellar" as const, - type: "smart" as const, - address: "GCKFBEIYTKP6RCZX6LRQW2JVAVLMGGVSNESWKN7L2YGQNI2DCOHVHJVY", - owner: "test-owner", - config: { - adminSigner: { - type: "external-wallet" as const, - address: "GADMINSGNERADDRESS123456789012345678901234567890123456", - locator: "external-wallet:GADMINSGNERADDRESS123456789012345678901234567890123456", - }, - delegatedSigners: [ - { - type: "external-wallet" as const, - address: "GEBXL4E6XGBCC7S33CD5EZTYN5NIXRDSIEBJPQB7ZF448ABCDEFGH", - locator: "external-wallet:GEBXL4E6XGBCC7S33CD5EZTYN5NIXRDSIEBJPQB7ZF448ABCDEFGH", - }, - { - type: "external-wallet" as const, - address: "G9WZDXWBBMKG8ZTBNMQUXVQRAYRZZDSGYLDVL9ZYTAWWABCDEFGH", - locator: "external-wallet:G9WZDXWBBMKG8ZTBNMQUXVQRAYRZZDSGYLDVL9ZYTAWWABCDEFGH", - }, - ], - }, - createdAt: Date.now(), - } as GetWalletSuccessResponse; - - // Mock Stellar wallet response without delegated signers - const mockStellarWalletWithoutDelegatedSigners = { - chainType: "stellar" as const, - type: "smart" as const, - address: "GCKFBEIYTKP6RCZX6LRQW2JVAVLMGGVSNESWKN7L2YGQNI2DCOHVHJVY", - owner: "test-owner", - config: { - adminSigner: { - type: "external-wallet" as const, - address: "GADMINSGNERADDRESS123456789012345678901234567890123456", - locator: "external-wallet:GADMINSGNERADDRESS123456789012345678901234567890123456", - }, - }, - createdAt: Date.now(), - } as GetWalletSuccessResponse; - - const mockValidStellarArgs: WalletArgsFor<"stellar"> = { - chain: "stellar", - signer: { - type: "external-wallet", - address: "GADMINSGNERADDRESS123456789012345678901234567890123456", - }, - delegatedSigners: [ - { signer: "external-wallet:GEBXL4E6XGBCC7S33CD5EZTYN5NIXRDSIEBJPQB7ZF448ABCDEFGH" }, - { signer: "external-wallet:G9WZDXWBBMKG8ZTBNMQUXVQRAYRZZDSGYLDVL9ZYTAWWABCDEFGH" }, - ], - }; - - beforeEach(() => { - vi.resetAllMocks(); - - mockApiClient = { - isServerSide: false, - crossmint: { projectId: "test-project" }, - getWallet: vi.fn(), - createWallet: vi.fn(), - }; - - walletFactory = new WalletFactory(mockApiClient as unknown as ApiClient); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe("Happy Path", () => { - it("should successfully validate matching delegated signers", async () => { - mockApiClient.getWallet.mockResolvedValue(mockStellarWalletWithDelegatedSigners); - - await expect(walletFactory.getOrCreateWallet(mockValidStellarArgs)).resolves.toBeDefined(); - - expect(mockApiClient.getWallet).toHaveBeenCalledWith("me:stellar:smart"); - }); - }); - - describe("Error Cases", () => { - it("should throw error when delegated signers are provided but wallet has none", async () => { - mockApiClient.getWallet.mockResolvedValue(mockStellarWalletWithoutDelegatedSigners); - - await expect(walletFactory.getOrCreateWallet(mockValidStellarArgs)).rejects.toThrow( - new WalletCreationError( - `2 delegated signer(s) specified, but wallet "GCKFBEIYTKP6RCZX6LRQW2JVAVLMGGVSNESWKN7L2YGQNI2DCOHVHJVY" has no delegated signers. When 'delegatedSigners' is provided to a method that may fetch an existing wallet, each specified delegated signer must exist in that wallet's configuration.` - ) - ); - }); - - it("should allow subset of delegated signers (wallet can have more than specified)", async () => { - mockApiClient.getWallet.mockResolvedValue(mockStellarWalletWithDelegatedSigners); - - const argsWithFewerSigners: WalletArgsFor<"stellar"> = { - chain: "stellar", - signer: { - type: "external-wallet", - address: "GADMINSGNERADDRESS123456789012345678901234567890123456", - }, - delegatedSigners: [{ signer: "external-wallet:GEBXL4E6XGBCC7S33CD5EZTYN5NIXRDSIEBJPQB7ZF448ABCDEFGH" }], - }; - - await expect(walletFactory.getOrCreateWallet(argsWithFewerSigners)).resolves.toBeDefined(); - }); - - it("should throw error when a delegated signer is not found in existing wallet", async () => { - mockApiClient.getWallet.mockResolvedValue(mockStellarWalletWithDelegatedSigners); - - const argsWithNonMatchingSigner: WalletArgsFor<"stellar"> = { - chain: "stellar", - signer: { - type: "external-wallet", - address: "GADMINSGNERADDRESS123456789012345678901234567890123456", - }, - delegatedSigners: [ - { signer: "external-wallet:GEBXL4E6XGBCC7S33CD5EZTYN5NIXRDSIEBJPQB7ZF448ABCDEFGH" }, - { signer: "external-wallet:GNONEXISTENTSIGNERADDRESS123456789012345678901234567890" }, - ], - }; - - await expect(walletFactory.getOrCreateWallet(argsWithNonMatchingSigner)).rejects.toThrow( - new WalletCreationError( - `Delegated signer 'external-wallet:GNONEXISTENTSIGNERADDRESS123456789012345678901234567890' does not exist in wallet "GCKFBEIYTKP6RCZX6LRQW2JVAVLMGGVSNESWKN7L2YGQNI2DCOHVHJVY". Available delegated signers: external-wallet:GEBXL4E6XGBCC7S33CD5EZTYN5NIXRDSIEBJPQB7ZF448ABCDEFGH, external-wallet:G9WZDXWBBMKG8ZTBNMQUXVQRAYRZZDSGYLDVL9ZYTAWWABCDEFGH. When 'delegatedSigners' is provided to a method that may fetch an existing wallet, each specified delegated signer must exist in that wallet's configuration.` - ) - ); - }); - }); - - describe("Edge Cases", () => { - it("should handle empty delegated signers array in both args and wallet", async () => { - const walletWithEmptyDelegatedSigners = { - chainType: "stellar" as const, - type: "smart" as const, - address: mockStellarWalletWithDelegatedSigners.address, - owner: mockStellarWalletWithDelegatedSigners.owner, - config: { - adminSigner: (mockStellarWalletWithDelegatedSigners.config as any)?.adminSigner, - delegatedSigners: [], - }, - createdAt: mockStellarWalletWithDelegatedSigners.createdAt, - } as GetWalletSuccessResponse; - - mockApiClient.getWallet.mockResolvedValue(walletWithEmptyDelegatedSigners); - - const argsWithEmptyDelegatedSigners: WalletArgsFor<"stellar"> = { - chain: "stellar", - signer: { - type: "external-wallet", - address: "GADMINSGNERADDRESS123456789012345678901234567890123456", - }, - delegatedSigners: [], - }; - - await expect(walletFactory.getOrCreateWallet(argsWithEmptyDelegatedSigners)).resolves.toBeDefined(); - }); - - it("should allow empty array when wallet has signers (no validation needed)", async () => { - mockApiClient.getWallet.mockResolvedValue(mockStellarWalletWithDelegatedSigners); - - const argsWithEmptyDelegatedSigners: WalletArgsFor<"stellar"> = { - chain: "stellar", - signer: { - type: "external-wallet", - address: "GADMINSGNERADDRESS123456789012345678901234567890123456", - }, - delegatedSigners: [], - }; - - await expect(walletFactory.getOrCreateWallet(argsWithEmptyDelegatedSigners)).resolves.toBeDefined(); - }); - - it("should maintain order independence when comparing delegated signers", async () => { - mockApiClient.getWallet.mockResolvedValue(mockStellarWalletWithDelegatedSigners); - - const argsWithDifferentOrder: WalletArgsFor<"stellar"> = { - chain: "stellar", - signer: { - type: "external-wallet", - address: "GADMINSGNERADDRESS123456789012345678901234567890123456", - }, - delegatedSigners: [ - { signer: "external-wallet:G9WZDXWBBMKG8ZTBNMQUXVQRAYRZZDSGYLDVL9ZYTAWWABCDEFGH" }, - { signer: "external-wallet:GEBXL4E6XGBCC7S33CD5EZTYN5NIXRDSIEBJPQB7ZF448ABCDEFGH" }, - ], - }; - - await expect(walletFactory.getOrCreateWallet(argsWithDifferentOrder)).resolves.toBeDefined(); - }); - }); -}); describe("WalletFactory - OnCreateConfig Support", () => { let walletFactory: WalletFactory; let mockApiClient: MockedApiClient; @@ -801,7 +188,6 @@ describe("WalletFactory - OnCreateConfig Support", () => { type: "external-wallet", address: "AdminSignerAddress123", }, - delegatedSigners: [{ signer: "external-wallet:DelegatedSignerAddress456" }], }; await walletFactory.getOrCreateWallet(args); diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index ad969d5c3..3f1023725 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -50,16 +50,8 @@ export class WalletFactory { public async createWallet(args: WalletArgsFor): Promise> { await args.options?.experimental_callbacks?.onWalletCreationStart?.(); - let adminSignerConfig: SignerConfigForChain; - let delegatedSigners: Array | undefined; - - if (args.onCreateConfig) { - adminSignerConfig = args.onCreateConfig.adminSigner; - delegatedSigners = args.onCreateConfig.delegatedSigners; - } else { - adminSignerConfig = args.signer; - delegatedSigners = args.delegatedSigners; - } + const adminSignerConfig = args.onCreateConfig ? args.onCreateConfig.adminSigner : args.signer; + const delegatedSigners = args.onCreateConfig?.delegatedSigners; this.mutateSignerFromCustomAuth({ ...args, signer: adminSignerConfig }, true); @@ -241,13 +233,6 @@ export class WalletFactory { existingWallet: GetWalletSuccessResponse, args: WalletArgsFor ): void { - const expectedAdminSigner = args.onCreateConfig ? args.onCreateConfig.adminSigner : args.signer; - const expectedDelegatedSigners = args.onCreateConfig - ? args.onCreateConfig.delegatedSigners - : args.delegatedSigners; - - this.mutateSignerFromCustomAuth({ ...args, signer: expectedAdminSigner }); - if (args.owner != null && existingWallet.owner != null && args.owner !== existingWallet.owner) { throw new WalletCreationError("Wallet owner does not match existing wallet's linked user"); } @@ -267,19 +252,37 @@ export class WalletFactory { return; } - const existingWalletSigner = (existingWallet?.config as any)?.adminSigner as AdminSignerConfig; + if (args.onCreateConfig) { + const expectedAdminSigner = args.onCreateConfig.adminSigner; + const existingWalletSigner = (existingWallet?.config as any)?.adminSigner as AdminSignerConfig; - if (expectedAdminSigner != null && existingWalletSigner != null) { - if (expectedAdminSigner.type !== existingWalletSigner.type) { - throw new WalletCreationError( - "The wallet signer type provided in the wallet config does not match the existing wallet's adminSigner type" - ); + this.mutateSignerFromCustomAuth({ ...args, signer: expectedAdminSigner }); + + if (expectedAdminSigner != null && existingWalletSigner != null) { + if (expectedAdminSigner.type !== existingWalletSigner.type) { + throw new WalletCreationError( + "The wallet signer type provided in onCreateConfig does not match the existing wallet's adminSigner type" + ); + } + compareSignerConfigs(expectedAdminSigner, existingWalletSigner); } - compareSignerConfigs(expectedAdminSigner, existingWalletSigner); - } - if (expectedDelegatedSigners != null) { - this.validateDelegatedSigners(existingWallet, expectedDelegatedSigners); + if (args.onCreateConfig.delegatedSigners != null) { + this.validateDelegatedSigners(existingWallet, args.onCreateConfig.delegatedSigners); + } + } else { + const existingWalletSigner = (existingWallet?.config as any)?.adminSigner as AdminSignerConfig; + + this.mutateSignerFromCustomAuth(args); + + if (args.signer != null && existingWalletSigner != null) { + if (args.signer.type !== existingWalletSigner.type) { + throw new WalletCreationError( + "The wallet signer type provided does not match the existing wallet's adminSigner type" + ); + } + compareSignerConfigs(args.signer, existingWalletSigner); + } } this.validateSignerCanUseWallet(existingWallet, args.signer); From 808915705bcb136bbf01b91cd367f353ed574fb4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:58:11 +0000 Subject: [PATCH 04/82] feat: make onCreateConfig required via WalletCreateArgs type - 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 --- .../providers/CrossmintWalletBaseProvider.tsx | 5 ++-- .../client/react-base/src/types/wallet.ts | 24 ++--------------- packages/wallets/src/index.ts | 1 + packages/wallets/src/sdk.ts | 6 ++--- packages/wallets/src/wallets/types.ts | 4 +++ .../src/wallets/wallet-factory.test.ts | 27 ------------------- .../wallets/src/wallets/wallet-factory.ts | 23 ++++------------ 7 files changed, 18 insertions(+), 72 deletions(-) diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index 927b8efc1..60f9c885d 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -6,6 +6,7 @@ import { type SignerConfigForChain, type Wallet, type WalletArgsFor, + type WalletCreateArgs, type PhoneSignerConfig, } from "@crossmint/wallets-sdk"; import type { HandshakeParent } from "@crossmint/client-sdk-window"; @@ -16,7 +17,7 @@ import type { CreateOnLogin } from "@/types"; export type CrossmintWalletBaseContext = { wallet: Wallet | undefined; status: "not-loaded" | "in-progress" | "loaded" | "error"; - getOrCreateWallet: (props: WalletArgsFor) => Promise | undefined>; + getOrCreateWallet: (props: WalletCreateArgs) => Promise | undefined>; getWallet: ( props: Pick, "chain" | "signer"> ) => Promise | undefined>; @@ -60,7 +61,7 @@ export function CrossmintWalletBaseProvider({ const [walletStatus, setWalletStatus] = useState<"not-loaded" | "in-progress" | "loaded" | "error">("not-loaded"); const getOrCreateWallet = useCallback( - async (args: WalletArgsFor) => { + async (args: WalletCreateArgs) => { if (experimental_customAuth?.jwt == null || walletStatus === "in-progress") { return undefined; } diff --git a/packages/client/react-base/src/types/wallet.ts b/packages/client/react-base/src/types/wallet.ts index dd5b9b832..f913f465b 100644 --- a/packages/client/react-base/src/types/wallet.ts +++ b/packages/client/react-base/src/types/wallet.ts @@ -6,6 +6,7 @@ import type { SignerConfigForChain, SolanaChain, StellarChain, + WalletCreateArgs, WalletPlugin, } from "@crossmint/wallets-sdk"; @@ -22,28 +23,7 @@ export type { export { EVMWallet, SolanaWallet, StellarWallet } from "@crossmint/wallets-sdk"; -export type CreateOnLogin = - | { - chain: SolanaChain; - signer: SignerConfigForChain; - owner?: string; - plugins?: WalletPlugin[]; - onCreateConfig?: OnCreateConfig; - } - | { - chain: EVMChain; - signer: SignerConfigForChain; - owner?: string; - plugins?: WalletPlugin[]; - onCreateConfig?: OnCreateConfig; - } - | { - chain: StellarChain; - signer: SignerConfigForChain; - owner?: string; - plugins?: WalletPlugin[]; - onCreateConfig?: OnCreateConfig; - }; +export type CreateOnLogin = WalletCreateArgs | WalletCreateArgs | WalletCreateArgs; export type BaseCrossmintWalletProviderProps = { createOnLogin?: CreateOnLogin; diff --git a/packages/wallets/src/index.ts b/packages/wallets/src/index.ts index 68cc5be0a..af67a1a78 100644 --- a/packages/wallets/src/index.ts +++ b/packages/wallets/src/index.ts @@ -19,6 +19,7 @@ export type { OnCreateConfig, Transaction, WalletArgsFor, + WalletCreateArgs, WalletPlugin, Signature, SolanaTransactionInput, diff --git a/packages/wallets/src/sdk.ts b/packages/wallets/src/sdk.ts index e0a357caf..706ce048e 100644 --- a/packages/wallets/src/sdk.ts +++ b/packages/wallets/src/sdk.ts @@ -3,7 +3,7 @@ import { ApiClient } from "./api"; import { WalletFactory } from "./wallets/wallet-factory"; import type { Wallet } from "./wallets/wallet"; import type { Chain } from "./chains/chains"; -import type { WalletArgsFor } from "./wallets/types"; +import type { WalletArgsFor, WalletCreateArgs } from "./wallets/types"; export class CrossmintWallets { private readonly walletFactory: WalletFactory; @@ -28,7 +28,7 @@ export class CrossmintWallets { * @param options - Wallet options * @returns An existing wallet or a new wallet */ - public async getOrCreateWallet(options: WalletArgsFor): Promise> { + public async getOrCreateWallet(options: WalletCreateArgs): Promise> { return await this.walletFactory.getOrCreateWallet(options); } @@ -47,7 +47,7 @@ export class CrossmintWallets { * @param options - Wallet options * @returns A new wallet */ - public async createWallet(options: WalletArgsFor): Promise> { + public async createWallet(options: WalletCreateArgs): Promise> { return await this.walletFactory.createWallet(options); } } diff --git a/packages/wallets/src/wallets/types.ts b/packages/wallets/src/wallets/types.ts index e0fa1cc08..3325c9648 100644 --- a/packages/wallets/src/wallets/types.ts +++ b/packages/wallets/src/wallets/types.ts @@ -128,6 +128,10 @@ export type WalletArgsFor = { onCreateConfig?: OnCreateConfig; }; +export type WalletCreateArgs = WalletArgsFor & { + onCreateConfig: OnCreateConfig; +}; + type ChainExtras = { solana: { mintHash?: string }; stellar: { contractId?: string }; diff --git a/packages/wallets/src/wallets/wallet-factory.test.ts b/packages/wallets/src/wallets/wallet-factory.test.ts index cbee653e1..75c256efb 100644 --- a/packages/wallets/src/wallets/wallet-factory.test.ts +++ b/packages/wallets/src/wallets/wallet-factory.test.ts @@ -177,31 +177,4 @@ describe("WalletFactory - OnCreateConfig Support", () => { }); }); - describe("Backward compatibility without onCreateConfig", () => { - it("should use signer as admin when onCreateConfig is not provided", async () => { - mockApiClient.getWallet.mockResolvedValue({ error: "not found" }); - mockApiClient.createWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); - - const args: WalletArgsFor<"solana"> = { - chain: "solana", - signer: { - type: "external-wallet", - address: "AdminSignerAddress123", - }, - }; - - await walletFactory.getOrCreateWallet(args); - - expect(mockApiClient.createWallet).toHaveBeenCalledWith( - expect.objectContaining({ - config: expect.objectContaining({ - adminSigner: expect.objectContaining({ - type: "external-wallet", - address: "AdminSignerAddress123", - }), - }), - }) - ); - }); - }); }); diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 3f1023725..23e3510ae 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -13,7 +13,7 @@ import type { Chain } from "../chains/chains"; import type { InternalSignerConfig, SignerConfigForChain } from "../signers/types"; import { Wallet } from "./wallet"; import { assembleSigner } from "../signers"; -import type { DelegatedSigner, WalletArgsFor, WalletOptions } from "./types"; +import type { DelegatedSigner, WalletArgsFor, WalletCreateArgs, WalletOptions } from "./types"; import { compareSignerConfigs } from "../utils/signer-validation"; const DELEGATED_SIGNER_MISMATCH_ERROR = @@ -22,7 +22,7 @@ const DELEGATED_SIGNER_MISMATCH_ERROR = export class WalletFactory { constructor(private readonly apiClient: ApiClient) {} - public async getOrCreateWallet(args: WalletArgsFor): Promise> { + public async getOrCreateWallet(args: WalletCreateArgs): Promise> { if (this.apiClient.isServerSide) { throw new WalletCreationError( "getOrCreateWallet can only be called from client-side code.\n- Make sure you're running this in the browser (or another client environment), not on your server.\n- Use your Crossmint Client API Key (not a server key)." @@ -47,11 +47,11 @@ export class WalletFactory { return this.createWalletInstance(existingWallet, args); } - public async createWallet(args: WalletArgsFor): Promise> { + public async createWallet(args: WalletCreateArgs): Promise> { await args.options?.experimental_callbacks?.onWalletCreationStart?.(); - const adminSignerConfig = args.onCreateConfig ? args.onCreateConfig.adminSigner : args.signer; - const delegatedSigners = args.onCreateConfig?.delegatedSigners; + const adminSignerConfig = args.onCreateConfig.adminSigner; + const delegatedSigners = args.onCreateConfig.delegatedSigners; this.mutateSignerFromCustomAuth({ ...args, signer: adminSignerConfig }, true); @@ -270,19 +270,6 @@ export class WalletFactory { if (args.onCreateConfig.delegatedSigners != null) { this.validateDelegatedSigners(existingWallet, args.onCreateConfig.delegatedSigners); } - } else { - const existingWalletSigner = (existingWallet?.config as any)?.adminSigner as AdminSignerConfig; - - this.mutateSignerFromCustomAuth(args); - - if (args.signer != null && existingWalletSigner != null) { - if (args.signer.type !== existingWalletSigner.type) { - throw new WalletCreationError( - "The wallet signer type provided does not match the existing wallet's adminSigner type" - ); - } - compareSignerConfigs(args.signer, existingWalletSigner); - } } this.validateSignerCanUseWallet(existingWallet, args.signer); From c6759325b4e64fef69682f6a8e67b4f3e09afdca Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:59:48 +0000 Subject: [PATCH 05/82] chore: fix lint issues Co-Authored-By: Guille --- .../src/providers/CrossmintWalletBaseProvider.tsx | 3 +-- packages/client/react-base/src/types/wallet.ts | 11 +---------- packages/wallets/src/wallets/wallet-factory.test.ts | 1 - 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index 60f9c885d..f6e46bf5a 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -205,8 +205,7 @@ export function CrossmintWalletBaseProvider({ await initializeWebView?.(); } - const chainType = - args.chain === "solana" ? "solana" : args.chain === "stellar" ? "stellar" : "evm"; + const chainType = args.chain === "solana" ? "solana" : args.chain === "stellar" ? "stellar" : "evm"; const walletLocator = `me:${chainType}:smart`; const wallet = await wallets.getWallet(walletLocator, { chain: args.chain, diff --git a/packages/client/react-base/src/types/wallet.ts b/packages/client/react-base/src/types/wallet.ts index f913f465b..aab63fac6 100644 --- a/packages/client/react-base/src/types/wallet.ts +++ b/packages/client/react-base/src/types/wallet.ts @@ -1,14 +1,5 @@ import type { UIConfig } from "@crossmint/common-sdk-base"; -import type { - DelegatedSigner, - EVMChain, - OnCreateConfig, - SignerConfigForChain, - SolanaChain, - StellarChain, - WalletCreateArgs, - WalletPlugin, -} from "@crossmint/wallets-sdk"; +import type { EVMChain, SolanaChain, StellarChain, WalletCreateArgs } from "@crossmint/wallets-sdk"; export type { Balances, diff --git a/packages/wallets/src/wallets/wallet-factory.test.ts b/packages/wallets/src/wallets/wallet-factory.test.ts index 75c256efb..fbdd8748c 100644 --- a/packages/wallets/src/wallets/wallet-factory.test.ts +++ b/packages/wallets/src/wallets/wallet-factory.test.ts @@ -176,5 +176,4 @@ describe("WalletFactory - OnCreateConfig Support", () => { ); }); }); - }); From b642c2c5ce803b03b94ebfe89e9ec7eab992684e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:33:59 +0000 Subject: [PATCH 06/82] chore: update demo apps to use onCreateConfig - 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 --- .../quickstart-devkit/app/providers.tsx | 66 +++++++++++++++---- apps/wallets/smart-wallet/expo/app/index.tsx | 6 +- .../next/src/app/_lib/providers.tsx | 3 + 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/apps/wallets/quickstart-devkit/app/providers.tsx b/apps/wallets/quickstart-devkit/app/providers.tsx index 18cf0c734..b12848361 100644 --- a/apps/wallets/quickstart-devkit/app/providers.tsx +++ b/apps/wallets/quickstart-devkit/app/providers.tsx @@ -49,7 +49,11 @@ function EVMCrossmintAuthProvider({ createOnLogin={ createOnLogin != null ? createOnLogin - : { chain: process.env.NEXT_PUBLIC_EVM_CHAIN as any, signer: { type: "email" } } + : { + chain: process.env.NEXT_PUBLIC_EVM_CHAIN as any, + signer: { type: "email" }, + onCreateConfig: { adminSigner: { type: "email" } }, + } } > {children} @@ -81,6 +85,7 @@ function EVMPrivyProvider({ children, apiKey }: { children: React.ReactNode; api createOnLogin={{ chain: process.env.NEXT_PUBLIC_EVM_CHAIN as any, signer: { type: "email" }, + onCreateConfig: { adminSigner: { type: "email" } }, }} > {children} @@ -116,7 +121,11 @@ function EVMFirebaseProvider({ children, apiKey }: { children: React.ReactNode; return ( {children} @@ -141,7 +150,13 @@ function SolanaCrossmintAuthProvider({ {children} @@ -170,7 +185,11 @@ function SolanaPrivyProvider({ children, apiKey }: { children: React.ReactNode; {children} @@ -197,7 +216,13 @@ function SolanaDynamicLabsProvider({ }} > - + {children} @@ -208,7 +233,13 @@ function SolanaDynamicLabsProvider({ function SolanaFirebaseProvider({ children, apiKey }: { children: React.ReactNode; apiKey?: string }) { return ( - + {children} @@ -227,9 +258,12 @@ function StellarCrossmintAuthProvider({ children, apiKey }: { children: React.Re createOnLogin={{ chain: "stellar", signer: { type: "email" }, - delegatedSigners: [ - { signer: "external-wallet:GDUNAPJW6JYL4JEBFR7B5RZZD6B4TOUEWPFTT3V47IHI7QJPA43UFEY6" }, - ], + onCreateConfig: { + adminSigner: { type: "email" }, + delegatedSigners: [ + { signer: "external-wallet:GDUNAPJW6JYL4JEBFR7B5RZZD6B4TOUEWPFTT3V47IHI7QJPA43UFEY6" }, + ], + }, }} > {children} @@ -261,9 +295,14 @@ function QueryParamsProvider({ children }: { children: React.ReactNode }) { return {children}; case "crossmint": default: - const createOnLogin: any = { chain: chainId, signer: { type: signerType } }; + const createOnLogin: any = { + chain: chainId, + signer: { type: signerType }, + onCreateConfig: { adminSigner: { type: signerType } }, + }; if (signerType === "phone" && phoneNumber != null) { createOnLogin.signer = { type: signerType, phone: decodeURIComponent(phoneNumber) }; + createOnLogin.onCreateConfig.adminSigner = { type: signerType, phone: decodeURIComponent(phoneNumber) }; } return ( @@ -281,9 +320,14 @@ function QueryParamsProvider({ children }: { children: React.ReactNode }) { return {children}; case "crossmint": default: - const createOnLogin: any = { chain: "solana", signer: { type: signerType } }; + const createOnLogin: any = { + chain: "solana", + signer: { type: signerType }, + onCreateConfig: { adminSigner: { type: signerType } }, + }; if (signerType === "phone" && phoneNumber != null) { createOnLogin.signer = { type: signerType, phone: decodeURIComponent(phoneNumber) }; + createOnLogin.onCreateConfig.adminSigner = { type: signerType, phone: decodeURIComponent(phoneNumber) }; } return ( diff --git a/apps/wallets/smart-wallet/expo/app/index.tsx b/apps/wallets/smart-wallet/expo/app/index.tsx index 56bd59383..16271fb04 100644 --- a/apps/wallets/smart-wallet/expo/app/index.tsx +++ b/apps/wallets/smart-wallet/expo/app/index.tsx @@ -73,7 +73,11 @@ export default function Index() { } setIsLoading(true); try { - await getOrCreateWallet({ chain: "base-sepolia", signer: { type: "email" } }); + await getOrCreateWallet({ + chain: "base-sepolia", + signer: { type: "email" }, + onCreateConfig: { adminSigner: { type: "email" } }, + }); } catch (error) { console.error("Error initializing wallet:", error); } finally { diff --git a/apps/wallets/smart-wallet/next/src/app/_lib/providers.tsx b/apps/wallets/smart-wallet/next/src/app/_lib/providers.tsx index 99021f668..2d4e46dc9 100644 --- a/apps/wallets/smart-wallet/next/src/app/_lib/providers.tsx +++ b/apps/wallets/smart-wallet/next/src/app/_lib/providers.tsx @@ -57,6 +57,9 @@ function CrossmintProviders({ children }: { children: ReactNode }) { createOnLogin={{ chain: walletType === "solana-smart-wallet" ? "solana" : (process.env.NEXT_PUBLIC_CHAIN as any), signer: { type: "api-key" }, + onCreateConfig: { + adminSigner: { type: "api-key" }, + }, }} > {children} From 41e35a252bbb8cc2b73649b0a85c4395926aea3d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:35:14 +0000 Subject: [PATCH 07/82] fix: correctly mutate signer in validateExistingWalletConfig - 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 --- packages/wallets/src/wallets/wallet-factory.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 23e3510ae..1ae518fab 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -253,10 +253,12 @@ export class WalletFactory { } if (args.onCreateConfig) { - const expectedAdminSigner = args.onCreateConfig.adminSigner; + let expectedAdminSigner = args.onCreateConfig.adminSigner; const existingWalletSigner = (existingWallet?.config as any)?.adminSigner as AdminSignerConfig; - this.mutateSignerFromCustomAuth({ ...args, signer: expectedAdminSigner }); + const tempArgs = { ...args, signer: expectedAdminSigner }; + this.mutateSignerFromCustomAuth(tempArgs); + expectedAdminSigner = tempArgs.signer; if (expectedAdminSigner != null && existingWalletSigner != null) { if (expectedAdminSigner.type !== existingWalletSigner.type) { From eb5c28e1711c753893ee53f7557ab0d8750c5956 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 11:47:18 +0000 Subject: [PATCH 08/82] fix: correctly mutate adminSignerConfig in createWallet - Create tempArgs to capture mutation from mutateSignerFromCustomAuth - Reassign adminSignerConfig from mutated tempArgs.signer - Ensures external wallet signer reassignment works correctly Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet-factory.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 1ae518fab..8d79caf59 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -50,10 +50,12 @@ export class WalletFactory { public async createWallet(args: WalletCreateArgs): Promise> { await args.options?.experimental_callbacks?.onWalletCreationStart?.(); - const adminSignerConfig = args.onCreateConfig.adminSigner; + let adminSignerConfig = args.onCreateConfig.adminSigner; const delegatedSigners = args.onCreateConfig.delegatedSigners; - this.mutateSignerFromCustomAuth({ ...args, signer: adminSignerConfig }, true); + const tempArgs = { ...args, signer: adminSignerConfig }; + this.mutateSignerFromCustomAuth(tempArgs, true); + adminSignerConfig = tempArgs.signer; const adminSigner = adminSignerConfig.type === "passkey" From 1be9a9f0d811b7b0fb7f3c0ce3ae89d4423af92e Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Tue, 14 Oct 2025 13:51:26 +0200 Subject: [PATCH 09/82] remove unnecesary type --- packages/wallets/src/wallets/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/wallets/src/wallets/types.ts b/packages/wallets/src/wallets/types.ts index 3325c9648..64562b460 100644 --- a/packages/wallets/src/wallets/types.ts +++ b/packages/wallets/src/wallets/types.ts @@ -125,7 +125,6 @@ export type WalletArgsFor = { owner?: string; plugins?: WalletPlugin[]; options?: WalletOptions; - onCreateConfig?: OnCreateConfig; }; export type WalletCreateArgs = WalletArgsFor & { From cfde33fea1c1a84013f19e0fca89f7c74ffd8286 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Tue, 14 Oct 2025 14:03:52 +0200 Subject: [PATCH 10/82] reuse common functionality from get and create --- .../providers/CrossmintWalletBaseProvider.tsx | 176 +++++++++--------- 1 file changed, 83 insertions(+), 93 deletions(-) diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index f6e46bf5a..e22bb9e71 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -60,6 +60,63 @@ export function CrossmintWalletBaseProvider({ const [wallet, setWallet] = useState | undefined>(undefined); const [walletStatus, setWalletStatus] = useState<"not-loaded" | "in-progress" | "loaded" | "error">("not-loaded"); + const resolveSignerConfig = useCallback( + (signer: SignerConfigForChain): SignerConfigForChain => { + if (signer.type === "email") { + const email = signer.email ?? experimental_customAuth?.email; + const _onAuthRequired = signer.onAuthRequired ?? onAuthRequired; + + if (email == null) { + throw new Error( + "Email not found in experimental_customAuth or signer. Please set email in experimental_customAuth or signer." + ); + } + return { + ...signer, + email, + onAuthRequired: _onAuthRequired, + }; + } + + if (signer.type === "phone") { + const phone = signer.phone ?? experimental_customAuth?.phone; + const _onAuthRequired = signer.onAuthRequired ?? onAuthRequired; + + if (phone == null) { + throw new Error("Phone not found in signer. Please set phone in signer."); + } + return { + ...signer, + phone, + onAuthRequired: _onAuthRequired, + }; + } + + if (signer.type === "external-wallet") { + const resolvedSigner = signer.address != null ? signer : experimental_customAuth?.externalWalletSigner; + + if (resolvedSigner == null) { + throw new Error( + "External wallet config not found in experimental_customAuth or signer. Please set it in experimental_customAuth or signer." + ); + } + return resolvedSigner as SignerConfigForChain; + } + + return signer as SignerConfigForChain; + }, + [experimental_customAuth, onAuthRequired] + ); + + const initializeWebViewIfNeeded = useCallback( + async (signer: SignerConfigForChain) => { + if (signer.type === "email" || signer.type === "phone") { + await initializeWebView?.(); + } + }, + [initializeWebView] + ); + const getOrCreateWallet = useCallback( async (args: WalletCreateArgs) => { if (experimental_customAuth?.jwt == null || walletStatus === "in-progress") { @@ -76,54 +133,15 @@ export function CrossmintWalletBaseProvider({ const _onWalletCreationStart = args.options?.experimental_callbacks?.onWalletCreationStart; const _onTransactionStart = args.options?.experimental_callbacks?.onTransactionStart; - if (args?.signer?.type === "email") { - const email = args.signer.email ?? experimental_customAuth?.email; - const _onAuthRequired = args.signer.onAuthRequired ?? onAuthRequired; - - if (email == null) { - throw new Error( - "Email not found in experimental_customAuth or signer. Please set email in experimental_customAuth or signer." - ); - } - args.signer = { - ...args.signer, - email, - onAuthRequired: _onAuthRequired, - }; - } - - if (args?.signer?.type === "phone") { - const phone = args.signer.phone ?? experimental_customAuth?.phone; - const _onAuthRequired = args.signer.onAuthRequired ?? onAuthRequired; - - if (phone == null) { - throw new Error("Phone not found in signer. Please set phone in signer."); - } - args.signer = { - ...args.signer, - phone, - onAuthRequired: _onAuthRequired, - }; - } - - if (args?.signer?.type === "external-wallet") { - const signer = - args.signer?.address != null ? args.signer : experimental_customAuth.externalWalletSigner; + // Resolve signer configuration + const resolvedSigner = resolveSignerConfig(args.signer) as SignerConfigForChain; - if (signer == null) { - throw new Error( - "External wallet config not found in experimental_customAuth or signer. Please set it in experimental_customAuth or signer." - ); - } - args.signer = signer as SignerConfigForChain; - } + // Initialize WebView if needed + await initializeWebViewIfNeeded(resolvedSigner); - if (args.signer.type === "email" || args.signer.type === "phone") { - await initializeWebView?.(); - } const wallet = await wallets.getOrCreateWallet({ chain: args.chain, - signer: args.signer, + signer: resolvedSigner, owner: args.owner, plugins: args.plugins, onCreateConfig: args.onCreateConfig, @@ -145,7 +163,16 @@ export function CrossmintWalletBaseProvider({ return undefined; } }, - [crossmint, experimental_customAuth] + [ + crossmint, + experimental_customAuth, + walletStatus, + wallet, + resolveSignerConfig, + initializeWebViewIfNeeded, + clientTEEConnection, + callbacks, + ] ); const getWallet = useCallback( @@ -157,59 +184,15 @@ export function CrossmintWalletBaseProvider({ try { const wallets = CrossmintWallets.from(crossmint); - let signer = args.signer; - - if (signer.type === "email") { - const email = signer.email ?? experimental_customAuth?.email; - const _onAuthRequired = signer.onAuthRequired ?? onAuthRequired; + const resolvedSigner = resolveSignerConfig(args.signer) as SignerConfigForChain; - if (email == null) { - throw new Error( - "Email not found in experimental_customAuth or signer. Please set email in experimental_customAuth or signer." - ); - } - signer = { - ...signer, - email, - onAuthRequired: _onAuthRequired, - }; - } - - if (signer.type === "phone") { - const phone = signer.phone ?? experimental_customAuth?.phone; - const _onAuthRequired = signer.onAuthRequired ?? onAuthRequired; - - if (phone == null) { - throw new Error("Phone not found in signer. Please set phone in signer."); - } - signer = { - ...signer, - phone, - onAuthRequired: _onAuthRequired, - }; - } - - if (signer.type === "external-wallet") { - const resolvedSigner = - signer.address != null ? signer : experimental_customAuth.externalWalletSigner; - - if (resolvedSigner == null) { - throw new Error( - "External wallet config not found in experimental_customAuth or signer. Please set it in experimental_customAuth or signer." - ); - } - signer = resolvedSigner as SignerConfigForChain; - } - - if (signer.type === "email" || signer.type === "phone") { - await initializeWebView?.(); - } + await initializeWebViewIfNeeded(resolvedSigner); const chainType = args.chain === "solana" ? "solana" : args.chain === "stellar" ? "stellar" : "evm"; const walletLocator = `me:${chainType}:smart`; const wallet = await wallets.getWallet(walletLocator, { chain: args.chain, - signer, + signer: resolvedSigner, options: { clientTEEConnection: clientTEEConnection?.(), experimental_callbacks: callbacks, @@ -221,7 +204,14 @@ export function CrossmintWalletBaseProvider({ return undefined; } }, - [crossmint, experimental_customAuth, onAuthRequired, clientTEEConnection, callbacks, initializeWebView] + [ + crossmint, + experimental_customAuth, + resolveSignerConfig, + initializeWebViewIfNeeded, + clientTEEConnection, + callbacks, + ] ); useEffect(() => { From d59062a80c885478ca7864d863d61a3c430a963c Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Tue, 14 Oct 2025 14:05:45 +0200 Subject: [PATCH 11/82] remove redundant comments --- .../react-base/src/providers/CrossmintWalletBaseProvider.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index e22bb9e71..dae2e4bb0 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -133,10 +133,8 @@ export function CrossmintWalletBaseProvider({ const _onWalletCreationStart = args.options?.experimental_callbacks?.onWalletCreationStart; const _onTransactionStart = args.options?.experimental_callbacks?.onTransactionStart; - // Resolve signer configuration const resolvedSigner = resolveSignerConfig(args.signer) as SignerConfigForChain; - // Initialize WebView if needed await initializeWebViewIfNeeded(resolvedSigner); const wallet = await wallets.getOrCreateWallet({ From c1ea37ea5230c1b8837fd55dda7c32210a0e179b Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Tue, 14 Oct 2025 14:10:01 +0200 Subject: [PATCH 12/82] fix tsc issue --- packages/wallets/src/wallets/wallet-factory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 8d79caf59..3df011515 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -233,7 +233,7 @@ export class WalletFactory { private validateExistingWalletConfig( existingWallet: GetWalletSuccessResponse, - args: WalletArgsFor + args: WalletArgsFor | WalletCreateArgs ): void { if (args.owner != null && existingWallet.owner != null && args.owner !== existingWallet.owner) { throw new WalletCreationError("Wallet owner does not match existing wallet's linked user"); @@ -254,7 +254,7 @@ export class WalletFactory { return; } - if (args.onCreateConfig) { + if ("onCreateConfig" in args) { let expectedAdminSigner = args.onCreateConfig.adminSigner; const existingWalletSigner = (existingWallet?.config as any)?.adminSigner as AdminSignerConfig; From 6b8b3f3afe2d0a489012a5d2d96a72595ae7470f Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Tue, 14 Oct 2025 16:03:01 +0200 Subject: [PATCH 13/82] fix linter --- apps/wallets/quickstart-devkit/app/providers.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/wallets/quickstart-devkit/app/providers.tsx b/apps/wallets/quickstart-devkit/app/providers.tsx index b12848361..c815ddc1c 100644 --- a/apps/wallets/quickstart-devkit/app/providers.tsx +++ b/apps/wallets/quickstart-devkit/app/providers.tsx @@ -302,7 +302,10 @@ function QueryParamsProvider({ children }: { children: React.ReactNode }) { }; if (signerType === "phone" && phoneNumber != null) { createOnLogin.signer = { type: signerType, phone: decodeURIComponent(phoneNumber) }; - createOnLogin.onCreateConfig.adminSigner = { type: signerType, phone: decodeURIComponent(phoneNumber) }; + createOnLogin.onCreateConfig.adminSigner = { + type: signerType, + phone: decodeURIComponent(phoneNumber), + }; } return ( @@ -327,7 +330,10 @@ function QueryParamsProvider({ children }: { children: React.ReactNode }) { }; if (signerType === "phone" && phoneNumber != null) { createOnLogin.signer = { type: signerType, phone: decodeURIComponent(phoneNumber) }; - createOnLogin.onCreateConfig.adminSigner = { type: signerType, phone: decodeURIComponent(phoneNumber) }; + createOnLogin.onCreateConfig.adminSigner = { + type: signerType, + phone: decodeURIComponent(phoneNumber), + }; } return ( From 3e6d5c2c4cfc8529f01c762896f69f051eb8ff70 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Tue, 14 Oct 2025 17:43:16 +0200 Subject: [PATCH 14/82] added client side specific get wallet --- .../src/providers/CrossmintWalletBaseProvider.tsx | 4 +--- packages/wallets/src/sdk.ts | 9 +++++++++ packages/wallets/src/wallets/wallet-factory.ts | 11 +++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index dae2e4bb0..7963dde39 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -186,9 +186,7 @@ export function CrossmintWalletBaseProvider({ await initializeWebViewIfNeeded(resolvedSigner); - const chainType = args.chain === "solana" ? "solana" : args.chain === "stellar" ? "stellar" : "evm"; - const walletLocator = `me:${chainType}:smart`; - const wallet = await wallets.getWallet(walletLocator, { + const wallet = await wallets.getClientSideWallet({ chain: args.chain, signer: resolvedSigner, options: { diff --git a/packages/wallets/src/sdk.ts b/packages/wallets/src/sdk.ts index 706ce048e..1db735b18 100644 --- a/packages/wallets/src/sdk.ts +++ b/packages/wallets/src/sdk.ts @@ -32,6 +32,15 @@ export class CrossmintWallets { return await this.walletFactory.getOrCreateWallet(options); } + /** + * Get an existing wallet by its locator, can only be called on the client side + * @param options - Wallet options + * @returns A wallet if found, throws WalletNotAvailableError if not found + */ + public async getClientSideWallet(options: WalletArgsFor): Promise> { + return await this.walletFactory.getClientSideWallet(options); + } + /** * Get an existing wallet by its locator, can only be called on the server side * @param walletLocator - Wallet locator diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 3df011515..772ab5e65 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -38,7 +38,18 @@ export class WalletFactory { return this.createWallet(args); } + public async getClientSideWallet(args: WalletArgsFor): Promise> { + const existingWallet = await this.apiClient.getWallet(`me:${this.getChainType(args.chain)}:smart`); + if ("error" in existingWallet) { + throw new WalletNotAvailableError(JSON.stringify(existingWallet)); + } + return this.createWalletInstance(existingWallet, args); + } + public async getWallet(walletLocator: string, args: WalletArgsFor): Promise> { + if (!this.apiClient.isServerSide) { + throw new WalletCreationError("getWallet is not supported on client side, use getOrCreateWallet instead"); + } const existingWallet = await this.apiClient.getWallet(walletLocator); if ("error" in existingWallet) { throw new WalletNotAvailableError(JSON.stringify(existingWallet)); From 133dc921d1d7d07ee87b4ab76b3be252ffe83f99 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Wed, 15 Oct 2025 16:35:39 +0200 Subject: [PATCH 15/82] fix signers and transactions --- packages/wallets/src/wallets/types.ts | 2 +- .../wallets/src/wallets/wallet-factory.ts | 121 +++++++++++------- packages/wallets/src/wallets/wallet.ts | 4 +- 3 files changed, 77 insertions(+), 50 deletions(-) diff --git a/packages/wallets/src/wallets/types.ts b/packages/wallets/src/wallets/types.ts index 64562b460..13a4bea32 100644 --- a/packages/wallets/src/wallets/types.ts +++ b/packages/wallets/src/wallets/types.ts @@ -97,7 +97,7 @@ export type DelegatedSigner = { export type OnCreateConfig = { adminSigner: SignerConfigForChain; - delegatedSigners?: Array; + delegatedSigners?: Array>; }; // Approvals diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 772ab5e65..b03b23f58 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -7,6 +7,7 @@ import type { GetWalletSuccessResponse, RegisterSignerPasskeyParams, DelegatedSigner as DelegatedSignerResponse, + RegisterSignerParams, } from "../api"; import { WalletCreationError, WalletNotAvailableError } from "../utils/errors"; import type { Chain } from "../chains/chains"; @@ -62,7 +63,16 @@ export class WalletFactory { await args.options?.experimental_callbacks?.onWalletCreationStart?.(); let adminSignerConfig = args.onCreateConfig.adminSigner; - const delegatedSigners = args.onCreateConfig.delegatedSigners; + const delegatedSigners = await Promise.all( + args.onCreateConfig.delegatedSigners?.map( + async (signer): Promise => { + if (signer.type === "passkey") { + return { signer: await this.createPasskeyAdminSigner(signer) }; + } + return { signer: this.getSignerLocator(signer) }; + } + ) ?? [] + ); const tempArgs = { ...args, signer: adminSignerConfig }; this.mutateSignerFromCustomAuth(tempArgs, true); @@ -131,49 +141,40 @@ export class WalletFactory { switch (signerArgs.type) { case "api-key": { - if (walletResponse.config?.adminSigner.type !== "api-key") { - throw new WalletCreationError("API key signer does not match the wallet's signer type"); - } + const walletSigner = this.getWalletSigner(walletResponse, "api-key"); return { type: "api-key", - address: walletResponse.config.adminSigner.address, - locator: walletResponse.config.adminSigner.locator, + address: walletSigner.address, + locator: walletSigner.locator, }; } - case "external-wallet": - if (walletResponse.config?.adminSigner.type !== "external-wallet") { - throw new WalletCreationError("External wallet signer does not match the wallet's signer type"); - } + case "external-wallet": { + const walletSigner = this.getWalletSigner(walletResponse, "external-wallet"); - return { ...walletResponse.config.adminSigner, ...signerArgs } as InternalSignerConfig; - - case "passkey": - if (walletResponse.config?.adminSigner.type !== "passkey") { - throw new WalletCreationError("Passkey signer does not match the wallet's signer type"); - } + return { ...walletSigner, ...signerArgs } as InternalSignerConfig; + } + case "passkey": { + const walletSigner = this.getWalletSigner(walletResponse, "passkey"); return { type: "passkey", - id: walletResponse.config.adminSigner.id, - name: walletResponse.config.adminSigner.name, - locator: walletResponse.config.adminSigner.locator, + id: walletSigner.id, + name: walletSigner.name, + locator: walletSigner.locator, onCreatePasskey: signerArgs.onCreatePasskey, onSignWithPasskey: signerArgs.onSignWithPasskey, }; - + } case "email": { - if (walletResponse.config?.adminSigner.type !== "email") { - throw new WalletCreationError("Email signer does not match the wallet's signer type"); - } + const walletSigner = this.getWalletSigner(walletResponse, "email"); - const { locator, email, address } = walletResponse.config.adminSigner; return { type: "email", - email, - locator, - address, + email: walletSigner.email, + locator: "locator" in walletSigner ? walletSigner.locator : this.getSignerLocator(signerArgs), + address: "address" in walletSigner ? walletSigner.address : walletResponse.address, crossmint: this.apiClient.crossmint, onAuthRequired: signerArgs.onAuthRequired, clientTEEConnection: options?.clientTEEConnection, @@ -181,16 +182,13 @@ export class WalletFactory { } case "phone": { - if (walletResponse.config?.adminSigner.type !== "phone") { - throw new WalletCreationError("Phone signer does not match the wallet's signer type"); - } + const walletSigner = this.getWalletSigner(walletResponse, "phone"); - const { locator, phone, address } = walletResponse.config.adminSigner; return { type: "phone", - phone, - locator, - address, + phone: walletSigner.phone, + locator: "locator" in walletSigner ? walletSigner.locator : this.getSignerLocator(signerArgs), + address: "address" in walletSigner ? walletSigner.address : walletResponse.address, crossmint: this.apiClient.crossmint, onAuthRequired: signerArgs.onAuthRequired, clientTEEConnection: options?.clientTEEConnection, @@ -202,6 +200,22 @@ export class WalletFactory { } } + private getWalletSigner["type"]>( + wallet: GetWalletSuccessResponse, + signerType: T + ): Extract { + const adminSigner = (wallet.config as any)?.adminSigner as AdminSignerConfig; + const delegatedSigners = ((wallet.config as any)?.delegatedSigners as DelegatedSignerResponse[]) || []; + if (adminSigner?.type === signerType) { + return adminSigner as Extract; + } + const delegatedSigner = delegatedSigners.find((ds) => ds.type === signerType); + if (delegatedSigner != null) { + return delegatedSigner as Extract; + } + throw new WalletCreationError(`${signerType} signer does not match the wallet's signer type`); + } + private async createPasskeyAdminSigner( signer: SignerConfigForChain ): Promise { @@ -304,11 +318,13 @@ export class WalletFactory { } catch {} } - const signerLocator = this.getSignerLocator(signer); - const isDelegated = delegatedSigners.some((ds) => ds.locator === signerLocator); + const delegatedSigner = delegatedSigners.find((ds) => ds.type === signer.type); - if (isDelegated) { - return; + if (delegatedSigner != null) { + try { + compareSignerConfigs(signer, delegatedSigner); + return; + } catch {} } throw new WalletCreationError( @@ -316,7 +332,7 @@ export class WalletFactory { ); } - private getSignerLocator(signer: SignerConfigForChain): string { + private getSignerLocator(signer: SignerConfigForChain | RegisterSignerPasskeyParams): string { if (signer.type === "external-wallet") { return `external-wallet:${signer.address}`; } @@ -326,8 +342,8 @@ export class WalletFactory { if (signer.type === "phone" && signer.phone) { return `phone:${signer.phone}`; } - if (signer.type === "passkey" && signer.name) { - return `passkey:${signer.name}`; + if (signer.type === "passkey" && "id" in signer) { + return `passkey:${signer.id}`; } if (signer.type === "api-key") { return "api-key"; @@ -335,9 +351,9 @@ export class WalletFactory { return signer.type; } - private validateDelegatedSigners( + private validateDelegatedSigners( existingWallet: GetWalletSuccessResponse, - inputDelegatedSigners: Array + inputDelegatedSigners: Array> ): void { const existingDelegatedSigners = (existingWallet?.config as any)?.delegatedSigners as | DelegatedSignerResponse[] @@ -355,19 +371,28 @@ export class WalletFactory { ); } - // Check that each input delegated signer exists in the wallet - // (wallet can have additional signers that weren't specified in input) - for (const argSigner of inputDelegatedSigners) { + for (const inputSigner of inputDelegatedSigners) { const matchingExistingSigner = existingDelegatedSigners.find( - (existingSigner) => existingSigner.locator === argSigner.signer + (existingSigner) => existingSigner.type === inputSigner.type ); if (matchingExistingSigner == null) { const walletSigners = existingDelegatedSigners.map((s) => s.locator).join(", "); throw new WalletCreationError( - `Delegated signer '${argSigner.signer}' does not exist in wallet "${existingWallet.address}". Available delegated signers: ${walletSigners}. ${DELEGATED_SIGNER_MISMATCH_ERROR}` + `Delegated signer '${inputSigner.type}' does not exist in wallet "${existingWallet.address}". Available delegated signers: ${walletSigners}. ${DELEGATED_SIGNER_MISMATCH_ERROR}` ); } + + if (inputSigner.type !== matchingExistingSigner.type) { + throw new WalletCreationError( + `Delegated signer type mismatch for '${inputSigner.type}'. Expected type '${matchingExistingSigner.type}' from existing wallet but found '${inputSigner.type}'` + ); + } + + compareSignerConfigs( + inputSigner as Record, + matchingExistingSigner as Record + ); } } diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index c5e81d552..f325e59ff 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -280,7 +280,9 @@ export class Wallet { const sendParams = { recipient, amount, - ...(options?.experimental_signer != null ? { signer: options.experimental_signer } : {}), + ...(options?.experimental_signer != null + ? { signer: options.experimental_signer } + : { signer: this.signer.locator() }), }; const transactionCreationResponse = await this.#apiClient.send(this.walletLocator, tokenLocator, sendParams); From 5bd3ce450ca7c22eac20b111315ca8929a97afdc Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Wed, 15 Oct 2025 16:44:59 +0200 Subject: [PATCH 16/82] fix delegated signer --- apps/wallets/quickstart-devkit/app/providers.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/wallets/quickstart-devkit/app/providers.tsx b/apps/wallets/quickstart-devkit/app/providers.tsx index c815ddc1c..05fa6126b 100644 --- a/apps/wallets/quickstart-devkit/app/providers.tsx +++ b/apps/wallets/quickstart-devkit/app/providers.tsx @@ -261,7 +261,10 @@ function StellarCrossmintAuthProvider({ children, apiKey }: { children: React.Re onCreateConfig: { adminSigner: { type: "email" }, delegatedSigners: [ - { signer: "external-wallet:GDUNAPJW6JYL4JEBFR7B5RZZD6B4TOUEWPFTT3V47IHI7QJPA43UFEY6" }, + { + type: "external-wallet", + address: "GDUNAPJW6JYL4JEBFR7B5RZZD6B4TOUEWPFTT3V47IHI7QJPA43UFEY6", + }, ], }, }} From 34f6f163b060fd58203d1b99c5d2e25a5f27c1d1 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Wed, 15 Oct 2025 17:03:45 +0200 Subject: [PATCH 17/82] remove redundant check --- packages/wallets/src/wallets/wallet-factory.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index b03b23f58..a7d8649f6 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -383,12 +383,6 @@ export class WalletFactory { ); } - if (inputSigner.type !== matchingExistingSigner.type) { - throw new WalletCreationError( - `Delegated signer type mismatch for '${inputSigner.type}'. Expected type '${matchingExistingSigner.type}' from existing wallet but found '${inputSigner.type}'` - ); - } - compareSignerConfigs( inputSigner as Record, matchingExistingSigner as Record From e0397c1d33719a673959c03e8134a420b3082110 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Wed, 15 Oct 2025 17:22:59 +0200 Subject: [PATCH 18/82] fix unit test --- .../wallets/src/wallets/wallet-factory.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/wallets/src/wallets/wallet-factory.test.ts b/packages/wallets/src/wallets/wallet-factory.test.ts index fbdd8748c..4b6198618 100644 --- a/packages/wallets/src/wallets/wallet-factory.test.ts +++ b/packages/wallets/src/wallets/wallet-factory.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi, type MockedFunction } import { WalletFactory } from "./wallet-factory"; import { WalletCreationError } from "../utils/errors"; import type { ApiClient, GetWalletSuccessResponse } from "../api"; -import type { WalletArgsFor } from "./types"; +import type { WalletArgsFor, WalletCreateArgs } from "./types"; type MockedApiClient = { isServerSide: boolean; @@ -59,7 +59,7 @@ describe("WalletFactory - OnCreateConfig Support", () => { mockApiClient.getWallet.mockResolvedValue({ error: "not found" }); mockApiClient.createWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); - const args: WalletArgsFor<"solana"> = { + const args: WalletCreateArgs<"solana"> = { chain: "solana", signer: { type: "external-wallet", @@ -70,7 +70,7 @@ describe("WalletFactory - OnCreateConfig Support", () => { type: "external-wallet", address: "AdminSignerAddress123", }, - delegatedSigners: [{ signer: "external-wallet:DelegatedSignerAddress456" }], + delegatedSigners: [{ type: "external-wallet", address: "DelegatedSignerAddress456" }], }, }; @@ -92,7 +92,7 @@ describe("WalletFactory - OnCreateConfig Support", () => { it("should validate existing wallet against onCreateConfig admin signer", async () => { mockApiClient.getWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); - const args: WalletArgsFor<"solana"> = { + const args: WalletCreateArgs<"solana"> = { chain: "solana", signer: { type: "external-wallet", @@ -103,7 +103,7 @@ describe("WalletFactory - OnCreateConfig Support", () => { type: "external-wallet", address: "AdminSignerAddress123", }, - delegatedSigners: [{ signer: "external-wallet:DelegatedSignerAddress456" }], + delegatedSigners: [{ type: "external-wallet", address: "DelegatedSignerAddress456" }], }, }; @@ -133,7 +133,7 @@ describe("WalletFactory - OnCreateConfig Support", () => { it("should validate that signer can use the wallet when onCreateConfig is provided", async () => { mockApiClient.getWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); - const argsWithValidDelegatedSigner: WalletArgsFor<"solana"> = { + const argsWithValidDelegatedSigner: WalletCreateArgs<"solana"> = { chain: "solana", signer: { type: "external-wallet", @@ -144,7 +144,7 @@ describe("WalletFactory - OnCreateConfig Support", () => { type: "external-wallet", address: "AdminSignerAddress123", }, - delegatedSigners: [{ signer: "external-wallet:DelegatedSignerAddress456" }], + delegatedSigners: [{ type: "external-wallet", address: "DelegatedSignerAddress456" }], }, }; @@ -154,7 +154,7 @@ describe("WalletFactory - OnCreateConfig Support", () => { it("should throw error when signer cannot use wallet with onCreateConfig", async () => { mockApiClient.getWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); - const argsWithInvalidSigner: WalletArgsFor<"solana"> = { + const argsWithInvalidSigner: WalletCreateArgs<"solana"> = { chain: "solana", signer: { type: "external-wallet", @@ -165,7 +165,7 @@ describe("WalletFactory - OnCreateConfig Support", () => { type: "external-wallet", address: "AdminSignerAddress123", }, - delegatedSigners: [{ signer: "external-wallet:DelegatedSignerAddress456" }], + delegatedSigners: [{ type: "external-wallet", address: "DelegatedSignerAddress456" }], }, }; From 9276bcf9b5d96476275aac4ec219b7c46baaaf33 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Wed, 15 Oct 2025 17:34:36 +0200 Subject: [PATCH 19/82] make oncreateconfig optional --- .../wallets/quickstart-devkit/app/providers.tsx | 17 ----------------- packages/wallets/src/wallets/types.ts | 2 +- packages/wallets/src/wallets/wallet-factory.ts | 8 ++++---- 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/apps/wallets/quickstart-devkit/app/providers.tsx b/apps/wallets/quickstart-devkit/app/providers.tsx index 05fa6126b..4108aba7a 100644 --- a/apps/wallets/quickstart-devkit/app/providers.tsx +++ b/apps/wallets/quickstart-devkit/app/providers.tsx @@ -52,7 +52,6 @@ function EVMCrossmintAuthProvider({ : { chain: process.env.NEXT_PUBLIC_EVM_CHAIN as any, signer: { type: "email" }, - onCreateConfig: { adminSigner: { type: "email" } }, } } > @@ -85,7 +84,6 @@ function EVMPrivyProvider({ children, apiKey }: { children: React.ReactNode; api createOnLogin={{ chain: process.env.NEXT_PUBLIC_EVM_CHAIN as any, signer: { type: "email" }, - onCreateConfig: { adminSigner: { type: "email" } }, }} > {children} @@ -124,7 +122,6 @@ function EVMFirebaseProvider({ children, apiKey }: { children: React.ReactNode; createOnLogin={{ chain: process.env.NEXT_PUBLIC_EVM_CHAIN as any, signer: { type: "email" }, - onCreateConfig: { adminSigner: { type: "email" } }, }} > {children} @@ -155,7 +152,6 @@ function SolanaCrossmintAuthProvider({ : { chain: "solana", signer: { type: "email" }, - onCreateConfig: { adminSigner: { type: "email" } }, } } > @@ -188,7 +184,6 @@ function SolanaPrivyProvider({ children, apiKey }: { children: React.ReactNode; createOnLogin={{ chain: "solana", signer: { type: "external-wallet" }, - onCreateConfig: { adminSigner: { type: "external-wallet" } }, }} > {children} @@ -220,7 +215,6 @@ function SolanaDynamicLabsProvider({ createOnLogin={{ chain: "solana", signer: { type: "external-wallet" }, - onCreateConfig: { adminSigner: { type: "external-wallet" } }, }} > {children} @@ -237,7 +231,6 @@ function SolanaFirebaseProvider({ children, apiKey }: { children: React.ReactNod createOnLogin={{ chain: "solana", signer: { type: "email" }, - onCreateConfig: { adminSigner: { type: "email" } }, }} > {children} @@ -301,14 +294,9 @@ function QueryParamsProvider({ children }: { children: React.ReactNode }) { const createOnLogin: any = { chain: chainId, signer: { type: signerType }, - onCreateConfig: { adminSigner: { type: signerType } }, }; if (signerType === "phone" && phoneNumber != null) { createOnLogin.signer = { type: signerType, phone: decodeURIComponent(phoneNumber) }; - createOnLogin.onCreateConfig.adminSigner = { - type: signerType, - phone: decodeURIComponent(phoneNumber), - }; } return ( @@ -329,14 +317,9 @@ function QueryParamsProvider({ children }: { children: React.ReactNode }) { const createOnLogin: any = { chain: "solana", signer: { type: signerType }, - onCreateConfig: { adminSigner: { type: signerType } }, }; if (signerType === "phone" && phoneNumber != null) { createOnLogin.signer = { type: signerType, phone: decodeURIComponent(phoneNumber) }; - createOnLogin.onCreateConfig.adminSigner = { - type: signerType, - phone: decodeURIComponent(phoneNumber), - }; } return ( diff --git a/packages/wallets/src/wallets/types.ts b/packages/wallets/src/wallets/types.ts index 13a4bea32..98fec27de 100644 --- a/packages/wallets/src/wallets/types.ts +++ b/packages/wallets/src/wallets/types.ts @@ -128,7 +128,7 @@ export type WalletArgsFor = { }; export type WalletCreateArgs = WalletArgsFor & { - onCreateConfig: OnCreateConfig; + onCreateConfig?: OnCreateConfig; }; type ChainExtras = { diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index a7d8649f6..225c42520 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -62,9 +62,9 @@ export class WalletFactory { public async createWallet(args: WalletCreateArgs): Promise> { await args.options?.experimental_callbacks?.onWalletCreationStart?.(); - let adminSignerConfig = args.onCreateConfig.adminSigner; + let adminSignerConfig = args.onCreateConfig?.adminSigner ?? args.signer; const delegatedSigners = await Promise.all( - args.onCreateConfig.delegatedSigners?.map( + args.onCreateConfig?.delegatedSigners?.map( async (signer): Promise => { if (signer.type === "passkey") { return { signer: await this.createPasskeyAdminSigner(signer) }; @@ -280,7 +280,7 @@ export class WalletFactory { } if ("onCreateConfig" in args) { - let expectedAdminSigner = args.onCreateConfig.adminSigner; + let expectedAdminSigner = args.onCreateConfig?.adminSigner ?? args.signer; const existingWalletSigner = (existingWallet?.config as any)?.adminSigner as AdminSignerConfig; const tempArgs = { ...args, signer: expectedAdminSigner }; @@ -296,7 +296,7 @@ export class WalletFactory { compareSignerConfigs(expectedAdminSigner, existingWalletSigner); } - if (args.onCreateConfig.delegatedSigners != null) { + if (args.onCreateConfig?.delegatedSigners != null) { this.validateDelegatedSigners(existingWallet, args.onCreateConfig.delegatedSigners); } } From f8905605bf20e081c4316504b0d74d4896fe47eb Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Wed, 15 Oct 2025 17:47:54 +0200 Subject: [PATCH 20/82] remove unnecesary format changes --- .../quickstart-devkit/app/providers.tsx | 46 ++++--------------- apps/wallets/smart-wallet/expo/app/index.tsx | 6 +-- .../next/src/app/_lib/providers.tsx | 3 -- 3 files changed, 9 insertions(+), 46 deletions(-) diff --git a/apps/wallets/quickstart-devkit/app/providers.tsx b/apps/wallets/quickstart-devkit/app/providers.tsx index 4108aba7a..4851bc14e 100644 --- a/apps/wallets/quickstart-devkit/app/providers.tsx +++ b/apps/wallets/quickstart-devkit/app/providers.tsx @@ -49,10 +49,7 @@ function EVMCrossmintAuthProvider({ createOnLogin={ createOnLogin != null ? createOnLogin - : { - chain: process.env.NEXT_PUBLIC_EVM_CHAIN as any, - signer: { type: "email" }, - } + : { chain: process.env.NEXT_PUBLIC_EVM_CHAIN as any, signer: { type: "email" } } } > {children} @@ -119,10 +116,7 @@ function EVMFirebaseProvider({ children, apiKey }: { children: React.ReactNode; return ( {children} @@ -147,12 +141,7 @@ function SolanaCrossmintAuthProvider({ {children} @@ -181,10 +170,7 @@ function SolanaPrivyProvider({ children, apiKey }: { children: React.ReactNode; {children} @@ -211,12 +197,7 @@ function SolanaDynamicLabsProvider({ }} > - + {children} @@ -227,12 +208,7 @@ function SolanaDynamicLabsProvider({ function SolanaFirebaseProvider({ children, apiKey }: { children: React.ReactNode; apiKey?: string }) { return ( - + {children} @@ -291,10 +267,7 @@ function QueryParamsProvider({ children }: { children: React.ReactNode }) { return {children}; case "crossmint": default: - const createOnLogin: any = { - chain: chainId, - signer: { type: signerType }, - }; + const createOnLogin: any = { chain: chainId, signer: { type: signerType } }; if (signerType === "phone" && phoneNumber != null) { createOnLogin.signer = { type: signerType, phone: decodeURIComponent(phoneNumber) }; } @@ -314,10 +287,7 @@ function QueryParamsProvider({ children }: { children: React.ReactNode }) { return {children}; case "crossmint": default: - const createOnLogin: any = { - chain: "solana", - signer: { type: signerType }, - }; + const createOnLogin: any = { chain: "solana", signer: { type: signerType } }; if (signerType === "phone" && phoneNumber != null) { createOnLogin.signer = { type: signerType, phone: decodeURIComponent(phoneNumber) }; } diff --git a/apps/wallets/smart-wallet/expo/app/index.tsx b/apps/wallets/smart-wallet/expo/app/index.tsx index 16271fb04..56bd59383 100644 --- a/apps/wallets/smart-wallet/expo/app/index.tsx +++ b/apps/wallets/smart-wallet/expo/app/index.tsx @@ -73,11 +73,7 @@ export default function Index() { } setIsLoading(true); try { - await getOrCreateWallet({ - chain: "base-sepolia", - signer: { type: "email" }, - onCreateConfig: { adminSigner: { type: "email" } }, - }); + await getOrCreateWallet({ chain: "base-sepolia", signer: { type: "email" } }); } catch (error) { console.error("Error initializing wallet:", error); } finally { diff --git a/apps/wallets/smart-wallet/next/src/app/_lib/providers.tsx b/apps/wallets/smart-wallet/next/src/app/_lib/providers.tsx index 2d4e46dc9..99021f668 100644 --- a/apps/wallets/smart-wallet/next/src/app/_lib/providers.tsx +++ b/apps/wallets/smart-wallet/next/src/app/_lib/providers.tsx @@ -57,9 +57,6 @@ function CrossmintProviders({ children }: { children: ReactNode }) { createOnLogin={{ chain: walletType === "solana-smart-wallet" ? "solana" : (process.env.NEXT_PUBLIC_CHAIN as any), signer: { type: "api-key" }, - onCreateConfig: { - adminSigner: { type: "api-key" }, - }, }} > {children} From b04e71d68221a5c5b12a0dc1a79aee54ee0d7eb2 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 16 Oct 2025 09:56:50 +0200 Subject: [PATCH 21/82] unify getWallet functionality --- packages/wallets/src/sdk.ts | 33 +-- .../src/wallets/wallet-factory.test.ts | 226 +++++++++++++++++- .../wallets/src/wallets/wallet-factory.ts | 40 +++- 3 files changed, 273 insertions(+), 26 deletions(-) diff --git a/packages/wallets/src/sdk.ts b/packages/wallets/src/sdk.ts index 1db735b18..eeeb2c4d0 100644 --- a/packages/wallets/src/sdk.ts +++ b/packages/wallets/src/sdk.ts @@ -33,22 +33,27 @@ export class CrossmintWallets { } /** - * Get an existing wallet by its locator, can only be called on the client side - * @param options - Wallet options - * @returns A wallet if found, throws WalletNotAvailableError if not found - */ - public async getClientSideWallet(options: WalletArgsFor): Promise> { - return await this.walletFactory.getClientSideWallet(options); - } - - /** - * Get an existing wallet by its locator, can only be called on the server side - * @param walletLocator - Wallet locator - * @param options - Wallet options + * Get an existing wallet + * Can be called on the client side or server side + * If called on the client side, just the wallet options must be provided + * If called on the server side, the wallet locator and options must be provided + * @param argsOrLocator - Wallet locator or wallet options + * @param maybeArgs - Wallet options * @returns A wallet if found, throws WalletNotAvailableError if not found */ - public async getWallet(walletLocator: string, options: WalletArgsFor): Promise> { - return await this.walletFactory.getWallet(walletLocator, options); + public async getWallet(args: WalletArgsFor): Promise>; + public async getWallet(walletLocator: string, args: WalletArgsFor): Promise>; + public async getWallet( + argsOrLocator: string | WalletArgsFor, + maybeArgs?: WalletArgsFor + ): Promise> { + if (typeof argsOrLocator === "string") { + if (maybeArgs == null) { + throw new Error("Args parameter is required when walletLocator is provided"); + } + return await this.walletFactory.getWallet(argsOrLocator, maybeArgs); + } + return await this.walletFactory.getWallet(argsOrLocator); } /** diff --git a/packages/wallets/src/wallets/wallet-factory.test.ts b/packages/wallets/src/wallets/wallet-factory.test.ts index 4b6198618..2ed699c2e 100644 --- a/packages/wallets/src/wallets/wallet-factory.test.ts +++ b/packages/wallets/src/wallets/wallet-factory.test.ts @@ -56,7 +56,7 @@ describe("WalletFactory - OnCreateConfig Support", () => { describe("getOrCreateWallet with onCreateConfig", () => { it("should create wallet with onCreateConfig admin signer when wallet does not exist", async () => { - mockApiClient.getWallet.mockResolvedValue({ error: "not found" }); + mockApiClient.getWallet.mockResolvedValue({ error: true, message: "not found" }); mockApiClient.createWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); const args: WalletCreateArgs<"solana"> = { @@ -113,7 +113,7 @@ describe("WalletFactory - OnCreateConfig Support", () => { it("should throw error when onCreateConfig admin signer does not match existing wallet", async () => { mockApiClient.getWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); - const args: WalletArgsFor<"solana"> = { + const args: WalletCreateArgs<"solana"> = { chain: "solana", signer: { type: "external-wallet", @@ -176,4 +176,226 @@ describe("WalletFactory - OnCreateConfig Support", () => { ); }); }); + + describe("getWallet - Unified client and server side usage", () => { + describe("Client-side usage", () => { + beforeEach(() => { + mockApiClient.isServerSide = false; + }); + + it("should fetch wallet with single parameter (args only)", async () => { + mockApiClient.getWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); + + const args: WalletArgsFor<"solana"> = { + chain: "solana", + signer: { + type: "external-wallet", + address: "AdminSignerAddress123", + }, + }; + + const wallet = await walletFactory.getWallet(args); + + expect(mockApiClient.getWallet).toHaveBeenCalledWith("me:solana:smart"); + expect(wallet).toBeDefined(); + expect(wallet.address).toBe("9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"); + }); + + it("should construct correct locator for EVM chains", async () => { + const evmWallet = { + chainType: "evm" as const, + type: "smart" as const, + address: "0x123", + owner: "test-owner", + config: { + adminSigner: { + type: "external-wallet" as const, + address: "AdminSignerAddress123", + locator: "external-wallet:AdminSignerAddress123", + }, + }, + createdAt: Date.now(), + } as GetWalletSuccessResponse; + mockApiClient.getWallet.mockResolvedValue(evmWallet); + + const args: WalletArgsFor<"base"> = { + chain: "base", + signer: { + type: "external-wallet", + address: "AdminSignerAddress123", + }, + }; + + await walletFactory.getWallet(args); + + expect(mockApiClient.getWallet).toHaveBeenCalledWith("me:evm:smart"); + }); + + it("should construct correct locator for Stellar chains", async () => { + const stellarWallet = { + chainType: "stellar" as const, + type: "smart" as const, + address: "GTEST123", + owner: "test-owner", + config: { + adminSigner: { + type: "external-wallet" as const, + address: "AdminSignerAddress123", + locator: "external-wallet:AdminSignerAddress123", + }, + }, + createdAt: Date.now(), + } as GetWalletSuccessResponse; + mockApiClient.getWallet.mockResolvedValue(stellarWallet); + + const args: WalletArgsFor<"stellar"> = { + chain: "stellar", + signer: { + type: "external-wallet", + address: "AdminSignerAddress123", + }, + }; + + await walletFactory.getWallet(args); + + expect(mockApiClient.getWallet).toHaveBeenCalledWith("me:stellar:smart"); + }); + + it("should throw error when trying to use walletLocator parameter on client side", async () => { + const args: WalletArgsFor<"solana"> = { + chain: "solana", + signer: { + type: "external-wallet", + address: "AdminSignerAddress123", + }, + }; + + await expect(walletFactory.getWallet("email:user@example.com:solana:smart", args)).rejects.toThrow( + new WalletCreationError( + "getWallet with walletLocator is not supported on client side, use getOrCreateWallet instead" + ) + ); + }); + + it("should throw error when wallet not found", async () => { + mockApiClient.getWallet.mockResolvedValue({ error: true, message: "not found" }); + + const args: WalletArgsFor<"solana"> = { + chain: "solana", + signer: { + type: "external-wallet", + address: "AdminSignerAddress123", + }, + }; + + await expect(walletFactory.getWallet(args)).rejects.toThrow(); + }); + }); + + describe("Server-side usage", () => { + beforeEach(() => { + mockApiClient.isServerSide = true; + }); + + it("should fetch wallet with walletLocator parameter", async () => { + mockApiClient.getWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); + + const walletLocator = "email:user@example.com:solana:smart"; + const args: WalletArgsFor<"solana"> = { + chain: "solana", + signer: { + type: "external-wallet", + address: "AdminSignerAddress123", + }, + }; + + const wallet = await walletFactory.getWallet(walletLocator, args); + + expect(mockApiClient.getWallet).toHaveBeenCalledWith(walletLocator); + expect(wallet).toBeDefined(); + expect(wallet.address).toBe("9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"); + }); + + it("should work with different walletLocator formats", async () => { + mockApiClient.getWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); + + const testCases = [ + "email:user@example.com:solana:smart", + "phone:+1234567890:evm:smart", + "external-wallet:0x123:evm:smart", + ]; + + const args: WalletArgsFor<"solana"> = { + chain: "solana", + signer: { + type: "external-wallet", + address: "AdminSignerAddress123", + }, + }; + + for (const locator of testCases) { + await walletFactory.getWallet(locator, args); + expect(mockApiClient.getWallet).toHaveBeenCalledWith(locator); + } + }); + + it("should throw error when walletLocator is not provided on server side", async () => { + const args: WalletArgsFor<"solana"> = { + chain: "solana", + signer: { + type: "external-wallet", + address: "AdminSignerAddress123", + }, + }; + + await expect(walletFactory.getWallet(args)).rejects.toThrow( + new WalletCreationError( + "getWallet on server side requires a walletLocator parameter. Use getWallet(walletLocator, args) instead." + ) + ); + }); + + it("should throw error when wallet not found", async () => { + mockApiClient.getWallet.mockResolvedValue({ error: true, message: "not found" }); + + const args: WalletArgsFor<"solana"> = { + chain: "solana", + signer: { + type: "external-wallet", + address: "AdminSignerAddress123", + }, + }; + + await expect(walletFactory.getWallet("email:user@example.com:solana:smart", args)).rejects.toThrow(); + }); + + it("should validate signer can use the wallet", async () => { + mockApiClient.getWallet.mockResolvedValue(mockWalletWithAdminAndDelegated); + + const validArgs: WalletArgsFor<"solana"> = { + chain: "solana", + signer: { + type: "external-wallet", + address: "AdminSignerAddress123", + }, + }; + + await expect( + walletFactory.getWallet("email:user@example.com:solana:smart", validArgs) + ).resolves.toBeDefined(); + + const invalidArgs: WalletArgsFor<"solana"> = { + chain: "solana", + signer: { + type: "external-wallet", + address: "UnauthorizedAddress", + }, + }; + + await expect( + walletFactory.getWallet("email:user@example.com:solana:smart", invalidArgs) + ).rejects.toThrow(WalletCreationError); + }); + }); + }); }); diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 225c42520..de2113e89 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -39,18 +39,38 @@ export class WalletFactory { return this.createWallet(args); } - public async getClientSideWallet(args: WalletArgsFor): Promise> { - const existingWallet = await this.apiClient.getWallet(`me:${this.getChainType(args.chain)}:smart`); - if ("error" in existingWallet) { - throw new WalletNotAvailableError(JSON.stringify(existingWallet)); + // Client-side + public async getWallet(args: WalletArgsFor): Promise>; + // Server-side + public async getWallet(walletLocator: string, args: WalletArgsFor): Promise>; + public async getWallet( + argsOrLocator: string | WalletArgsFor, + maybeArgs?: WalletArgsFor + ): Promise> { + let walletLocator: string; + let args: WalletArgsFor; + + if (typeof argsOrLocator === "string") { + if (!this.apiClient.isServerSide) { + throw new WalletCreationError( + "getWallet with walletLocator is not supported on client side, use getOrCreateWallet instead" + ); + } + if (maybeArgs == null) { + throw new WalletCreationError("Args parameter is required when walletLocator is provided"); + } + walletLocator = argsOrLocator; + args = maybeArgs; + } else { + if (this.apiClient.isServerSide) { + throw new WalletCreationError( + "getWallet on server side requires a walletLocator parameter. Use getWallet(walletLocator, args) instead." + ); + } + args = argsOrLocator; + walletLocator = `me:${this.getChainType(args.chain)}:smart`; } - return this.createWalletInstance(existingWallet, args); - } - public async getWallet(walletLocator: string, args: WalletArgsFor): Promise> { - if (!this.apiClient.isServerSide) { - throw new WalletCreationError("getWallet is not supported on client side, use getOrCreateWallet instead"); - } const existingWallet = await this.apiClient.getWallet(walletLocator); if ("error" in existingWallet) { throw new WalletNotAvailableError(JSON.stringify(existingWallet)); From 43c64d8719c9e9176207a07714b13b0a04bbe3aa Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 16 Oct 2025 09:58:54 +0200 Subject: [PATCH 22/82] remove admin from name --- packages/wallets/src/wallets/wallet-factory.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index de2113e89..1253536d9 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -87,7 +87,7 @@ export class WalletFactory { args.onCreateConfig?.delegatedSigners?.map( async (signer): Promise => { if (signer.type === "passkey") { - return { signer: await this.createPasskeyAdminSigner(signer) }; + return { signer: await this.createPasskeySigner(signer) }; } return { signer: this.getSignerLocator(signer) }; } @@ -100,7 +100,7 @@ export class WalletFactory { const adminSigner = adminSignerConfig.type === "passkey" - ? await this.createPasskeyAdminSigner(adminSignerConfig) + ? await this.createPasskeySigner(adminSignerConfig) : adminSignerConfig; const walletResponse = await this.apiClient.createWallet({ @@ -236,7 +236,7 @@ export class WalletFactory { throw new WalletCreationError(`${signerType} signer does not match the wallet's signer type`); } - private async createPasskeyAdminSigner( + private async createPasskeySigner( signer: SignerConfigForChain ): Promise { if (signer.type !== "passkey") { From 77faecace90f510d410b9ace99c091f414f81e5b Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 16 Oct 2025 10:11:18 +0200 Subject: [PATCH 23/82] fix react provider --- .../react-base/src/providers/CrossmintWalletBaseProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index 7963dde39..2334fa323 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -186,7 +186,7 @@ export function CrossmintWalletBaseProvider({ await initializeWebViewIfNeeded(resolvedSigner); - const wallet = await wallets.getClientSideWallet({ + const wallet = await wallets.getWallet({ chain: args.chain, signer: resolvedSigner, options: { From 6891aa5de074012fcebd798731ab206854102f83 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 16 Oct 2025 10:41:18 +0200 Subject: [PATCH 24/82] remove unnecessary types --- packages/wallets/src/wallets/wallet-factory.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 1253536d9..20e137962 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -403,10 +403,7 @@ export class WalletFactory { ); } - compareSignerConfigs( - inputSigner as Record, - matchingExistingSigner as Record - ); + compareSignerConfigs(inputSigner, matchingExistingSigner); } } From ee8195ba2dfe091cfe76afb1349733ef602a5710 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 16 Oct 2025 14:54:29 +0200 Subject: [PATCH 25/82] remove optional param --- packages/wallets/src/wallets/wallet-factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 20e137962..c5a08236b 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -301,7 +301,7 @@ export class WalletFactory { if ("onCreateConfig" in args) { let expectedAdminSigner = args.onCreateConfig?.adminSigner ?? args.signer; - const existingWalletSigner = (existingWallet?.config as any)?.adminSigner as AdminSignerConfig; + const existingWalletSigner = (existingWallet.config as any)?.adminSigner as AdminSignerConfig; const tempArgs = { ...args, signer: expectedAdminSigner }; this.mutateSignerFromCustomAuth(tempArgs); From 199d2abc7542c1b5ef2655deea5eb6466696f8bb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 16:02:17 +0000 Subject: [PATCH 26/82] feat(wallets): add shadow signer support for automatic delegated signers - 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 --- .../providers/CrossmintWalletBaseProvider.tsx | 6 + packages/wallets/src/utils/shadow-signer.ts | 105 ++++++++++++++++++ packages/wallets/src/wallets/types.ts | 3 + .../wallets/src/wallets/wallet-factory.ts | 21 +++- 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 packages/wallets/src/utils/shadow-signer.ts diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index 2334fa323..cc6fe7277 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -44,6 +44,9 @@ export interface CrossmintWalletBaseProviderProps { onAuthRequired?: EmailSignerConfig["onAuthRequired"] | PhoneSignerConfig["onAuthRequired"]; clientTEEConnection?: () => HandshakeParent; initializeWebView?: () => Promise; + shadowSigner?: { + enabled?: boolean; + }; } export function CrossmintWalletBaseProvider({ @@ -53,6 +56,7 @@ export function CrossmintWalletBaseProvider({ onAuthRequired, clientTEEConnection, initializeWebView, + shadowSigner, }: CrossmintWalletBaseProviderProps) { const { crossmint, experimental_customAuth } = useCrossmint( "CrossmintWalletBaseProvider must be used within CrossmintProvider" @@ -145,6 +149,7 @@ export function CrossmintWalletBaseProvider({ onCreateConfig: args.onCreateConfig, options: { clientTEEConnection: clientTEEConnection?.(), + shadowSigner: shadowSigner, experimental_callbacks: { onWalletCreationStart: _onWalletCreationStart ?? callbacks?.onWalletCreationStart, onTransactionStart: _onTransactionStart ?? callbacks?.onTransactionStart, @@ -191,6 +196,7 @@ export function CrossmintWalletBaseProvider({ signer: resolvedSigner, options: { clientTEEConnection: clientTEEConnection?.(), + shadowSigner: shadowSigner, experimental_callbacks: callbacks, }, }); diff --git a/packages/wallets/src/utils/shadow-signer.ts b/packages/wallets/src/utils/shadow-signer.ts new file mode 100644 index 000000000..ff5d275e5 --- /dev/null +++ b/packages/wallets/src/utils/shadow-signer.ts @@ -0,0 +1,105 @@ +import { WebAuthnP256 } from "ox"; +import { encode as encodeBase58 } from "bs58"; +import type { Chain } from "../chains/chains"; +import type { RegisterSignerParams } from "../api/types"; + +const SHADOW_SIGNER_STORAGE_KEY = "crossmint_shadow_signer"; + +export type ShadowSignerData = { + chain: Chain; + walletAddress: string; + publicKey: string; + createdAt: number; +}; + +export type ShadowSignerResult = { + delegatedSigner: RegisterSignerParams; + publicKey: string; +}; + +/** + * Generate a shadow signer for the given chain. + * For Solana/Stellar: Creates an ed25519 keypair and returns external-wallet signer + * For EVM chains: Creates a p256 passkey credential + * For Flow: Throws an error (not supported) + */ +export async function generateShadowSigner(chain: Chain): Promise { + if (chain === "solana" || chain === "stellar") { + const keyPair = (await window.crypto.subtle.generateKey( + { + name: "Ed25519", + // @ts-expect-error - Ed25519 is not in TypeScript's lib yet but is supported in modern browsers + namedCurve: "Ed25519", + }, + false, // non-extractable + ["sign", "verify"] + )) as CryptoKeyPair; + + const publicKeyBuffer = await window.crypto.subtle.exportKey("raw", keyPair.publicKey); + const publicKeyBase58 = encodeBase58(new Uint8Array(publicKeyBuffer)); + + return { + delegatedSigner: { signer: `external-wallet:${publicKeyBase58}` }, + publicKey: publicKeyBase58, + }; + } + + if (chain === "flow" || chain === "flow-testnet") { + throw new Error("Shadow signers are not supported on Flow chains"); + } + + const passkeyName = `Shadow Signer ${Date.now()}`; + const credential = await WebAuthnP256.createCredential({ name: passkeyName }); + + return { + delegatedSigner: { + signer: { + type: "passkey", + id: credential.id, + name: passkeyName, + publicKey: { + x: credential.publicKey.x.toString(), + y: credential.publicKey.y.toString(), + }, + }, + chain: chain as any, // RegisterSignerChain type + }, + publicKey: credential.id, + }; +} + +/** + * Store shadow signer metadata in localStorage + */ +export function storeShadowSigner(walletAddress: string, chain: Chain, publicKey: string): void { + const data: ShadowSignerData = { + chain, + walletAddress, + publicKey, + createdAt: Date.now(), + }; + + localStorage.setItem(`${SHADOW_SIGNER_STORAGE_KEY}_${walletAddress}`, JSON.stringify(data)); +} + +/** + * Retrieve shadow signer metadata from localStorage + */ +export function getShadowSigner(walletAddress: string): ShadowSignerData | null { + const stored = localStorage.getItem(`${SHADOW_SIGNER_STORAGE_KEY}_${walletAddress}`); + return stored ? JSON.parse(stored) : null; +} + +/** + * Check if a shadow signer exists for the given wallet + */ +export function hasShadowSigner(walletAddress: string): boolean { + return getShadowSigner(walletAddress) !== null; +} + +/** + * Remove shadow signer metadata from localStorage + */ +export function removeShadowSigner(walletAddress: string): void { + localStorage.removeItem(`${SHADOW_SIGNER_STORAGE_KEY}_${walletAddress}`); +} diff --git a/packages/wallets/src/wallets/types.ts b/packages/wallets/src/wallets/types.ts index 98fec27de..ae11af854 100644 --- a/packages/wallets/src/wallets/types.ts +++ b/packages/wallets/src/wallets/types.ts @@ -117,6 +117,9 @@ export type WalletPlugin = C extends StellarChain ? StellarWall export type WalletOptions = { experimental_callbacks?: Callbacks; clientTEEConnection?: HandshakeParent; + shadowSigner?: { + enabled?: boolean; + }; }; export type WalletArgsFor = { diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index c5a08236b..a5a00849a 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -16,6 +16,7 @@ import { Wallet } from "./wallet"; import { assembleSigner } from "../signers"; import type { DelegatedSigner, WalletArgsFor, WalletCreateArgs, WalletOptions } from "./types"; import { compareSignerConfigs } from "../utils/signer-validation"; +import { generateShadowSigner, storeShadowSigner } from "../utils/shadow-signer"; const DELEGATED_SIGNER_MISMATCH_ERROR = "When 'delegatedSigners' is provided to a method that may fetch an existing wallet, each specified delegated signer must exist in that wallet's configuration."; @@ -83,7 +84,7 @@ export class WalletFactory { await args.options?.experimental_callbacks?.onWalletCreationStart?.(); let adminSignerConfig = args.onCreateConfig?.adminSigner ?? args.signer; - const delegatedSigners = await Promise.all( + let delegatedSigners = await Promise.all( args.onCreateConfig?.delegatedSigners?.map( async (signer): Promise => { if (signer.type === "passkey") { @@ -94,6 +95,20 @@ export class WalletFactory { ) ?? [] ); + // Generate shadow signer if enabled (default true) and client-side + const shadowSignerEnabled = args.options?.shadowSigner?.enabled !== false; + let shadowSignerPublicKey: string | null = null; + + if (!this.apiClient.isServerSide && shadowSignerEnabled) { + try { + const { delegatedSigner, publicKey } = await generateShadowSigner(args.chain); + delegatedSigners = [...delegatedSigners, delegatedSigner]; + shadowSignerPublicKey = publicKey; + } catch (error) { + console.warn("Failed to create shadow signer:", error); + } + } + const tempArgs = { ...args, signer: adminSignerConfig }; this.mutateSignerFromCustomAuth(tempArgs, true); adminSignerConfig = tempArgs.signer; @@ -118,6 +133,10 @@ export class WalletFactory { throw new WalletCreationError(JSON.stringify(walletResponse)); } + if (!this.apiClient.isServerSide && shadowSignerEnabled && shadowSignerPublicKey != null) { + storeShadowSigner(walletResponse.address, args.chain, shadowSignerPublicKey); + } + return this.createWalletInstance(walletResponse, args); } From 1b0eba3cf3c434aa73f712a5e10768411969f1da Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 16:04:09 +0000 Subject: [PATCH 27/82] fix: remove unused @ts-expect-error directive Co-Authored-By: Guille --- packages/wallets/src/utils/shadow-signer.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/wallets/src/utils/shadow-signer.ts b/packages/wallets/src/utils/shadow-signer.ts index ff5d275e5..3b024c279 100644 --- a/packages/wallets/src/utils/shadow-signer.ts +++ b/packages/wallets/src/utils/shadow-signer.ts @@ -28,10 +28,9 @@ export async function generateShadowSigner(chain: Chain): Promise Date: Fri, 17 Oct 2025 09:10:06 +0000 Subject: [PATCH 28/82] refactor: replace 'as any' with custom SmartWalletConfig type - 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 --- .../wallets/src/wallets/wallet-factory.ts | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index c5a08236b..87e1fad17 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -20,6 +20,11 @@ import { compareSignerConfigs } from "../utils/signer-validation"; const DELEGATED_SIGNER_MISMATCH_ERROR = "When 'delegatedSigners' is provided to a method that may fetch an existing wallet, each specified delegated signer must exist in that wallet's configuration."; +type SmartWalletConfig = { + adminSigner: AdminSignerConfig; + delegatedSigners?: DelegatedSignerResponse[]; +}; + export class WalletFactory { constructor(private readonly apiClient: ApiClient) {} @@ -57,7 +62,7 @@ export class WalletFactory { ); } if (maybeArgs == null) { - throw new WalletCreationError("Args parameter is required when walletLocator is provided"); + throw new WalletCreationError("Wallet configuration (chain, signer, etc.) is required when using walletLocator"); } walletLocator = argsOrLocator; args = maybeArgs; @@ -224,8 +229,9 @@ export class WalletFactory { wallet: GetWalletSuccessResponse, signerType: T ): Extract { - const adminSigner = (wallet.config as any)?.adminSigner as AdminSignerConfig; - const delegatedSigners = ((wallet.config as any)?.delegatedSigners as DelegatedSignerResponse[]) || []; + const config = wallet.config as SmartWalletConfig; + const adminSigner = config?.adminSigner; + const delegatedSigners = config?.delegatedSigners || []; if (adminSigner?.type === signerType) { return adminSigner as Extract; } @@ -301,7 +307,8 @@ export class WalletFactory { if ("onCreateConfig" in args) { let expectedAdminSigner = args.onCreateConfig?.adminSigner ?? args.signer; - const existingWalletSigner = (existingWallet.config as any)?.adminSigner as AdminSignerConfig; + const config = existingWallet.config as SmartWalletConfig; + const existingWalletSigner = config?.adminSigner; const tempArgs = { ...args, signer: expectedAdminSigner }; this.mutateSignerFromCustomAuth(tempArgs); @@ -328,8 +335,9 @@ export class WalletFactory { wallet: GetWalletSuccessResponse, signer: SignerConfigForChain ): void { - const adminSigner = (wallet.config as any)?.adminSigner as AdminSignerConfig; - const delegatedSigners = ((wallet.config as any)?.delegatedSigners as DelegatedSignerResponse[]) || []; + const config = wallet.config as SmartWalletConfig; + const adminSigner = config?.adminSigner; + const delegatedSigners = config?.delegatedSigners || []; if (adminSigner != null && signer.type === adminSigner.type) { try { @@ -375,9 +383,8 @@ export class WalletFactory { existingWallet: GetWalletSuccessResponse, inputDelegatedSigners: Array> ): void { - const existingDelegatedSigners = (existingWallet?.config as any)?.delegatedSigners as - | DelegatedSignerResponse[] - | undefined; + const config = existingWallet.config as SmartWalletConfig; + const existingDelegatedSigners = config?.delegatedSigners; // If no delegated signers specified in input, no validation needed if (inputDelegatedSigners.length === 0) { From 05fbb32079b5df79a58281b0db56d50e742e8836 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:11:42 +0000 Subject: [PATCH 29/82] fix: format error message to meet line length requirements Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet-factory.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 87e1fad17..6c88cfd7f 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -62,7 +62,9 @@ export class WalletFactory { ); } if (maybeArgs == null) { - throw new WalletCreationError("Wallet configuration (chain, signer, etc.) is required when using walletLocator"); + throw new WalletCreationError( + "Wallet configuration (chain, signer, etc.) is required when using walletLocator" + ); } walletLocator = argsOrLocator; args = maybeArgs; From 75a3e8fd3908cb11c54ca56d30fa73736f7d1d5a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:23:22 +0000 Subject: [PATCH 30/82] refactor: remove Flow-specific check, let API handle incompatible chains Co-Authored-By: Guille --- packages/wallets/src/utils/shadow-signer.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/wallets/src/utils/shadow-signer.ts b/packages/wallets/src/utils/shadow-signer.ts index 3b024c279..9fd673906 100644 --- a/packages/wallets/src/utils/shadow-signer.ts +++ b/packages/wallets/src/utils/shadow-signer.ts @@ -21,7 +21,6 @@ export type ShadowSignerResult = { * Generate a shadow signer for the given chain. * For Solana/Stellar: Creates an ed25519 keypair and returns external-wallet signer * For EVM chains: Creates a p256 passkey credential - * For Flow: Throws an error (not supported) */ export async function generateShadowSigner(chain: Chain): Promise { if (chain === "solana" || chain === "stellar") { @@ -43,10 +42,6 @@ export async function generateShadowSigner(chain: Chain): Promise Date: Fri, 17 Oct 2025 09:28:56 +0000 Subject: [PATCH 31/82] feat: use shadow signer as active signer for wallet instances - Modified createWallet to set shadow signer as the active signer - Updated getOrCreateWallet to use shadow signer when retrieving existing wallets - Shadow signer now becomes the default signer instead of the one passed by the user Co-Authored-By: Guille --- .../wallets/src/wallets/wallet-factory.ts | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index a5a00849a..8007d3984 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -16,7 +16,7 @@ import { Wallet } from "./wallet"; import { assembleSigner } from "../signers"; import type { DelegatedSigner, WalletArgsFor, WalletCreateArgs, WalletOptions } from "./types"; import { compareSignerConfigs } from "../utils/signer-validation"; -import { generateShadowSigner, storeShadowSigner } from "../utils/shadow-signer"; +import { generateShadowSigner, storeShadowSigner, getShadowSigner } from "../utils/shadow-signer"; const DELEGATED_SIGNER_MISMATCH_ERROR = "When 'delegatedSigners' is provided to a method that may fetch an existing wallet, each specified delegated signer must exist in that wallet's configuration."; @@ -34,7 +34,31 @@ export class WalletFactory { const existingWallet = await this.apiClient.getWallet(`me:${this.getChainType(args.chain)}:smart`); if (existingWallet != null && !("error" in existingWallet)) { - return this.createWalletInstance(existingWallet, args); + const shadowSignerEnabled = args.options?.shadowSigner?.enabled !== false; + let walletInstanceArgs = args; + + if (shadowSignerEnabled) { + const shadowData = getShadowSigner(existingWallet.address); + if (shadowData != null) { + let shadowSignerConfig: SignerConfigForChain; + if (args.chain === "solana" || args.chain === "stellar") { + shadowSignerConfig = { + type: "external-wallet", + address: shadowData.publicKey, + } as SignerConfigForChain; + } else { + shadowSignerConfig = { + type: "passkey", + name: `Shadow Signer`, + onCreatePasskey: undefined, + onSignWithPasskey: undefined, + } as SignerConfigForChain; + } + walletInstanceArgs = { ...args, signer: shadowSignerConfig }; + } + } + + return this.createWalletInstance(existingWallet, walletInstanceArgs); } return this.createWallet(args); @@ -95,15 +119,30 @@ export class WalletFactory { ) ?? [] ); - // Generate shadow signer if enabled (default true) and client-side const shadowSignerEnabled = args.options?.shadowSigner?.enabled !== false; let shadowSignerPublicKey: string | null = null; + let shadowSignerConfig: SignerConfigForChain | null = null; if (!this.apiClient.isServerSide && shadowSignerEnabled) { try { const { delegatedSigner, publicKey } = await generateShadowSigner(args.chain); delegatedSigners = [...delegatedSigners, delegatedSigner]; shadowSignerPublicKey = publicKey; + + if (args.chain === "solana" || args.chain === "stellar") { + shadowSignerConfig = { + type: "external-wallet", + address: publicKey, + } as SignerConfigForChain; + } else { + const passkeyData = (delegatedSigner.signer as RegisterSignerPasskeyParams); + shadowSignerConfig = { + type: "passkey", + name: passkeyData.name, + onCreatePasskey: undefined, + onSignWithPasskey: undefined, + } as SignerConfigForChain; + } } catch (error) { console.warn("Failed to create shadow signer:", error); } @@ -137,7 +176,11 @@ export class WalletFactory { storeShadowSigner(walletResponse.address, args.chain, shadowSignerPublicKey); } - return this.createWalletInstance(walletResponse, args); + const walletInstanceArgs = shadowSignerConfig != null + ? { ...args, signer: shadowSignerConfig } + : args; + + return this.createWalletInstance(walletResponse, walletInstanceArgs); } private createWalletInstance( From 3d92392512f74cb35f7c56c819729b549b837d85 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:31:03 +0000 Subject: [PATCH 32/82] fix: apply biome lint formatting Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet-factory.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 8007d3984..a08009c79 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -135,7 +135,7 @@ export class WalletFactory { address: publicKey, } as SignerConfigForChain; } else { - const passkeyData = (delegatedSigner.signer as RegisterSignerPasskeyParams); + const passkeyData = delegatedSigner.signer as RegisterSignerPasskeyParams; shadowSignerConfig = { type: "passkey", name: passkeyData.name, @@ -176,9 +176,7 @@ export class WalletFactory { storeShadowSigner(walletResponse.address, args.chain, shadowSignerPublicKey); } - const walletInstanceArgs = shadowSignerConfig != null - ? { ...args, signer: shadowSignerConfig } - : args; + const walletInstanceArgs = shadowSignerConfig != null ? { ...args, signer: shadowSignerConfig } : args; return this.createWalletInstance(walletResponse, walletInstanceArgs); } From dbaf09d334653fab3f16a07d44686ef5c1ae3698 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:41:43 +0000 Subject: [PATCH 33/82] fix: add localStorage checks for Node.js test environment Co-Authored-By: Guille --- packages/wallets/src/utils/shadow-signer.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/wallets/src/utils/shadow-signer.ts b/packages/wallets/src/utils/shadow-signer.ts index 9fd673906..9db68a5c0 100644 --- a/packages/wallets/src/utils/shadow-signer.ts +++ b/packages/wallets/src/utils/shadow-signer.ts @@ -66,6 +66,9 @@ export async function generateShadowSigner(chain: Chain): Promise Date: Fri, 17 Oct 2025 15:28:00 +0200 Subject: [PATCH 34/82] wip shadow --- packages/wallets/src/utils/shadow-signer.ts | 4 +- .../wallets/src/wallets/wallet-factory.ts | 37 ++++++++++++++----- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/wallets/src/utils/shadow-signer.ts b/packages/wallets/src/utils/shadow-signer.ts index 9db68a5c0..efce50f5b 100644 --- a/packages/wallets/src/utils/shadow-signer.ts +++ b/packages/wallets/src/utils/shadow-signer.ts @@ -10,6 +10,7 @@ export type ShadowSignerData = { walletAddress: string; publicKey: string; createdAt: number; + name: string; }; export type ShadowSignerResult = { @@ -65,7 +66,7 @@ export async function generateShadowSigner(chain: Chain): Promise; } - walletInstanceArgs = { ...args, signer: shadowSignerConfig }; + walletInstanceArgs = { + ...args, + signer: shadowSignerConfig, + onCreateConfig: args.onCreateConfig ?? { adminSigner: args.signer }, + }; } } @@ -123,7 +125,11 @@ export class WalletFactory { let shadowSignerPublicKey: string | null = null; let shadowSignerConfig: SignerConfigForChain | null = null; - if (!this.apiClient.isServerSide && shadowSignerEnabled) { + if ( + !this.apiClient.isServerSide && + shadowSignerEnabled && + (args.signer.type === "email" || args.signer.type === "phone") + ) { try { const { delegatedSigner, publicKey } = await generateShadowSigner(args.chain); delegatedSigners = [...delegatedSigners, delegatedSigner]; @@ -139,8 +145,6 @@ export class WalletFactory { shadowSignerConfig = { type: "passkey", name: passkeyData.name, - onCreatePasskey: undefined, - onSignWithPasskey: undefined, } as SignerConfigForChain; } } catch (error) { @@ -173,10 +177,22 @@ export class WalletFactory { } if (!this.apiClient.isServerSide && shadowSignerEnabled && shadowSignerPublicKey != null) { - storeShadowSigner(walletResponse.address, args.chain, shadowSignerPublicKey); + storeShadowSigner( + walletResponse.address, + args.chain, + shadowSignerPublicKey, + shadowSignerConfig?.type === "passkey" ? shadowSignerConfig.name : undefined + ); } - const walletInstanceArgs = shadowSignerConfig != null ? { ...args, signer: shadowSignerConfig } : args; + const walletInstanceArgs = + shadowSignerConfig != null + ? { + ...args, + signer: shadowSignerConfig, + onCreateConfig: { adminSigner: args.onCreateConfig?.adminSigner ?? args.signer }, + } + : args; return this.createWalletInstance(walletResponse, walletInstanceArgs); } @@ -390,6 +406,9 @@ export class WalletFactory { ): void { const adminSigner = (wallet.config as any)?.adminSigner as AdminSignerConfig; const delegatedSigners = ((wallet.config as any)?.delegatedSigners as DelegatedSignerResponse[]) || []; + console.log("adminSigner", adminSigner); + console.log("delegatedSigners", delegatedSigners); + console.log("signer", signer); if (adminSigner != null && signer.type === adminSigner.type) { try { From 6cd46ee75c67cdbe425e973607a58c865e51c991 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Mon, 20 Oct 2025 11:07:42 -0400 Subject: [PATCH 35/82] remove support for evm --- packages/wallets/src/utils/shadow-signer.ts | 24 ++------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/packages/wallets/src/utils/shadow-signer.ts b/packages/wallets/src/utils/shadow-signer.ts index efce50f5b..d9a1f2cf5 100644 --- a/packages/wallets/src/utils/shadow-signer.ts +++ b/packages/wallets/src/utils/shadow-signer.ts @@ -1,4 +1,3 @@ -import { WebAuthnP256 } from "ox"; import { encode as encodeBase58 } from "bs58"; import type { Chain } from "../chains/chains"; import type { RegisterSignerParams } from "../api/types"; @@ -10,7 +9,6 @@ export type ShadowSignerData = { walletAddress: string; publicKey: string; createdAt: number; - name: string; }; export type ShadowSignerResult = { @@ -42,25 +40,8 @@ export async function generateShadowSigner(chain: Chain): Promise Date: Mon, 20 Oct 2025 11:18:07 -0400 Subject: [PATCH 36/82] add passkey logic to delegate signer comparison --- packages/wallets/src/signers/types.ts | 1 + .../wallets/src/wallets/wallet-factory.ts | 20 +++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/wallets/src/signers/types.ts b/packages/wallets/src/signers/types.ts index 0d7720018..5d688fca6 100644 --- a/packages/wallets/src/signers/types.ts +++ b/packages/wallets/src/signers/types.ts @@ -63,6 +63,7 @@ export type BaseSignerConfig = ExternalWalletSignerConfigForCha export type PasskeySignerConfig = { type: "passkey"; name?: string; + id?: string; onCreatePasskey?: (name: string) => Promise<{ id: string; publicKey: { x: string; y: string } }>; onSignWithPasskey?: (message: string) => Promise; }; diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index c5a08236b..5161fbcc7 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -11,11 +11,12 @@ import type { } from "../api"; import { WalletCreationError, WalletNotAvailableError } from "../utils/errors"; import type { Chain } from "../chains/chains"; -import type { InternalSignerConfig, SignerConfigForChain } from "../signers/types"; +import type { InternalSignerConfig, PasskeySignerConfig, SignerConfigForChain } from "../signers/types"; import { Wallet } from "./wallet"; import { assembleSigner } from "../signers"; import type { DelegatedSigner, WalletArgsFor, WalletCreateArgs, WalletOptions } from "./types"; import { compareSignerConfigs } from "../utils/signer-validation"; +import { DelegatedSignerV2025Dto } from "@/api/gen/types.gen"; const DELEGATED_SIGNER_MISMATCH_ERROR = "When 'delegatedSigners' is provided to a method that may fetch an existing wallet, each specified delegated signer must exist in that wallet's configuration."; @@ -390,11 +391,22 @@ export class WalletFactory { `${inputDelegatedSigners.length} delegated signer(s) specified, but wallet "${existingWallet.address}" has no delegated signers. ${DELEGATED_SIGNER_MISMATCH_ERROR}` ); } + const existingPasskeyIds = existingDelegatedSigners.filter((s) => s.type === "passkey").length; for (const inputSigner of inputDelegatedSigners) { - const matchingExistingSigner = existingDelegatedSigners.find( - (existingSigner) => existingSigner.type === inputSigner.type - ); + const matchingExistingSigner = existingDelegatedSigners.find((existingSigner) => { + if (inputSigner.type === "passkey") { + if (inputSigner.id == null && existingPasskeyIds === 1) { + return existingSigner.type === "passkey"; + } + if (inputSigner.id == null && existingPasskeyIds > 1) { + throw new WalletCreationError( + "When creating a wallet with multiple passkeys, you must provide the passkey ID for each passkey." + ); + } + } + return existingSigner.locator === this.getSignerLocator(inputSigner); + }); if (matchingExistingSigner == null) { const walletSigners = existingDelegatedSigners.map((s) => s.locator).join(", "); From b588a95644b1258b29cfdd4eef7e47a9bd767316 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Mon, 20 Oct 2025 12:55:46 -0400 Subject: [PATCH 37/82] fix wallet with multiple passkey signers --- packages/wallets/src/signers/types.ts | 1 + .../wallets/src/wallets/wallet-factory.ts | 88 ++++++++++++++----- 2 files changed, 69 insertions(+), 20 deletions(-) diff --git a/packages/wallets/src/signers/types.ts b/packages/wallets/src/signers/types.ts index 5d688fca6..f15b6f851 100644 --- a/packages/wallets/src/signers/types.ts +++ b/packages/wallets/src/signers/types.ts @@ -64,6 +64,7 @@ export type PasskeySignerConfig = { type: "passkey"; name?: string; id?: string; + locator?: string; onCreatePasskey?: (name: string) => Promise<{ id: string; publicKey: { x: string; y: string } }>; onSignWithPasskey?: (message: string) => Promise; }; diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 48cf5d989..7f1c31766 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -16,13 +16,12 @@ import { Wallet } from "./wallet"; import { assembleSigner } from "../signers"; import type { DelegatedSigner, WalletArgsFor, WalletCreateArgs, WalletOptions } from "./types"; import { compareSignerConfigs } from "../utils/signer-validation"; -import { DelegatedSignerV2025Dto } from "@/api/gen/types.gen"; const DELEGATED_SIGNER_MISMATCH_ERROR = "When 'delegatedSigners' is provided to a method that may fetch an existing wallet, each specified delegated signer must exist in that wallet's configuration."; type SmartWalletConfig = { - adminSigner: AdminSignerConfig; + adminSigner: AdminSignerConfig | PasskeySignerConfig; delegatedSigners?: DelegatedSignerResponse[]; }; @@ -93,9 +92,12 @@ export class WalletFactory { let adminSignerConfig = args.onCreateConfig?.adminSigner ?? args.signer; const delegatedSigners = await Promise.all( args.onCreateConfig?.delegatedSigners?.map( - async (signer): Promise => { + async (signer): Promise => { if (signer.type === "passkey") { - return { signer: await this.createPasskeySigner(signer) }; + if (signer.id == null) { + return { signer: await this.createPasskeySigner(signer) }; + } + return { signer }; } return { signer: this.getSignerLocator(signer) }; } @@ -107,7 +109,7 @@ export class WalletFactory { adminSignerConfig = tempArgs.signer; const adminSigner = - adminSignerConfig.type === "passkey" + adminSignerConfig.type === "passkey" && adminSignerConfig.id == null ? await this.createPasskeySigner(adminSignerConfig) : adminSignerConfig; @@ -308,7 +310,7 @@ export class WalletFactory { return; } - if ("onCreateConfig" in args) { + if ("onCreateConfig" in args && args.onCreateConfig != null) { let expectedAdminSigner = args.onCreateConfig?.adminSigner ?? args.signer; const config = existingWallet.config as SmartWalletConfig; const existingWalletSigner = config?.adminSigner; @@ -327,7 +329,15 @@ export class WalletFactory { } if (args.onCreateConfig?.delegatedSigners != null) { - this.validateDelegatedSigners(existingWallet, args.onCreateConfig.delegatedSigners); + const numberOfPasskeySigners = + args.onCreateConfig.delegatedSigners?.filter((s) => s.type === "passkey").length + + (config.adminSigner?.type === "passkey" ? 1 : 0); + + this.validateDelegatedSigners( + existingWallet, + args.onCreateConfig.delegatedSigners, + numberOfPasskeySigners + ); } } @@ -341,15 +351,35 @@ export class WalletFactory { const config = wallet.config as SmartWalletConfig; const adminSigner = config?.adminSigner; const delegatedSigners = config?.delegatedSigners || []; + const numberOfPasskeySigners = + delegatedSigners.filter((s) => s.type === "passkey").length + + ((adminSigner as any)?.type === "passkey" ? 1 : 0); + + console.log("numberOfPasskeySigners", numberOfPasskeySigners); + console.log("adminSigner", adminSigner); + console.log("signer", signer); + console.log( + "this.isMatchingPasskeySigner(signer, adminSigner, numberOfPasskeySigners)", + this.isMatchingPasskeySigner(signer, adminSigner, numberOfPasskeySigners) + ); + console.log("this.getSignerLocator(signer)", this.getSignerLocator(signer)); - if (adminSigner != null && signer.type === adminSigner.type) { + if ( + adminSigner != null && + (this.isMatchingPasskeySigner(signer, adminSigner, numberOfPasskeySigners) || + this.getSignerLocator(signer) === (adminSigner as PasskeySignerConfig).locator) + ) { try { compareSignerConfigs(signer, adminSigner); return; } catch {} } - const delegatedSigner = delegatedSigners.find((ds) => ds.type === signer.type); + const delegatedSigner = delegatedSigners.find( + (ds) => + this.isMatchingPasskeySigner(signer, ds, numberOfPasskeySigners) || + this.getSignerLocator(signer) === ds.locator + ); if (delegatedSigner != null) { try { @@ -384,7 +414,8 @@ export class WalletFactory { private validateDelegatedSigners( existingWallet: GetWalletSuccessResponse, - inputDelegatedSigners: Array> + inputDelegatedSigners: Array>, + numberOfPasskeySigners: number ): void { const config = existingWallet.config as SmartWalletConfig; const existingDelegatedSigners = config?.delegatedSigners; @@ -400,19 +431,13 @@ export class WalletFactory { `${inputDelegatedSigners.length} delegated signer(s) specified, but wallet "${existingWallet.address}" has no delegated signers. ${DELEGATED_SIGNER_MISMATCH_ERROR}` ); } - const existingPasskeyIds = existingDelegatedSigners.filter((s) => s.type === "passkey").length; + + inputDelegatedSigners.forEach((s) => this.mutateSignerFromCustomAuth({ signer: s } as WalletArgsFor)); for (const inputSigner of inputDelegatedSigners) { const matchingExistingSigner = existingDelegatedSigners.find((existingSigner) => { - if (inputSigner.type === "passkey") { - if (inputSigner.id == null && existingPasskeyIds === 1) { - return existingSigner.type === "passkey"; - } - if (inputSigner.id == null && existingPasskeyIds > 1) { - throw new WalletCreationError( - "When creating a wallet with multiple passkeys, you must provide the passkey ID for each passkey." - ); - } + if (this.isMatchingPasskeySigner(inputSigner, existingSigner, numberOfPasskeySigners)) { + return true; } return existingSigner.locator === this.getSignerLocator(inputSigner); }); @@ -428,6 +453,29 @@ export class WalletFactory { } } + /* + Checks if the input signer is a matching passkey signer to the existing signer. + If the existing wallet has only one passkey, the input signer can be a passkey signer without an ID. + If the existing wallet has multiple passkeys, the input signer must be a passkey signer with an ID. + */ + private isMatchingPasskeySigner( + inputSigner: SignerConfigForChain, + existingSigner: SmartWalletConfig["adminSigner"] | DelegatedSignerResponse, + numberOfPasskeySigners: number + ): boolean { + if (inputSigner.type === "passkey") { + if (inputSigner.id == null && numberOfPasskeySigners === 1) { + return existingSigner.type === "passkey"; + } + if (inputSigner.id == null && numberOfPasskeySigners > 1) { + throw new WalletCreationError( + "When creating a wallet with multiple passkeys, you must provide the passkey ID for each passkey." + ); + } + } + return false; + } + private getChainType(chain: Chain): "solana" | "evm" | "stellar" { if (chain === "solana") { return "solana"; From e5516f07bd636cfa5469a498e807411564b73530 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Mon, 20 Oct 2025 14:24:04 -0400 Subject: [PATCH 38/82] remove console.log --- packages/wallets/src/wallets/wallet-factory.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 7f1c31766..0f5951492 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -355,15 +355,6 @@ export class WalletFactory { delegatedSigners.filter((s) => s.type === "passkey").length + ((adminSigner as any)?.type === "passkey" ? 1 : 0); - console.log("numberOfPasskeySigners", numberOfPasskeySigners); - console.log("adminSigner", adminSigner); - console.log("signer", signer); - console.log( - "this.isMatchingPasskeySigner(signer, adminSigner, numberOfPasskeySigners)", - this.isMatchingPasskeySigner(signer, adminSigner, numberOfPasskeySigners) - ); - console.log("this.getSignerLocator(signer)", this.getSignerLocator(signer)); - if ( adminSigner != null && (this.isMatchingPasskeySigner(signer, adminSigner, numberOfPasskeySigners) || From 8b024c7d90b34776ffb74e3fe2d7d2380adbdaef Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Mon, 20 Oct 2025 14:44:26 -0400 Subject: [PATCH 39/82] remove all shadow passkey references --- packages/wallets/src/utils/shadow-signer.ts | 2 +- .../wallets/src/wallets/wallet-factory.ts | 46 ++++++------------- 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/packages/wallets/src/utils/shadow-signer.ts b/packages/wallets/src/utils/shadow-signer.ts index d9a1f2cf5..ed6c2627e 100644 --- a/packages/wallets/src/utils/shadow-signer.ts +++ b/packages/wallets/src/utils/shadow-signer.ts @@ -47,7 +47,7 @@ export async function generateShadowSigner(chain: Chain): Promise; - if (args.chain === "solana" || args.chain === "stellar") { - shadowSignerConfig = { - type: "external-wallet", - address: shadowData.publicKey, - } as SignerConfigForChain; - } else { - shadowSignerConfig = { - type: "passkey", - name: shadowData.name, - } as SignerConfigForChain; - } + const shadowSignerConfig = { + type: "external-wallet", + address: shadowData.publicKey, + } as SignerConfigForChain; walletInstanceArgs = { ...args, signer: shadowSignerConfig, @@ -121,7 +114,8 @@ export class WalletFactory { ) ?? [] ); - const shadowSignerEnabled = args.options?.shadowSigner?.enabled !== false; + const shadowSignerEnabled = + (args.chain === "solana" || args.chain === "stellar") && args.options?.shadowSigner?.enabled !== false; let shadowSignerPublicKey: string | null = null; let shadowSignerConfig: SignerConfigForChain | null = null; @@ -135,18 +129,11 @@ export class WalletFactory { delegatedSigners = [...delegatedSigners, delegatedSigner]; shadowSignerPublicKey = publicKey; - if (args.chain === "solana" || args.chain === "stellar") { - shadowSignerConfig = { - type: "external-wallet", - address: publicKey, - } as SignerConfigForChain; - } else { - const passkeyData = delegatedSigner.signer as RegisterSignerPasskeyParams; - shadowSignerConfig = { - type: "passkey", - name: passkeyData.name, - } as SignerConfigForChain; - } + shadowSignerConfig = { + type: "external-wallet", + address: publicKey, + } as SignerConfigForChain; + delegatedSigners = [...delegatedSigners, { signer: this.getSignerLocator(shadowSignerConfig) }]; } catch (error) { console.warn("Failed to create shadow signer:", error); } @@ -177,12 +164,7 @@ export class WalletFactory { } if (!this.apiClient.isServerSide && shadowSignerEnabled && shadowSignerPublicKey != null) { - storeShadowSigner( - walletResponse.address, - args.chain, - shadowSignerPublicKey, - shadowSignerConfig?.type === "passkey" ? shadowSignerConfig.name : undefined - ); + storeShadowSigner(walletResponse.address, args.chain, shadowSignerPublicKey); } const walletInstanceArgs = From 4da2a7f27e5c256d6ad445458854f1ab74ba91d8 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Mon, 20 Oct 2025 14:56:41 -0400 Subject: [PATCH 40/82] appplied albertos comment --- .../wallets/src/wallets/wallet-factory.ts | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 0f5951492..e4774e79e 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -63,7 +63,7 @@ export class WalletFactory { } if (maybeArgs == null) { throw new WalletCreationError( - "Wallet configuration (chain, signer, etc.) is required when using walletLocator" + "Wallet configuration is required when using walletLocator: https://docs.crossmint.com/sdk-reference/wallets/type-aliases/WalletArgsFor" ); } walletLocator = argsOrLocator; @@ -329,15 +329,7 @@ export class WalletFactory { } if (args.onCreateConfig?.delegatedSigners != null) { - const numberOfPasskeySigners = - args.onCreateConfig.delegatedSigners?.filter((s) => s.type === "passkey").length + - (config.adminSigner?.type === "passkey" ? 1 : 0); - - this.validateDelegatedSigners( - existingWallet, - args.onCreateConfig.delegatedSigners, - numberOfPasskeySigners - ); + this.validateDelegatedSigners(existingWallet, args.onCreateConfig.delegatedSigners); } } @@ -351,13 +343,10 @@ export class WalletFactory { const config = wallet.config as SmartWalletConfig; const adminSigner = config?.adminSigner; const delegatedSigners = config?.delegatedSigners || []; - const numberOfPasskeySigners = - delegatedSigners.filter((s) => s.type === "passkey").length + - ((adminSigner as any)?.type === "passkey" ? 1 : 0); if ( adminSigner != null && - (this.isMatchingPasskeySigner(signer, adminSigner, numberOfPasskeySigners) || + (this.isMatchingPasskeySigner(signer, adminSigner, config) || this.getSignerLocator(signer) === (adminSigner as PasskeySignerConfig).locator) ) { try { @@ -367,9 +356,7 @@ export class WalletFactory { } const delegatedSigner = delegatedSigners.find( - (ds) => - this.isMatchingPasskeySigner(signer, ds, numberOfPasskeySigners) || - this.getSignerLocator(signer) === ds.locator + (ds) => this.isMatchingPasskeySigner(signer, ds, config) || this.getSignerLocator(signer) === ds.locator ); if (delegatedSigner != null) { @@ -405,8 +392,7 @@ export class WalletFactory { private validateDelegatedSigners( existingWallet: GetWalletSuccessResponse, - inputDelegatedSigners: Array>, - numberOfPasskeySigners: number + inputDelegatedSigners: Array> ): void { const config = existingWallet.config as SmartWalletConfig; const existingDelegatedSigners = config?.delegatedSigners; @@ -424,10 +410,9 @@ export class WalletFactory { } inputDelegatedSigners.forEach((s) => this.mutateSignerFromCustomAuth({ signer: s } as WalletArgsFor)); - for (const inputSigner of inputDelegatedSigners) { const matchingExistingSigner = existingDelegatedSigners.find((existingSigner) => { - if (this.isMatchingPasskeySigner(inputSigner, existingSigner, numberOfPasskeySigners)) { + if (this.isMatchingPasskeySigner(inputSigner, existingSigner, config)) { return true; } return existingSigner.locator === this.getSignerLocator(inputSigner); @@ -452,8 +437,11 @@ export class WalletFactory { private isMatchingPasskeySigner( inputSigner: SignerConfigForChain, existingSigner: SmartWalletConfig["adminSigner"] | DelegatedSignerResponse, - numberOfPasskeySigners: number + walletConfig: SmartWalletConfig ): boolean { + const numberOfPasskeySigners = + (walletConfig.delegatedSigners?.filter((s) => s.type === "passkey").length ?? 0) + + (walletConfig.adminSigner.type === "passkey" ? 1 : 0); if (inputSigner.type === "passkey") { if (inputSigner.id == null && numberOfPasskeySigners === 1) { return existingSigner.type === "passkey"; From 69a147a0844447da7110e300b32447a799d6fede Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Mon, 20 Oct 2025 18:37:52 -0400 Subject: [PATCH 41/82] differentiate wallet creation for stellar and solana --- packages/wallets/src/utils/shadow-signer.ts | 17 ++- .../wallets/src/wallets/wallet-factory.ts | 115 ++++++++++-------- 2 files changed, 78 insertions(+), 54 deletions(-) diff --git a/packages/wallets/src/utils/shadow-signer.ts b/packages/wallets/src/utils/shadow-signer.ts index ed6c2627e..db8a2654b 100644 --- a/packages/wallets/src/utils/shadow-signer.ts +++ b/packages/wallets/src/utils/shadow-signer.ts @@ -1,4 +1,5 @@ import { encode as encodeBase58 } from "bs58"; +import { StrKey } from "@stellar/stellar-sdk"; import type { Chain } from "../chains/chains"; import type { RegisterSignerParams } from "../api/types"; @@ -19,7 +20,6 @@ export type ShadowSignerResult = { /** * Generate a shadow signer for the given chain. * For Solana/Stellar: Creates an ed25519 keypair and returns external-wallet signer - * For EVM chains: Creates a p256 passkey credential */ export async function generateShadowSigner(chain: Chain): Promise { if (chain === "solana" || chain === "stellar") { @@ -33,11 +33,20 @@ export async function generateShadowSigner(chain: Chain): Promise; - walletInstanceArgs = { - ...args, - signer: shadowSignerConfig, - onCreateConfig: args.onCreateConfig ?? { adminSigner: args.signer }, - }; - } - } + const shadowSignerEnabled = this.isShadowSignerEnabled(args.chain, args.options); + const walletInstanceArgs = shadowSignerEnabled + ? this.setShadowSignerAsSigner(existingWallet.address, args) + : args; return this.createWalletInstance(existingWallet, walletInstanceArgs); } @@ -124,30 +110,10 @@ export class WalletFactory { ) ?? [] ); - const shadowSignerEnabled = - (args.chain === "solana" || args.chain === "stellar") && args.options?.shadowSigner?.enabled !== false; - let shadowSignerPublicKey: string | null = null; - let shadowSignerConfig: SignerConfigForChain | null = null; - - if ( - !this.apiClient.isServerSide && - shadowSignerEnabled && - (args.signer.type === "email" || args.signer.type === "phone") - ) { - try { - const { delegatedSigner, publicKey } = await generateShadowSigner(args.chain); - delegatedSigners = [...delegatedSigners, delegatedSigner]; - shadowSignerPublicKey = publicKey; - - shadowSignerConfig = { - type: "external-wallet", - address: publicKey, - } as SignerConfigForChain; - delegatedSigners = [...delegatedSigners, { signer: this.getSignerLocator(shadowSignerConfig) }]; - } catch (error) { - console.warn("Failed to create shadow signer:", error); - } - } + const shadowSignerEnabled = this.isShadowSignerEnabled(args.chain, args.options); + const { delegatedSigners: updatedDelegatedSigners, shadowSignerPublicKey } = + await this.addShadowSignerToDelegatedSignersIfNeeded(args, delegatedSigners, shadowSignerEnabled); + delegatedSigners = updatedDelegatedSigners; const tempArgs = { ...args, signer: adminSignerConfig }; this.mutateSignerFromCustomAuth(tempArgs, true); @@ -177,14 +143,9 @@ export class WalletFactory { storeShadowSigner(walletResponse.address, args.chain, shadowSignerPublicKey); } - const walletInstanceArgs = - shadowSignerConfig != null - ? { - ...args, - signer: shadowSignerConfig, - onCreateConfig: { adminSigner: args.onCreateConfig?.adminSigner ?? args.signer }, - } - : args; + const walletInstanceArgs = shadowSignerEnabled + ? this.setShadowSignerAsSigner(walletResponse.address, args) + : args; return this.createWalletInstance(walletResponse, walletInstanceArgs); } @@ -513,6 +474,60 @@ export class WalletFactory { return false; } + private isShadowSignerEnabled(chain: Chain, options?: WalletOptions): boolean { + return (chain === "solana" || chain === "stellar") && options?.shadowSigner?.enabled !== false; + } + + private async addShadowSignerToDelegatedSignersIfNeeded( + args: WalletCreateArgs, + delegatedSigners: Array, + shadowSignerEnabled: boolean + ): Promise<{ + delegatedSigners: Array; + shadowSignerPublicKey: string | null; + }> { + if ( + !this.apiClient.isServerSide && + shadowSignerEnabled && + (args.signer.type === "email" || args.signer.type === "phone") + ) { + try { + const { delegatedSigner, publicKey } = await generateShadowSigner(args.chain); + + return { + delegatedSigners: [...delegatedSigners, delegatedSigner], + shadowSignerPublicKey: publicKey, + }; + } catch (error) { + console.warn("Failed to create shadow signer:", error); + } + } + + return { + delegatedSigners, + shadowSignerPublicKey: null, + }; + } + + private setShadowSignerAsSigner( + walletAddress: string, + args: WalletCreateArgs + ): WalletCreateArgs { + const shadowData = getShadowSigner(walletAddress); + if (shadowData != null) { + const shadowSignerConfig = { + type: "external-wallet", + address: shadowData.publicKey, + } as SignerConfigForChain; + return { + ...args, + signer: shadowSignerConfig, + onCreateConfig: args.onCreateConfig ?? { adminSigner: args.signer }, + }; + } + return args; + } + private getChainType(chain: Chain): "solana" | "evm" | "stellar" { if (chain === "solana") { return "solana"; From 8f2aca8ebac3670e71da3c39a01c049691fcd44d Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Tue, 21 Oct 2025 15:24:37 -0400 Subject: [PATCH 42/82] fix shadow signer for stellar --- .../providers/CrossmintWalletBaseProvider.tsx | 2 +- packages/wallets/src/utils/shadow-signer.ts | 82 ++++++--- .../wallets/src/wallets/wallet-factory.ts | 160 ++++++++++++++---- 3 files changed, 181 insertions(+), 63 deletions(-) diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index 0bdcd0cbb..9a8202c6c 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -123,7 +123,7 @@ export function CrossmintWalletBaseProvider({ ); const getOrCreateWallet = useCallback( - async (_args: WalletArgsFor) => { + async (_args: WalletCreateArgs) => { // Deep clone the args object to avoid mutating the original object const args = cloneDeep(_args); if (experimental_customAuth?.jwt == null || walletStatus === "in-progress") { diff --git a/packages/wallets/src/utils/shadow-signer.ts b/packages/wallets/src/utils/shadow-signer.ts index db8a2654b..245790ccb 100644 --- a/packages/wallets/src/utils/shadow-signer.ts +++ b/packages/wallets/src/utils/shadow-signer.ts @@ -4,6 +4,8 @@ import type { Chain } from "../chains/chains"; import type { RegisterSignerParams } from "../api/types"; const SHADOW_SIGNER_STORAGE_KEY = "crossmint_shadow_signer"; +const SHADOW_SIGNER_DB_NAME = "crossmint_shadow_keys"; +const SHADOW_SIGNER_DB_STORE = "keys"; export type ShadowSignerData = { chain: Chain; @@ -15,12 +17,23 @@ export type ShadowSignerData = { export type ShadowSignerResult = { delegatedSigner: RegisterSignerParams; publicKey: string; + privateKey: CryptoKey; }; -/** - * Generate a shadow signer for the given chain. - * For Solana/Stellar: Creates an ed25519 keypair and returns external-wallet signer - */ +async function openDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(SHADOW_SIGNER_DB_NAME, 1); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(SHADOW_SIGNER_DB_STORE)) { + db.createObjectStore(SHADOW_SIGNER_DB_STORE); + } + }; + }); +} + export async function generateShadowSigner(chain: Chain): Promise { if (chain === "solana" || chain === "stellar") { const keyPair = (await window.crypto.subtle.generateKey( @@ -47,19 +60,33 @@ export async function generateShadowSigner(chain: Chain): Promise { + if (typeof localStorage === "undefined" || typeof indexedDB === "undefined") { return; } + + const db = await openDB(); + const tx = db.transaction([SHADOW_SIGNER_DB_STORE], "readwrite"); + const store = tx.objectStore(SHADOW_SIGNER_DB_STORE); + store.put(privateKey, walletAddress); + + await new Promise((resolve, reject) => { + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + const data: ShadowSignerData = { chain, walletAddress, @@ -70,9 +97,6 @@ export function storeShadowSigner(walletAddress: string, chain: Chain, publicKey localStorage.setItem(`${SHADOW_SIGNER_STORAGE_KEY}_${walletAddress}`, JSON.stringify(data)); } -/** - * Retrieve shadow signer metadata from localStorage - */ export function getShadowSigner(walletAddress: string): ShadowSignerData | null { if (typeof localStorage === "undefined") { return null; @@ -81,19 +105,27 @@ export function getShadowSigner(walletAddress: string): ShadowSignerData | null return stored ? JSON.parse(stored) : null; } -/** - * Check if a shadow signer exists for the given wallet - */ -export function hasShadowSigner(walletAddress: string): boolean { - return getShadowSigner(walletAddress) !== null; -} +export async function getShadowSignerPrivateKey(walletAddress: string): Promise { + if (typeof indexedDB === "undefined") { + return null; + } -/** - * Remove shadow signer metadata from localStorage - */ -export function removeShadowSigner(walletAddress: string): void { - if (typeof localStorage === "undefined") { - return; + try { + const db = await openDB(); + const tx = db.transaction([SHADOW_SIGNER_DB_STORE], "readonly"); + const store = tx.objectStore(SHADOW_SIGNER_DB_STORE); + const request = store.get(walletAddress); + + return new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result || null); + request.onerror = () => reject(request.error); + }); + } catch (error) { + console.warn("Failed to retrieve shadow signer private key:", error); + return null; } - localStorage.removeItem(`${SHADOW_SIGNER_STORAGE_KEY}_${walletAddress}`); +} + +export function hasShadowSigner(walletAddress: string): boolean { + return getShadowSigner(walletAddress) !== null; } diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index d5c431c5b..d3beb5707 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -11,12 +11,29 @@ import type { } from "../api"; import { WalletCreationError, WalletNotAvailableError } from "../utils/errors"; import type { Chain } from "../chains/chains"; -import type { InternalSignerConfig, PasskeySignerConfig, SignerConfigForChain } from "../signers/types"; +import type { + ApiKeyInternalSignerConfig, + EmailInternalSignerConfig, + EmailSignerConfig, + InternalSignerConfig, + PasskeyInternalSignerConfig, + PasskeySignerConfig, + PhoneInternalSignerConfig, + PhoneSignerConfig, + SignerConfigForChain, +} from "../signers/types"; +import type { SolanaExternalWalletSignerConfig, StellarExternalWalletSignerConfig } from "../signers/types"; import { Wallet } from "./wallet"; import { assembleSigner } from "../signers"; import type { DelegatedSigner, WalletArgsFor, WalletCreateArgs, WalletOptions } from "./types"; import { compareSignerConfigs } from "../utils/signer-validation"; -import { generateShadowSigner, storeShadowSigner, getShadowSigner } from "../utils/shadow-signer"; +import { + generateShadowSigner, + storeShadowSigner, + getShadowSigner, + getShadowSignerPrivateKey, +} from "../utils/shadow-signer"; +import { PublicKey } from "@solana/web3.js"; const DELEGATED_SIGNER_MISMATCH_ERROR = "When 'delegatedSigners' is provided to a method that may fetch an existing wallet, each specified delegated signer must exist in that wallet's configuration."; @@ -111,8 +128,11 @@ export class WalletFactory { ); const shadowSignerEnabled = this.isShadowSignerEnabled(args.chain, args.options); - const { delegatedSigners: updatedDelegatedSigners, shadowSignerPublicKey } = - await this.addShadowSignerToDelegatedSignersIfNeeded(args, delegatedSigners, shadowSignerEnabled); + const { + delegatedSigners: updatedDelegatedSigners, + shadowSignerPublicKey, + shadowSignerPrivateKey, + } = await this.addShadowSignerToDelegatedSignersIfNeeded(args, delegatedSigners, shadowSignerEnabled); delegatedSigners = updatedDelegatedSigners; const tempArgs = { ...args, signer: adminSignerConfig }; @@ -139,8 +159,13 @@ export class WalletFactory { throw new WalletCreationError(JSON.stringify(walletResponse)); } - if (!this.apiClient.isServerSide && shadowSignerEnabled && shadowSignerPublicKey != null) { - storeShadowSigner(walletResponse.address, args.chain, shadowSignerPublicKey); + if ( + !this.apiClient.isServerSide && + shadowSignerEnabled && + shadowSignerPublicKey != null && + shadowSignerPrivateKey != null + ) { + await storeShadowSigner(walletResponse.address, args.chain, shadowSignerPublicKey, shadowSignerPrivateKey); } const walletInstanceArgs = shadowSignerEnabled @@ -155,7 +180,6 @@ export class WalletFactory { args: WalletArgsFor ): Wallet { this.validateExistingWalletConfig(walletResponse, args); - const signerConfig = this.toInternalSignerConfig(walletResponse, args.signer, args.options); return new Wallet( { @@ -190,22 +214,23 @@ export class WalletFactory { switch (signerArgs.type) { case "api-key": { - const walletSigner = this.getWalletSigner(walletResponse, "api-key"); - return { type: "api-key", - address: walletSigner.address, - locator: walletSigner.locator, - }; + locator: this.getSignerLocator(signerArgs), + address: walletResponse.address, + } as ApiKeyInternalSignerConfig; } case "external-wallet": { - const walletSigner = this.getWalletSigner(walletResponse, "external-wallet"); + const walletSigner = this.getWalletSigner(walletResponse, this.getSignerLocator(signerArgs)); return { ...walletSigner, ...signerArgs } as InternalSignerConfig; } case "passkey": { - const walletSigner = this.getWalletSigner(walletResponse, "passkey"); + const walletSigner = this.getWalletSigner( + walletResponse, + this.getSignerLocator(signerArgs) + ) as PasskeySignerConfig; return { type: "passkey", @@ -214,10 +239,13 @@ export class WalletFactory { locator: walletSigner.locator, onCreatePasskey: signerArgs.onCreatePasskey, onSignWithPasskey: signerArgs.onSignWithPasskey, - }; + } as PasskeyInternalSignerConfig; } case "email": { - const walletSigner = this.getWalletSigner(walletResponse, "email"); + const walletSigner = this.getWalletSigner( + walletResponse, + this.getSignerLocator(signerArgs) + ) as EmailSignerConfig; return { type: "email", @@ -227,11 +255,14 @@ export class WalletFactory { crossmint: this.apiClient.crossmint, onAuthRequired: signerArgs.onAuthRequired, clientTEEConnection: options?.clientTEEConnection, - }; + } as EmailInternalSignerConfig; } case "phone": { - const walletSigner = this.getWalletSigner(walletResponse, "phone"); + const walletSigner = this.getWalletSigner( + walletResponse, + this.getSignerLocator(signerArgs) + ) as PhoneSignerConfig; return { type: "phone", @@ -241,7 +272,7 @@ export class WalletFactory { crossmint: this.apiClient.crossmint, onAuthRequired: signerArgs.onAuthRequired, clientTEEConnection: options?.clientTEEConnection, - }; + } as PhoneInternalSignerConfig; } default: @@ -249,21 +280,21 @@ export class WalletFactory { } } - private getWalletSigner["type"]>( + private getWalletSigner( wallet: GetWalletSuccessResponse, - signerType: T - ): Extract { + signerLocator: string + ): AdminSignerConfig | DelegatedSignerResponse | PasskeySignerConfig { const config = wallet.config as SmartWalletConfig; const adminSigner = config?.adminSigner; const delegatedSigners = config?.delegatedSigners || []; - if (adminSigner?.type === signerType) { - return adminSigner as Extract; + if ("locator" in adminSigner && adminSigner.locator === signerLocator) { + return adminSigner; } - const delegatedSigner = delegatedSigners.find((ds) => ds.type === signerType); + const delegatedSigner = delegatedSigners.find((ds) => ds.locator === signerLocator); if (delegatedSigner != null) { - return delegatedSigner as Extract; + return delegatedSigner; } - throw new WalletCreationError(`${signerType} signer does not match the wallet's signer type`); + throw new WalletCreationError(`${signerLocator} signer does not match the wallet's signer type`); } private async createPasskeySigner( @@ -485,6 +516,7 @@ export class WalletFactory { ): Promise<{ delegatedSigners: Array; shadowSignerPublicKey: string | null; + shadowSignerPrivateKey: CryptoKey | null; }> { if ( !this.apiClient.isServerSide && @@ -492,11 +524,12 @@ export class WalletFactory { (args.signer.type === "email" || args.signer.type === "phone") ) { try { - const { delegatedSigner, publicKey } = await generateShadowSigner(args.chain); + const { delegatedSigner, publicKey, privateKey } = await generateShadowSigner(args.chain); return { delegatedSigners: [...delegatedSigners, delegatedSigner], shadowSignerPublicKey: publicKey, + shadowSignerPrivateKey: privateKey, }; } catch (error) { console.warn("Failed to create shadow signer:", error); @@ -506,6 +539,7 @@ export class WalletFactory { return { delegatedSigners, shadowSignerPublicKey: null, + shadowSignerPrivateKey: null, }; } @@ -515,15 +549,67 @@ export class WalletFactory { ): WalletCreateArgs { const shadowData = getShadowSigner(walletAddress); if (shadowData != null) { - const shadowSignerConfig = { - type: "external-wallet", - address: shadowData.publicKey, - } as SignerConfigForChain; - return { - ...args, - signer: shadowSignerConfig, - onCreateConfig: args.onCreateConfig ?? { adminSigner: args.signer }, - }; + if (args.chain === "solana") { + const shadowSignerConfig: SolanaExternalWalletSignerConfig = { + type: "external-wallet", + address: shadowData.publicKey, + onSignTransaction: async (transaction) => { + const privateKey = await getShadowSignerPrivateKey(walletAddress); + if (!privateKey) { + 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; + }, + }; + + return { + ...args, + signer: shadowSignerConfig as SignerConfigForChain, + onCreateConfig: args.onCreateConfig ?? { adminSigner: args.signer }, + }; + } else if (args.chain === "stellar") { + const shadowSignerConfig: StellarExternalWalletSignerConfig = { + type: "external-wallet", + address: shadowData.publicKey, + onSignStellarTransaction: async (payload) => { + const privateKey = await getShadowSignerPrivateKey(walletAddress); + if (!privateKey) { + 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; + }, + }; + + return { + ...args, + signer: shadowSignerConfig as SignerConfigForChain, + onCreateConfig: args.onCreateConfig ?? { adminSigner: args.signer }, + }; + } } return args; } From 13f43690f8029371ffbde0d8295b54859f4c6509 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 23 Oct 2025 09:48:23 -0400 Subject: [PATCH 43/82] implement shadow signers for react native --- apps/wallets/smart-wallet/expo/app/index.tsx | 2 +- packages/wallets/package.json | 3 + packages/wallets/src/types/base32.js.d.ts | 4 + .../src/utils/encodeEd25519PublicKey.ts | 64 ++++++++ .../utils/shadow-signer-storage-browser.ts | 97 +++++++++++++ .../src/utils/shadow-signer-storage-rn.ts | 77 ++++++++++ packages/wallets/src/utils/shadow-signer.ts | 111 +++++++------- .../wallets/src/wallets/wallet-factory.ts | 18 +-- packages/wallets/tsup.config.ts | 19 ++- pnpm-lock.yaml | 137 +++++++++++++++++- 10 files changed, 456 insertions(+), 76 deletions(-) create mode 100644 packages/wallets/src/types/base32.js.d.ts create mode 100644 packages/wallets/src/utils/encodeEd25519PublicKey.ts create mode 100644 packages/wallets/src/utils/shadow-signer-storage-browser.ts create mode 100644 packages/wallets/src/utils/shadow-signer-storage-rn.ts diff --git a/apps/wallets/smart-wallet/expo/app/index.tsx b/apps/wallets/smart-wallet/expo/app/index.tsx index 56bd59383..5f105d7fe 100644 --- a/apps/wallets/smart-wallet/expo/app/index.tsx +++ b/apps/wallets/smart-wallet/expo/app/index.tsx @@ -73,7 +73,7 @@ export default function Index() { } setIsLoading(true); try { - await getOrCreateWallet({ chain: "base-sepolia", signer: { type: "email" } }); + await getOrCreateWallet({ chain: "solana", signer: { type: "email" } }); } catch (error) { console.error("Error initializing wallet:", error); } finally { diff --git a/packages/wallets/package.json b/packages/wallets/package.json index 02114c0cb..092d15ee6 100644 --- a/packages/wallets/package.json +++ b/packages/wallets/package.json @@ -27,11 +27,14 @@ "@crossmint/client-signers": "workspace:*", "@crossmint/common-sdk-base": "workspace:*", "@hey-api/client-fetch": "0.8.1", + "@react-native-async-storage/async-storage": "2.2.0", "@solana/web3.js": "1.98.1", "@stellar/stellar-sdk": "v14.0.0-rc.3", "abitype": "1.0.8", + "base32.js": "0.1.0", "bs58": "5.0.0", "ox": "0.6.9", + "react-native-webview-crypto": "0.0.27", "tweetnacl": "1.0.3", "viem": "2.33.1" }, diff --git a/packages/wallets/src/types/base32.js.d.ts b/packages/wallets/src/types/base32.js.d.ts new file mode 100644 index 000000000..e90c00138 --- /dev/null +++ b/packages/wallets/src/types/base32.js.d.ts @@ -0,0 +1,4 @@ +declare module 'base32.js' { + export function encode(input: string | Buffer): string; + export function decode(input: string): Buffer; +} diff --git a/packages/wallets/src/utils/encodeEd25519PublicKey.ts b/packages/wallets/src/utils/encodeEd25519PublicKey.ts new file mode 100644 index 000000000..e7e0185fc --- /dev/null +++ b/packages/wallets/src/utils/encodeEd25519PublicKey.ts @@ -0,0 +1,64 @@ +/** + * This code was copied from the stellar-sdk-base library. + * https://github.com/stellar/js-stellar-base/blob/5bac0b60bcd01794bb0b11fd8b6a45e486c28626/src/strkey.js#L400 + * This is because the stellar-sdk library is not compatible with React Native. + */ + +import base32 from "base32.js"; + +function calculateChecksum(payload: Uint8Array): Uint8Array { + const crcTable = [ + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, + 0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, 0xa35a, + 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, + 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, + 0x2802, 0x3823, 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, + 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, + 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, + 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, + 0x5004, 0x4025, 0x7046, 0x6067, 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 0x1290, + 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, + 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, + 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c, + 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, + 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, + 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, + ]; + + let crc16 = 0x0; + + for (let i = 0; i < payload.length; i += 1) { + const byte = payload[i]; + const lookupIndex = (crc16 >> 8) ^ byte; + crc16 = (crc16 << 8) ^ crcTable[lookupIndex]; + crc16 &= 0xffff; + } + const checksum = new Uint8Array(2); + checksum[0] = crc16 & 0xff; + checksum[1] = (crc16 >> 8) & 0xff; + return checksum; +} + +export function encodeEd25519PublicKey(data: Uint8Array): string { + if (data === null || data === undefined) { + throw new Error("cannot encode null data"); + } + + const versionByte = 6 << 3; + + const versionBuffer = new Uint8Array([versionByte]); + const payload = new Uint8Array(versionBuffer.length + data.length); + payload.set(versionBuffer); + payload.set(data, versionBuffer.length); + + const checksum = calculateChecksum(payload); + const unencoded = new Uint8Array(payload.length + checksum.length); + unencoded.set(payload); + unencoded.set(checksum, payload.length); + + return base32.encode(Buffer.from(unencoded)); +} diff --git a/packages/wallets/src/utils/shadow-signer-storage-browser.ts b/packages/wallets/src/utils/shadow-signer-storage-browser.ts new file mode 100644 index 000000000..537135b46 --- /dev/null +++ b/packages/wallets/src/utils/shadow-signer-storage-browser.ts @@ -0,0 +1,97 @@ +import type { ShadowSignerData, ShadowSignerStorage } from "./shadow-signer"; + +export class BrowserShadowSignerStorage implements ShadowSignerStorage { + private readonly SHADOW_SIGNER_DB_NAME = "crossmint_shadow_keys"; + private readonly SHADOW_SIGNER_DB_STORE = "keys"; + private readonly SHADOW_SIGNER_STORAGE_KEY = "crossmint_shadow_signer"; + + private async openDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.SHADOW_SIGNER_DB_NAME, 1); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(this.SHADOW_SIGNER_DB_STORE)) { + db.createObjectStore(this.SHADOW_SIGNER_DB_STORE); + } + }; + }); + } + + async storePrivateKey(walletAddress: string, privateKey: CryptoKey): Promise { + if (typeof indexedDB === "undefined") { + return; + } + + const db = await this.openDB(); + const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readwrite"); + const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); + store.put(privateKey, walletAddress); + + return new Promise((resolve, reject) => { + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + } + + async getPrivateKey(walletAddress: string): Promise { + if (typeof indexedDB === "undefined") { + return null; + } + + try { + const db = await this.openDB(); + const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readonly"); + const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); + const request = store.get(walletAddress); + + return new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result || null); + request.onerror = () => reject(request.error); + }); + } catch (error) { + console.warn("Failed to retrieve private key from IndexedDB:", error); + return null; + } + } + + async removePrivateKey(walletAddress: string): Promise { + if (typeof indexedDB === "undefined") { + return; + } + + const db = await this.openDB(); + const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readwrite"); + const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); + store.delete(walletAddress); + + return new Promise((resolve, reject) => { + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + } + + storeMetadata(walletAddress: string, data: ShadowSignerData): Promise { + if (typeof localStorage === "undefined") { + return Promise.resolve(); + } + + localStorage.setItem(`${this.SHADOW_SIGNER_STORAGE_KEY}_${walletAddress}`, JSON.stringify(data)); + return Promise.resolve(); + } + + getMetadata(walletAddress: string): Promise { + if (typeof localStorage === "undefined") { + return Promise.resolve(null); + } + + try { + const stored = localStorage.getItem(`${this.SHADOW_SIGNER_STORAGE_KEY}_${walletAddress}`); + return Promise.resolve(stored ? JSON.parse(stored) : null); + } catch (error) { + console.warn("Failed to retrieve metadata from localStorage:", error); + return Promise.resolve(null); + } + } +} diff --git a/packages/wallets/src/utils/shadow-signer-storage-rn.ts b/packages/wallets/src/utils/shadow-signer-storage-rn.ts new file mode 100644 index 000000000..957991bc0 --- /dev/null +++ b/packages/wallets/src/utils/shadow-signer-storage-rn.ts @@ -0,0 +1,77 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import type { ShadowSignerData, ShadowSignerStorage } from "./shadow-signer"; + +export class ReactNativeShadowSignerStorage implements ShadowSignerStorage { + private readonly SHADOW_SIGNER_DB_NAME = "crossmint_shadow_keys"; + private readonly SHADOW_SIGNER_DB_STORE = "keys"; + private readonly SHADOW_SIGNER_STORAGE_KEY = "crossmint_shadow_signer"; + + private async openDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.SHADOW_SIGNER_DB_NAME, 1); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(this.SHADOW_SIGNER_DB_STORE)) { + db.createObjectStore(this.SHADOW_SIGNER_DB_STORE); + } + }; + }); + } + + async storePrivateKey(walletAddress: string, privateKey: CryptoKey): Promise { + const db = await this.openDB(); + const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readwrite"); + const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); + store.put(privateKey, walletAddress); + + return new Promise((resolve, reject) => { + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + } + + async getPrivateKey(walletAddress: string): Promise { + try { + const db = await this.openDB(); + const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readonly"); + const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); + const request = store.get(walletAddress); + + return new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result || null); + request.onerror = () => reject(request.error); + }); + } catch (error) { + console.warn("Failed to retrieve private key from IndexedDB:", error); + return null; + } + } + + async removePrivateKey(walletAddress: string): Promise { + const db = await this.openDB(); + const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readwrite"); + const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); + store.delete(walletAddress); + + return new Promise((resolve, reject) => { + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + } + + async storeMetadata(walletAddress: string, data: ShadowSignerData): Promise { + await AsyncStorage.setItem(`${this.SHADOW_SIGNER_STORAGE_KEY}_${walletAddress}`, JSON.stringify(data)); + } + + async getMetadata(walletAddress: string): Promise { + try { + const stored = await AsyncStorage.getItem(`${this.SHADOW_SIGNER_STORAGE_KEY}_${walletAddress}`); + return stored ? JSON.parse(stored) : null; + } catch (error) { + console.warn("Failed to retrieve metadata from AsyncStorage:", error); + return null; + } + } +} diff --git a/packages/wallets/src/utils/shadow-signer.ts b/packages/wallets/src/utils/shadow-signer.ts index 245790ccb..60ded64c1 100644 --- a/packages/wallets/src/utils/shadow-signer.ts +++ b/packages/wallets/src/utils/shadow-signer.ts @@ -1,11 +1,9 @@ import { encode as encodeBase58 } from "bs58"; -import { StrKey } from "@stellar/stellar-sdk"; import type { Chain } from "../chains/chains"; import type { RegisterSignerParams } from "../api/types"; - -const SHADOW_SIGNER_STORAGE_KEY = "crossmint_shadow_signer"; -const SHADOW_SIGNER_DB_NAME = "crossmint_shadow_keys"; -const SHADOW_SIGNER_DB_STORE = "keys"; +import { encodeEd25519PublicKey } from "./encodeEd25519PublicKey"; +import { BrowserShadowSignerStorage } from "./shadow-signer-storage-browser"; +import { ReactNativeShadowSignerStorage } from "./shadow-signer-storage-rn"; export type ShadowSignerData = { chain: Chain; @@ -20,18 +18,28 @@ export type ShadowSignerResult = { privateKey: CryptoKey; }; -async function openDB(): Promise { - return new Promise((resolve, reject) => { - const request = indexedDB.open(SHADOW_SIGNER_DB_NAME, 1); - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result); - request.onupgradeneeded = (event) => { - const db = (event.target as IDBOpenDBRequest).result; - if (!db.objectStoreNames.contains(SHADOW_SIGNER_DB_STORE)) { - db.createObjectStore(SHADOW_SIGNER_DB_STORE); - } - }; - }); +export interface ShadowSignerStorage { + storePrivateKey(walletAddress: string, privateKey: CryptoKey): Promise; + getPrivateKey(walletAddress: string): Promise; + removePrivateKey(walletAddress: string): Promise; + storeMetadata(walletAddress: string, data: ShadowSignerData): Promise; + getMetadata(walletAddress: string): Promise; +} + +let storageInstance: ShadowSignerStorage | null = null; + +function getStorage(): ShadowSignerStorage { + if (!storageInstance) { + const isReactNative = typeof navigator !== "undefined" && navigator.product === "ReactNative"; + const isExpo = typeof global !== "undefined" && (global as { expo?: unknown }).expo; + + if (isReactNative || isExpo) { + storageInstance = new ReactNativeShadowSignerStorage(); + } else { + storageInstance = new BrowserShadowSignerStorage(); + } + } + return storageInstance; } export async function generateShadowSigner(chain: Chain): Promise { @@ -40,7 +48,7 @@ export async function generateShadowSigner(chain: Chain): Promise { - if (typeof localStorage === "undefined" || typeof indexedDB === "undefined") { - return; - } - - const db = await openDB(); - const tx = db.transaction([SHADOW_SIGNER_DB_STORE], "readwrite"); - const store = tx.objectStore(SHADOW_SIGNER_DB_STORE); - store.put(privateKey, walletAddress); - - await new Promise((resolve, reject) => { - tx.oncomplete = () => resolve(); - tx.onerror = () => reject(tx.error); - }); + const storage = getStorage(); + try { + await storage.storePrivateKey(walletAddress, privateKey); - const data: ShadowSignerData = { - chain, - walletAddress, - publicKey, - createdAt: Date.now(), - }; + const data: ShadowSignerData = { + chain, + walletAddress, + publicKey, + createdAt: Date.now(), + }; - localStorage.setItem(`${SHADOW_SIGNER_STORAGE_KEY}_${walletAddress}`, JSON.stringify(data)); + await storage.storeMetadata(walletAddress, data); + } catch (error) { + console.warn("Failed to store shadow signer:", error); + } } -export function getShadowSigner(walletAddress: string): ShadowSignerData | null { - if (typeof localStorage === "undefined") { +export async function getShadowSigner(walletAddress: string): Promise { + const storage = getStorage(); + try { + return await storage.getMetadata(walletAddress); + } catch (error) { + console.warn("Failed to get shadow signer:", error); return null; } - const stored = localStorage.getItem(`${SHADOW_SIGNER_STORAGE_KEY}_${walletAddress}`); - return stored ? JSON.parse(stored) : null; } export async function getShadowSignerPrivateKey(walletAddress: string): Promise { - if (typeof indexedDB === "undefined") { - return null; - } - + const storage = getStorage(); try { - const db = await openDB(); - const tx = db.transaction([SHADOW_SIGNER_DB_STORE], "readonly"); - const store = tx.objectStore(SHADOW_SIGNER_DB_STORE); - const request = store.get(walletAddress); - - return new Promise((resolve, reject) => { - request.onsuccess = () => resolve(request.result || null); - request.onerror = () => reject(request.error); - }); + return await storage.getPrivateKey(walletAddress); } catch (error) { console.warn("Failed to retrieve shadow signer private key:", error); return null; } } -export function hasShadowSigner(walletAddress: string): boolean { - return getShadowSigner(walletAddress) !== null; +export async function hasShadowSigner(walletAddress: string): Promise { + const signer = await getShadowSigner(walletAddress); + return signer !== null; } diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index d3beb5707..bccf6fc32 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -58,7 +58,7 @@ export class WalletFactory { if (existingWallet != null && !("error" in existingWallet)) { const shadowSignerEnabled = this.isShadowSignerEnabled(args.chain, args.options); const walletInstanceArgs = shadowSignerEnabled - ? this.setShadowSignerAsSigner(existingWallet.address, args) + ? await this.setShadowSignerAsSigner(existingWallet.address, args) : args; return this.createWalletInstance(existingWallet, walletInstanceArgs); @@ -169,7 +169,7 @@ export class WalletFactory { } const walletInstanceArgs = shadowSignerEnabled - ? this.setShadowSignerAsSigner(walletResponse.address, args) + ? await this.setShadowSignerAsSigner(walletResponse.address, args) : args; return this.createWalletInstance(walletResponse, walletInstanceArgs); @@ -543,11 +543,11 @@ export class WalletFactory { }; } - private setShadowSignerAsSigner( + private async setShadowSignerAsSigner( walletAddress: string, args: WalletCreateArgs - ): WalletCreateArgs { - const shadowData = getShadowSigner(walletAddress); + ): Promise> { + const shadowData = await getShadowSigner(walletAddress); if (shadowData != null) { if (args.chain === "solana") { const shadowSignerConfig: SolanaExternalWalletSignerConfig = { @@ -555,8 +555,8 @@ export class WalletFactory { address: shadowData.publicKey, onSignTransaction: async (transaction) => { const privateKey = await getShadowSignerPrivateKey(walletAddress); - if (!privateKey) { - throw new Error("Shadow signer private key not found"); + if (!privateKey || !(privateKey instanceof CryptoKey)) { + throw new Error("Shadow signer private key not found or invalid type"); } const messageBytes = new Uint8Array(transaction.message.serialize()); @@ -584,8 +584,8 @@ export class WalletFactory { address: shadowData.publicKey, onSignStellarTransaction: async (payload) => { const privateKey = await getShadowSignerPrivateKey(walletAddress); - if (!privateKey) { - throw new Error("Shadow signer private key not found"); + if (!privateKey || !(privateKey instanceof CryptoKey)) { + throw new Error("Shadow signer private key not found or invalid type"); } const transactionString = typeof payload === "string" ? payload : (payload as any).tx; diff --git a/packages/wallets/tsup.config.ts b/packages/wallets/tsup.config.ts index c86148398..c7e3668f0 100644 --- a/packages/wallets/tsup.config.ts +++ b/packages/wallets/tsup.config.ts @@ -1,3 +1,20 @@ import { treeShakableConfig } from "../../tsup.config.base"; +import type { Options } from "tsup"; -export default treeShakableConfig; +const config: Options = { + ...treeShakableConfig, + // Exclude test files and React Native storage from the main build + entry: [ + "src/**/*.(ts|tsx)", + "!src/**/*.test.(ts|tsx)", + "!src/utils/shadow-signer-storage-rn.ts", // Exclude RN storage implementation + ], + // Add external dependencies that should not be bundled + external: ["react-native-webview-crypto", "@react-native-async-storage/async-storage"], + // Define environment variables for conditional compilation + define: { + "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"), + }, +}; + +export default config; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 737d8cb24..c8110cc4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1057,6 +1057,9 @@ importers: '@hey-api/client-fetch': specifier: 0.8.1 version: 0.8.1 + '@react-native-async-storage/async-storage': + specifier: 2.2.0 + version: 2.2.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) '@solana/web3.js': specifier: 1.98.1 version: 1.98.1(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -1066,12 +1069,24 @@ importers: abitype: specifier: 1.0.8 version: 1.0.8(typescript@5.9.3)(zod@3.25.76) + base32.js: + specifier: 0.1.0 + version: 0.1.0 bs58: specifier: 5.0.0 version: 5.0.0 + expo-crypto: + specifier: 15.0.7 + version: 15.0.7(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10)) ox: specifier: 0.6.9 version: 0.6.9(typescript@5.9.3)(zod@3.25.76) + react-native-keychain: + specifier: 10.0.0 + version: 10.0.0 + react-native-webview-crypto: + specifier: 0.0.27 + version: 0.0.27(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) tweetnacl: specifier: 1.0.3 version: 1.0.3 @@ -4614,6 +4629,11 @@ packages: react: 19.1.0 react-dom: 19.1.0 + '@react-native-async-storage/async-storage@2.2.0': + resolution: {integrity: sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==} + peerDependencies: + react-native: 0.81.4 + '@react-native/assets-registry@0.81.4': resolution: {integrity: sha512-AMcDadefBIjD10BRqkWw+W/VdvXEomR6aEZ0fhQRAv7igrBzb4PTn4vHKYg+sUK0e3wa74kcMy2DLc/HtnGcMA==} engines: {node: '>= 20.19.4'} @@ -7837,6 +7857,11 @@ packages: expo: '*' react-native: 0.81.4 + expo-crypto@15.0.7: + resolution: {integrity: sha512-FUo41TwwGT2e5rA45PsjezI868Ch3M6wbCZsmqTWdF/hr+HyPcrp1L//dsh/hsrsyrQdpY/U96Lu71/wXePJeg==} + peerDependencies: + expo: '*' + expo-device@8.0.9: resolution: {integrity: sha512-XqRpaljDNAYZGZzMpC+b9KZfzfydtkwx3pJAp6ODDH+O/5wjAw+mLc5wQMGJCx8/aqVmMsAokec7iebxDPFZDA==} peerDependencies: @@ -8005,6 +8030,9 @@ packages: fast-base64-decode@1.0.0: resolution: {integrity: sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==} + fast-base64-encode@1.0.0: + resolution: {integrity: sha512-z2XCzVK4fde2cuTEHu2QGkLD6BPtJNKJPn0Z7oINvmhq/quUuIIVPYKUdN0gYeZqOyurjJjBH/bUzK5gafyHvw==} + fast-copy@3.0.2: resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} @@ -8779,6 +8807,10 @@ packages: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} engines: {node: '>=0.10.0'} + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + is-plain-obj@3.0.0: resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} engines: {node: '>=10'} @@ -9555,6 +9587,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.17.3: + resolution: {integrity: sha512-H+sg4+uBLOBrw9833P6gCURJjV+puWPbxM8S3H4ORlhVCmQpF5yCE50bc4Exaqm9U5Nhjw83Okq1azyb1U7mxw==} + log-symbols@2.2.0: resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} engines: {node: '>=4'} @@ -9717,6 +9752,10 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-options@3.0.4: + resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==} + engines: {node: '>=10'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -11136,6 +11175,10 @@ packages: react: 19.1.0 react-native: 0.81.4 + react-native-keychain@10.0.0: + resolution: {integrity: sha512-YzPKSAnSzGEJ12IK6CctNLU79T1W15WDrElRQ+1/FsOazGX9ucFPTQwgYe8Dy8jiSEDJKM4wkVa3g4lD2Z+Pnw==} + engines: {node: '>=16'} + react-native-quick-base64@2.1.2: resolution: {integrity: sha512-xghaXpWdB0ji8OwYyo0fWezRroNxiNFCNFpGUIyE7+qc4gA/IGWnysIG5L0MbdoORv8FkTKUvfd6yCUN5R2VFA==} peerDependencies: @@ -11168,6 +11211,13 @@ packages: react: 19.1.0 react-dom: 19.1.0 + react-native-webview-crypto@0.0.27: + resolution: {integrity: sha512-N4jyn9AbKG/VkQK23R1o+k/nyS2Kv20Tq27VpoQcCg4zx4hvx8y5Y9rjVehzTvUebZX4XdGTzSWGn1fFj98eXA==} + peerDependencies: + react: 19.1.0 + react-native: 0.81.4 + react-native-webview: '>=8.*' + react-native-webview@13.15.0: resolution: {integrity: sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==} peerDependencies: @@ -13090,6 +13140,9 @@ packages: resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} engines: {node: '>=0.8.0'} + webview-crypto@0.1.13: + resolution: {integrity: sha512-8nRkNvvYchoFi32tooLX6qZzG4iCoxOBGsamZnZ1BnN4Nl6cATiIOUzwWDjUpfMu8Mvf+t3Dn0p9cSLrnZfwtg==} + whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} @@ -16404,7 +16457,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: - expo-router: 6.0.12(beoeo6req5s7uiob6vyzmad7bi) + expo-router: 6.0.12(@types/react@19.1.10)(expo-constants@18.0.9)(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -16581,7 +16634,7 @@ snapshots: - supports-color - utf-8-validate - '@expo/metro-runtime@6.1.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)': + '@expo/metro-runtime@6.1.2(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)': dependencies: anser: 1.4.10 expo: 54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) @@ -19301,6 +19354,11 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + '@react-native-async-storage/async-storage@2.2.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))': + dependencies: + merge-options: 3.0.4 + react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + '@react-native/assets-registry@0.81.4': {} '@react-native/babel-plugin-codegen@0.81.4(@babel/core@7.28.4)': @@ -25499,6 +25557,11 @@ snapshots: transitivePeerDependencies: - supports-color + expo-crypto@15.0.7(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10)): + dependencies: + base64-js: 1.5.1 + expo: 54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) + expo-device@8.0.9(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10)): dependencies: expo: 54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) @@ -25550,9 +25613,47 @@ snapshots: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + expo-router@6.0.12(@types/react@19.1.10)(expo-constants@18.0.9)(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): + dependencies: + '@expo/metro-runtime': 6.1.2(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + '@expo/schema-utils': 0.1.7 + '@radix-ui/react-slot': 1.2.0(@types/react@19.1.10)(react@19.1.0) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.1.0(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-navigation/bottom-tabs': 7.4.9(@react-navigation/native@7.1.18(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + '@react-navigation/native': 7.1.18(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + '@react-navigation/native-stack': 7.3.28(@react-navigation/native@7.1.18(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + client-only: 0.0.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) + expo-constants: 18.0.9(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) + expo-server: 1.0.1 + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.11 + query-string: 7.1.3 + react: 19.1.0 + react-fast-compare: 3.2.2 + react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + semver: 7.6.3 + server-only: 0.0.1 + sf-symbols-typescript: 2.1.0 + shallowequal: 1.1.0 + use-latest-callback: 0.2.6(react@19.1.0) + vaul: 1.1.2(@types/react-dom@19.1.0(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + optionalDependencies: + react-dom: 19.1.0(react@19.1.0) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - '@types/react' + - '@types/react-dom' + - supports-color + optional: true + expo-router@6.0.12(beoeo6req5s7uiob6vyzmad7bi): dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + '@expo/metro-runtime': 6.1.2(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) '@expo/schema-utils': 0.1.7 '@radix-ui/react-slot': 1.2.0(@types/react@19.1.10)(react@19.1.0) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.1.0(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -25661,7 +25762,7 @@ snapshots: react-refresh: 0.14.2 whatwg-url-without-unicode: 8.0.0-3 optionalDependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + '@expo/metro-runtime': 6.1.2(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) react-native-webview: 13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) transitivePeerDependencies: - '@babel/core' @@ -25718,7 +25819,7 @@ snapshots: extension-port-stream@3.0.0: dependencies: - readable-stream: 3.6.2 + readable-stream: 4.7.0 webextension-polyfill: 0.10.0 external-editor@3.1.0: @@ -25731,6 +25832,8 @@ snapshots: fast-base64-decode@1.0.0: {} + fast-base64-encode@1.0.0: {} + fast-copy@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -26588,6 +26691,8 @@ snapshots: is-plain-obj@1.1.0: {} + is-plain-obj@2.1.0: {} + is-plain-obj@3.0.0: {} is-plain-obj@4.1.0: {} @@ -27708,6 +27813,8 @@ snapshots: lodash@4.17.21: {} + lodash@4.17.3: {} + log-symbols@2.2.0: dependencies: chalk: 2.4.2 @@ -27950,6 +28057,10 @@ snapshots: merge-descriptors@1.0.3: {} + merge-options@3.0.4: + dependencies: + is-plain-obj: 2.1.0 + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -29894,6 +30005,8 @@ snapshots: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + react-native-keychain@10.0.0: {} + react-native-quick-base64@2.1.2(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): dependencies: base64-js: 1.5.1 @@ -29937,6 +30050,15 @@ snapshots: transitivePeerDependencies: - encoding + react-native-webview-crypto@0.0.27(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): + dependencies: + encode-utf8: 1.0.3 + fast-base64-encode: 1.0.0 + react: 19.1.0 + react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) + react-native-webview: 13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + webview-crypto: 0.1.13 + react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): dependencies: escape-string-regexp: 4.0.0 @@ -32329,6 +32451,11 @@ snapshots: websocket-extensions@0.1.4: {} + webview-crypto@0.1.13: + dependencies: + lodash: 4.17.3 + serialize-error: 2.1.0 + whatwg-encoding@2.0.0: dependencies: iconv-lite: 0.6.3 From 55e291f82af42dccda643400954332d628769236 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 23 Oct 2025 10:33:28 -0400 Subject: [PATCH 44/82] move signing shado logic to signer --- packages/wallets/src/signers/index.ts | 10 +- .../non-custodial/ncs-solana-signer.ts | 47 ++++++++- .../non-custodial/ncs-stellar-signer.ts | 49 +++++++++- .../wallets/src/wallets/wallet-factory.ts | 95 +------------------ 4 files changed, 102 insertions(+), 99 deletions(-) diff --git a/packages/wallets/src/signers/index.ts b/packages/wallets/src/signers/index.ts index 4dfe0ec05..93058a3f9 100644 --- a/packages/wallets/src/signers/index.ts +++ b/packages/wallets/src/signers/index.ts @@ -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(chain: C, config: InternalSignerConfig): Signer { +export function assembleSigner( + chain: C, + config: InternalSignerConfig, + 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": diff --git a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts index ddf96fa4d..dadeb59de 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts @@ -1,11 +1,24 @@ -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 { getShadowSigner, getShadowSignerPrivateKey, type ShadowSignerData } from "@/utils/shadow-signer"; +import { SolanaExternalWalletSigner } from "../solana-external-wallet"; +import type { SolanaChain } from "@/chains/chains"; export class SolanaNonCustodialSigner extends NonCustodialSigner { - constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig) { + private shadowSigner: SolanaExternalWalletSigner | null = null; + + constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig, walletAddress: string) { super(config); + const shadowSigner = getShadowSigner(walletAddress); + if (shadowSigner != null) { + this.shadowSigner = new SolanaExternalWalletSigner(this.getShadowSignerConfig(shadowSigner, walletAddress)); + } } async signMessage() { @@ -13,6 +26,9 @@ export class SolanaNonCustodialSigner extends NonCustodialSigner { } async signTransaction(transaction: string): Promise<{ signature: string }> { + if (this.shadowSigner != null) { + return await this.shadowSigner.signTransaction(transaction); + } await this.handleAuthRequired(); const jwt = this.getJwtOrThrow(); @@ -60,4 +76,29 @@ export class SolanaNonCustodialSigner extends NonCustodialSigner { ); } } + + private getShadowSignerConfig( + shadowData: ShadowSignerData, + walletAddress: string + ): ExternalWalletInternalSignerConfig { + return { + type: "external-wallet", + address: shadowData.publicKey, + locator: `external-wallet-${shadowData.publicKey}`, + onSignTransaction: async (transaction) => { + const privateKey = await getShadowSignerPrivateKey(walletAddress); + if (!privateKey) { + 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; + }, + }; + } } diff --git a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts index dc0660b98..cd1012044 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts @@ -1,9 +1,24 @@ -import type { EmailInternalSignerConfig, PhoneInternalSignerConfig } from "../types"; +import { getShadowSigner, getShadowSignerPrivateKey, type ShadowSignerData } from "@/utils/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) { + private shadowSigner: StellarExternalWalletSigner | null = null; + constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig, walletAddress: string) { super(config); + + const shadowSigner = getShadowSigner(walletAddress); + if (shadowSigner != null) { + this.shadowSigner = new StellarExternalWalletSigner( + this.getShadowSignerConfig(shadowSigner, walletAddress) + ); + } } async signMessage() { @@ -11,6 +26,9 @@ export class StellarNonCustodialSigner extends NonCustodialSigner { } async signTransaction(payload: string): Promise<{ signature: string }> { + if (this.shadowSigner != null) { + return await this.shadowSigner.signTransaction(payload); + } await this.handleAuthRequired(); const jwt = this.getJwtOrThrow(); @@ -58,4 +76,31 @@ export class StellarNonCustodialSigner extends NonCustodialSigner { ); } } + + private getShadowSignerConfig( + shadowData: ShadowSignerData, + walletAddress: string + ): ExternalWalletInternalSignerConfig { + return { + type: "external-wallet", + address: shadowData.publicKey, + locator: `external-wallet-${shadowData.publicKey}`, + onSignStellarTransaction: async (payload) => { + const privateKey = await getShadowSignerPrivateKey(walletAddress); + if (!privateKey) { + 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; + }, + }; + } } diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index d3beb5707..140afc514 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -22,18 +22,11 @@ import type { PhoneSignerConfig, SignerConfigForChain, } from "../signers/types"; -import type { SolanaExternalWalletSignerConfig, StellarExternalWalletSignerConfig } from "../signers/types"; import { Wallet } from "./wallet"; import { assembleSigner } from "../signers"; import type { DelegatedSigner, WalletArgsFor, WalletCreateArgs, WalletOptions } from "./types"; import { compareSignerConfigs } from "../utils/signer-validation"; -import { - generateShadowSigner, - storeShadowSigner, - getShadowSigner, - getShadowSignerPrivateKey, -} from "../utils/shadow-signer"; -import { PublicKey } from "@solana/web3.js"; +import { generateShadowSigner, storeShadowSigner } from "../utils/shadow-signer"; const DELEGATED_SIGNER_MISMATCH_ERROR = "When 'delegatedSigners' is provided to a method that may fetch an existing wallet, each specified delegated signer must exist in that wallet's configuration."; @@ -56,12 +49,7 @@ export class WalletFactory { const existingWallet = await this.apiClient.getWallet(`me:${this.getChainType(args.chain)}:smart`); if (existingWallet != null && !("error" in existingWallet)) { - const shadowSignerEnabled = this.isShadowSignerEnabled(args.chain, args.options); - const walletInstanceArgs = shadowSignerEnabled - ? this.setShadowSignerAsSigner(existingWallet.address, args) - : args; - - return this.createWalletInstance(existingWallet, walletInstanceArgs); + return this.createWalletInstance(existingWallet, args); } return this.createWallet(args); @@ -168,11 +156,7 @@ export class WalletFactory { await storeShadowSigner(walletResponse.address, args.chain, shadowSignerPublicKey, shadowSignerPrivateKey); } - const walletInstanceArgs = shadowSignerEnabled - ? this.setShadowSignerAsSigner(walletResponse.address, args) - : args; - - return this.createWalletInstance(walletResponse, walletInstanceArgs); + return this.createWalletInstance(walletResponse, args); } private createWalletInstance( @@ -186,7 +170,7 @@ export class WalletFactory { chain: args.chain, address: walletResponse.address, owner: walletResponse.owner, - signer: assembleSigner(args.chain, signerConfig), + signer: assembleSigner(args.chain, signerConfig, walletResponse.address), options: args.options, }, this.apiClient @@ -543,77 +527,6 @@ export class WalletFactory { }; } - private setShadowSignerAsSigner( - walletAddress: string, - args: WalletCreateArgs - ): WalletCreateArgs { - const shadowData = getShadowSigner(walletAddress); - if (shadowData != null) { - if (args.chain === "solana") { - const shadowSignerConfig: SolanaExternalWalletSignerConfig = { - type: "external-wallet", - address: shadowData.publicKey, - onSignTransaction: async (transaction) => { - const privateKey = await getShadowSignerPrivateKey(walletAddress); - if (!privateKey) { - 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; - }, - }; - - return { - ...args, - signer: shadowSignerConfig as SignerConfigForChain, - onCreateConfig: args.onCreateConfig ?? { adminSigner: args.signer }, - }; - } else if (args.chain === "stellar") { - const shadowSignerConfig: StellarExternalWalletSignerConfig = { - type: "external-wallet", - address: shadowData.publicKey, - onSignStellarTransaction: async (payload) => { - const privateKey = await getShadowSignerPrivateKey(walletAddress); - if (!privateKey) { - 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; - }, - }; - - return { - ...args, - signer: shadowSignerConfig as SignerConfigForChain, - onCreateConfig: args.onCreateConfig ?? { adminSigner: args.signer }, - }; - } - } - return args; - } - private getChainType(chain: Chain): "solana" | "evm" | "stellar" { if (chain === "solana") { return "solana"; From 6aace1bc20c65973f6136a51f5522ea9ea7df8c9 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 23 Oct 2025 10:43:59 -0400 Subject: [PATCH 45/82] change shado signer option config --- packages/wallets/src/signers/types.ts | 6 ++++++ packages/wallets/src/utils/shadow-signer.ts | 4 ++-- packages/wallets/src/wallets/types.ts | 3 --- packages/wallets/src/wallets/wallet-factory.ts | 13 ++++++++----- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/wallets/src/signers/types.ts b/packages/wallets/src/signers/types.ts index f15b6f851..c558df2f6 100644 --- a/packages/wallets/src/signers/types.ts +++ b/packages/wallets/src/signers/types.ts @@ -29,6 +29,9 @@ export class AuthRejectedError extends Error { export type EmailSignerConfig = { type: "email"; email?: string; + shadowSigner?: { + enabled: boolean; + }; onAuthRequired?: ( needsAuth: boolean, sendEmailWithOtp: () => Promise, @@ -40,6 +43,9 @@ export type EmailSignerConfig = { export type PhoneSignerConfig = { type: "phone"; phone?: string; + shadowSigner?: { + enabled: boolean; + }; onAuthRequired?: ( needsAuth: boolean, sendEmailWithOtp: () => Promise, diff --git a/packages/wallets/src/utils/shadow-signer.ts b/packages/wallets/src/utils/shadow-signer.ts index 245790ccb..1e9564493 100644 --- a/packages/wallets/src/utils/shadow-signer.ts +++ b/packages/wallets/src/utils/shadow-signer.ts @@ -15,7 +15,7 @@ export type ShadowSignerData = { }; export type ShadowSignerResult = { - delegatedSigner: RegisterSignerParams; + shadowSigner: RegisterSignerParams; publicKey: string; privateKey: CryptoKey; }; @@ -58,7 +58,7 @@ export async function generateShadowSigner(chain: Chain): Promise = C extends StellarChain ? StellarWall export type WalletOptions = { experimental_callbacks?: Callbacks; clientTEEConnection?: HandshakeParent; - shadowSigner?: { - enabled?: boolean; - }; }; export type WalletArgsFor = { diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 140afc514..78fca7f58 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -115,7 +115,7 @@ export class WalletFactory { ) ?? [] ); - const shadowSignerEnabled = this.isShadowSignerEnabled(args.chain, args.options); + const shadowSignerEnabled = this.isShadowSignerEnabled(args.chain, args.signer); const { delegatedSigners: updatedDelegatedSigners, shadowSignerPublicKey, @@ -489,8 +489,11 @@ export class WalletFactory { return false; } - private isShadowSignerEnabled(chain: Chain, options?: WalletOptions): boolean { - return (chain === "solana" || chain === "stellar") && options?.shadowSigner?.enabled !== false; + private isShadowSignerEnabled(chain: Chain, signer: SignerConfigForChain): boolean { + if ((chain === "solana" || chain === "stellar") && (signer.type === "email" || signer.type === "phone")) { + return signer.shadowSigner?.enabled !== false; + } + return false; } private async addShadowSignerToDelegatedSignersIfNeeded( @@ -508,10 +511,10 @@ export class WalletFactory { (args.signer.type === "email" || args.signer.type === "phone") ) { try { - const { delegatedSigner, publicKey, privateKey } = await generateShadowSigner(args.chain); + const { shadowSigner, publicKey, privateKey } = await generateShadowSigner(args.chain); return { - delegatedSigners: [...delegatedSigners, delegatedSigner], + delegatedSigners: [...delegatedSigners, shadowSigner], shadowSignerPublicKey: publicKey, shadowSignerPrivateKey: privateKey, }; From a550bb86b52f75c89ce09851d09efd0e9d30cf07 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 23 Oct 2025 13:02:17 -0400 Subject: [PATCH 46/82] move delegated signer logic to a new function --- .../providers/CrossmintWalletBaseProvider.tsx | 6 -- .../non-custodial/ncs-solana-signer.ts | 2 +- .../non-custodial/ncs-stellar-signer.ts | 2 +- packages/wallets/src/utils/shadow-signer.ts | 13 ++- .../wallets/src/wallets/wallet-factory.ts | 98 +++++++++++-------- 5 files changed, 68 insertions(+), 53 deletions(-) diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index 9a8202c6c..4c5c26617 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -45,9 +45,6 @@ export interface CrossmintWalletBaseProviderProps { onAuthRequired?: EmailSignerConfig["onAuthRequired"] | PhoneSignerConfig["onAuthRequired"]; clientTEEConnection?: () => HandshakeParent; initializeWebView?: () => Promise; - shadowSigner?: { - enabled?: boolean; - }; } export function CrossmintWalletBaseProvider({ @@ -57,7 +54,6 @@ export function CrossmintWalletBaseProvider({ onAuthRequired, clientTEEConnection, initializeWebView, - shadowSigner, }: CrossmintWalletBaseProviderProps) { const { crossmint, experimental_customAuth } = useCrossmint( "CrossmintWalletBaseProvider must be used within CrossmintProvider" @@ -152,7 +148,6 @@ export function CrossmintWalletBaseProvider({ onCreateConfig: args.onCreateConfig, options: { clientTEEConnection: clientTEEConnection?.(), - shadowSigner: shadowSigner, experimental_callbacks: { onWalletCreationStart: _onWalletCreationStart ?? callbacks?.onWalletCreationStart, onTransactionStart: _onTransactionStart ?? callbacks?.onTransactionStart, @@ -199,7 +194,6 @@ export function CrossmintWalletBaseProvider({ signer: resolvedSigner, options: { clientTEEConnection: clientTEEConnection?.(), - shadowSigner: shadowSigner, experimental_callbacks: callbacks, }, }); diff --git a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts index dadeb59de..32e5e2b3c 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts @@ -46,7 +46,7 @@ export class SolanaNonCustodialSigner extends NonCustodialSigner { }, data: { keyType: "ed25519", - bytes: base58.encode(messageData), + bytes: base58.encode(new Uint8Array(messageData)), encoding: "base58", }, }, diff --git a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts index cd1012044..b8c31c72f 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts @@ -14,7 +14,7 @@ export class StellarNonCustodialSigner extends NonCustodialSigner { super(config); const shadowSigner = getShadowSigner(walletAddress); - if (shadowSigner != null) { + if (shadowSigner != null && config.shadowSigner?.enabled !== false) { this.shadowSigner = new StellarExternalWalletSigner( this.getShadowSignerConfig(shadowSigner, walletAddress) ); diff --git a/packages/wallets/src/utils/shadow-signer.ts b/packages/wallets/src/utils/shadow-signer.ts index 1e9564493..2657ec07f 100644 --- a/packages/wallets/src/utils/shadow-signer.ts +++ b/packages/wallets/src/utils/shadow-signer.ts @@ -1,7 +1,7 @@ import { encode as encodeBase58 } from "bs58"; import { StrKey } from "@stellar/stellar-sdk"; import type { Chain } from "../chains/chains"; -import type { RegisterSignerParams } from "../api/types"; +import type { BaseExternalWalletSignerConfig } from "@crossmint/common-sdk-base"; const SHADOW_SIGNER_STORAGE_KEY = "crossmint_shadow_signer"; const SHADOW_SIGNER_DB_NAME = "crossmint_shadow_keys"; @@ -14,8 +14,8 @@ export type ShadowSignerData = { createdAt: number; }; -export type ShadowSignerResult = { - shadowSigner: RegisterSignerParams; +export type ShadowSignerResult = { + shadowSigner: BaseExternalWalletSignerConfig; publicKey: string; privateKey: CryptoKey; }; @@ -34,7 +34,7 @@ async function openDB(): Promise { }); } -export async function generateShadowSigner(chain: Chain): Promise { +export async function generateShadowSigner(chain: C): Promise> { if (chain === "solana" || chain === "stellar") { const keyPair = (await window.crypto.subtle.generateKey( { @@ -58,7 +58,10 @@ export async function generateShadowSigner(chain: Chain): Promise => { - if (signer.type === "passkey") { - if (signer.id == null) { - return { signer: await this.createPasskeySigner(signer) }; - } - return { signer }; - } - return { signer: this.getSignerLocator(signer) }; - } - ) ?? [] + const { delegatedSigners, shadowSignerPublicKey, shadowSignerPrivateKey } = await this.buildDelegatedSigners( + adminSignerConfig, + args ); - const shadowSignerEnabled = this.isShadowSignerEnabled(args.chain, args.signer); - const { - delegatedSigners: updatedDelegatedSigners, - shadowSignerPublicKey, - shadowSignerPrivateKey, - } = await this.addShadowSignerToDelegatedSignersIfNeeded(args, delegatedSigners, shadowSignerEnabled); - delegatedSigners = updatedDelegatedSigners; - const tempArgs = { ...args, signer: adminSignerConfig }; this.mutateSignerFromCustomAuth(tempArgs, true); adminSignerConfig = tempArgs.signer; @@ -147,12 +130,7 @@ export class WalletFactory { throw new WalletCreationError(JSON.stringify(walletResponse)); } - if ( - !this.apiClient.isServerSide && - shadowSignerEnabled && - shadowSignerPublicKey != null && - shadowSignerPrivateKey != null - ) { + if (shadowSignerPublicKey != null && shadowSignerPrivateKey != null) { await storeShadowSigner(walletResponse.address, args.chain, shadowSignerPublicKey, shadowSignerPrivateKey); } @@ -489,32 +467,72 @@ export class WalletFactory { return false; } - private isShadowSignerEnabled(chain: Chain, signer: SignerConfigForChain): boolean { - if ((chain === "solana" || chain === "stellar") && (signer.type === "email" || signer.type === "phone")) { - return signer.shadowSigner?.enabled !== false; - } - return false; + private isShadowSignerEnabled( + chain: C, + adminSigner: SignerConfigForChain, + delegatedSigners: Array> = [] + ): boolean { + const ncSigners = [adminSigner, ...delegatedSigners].filter( + (signer) => signer.type === "email" || signer.type === "phone" + ) as Array; + return ( + !this.apiClient.isServerSide && + (chain === "solana" || chain === "stellar") && + ncSigners.length > 0 && + ncSigners.some((signer) => signer.shadowSigner?.enabled !== false) + ); + } + + private async buildDelegatedSigners( + adminSigner: SignerConfigForChain, + args: WalletCreateArgs + ): Promise<{ + delegatedSigners: Array; + shadowSignerPublicKey: string | null; + shadowSignerPrivateKey: CryptoKey | null; + }> { + const { + delegatedSigners: updatedDelegatedSigners, + shadowSignerPublicKey, + shadowSignerPrivateKey, + } = await this.addShadowSignerToDelegatedSignersIfNeeded( + args, + adminSigner, + args.onCreateConfig?.delegatedSigners + ); + + const delegatedSigners = await Promise.all( + updatedDelegatedSigners?.map( + async (signer): Promise => { + if (signer.type === "passkey") { + if (signer.id == null) { + return { signer: await this.createPasskeySigner(signer) }; + } + return { signer }; + } + return { signer: this.getSignerLocator(signer) }; + } + ) ?? [] + ); + + return { delegatedSigners, shadowSignerPublicKey, shadowSignerPrivateKey }; } private async addShadowSignerToDelegatedSignersIfNeeded( args: WalletCreateArgs, - delegatedSigners: Array, - shadowSignerEnabled: boolean + adminSigner: SignerConfigForChain, + delegatedSigners?: Array> ): Promise<{ - delegatedSigners: Array; + delegatedSigners: Array> | undefined; shadowSignerPublicKey: string | null; shadowSignerPrivateKey: CryptoKey | null; }> { - if ( - !this.apiClient.isServerSide && - shadowSignerEnabled && - (args.signer.type === "email" || args.signer.type === "phone") - ) { + if (this.isShadowSignerEnabled(args.chain, adminSigner, delegatedSigners)) { try { const { shadowSigner, publicKey, privateKey } = await generateShadowSigner(args.chain); return { - delegatedSigners: [...delegatedSigners, shadowSigner], + delegatedSigners: [...(delegatedSigners ?? []), shadowSigner as SignerConfigForChain], shadowSignerPublicKey: publicKey, shadowSignerPrivateKey: privateKey, }; From 16f5f1ba4c8585e9d20b1ea61381e313a1f3639e Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 23 Oct 2025 15:46:11 -0400 Subject: [PATCH 47/82] abstract signature --- .../src/signers/evm-external-wallet.ts | 22 +++------- .../src/signers/external-wallet-signer.ts | 25 +++++++++++ .../signers/non-custodial/ncs-evm-signer.ts | 11 ++++- .../src/signers/non-custodial/ncs-signer.ts | 41 ++++++++++++++++++- .../non-custodial/ncs-solana-signer.ts | 13 ++---- .../non-custodial/ncs-stellar-signer.ts | 15 ++----- .../src/signers/solana-external-wallet.ts | 22 +++------- .../src/signers/stellar-external-wallet.ts | 22 +++------- 8 files changed, 98 insertions(+), 73 deletions(-) create mode 100644 packages/wallets/src/signers/external-wallet-signer.ts diff --git a/packages/wallets/src/signers/evm-external-wallet.ts b/packages/wallets/src/signers/evm-external-wallet.ts index e44a85a76..601619a17 100644 --- a/packages/wallets/src/signers/evm-external-wallet.ts +++ b/packages/wallets/src/signers/evm-external-wallet.ts @@ -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 { provider?: GenericEIP1193Provider | ViemEIP1193Provider; viemAccount?: Account; - constructor(private config: ExternalWalletInternalSignerConfig) { - if (config.address == null) { - throw new Error("Please provide an address for the External Wallet Signer"); - } - this._address = config.address; + constructor(config: ExternalWalletInternalSignerConfig) { + 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({ diff --git a/packages/wallets/src/signers/external-wallet-signer.ts b/packages/wallets/src/signers/external-wallet-signer.ts new file mode 100644 index 000000000..f471ee422 --- /dev/null +++ b/packages/wallets/src/signers/external-wallet-signer.ts @@ -0,0 +1,25 @@ +import type { ExternalWalletInternalSignerConfig, Signer } from "./types"; +import type { Chain } from "../chains/chains"; + +export abstract class ExternalWalletSigner implements Signer { + type = "external-wallet" as const; + protected _address: string; + + constructor(protected config: ExternalWalletInternalSignerConfig) { + 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 }>; +} diff --git a/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts index ecb5ab480..f96414e10 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts @@ -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) { @@ -64,4 +69,8 @@ export class EVMNonCustodialSigner extends NonCustodialSigner { ); } } + + protected getShadowSignerConfig(): ExternalWalletInternalSignerConfig { + throw new Error("Shadow signer not implemented for EVM chains"); + } } diff --git a/packages/wallets/src/signers/non-custodial/ncs-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-signer.ts index 35f92a3f4..5767ebe2f 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-signer.ts @@ -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, ShadowSignerData } from "@/utils/shadow-signer"; +import { Chain } from "@/chains/chains"; +import { ExternalWalletSigner } from "../external-wallet-signer"; export abstract class NonCustodialSigner implements Signer { public readonly type: "email" | "phone"; @@ -13,6 +22,7 @@ export abstract class NonCustodialSigner implements Signer { reject: (error: Error) => void; } | null = null; private _initializationPromise: Promise | null = null; + protected shadowSigner: ExternalWalletSigner | null = null; constructor(protected config: EmailInternalSignerConfig | PhoneInternalSignerConfig) { this.initialize(); @@ -20,10 +30,16 @@ export abstract class NonCustodialSigner implements Signer { } 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; } @@ -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) { @@ -267,6 +287,25 @@ export abstract class NonCustodialSigner implements Signer { this._authPromise?.reject(error); throw error; } + + protected abstract getShadowSignerConfig( + shadowSigner: ShadowSignerData, + walletAddress: string + ): ExternalWalletInternalSignerConfig; + + protected initializeShadowSigner( + walletAddress: string, + ExternalWalletSignerClass: new (config: ExternalWalletInternalSignerConfig) => ExternalWalletSigner + ) { + 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 + ); + } + } + } } export const DEFAULT_EVENT_OPTIONS = { diff --git a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts index 32e5e2b3c..d3e0ebab5 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts @@ -6,19 +6,14 @@ import type { PhoneInternalSignerConfig, } from "../types"; import { NonCustodialSigner, DEFAULT_EVENT_OPTIONS } from "./ncs-signer"; -import { getShadowSigner, getShadowSignerPrivateKey, type ShadowSignerData } from "@/utils/shadow-signer"; +import { getShadowSignerPrivateKey, type ShadowSignerData } from "@/utils/shadow-signer"; import { SolanaExternalWalletSigner } from "../solana-external-wallet"; import type { SolanaChain } from "@/chains/chains"; export class SolanaNonCustodialSigner extends NonCustodialSigner { - private shadowSigner: SolanaExternalWalletSigner | null = null; - constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig, walletAddress: string) { super(config); - const shadowSigner = getShadowSigner(walletAddress); - if (shadowSigner != null) { - this.shadowSigner = new SolanaExternalWalletSigner(this.getShadowSignerConfig(shadowSigner, walletAddress)); - } + this.initializeShadowSigner(walletAddress, SolanaExternalWalletSigner); } async signMessage() { @@ -77,14 +72,14 @@ export class SolanaNonCustodialSigner extends NonCustodialSigner { } } - private getShadowSignerConfig( + protected getShadowSignerConfig( shadowData: ShadowSignerData, walletAddress: string ): ExternalWalletInternalSignerConfig { return { type: "external-wallet", address: shadowData.publicKey, - locator: `external-wallet-${shadowData.publicKey}`, + locator: `external-wallet:${shadowData.publicKey}`, onSignTransaction: async (transaction) => { const privateKey = await getShadowSignerPrivateKey(walletAddress); if (!privateKey) { diff --git a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts index b8c31c72f..8bce03be9 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts @@ -1,4 +1,4 @@ -import { getShadowSigner, getShadowSignerPrivateKey, type ShadowSignerData } from "@/utils/shadow-signer"; +import { getShadowSignerPrivateKey, type ShadowSignerData } from "@/utils/shadow-signer"; import type { EmailInternalSignerConfig, ExternalWalletInternalSignerConfig, @@ -9,16 +9,9 @@ import { StellarExternalWalletSigner } from "../stellar-external-wallet"; import type { StellarChain } from "@/chains/chains"; export class StellarNonCustodialSigner extends NonCustodialSigner { - private shadowSigner: StellarExternalWalletSigner | null = null; constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig, walletAddress: string) { super(config); - - const shadowSigner = getShadowSigner(walletAddress); - if (shadowSigner != null && config.shadowSigner?.enabled !== false) { - this.shadowSigner = new StellarExternalWalletSigner( - this.getShadowSignerConfig(shadowSigner, walletAddress) - ); - } + this.initializeShadowSigner(walletAddress, StellarExternalWalletSigner); } async signMessage() { @@ -77,14 +70,14 @@ export class StellarNonCustodialSigner extends NonCustodialSigner { } } - private getShadowSignerConfig( + protected getShadowSignerConfig( shadowData: ShadowSignerData, walletAddress: string ): ExternalWalletInternalSignerConfig { return { type: "external-wallet", address: shadowData.publicKey, - locator: `external-wallet-${shadowData.publicKey}`, + locator: `external-wallet:${shadowData.publicKey}`, onSignStellarTransaction: async (payload) => { const privateKey = await getShadowSignerPrivateKey(walletAddress); if (!privateKey) { diff --git a/packages/wallets/src/signers/solana-external-wallet.ts b/packages/wallets/src/signers/solana-external-wallet.ts index 5a65af5f1..8ae3f6fb8 100644 --- a/packages/wallets/src/signers/solana-external-wallet.ts +++ b/packages/wallets/src/signers/solana-external-wallet.ts @@ -1,30 +1,18 @@ import { PublicKey, VersionedTransaction } from "@solana/web3.js"; import base58 from "bs58"; -import type { ExternalWalletInternalSignerConfig, Signer } from "./types"; +import type { ExternalWalletInternalSignerConfig } from "./types"; import { TransactionFailedError } from "../utils/errors"; import type { SolanaChain } from "@/chains/chains"; +import { ExternalWalletSigner } from "./external-wallet-signer"; -export class SolanaExternalWalletSigner implements Signer { - type = "external-wallet" as const; - private _address: string; +export class SolanaExternalWalletSigner extends ExternalWalletSigner { onSignTransaction?: (transaction: VersionedTransaction) => Promise; - constructor(private config: ExternalWalletInternalSignerConfig) { - if (config.address == null) { - throw new Error("Please provide an address for the External Wallet Signer"); - } - this._address = config.address; + constructor(config: ExternalWalletInternalSignerConfig) { + super(config); this.onSignTransaction = config.onSignTransaction; } - address() { - return this._address; - } - - locator() { - return this.config.locator; - } - async signMessage() { return await Promise.reject(new Error("signMessage method not implemented for solana external wallet signer")); } diff --git a/packages/wallets/src/signers/stellar-external-wallet.ts b/packages/wallets/src/signers/stellar-external-wallet.ts index 4484cea71..9b79c3ac3 100644 --- a/packages/wallets/src/signers/stellar-external-wallet.ts +++ b/packages/wallets/src/signers/stellar-external-wallet.ts @@ -1,27 +1,15 @@ -import type { ExternalWalletInternalSignerConfig, Signer } from "./types"; +import type { ExternalWalletInternalSignerConfig } from "./types"; import type { StellarChain } from "@/chains/chains"; +import { ExternalWalletSigner } from "./external-wallet-signer"; -export class StellarExternalWalletSigner implements Signer { - type = "external-wallet" as const; - private _address: string; +export class StellarExternalWalletSigner extends ExternalWalletSigner { onSignStellarTransaction?: (payload: string) => Promise; - constructor(private config: ExternalWalletInternalSignerConfig) { - if (config.address == null) { - throw new Error("Please provide an address for the External Wallet Signer"); - } - this._address = config.address; + constructor(config: ExternalWalletInternalSignerConfig) { + super(config); this.onSignStellarTransaction = config.onSignStellarTransaction; } - address() { - return this._address; - } - - locator() { - return this.config.locator; - } - async signMessage() { return await Promise.reject(new Error("signMessage method not implemented for stellar external wallet signer")); } From bc4908079b396d2e7de96f4ee751255375744a76 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 23 Oct 2025 15:53:56 -0400 Subject: [PATCH 48/82] remove unnecessary change --- packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts index d3e0ebab5..3ae9c64a2 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts @@ -41,7 +41,7 @@ export class SolanaNonCustodialSigner extends NonCustodialSigner { }, data: { keyType: "ed25519", - bytes: base58.encode(new Uint8Array(messageData)), + bytes: base58.encode(messageData), encoding: "base58", }, }, From 28682a3bc08ce45148306c7bfafe27661234aeae Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 23 Oct 2025 16:00:03 -0400 Subject: [PATCH 49/82] restructure folder --- packages/wallets/src/signers/non-custodial/ncs-signer.ts | 6 +++--- .../wallets/src/signers/non-custodial/ncs-solana-signer.ts | 2 +- .../wallets/src/signers/non-custodial/ncs-stellar-signer.ts | 2 +- .../shadow-signer.ts => signers/shadow-signer/index.ts} | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) rename packages/wallets/src/{utils/shadow-signer.ts => signers/shadow-signer/index.ts} (98%) diff --git a/packages/wallets/src/signers/non-custodial/ncs-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-signer.ts index 5767ebe2f..ede884bb4 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-signer.ts @@ -9,9 +9,9 @@ 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, ShadowSignerData } from "@/utils/shadow-signer"; -import { Chain } from "@/chains/chains"; -import { ExternalWalletSigner } from "../external-wallet-signer"; +import { getShadowSigner, hasShadowSigner } from "@/signers/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"; diff --git a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts index 3ae9c64a2..0c9ba6004 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts @@ -6,7 +6,7 @@ import type { PhoneInternalSignerConfig, } from "../types"; import { NonCustodialSigner, DEFAULT_EVENT_OPTIONS } from "./ncs-signer"; -import { getShadowSignerPrivateKey, type ShadowSignerData } from "@/utils/shadow-signer"; +import { getShadowSignerPrivateKey } from "@/signers/shadow-signer"; import { SolanaExternalWalletSigner } from "../solana-external-wallet"; import type { SolanaChain } from "@/chains/chains"; diff --git a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts index 8bce03be9..a1d4bb603 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts @@ -1,4 +1,4 @@ -import { getShadowSignerPrivateKey, type ShadowSignerData } from "@/utils/shadow-signer"; +import { getShadowSignerPrivateKey } from "@/signers/shadow-signer"; import type { EmailInternalSignerConfig, ExternalWalletInternalSignerConfig, diff --git a/packages/wallets/src/utils/shadow-signer.ts b/packages/wallets/src/signers/shadow-signer/index.ts similarity index 98% rename from packages/wallets/src/utils/shadow-signer.ts rename to packages/wallets/src/signers/shadow-signer/index.ts index 2657ec07f..38c2b04a2 100644 --- a/packages/wallets/src/utils/shadow-signer.ts +++ b/packages/wallets/src/signers/shadow-signer/index.ts @@ -1,6 +1,6 @@ import { encode as encodeBase58 } from "bs58"; import { StrKey } from "@stellar/stellar-sdk"; -import type { Chain } from "../chains/chains"; +import type { Chain } from "@/chains/chains"; import type { BaseExternalWalletSignerConfig } from "@crossmint/common-sdk-base"; const SHADOW_SIGNER_STORAGE_KEY = "crossmint_shadow_signer"; From af821064ec08c467b31cf6ad3edc0410048e9e8e Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 23 Oct 2025 16:13:02 -0400 Subject: [PATCH 50/82] improve has shadow signer condition --- packages/wallets/src/signers/non-custodial/ncs-signer.ts | 2 +- packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts | 2 +- packages/wallets/src/signers/shadow-signer/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/wallets/src/signers/non-custodial/ncs-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-signer.ts index ede884bb4..5cc5c6bd2 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-signer.ts @@ -9,7 +9,7 @@ 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 } from "@/signers/shadow-signer"; +import { getShadowSigner, hasShadowSigner, type ShadowSignerData } from "@/signers/shadow-signer"; import type { Chain } from "@/chains/chains"; import type { ExternalWalletSigner } from "../external-wallet-signer"; diff --git a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts index 0c9ba6004..25da7d800 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts @@ -6,7 +6,7 @@ import type { PhoneInternalSignerConfig, } from "../types"; import { NonCustodialSigner, DEFAULT_EVENT_OPTIONS } from "./ncs-signer"; -import { getShadowSignerPrivateKey } from "@/signers/shadow-signer"; +import { getShadowSignerPrivateKey, type ShadowSignerData } from "@/signers/shadow-signer"; import { SolanaExternalWalletSigner } from "../solana-external-wallet"; import type { SolanaChain } from "@/chains/chains"; diff --git a/packages/wallets/src/signers/shadow-signer/index.ts b/packages/wallets/src/signers/shadow-signer/index.ts index 38c2b04a2..165d06a20 100644 --- a/packages/wallets/src/signers/shadow-signer/index.ts +++ b/packages/wallets/src/signers/shadow-signer/index.ts @@ -130,5 +130,5 @@ export async function getShadowSignerPrivateKey(walletAddress: string): Promise< } export function hasShadowSigner(walletAddress: string): boolean { - return getShadowSigner(walletAddress) !== null; + return getShadowSigner(walletAddress) !== null && getShadowSignerPrivateKey(walletAddress) !== null; } From 3b85db97ada553af226817cbd554da9183f8c373 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 23 Oct 2025 16:57:20 -0400 Subject: [PATCH 51/82] fix import --- packages/wallets/src/wallets/wallet-factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 6b801b3c3..a0690c8a5 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -26,7 +26,7 @@ import { Wallet } from "./wallet"; import { assembleSigner } from "../signers"; import type { DelegatedSigner, WalletArgsFor, WalletCreateArgs, WalletOptions } from "./types"; import { compareSignerConfigs } from "../utils/signer-validation"; -import { generateShadowSigner, storeShadowSigner } from "../utils/shadow-signer"; +import { generateShadowSigner, storeShadowSigner } from "@/signers/shadow-signer"; const DELEGATED_SIGNER_MISMATCH_ERROR = "When 'delegatedSigners' is provided to a method that may fetch an existing wallet, each specified delegated signer must exist in that wallet's configuration."; From adaee178eeac208dd210d9cc4cd05eb41b0c6693 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 23 Oct 2025 17:02:44 -0400 Subject: [PATCH 52/82] fix import --- .../wallets/src/signers/non-custodial/ncs-stellar-signer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts index a1d4bb603..fcc63754c 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts @@ -1,4 +1,4 @@ -import { getShadowSignerPrivateKey } from "@/signers/shadow-signer"; +import { getShadowSignerPrivateKey, type ShadowSignerData } from "@/signers/shadow-signer"; import type { EmailInternalSignerConfig, ExternalWalletInternalSignerConfig, From 89be46de9f880c569269903e6f7bac2889aa35da Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 23 Oct 2025 17:08:44 -0400 Subject: [PATCH 53/82] move shadow signers --- .../shadow-signers}/encodeEd25519PublicKey.ts | 0 .../shadow-signer.ts => signers/shadow-signers/index.ts} | 4 ++-- .../shadow-signers}/shadow-signer-storage-browser.ts | 2 +- .../shadow-signers}/shadow-signer-storage-rn.ts | 2 +- packages/wallets/src/wallets/wallet-factory.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename packages/wallets/src/{utils => signers/shadow-signers}/encodeEd25519PublicKey.ts (100%) rename packages/wallets/src/{utils/shadow-signer.ts => signers/shadow-signers/index.ts} (97%) rename packages/wallets/src/{utils => signers/shadow-signers}/shadow-signer-storage-browser.ts (97%) rename packages/wallets/src/{utils => signers/shadow-signers}/shadow-signer-storage-rn.ts (97%) diff --git a/packages/wallets/src/utils/encodeEd25519PublicKey.ts b/packages/wallets/src/signers/shadow-signers/encodeEd25519PublicKey.ts similarity index 100% rename from packages/wallets/src/utils/encodeEd25519PublicKey.ts rename to packages/wallets/src/signers/shadow-signers/encodeEd25519PublicKey.ts diff --git a/packages/wallets/src/utils/shadow-signer.ts b/packages/wallets/src/signers/shadow-signers/index.ts similarity index 97% rename from packages/wallets/src/utils/shadow-signer.ts rename to packages/wallets/src/signers/shadow-signers/index.ts index 60ded64c1..613ce4aa0 100644 --- a/packages/wallets/src/utils/shadow-signer.ts +++ b/packages/wallets/src/signers/shadow-signers/index.ts @@ -1,6 +1,6 @@ import { encode as encodeBase58 } from "bs58"; -import type { Chain } from "../chains/chains"; -import type { RegisterSignerParams } from "../api/types"; +import type { Chain } from "@/chains/chains"; +import type { RegisterSignerParams } from "@/api/types"; import { encodeEd25519PublicKey } from "./encodeEd25519PublicKey"; import { BrowserShadowSignerStorage } from "./shadow-signer-storage-browser"; import { ReactNativeShadowSignerStorage } from "./shadow-signer-storage-rn"; diff --git a/packages/wallets/src/utils/shadow-signer-storage-browser.ts b/packages/wallets/src/signers/shadow-signers/shadow-signer-storage-browser.ts similarity index 97% rename from packages/wallets/src/utils/shadow-signer-storage-browser.ts rename to packages/wallets/src/signers/shadow-signers/shadow-signer-storage-browser.ts index 537135b46..d00c80e12 100644 --- a/packages/wallets/src/utils/shadow-signer-storage-browser.ts +++ b/packages/wallets/src/signers/shadow-signers/shadow-signer-storage-browser.ts @@ -1,4 +1,4 @@ -import type { ShadowSignerData, ShadowSignerStorage } from "./shadow-signer"; +import type { ShadowSignerData, ShadowSignerStorage } from "."; export class BrowserShadowSignerStorage implements ShadowSignerStorage { private readonly SHADOW_SIGNER_DB_NAME = "crossmint_shadow_keys"; diff --git a/packages/wallets/src/utils/shadow-signer-storage-rn.ts b/packages/wallets/src/signers/shadow-signers/shadow-signer-storage-rn.ts similarity index 97% rename from packages/wallets/src/utils/shadow-signer-storage-rn.ts rename to packages/wallets/src/signers/shadow-signers/shadow-signer-storage-rn.ts index 957991bc0..465d8c070 100644 --- a/packages/wallets/src/utils/shadow-signer-storage-rn.ts +++ b/packages/wallets/src/signers/shadow-signers/shadow-signer-storage-rn.ts @@ -1,5 +1,5 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; -import type { ShadowSignerData, ShadowSignerStorage } from "./shadow-signer"; +import type { ShadowSignerData, ShadowSignerStorage } from "."; export class ReactNativeShadowSignerStorage implements ShadowSignerStorage { private readonly SHADOW_SIGNER_DB_NAME = "crossmint_shadow_keys"; diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index bccf6fc32..d203d8ccc 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -32,7 +32,7 @@ import { storeShadowSigner, getShadowSigner, getShadowSignerPrivateKey, -} from "../utils/shadow-signer"; +} from "../signers/shadow-signers"; import { PublicKey } from "@solana/web3.js"; const DELEGATED_SIGNER_MISMATCH_ERROR = From 740f37e15160bd1405ed377051f6595bf6e17540 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 23 Oct 2025 17:09:32 -0400 Subject: [PATCH 54/82] fix naming --- .../{shadow-signers => shadow-signer}/encodeEd25519PublicKey.ts | 0 .../src/signers/{shadow-signers => shadow-signer}/index.ts | 0 .../shadow-signer-storage-browser.ts | 0 .../{shadow-signers => shadow-signer}/shadow-signer-storage-rn.ts | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename packages/wallets/src/signers/{shadow-signers => shadow-signer}/encodeEd25519PublicKey.ts (100%) rename packages/wallets/src/signers/{shadow-signers => shadow-signer}/index.ts (100%) rename packages/wallets/src/signers/{shadow-signers => shadow-signer}/shadow-signer-storage-browser.ts (100%) rename packages/wallets/src/signers/{shadow-signers => shadow-signer}/shadow-signer-storage-rn.ts (100%) diff --git a/packages/wallets/src/signers/shadow-signers/encodeEd25519PublicKey.ts b/packages/wallets/src/signers/shadow-signer/encodeEd25519PublicKey.ts similarity index 100% rename from packages/wallets/src/signers/shadow-signers/encodeEd25519PublicKey.ts rename to packages/wallets/src/signers/shadow-signer/encodeEd25519PublicKey.ts diff --git a/packages/wallets/src/signers/shadow-signers/index.ts b/packages/wallets/src/signers/shadow-signer/index.ts similarity index 100% rename from packages/wallets/src/signers/shadow-signers/index.ts rename to packages/wallets/src/signers/shadow-signer/index.ts diff --git a/packages/wallets/src/signers/shadow-signers/shadow-signer-storage-browser.ts b/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts similarity index 100% rename from packages/wallets/src/signers/shadow-signers/shadow-signer-storage-browser.ts rename to packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts diff --git a/packages/wallets/src/signers/shadow-signers/shadow-signer-storage-rn.ts b/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-rn.ts similarity index 100% rename from packages/wallets/src/signers/shadow-signers/shadow-signer-storage-rn.ts rename to packages/wallets/src/signers/shadow-signer/shadow-signer-storage-rn.ts From 7a345c5f6a2abbc04b23ba633523eba4de3fbb01 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 23 Oct 2025 17:25:45 -0400 Subject: [PATCH 55/82] fix vitest test in wallets package --- packages/wallets/vitest.config.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 packages/wallets/vitest.config.ts diff --git a/packages/wallets/vitest.config.ts b/packages/wallets/vitest.config.ts new file mode 100644 index 000000000..6792b687e --- /dev/null +++ b/packages/wallets/vitest.config.ts @@ -0,0 +1,10 @@ +import { resolve } from "path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + // This is needed because we are using the @ symbol to import from the src folder. + // Otherwise, Vitest will yell at us. + alias: [{ find: "@", replacement: resolve(__dirname, "./src") }], + }, +}); From 0df29c4350e748b91460a17dd63811685ec518fa Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 23:56:27 +0000 Subject: [PATCH 56/82] refactor: address PR feedback from Alberto - 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 --- .../signers/non-custodial/ncs-evm-signer.ts | 2 +- .../src/signers/non-custodial/ncs-signer.ts | 4 ++-- .../signers/non-custodial/ncs-solana-signer.ts | 6 +++--- .../non-custodial/ncs-stellar-signer.ts | 6 +++--- packages/wallets/src/wallets/wallet-factory.ts | 18 ++++++++++++------ 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts index f96414e10..1890397f0 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts @@ -6,7 +6,7 @@ import type { 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"; +import type { EVMChain } from "../../chains/chains"; export class EVMNonCustodialSigner extends NonCustodialSigner { constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig) { diff --git a/packages/wallets/src/signers/non-custodial/ncs-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-signer.ts index 5cc5c6bd2..fb6083e8e 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-signer.ts @@ -9,8 +9,8 @@ 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 "@/signers/shadow-signer"; -import type { Chain } from "@/chains/chains"; +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 { diff --git a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts index 25da7d800..0c3b343f8 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts @@ -6,9 +6,9 @@ import type { PhoneInternalSignerConfig, } from "../types"; import { NonCustodialSigner, DEFAULT_EVENT_OPTIONS } from "./ncs-signer"; -import { getShadowSignerPrivateKey, type ShadowSignerData } from "@/signers/shadow-signer"; +import { getShadowSignerPrivateKey, type ShadowSignerData } from "../shadow-signer"; import { SolanaExternalWalletSigner } from "../solana-external-wallet"; -import type { SolanaChain } from "@/chains/chains"; +import type { SolanaChain } from "../../chains/chains"; export class SolanaNonCustodialSigner extends NonCustodialSigner { constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig, walletAddress: string) { @@ -82,7 +82,7 @@ export class SolanaNonCustodialSigner extends NonCustodialSigner { locator: `external-wallet:${shadowData.publicKey}`, onSignTransaction: async (transaction) => { const privateKey = await getShadowSignerPrivateKey(walletAddress); - if (!privateKey) { + if (privateKey == null) { throw new Error("Shadow signer private key not found"); } diff --git a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts index fcc63754c..b75dc98fc 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts @@ -1,4 +1,4 @@ -import { getShadowSignerPrivateKey, type ShadowSignerData } from "@/signers/shadow-signer"; +import { getShadowSignerPrivateKey, type ShadowSignerData } from "../shadow-signer"; import type { EmailInternalSignerConfig, ExternalWalletInternalSignerConfig, @@ -6,7 +6,7 @@ import type { } from "../types"; import { DEFAULT_EVENT_OPTIONS, NonCustodialSigner } from "./ncs-signer"; import { StellarExternalWalletSigner } from "../stellar-external-wallet"; -import type { StellarChain } from "@/chains/chains"; +import type { StellarChain } from "../../chains/chains"; export class StellarNonCustodialSigner extends NonCustodialSigner { constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig, walletAddress: string) { @@ -80,7 +80,7 @@ export class StellarNonCustodialSigner extends NonCustodialSigner { locator: `external-wallet:${shadowData.publicKey}`, onSignStellarTransaction: async (payload) => { const privateKey = await getShadowSignerPrivateKey(walletAddress); - if (!privateKey) { + if (privateKey == null) { throw new Error("Shadow signer private key not found"); } diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index a0690c8a5..e2c53e364 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -26,7 +26,7 @@ import { Wallet } from "./wallet"; import { assembleSigner } from "../signers"; import type { DelegatedSigner, WalletArgsFor, WalletCreateArgs, WalletOptions } from "./types"; import { compareSignerConfigs } from "../utils/signer-validation"; -import { generateShadowSigner, storeShadowSigner } from "@/signers/shadow-signer"; +import { generateShadowSigner, storeShadowSigner } from "../signers/shadow-signer"; const DELEGATED_SIGNER_MISMATCH_ERROR = "When 'delegatedSigners' is provided to a method that may fetch an existing wallet, each specified delegated signer must exist in that wallet's configuration."; @@ -492,7 +492,7 @@ export class WalletFactory { shadowSignerPrivateKey: CryptoKey | null; }> { const { - delegatedSigners: updatedDelegatedSigners, + delegatedSigners, shadowSignerPublicKey, shadowSignerPrivateKey, } = await this.addShadowSignerToDelegatedSignersIfNeeded( @@ -501,8 +501,16 @@ export class WalletFactory { args.onCreateConfig?.delegatedSigners ); - const delegatedSigners = await Promise.all( - updatedDelegatedSigners?.map( + const registeredDelegatedSigners = await this.registerDelegatedSigners(delegatedSigners); + + return { delegatedSigners: registeredDelegatedSigners, shadowSignerPublicKey, shadowSignerPrivateKey }; + } + + private async registerDelegatedSigners( + delegatedSigners?: Array> + ): Promise> { + return await Promise.all( + delegatedSigners?.map( async (signer): Promise => { if (signer.type === "passkey") { if (signer.id == null) { @@ -514,8 +522,6 @@ export class WalletFactory { } ) ?? [] ); - - return { delegatedSigners, shadowSignerPublicKey, shadowSignerPrivateKey }; } private async addShadowSignerToDelegatedSignersIfNeeded( From 4d2b6844b0542485b2b8b19d209a5786781e1dd6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 23:58:29 +0000 Subject: [PATCH 57/82] fix: apply biome formatting Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet-factory.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index e2c53e364..c8464ef87 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -491,15 +491,12 @@ export class WalletFactory { shadowSignerPublicKey: string | null; shadowSignerPrivateKey: CryptoKey | null; }> { - const { - delegatedSigners, - shadowSignerPublicKey, - shadowSignerPrivateKey, - } = await this.addShadowSignerToDelegatedSignersIfNeeded( - args, - adminSigner, - args.onCreateConfig?.delegatedSigners - ); + const { delegatedSigners, shadowSignerPublicKey, shadowSignerPrivateKey } = + await this.addShadowSignerToDelegatedSignersIfNeeded( + args, + adminSigner, + args.onCreateConfig?.delegatedSigners + ); const registeredDelegatedSigners = await this.registerDelegatedSigners(delegatedSigners); From 87c6e61163c6f2d506764a8e6b34edcf5cca17a0 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Fri, 24 Oct 2025 10:36:47 -0400 Subject: [PATCH 58/82] move storage to rn package --- .../providers/CrossmintWalletBaseProvider.tsx | 7 ++ .../src/providers/CrossmintWalletProvider.tsx | 4 + .../src/utils/ShadowSignerStorage.ts | 77 +++++++++++++++++++ packages/wallets/src/index.ts | 3 + packages/wallets/src/signers/index.ts | 10 ++- .../signers/non-custodial/ncs-evm-signer.ts | 8 +- .../src/signers/non-custodial/ncs-signer.ts | 12 ++- .../non-custodial/ncs-solana-signer.ts | 13 +++- .../non-custodial/ncs-stellar-signer.ts | 13 +++- .../src/signers/shadow-signer/index.ts | 37 +++++---- .../shadow-signer/shadow-signer-storage-rn.ts | 77 ------------------- packages/wallets/src/wallets/types.ts | 2 + .../wallets/src/wallets/wallet-factory.ts | 15 +++- 13 files changed, 168 insertions(+), 110 deletions(-) create mode 100644 packages/client/ui/react-native/src/utils/ShadowSignerStorage.ts delete mode 100644 packages/wallets/src/signers/shadow-signer/shadow-signer-storage-rn.ts diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index 4c5c26617..fc57c0610 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -14,6 +14,7 @@ import type { signerInboundEvents, signerOutboundEvents } from "@crossmint/clien import { useCrossmint } from "@/hooks"; import type { CreateOnLogin } from "@/types"; import cloneDeep from "lodash.clonedeep"; +import type { ShadowSignerStorage } from "@crossmint/wallets-sdk"; export type CrossmintWalletBaseContext = { wallet: Wallet | undefined; @@ -45,6 +46,7 @@ export interface CrossmintWalletBaseProviderProps { onAuthRequired?: EmailSignerConfig["onAuthRequired"] | PhoneSignerConfig["onAuthRequired"]; clientTEEConnection?: () => HandshakeParent; initializeWebView?: () => Promise; + shadowSignerStorage?: ShadowSignerStorage; } export function CrossmintWalletBaseProvider({ @@ -54,6 +56,7 @@ export function CrossmintWalletBaseProvider({ onAuthRequired, clientTEEConnection, initializeWebView, + shadowSignerStorage, }: CrossmintWalletBaseProviderProps) { const { crossmint, experimental_customAuth } = useCrossmint( "CrossmintWalletBaseProvider must be used within CrossmintProvider" @@ -152,6 +155,7 @@ export function CrossmintWalletBaseProvider({ onWalletCreationStart: _onWalletCreationStart ?? callbacks?.onWalletCreationStart, onTransactionStart: _onTransactionStart ?? callbacks?.onTransactionStart, }, + shadowSignerStorage, }, }); setWallet(wallet); @@ -173,6 +177,7 @@ export function CrossmintWalletBaseProvider({ initializeWebViewIfNeeded, clientTEEConnection, callbacks, + shadowSignerStorage, ] ); @@ -195,6 +200,7 @@ export function CrossmintWalletBaseProvider({ options: { clientTEEConnection: clientTEEConnection?.(), experimental_callbacks: callbacks, + shadowSignerStorage, }, }); return wallet; @@ -210,6 +216,7 @@ export function CrossmintWalletBaseProvider({ initializeWebViewIfNeeded, clientTEEConnection, callbacks, + shadowSignerStorage, ] ); diff --git a/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx b/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx index 2bb65fb87..267d8523a 100644 --- a/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx +++ b/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx @@ -6,6 +6,7 @@ import { environmentUrlConfig, signerInboundEvents, signerOutboundEvents } from import { validateAPIKey } from "@crossmint/common-sdk-base"; import { type CreateOnLogin, CrossmintWalletBaseProvider } from "@crossmint/client-sdk-react-base"; import { useCrossmint } from "@/hooks"; +import { ReactNativeShadowSignerStorage } from "@/utils/ShadowSignerStorage"; const throwNotAvailable = (functionName: string) => () => { throw new Error(`${functionName} is not available. Make sure you're using an email signer wallet.`); @@ -51,6 +52,8 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: return environmentUrlConfig[parsedAPIKey.environment]; }, [parsedAPIKey.environment]); + const shadowSignerStorage = useMemo(() => new ReactNativeShadowSignerStorage(), []); + const webviewRef = useRef(null); const webViewParentRef = useRef | null>( null @@ -193,6 +196,7 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: clientTEEConnection={getClientTEEConnection} initializeWebView={initializeWebView} callbacks={callbacks} + shadowSignerStorage={shadowSignerStorage} > {children} diff --git a/packages/client/ui/react-native/src/utils/ShadowSignerStorage.ts b/packages/client/ui/react-native/src/utils/ShadowSignerStorage.ts new file mode 100644 index 000000000..7670638d4 --- /dev/null +++ b/packages/client/ui/react-native/src/utils/ShadowSignerStorage.ts @@ -0,0 +1,77 @@ +import type { ShadowSignerStorage, ShadowSignerData } from "@crossmint/wallets-sdk"; +import { SecureStorage } from "./SecureStorage"; + +export class ReactNativeShadowSignerStorage implements ShadowSignerStorage { + private readonly SHADOW_SIGNER_STORAGE_KEY = "crossmint_shadow_signer"; + private secureStorage = new SecureStorage(); + + async storePrivateKey(walletAddress: string, privateKey: CryptoKey): Promise { + try { + const privateKeyBuffer = await window.crypto.subtle.exportKey("raw", privateKey); + const privateKeyBytes = new Uint8Array(privateKeyBuffer); + + const privateKeyBase64 = btoa(String.fromCharCode(...privateKeyBytes)); + + await this.secureStorage.set(`${this.SHADOW_SIGNER_STORAGE_KEY}_key_${walletAddress}`, privateKeyBase64); + } catch (error) { + console.error("Failed to store private key:", error); + throw error; + } + } + + async getPrivateKey(walletAddress: string): Promise { + try { + const stored = await this.secureStorage.get(`${this.SHADOW_SIGNER_STORAGE_KEY}_key_${walletAddress}`); + if (!stored) { + return null; + } + + const privateKeyBytes = Uint8Array.from(atob(stored), (c) => c.charCodeAt(0)); + + return await window.crypto.subtle.importKey( + "raw", + privateKeyBytes, + { + name: "Ed25519", + namedCurve: "Ed25519", + } as AlgorithmIdentifier, + false, + ["sign"] + ); + } catch (error) { + console.warn("Failed to retrieve private key from SecureStorage:", error); + return null; + } + } + + async removePrivateKey(walletAddress: string): Promise { + try { + await this.secureStorage.remove(`${this.SHADOW_SIGNER_STORAGE_KEY}_key_${walletAddress}`); + } catch (error) { + console.error("Failed to remove private key:", error); + throw error; + } + } + + async storeMetadata(walletAddress: string, data: ShadowSignerData): Promise { + try { + await this.secureStorage.set( + `${this.SHADOW_SIGNER_STORAGE_KEY}_meta_${walletAddress}`, + JSON.stringify(data) + ); + } catch (error) { + console.error("Failed to store metadata:", error); + throw error; + } + } + + async getMetadata(walletAddress: string): Promise { + try { + const stored = await this.secureStorage.get(`${this.SHADOW_SIGNER_STORAGE_KEY}_meta_${walletAddress}`); + return stored ? JSON.parse(stored) : null; + } catch (error) { + console.warn("Failed to retrieve metadata from SecureStorage:", error); + return null; + } + } +} diff --git a/packages/wallets/src/index.ts b/packages/wallets/src/index.ts index af67a1a78..6ae9ae4e2 100644 --- a/packages/wallets/src/index.ts +++ b/packages/wallets/src/index.ts @@ -4,6 +4,9 @@ export { createCrossmint, CrossmintWallets } from "./sdk"; // API export { ApiClient as WalletsApiClient } from "./api"; +// Types +export type { ShadowSignerStorage, ShadowSignerData } from "./signers/shadow-signer"; + // Wallets export { Wallet } from "./wallets/wallet"; export { SolanaWallet } from "./wallets/solana"; diff --git a/packages/wallets/src/signers/index.ts b/packages/wallets/src/signers/index.ts index 93058a3f9..204b9c899 100644 --- a/packages/wallets/src/signers/index.ts +++ b/packages/wallets/src/signers/index.ts @@ -7,22 +7,24 @@ import { SolanaApiKeySigner } from "./solana-api-key"; import type { Chain } from "../chains/chains"; import type { InternalSignerConfig, Signer } from "./types"; import { StellarExternalWalletSigner } from "./stellar-external-wallet"; +import type { ShadowSignerStorage } from "./shadow-signer"; export function assembleSigner( chain: C, config: InternalSignerConfig, - walletAddress: string + walletAddress: string, + shadowSignerStorage?: ShadowSignerStorage ): Signer { switch (config.type) { case "email": case "phone": if (chain === "solana") { - return new SolanaNonCustodialSigner(config, walletAddress); + return new SolanaNonCustodialSigner(config, walletAddress, shadowSignerStorage); } if (chain === "stellar") { - return new StellarNonCustodialSigner(config, walletAddress); + return new StellarNonCustodialSigner(config, walletAddress, shadowSignerStorage); } - return new EVMNonCustodialSigner(config); + return new EVMNonCustodialSigner(config, shadowSignerStorage); case "api-key": return chain === "solana" ? new SolanaApiKeySigner(config) : new EVMApiKeySigner(config); diff --git a/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts index f96414e10..d9327c631 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts @@ -7,10 +7,14 @@ 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"; +import type { ShadowSignerStorage } from "@/signers/shadow-signer"; export class EVMNonCustodialSigner extends NonCustodialSigner { - constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig) { - super(config); + constructor( + config: EmailInternalSignerConfig | PhoneInternalSignerConfig, + shadowSignerStorage?: ShadowSignerStorage + ) { + super(config, shadowSignerStorage); } async signMessage(message: string) { diff --git a/packages/wallets/src/signers/non-custodial/ncs-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-signer.ts index aab358e1a..04e6b60f1 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-signer.ts @@ -12,6 +12,7 @@ import type { SignerOutputEvent } from "@crossmint/client-signers"; import { getShadowSigner, hasShadowSigner, type ShadowSignerData } from "@/signers/shadow-signer"; import type { Chain } from "@/chains/chains"; import type { ExternalWalletSigner } from "../external-wallet-signer"; +import type { ShadowSignerStorage } from "@/signers/shadow-signer"; export abstract class NonCustodialSigner implements Signer { public readonly type: "email" | "phone"; @@ -23,8 +24,13 @@ export abstract class NonCustodialSigner implements Signer { } | null = null; private _initializationPromise: Promise | null = null; protected shadowSigner: ExternalWalletSigner | null = null; + protected shadowSignerStorage?: ShadowSignerStorage; - constructor(protected config: EmailInternalSignerConfig | PhoneInternalSignerConfig) { + constructor( + protected config: EmailInternalSignerConfig | PhoneInternalSignerConfig, + shadowSignerStorage?: ShadowSignerStorage + ) { + this.shadowSignerStorage = shadowSignerStorage; this.initialize(); this.type = this.config.type; } @@ -297,8 +303,8 @@ export abstract class NonCustodialSigner implements Signer { walletAddress: string, ExternalWalletSignerClass: new (config: ExternalWalletInternalSignerConfig) => ExternalWalletSigner ) { - if (await hasShadowSigner(walletAddress)) { - const shadowSigner = await getShadowSigner(walletAddress); + if (await hasShadowSigner(walletAddress, this.shadowSignerStorage)) { + const shadowSigner = await getShadowSigner(walletAddress, this.shadowSignerStorage); if (shadowSigner != null && this.config.shadowSigner?.enabled !== false) { this.shadowSigner = new ExternalWalletSignerClass( this.getShadowSignerConfig(shadowSigner, walletAddress) as ExternalWalletInternalSignerConfig diff --git a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts index 25da7d800..524764343 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts @@ -9,10 +9,15 @@ import { NonCustodialSigner, DEFAULT_EVENT_OPTIONS } from "./ncs-signer"; import { getShadowSignerPrivateKey, type ShadowSignerData } from "@/signers/shadow-signer"; import { SolanaExternalWalletSigner } from "../solana-external-wallet"; import type { SolanaChain } from "@/chains/chains"; +import type { ShadowSignerStorage } from "@/signers/shadow-signer"; export class SolanaNonCustodialSigner extends NonCustodialSigner { - constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig, walletAddress: string) { - super(config); + constructor( + config: EmailInternalSignerConfig | PhoneInternalSignerConfig, + walletAddress: string, + shadowSignerStorage?: ShadowSignerStorage + ) { + super(config, shadowSignerStorage); this.initializeShadowSigner(walletAddress, SolanaExternalWalletSigner); } @@ -41,7 +46,7 @@ export class SolanaNonCustodialSigner extends NonCustodialSigner { }, data: { keyType: "ed25519", - bytes: base58.encode(messageData), + bytes: base58.encode(new Uint8Array(messageData)), encoding: "base58", }, }, @@ -81,7 +86,7 @@ export class SolanaNonCustodialSigner extends NonCustodialSigner { address: shadowData.publicKey, locator: `external-wallet:${shadowData.publicKey}`, onSignTransaction: async (transaction) => { - const privateKey = await getShadowSignerPrivateKey(walletAddress); + const privateKey = await getShadowSignerPrivateKey(walletAddress, this.shadowSignerStorage); if (!privateKey) { throw new Error("Shadow signer private key not found"); } diff --git a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts index fcc63754c..7cf7af757 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts @@ -7,10 +7,15 @@ import type { import { DEFAULT_EVENT_OPTIONS, NonCustodialSigner } from "./ncs-signer"; import { StellarExternalWalletSigner } from "../stellar-external-wallet"; import type { StellarChain } from "@/chains/chains"; +import type { ShadowSignerStorage } from "@/signers/shadow-signer"; export class StellarNonCustodialSigner extends NonCustodialSigner { - constructor(config: EmailInternalSignerConfig | PhoneInternalSignerConfig, walletAddress: string) { - super(config); + constructor( + config: EmailInternalSignerConfig | PhoneInternalSignerConfig, + walletAddress: string, + shadowSignerStorage?: ShadowSignerStorage + ) { + super(config, shadowSignerStorage); this.initializeShadowSigner(walletAddress, StellarExternalWalletSigner); } @@ -79,12 +84,12 @@ export class StellarNonCustodialSigner extends NonCustodialSigner { address: shadowData.publicKey, locator: `external-wallet:${shadowData.publicKey}`, onSignStellarTransaction: async (payload) => { - const privateKey = await getShadowSignerPrivateKey(walletAddress); + const privateKey = await getShadowSignerPrivateKey(walletAddress, this.shadowSignerStorage); if (!privateKey) { throw new Error("Shadow signer private key not found"); } - const transactionString = typeof payload === "string" ? payload : (payload as any).tx; + const transactionString = typeof payload === "string" ? payload : (payload as { tx: string }).tx; const messageBytes = Uint8Array.from(atob(transactionString), (c) => c.charCodeAt(0)); diff --git a/packages/wallets/src/signers/shadow-signer/index.ts b/packages/wallets/src/signers/shadow-signer/index.ts index f16a06392..4e6fd934f 100644 --- a/packages/wallets/src/signers/shadow-signer/index.ts +++ b/packages/wallets/src/signers/shadow-signer/index.ts @@ -2,7 +2,6 @@ import { encode as encodeBase58 } from "bs58"; import type { Chain } from "@/chains/chains"; import { encodeEd25519PublicKey } from "./encodeEd25519PublicKey"; import { BrowserShadowSignerStorage } from "./shadow-signer-storage-browser"; -import { ReactNativeShadowSignerStorage } from "./shadow-signer-storage-rn"; import type { BaseExternalWalletSignerConfig } from "@crossmint/common-sdk-base"; export type ShadowSignerData = { @@ -34,7 +33,7 @@ function getStorage(): ShadowSignerStorage { const isExpo = typeof global !== "undefined" && (global as { expo?: unknown }).expo; if (isReactNative || isExpo) { - storageInstance = new ReactNativeShadowSignerStorage(); + throw new Error("ReactNativeShadowSignerStorage must be provided explicitly for React Native environments"); } else { storageInstance = new BrowserShadowSignerStorage(); } @@ -80,11 +79,12 @@ export async function storeShadowSigner( walletAddress: string, chain: Chain, publicKey: string, - privateKey: CryptoKey + privateKey: CryptoKey, + storage?: ShadowSignerStorage ): Promise { - const storage = getStorage(); + const storageInstance = storage ?? getStorage(); try { - await storage.storePrivateKey(walletAddress, privateKey); + await storageInstance.storePrivateKey(walletAddress, privateKey); const data: ShadowSignerData = { chain, @@ -93,32 +93,41 @@ export async function storeShadowSigner( createdAt: Date.now(), }; - await storage.storeMetadata(walletAddress, data); + await storageInstance.storeMetadata(walletAddress, data); } catch (error) { console.warn("Failed to store shadow signer:", error); } } -export async function getShadowSigner(walletAddress: string): Promise { - const storage = getStorage(); +export async function getShadowSigner( + walletAddress: string, + storage?: ShadowSignerStorage +): Promise { + const storageInstance = storage ?? getStorage(); try { - return await storage.getMetadata(walletAddress); + return await storageInstance.getMetadata(walletAddress); } catch (error) { console.warn("Failed to get shadow signer:", error); return null; } } -export async function getShadowSignerPrivateKey(walletAddress: string): Promise { - const storage = getStorage(); +export async function getShadowSignerPrivateKey( + walletAddress: string, + storage?: ShadowSignerStorage +): Promise { + const storageInstance = storage ?? getStorage(); try { - return await storage.getPrivateKey(walletAddress); + return await storageInstance.getPrivateKey(walletAddress); } catch (error) { console.warn("Failed to retrieve shadow signer private key:", error); return null; } } -export async function hasShadowSigner(walletAddress: string): Promise { - return (await getShadowSigner(walletAddress)) !== null && (await getShadowSignerPrivateKey(walletAddress)) !== null; +export async function hasShadowSigner(walletAddress: string, storage?: ShadowSignerStorage): Promise { + return ( + (await getShadowSigner(walletAddress, storage)) !== null && + (await getShadowSignerPrivateKey(walletAddress, storage)) !== null + ); } diff --git a/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-rn.ts b/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-rn.ts deleted file mode 100644 index 465d8c070..000000000 --- a/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-rn.ts +++ /dev/null @@ -1,77 +0,0 @@ -import AsyncStorage from "@react-native-async-storage/async-storage"; -import type { ShadowSignerData, ShadowSignerStorage } from "."; - -export class ReactNativeShadowSignerStorage implements ShadowSignerStorage { - private readonly SHADOW_SIGNER_DB_NAME = "crossmint_shadow_keys"; - private readonly SHADOW_SIGNER_DB_STORE = "keys"; - private readonly SHADOW_SIGNER_STORAGE_KEY = "crossmint_shadow_signer"; - - private async openDB(): Promise { - return new Promise((resolve, reject) => { - const request = indexedDB.open(this.SHADOW_SIGNER_DB_NAME, 1); - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result); - request.onupgradeneeded = (event) => { - const db = (event.target as IDBOpenDBRequest).result; - if (!db.objectStoreNames.contains(this.SHADOW_SIGNER_DB_STORE)) { - db.createObjectStore(this.SHADOW_SIGNER_DB_STORE); - } - }; - }); - } - - async storePrivateKey(walletAddress: string, privateKey: CryptoKey): Promise { - const db = await this.openDB(); - const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readwrite"); - const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); - store.put(privateKey, walletAddress); - - return new Promise((resolve, reject) => { - tx.oncomplete = () => resolve(); - tx.onerror = () => reject(tx.error); - }); - } - - async getPrivateKey(walletAddress: string): Promise { - try { - const db = await this.openDB(); - const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readonly"); - const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); - const request = store.get(walletAddress); - - return new Promise((resolve, reject) => { - request.onsuccess = () => resolve(request.result || null); - request.onerror = () => reject(request.error); - }); - } catch (error) { - console.warn("Failed to retrieve private key from IndexedDB:", error); - return null; - } - } - - async removePrivateKey(walletAddress: string): Promise { - const db = await this.openDB(); - const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readwrite"); - const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); - store.delete(walletAddress); - - return new Promise((resolve, reject) => { - tx.oncomplete = () => resolve(); - tx.onerror = () => reject(tx.error); - }); - } - - async storeMetadata(walletAddress: string, data: ShadowSignerData): Promise { - await AsyncStorage.setItem(`${this.SHADOW_SIGNER_STORAGE_KEY}_${walletAddress}`, JSON.stringify(data)); - } - - async getMetadata(walletAddress: string): Promise { - try { - const stored = await AsyncStorage.getItem(`${this.SHADOW_SIGNER_STORAGE_KEY}_${walletAddress}`); - return stored ? JSON.parse(stored) : null; - } catch (error) { - console.warn("Failed to retrieve metadata from AsyncStorage:", error); - return null; - } - } -} diff --git a/packages/wallets/src/wallets/types.ts b/packages/wallets/src/wallets/types.ts index 98fec27de..743e6fa4f 100644 --- a/packages/wallets/src/wallets/types.ts +++ b/packages/wallets/src/wallets/types.ts @@ -6,6 +6,7 @@ import type { Abi } from "abitype"; import type { CreateTransactionSuccessResponse } from "../api"; import type { Chain, EVMSmartWalletChain, StellarChain } from "../chains/chains"; import type { SignerConfigForChain, Signer, BaseSignResult, PasskeySignResult } from "../signers/types"; +import type { ShadowSignerStorage } from "../signers/shadow-signer"; export type { Activity } from "../api/types"; @@ -117,6 +118,7 @@ export type WalletPlugin = C extends StellarChain ? StellarWall export type WalletOptions = { experimental_callbacks?: Callbacks; clientTEEConnection?: HandshakeParent; + shadowSignerStorage?: ShadowSignerStorage; }; export type WalletArgsFor = { diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index a0690c8a5..ef4cb8bc6 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -131,7 +131,13 @@ export class WalletFactory { } if (shadowSignerPublicKey != null && shadowSignerPrivateKey != null) { - await storeShadowSigner(walletResponse.address, args.chain, shadowSignerPublicKey, shadowSignerPrivateKey); + await storeShadowSigner( + walletResponse.address, + args.chain, + shadowSignerPublicKey, + shadowSignerPrivateKey, + args.options?.shadowSignerStorage + ); } return this.createWalletInstance(walletResponse, args); @@ -148,7 +154,12 @@ export class WalletFactory { chain: args.chain, address: walletResponse.address, owner: walletResponse.owner, - signer: assembleSigner(args.chain, signerConfig, walletResponse.address), + signer: assembleSigner( + args.chain, + signerConfig, + walletResponse.address, + args.options?.shadowSignerStorage + ), options: args.options, }, this.apiClient From e9550dcfea5dc52771adc8f02a799f3efd6f1668 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Mon, 27 Oct 2025 19:01:29 +0100 Subject: [PATCH 59/82] make shad signer work for react native --- apps/wallets/smart-wallet/expo/app/index.tsx | 2 + .../providers/CrossmintWalletBaseProvider.tsx | 1 - .../src/providers/CrossmintWalletProvider.tsx | 69 +++- .../src/utils/ShadowSignerStorage.ts | 77 ----- .../src/utils/WebViewShadowSignerStorage.ts | 294 ++++++++++++++++++ .../src/utils/shadow-signer-storage-events.ts | 80 +++++ .../src/utils/webview-storage-injected.ts | 137 ++++++++ packages/wallets/package.json | 2 - .../src/signers/non-custodial/ncs-signer.ts | 9 +- .../non-custodial/ncs-solana-signer.ts | 16 +- .../non-custodial/ncs-stellar-signer.ts | 16 +- .../src/signers/shadow-signer/index.ts | 73 ++--- .../shadow-signer-storage-browser.ts | 57 ++-- .../wallets/src/wallets/wallet-factory.ts | 38 ++- packages/wallets/tsup.config.ts | 12 - pnpm-lock.yaml | 132 +------- 16 files changed, 689 insertions(+), 326 deletions(-) delete mode 100644 packages/client/ui/react-native/src/utils/ShadowSignerStorage.ts create mode 100644 packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts create mode 100644 packages/client/ui/react-native/src/utils/shadow-signer-storage-events.ts create mode 100644 packages/client/ui/react-native/src/utils/webview-storage-injected.ts diff --git a/apps/wallets/smart-wallet/expo/app/index.tsx b/apps/wallets/smart-wallet/expo/app/index.tsx index 5f105d7fe..60144a436 100644 --- a/apps/wallets/smart-wallet/expo/app/index.tsx +++ b/apps/wallets/smart-wallet/expo/app/index.tsx @@ -19,6 +19,8 @@ export default function Index() { const walletAddress = useMemo(() => wallet?.address, [wallet]); const url = Linking.useURL(); + console.log("wallet", wallet); + const [balances, setBalances] = useState(null); const [isLoading, setIsLoading] = useState(false); diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index fc57c0610..c6f19ba79 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -142,7 +142,6 @@ export function CrossmintWalletBaseProvider({ const resolvedSigner = resolveSignerConfig(args.signer) as SignerConfigForChain; await initializeWebViewIfNeeded(resolvedSigner); - const wallet = await wallets.getOrCreateWallet({ chain: args.chain, signer: resolvedSigner, diff --git a/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx b/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx index 267d8523a..414ac78cb 100644 --- a/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx +++ b/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx @@ -6,7 +6,7 @@ import { environmentUrlConfig, signerInboundEvents, signerOutboundEvents } from import { validateAPIKey } from "@crossmint/common-sdk-base"; import { type CreateOnLogin, CrossmintWalletBaseProvider } from "@crossmint/client-sdk-react-base"; import { useCrossmint } from "@/hooks"; -import { ReactNativeShadowSignerStorage } from "@/utils/ShadowSignerStorage"; +import { WebViewShadowSignerStorage } from "@/utils/WebViewShadowSignerStorage"; const throwNotAvailable = (functionName: string) => () => { throw new Error(`${functionName} is not available. Make sure you're using an email signer wallet.`); @@ -52,13 +52,15 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: return environmentUrlConfig[parsedAPIKey.environment]; }, [parsedAPIKey.environment]); - const shadowSignerStorage = useMemo(() => new ReactNativeShadowSignerStorage(), []); + const shadowSignerStorage = useMemo(() => new WebViewShadowSignerStorage(), []); const webviewRef = useRef(null); const webViewParentRef = useRef | null>( null ); + const shadowSignerWebViewRef = useRef(null); + // Use useState only for needsAuth since it needs to trigger re-renders const [needsAuth, setNeedsAuth] = useState(false); @@ -97,6 +99,13 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: } }, []); + const onShadowSignerWebViewLoad = useCallback(() => { + if (shadowSignerStorage instanceof WebViewShadowSignerStorage && shadowSignerWebViewRef.current) { + console.log("[ShadowSignerStorage] WebView loaded, injecting storage handler..."); + shadowSignerStorage.initialize(shadowSignerWebViewRef); + } + }, [shadowSignerStorage]); + const handleMessage = useCallback((event: WebViewMessageEvent) => { const parent = webViewParentRef.current; if (parent == null) { @@ -117,7 +126,7 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: return argStr; } return JSON.parse(argStr); - } catch (e) { + } catch { return argStr; } }); @@ -146,6 +155,15 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: parent.handleMessage(event); }, []); + const handleShadowSignerMessage = useCallback( + (event: WebViewMessageEvent) => { + if (shadowSignerStorage instanceof WebViewShadowSignerStorage) { + shadowSignerStorage.handleMessage(event); + } + }, + [shadowSignerStorage] + ); + const getClientTEEConnection = () => { if (webViewParentRef.current == null) { throw new Error("WebView not ready or handshake incomplete"); @@ -155,15 +173,26 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: const initializeWebView = async () => { setNeedsWebView(true); + + // Wait for both WebViews to be ready let attempts = 0; const maxAttempts = 100; // 5 seconds total with 50ms intervals + + // Wait for email/phone signer WebView while (webViewParentRef.current == null && attempts < maxAttempts) { await new Promise((resolve) => setTimeout(resolve, 50)); attempts++; } if (webViewParentRef.current == null) { - throw new Error("WebView not ready or handshake incomplete"); + throw new Error("Email/Phone signer WebView not ready or handshake incomplete"); + } + + // Wait for shadow signer WebView if using WebViewShadowSignerStorage + if (shadowSignerStorage instanceof WebViewShadowSignerStorage) { + console.log("[initializeWebView] Waiting for shadow signer WebView to be ready..."); + // The storage has a ready promise that resolves when injected + await shadowSignerStorage.waitForReady(); } }; @@ -173,6 +202,7 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: verifyOtp: (otp: string) => Promise, reject: () => void ) => { + console.log("onAuthRequired", needsAuth); setNeedsAuth(needsAuth); sendEmailWithOtpRef.current = sendEmailWithOtp; verifyOtpRef.current = verifyOtp; @@ -239,6 +269,37 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: /> )} + {needsWebView && ( + + ", + baseUrl: "https://crossmint-shadow-signer.local", + }} + onLoadEnd={onShadowSignerWebViewLoad} + onMessage={handleShadowSignerMessage} + onError={(syntheticEvent) => { + console.error("[ShadowSignerStorage] WebView error:", syntheticEvent.nativeEvent); + }} + style={{ + width: 1, + height: 1, + }} + javaScriptEnabled={true} + incognito={false} + cacheEnabled={true} + cacheMode="LOAD_DEFAULT" + /> + + )} ); } diff --git a/packages/client/ui/react-native/src/utils/ShadowSignerStorage.ts b/packages/client/ui/react-native/src/utils/ShadowSignerStorage.ts deleted file mode 100644 index 7670638d4..000000000 --- a/packages/client/ui/react-native/src/utils/ShadowSignerStorage.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { ShadowSignerStorage, ShadowSignerData } from "@crossmint/wallets-sdk"; -import { SecureStorage } from "./SecureStorage"; - -export class ReactNativeShadowSignerStorage implements ShadowSignerStorage { - private readonly SHADOW_SIGNER_STORAGE_KEY = "crossmint_shadow_signer"; - private secureStorage = new SecureStorage(); - - async storePrivateKey(walletAddress: string, privateKey: CryptoKey): Promise { - try { - const privateKeyBuffer = await window.crypto.subtle.exportKey("raw", privateKey); - const privateKeyBytes = new Uint8Array(privateKeyBuffer); - - const privateKeyBase64 = btoa(String.fromCharCode(...privateKeyBytes)); - - await this.secureStorage.set(`${this.SHADOW_SIGNER_STORAGE_KEY}_key_${walletAddress}`, privateKeyBase64); - } catch (error) { - console.error("Failed to store private key:", error); - throw error; - } - } - - async getPrivateKey(walletAddress: string): Promise { - try { - const stored = await this.secureStorage.get(`${this.SHADOW_SIGNER_STORAGE_KEY}_key_${walletAddress}`); - if (!stored) { - return null; - } - - const privateKeyBytes = Uint8Array.from(atob(stored), (c) => c.charCodeAt(0)); - - return await window.crypto.subtle.importKey( - "raw", - privateKeyBytes, - { - name: "Ed25519", - namedCurve: "Ed25519", - } as AlgorithmIdentifier, - false, - ["sign"] - ); - } catch (error) { - console.warn("Failed to retrieve private key from SecureStorage:", error); - return null; - } - } - - async removePrivateKey(walletAddress: string): Promise { - try { - await this.secureStorage.remove(`${this.SHADOW_SIGNER_STORAGE_KEY}_key_${walletAddress}`); - } catch (error) { - console.error("Failed to remove private key:", error); - throw error; - } - } - - async storeMetadata(walletAddress: string, data: ShadowSignerData): Promise { - try { - await this.secureStorage.set( - `${this.SHADOW_SIGNER_STORAGE_KEY}_meta_${walletAddress}`, - JSON.stringify(data) - ); - } catch (error) { - console.error("Failed to store metadata:", error); - throw error; - } - } - - async getMetadata(walletAddress: string): Promise { - try { - const stored = await this.secureStorage.get(`${this.SHADOW_SIGNER_STORAGE_KEY}_meta_${walletAddress}`); - return stored ? JSON.parse(stored) : null; - } catch (error) { - console.warn("Failed to retrieve metadata from SecureStorage:", error); - return null; - } - } -} diff --git a/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts b/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts new file mode 100644 index 000000000..582cb2b6e --- /dev/null +++ b/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts @@ -0,0 +1,294 @@ +import type { ShadowSignerStorage, ShadowSignerData } from "@crossmint/wallets-sdk"; +import { SecureStorage } from "./SecureStorage"; +import { SHADOW_SIGNER_STORAGE_INJECTED_JS } from "./webview-storage-injected"; +import type { RefObject } from "react"; +import type { WebView } from "react-native-webview"; +import * as SecureStore from "expo-secure-store"; + +/** + * Shadow signer storage using WebView's IndexedDB. + * + * This leverages the existing react-native-webview-crypto WebView to access IndexedDB. + * + * Architecture: + * 1. Keys generated in WebView using Web Crypto API (non-extractable) + * 2. Keys stored in WebView's IndexedDB via structured cloning + * 3. Signing happens in WebView (keys retrieved from IndexedDB) + * 4. Only signatures cross the bridge to React Native + * 5. CryptoKey objects NEVER leave the WebView + * + * Benefits: + * - ✅ Non-extractable CryptoKey objects (cannot be exported once created) + * - ✅ Keys stored in persistent IndexedDB (survives app restarts) + * - ✅ No native code required + * - ✅ Uses existing WebView infrastructure + * - ✅ Same security model as browser implementation + * + * Security: + * - Keys created as non-extractable in WebView (extractable: false) + * - crypto.subtle.exportKey() will ALWAYS fail on these keys + * - Even if code is added later, keys remain non-extractable (immutable flag) + * - Stored in WebView's persistent IndexedDB storage + * + * Persistence: + * - ✅ IndexedDB persists across app restarts + * - Location: Library/WebKit/WebsiteData/ (iOS), app_webview/ (Android) + * - Only cleared when app uninstalled or storage explicitly cleared + */ +export class WebViewShadowSignerStorage implements ShadowSignerStorage { + private readonly SHADOW_SIGNER_STORAGE_KEY = "crossmint_shadow_signer"; + private secureStorage = new SecureStorage(); + private webViewRef: RefObject | null = null; + private isInjected = false; + private isReady = false; + private readyPromise: Promise; + private readyResolve: (() => void) | null = null; + + constructor() { + // Create ready promise immediately so operations can wait for it + this.readyPromise = new Promise((resolve) => { + this.readyResolve = resolve; + }); + } + + /** + * Initializes the WebView storage with a WebView ref. + * + * @param webViewRef Reference to the WebView instance + */ + initialize(webViewRef: RefObject): void { + this.webViewRef = webViewRef; + // Inject the storage handler (this will resolve the ready promise) + this.injectStorageHandler(); + } + + /** + * Injects the IndexedDB storage handler into the WebView. + * This only needs to be done once when the WebView loads. + */ + private injectStorageHandler(): void { + if (this.isInjected || !this.webViewRef?.current) { + return; + } + + try { + this.webViewRef.current.injectJavaScript(SHADOW_SIGNER_STORAGE_INJECTED_JS); + this.isInjected = true; + this.isReady = true; + console.log("[WebViewShadowSignerStorage] Storage handler injected into WebView"); + + // Resolve the ready promise + if (this.readyResolve) { + this.readyResolve(); + this.readyResolve = null; + } + } catch (error) { + console.error("[WebViewShadowSignerStorage] Failed to inject storage handler:", error); + } + } + + /** + * Waits for the WebView to be ready before proceeding. + */ + private async ensureReady(): Promise { + if (this.isReady) { + return; + } + + console.log("[WebViewShadowSignerStorage] Waiting for WebView to be ready..."); + await this.readyPromise; + } + + /** + * Public method to wait for WebView to be ready. + * Used by initializeWebView to ensure shadow signer WebView is ready before wallet creation. + */ + async waitForReady(): Promise { + return this.ensureReady(); + } + + private pendingRequests = new Map< + string, + { + resolve: (value: Record) => void; + reject: (error: unknown) => void; + timeout: ReturnType; + } + >(); + + /** + * Message handler that should be called from the WebView's onMessage prop. + * Pass this to your WebView component or call it from your existing message handler. + */ + handleMessage = (event: { nativeEvent: { data: string } }) => { + try { + const message = JSON.parse(event.nativeEvent.data) as { + type?: string; + id?: string; + result?: Record; + error?: string; + }; + + if (message.type === "SHADOW_SIGNER_RESPONSE" && message.id) { + const pending = this.pendingRequests.get(message.id); + if (pending) { + clearTimeout(pending.timeout); + this.pendingRequests.delete(message.id); + + if (message.error) { + pending.reject(new Error(message.error)); + } else { + pending.resolve(message.result ?? {}); + } + } + } + } catch { + // Ignore parse errors - might be other messages + } + }; + + /** + * Calls a function in the WebView and returns the result. + */ + private async callWebViewFunction( + operation: string, + params: Record + ): Promise> { + // Wait for WebView to be ready + await this.ensureReady(); + + const webView = this.webViewRef?.current; + if (!webView) { + throw new Error("WebView not available. Make sure to initialize() with a WebView ref."); + } + + const id = `shadow_${Date.now()}_${Math.random().toString(36).slice(2)}`; + + return new Promise((resolve, reject) => { + const timeout: ReturnType = setTimeout(() => { + this.pendingRequests.delete(id); + reject(new Error(`WebView storage operation timed out: ${operation}`)); + }, 30000); + + this.pendingRequests.set(id, { resolve, reject, timeout }); + + // Inject JavaScript to call the storage function + const script = ` +(async function() { + try { + const result = await window.__crossmintShadowSignerStorage('${operation}', ${JSON.stringify(params)}); + window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'SHADOW_SIGNER_RESPONSE', + id: '${id}', + result: result + })); + } catch (error) { + window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'SHADOW_SIGNER_RESPONSE', + id: '${id}', + error: error.message || String(error) + })); + } +})(); +true; + `; + + webView.injectJavaScript(script); + }); + } + + // ===== ShadowSignerStorage Interface Implementation ===== + + /** + * Stores metadata in React Native SecureStore. + */ + async storeMetadata(walletAddress: string, data: ShadowSignerData): Promise { + try { + await this.secureStorage.set( + `${this.SHADOW_SIGNER_STORAGE_KEY}_meta_${walletAddress}`, + JSON.stringify(data) + ); + } catch (error) { + console.error("Failed to store metadata:", error); + throw error; + } + } + + /** + * Gets metadata from React Native SecureStore. + */ + async getMetadata(walletAddress: string): Promise { + try { + const key = `${this.SHADOW_SIGNER_STORAGE_KEY}_meta_${walletAddress}`; + console.log("[WebViewShadowSignerStorage] Getting metadata for key:", key); + + // Use SecureStore directly to avoid the value/expiresAt wrapping from SecureStorage + const stored = await SecureStore.getItemAsync(key); + + console.log("[WebViewShadowSignerStorage] Retrieved raw metadata:", stored); + + if (!stored) { + return null; + } + + const parsed = JSON.parse(stored); + console.log("[WebViewShadowSignerStorage] Parsed metadata:", parsed); + + return parsed; + } catch (error) { + console.error("[WebViewShadowSignerStorage] Failed to retrieve metadata from SecureStorage:", error); + return null; + } + } + + /** + * Generates a key pair in the WebView and stores it in IndexedDB. + * Returns the public key as base64. + * The private key is stored in IndexedDB indexed by the public key. + */ + async keyGenerator(): Promise { + const publicKeyBytes = await this.generateKeyInWebView(); + const publicKeyBase64 = Buffer.from(publicKeyBytes).toString("base64"); + + // Key is already stored in WebView's IndexedDB, indexed by publicKeyBase64 + + return publicKeyBase64; + } + + /** + * Signs data using a key stored in WebView's IndexedDB. + * @param publicKeyBase64 - Base64-encoded public key used to lookup the private key + */ + async sign(publicKeyBase64: string, data: Uint8Array): Promise { + return await this.signInWebView(publicKeyBase64, data); + } + + // ===== WebView-Specific Methods ===== + + /** + * Generates a key pair in the WebView and stores it in IndexedDB. + * The key is created as non-extractable and cannot be exported. + * Returns the public key bytes. + * + * The key is stored in IndexedDB indexed by its base64-encoded public key. + */ + private async generateKeyInWebView(): Promise { + const response = await this.callWebViewFunction("generate", {}); + const publicKeyBytes = response.publicKeyBytes as number[]; + return new Uint8Array(publicKeyBytes); + } + + /** + * Signs data using a key stored in WebView's IndexedDB. + * Signing happens entirely in the WebView; private key never crosses the bridge. + * + * @param publicKeyBase64 - Base64-encoded public key used to lookup the private key in IndexedDB + * @param data - Data to sign + */ + private async signInWebView(publicKeyBase64: string, data: Uint8Array): Promise { + const messageBytes = Array.from(data); + const response = await this.callWebViewFunction("sign", { publicKey: publicKeyBase64, messageBytes }); + const signatureBytes = response.signatureBytes as number[]; + return new Uint8Array(signatureBytes); + } +} diff --git a/packages/client/ui/react-native/src/utils/shadow-signer-storage-events.ts b/packages/client/ui/react-native/src/utils/shadow-signer-storage-events.ts new file mode 100644 index 000000000..518d1ec32 --- /dev/null +++ b/packages/client/ui/react-native/src/utils/shadow-signer-storage-events.ts @@ -0,0 +1,80 @@ +import { z } from "zod"; + +/** + * Shadow Signer Storage Events + * + * These events are used for communication between React Native and the WebView + * for shadow signer key storage and signing operations. + */ + +// ===== Schemas ===== + +const GenerateKeyRequestSchema = z.object({ + walletAddress: z.string(), + chain: z.string(), +}); + +const GenerateKeyResponseSchema = z.object({ + publicKeyBytes: z.array(z.number()), +}); + +const SignRequestSchema = z.object({ + walletAddress: z.string(), + messageBytes: z.array(z.number()), +}); + +const SignResponseSchema = z.object({ + signatureBytes: z.array(z.number()), +}); + +const HasKeyRequestSchema = z.object({ + walletAddress: z.string(), +}); + +const HasKeyResponseSchema = z.object({ + exists: z.boolean(), +}); + +const DeleteKeyRequestSchema = z.object({ + walletAddress: z.string(), +}); + +const DeleteKeyResponseSchema = z.object({ + success: z.boolean(), +}); + +// ===== Event Maps ===== + +/** + * Events sent from React Native to WebView (requests) + */ +export const shadowSignerStorageInboundEvents = { + "request:shadow-storage-generate": GenerateKeyRequestSchema, + "request:shadow-storage-sign": SignRequestSchema, + "request:shadow-storage-has-key": HasKeyRequestSchema, + "request:shadow-storage-delete": DeleteKeyRequestSchema, +} as const; + +/** + * Events sent from WebView to React Native (responses) + */ +export const shadowSignerStorageOutboundEvents = { + "response:shadow-storage-generate": GenerateKeyResponseSchema, + "response:shadow-storage-sign": SignResponseSchema, + "response:shadow-storage-has-key": HasKeyResponseSchema, + "response:shadow-storage-delete": DeleteKeyResponseSchema, +} as const; + +// ===== Types ===== + +export type ShadowSignerStorageInboundEvents = typeof shadowSignerStorageInboundEvents; +export type ShadowSignerStorageOutboundEvents = typeof shadowSignerStorageOutboundEvents; + +export type GenerateKeyRequest = z.infer; +export type GenerateKeyResponse = z.infer; +export type SignRequest = z.infer; +export type SignResponse = z.infer; +export type HasKeyRequest = z.infer; +export type HasKeyResponse = z.infer; +export type DeleteKeyRequest = z.infer; +export type DeleteKeyResponse = z.infer; diff --git a/packages/client/ui/react-native/src/utils/webview-storage-injected.ts b/packages/client/ui/react-native/src/utils/webview-storage-injected.ts new file mode 100644 index 000000000..3de844808 --- /dev/null +++ b/packages/client/ui/react-native/src/utils/webview-storage-injected.ts @@ -0,0 +1,137 @@ +/** + * JavaScript code to be injected into the WebView for shadow signer storage. + * This code runs in the WebView context and has access to IndexedDB and Web Crypto API. + * + * The code handles: + * - Ed25519 key generation (non-extractable) + * - Storing keys in IndexedDB via structured cloning + * - Signing operations using stored keys + * - Key management (check existence, delete) + * + * Security: + * - Keys are generated with extractable: false + * - crypto.subtle.exportKey() will fail on these keys + * - Keys can only be used for signing + * - Once created, they remain non-extractable forever + */ +export const SHADOW_SIGNER_STORAGE_INJECTED_JS = ` +(function() { + const DB_NAME = 'crossmint_shadow_keys'; + const STORE_NAME = 'keys'; + let db = null; + + // Open IndexedDB + async function openDB() { + if (db) return db; + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, 1); + request.onerror = () => reject(request.error); + request.onsuccess = () => { + db = request.result; + resolve(db); + }; + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME); + } + }; + }); + } + + // Initialize DB immediately + openDB().then(() => { + console.log('[CrossmintShadowSigner] IndexedDB ready for non-extractable key storage'); + }).catch(e => { + console.error('[CrossmintShadowSigner] IndexedDB init failed:', e); + }); + + // Storage operation handler + window.__crossmintShadowSignerStorage = async function(operation, params) { + try { + await openDB(); + let result; + + switch (operation) { + case 'generate': { + console.log('[CrossmintShadowSigner] Generating new Ed25519 key pair (non-extractable)...'); + + // Generate Ed25519 key pair (NON-EXTRACTABLE) + const keyPair = await crypto.subtle.generateKey( + { name: 'Ed25519', namedCurve: 'Ed25519' }, + false, // ← NON-EXTRACTABLE - this is PERMANENT and IMMUTABLE + ['sign', 'verify'] + ); + + // Export public key only (public keys are exportable) + const publicKeyBuffer = await crypto.subtle.exportKey('raw', keyPair.publicKey); + const publicKeyBytes = new Uint8Array(publicKeyBuffer); + + // Convert public key to base64 to use as the index + const publicKeyBase64 = btoa(String.fromCharCode.apply(null, publicKeyBytes)); + + console.log('[CrossmintShadowSigner] Key pair generated, storing in IndexedDB with public key as index...'); + + // Store private key in IndexedDB indexed by publicKeyBase64 + // The CryptoKey object is cloned with its non-extractable flag intact + const tx = db.transaction([STORE_NAME], 'readwrite'); + tx.objectStore(STORE_NAME).put(keyPair.privateKey, publicKeyBase64); + await new Promise((resolve, reject) => { + tx.oncomplete = resolve; + tx.onerror = () => reject(tx.error); + }); + + console.log('[CrossmintShadowSigner] Private key stored in IndexedDB[publicKey]'); + console.log('[CrossmintShadowSigner] ✅ Key generation complete'); + + result = { publicKeyBytes: Array.from(publicKeyBytes) }; + break; + } + + case 'sign': { + const { publicKey, messageBytes } = params; + + console.log('[CrossmintShadowSigner] Retrieving key from IndexedDB for signing...'); + + // Retrieve non-extractable private key from IndexedDB using publicKey as index + const tx = db.transaction([STORE_NAME], 'readonly'); + const request = tx.objectStore(STORE_NAME).get(publicKey); + const privateKey = await new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + + if (!privateKey) { + throw new Error('Private key not found for public key: ' + publicKey); + } + + console.log('[CrossmintShadowSigner] Key retrieved, signing...'); + + // Sign using the non-extractable key + // The key is used for signing but cannot be exported + const signature = await crypto.subtle.sign( + { name: 'Ed25519' }, + privateKey, + new Uint8Array(messageBytes) + ); + + console.log('[CrossmintShadowSigner] ✅ Signing complete'); + result = { signatureBytes: Array.from(new Uint8Array(signature)) }; + break; + } + + default: + throw new Error('Unknown operation: ' + operation); + } + + return result; + } catch (error) { + console.error('[CrossmintShadowSigner] Operation failed:', operation, error); + throw error; + } + }; + + console.log('[CrossmintShadowSigner] Storage handler installed in WebView'); +})(); +true; +`; diff --git a/packages/wallets/package.json b/packages/wallets/package.json index 092d15ee6..bc8463679 100644 --- a/packages/wallets/package.json +++ b/packages/wallets/package.json @@ -27,14 +27,12 @@ "@crossmint/client-signers": "workspace:*", "@crossmint/common-sdk-base": "workspace:*", "@hey-api/client-fetch": "0.8.1", - "@react-native-async-storage/async-storage": "2.2.0", "@solana/web3.js": "1.98.1", "@stellar/stellar-sdk": "v14.0.0-rc.3", "abitype": "1.0.8", "base32.js": "0.1.0", "bs58": "5.0.0", "ox": "0.6.9", - "react-native-webview-crypto": "0.0.27", "tweetnacl": "1.0.3", "viem": "2.33.1" }, diff --git a/packages/wallets/src/signers/non-custodial/ncs-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-signer.ts index 53d9e9dec..e45298fcc 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-signer.ts @@ -13,6 +13,7 @@ import { getShadowSigner, hasShadowSigner, type ShadowSignerData } from "../shad import type { Chain } from "../../chains/chains"; import type { ExternalWalletSigner } from "../external-wallet-signer"; import type { ShadowSignerStorage } from "@/signers/shadow-signer"; +import { getStorage } from "../shadow-signer"; export abstract class NonCustodialSigner implements Signer { public readonly type: "email" | "phone"; @@ -30,7 +31,7 @@ export abstract class NonCustodialSigner implements Signer { protected config: EmailInternalSignerConfig | PhoneInternalSignerConfig, shadowSignerStorage?: ShadowSignerStorage ) { - this.shadowSignerStorage = shadowSignerStorage; + this.shadowSignerStorage = shadowSignerStorage ?? getStorage(); this.initialize(); this.type = this.config.type; } @@ -105,6 +106,7 @@ export abstract class NonCustodialSigner implements Signer { } protected async handleAuthRequired() { + console.log("Shadow signer is not null", this.shadowSigner); if (this.shadowSigner != null) { return; } @@ -303,12 +305,17 @@ export abstract class NonCustodialSigner implements Signer { walletAddress: string, ExternalWalletSignerClass: new (config: ExternalWalletInternalSignerConfig) => ExternalWalletSigner ) { + console.log("initializeShadowSigner", walletAddress, this.shadowSignerStorage); + console.log("hasShadowSigner", await hasShadowSigner(walletAddress, this.shadowSignerStorage)); if (await hasShadowSigner(walletAddress, this.shadowSignerStorage)) { const shadowSigner = await getShadowSigner(walletAddress, this.shadowSignerStorage); + console.log("shadowSigner", shadowSigner); if (shadowSigner != null && this.config.shadowSigner?.enabled !== false) { + console.log("creating shadow signer", shadowSigner); this.shadowSigner = new ExternalWalletSignerClass( this.getShadowSignerConfig(shadowSigner, walletAddress) as ExternalWalletInternalSignerConfig ); + console.log("shadowSigner created", this.shadowSigner); } } } diff --git a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts index 8bddf6d53..76b5fe19b 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts @@ -6,7 +6,7 @@ import type { PhoneInternalSignerConfig, } from "../types"; import { NonCustodialSigner, DEFAULT_EVENT_OPTIONS } from "./ncs-signer"; -import { getShadowSignerPrivateKey, type ShadowSignerData } from "../shadow-signer"; +import type { ShadowSignerData } from "../shadow-signer"; import { SolanaExternalWalletSigner } from "../solana-external-wallet"; import type { SolanaChain } from "../../chains/chains"; import type { ShadowSignerStorage } from "@/signers/shadow-signer"; @@ -77,24 +77,20 @@ export class SolanaNonCustodialSigner extends NonCustodialSigner { } } - protected getShadowSignerConfig( - shadowData: ShadowSignerData, - walletAddress: string - ): ExternalWalletInternalSignerConfig { + protected getShadowSignerConfig(shadowData: ShadowSignerData): ExternalWalletInternalSignerConfig { return { type: "external-wallet", address: shadowData.publicKey, locator: `external-wallet:${shadowData.publicKey}`, onSignTransaction: async (transaction) => { - const privateKey = await getShadowSignerPrivateKey(walletAddress, this.shadowSignerStorage); - if (privateKey == null) { - throw new Error("Shadow signer private key not found"); + if (!this.shadowSignerStorage) { + throw new Error("Shadow signer storage not available"); } const messageBytes = new Uint8Array(transaction.message.serialize()); - const signatureBuffer = await window.crypto.subtle.sign({ name: "Ed25519" }, privateKey, messageBytes); - const signature = new Uint8Array(signatureBuffer); + const signature = await this.shadowSignerStorage.sign(shadowData.publicKeyBase64, messageBytes); + transaction.addSignature(new PublicKey(shadowData.publicKey), signature); return transaction; diff --git a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts index 2d1800f06..c630aa9d1 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts @@ -1,4 +1,4 @@ -import { getShadowSignerPrivateKey, type ShadowSignerData } from "../shadow-signer"; +import type { ShadowSignerData } from "../shadow-signer"; import type { EmailInternalSignerConfig, ExternalWalletInternalSignerConfig, @@ -75,27 +75,21 @@ export class StellarNonCustodialSigner extends NonCustodialSigner { } } - protected getShadowSignerConfig( - shadowData: ShadowSignerData, - walletAddress: string - ): ExternalWalletInternalSignerConfig { + protected getShadowSignerConfig(shadowData: ShadowSignerData): ExternalWalletInternalSignerConfig { return { type: "external-wallet", address: shadowData.publicKey, locator: `external-wallet:${shadowData.publicKey}`, onSignStellarTransaction: async (payload) => { - const privateKey = await getShadowSignerPrivateKey(walletAddress, this.shadowSignerStorage); - if (privateKey == null) { - throw new Error("Shadow signer private key not found"); + if (!this.shadowSignerStorage) { + throw new Error("Shadow signer storage not available"); } const transactionString = typeof payload === "string" ? payload : (payload as { tx: string }).tx; - const messageBytes = Uint8Array.from(atob(transactionString), (c) => c.charCodeAt(0)); - const signatureBuffer = await window.crypto.subtle.sign({ name: "Ed25519" }, privateKey, messageBytes); + const signature = await this.shadowSignerStorage.sign(shadowData.publicKeyBase64, messageBytes); - const signature = new Uint8Array(signatureBuffer); const signatureBase64 = btoa(String.fromCharCode(...signature)); return signatureBase64; }, diff --git a/packages/wallets/src/signers/shadow-signer/index.ts b/packages/wallets/src/signers/shadow-signer/index.ts index 4e6fd934f..76ab40d2b 100644 --- a/packages/wallets/src/signers/shadow-signer/index.ts +++ b/packages/wallets/src/signers/shadow-signer/index.ts @@ -8,26 +8,25 @@ export type ShadowSignerData = { chain: Chain; walletAddress: string; publicKey: string; + publicKeyBase64: string; createdAt: number; }; export type ShadowSignerResult = { shadowSigner: BaseExternalWalletSignerConfig; publicKey: string; - privateKey: CryptoKey; }; export interface ShadowSignerStorage { - storePrivateKey(walletAddress: string, privateKey: CryptoKey): Promise; - getPrivateKey(walletAddress: string): Promise; - removePrivateKey(walletAddress: string): Promise; + keyGenerator(chain: string): Promise; + sign(publicKey: string, data: Uint8Array): Promise; storeMetadata(walletAddress: string, data: ShadowSignerData): Promise; getMetadata(walletAddress: string): Promise; } let storageInstance: ShadowSignerStorage | null = null; -function getStorage(): ShadowSignerStorage { +export function getStorage(): ShadowSignerStorage { if (!storageInstance) { const isReactNative = typeof navigator !== "undefined" && navigator.product === "ReactNative"; const isExpo = typeof global !== "undefined" && (global as { expo?: unknown }).expo; @@ -41,18 +40,14 @@ function getStorage(): ShadowSignerStorage { return storageInstance; } -export async function generateShadowSigner(chain: C): Promise { +export async function generateShadowSigner( + chain: C, + storage?: ShadowSignerStorage +): Promise { + const storageInstance = storage ?? getStorage(); if (chain === "solana" || chain === "stellar") { - const keyPair = (await window.crypto.subtle.generateKey( - { - name: "Ed25519", - namedCurve: "Ed25519", - } as AlgorithmIdentifier, - false, - ["sign", "verify"] - )) as CryptoKeyPair; - - const publicKeyBuffer = await window.crypto.subtle.exportKey("raw", keyPair.publicKey); + const publicKeyBase64 = await storageInstance.keyGenerator(chain); + const publicKeyBuffer = Buffer.from(publicKeyBase64, "base64"); const publicKeyBytes = new Uint8Array(publicKeyBuffer); let encodedPublicKey: string; @@ -68,7 +63,7 @@ export async function generateShadowSigner(chain: C): Promise { - const storageInstance = storage ?? getStorage(); try { - await storageInstance.storePrivateKey(walletAddress, privateKey); + console.log("[storeShadowSigner] Storing metadata for wallet:", walletAddress, "publicKey:", publicKey); const data: ShadowSignerData = { chain, walletAddress, - publicKey, + publicKey, // Chain-specific (Base58/G-address) for external wallet + publicKeyBase64, // Base64 for IndexedDB lookup createdAt: Date.now(), }; - await storageInstance.storeMetadata(walletAddress, data); + await storage.storeMetadata(walletAddress, data); + console.log("[storeShadowSigner] Metadata stored successfully"); } catch (error) { - console.warn("Failed to store shadow signer:", error); + console.error("Failed to store shadow signer metadata:", error); + throw error; } } @@ -105,29 +102,19 @@ export async function getShadowSigner( ): Promise { const storageInstance = storage ?? getStorage(); try { - return await storageInstance.getMetadata(walletAddress); - } catch (error) { - console.warn("Failed to get shadow signer:", error); - return null; - } -} - -export async function getShadowSignerPrivateKey( - walletAddress: string, - storage?: ShadowSignerStorage -): Promise { - const storageInstance = storage ?? getStorage(); - try { - return await storageInstance.getPrivateKey(walletAddress); + console.log("[getShadowSigner] Getting shadow signer for wallet:", walletAddress); + const result = await storageInstance.getMetadata(walletAddress); + console.log("[getShadowSigner] Result:", result ? "found" : "not found"); + return result; } catch (error) { - console.warn("Failed to retrieve shadow signer private key:", error); + console.error("[getShadowSigner] Failed to get shadow signer:", error); return null; } } export async function hasShadowSigner(walletAddress: string, storage?: ShadowSignerStorage): Promise { - return ( - (await getShadowSigner(walletAddress, storage)) !== null && - (await getShadowSignerPrivateKey(walletAddress, storage)) !== null - ); + console.log("[hasShadowSigner] Checking for wallet:", walletAddress); + const result = (await getShadowSigner(walletAddress, storage)) !== null; + console.log("[hasShadowSigner] Result:", result); + return result; } diff --git a/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts b/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts index d00c80e12..0935cf421 100644 --- a/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts +++ b/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts @@ -19,7 +19,40 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage { }); } - async storePrivateKey(walletAddress: string, privateKey: CryptoKey): Promise { + async keyGenerator(chain: string): Promise { + if (chain === "solana" || chain === "stellar") { + const keyPair = (await window.crypto.subtle.generateKey( + { + name: "Ed25519", + namedCurve: "Ed25519", + } as AlgorithmIdentifier, + false, + ["sign", "verify"] + )) as CryptoKeyPair; + + const publicKeyBuffer = await window.crypto.subtle.exportKey("raw", keyPair.publicKey); + const publicKeyBytes = new Uint8Array(publicKeyBuffer); + const publicKeyBase64 = Buffer.from(publicKeyBytes).toString("base64"); + + await this.storePrivateKeyByPublicKey(publicKeyBase64, keyPair.privateKey); + + return publicKeyBase64; + } + throw new Error("Unsupported chain for browser shadow signer"); + } + + async sign(publicKeyBase64: string, data: Uint8Array): Promise { + const privateKey = await this.getPrivateKeyByPublicKey(publicKeyBase64); + if (!privateKey) { + throw new Error(`No private key found for public key: ${publicKeyBase64}`); + } + + const signature = await window.crypto.subtle.sign({ name: "Ed25519" }, privateKey, data as BufferSource); + + return new Uint8Array(signature); + } + + private async storePrivateKeyByPublicKey(publicKey: string, privateKey: CryptoKey): Promise { if (typeof indexedDB === "undefined") { return; } @@ -27,7 +60,7 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage { const db = await this.openDB(); const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readwrite"); const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); - store.put(privateKey, walletAddress); + store.put(privateKey, publicKey); return new Promise((resolve, reject) => { tx.oncomplete = () => resolve(); @@ -35,7 +68,7 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage { }); } - async getPrivateKey(walletAddress: string): Promise { + private async getPrivateKeyByPublicKey(publicKey: string): Promise { if (typeof indexedDB === "undefined") { return null; } @@ -44,7 +77,7 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage { const db = await this.openDB(); const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readonly"); const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); - const request = store.get(walletAddress); + const request = store.get(publicKey); return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result || null); @@ -56,22 +89,6 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage { } } - async removePrivateKey(walletAddress: string): Promise { - if (typeof indexedDB === "undefined") { - return; - } - - const db = await this.openDB(); - const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readwrite"); - const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); - store.delete(walletAddress); - - return new Promise((resolve, reject) => { - tx.oncomplete = () => resolve(); - tx.onerror = () => reject(tx.error); - }); - } - storeMetadata(walletAddress: string, data: ShadowSignerData): Promise { if (typeof localStorage === "undefined") { return Promise.resolve(); diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 722c6fbd8..8c7035993 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -47,7 +47,7 @@ export class WalletFactory { } const existingWallet = await this.apiClient.getWallet(`me:${this.getChainType(args.chain)}:smart`); - + console.log("existingWallet", existingWallet); if (existingWallet != null && !("error" in existingWallet)) { return this.createWalletInstance(existingWallet, args); } @@ -98,14 +98,13 @@ export class WalletFactory { } public async createWallet(args: WalletCreateArgs): Promise> { + console.log("createWallet", args); await args.options?.experimental_callbacks?.onWalletCreationStart?.(); let adminSignerConfig = args.onCreateConfig?.adminSigner ?? args.signer; - const { delegatedSigners, shadowSignerPublicKey, shadowSignerPrivateKey } = await this.buildDelegatedSigners( - adminSignerConfig, - args - ); - + const { delegatedSigners, shadowSignerPublicKey, shadowSignerPublicKeyBase64 } = + await this.buildDelegatedSigners(adminSignerConfig, args); + console.log("delegatedSigners", delegatedSigners); const tempArgs = { ...args, signer: adminSignerConfig }; this.mutateSignerFromCustomAuth(tempArgs, true); adminSignerConfig = tempArgs.signer; @@ -130,13 +129,13 @@ export class WalletFactory { throw new WalletCreationError(JSON.stringify(walletResponse)); } - if (shadowSignerPublicKey != null && shadowSignerPrivateKey != null) { + if (shadowSignerPublicKey != null && shadowSignerPublicKeyBase64 != null && args.options?.shadowSignerStorage) { await storeShadowSigner( walletResponse.address, args.chain, shadowSignerPublicKey, - shadowSignerPrivateKey, - args.options?.shadowSignerStorage + shadowSignerPublicKeyBase64, + args.options.shadowSignerStorage ); } @@ -500,18 +499,19 @@ export class WalletFactory { ): Promise<{ delegatedSigners: Array; shadowSignerPublicKey: string | null; - shadowSignerPrivateKey: CryptoKey | null; + shadowSignerPublicKeyBase64: string | null; }> { - const { delegatedSigners, shadowSignerPublicKey, shadowSignerPrivateKey } = + console.log("buildDelegatedSigners", adminSigner, args); + const { delegatedSigners, shadowSignerPublicKey, shadowSignerPublicKeyBase64 } = await this.addShadowSignerToDelegatedSignersIfNeeded( args, adminSigner, args.onCreateConfig?.delegatedSigners ); - + console.log("shadowSignerPublicKey", shadowSignerPublicKey); const registeredDelegatedSigners = await this.registerDelegatedSigners(delegatedSigners); - return { delegatedSigners: registeredDelegatedSigners, shadowSignerPublicKey, shadowSignerPrivateKey }; + return { delegatedSigners: registeredDelegatedSigners, shadowSignerPublicKey, shadowSignerPublicKeyBase64 }; } private async registerDelegatedSigners( @@ -539,26 +539,30 @@ export class WalletFactory { ): Promise<{ delegatedSigners: Array> | undefined; shadowSignerPublicKey: string | null; - shadowSignerPrivateKey: CryptoKey | null; + shadowSignerPublicKeyBase64: string | null; }> { if (this.isShadowSignerEnabled(args.chain, adminSigner, delegatedSigners)) { try { - const { shadowSigner, publicKey, privateKey } = await generateShadowSigner(args.chain); + const { shadowSigner, publicKey, publicKeyBase64 } = await generateShadowSigner( + args.chain, + args.options?.shadowSignerStorage + ); return { delegatedSigners: [...(delegatedSigners ?? []), shadowSigner as SignerConfigForChain], shadowSignerPublicKey: publicKey, - shadowSignerPrivateKey: privateKey, + shadowSignerPublicKeyBase64: publicKeyBase64, }; } catch (error) { console.warn("Failed to create shadow signer:", error); + throw error; } } return { delegatedSigners, shadowSignerPublicKey: null, - shadowSignerPrivateKey: null, + shadowSignerPublicKeyBase64: null, }; } diff --git a/packages/wallets/tsup.config.ts b/packages/wallets/tsup.config.ts index c7e3668f0..ee36ffadb 100644 --- a/packages/wallets/tsup.config.ts +++ b/packages/wallets/tsup.config.ts @@ -3,18 +3,6 @@ import type { Options } from "tsup"; const config: Options = { ...treeShakableConfig, - // Exclude test files and React Native storage from the main build - entry: [ - "src/**/*.(ts|tsx)", - "!src/**/*.test.(ts|tsx)", - "!src/utils/shadow-signer-storage-rn.ts", // Exclude RN storage implementation - ], - // Add external dependencies that should not be bundled - external: ["react-native-webview-crypto", "@react-native-async-storage/async-storage"], - // Define environment variables for conditional compilation - define: { - "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"), - }, }; export default config; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8110cc4f..eec365463 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1057,9 +1057,6 @@ importers: '@hey-api/client-fetch': specifier: 0.8.1 version: 0.8.1 - '@react-native-async-storage/async-storage': - specifier: 2.2.0 - version: 2.2.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) '@solana/web3.js': specifier: 1.98.1 version: 1.98.1(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@5.0.10) @@ -1075,18 +1072,9 @@ importers: bs58: specifier: 5.0.0 version: 5.0.0 - expo-crypto: - specifier: 15.0.7 - version: 15.0.7(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10)) ox: specifier: 0.6.9 version: 0.6.9(typescript@5.9.3)(zod@3.25.76) - react-native-keychain: - specifier: 10.0.0 - version: 10.0.0 - react-native-webview-crypto: - specifier: 0.0.27 - version: 0.0.27(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) tweetnacl: specifier: 1.0.3 version: 1.0.3 @@ -4629,11 +4617,6 @@ packages: react: 19.1.0 react-dom: 19.1.0 - '@react-native-async-storage/async-storage@2.2.0': - resolution: {integrity: sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==} - peerDependencies: - react-native: 0.81.4 - '@react-native/assets-registry@0.81.4': resolution: {integrity: sha512-AMcDadefBIjD10BRqkWw+W/VdvXEomR6aEZ0fhQRAv7igrBzb4PTn4vHKYg+sUK0e3wa74kcMy2DLc/HtnGcMA==} engines: {node: '>= 20.19.4'} @@ -7857,11 +7840,6 @@ packages: expo: '*' react-native: 0.81.4 - expo-crypto@15.0.7: - resolution: {integrity: sha512-FUo41TwwGT2e5rA45PsjezI868Ch3M6wbCZsmqTWdF/hr+HyPcrp1L//dsh/hsrsyrQdpY/U96Lu71/wXePJeg==} - peerDependencies: - expo: '*' - expo-device@8.0.9: resolution: {integrity: sha512-XqRpaljDNAYZGZzMpC+b9KZfzfydtkwx3pJAp6ODDH+O/5wjAw+mLc5wQMGJCx8/aqVmMsAokec7iebxDPFZDA==} peerDependencies: @@ -8030,9 +8008,6 @@ packages: fast-base64-decode@1.0.0: resolution: {integrity: sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==} - fast-base64-encode@1.0.0: - resolution: {integrity: sha512-z2XCzVK4fde2cuTEHu2QGkLD6BPtJNKJPn0Z7oINvmhq/quUuIIVPYKUdN0gYeZqOyurjJjBH/bUzK5gafyHvw==} - fast-copy@3.0.2: resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} @@ -8807,10 +8782,6 @@ packages: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} engines: {node: '>=0.10.0'} - is-plain-obj@2.1.0: - resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} - engines: {node: '>=8'} - is-plain-obj@3.0.0: resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} engines: {node: '>=10'} @@ -9587,9 +9558,6 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - lodash@4.17.3: - resolution: {integrity: sha512-H+sg4+uBLOBrw9833P6gCURJjV+puWPbxM8S3H4ORlhVCmQpF5yCE50bc4Exaqm9U5Nhjw83Okq1azyb1U7mxw==} - log-symbols@2.2.0: resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} engines: {node: '>=4'} @@ -9752,10 +9720,6 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - merge-options@3.0.4: - resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==} - engines: {node: '>=10'} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -11175,10 +11139,6 @@ packages: react: 19.1.0 react-native: 0.81.4 - react-native-keychain@10.0.0: - resolution: {integrity: sha512-YzPKSAnSzGEJ12IK6CctNLU79T1W15WDrElRQ+1/FsOazGX9ucFPTQwgYe8Dy8jiSEDJKM4wkVa3g4lD2Z+Pnw==} - engines: {node: '>=16'} - react-native-quick-base64@2.1.2: resolution: {integrity: sha512-xghaXpWdB0ji8OwYyo0fWezRroNxiNFCNFpGUIyE7+qc4gA/IGWnysIG5L0MbdoORv8FkTKUvfd6yCUN5R2VFA==} peerDependencies: @@ -11211,13 +11171,6 @@ packages: react: 19.1.0 react-dom: 19.1.0 - react-native-webview-crypto@0.0.27: - resolution: {integrity: sha512-N4jyn9AbKG/VkQK23R1o+k/nyS2Kv20Tq27VpoQcCg4zx4hvx8y5Y9rjVehzTvUebZX4XdGTzSWGn1fFj98eXA==} - peerDependencies: - react: 19.1.0 - react-native: 0.81.4 - react-native-webview: '>=8.*' - react-native-webview@13.15.0: resolution: {integrity: sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==} peerDependencies: @@ -13140,9 +13093,6 @@ packages: resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} engines: {node: '>=0.8.0'} - webview-crypto@0.1.13: - resolution: {integrity: sha512-8nRkNvvYchoFi32tooLX6qZzG4iCoxOBGsamZnZ1BnN4Nl6cATiIOUzwWDjUpfMu8Mvf+t3Dn0p9cSLrnZfwtg==} - whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} @@ -16457,7 +16407,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: - expo-router: 6.0.12(@types/react@19.1.10)(expo-constants@18.0.9)(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + expo-router: 6.0.12(beoeo6req5s7uiob6vyzmad7bi) react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -16634,7 +16584,7 @@ snapshots: - supports-color - utf-8-validate - '@expo/metro-runtime@6.1.2(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)': + '@expo/metro-runtime@6.1.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)': dependencies: anser: 1.4.10 expo: 54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) @@ -19354,11 +19304,6 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@react-native-async-storage/async-storage@2.2.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))': - dependencies: - merge-options: 3.0.4 - react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) - '@react-native/assets-registry@0.81.4': {} '@react-native/babel-plugin-codegen@0.81.4(@babel/core@7.28.4)': @@ -25557,11 +25502,6 @@ snapshots: transitivePeerDependencies: - supports-color - expo-crypto@15.0.7(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10)): - dependencies: - base64-js: 1.5.1 - expo: 54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) - expo-device@8.0.9(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10)): dependencies: expo: 54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) @@ -25613,47 +25553,9 @@ snapshots: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) - expo-router@6.0.12(@types/react@19.1.10)(expo-constants@18.0.9)(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): - dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) - '@expo/schema-utils': 0.1.7 - '@radix-ui/react-slot': 1.2.0(@types/react@19.1.10)(react@19.1.0) - '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.1.0(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-navigation/bottom-tabs': 7.4.9(@react-navigation/native@7.1.18(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) - '@react-navigation/native': 7.1.18(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) - '@react-navigation/native-stack': 7.3.28(@react-navigation/native@7.1.18(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) - client-only: 0.0.1 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - expo: 54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10) - expo-constants: 18.0.9(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10)) - expo-server: 1.0.1 - fast-deep-equal: 3.1.3 - invariant: 2.2.4 - nanoid: 3.3.11 - query-string: 7.1.3 - react: 19.1.0 - react-fast-compare: 3.2.2 - react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) - react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) - semver: 7.6.3 - server-only: 0.0.1 - sf-symbols-typescript: 2.1.0 - shallowequal: 1.1.0 - use-latest-callback: 0.2.6(react@19.1.0) - vaul: 1.1.2(@types/react-dom@19.1.0(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - optionalDependencies: - react-dom: 19.1.0(react@19.1.0) - transitivePeerDependencies: - - '@react-native-masked-view/masked-view' - - '@types/react' - - '@types/react-dom' - - supports-color - optional: true - expo-router@6.0.12(beoeo6req5s7uiob6vyzmad7bi): dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + '@expo/metro-runtime': 6.1.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) '@expo/schema-utils': 0.1.7 '@radix-ui/react-slot': 1.2.0(@types/react@19.1.10)(react@19.1.0) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.1.0(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -25762,7 +25664,7 @@ snapshots: react-refresh: 0.14.2 whatwg-url-without-unicode: 8.0.0-3 optionalDependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.13(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(bufferutil@4.0.9)(expo-router@6.0.12)(graphql@16.11.0)(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0)(utf-8-validate@5.0.10))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) + '@expo/metro-runtime': 6.1.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) react-native-webview: 13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) transitivePeerDependencies: - '@babel/core' @@ -25832,8 +25734,6 @@ snapshots: fast-base64-decode@1.0.0: {} - fast-base64-encode@1.0.0: {} - fast-copy@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -26691,8 +26591,6 @@ snapshots: is-plain-obj@1.1.0: {} - is-plain-obj@2.1.0: {} - is-plain-obj@3.0.0: {} is-plain-obj@4.1.0: {} @@ -27813,8 +27711,6 @@ snapshots: lodash@4.17.21: {} - lodash@4.17.3: {} - log-symbols@2.2.0: dependencies: chalk: 2.4.2 @@ -28057,10 +27953,6 @@ snapshots: merge-descriptors@1.0.3: {} - merge-options@3.0.4: - dependencies: - is-plain-obj: 2.1.0 - merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -30005,8 +29897,6 @@ snapshots: react: 19.1.0 react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) - react-native-keychain@10.0.0: {} - react-native-quick-base64@2.1.2(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): dependencies: base64-js: 1.5.1 @@ -30050,15 +29940,6 @@ snapshots: transitivePeerDependencies: - encoding - react-native-webview-crypto@0.0.27(react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): - dependencies: - encode-utf8: 1.0.3 - fast-base64-encode: 1.0.0 - react: 19.1.0 - react-native: 0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10) - react-native-webview: 13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0) - webview-crypto: 0.1.13 - react-native-webview@13.15.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.0)(utf-8-validate@5.0.10))(react@19.1.0): dependencies: escape-string-regexp: 4.0.0 @@ -32451,11 +32332,6 @@ snapshots: websocket-extensions@0.1.4: {} - webview-crypto@0.1.13: - dependencies: - lodash: 4.17.3 - serialize-error: 2.1.0 - whatwg-encoding@2.0.0: dependencies: iconv-lite: 0.6.3 From ee6c518f6a14a22df7d2d97edae0a3d53a10b55a Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Tue, 28 Oct 2025 10:49:32 +0100 Subject: [PATCH 60/82] applied comments --- .../src/signers/non-custodial/ncs-signer.ts | 4 +- .../non-custodial/ncs-stellar-signer.ts | 2 +- .../wallets/src/wallets/wallet-factory.ts | 43 +++++++++---------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/packages/wallets/src/signers/non-custodial/ncs-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-signer.ts index 5cc5c6bd2..fb6083e8e 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-signer.ts @@ -9,8 +9,8 @@ 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 "@/signers/shadow-signer"; -import type { Chain } from "@/chains/chains"; +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 { diff --git a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts index fcc63754c..42fe31573 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts @@ -80,7 +80,7 @@ export class StellarNonCustodialSigner extends NonCustodialSigner { locator: `external-wallet:${shadowData.publicKey}`, onSignStellarTransaction: async (payload) => { const privateKey = await getShadowSignerPrivateKey(walletAddress); - if (!privateKey) { + if (privateKey == null) { throw new Error("Shadow signer private key not found"); } diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index a0690c8a5..b2d85cac8 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -491,31 +491,30 @@ export class WalletFactory { shadowSignerPublicKey: string | null; shadowSignerPrivateKey: CryptoKey | null; }> { - const { - delegatedSigners: updatedDelegatedSigners, - shadowSignerPublicKey, - shadowSignerPrivateKey, - } = await this.addShadowSignerToDelegatedSignersIfNeeded( - args, - adminSigner, - args.onCreateConfig?.delegatedSigners - ); + const { delegatedSigners, shadowSignerPublicKey, shadowSignerPrivateKey } = + await this.addShadowSignerToDelegatedSignersIfNeeded( + args, + adminSigner, + args.onCreateConfig?.delegatedSigners + ); - const delegatedSigners = await Promise.all( - updatedDelegatedSigners?.map( - async (signer): Promise => { - if (signer.type === "passkey") { - if (signer.id == null) { - return { signer: await this.createPasskeySigner(signer) }; - } - return { signer }; - } - return { signer: this.getSignerLocator(signer) }; - } - ) ?? [] + const updatedDelegatedSigners = await Promise.all( + delegatedSigners?.map((signer) => this.assembleDelegatedSigner(signer)) ?? [] ); - return { delegatedSigners, shadowSignerPublicKey, shadowSignerPrivateKey }; + return { delegatedSigners: updatedDelegatedSigners, shadowSignerPublicKey, shadowSignerPrivateKey }; + } + + private async assembleDelegatedSigner( + signer: SignerConfigForChain + ): Promise { + if (signer.type === "passkey") { + if (signer.id == null) { + return { signer: await this.createPasskeySigner(signer) }; + } + return { signer }; + } + return { signer: this.getSignerLocator(signer) }; } private async addShadowSignerToDelegatedSignersIfNeeded( From c42de5f81060d08f1f8ad614fd7738d60d09393e Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Tue, 28 Oct 2025 10:52:21 +0100 Subject: [PATCH 61/82] added changeset --- .changeset/sixty-ladybugs-dance.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/sixty-ladybugs-dance.md diff --git a/.changeset/sixty-ladybugs-dance.md b/.changeset/sixty-ladybugs-dance.md new file mode 100644 index 000000000..7a53a7eab --- /dev/null +++ b/.changeset/sixty-ladybugs-dance.md @@ -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 From ff117e4db41111e7659c3d519a02c3dcf3278a55 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Tue, 28 Oct 2025 11:52:15 +0100 Subject: [PATCH 62/82] cleanup PR --- apps/wallets/smart-wallet/expo/app/index.tsx | 2 - .../providers/CrossmintWalletBaseProvider.tsx | 1 + .../src/providers/CrossmintWalletProvider.tsx | 5 - .../src/utils/WebViewShadowSignerStorage.ts | 109 +----------- .../src/utils/shadow-signer-storage-events.ts | 80 --------- .../src/utils/webview-storage-injected.ts | 158 +++++++++--------- .../src/signers/shadow-signer/index.ts | 8 +- .../shadow-signer-storage-browser.ts | 37 ++-- .../wallets/src/wallets/wallet-factory.ts | 3 +- packages/wallets/tsup.config.ts | 7 +- 10 files changed, 104 insertions(+), 306 deletions(-) delete mode 100644 packages/client/ui/react-native/src/utils/shadow-signer-storage-events.ts diff --git a/apps/wallets/smart-wallet/expo/app/index.tsx b/apps/wallets/smart-wallet/expo/app/index.tsx index 60144a436..5f105d7fe 100644 --- a/apps/wallets/smart-wallet/expo/app/index.tsx +++ b/apps/wallets/smart-wallet/expo/app/index.tsx @@ -19,8 +19,6 @@ export default function Index() { const walletAddress = useMemo(() => wallet?.address, [wallet]); const url = Linking.useURL(); - console.log("wallet", wallet); - const [balances, setBalances] = useState(null); const [isLoading, setIsLoading] = useState(false); diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index c6f19ba79..fc57c0610 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -142,6 +142,7 @@ export function CrossmintWalletBaseProvider({ const resolvedSigner = resolveSignerConfig(args.signer) as SignerConfigForChain; await initializeWebViewIfNeeded(resolvedSigner); + const wallet = await wallets.getOrCreateWallet({ chain: args.chain, signer: resolvedSigner, diff --git a/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx b/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx index 414ac78cb..e2572f9a0 100644 --- a/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx +++ b/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx @@ -174,11 +174,9 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: const initializeWebView = async () => { setNeedsWebView(true); - // Wait for both WebViews to be ready let attempts = 0; const maxAttempts = 100; // 5 seconds total with 50ms intervals - // Wait for email/phone signer WebView while (webViewParentRef.current == null && attempts < maxAttempts) { await new Promise((resolve) => setTimeout(resolve, 50)); attempts++; @@ -188,10 +186,8 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: throw new Error("Email/Phone signer WebView not ready or handshake incomplete"); } - // Wait for shadow signer WebView if using WebViewShadowSignerStorage if (shadowSignerStorage instanceof WebViewShadowSignerStorage) { console.log("[initializeWebView] Waiting for shadow signer WebView to be ready..."); - // The storage has a ready promise that resolves when injected await shadowSignerStorage.waitForReady(); } }; @@ -202,7 +198,6 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: verifyOtp: (otp: string) => Promise, reject: () => void ) => { - console.log("onAuthRequired", needsAuth); setNeedsAuth(needsAuth); sendEmailWithOtpRef.current = sendEmailWithOtp; verifyOtpRef.current = verifyOtp; diff --git a/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts b/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts index 582cb2b6e..6f4959403 100644 --- a/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts +++ b/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts @@ -5,67 +5,25 @@ import type { RefObject } from "react"; import type { WebView } from "react-native-webview"; import * as SecureStore from "expo-secure-store"; -/** - * Shadow signer storage using WebView's IndexedDB. - * - * This leverages the existing react-native-webview-crypto WebView to access IndexedDB. - * - * Architecture: - * 1. Keys generated in WebView using Web Crypto API (non-extractable) - * 2. Keys stored in WebView's IndexedDB via structured cloning - * 3. Signing happens in WebView (keys retrieved from IndexedDB) - * 4. Only signatures cross the bridge to React Native - * 5. CryptoKey objects NEVER leave the WebView - * - * Benefits: - * - ✅ Non-extractable CryptoKey objects (cannot be exported once created) - * - ✅ Keys stored in persistent IndexedDB (survives app restarts) - * - ✅ No native code required - * - ✅ Uses existing WebView infrastructure - * - ✅ Same security model as browser implementation - * - * Security: - * - Keys created as non-extractable in WebView (extractable: false) - * - crypto.subtle.exportKey() will ALWAYS fail on these keys - * - Even if code is added later, keys remain non-extractable (immutable flag) - * - Stored in WebView's persistent IndexedDB storage - * - * Persistence: - * - ✅ IndexedDB persists across app restarts - * - Location: Library/WebKit/WebsiteData/ (iOS), app_webview/ (Android) - * - Only cleared when app uninstalled or storage explicitly cleared - */ export class WebViewShadowSignerStorage implements ShadowSignerStorage { private readonly SHADOW_SIGNER_STORAGE_KEY = "crossmint_shadow_signer"; private secureStorage = new SecureStorage(); private webViewRef: RefObject | null = null; private isInjected = false; - private isReady = false; private readyPromise: Promise; private readyResolve: (() => void) | null = null; constructor() { - // Create ready promise immediately so operations can wait for it this.readyPromise = new Promise((resolve) => { this.readyResolve = resolve; }); } - /** - * Initializes the WebView storage with a WebView ref. - * - * @param webViewRef Reference to the WebView instance - */ initialize(webViewRef: RefObject): void { this.webViewRef = webViewRef; - // Inject the storage handler (this will resolve the ready promise) this.injectStorageHandler(); } - /** - * Injects the IndexedDB storage handler into the WebView. - * This only needs to be done once when the WebView loads. - */ private injectStorageHandler(): void { if (this.isInjected || !this.webViewRef?.current) { return; @@ -74,10 +32,8 @@ export class WebViewShadowSignerStorage implements ShadowSignerStorage { try { this.webViewRef.current.injectJavaScript(SHADOW_SIGNER_STORAGE_INJECTED_JS); this.isInjected = true; - this.isReady = true; console.log("[WebViewShadowSignerStorage] Storage handler injected into WebView"); - // Resolve the ready promise if (this.readyResolve) { this.readyResolve(); this.readyResolve = null; @@ -87,24 +43,8 @@ export class WebViewShadowSignerStorage implements ShadowSignerStorage { } } - /** - * Waits for the WebView to be ready before proceeding. - */ - private async ensureReady(): Promise { - if (this.isReady) { - return; - } - - console.log("[WebViewShadowSignerStorage] Waiting for WebView to be ready..."); - await this.readyPromise; - } - - /** - * Public method to wait for WebView to be ready. - * Used by initializeWebView to ensure shadow signer WebView is ready before wallet creation. - */ async waitForReady(): Promise { - return this.ensureReady(); + await this.readyPromise; } private pendingRequests = new Map< @@ -116,10 +56,6 @@ export class WebViewShadowSignerStorage implements ShadowSignerStorage { } >(); - /** - * Message handler that should be called from the WebView's onMessage prop. - * Pass this to your WebView component or call it from your existing message handler. - */ handleMessage = (event: { nativeEvent: { data: string } }) => { try { const message = JSON.parse(event.nativeEvent.data) as { @@ -147,16 +83,10 @@ export class WebViewShadowSignerStorage implements ShadowSignerStorage { } }; - /** - * Calls a function in the WebView and returns the result. - */ private async callWebViewFunction( operation: string, params: Record ): Promise> { - // Wait for WebView to be ready - await this.ensureReady(); - const webView = this.webViewRef?.current; if (!webView) { throw new Error("WebView not available. Make sure to initialize() with a WebView ref."); @@ -172,7 +102,6 @@ export class WebViewShadowSignerStorage implements ShadowSignerStorage { this.pendingRequests.set(id, { resolve, reject, timeout }); - // Inject JavaScript to call the storage function const script = ` (async function() { try { @@ -197,11 +126,6 @@ true; }); } - // ===== ShadowSignerStorage Interface Implementation ===== - - /** - * Stores metadata in React Native SecureStore. - */ async storeMetadata(walletAddress: string, data: ShadowSignerData): Promise { try { await this.secureStorage.set( @@ -214,15 +138,11 @@ true; } } - /** - * Gets metadata from React Native SecureStore. - */ async getMetadata(walletAddress: string): Promise { try { const key = `${this.SHADOW_SIGNER_STORAGE_KEY}_meta_${walletAddress}`; console.log("[WebViewShadowSignerStorage] Getting metadata for key:", key); - // Use SecureStore directly to avoid the value/expiresAt wrapping from SecureStorage const stored = await SecureStore.getItemAsync(key); console.log("[WebViewShadowSignerStorage] Retrieved raw metadata:", stored); @@ -241,50 +161,23 @@ true; } } - /** - * Generates a key pair in the WebView and stores it in IndexedDB. - * Returns the public key as base64. - * The private key is stored in IndexedDB indexed by the public key. - */ async keyGenerator(): Promise { const publicKeyBytes = await this.generateKeyInWebView(); const publicKeyBase64 = Buffer.from(publicKeyBytes).toString("base64"); - // Key is already stored in WebView's IndexedDB, indexed by publicKeyBase64 - return publicKeyBase64; } - /** - * Signs data using a key stored in WebView's IndexedDB. - * @param publicKeyBase64 - Base64-encoded public key used to lookup the private key - */ async sign(publicKeyBase64: string, data: Uint8Array): Promise { return await this.signInWebView(publicKeyBase64, data); } - // ===== WebView-Specific Methods ===== - - /** - * Generates a key pair in the WebView and stores it in IndexedDB. - * The key is created as non-extractable and cannot be exported. - * Returns the public key bytes. - * - * The key is stored in IndexedDB indexed by its base64-encoded public key. - */ private async generateKeyInWebView(): Promise { const response = await this.callWebViewFunction("generate", {}); const publicKeyBytes = response.publicKeyBytes as number[]; return new Uint8Array(publicKeyBytes); } - /** - * Signs data using a key stored in WebView's IndexedDB. - * Signing happens entirely in the WebView; private key never crosses the bridge. - * - * @param publicKeyBase64 - Base64-encoded public key used to lookup the private key in IndexedDB - * @param data - Data to sign - */ private async signInWebView(publicKeyBase64: string, data: Uint8Array): Promise { const messageBytes = Array.from(data); const response = await this.callWebViewFunction("sign", { publicKey: publicKeyBase64, messageBytes }); diff --git a/packages/client/ui/react-native/src/utils/shadow-signer-storage-events.ts b/packages/client/ui/react-native/src/utils/shadow-signer-storage-events.ts deleted file mode 100644 index 518d1ec32..000000000 --- a/packages/client/ui/react-native/src/utils/shadow-signer-storage-events.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { z } from "zod"; - -/** - * Shadow Signer Storage Events - * - * These events are used for communication between React Native and the WebView - * for shadow signer key storage and signing operations. - */ - -// ===== Schemas ===== - -const GenerateKeyRequestSchema = z.object({ - walletAddress: z.string(), - chain: z.string(), -}); - -const GenerateKeyResponseSchema = z.object({ - publicKeyBytes: z.array(z.number()), -}); - -const SignRequestSchema = z.object({ - walletAddress: z.string(), - messageBytes: z.array(z.number()), -}); - -const SignResponseSchema = z.object({ - signatureBytes: z.array(z.number()), -}); - -const HasKeyRequestSchema = z.object({ - walletAddress: z.string(), -}); - -const HasKeyResponseSchema = z.object({ - exists: z.boolean(), -}); - -const DeleteKeyRequestSchema = z.object({ - walletAddress: z.string(), -}); - -const DeleteKeyResponseSchema = z.object({ - success: z.boolean(), -}); - -// ===== Event Maps ===== - -/** - * Events sent from React Native to WebView (requests) - */ -export const shadowSignerStorageInboundEvents = { - "request:shadow-storage-generate": GenerateKeyRequestSchema, - "request:shadow-storage-sign": SignRequestSchema, - "request:shadow-storage-has-key": HasKeyRequestSchema, - "request:shadow-storage-delete": DeleteKeyRequestSchema, -} as const; - -/** - * Events sent from WebView to React Native (responses) - */ -export const shadowSignerStorageOutboundEvents = { - "response:shadow-storage-generate": GenerateKeyResponseSchema, - "response:shadow-storage-sign": SignResponseSchema, - "response:shadow-storage-has-key": HasKeyResponseSchema, - "response:shadow-storage-delete": DeleteKeyResponseSchema, -} as const; - -// ===== Types ===== - -export type ShadowSignerStorageInboundEvents = typeof shadowSignerStorageInboundEvents; -export type ShadowSignerStorageOutboundEvents = typeof shadowSignerStorageOutboundEvents; - -export type GenerateKeyRequest = z.infer; -export type GenerateKeyResponse = z.infer; -export type SignRequest = z.infer; -export type SignResponse = z.infer; -export type HasKeyRequest = z.infer; -export type HasKeyResponse = z.infer; -export type DeleteKeyRequest = z.infer; -export type DeleteKeyResponse = z.infer; diff --git a/packages/client/ui/react-native/src/utils/webview-storage-injected.ts b/packages/client/ui/react-native/src/utils/webview-storage-injected.ts index 3de844808..81559c7e8 100644 --- a/packages/client/ui/react-native/src/utils/webview-storage-injected.ts +++ b/packages/client/ui/react-native/src/utils/webview-storage-injected.ts @@ -1,28 +1,13 @@ -/** - * JavaScript code to be injected into the WebView for shadow signer storage. - * This code runs in the WebView context and has access to IndexedDB and Web Crypto API. - * - * The code handles: - * - Ed25519 key generation (non-extractable) - * - Storing keys in IndexedDB via structured cloning - * - Signing operations using stored keys - * - Key management (check existence, delete) - * - * Security: - * - Keys are generated with extractable: false - * - crypto.subtle.exportKey() will fail on these keys - * - Keys can only be used for signing - * - Once created, they remain non-extractable forever - */ -export const SHADOW_SIGNER_STORAGE_INJECTED_JS = ` -(function() { - const DB_NAME = 'crossmint_shadow_keys'; - const STORE_NAME = 'keys'; - let db = null; +function webviewStorageInjectedCode() { + const DB_NAME = "crossmint_shadow_keys"; + const STORE_NAME = "keys"; + let db: IDBDatabase | null = null; // Open IndexedDB - async function openDB() { - if (db) return db; + async function openDB(): Promise { + if (db) { + return db; + } return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, 1); request.onerror = () => reject(request.error); @@ -31,7 +16,7 @@ export const SHADOW_SIGNER_STORAGE_INJECTED_JS = ` resolve(db); }; request.onupgradeneeded = (event) => { - const db = event.target.result; + const db = (event.target as IDBOpenDBRequest).result; if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME); } @@ -39,99 +24,112 @@ export const SHADOW_SIGNER_STORAGE_INJECTED_JS = ` }); } - // Initialize DB immediately - openDB().then(() => { - console.log('[CrossmintShadowSigner] IndexedDB ready for non-extractable key storage'); - }).catch(e => { - console.error('[CrossmintShadowSigner] IndexedDB init failed:', e); - }); + openDB() + .then(() => { + console.log("[CrossmintShadowSigner] IndexedDB ready for non-extractable key storage"); + }) + .catch((e) => { + console.error("[CrossmintShadowSigner] IndexedDB init failed:", e); + }); - // Storage operation handler - window.__crossmintShadowSignerStorage = async function(operation, params) { + (window as unknown as Record).__crossmintShadowSignerStorage = async function ( + operation: string, + params: Record + ): Promise> { try { await openDB(); - let result; + let result: Record; switch (operation) { - case 'generate': { - console.log('[CrossmintShadowSigner] Generating new Ed25519 key pair (non-extractable)...'); - - // Generate Ed25519 key pair (NON-EXTRACTABLE) - const keyPair = await crypto.subtle.generateKey( - { name: 'Ed25519', namedCurve: 'Ed25519' }, - false, // ← NON-EXTRACTABLE - this is PERMANENT and IMMUTABLE - ['sign', 'verify'] - ); + case "generate": { + console.log("[CrossmintShadowSigner] Generating new Ed25519 key pair (non-extractable)..."); - // Export public key only (public keys are exportable) - const publicKeyBuffer = await crypto.subtle.exportKey('raw', keyPair.publicKey); + const keyPair = (await crypto.subtle.generateKey( + { name: "Ed25519", namedCurve: "Ed25519" } as EcKeyGenParams, + false, + ["sign", "verify"] + )) as CryptoKeyPair; + + const publicKeyBuffer = await crypto.subtle.exportKey("raw", keyPair.publicKey); const publicKeyBytes = new Uint8Array(publicKeyBuffer); - - // Convert public key to base64 to use as the index - const publicKeyBase64 = btoa(String.fromCharCode.apply(null, publicKeyBytes)); - console.log('[CrossmintShadowSigner] Key pair generated, storing in IndexedDB with public key as index...'); + const publicKeyBase64 = btoa(String.fromCharCode.apply(null, Array.from(publicKeyBytes))); + + console.log( + "[CrossmintShadowSigner] Key pair generated, storing in IndexedDB with public key as index..." + ); - // Store private key in IndexedDB indexed by publicKeyBase64 - // The CryptoKey object is cloned with its non-extractable flag intact - const tx = db.transaction([STORE_NAME], 'readwrite'); + if (!db) { + throw new Error("Database not initialized"); + } + const tx = db.transaction([STORE_NAME], "readwrite"); tx.objectStore(STORE_NAME).put(keyPair.privateKey, publicKeyBase64); - await new Promise((resolve, reject) => { - tx.oncomplete = resolve; - tx.onerror = () => reject(tx.error); + await new Promise((resolve, reject) => { + tx.oncomplete = () => { + resolve(); + }; + tx.onerror = () => { + reject(tx.error); + }; }); - console.log('[CrossmintShadowSigner] Private key stored in IndexedDB[publicKey]'); - console.log('[CrossmintShadowSigner] ✅ Key generation complete'); - + console.log("[CrossmintShadowSigner] Private key stored in IndexedDB[publicKey]"); + console.log("[CrossmintShadowSigner] ✅ Key generation complete"); + result = { publicKeyBytes: Array.from(publicKeyBytes) }; break; } - case 'sign': { + case "sign": { const { publicKey, messageBytes } = params; - - console.log('[CrossmintShadowSigner] Retrieving key from IndexedDB for signing...'); - - // Retrieve non-extractable private key from IndexedDB using publicKey as index - const tx = db.transaction([STORE_NAME], 'readonly'); - const request = tx.objectStore(STORE_NAME).get(publicKey); - const privateKey = await new Promise((resolve, reject) => { - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(request.error); + + console.log("[CrossmintShadowSigner] Retrieving key from IndexedDB for signing..."); + + if (!db) { + throw new Error("Database not initialized"); + } + const tx = db.transaction([STORE_NAME], "readonly"); + const request = tx.objectStore(STORE_NAME).get(publicKey as string); + const privateKey = await new Promise((resolve, reject) => { + request.onsuccess = () => { + resolve(request.result); + }; + request.onerror = () => { + reject(request.error); + }; }); if (!privateKey) { - throw new Error('Private key not found for public key: ' + publicKey); + throw new Error("Private key not found for public key: " + publicKey); } - console.log('[CrossmintShadowSigner] Key retrieved, signing...'); + console.log("[CrossmintShadowSigner] Key retrieved, signing..."); - // Sign using the non-extractable key - // The key is used for signing but cannot be exported const signature = await crypto.subtle.sign( - { name: 'Ed25519' }, + { name: "Ed25519" } as AlgorithmIdentifier, privateKey, - new Uint8Array(messageBytes) + new Uint8Array(messageBytes as number[]) ); - console.log('[CrossmintShadowSigner] ✅ Signing complete'); + console.log("[CrossmintShadowSigner] ✅ Signing complete"); result = { signatureBytes: Array.from(new Uint8Array(signature)) }; break; } default: - throw new Error('Unknown operation: ' + operation); + throw new Error("Unknown operation: " + operation); } return result; } catch (error) { - console.error('[CrossmintShadowSigner] Operation failed:', operation, error); + console.error("[CrossmintShadowSigner] Operation failed:", operation, error); throw error; } }; - console.log('[CrossmintShadowSigner] Storage handler installed in WebView'); -})(); -true; -`; + console.log("[CrossmintShadowSigner] Storage handler installed in WebView"); +} + +// Convert function to string and wrap as IIFE +// The 'true;' at the end is required by React Native WebView's injectJavaScript +export const SHADOW_SIGNER_STORAGE_INJECTED_JS = `(${webviewStorageInjectedCode.toString()})();\ntrue;`; diff --git a/packages/wallets/src/signers/shadow-signer/index.ts b/packages/wallets/src/signers/shadow-signer/index.ts index 4ca87d981..e0ca89645 100644 --- a/packages/wallets/src/signers/shadow-signer/index.ts +++ b/packages/wallets/src/signers/shadow-signer/index.ts @@ -18,7 +18,7 @@ export type ShadowSignerResult = { }; export interface ShadowSignerStorage { - keyGenerator(chain: string): Promise; + keyGenerator(): Promise; sign(publicKey: string, data: Uint8Array): Promise; storeMetadata(walletAddress: string, data: ShadowSignerData): Promise; getMetadata(walletAddress: string): Promise; @@ -46,14 +46,16 @@ export async function generateShadowSigner( ): Promise { const storageInstance = storage ?? getStorage(); if (chain === "solana" || chain === "stellar") { - const publicKeyBase64 = await storageInstance.keyGenerator(chain); + const publicKeyBase64 = await storageInstance.keyGenerator(); const publicKeyBuffer = Buffer.from(publicKeyBase64, "base64"); const publicKeyBytes = new Uint8Array(publicKeyBuffer); let encodedPublicKey: string; if (chain === "stellar") { + // Stellar uses Ed25519 encoding (Base32 with version byte and checksum) encodedPublicKey = encodeEd25519PublicKey(publicKeyBytes); } else { + // Solana uses Base58 encoding encodedPublicKey = encodeBase58(publicKeyBytes); } @@ -107,7 +109,7 @@ export async function getShadowSigner( console.log("[getShadowSigner] Result:", result ? "found" : "not found"); return result; } catch (error) { - console.error("[getShadowSigner] Failed to get shadow signer:", error); + console.warn("[getShadowSigner] Failed to get shadow signer:", error); return null; } } diff --git a/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts b/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts index 0935cf421..00cd38a22 100644 --- a/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts +++ b/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts @@ -19,26 +19,23 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage { }); } - async keyGenerator(chain: string): Promise { - if (chain === "solana" || chain === "stellar") { - const keyPair = (await window.crypto.subtle.generateKey( - { - name: "Ed25519", - namedCurve: "Ed25519", - } as AlgorithmIdentifier, - false, - ["sign", "verify"] - )) as CryptoKeyPair; - - const publicKeyBuffer = await window.crypto.subtle.exportKey("raw", keyPair.publicKey); - const publicKeyBytes = new Uint8Array(publicKeyBuffer); - const publicKeyBase64 = Buffer.from(publicKeyBytes).toString("base64"); - - await this.storePrivateKeyByPublicKey(publicKeyBase64, keyPair.privateKey); - - return publicKeyBase64; - } - throw new Error("Unsupported chain for browser shadow signer"); + async keyGenerator(): Promise { + const keyPair = (await window.crypto.subtle.generateKey( + { + name: "Ed25519", + namedCurve: "Ed25519", + } as AlgorithmIdentifier, + false, + ["sign", "verify"] + )) as CryptoKeyPair; + + const publicKeyBuffer = await window.crypto.subtle.exportKey("raw", keyPair.publicKey); + const publicKeyBytes = new Uint8Array(publicKeyBuffer); + const publicKeyBase64 = Buffer.from(publicKeyBytes).toString("base64"); + + await this.storePrivateKeyByPublicKey(publicKeyBase64, keyPair.privateKey); + + return publicKeyBase64; } async sign(publicKeyBase64: string, data: Uint8Array): Promise { diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 5c8663b64..e5a4f4499 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -47,7 +47,7 @@ export class WalletFactory { } const existingWallet = await this.apiClient.getWallet(`me:${this.getChainType(args.chain)}:smart`); - console.log("existingWallet", existingWallet); + if (existingWallet != null && !("error" in existingWallet)) { return this.createWalletInstance(existingWallet, args); } @@ -98,7 +98,6 @@ export class WalletFactory { } public async createWallet(args: WalletCreateArgs): Promise> { - console.log("createWallet", args); await args.options?.experimental_callbacks?.onWalletCreationStart?.(); let adminSignerConfig = args.onCreateConfig?.adminSigner ?? args.signer; diff --git a/packages/wallets/tsup.config.ts b/packages/wallets/tsup.config.ts index ee36ffadb..c86148398 100644 --- a/packages/wallets/tsup.config.ts +++ b/packages/wallets/tsup.config.ts @@ -1,8 +1,3 @@ import { treeShakableConfig } from "../../tsup.config.base"; -import type { Options } from "tsup"; -const config: Options = { - ...treeShakableConfig, -}; - -export default config; +export default treeShakableConfig; From bf218e08405ee4e5c8213f9372a4cf11ecd3a4e0 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Tue, 28 Oct 2025 13:03:31 +0100 Subject: [PATCH 63/82] change back to string --- .../src/utils/webview-storage-injected.ts | 113 ++++++++---------- 1 file changed, 51 insertions(+), 62 deletions(-) diff --git a/packages/client/ui/react-native/src/utils/webview-storage-injected.ts b/packages/client/ui/react-native/src/utils/webview-storage-injected.ts index 81559c7e8..505486072 100644 --- a/packages/client/ui/react-native/src/utils/webview-storage-injected.ts +++ b/packages/client/ui/react-native/src/utils/webview-storage-injected.ts @@ -1,102 +1,93 @@ -function webviewStorageInjectedCode() { - const DB_NAME = "crossmint_shadow_keys"; - const STORE_NAME = "keys"; - let db: IDBDatabase | null = null; +// Plain JavaScript string - no function stringification +export const SHADOW_SIGNER_STORAGE_INJECTED_JS = ` +(function() { + console.log("[CrossmintShadowSigner] Starting injection..."); + + var DB_NAME = "crossmint_shadow_keys"; + var STORE_NAME = "keys"; + var db = null; // Open IndexedDB - async function openDB(): Promise { + function openDB() { if (db) { - return db; + return Promise.resolve(db); } - return new Promise((resolve, reject) => { - const request = indexedDB.open(DB_NAME, 1); - request.onerror = () => reject(request.error); - request.onsuccess = () => { + return new Promise(function(resolve, reject) { + var request = indexedDB.open(DB_NAME, 1); + request.onerror = function() { reject(request.error); }; + request.onsuccess = function() { db = request.result; resolve(db); }; - request.onupgradeneeded = (event) => { - const db = (event.target as IDBOpenDBRequest).result; - if (!db.objectStoreNames.contains(STORE_NAME)) { - db.createObjectStore(STORE_NAME); + request.onupgradeneeded = function(event) { + var database = event.target.result; + if (!database.objectStoreNames.contains(STORE_NAME)) { + database.createObjectStore(STORE_NAME); } }; }); } openDB() - .then(() => { + .then(function() { console.log("[CrossmintShadowSigner] IndexedDB ready for non-extractable key storage"); }) - .catch((e) => { + .catch(function(e) { console.error("[CrossmintShadowSigner] IndexedDB init failed:", e); }); - (window as unknown as Record).__crossmintShadowSignerStorage = async function ( - operation: string, - params: Record - ): Promise> { + window.__crossmintShadowSignerStorage = async function(operation, params) { + console.log("[CrossmintShadowSigner] Function called with operation:", operation); try { await openDB(); - let result: Record; + var result; switch (operation) { - case "generate": { + case "generate": console.log("[CrossmintShadowSigner] Generating new Ed25519 key pair (non-extractable)..."); - const keyPair = (await crypto.subtle.generateKey( - { name: "Ed25519", namedCurve: "Ed25519" } as EcKeyGenParams, + var keyPair = await crypto.subtle.generateKey( + { name: "Ed25519", namedCurve: "Ed25519" }, false, ["sign", "verify"] - )) as CryptoKeyPair; - - const publicKeyBuffer = await crypto.subtle.exportKey("raw", keyPair.publicKey); - const publicKeyBytes = new Uint8Array(publicKeyBuffer); + ); - const publicKeyBase64 = btoa(String.fromCharCode.apply(null, Array.from(publicKeyBytes))); + var publicKeyBuffer = await crypto.subtle.exportKey("raw", keyPair.publicKey); + var publicKeyBytes = new Uint8Array(publicKeyBuffer); + var publicKeyBase64 = btoa(String.fromCharCode.apply(null, Array.from(publicKeyBytes))); - console.log( - "[CrossmintShadowSigner] Key pair generated, storing in IndexedDB with public key as index..." - ); + console.log("[CrossmintShadowSigner] Key pair generated, storing in IndexedDB..."); if (!db) { throw new Error("Database not initialized"); } - const tx = db.transaction([STORE_NAME], "readwrite"); + var tx = db.transaction([STORE_NAME], "readwrite"); tx.objectStore(STORE_NAME).put(keyPair.privateKey, publicKeyBase64); - await new Promise((resolve, reject) => { - tx.oncomplete = () => { - resolve(); - }; - tx.onerror = () => { - reject(tx.error); - }; + await new Promise(function(resolve, reject) { + tx.oncomplete = function() { resolve(); }; + tx.onerror = function() { reject(tx.error); }; }); - console.log("[CrossmintShadowSigner] Private key stored in IndexedDB[publicKey]"); + console.log("[CrossmintShadowSigner] Private key stored in IndexedDB"); console.log("[CrossmintShadowSigner] ✅ Key generation complete"); result = { publicKeyBytes: Array.from(publicKeyBytes) }; break; - } - case "sign": { - const { publicKey, messageBytes } = params; + case "sign": + var publicKey = params.publicKey; + var messageBytes = params.messageBytes; console.log("[CrossmintShadowSigner] Retrieving key from IndexedDB for signing..."); if (!db) { throw new Error("Database not initialized"); } - const tx = db.transaction([STORE_NAME], "readonly"); - const request = tx.objectStore(STORE_NAME).get(publicKey as string); - const privateKey = await new Promise((resolve, reject) => { - request.onsuccess = () => { - resolve(request.result); - }; - request.onerror = () => { - reject(request.error); - }; + var tx = db.transaction([STORE_NAME], "readonly"); + var request = tx.objectStore(STORE_NAME).get(publicKey); + var privateKey = await new Promise(function(resolve, reject) { + request.onsuccess = function() { resolve(request.result); }; + request.onerror = function() { reject(request.error); }; }); if (!privateKey) { @@ -105,16 +96,15 @@ function webviewStorageInjectedCode() { console.log("[CrossmintShadowSigner] Key retrieved, signing..."); - const signature = await crypto.subtle.sign( - { name: "Ed25519" } as AlgorithmIdentifier, + var signature = await crypto.subtle.sign( + { name: "Ed25519" }, privateKey, - new Uint8Array(messageBytes as number[]) + new Uint8Array(messageBytes) ); console.log("[CrossmintShadowSigner] ✅ Signing complete"); result = { signatureBytes: Array.from(new Uint8Array(signature)) }; break; - } default: throw new Error("Unknown operation: " + operation); @@ -128,8 +118,7 @@ function webviewStorageInjectedCode() { }; console.log("[CrossmintShadowSigner] Storage handler installed in WebView"); -} - -// Convert function to string and wrap as IIFE -// The 'true;' at the end is required by React Native WebView's injectJavaScript -export const SHADOW_SIGNER_STORAGE_INJECTED_JS = `(${webviewStorageInjectedCode.toString()})();\ntrue;`; + console.log("[CrossmintShadowSigner] Function type:", typeof window.__crossmintShadowSignerStorage); +})(); +true; +`; From 30827d667df23639d4f80655c39278aefb8fbae9 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Tue, 28 Oct 2025 13:09:30 +0100 Subject: [PATCH 64/82] revert chain change --- apps/wallets/smart-wallet/expo/app/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/wallets/smart-wallet/expo/app/index.tsx b/apps/wallets/smart-wallet/expo/app/index.tsx index 5f105d7fe..6966f5617 100644 --- a/apps/wallets/smart-wallet/expo/app/index.tsx +++ b/apps/wallets/smart-wallet/expo/app/index.tsx @@ -73,7 +73,7 @@ export default function Index() { } setIsLoading(true); try { - await getOrCreateWallet({ chain: "solana", signer: { type: "email" } }); + await getOrCreateWallet({ chain: "base-sepolia", signer: { type: "email" } }); } catch (error) { console.error("Error initializing wallet:", error); } finally { @@ -127,7 +127,7 @@ export default function Index() { } setIsLoading(true); try { - const tx = await wallet.send(recipientAddress, "usdc", amount); + const tx = await wallet.send(recipientAddress, "xlm", amount); console.log(`Sent ${amount} USDC to ${recipientAddress}. Tx Link: ${tx.explorerLink}`); setTxLink(tx.explorerLink); setRecipientAddress(""); From ad5c5d5383a8a5ac56b2071924762e1e18f4f75b Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Tue, 28 Oct 2025 13:09:59 +0100 Subject: [PATCH 65/82] move back to usdc --- apps/wallets/smart-wallet/expo/app/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/wallets/smart-wallet/expo/app/index.tsx b/apps/wallets/smart-wallet/expo/app/index.tsx index 6966f5617..56bd59383 100644 --- a/apps/wallets/smart-wallet/expo/app/index.tsx +++ b/apps/wallets/smart-wallet/expo/app/index.tsx @@ -127,7 +127,7 @@ export default function Index() { } setIsLoading(true); try { - const tx = await wallet.send(recipientAddress, "xlm", amount); + const tx = await wallet.send(recipientAddress, "usdc", amount); console.log(`Sent ${amount} USDC to ${recipientAddress}. Tx Link: ${tx.explorerLink}`); setTxLink(tx.explorerLink); setRecipientAddress(""); From 099b4561e43b6204527b0e142265ad6cad4bdd2c Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Tue, 28 Oct 2025 13:55:24 +0100 Subject: [PATCH 66/82] fix browser storage --- .../src/signers/shadow-signer/index.ts | 22 ++++++++----------- .../wallets/src/wallets/wallet-factory.ts | 5 ++--- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/wallets/src/signers/shadow-signer/index.ts b/packages/wallets/src/signers/shadow-signer/index.ts index e0ca89645..867c27bf1 100644 --- a/packages/wallets/src/signers/shadow-signer/index.ts +++ b/packages/wallets/src/signers/shadow-signer/index.ts @@ -24,20 +24,15 @@ export interface ShadowSignerStorage { getMetadata(walletAddress: string): Promise; } -let storageInstance: ShadowSignerStorage | null = null; - export function getStorage(): ShadowSignerStorage { - if (!storageInstance) { - const isReactNative = typeof navigator !== "undefined" && navigator.product === "ReactNative"; - const isExpo = typeof global !== "undefined" && (global as { expo?: unknown }).expo; + const isReactNative = typeof navigator !== "undefined" && navigator.product === "ReactNative"; + const isExpo = typeof global !== "undefined" && (global as { expo?: unknown }).expo; - if (isReactNative || isExpo) { - throw new Error("ReactNativeShadowSignerStorage must be provided explicitly for React Native environments"); - } else { - storageInstance = new BrowserShadowSignerStorage(); - } + if (isReactNative || isExpo) { + throw new Error("ReactNativeShadowSignerStorage must be provided explicitly for React Native environments"); + } else { + return new BrowserShadowSignerStorage(); } - return storageInstance; } export async function generateShadowSigner( @@ -77,8 +72,9 @@ export async function storeShadowSigner( chain: Chain, publicKey: string, publicKeyBase64: string, - storage: ShadowSignerStorage + storage?: ShadowSignerStorage ): Promise { + const storageInstance = storage ?? getStorage(); try { console.log("[storeShadowSigner] Storing metadata for wallet:", walletAddress, "publicKey:", publicKey); @@ -90,7 +86,7 @@ export async function storeShadowSigner( createdAt: Date.now(), }; - await storage.storeMetadata(walletAddress, data); + await storageInstance.storeMetadata(walletAddress, data); console.log("[storeShadowSigner] Metadata stored successfully"); } catch (error) { console.error("Failed to store shadow signer metadata:", error); diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index e5a4f4499..4af491ce3 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -126,14 +126,13 @@ export class WalletFactory { if ("error" in walletResponse) { throw new WalletCreationError(JSON.stringify(walletResponse)); } - - if (shadowSignerPublicKey != null && shadowSignerPublicKeyBase64 != null && args.options?.shadowSignerStorage) { + if (shadowSignerPublicKey != null && shadowSignerPublicKeyBase64 != null) { await storeShadowSigner( walletResponse.address, args.chain, shadowSignerPublicKey, shadowSignerPublicKeyBase64, - args.options.shadowSignerStorage + args.options?.shadowSignerStorage ); } From 12a693cbf30ea6525ab680e50da460557d848f0b Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Tue, 28 Oct 2025 14:00:38 +0100 Subject: [PATCH 67/82] add changeset --- .changeset/heavy-cobras-dance.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/heavy-cobras-dance.md diff --git a/.changeset/heavy-cobras-dance.md b/.changeset/heavy-cobras-dance.md new file mode 100644 index 000000000..2fa382ddf --- /dev/null +++ b/.changeset/heavy-cobras-dance.md @@ -0,0 +1,7 @@ +--- +"@crossmint/client-sdk-react-native-ui": minor +"@crossmint/client-sdk-react-base": minor +"@crossmint/wallets-sdk": minor +--- + +Support of Shadow Signers in React Native From 5e65a97588646d4d4d0bc0ceab7b8b7879453140 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Wed, 29 Oct 2025 13:57:59 +0100 Subject: [PATCH 68/82] add signer evm, needs to fix signature --- .../src/utils/WebViewShadowSignerStorage.ts | 8 +- .../src/utils/webview-storage-injected.ts | 75 +++++++++++++---- packages/common/base/src/types/signers.ts | 6 ++ .../wallets/src/signers/evm-p256-keypair.ts | 70 ++++++++++++++++ packages/wallets/src/signers/index.ts | 7 +- .../signers/non-custodial/ncs-evm-signer.ts | 30 +++++-- .../src/signers/non-custodial/ncs-signer.ts | 21 +++-- .../src/signers/shadow-signer/index.ts | 34 ++++++-- .../shadow-signer-storage-browser.ts | 84 +++++++++++++++++-- packages/wallets/src/signers/types.ts | 16 +++- .../wallets/src/wallets/wallet-factory.ts | 34 ++++++-- 11 files changed, 325 insertions(+), 60 deletions(-) create mode 100644 packages/wallets/src/signers/evm-p256-keypair.ts diff --git a/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts b/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts index 6f4959403..545c491ce 100644 --- a/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts +++ b/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts @@ -161,8 +161,8 @@ true; } } - async keyGenerator(): Promise { - const publicKeyBytes = await this.generateKeyInWebView(); + async keyGenerator(chain: string): Promise { + const publicKeyBytes = await this.generateKeyInWebView(chain); const publicKeyBase64 = Buffer.from(publicKeyBytes).toString("base64"); return publicKeyBase64; @@ -172,8 +172,8 @@ true; return await this.signInWebView(publicKeyBase64, data); } - private async generateKeyInWebView(): Promise { - const response = await this.callWebViewFunction("generate", {}); + private async generateKeyInWebView(chain: string): Promise { + const response = await this.callWebViewFunction("generate", { chain }); const publicKeyBytes = response.publicKeyBytes as number[]; return new Uint8Array(publicKeyBytes); } diff --git a/packages/client/ui/react-native/src/utils/webview-storage-injected.ts b/packages/client/ui/react-native/src/utils/webview-storage-injected.ts index 505486072..b507c1e49 100644 --- a/packages/client/ui/react-native/src/utils/webview-storage-injected.ts +++ b/packages/client/ui/react-native/src/utils/webview-storage-injected.ts @@ -44,13 +44,33 @@ export const SHADOW_SIGNER_STORAGE_INJECTED_JS = ` switch (operation) { case "generate": - console.log("[CrossmintShadowSigner] Generating new Ed25519 key pair (non-extractable)..."); - - var keyPair = await crypto.subtle.generateKey( - { name: "Ed25519", namedCurve: "Ed25519" }, - false, - ["sign", "verify"] - ); + var chain = params.chain || "solana"; + var isEVM = chain !== "solana" && chain !== "stellar"; + + console.log("[CrossmintShadowSigner] Generating new key pair for chain:", chain, "isEVM:", isEVM); + + var keyPair; + var algorithm; + + if (isEVM) { + // For EVM chains, use P256 (secp256r1) + keyPair = await crypto.subtle.generateKey( + { name: "ECDSA", namedCurve: "P-256" }, + false, + ["sign", "verify"] + ); + algorithm = "P-256"; + console.log("[CrossmintShadowSigner] Generated P-256 key pair (non-extractable)"); + } else { + // For Solana/Stellar, use Ed25519 + keyPair = await crypto.subtle.generateKey( + { name: "Ed25519", namedCurve: "Ed25519" }, + false, + ["sign", "verify"] + ); + algorithm = "Ed25519"; + console.log("[CrossmintShadowSigner] Generated Ed25519 key pair (non-extractable)"); + } var publicKeyBuffer = await crypto.subtle.exportKey("raw", keyPair.publicKey); var publicKeyBytes = new Uint8Array(publicKeyBuffer); @@ -63,12 +83,13 @@ export const SHADOW_SIGNER_STORAGE_INJECTED_JS = ` } var tx = db.transaction([STORE_NAME], "readwrite"); tx.objectStore(STORE_NAME).put(keyPair.privateKey, publicKeyBase64); + tx.objectStore(STORE_NAME).put(algorithm, publicKeyBase64 + "_algorithm"); await new Promise(function(resolve, reject) { tx.oncomplete = function() { resolve(); }; tx.onerror = function() { reject(tx.error); }; }); - console.log("[CrossmintShadowSigner] Private key stored in IndexedDB"); + console.log("[CrossmintShadowSigner] Private key and algorithm stored in IndexedDB"); console.log("[CrossmintShadowSigner] ✅ Key generation complete"); result = { publicKeyBytes: Array.from(publicKeyBytes) }; @@ -84,23 +105,41 @@ export const SHADOW_SIGNER_STORAGE_INJECTED_JS = ` throw new Error("Database not initialized"); } var tx = db.transaction([STORE_NAME], "readonly"); - var request = tx.objectStore(STORE_NAME).get(publicKey); + var keyRequest = tx.objectStore(STORE_NAME).get(publicKey); + var algorithmRequest = tx.objectStore(STORE_NAME).get(publicKey + "_algorithm"); + var privateKey = await new Promise(function(resolve, reject) { - request.onsuccess = function() { resolve(request.result); }; - request.onerror = function() { reject(request.error); }; + keyRequest.onsuccess = function() { resolve(keyRequest.result); }; + keyRequest.onerror = function() { reject(keyRequest.error); }; + }); + + var algorithm = await new Promise(function(resolve, reject) { + algorithmRequest.onsuccess = function() { resolve(algorithmRequest.result); }; + algorithmRequest.onerror = function() { resolve("Ed25519"); }; // Default to Ed25519 }); if (!privateKey) { throw new Error("Private key not found for public key: " + publicKey); } - console.log("[CrossmintShadowSigner] Key retrieved, signing..."); - - var signature = await crypto.subtle.sign( - { name: "Ed25519" }, - privateKey, - new Uint8Array(messageBytes) - ); + console.log("[CrossmintShadowSigner] Key retrieved, signing with algorithm:", algorithm); + + var signature; + if (algorithm === "P-256") { + // For P256, use ECDSA with SHA-256 + signature = await crypto.subtle.sign( + { name: "ECDSA", hash: { name: "SHA-256" } }, + privateKey, + new Uint8Array(messageBytes) + ); + } else { + // Default to Ed25519 + signature = await crypto.subtle.sign( + { name: "Ed25519" }, + privateKey, + new Uint8Array(messageBytes) + ); + } console.log("[CrossmintShadowSigner] ✅ Signing complete"); result = { signatureBytes: Array.from(new Uint8Array(signature)) }; diff --git a/packages/common/base/src/types/signers.ts b/packages/common/base/src/types/signers.ts index b40231b02..d1e4f99c3 100644 --- a/packages/common/base/src/types/signers.ts +++ b/packages/common/base/src/types/signers.ts @@ -25,3 +25,9 @@ export type SolanaExternalWalletSignerConfig = BaseExternalWalletSignerConfig & export type StellarExternalWalletSignerConfig = BaseExternalWalletSignerConfig & { onSignStellarTransaction?: (transaction: string) => Promise; }; + +export type EVM256KeypairSignerConfig = { + type: "evm-p256-keypair"; + publicKey: string; + chain: string; +}; diff --git a/packages/wallets/src/signers/evm-p256-keypair.ts b/packages/wallets/src/signers/evm-p256-keypair.ts new file mode 100644 index 000000000..101282ffe --- /dev/null +++ b/packages/wallets/src/signers/evm-p256-keypair.ts @@ -0,0 +1,70 @@ +import type { Signer, EVM256KeypairInternalSignerConfig } from "./types"; +import { concat, toHex, sha256 } from "viem"; + +export class EVM256KeypairSigner implements Signer { + type = "evm-p256-keypair" as const; + private publicKey: string; + private chain: string; + private _locator: string; + private onSignTransaction: (publicKeyBase64: string, data: Uint8Array) => Promise; + private readonly STUB_ORIGIN = "https://crossmint.com"; + + constructor(config: EVM256KeypairInternalSignerConfig) { + this.chain = config.chain; + this.publicKey = config.publicKey; + this._locator = config.locator; + this.onSignTransaction = config.onSignTransaction; + } + + address() { + return this.publicKey; + } + + locator() { + return this._locator; + } + + async signMessage(message: string): Promise<{ signature: string }> { + return await this.createWebAuthnSignature(message); + } + + async signTransaction(transaction: string): Promise<{ signature: string }> { + return await this.createWebAuthnSignature(transaction); + } + + private async createWebAuthnSignature(challenge: string): Promise<{ signature: string }> { + const STUB_ORIGIN = "https://crossmint.com"; + + // 1. Create clientDataJSON with base64url encoded challenge + const challengeHex = challenge.replace("0x", ""); + const challengeBase64 = Buffer.from(challengeHex, "hex").toString("base64"); + const challengeBase64url = challengeBase64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + + const clientDataJSON = JSON.stringify({ + type: "webauthn.get", + challenge: challengeBase64url, + origin: STUB_ORIGIN, + crossOrigin: false, + }); + + // 2. Create authenticatorData + const rpIdHashHex = sha256(toHex(new TextEncoder().encode(STUB_ORIGIN))); + const flags = toHex(new Uint8Array([0x05])); + const signCount = toHex(new Uint8Array([0x00, 0x00, 0x00, 0x00])); + const authenticatorData = concat([rpIdHashHex, flags, signCount]); + + // 3. Create signature message + const clientDataHash = sha256(toHex(new TextEncoder().encode(clientDataJSON))); + const signatureMessage = concat([authenticatorData, clientDataHash]); + + // 4. Sign with P256 private key + const signatureMessageBytes = new Uint8Array(Buffer.from(signatureMessage.slice(2), "hex")); + const signatureBytes = await this.onSignTransaction(this.publicKey, signatureMessageBytes); + + // 5. Return r + s as hex string (backend will encode to WebAuthn) + const rHex = Buffer.from(signatureBytes.slice(0, 32)).toString("hex").padStart(64, "0"); + const sHex = Buffer.from(signatureBytes.slice(32, 64)).toString("hex").padStart(64, "0"); + + return { signature: "0x" + rHex + sHex }; + } +} diff --git a/packages/wallets/src/signers/index.ts b/packages/wallets/src/signers/index.ts index 204b9c899..b2e438490 100644 --- a/packages/wallets/src/signers/index.ts +++ b/packages/wallets/src/signers/index.ts @@ -7,6 +7,7 @@ import { SolanaApiKeySigner } from "./solana-api-key"; import type { Chain } from "../chains/chains"; import type { InternalSignerConfig, Signer } from "./types"; import { StellarExternalWalletSigner } from "./stellar-external-wallet"; +import { EVM256KeypairSigner } from "./evm-p256-keypair"; import type { ShadowSignerStorage } from "./shadow-signer"; export function assembleSigner( @@ -24,7 +25,7 @@ export function assembleSigner( if (chain === "stellar") { return new StellarNonCustodialSigner(config, walletAddress, shadowSignerStorage); } - return new EVMNonCustodialSigner(config, shadowSignerStorage); + return new EVMNonCustodialSigner(config, walletAddress, shadowSignerStorage); case "api-key": return chain === "solana" ? new SolanaApiKeySigner(config) : new EVMApiKeySigner(config); @@ -37,6 +38,10 @@ export function assembleSigner( } return new EVMExternalWalletSigner(config); + case "evm-p256-keypair": { + return new EVM256KeypairSigner(config); + } + case "passkey": return new PasskeySigner(config); } diff --git a/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts index 3cd2c896d..3dc88f48a 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts @@ -1,20 +1,18 @@ -import type { - EmailInternalSignerConfig, - ExternalWalletInternalSignerConfig, - PhoneInternalSignerConfig, -} from "../types"; +import type { EmailInternalSignerConfig, PhoneInternalSignerConfig, EVM256KeypairInternalSignerConfig } 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"; -import type { ShadowSignerStorage } from "@/signers/shadow-signer"; +import type { ShadowSignerData, ShadowSignerStorage } from "@/signers/shadow-signer"; +import { EVM256KeypairSigner } from "../evm-p256-keypair"; export class EVMNonCustodialSigner extends NonCustodialSigner { constructor( config: EmailInternalSignerConfig | PhoneInternalSignerConfig, + walletAddress: string, shadowSignerStorage?: ShadowSignerStorage ) { super(config, shadowSignerStorage); + this.initializeShadowSigner(walletAddress, EVM256KeypairSigner); } async signMessage(message: string) { @@ -24,6 +22,9 @@ export class EVMNonCustodialSigner extends NonCustodialSigner { } async signTransaction(transaction: string): Promise<{ signature: string }> { + if (this.shadowSigner != null) { + return await this.shadowSigner.signTransaction(transaction); + } return await this.sign(transaction); } @@ -74,7 +75,18 @@ export class EVMNonCustodialSigner extends NonCustodialSigner { } } - protected getShadowSignerConfig(): ExternalWalletInternalSignerConfig { - throw new Error("Shadow signer not implemented for EVM chains"); + protected getShadowSignerConfig( + shadowSigner: ShadowSignerData, + _walletAddress: string + ): EVM256KeypairInternalSignerConfig { + return { + type: "evm-p256-keypair", + publicKey: shadowSigner.publicKey, + chain: shadowSigner.chain, + locator: `evm-p256-keypair:${shadowSigner.chain}:${shadowSigner.publicKey}`, + onSignTransaction: async (pubKey: string, data: Uint8Array) => { + return await this.shadowSignerStorage.sign(pubKey, data); + }, + }; } } diff --git a/packages/wallets/src/signers/non-custodial/ncs-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-signer.ts index 3b1d15869..6140bdbff 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-signer.ts @@ -1,6 +1,7 @@ import type { BaseSignResult, EmailInternalSignerConfig, + EVM256KeypairInternalSignerConfig, ExternalWalletInternalSignerConfig, PhoneInternalSignerConfig, Signer, @@ -14,6 +15,7 @@ import type { Chain } from "../../chains/chains"; import type { ExternalWalletSigner } from "../external-wallet-signer"; import type { ShadowSignerStorage } from "@/signers/shadow-signer"; import { getStorage } from "../shadow-signer"; +import type { EVM256KeypairSigner } from "../evm-p256-keypair"; export abstract class NonCustodialSigner implements Signer { public readonly type: "email" | "phone"; @@ -24,8 +26,8 @@ export abstract class NonCustodialSigner implements Signer { reject: (error: Error) => void; } | null = null; private _initializationPromise: Promise | null = null; - protected shadowSigner: ExternalWalletSigner | null = null; - protected shadowSignerStorage?: ShadowSignerStorage; + protected shadowSigner: ExternalWalletSigner | EVM256KeypairSigner | null = null; + protected shadowSignerStorage: ShadowSignerStorage; constructor( protected config: EmailInternalSignerConfig | PhoneInternalSignerConfig, @@ -298,18 +300,25 @@ export abstract class NonCustodialSigner implements Signer { protected abstract getShadowSignerConfig( shadowSigner: ShadowSignerData, walletAddress: string - ): ExternalWalletInternalSignerConfig; + ): ExternalWalletInternalSignerConfig | EVM256KeypairInternalSignerConfig; protected async initializeShadowSigner( walletAddress: string, - ExternalWalletSignerClass: new (config: ExternalWalletInternalSignerConfig) => ExternalWalletSigner + ExternalWalletSignerClass: + | (new ( + config: ExternalWalletInternalSignerConfig + ) => ExternalWalletSigner) + | (new ( + config: EVM256KeypairInternalSignerConfig + ) => EVM256KeypairSigner) ) { if (await hasShadowSigner(walletAddress, this.shadowSignerStorage)) { const shadowSigner = await getShadowSigner(walletAddress, this.shadowSignerStorage); if (shadowSigner != null && this.config.shadowSigner?.enabled !== false) { + const signerConfig = this.getShadowSignerConfig(shadowSigner, walletAddress); this.shadowSigner = new ExternalWalletSignerClass( - this.getShadowSignerConfig(shadowSigner, walletAddress) as ExternalWalletInternalSignerConfig - ); + signerConfig as ExternalWalletInternalSignerConfig & EVM256KeypairInternalSignerConfig + ) as ExternalWalletSigner | EVM256KeypairSigner; } } } diff --git a/packages/wallets/src/signers/shadow-signer/index.ts b/packages/wallets/src/signers/shadow-signer/index.ts index 867c27bf1..71e608e58 100644 --- a/packages/wallets/src/signers/shadow-signer/index.ts +++ b/packages/wallets/src/signers/shadow-signer/index.ts @@ -2,7 +2,7 @@ import { encode as encodeBase58 } from "bs58"; import type { Chain } from "@/chains/chains"; import { encodeEd25519PublicKey } from "./encodeEd25519PublicKey"; import { BrowserShadowSignerStorage } from "./shadow-signer-storage-browser"; -import type { BaseExternalWalletSignerConfig } from "@crossmint/common-sdk-base"; +import type { BaseExternalWalletSignerConfig, EVM256KeypairSignerConfig } from "@crossmint/common-sdk-base"; export type ShadowSignerData = { chain: Chain; @@ -13,12 +13,12 @@ export type ShadowSignerData = { }; export type ShadowSignerResult = { - shadowSigner: BaseExternalWalletSignerConfig; + shadowSigner: BaseExternalWalletSignerConfig | EVM256KeypairSignerConfig; publicKey: string; }; export interface ShadowSignerStorage { - keyGenerator(): Promise; + keyGenerator(chain: Chain): Promise; sign(publicKey: string, data: Uint8Array): Promise; storeMetadata(walletAddress: string, data: ShadowSignerData): Promise; getMetadata(walletAddress: string): Promise; @@ -40,8 +40,9 @@ export async function generateShadowSigner( storage?: ShadowSignerStorage ): Promise { const storageInstance = storage ?? getStorage(); + const publicKeyBase64 = await storageInstance.keyGenerator(chain); + if (chain === "solana" || chain === "stellar") { - const publicKeyBase64 = await storageInstance.keyGenerator(); const publicKeyBuffer = Buffer.from(publicKeyBase64, "base64"); const publicKeyBytes = new Uint8Array(publicKeyBuffer); @@ -49,9 +50,11 @@ export async function generateShadowSigner( if (chain === "stellar") { // Stellar uses Ed25519 encoding (Base32 with version byte and checksum) encodedPublicKey = encodeEd25519PublicKey(publicKeyBytes); - } else { + } else if (chain === "solana") { // Solana uses Base58 encoding encodedPublicKey = encodeBase58(publicKeyBytes); + } else { + throw new Error("Unsupported chain"); } return { @@ -63,8 +66,25 @@ export async function generateShadowSigner( publicKeyBase64, }; } - // TODO: Add support for EVM chains - throw new Error("Unsupported chain"); + + // For EVM chains, extract x and y coordinates from P256 public key + const publicKeyBuffer = Buffer.from(publicKeyBase64, "base64"); + const publicKeyBytes = new Uint8Array(publicKeyBuffer); + + // P256 public key format: 1 byte (0x04) + 32 bytes (x) + 32 bytes (y) + if (publicKeyBytes.length !== 65 || publicKeyBytes[0] !== 0x04) { + throw new Error("Invalid P256 public key format"); + } + + return { + shadowSigner: { + type: "evm-p256-keypair", + publicKey: publicKeyBase64, + chain, + }, + publicKey: publicKeyBase64, + publicKeyBase64, + }; } export async function storeShadowSigner( diff --git a/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts b/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts index 00cd38a22..3a2402dc4 100644 --- a/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts +++ b/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts @@ -1,5 +1,5 @@ import type { ShadowSignerData, ShadowSignerStorage } from "."; - +import type { Chain } from "../../chains/chains"; export class BrowserShadowSignerStorage implements ShadowSignerStorage { private readonly SHADOW_SIGNER_DB_NAME = "crossmint_shadow_keys"; private readonly SHADOW_SIGNER_DB_STORE = "keys"; @@ -19,12 +19,32 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage { }); } - async keyGenerator(): Promise { + async keyGenerator(chain: Chain): Promise { + if (chain === "solana" || chain === "stellar") { + const keyPair = (await window.crypto.subtle.generateKey( + { + name: "Ed25519", + namedCurve: "Ed25519", + } as AlgorithmIdentifier, + false, + ["sign", "verify"] + )) as CryptoKeyPair; + + const publicKeyBuffer = await window.crypto.subtle.exportKey("raw", keyPair.publicKey); + const publicKeyBytes = new Uint8Array(publicKeyBuffer); + const publicKeyBase64 = Buffer.from(publicKeyBytes).toString("base64"); + + await this.storePrivateKeyByPublicKey(publicKeyBase64, keyPair.privateKey); + + return publicKeyBase64; + } + + // For EVM chains, use P256 (secp256r1) const keyPair = (await window.crypto.subtle.generateKey( { - name: "Ed25519", - namedCurve: "Ed25519", - } as AlgorithmIdentifier, + name: "ECDSA", + namedCurve: "P-256", + }, false, ["sign", "verify"] )) as CryptoKeyPair; @@ -34,6 +54,7 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage { const publicKeyBase64 = Buffer.from(publicKeyBytes).toString("base64"); await this.storePrivateKeyByPublicKey(publicKeyBase64, keyPair.privateKey); + await this.storeKeyAlgorithm(publicKeyBase64, "P-256"); return publicKeyBase64; } @@ -44,6 +65,22 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage { throw new Error(`No private key found for public key: ${publicKeyBase64}`); } + const algorithm = await this.getKeyAlgorithm(publicKeyBase64); + + if (algorithm === "P-256") { + // For P256, use ECDSA with SHA-256 + const signature = await window.crypto.subtle.sign( + { + name: "ECDSA", + hash: { name: "SHA-256" }, + }, + privateKey, + data as BufferSource + ); + return new Uint8Array(signature); + } + + // Default to Ed25519 for Solana/Stellar const signature = await window.crypto.subtle.sign({ name: "Ed25519" }, privateKey, data as BufferSource); return new Uint8Array(signature); @@ -86,6 +123,43 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage { } } + private async storeKeyAlgorithm(publicKey: string, algorithm: string): Promise { + if (typeof indexedDB === "undefined") { + return; + } + + const db = await this.openDB(); + const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readwrite"); + const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); + store.put(algorithm, `${publicKey}_algorithm`); + + return new Promise((resolve, reject) => { + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + } + + private async getKeyAlgorithm(publicKey: string): Promise { + if (typeof indexedDB === "undefined") { + return null; + } + + try { + const db = await this.openDB(); + const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readonly"); + const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); + const request = store.get(`${publicKey}_algorithm`); + + return new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result || null); + request.onerror = () => reject(request.error); + }); + } catch (error) { + console.warn("Failed to retrieve key algorithm from IndexedDB:", error); + return null; + } + } + storeMetadata(walletAddress: string, data: ShadowSignerData): Promise { if (typeof localStorage === "undefined") { return Promise.resolve(); diff --git a/packages/wallets/src/signers/types.ts b/packages/wallets/src/signers/types.ts index c558df2f6..e2104dfe0 100644 --- a/packages/wallets/src/signers/types.ts +++ b/packages/wallets/src/signers/types.ts @@ -3,6 +3,7 @@ import type { HandshakeParent } from "@crossmint/client-sdk-window"; import type { signerInboundEvents, signerOutboundEvents } from "@crossmint/client-signers"; import type { Crossmint, + EVM256KeypairSignerConfig, EvmExternalWalletSignerConfig, SolanaExternalWalletSignerConfig, StellarExternalWalletSignerConfig, @@ -10,6 +11,7 @@ import type { import type { Chain, SolanaChain, StellarChain } from "../chains/chains"; export type { + EVM256KeypairSignerConfig, EvmExternalWalletSignerConfig, SolanaExternalWalletSignerConfig, StellarExternalWalletSignerConfig, @@ -64,7 +66,10 @@ export type ExternalWalletSignerConfigForChain = C extends Sola export type ApiKeySignerConfig = { type: "api-key" }; -export type BaseSignerConfig = ExternalWalletSignerConfigForChain | ApiKeySignerConfig; +export type BaseSignerConfig = + | ExternalWalletSignerConfigForChain + | ApiKeySignerConfig + | (C extends SolanaChain | StellarChain ? never : EVM256KeypairSignerConfig); export type PasskeySignerConfig = { type: "passkey"; @@ -103,12 +108,18 @@ export type ExternalWalletInternalSignerConfig = ExternalWallet locator: string; }; +export type EVM256KeypairInternalSignerConfig = EVM256KeypairSignerConfig & { + locator: string; + onSignTransaction: (publicKeyBase64: string, data: Uint8Array) => Promise; +}; + export type InternalSignerConfig = | EmailInternalSignerConfig | PhoneInternalSignerConfig | PasskeyInternalSignerConfig | ApiKeyInternalSignerConfig - | ExternalWalletInternalSignerConfig; + | ExternalWalletInternalSignerConfig + | EVM256KeypairInternalSignerConfig; //////////////////////////////////////////////////////////// // Signers @@ -139,6 +150,7 @@ type SignResultMap = { phone: BaseSignResult; "api-key": BaseSignResult; "external-wallet": BaseSignResult; + "evm-p256-keypair": BaseSignResult; passkey: PasskeySignResult; }; diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 4af491ce3..a1c36ed86 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -15,6 +15,7 @@ import type { ApiKeyInternalSignerConfig, EmailInternalSignerConfig, EmailSignerConfig, + EVM256KeypairSignerConfig, InternalSignerConfig, PasskeyInternalSignerConfig, PasskeySignerConfig, @@ -195,6 +196,11 @@ export class WalletFactory { return { ...walletSigner, ...signerArgs } as InternalSignerConfig; } + case "evm-p256-keypair": { + const walletSigner = this.getWalletSigner(walletResponse, this.getSignerLocator(signerArgs)); + + return { ...walletSigner, ...signerArgs } as InternalSignerConfig; + } case "passkey": { const walletSigner = this.getWalletSigner( walletResponse, @@ -406,6 +412,9 @@ export class WalletFactory { if (signer.type === "api-key") { return "api-key"; } + if (signer.type === "evm-p256-keypair") { + return `evm-p256-keypair:${signer.chain}:${signer.publicKey}`; + } return signer.type; } @@ -475,7 +484,6 @@ export class WalletFactory { } private isShadowSignerEnabled( - chain: C, adminSigner: SignerConfigForChain, delegatedSigners: Array> = [] ): boolean { @@ -484,7 +492,6 @@ export class WalletFactory { ) as Array; return ( !this.apiClient.isServerSide && - (chain === "solana" || chain === "stellar") && ncSigners.length > 0 && ncSigners.some((signer) => signer.shadowSigner?.enabled !== false) ); @@ -494,9 +501,11 @@ export class WalletFactory { adminSigner: SignerConfigForChain, args: WalletCreateArgs ): Promise<{ - delegatedSigners: Array; + delegatedSigners: Array< + DelegatedSigner | RegisterSignerParams | { signer: PasskeySignerConfig | EVM256KeypairSignerConfig } + >; shadowSignerPublicKey: string | null; - shadowSignerPublicKeyBase64: string | null; + shadowSignerPublicKeyBase64?: string | null; }> { const { delegatedSigners, shadowSignerPublicKey, shadowSignerPublicKeyBase64 } = await this.addShadowSignerToDelegatedSignersIfNeeded( @@ -511,16 +520,25 @@ export class WalletFactory { private async registerDelegatedSigners( delegatedSigners?: Array> - ): Promise> { + ): Promise< + Array + > { return await Promise.all( delegatedSigners?.map( - async (signer): Promise => { + async ( + signer + ): Promise< + DelegatedSigner | RegisterSignerParams | { signer: PasskeySignerConfig | EVM256KeypairSignerConfig } + > => { if (signer.type === "passkey") { if (signer.id == null) { return { signer: await this.createPasskeySigner(signer) }; } return { signer }; } + if (signer.type === "evm-p256-keypair") { + return { signer }; + } return { signer: this.getSignerLocator(signer) }; } ) ?? [] @@ -534,9 +552,9 @@ export class WalletFactory { ): Promise<{ delegatedSigners: Array> | undefined; shadowSignerPublicKey: string | null; - shadowSignerPublicKeyBase64: string | null; + shadowSignerPublicKeyBase64?: string | null; }> { - if (this.isShadowSignerEnabled(args.chain, adminSigner, delegatedSigners)) { + if (this.isShadowSignerEnabled(adminSigner, delegatedSigners)) { try { const { shadowSigner, publicKey, publicKeyBase64 } = await generateShadowSigner( args.chain, From 6e8fe5676a78ff262f7477de44ca389e16d3b5d9 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Wed, 29 Oct 2025 15:14:57 +0100 Subject: [PATCH 69/82] make browserstorage a string to inject in webview --- apps/wallets/smart-wallet/expo/app/index.tsx | 2 +- .../src/utils/webview-storage-injected.ts | 126 ++++++------------ packages/wallets/package.json | 11 +- .../src/signers/shadow-signer/index.ts | 2 +- .../encodeEd25519PublicKey.ts | 0 packages/wallets/tsup.config.ts | 66 ++++++++- 6 files changed, 116 insertions(+), 91 deletions(-) rename packages/wallets/src/{signers/shadow-signer => utils}/encodeEd25519PublicKey.ts (100%) diff --git a/apps/wallets/smart-wallet/expo/app/index.tsx b/apps/wallets/smart-wallet/expo/app/index.tsx index 56bd59383..5f105d7fe 100644 --- a/apps/wallets/smart-wallet/expo/app/index.tsx +++ b/apps/wallets/smart-wallet/expo/app/index.tsx @@ -73,7 +73,7 @@ export default function Index() { } setIsLoading(true); try { - await getOrCreateWallet({ chain: "base-sepolia", signer: { type: "email" } }); + await getOrCreateWallet({ chain: "solana", signer: { type: "email" } }); } catch (error) { console.error("Error initializing wallet:", error); } finally { diff --git a/packages/client/ui/react-native/src/utils/webview-storage-injected.ts b/packages/client/ui/react-native/src/utils/webview-storage-injected.ts index 505486072..275d757c0 100644 --- a/packages/client/ui/react-native/src/utils/webview-storage-injected.ts +++ b/packages/client/ui/react-native/src/utils/webview-storage-injected.ts @@ -1,109 +1,63 @@ -// Plain JavaScript string - no function stringification +/** + * WebView injection script for BrowserShadowSignerStorage + * + * This file creates the complete webview injection script by: + * 1. Importing the compiled BrowserShadowSignerStorage class from @crossmint/wallets-sdk + * 2. Wrapping it with the React Native WebView API + * + * The storage class comes from: + * @crossmint/wallets-sdk/src/signers/shadow-signer/shadow-signer-storage-browser.ts + * + * To rebuild the storage class: pnpm build in packages/wallets + * + * Note: The import will show a linter error until @crossmint/wallets-sdk is built. + * The file is auto-generated during the wallets package build process. + */ + +import { BROWSER_SHADOW_SIGNER_STORAGE_SCRIPT } from "@crossmint/wallets-sdk/dist/injected/shadow-signer-storage-browser-script"; + +// Create the complete webview injection script export const SHADOW_SIGNER_STORAGE_INJECTED_JS = ` (function() { console.log("[CrossmintShadowSigner] Starting injection..."); - var DB_NAME = "crossmint_shadow_keys"; - var STORE_NAME = "keys"; - var db = null; - - // Open IndexedDB - function openDB() { - if (db) { - return Promise.resolve(db); - } - return new Promise(function(resolve, reject) { - var request = indexedDB.open(DB_NAME, 1); - request.onerror = function() { reject(request.error); }; - request.onsuccess = function() { - db = request.result; - resolve(db); - }; - request.onupgradeneeded = function(event) { - var database = event.target.result; - if (!database.objectStoreNames.contains(STORE_NAME)) { - database.createObjectStore(STORE_NAME); - } - }; - }); - } - - openDB() - .then(function() { - console.log("[CrossmintShadowSigner] IndexedDB ready for non-extractable key storage"); - }) - .catch(function(e) { - console.error("[CrossmintShadowSigner] IndexedDB init failed:", e); - }); + // Inject the compiled BrowserShadowSignerStorage class + ${BROWSER_SHADOW_SIGNER_STORAGE_SCRIPT} + + // Create storage instance + var storage = new CrossmintBrowserStorage.BrowserShadowSignerStorage(); + console.log("[CrossmintShadowSigner] Storage instance created"); + // Expose the API that React Native expects window.__crossmintShadowSignerStorage = async function(operation, params) { console.log("[CrossmintShadowSigner] Function called with operation:", operation); try { - await openDB(); var result; switch (operation) { case "generate": console.log("[CrossmintShadowSigner] Generating new Ed25519 key pair (non-extractable)..."); - - var keyPair = await crypto.subtle.generateKey( - { name: "Ed25519", namedCurve: "Ed25519" }, - false, - ["sign", "verify"] - ); - - var publicKeyBuffer = await crypto.subtle.exportKey("raw", keyPair.publicKey); - var publicKeyBytes = new Uint8Array(publicKeyBuffer); - var publicKeyBase64 = btoa(String.fromCharCode.apply(null, Array.from(publicKeyBytes))); - - console.log("[CrossmintShadowSigner] Key pair generated, storing in IndexedDB..."); - - if (!db) { - throw new Error("Database not initialized"); - } - var tx = db.transaction([STORE_NAME], "readwrite"); - tx.objectStore(STORE_NAME).put(keyPair.privateKey, publicKeyBase64); - await new Promise(function(resolve, reject) { - tx.oncomplete = function() { resolve(); }; - tx.onerror = function() { reject(tx.error); }; - }); - - console.log("[CrossmintShadowSigner] Private key stored in IndexedDB"); + var publicKeyBase64 = await storage.keyGenerator(); + var publicKeyBytes = Array.from(atob(publicKeyBase64).split('').map(function(c) { + return c.charCodeAt(0); + })); console.log("[CrossmintShadowSigner] ✅ Key generation complete"); - - result = { publicKeyBytes: Array.from(publicKeyBytes) }; + result = { publicKeyBytes: publicKeyBytes }; break; case "sign": + if (!params) { + throw new Error("Sign operation requires params"); + } + var publicKey = params.publicKey; var messageBytes = params.messageBytes; - - console.log("[CrossmintShadowSigner] Retrieving key from IndexedDB for signing..."); - - if (!db) { - throw new Error("Database not initialized"); - } - var tx = db.transaction([STORE_NAME], "readonly"); - var request = tx.objectStore(STORE_NAME).get(publicKey); - var privateKey = await new Promise(function(resolve, reject) { - request.onsuccess = function() { resolve(request.result); }; - request.onerror = function() { reject(request.error); }; - }); - - if (!privateKey) { - throw new Error("Private key not found for public key: " + publicKey); - } - - console.log("[CrossmintShadowSigner] Key retrieved, signing..."); - - var signature = await crypto.subtle.sign( - { name: "Ed25519" }, - privateKey, - new Uint8Array(messageBytes) - ); - + + console.log("[CrossmintShadowSigner] Signing..."); + var signatureBytes = await storage.sign(publicKey, new Uint8Array(messageBytes)); + console.log("[CrossmintShadowSigner] ✅ Signing complete"); - result = { signatureBytes: Array.from(new Uint8Array(signature)) }; + result = { signatureBytes: Array.from(signatureBytes) }; break; default: diff --git a/packages/wallets/package.json b/packages/wallets/package.json index 9328d0f43..3b4b3631b 100644 --- a/packages/wallets/package.json +++ b/packages/wallets/package.json @@ -7,8 +7,15 @@ "sideEffects": false, "type": "module", "exports": { - "import": "./dist/index.js", - "require": "./dist/index.cjs" + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./dist/injected/shadow-signer-storage-browser-script": { + "import": "./dist/injected/shadow-signer-storage-browser-script.js", + "require": "./dist/injected/shadow-signer-storage-browser-script.js", + "types": "./dist/injected/shadow-signer-storage-browser-script.d.ts" + } }, "main": "./dist/index.cjs", "module": "./dist/index.js", diff --git a/packages/wallets/src/signers/shadow-signer/index.ts b/packages/wallets/src/signers/shadow-signer/index.ts index 867c27bf1..905f546af 100644 --- a/packages/wallets/src/signers/shadow-signer/index.ts +++ b/packages/wallets/src/signers/shadow-signer/index.ts @@ -1,6 +1,6 @@ import { encode as encodeBase58 } from "bs58"; import type { Chain } from "@/chains/chains"; -import { encodeEd25519PublicKey } from "./encodeEd25519PublicKey"; +import { encodeEd25519PublicKey } from "../../utils/encodeEd25519PublicKey"; import { BrowserShadowSignerStorage } from "./shadow-signer-storage-browser"; import type { BaseExternalWalletSignerConfig } from "@crossmint/common-sdk-base"; diff --git a/packages/wallets/src/signers/shadow-signer/encodeEd25519PublicKey.ts b/packages/wallets/src/utils/encodeEd25519PublicKey.ts similarity index 100% rename from packages/wallets/src/signers/shadow-signer/encodeEd25519PublicKey.ts rename to packages/wallets/src/utils/encodeEd25519PublicKey.ts diff --git a/packages/wallets/tsup.config.ts b/packages/wallets/tsup.config.ts index c86148398..1300a769a 100644 --- a/packages/wallets/tsup.config.ts +++ b/packages/wallets/tsup.config.ts @@ -1,3 +1,67 @@ +import type { Options } from "tsup"; import { treeShakableConfig } from "../../tsup.config.base"; +import { readFileSync, writeFileSync, mkdirSync } from "fs"; +import { join } from "path"; -export default treeShakableConfig; +const config: Options = { + ...treeShakableConfig, + async onSuccess() { + // Build the BrowserShadowSignerStorage as a standalone injectable script + const { build } = await import("esbuild"); + + const outDir = "./dist/injected"; + mkdirSync(outDir, { recursive: true }); + + // Compile just the storage class as an IIFE for use in webviews + await build({ + entryPoints: ["./src/signers/shadow-signer/shadow-signer-storage-browser.ts"], + bundle: true, + minify: true, + format: "iife", + globalName: "CrossmintBrowserStorage", + platform: "browser", + target: "es2020", + outfile: `${outDir}/shadow-signer-storage-browser.js`, + }); + + // Read the compiled JavaScript + const jsContent = readFileSync(join(outDir, "shadow-signer-storage-browser.js"), "utf-8"); + + // Create TypeScript file that exports the script as a string + const tsContent = `// Auto-generated file - do not edit manually +// This file contains the compiled BrowserShadowSignerStorage class as an IIFE +// Source: packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts +// +// Usage: Import this in your webview wrapper to access the storage class +// The class is available as: CrossmintBrowserStorage.BrowserShadowSignerStorage + +export const BROWSER_SHADOW_SIGNER_STORAGE_SCRIPT = ${JSON.stringify(jsContent)}; +`; + + writeFileSync(join(outDir, "shadow-signer-storage-browser-script.ts"), tsContent); + + // Create the type definition file + const dtsContent = `// Auto-generated file - do not edit manually +// This file contains the compiled BrowserShadowSignerStorage class as an IIFE +// Source: packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts + +export declare const BROWSER_SHADOW_SIGNER_STORAGE_SCRIPT: string; +`; + + writeFileSync(join(outDir, "shadow-signer-storage-browser-script.d.ts"), dtsContent); + + // Compile the TypeScript export file to JavaScript + await build({ + entryPoints: [join(outDir, "shadow-signer-storage-browser-script.ts")], + bundle: false, + format: "esm", + platform: "node", + target: "es2020", + outfile: `${outDir}/shadow-signer-storage-browser-script.js`, + }); + + console.log("✓ Built injectable BrowserShadowSignerStorage script"); + }, +}; + +export default config; From bbdc645202cee810c31b8305baa876214b507e85 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Wed, 29 Oct 2025 15:19:29 +0100 Subject: [PATCH 70/82] add all the == null missing --- .../src/providers/CrossmintWalletBaseProvider.tsx | 2 +- .../ui/react-native/src/utils/WebViewShadowSignerStorage.ts | 6 +++--- .../ui/react-native/src/utils/webview-storage-injected.ts | 2 +- .../wallets/src/signers/non-custodial/ncs-stellar-signer.ts | 2 +- .../signers/shadow-signer/shadow-signer-storage-browser.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx index fc57c0610..b631ff3c4 100644 --- a/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx +++ b/packages/client/react-base/src/providers/CrossmintWalletBaseProvider.tsx @@ -8,13 +8,13 @@ import { type WalletArgsFor, type WalletCreateArgs, type PhoneSignerConfig, + type ShadowSignerStorage, } from "@crossmint/wallets-sdk"; import type { HandshakeParent } from "@crossmint/client-sdk-window"; import type { signerInboundEvents, signerOutboundEvents } from "@crossmint/client-signers"; import { useCrossmint } from "@/hooks"; import type { CreateOnLogin } from "@/types"; import cloneDeep from "lodash.clonedeep"; -import type { ShadowSignerStorage } from "@crossmint/wallets-sdk"; export type CrossmintWalletBaseContext = { wallet: Wallet | undefined; diff --git a/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts b/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts index 6f4959403..10a4a8f92 100644 --- a/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts +++ b/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts @@ -25,7 +25,7 @@ export class WebViewShadowSignerStorage implements ShadowSignerStorage { } private injectStorageHandler(): void { - if (this.isInjected || !this.webViewRef?.current) { + if (this.isInjected || this.webViewRef?.current == null) { return; } @@ -88,7 +88,7 @@ export class WebViewShadowSignerStorage implements ShadowSignerStorage { params: Record ): Promise> { const webView = this.webViewRef?.current; - if (!webView) { + if (webView == null) { throw new Error("WebView not available. Make sure to initialize() with a WebView ref."); } @@ -147,7 +147,7 @@ true; console.log("[WebViewShadowSignerStorage] Retrieved raw metadata:", stored); - if (!stored) { + if (stored == null) { return null; } diff --git a/packages/client/ui/react-native/src/utils/webview-storage-injected.ts b/packages/client/ui/react-native/src/utils/webview-storage-injected.ts index 275d757c0..75cce13b1 100644 --- a/packages/client/ui/react-native/src/utils/webview-storage-injected.ts +++ b/packages/client/ui/react-native/src/utils/webview-storage-injected.ts @@ -46,7 +46,7 @@ export const SHADOW_SIGNER_STORAGE_INJECTED_JS = ` break; case "sign": - if (!params) { + if (params == null) { throw new Error("Sign operation requires params"); } diff --git a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts index c630aa9d1..83b423489 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts @@ -81,7 +81,7 @@ export class StellarNonCustodialSigner extends NonCustodialSigner { address: shadowData.publicKey, locator: `external-wallet:${shadowData.publicKey}`, onSignStellarTransaction: async (payload) => { - if (!this.shadowSignerStorage) { + if (this.shadowSignerStorage == null) { throw new Error("Shadow signer storage not available"); } diff --git a/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts b/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts index 00cd38a22..1faca62b0 100644 --- a/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts +++ b/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts @@ -40,7 +40,7 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage { async sign(publicKeyBase64: string, data: Uint8Array): Promise { const privateKey = await this.getPrivateKeyByPublicKey(publicKeyBase64); - if (!privateKey) { + if (privateKey == null) { throw new Error(`No private key found for public key: ${publicKeyBase64}`); } From b45a082176d075940ef2084e296e30c763f776dc Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Wed, 29 Oct 2025 16:58:11 +0100 Subject: [PATCH 71/82] move shadow signer to its own class --- .../src/signers/non-custodial/ncs-signer.ts | 51 +++++--------- .../non-custodial/ncs-solana-signer.ts | 41 +++--------- .../non-custodial/ncs-stellar-signer.ts | 39 +++-------- .../src/signers/shadow-signer/index.ts | 4 ++ .../signers/shadow-signer/shadow-signer.ts | 66 +++++++++++++++++++ .../shadow-signer/solana-shadow-signer.ts | 29 ++++++++ .../shadow-signer/stellar-shadow-signer.ts | 28 ++++++++ 7 files changed, 159 insertions(+), 99 deletions(-) create mode 100644 packages/wallets/src/signers/shadow-signer/shadow-signer.ts create mode 100644 packages/wallets/src/signers/shadow-signer/solana-shadow-signer.ts create mode 100644 packages/wallets/src/signers/shadow-signer/stellar-shadow-signer.ts diff --git a/packages/wallets/src/signers/non-custodial/ncs-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-signer.ts index 3b1d15869..14726c750 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-signer.ts @@ -1,19 +1,11 @@ -import type { - BaseSignResult, - EmailInternalSignerConfig, - ExternalWalletInternalSignerConfig, - PhoneInternalSignerConfig, - Signer, -} from "../types"; +import type { BaseSignResult, EmailInternalSignerConfig, 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"; import type { ShadowSignerStorage } from "@/signers/shadow-signer"; -import { getStorage } from "../shadow-signer"; +import { getStorage, type ShadowSigner } from "../shadow-signer"; +import type { Chain } from "../../chains/chains"; export abstract class NonCustodialSigner implements Signer { public readonly type: "email" | "phone"; @@ -24,7 +16,7 @@ export abstract class NonCustodialSigner implements Signer { reject: (error: Error) => void; } | null = null; private _initializationPromise: Promise | null = null; - protected shadowSigner: ExternalWalletSigner | null = null; + protected shadowSigner?: ShadowSigner; protected shadowSignerStorage?: ShadowSignerStorage; constructor( @@ -37,14 +29,14 @@ export abstract class NonCustodialSigner implements Signer { } locator() { - if (this.shadowSigner != null) { + if (this.shadowSigner?.hasShadowSigner()) { return this.shadowSigner.locator(); } return this.config.locator; } address() { - if (this.shadowSigner != null) { + if (this.shadowSigner?.hasShadowSigner()) { return this.shadowSigner.address(); } return this.config.address; @@ -71,7 +63,10 @@ export abstract class NonCustodialSigner implements Signer { // If there's already an initialization in progress, wait for it if (this._initializationPromise) { await this._initializationPromise; - return this.config.clientTEEConnection!; + if (this.config.clientTEEConnection == null) { + throw new Error("Failed to initialize TEE connection"); + } + return this.config.clientTEEConnection; } // Start initialization and store the promise to prevent concurrent initializations @@ -85,7 +80,10 @@ export abstract class NonCustodialSigner implements Signer { } } - return this.config.clientTEEConnection!; + if (this.config.clientTEEConnection == null) { + throw new Error("TEE connection is not initialized"); + } + return this.config.clientTEEConnection; } private async initializeTEEConnection(): Promise { @@ -106,7 +104,7 @@ export abstract class NonCustodialSigner implements Signer { } protected async handleAuthRequired() { - if (this.shadowSigner != null) { + if (this.shadowSigner?.hasShadowSigner()) { return; } @@ -294,25 +292,6 @@ export abstract class NonCustodialSigner implements Signer { this._authPromise?.reject(error); throw error; } - - protected abstract getShadowSignerConfig( - shadowSigner: ShadowSignerData, - walletAddress: string - ): ExternalWalletInternalSignerConfig; - - protected async initializeShadowSigner( - walletAddress: string, - ExternalWalletSignerClass: new (config: ExternalWalletInternalSignerConfig) => ExternalWalletSigner - ) { - if (await hasShadowSigner(walletAddress, this.shadowSignerStorage)) { - const shadowSigner = await getShadowSigner(walletAddress, this.shadowSignerStorage); - if (shadowSigner != null && this.config.shadowSigner?.enabled !== false) { - this.shadowSigner = new ExternalWalletSignerClass( - this.getShadowSignerConfig(shadowSigner, walletAddress) as ExternalWalletInternalSignerConfig - ); - } - } - } } export const DEFAULT_EVENT_OPTIONS = { diff --git a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts index 76b5fe19b..deaa75d2c 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts @@ -1,14 +1,8 @@ -import { PublicKey, VersionedTransaction } from "@solana/web3.js"; +import { VersionedTransaction } from "@solana/web3.js"; import base58 from "bs58"; -import type { - EmailInternalSignerConfig, - ExternalWalletInternalSignerConfig, - PhoneInternalSignerConfig, -} from "../types"; +import type { EmailInternalSignerConfig, PhoneInternalSignerConfig } from "../types"; import { NonCustodialSigner, DEFAULT_EVENT_OPTIONS } from "./ncs-signer"; -import type { ShadowSignerData } from "../shadow-signer"; -import { SolanaExternalWalletSigner } from "../solana-external-wallet"; -import type { SolanaChain } from "../../chains/chains"; +import { SolanaShadowSigner } from "../shadow-signer"; import type { ShadowSignerStorage } from "@/signers/shadow-signer"; export class SolanaNonCustodialSigner extends NonCustodialSigner { @@ -18,7 +12,11 @@ export class SolanaNonCustodialSigner extends NonCustodialSigner { shadowSignerStorage?: ShadowSignerStorage ) { super(config, shadowSignerStorage); - this.initializeShadowSigner(walletAddress, SolanaExternalWalletSigner); + this.shadowSigner = new SolanaShadowSigner( + walletAddress, + this.shadowSignerStorage, + this.config.shadowSigner?.enabled !== false + ); } async signMessage() { @@ -26,7 +24,7 @@ export class SolanaNonCustodialSigner extends NonCustodialSigner { } async signTransaction(transaction: string): Promise<{ signature: string }> { - if (this.shadowSigner != null) { + if (this.shadowSigner?.hasShadowSigner()) { return await this.shadowSigner.signTransaction(transaction); } await this.handleAuthRequired(); @@ -76,25 +74,4 @@ export class SolanaNonCustodialSigner extends NonCustodialSigner { ); } } - - protected getShadowSignerConfig(shadowData: ShadowSignerData): ExternalWalletInternalSignerConfig { - return { - type: "external-wallet", - address: shadowData.publicKey, - locator: `external-wallet:${shadowData.publicKey}`, - onSignTransaction: async (transaction) => { - if (!this.shadowSignerStorage) { - throw new Error("Shadow signer storage not available"); - } - - const messageBytes = new Uint8Array(transaction.message.serialize()); - - const signature = await this.shadowSignerStorage.sign(shadowData.publicKeyBase64, messageBytes); - - transaction.addSignature(new PublicKey(shadowData.publicKey), signature); - - return transaction; - }, - }; - } } diff --git a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts index 83b423489..520803295 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts @@ -1,12 +1,6 @@ -import type { ShadowSignerData } from "../shadow-signer"; -import type { - EmailInternalSignerConfig, - ExternalWalletInternalSignerConfig, - PhoneInternalSignerConfig, -} from "../types"; +import type { EmailInternalSignerConfig, PhoneInternalSignerConfig } from "../types"; import { DEFAULT_EVENT_OPTIONS, NonCustodialSigner } from "./ncs-signer"; -import { StellarExternalWalletSigner } from "../stellar-external-wallet"; -import type { StellarChain } from "../../chains/chains"; +import { StellarShadowSigner } from "../shadow-signer"; import type { ShadowSignerStorage } from "@/signers/shadow-signer"; export class StellarNonCustodialSigner extends NonCustodialSigner { @@ -16,7 +10,11 @@ export class StellarNonCustodialSigner extends NonCustodialSigner { shadowSignerStorage?: ShadowSignerStorage ) { super(config, shadowSignerStorage); - this.initializeShadowSigner(walletAddress, StellarExternalWalletSigner); + this.shadowSigner = new StellarShadowSigner( + walletAddress, + this.shadowSignerStorage, + this.config.shadowSigner?.enabled !== false + ); } async signMessage() { @@ -24,7 +22,7 @@ export class StellarNonCustodialSigner extends NonCustodialSigner { } async signTransaction(payload: string): Promise<{ signature: string }> { - if (this.shadowSigner != null) { + if (this.shadowSigner?.hasShadowSigner()) { return await this.shadowSigner.signTransaction(payload); } await this.handleAuthRequired(); @@ -74,25 +72,4 @@ export class StellarNonCustodialSigner extends NonCustodialSigner { ); } } - - protected getShadowSignerConfig(shadowData: ShadowSignerData): ExternalWalletInternalSignerConfig { - return { - type: "external-wallet", - address: shadowData.publicKey, - locator: `external-wallet:${shadowData.publicKey}`, - onSignStellarTransaction: async (payload) => { - if (this.shadowSignerStorage == null) { - throw new Error("Shadow signer storage not available"); - } - - const transactionString = typeof payload === "string" ? payload : (payload as { tx: string }).tx; - const messageBytes = Uint8Array.from(atob(transactionString), (c) => c.charCodeAt(0)); - - const signature = await this.shadowSignerStorage.sign(shadowData.publicKeyBase64, messageBytes); - - const signatureBase64 = btoa(String.fromCharCode(...signature)); - return signatureBase64; - }, - }; - } } diff --git a/packages/wallets/src/signers/shadow-signer/index.ts b/packages/wallets/src/signers/shadow-signer/index.ts index 905f546af..b0ee64c68 100644 --- a/packages/wallets/src/signers/shadow-signer/index.ts +++ b/packages/wallets/src/signers/shadow-signer/index.ts @@ -4,6 +4,10 @@ import { encodeEd25519PublicKey } from "../../utils/encodeEd25519PublicKey"; import { BrowserShadowSignerStorage } from "./shadow-signer-storage-browser"; import type { BaseExternalWalletSignerConfig } from "@crossmint/common-sdk-base"; +export { ShadowSigner } from "./shadow-signer"; +export { SolanaShadowSigner } from "./solana-shadow-signer"; +export { StellarShadowSigner } from "./stellar-shadow-signer"; + export type ShadowSignerData = { chain: Chain; walletAddress: string; diff --git a/packages/wallets/src/signers/shadow-signer/shadow-signer.ts b/packages/wallets/src/signers/shadow-signer/shadow-signer.ts new file mode 100644 index 000000000..71c4a36e1 --- /dev/null +++ b/packages/wallets/src/signers/shadow-signer/shadow-signer.ts @@ -0,0 +1,66 @@ +import type { Chain } from "@/chains/chains"; +import type { ExternalWalletInternalSignerConfig } from "../types"; +import type { ExternalWalletSigner } from "../external-wallet-signer"; +import { + getShadowSigner, + getStorage, + hasShadowSigner as checkStorageForShadowSigner, + type ShadowSignerData, + type ShadowSignerStorage, +} from "./index"; + +export abstract class ShadowSigner { + protected storage: ShadowSignerStorage; + protected signer: ExternalWalletSigner | null = null; + + constructor(walletAddress: string, storage?: ShadowSignerStorage, enabled = true) { + this.storage = storage ?? getStorage(); + this.initialize(walletAddress, enabled); + } + + abstract getShadowSignerConfig(shadowData: ShadowSignerData): ExternalWalletInternalSignerConfig; + + protected abstract getExternalWalletSignerClass(): new ( + config: ExternalWalletInternalSignerConfig + ) => ExternalWalletSigner; + + private async initialize(walletAddress: string, enabled: boolean): Promise { + if (!enabled) { + return; + } + + if (await checkStorageForShadowSigner(walletAddress, this.storage)) { + const shadowData = await getShadowSigner(walletAddress, this.storage); + if (shadowData != null) { + const config = this.getShadowSignerConfig(shadowData); + const ExternalWalletSignerClass = this.getExternalWalletSignerClass(); + this.signer = new ExternalWalletSignerClass(config); + } + } + } + + hasShadowSigner(): this is { signer: ExternalWalletSigner } { + return this.signer != null; + } + + async signTransaction(transaction: string): Promise<{ signature: string }> { + if (!this.hasShadowSigner()) { + throw new Error("Shadow signer not initialized"); + } + return await this.signer.signTransaction(transaction); + } + + locator(): string { + if (!this.hasShadowSigner()) { + throw new Error("Shadow signer not initialized"); + } + return this.signer.locator(); + } + + address(): string { + if (!this.hasShadowSigner()) { + throw new Error("Shadow signer not initialized"); + } + return this.signer.address(); + } +} diff --git a/packages/wallets/src/signers/shadow-signer/solana-shadow-signer.ts b/packages/wallets/src/signers/shadow-signer/solana-shadow-signer.ts new file mode 100644 index 000000000..881f3d2f4 --- /dev/null +++ b/packages/wallets/src/signers/shadow-signer/solana-shadow-signer.ts @@ -0,0 +1,29 @@ +import { PublicKey } from "@solana/web3.js"; +import { ShadowSigner } from "./shadow-signer"; +import type { SolanaChain } from "@/chains/chains"; +import type { ExternalWalletInternalSignerConfig } from "../types"; +import type { ShadowSignerData } from "./index"; +import { SolanaExternalWalletSigner } from "../solana-external-wallet"; + +export class SolanaShadowSigner extends ShadowSigner { + protected getExternalWalletSignerClass() { + return SolanaExternalWalletSigner; + } + + getShadowSignerConfig(shadowData: ShadowSignerData): ExternalWalletInternalSignerConfig { + return { + type: "external-wallet", + address: shadowData.publicKey, + locator: `external-wallet:${shadowData.publicKey}`, + onSignTransaction: async (transaction) => { + const messageBytes = new Uint8Array(transaction.message.serialize()); + + const signature = await this.storage.sign(shadowData.publicKeyBase64, messageBytes); + + transaction.addSignature(new PublicKey(shadowData.publicKey), signature); + + return transaction; + }, + }; + } +} diff --git a/packages/wallets/src/signers/shadow-signer/stellar-shadow-signer.ts b/packages/wallets/src/signers/shadow-signer/stellar-shadow-signer.ts new file mode 100644 index 000000000..d429474e3 --- /dev/null +++ b/packages/wallets/src/signers/shadow-signer/stellar-shadow-signer.ts @@ -0,0 +1,28 @@ +import { ShadowSigner } from "./shadow-signer"; +import type { StellarChain } from "@/chains/chains"; +import type { ExternalWalletInternalSignerConfig } from "../types"; +import type { ShadowSignerData } from "./index"; +import { StellarExternalWalletSigner } from "../stellar-external-wallet"; + +export class StellarShadowSigner extends ShadowSigner { + protected getExternalWalletSignerClass() { + return StellarExternalWalletSigner; + } + + getShadowSignerConfig(shadowData: ShadowSignerData): ExternalWalletInternalSignerConfig { + return { + type: "external-wallet", + address: shadowData.publicKey, + locator: `external-wallet:${shadowData.publicKey}`, + onSignStellarTransaction: async (payload) => { + const transactionString = typeof payload === "string" ? payload : (payload as { tx: string }).tx; + const messageBytes = Uint8Array.from(atob(transactionString), (c) => c.charCodeAt(0)); + + const signature = await this.storage.sign(shadowData.publicKeyBase64, messageBytes); + + const signatureBase64 = btoa(String.fromCharCode(...signature)); + return signatureBase64; + }, + }; + } +} From e2669a03517db59d4c8c98027d9f682d45139c7c Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Wed, 29 Oct 2025 17:20:00 +0100 Subject: [PATCH 72/82] fix imports --- apps/wallets/smart-wallet/expo/app/index.tsx | 2 +- packages/wallets/src/index.ts | 2 +- .../signers/non-custodial/ncs-evm-signer.ts | 2 +- .../src/signers/non-custodial/ncs-signer.ts | 3 +- .../non-custodial/ncs-solana-signer.ts | 3 +- .../non-custodial/ncs-stellar-signer.ts | 3 +- .../src/signers/shadow-signer/index.ts | 134 +++--------------- .../shadow-signer-storage-browser.ts | 2 +- .../signers/shadow-signer/shadow-signer.ts | 2 +- .../shadow-signer/solana-shadow-signer.ts | 2 +- .../shadow-signer/stellar-shadow-signer.ts | 2 +- .../src/signers/shadow-signer/utils.ts | 116 +++++++++++++++ 12 files changed, 143 insertions(+), 130 deletions(-) create mode 100644 packages/wallets/src/signers/shadow-signer/utils.ts diff --git a/apps/wallets/smart-wallet/expo/app/index.tsx b/apps/wallets/smart-wallet/expo/app/index.tsx index 5f105d7fe..56bd59383 100644 --- a/apps/wallets/smart-wallet/expo/app/index.tsx +++ b/apps/wallets/smart-wallet/expo/app/index.tsx @@ -73,7 +73,7 @@ export default function Index() { } setIsLoading(true); try { - await getOrCreateWallet({ chain: "solana", signer: { type: "email" } }); + await getOrCreateWallet({ chain: "base-sepolia", signer: { type: "email" } }); } catch (error) { console.error("Error initializing wallet:", error); } finally { diff --git a/packages/wallets/src/index.ts b/packages/wallets/src/index.ts index 6ae9ae4e2..55a8a9661 100644 --- a/packages/wallets/src/index.ts +++ b/packages/wallets/src/index.ts @@ -5,7 +5,7 @@ export { createCrossmint, CrossmintWallets } from "./sdk"; export { ApiClient as WalletsApiClient } from "./api"; // Types -export type { ShadowSignerStorage, ShadowSignerData } from "./signers/shadow-signer"; +export type { ShadowSignerStorage, ShadowSignerData } from "./signers/shadow-signer/utils"; // Wallets export { Wallet } from "./wallets/wallet"; diff --git a/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts index 3cd2c896d..3b1bc2e21 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts @@ -7,7 +7,7 @@ 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"; -import type { ShadowSignerStorage } from "@/signers/shadow-signer"; +import type { ShadowSignerStorage } from "../shadow-signer"; export class EVMNonCustodialSigner extends NonCustodialSigner { constructor( diff --git a/packages/wallets/src/signers/non-custodial/ncs-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-signer.ts index 14726c750..5fe08764e 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-signer.ts @@ -3,8 +3,7 @@ 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 type { ShadowSignerStorage } from "@/signers/shadow-signer"; -import { getStorage, type ShadowSigner } from "../shadow-signer"; +import { getStorage, type ShadowSignerStorage, type ShadowSigner } from "../shadow-signer"; import type { Chain } from "../../chains/chains"; export abstract class NonCustodialSigner implements Signer { diff --git a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts index deaa75d2c..bd4c26dc3 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-solana-signer.ts @@ -2,8 +2,7 @@ import { VersionedTransaction } from "@solana/web3.js"; import base58 from "bs58"; import type { EmailInternalSignerConfig, PhoneInternalSignerConfig } from "../types"; import { NonCustodialSigner, DEFAULT_EVENT_OPTIONS } from "./ncs-signer"; -import { SolanaShadowSigner } from "../shadow-signer"; -import type { ShadowSignerStorage } from "@/signers/shadow-signer"; +import { SolanaShadowSigner, type ShadowSignerStorage } from "../shadow-signer"; export class SolanaNonCustodialSigner extends NonCustodialSigner { constructor( diff --git a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts index 520803295..5e0a32778 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-stellar-signer.ts @@ -1,7 +1,6 @@ import type { EmailInternalSignerConfig, PhoneInternalSignerConfig } from "../types"; import { DEFAULT_EVENT_OPTIONS, NonCustodialSigner } from "./ncs-signer"; -import { StellarShadowSigner } from "../shadow-signer"; -import type { ShadowSignerStorage } from "@/signers/shadow-signer"; +import { StellarShadowSigner, type ShadowSignerStorage } from "../shadow-signer"; export class StellarNonCustodialSigner extends NonCustodialSigner { constructor( diff --git a/packages/wallets/src/signers/shadow-signer/index.ts b/packages/wallets/src/signers/shadow-signer/index.ts index b0ee64c68..c484c5bfa 100644 --- a/packages/wallets/src/signers/shadow-signer/index.ts +++ b/packages/wallets/src/signers/shadow-signer/index.ts @@ -1,120 +1,20 @@ -import { encode as encodeBase58 } from "bs58"; -import type { Chain } from "@/chains/chains"; -import { encodeEd25519PublicKey } from "../../utils/encodeEd25519PublicKey"; -import { BrowserShadowSignerStorage } from "./shadow-signer-storage-browser"; -import type { BaseExternalWalletSignerConfig } from "@crossmint/common-sdk-base"; - +// Export utilities and types first +export type { + ShadowSignerData, + ShadowSignerResult, + ShadowSignerStorage, +} from "./utils"; + +export { + getStorage, + generateShadowSigner, + storeShadowSigner, + getShadowSigner, + hasShadowSigner, +} from "./utils"; + +// Export classes last to ensure dependencies are resolved export { ShadowSigner } from "./shadow-signer"; +export type { ShadowSigner as ShadowSignerType } from "./shadow-signer"; export { SolanaShadowSigner } from "./solana-shadow-signer"; export { StellarShadowSigner } from "./stellar-shadow-signer"; - -export type ShadowSignerData = { - chain: Chain; - walletAddress: string; - publicKey: string; - publicKeyBase64: string; - createdAt: number; -}; - -export type ShadowSignerResult = { - shadowSigner: BaseExternalWalletSignerConfig; - publicKey: string; -}; - -export interface ShadowSignerStorage { - keyGenerator(): Promise; - sign(publicKey: string, data: Uint8Array): Promise; - storeMetadata(walletAddress: string, data: ShadowSignerData): Promise; - getMetadata(walletAddress: string): Promise; -} - -export function getStorage(): ShadowSignerStorage { - const isReactNative = typeof navigator !== "undefined" && navigator.product === "ReactNative"; - const isExpo = typeof global !== "undefined" && (global as { expo?: unknown }).expo; - - if (isReactNative || isExpo) { - throw new Error("ReactNativeShadowSignerStorage must be provided explicitly for React Native environments"); - } else { - return new BrowserShadowSignerStorage(); - } -} - -export async function generateShadowSigner( - chain: C, - storage?: ShadowSignerStorage -): Promise { - const storageInstance = storage ?? getStorage(); - if (chain === "solana" || chain === "stellar") { - const publicKeyBase64 = await storageInstance.keyGenerator(); - const publicKeyBuffer = Buffer.from(publicKeyBase64, "base64"); - const publicKeyBytes = new Uint8Array(publicKeyBuffer); - - let encodedPublicKey: string; - if (chain === "stellar") { - // Stellar uses Ed25519 encoding (Base32 with version byte and checksum) - encodedPublicKey = encodeEd25519PublicKey(publicKeyBytes); - } else { - // Solana uses Base58 encoding - encodedPublicKey = encodeBase58(publicKeyBytes); - } - - return { - shadowSigner: { - type: "external-wallet", - address: encodedPublicKey, - }, - publicKey: encodedPublicKey, - publicKeyBase64, - }; - } - // TODO: Add support for EVM chains - throw new Error("Unsupported chain"); -} - -export async function storeShadowSigner( - walletAddress: string, - chain: Chain, - publicKey: string, - publicKeyBase64: string, - storage?: ShadowSignerStorage -): Promise { - const storageInstance = storage ?? getStorage(); - try { - console.log("[storeShadowSigner] Storing metadata for wallet:", walletAddress, "publicKey:", publicKey); - - const data: ShadowSignerData = { - chain, - walletAddress, - publicKey, - publicKeyBase64, - createdAt: Date.now(), - }; - - await storageInstance.storeMetadata(walletAddress, data); - console.log("[storeShadowSigner] Metadata stored successfully"); - } catch (error) { - console.error("Failed to store shadow signer metadata:", error); - throw error; - } -} - -export async function getShadowSigner( - walletAddress: string, - storage?: ShadowSignerStorage -): Promise { - const storageInstance = storage ?? getStorage(); - try { - console.log("[getShadowSigner] Getting shadow signer for wallet:", walletAddress); - const result = await storageInstance.getMetadata(walletAddress); - console.log("[getShadowSigner] Result:", result ? "found" : "not found"); - return result; - } catch (error) { - console.warn("[getShadowSigner] Failed to get shadow signer:", error); - return null; - } -} - -export async function hasShadowSigner(walletAddress: string, storage?: ShadowSignerStorage): Promise { - const shadowSigner = await getShadowSigner(walletAddress, storage); - return shadowSigner !== null; -} diff --git a/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts b/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts index 1faca62b0..82069a168 100644 --- a/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts +++ b/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts @@ -1,4 +1,4 @@ -import type { ShadowSignerData, ShadowSignerStorage } from "."; +import type { ShadowSignerData, ShadowSignerStorage } from "./utils"; export class BrowserShadowSignerStorage implements ShadowSignerStorage { private readonly SHADOW_SIGNER_DB_NAME = "crossmint_shadow_keys"; diff --git a/packages/wallets/src/signers/shadow-signer/shadow-signer.ts b/packages/wallets/src/signers/shadow-signer/shadow-signer.ts index 71c4a36e1..5f5f9d451 100644 --- a/packages/wallets/src/signers/shadow-signer/shadow-signer.ts +++ b/packages/wallets/src/signers/shadow-signer/shadow-signer.ts @@ -7,7 +7,7 @@ import { hasShadowSigner as checkStorageForShadowSigner, type ShadowSignerData, type ShadowSignerStorage, -} from "./index"; +} from "./utils"; export abstract class ShadowSigner { protected storage: ShadowSignerStorage; diff --git a/packages/wallets/src/signers/shadow-signer/solana-shadow-signer.ts b/packages/wallets/src/signers/shadow-signer/solana-shadow-signer.ts index 881f3d2f4..80ed4f66d 100644 --- a/packages/wallets/src/signers/shadow-signer/solana-shadow-signer.ts +++ b/packages/wallets/src/signers/shadow-signer/solana-shadow-signer.ts @@ -2,7 +2,7 @@ import { PublicKey } from "@solana/web3.js"; import { ShadowSigner } from "./shadow-signer"; import type { SolanaChain } from "@/chains/chains"; import type { ExternalWalletInternalSignerConfig } from "../types"; -import type { ShadowSignerData } from "./index"; +import type { ShadowSignerData } from "./utils"; import { SolanaExternalWalletSigner } from "../solana-external-wallet"; export class SolanaShadowSigner extends ShadowSigner { diff --git a/packages/wallets/src/signers/shadow-signer/stellar-shadow-signer.ts b/packages/wallets/src/signers/shadow-signer/stellar-shadow-signer.ts index d429474e3..55c6b3918 100644 --- a/packages/wallets/src/signers/shadow-signer/stellar-shadow-signer.ts +++ b/packages/wallets/src/signers/shadow-signer/stellar-shadow-signer.ts @@ -1,7 +1,7 @@ import { ShadowSigner } from "./shadow-signer"; import type { StellarChain } from "@/chains/chains"; import type { ExternalWalletInternalSignerConfig } from "../types"; -import type { ShadowSignerData } from "./index"; +import type { ShadowSignerData } from "./utils"; import { StellarExternalWalletSigner } from "../stellar-external-wallet"; export class StellarShadowSigner extends ShadowSigner { diff --git a/packages/wallets/src/signers/shadow-signer/utils.ts b/packages/wallets/src/signers/shadow-signer/utils.ts new file mode 100644 index 000000000..905f546af --- /dev/null +++ b/packages/wallets/src/signers/shadow-signer/utils.ts @@ -0,0 +1,116 @@ +import { encode as encodeBase58 } from "bs58"; +import type { Chain } from "@/chains/chains"; +import { encodeEd25519PublicKey } from "../../utils/encodeEd25519PublicKey"; +import { BrowserShadowSignerStorage } from "./shadow-signer-storage-browser"; +import type { BaseExternalWalletSignerConfig } from "@crossmint/common-sdk-base"; + +export type ShadowSignerData = { + chain: Chain; + walletAddress: string; + publicKey: string; + publicKeyBase64: string; + createdAt: number; +}; + +export type ShadowSignerResult = { + shadowSigner: BaseExternalWalletSignerConfig; + publicKey: string; +}; + +export interface ShadowSignerStorage { + keyGenerator(): Promise; + sign(publicKey: string, data: Uint8Array): Promise; + storeMetadata(walletAddress: string, data: ShadowSignerData): Promise; + getMetadata(walletAddress: string): Promise; +} + +export function getStorage(): ShadowSignerStorage { + const isReactNative = typeof navigator !== "undefined" && navigator.product === "ReactNative"; + const isExpo = typeof global !== "undefined" && (global as { expo?: unknown }).expo; + + if (isReactNative || isExpo) { + throw new Error("ReactNativeShadowSignerStorage must be provided explicitly for React Native environments"); + } else { + return new BrowserShadowSignerStorage(); + } +} + +export async function generateShadowSigner( + chain: C, + storage?: ShadowSignerStorage +): Promise { + const storageInstance = storage ?? getStorage(); + if (chain === "solana" || chain === "stellar") { + const publicKeyBase64 = await storageInstance.keyGenerator(); + const publicKeyBuffer = Buffer.from(publicKeyBase64, "base64"); + const publicKeyBytes = new Uint8Array(publicKeyBuffer); + + let encodedPublicKey: string; + if (chain === "stellar") { + // Stellar uses Ed25519 encoding (Base32 with version byte and checksum) + encodedPublicKey = encodeEd25519PublicKey(publicKeyBytes); + } else { + // Solana uses Base58 encoding + encodedPublicKey = encodeBase58(publicKeyBytes); + } + + return { + shadowSigner: { + type: "external-wallet", + address: encodedPublicKey, + }, + publicKey: encodedPublicKey, + publicKeyBase64, + }; + } + // TODO: Add support for EVM chains + throw new Error("Unsupported chain"); +} + +export async function storeShadowSigner( + walletAddress: string, + chain: Chain, + publicKey: string, + publicKeyBase64: string, + storage?: ShadowSignerStorage +): Promise { + const storageInstance = storage ?? getStorage(); + try { + console.log("[storeShadowSigner] Storing metadata for wallet:", walletAddress, "publicKey:", publicKey); + + const data: ShadowSignerData = { + chain, + walletAddress, + publicKey, + publicKeyBase64, + createdAt: Date.now(), + }; + + await storageInstance.storeMetadata(walletAddress, data); + console.log("[storeShadowSigner] Metadata stored successfully"); + } catch (error) { + console.error("Failed to store shadow signer metadata:", error); + throw error; + } +} + +export async function getShadowSigner( + walletAddress: string, + storage?: ShadowSignerStorage +): Promise { + const storageInstance = storage ?? getStorage(); + try { + console.log("[getShadowSigner] Getting shadow signer for wallet:", walletAddress); + const result = await storageInstance.getMetadata(walletAddress); + console.log("[getShadowSigner] Result:", result ? "found" : "not found"); + return result; + } catch (error) { + console.warn("[getShadowSigner] Failed to get shadow signer:", error); + return null; + } +} + +export async function hasShadowSigner(walletAddress: string, storage?: ShadowSignerStorage): Promise { + const shadowSigner = await getShadowSigner(walletAddress, storage); + return shadowSigner !== null; +} From 4cf0b973a6b43293457ea9b5d5aa43aa4789a227 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Wed, 29 Oct 2025 18:40:51 +0100 Subject: [PATCH 73/82] fix merge conflicts --- .../wallets/src/signers/non-custodial/ncs-signer.ts | 2 +- .../src/signers/shadow-signer/evm-shadow-signer.ts | 10 +++++++--- .../wallets/src/signers/shadow-signer/shadow-signer.ts | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/wallets/src/signers/non-custodial/ncs-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-signer.ts index 5fe08764e..a5513073f 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-signer.ts @@ -15,7 +15,7 @@ export abstract class NonCustodialSigner implements Signer { reject: (error: Error) => void; } | null = null; private _initializationPromise: Promise | null = null; - protected shadowSigner?: ShadowSigner; + protected shadowSigner?: ShadowSigner; protected shadowSignerStorage?: ShadowSignerStorage; constructor( diff --git a/packages/wallets/src/signers/shadow-signer/evm-shadow-signer.ts b/packages/wallets/src/signers/shadow-signer/evm-shadow-signer.ts index 24b8256a1..67ed29bac 100644 --- a/packages/wallets/src/signers/shadow-signer/evm-shadow-signer.ts +++ b/packages/wallets/src/signers/shadow-signer/evm-shadow-signer.ts @@ -1,10 +1,14 @@ import { ShadowSigner } from "./shadow-signer"; -import type { EVMChain } from "@/chains/chains"; +import type { EVMSmartWalletChain } from "@/chains/chains"; import type { EVM256KeypairInternalSignerConfig } from "../types"; import type { ShadowSignerData } from "./utils"; import { EVM256KeypairSigner } from "../evm-p256-keypair"; -export class EVMShadowSigner extends ShadowSigner { +export class EVMShadowSigner extends ShadowSigner< + EVMSmartWalletChain, + EVM256KeypairSigner, + EVM256KeypairInternalSignerConfig +> { protected getSignerClass() { return EVM256KeypairSigner; } @@ -13,7 +17,7 @@ export class EVMShadowSigner extends ShadowSigner { return await this.storage.sign(pubKey, data); diff --git a/packages/wallets/src/signers/shadow-signer/shadow-signer.ts b/packages/wallets/src/signers/shadow-signer/shadow-signer.ts index 6620abc3c..d8cab7180 100644 --- a/packages/wallets/src/signers/shadow-signer/shadow-signer.ts +++ b/packages/wallets/src/signers/shadow-signer/shadow-signer.ts @@ -7,8 +7,9 @@ import { type ShadowSignerData, type ShadowSignerStorage, } from "./utils"; +import type { InternalSignerConfig } from "../types"; -export abstract class ShadowSigner { +export abstract class ShadowSigner> { protected storage: ShadowSignerStorage; protected signer: S | null = null; From 2111f546dfed10e4afd0b875c7c04fea75c7c9f8 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Wed, 29 Oct 2025 19:20:42 +0100 Subject: [PATCH 74/82] fix signer --- .../wallets/src/signers/evm-p256-keypair.ts | 30 ++++++++----- .../shadow-signer-storage-browser.ts | 43 +------------------ .../src/signers/shadow-signer/utils.ts | 9 ++-- 3 files changed, 27 insertions(+), 55 deletions(-) diff --git a/packages/wallets/src/signers/evm-p256-keypair.ts b/packages/wallets/src/signers/evm-p256-keypair.ts index 101282ffe..4bbb30d47 100644 --- a/packages/wallets/src/signers/evm-p256-keypair.ts +++ b/packages/wallets/src/signers/evm-p256-keypair.ts @@ -1,6 +1,5 @@ import type { Signer, EVM256KeypairInternalSignerConfig } from "./types"; -import { concat, toHex, sha256 } from "viem"; - +import { keccak256, sha256, toHex } from "viem"; export class EVM256KeypairSigner implements Signer { type = "evm-p256-keypair" as const; private publicKey: string; @@ -48,20 +47,31 @@ export class EVM256KeypairSigner implements Signer { }); // 2. Create authenticatorData - const rpIdHashHex = sha256(toHex(new TextEncoder().encode(STUB_ORIGIN))); - const flags = toHex(new Uint8Array([0x05])); - const signCount = toHex(new Uint8Array([0x00, 0x00, 0x00, 0x00])); - const authenticatorData = concat([rpIdHashHex, flags, signCount]); + // IMPORTANT: Use keccak256 for rpIdHash to match backend (line 182 in backend) + const originBytes = new TextEncoder().encode(STUB_ORIGIN); + const rpIdHash = keccak256(toHex(originBytes)); + + // flags: 0x05 = User Present (0x01) + User Verified (0x04) + const flags = "05"; + + // signCount: 4 bytes, all zeros + const signCount = "00000000"; + + const authenticatorData = (rpIdHash + flags + signCount) as `0x${string}`; + + // 3. Create signature message: authenticatorData + sha256(clientDataJSON) + // This matches what the backend expects and what WebAuthn spec requires + const clientDataJSONBytes = new TextEncoder().encode(clientDataJSON); + const clientDataHash = sha256(toHex(clientDataJSONBytes)); - // 3. Create signature message - const clientDataHash = sha256(toHex(new TextEncoder().encode(clientDataJSON))); - const signatureMessage = concat([authenticatorData, clientDataHash]); + const signatureMessage = (authenticatorData + clientDataHash.slice(2)) as `0x${string}`; // 4. Sign with P256 private key + // Web Crypto API will internally do: sign(sha256(signatureMessage)) const signatureMessageBytes = new Uint8Array(Buffer.from(signatureMessage.slice(2), "hex")); const signatureBytes = await this.onSignTransaction(this.publicKey, signatureMessageBytes); - // 5. Return r + s as hex string (backend will encode to WebAuthn) + // 5. Return r + s as hex string const rHex = Buffer.from(signatureBytes.slice(0, 32)).toString("hex").padStart(64, "0"); const sHex = Buffer.from(signatureBytes.slice(32, 64)).toString("hex").padStart(64, "0"); diff --git a/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts b/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts index 69fc536b1..b1b7a3bb2 100644 --- a/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts +++ b/packages/wallets/src/signers/shadow-signer/shadow-signer-storage-browser.ts @@ -36,7 +36,6 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage { const publicKeyBase64 = Buffer.from(publicKeyBytes).toString("base64"); await this.storePrivateKeyByPublicKey(publicKeyBase64, keyPair.privateKey); - await this.storeKeyAlgorithm(publicKeyBase64, "Ed25519"); return publicKeyBase64; } @@ -56,7 +55,6 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage { const publicKeyBase64 = Buffer.from(publicKeyBytes).toString("base64"); await this.storePrivateKeyByPublicKey(publicKeyBase64, keyPair.privateKey); - await this.storeKeyAlgorithm(publicKeyBase64, "P-256"); return publicKeyBase64; } @@ -67,9 +65,9 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage { throw new Error(`No private key found for public key: ${publicKeyBase64}`); } - const algorithm = await this.getKeyAlgorithm(publicKeyBase64); + const algorithmName = privateKey.algorithm.name; - if (algorithm === "P-256") { + if (algorithmName === "ECDSA") { // For P256, use ECDSA with SHA-256 const signature = await window.crypto.subtle.sign( { @@ -125,43 +123,6 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage { } } - private async storeKeyAlgorithm(publicKey: string, algorithm: string): Promise { - if (typeof indexedDB === "undefined") { - return; - } - - const db = await this.openDB(); - const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readwrite"); - const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); - store.put(algorithm, `${publicKey}_algorithm`); - - return new Promise((resolve, reject) => { - tx.oncomplete = () => resolve(); - tx.onerror = () => reject(tx.error); - }); - } - - private async getKeyAlgorithm(publicKey: string): Promise { - if (typeof indexedDB === "undefined") { - return null; - } - - try { - const db = await this.openDB(); - const tx = db.transaction([this.SHADOW_SIGNER_DB_STORE], "readonly"); - const store = tx.objectStore(this.SHADOW_SIGNER_DB_STORE); - const request = store.get(`${publicKey}_algorithm`); - - return new Promise((resolve, reject) => { - request.onsuccess = () => resolve(request.result || null); - request.onerror = () => reject(request.error); - }); - } catch (error) { - console.warn("Failed to retrieve key algorithm from IndexedDB:", error); - return null; - } - } - storeMetadata(walletAddress: string, data: ShadowSignerData): Promise { if (typeof localStorage === "undefined") { return Promise.resolve(); diff --git a/packages/wallets/src/signers/shadow-signer/utils.ts b/packages/wallets/src/signers/shadow-signer/utils.ts index 47618f705..86c1afe94 100644 --- a/packages/wallets/src/signers/shadow-signer/utils.ts +++ b/packages/wallets/src/signers/shadow-signer/utils.ts @@ -2,7 +2,7 @@ import { encode as encodeBase58 } from "bs58"; import type { Chain } from "@/chains/chains"; import { encodeEd25519PublicKey } from "../../utils/encodeEd25519PublicKey"; import { BrowserShadowSignerStorage } from "./shadow-signer-storage-browser"; -import type { BaseExternalWalletSignerConfig } from "@crossmint/common-sdk-base"; +import type { BaseExternalWalletSignerConfig, EVM256KeypairSignerConfig } from "@crossmint/common-sdk-base"; export type ShadowSignerData = { chain: Chain; @@ -13,7 +13,7 @@ export type ShadowSignerData = { }; export type ShadowSignerResult = { - shadowSigner: BaseExternalWalletSignerConfig; + shadowSigner: BaseExternalWalletSignerConfig | EVM256KeypairSignerConfig; publicKey: string; }; @@ -68,8 +68,9 @@ export async function generateShadowSigner( // For EVM chains, the public key is the base64 P256 public key return { shadowSigner: { - type: "external-wallet", - address: publicKeyBase64, + type: "evm-p256-keypair", + publicKey: publicKeyBase64, + chain, }, publicKey: publicKeyBase64, publicKeyBase64, From 15ea8ed3b6a0076689ade465d38741292cb25061 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 30 Oct 2025 13:09:44 +0100 Subject: [PATCH 75/82] applied comments --- apps/wallets/smart-wallet/expo/app/index.tsx | 2 +- .../ui/react-native/src/utils/WebViewShadowSignerStorage.ts | 2 +- ...ge-injected.ts => webview-shadow-signer-storage-injected.ts} | 0 packages/wallets/src/signers/shadow-signer/utils.ts | 2 ++ 4 files changed, 4 insertions(+), 2 deletions(-) rename packages/client/ui/react-native/src/utils/{webview-storage-injected.ts => webview-shadow-signer-storage-injected.ts} (100%) diff --git a/apps/wallets/smart-wallet/expo/app/index.tsx b/apps/wallets/smart-wallet/expo/app/index.tsx index 56bd59383..5f105d7fe 100644 --- a/apps/wallets/smart-wallet/expo/app/index.tsx +++ b/apps/wallets/smart-wallet/expo/app/index.tsx @@ -73,7 +73,7 @@ export default function Index() { } setIsLoading(true); try { - await getOrCreateWallet({ chain: "base-sepolia", signer: { type: "email" } }); + await getOrCreateWallet({ chain: "solana", signer: { type: "email" } }); } catch (error) { console.error("Error initializing wallet:", error); } finally { diff --git a/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts b/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts index 10a4a8f92..3b03fce12 100644 --- a/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts +++ b/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts @@ -1,6 +1,6 @@ import type { ShadowSignerStorage, ShadowSignerData } from "@crossmint/wallets-sdk"; import { SecureStorage } from "./SecureStorage"; -import { SHADOW_SIGNER_STORAGE_INJECTED_JS } from "./webview-storage-injected"; +import { SHADOW_SIGNER_STORAGE_INJECTED_JS } from "./webview-shadow-signer-storage-injected"; import type { RefObject } from "react"; import type { WebView } from "react-native-webview"; import * as SecureStore from "expo-secure-store"; diff --git a/packages/client/ui/react-native/src/utils/webview-storage-injected.ts b/packages/client/ui/react-native/src/utils/webview-shadow-signer-storage-injected.ts similarity index 100% rename from packages/client/ui/react-native/src/utils/webview-storage-injected.ts rename to packages/client/ui/react-native/src/utils/webview-shadow-signer-storage-injected.ts diff --git a/packages/wallets/src/signers/shadow-signer/utils.ts b/packages/wallets/src/signers/shadow-signer/utils.ts index 905f546af..6c50e213c 100644 --- a/packages/wallets/src/signers/shadow-signer/utils.ts +++ b/packages/wallets/src/signers/shadow-signer/utils.ts @@ -60,6 +60,8 @@ export async function generateShadowSigner( address: encodedPublicKey, }, publicKey: encodedPublicKey, + // We need to return the public key base64 here because its the only way to retrieve the private key from the storage + // as Stellar and Solana encoded the public key after storing the private key publicKeyBase64, }; } From 6b6d371d8abd35f5164fb387a222ed7efe531a24 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 30 Oct 2025 14:26:50 +0100 Subject: [PATCH 76/82] avoid injectin javascript --- .../src/providers/CrossmintWalletProvider.tsx | 13 ++-- .../src/utils/WebViewShadowSignerStorage.ts | 65 +++++-------------- .../webview-shadow-signer-storage-injected.ts | 62 ++++++++++++------ packages/wallets/tsup.config.ts | 22 ++++++- 4 files changed, 88 insertions(+), 74 deletions(-) diff --git a/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx b/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx index e2572f9a0..3a8f5876d 100644 --- a/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx +++ b/packages/client/ui/react-native/src/providers/CrossmintWalletProvider.tsx @@ -1,12 +1,14 @@ import { type ReactNode, useCallback, useEffect, useRef, useMemo, createContext, useState } from "react"; import { View } from "react-native"; import type { WebView, WebViewMessageEvent } from "react-native-webview"; +import { WebView as RNRawWebView } from "react-native-webview"; import { RNWebView, WebViewParent } from "@crossmint/client-sdk-rn-window"; import { environmentUrlConfig, signerInboundEvents, signerOutboundEvents } from "@crossmint/client-signers"; import { validateAPIKey } from "@crossmint/common-sdk-base"; import { type CreateOnLogin, CrossmintWalletBaseProvider } from "@crossmint/client-sdk-react-base"; import { useCrossmint } from "@/hooks"; import { WebViewShadowSignerStorage } from "@/utils/WebViewShadowSignerStorage"; +import { SHADOW_SIGNER_STORAGE_INJECTED_JS } from "@/utils/webview-shadow-signer-storage-injected"; const throwNotAvailable = (functionName: string) => () => { throw new Error(`${functionName} is not available. Make sure you're using an email signer wallet.`); @@ -60,6 +62,8 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: ); const shadowSignerWebViewRef = useRef(null); + const [shadowSignerHash, setShadowSignerHash] = useState(""); + const shadowSignerBaseUrl = "https://crossmint-shadow-signer.local"; // Use useState only for needsAuth since it needs to trigger re-renders const [needsAuth, setNeedsAuth] = useState(false); @@ -101,8 +105,8 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: const onShadowSignerWebViewLoad = useCallback(() => { if (shadowSignerStorage instanceof WebViewShadowSignerStorage && shadowSignerWebViewRef.current) { - console.log("[ShadowSignerStorage] WebView loaded, injecting storage handler..."); - shadowSignerStorage.initialize(shadowSignerWebViewRef); + console.log("[ShadowSignerStorage] WebView loaded (pre-injected script)"); + shadowSignerStorage.initialize(shadowSignerWebViewRef, (hash: string) => setShadowSignerHash(hash)); } }, [shadowSignerStorage]); @@ -273,11 +277,11 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: overflow: "hidden", }} > - ", - baseUrl: "https://crossmint-shadow-signer.local", + baseUrl: `${shadowSignerBaseUrl}${shadowSignerHash}`, }} onLoadEnd={onShadowSignerWebViewLoad} onMessage={handleShadowSignerMessage} @@ -292,6 +296,7 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }: incognito={false} cacheEnabled={true} cacheMode="LOAD_DEFAULT" + injectedJavaScriptBeforeContentLoaded={SHADOW_SIGNER_STORAGE_INJECTED_JS} /> )} diff --git a/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts b/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts index 3b03fce12..44f2a438c 100644 --- a/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts +++ b/packages/client/ui/react-native/src/utils/WebViewShadowSignerStorage.ts @@ -1,15 +1,15 @@ import type { ShadowSignerStorage, ShadowSignerData } from "@crossmint/wallets-sdk"; import { SecureStorage } from "./SecureStorage"; -import { SHADOW_SIGNER_STORAGE_INJECTED_JS } from "./webview-shadow-signer-storage-injected"; import type { RefObject } from "react"; import type { WebView } from "react-native-webview"; import * as SecureStore from "expo-secure-store"; +import { Buffer } from "buffer/"; export class WebViewShadowSignerStorage implements ShadowSignerStorage { private readonly SHADOW_SIGNER_STORAGE_KEY = "crossmint_shadow_signer"; private secureStorage = new SecureStorage(); private webViewRef: RefObject | null = null; - private isInjected = false; + private sendCommandViaHash: ((hash: string) => void) | null = null; private readyPromise: Promise; private readyResolve: (() => void) | null = null; @@ -19,30 +19,19 @@ export class WebViewShadowSignerStorage implements ShadowSignerStorage { }); } - initialize(webViewRef: RefObject): void { + initialize(webViewRef: RefObject, sendCommandViaHash?: (hash: string) => void): void { this.webViewRef = webViewRef; - this.injectStorageHandler(); - } - - private injectStorageHandler(): void { - if (this.isInjected || this.webViewRef?.current == null) { - return; + if (sendCommandViaHash) { + this.sendCommandViaHash = sendCommandViaHash; } - - try { - this.webViewRef.current.injectJavaScript(SHADOW_SIGNER_STORAGE_INJECTED_JS); - this.isInjected = true; - console.log("[WebViewShadowSignerStorage] Storage handler injected into WebView"); - - if (this.readyResolve) { - this.readyResolve(); - this.readyResolve = null; - } - } catch (error) { - console.error("[WebViewShadowSignerStorage] Failed to inject storage handler:", error); + if (this.readyResolve) { + this.readyResolve(); + this.readyResolve = null; } } + // No runtime JS injection; handler is pre-injected by the WebView + async waitForReady(): Promise { await this.readyPromise; } @@ -87,9 +76,8 @@ export class WebViewShadowSignerStorage implements ShadowSignerStorage { operation: string, params: Record ): Promise> { - const webView = this.webViewRef?.current; - if (webView == null) { - throw new Error("WebView not available. Make sure to initialize() with a WebView ref."); + if (this.sendCommandViaHash == null) { + throw new Error("Shadow signer command channel not initialized"); } const id = `shadow_${Date.now()}_${Math.random().toString(36).slice(2)}`; @@ -102,27 +90,10 @@ export class WebViewShadowSignerStorage implements ShadowSignerStorage { this.pendingRequests.set(id, { resolve, reject, timeout }); - const script = ` -(async function() { - try { - const result = await window.__crossmintShadowSignerStorage('${operation}', ${JSON.stringify(params)}); - window.ReactNativeWebView.postMessage(JSON.stringify({ - type: 'SHADOW_SIGNER_RESPONSE', - id: '${id}', - result: result - })); - } catch (error) { - window.ReactNativeWebView.postMessage(JSON.stringify({ - type: 'SHADOW_SIGNER_RESPONSE', - id: '${id}', - error: error.message || String(error) - })); - } -})(); -true; - `; - - webView.injectJavaScript(script); + const payload = { id, operation, params }; + const b64 = btoa(JSON.stringify(payload)); + const hash = `#cmShadow=${encodeURIComponent(b64)}`; + this.sendCommandViaHash!(hash); }); } @@ -163,9 +134,7 @@ true; async keyGenerator(): Promise { const publicKeyBytes = await this.generateKeyInWebView(); - const publicKeyBase64 = Buffer.from(publicKeyBytes).toString("base64"); - - return publicKeyBase64; + return Buffer.from(publicKeyBytes).toString("base64"); } async sign(publicKeyBase64: string, data: Uint8Array): Promise { diff --git a/packages/client/ui/react-native/src/utils/webview-shadow-signer-storage-injected.ts b/packages/client/ui/react-native/src/utils/webview-shadow-signer-storage-injected.ts index 75cce13b1..cc9ca9fd0 100644 --- a/packages/client/ui/react-native/src/utils/webview-shadow-signer-storage-injected.ts +++ b/packages/client/ui/react-native/src/utils/webview-shadow-signer-storage-injected.ts @@ -28,51 +28,71 @@ export const SHADOW_SIGNER_STORAGE_INJECTED_JS = ` var storage = new CrossmintBrowserStorage.BrowserShadowSignerStorage(); console.log("[CrossmintShadowSigner] Storage instance created"); - // Expose the API that React Native expects - window.__crossmintShadowSignerStorage = async function(operation, params) { - console.log("[CrossmintShadowSigner] Function called with operation:", operation); + // Hash-based command channel: #cmShadow= + function decodeBase64ToJson(b64) { try { - var result; + var json = atob(b64); + return JSON.parse(json); + } catch (e) { + return null; + } + } + async function handleCommand(command) { + if (!command || typeof command !== 'object') return; + var id = command.id; + var operation = command.operation; + var params = command.params || {}; + try { + var result; switch (operation) { case "generate": console.log("[CrossmintShadowSigner] Generating new Ed25519 key pair (non-extractable)..."); var publicKeyBase64 = await storage.keyGenerator(); - var publicKeyBytes = Array.from(atob(publicKeyBase64).split('').map(function(c) { - return c.charCodeAt(0); - })); + var publicKeyBytes = Array.from(atob(publicKeyBase64).split('').map(function(c){return c.charCodeAt(0);})); console.log("[CrossmintShadowSigner] ✅ Key generation complete"); result = { publicKeyBytes: publicKeyBytes }; break; - case "sign": - if (params == null) { - throw new Error("Sign operation requires params"); - } - + if (params == null) { throw new Error("Sign operation requires params"); } var publicKey = params.publicKey; var messageBytes = params.messageBytes; - console.log("[CrossmintShadowSigner] Signing..."); var signatureBytes = await storage.sign(publicKey, new Uint8Array(messageBytes)); - console.log("[CrossmintShadowSigner] ✅ Signing complete"); result = { signatureBytes: Array.from(signatureBytes) }; break; - default: throw new Error("Unknown operation: " + operation); } - - return result; + window.ReactNativeWebView && window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'SHADOW_SIGNER_RESPONSE', + id: id, + result: result + })); } catch (error) { - console.error("[CrossmintShadowSigner] Operation failed:", operation, error); - throw error; + window.ReactNativeWebView && window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'SHADOW_SIGNER_RESPONSE', + id: id, + error: (error && (error.message || String(error))) || 'Unknown error' + })); } - }; + } + + function parseAndHandleHash() { + var hash = location.hash || ''; + var prefix = '#cmShadow='; + if (hash.indexOf(prefix) !== 0) return; + var b64 = hash.slice(prefix.length); + var cmd = decodeBase64ToJson(decodeURIComponent(b64)); + try { history.replaceState('', document.title, location.pathname + location.search); } catch (_) {} + handleCommand(cmd); + } + + parseAndHandleHash(); + window.addEventListener('hashchange', parseAndHandleHash, false); console.log("[CrossmintShadowSigner] Storage handler installed in WebView"); - console.log("[CrossmintShadowSigner] Function type:", typeof window.__crossmintShadowSignerStorage); })(); true; `; diff --git a/packages/wallets/tsup.config.ts b/packages/wallets/tsup.config.ts index 1300a769a..7b3ae1157 100644 --- a/packages/wallets/tsup.config.ts +++ b/packages/wallets/tsup.config.ts @@ -25,7 +25,27 @@ const config: Options = { }); // Read the compiled JavaScript - const jsContent = readFileSync(join(outDir, "shadow-signer-storage-browser.js"), "utf-8"); + const jsRaw = readFileSync(join(outDir, "shadow-signer-storage-browser.js"), "utf-8"); + + // Generate a robust Buffer polyfill from the existing 'buffer/' package + const polyfillBuild = await build({ + stdin: { + contents: `import { Buffer } from "buffer/";\n(function(){ if (typeof window !== 'undefined' && typeof (window as any).Buffer === 'undefined') { (window as any).Buffer = Buffer as any; } })();`, + resolveDir: process.cwd(), + sourcefile: "polyfill-buffer.ts", + loader: "ts", + }, + bundle: true, + minify: true, + format: "iife", + platform: "browser", + target: "es2020", + write: false, + }); + + const polyfills = polyfillBuild.outputFiles?.[0]?.text ?? ""; + + const jsContent = polyfills + "\n" + jsRaw; // Create TypeScript file that exports the script as a string const tsContent = `// Auto-generated file - do not edit manually From 2ce197224e74dceb70729abd070768cea3997051 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 30 Oct 2025 16:35:29 +0100 Subject: [PATCH 77/82] don't make publicKeyBase64 optional --- packages/wallets/src/wallets/wallet-factory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index a1c36ed86..fefc7a3d3 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -505,7 +505,7 @@ export class WalletFactory { DelegatedSigner | RegisterSignerParams | { signer: PasskeySignerConfig | EVM256KeypairSignerConfig } >; shadowSignerPublicKey: string | null; - shadowSignerPublicKeyBase64?: string | null; + shadowSignerPublicKeyBase64: string | null; }> { const { delegatedSigners, shadowSignerPublicKey, shadowSignerPublicKeyBase64 } = await this.addShadowSignerToDelegatedSignersIfNeeded( @@ -552,7 +552,7 @@ export class WalletFactory { ): Promise<{ delegatedSigners: Array> | undefined; shadowSignerPublicKey: string | null; - shadowSignerPublicKeyBase64?: string | null; + shadowSignerPublicKeyBase64: string | null; }> { if (this.isShadowSignerEnabled(adminSigner, delegatedSigners)) { try { From 35595aa40d23254869d929e811a1f69c2542d216 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Thu, 30 Oct 2025 16:38:34 +0100 Subject: [PATCH 78/82] add changeset --- .changeset/cyan-coats-invite.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/cyan-coats-invite.md diff --git a/.changeset/cyan-coats-invite.md b/.changeset/cyan-coats-invite.md new file mode 100644 index 000000000..fffe43d4d --- /dev/null +++ b/.changeset/cyan-coats-invite.md @@ -0,0 +1,7 @@ +--- +"@crossmint/wallets-sdk": minor +"@crossmint/client-sdk-react-native-ui": patch +"@crossmint/common-sdk-base": patch +--- + +Support EVM Shadow Signers From dabde79bde814cf53d0e137595fa264a2132b100 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Mon, 17 Nov 2025 09:09:50 -0300 Subject: [PATCH 79/82] add shadow signer to locator --- packages/wallets/src/signers/passkey.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/wallets/src/signers/passkey.ts b/packages/wallets/src/signers/passkey.ts index 43f8f1aa0..af880937f 100644 --- a/packages/wallets/src/signers/passkey.ts +++ b/packages/wallets/src/signers/passkey.ts @@ -20,6 +20,9 @@ export class PasskeySigner implements Signer { } locator() { + if (this.shadowSigner?.hasShadowSigner()) { + return this.shadowSigner.locator(); + } return this.config.locator; } From 6fbf44b54d6d17798ebad70a1fc477cbbe310774 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Mon, 17 Nov 2025 09:10:53 -0300 Subject: [PATCH 80/82] fix passkey creation --- packages/wallets/src/wallets/wallet-factory.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 611ea983c..b347c86d8 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -273,6 +273,12 @@ export class WalletFactory { if (delegatedSigner != null) { return delegatedSigner; } + if (signerLocator === "passkey") { + const passkeySigner = [adminSigner, ...delegatedSigners].find((s) => s.type === "passkey"); + if (passkeySigner != null) { + return passkeySigner; + } + } throw new WalletCreationError(`${signerLocator} signer does not match the wallet's signer type`); } From 1757b147d44dac664e7449da23ee1f9104d98b0b Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Mon, 17 Nov 2025 14:44:58 -0300 Subject: [PATCH 81/82] fix passkeys --- packages/wallets/src/signers/passkey.ts | 16 ++-------------- packages/wallets/src/wallets/wallet-factory.ts | 12 ++++-------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/packages/wallets/src/signers/passkey.ts b/packages/wallets/src/signers/passkey.ts index af880937f..0c880935e 100644 --- a/packages/wallets/src/signers/passkey.ts +++ b/packages/wallets/src/signers/passkey.ts @@ -26,21 +26,9 @@ export class PasskeySigner implements Signer { return this.config.locator; } - async signMessage(message: string): Promise { + async signMessage(message: string): Promise { if (this.shadowSigner?.hasShadowSigner()) { - const result = await this.shadowSigner.signTransaction(message); - // Convert the shadow signer result to PasskeySignResult format - // Shadow signer returns { signature: string } where signature is "0x" + r + s - const signatureHex = result.signature.replace("0x", ""); - const r = signatureHex.slice(0, 64); - const s = signatureHex.slice(64, 128); - return { - signature: { - r: `0x${r}`, - s: `0x${s}`, - }, - metadata: {} as any, // Shadow signer doesn't provide metadata - }; + return this.shadowSigner.signTransaction(message); } if (this.config.onSignWithPasskey) { return await this.config.onSignWithPasskey(message); diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 2a0b2b0d1..8de677e92 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -156,7 +156,7 @@ export class WalletFactory { args.chain, signerConfig, walletResponse.address, - this.isShadowSignerEnabled(args.chain, args.options), + this.isShadowSignerEnabled(args.options), args.options?.shadowSignerStorage ), options: args.options, @@ -491,12 +491,8 @@ export class WalletFactory { return false; } - private isShadowSignerEnabled(chain: C, options: WalletOptions | undefined): boolean { - return ( - !this.apiClient.isServerSide && - (chain === "solana" || chain === "stellar") && - options?.shadowSignerEnabled !== false - ); + private isShadowSignerEnabled(options: WalletOptions | undefined): boolean { + return !this.apiClient.isServerSide && options?.shadowSignerEnabled !== false; } private async buildDelegatedSigners( @@ -539,7 +535,7 @@ export class WalletFactory { shadowSignerPublicKey: string | null; shadowSignerPublicKeyBase64: string | null; }> { - if (this.isShadowSignerEnabled(args.chain, args.options)) { + if (this.isShadowSignerEnabled(args.options)) { try { const { shadowSigner, publicKeyBase64 } = await generateShadowSigner( args.chain, From 7e6c42e11274ca0963a11ec8d4369e4dd85268b4 Mon Sep 17 00:00:00 2001 From: Guille Aszyn Date: Wed, 26 Nov 2025 18:23:31 +0100 Subject: [PATCH 82/82] apply comments --- .../shadow-signer/evm-shadow-signer.ts | 2 +- .../signers/shadow-signer/shadow-signer.ts | 19 +++++++++++++++---- .../shadow-signer/solana-shadow-signer.ts | 6 +++--- .../shadow-signer/stellar-shadow-signer.ts | 6 +++--- packages/wallets/src/signers/types.ts | 1 + 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/wallets/src/signers/shadow-signer/evm-shadow-signer.ts b/packages/wallets/src/signers/shadow-signer/evm-shadow-signer.ts index 7aeee2ee5..a2fe3f7ba 100644 --- a/packages/wallets/src/signers/shadow-signer/evm-shadow-signer.ts +++ b/packages/wallets/src/signers/shadow-signer/evm-shadow-signer.ts @@ -9,7 +9,7 @@ export class EVMShadowSigner extends ShadowSigner< P256KeypairSigner, P256KeypairInternalSignerConfig > { - protected getSignerClass() { + protected getWrappedSignerClass() { return P256KeypairSigner; } diff --git a/packages/wallets/src/signers/shadow-signer/shadow-signer.ts b/packages/wallets/src/signers/shadow-signer/shadow-signer.ts index e58974774..02ee0d65f 100644 --- a/packages/wallets/src/signers/shadow-signer/shadow-signer.ts +++ b/packages/wallets/src/signers/shadow-signer/shadow-signer.ts @@ -1,5 +1,5 @@ import type { Chain } from "@/chains/chains"; -import type { Signer } from "../types"; +import type { BaseSignResult, Signer } from "../types"; import { getShadowSigner, getStorage, @@ -9,9 +9,12 @@ import { } from "./utils"; import type { InternalSignerConfig } from "../types"; -export abstract class ShadowSigner> { +export abstract class ShadowSigner> + implements Signer +{ protected storage: ShadowSignerStorage; protected signer: S | null = null; + public readonly type: "device" = "device" as const; constructor(walletAddress?: string, storage?: ShadowSignerStorage, enabled = true) { this.storage = storage ?? getStorage(); @@ -20,7 +23,7 @@ export abstract class ShadowSigner S; + protected abstract getWrappedSignerClass(): new (config: Config) => S; private async initialize(walletAddress: string | undefined, enabled: boolean): Promise { if (!enabled || walletAddress == null) { @@ -31,7 +34,7 @@ export abstract class ShadowSigner { + if (!this.hasShadowSigner()) { + throw new Error("Shadow signer not initialized"); + } + const result = await this.signer.signMessage(message); + return result as BaseSignResult; + } + async signTransaction(transaction: string): Promise<{ signature: string }> { if (!this.hasShadowSigner()) { throw new Error("Shadow signer not initialized"); diff --git a/packages/wallets/src/signers/shadow-signer/solana-shadow-signer.ts b/packages/wallets/src/signers/shadow-signer/solana-shadow-signer.ts index 5457b9f10..e121be6c6 100644 --- a/packages/wallets/src/signers/shadow-signer/solana-shadow-signer.ts +++ b/packages/wallets/src/signers/shadow-signer/solana-shadow-signer.ts @@ -1,4 +1,4 @@ -import { PublicKey } from "@solana/web3.js"; +import { PublicKey, type VersionedTransaction } from "@solana/web3.js"; import { ShadowSigner } from "./shadow-signer"; import type { SolanaChain } from "@/chains/chains"; import type { ExternalWalletInternalSignerConfig } from "../types"; @@ -10,7 +10,7 @@ export class SolanaShadowSigner extends ShadowSigner< SolanaExternalWalletSigner, ExternalWalletInternalSignerConfig > { - protected getSignerClass() { + protected getWrappedSignerClass() { return SolanaExternalWalletSigner; } @@ -19,7 +19,7 @@ export class SolanaShadowSigner extends ShadowSigner< type: "external-wallet", address: shadowData.publicKey, locator: `external-wallet:${shadowData.publicKey}`, - onSignTransaction: async (transaction) => { + onSignTransaction: async (transaction: VersionedTransaction) => { const messageBytes = new Uint8Array(transaction.message.serialize()); const signature = await this.storage.sign(shadowData.publicKeyBase64, messageBytes); diff --git a/packages/wallets/src/signers/shadow-signer/stellar-shadow-signer.ts b/packages/wallets/src/signers/shadow-signer/stellar-shadow-signer.ts index 4eef05fae..1e2c4a369 100644 --- a/packages/wallets/src/signers/shadow-signer/stellar-shadow-signer.ts +++ b/packages/wallets/src/signers/shadow-signer/stellar-shadow-signer.ts @@ -9,7 +9,7 @@ export class StellarShadowSigner extends ShadowSigner< StellarExternalWalletSigner, ExternalWalletInternalSignerConfig > { - protected getSignerClass() { + protected getWrappedSignerClass() { return StellarExternalWalletSigner; } @@ -18,8 +18,8 @@ export class StellarShadowSigner extends ShadowSigner< type: "external-wallet", address: shadowData.publicKey, locator: `external-wallet:${shadowData.publicKey}`, - onSignStellarTransaction: async (payload) => { - const transactionString = typeof payload === "string" ? payload : (payload as { tx: string }).tx; + onSignStellarTransaction: async (payload: string | { tx: string }) => { + const transactionString = typeof payload === "string" ? payload : payload.tx; const messageBytes = Uint8Array.from(atob(transactionString), (c) => c.charCodeAt(0)); const signature = await this.storage.sign(shadowData.publicKeyBase64, messageBytes); diff --git a/packages/wallets/src/signers/types.ts b/packages/wallets/src/signers/types.ts index 5c8a5cceb..667d62553 100644 --- a/packages/wallets/src/signers/types.ts +++ b/packages/wallets/src/signers/types.ts @@ -157,6 +157,7 @@ type SignResultMap = { "external-wallet": BaseSignResult; "p256-keypair": BaseSignResult; passkey: PasskeySignResult; + device: BaseSignResult; }; export interface Signer {