From ebb7fd19ded1b41311ae3aa4ecd5263b5778e692 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 15:29:26 +0000 Subject: [PATCH 01/24] feat(wallets): normalize EVM server signer derivation to use 'evm' chain type with legacy fallback Co-Authored-By: Guille --- .changeset/evm-server-signer-normalization.md | 11 ++ .../server/helpers/derive-server-signer.ts | 35 ++++- .../src/signers/server/helpers/index.ts | 6 +- packages/wallets/src/signers/server/index.ts | 2 +- .../src/signers/server/server-signers.test.ts | 67 ++++++++- .../src/utils/server-key-derivation.test.ts | 6 + .../src/utils/server-key-derivation.ts | 3 +- packages/wallets/src/wallets/wallet.test.ts | 131 ++++++++++++++++-- packages/wallets/src/wallets/wallet.ts | 82 +++++++++-- 9 files changed, 310 insertions(+), 33 deletions(-) create mode 100644 .changeset/evm-server-signer-normalization.md diff --git a/.changeset/evm-server-signer-normalization.md b/.changeset/evm-server-signer-normalization.md new file mode 100644 index 000000000..3544d6cc8 --- /dev/null +++ b/.changeset/evm-server-signer-normalization.md @@ -0,0 +1,11 @@ +--- +"@crossmint/wallets-sdk": minor +--- + +Normalize EVM server signer key derivation to use "evm" chain type + +- Add `deriveServerSignerCandidates` helper that returns both primary ("evm") and legacy (chain-specific) derivations +- Update `deriveServerSignerDetails` to use normalized "evm" chain type for all EVM chains +- Implement dual-derivation fallback in `useSigner`: try primary first, fall back to legacy for backward compatibility +- Update `isRecoverySigner` to match against either derivation +- Cache resolved server derivation in `buildInternalSignerConfig` for signing consistency diff --git a/packages/wallets/src/signers/server/helpers/derive-server-signer.ts b/packages/wallets/src/signers/server/helpers/derive-server-signer.ts index 059459051..8dfff8c9c 100644 --- a/packages/wallets/src/signers/server/helpers/derive-server-signer.ts +++ b/packages/wallets/src/signers/server/helpers/derive-server-signer.ts @@ -30,8 +30,39 @@ export function deriveServerSignerDetails( throw new Error("Server signers can only be used from server-side code."); } - const chainStr = typeof chain === "string" ? chain : String(chain); - const derivedKeyBytes = deriveKeyBytes(signer.secret, projectId, environment, chainStr); + const chainType = getChainType(chain); + const derivedKeyBytes = deriveKeyBytes(signer.secret, projectId, environment, chainType); const derivedAddress = deriveServerSignerAddress(derivedKeyBytes, chain); return { derivedKeyBytes, derivedAddress }; } + +export function deriveServerSignerCandidates( + signer: ServerSignerConfig, + chain: Chain, + projectId: string, + environment: string +): { + primary: { derivedKeyBytes: Uint8Array; derivedAddress: string }; + legacy: { derivedKeyBytes: Uint8Array; derivedAddress: string } | null; +} { + if (typeof window !== "undefined") { + throw new Error("Server signers can only be used from server-side code."); + } + + const chainType = getChainType(chain); + + // Primary: use normalized chain type ("evm" | "solana" | "stellar") + const primaryBytes = deriveKeyBytes(signer.secret, projectId, environment, chainType); + const primaryAddress = deriveServerSignerAddress(primaryBytes, chain); + + // Legacy: chain-specific derivation (only matters for EVM where chain !== chainType) + const chainStr = typeof chain === "string" ? chain : String(chain); + let legacy: { derivedKeyBytes: Uint8Array; derivedAddress: string } | null = null; + if (chainType === "evm" && chainStr !== "evm") { + const legacyBytes = deriveKeyBytes(signer.secret, projectId, environment, chainStr); + const legacyAddress = deriveServerSignerAddress(legacyBytes, chain); + legacy = { derivedKeyBytes: legacyBytes, derivedAddress: legacyAddress }; + } + + return { primary: { derivedKeyBytes: primaryBytes, derivedAddress: primaryAddress }, legacy }; +} diff --git a/packages/wallets/src/signers/server/helpers/index.ts b/packages/wallets/src/signers/server/helpers/index.ts index a5e263eab..63894107d 100644 --- a/packages/wallets/src/signers/server/helpers/index.ts +++ b/packages/wallets/src/signers/server/helpers/index.ts @@ -1,2 +1,6 @@ -export { deriveServerSignerAddress, deriveServerSignerDetails } from "./derive-server-signer"; +export { + deriveServerSignerAddress, + deriveServerSignerDetails, + deriveServerSignerCandidates, +} from "./derive-server-signer"; export { getChainType } from "./get-chain-type"; diff --git a/packages/wallets/src/signers/server/index.ts b/packages/wallets/src/signers/server/index.ts index 32afd7c26..348300871 100644 --- a/packages/wallets/src/signers/server/index.ts +++ b/packages/wallets/src/signers/server/index.ts @@ -2,4 +2,4 @@ export { assembleServerSigner } from "./assemble-server-signer"; export { EVMServerSigner } from "./evm-server-signer"; export { SolanaServerSigner } from "./solana-server-signer"; export { StellarServerSigner } from "./stellar-server-signer"; -export { deriveServerSignerAddress, deriveServerSignerDetails } from "./helpers"; +export { deriveServerSignerAddress, deriveServerSignerDetails, deriveServerSignerCandidates } from "./helpers"; diff --git a/packages/wallets/src/signers/server/server-signers.test.ts b/packages/wallets/src/signers/server/server-signers.test.ts index 6e79ae5fe..af9ed5923 100644 --- a/packages/wallets/src/signers/server/server-signers.test.ts +++ b/packages/wallets/src/signers/server/server-signers.test.ts @@ -8,7 +8,12 @@ import type { ServerInternalSignerConfig } from "../types"; import { EVMServerSigner } from "./evm-server-signer"; import { SolanaServerSigner } from "./solana-server-signer"; import { StellarServerSigner } from "./stellar-server-signer"; -import { assembleServerSigner, deriveServerSignerAddress, deriveServerSignerDetails } from "./index"; +import { + assembleServerSigner, + deriveServerSignerAddress, + deriveServerSignerDetails, + deriveServerSignerCandidates, +} from "./index"; const TEST_SECRET = "a".repeat(64); const PROJECT_ID = "project-123"; @@ -187,6 +192,16 @@ describe("deriveServerSignerDetails", () => { expect(result.derivedAddress).toMatch(/^0x[0-9a-fA-F]{40}$/); }); + it("uses normalized 'evm' chain type for EVM chains", () => { + const config = { type: "server" as const, secret: TEST_SECRET }; + const baseSepolia = deriveServerSignerDetails(config, "base-sepolia", PROJECT_ID, ENVIRONMENT); + const ethereum = deriveServerSignerDetails(config, "ethereum", PROJECT_ID, ENVIRONMENT); + + // Both EVM chains should produce the same derivation (both use "evm") + expect(baseSepolia.derivedAddress).toBe(ethereum.derivedAddress); + expect(baseSepolia.derivedKeyBytes).toEqual(ethereum.derivedKeyBytes); + }); + it("throws when called in a browser environment", () => { const config = { type: "server" as const, secret: TEST_SECRET }; @@ -200,3 +215,53 @@ describe("deriveServerSignerDetails", () => { } }); }); + +describe("deriveServerSignerCandidates", () => { + const config = { type: "server" as const, secret: TEST_SECRET }; + + it("returns primary (evm) and legacy (chain-specific) for EVM chains", () => { + const result = deriveServerSignerCandidates(config, "base-sepolia", PROJECT_ID, ENVIRONMENT); + + expect(result.primary.derivedKeyBytes).toBeInstanceOf(Uint8Array); + expect(result.primary.derivedAddress).toMatch(/^0x[0-9a-fA-F]{40}$/); + expect(result.legacy).not.toBeNull(); + expect(result.legacy!.derivedKeyBytes).toBeInstanceOf(Uint8Array); + expect(result.legacy!.derivedAddress).toMatch(/^0x[0-9a-fA-F]{40}$/); + + // Primary and legacy should produce different addresses for EVM chains + expect(result.primary.derivedAddress).not.toBe(result.legacy!.derivedAddress); + }); + + it("returns null legacy for solana", () => { + const result = deriveServerSignerCandidates(config, "solana", PROJECT_ID, ENVIRONMENT); + + expect(result.primary.derivedKeyBytes).toBeInstanceOf(Uint8Array); + expect(result.legacy).toBeNull(); + }); + + it("returns null legacy for stellar", () => { + const result = deriveServerSignerCandidates(config, "stellar", PROJECT_ID, ENVIRONMENT); + + expect(result.primary.derivedKeyBytes).toBeInstanceOf(Uint8Array); + expect(result.legacy).toBeNull(); + }); + + it("primary matches deriveServerSignerDetails output", () => { + const candidates = deriveServerSignerCandidates(config, "base-sepolia", PROJECT_ID, ENVIRONMENT); + const details = deriveServerSignerDetails(config, "base-sepolia", PROJECT_ID, ENVIRONMENT); + + expect(candidates.primary.derivedAddress).toBe(details.derivedAddress); + expect(candidates.primary.derivedKeyBytes).toEqual(details.derivedKeyBytes); + }); + + it("throws when called in a browser environment", () => { + vi.stubGlobal("window", {}); + try { + expect(() => deriveServerSignerCandidates(config, "base-sepolia", PROJECT_ID, ENVIRONMENT)).toThrow( + "Server signers can only be used from server-side code." + ); + } finally { + vi.unstubAllGlobals(); + } + }); +}); diff --git a/packages/wallets/src/utils/server-key-derivation.test.ts b/packages/wallets/src/utils/server-key-derivation.test.ts index a5057b1ec..74d7fa3bf 100644 --- a/packages/wallets/src/utils/server-key-derivation.test.ts +++ b/packages/wallets/src/utils/server-key-derivation.test.ts @@ -34,6 +34,12 @@ describe("deriveKeyBytes", () => { expect(solana).not.toEqual(stellar); }); + it("produces different keys for 'evm' vs chain-specific strings like 'base-sepolia'", () => { + const evm = deriveKeyBytes(TEST_SECRET, PROJECT_ID, ENVIRONMENT, "evm"); + const baseSepolia = deriveKeyBytes(TEST_SECRET, PROJECT_ID, ENVIRONMENT, "base-sepolia"); + expect(evm).not.toEqual(baseSepolia); + }); + it("produces different keys for different environments", () => { const staging = deriveKeyBytes(TEST_SECRET, PROJECT_ID, "staging", "ethereum"); const production = deriveKeyBytes(TEST_SECRET, PROJECT_ID, "production", "ethereum"); diff --git a/packages/wallets/src/utils/server-key-derivation.ts b/packages/wallets/src/utils/server-key-derivation.ts index 6df1ece8a..a0e3ae02b 100644 --- a/packages/wallets/src/utils/server-key-derivation.ts +++ b/packages/wallets/src/utils/server-key-derivation.ts @@ -14,7 +14,8 @@ const SECRET_PREFIX = "xmsk1_"; * @param secret - Master secret (with or without xmsk1_ prefix), 64-char hex * @param projectId - Project ID from the API key * @param environment - Environment from the API key (staging, production) - * @param chain - Chain identifier (e.g., "base-sepolia", "ethereum") + * @param chain - Chain identifier. For new EVM derivations, pass "evm" (normalized). + * Legacy wallets may still use chain-specific strings (e.g., "base-sepolia", "ethereum"). * @returns Raw 32-byte derived key */ export function deriveKeyBytes(secret: string, projectId: string, environment: string, chain: string): Uint8Array { diff --git a/packages/wallets/src/wallets/wallet.test.ts b/packages/wallets/src/wallets/wallet.test.ts index 39ecb58b2..b34a04087 100644 --- a/packages/wallets/src/wallets/wallet.test.ts +++ b/packages/wallets/src/wallets/wallet.test.ts @@ -23,6 +23,10 @@ vi.mock("@/signers/server", async (importOriginal) => { derivedKeyBytes: new Uint8Array(32), derivedAddress: "0xDerivedServerAddress", }), + deriveServerSignerCandidates: vi.fn().mockReturnValue({ + primary: { derivedKeyBytes: new Uint8Array(32), derivedAddress: "0xDerivedServerAddress" }, + legacy: { derivedKeyBytes: new Uint8Array(32).fill(1), derivedAddress: "0xLegacyServerAddress" }, + }), assembleServerSigner: vi.fn().mockReturnValue({ type: "server", locator: () => "server:0xDerivedServerAddress", @@ -1710,12 +1714,19 @@ describe("Wallet - useSigner()", () => { }); it("addSigner should succeed after useSigner with matching server signer and API-sourced recovery", async () => { - const { deriveServerSignerDetails, assembleServerSigner } = await import("@/signers/server"); + const { deriveServerSignerDetails, deriveServerSignerCandidates, assembleServerSigner } = await import( + "@/signers/server" + ); const mockedDerive = vi.mocked(deriveServerSignerDetails); mockedDerive.mockReturnValue({ derivedKeyBytes: new Uint8Array(32), derivedAddress: "0xRecoveryAddress", }); + const mockedCandidates = vi.mocked(deriveServerSignerCandidates); + mockedCandidates.mockReturnValue({ + primary: { derivedKeyBytes: new Uint8Array(32), derivedAddress: "0xRecoveryAddress" }, + legacy: null, + }); // Override assembleServerSigner so the assembled signer address matches recovery const mockedAssemble = vi.mocked(assembleServerSigner); mockedAssemble.mockReturnValue({ @@ -1737,17 +1748,6 @@ describe("Wallet - useSigner()", () => { mockApiClient as unknown as ApiClient ); vi.spyOn(wallet, "signers").mockResolvedValue([]); - mockApiClient.getSigner.mockImplementation((_walletLocator: string, signerLocator: string) => { - if (signerLocator === "server:0xRecoveryAddress") { - return Promise.resolve({ - type: "server", - address: "0xRecoveryAddress", - locator: "server:0xRecoveryAddress", - chains: { "base-sepolia": { status: "active", id: "sig-server" } }, - }); - } - return Promise.reject(new Error("Signer not found")); - }); // Set the matching recovery server signer await wallet.useSigner({ type: "server", secret: "recovery-secret" } as any); @@ -1953,6 +1953,113 @@ describe("Wallet - useSigner()", () => { ); }); }); + + describe("server signer EVM normalization fallback", () => { + it("useSigner works when only the primary (evm) derivation is registered", async () => { + const { deriveServerSignerCandidates } = await import("@/signers/server"); + const mockedCandidates = vi.mocked(deriveServerSignerCandidates); + mockedCandidates.mockReturnValue({ + primary: { derivedKeyBytes: new Uint8Array(32), derivedAddress: "0xPrimaryAddress" }, + legacy: { derivedKeyBytes: new Uint8Array(32).fill(1), derivedAddress: "0xLegacyAddress" }, + }); + + mockApiClient = createMockApiClient(); + const wallet = new Wallet( + { + chain: "base-sepolia" as const, + address: "0x1234567890123456789012345678901234567890", + recovery: { type: "api-key" } as any, + }, + mockApiClient as unknown as ApiClient + ); + // Primary is registered in the signers list + vi.spyOn(wallet, "signers").mockResolvedValue([ + { + type: "server", + address: "0xPrimaryAddress", + locator: "server:0xPrimaryAddress", + status: "success" as const, + } as any, + ]); + + await wallet.useSigner({ type: "server", secret: "test-secret" } as any); + + expect(wallet.signer).toBeDefined(); + expect(wallet.signer?.type).toBe("server"); + }); + + it("useSigner falls back to legacy derivation when primary is not registered", async () => { + const { deriveServerSignerCandidates, assembleServerSigner } = await import("@/signers/server"); + const mockedCandidates = vi.mocked(deriveServerSignerCandidates); + mockedCandidates.mockReturnValue({ + primary: { derivedKeyBytes: new Uint8Array(32), derivedAddress: "0xPrimaryAddress" }, + legacy: { derivedKeyBytes: new Uint8Array(32).fill(1), derivedAddress: "0xLegacyAddress" }, + }); + const mockedAssemble = vi.mocked(assembleServerSigner); + mockedAssemble.mockReturnValue({ + type: "server", + locator: () => "server:0xLegacyAddress", + address: () => "0xLegacyAddress", + status: undefined, + signMessage: vi.fn().mockResolvedValue({ signature: "0xmocksig" }), + signTransaction: vi.fn().mockResolvedValue({ signature: "0xmocksig" }), + } as any); + + mockApiClient = createMockApiClient(); + const wallet = new Wallet( + { + chain: "base-sepolia" as const, + address: "0x1234567890123456789012345678901234567890", + recovery: { type: "api-key" } as any, + }, + mockApiClient as unknown as ApiClient + ); + // Only legacy is in the signers list (not primary) + vi.spyOn(wallet, "signers").mockResolvedValue([ + { + type: "server", + address: "0xLegacyAddress", + locator: "server:0xLegacyAddress", + status: "success" as const, + } as any, + ]); + + await wallet.useSigner({ type: "server", secret: "test-secret" } as any); + + expect(wallet.signer).toBeDefined(); + expect(wallet.signer?.type).toBe("server"); + }); + + it("isRecoverySigner matches when recovery address matches legacy derivation", async () => { + const { deriveServerSignerCandidates } = await import("@/signers/server"); + const mockedCandidates = vi.mocked(deriveServerSignerCandidates); + mockedCandidates.mockReturnValue({ + primary: { derivedKeyBytes: new Uint8Array(32), derivedAddress: "0xPrimaryAddress" }, + legacy: { derivedKeyBytes: new Uint8Array(32).fill(1), derivedAddress: "0xLegacyAddress" }, + }); + + mockApiClient = createMockApiClient(); + // Recovery was registered with the legacy address + const wallet = new Wallet( + { + chain: "base-sepolia" as const, + address: "0x1234567890123456789012345678901234567890", + recovery: { type: "server", address: "0xLegacyAddress" } as ApiSourcedServerSignerConfig, + }, + mockApiClient as unknown as ApiClient + ); + vi.spyOn(wallet, "signers").mockResolvedValue([]); + + // Neither primary nor legacy registered as delegated — falls through to isRecoverySigner + mockApiClient.getSigner.mockRejectedValue(new Error("Signer not found")); + + // Should succeed because legacy derivation matches the API-sourced recovery address + await wallet.useSigner({ type: "server", secret: "test-secret" } as any); + + expect(wallet.signer).toBeDefined(); + expect(wallet.signer?.type).toBe("server"); + }); + }); }); describe("Wallet - recover()", () => { diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index b1f79f1dc..4e881d193 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -68,7 +68,7 @@ import type { import { type ApiSourcedServerSignerConfig, isApiSourcedServerSignerConfig, AuthRejectedError } from "../signers/types"; import { assembleSigner } from "../signers"; import { NonCustodialSigner } from "../signers/non-custodial"; -import { deriveServerSignerDetails } from "../signers/server"; +import { deriveServerSignerDetails, deriveServerSignerCandidates } from "../signers/server"; import { walletsLogger } from "../logger"; import { getSignerLocator } from "../utils/signer-locator"; @@ -98,6 +98,7 @@ export class Wallet { #initialSigners: SignerConfigForChain[]; #needsRecovery = false; #deviceSignerApproved = false; + #resolvedServerDerivation: { derivedKeyBytes: Uint8Array; derivedAddress: string } | null = null; #signerInitialization: Promise; #recovering: Promise | null = null; @@ -884,6 +885,7 @@ export class Wallet { }) public async useSigner(signer: SignerConfigForChain): Promise { walletsLogger.info("wallet.useSigner.start"); + this.#resolvedServerDerivation = null; this.validateSignerInput(signer); let isAdminSigner = false; @@ -928,6 +930,32 @@ export class Wallet { } } + // For server signers, try primary (evm) derivation first, fall back to legacy (chain-specific) + if (signer.type === "server") { + const { primary, legacy } = deriveServerSignerCandidates( + signer, + this.chain, + this.#apiClient.projectId, + this.#apiClient.environment + ); + if (await this.signerIsRegistered(`server:${primary.derivedAddress}`)) { + this.#resolvedServerDerivation = primary; + this.#needsRecovery = false; + return false; + } + if (legacy && (await this.signerIsRegistered(`server:${legacy.derivedAddress}`))) { + this.#resolvedServerDerivation = legacy; + this.#needsRecovery = false; + return false; + } + // Neither found — fall back to recovery + if (this.isRecoverySigner(signer)) { + this.#needsRecovery = false; + return true; + } + throw new Error(`Signer "server:${primary.derivedAddress}" is not registered in this wallet.`); + } + // Check if this is a registered signer const locator = this.resolveSignerLocator(signer); if (await this.signerIsRegistered(locator)) { @@ -972,6 +1000,11 @@ export class Wallet { */ private resolveSignerLocator(signer: SignerConfigForChain | ExternalWalletRegistrationConfig): string { if (signer.type === "server") { + // Use cached if available (set by resolveNonDeviceSigner) + if (this.#resolvedServerDerivation) { + return `server:${this.#resolvedServerDerivation.derivedAddress}`; + } + // Default: new "evm" derivation const { derivedAddress } = deriveServerSignerDetails( signer, this.chain, @@ -1461,20 +1494,29 @@ export class Wallet { // For server signers, the API-sourced recovery config has no secret, so we // can't derive a locator from it. Compare using the address field instead. if (signerConfig.type === "server" && recovery.type === "server") { - const resolveAddress = (config: ServerSignerConfig | ApiSourcedServerSignerConfig) => - isApiSourcedServerSignerConfig(config) - ? config.address - : deriveServerSignerDetails( - config, - this.chain, - this.#apiClient.projectId, - this.#apiClient.environment - ).derivedAddress; - - const signerAddress = resolveAddress(signerConfig); - const recoveryAddress = resolveAddress(recovery); - - if (signerAddress !== recoveryAddress) { + const resolveAddresses = (config: ServerSignerConfig | ApiSourcedServerSignerConfig): string[] => { + if (isApiSourcedServerSignerConfig(config)) { + return [config.address]; + } + const { primary, legacy } = deriveServerSignerCandidates( + config, + this.chain, + this.#apiClient.projectId, + this.#apiClient.environment + ); + const addresses = [primary.derivedAddress]; + if (legacy) { + addresses.push(legacy.derivedAddress); + } + return addresses; + }; + + const signerAddresses = resolveAddresses(signerConfig); + const recoveryAddresses = resolveAddresses(recovery); + + // Match if any signer address matches any recovery address + const matches = signerAddresses.some((a) => recoveryAddresses.includes(a)); + if (!matches) { return false; } } else { @@ -1659,6 +1701,16 @@ export class Wallet { address: this.address, } as InternalSignerConfig; case "server": { + // Use cached resolution from resolveNonDeviceSigner (matches what is on-chain) + if (this.#resolvedServerDerivation) { + return { + type: "server", + derivedKeyBytes: this.#resolvedServerDerivation.derivedKeyBytes, + locator: `server:${this.#resolvedServerDerivation.derivedAddress}` as ServerSignerLocator, + address: this.#resolvedServerDerivation.derivedAddress, + } as InternalSignerConfig; + } + // Fallback: new "evm" derivation (for recovery signer path where cache may not be set) const { derivedKeyBytes, derivedAddress } = deriveServerSignerDetails( config, this.chain, From fb328f0fecb352ab21b9dfc1598a6ba2ea307b86 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 15:38:36 +0000 Subject: [PATCH 02/24] fix(wallets): set #resolvedServerDerivation for server recovery signers to use correct key Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet.test.ts | 52 ++++++++++++++++++++- packages/wallets/src/wallets/wallet.ts | 19 ++++++-- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/packages/wallets/src/wallets/wallet.test.ts b/packages/wallets/src/wallets/wallet.test.ts index b34a04087..a010e0465 100644 --- a/packages/wallets/src/wallets/wallet.test.ts +++ b/packages/wallets/src/wallets/wallet.test.ts @@ -2030,13 +2030,22 @@ describe("Wallet - useSigner()", () => { expect(wallet.signer?.type).toBe("server"); }); - it("isRecoverySigner matches when recovery address matches legacy derivation", async () => { - const { deriveServerSignerCandidates } = await import("@/signers/server"); + it("isRecoverySigner matches when recovery address matches legacy derivation and uses legacy key", async () => { + const { deriveServerSignerCandidates, assembleServerSigner } = await import("@/signers/server"); const mockedCandidates = vi.mocked(deriveServerSignerCandidates); mockedCandidates.mockReturnValue({ primary: { derivedKeyBytes: new Uint8Array(32), derivedAddress: "0xPrimaryAddress" }, legacy: { derivedKeyBytes: new Uint8Array(32).fill(1), derivedAddress: "0xLegacyAddress" }, }); + const mockedAssemble = vi.mocked(assembleServerSigner); + mockedAssemble.mockReturnValue({ + type: "server", + locator: () => "server:0xLegacyAddress", + address: () => "0xLegacyAddress", + status: undefined, + signMessage: vi.fn().mockResolvedValue({ signature: "0xmocksig" }), + signTransaction: vi.fn().mockResolvedValue({ signature: "0xmocksig" }), + } as any); mockApiClient = createMockApiClient(); // Recovery was registered with the legacy address @@ -2058,6 +2067,45 @@ describe("Wallet - useSigner()", () => { expect(wallet.signer).toBeDefined(); expect(wallet.signer?.type).toBe("server"); + // The signer should use the legacy address (not primary), since that matches on-chain + expect(wallet.signer?.address()).toBe("0xLegacyAddress"); + }); + + it("recovery server signer uses primary derivation when recovery address matches primary", async () => { + const { deriveServerSignerCandidates, assembleServerSigner } = await import("@/signers/server"); + const mockedCandidates = vi.mocked(deriveServerSignerCandidates); + mockedCandidates.mockReturnValue({ + primary: { derivedKeyBytes: new Uint8Array(32), derivedAddress: "0xPrimaryAddress" }, + legacy: { derivedKeyBytes: new Uint8Array(32).fill(1), derivedAddress: "0xLegacyAddress" }, + }); + const mockedAssemble = vi.mocked(assembleServerSigner); + mockedAssemble.mockReturnValue({ + type: "server", + locator: () => "server:0xPrimaryAddress", + address: () => "0xPrimaryAddress", + status: undefined, + signMessage: vi.fn().mockResolvedValue({ signature: "0xmocksig" }), + signTransaction: vi.fn().mockResolvedValue({ signature: "0xmocksig" }), + } as any); + + mockApiClient = createMockApiClient(); + // Recovery was registered with the primary (evm) address + const wallet = new Wallet( + { + chain: "base-sepolia" as const, + address: "0x1234567890123456789012345678901234567890", + recovery: { type: "server", address: "0xPrimaryAddress" } as ApiSourcedServerSignerConfig, + }, + mockApiClient as unknown as ApiClient + ); + vi.spyOn(wallet, "signers").mockResolvedValue([]); + mockApiClient.getSigner.mockRejectedValue(new Error("Signer not found")); + + await wallet.useSigner({ type: "server", secret: "test-secret" } as any); + + expect(wallet.signer).toBeDefined(); + expect(wallet.signer?.type).toBe("server"); + expect(wallet.signer?.address()).toBe("0xPrimaryAddress"); }); }); }); diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index 4e881d193..d8a42cdb4 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -907,12 +907,14 @@ export class Wallet { * Returns true if the signer is an admin (recovery) signer. */ private async resolveNonDeviceSigner(signer: SignerConfigForChain): Promise { - // For non-passkey signers, check if this is the recovery (admin) signer first. + // For non-passkey, non-server signers, check if this is the recovery (admin) signer first. // Admin signers are always approved — skip the registration check and getSigner API call // which only works for delegated signers (returns 404/400 for admin signers). // Passkeys are excluded because isRecoverySigner matches by type only, which could // incorrectly match a delegated passkey. - if (signer.type !== "passkey" && this.isRecoverySigner(signer)) { + // Server signers are excluded so they always flow through the server signer block below, + // which sets #resolvedServerDerivation to the correct (primary or legacy) key. + if (signer.type !== "passkey" && signer.type !== "server" && this.isRecoverySigner(signer)) { this.#needsRecovery = false; return true; } @@ -948,8 +950,19 @@ export class Wallet { this.#needsRecovery = false; return false; } - // Neither found — fall back to recovery + // Neither found as delegated — check if this is the recovery (admin) signer. + // Capture the API-sourced recovery address before isRecoverySigner upgrades #recovery. + const recoveryAddress = + this.#recovery.type === "server" && isApiSourcedServerSignerConfig(this.#recovery) + ? this.#recovery.address + : null; if (this.isRecoverySigner(signer)) { + // Resolve which derivation matches the on-chain recovery address + if (recoveryAddress != null && legacy && legacy.derivedAddress === recoveryAddress) { + this.#resolvedServerDerivation = legacy; + } else { + this.#resolvedServerDerivation = primary; + } this.#needsRecovery = false; return true; } From 4ebcd27c53d2b903cd9891a80c6f44723597ca5b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 15:55:17 +0000 Subject: [PATCH 03/24] =?UTF-8?q?fix(wallets):=20address=20review=20feedba?= =?UTF-8?q?ck=20=E2=80=94=20validateSigners=20dual-derivation,=20condition?= =?UTF-8?q?al=20cache=20reset,=20resolveServerSignerApiLocator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Guille --- packages/wallets/src/wallets/evm.ts | 4 +-- .../wallets/src/wallets/wallet-factory.ts | 9 ++++--- packages/wallets/src/wallets/wallet.ts | 27 +++++++++++++++++-- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/packages/wallets/src/wallets/evm.ts b/packages/wallets/src/wallets/evm.ts index 5768deff6..23df8ee5d 100644 --- a/packages/wallets/src/wallets/evm.ts +++ b/packages/wallets/src/wallets/evm.ts @@ -15,7 +15,7 @@ import { Wallet } from "./wallet"; import type { Chain, EVMChain } from "../chains/chains"; import { InvalidTypedDataError, SignatureNotCreatedError, TransactionNotCreatedError } from "../utils/errors"; import type { CreateTransactionParams, CreateTransactionSuccessResponse } from "@/api"; -import { deriveServerSignerDetails } from "../signers/server"; +import type { ServerSignerConfig } from "../signers/types"; import { walletsLogger } from "../logger"; export class EVMWallet extends Wallet { @@ -222,7 +222,7 @@ export class EVMWallet extends Wallet { } else if (typeof options.signer === "string") { signer = options.signer; } else { - signer = `server:${deriveServerSignerDetails(options.signer, this.chain, this.apiClient.projectId, this.apiClient.environment).derivedAddress}`; + signer = this.resolveServerSignerApiLocator(options.signer as ServerSignerConfig); } const transactionCreationResponse = await this.apiClient.createTransaction(this.walletLocator, { params: { diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index a25617692..ccf1c2c1e 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -23,7 +23,7 @@ import { Wallet } from "./wallet"; import type { WalletArgsFor, WalletCreateArgs } from "./types"; import { compareSignerConfigs, normalizeValueForComparison } from "../utils/signer-validation"; import { getSignerLocator } from "../utils/signer-locator"; -import { deriveServerSignerDetails } from "../signers/server"; +import { deriveServerSignerDetails, deriveServerSignerCandidates } from "../signers/server"; import type { DeviceSignerKeyStorage } from "@/utils/device-signers/DeviceSignerKeyStorage"; import { createDeviceSigner } from "@/utils/device-signers"; @@ -342,13 +342,16 @@ export class WalletFactory { return true; } if (inputSigner.type === "server") { - const { derivedAddress } = deriveServerSignerDetails( + const { primary, legacy } = deriveServerSignerCandidates( inputSigner, chain, this.apiClient.projectId, this.apiClient.environment ); - return existingSigner.locator === `server:${derivedAddress}`; + return ( + existingSigner.locator === `server:${primary.derivedAddress}` || + (legacy != null && existingSigner.locator === `server:${legacy.derivedAddress}`) + ); } return existingSigner.locator === getSignerLocator(inputSigner); }); diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index d8a42cdb4..aebe2911b 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -253,6 +253,24 @@ export class Wallet { return this.#options; } + /** + * Resolve a ServerSignerConfig to an API locator string. + * Uses the cached derivation from useSigner when available (matches on-chain registration), + * otherwise falls back to the normalized "evm" derivation (correct for new wallets). + */ + protected resolveServerSignerApiLocator(signer: ServerSignerConfig): string { + if (this.#resolvedServerDerivation) { + return `server:${this.#resolvedServerDerivation.derivedAddress}`; + } + const { derivedAddress } = deriveServerSignerDetails( + signer, + this.chain, + this.#apiClient.projectId, + this.#apiClient.environment + ); + return `server:${derivedAddress}`; + } + /** * Get the recovery signer config * @returns The recovery signer config @@ -533,7 +551,7 @@ export class Wallet { } else if (typeof options.signer === "string") { signer = options.signer; } else { - signer = `server:${deriveServerSignerDetails(options.signer, this.chain, this.#apiClient.projectId, this.#apiClient.environment).derivedAddress}`; + signer = this.resolveServerSignerApiLocator(options.signer); } const sendParams = { @@ -885,7 +903,12 @@ export class Wallet { }) public async useSigner(signer: SignerConfigForChain): Promise { walletsLogger.info("wallet.useSigner.start"); - this.#resolvedServerDerivation = null; + // Only reset when processing a fresh server signer; for other signer types the cached + // derivation must be preserved so that withRecoverySigner (addSigner / removeSigner) + // continues to use the correct primary-or-legacy key for a server recovery signer. + if (signer.type === "server") { + this.#resolvedServerDerivation = null; + } this.validateSignerInput(signer); let isAdminSigner = false; From 42d846660ffba2efac0efccbf0a23b52e77755c2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 16:05:00 +0000 Subject: [PATCH 04/24] fix(wallets): store API-sourced recovery address for repeated useSigner calls, derive fresh in resolveServerSignerApiLocator Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index aebe2911b..dcb16cd98 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -99,6 +99,7 @@ export class Wallet { #needsRecovery = false; #deviceSignerApproved = false; #resolvedServerDerivation: { derivedKeyBytes: Uint8Array; derivedAddress: string } | null = null; + #apiSourcedRecoveryAddress: string | null = null; #signerInitialization: Promise; #recovering: Promise | null = null; @@ -111,6 +112,9 @@ export class Wallet { this.#options = options; this.alias = alias; this.#recovery = recovery; + if (recovery.type === "server" && isApiSourcedServerSignerConfig(recovery)) { + this.#apiSourcedRecoveryAddress = recovery.address; + } this.#initialSigners = signers ?? []; this.#signer = signer; // Can be set by useSigner this.#signerInitialization = this.initDefaultSigner(); @@ -255,13 +259,9 @@ export class Wallet { /** * Resolve a ServerSignerConfig to an API locator string. - * Uses the cached derivation from useSigner when available (matches on-chain registration), - * otherwise falls back to the normalized "evm" derivation (correct for new wallets). + * Always derives fresh from the passed config using the normalized chain type. */ protected resolveServerSignerApiLocator(signer: ServerSignerConfig): string { - if (this.#resolvedServerDerivation) { - return `server:${this.#resolvedServerDerivation.derivedAddress}`; - } const { derivedAddress } = deriveServerSignerDetails( signer, this.chain, @@ -974,14 +974,15 @@ export class Wallet { return false; } // Neither found as delegated — check if this is the recovery (admin) signer. - // Capture the API-sourced recovery address before isRecoverySigner upgrades #recovery. - const recoveryAddress = - this.#recovery.type === "server" && isApiSourcedServerSignerConfig(this.#recovery) - ? this.#recovery.address - : null; if (this.isRecoverySigner(signer)) { - // Resolve which derivation matches the on-chain recovery address - if (recoveryAddress != null && legacy && legacy.derivedAddress === recoveryAddress) { + // Resolve which derivation matches the on-chain recovery address. + // #apiSourcedRecoveryAddress is captured once from the original API response + // and survives across isRecoverySigner upgrades and repeated useSigner calls. + if ( + this.#apiSourcedRecoveryAddress != null && + legacy && + legacy.derivedAddress === this.#apiSourcedRecoveryAddress + ) { this.#resolvedServerDerivation = legacy; } else { this.#resolvedServerDerivation = primary; From b8198eb6db59d338f6f12ac9e712a0c9f24d1992 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 16:15:50 +0000 Subject: [PATCH 05/24] fix(wallets): remove cache from resolveSignerLocator to prevent cross-signer contamination Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index dcb16cd98..806d7b585 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -1037,11 +1037,6 @@ export class Wallet { */ private resolveSignerLocator(signer: SignerConfigForChain | ExternalWalletRegistrationConfig): string { if (signer.type === "server") { - // Use cached if available (set by resolveNonDeviceSigner) - if (this.#resolvedServerDerivation) { - return `server:${this.#resolvedServerDerivation.derivedAddress}`; - } - // Default: new "evm" derivation const { derivedAddress } = deriveServerSignerDetails( signer, this.chain, From f24e5f43945b4cea29a1de17026f156e2baeef1d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 16:22:35 +0000 Subject: [PATCH 06/24] fix(wallets): restore cache in resolveSignerLocator for legacy wallet removeSigner support Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index 806d7b585..51cb4389e 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -1037,6 +1037,11 @@ export class Wallet { */ private resolveSignerLocator(signer: SignerConfigForChain | ExternalWalletRegistrationConfig): string { if (signer.type === "server") { + // Use cached derivation from resolveNonDeviceSigner when available. + // This ensures removeSigner on a legacy wallet uses the correct (legacy) locator. + if (this.#resolvedServerDerivation) { + return `server:${this.#resolvedServerDerivation.derivedAddress}`; + } const { derivedAddress } = deriveServerSignerDetails( signer, this.chain, From df4d9da4b25f1fc1e2edcdadddd9e97a310a20c3 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 16:30:19 +0000 Subject: [PATCH 07/24] fix(wallets): restore cache in resolveServerSignerApiLocator for legacy wallet support Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index 51cb4389e..c94e7229e 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -259,9 +259,13 @@ export class Wallet { /** * Resolve a ServerSignerConfig to an API locator string. - * Always derives fresh from the passed config using the normalized chain type. + * Uses the cached derivation from useSigner when available (handles legacy wallets), + * otherwise falls back to the normalized "evm" derivation (correct for new wallets). */ protected resolveServerSignerApiLocator(signer: ServerSignerConfig): string { + if (this.#resolvedServerDerivation) { + return `server:${this.#resolvedServerDerivation.derivedAddress}`; + } const { derivedAddress } = deriveServerSignerDetails( signer, this.chain, From d109ad97409865f61a69ee7fba7770c395a9dc1e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 16:42:48 +0000 Subject: [PATCH 08/24] =?UTF-8?q?fix(wallets):=20validate=20cache=20owners?= =?UTF-8?q?hip=20before=20using=20it=20=E2=80=94=20prevent=20cross-signer?= =?UTF-8?q?=20contamination?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three cache consumers (resolveServerSignerApiLocator, resolveSignerLocator, buildInternalSignerConfig) now verify the cached derivation belongs to the signer being processed by checking if it matches either the primary or legacy derivation. Falls back to fresh derivation on mismatch. Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet.ts | 75 +++++++++++++++++++------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index c94e7229e..0db1716e8 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -259,12 +259,22 @@ export class Wallet { /** * Resolve a ServerSignerConfig to an API locator string. - * Uses the cached derivation from useSigner when available (handles legacy wallets), + * Uses the cached derivation from useSigner when it belongs to this signer (handles legacy wallets), * otherwise falls back to the normalized "evm" derivation (correct for new wallets). */ protected resolveServerSignerApiLocator(signer: ServerSignerConfig): string { if (this.#resolvedServerDerivation) { - return `server:${this.#resolvedServerDerivation.derivedAddress}`; + const { primary, legacy } = deriveServerSignerCandidates( + signer, + this.chain, + this.#apiClient.projectId, + this.#apiClient.environment + ); + const cachedAddr = this.#resolvedServerDerivation.derivedAddress; + if (cachedAddr === primary.derivedAddress || (legacy != null && cachedAddr === legacy.derivedAddress)) { + return `server:${cachedAddr}`; + } + return `server:${primary.derivedAddress}`; } const { derivedAddress } = deriveServerSignerDetails( signer, @@ -1041,10 +1051,19 @@ export class Wallet { */ private resolveSignerLocator(signer: SignerConfigForChain | ExternalWalletRegistrationConfig): string { if (signer.type === "server") { - // Use cached derivation from resolveNonDeviceSigner when available. - // This ensures removeSigner on a legacy wallet uses the correct (legacy) locator. + // Use cached derivation when it belongs to this signer (handles legacy wallets). if (this.#resolvedServerDerivation) { - return `server:${this.#resolvedServerDerivation.derivedAddress}`; + const { primary, legacy } = deriveServerSignerCandidates( + signer, + this.chain, + this.#apiClient.projectId, + this.#apiClient.environment + ); + const cachedAddr = this.#resolvedServerDerivation.derivedAddress; + if (cachedAddr === primary.derivedAddress || (legacy != null && cachedAddr === legacy.derivedAddress)) { + return `server:${cachedAddr}`; + } + return `server:${primary.derivedAddress}`; } const { derivedAddress } = deriveServerSignerDetails( signer, @@ -1742,27 +1761,45 @@ export class Wallet { address: this.address, } as InternalSignerConfig; case "server": { - // Use cached resolution from resolveNonDeviceSigner (matches what is on-chain) - if (this.#resolvedServerDerivation) { - return { - type: "server", - derivedKeyBytes: this.#resolvedServerDerivation.derivedKeyBytes, - locator: `server:${this.#resolvedServerDerivation.derivedAddress}` as ServerSignerLocator, - address: this.#resolvedServerDerivation.derivedAddress, - } as InternalSignerConfig; - } - // Fallback: new "evm" derivation (for recovery signer path where cache may not be set) - const { derivedKeyBytes, derivedAddress } = deriveServerSignerDetails( + const { primary, legacy } = deriveServerSignerCandidates( config, this.chain, this.#apiClient.projectId, this.#apiClient.environment ); + // Use cached resolution when it belongs to this signer (matches what is on-chain) + if (this.#resolvedServerDerivation) { + const cachedAddr = this.#resolvedServerDerivation.derivedAddress; + if ( + cachedAddr === primary.derivedAddress || + (legacy != null && cachedAddr === legacy.derivedAddress) + ) { + return { + type: "server", + derivedKeyBytes: this.#resolvedServerDerivation.derivedKeyBytes, + locator: `server:${cachedAddr}` as ServerSignerLocator, + address: cachedAddr, + } as InternalSignerConfig; + } + } + // Fallback: prefer legacy if it matches the original recovery address + if ( + this.#apiSourcedRecoveryAddress != null && + legacy != null && + legacy.derivedAddress === this.#apiSourcedRecoveryAddress + ) { + return { + type: "server", + derivedKeyBytes: legacy.derivedKeyBytes, + locator: `server:${legacy.derivedAddress}` as ServerSignerLocator, + address: legacy.derivedAddress, + } as InternalSignerConfig; + } return { type: "server", - derivedKeyBytes, - locator: `server:${derivedAddress}` as ServerSignerLocator, - address: derivedAddress, + derivedKeyBytes: primary.derivedKeyBytes, + locator: `server:${primary.derivedAddress}` as ServerSignerLocator, + address: primary.derivedAddress, } as InternalSignerConfig; } default: From 81ab88a2de425e7004d2b4ac534c08348aa4d288 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 16:59:58 +0000 Subject: [PATCH 09/24] fix(wallets): pass API recovery address from factory to wallet constructor The factory replaces the API-sourced recovery config (address-only) with the user-provided config (secret-only), so #apiSourcedRecoveryAddress was never populated in the production path. Now the factory extracts the API recovery address and passes it as a separate constructor parameter, ensuring legacy wallet derivation detection works correctly for admin signers. Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet-factory.ts | 8 ++++++++ packages/wallets/src/wallets/wallet.ts | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index ccf1c2c1e..ec2acfb4c 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -204,6 +204,13 @@ export class WalletFactory { signers = createArgs.signers as SignerResponse[]; } + // Preserve the API-sourced recovery address so the wallet can identify + // legacy derivations even when the user-provided config replaces the API one. + const apiRecoveryAddress = + apiRecovery.type === "server" && "address" in apiRecovery && !("secret" in apiRecovery) + ? (apiRecovery as { address: string }).address + : undefined; + const wallet = new Wallet( { chain: args.chain, @@ -212,6 +219,7 @@ export class WalletFactory { options: args.options, alias: args.alias, recovery, + apiRecoveryAddress, signers: (signers ?? []) as SignerConfigForChain[], }, this.apiClient diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index 0db1716e8..e626525ab 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -82,6 +82,7 @@ type WalletContructorType = { alias?: string; options?: WalletOptions; recovery: RecoverySignerConfigForChain; + apiRecoveryAddress?: string; signers?: SignerConfigForChain[]; signer?: SignerAdapter; }; @@ -104,7 +105,7 @@ export class Wallet { #recovering: Promise | null = null; constructor(args: WalletContructorType, apiClient: ApiClient) { - const { chain, address, owner, options, alias, recovery, signers, signer } = args; + const { chain, address, owner, options, alias, recovery, apiRecoveryAddress, signers, signer } = args; this.#apiClient = apiClient; this.chain = chain; this.address = address; @@ -112,7 +113,9 @@ export class Wallet { this.#options = options; this.alias = alias; this.#recovery = recovery; - if (recovery.type === "server" && isApiSourcedServerSignerConfig(recovery)) { + if (apiRecoveryAddress != null) { + this.#apiSourcedRecoveryAddress = apiRecoveryAddress; + } else if (recovery.type === "server" && isApiSourcedServerSignerConfig(recovery)) { this.#apiSourcedRecoveryAddress = recovery.address; } this.#initialSigners = signers ?? []; From 0a43d99bb7966c66b475d03abade7f8a7b3b2342 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 17:16:02 +0000 Subject: [PATCH 10/24] fix(wallets): propagate apiRecoveryAddress through all wallet subclass constructors Co-Authored-By: Guille --- packages/wallets/src/wallets/evm.ts | 1 + packages/wallets/src/wallets/solana.ts | 1 + packages/wallets/src/wallets/stellar.ts | 1 + packages/wallets/src/wallets/wallet.ts | 4 ++++ 4 files changed, 7 insertions(+) diff --git a/packages/wallets/src/wallets/evm.ts b/packages/wallets/src/wallets/evm.ts index 23df8ee5d..59e13d3c6 100644 --- a/packages/wallets/src/wallets/evm.ts +++ b/packages/wallets/src/wallets/evm.ts @@ -28,6 +28,7 @@ export class EVMWallet extends Wallet { options: Wallet.getOptions(wallet), alias: wallet.alias, recovery: Wallet.getRecovery(wallet), + apiRecoveryAddress: Wallet.getApiRecoveryAddress(wallet), signer: wallet.signer, signers: Wallet.getInitialSigners(wallet), }, diff --git a/packages/wallets/src/wallets/solana.ts b/packages/wallets/src/wallets/solana.ts index 52a24aa1b..cbeafdedd 100644 --- a/packages/wallets/src/wallets/solana.ts +++ b/packages/wallets/src/wallets/solana.ts @@ -25,6 +25,7 @@ export class SolanaWallet extends Wallet { options: Wallet.getOptions(wallet), alias: wallet.alias, recovery: Wallet.getRecovery(wallet), + apiRecoveryAddress: Wallet.getApiRecoveryAddress(wallet), signer: wallet.signer, signers: Wallet.getInitialSigners(wallet), }, diff --git a/packages/wallets/src/wallets/stellar.ts b/packages/wallets/src/wallets/stellar.ts index e6d08bdea..6748b7b83 100644 --- a/packages/wallets/src/wallets/stellar.ts +++ b/packages/wallets/src/wallets/stellar.ts @@ -28,6 +28,7 @@ export class StellarWallet extends Wallet { options: Wallet.getOptions(wallet), alias: wallet.alias, recovery: Wallet.getRecovery(wallet), + apiRecoveryAddress: Wallet.getApiRecoveryAddress(wallet), signer: wallet.signer, signers: Wallet.getInitialSigners(wallet), }, diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index e626525ab..4ea95578d 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -252,6 +252,10 @@ export class Wallet { return wallet.#initialSigners; } + protected static getApiRecoveryAddress(wallet: Wallet): string | undefined { + return wallet.#apiSourcedRecoveryAddress ?? undefined; + } + public get apiClient(): ApiClient { return this.#apiClient; } From 8f39cac6a8afdf2eecca63893a558b09b2d837ca Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 17:23:05 +0000 Subject: [PATCH 11/24] fix(wallets): include both primary and legacy addresses in unregistered signer error Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index 4ea95578d..8ec67a224 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -1011,7 +1011,10 @@ export class Wallet { this.#needsRecovery = false; return true; } - throw new Error(`Signer "server:${primary.derivedAddress}" is not registered in this wallet.`); + const tried = legacy + ? `"server:${primary.derivedAddress}" or "server:${legacy.derivedAddress}"` + : `"server:${primary.derivedAddress}"`; + throw new Error(`Signer ${tried} is not registered in this wallet.`); } // Check if this is a registered signer From b9a06a082b08c388a00a89ed97daec2a6f4b8379 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 09:40:47 +0000 Subject: [PATCH 12/24] refactor(wallets): extract resolveServerSigner and deduplicate derivation helpers - Extract server signer logic from resolveNonDeviceSigner into resolveServerSigner - Add deriveServerCandidates() to centralize the 4-arg derivation call - Add getOwnedCachedDerivation() to centralize cache ownership validation - Simplify resolveSignerLocator to delegate to resolveServerSignerApiLocator - Reduce duplication across resolveServerSignerApiLocator, resolveSignerLocator, isRecoverySigner, and buildInternalSignerConfig Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet.ts | 185 +++++++++++-------------- 1 file changed, 82 insertions(+), 103 deletions(-) diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index 8ec67a224..b544df91d 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -264,32 +264,43 @@ export class Wallet { return this.#options; } + /** + * Derive both primary ("evm") and legacy (chain-specific) server signer candidates. + */ + private deriveServerCandidates(signer: ServerSignerConfig) { + return deriveServerSignerCandidates(signer, this.chain, this.#apiClient.projectId, this.#apiClient.environment); + } + + /** + * Return the cached derivation if it belongs to the given signer (i.e. its address + * matches either the primary or legacy candidate). Returns null otherwise. + */ + private getOwnedCachedDerivation( + signer: ServerSignerConfig + ): { derivedKeyBytes: Uint8Array; derivedAddress: string } | null { + if (!this.#resolvedServerDerivation) { + return null; + } + const { primary, legacy } = this.deriveServerCandidates(signer); + const cachedAddr = this.#resolvedServerDerivation.derivedAddress; + if (cachedAddr === primary.derivedAddress || (legacy != null && cachedAddr === legacy.derivedAddress)) { + return this.#resolvedServerDerivation; + } + return null; + } + /** * Resolve a ServerSignerConfig to an API locator string. * Uses the cached derivation from useSigner when it belongs to this signer (handles legacy wallets), * otherwise falls back to the normalized "evm" derivation (correct for new wallets). */ protected resolveServerSignerApiLocator(signer: ServerSignerConfig): string { - if (this.#resolvedServerDerivation) { - const { primary, legacy } = deriveServerSignerCandidates( - signer, - this.chain, - this.#apiClient.projectId, - this.#apiClient.environment - ); - const cachedAddr = this.#resolvedServerDerivation.derivedAddress; - if (cachedAddr === primary.derivedAddress || (legacy != null && cachedAddr === legacy.derivedAddress)) { - return `server:${cachedAddr}`; - } - return `server:${primary.derivedAddress}`; + const cached = this.getOwnedCachedDerivation(signer); + if (cached) { + return `server:${cached.derivedAddress}`; } - const { derivedAddress } = deriveServerSignerDetails( - signer, - this.chain, - this.#apiClient.projectId, - this.#apiClient.environment - ); - return `server:${derivedAddress}`; + const { primary } = this.deriveServerCandidates(signer); + return `server:${primary.derivedAddress}`; } /** @@ -976,45 +987,8 @@ export class Wallet { } } - // For server signers, try primary (evm) derivation first, fall back to legacy (chain-specific) if (signer.type === "server") { - const { primary, legacy } = deriveServerSignerCandidates( - signer, - this.chain, - this.#apiClient.projectId, - this.#apiClient.environment - ); - if (await this.signerIsRegistered(`server:${primary.derivedAddress}`)) { - this.#resolvedServerDerivation = primary; - this.#needsRecovery = false; - return false; - } - if (legacy && (await this.signerIsRegistered(`server:${legacy.derivedAddress}`))) { - this.#resolvedServerDerivation = legacy; - this.#needsRecovery = false; - return false; - } - // Neither found as delegated — check if this is the recovery (admin) signer. - if (this.isRecoverySigner(signer)) { - // Resolve which derivation matches the on-chain recovery address. - // #apiSourcedRecoveryAddress is captured once from the original API response - // and survives across isRecoverySigner upgrades and repeated useSigner calls. - if ( - this.#apiSourcedRecoveryAddress != null && - legacy && - legacy.derivedAddress === this.#apiSourcedRecoveryAddress - ) { - this.#resolvedServerDerivation = legacy; - } else { - this.#resolvedServerDerivation = primary; - } - this.#needsRecovery = false; - return true; - } - const tried = legacy - ? `"server:${primary.derivedAddress}" or "server:${legacy.derivedAddress}"` - : `"server:${primary.derivedAddress}"`; - throw new Error(`Signer ${tried} is not registered in this wallet.`); + return this.resolveServerSigner(signer); } // Check if this is a registered signer @@ -1033,6 +1007,46 @@ export class Wallet { throw new Error(`Signer "${locator}" is not registered in this wallet.`); } + /** + * Resolve a server signer: try primary ("evm") derivation first, fall back to legacy + * (chain-specific) derivation when checking registration. Sets #resolvedServerDerivation + * to whichever derivation is on-chain. Returns true if the signer is the admin (recovery) signer. + */ + private async resolveServerSigner(signer: ServerSignerConfig): Promise { + const { primary, legacy } = this.deriveServerCandidates(signer); + if (await this.signerIsRegistered(`server:${primary.derivedAddress}`)) { + this.#resolvedServerDerivation = primary; + this.#needsRecovery = false; + return false; + } + if (legacy && (await this.signerIsRegistered(`server:${legacy.derivedAddress}`))) { + this.#resolvedServerDerivation = legacy; + this.#needsRecovery = false; + return false; + } + // Neither found as delegated — check if this is the recovery (admin) signer. + if (this.isRecoverySigner(signer)) { + // Resolve which derivation matches the on-chain recovery address. + // #apiSourcedRecoveryAddress is captured once from the original API response + // and survives across isRecoverySigner upgrades and repeated useSigner calls. + if ( + this.#apiSourcedRecoveryAddress != null && + legacy && + legacy.derivedAddress === this.#apiSourcedRecoveryAddress + ) { + this.#resolvedServerDerivation = legacy; + } else { + this.#resolvedServerDerivation = primary; + } + this.#needsRecovery = false; + return true; + } + const tried = legacy + ? `"server:${primary.derivedAddress}" or "server:${legacy.derivedAddress}"` + : `"server:${primary.derivedAddress}"`; + throw new Error(`Signer ${tried} is not registered in this wallet.`); + } + /** * Try to auto-select a passkey credential from registered signers. * Returns true if a credential was auto-selected, false if no passkey signers exist. @@ -1061,27 +1075,7 @@ export class Wallet { */ private resolveSignerLocator(signer: SignerConfigForChain | ExternalWalletRegistrationConfig): string { if (signer.type === "server") { - // Use cached derivation when it belongs to this signer (handles legacy wallets). - if (this.#resolvedServerDerivation) { - const { primary, legacy } = deriveServerSignerCandidates( - signer, - this.chain, - this.#apiClient.projectId, - this.#apiClient.environment - ); - const cachedAddr = this.#resolvedServerDerivation.derivedAddress; - if (cachedAddr === primary.derivedAddress || (legacy != null && cachedAddr === legacy.derivedAddress)) { - return `server:${cachedAddr}`; - } - return `server:${primary.derivedAddress}`; - } - const { derivedAddress } = deriveServerSignerDetails( - signer, - this.chain, - this.#apiClient.projectId, - this.#apiClient.environment - ); - return `server:${derivedAddress}`; + return this.resolveServerSignerApiLocator(signer); } return getSignerLocator(signer); } @@ -1568,12 +1562,7 @@ export class Wallet { if (isApiSourcedServerSignerConfig(config)) { return [config.address]; } - const { primary, legacy } = deriveServerSignerCandidates( - config, - this.chain, - this.#apiClient.projectId, - this.#apiClient.environment - ); + const { primary, legacy } = this.deriveServerCandidates(config); const addresses = [primary.derivedAddress]; if (legacy) { addresses.push(legacy.derivedAddress); @@ -1771,28 +1760,18 @@ export class Wallet { address: this.address, } as InternalSignerConfig; case "server": { - const { primary, legacy } = deriveServerSignerCandidates( - config, - this.chain, - this.#apiClient.projectId, - this.#apiClient.environment - ); // Use cached resolution when it belongs to this signer (matches what is on-chain) - if (this.#resolvedServerDerivation) { - const cachedAddr = this.#resolvedServerDerivation.derivedAddress; - if ( - cachedAddr === primary.derivedAddress || - (legacy != null && cachedAddr === legacy.derivedAddress) - ) { - return { - type: "server", - derivedKeyBytes: this.#resolvedServerDerivation.derivedKeyBytes, - locator: `server:${cachedAddr}` as ServerSignerLocator, - address: cachedAddr, - } as InternalSignerConfig; - } + const cached = this.getOwnedCachedDerivation(config); + if (cached) { + return { + type: "server", + derivedKeyBytes: cached.derivedKeyBytes, + locator: `server:${cached.derivedAddress}` as ServerSignerLocator, + address: cached.derivedAddress, + } as InternalSignerConfig; } // Fallback: prefer legacy if it matches the original recovery address + const { primary, legacy } = this.deriveServerCandidates(config); if ( this.#apiSourcedRecoveryAddress != null && legacy != null && From 9d7e1fce02715922c92bb33c01fa9603f3502b6b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 09:44:15 +0000 Subject: [PATCH 13/24] fix(wallets): fix type error in resolveServerSigner parameter type Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index b544df91d..178dc28f2 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -1012,7 +1012,7 @@ export class Wallet { * (chain-specific) derivation when checking registration. Sets #resolvedServerDerivation * to whichever derivation is on-chain. Returns true if the signer is the admin (recovery) signer. */ - private async resolveServerSigner(signer: ServerSignerConfig): Promise { + private async resolveServerSigner(signer: ServerSignerConfig & SignerConfigForChain): Promise { const { primary, legacy } = this.deriveServerCandidates(signer); if (await this.signerIsRegistered(`server:${primary.derivedAddress}`)) { this.#resolvedServerDerivation = primary; From 9e83f1ec907907903d5befd2b3d4cdde1bddf0a8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 09:46:20 +0000 Subject: [PATCH 14/24] fix(wallets): use type assertion for isRecoverySigner in resolveServerSigner Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index 178dc28f2..43d9c0e6c 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -1012,7 +1012,7 @@ export class Wallet { * (chain-specific) derivation when checking registration. Sets #resolvedServerDerivation * to whichever derivation is on-chain. Returns true if the signer is the admin (recovery) signer. */ - private async resolveServerSigner(signer: ServerSignerConfig & SignerConfigForChain): Promise { + private async resolveServerSigner(signer: ServerSignerConfig): Promise { const { primary, legacy } = this.deriveServerCandidates(signer); if (await this.signerIsRegistered(`server:${primary.derivedAddress}`)) { this.#resolvedServerDerivation = primary; @@ -1025,7 +1025,7 @@ export class Wallet { return false; } // Neither found as delegated — check if this is the recovery (admin) signer. - if (this.isRecoverySigner(signer)) { + if (this.isRecoverySigner(signer as SignerConfigForChain)) { // Resolve which derivation matches the on-chain recovery address. // #apiSourcedRecoveryAddress is captured once from the original API response // and survives across isRecoverySigner upgrades and repeated useSigner calls. From d2d15d113b6b9389b40aeb64391a1890d42218c1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 10:35:18 +0000 Subject: [PATCH 15/24] refactor(wallets): address PR review - rename helpers, use explicit null checks, clean up candidates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Clean up deriveServerSignerCandidates with early return pattern (comment 57) - Rename deriveServerCandidates → deriveServerSignerCandidates for consistency (comment 60) - Rename #resolvedServerDerivation → #resolvedServerSigner for clarity (comment 61) - Rename getOwnedCachedDerivation → matchResolvedServerSigner (comment 62) - Use == null / != null instead of ! and truthiness for all null checks (comments 61, 63) Co-Authored-By: Guille --- .../server/helpers/derive-server-signer.ts | 7 +- packages/wallets/src/wallets/wallet.ts | 75 +++++++++++-------- 2 files changed, 45 insertions(+), 37 deletions(-) diff --git a/packages/wallets/src/signers/server/helpers/derive-server-signer.ts b/packages/wallets/src/signers/server/helpers/derive-server-signer.ts index 8dfff8c9c..811c1e3b4 100644 --- a/packages/wallets/src/signers/server/helpers/derive-server-signer.ts +++ b/packages/wallets/src/signers/server/helpers/derive-server-signer.ts @@ -51,18 +51,17 @@ export function deriveServerSignerCandidates( const chainType = getChainType(chain); - // Primary: use normalized chain type ("evm" | "solana" | "stellar") const primaryBytes = deriveKeyBytes(signer.secret, projectId, environment, chainType); const primaryAddress = deriveServerSignerAddress(primaryBytes, chain); + const primary = { derivedKeyBytes: primaryBytes, derivedAddress: primaryAddress }; // Legacy: chain-specific derivation (only matters for EVM where chain !== chainType) const chainStr = typeof chain === "string" ? chain : String(chain); - let legacy: { derivedKeyBytes: Uint8Array; derivedAddress: string } | null = null; if (chainType === "evm" && chainStr !== "evm") { const legacyBytes = deriveKeyBytes(signer.secret, projectId, environment, chainStr); const legacyAddress = deriveServerSignerAddress(legacyBytes, chain); - legacy = { derivedKeyBytes: legacyBytes, derivedAddress: legacyAddress }; + return { primary, legacy: { derivedKeyBytes: legacyBytes, derivedAddress: legacyAddress } }; } - return { primary: { derivedKeyBytes: primaryBytes, derivedAddress: primaryAddress }, legacy }; + return { primary, legacy: null }; } diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index 43d9c0e6c..8a3d01dc0 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -68,7 +68,10 @@ import type { import { type ApiSourcedServerSignerConfig, isApiSourcedServerSignerConfig, AuthRejectedError } from "../signers/types"; import { assembleSigner } from "../signers"; import { NonCustodialSigner } from "../signers/non-custodial"; -import { deriveServerSignerDetails, deriveServerSignerCandidates } from "../signers/server"; +import { + deriveServerSignerDetails, + deriveServerSignerCandidates as deriveServerSignerCandidatesHelper, +} from "../signers/server"; import { walletsLogger } from "../logger"; import { getSignerLocator } from "../utils/signer-locator"; @@ -99,7 +102,7 @@ export class Wallet { #initialSigners: SignerConfigForChain[]; #needsRecovery = false; #deviceSignerApproved = false; - #resolvedServerDerivation: { derivedKeyBytes: Uint8Array; derivedAddress: string } | null = null; + #resolvedServerSigner: { derivedKeyBytes: Uint8Array; derivedAddress: string } | null = null; #apiSourcedRecoveryAddress: string | null = null; #signerInitialization: Promise; #recovering: Promise | null = null; @@ -267,24 +270,29 @@ export class Wallet { /** * Derive both primary ("evm") and legacy (chain-specific) server signer candidates. */ - private deriveServerCandidates(signer: ServerSignerConfig) { - return deriveServerSignerCandidates(signer, this.chain, this.#apiClient.projectId, this.#apiClient.environment); + private deriveServerSignerCandidates(signer: ServerSignerConfig) { + return deriveServerSignerCandidatesHelper( + signer, + this.chain, + this.#apiClient.projectId, + this.#apiClient.environment + ); } /** - * Return the cached derivation if it belongs to the given signer (i.e. its address + * Return the resolved server signer if it matches the given config (i.e. its address * matches either the primary or legacy candidate). Returns null otherwise. */ - private getOwnedCachedDerivation( + private matchResolvedServerSigner( signer: ServerSignerConfig ): { derivedKeyBytes: Uint8Array; derivedAddress: string } | null { - if (!this.#resolvedServerDerivation) { + if (this.#resolvedServerSigner == null) { return null; } - const { primary, legacy } = this.deriveServerCandidates(signer); - const cachedAddr = this.#resolvedServerDerivation.derivedAddress; + const { primary, legacy } = this.deriveServerSignerCandidates(signer); + const cachedAddr = this.#resolvedServerSigner.derivedAddress; if (cachedAddr === primary.derivedAddress || (legacy != null && cachedAddr === legacy.derivedAddress)) { - return this.#resolvedServerDerivation; + return this.#resolvedServerSigner; } return null; } @@ -295,11 +303,11 @@ export class Wallet { * otherwise falls back to the normalized "evm" derivation (correct for new wallets). */ protected resolveServerSignerApiLocator(signer: ServerSignerConfig): string { - const cached = this.getOwnedCachedDerivation(signer); - if (cached) { - return `server:${cached.derivedAddress}`; + const resolved = this.matchResolvedServerSigner(signer); + if (resolved != null) { + return `server:${resolved.derivedAddress}`; } - const { primary } = this.deriveServerCandidates(signer); + const { primary } = this.deriveServerSignerCandidates(signer); return `server:${primary.derivedAddress}`; } @@ -939,7 +947,7 @@ export class Wallet { // derivation must be preserved so that withRecoverySigner (addSigner / removeSigner) // continues to use the correct primary-or-legacy key for a server recovery signer. if (signer.type === "server") { - this.#resolvedServerDerivation = null; + this.#resolvedServerSigner = null; } this.validateSignerInput(signer); @@ -1013,14 +1021,14 @@ export class Wallet { * to whichever derivation is on-chain. Returns true if the signer is the admin (recovery) signer. */ private async resolveServerSigner(signer: ServerSignerConfig): Promise { - const { primary, legacy } = this.deriveServerCandidates(signer); + const { primary, legacy } = this.deriveServerSignerCandidates(signer); if (await this.signerIsRegistered(`server:${primary.derivedAddress}`)) { - this.#resolvedServerDerivation = primary; + this.#resolvedServerSigner = primary; this.#needsRecovery = false; return false; } - if (legacy && (await this.signerIsRegistered(`server:${legacy.derivedAddress}`))) { - this.#resolvedServerDerivation = legacy; + if (legacy != null && (await this.signerIsRegistered(`server:${legacy.derivedAddress}`))) { + this.#resolvedServerSigner = legacy; this.#needsRecovery = false; return false; } @@ -1031,19 +1039,20 @@ export class Wallet { // and survives across isRecoverySigner upgrades and repeated useSigner calls. if ( this.#apiSourcedRecoveryAddress != null && - legacy && + legacy != null && legacy.derivedAddress === this.#apiSourcedRecoveryAddress ) { - this.#resolvedServerDerivation = legacy; + this.#resolvedServerSigner = legacy; } else { - this.#resolvedServerDerivation = primary; + this.#resolvedServerSigner = primary; } this.#needsRecovery = false; return true; } - const tried = legacy - ? `"server:${primary.derivedAddress}" or "server:${legacy.derivedAddress}"` - : `"server:${primary.derivedAddress}"`; + const tried = + legacy != null + ? `"server:${primary.derivedAddress}" or "server:${legacy.derivedAddress}"` + : `"server:${primary.derivedAddress}"`; throw new Error(`Signer ${tried} is not registered in this wallet.`); } @@ -1562,9 +1571,9 @@ export class Wallet { if (isApiSourcedServerSignerConfig(config)) { return [config.address]; } - const { primary, legacy } = this.deriveServerCandidates(config); + const { primary, legacy } = this.deriveServerSignerCandidates(config); const addresses = [primary.derivedAddress]; - if (legacy) { + if (legacy != null) { addresses.push(legacy.derivedAddress); } return addresses; @@ -1761,17 +1770,17 @@ export class Wallet { } as InternalSignerConfig; case "server": { // Use cached resolution when it belongs to this signer (matches what is on-chain) - const cached = this.getOwnedCachedDerivation(config); - if (cached) { + const resolved = this.matchResolvedServerSigner(config); + if (resolved != null) { return { type: "server", - derivedKeyBytes: cached.derivedKeyBytes, - locator: `server:${cached.derivedAddress}` as ServerSignerLocator, - address: cached.derivedAddress, + derivedKeyBytes: resolved.derivedKeyBytes, + locator: `server:${resolved.derivedAddress}` as ServerSignerLocator, + address: resolved.derivedAddress, } as InternalSignerConfig; } // Fallback: prefer legacy if it matches the original recovery address - const { primary, legacy } = this.deriveServerCandidates(config); + const { primary, legacy } = this.deriveServerSignerCandidates(config); if ( this.#apiSourcedRecoveryAddress != null && legacy != null && From 239ceb86f3588f618aa50ebdfdda3cc859b06846 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 10:41:53 +0000 Subject: [PATCH 16/24] fix(wallets): update stale comment references to #resolvedServerSigner Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index 8a3d01dc0..f20e46e37 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -976,7 +976,7 @@ export class Wallet { // Passkeys are excluded because isRecoverySigner matches by type only, which could // incorrectly match a delegated passkey. // Server signers are excluded so they always flow through the server signer block below, - // which sets #resolvedServerDerivation to the correct (primary or legacy) key. + // which sets #resolvedServerSigner to the correct (primary or legacy) key. if (signer.type !== "passkey" && signer.type !== "server" && this.isRecoverySigner(signer)) { this.#needsRecovery = false; return true; @@ -1017,7 +1017,7 @@ export class Wallet { /** * Resolve a server signer: try primary ("evm") derivation first, fall back to legacy - * (chain-specific) derivation when checking registration. Sets #resolvedServerDerivation + * (chain-specific) derivation when checking registration. Sets #resolvedServerSigner * to whichever derivation is on-chain. Returns true if the signer is the admin (recovery) signer. */ private async resolveServerSigner(signer: ServerSignerConfig): Promise { From 56a3723b51b2fdada7295c66c0d25918479d620d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 11:11:00 +0000 Subject: [PATCH 17/24] refactor(wallets): consolidate server signer derivation resolution into single method - Add resolveServerSignerDerivation() as single sync decision point for primary vs legacy - Simplify resolveServerSignerApiLocator to one-liner delegating to new method - Simplify buildInternalSignerConfig server case from 20 lines to 5 - Make solana/stellar use resolveServerSignerApiLocator instead of direct deriveServerSignerDetails - Make addSigner use resolveServerSignerApiLocator instead of direct deriveServerSignerDetails - Remove unused deriveServerSignerDetails import from wallet.ts Co-Authored-By: Guille --- packages/wallets/src/wallets/solana.ts | 3 +- packages/wallets/src/wallets/stellar.ts | 3 +- packages/wallets/src/wallets/wallet.ts | 68 +++++++++++-------------- 3 files changed, 31 insertions(+), 43 deletions(-) diff --git a/packages/wallets/src/wallets/solana.ts b/packages/wallets/src/wallets/solana.ts index cbeafdedd..a9b5b8b35 100644 --- a/packages/wallets/src/wallets/solana.ts +++ b/packages/wallets/src/wallets/solana.ts @@ -12,7 +12,6 @@ import { Wallet } from "./wallet"; import { TransactionNotCreatedError } from "../utils/errors"; import { SolanaExternalWalletSigner } from "@/signers/solana-external-wallet"; import type { CreateTransactionSuccessResponse } from "@/api"; -import { deriveServerSignerDetails } from "../signers/server"; import { walletsLogger } from "../logger"; export class SolanaWallet extends Wallet { @@ -104,7 +103,7 @@ export class SolanaWallet extends Wallet { } else if (typeof params.options.signer === "string") { signer = params.options.signer; } else { - signer = `server:${deriveServerSignerDetails(params.options.signer, this.chain, this.apiClient.projectId, this.apiClient.environment).derivedAddress}`; + signer = this.resolveServerSignerApiLocator(params.options.signer); } let serializedTransaction: string; diff --git a/packages/wallets/src/wallets/stellar.ts b/packages/wallets/src/wallets/stellar.ts index 6748b7b83..f76baa486 100644 --- a/packages/wallets/src/wallets/stellar.ts +++ b/packages/wallets/src/wallets/stellar.ts @@ -12,7 +12,6 @@ import type { import { Wallet } from "./wallet"; import { TransactionNotCreatedError } from "../utils/errors"; import type { CreateTransactionSuccessResponse } from "@/api"; -import { deriveServerSignerDetails } from "../signers/server"; import { walletsLogger } from "../logger"; import type { ServerSignerConfig } from "../signers/types"; @@ -275,7 +274,7 @@ export class StellarWallet extends Wallet { if (typeof signerOverride === "string") { return signerOverride; } - return `server:${deriveServerSignerDetails(signerOverride, this.chain, this.apiClient.projectId, this.apiClient.environment).derivedAddress}`; + return this.resolveServerSignerApiLocator(signerOverride); } } diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index f20e46e37..bbe66f434 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -68,10 +68,7 @@ import type { import { type ApiSourcedServerSignerConfig, isApiSourcedServerSignerConfig, AuthRejectedError } from "../signers/types"; import { assembleSigner } from "../signers"; import { NonCustodialSigner } from "../signers/non-custodial"; -import { - deriveServerSignerDetails, - deriveServerSignerCandidates as deriveServerSignerCandidatesHelper, -} from "../signers/server"; +import { deriveServerSignerCandidates as deriveServerSignerCandidatesHelper } from "../signers/server"; import { walletsLogger } from "../logger"; import { getSignerLocator } from "../utils/signer-locator"; @@ -298,17 +295,33 @@ export class Wallet { } /** - * Resolve a ServerSignerConfig to an API locator string. - * Uses the cached derivation from useSigner when it belongs to this signer (handles legacy wallets), - * otherwise falls back to the normalized "evm" derivation (correct for new wallets). + * Resolve which derivation (primary "evm" or legacy chain-specific) to use for a server signer. + * Priority: cached resolution → legacy if it matches API recovery address → primary. */ - protected resolveServerSignerApiLocator(signer: ServerSignerConfig): string { + private resolveServerSignerDerivation(signer: ServerSignerConfig): { + derivedKeyBytes: Uint8Array; + derivedAddress: string; + } { const resolved = this.matchResolvedServerSigner(signer); if (resolved != null) { - return `server:${resolved.derivedAddress}`; + return resolved; + } + const { primary, legacy } = this.deriveServerSignerCandidates(signer); + if ( + this.#apiSourcedRecoveryAddress != null && + legacy != null && + legacy.derivedAddress === this.#apiSourcedRecoveryAddress + ) { + return legacy; } - const { primary } = this.deriveServerSignerCandidates(signer); - return `server:${primary.derivedAddress}`; + return primary; + } + + /** + * Resolve a ServerSignerConfig to an API locator string. + */ + protected resolveServerSignerApiLocator(signer: ServerSignerConfig): string { + return `server:${this.resolveServerSignerDerivation(signer).derivedAddress}`; } /** @@ -721,7 +734,7 @@ export class Wallet { // Resolve server signer config to locator string const resolvedSigner = typeof signer === "object" && "type" in signer && signer.type === "server" - ? (`server:${deriveServerSignerDetails(signer, this.chain, this.#apiClient.projectId, this.#apiClient.environment).derivedAddress}` as const) + ? (this.resolveServerSignerApiLocator(signer) as `server:${string}`) : signer; return this.withRecoverySigner(async () => { @@ -1769,35 +1782,12 @@ export class Wallet { address: this.address, } as InternalSignerConfig; case "server": { - // Use cached resolution when it belongs to this signer (matches what is on-chain) - const resolved = this.matchResolvedServerSigner(config); - if (resolved != null) { - return { - type: "server", - derivedKeyBytes: resolved.derivedKeyBytes, - locator: `server:${resolved.derivedAddress}` as ServerSignerLocator, - address: resolved.derivedAddress, - } as InternalSignerConfig; - } - // Fallback: prefer legacy if it matches the original recovery address - const { primary, legacy } = this.deriveServerSignerCandidates(config); - if ( - this.#apiSourcedRecoveryAddress != null && - legacy != null && - legacy.derivedAddress === this.#apiSourcedRecoveryAddress - ) { - return { - type: "server", - derivedKeyBytes: legacy.derivedKeyBytes, - locator: `server:${legacy.derivedAddress}` as ServerSignerLocator, - address: legacy.derivedAddress, - } as InternalSignerConfig; - } + const { derivedKeyBytes, derivedAddress } = this.resolveServerSignerDerivation(config); return { type: "server", - derivedKeyBytes: primary.derivedKeyBytes, - locator: `server:${primary.derivedAddress}` as ServerSignerLocator, - address: primary.derivedAddress, + derivedKeyBytes, + locator: `server:${derivedAddress}` as ServerSignerLocator, + address: derivedAddress, } as InternalSignerConfig; } default: From 2aefc11e7548034c2b1f85b67686bc3947e6113a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 11:25:47 +0000 Subject: [PATCH 18/24] fix(wallets): handle legacy delegated server signers in resolveServerSignerDerivation Check #initialSigners (from API response) for matching legacy derivation address, covering delegated server signers in addition to recovery signers. Fixes removeSigner and send(options.signer) for legacy wallets when useSigner was not called with that signer. Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index bbe66f434..bf63ae525 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -296,7 +296,7 @@ export class Wallet { /** * Resolve which derivation (primary "evm" or legacy chain-specific) to use for a server signer. - * Priority: cached resolution → legacy if it matches API recovery address → primary. + * Priority: cached resolution → legacy if it matches a known on-chain address → primary. */ private resolveServerSignerDerivation(signer: ServerSignerConfig): { derivedKeyBytes: Uint8Array; @@ -307,12 +307,19 @@ export class Wallet { return resolved; } const { primary, legacy } = this.deriveServerSignerCandidates(signer); - if ( - this.#apiSourcedRecoveryAddress != null && - legacy != null && - legacy.derivedAddress === this.#apiSourcedRecoveryAddress - ) { - return legacy; + if (legacy != null) { + // Use legacy if it matches the recovery address or any known on-chain signer + if (legacy.derivedAddress === this.#apiSourcedRecoveryAddress) { + return legacy; + } + if ( + this.#initialSigners.some( + (s) => + s.type === "server" && isApiSourcedServerSignerConfig(s) && s.address === legacy.derivedAddress + ) + ) { + return legacy; + } } return primary; } From 018bd5fb6fe336b1e693e3307079dc88d464460c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 11:40:18 +0000 Subject: [PATCH 19/24] fix(wallets): preserve API-sourced delegated signer addresses for legacy derivation When the factory replaces API-sourced delegated signers with user-provided configs, the on-chain addresses were lost. This caused resolveServerSignerDerivation to default to primary for legacy delegated server signers (e.g. removeSigner, auto-assembly). Adds apiDelegatedSignerAddresses to preserve these addresses, mirroring the existing apiRecoveryAddress pattern for recovery signers. Co-Authored-By: Guille --- packages/wallets/src/wallets/evm.ts | 1 + packages/wallets/src/wallets/solana.ts | 1 + packages/wallets/src/wallets/stellar.ts | 1 + .../wallets/src/wallets/wallet-factory.ts | 10 +++++++- packages/wallets/src/wallets/wallet.ts | 23 +++++++++++++++++-- 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/wallets/src/wallets/evm.ts b/packages/wallets/src/wallets/evm.ts index 59e13d3c6..e41e12d89 100644 --- a/packages/wallets/src/wallets/evm.ts +++ b/packages/wallets/src/wallets/evm.ts @@ -29,6 +29,7 @@ export class EVMWallet extends Wallet { alias: wallet.alias, recovery: Wallet.getRecovery(wallet), apiRecoveryAddress: Wallet.getApiRecoveryAddress(wallet), + apiDelegatedSignerAddresses: Wallet.getApiDelegatedSignerAddresses(wallet), signer: wallet.signer, signers: Wallet.getInitialSigners(wallet), }, diff --git a/packages/wallets/src/wallets/solana.ts b/packages/wallets/src/wallets/solana.ts index a9b5b8b35..49f7e4640 100644 --- a/packages/wallets/src/wallets/solana.ts +++ b/packages/wallets/src/wallets/solana.ts @@ -25,6 +25,7 @@ export class SolanaWallet extends Wallet { alias: wallet.alias, recovery: Wallet.getRecovery(wallet), apiRecoveryAddress: Wallet.getApiRecoveryAddress(wallet), + apiDelegatedSignerAddresses: Wallet.getApiDelegatedSignerAddresses(wallet), signer: wallet.signer, signers: Wallet.getInitialSigners(wallet), }, diff --git a/packages/wallets/src/wallets/stellar.ts b/packages/wallets/src/wallets/stellar.ts index f76baa486..be53f988a 100644 --- a/packages/wallets/src/wallets/stellar.ts +++ b/packages/wallets/src/wallets/stellar.ts @@ -28,6 +28,7 @@ export class StellarWallet extends Wallet { alias: wallet.alias, recovery: Wallet.getRecovery(wallet), apiRecoveryAddress: Wallet.getApiRecoveryAddress(wallet), + apiDelegatedSignerAddresses: Wallet.getApiDelegatedSignerAddresses(wallet), signer: wallet.signer, signers: Wallet.getInitialSigners(wallet), }, diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index ec2acfb4c..054e1858f 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -195,7 +195,8 @@ export class WalletFactory { ? createArgs.recovery : apiRecovery; - let signers = (walletResponse.config as SmartWalletConfig).delegatedSigners; + const apiDelegatedSigners = (walletResponse.config as SmartWalletConfig).delegatedSigners; + let signers = apiDelegatedSigners; if ( signers != null && signers.length === 1 && @@ -211,6 +212,12 @@ export class WalletFactory { ? (apiRecovery as { address: string }).address : undefined; + // Preserve the API-sourced delegated signer addresses so the wallet can identify + // legacy derivations for delegated server signers (same pattern as apiRecoveryAddress). + const apiDelegatedSignerAddresses = (apiDelegatedSigners ?? []) + .filter((s) => s.type === "server" && "address" in s && !("secret" in s)) + .map((s) => (s as { address: string }).address); + const wallet = new Wallet( { chain: args.chain, @@ -220,6 +227,7 @@ export class WalletFactory { alias: args.alias, recovery, apiRecoveryAddress, + apiDelegatedSignerAddresses, signers: (signers ?? []) as SignerConfigForChain[], }, this.apiClient diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index bf63ae525..802809dd4 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -83,6 +83,7 @@ type WalletContructorType = { options?: WalletOptions; recovery: RecoverySignerConfigForChain; apiRecoveryAddress?: string; + apiDelegatedSignerAddresses?: string[]; signers?: SignerConfigForChain[]; signer?: SignerAdapter; }; @@ -101,11 +102,23 @@ export class Wallet { #deviceSignerApproved = false; #resolvedServerSigner: { derivedKeyBytes: Uint8Array; derivedAddress: string } | null = null; #apiSourcedRecoveryAddress: string | null = null; + #apiSourcedDelegatedAddresses: string[] = []; #signerInitialization: Promise; #recovering: Promise | null = null; constructor(args: WalletContructorType, apiClient: ApiClient) { - const { chain, address, owner, options, alias, recovery, apiRecoveryAddress, signers, signer } = args; + const { + chain, + address, + owner, + options, + alias, + recovery, + apiRecoveryAddress, + apiDelegatedSignerAddresses, + signers, + signer, + } = args; this.#apiClient = apiClient; this.chain = chain; this.address = address; @@ -118,6 +131,7 @@ export class Wallet { } else if (recovery.type === "server" && isApiSourcedServerSignerConfig(recovery)) { this.#apiSourcedRecoveryAddress = recovery.address; } + this.#apiSourcedDelegatedAddresses = apiDelegatedSignerAddresses ?? []; this.#initialSigners = signers ?? []; this.#signer = signer; // Can be set by useSigner this.#signerInitialization = this.initDefaultSigner(); @@ -256,6 +270,10 @@ export class Wallet { return wallet.#apiSourcedRecoveryAddress ?? undefined; } + protected static getApiDelegatedSignerAddresses(wallet: Wallet): string[] { + return wallet.#apiSourcedDelegatedAddresses; + } + public get apiClient(): ApiClient { return this.#apiClient; } @@ -316,7 +334,8 @@ export class Wallet { this.#initialSigners.some( (s) => s.type === "server" && isApiSourcedServerSignerConfig(s) && s.address === legacy.derivedAddress - ) + ) || + this.#apiSourcedDelegatedAddresses.includes(legacy.derivedAddress) ) { return legacy; } From 7941682b8b92af1e14a36fca395c5fa9c37e6e0d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 16:01:07 +0000 Subject: [PATCH 20/24] refactor(wallets): extract DerivedServerSigner type, generalize API address preservation - Extract DerivedServerSigner type for { derivedKeyBytes, derivedAddress } and reuse it - Remove import alias by renaming private wrapper to deriveSignerCandidates - Make apiRecoveryAddress / apiDelegatedSignerAddresses apply to any signer type Co-Authored-By: Guille --- .../server/helpers/derive-server-signer.ts | 11 +++++-- .../src/signers/server/helpers/index.ts | 1 + packages/wallets/src/signers/server/index.ts | 7 ++++- .../wallets/src/wallets/wallet-factory.ts | 6 ++-- packages/wallets/src/wallets/wallet.ts | 30 +++++++------------ 5 files changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/wallets/src/signers/server/helpers/derive-server-signer.ts b/packages/wallets/src/signers/server/helpers/derive-server-signer.ts index 811c1e3b4..d1d5a7982 100644 --- a/packages/wallets/src/signers/server/helpers/derive-server-signer.ts +++ b/packages/wallets/src/signers/server/helpers/derive-server-signer.ts @@ -8,6 +8,11 @@ import { deriveKeyBytes } from "../../../utils/server-key-derivation"; import { ed25519KeypairFromSeed, encodeStellarPublicKey } from "../../../utils/stellar"; import { getChainType } from "./get-chain-type"; +export type DerivedServerSigner = { + derivedKeyBytes: Uint8Array; + derivedAddress: string; +}; + export function deriveServerSignerAddress(keyBytes: Uint8Array, chain: Chain): string { const chainType = getChainType(chain); switch (chainType) { @@ -25,7 +30,7 @@ export function deriveServerSignerDetails( chain: Chain, projectId: string, environment: string -): { derivedKeyBytes: Uint8Array; derivedAddress: string } { +): DerivedServerSigner { if (typeof window !== "undefined") { throw new Error("Server signers can only be used from server-side code."); } @@ -42,8 +47,8 @@ export function deriveServerSignerCandidates( projectId: string, environment: string ): { - primary: { derivedKeyBytes: Uint8Array; derivedAddress: string }; - legacy: { derivedKeyBytes: Uint8Array; derivedAddress: string } | null; + primary: DerivedServerSigner; + legacy: DerivedServerSigner | null; } { if (typeof window !== "undefined") { throw new Error("Server signers can only be used from server-side code."); diff --git a/packages/wallets/src/signers/server/helpers/index.ts b/packages/wallets/src/signers/server/helpers/index.ts index 63894107d..b762c2c2c 100644 --- a/packages/wallets/src/signers/server/helpers/index.ts +++ b/packages/wallets/src/signers/server/helpers/index.ts @@ -1,4 +1,5 @@ export { + type DerivedServerSigner, deriveServerSignerAddress, deriveServerSignerDetails, deriveServerSignerCandidates, diff --git a/packages/wallets/src/signers/server/index.ts b/packages/wallets/src/signers/server/index.ts index 348300871..ebac22ddc 100644 --- a/packages/wallets/src/signers/server/index.ts +++ b/packages/wallets/src/signers/server/index.ts @@ -2,4 +2,9 @@ export { assembleServerSigner } from "./assemble-server-signer"; export { EVMServerSigner } from "./evm-server-signer"; export { SolanaServerSigner } from "./solana-server-signer"; export { StellarServerSigner } from "./stellar-server-signer"; -export { deriveServerSignerAddress, deriveServerSignerDetails, deriveServerSignerCandidates } from "./helpers"; +export { + type DerivedServerSigner, + deriveServerSignerAddress, + deriveServerSignerDetails, + deriveServerSignerCandidates, +} from "./helpers"; diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 054e1858f..e99bdfe6e 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -208,14 +208,14 @@ export class WalletFactory { // Preserve the API-sourced recovery address so the wallet can identify // legacy derivations even when the user-provided config replaces the API one. const apiRecoveryAddress = - apiRecovery.type === "server" && "address" in apiRecovery && !("secret" in apiRecovery) + "address" in apiRecovery && !("secret" in apiRecovery) ? (apiRecovery as { address: string }).address : undefined; // Preserve the API-sourced delegated signer addresses so the wallet can identify - // legacy derivations for delegated server signers (same pattern as apiRecoveryAddress). + // legacy derivations even when the user-provided config replaces the API one. const apiDelegatedSignerAddresses = (apiDelegatedSigners ?? []) - .filter((s) => s.type === "server" && "address" in s && !("secret" in s)) + .filter((s) => "address" in s && !("secret" in s)) .map((s) => (s as { address: string }).address); const wallet = new Wallet( diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index 802809dd4..9a0dedc59 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -68,7 +68,7 @@ import type { import { type ApiSourcedServerSignerConfig, isApiSourcedServerSignerConfig, AuthRejectedError } from "../signers/types"; import { assembleSigner } from "../signers"; import { NonCustodialSigner } from "../signers/non-custodial"; -import { deriveServerSignerCandidates as deriveServerSignerCandidatesHelper } from "../signers/server"; +import { type DerivedServerSigner, deriveServerSignerCandidates } from "../signers/server"; import { walletsLogger } from "../logger"; import { getSignerLocator } from "../utils/signer-locator"; @@ -100,7 +100,7 @@ export class Wallet { #initialSigners: SignerConfigForChain[]; #needsRecovery = false; #deviceSignerApproved = false; - #resolvedServerSigner: { derivedKeyBytes: Uint8Array; derivedAddress: string } | null = null; + #resolvedServerSigner: DerivedServerSigner | null = null; #apiSourcedRecoveryAddress: string | null = null; #apiSourcedDelegatedAddresses: string[] = []; #signerInitialization: Promise; @@ -285,26 +285,19 @@ export class Wallet { /** * Derive both primary ("evm") and legacy (chain-specific) server signer candidates. */ - private deriveServerSignerCandidates(signer: ServerSignerConfig) { - return deriveServerSignerCandidatesHelper( - signer, - this.chain, - this.#apiClient.projectId, - this.#apiClient.environment - ); + private deriveSignerCandidates(signer: ServerSignerConfig) { + return deriveServerSignerCandidates(signer, this.chain, this.#apiClient.projectId, this.#apiClient.environment); } /** * Return the resolved server signer if it matches the given config (i.e. its address * matches either the primary or legacy candidate). Returns null otherwise. */ - private matchResolvedServerSigner( - signer: ServerSignerConfig - ): { derivedKeyBytes: Uint8Array; derivedAddress: string } | null { + private matchResolvedServerSigner(signer: ServerSignerConfig): DerivedServerSigner | null { if (this.#resolvedServerSigner == null) { return null; } - const { primary, legacy } = this.deriveServerSignerCandidates(signer); + const { primary, legacy } = this.deriveSignerCandidates(signer); const cachedAddr = this.#resolvedServerSigner.derivedAddress; if (cachedAddr === primary.derivedAddress || (legacy != null && cachedAddr === legacy.derivedAddress)) { return this.#resolvedServerSigner; @@ -316,15 +309,12 @@ export class Wallet { * Resolve which derivation (primary "evm" or legacy chain-specific) to use for a server signer. * Priority: cached resolution → legacy if it matches a known on-chain address → primary. */ - private resolveServerSignerDerivation(signer: ServerSignerConfig): { - derivedKeyBytes: Uint8Array; - derivedAddress: string; - } { + private resolveServerSignerDerivation(signer: ServerSignerConfig): DerivedServerSigner { const resolved = this.matchResolvedServerSigner(signer); if (resolved != null) { return resolved; } - const { primary, legacy } = this.deriveServerSignerCandidates(signer); + const { primary, legacy } = this.deriveSignerCandidates(signer); if (legacy != null) { // Use legacy if it matches the recovery address or any known on-chain signer if (legacy.derivedAddress === this.#apiSourcedRecoveryAddress) { @@ -1060,7 +1050,7 @@ export class Wallet { * to whichever derivation is on-chain. Returns true if the signer is the admin (recovery) signer. */ private async resolveServerSigner(signer: ServerSignerConfig): Promise { - const { primary, legacy } = this.deriveServerSignerCandidates(signer); + const { primary, legacy } = this.deriveSignerCandidates(signer); if (await this.signerIsRegistered(`server:${primary.derivedAddress}`)) { this.#resolvedServerSigner = primary; this.#needsRecovery = false; @@ -1610,7 +1600,7 @@ export class Wallet { if (isApiSourcedServerSignerConfig(config)) { return [config.address]; } - const { primary, legacy } = this.deriveServerSignerCandidates(config); + const { primary, legacy } = this.deriveSignerCandidates(config); const addresses = [primary.derivedAddress]; if (legacy != null) { addresses.push(legacy.derivedAddress); From c49a201df93590218ce0fba08f43b3f526cd5bfd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 16:14:44 +0000 Subject: [PATCH 21/24] refactor(wallets): rename API address fields to be explicitly server-signer-scoped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename apiRecoveryAddress → apiServerRecoveryAddress - Rename apiDelegatedSignerAddresses → apiServerDelegatedAddresses - Rename private fields and static getters accordingly - Restore type === 'server' filter (these fields are only used by server signer derivation) Co-Authored-By: Guille --- packages/wallets/src/wallets/evm.ts | 4 +- packages/wallets/src/wallets/solana.ts | 4 +- packages/wallets/src/wallets/stellar.ts | 4 +- .../wallets/src/wallets/wallet-factory.ts | 16 ++++---- packages/wallets/src/wallets/wallet.ts | 38 +++++++++---------- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/wallets/src/wallets/evm.ts b/packages/wallets/src/wallets/evm.ts index e41e12d89..9b51af367 100644 --- a/packages/wallets/src/wallets/evm.ts +++ b/packages/wallets/src/wallets/evm.ts @@ -28,8 +28,8 @@ export class EVMWallet extends Wallet { options: Wallet.getOptions(wallet), alias: wallet.alias, recovery: Wallet.getRecovery(wallet), - apiRecoveryAddress: Wallet.getApiRecoveryAddress(wallet), - apiDelegatedSignerAddresses: Wallet.getApiDelegatedSignerAddresses(wallet), + apiServerRecoveryAddress: Wallet.getApiServerRecoveryAddress(wallet), + apiServerDelegatedAddresses: Wallet.getApiServerDelegatedAddresses(wallet), signer: wallet.signer, signers: Wallet.getInitialSigners(wallet), }, diff --git a/packages/wallets/src/wallets/solana.ts b/packages/wallets/src/wallets/solana.ts index 49f7e4640..f0d2c2c3e 100644 --- a/packages/wallets/src/wallets/solana.ts +++ b/packages/wallets/src/wallets/solana.ts @@ -24,8 +24,8 @@ export class SolanaWallet extends Wallet { options: Wallet.getOptions(wallet), alias: wallet.alias, recovery: Wallet.getRecovery(wallet), - apiRecoveryAddress: Wallet.getApiRecoveryAddress(wallet), - apiDelegatedSignerAddresses: Wallet.getApiDelegatedSignerAddresses(wallet), + apiServerRecoveryAddress: Wallet.getApiServerRecoveryAddress(wallet), + apiServerDelegatedAddresses: Wallet.getApiServerDelegatedAddresses(wallet), signer: wallet.signer, signers: Wallet.getInitialSigners(wallet), }, diff --git a/packages/wallets/src/wallets/stellar.ts b/packages/wallets/src/wallets/stellar.ts index be53f988a..bc9f3a8d0 100644 --- a/packages/wallets/src/wallets/stellar.ts +++ b/packages/wallets/src/wallets/stellar.ts @@ -27,8 +27,8 @@ export class StellarWallet extends Wallet { options: Wallet.getOptions(wallet), alias: wallet.alias, recovery: Wallet.getRecovery(wallet), - apiRecoveryAddress: Wallet.getApiRecoveryAddress(wallet), - apiDelegatedSignerAddresses: Wallet.getApiDelegatedSignerAddresses(wallet), + apiServerRecoveryAddress: Wallet.getApiServerRecoveryAddress(wallet), + apiServerDelegatedAddresses: Wallet.getApiServerDelegatedAddresses(wallet), signer: wallet.signer, signers: Wallet.getInitialSigners(wallet), }, diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index e99bdfe6e..4848e04fd 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -205,17 +205,17 @@ export class WalletFactory { signers = createArgs.signers as SignerResponse[]; } - // Preserve the API-sourced recovery address so the wallet can identify + // Preserve the API-sourced server signer recovery address so the wallet can identify // legacy derivations even when the user-provided config replaces the API one. - const apiRecoveryAddress = - "address" in apiRecovery && !("secret" in apiRecovery) + const apiServerRecoveryAddress = + apiRecovery.type === "server" && "address" in apiRecovery && !("secret" in apiRecovery) ? (apiRecovery as { address: string }).address : undefined; - // Preserve the API-sourced delegated signer addresses so the wallet can identify + // Preserve the API-sourced server signer delegated addresses so the wallet can identify // legacy derivations even when the user-provided config replaces the API one. - const apiDelegatedSignerAddresses = (apiDelegatedSigners ?? []) - .filter((s) => "address" in s && !("secret" in s)) + const apiServerDelegatedAddresses = (apiDelegatedSigners ?? []) + .filter((s) => s.type === "server" && "address" in s && !("secret" in s)) .map((s) => (s as { address: string }).address); const wallet = new Wallet( @@ -226,8 +226,8 @@ export class WalletFactory { options: args.options, alias: args.alias, recovery, - apiRecoveryAddress, - apiDelegatedSignerAddresses, + apiServerRecoveryAddress, + apiServerDelegatedAddresses, signers: (signers ?? []) as SignerConfigForChain[], }, this.apiClient diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index 9a0dedc59..c90ce4482 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -82,8 +82,8 @@ type WalletContructorType = { alias?: string; options?: WalletOptions; recovery: RecoverySignerConfigForChain; - apiRecoveryAddress?: string; - apiDelegatedSignerAddresses?: string[]; + apiServerRecoveryAddress?: string; + apiServerDelegatedAddresses?: string[]; signers?: SignerConfigForChain[]; signer?: SignerAdapter; }; @@ -101,8 +101,8 @@ export class Wallet { #needsRecovery = false; #deviceSignerApproved = false; #resolvedServerSigner: DerivedServerSigner | null = null; - #apiSourcedRecoveryAddress: string | null = null; - #apiSourcedDelegatedAddresses: string[] = []; + #apiServerRecoveryAddress: string | null = null; + #apiServerDelegatedAddresses: string[] = []; #signerInitialization: Promise; #recovering: Promise | null = null; @@ -114,8 +114,8 @@ export class Wallet { options, alias, recovery, - apiRecoveryAddress, - apiDelegatedSignerAddresses, + apiServerRecoveryAddress, + apiServerDelegatedAddresses, signers, signer, } = args; @@ -126,12 +126,12 @@ export class Wallet { this.#options = options; this.alias = alias; this.#recovery = recovery; - if (apiRecoveryAddress != null) { - this.#apiSourcedRecoveryAddress = apiRecoveryAddress; + if (apiServerRecoveryAddress != null) { + this.#apiServerRecoveryAddress = apiServerRecoveryAddress; } else if (recovery.type === "server" && isApiSourcedServerSignerConfig(recovery)) { - this.#apiSourcedRecoveryAddress = recovery.address; + this.#apiServerRecoveryAddress = recovery.address; } - this.#apiSourcedDelegatedAddresses = apiDelegatedSignerAddresses ?? []; + this.#apiServerDelegatedAddresses = apiServerDelegatedAddresses ?? []; this.#initialSigners = signers ?? []; this.#signer = signer; // Can be set by useSigner this.#signerInitialization = this.initDefaultSigner(); @@ -266,12 +266,12 @@ export class Wallet { return wallet.#initialSigners; } - protected static getApiRecoveryAddress(wallet: Wallet): string | undefined { - return wallet.#apiSourcedRecoveryAddress ?? undefined; + protected static getApiServerRecoveryAddress(wallet: Wallet): string | undefined { + return wallet.#apiServerRecoveryAddress ?? undefined; } - protected static getApiDelegatedSignerAddresses(wallet: Wallet): string[] { - return wallet.#apiSourcedDelegatedAddresses; + protected static getApiServerDelegatedAddresses(wallet: Wallet): string[] { + return wallet.#apiServerDelegatedAddresses; } public get apiClient(): ApiClient { @@ -317,7 +317,7 @@ export class Wallet { const { primary, legacy } = this.deriveSignerCandidates(signer); if (legacy != null) { // Use legacy if it matches the recovery address or any known on-chain signer - if (legacy.derivedAddress === this.#apiSourcedRecoveryAddress) { + if (legacy.derivedAddress === this.#apiServerRecoveryAddress) { return legacy; } if ( @@ -325,7 +325,7 @@ export class Wallet { (s) => s.type === "server" && isApiSourcedServerSignerConfig(s) && s.address === legacy.derivedAddress ) || - this.#apiSourcedDelegatedAddresses.includes(legacy.derivedAddress) + this.#apiServerDelegatedAddresses.includes(legacy.derivedAddress) ) { return legacy; } @@ -1064,12 +1064,12 @@ export class Wallet { // Neither found as delegated — check if this is the recovery (admin) signer. if (this.isRecoverySigner(signer as SignerConfigForChain)) { // Resolve which derivation matches the on-chain recovery address. - // #apiSourcedRecoveryAddress is captured once from the original API response + // #apiServerRecoveryAddress is captured once from the original API response // and survives across isRecoverySigner upgrades and repeated useSigner calls. if ( - this.#apiSourcedRecoveryAddress != null && + this.#apiServerRecoveryAddress != null && legacy != null && - legacy.derivedAddress === this.#apiSourcedRecoveryAddress + legacy.derivedAddress === this.#apiServerRecoveryAddress ) { this.#resolvedServerSigner = legacy; } else { From a778035fbba43bae5ed4c3d23ee4ad26407da01a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 16:21:49 +0000 Subject: [PATCH 22/24] perf(wallets): fetch signer list once in resolveServerSigner Avoid double API call for legacy wallets by fetching the signer list once and checking both primary and legacy addresses against it locally. Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index c90ce4482..689af9f1b 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -1051,12 +1051,13 @@ export class Wallet { */ private async resolveServerSigner(signer: ServerSignerConfig): Promise { const { primary, legacy } = this.deriveSignerCandidates(signer); - if (await this.signerIsRegistered(`server:${primary.derivedAddress}`)) { + const existingSigners = await this.signers(); + if (existingSigners.some((s) => s.locator === `server:${primary.derivedAddress}`)) { this.#resolvedServerSigner = primary; this.#needsRecovery = false; return false; } - if (legacy != null && (await this.signerIsRegistered(`server:${legacy.derivedAddress}`))) { + if (legacy != null && existingSigners.some((s) => s.locator === `server:${legacy.derivedAddress}`)) { this.#resolvedServerSigner = legacy; this.#needsRecovery = false; return false; From 7fa85f04d4d9f5c4f4dac4546fd8d0d09d0c3ef4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 09:21:26 +0000 Subject: [PATCH 23/24] refactor(wallets): restore deriveServerSignerCandidates name with import alias Co-Authored-By: Guille --- packages/wallets/src/wallets/wallet.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index 689af9f1b..f57064d0d 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -68,7 +68,10 @@ import type { import { type ApiSourcedServerSignerConfig, isApiSourcedServerSignerConfig, AuthRejectedError } from "../signers/types"; import { assembleSigner } from "../signers"; import { NonCustodialSigner } from "../signers/non-custodial"; -import { type DerivedServerSigner, deriveServerSignerCandidates } from "../signers/server"; +import { + type DerivedServerSigner, + deriveServerSignerCandidates as deriveServerSignerCandidatesHelper, +} from "../signers/server"; import { walletsLogger } from "../logger"; import { getSignerLocator } from "../utils/signer-locator"; @@ -285,8 +288,13 @@ export class Wallet { /** * Derive both primary ("evm") and legacy (chain-specific) server signer candidates. */ - private deriveSignerCandidates(signer: ServerSignerConfig) { - return deriveServerSignerCandidates(signer, this.chain, this.#apiClient.projectId, this.#apiClient.environment); + private deriveServerSignerCandidates(signer: ServerSignerConfig) { + return deriveServerSignerCandidatesHelper( + signer, + this.chain, + this.#apiClient.projectId, + this.#apiClient.environment + ); } /** @@ -297,7 +305,7 @@ export class Wallet { if (this.#resolvedServerSigner == null) { return null; } - const { primary, legacy } = this.deriveSignerCandidates(signer); + const { primary, legacy } = this.deriveServerSignerCandidates(signer); const cachedAddr = this.#resolvedServerSigner.derivedAddress; if (cachedAddr === primary.derivedAddress || (legacy != null && cachedAddr === legacy.derivedAddress)) { return this.#resolvedServerSigner; @@ -314,7 +322,7 @@ export class Wallet { if (resolved != null) { return resolved; } - const { primary, legacy } = this.deriveSignerCandidates(signer); + const { primary, legacy } = this.deriveServerSignerCandidates(signer); if (legacy != null) { // Use legacy if it matches the recovery address or any known on-chain signer if (legacy.derivedAddress === this.#apiServerRecoveryAddress) { @@ -1050,7 +1058,7 @@ export class Wallet { * to whichever derivation is on-chain. Returns true if the signer is the admin (recovery) signer. */ private async resolveServerSigner(signer: ServerSignerConfig): Promise { - const { primary, legacy } = this.deriveSignerCandidates(signer); + const { primary, legacy } = this.deriveServerSignerCandidates(signer); const existingSigners = await this.signers(); if (existingSigners.some((s) => s.locator === `server:${primary.derivedAddress}`)) { this.#resolvedServerSigner = primary; @@ -1601,7 +1609,7 @@ export class Wallet { if (isApiSourcedServerSignerConfig(config)) { return [config.address]; } - const { primary, legacy } = this.deriveSignerCandidates(config); + const { primary, legacy } = this.deriveServerSignerCandidates(config); const addresses = [primary.derivedAddress]; if (legacy != null) { addresses.push(legacy.derivedAddress); From 9e150e26ea1c60c764c7cee54e6f49a6e52854f7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:54:08 +0000 Subject: [PATCH 24/24] refactor(wallets): rename apiServer* to apiRecoveryServerSigner*/apiDelegatedServerSigner* for clarity Co-Authored-By: Guille --- packages/wallets/src/wallets/evm.ts | 4 +- packages/wallets/src/wallets/solana.ts | 4 +- packages/wallets/src/wallets/stellar.ts | 4 +- .../wallets/src/wallets/wallet-factory.ts | 8 ++-- packages/wallets/src/wallets/wallet.ts | 38 +++++++++---------- 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/packages/wallets/src/wallets/evm.ts b/packages/wallets/src/wallets/evm.ts index 9b51af367..52a52ea05 100644 --- a/packages/wallets/src/wallets/evm.ts +++ b/packages/wallets/src/wallets/evm.ts @@ -28,8 +28,8 @@ export class EVMWallet extends Wallet { options: Wallet.getOptions(wallet), alias: wallet.alias, recovery: Wallet.getRecovery(wallet), - apiServerRecoveryAddress: Wallet.getApiServerRecoveryAddress(wallet), - apiServerDelegatedAddresses: Wallet.getApiServerDelegatedAddresses(wallet), + apiRecoveryServerSignerAddress: Wallet.getApiRecoveryServerSignerAddress(wallet), + apiDelegatedServerSignerAddresses: Wallet.getApiDelegatedServerSignerAddresses(wallet), signer: wallet.signer, signers: Wallet.getInitialSigners(wallet), }, diff --git a/packages/wallets/src/wallets/solana.ts b/packages/wallets/src/wallets/solana.ts index f0d2c2c3e..acf8cc725 100644 --- a/packages/wallets/src/wallets/solana.ts +++ b/packages/wallets/src/wallets/solana.ts @@ -24,8 +24,8 @@ export class SolanaWallet extends Wallet { options: Wallet.getOptions(wallet), alias: wallet.alias, recovery: Wallet.getRecovery(wallet), - apiServerRecoveryAddress: Wallet.getApiServerRecoveryAddress(wallet), - apiServerDelegatedAddresses: Wallet.getApiServerDelegatedAddresses(wallet), + apiRecoveryServerSignerAddress: Wallet.getApiRecoveryServerSignerAddress(wallet), + apiDelegatedServerSignerAddresses: Wallet.getApiDelegatedServerSignerAddresses(wallet), signer: wallet.signer, signers: Wallet.getInitialSigners(wallet), }, diff --git a/packages/wallets/src/wallets/stellar.ts b/packages/wallets/src/wallets/stellar.ts index bc9f3a8d0..1c6b8d11d 100644 --- a/packages/wallets/src/wallets/stellar.ts +++ b/packages/wallets/src/wallets/stellar.ts @@ -27,8 +27,8 @@ export class StellarWallet extends Wallet { options: Wallet.getOptions(wallet), alias: wallet.alias, recovery: Wallet.getRecovery(wallet), - apiServerRecoveryAddress: Wallet.getApiServerRecoveryAddress(wallet), - apiServerDelegatedAddresses: Wallet.getApiServerDelegatedAddresses(wallet), + apiRecoveryServerSignerAddress: Wallet.getApiRecoveryServerSignerAddress(wallet), + apiDelegatedServerSignerAddresses: Wallet.getApiDelegatedServerSignerAddresses(wallet), signer: wallet.signer, signers: Wallet.getInitialSigners(wallet), }, diff --git a/packages/wallets/src/wallets/wallet-factory.ts b/packages/wallets/src/wallets/wallet-factory.ts index 4848e04fd..be4869f00 100644 --- a/packages/wallets/src/wallets/wallet-factory.ts +++ b/packages/wallets/src/wallets/wallet-factory.ts @@ -207,14 +207,14 @@ export class WalletFactory { // Preserve the API-sourced server signer recovery address so the wallet can identify // legacy derivations even when the user-provided config replaces the API one. - const apiServerRecoveryAddress = + const apiRecoveryServerSignerAddress = apiRecovery.type === "server" && "address" in apiRecovery && !("secret" in apiRecovery) ? (apiRecovery as { address: string }).address : undefined; // Preserve the API-sourced server signer delegated addresses so the wallet can identify // legacy derivations even when the user-provided config replaces the API one. - const apiServerDelegatedAddresses = (apiDelegatedSigners ?? []) + const apiDelegatedServerSignerAddresses = (apiDelegatedSigners ?? []) .filter((s) => s.type === "server" && "address" in s && !("secret" in s)) .map((s) => (s as { address: string }).address); @@ -226,8 +226,8 @@ export class WalletFactory { options: args.options, alias: args.alias, recovery, - apiServerRecoveryAddress, - apiServerDelegatedAddresses, + apiRecoveryServerSignerAddress, + apiDelegatedServerSignerAddresses, signers: (signers ?? []) as SignerConfigForChain[], }, this.apiClient diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index f57064d0d..3071ab25c 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -85,8 +85,8 @@ type WalletContructorType = { alias?: string; options?: WalletOptions; recovery: RecoverySignerConfigForChain; - apiServerRecoveryAddress?: string; - apiServerDelegatedAddresses?: string[]; + apiRecoveryServerSignerAddress?: string; + apiDelegatedServerSignerAddresses?: string[]; signers?: SignerConfigForChain[]; signer?: SignerAdapter; }; @@ -104,8 +104,8 @@ export class Wallet { #needsRecovery = false; #deviceSignerApproved = false; #resolvedServerSigner: DerivedServerSigner | null = null; - #apiServerRecoveryAddress: string | null = null; - #apiServerDelegatedAddresses: string[] = []; + #apiRecoveryServerSignerAddress: string | null = null; + #apiDelegatedServerSignerAddresses: string[] = []; #signerInitialization: Promise; #recovering: Promise | null = null; @@ -117,8 +117,8 @@ export class Wallet { options, alias, recovery, - apiServerRecoveryAddress, - apiServerDelegatedAddresses, + apiRecoveryServerSignerAddress, + apiDelegatedServerSignerAddresses, signers, signer, } = args; @@ -129,12 +129,12 @@ export class Wallet { this.#options = options; this.alias = alias; this.#recovery = recovery; - if (apiServerRecoveryAddress != null) { - this.#apiServerRecoveryAddress = apiServerRecoveryAddress; + if (apiRecoveryServerSignerAddress != null) { + this.#apiRecoveryServerSignerAddress = apiRecoveryServerSignerAddress; } else if (recovery.type === "server" && isApiSourcedServerSignerConfig(recovery)) { - this.#apiServerRecoveryAddress = recovery.address; + this.#apiRecoveryServerSignerAddress = recovery.address; } - this.#apiServerDelegatedAddresses = apiServerDelegatedAddresses ?? []; + this.#apiDelegatedServerSignerAddresses = apiDelegatedServerSignerAddresses ?? []; this.#initialSigners = signers ?? []; this.#signer = signer; // Can be set by useSigner this.#signerInitialization = this.initDefaultSigner(); @@ -269,12 +269,12 @@ export class Wallet { return wallet.#initialSigners; } - protected static getApiServerRecoveryAddress(wallet: Wallet): string | undefined { - return wallet.#apiServerRecoveryAddress ?? undefined; + protected static getApiRecoveryServerSignerAddress(wallet: Wallet): string | undefined { + return wallet.#apiRecoveryServerSignerAddress ?? undefined; } - protected static getApiServerDelegatedAddresses(wallet: Wallet): string[] { - return wallet.#apiServerDelegatedAddresses; + protected static getApiDelegatedServerSignerAddresses(wallet: Wallet): string[] { + return wallet.#apiDelegatedServerSignerAddresses; } public get apiClient(): ApiClient { @@ -325,7 +325,7 @@ export class Wallet { const { primary, legacy } = this.deriveServerSignerCandidates(signer); if (legacy != null) { // Use legacy if it matches the recovery address or any known on-chain signer - if (legacy.derivedAddress === this.#apiServerRecoveryAddress) { + if (legacy.derivedAddress === this.#apiRecoveryServerSignerAddress) { return legacy; } if ( @@ -333,7 +333,7 @@ export class Wallet { (s) => s.type === "server" && isApiSourcedServerSignerConfig(s) && s.address === legacy.derivedAddress ) || - this.#apiServerDelegatedAddresses.includes(legacy.derivedAddress) + this.#apiDelegatedServerSignerAddresses.includes(legacy.derivedAddress) ) { return legacy; } @@ -1073,12 +1073,12 @@ export class Wallet { // Neither found as delegated — check if this is the recovery (admin) signer. if (this.isRecoverySigner(signer as SignerConfigForChain)) { // Resolve which derivation matches the on-chain recovery address. - // #apiServerRecoveryAddress is captured once from the original API response + // #apiRecoveryServerSignerAddress is captured once from the original API response // and survives across isRecoverySigner upgrades and repeated useSigner calls. if ( - this.#apiServerRecoveryAddress != null && + this.#apiRecoveryServerSignerAddress != null && legacy != null && - legacy.derivedAddress === this.#apiServerRecoveryAddress + legacy.derivedAddress === this.#apiRecoveryServerSignerAddress ) { this.#resolvedServerSigner = legacy; } else {