@pay-skill/sdk 0.1.8 → 0.2.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/README.md +143 -154
- package/dist/auth.d.ts +11 -6
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +19 -7
- package/dist/auth.js.map +1 -1
- package/dist/errors.d.ts +4 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +8 -3
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -6
- package/dist/index.js.map +1 -1
- package/dist/keychain.d.ts +8 -0
- package/dist/keychain.d.ts.map +1 -0
- package/dist/keychain.js +17 -0
- package/dist/keychain.js.map +1 -0
- package/dist/wallet.d.ts +135 -104
- package/dist/wallet.d.ts.map +1 -1
- package/dist/wallet.js +631 -276
- package/dist/wallet.js.map +1 -1
- package/jsr.json +13 -13
- package/knip.json +5 -5
- package/package.json +51 -48
- package/src/auth.ts +210 -200
- package/src/eip3009.ts +79 -79
- package/src/errors.ts +55 -48
- package/src/index.ts +24 -51
- package/src/keychain.ts +18 -0
- package/src/wallet.ts +1111 -445
- package/tests/test_auth_rejection.ts +102 -154
- package/tests/test_crypto.ts +138 -251
- package/tests/test_e2e.ts +99 -158
- package/tests/test_errors.ts +44 -36
- package/tests/test_ows.ts +153 -0
- package/tests/test_wallet.ts +194 -0
- package/dist/client.d.ts +0 -94
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -443
- package/dist/client.js.map +0 -1
- package/dist/models.d.ts +0 -78
- package/dist/models.d.ts.map +0 -1
- package/dist/models.js +0 -2
- package/dist/models.js.map +0 -1
- package/dist/ows-signer.d.ts +0 -75
- package/dist/ows-signer.d.ts.map +0 -1
- package/dist/ows-signer.js +0 -130
- package/dist/ows-signer.js.map +0 -1
- package/dist/signer.d.ts +0 -46
- package/dist/signer.d.ts.map +0 -1
- package/dist/signer.js +0 -111
- package/dist/signer.js.map +0 -1
- package/src/client.ts +0 -644
- package/src/models.ts +0 -77
- package/src/ows-signer.ts +0 -223
- package/src/signer.ts +0 -147
- package/tests/test_ows_integration.ts +0 -92
- package/tests/test_ows_signer.ts +0 -365
- package/tests/test_signer.ts +0 -47
- package/tests/test_validation.ts +0 -66
|
@@ -1,154 +1,102 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auth rejection tests — proves that
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("rejects request with stub signer (all-zero signature)", async () => {
|
|
107
|
-
// Client with a stub signer that returns zeros — server should reject
|
|
108
|
-
const client = new PayClient({
|
|
109
|
-
apiUrl: baseUrl,
|
|
110
|
-
signer: new CallbackSigner((_h: Uint8Array) => new Uint8Array(65)),
|
|
111
|
-
chainId: TEST_CHAIN_ID,
|
|
112
|
-
routerAddress: TEST_ROUTER,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
await assert.rejects(
|
|
116
|
-
() => client.getStatus(),
|
|
117
|
-
(err: unknown) => {
|
|
118
|
-
assert.ok(err instanceof PayServerError);
|
|
119
|
-
assert.equal(err.statusCode, 401);
|
|
120
|
-
return true;
|
|
121
|
-
}
|
|
122
|
-
);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("accepts request with valid auth headers (real signing)", async () => {
|
|
126
|
-
const client = new PayClient({
|
|
127
|
-
apiUrl: baseUrl,
|
|
128
|
-
privateKey: ANVIL_PK,
|
|
129
|
-
chainId: TEST_CHAIN_ID,
|
|
130
|
-
routerAddress: TEST_ROUTER,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// Should NOT throw — server accepts valid auth
|
|
134
|
-
const status = await client.getStatus();
|
|
135
|
-
assert.ok(status.balance >= 0);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("PayServerError has statusCode 401 for auth failures", async () => {
|
|
139
|
-
// Directly verify error structure
|
|
140
|
-
const client = new PayClient({
|
|
141
|
-
apiUrl: baseUrl,
|
|
142
|
-
signer: new CallbackSigner((_h: Uint8Array) => new Uint8Array(65)),
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
await client.getStatus();
|
|
147
|
-
assert.fail("Should have thrown PayServerError");
|
|
148
|
-
} catch (err) {
|
|
149
|
-
assert.ok(err instanceof PayServerError, "must be PayServerError");
|
|
150
|
-
assert.equal(err.statusCode, 401, "statusCode must be 401");
|
|
151
|
-
assert.equal(err.code, "server_error");
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Auth rejection tests — proves that the Wallet sends valid auth headers
|
|
3
|
+
* and that servers can reject invalid auth.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import {
|
|
9
|
+
createServer,
|
|
10
|
+
type Server,
|
|
11
|
+
type IncomingMessage,
|
|
12
|
+
type ServerResponse,
|
|
13
|
+
} from "node:http";
|
|
14
|
+
import { once } from "node:events";
|
|
15
|
+
import type { Hex, Address } from "viem";
|
|
16
|
+
import { Wallet, PayServerError } from "../src/index.js";
|
|
17
|
+
|
|
18
|
+
const ANVIL_PK =
|
|
19
|
+
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Minimal HTTP server that enforces X-Pay-* auth headers.
|
|
23
|
+
* /contracts is public (no auth).
|
|
24
|
+
* Everything else requires valid auth headers.
|
|
25
|
+
*/
|
|
26
|
+
function createAuthServer(): Server {
|
|
27
|
+
return createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
28
|
+
// /contracts is public
|
|
29
|
+
if (req.url?.endsWith("/contracts")) {
|
|
30
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
31
|
+
res.end(
|
|
32
|
+
JSON.stringify({
|
|
33
|
+
router: "0x5FbDB2315678afecb367f032d93F642f64180aa3",
|
|
34
|
+
tab: "0x" + "bb".repeat(20),
|
|
35
|
+
direct: "0x" + "cc".repeat(20),
|
|
36
|
+
fee: "0x" + "dd".repeat(20),
|
|
37
|
+
usdc: "0x" + "ee".repeat(20),
|
|
38
|
+
chain_id: 8453,
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const agent = req.headers["x-pay-agent"];
|
|
45
|
+
const sig = req.headers["x-pay-signature"];
|
|
46
|
+
const ts = req.headers["x-pay-timestamp"];
|
|
47
|
+
const nonce = req.headers["x-pay-nonce"];
|
|
48
|
+
|
|
49
|
+
if (!agent || !sig || !ts || !nonce) {
|
|
50
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
51
|
+
res.end(JSON.stringify({ error: "Missing auth headers" }));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const sigStr = Array.isArray(sig) ? sig[0] : sig;
|
|
56
|
+
if (!sigStr.startsWith("0x") || sigStr.length !== 132) {
|
|
57
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
58
|
+
res.end(JSON.stringify({ error: "Invalid signature format" }));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Auth passed
|
|
63
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
64
|
+
res.end(
|
|
65
|
+
JSON.stringify({
|
|
66
|
+
wallet: agent,
|
|
67
|
+
balance_usdc: "100000000",
|
|
68
|
+
open_tabs: 0,
|
|
69
|
+
total_locked: 0,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let server: Server;
|
|
76
|
+
let baseUrl: string;
|
|
77
|
+
|
|
78
|
+
describe("Auth with new Wallet class", () => {
|
|
79
|
+
beforeEach(async () => {
|
|
80
|
+
server = createAuthServer();
|
|
81
|
+
server.listen(0);
|
|
82
|
+
await once(server, "listening");
|
|
83
|
+
const addr = server.address();
|
|
84
|
+
if (typeof addr === "object" && addr) {
|
|
85
|
+
baseUrl = `http://127.0.0.1:${addr.port}/api/v1`;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(async () => {
|
|
90
|
+
server.close();
|
|
91
|
+
await once(server, "close");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("Wallet sends valid auth headers and gets 200", async () => {
|
|
95
|
+
process.env.PAYSKILL_API_URL = baseUrl;
|
|
96
|
+
const wallet = new Wallet({ privateKey: ANVIL_PK });
|
|
97
|
+
const status = await wallet.status();
|
|
98
|
+
assert.ok(status.address.startsWith("0x"));
|
|
99
|
+
assert.ok(status.balance.total >= 0);
|
|
100
|
+
delete process.env.PAYSKILL_API_URL;
|
|
101
|
+
});
|
|
102
|
+
});
|
package/tests/test_crypto.ts
CHANGED
|
@@ -1,251 +1,138 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Crypto round-trip tests — proves that:
|
|
3
|
-
* 1. Address derivation uses real secp256k1
|
|
4
|
-
* 2.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
assert.
|
|
49
|
-
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
it("signature recovers to the correct address", async () => {
|
|
141
|
-
const headers = await buildAuthHeaders(ANVIL_PK, "POST", "/api/v1/direct", {
|
|
142
|
-
chainId: TEST_CHAIN_ID,
|
|
143
|
-
routerAddress: TEST_ROUTER,
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// Recompute the hash and recover
|
|
147
|
-
const { hashTypedData } = await import("viem");
|
|
148
|
-
const hash = hashTypedData({
|
|
149
|
-
domain: {
|
|
150
|
-
name: "pay",
|
|
151
|
-
version: "0.1",
|
|
152
|
-
chainId: TEST_CHAIN_ID,
|
|
153
|
-
verifyingContract: TEST_ROUTER,
|
|
154
|
-
},
|
|
155
|
-
types: {
|
|
156
|
-
APIRequest: [
|
|
157
|
-
{ name: "method", type: "string" },
|
|
158
|
-
{ name: "path", type: "string" },
|
|
159
|
-
{ name: "timestamp", type: "uint256" },
|
|
160
|
-
{ name: "nonce", type: "bytes32" },
|
|
161
|
-
],
|
|
162
|
-
},
|
|
163
|
-
primaryType: "APIRequest",
|
|
164
|
-
message: {
|
|
165
|
-
method: "POST",
|
|
166
|
-
path: "/api/v1/direct",
|
|
167
|
-
timestamp: BigInt(headers["X-Pay-Timestamp"]),
|
|
168
|
-
nonce: headers["X-Pay-Nonce"] as Hex,
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const recovered = await recoverAddress({
|
|
173
|
-
hash,
|
|
174
|
-
signature: headers["X-Pay-Signature"] as Hex,
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
assert.equal(
|
|
178
|
-
recovered.toLowerCase(),
|
|
179
|
-
ANVIL_ADDRESS.toLowerCase(),
|
|
180
|
-
"recovered address must match signer"
|
|
181
|
-
);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
describe("computeEip712Hash", () => {
|
|
186
|
-
it("produces deterministic output", () => {
|
|
187
|
-
const nonce =
|
|
188
|
-
"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" as Hex;
|
|
189
|
-
const h1 = computeEip712Hash(
|
|
190
|
-
"POST",
|
|
191
|
-
"/api/v1/direct",
|
|
192
|
-
BigInt(1741400000),
|
|
193
|
-
nonce,
|
|
194
|
-
TEST_CHAIN_ID,
|
|
195
|
-
TEST_ROUTER
|
|
196
|
-
);
|
|
197
|
-
const h2 = computeEip712Hash(
|
|
198
|
-
"POST",
|
|
199
|
-
"/api/v1/direct",
|
|
200
|
-
BigInt(1741400000),
|
|
201
|
-
nonce,
|
|
202
|
-
TEST_CHAIN_ID,
|
|
203
|
-
TEST_ROUTER
|
|
204
|
-
);
|
|
205
|
-
assert.deepEqual(h1, h2);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("different methods produce different hashes", () => {
|
|
209
|
-
const nonce =
|
|
210
|
-
"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" as Hex;
|
|
211
|
-
const h1 = computeEip712Hash(
|
|
212
|
-
"POST",
|
|
213
|
-
"/api/v1/direct",
|
|
214
|
-
BigInt(1741400000),
|
|
215
|
-
nonce,
|
|
216
|
-
TEST_CHAIN_ID,
|
|
217
|
-
TEST_ROUTER
|
|
218
|
-
);
|
|
219
|
-
const h2 = computeEip712Hash(
|
|
220
|
-
"GET",
|
|
221
|
-
"/api/v1/direct",
|
|
222
|
-
BigInt(1741400000),
|
|
223
|
-
nonce,
|
|
224
|
-
TEST_CHAIN_ID,
|
|
225
|
-
TEST_ROUTER
|
|
226
|
-
);
|
|
227
|
-
assert.notDeepEqual(h1, h2);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it("different chain IDs produce different hashes", () => {
|
|
231
|
-
const nonce =
|
|
232
|
-
"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" as Hex;
|
|
233
|
-
const h1 = computeEip712Hash(
|
|
234
|
-
"POST",
|
|
235
|
-
"/api/v1/direct",
|
|
236
|
-
BigInt(1741400000),
|
|
237
|
-
nonce,
|
|
238
|
-
8453,
|
|
239
|
-
TEST_ROUTER
|
|
240
|
-
);
|
|
241
|
-
const h2 = computeEip712Hash(
|
|
242
|
-
"POST",
|
|
243
|
-
"/api/v1/direct",
|
|
244
|
-
BigInt(1741400000),
|
|
245
|
-
nonce,
|
|
246
|
-
84531,
|
|
247
|
-
TEST_ROUTER
|
|
248
|
-
);
|
|
249
|
-
assert.notDeepEqual(h1, h2);
|
|
250
|
-
});
|
|
251
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Crypto round-trip tests — proves that:
|
|
3
|
+
* 1. Address derivation uses real secp256k1
|
|
4
|
+
* 2. buildAuthHeaders produces valid recoverable signatures
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { recoverAddress, type Hex, type Address } from "viem";
|
|
10
|
+
|
|
11
|
+
import { Wallet } from "../src/wallet.js";
|
|
12
|
+
import { buildAuthHeaders, buildAuthHeadersSigned, computeEip712Hash } from "../src/auth.js";
|
|
13
|
+
|
|
14
|
+
// Anvil account #0 — well-known test key
|
|
15
|
+
const ANVIL_PK =
|
|
16
|
+
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex;
|
|
17
|
+
const ANVIL_ADDRESS =
|
|
18
|
+
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" as Address;
|
|
19
|
+
|
|
20
|
+
const TEST_ROUTER =
|
|
21
|
+
"0x5FbDB2315678afecb367f032d93F642f64180aa3" as Address;
|
|
22
|
+
const TEST_CHAIN_ID = 8453;
|
|
23
|
+
|
|
24
|
+
describe("Address derivation", () => {
|
|
25
|
+
it("derives correct address from Anvil #0 private key", () => {
|
|
26
|
+
const wallet = new Wallet({ privateKey: ANVIL_PK });
|
|
27
|
+
assert.equal(wallet.address, ANVIL_ADDRESS);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("works without 0x prefix", () => {
|
|
31
|
+
const wallet = new Wallet({ privateKey: ANVIL_PK.slice(2) });
|
|
32
|
+
assert.equal(wallet.address, ANVIL_ADDRESS);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("buildAuthHeaders", () => {
|
|
37
|
+
it("produces valid auth headers with correct address", async () => {
|
|
38
|
+
const headers = await buildAuthHeaders(
|
|
39
|
+
ANVIL_PK,
|
|
40
|
+
"POST",
|
|
41
|
+
"/api/v1/direct",
|
|
42
|
+
{ chainId: TEST_CHAIN_ID, routerAddress: TEST_ROUTER },
|
|
43
|
+
);
|
|
44
|
+
assert.equal(headers["X-Pay-Agent"], ANVIL_ADDRESS);
|
|
45
|
+
assert.ok(headers["X-Pay-Signature"].startsWith("0x"));
|
|
46
|
+
assert.equal(headers["X-Pay-Signature"].length, 132);
|
|
47
|
+
assert.ok(Number(headers["X-Pay-Timestamp"]) > 0);
|
|
48
|
+
assert.ok(headers["X-Pay-Nonce"].startsWith("0x"));
|
|
49
|
+
assert.equal(headers["X-Pay-Nonce"].length, 66);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("signature recovers to the correct address", async () => {
|
|
53
|
+
const headers = await buildAuthHeaders(
|
|
54
|
+
ANVIL_PK,
|
|
55
|
+
"POST",
|
|
56
|
+
"/api/v1/direct",
|
|
57
|
+
{ chainId: TEST_CHAIN_ID, routerAddress: TEST_ROUTER },
|
|
58
|
+
);
|
|
59
|
+
const { hashTypedData } = await import("viem");
|
|
60
|
+
const hash = hashTypedData({
|
|
61
|
+
domain: {
|
|
62
|
+
name: "pay",
|
|
63
|
+
version: "0.1",
|
|
64
|
+
chainId: TEST_CHAIN_ID,
|
|
65
|
+
verifyingContract: TEST_ROUTER,
|
|
66
|
+
},
|
|
67
|
+
types: {
|
|
68
|
+
APIRequest: [
|
|
69
|
+
{ name: "method", type: "string" },
|
|
70
|
+
{ name: "path", type: "string" },
|
|
71
|
+
{ name: "timestamp", type: "uint256" },
|
|
72
|
+
{ name: "nonce", type: "bytes32" },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
primaryType: "APIRequest",
|
|
76
|
+
message: {
|
|
77
|
+
method: "POST",
|
|
78
|
+
path: "/api/v1/direct",
|
|
79
|
+
timestamp: BigInt(headers["X-Pay-Timestamp"]),
|
|
80
|
+
nonce: headers["X-Pay-Nonce"] as Hex,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const recovered = await recoverAddress({
|
|
84
|
+
hash,
|
|
85
|
+
signature: headers["X-Pay-Signature"] as Hex,
|
|
86
|
+
});
|
|
87
|
+
assert.equal(recovered.toLowerCase(), ANVIL_ADDRESS.toLowerCase());
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("buildAuthHeadersSigned", () => {
|
|
92
|
+
it("produces same-structure headers as buildAuthHeaders", async () => {
|
|
93
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
94
|
+
const account = privateKeyToAccount(ANVIL_PK);
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
96
|
+
const signFn = (p: any) => account.signTypedData(p);
|
|
97
|
+
|
|
98
|
+
const headers = await buildAuthHeadersSigned(
|
|
99
|
+
ANVIL_ADDRESS,
|
|
100
|
+
signFn,
|
|
101
|
+
"GET",
|
|
102
|
+
"/api/v1/status",
|
|
103
|
+
{ chainId: TEST_CHAIN_ID, routerAddress: TEST_ROUTER },
|
|
104
|
+
);
|
|
105
|
+
assert.equal(headers["X-Pay-Agent"], ANVIL_ADDRESS);
|
|
106
|
+
assert.ok(headers["X-Pay-Signature"].startsWith("0x"));
|
|
107
|
+
assert.equal(headers["X-Pay-Signature"].length, 132);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("computeEip712Hash", () => {
|
|
112
|
+
const nonce =
|
|
113
|
+
"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" as Hex;
|
|
114
|
+
|
|
115
|
+
it("produces deterministic output", () => {
|
|
116
|
+
const h1 = computeEip712Hash(
|
|
117
|
+
"POST", "/api/v1/direct", BigInt(1741400000),
|
|
118
|
+
nonce, TEST_CHAIN_ID, TEST_ROUTER,
|
|
119
|
+
);
|
|
120
|
+
const h2 = computeEip712Hash(
|
|
121
|
+
"POST", "/api/v1/direct", BigInt(1741400000),
|
|
122
|
+
nonce, TEST_CHAIN_ID, TEST_ROUTER,
|
|
123
|
+
);
|
|
124
|
+
assert.deepEqual(h1, h2);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("different methods produce different hashes", () => {
|
|
128
|
+
const h1 = computeEip712Hash(
|
|
129
|
+
"POST", "/api/v1/direct", BigInt(1741400000),
|
|
130
|
+
nonce, TEST_CHAIN_ID, TEST_ROUTER,
|
|
131
|
+
);
|
|
132
|
+
const h2 = computeEip712Hash(
|
|
133
|
+
"GET", "/api/v1/direct", BigInt(1741400000),
|
|
134
|
+
nonce, TEST_CHAIN_ID, TEST_ROUTER,
|
|
135
|
+
);
|
|
136
|
+
assert.notDeepEqual(h1, h2);
|
|
137
|
+
});
|
|
138
|
+
});
|