@rickydata/agent0-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +17 -0
- package/Dockerfile +25 -0
- package/README.md +85 -0
- package/dist/auth/sdk-client.d.ts +47 -0
- package/dist/auth/sdk-client.js +142 -0
- package/dist/auth/sdk-client.js.map +1 -0
- package/dist/auth/token.d.ts +5 -0
- package/dist/auth/token.js +6 -0
- package/dist/auth/token.js.map +1 -0
- package/dist/auth/wallet-derivation.d.ts +26 -0
- package/dist/auth/wallet-derivation.js +76 -0
- package/dist/auth/wallet-derivation.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +140 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/a2a.d.ts +3 -0
- package/dist/tools/a2a.js +170 -0
- package/dist/tools/a2a.js.map +1 -0
- package/dist/tools/discovery.d.ts +3 -0
- package/dist/tools/discovery.js +465 -0
- package/dist/tools/discovery.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +38 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/payments.d.ts +3 -0
- package/dist/tools/payments.js +124 -0
- package/dist/tools/payments.js.map +1 -0
- package/dist/tools/registration.d.ts +3 -0
- package/dist/tools/registration.js +324 -0
- package/dist/tools/registration.js.map +1 -0
- package/dist/tools/reputation.d.ts +3 -0
- package/dist/tools/reputation.js +147 -0
- package/dist/tools/reputation.js.map +1 -0
- package/dist/utils/chains.d.ts +10 -0
- package/dist/utils/chains.js +33 -0
- package/dist/utils/chains.js.map +1 -0
- package/dist/utils/trust-labels.d.ts +10 -0
- package/dist/utils/trust-labels.js +48 -0
- package/dist/utils/trust-labels.js.map +1 -0
- package/dist/utils/validation.d.ts +12 -0
- package/dist/utils/validation.js +19 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +32 -0
- package/src/auth/sdk-client.ts +171 -0
- package/src/auth/token.ts +19 -0
- package/src/auth/wallet-derivation.ts +91 -0
- package/src/index.ts +184 -0
- package/src/tools/a2a.ts +205 -0
- package/src/tools/discovery.ts +517 -0
- package/src/tools/index.ts +45 -0
- package/src/tools/payments.ts +146 -0
- package/src/tools/registration.ts +389 -0
- package/src/tools/reputation.ts +183 -0
- package/src/utils/chains.ts +42 -0
- package/src/utils/trust-labels.ts +53 -0
- package/src/utils/validation.ts +20 -0
- package/tests/a2a.test.ts +234 -0
- package/tests/chains.test.ts +57 -0
- package/tests/discovery.test.ts +455 -0
- package/tests/e2e.test.ts +234 -0
- package/tests/payments.test.ts +148 -0
- package/tests/registration.test.ts +313 -0
- package/tests/reputation.test.ts +231 -0
- package/tests/sdk-client.test.ts +143 -0
- package/tests/tool-router.test.ts +28 -0
- package/tests/trust-labels.test.ts +229 -0
- package/tests/validation.test.ts +132 -0
- package/tests/wallet-derivation.test.ts +109 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Mock agent0-sdk before importing the module under test
|
|
4
|
+
vi.mock("agent0-sdk", () => {
|
|
5
|
+
const MockSDK = vi.fn().mockImplementation(() => ({
|
|
6
|
+
searchAgents: vi.fn(),
|
|
7
|
+
getAgent: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
return { SDK: MockSDK };
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
getReadOnlySDK,
|
|
14
|
+
getAuthenticatedSDK,
|
|
15
|
+
setDerivedKey,
|
|
16
|
+
hasAuthentication,
|
|
17
|
+
getCurrentChainId,
|
|
18
|
+
setChainId,
|
|
19
|
+
getAuthStatus,
|
|
20
|
+
} from "../src/auth/sdk-client.js";
|
|
21
|
+
import { SDK } from "agent0-sdk";
|
|
22
|
+
|
|
23
|
+
describe("sdk-client", () => {
|
|
24
|
+
const origEnv = { ...process.env };
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
// Reset module state by clearing cached SDK instances
|
|
29
|
+
setChainId(11155111);
|
|
30
|
+
delete process.env.ERC8004_PRIVATE_KEY;
|
|
31
|
+
delete process.env.ERC8004_DERIVED_KEY;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
process.env = { ...origEnv };
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("getReadOnlySDK", () => {
|
|
39
|
+
it("returns an SDK instance", () => {
|
|
40
|
+
const sdk = getReadOnlySDK();
|
|
41
|
+
expect(sdk).toBeDefined();
|
|
42
|
+
expect(SDK).toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("caches the SDK for the same chain", () => {
|
|
46
|
+
const sdk1 = getReadOnlySDK();
|
|
47
|
+
const sdk2 = getReadOnlySDK();
|
|
48
|
+
expect(sdk1).toBe(sdk2);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("creates new SDK for a different chain", () => {
|
|
52
|
+
const sdk1 = getReadOnlySDK(1);
|
|
53
|
+
const sdk2 = getReadOnlySDK(8453);
|
|
54
|
+
// Different chain IDs should create different instances
|
|
55
|
+
// (though the second call changes the cached chain)
|
|
56
|
+
expect(SDK).toHaveBeenCalledTimes(2);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("passes chainId to SDK config", () => {
|
|
60
|
+
getReadOnlySDK(8453);
|
|
61
|
+
expect(SDK).toHaveBeenCalledWith(
|
|
62
|
+
expect.objectContaining({ chainId: 8453 }),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("getAuthenticatedSDK", () => {
|
|
68
|
+
it("returns null when no private key is available", () => {
|
|
69
|
+
const sdk = getAuthenticatedSDK();
|
|
70
|
+
expect(sdk).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("returns SDK when ERC8004_PRIVATE_KEY env is set", () => {
|
|
74
|
+
process.env.ERC8004_PRIVATE_KEY = "0x1234";
|
|
75
|
+
const sdk = getAuthenticatedSDK();
|
|
76
|
+
expect(sdk).not.toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns SDK when ERC8004_DERIVED_KEY env is set", () => {
|
|
80
|
+
process.env.ERC8004_DERIVED_KEY = "0xabcd";
|
|
81
|
+
const sdk = getAuthenticatedSDK();
|
|
82
|
+
expect(sdk).not.toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns SDK after setDerivedKey is called", () => {
|
|
86
|
+
setDerivedKey("0xdeadbeef");
|
|
87
|
+
const sdk = getAuthenticatedSDK();
|
|
88
|
+
expect(sdk).not.toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("hasAuthentication", () => {
|
|
93
|
+
it("returns true with env key", () => {
|
|
94
|
+
process.env.ERC8004_PRIVATE_KEY = "0x1234";
|
|
95
|
+
expect(hasAuthentication()).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("returns true after setDerivedKey", () => {
|
|
99
|
+
setDerivedKey("0xdeadbeef");
|
|
100
|
+
expect(hasAuthentication()).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("getCurrentChainId / setChainId", () => {
|
|
105
|
+
it("defaults to 11155111", () => {
|
|
106
|
+
expect(getCurrentChainId()).toBe(11155111);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("setChainId updates the current chain", () => {
|
|
110
|
+
setChainId(8453);
|
|
111
|
+
expect(getCurrentChainId()).toBe(8453);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("getAuthStatus", () => {
|
|
116
|
+
it("returns env source when ERC8004_PRIVATE_KEY set", () => {
|
|
117
|
+
process.env.ERC8004_PRIVATE_KEY = "0x1234";
|
|
118
|
+
const status = getAuthStatus();
|
|
119
|
+
expect(status.hasKey).toBe(true);
|
|
120
|
+
expect(status.source).toBe("env:ERC8004_PRIVATE_KEY");
|
|
121
|
+
expect(status.isReadOnly).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("returns derived source after setDerivedKey", () => {
|
|
125
|
+
setDerivedKey("0xdeadbeef");
|
|
126
|
+
const status = getAuthStatus();
|
|
127
|
+
expect(status.source).toBe("derived");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("includes current chainId", () => {
|
|
131
|
+
setChainId(8453);
|
|
132
|
+
const status = getAuthStatus();
|
|
133
|
+
expect(status.chainId).toBe(8453);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("env key takes priority over derived key", () => {
|
|
137
|
+
setDerivedKey("0xdeadbeef");
|
|
138
|
+
process.env.ERC8004_PRIVATE_KEY = "0x1234";
|
|
139
|
+
const status = getAuthStatus();
|
|
140
|
+
expect(status.source).toBe("env:ERC8004_PRIVATE_KEY");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { TOOLS, handleToolCall } from "../src/tools/index.js";
|
|
3
|
+
|
|
4
|
+
describe("tool router", () => {
|
|
5
|
+
it("TOOLS is an array", () => {
|
|
6
|
+
expect(Array.isArray(TOOLS)).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("returns error for unknown tool name", async () => {
|
|
10
|
+
const result = await handleToolCall("nonexistent_tool", {});
|
|
11
|
+
expect(result).toEqual({ error: "Unknown tool: nonexistent_tool" });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("all tools have required MCP fields", () => {
|
|
15
|
+
for (const tool of TOOLS) {
|
|
16
|
+
expect(tool.name).toBeTypeOf("string");
|
|
17
|
+
expect(tool.name.length).toBeGreaterThan(0);
|
|
18
|
+
expect(tool.description).toBeTypeOf("string");
|
|
19
|
+
expect(tool.inputSchema).toBeDefined();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("no duplicate tool names", () => {
|
|
24
|
+
const names = TOOLS.map((t) => t.name);
|
|
25
|
+
const unique = new Set(names);
|
|
26
|
+
expect(unique.size).toBe(names.length);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { computeTrustLabel } from "../src/utils/trust-labels.js";
|
|
3
|
+
|
|
4
|
+
describe("computeTrustLabel", () => {
|
|
5
|
+
// =========================================================================
|
|
6
|
+
// 1. "Untrusted" — count >= 5 AND avg < -50
|
|
7
|
+
// =========================================================================
|
|
8
|
+
describe("Untrusted (count >= 5, avg < -50)", () => {
|
|
9
|
+
it("matches at exact boundary: count=5, avg=-51", () => {
|
|
10
|
+
const result = computeTrustLabel(5, -51);
|
|
11
|
+
expect(result.label).toBe("Untrusted");
|
|
12
|
+
expect(result.emoji).toBe("\u{1F534}");
|
|
13
|
+
expect(result.display).toContain("-51/100");
|
|
14
|
+
expect(result.display).toContain("5 reviews");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("matches with extreme values: count=100, avg=-100", () => {
|
|
18
|
+
expect(computeTrustLabel(100, -100).label).toBe("Untrusted");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("does NOT match at avg=-50 exactly (not < -50)", () => {
|
|
22
|
+
expect(computeTrustLabel(5, -50).label).not.toBe("Untrusted");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("does NOT match with count=4 even if avg is very low", () => {
|
|
26
|
+
// count < 5 means this rule is skipped; falls to "Caution" (avg < 0)
|
|
27
|
+
expect(computeTrustLabel(4, -80).label).toBe("Caution");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// =========================================================================
|
|
32
|
+
// 2. "Caution" — avg < 0 (catches everything with negative avg not already Untrusted)
|
|
33
|
+
// =========================================================================
|
|
34
|
+
describe("Caution (avg < 0)", () => {
|
|
35
|
+
it("matches at avg=-1 with any count", () => {
|
|
36
|
+
const result = computeTrustLabel(1, -1);
|
|
37
|
+
expect(result.label).toBe("Caution");
|
|
38
|
+
expect(result.emoji).toBe("\u{1F7E0}");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("matches at avg=-50 with count=5 (boundary not caught by Untrusted)", () => {
|
|
42
|
+
// Untrusted requires avg < -50, so avg=-50 falls through to Caution
|
|
43
|
+
expect(computeTrustLabel(5, -50).label).toBe("Caution");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("matches with count=0 and avg=-10", () => {
|
|
47
|
+
expect(computeTrustLabel(0, -10).label).toBe("Caution");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("does NOT match at avg=0 exactly", () => {
|
|
51
|
+
expect(computeTrustLabel(1, 0).label).not.toBe("Caution");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// =========================================================================
|
|
56
|
+
// 3. "Highly Trusted" — count >= 20 AND avg >= 80
|
|
57
|
+
// =========================================================================
|
|
58
|
+
describe("Highly Trusted (count >= 20, avg >= 80)", () => {
|
|
59
|
+
it("matches at exact boundary: count=20, avg=80", () => {
|
|
60
|
+
const result = computeTrustLabel(20, 80);
|
|
61
|
+
expect(result.label).toBe("Highly Trusted");
|
|
62
|
+
expect(result.emoji).toBe("\u2B50");
|
|
63
|
+
expect(result.display).toContain("80/100");
|
|
64
|
+
expect(result.display).toContain("20 reviews");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("matches with high values: count=1000, avg=100", () => {
|
|
68
|
+
expect(computeTrustLabel(1000, 100).label).toBe("Highly Trusted");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("does NOT match with count=19 even if avg=100", () => {
|
|
72
|
+
// Falls to Trusted (count >= 10, avg >= 70)
|
|
73
|
+
expect(computeTrustLabel(19, 100).label).toBe("Trusted");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does NOT match with avg=79 even if count=100", () => {
|
|
77
|
+
// Falls to Trusted (count >= 10, avg >= 70)
|
|
78
|
+
expect(computeTrustLabel(100, 79).label).toBe("Trusted");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// =========================================================================
|
|
83
|
+
// 4. "Trusted" — count >= 10 AND avg >= 70
|
|
84
|
+
// =========================================================================
|
|
85
|
+
describe("Trusted (count >= 10, avg >= 70)", () => {
|
|
86
|
+
it("matches at exact boundary: count=10, avg=70", () => {
|
|
87
|
+
const result = computeTrustLabel(10, 70);
|
|
88
|
+
expect(result.label).toBe("Trusted");
|
|
89
|
+
expect(result.emoji).toBe("\u{1F7E2}");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("matches at count=19, avg=79 (just below Highly Trusted)", () => {
|
|
93
|
+
expect(computeTrustLabel(19, 79).label).toBe("Trusted");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("does NOT match with count=9", () => {
|
|
97
|
+
// Falls to Established (count >= 5, avg >= 50)
|
|
98
|
+
expect(computeTrustLabel(9, 70).label).toBe("Established");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("does NOT match with avg=69", () => {
|
|
102
|
+
// Falls to Established (count >= 5, avg >= 50) if count >= 5
|
|
103
|
+
expect(computeTrustLabel(10, 69).label).toBe("Established");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// =========================================================================
|
|
108
|
+
// 5. "Established" — count >= 5 AND avg >= 50
|
|
109
|
+
// =========================================================================
|
|
110
|
+
describe("Established (count >= 5, avg >= 50)", () => {
|
|
111
|
+
it("matches at exact boundary: count=5, avg=50", () => {
|
|
112
|
+
const result = computeTrustLabel(5, 50);
|
|
113
|
+
expect(result.label).toBe("Established");
|
|
114
|
+
expect(result.emoji).toBe("\u{1F7E2}");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("matches at count=9, avg=69 (just below Trusted)", () => {
|
|
118
|
+
expect(computeTrustLabel(9, 69).label).toBe("Established");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("does NOT match with count=4", () => {
|
|
122
|
+
// Falls to Emerging (count > 0)
|
|
123
|
+
expect(computeTrustLabel(4, 50).label).toBe("Emerging");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("does NOT match with avg=49", () => {
|
|
127
|
+
// Falls to Emerging (count > 0)
|
|
128
|
+
expect(computeTrustLabel(5, 49).label).toBe("Emerging");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// =========================================================================
|
|
133
|
+
// 6. "Emerging" — count > 0
|
|
134
|
+
// =========================================================================
|
|
135
|
+
describe("Emerging (count > 0)", () => {
|
|
136
|
+
it("matches with count=1, avg=0", () => {
|
|
137
|
+
const result = computeTrustLabel(1, 0);
|
|
138
|
+
expect(result.label).toBe("Emerging");
|
|
139
|
+
expect(result.emoji).toBe("\u{1F535}");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("matches with count=4, avg=49 (below Established threshold)", () => {
|
|
143
|
+
expect(computeTrustLabel(4, 49).label).toBe("Emerging");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("matches with count=1, avg=99 (high avg but low count)", () => {
|
|
147
|
+
expect(computeTrustLabel(1, 99).label).toBe("Emerging");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("does NOT match with count=0", () => {
|
|
151
|
+
expect(computeTrustLabel(0, 0).label).not.toBe("Emerging");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// =========================================================================
|
|
156
|
+
// 7. "No Data" — count = 0 (fallback)
|
|
157
|
+
// =========================================================================
|
|
158
|
+
describe("No Data (count = 0)", () => {
|
|
159
|
+
it("matches with count=0, avg=0", () => {
|
|
160
|
+
const result = computeTrustLabel(0, 0);
|
|
161
|
+
expect(result.label).toBe("No Data");
|
|
162
|
+
expect(result.emoji).toBe("\u26AA");
|
|
163
|
+
expect(result.display).toBe("\u26AA No Data -- 0/100 (0 reviews)");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("matches with count=0 regardless of avg value", () => {
|
|
167
|
+
// avg could be anything when count=0, but avg < 0 check comes first
|
|
168
|
+
// count=0, avg=50 -> avg is not < -50 (skip Untrusted), avg is not < 0 (skip Caution),
|
|
169
|
+
// count < 20 (skip HT), count < 10 (skip T), count < 5 (skip E), count = 0 (skip Emerging) -> No Data
|
|
170
|
+
expect(computeTrustLabel(0, 50).label).toBe("No Data");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("Caution takes priority over No Data when avg < 0 and count = 0", () => {
|
|
174
|
+
// count=0, avg=-10 -> Untrusted: count < 5 NO; Caution: avg < 0 YES
|
|
175
|
+
expect(computeTrustLabel(0, -10).label).toBe("Caution");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// =========================================================================
|
|
180
|
+
// First-match-wins ordering verification
|
|
181
|
+
// =========================================================================
|
|
182
|
+
describe("first-match-wins ordering", () => {
|
|
183
|
+
it("Untrusted beats Caution when both could match", () => {
|
|
184
|
+
// count=5, avg=-60: matches Untrusted (count>=5, avg<-50) AND Caution (avg<0)
|
|
185
|
+
expect(computeTrustLabel(5, -60).label).toBe("Untrusted");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("Highly Trusted beats Trusted when both could match", () => {
|
|
189
|
+
// count=20, avg=80: matches HT AND Trusted AND Established
|
|
190
|
+
expect(computeTrustLabel(20, 80).label).toBe("Highly Trusted");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("Trusted beats Established when both could match", () => {
|
|
194
|
+
// count=10, avg=70: matches Trusted AND Established
|
|
195
|
+
expect(computeTrustLabel(10, 70).label).toBe("Trusted");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// =========================================================================
|
|
200
|
+
// Display string format
|
|
201
|
+
// =========================================================================
|
|
202
|
+
describe("display string format", () => {
|
|
203
|
+
it("formats correctly for Untrusted", () => {
|
|
204
|
+
const r = computeTrustLabel(10, -80);
|
|
205
|
+
expect(r.display).toBe("\u{1F534} Untrusted -- -80/100 (10 reviews)");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("formats correctly for Caution", () => {
|
|
209
|
+
const r = computeTrustLabel(3, -25);
|
|
210
|
+
expect(r.display).toBe("\u{1F7E0} Caution -- -25/100 (3 reviews)");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("formats correctly for Highly Trusted", () => {
|
|
214
|
+
const r = computeTrustLabel(50, 95);
|
|
215
|
+
expect(r.display).toBe("\u2B50 Highly Trusted -- 95/100 (50 reviews)");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("formats correctly for Emerging", () => {
|
|
219
|
+
const r = computeTrustLabel(2, 40);
|
|
220
|
+
expect(r.display).toBe("\u{1F535} Emerging -- 40/100 (2 reviews)");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("No Data always shows 0/100 (0 reviews)", () => {
|
|
224
|
+
expect(computeTrustLabel(0, 0).display).toBe(
|
|
225
|
+
"\u26AA No Data -- 0/100 (0 reviews)",
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
isValidAddress,
|
|
4
|
+
isValidBytes32,
|
|
5
|
+
isValidChainId,
|
|
6
|
+
} from "../src/utils/validation.js";
|
|
7
|
+
|
|
8
|
+
describe("isValidAddress", () => {
|
|
9
|
+
it("accepts a valid lowercase address", () => {
|
|
10
|
+
expect(isValidAddress("0x75992f829df3b5d515d70db0f77a98171ce261ef")).toBe(
|
|
11
|
+
true,
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("accepts a valid checksummed address", () => {
|
|
16
|
+
expect(isValidAddress("0x75992f829DF3B5d515D70DB0f77A98171cE261EF")).toBe(
|
|
17
|
+
true,
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("rejects address without 0x prefix", () => {
|
|
22
|
+
expect(isValidAddress("75992f829df3b5d515d70db0f77a98171ce261ef")).toBe(
|
|
23
|
+
false,
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("rejects address that is too short", () => {
|
|
28
|
+
expect(isValidAddress("0x75992f829df3b5d515d70db0f77a98171ce261e")).toBe(
|
|
29
|
+
false,
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("rejects address that is too long", () => {
|
|
34
|
+
expect(isValidAddress("0x75992f829df3b5d515d70db0f77a98171ce261efff")).toBe(
|
|
35
|
+
false,
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("rejects address with non-hex characters", () => {
|
|
40
|
+
expect(isValidAddress("0x75992f829df3b5d515d70db0f77a98171ce261eG")).toBe(
|
|
41
|
+
false,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("rejects empty string", () => {
|
|
46
|
+
expect(isValidAddress("")).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("rejects 0x alone", () => {
|
|
50
|
+
expect(isValidAddress("0x")).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("isValidBytes32", () => {
|
|
55
|
+
it("accepts a valid bytes32 hex string", () => {
|
|
56
|
+
const validBytes32 =
|
|
57
|
+
"0x0000000000000000000000000000000000000000000000000000000000000001";
|
|
58
|
+
expect(isValidBytes32(validBytes32)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("accepts uppercase hex", () => {
|
|
62
|
+
const validBytes32 =
|
|
63
|
+
"0xABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789";
|
|
64
|
+
expect(isValidBytes32(validBytes32)).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("rejects without 0x prefix", () => {
|
|
68
|
+
expect(
|
|
69
|
+
isValidBytes32(
|
|
70
|
+
"0000000000000000000000000000000000000000000000000000000000000001",
|
|
71
|
+
),
|
|
72
|
+
).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("rejects too short", () => {
|
|
76
|
+
expect(
|
|
77
|
+
isValidBytes32(
|
|
78
|
+
"0x000000000000000000000000000000000000000000000000000000000000001",
|
|
79
|
+
),
|
|
80
|
+
).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("rejects too long", () => {
|
|
84
|
+
expect(
|
|
85
|
+
isValidBytes32(
|
|
86
|
+
"0x00000000000000000000000000000000000000000000000000000000000000011",
|
|
87
|
+
),
|
|
88
|
+
).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("rejects non-hex characters", () => {
|
|
92
|
+
expect(
|
|
93
|
+
isValidBytes32(
|
|
94
|
+
"0xGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG",
|
|
95
|
+
),
|
|
96
|
+
).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("isValidChainId", () => {
|
|
101
|
+
it("accepts 1 (Ethereum mainnet)", () => {
|
|
102
|
+
expect(isValidChainId(1)).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("accepts 8453 (Base)", () => {
|
|
106
|
+
expect(isValidChainId(8453)).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("accepts large chain ID", () => {
|
|
110
|
+
expect(isValidChainId(999999)).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("rejects 0", () => {
|
|
114
|
+
expect(isValidChainId(0)).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("rejects negative numbers", () => {
|
|
118
|
+
expect(isValidChainId(-1)).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("rejects non-integer", () => {
|
|
122
|
+
expect(isValidChainId(1.5)).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("rejects NaN", () => {
|
|
126
|
+
expect(isValidChainId(NaN)).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("rejects Infinity", () => {
|
|
130
|
+
expect(isValidChainId(Infinity)).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Mock ethers to avoid needing a real crypto environment
|
|
4
|
+
vi.mock("ethers", () => ({
|
|
5
|
+
ethers: {
|
|
6
|
+
getBytes: vi.fn((hex: string) =>
|
|
7
|
+
Uint8Array.from(Buffer.from(hex.replace("0x", ""), "hex")),
|
|
8
|
+
),
|
|
9
|
+
hexlify: vi.fn((bytes: Uint8Array) =>
|
|
10
|
+
"0x" + Buffer.from(bytes).toString("hex"),
|
|
11
|
+
),
|
|
12
|
+
Wallet: vi.fn().mockImplementation((key: string) => ({
|
|
13
|
+
address: "0xDerived" + key.slice(2, 10),
|
|
14
|
+
privateKey: key,
|
|
15
|
+
})),
|
|
16
|
+
verifyMessage: vi.fn().mockReturnValue("0xSignerAddress"),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
getDerivationMessage,
|
|
22
|
+
deriveWalletFromSignature,
|
|
23
|
+
verifyDerivationSignature,
|
|
24
|
+
} from "../src/auth/wallet-derivation.js";
|
|
25
|
+
|
|
26
|
+
describe("wallet-derivation", () => {
|
|
27
|
+
describe("getDerivationMessage", () => {
|
|
28
|
+
it("returns a non-empty string", () => {
|
|
29
|
+
const msg = getDerivationMessage();
|
|
30
|
+
expect(typeof msg).toBe("string");
|
|
31
|
+
expect(msg.length).toBeGreaterThan(0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("is deterministic (no nonces/timestamps)", () => {
|
|
35
|
+
const msg1 = getDerivationMessage();
|
|
36
|
+
const msg2 = getDerivationMessage();
|
|
37
|
+
expect(msg1).toBe(msg2);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("contains ERC-8004 context", () => {
|
|
41
|
+
expect(getDerivationMessage()).toContain("ERC-8004");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("contains safety notice about funds", () => {
|
|
45
|
+
expect(getDerivationMessage()).toContain(
|
|
46
|
+
"does not grant access to your funds",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("deriveWalletFromSignature", () => {
|
|
52
|
+
it("returns address and privateKey", () => {
|
|
53
|
+
// Use a valid-looking hex signature (65 bytes = 130 hex chars)
|
|
54
|
+
const sig =
|
|
55
|
+
"0x" + "ab".repeat(65);
|
|
56
|
+
const wallet = deriveWalletFromSignature(sig);
|
|
57
|
+
expect(wallet.address).toBeTypeOf("string");
|
|
58
|
+
expect(wallet.privateKey).toBeTypeOf("string");
|
|
59
|
+
expect(wallet.privateKey).toMatch(/^0x/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("is deterministic: same signature -> same key", () => {
|
|
63
|
+
const sig = "0x" + "cd".repeat(65);
|
|
64
|
+
const w1 = deriveWalletFromSignature(sig);
|
|
65
|
+
const w2 = deriveWalletFromSignature(sig);
|
|
66
|
+
expect(w1.privateKey).toBe(w2.privateKey);
|
|
67
|
+
expect(w1.address).toBe(w2.address);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("different signatures -> different keys", () => {
|
|
71
|
+
const sig1 = "0x" + "aa".repeat(65);
|
|
72
|
+
const sig2 = "0x" + "bb".repeat(65);
|
|
73
|
+
const w1 = deriveWalletFromSignature(sig1);
|
|
74
|
+
const w2 = deriveWalletFromSignature(sig2);
|
|
75
|
+
expect(w1.privateKey).not.toBe(w2.privateKey);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("throws for empty signature", () => {
|
|
79
|
+
expect(() => deriveWalletFromSignature("")).toThrow("Invalid signature");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("throws for signature without 0x prefix", () => {
|
|
83
|
+
expect(() => deriveWalletFromSignature("abcdef")).toThrow(
|
|
84
|
+
"Invalid signature",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("verifyDerivationSignature", () => {
|
|
90
|
+
it("returns true when signer matches expected address", () => {
|
|
91
|
+
// Mock is set to return "0xSignerAddress"
|
|
92
|
+
expect(
|
|
93
|
+
verifyDerivationSignature("0xabc123", "0xSignerAddress"),
|
|
94
|
+
).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("is case-insensitive for address comparison", () => {
|
|
98
|
+
expect(
|
|
99
|
+
verifyDerivationSignature("0xabc123", "0xsigneraddress"),
|
|
100
|
+
).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns false when signer does not match", () => {
|
|
104
|
+
expect(
|
|
105
|
+
verifyDerivationSignature("0xabc123", "0xDifferentAddress"),
|
|
106
|
+
).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
package/tsconfig.json
ADDED