Skip to content

Commit dddbc5e

Browse files
committed
feat: populate verifyContext from core.verify.resolve attestation
1 parent f17a679 commit dddbc5e

4 files changed

Lines changed: 333 additions & 12 deletions

File tree

packages/auth-client/src/controllers/engine.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
isJsonRpcResponse,
99
isJsonRpcResult,
1010
} from "@exodus/walletconnect-jsonrpc-utils";
11-
import { RelayerTypes, Verify } from "@exodus/walletconnect-types";
11+
import { RelayerTypes } from "@exodus/walletconnect-types";
1212
import { getInternalError, hashKey, TYPE_1 } from "@exodus/walletconnect-utils";
1313
import { AUTH_CLIENT_PUBLIC_KEY_NAME, ENGINE_RPC_OPTS } from "../constants";
1414
import { AuthClientTypes, AuthEngineTypes, IAuthEngine, JsonRpcTypes } from "../types";
@@ -17,6 +17,7 @@ import { verifySignature } from "../utils/signature";
1717
import { getPendingRequest, getPendingRequests } from "../utils/store";
1818
import { isValidRequest, isValidRespond } from "../utils/validators";
1919
import { hashMessage } from "../utils/crypto";
20+
import { buildVerifyContext } from "../utils/verifyContext";
2021

2122
export class AuthEngine extends IAuthEngine {
2223
private initialized = false;
@@ -361,7 +362,7 @@ export class AuthEngine extends IAuthEngine {
361362
});
362363

363364
const hash = hashMessage(JSON.stringify(payload));
364-
const verifyContext = await this.getVerifyContext(hash, this.client.metadata);
365+
const verifyContext = await this.getVerifyContext(hash, requester.metadata);
365366

366367
this.client.emit("auth_request", {
367368
id: payload.id,
@@ -426,15 +427,13 @@ export class AuthEngine extends IAuthEngine {
426427
}
427428
};
428429

429-
private getVerifyContext = async (_hash: string, metadata: AuthClientTypes.Metadata) => {
430-
const context: Verify.Context = {
431-
verified: {
432-
verifyUrl: metadata.verifyUrl || "",
433-
validation: "UNKNOWN",
434-
origin: metadata.url || "",
430+
private getVerifyContext = (hash: string, metadata: AuthClientTypes.Metadata) =>
431+
buildVerifyContext(
432+
{
433+
resolve: (args) => this.client.core.verify.resolve(args),
434+
logError: (e) => this.client.logger.error(e),
435435
},
436-
};
437-
438-
return context;
439-
};
436+
hash,
437+
metadata,
438+
);
440439
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Verify } from "@exodus/walletconnect-types";
2+
import { AuthClientTypes } from "../types";
3+
4+
export interface BuildVerifyContextDeps {
5+
resolve: (args: {
6+
attestationId: string;
7+
verifyUrl?: string;
8+
}) => Promise<{ origin?: string; isScam?: boolean } | string | undefined>;
9+
logError?: (err: unknown) => void;
10+
}
11+
12+
export const buildVerifyContext = async (
13+
{ resolve, logError }: BuildVerifyContextDeps,
14+
attestationId: string,
15+
metadata: AuthClientTypes.Metadata,
16+
): Promise<Verify.Context> => {
17+
if (!metadata?.url) {
18+
const context: Verify.Context = {
19+
verified: { verifyUrl: "", validation: "UNKNOWN", origin: "" },
20+
};
21+
try {
22+
const attestation = await resolve({ attestationId });
23+
if (typeof attestation === "object" && attestation?.isScam) {
24+
context.verified.isScam = true;
25+
}
26+
} catch (e) {
27+
logError?.(e);
28+
}
29+
return context;
30+
}
31+
32+
const context: Verify.Context = {
33+
verified: {
34+
verifyUrl: metadata.verifyUrl || "",
35+
validation: "UNKNOWN",
36+
origin: metadata.url || "",
37+
},
38+
};
39+
40+
try {
41+
const attestation = await resolve({
42+
attestationId,
43+
verifyUrl: metadata.verifyUrl,
44+
});
45+
if (attestation) {
46+
const origin = typeof attestation === "string" ? attestation : (attestation as any).origin;
47+
if (origin && typeof origin === "string") {
48+
context.verified.origin = origin;
49+
let claimedOrigin = metadata.url || "";
50+
try {
51+
claimedOrigin = new URL(metadata.url).origin;
52+
} catch {}
53+
context.verified.validation = origin === claimedOrigin ? "VALID" : "INVALID";
54+
}
55+
if (typeof attestation === "object" && (attestation as any).isScam) {
56+
context.verified.isScam = true;
57+
}
58+
}
59+
} catch (e) {
60+
logError?.(e);
61+
}
62+
63+
return context;
64+
};
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { expect, describe, it, vi, beforeEach } from "vitest";
2+
import { AuthEngine } from "../src/controllers/engine";
3+
import { AuthClientTypes } from "../src/types";
4+
5+
const WALLET_URL = "https://mywallet.app";
6+
const DAPP_URL = "https://app.uniswap.org";
7+
8+
const walletMetadata: AuthClientTypes.Metadata = {
9+
name: "My Wallet",
10+
description: "",
11+
url: WALLET_URL,
12+
icons: [],
13+
};
14+
15+
const dappMetadata: AuthClientTypes.Metadata = {
16+
name: "Uniswap",
17+
description: "",
18+
url: DAPP_URL,
19+
icons: [],
20+
};
21+
22+
const authRequestPayload = {
23+
id: 1,
24+
jsonrpc: "2.0" as const,
25+
method: "wc_authRequest" as const,
26+
params: {
27+
requester: {
28+
publicKey: "abc123",
29+
metadata: dappMetadata,
30+
},
31+
payloadParams: {
32+
type: "eip4361" as const,
33+
chainId: "eip155:1",
34+
domain: "app.uniswap.org",
35+
aud: "https://app.uniswap.org/login",
36+
version: "1",
37+
nonce: "deadbeef",
38+
iat: new Date().toISOString(),
39+
},
40+
},
41+
};
42+
43+
function createMockClient(verifyResolveReturn: unknown) {
44+
return {
45+
metadata: walletMetadata,
46+
logger: {
47+
info: vi.fn(),
48+
error: vi.fn(),
49+
debug: vi.fn(),
50+
},
51+
requests: {
52+
set: vi.fn().mockResolvedValue(undefined),
53+
},
54+
core: {
55+
verify: {
56+
resolve: vi.fn().mockResolvedValue(verifyResolveReturn),
57+
},
58+
pairing: {
59+
register: vi.fn(),
60+
},
61+
},
62+
emit: vi.fn(),
63+
} as any;
64+
}
65+
66+
describe("AuthEngine.onAuthRequest verifyContext wiring", () => {
67+
it("produces VALID when attested origin matches requester (dApp) URL", async () => {
68+
const mockClient = createMockClient({ origin: DAPP_URL });
69+
const engine = new AuthEngine(mockClient);
70+
71+
await (engine as any).onAuthRequest("topic", authRequestPayload);
72+
73+
const [eventName, eventArgs] = mockClient.emit.mock.calls[0];
74+
expect(eventName).toBe("auth_request");
75+
expect(eventArgs.verifyContext.verified.validation).toBe("VALID");
76+
expect(eventArgs.verifyContext.verified.origin).toBe(DAPP_URL);
77+
});
78+
79+
it("produces INVALID when attested origin matches wallet URL but not requester URL (regression guard)", async () => {
80+
const mockClient = createMockClient({ origin: WALLET_URL });
81+
const engine = new AuthEngine(mockClient);
82+
83+
await (engine as any).onAuthRequest("topic", authRequestPayload);
84+
85+
const [eventName, eventArgs] = mockClient.emit.mock.calls[0];
86+
expect(eventName).toBe("auth_request");
87+
expect(eventArgs.verifyContext.verified.validation).toBe("INVALID");
88+
expect(eventArgs.verifyContext.verified.origin).toBe(WALLET_URL);
89+
});
90+
});
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { expect, describe, it, vi } from "vitest";
2+
import { buildVerifyContext } from "../src/utils/verifyContext";
3+
4+
const noopLog = (_err: unknown) => {
5+
return;
6+
};
7+
8+
describe("buildVerifyContext", () => {
9+
it("returns UNKNOWN when resolver returns undefined", async () => {
10+
const ctx = await buildVerifyContext(
11+
{ resolve: () => Promise.resolve(undefined), logError: noopLog },
12+
"hash",
13+
{ name: "x", description: "", url: "https://app.uniswap.org", icons: [] },
14+
);
15+
expect(ctx.verified.validation).toBe("UNKNOWN");
16+
expect(ctx.verified.origin).toBe("https://app.uniswap.org");
17+
});
18+
19+
it("returns UNKNOWN when resolver throws", async () => {
20+
const ctx = await buildVerifyContext(
21+
{
22+
resolve: () => Promise.reject(new Error("network")),
23+
logError: noopLog,
24+
},
25+
"hash",
26+
{ name: "x", description: "", url: "https://app.uniswap.org", icons: [] },
27+
);
28+
expect(ctx.verified.validation).toBe("UNKNOWN");
29+
});
30+
31+
it("returns VALID when attested origin matches requester metadata.url", async () => {
32+
const ctx = await buildVerifyContext(
33+
{
34+
resolve: () => Promise.resolve({ origin: "https://app.uniswap.org" }),
35+
logError: noopLog,
36+
},
37+
"hash",
38+
{ name: "Uniswap", description: "", url: "https://app.uniswap.org", icons: [] },
39+
);
40+
expect(ctx.verified.validation).toBe("VALID");
41+
expect(ctx.verified.origin).toBe("https://app.uniswap.org");
42+
});
43+
44+
it("returns INVALID when attested origin differs from claimed metadata.url", async () => {
45+
const ctx = await buildVerifyContext(
46+
{
47+
resolve: () => Promise.resolve({ origin: "https://attacker.com" }),
48+
logError: noopLog,
49+
},
50+
"hash",
51+
{ name: "Uniswap", description: "", url: "https://app.uniswap.org", icons: [] },
52+
);
53+
expect(ctx.verified.validation).toBe("INVALID");
54+
expect(ctx.verified.origin).toBe("https://attacker.com");
55+
});
56+
57+
it("propagates isScam=true from attestation", async () => {
58+
const ctx = await buildVerifyContext(
59+
{
60+
resolve: () => Promise.resolve({ origin: "https://attacker.com", isScam: true }),
61+
logError: noopLog,
62+
},
63+
"hash",
64+
{ name: "Uniswap", description: "", url: "https://app.uniswap.org", icons: [] },
65+
);
66+
expect(ctx.verified.isScam).toBe(true);
67+
});
68+
69+
it("sets isScam for any truthy attestation value (matches upstream)", async () => {
70+
const ctx = await buildVerifyContext(
71+
{
72+
resolve: () =>
73+
Promise.resolve({ origin: "https://x.com", isScam: 1 as unknown as boolean }),
74+
logError: noopLog,
75+
},
76+
"hash",
77+
{ name: "x", description: "", url: "https://x.com", icons: [] },
78+
);
79+
expect(ctx.verified.isScam).toBe(true);
80+
});
81+
82+
it("supports string-shaped attestation response (legacy)", async () => {
83+
const ctx = await buildVerifyContext(
84+
{ resolve: () => Promise.resolve("https://app.uniswap.org"), logError: noopLog },
85+
"hash",
86+
{ name: "Uniswap", description: "", url: "https://app.uniswap.org", icons: [] },
87+
);
88+
expect(ctx.verified.validation).toBe("VALID");
89+
expect(ctx.verified.origin).toBe("https://app.uniswap.org");
90+
});
91+
92+
it("forwards verifyUrl from metadata to resolver", async () => {
93+
const spy = vi.fn().mockResolvedValue(undefined);
94+
await buildVerifyContext({ resolve: spy, logError: noopLog }, "h", {
95+
name: "x",
96+
description: "",
97+
url: "https://x.com",
98+
icons: [],
99+
verifyUrl: "https://verify.example.com",
100+
});
101+
expect(spy).toHaveBeenCalledWith({
102+
attestationId: "h",
103+
verifyUrl: "https://verify.example.com",
104+
});
105+
});
106+
107+
it("regression: legitimate dApp must produce VALID when caller passes dApp metadata", async () => {
108+
const dappMetadata = {
109+
name: "Uniswap",
110+
description: "",
111+
url: "https://app.uniswap.org",
112+
icons: [],
113+
};
114+
const ctx = await buildVerifyContext(
115+
{
116+
resolve: () => Promise.resolve({ origin: "https://app.uniswap.org" }),
117+
logError: noopLog,
118+
},
119+
"hash",
120+
dappMetadata,
121+
);
122+
expect(ctx.verified.validation).toBe("VALID");
123+
});
124+
125+
it("returns UNKNOWN with empty origin when metadata is undefined", async () => {
126+
const ctx = await buildVerifyContext(
127+
{ resolve: () => Promise.resolve(undefined), logError: noopLog },
128+
"hash",
129+
undefined as any,
130+
);
131+
expect(ctx.verified.validation).toBe("UNKNOWN");
132+
expect(ctx.verified.origin).toBe("");
133+
expect(ctx.verified.verifyUrl).toBe("");
134+
});
135+
136+
it("returns UNKNOWN with empty origin when metadata is null", async () => {
137+
const ctx = await buildVerifyContext(
138+
{ resolve: () => Promise.resolve(undefined), logError: noopLog },
139+
"hash",
140+
null as any,
141+
);
142+
expect(ctx.verified.validation).toBe("UNKNOWN");
143+
expect(ctx.verified.origin).toBe("");
144+
});
145+
146+
it("returns UNKNOWN with empty origin when metadata.url is empty string", async () => {
147+
const ctx = await buildVerifyContext(
148+
{ resolve: () => Promise.resolve(undefined), logError: noopLog },
149+
"hash",
150+
{ name: "x", description: "", url: "", icons: [] },
151+
);
152+
expect(ctx.verified.validation).toBe("UNKNOWN");
153+
expect(ctx.verified.origin).toBe("");
154+
});
155+
156+
it("propagates isScam even when metadata is undefined", async () => {
157+
const ctx = await buildVerifyContext(
158+
{
159+
resolve: () => Promise.resolve({ origin: "https://attacker.com", isScam: true }),
160+
logError: noopLog,
161+
},
162+
"hash",
163+
undefined as any,
164+
);
165+
expect(ctx.verified.isScam).toBe(true);
166+
expect(ctx.verified.validation).toBe("UNKNOWN");
167+
});
168+
});

0 commit comments

Comments
 (0)