Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 48 additions & 24 deletions tests/credentialSigner.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,55 @@
import {describe, expect, it} from "vitest";
import {afterEach, describe, expect, it, vi} from "vitest";

import {RequestUtils} from "../src/utils/requestUtils";
import type {CredentialSigner} from "../src/credentialSigner";
import {createSignedFetch} from "../src/signedFetch";

describe("RequestUtils", () => {
it("builds wallet auth request vectors", () => {
const projectId = "project-id";
class RecordingSigner implements CredentialSigner {
readonly signingAlgorithm = "ecdsa-p256-sha256";
readonly preimages: string[] = [];

expect(RequestUtils.buildWalletRequestPreimage(
"/CommitVerifier",
"42",
projectId,
"{\"walletId\":\"wallet-id\"}",
)).toBe(
async credentialId(): Promise<string> {
return `0x04${"11".repeat(64)}`;
}

async nextNonce(): Promise<string> {
return "42";
}

async sign(preimage: string): Promise<string> {
this.preimages.push(preimage);
return `0x${"22".repeat(64)}`;
}
}

afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});

describe("wallet request signing", () => {
it("signs wallet RPC requests with the canonical preimage and signature header", async () => {
const body = "{\"walletId\":\"wallet-id\"}";
const signer = new RecordingSigner();
const fetchMock = vi.fn(async () => new Response("{}", {status: 200}));
vi.stubGlobal("fetch", fetchMock);

const signedFetch = createSignedFetch("public-api-key", signer, "project-id");
await signedFetch("https://wallet.example/rpc/Wallet/CommitVerifier", {
method: "POST",
body,
});

expect(signer.preimages).toEqual([
"POST /rpc/Wallet/CommitVerifier\n" +
"nonce: 42\n" +
`scope: ${projectId}\n\n` +
"{\"walletId\":\"wallet-id\"}",
);

expect(RequestUtils.buildWalletSignatureHeader(
"ecdsa-p256-sha256",
projectId,
`0x04${"11".repeat(64)}`,
"42",
`0x${"22".repeat(64)}`,
)).toBe(
`alg="ecdsa-p256-sha256", scope="${projectId}", cred="0x04${"11".repeat(64)}", nonce=42, sig="0x${"22".repeat(64)}"`,
);
"scope: project-id\n\n" +
body,
]);

const headers = fetchMock.mock.calls[0][1]?.headers as Record<string, string>;
expect(headers).toMatchObject({
"X-Access-Key": "public-api-key",
"OMS-Wallet-Signature": `alg="ecdsa-p256-sha256", scope="project-id", cred="0x04${"11".repeat(64)}", nonce=42, sig="0x${"22".repeat(64)}"`,
});
});
});
76 changes: 31 additions & 45 deletions tests/networks.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {describe, expect, it} from "vitest";

import {
Networks,
OMSClient,
findNetworkById,
findNetworkByName,
Expand All @@ -11,54 +10,41 @@ import {
describe("Networks", () => {
it("exposes the supported network registry", () => {
expect(supportedNetworks).toEqual([
Networks.mainnet,
Networks.sepolia,
Networks.polygon,
Networks.amoy,
Networks.arbitrum,
Networks.arbitrumSepolia,
Networks.optimism,
Networks.optimismSepolia,
Networks.base,
Networks.baseSepolia,
Networks.bsc,
Networks.bscTestnet,
Networks.arbitrumNova,
Networks.avalanche,
Networks.avalancheTestnet,
Networks.katana,
]);
expect(Networks.katana).toEqual({
id: 747474,
name: "katana",
nativeTokenSymbol: "ETH",
explorerUrl: "https://katanascan.com",
displayName: "Katana",
});
expect(supportedNetworks.map(network => network.displayName)).toEqual([
"Ethereum",
"Sepolia",
"Polygon",
"Polygon Amoy",
"Arbitrum",
"Arbitrum Sepolia",
"Optimism",
"Optimism Sepolia",
"Base",
"Base Sepolia",
"BSC",
"BSC Testnet",
"Arbitrum Nova",
"Avalanche",
"Avalanche Testnet",
"Katana",
{id: 1, name: "mainnet", nativeTokenSymbol: "ETH", explorerUrl: "https://etherscan.io", displayName: "Ethereum"},
{id: 11155111, name: "sepolia", nativeTokenSymbol: "ETH", explorerUrl: "https://sepolia.etherscan.io", displayName: "Sepolia"},
{id: 137, name: "polygon", nativeTokenSymbol: "POL", explorerUrl: "https://polygonscan.com", displayName: "Polygon"},
{id: 80002, name: "amoy", nativeTokenSymbol: "POL", explorerUrl: "https://amoy.polygonscan.com", displayName: "Polygon Amoy"},
{id: 42161, name: "arbitrum", nativeTokenSymbol: "ETH", explorerUrl: "https://arbiscan.io", displayName: "Arbitrum"},
{id: 421614, name: "arbitrum-sepolia", nativeTokenSymbol: "ETH", explorerUrl: "https://sepolia.arbiscan.io", displayName: "Arbitrum Sepolia"},
{id: 10, name: "optimism", nativeTokenSymbol: "ETH", explorerUrl: "https://optimistic.etherscan.io", displayName: "Optimism"},
{id: 11155420, name: "optimism-sepolia", nativeTokenSymbol: "ETH", explorerUrl: "https://sepolia-optimism.etherscan.io", displayName: "Optimism Sepolia"},
{id: 8453, name: "base", nativeTokenSymbol: "ETH", explorerUrl: "https://basescan.org", displayName: "Base"},
{id: 84532, name: "base-sepolia", nativeTokenSymbol: "ETH", explorerUrl: "https://sepolia.basescan.org", displayName: "Base Sepolia"},
{id: 56, name: "bsc", nativeTokenSymbol: "BNB", explorerUrl: "https://bscscan.com", displayName: "BSC"},
{id: 97, name: "bsc-testnet", nativeTokenSymbol: "BNB", explorerUrl: "https://testnet.bscscan.com", displayName: "BSC Testnet"},
{id: 42170, name: "arbitrum-nova", nativeTokenSymbol: "ETH", explorerUrl: "https://nova.arbiscan.io", displayName: "Arbitrum Nova"},
{id: 43114, name: "avalanche", nativeTokenSymbol: "AVAX", explorerUrl: "https://subnets.avax.network/c-chain", displayName: "Avalanche"},
{id: 43113, name: "avalanche-testnet", nativeTokenSymbol: "AVAX", explorerUrl: "https://subnets-test.avax.network/c-chain", displayName: "Avalanche Testnet"},
{id: 747474, name: "katana", nativeTokenSymbol: "ETH", explorerUrl: "https://katanascan.com", displayName: "Katana"},
]);
});

it("looks up networks by id or name", () => {
expect(findNetworkById(43113)).toBe(Networks.avalancheTestnet);
expect(findNetworkById(421614)).toBe(Networks.arbitrumSepolia);
expect(findNetworkByName("base-sepolia")).toBe(Networks.baseSepolia);
expect(findNetworkById(43113)).toMatchObject({
id: 43113,
name: "avalanche-testnet",
displayName: "Avalanche Testnet",
});
expect(findNetworkById(421614)).toMatchObject({
id: 421614,
name: "arbitrum-sepolia",
displayName: "Arbitrum Sepolia",
});
expect(findNetworkByName(" BASE-SEPOLIA ")).toMatchObject({
id: 84532,
name: "base-sepolia",
displayName: "Base Sepolia",
});
expect(findNetworkByName("Ethereum")).toBeUndefined();
});

Expand Down
29 changes: 11 additions & 18 deletions tests/oidcRedirectAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {afterEach, describe, expect, it, vi} from "vitest";
import {WalletClient} from "../src/clients/walletClient";
import type {CredentialSigner} from "../src/credentialSigner";
import {defineOmsEnvironment, type OidcProviderConfig, type OmsEnvironment} from "../src/omsEnvironment";
import {defaultGoogleClientId, defaultRelayRedirectUri, googleOidcProvider} from "../src/oidc";
import {googleOidcProvider} from "../src/oidc";
import {MemoryStorageManager} from "../src/storageManager";
import {WalletType} from "../src/generated/waas.gen";
import {Constants} from "../src/utils/constants";
Expand All @@ -13,6 +13,9 @@ import {
redirectUriFromCurrentUrl,
} from "../src/utils/oidcRedirect";

const expectedDefaultGoogleClientId = "970987756660-0dh5gubqfiugm452raf7mm39qaq639hn.apps.googleusercontent.com";
const expectedDefaultRelayRedirectUri = "https://waas-cf-relay-staging.0xsequence.workers.dev/callback";

class MockSigner implements CredentialSigner {
readonly signingAlgorithm = "ecdsa-p256-sha256";
readonly preimages: string[] = [];
Expand Down Expand Up @@ -55,7 +58,7 @@ describe("WalletClient OIDC redirect auth", () => {
metadata: {
iss: "https://accounts.google.com",
aud: "google-client",
redirect_uri: defaultRelayRedirectUri,
redirect_uri: expectedDefaultRelayRedirectUri,
},
});

Expand All @@ -78,7 +81,7 @@ describe("WalletClient OIDC redirect auth", () => {
const authorizeUrl = new URL(result.url);
expect(authorizeUrl.origin + authorizeUrl.pathname).toBe("https://accounts.google.com/o/oauth2/v2/auth");
expect(authorizeUrl.searchParams.get("client_id")).toBe("google-client");
expect(authorizeUrl.searchParams.get("redirect_uri")).toBe(defaultRelayRedirectUri);
expect(authorizeUrl.searchParams.get("redirect_uri")).toBe(expectedDefaultRelayRedirectUri);
expect(authorizeUrl.searchParams.get("response_type")).toBe("code");
expect(authorizeUrl.searchParams.get("scope")).toBe("openid email profile");
expect(authorizeUrl.searchParams.get("state")).toBe(result.state);
Expand Down Expand Up @@ -164,17 +167,7 @@ describe("WalletClient OIDC redirect auth", () => {
const state = decodeOidcState(result.state);
expect(state.scope).toBe("proj_custom");
expect(state.redirect_uri).toBe("https://app.example/auth/callback");
expect(signer.preimages).toEqual([
`POST /rpc/Wallet/CommitVerifier\nnonce: 42\nscope: proj_custom\n\n${JSON.stringify({
identityType: "oidc",
authMode: "auth-code-pkce",
metadata: {
iss: "https://accounts.google.com",
aud: "google-client",
redirect_uri: "https://relay.example/callback",
},
})}`,
]);
expect(signer.preimages).toHaveLength(1);
});

it("supports direct provider config objects", async () => {
Expand Down Expand Up @@ -215,8 +208,8 @@ describe("WalletClient OIDC redirect auth", () => {

it("uses Google provider defaults", () => {
expect(googleOidcProvider()).toMatchObject({
clientId: defaultGoogleClientId,
relayRedirectUri: defaultRelayRedirectUri,
clientId: expectedDefaultGoogleClientId,
relayRedirectUri: expectedDefaultRelayRedirectUri,
issuer: "https://accounts.google.com",
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
scopes: ["openid", "email", "profile"],
Expand Down Expand Up @@ -581,7 +574,7 @@ describe("WalletClient OIDC redirect auth", () => {
const body = JSON.parse(init?.body as string);

if (url.endsWith("/CommitVerifier")) {
expect(body.metadata.redirect_uri).toBe(defaultRelayRedirectUri);
expect(body.metadata.redirect_uri).toBe(expectedDefaultRelayRedirectUri);
return jsonResponse({
verifier: "verifier-1",
challenge: "challenge-1",
Expand Down Expand Up @@ -619,7 +612,7 @@ describe("WalletClient OIDC redirect auth", () => {
});

const assignedUrl = new URL(assignUrl.mock.calls[0][0]);
expect(assignedUrl.searchParams.get("redirect_uri")).toBe(defaultRelayRedirectUri);
expect(assignedUrl.searchParams.get("redirect_uri")).toBe(expectedDefaultRelayRedirectUri);
expect(redirectUriFromCurrentUrl("https://app.example/login?from=home#section")).toBe("https://app.example/login");

const replaceUrl = vi.fn();
Expand Down
16 changes: 9 additions & 7 deletions tests/walletErrors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe("WalletClient errors", () => {
});

it("maps consumed auth commitments to a specific SDK error code", async () => {
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url.endsWith("/CompleteAuth")) {
return jsonResponse({
Expand All @@ -81,7 +81,8 @@ describe("WalletClient errors", () => {
}

throw new Error(`Unexpected request: ${url}`);
}));
});
vi.stubGlobal("fetch", fetchMock);

const wallet = new WalletClient({
publicApiKey: "public-api-key",
Expand All @@ -98,7 +99,12 @@ describe("WalletClient errors", () => {
status: 400,
retryable: false,
});
expect(activeEmailAuthAttempt(wallet)).toBeUndefined();
await expect(wallet.completeEmailAuth({code: "123456"})).rejects.toMatchObject({
code: "OMS_SESSION_MISSING",
operation: "wallet.completeEmailAuth",
message: "No pending email auth attempt",
});
expect(fetchMock).toHaveBeenCalledOnce();
});
});

Expand All @@ -109,10 +115,6 @@ function seedEmailAuthAttempt(wallet: WalletClient): void {
};
}

function activeEmailAuthAttempt(wallet: WalletClient): unknown {
return (wallet as any).activeEmailAuthAttempt;
}

function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
Expand Down
33 changes: 23 additions & 10 deletions tests/walletSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,6 @@ function seedEmailAuthAttempt(
(wallet as any).activeEmailAuthAttempt = {verifier, challenge};
}

function activeEmailAuthAttempt(wallet: WalletClient): unknown {
return (wallet as any).activeEmailAuthAttempt;
}

describe("WalletClient session storage", () => {
it("falls back to memory storage when localStorage is unavailable", () => {
vi.stubGlobal("localStorage", undefined);
Expand Down Expand Up @@ -114,7 +110,11 @@ describe("WalletClient session storage", () => {
await wallet.signOut();

expect(wallet.walletAddress).toBeUndefined();
expect(activeEmailAuthAttempt(wallet)).toBeUndefined();
await expect(wallet.completeEmailAuth({code: "123456"})).rejects.toMatchObject({
code: "OMS_SESSION_MISSING",
operation: "wallet.completeEmailAuth",
message: "No pending email auth attempt",
});
expect(wallet.session).toEqual({
walletAddress: undefined,
expiresAt: undefined,
Expand Down Expand Up @@ -323,10 +323,19 @@ describe("WalletClient session storage", () => {

it("does not persist stale automatic email auth after a newer email auth starts", async () => {
const completeAuth = deferred<Response>();
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
const body = init?.body ? JSON.parse(init.body as string) : undefined;

if (url.endsWith("/CompleteAuth")) {
if (body?.verifier === "verifier-2") {
return jsonResponse({
identity: {type: "email", sub: "user-2"},
email: "new@example.com",
wallets: [testWallet("wallet-new", WalletType.Ethereum, "22")],
credential: testCredential(),
});
}
return completeAuth.promise;
}

Expand All @@ -338,6 +347,9 @@ describe("WalletClient session storage", () => {
}

if (url.endsWith("/UseWallet")) {
if (body?.walletId === "wallet-new") {
return jsonResponse({wallet: testWallet("wallet-new", WalletType.Ethereum, "22")});
}
throw new Error("UseWallet should not be called for stale auth");
}

Expand Down Expand Up @@ -371,11 +383,12 @@ describe("WalletClient session storage", () => {
message: "Email auth attempt is no longer active",
});
expect(wallet.walletAddress).toBeUndefined();
expect(activeEmailAuthAttempt(wallet)).toMatchObject({
verifier: "verifier-2",
challenge: "challenge-2",
});
expect(requestCount(fetchMock, "/UseWallet")).toBe(0);
await expect(wallet.completeEmailAuth({code: "222222"})).resolves.toMatchObject({
wallet: {id: "wallet-new"},
});
expect(wallet.walletAddress).toBe("0x2222222222222222222222222222222222222222");
expect(requestCount(fetchMock, "/UseWallet")).toBe(1);
});

it("allows email auth completion retry after a failed completion request", async () => {
Expand Down
Loading