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 diff --git a/packages/common/base/src/types/signers.ts b/packages/common/base/src/types/signers.ts index b40231b02..8160e3fa4 100644 --- a/packages/common/base/src/types/signers.ts +++ b/packages/common/base/src/types/signers.ts @@ -25,3 +25,8 @@ export type SolanaExternalWalletSignerConfig = BaseExternalWalletSignerConfig & export type StellarExternalWalletSignerConfig = BaseExternalWalletSignerConfig & { onSignStellarTransaction?: (transaction: string) => Promise; }; + +export type P256KeypairSignerConfig = { + type: "p256-keypair"; + address: string; +}; diff --git a/packages/wallets/src/signers/evm-external-wallet.ts b/packages/wallets/src/signers/evm-external-wallet.ts index 601619a17..3c51fa4cf 100644 --- a/packages/wallets/src/signers/evm-external-wallet.ts +++ b/packages/wallets/src/signers/evm-external-wallet.ts @@ -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 { provider?: GenericEIP1193Provider | ViemEIP1193Provider; viemAccount?: Account; + protected shadowSigner?: EVMShadowSigner; + protected shadowSignerStorage?: ShadowSignerStorage; - constructor(config: ExternalWalletInternalSignerConfig) { + constructor( + config: ExternalWalletInternalSignerConfig, + walletAddress?: string, + shadowSignerEnabled?: boolean, + 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", @@ -43,6 +56,9 @@ export class EVMExternalWalletSigner extends ExternalWalletSigner { } async signTransaction(transaction: string) { + if (this.shadowSigner?.hasShadowSigner()) { + return await this.shadowSigner.signTransaction(transaction); + } return await this.signMessage(transaction); } } diff --git a/packages/wallets/src/signers/index.ts b/packages/wallets/src/signers/index.ts index 824a6f01b..c1a7a445f 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 { P256KeypairSigner } from "./p256-keypair"; import type { ShadowSignerStorage } from "./shadow-signer"; export function assembleSigner( @@ -25,7 +26,7 @@ export function assembleSigner( 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); @@ -36,9 +37,12 @@ export function assembleSigner( 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); } } 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 fb8f85f87..9a366a7bb 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 { 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) { @@ -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); } @@ -74,10 +75,6 @@ export class EVMNonCustodialSigner extends NonCustodialSigner { } } - protected getShadowSignerConfig(): ExternalWalletInternalSignerConfig { - throw new Error("Shadow signer not implemented for EVM chains"); - } - protected getChainKeyParams(): { scheme: "secp256k1"; encoding: "hex" } { return { scheme: "secp256k1", diff --git a/packages/wallets/src/signers/non-custodial/ncs-signer.ts b/packages/wallets/src/signers/non-custodial/ncs-signer.ts index cfe50b8ac..059a71a9f 100644 --- a/packages/wallets/src/signers/non-custodial/ncs-signer.ts +++ b/packages/wallets/src/signers/non-custodial/ncs-signer.ts @@ -21,7 +21,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/p256-keypair.ts b/packages/wallets/src/signers/p256-keypair.ts new file mode 100644 index 000000000..7851a9bd7 --- /dev/null +++ b/packages/wallets/src/signers/p256-keypair.ts @@ -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; + 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 }; + } +} diff --git a/packages/wallets/src/signers/passkey.ts b/packages/wallets/src/signers/passkey.ts index 1c47df6b8..0c880935e 100644 --- a/packages/wallets/src/signers/passkey.ts +++ b/packages/wallets/src/signers/passkey.ts @@ -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 { + async signMessage(message: string): Promise { + if (this.shadowSigner?.hasShadowSigner()) { + return this.shadowSigner.signTransaction(message); + } if (this.config.onSignWithPasskey) { return await this.config.onSignWithPasskey(message); } diff --git a/packages/wallets/src/signers/shadow-signer/evm-shadow-signer.ts b/packages/wallets/src/signers/shadow-signer/evm-shadow-signer.ts new file mode 100644 index 000000000..a2fe3f7ba --- /dev/null +++ b/packages/wallets/src/signers/shadow-signer/evm-shadow-signer.ts @@ -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); + }, + }; + } +} diff --git a/packages/wallets/src/signers/shadow-signer/index.ts b/packages/wallets/src/signers/shadow-signer/index.ts index c484c5bfa..bbd0e0df3 100644 --- a/packages/wallets/src/signers/shadow-signer/index.ts +++ b/packages/wallets/src/signers/shadow-signer/index.ts @@ -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"; 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 82069a168..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 @@ -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"; @@ -19,12 +20,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; @@ -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); diff --git a/packages/wallets/src/signers/shadow-signer/shadow-signer.ts b/packages/wallets/src/signers/shadow-signer/shadow-signer.ts index 5a4368d97..02ee0d65f 100644 --- a/packages/wallets/src/signers/shadow-signer/shadow-signer.ts +++ b/packages/wallets/src/signers/shadow-signer/shadow-signer.ts @@ -1,6 +1,5 @@ import type { Chain } from "@/chains/chains"; -import type { ExternalWalletInternalSignerConfig } from "../types"; -import type { ExternalWalletSigner } from "../external-wallet-signer"; +import type { BaseSignResult, Signer } from "../types"; import { getShadowSigner, getStorage, @@ -8,21 +7,23 @@ import { type ShadowSignerData, type ShadowSignerStorage, } from "./utils"; +import type { InternalSignerConfig } from "../types"; -export abstract class ShadowSigner { +export abstract class ShadowSigner> + implements Signer +{ protected storage: ShadowSignerStorage; - protected signer: ExternalWalletSigner | null = null; + protected signer: S | null = null; + public readonly type: "device" = "device" as const; constructor(walletAddress?: string, storage?: ShadowSignerStorage, enabled = true) { this.storage = storage ?? getStorage(); this.initialize(walletAddress, enabled); } - abstract getShadowSignerConfig(shadowData: ShadowSignerData): ExternalWalletInternalSignerConfig; + abstract getShadowSignerConfig(shadowData: ShadowSignerData): Config; - protected abstract getExternalWalletSignerClass(): new ( - config: ExternalWalletInternalSignerConfig - ) => ExternalWalletSigner; + protected abstract getWrappedSignerClass(): new (config: Config) => S; private async initialize(walletAddress: string | undefined, enabled: boolean): Promise { if (!enabled || walletAddress == null) { @@ -33,21 +34,30 @@ export abstract class ShadowSigner { const shadowData = await getShadowSigner(walletAddress, this.storage); if (shadowData != null) { const config = this.getShadowSignerConfig(shadowData); - const ExternalWalletSignerClass = this.getExternalWalletSignerClass(); - this.signer = new ExternalWalletSignerClass(config); + const SignerClass = this.getWrappedSignerClass(); + this.signer = new SignerClass(config); } } } - hasShadowSigner(): this is { signer: ExternalWalletSigner } { + hasShadowSigner(): this is { signer: S } { return this.signer != null; } + async signMessage(message: string): Promise { + 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"); } - return await this.signer.signTransaction(transaction); + const result = await this.signer.signTransaction(transaction); + return result as { signature: string }; } locator(): string { @@ -61,6 +71,9 @@ export abstract class ShadowSigner { if (!this.hasShadowSigner()) { throw new Error("Shadow signer not initialized"); } + if (this.signer.address == null) { + throw new Error("Signer does not have an address method"); + } 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 index d954d01d9..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,17 +1,16 @@ -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"; -import type { ShadowSignerData, ShadowSignerStorage } from "./utils"; +import type { ShadowSignerData } from "./utils"; import { SolanaExternalWalletSigner } from "../solana-external-wallet"; -export class SolanaShadowSigner extends ShadowSigner { - protected getExternalWalletSignerClass(): new ( - config: ExternalWalletInternalSignerConfig, - walletAddress?: string, - shadowSignerEnabled?: boolean, - shadowSignerStorage?: ShadowSignerStorage - ) => SolanaExternalWalletSigner { +export class SolanaShadowSigner extends ShadowSigner< + SolanaChain, + SolanaExternalWalletSigner, + ExternalWalletInternalSignerConfig +> { + protected getWrappedSignerClass() { return SolanaExternalWalletSigner; } @@ -20,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 b773eda81..1e2c4a369 100644 --- a/packages/wallets/src/signers/shadow-signer/stellar-shadow-signer.ts +++ b/packages/wallets/src/signers/shadow-signer/stellar-shadow-signer.ts @@ -1,16 +1,15 @@ import { ShadowSigner } from "./shadow-signer"; import type { StellarChain } from "@/chains/chains"; import type { ExternalWalletInternalSignerConfig } from "../types"; -import type { ShadowSignerData, ShadowSignerStorage } from "./utils"; +import type { ShadowSignerData } from "./utils"; import { StellarExternalWalletSigner } from "../stellar-external-wallet"; -export class StellarShadowSigner extends ShadowSigner { - protected getExternalWalletSignerClass(): new ( - config: ExternalWalletInternalSignerConfig, - walletAddress?: string, - shadowSignerEnabled?: boolean, - shadowSignerStorage?: ShadowSignerStorage - ) => StellarExternalWalletSigner { +export class StellarShadowSigner extends ShadowSigner< + StellarChain, + StellarExternalWalletSigner, + ExternalWalletInternalSignerConfig +> { + protected getWrappedSignerClass() { return StellarExternalWalletSigner; } @@ -19,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/shadow-signer/utils.ts b/packages/wallets/src/signers/shadow-signer/utils.ts index d283bc9e3..02ccf7ff0 100644 --- a/packages/wallets/src/signers/shadow-signer/utils.ts +++ b/packages/wallets/src/signers/shadow-signer/utils.ts @@ -18,7 +18,7 @@ export type ShadowSignerResult = { }; 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,32 +40,31 @@ export async function generateShadowSigner( 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); + const publicKeyBase64 = await storageInstance.keyGenerator(chain); - 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); - } + const publicKeyBuffer = Buffer.from(publicKeyBase64, "base64"); + const publicKeyBytes = new Uint8Array(publicKeyBuffer); - return { - shadowSigner: { - type: "device", - address: 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, - }; + let encodedPublicKey: string; + if (chain === "stellar") { + // Stellar uses Ed25519 encoding (Base32 with version byte and checksum) + encodedPublicKey = encodeEd25519PublicKey(publicKeyBytes); + } else if (chain === "solana") { + // Solana uses Base58 encoding + encodedPublicKey = encodeBase58(publicKeyBytes); + } else { + encodedPublicKey = publicKeyBase64; } - // TODO: Add support for EVM chains - throw new Error("Unsupported chain"); + + return { + shadowSigner: { + type: "device", + address: 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, + }; } export async function storeShadowSigner( diff --git a/packages/wallets/src/signers/types.ts b/packages/wallets/src/signers/types.ts index 3aa6f7117..667d62553 100644 --- a/packages/wallets/src/signers/types.ts +++ b/packages/wallets/src/signers/types.ts @@ -8,6 +8,7 @@ import type { } from "@crossmint/client-signers"; import type { Crossmint, + P256KeypairSignerConfig, EvmExternalWalletSignerConfig, SolanaExternalWalletSignerConfig, StellarExternalWalletSignerConfig, @@ -15,6 +16,7 @@ import type { import type { Chain, SolanaChain, StellarChain } from "../chains/chains"; export type { + P256KeypairSignerConfig, EvmExternalWalletSignerConfig, SolanaExternalWalletSignerConfig, StellarExternalWalletSignerConfig, @@ -71,7 +73,8 @@ export type ApiKeySignerConfig = { type: "api-key" }; export type BaseSignerConfig = | ExternalWalletSignerConfigForChain | ApiKeySignerConfig - | ShadowSignerConfig; + | ShadowSignerConfig + | (C extends SolanaChain | StellarChain ? never : P256KeypairSignerConfig); export type PasskeySignerConfig = { type: "passkey"; @@ -110,12 +113,18 @@ export type ExternalWalletInternalSignerConfig = ExternalWallet locator: string; }; +export type P256KeypairInternalSignerConfig = P256KeypairSignerConfig & { + locator: string; + onSignTransaction: (publicKeyBase64: string, data: Uint8Array) => Promise; +}; + export type InternalSignerConfig = | EmailInternalSignerConfig | PhoneInternalSignerConfig | PasskeyInternalSignerConfig | ApiKeyInternalSignerConfig - | ExternalWalletInternalSignerConfig; + | ExternalWalletInternalSignerConfig + | P256KeypairInternalSignerConfig; //////////////////////////////////////////////////////////// // Signers @@ -146,7 +155,9 @@ type SignResultMap = { phone: BaseSignResult; "api-key": BaseSignResult; "external-wallet": BaseSignResult; + "p256-keypair": BaseSignResult; passkey: PasskeySignResult; + device: BaseSignResult; }; export interface Signer { 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,