Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
2033ee9
feat: add onCreateConfig support to separate admin and usage signers
devin-ai-integration[bot] Oct 14, 2025
8a5417a
fix: export OnCreateConfig and fix getChainType access
devin-ai-integration[bot] Oct 14, 2025
a69a978
refactor: remove args.delegatedSigners field (BREAKING CHANGE)
devin-ai-integration[bot] Oct 14, 2025
8089157
feat: make onCreateConfig required via WalletCreateArgs type
devin-ai-integration[bot] Oct 14, 2025
c675932
chore: fix lint issues
devin-ai-integration[bot] Oct 14, 2025
b642c2c
chore: update demo apps to use onCreateConfig
devin-ai-integration[bot] Oct 14, 2025
41e35a2
fix: correctly mutate signer in validateExistingWalletConfig
devin-ai-integration[bot] Oct 14, 2025
eb5c28e
fix: correctly mutate adminSignerConfig in createWallet
devin-ai-integration[bot] Oct 14, 2025
1be9a9f
remove unnecesary type
guilleasz-crossmint Oct 14, 2025
cfde33f
reuse common functionality from get and create
guilleasz-crossmint Oct 14, 2025
d59062a
remove redundant comments
guilleasz-crossmint Oct 14, 2025
c1ea37e
fix tsc issue
guilleasz-crossmint Oct 14, 2025
6b8b3f3
fix linter
guilleasz-crossmint Oct 14, 2025
3e6d5c2
added client side specific get wallet
guilleasz-crossmint Oct 14, 2025
133dc92
fix signers and transactions
guilleasz-crossmint Oct 15, 2025
5bd3ce4
fix delegated signer
guilleasz-crossmint Oct 15, 2025
34f6f16
remove redundant check
guilleasz-crossmint Oct 15, 2025
e0397c1
fix unit test
guilleasz-crossmint Oct 15, 2025
9276bcf
make oncreateconfig optional
guilleasz-crossmint Oct 15, 2025
f890560
remove unnecesary format changes
guilleasz-crossmint Oct 15, 2025
b04e71d
unify getWallet functionality
guilleasz-crossmint Oct 16, 2025
43c64d8
remove admin from name
guilleasz-crossmint Oct 16, 2025
77faeca
fix react provider
guilleasz-crossmint Oct 16, 2025
6891aa5
remove unnecessary types
guilleasz-crossmint Oct 16, 2025
ee8195b
remove optional param
guilleasz-crossmint Oct 16, 2025
199d2ab
feat(wallets): add shadow signer support for automatic delegated signers
devin-ai-integration[bot] Oct 16, 2025
1b0eba3
fix: remove unused @ts-expect-error directive
devin-ai-integration[bot] Oct 16, 2025
a1afbb5
refactor: replace 'as any' with custom SmartWalletConfig type
devin-ai-integration[bot] Oct 17, 2025
05fbb32
fix: format error message to meet line length requirements
devin-ai-integration[bot] Oct 17, 2025
75a3e8f
refactor: remove Flow-specific check, let API handle incompatible chains
devin-ai-integration[bot] Oct 17, 2025
c23d2b6
feat: use shadow signer as active signer for wallet instances
devin-ai-integration[bot] Oct 17, 2025
3d92392
fix: apply biome lint formatting
devin-ai-integration[bot] Oct 17, 2025
dbaf09d
fix: add localStorage checks for Node.js test environment
devin-ai-integration[bot] Oct 17, 2025
1351429
Merge branch 'main' of github.com:Crossmint/crossmint-sdk into devin/…
guilleasz-crossmint Oct 17, 2025
050c6be
wip shadow
guilleasz-crossmint Oct 17, 2025
6cd46ee
remove support for evm
guilleasz-crossmint Oct 20, 2025
c66cb23
add passkey logic to delegate signer comparison
guilleasz-crossmint Oct 20, 2025
14914bc
Merge branch 'guillea/wal-4751-allowing-delegated-signers-to-use-the-…
guilleasz-crossmint Oct 20, 2025
b588a95
fix wallet with multiple passkey signers
guilleasz-crossmint Oct 20, 2025
e5516f0
remove console.log
guilleasz-crossmint Oct 20, 2025
8b024c7
remove all shadow passkey references
guilleasz-crossmint Oct 20, 2025
4da2a7f
appplied albertos comment
guilleasz-crossmint Oct 20, 2025
9c31e78
Merge branch 'guillea/wal-4751-allowing-delegated-signers-to-use-the-…
guilleasz-crossmint Oct 20, 2025
69a147a
differentiate wallet creation for stellar and solana
guilleasz-crossmint Oct 20, 2025
c34c4e2
Merge branch 'wallets-v1' of github.com:Crossmint/crossmint-sdk into …
guilleasz-crossmint Oct 20, 2025
8f2aca8
fix shadow signer for stellar
guilleasz-crossmint Oct 21, 2025
13f4369
implement shadow signers for react native
guilleasz-crossmint Oct 23, 2025
55e291f
move signing shado logic to signer
guilleasz-crossmint Oct 23, 2025
6aace1b
change shado signer option config
guilleasz-crossmint Oct 23, 2025
a550bb8
move delegated signer logic to a new function
guilleasz-crossmint Oct 23, 2025
16f5f1b
abstract signature
guilleasz-crossmint Oct 23, 2025
bc49080
remove unnecessary change
guilleasz-crossmint Oct 23, 2025
28682a3
restructure folder
guilleasz-crossmint Oct 23, 2025
af82106
improve has shadow signer condition
guilleasz-crossmint Oct 23, 2025
3b85db9
fix import
guilleasz-crossmint Oct 23, 2025
adaee17
fix import
guilleasz-crossmint Oct 23, 2025
ea04677
Merge branch 'wallets-v1' of github.com:Crossmint/crossmint-sdk into …
guilleasz-crossmint Oct 23, 2025
89be46d
move shadow signers
guilleasz-crossmint Oct 23, 2025
740f37e
fix naming
guilleasz-crossmint Oct 23, 2025
33e7177
Merge branch 'devin/wal-shadow-signers-1760630286' into guillea/wal-7…
guilleasz-crossmint Oct 23, 2025
7a345c5
fix vitest test in wallets package
guilleasz-crossmint Oct 23, 2025
1d7a55f
Merge branch 'devin/wal-shadow-signers-1760630286' into guillea/wal-7…
guilleasz-crossmint Oct 23, 2025
0df29c4
refactor: address PR feedback from Alberto
devin-ai-integration[bot] Oct 23, 2025
4d2b684
fix: apply biome formatting
devin-ai-integration[bot] Oct 23, 2025
87c6e61
move storage to rn package
guilleasz-crossmint Oct 24, 2025
450a323
Merge branch 'devin/wal-shadow-signers-1760630286' of github.com:Cros…
guilleasz-crossmint Oct 24, 2025
e9550dc
make shad signer work for react native
guilleasz-crossmint Oct 27, 2025
6c4f679
Merge branch 'wallets-v1' into devin/wal-shadow-signers-1760630286
guilleasz-crossmint Oct 28, 2025
ee6c518
applied comments
guilleasz-crossmint Oct 28, 2025
ccacf80
Merge branch 'devin/wal-shadow-signers-1760630286' of github.com:Cros…
guilleasz-crossmint Oct 28, 2025
c42de5f
added changeset
guilleasz-crossmint Oct 28, 2025
ad35f5b
Merge branch 'devin/wal-shadow-signers-1760630286' into guillea/wal-7…
guilleasz-crossmint Oct 28, 2025
0b19137
Merge branch 'wallets-v1' of github.com:Crossmint/crossmint-sdk into …
guilleasz-crossmint Oct 28, 2025
ff117e4
cleanup PR
guilleasz-crossmint Oct 28, 2025
bf218e0
change back to string
guilleasz-crossmint Oct 28, 2025
30827d6
revert chain change
guilleasz-crossmint Oct 28, 2025
ad5c5d5
move back to usdc
guilleasz-crossmint Oct 28, 2025
099b456
fix browser storage
guilleasz-crossmint Oct 28, 2025
12a693c
add changeset
guilleasz-crossmint Oct 28, 2025
5e65a97
add signer evm, needs to fix signature
guilleasz-crossmint Oct 29, 2025
6e8fe56
make browserstorage a string to inject in webview
guilleasz-crossmint Oct 29, 2025
bbdc645
add all the == null missing
guilleasz-crossmint Oct 29, 2025
b45a082
move shadow signer to its own class
guilleasz-crossmint Oct 29, 2025
e2669a0
fix imports
guilleasz-crossmint Oct 29, 2025
42c359b
Merge branch 'guillea/wal-7064-support-shadow-signers-for-react-nativ…
guilleasz-crossmint Oct 29, 2025
4cf0b97
fix merge conflicts
guilleasz-crossmint Oct 29, 2025
2111f54
fix signer
guilleasz-crossmint Oct 29, 2025
15ea8ed
applied comments
guilleasz-crossmint Oct 30, 2025
6b6d371
avoid injectin javascript
guilleasz-crossmint Oct 30, 2025
cf3a19d
Merge branch 'guillea/wal-7064-support-shadow-signers-for-react-nativ…
guilleasz-crossmint Oct 30, 2025
2af0201
Merge branch 'wallets-v1' of github.com:Crossmint/crossmint-sdk into …
guilleasz-crossmint Oct 30, 2025
2ce1972
don't make publicKeyBase64 optional
guilleasz-crossmint Oct 30, 2025
35595aa
add changeset
guilleasz-crossmint Oct 30, 2025
7368af6
add device signer to evm
guilleasz-crossmint Nov 14, 2025
dabde79
add shadow signer to locator
guilleasz-crossmint Nov 17, 2025
6fbf44b
fix passkey creation
guilleasz-crossmint Nov 17, 2025
a23e334
Merge branch 'wallets-v1' into guillea/wal-7240-add-support-for-evm-s…
guilleasz-crossmint Nov 17, 2025
1757b14
fix passkeys
guilleasz-crossmint Nov 17, 2025
f6dd7d3
Merge branch 'wallets-v1' into guillea/wal-7240-add-support-for-evm-s…
guilleasz-crossmint Nov 17, 2025
7e6c42e
apply comments
guilleasz-crossmint Nov 26, 2025
File filter

Filter by extension

Filter by extension

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

Support EVM Shadow Signers
5 changes: 5 additions & 0 deletions packages/common/base/src/types/signers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ export type SolanaExternalWalletSignerConfig = BaseExternalWalletSignerConfig &
export type StellarExternalWalletSignerConfig = BaseExternalWalletSignerConfig & {
onSignStellarTransaction?: (transaction: string) => Promise<string>;
};

export type P256KeypairSignerConfig = {
type: "p256-keypair";
address: string;
};
18 changes: 17 additions & 1 deletion packages/wallets/src/signers/evm-external-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,31 @@ import type { Account, EIP1193Provider as ViemEIP1193Provider } from "viem";
import type { GenericEIP1193Provider, ExternalWalletInternalSignerConfig } from "./types";
import type { EVMChain } from "@/chains/chains";
import { ExternalWalletSigner } from "./external-wallet-signer";
import { EVMShadowSigner, type ShadowSignerStorage } from "./shadow-signer";

export class EVMExternalWalletSigner extends ExternalWalletSigner<EVMChain> {
provider?: GenericEIP1193Provider | ViemEIP1193Provider;
viemAccount?: Account;
protected shadowSigner?: EVMShadowSigner;
protected shadowSignerStorage?: ShadowSignerStorage;

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

Choose a reason for hiding this comment

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

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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

shadowSignerStorage?: ShadowSignerStorage
) {
super(config);
this.provider = config.provider;
this.viemAccount = config.viemAccount;
this.shadowSignerStorage = shadowSignerStorage;
this.shadowSigner = new EVMShadowSigner(walletAddress, this.shadowSignerStorage, shadowSignerEnabled);
}

async signMessage(message: string) {
if (this.shadowSigner?.hasShadowSigner()) {
return await this.shadowSigner.signTransaction(message);
}
if (this.provider != null) {
const signature = await this.provider.request({
method: "personal_sign",
Expand Down Expand Up @@ -43,6 +56,9 @@ export class EVMExternalWalletSigner extends ExternalWalletSigner<EVMChain> {
}

async signTransaction(transaction: string) {
if (this.shadowSigner?.hasShadowSigner()) {
return await this.shadowSigner.signTransaction(transaction);
}
return await this.signMessage(transaction);
}
}
10 changes: 7 additions & 3 deletions packages/wallets/src/signers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { P256KeypairSigner } from "./p256-keypair";
import type { ShadowSignerStorage } from "./shadow-signer";

export function assembleSigner<C extends Chain>(
Expand All @@ -25,7 +26,7 @@ export function assembleSigner<C extends Chain>(
if (chain === "stellar") {
return new StellarNonCustodialSigner(config, walletAddress, shadowSignerEnabled, shadowSignerStorage);
}
return new EVMNonCustodialSigner(config, shadowSignerStorage);
return new EVMNonCustodialSigner(config, walletAddress, shadowSignerEnabled, shadowSignerStorage);
case "api-key":
return chain === "solana" ? new SolanaApiKeySigner(config) : new EVMApiKeySigner(config);

Expand All @@ -36,9 +37,12 @@ export function assembleSigner<C extends Chain>(
if (chain === "stellar") {
return new StellarExternalWalletSigner(config, walletAddress, shadowSignerEnabled, shadowSignerStorage);
}
return new EVMExternalWalletSigner(config);
return new EVMExternalWalletSigner(config, walletAddress, shadowSignerEnabled, shadowSignerStorage);

case "p256-keypair": {
return new P256KeypairSigner(config);
}
case "passkey":
return new PasskeySigner(config);
return new PasskeySigner(config, walletAddress, shadowSignerEnabled, shadowSignerStorage);
}
}
19 changes: 8 additions & 11 deletions packages/wallets/src/signers/non-custodial/ncs-evm-signer.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
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";
import type { ShadowSignerStorage } from "../shadow-signer";
import type { EmailInternalSignerConfig, PhoneInternalSignerConfig } from "../types";
import { EVMShadowSigner, type ShadowSignerStorage } from "../shadow-signer";

export class EVMNonCustodialSigner extends NonCustodialSigner {
constructor(
config: EmailInternalSignerConfig | PhoneInternalSignerConfig,
walletAddress: string,
shadowSignerEnabled: boolean,
shadowSignerStorage?: ShadowSignerStorage
) {
super(config, shadowSignerStorage);
this.shadowSigner = new EVMShadowSigner(walletAddress, this.shadowSignerStorage, shadowSignerEnabled);
}

async signMessage(message: string) {
Expand All @@ -24,6 +22,9 @@ export class EVMNonCustodialSigner extends NonCustodialSigner {
}

async signTransaction(transaction: string): Promise<{ signature: string }> {
if (this.shadowSigner?.hasShadowSigner()) {
return await this.shadowSigner.signTransaction(transaction);
}
return await this.sign(transaction);
}

Expand Down Expand Up @@ -74,10 +75,6 @@ export class EVMNonCustodialSigner extends NonCustodialSigner {
}
}

protected getShadowSignerConfig(): ExternalWalletInternalSignerConfig<EVMChain> {
throw new Error("Shadow signer not implemented for EVM chains");
}

protected getChainKeyParams(): { scheme: "secp256k1"; encoding: "hex" } {
return {
scheme: "secp256k1",
Expand Down
2 changes: 1 addition & 1 deletion packages/wallets/src/signers/non-custodial/ncs-signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export abstract class NonCustodialSigner implements Signer {
reject: (error: Error) => void;
} | null = null;
private _initializationPromise: Promise<void> | null = null;
protected shadowSigner?: ShadowSigner<Chain>;
protected shadowSigner?: ShadowSigner<Chain, Signer, any>;
protected shadowSignerStorage?: ShadowSignerStorage;

constructor(
Expand Down
79 changes: 79 additions & 0 deletions packages/wallets/src/signers/p256-keypair.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { Signer, P256KeypairInternalSignerConfig } from "./types";
import { keccak256, sha256, toHex } from "viem";

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

Choose a reason for hiding this comment

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

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

Copy link
Contributor Author

@guilleasz-crossmint guilleasz-crossmint Nov 26, 2025

Choose a reason for hiding this comment

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

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

private readonly STUB_ORIGIN = "https://crossmint.com";

constructor(config: P256KeypairInternalSignerConfig) {
this._address = config.address;
this._locator = config.locator;
this.onSignTransaction = config.onSignTransaction;
}

address() {
return this._address;
}

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
// 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));

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.address(), signatureMessageBytes);

// 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");

return { signature: "0x" + rHex + sHex };
}
}
20 changes: 18 additions & 2 deletions packages/wallets/src/signers/passkey.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
import { WebAuthnP256 } from "ox";
import type { PasskeyInternalSignerConfig, PasskeySignResult, Signer } from "./types";
import { EVMShadowSigner, type ShadowSignerStorage } from "./shadow-signer";

export class PasskeySigner implements Signer {
type = "passkey" as const;
id: string;
protected shadowSigner?: EVMShadowSigner;
protected shadowSignerStorage?: ShadowSignerStorage;

constructor(private config: PasskeyInternalSignerConfig) {
constructor(
private config: PasskeyInternalSignerConfig,
walletAddress?: string,
shadowSignerEnabled?: boolean,
shadowSignerStorage?: ShadowSignerStorage
) {
this.id = config.id;
this.shadowSignerStorage = shadowSignerStorage;
this.shadowSigner = new EVMShadowSigner(walletAddress, this.shadowSignerStorage, shadowSignerEnabled);
}

locator() {
if (this.shadowSigner?.hasShadowSigner()) {
return this.shadowSigner.locator();
}
return this.config.locator;
}

async signMessage(message: string): Promise<PasskeySignResult> {
async signMessage(message: string): Promise<PasskeySignResult | { signature: string }> {
if (this.shadowSigner?.hasShadowSigner()) {
return this.shadowSigner.signTransaction(message);
}
if (this.config.onSignWithPasskey) {
return await this.config.onSignWithPasskey(message);
}
Expand Down
26 changes: 26 additions & 0 deletions packages/wallets/src/signers/shadow-signer/evm-shadow-signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ShadowSigner } from "./shadow-signer";
import type { EVMSmartWalletChain } from "@/chains/chains";
import type { P256KeypairInternalSignerConfig } from "../types";
import type { ShadowSignerData } from "./utils";
import { P256KeypairSigner } from "../p256-keypair";

export class EVMShadowSigner extends ShadowSigner<
EVMSmartWalletChain,
P256KeypairSigner,
P256KeypairInternalSignerConfig
> {
protected getWrappedSignerClass() {
return P256KeypairSigner;
}

getShadowSignerConfig(shadowData: ShadowSignerData): P256KeypairInternalSignerConfig {
return {
type: "p256-keypair",
address: shadowData.publicKeyBase64,
locator: `p256-keypair:${shadowData.publicKeyBase64}`,
onSignTransaction: async (pubKey: string, data: Uint8Array) => {
return await this.storage.sign(pubKey, data);
},
};
}
}
1 change: 1 addition & 0 deletions packages/wallets/src/signers/shadow-signer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ 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 { EVMShadowSigner } from "./evm-shadow-signer";
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ShadowSignerData, ShadowSignerStorage } from "./utils";
import type { Chain } from "../../chains/chains";

export class BrowserShadowSignerStorage implements ShadowSignerStorage {
private readonly SHADOW_SIGNER_DB_NAME = "crossmint_shadow_keys";
Expand All @@ -19,12 +20,32 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage {
});
}

async keyGenerator(): Promise<string> {
async keyGenerator(chain: Chain): Promise<string> {
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;
Expand All @@ -44,6 +65,22 @@ export class BrowserShadowSignerStorage implements ShadowSignerStorage {
throw new Error(`No private key found for public key: ${publicKeyBase64}`);
}

const algorithmName = privateKey.algorithm.name;

if (algorithmName === "ECDSA") {
// 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);
Expand Down
Loading