Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions .changeset/fix-server-signer-recovery-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@crossmint/wallets-sdk": patch
---

Fix TypeError in `isRecoverySigner` when recovery config from API lacks `secret` field

When calling `wallet.useSigner({ type: "server", secret: "xmsk1_..." })` on a wallet fetched via `getWallet`, the `isRecoverySigner()` method would call `deriveServerSignerDetails()` on the API-sourced recovery config which has shape `{type: "server", address: "..."}` without a `secret` field. This caused `stripAndValidateSecret(undefined)` to throw `TypeError: Cannot read properties of undefined (reading 'startsWith')`.

The fix uses the `address` field from the API recovery config directly instead of attempting to re-derive it from a non-existent secret.
26 changes: 19 additions & 7 deletions packages/wallets/src/wallets/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1351,13 +1351,25 @@ export class Wallet<C extends Chain> {
this.#apiClient.projectId,
this.#apiClient.environment
).derivedAddress;
const recoveryDerived = deriveServerSignerDetails(
recovery,
this.chain,
this.#apiClient.projectId,
this.#apiClient.environment
).derivedAddress;
return inputDerived === recoveryDerived;

// When the recovery config comes from the API (via getWallet), it has
// {type: "server", address: "..."} without the secret — use it directly.
// When it comes from the user (via createWallet), it has a secret so we can derive.
if ("address" in recovery && typeof recovery.address === "string") {
return inputDerived === recovery.address;
}
Comment on lines +1358 to +1360
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Type definition does not reflect API runtime shape

ServerSignerConfig is typed as { type: "server"; secret: string } with no address field, but the API-sourced recovery object has { type: "server", address: "..." }. The "address" in recovery runtime guard works, but recovery.address is only accessible because TypeScript's in-narrowing widens the type; the declared type never acknowledges this field. This creates a gap where tools like IDE autocomplete, exhaustiveness checks, or future type-level guards may behave unexpectedly.

Consider introducing a discriminated union such as:

export type ServerSignerConfig =
    | { type: "server"; secret: string }          // user-provided (createWallet)
    | { type: "server"; address: string };         // API-sourced  (getWallet)

or adding address?: string to the existing type, so the runtime shape is reflected statically.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/wallets/src/wallets/wallet.ts
Line: 1358-1360

Comment:
**Type definition does not reflect API runtime shape**

`ServerSignerConfig` is typed as `{ type: "server"; secret: string }` with no `address` field, but the API-sourced recovery object has `{ type: "server", address: "..." }`. The `"address" in recovery` runtime guard works, but `recovery.address` is only accessible because TypeScript's `in`-narrowing widens the type; the declared type never acknowledges this field. This creates a gap where tools like IDE autocomplete, exhaustiveness checks, or future type-level guards may behave unexpectedly.

Consider introducing a discriminated union such as:

```ts
export type ServerSignerConfig =
    | { type: "server"; secret: string }          // user-provided (createWallet)
    | { type: "server"; address: string };         // API-sourced  (getWallet)
```

or adding `address?: string` to the existing type, so the runtime shape is reflected statically.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed that the type definition doesn't reflect the runtime shape. However, updating ServerSignerConfig to a discriminated union would be a broader change that touches many consumers across the codebase (wallet-factory, signer locator utils, etc.) and is better suited as a separate follow-up PR.

The "address" in recovery runtime guard is safe and idiomatic TypeScript narrowing for this bugfix. Keeping the scope minimal here to unblock the customer.

if ("secret" in recovery && typeof recovery.secret === "string") {
return (
inputDerived ===
deriveServerSignerDetails(
recovery,
this.chain,
this.#apiClient.projectId,
this.#apiClient.environment
).derivedAddress
);
}
return false;
}

// For other types, compare locators
Expand Down
Loading