@4mica/sdk 0.5.0 → 0.5.2
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 +251 -395
- package/package.json +36 -3
- package/.eslintrc.cjs +0 -29
- package/.github/workflows/ci.yml +0 -31
- package/.prettierignore +0 -3
- package/.prettierrc +0 -6
- package/eslint.config.mjs +0 -22
- package/src/abi/core4mica.json +0 -1605
- package/src/abi/erc20.json +0 -187
- package/src/auth.ts +0 -325
- package/src/bls.ts +0 -76
- package/src/client.ts +0 -347
- package/src/config.ts +0 -149
- package/src/contract.ts +0 -194
- package/src/errors.ts +0 -40
- package/src/guarantee.ts +0 -96
- package/src/index.ts +0 -12
- package/src/models.ts +0 -309
- package/src/rpc.ts +0 -225
- package/src/signing.ts +0 -330
- package/src/utils.ts +0 -75
- package/src/x402/index.ts +0 -192
- package/src/x402/models.ts +0 -94
- package/tests/auth.integration.test.ts +0 -97
- package/tests/auth.test.ts +0 -292
- package/tests/config.test.ts +0 -56
- package/tests/guarantee.test.ts +0 -26
- package/tests/rpc.test.ts +0 -76
- package/tests/signing.test.ts +0 -52
- package/tests/utils.test.ts +0 -35
- package/tests/x402.test.ts +0 -152
- package/tsconfig.build.json +0 -5
- package/tsconfig.json +0 -15
- package/vitest.config.ts +0 -12
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { Wallet } from 'ethers';
|
|
3
|
-
import { AuthSession } from '../src/auth';
|
|
4
|
-
import { Client } from '../src/client';
|
|
5
|
-
import { ConfigBuilder } from '../src/config';
|
|
6
|
-
import { RpcError } from '../src/errors';
|
|
7
|
-
|
|
8
|
-
const DEFAULT_RPC_URL = 'http://127.0.0.1:3000';
|
|
9
|
-
const DEFAULT_AUTH_URL = 'http://127.0.0.1:3000';
|
|
10
|
-
const DEFAULT_PAYER_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
|
|
11
|
-
|
|
12
|
-
const integrationEnabled = process.env['4MICA_INTEGRATION'] === '1';
|
|
13
|
-
const describeIntegration = integrationEnabled ? describe : describe.skip;
|
|
14
|
-
|
|
15
|
-
const resolveRpcUrl = (): string => process.env['4MICA_RPC_URL'] ?? DEFAULT_RPC_URL;
|
|
16
|
-
const resolveAuthUrl = (rpcUrl: string): string =>
|
|
17
|
-
process.env['4MICA_AUTH_URL'] ?? rpcUrl ?? DEFAULT_AUTH_URL;
|
|
18
|
-
const resolvePrivateKey = (): string =>
|
|
19
|
-
process.env['PAYER_KEY'] ?? process.env['4MICA_WALLET_PRIVATE_KEY'] ?? DEFAULT_PAYER_KEY;
|
|
20
|
-
|
|
21
|
-
const isUnauthorized = (err: unknown): boolean => err instanceof RpcError && err.status === 401;
|
|
22
|
-
|
|
23
|
-
const resolveBearerToken = async (authUrl: string, privateKey: string): Promise<string> => {
|
|
24
|
-
const token = process.env['4MICA_BEARER_TOKEN'];
|
|
25
|
-
if (token) {
|
|
26
|
-
return token;
|
|
27
|
-
}
|
|
28
|
-
const session = new AuthSession({ authUrl, privateKey });
|
|
29
|
-
try {
|
|
30
|
-
const tokens = await session.login();
|
|
31
|
-
return tokens.accessToken;
|
|
32
|
-
} finally {
|
|
33
|
-
await session.logout();
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
describeIntegration('Auth integration', () => {
|
|
38
|
-
it('auth login allows core request', async () => {
|
|
39
|
-
const rpcUrl = resolveRpcUrl();
|
|
40
|
-
const authUrl = resolveAuthUrl(rpcUrl);
|
|
41
|
-
const privateKey = resolvePrivateKey();
|
|
42
|
-
|
|
43
|
-
const cfg = new ConfigBuilder()
|
|
44
|
-
.rpcUrl(rpcUrl)
|
|
45
|
-
.walletPrivateKey(privateKey)
|
|
46
|
-
.authUrl(authUrl)
|
|
47
|
-
.enableAuth()
|
|
48
|
-
.build();
|
|
49
|
-
|
|
50
|
-
const client = await Client.new(cfg);
|
|
51
|
-
try {
|
|
52
|
-
const tokens = await client.login();
|
|
53
|
-
expect(tokens.accessToken).toBeTruthy();
|
|
54
|
-
expect(tokens.refreshToken).toBeTruthy();
|
|
55
|
-
|
|
56
|
-
const userAddress = new Wallet(privateKey).address;
|
|
57
|
-
const asset = process.env['ASSET_ADDRESS'] ?? '0x0000000000000000000000000000000000000000';
|
|
58
|
-
try {
|
|
59
|
-
await client.rpc.getUserAssetBalance(userAddress, asset);
|
|
60
|
-
} catch (err) {
|
|
61
|
-
if (isUnauthorized(err)) {
|
|
62
|
-
throw err;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
} finally {
|
|
66
|
-
await client.aclose();
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('bearer token allows core request', async () => {
|
|
71
|
-
const rpcUrl = resolveRpcUrl();
|
|
72
|
-
const authUrl = resolveAuthUrl(rpcUrl);
|
|
73
|
-
const privateKey = resolvePrivateKey();
|
|
74
|
-
const bearerToken = await resolveBearerToken(authUrl, privateKey);
|
|
75
|
-
|
|
76
|
-
const cfg = new ConfigBuilder()
|
|
77
|
-
.rpcUrl(rpcUrl)
|
|
78
|
-
.walletPrivateKey(privateKey)
|
|
79
|
-
.bearerToken(bearerToken)
|
|
80
|
-
.build();
|
|
81
|
-
|
|
82
|
-
const client = await Client.new(cfg);
|
|
83
|
-
try {
|
|
84
|
-
const userAddress = new Wallet(privateKey).address;
|
|
85
|
-
const asset = process.env['ASSET_ADDRESS'] ?? '0x0000000000000000000000000000000000000000';
|
|
86
|
-
try {
|
|
87
|
-
await client.rpc.getUserAssetBalance(userAddress, asset);
|
|
88
|
-
} catch (err) {
|
|
89
|
-
if (isUnauthorized(err)) {
|
|
90
|
-
throw err;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
} finally {
|
|
94
|
-
await client.aclose();
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
});
|
package/tests/auth.test.ts
DELETED
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { AuthClient, AuthSession, buildSiweMessage } from '../src/auth';
|
|
3
|
-
import { AuthApiError, AuthConfigError, AuthDecodeError, AuthTransportError } from '../src/errors';
|
|
4
|
-
import type { FetchFn } from '../src/rpc';
|
|
5
|
-
import { createLocalSigner, ValidationError } from '../src';
|
|
6
|
-
|
|
7
|
-
const PRIVATE_KEY = '0x59c6995e998f97a5a0044976f7be35d5ad91c0cfa55b5cfb20b07a1c60f4c5bc';
|
|
8
|
-
|
|
9
|
-
const noncePayload = {
|
|
10
|
-
nonce: 'nonce-123',
|
|
11
|
-
siwe: {
|
|
12
|
-
domain: 'example.com',
|
|
13
|
-
uri: 'https://example.com/login',
|
|
14
|
-
chain_id: 1,
|
|
15
|
-
statement: 'Sign in',
|
|
16
|
-
expiration: '2024-01-01T00:00:00Z',
|
|
17
|
-
issued_at: '2024-01-01T00:00:00Z',
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
describe('buildSiweMessage', () => {
|
|
22
|
-
it('formats message for signing', () => {
|
|
23
|
-
const message = buildSiweMessage({
|
|
24
|
-
domain: 'example.com',
|
|
25
|
-
address: '0xabc',
|
|
26
|
-
statement: 'Sign in',
|
|
27
|
-
uri: 'https://example.com',
|
|
28
|
-
chainId: 1,
|
|
29
|
-
nonce: 'nonce',
|
|
30
|
-
issuedAt: '2024-01-01T00:00:00Z',
|
|
31
|
-
expiration: '2024-01-02T00:00:00Z',
|
|
32
|
-
});
|
|
33
|
-
expect(message).toBe(
|
|
34
|
-
[
|
|
35
|
-
'example.com wants you to sign in with your Ethereum account:',
|
|
36
|
-
'0xabc',
|
|
37
|
-
'',
|
|
38
|
-
'Sign in',
|
|
39
|
-
'',
|
|
40
|
-
'URI: https://example.com',
|
|
41
|
-
'Version: 1',
|
|
42
|
-
'Chain ID: 1',
|
|
43
|
-
'Nonce: nonce',
|
|
44
|
-
'Issued At: 2024-01-01T00:00:00Z',
|
|
45
|
-
'Expiration Time: 2024-01-02T00:00:00Z',
|
|
46
|
-
].join('\n')
|
|
47
|
-
);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe('AuthClient', () => {
|
|
52
|
-
it('parses siwe templates in snake_case and camelCase', async () => {
|
|
53
|
-
const snakeFetch = vi.fn<FetchFn>(async () => {
|
|
54
|
-
return new Response(JSON.stringify(noncePayload), { status: 200 });
|
|
55
|
-
});
|
|
56
|
-
const snakeClient = new AuthClient('https://auth.example.com', snakeFetch);
|
|
57
|
-
const snake = await snakeClient.getNonce('0x123');
|
|
58
|
-
expect(snake.siwe.chainId).toBe(1);
|
|
59
|
-
expect(snake.siwe.issuedAt).toBe('2024-01-01T00:00:00Z');
|
|
60
|
-
|
|
61
|
-
const camelFetch = vi.fn<FetchFn>(async () => {
|
|
62
|
-
return new Response(
|
|
63
|
-
JSON.stringify({
|
|
64
|
-
nonce: 'nonce-456',
|
|
65
|
-
siwe: {
|
|
66
|
-
domain: 'example.com',
|
|
67
|
-
uri: 'https://example.com/login',
|
|
68
|
-
chainId: 5,
|
|
69
|
-
statement: 'Sign in',
|
|
70
|
-
expiration: '2024-02-01T00:00:00Z',
|
|
71
|
-
issuedAt: '2024-02-01T00:00:00Z',
|
|
72
|
-
},
|
|
73
|
-
}),
|
|
74
|
-
{ status: 200 }
|
|
75
|
-
);
|
|
76
|
-
});
|
|
77
|
-
const camelClient = new AuthClient('https://auth.example.com', camelFetch);
|
|
78
|
-
const camel = await camelClient.getNonce('0x123');
|
|
79
|
-
expect(camel.siwe.chainId).toBe(5);
|
|
80
|
-
expect(camel.siwe.issuedAt).toBe('2024-02-01T00:00:00Z');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('throws on invalid JSON responses', async () => {
|
|
84
|
-
const fetchMock = vi.fn<FetchFn>(async () => new Response('not-json', { status: 200 }));
|
|
85
|
-
const client = new AuthClient('https://auth.example.com', fetchMock);
|
|
86
|
-
await expect(client.getNonce('0x123')).rejects.toThrow(AuthDecodeError);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('surfaces non-2xx errors with json bodies', async () => {
|
|
90
|
-
const fetchMock = vi.fn<FetchFn>(async () => {
|
|
91
|
-
return new Response(JSON.stringify({ error: 'nope' }), { status: 401 });
|
|
92
|
-
});
|
|
93
|
-
const client = new AuthClient('https://auth.example.com', fetchMock);
|
|
94
|
-
await expect(client.getNonce('0x123')).rejects.toThrow(AuthApiError);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('surfaces non-2xx errors with text bodies', async () => {
|
|
98
|
-
const fetchMock = vi.fn<FetchFn>(async () => new Response('nope', { status: 500 }));
|
|
99
|
-
const client = new AuthClient('https://auth.example.com', fetchMock);
|
|
100
|
-
await expect(client.getNonce('0x123')).rejects.toThrow(AuthApiError);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('wraps transport errors', async () => {
|
|
104
|
-
const fetchMock = vi.fn<FetchFn>(async () => {
|
|
105
|
-
throw new Error('boom');
|
|
106
|
-
});
|
|
107
|
-
const client = new AuthClient('https://auth.example.com', fetchMock);
|
|
108
|
-
await expect(client.getNonce('0x123')).rejects.toThrow(AuthTransportError);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('AuthSession', () => {
|
|
113
|
-
it('caches tokens and refreshes when expiring', async () => {
|
|
114
|
-
let nowMs = 0;
|
|
115
|
-
const nowSpy = vi.spyOn(Date, 'now').mockImplementation(() => nowMs);
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
const calls = { nonce: 0, verify: 0, refresh: 0 };
|
|
119
|
-
const fetchMock: FetchFn = async (input, init) => {
|
|
120
|
-
const path = new URL(input.toString()).pathname;
|
|
121
|
-
if (path === '/auth/nonce') {
|
|
122
|
-
calls.nonce += 1;
|
|
123
|
-
const body = JSON.parse(init?.body as string);
|
|
124
|
-
expect(body.address).toBeTruthy();
|
|
125
|
-
return new Response(JSON.stringify(noncePayload), { status: 200 });
|
|
126
|
-
}
|
|
127
|
-
if (path === '/auth/verify') {
|
|
128
|
-
calls.verify += 1;
|
|
129
|
-
const body = JSON.parse(init?.body as string);
|
|
130
|
-
expect(body.address).toBeTruthy();
|
|
131
|
-
return new Response(
|
|
132
|
-
JSON.stringify({
|
|
133
|
-
access_token: 'access-1',
|
|
134
|
-
refresh_token: 'refresh-1',
|
|
135
|
-
expires_in: 120,
|
|
136
|
-
}),
|
|
137
|
-
{ status: 200 }
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
if (path === '/auth/refresh') {
|
|
141
|
-
calls.refresh += 1;
|
|
142
|
-
return new Response(
|
|
143
|
-
JSON.stringify({
|
|
144
|
-
access_token: 'access-2',
|
|
145
|
-
refresh_token: 'refresh-2',
|
|
146
|
-
expires_in: 120,
|
|
147
|
-
}),
|
|
148
|
-
{ status: 200 }
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
return new Response('not found', { status: 404 });
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
const session = new AuthSession({
|
|
155
|
-
authUrl: 'https://auth.example.com',
|
|
156
|
-
signer: createLocalSigner(PRIVATE_KEY),
|
|
157
|
-
refreshMarginSecs: 30,
|
|
158
|
-
fetchFn: fetchMock,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
const first = await session.accessToken();
|
|
162
|
-
expect(first).toBe('access-1');
|
|
163
|
-
expect(calls.nonce).toBe(1);
|
|
164
|
-
expect(calls.verify).toBe(1);
|
|
165
|
-
|
|
166
|
-
nowMs = 10_000;
|
|
167
|
-
const cached = await session.accessToken();
|
|
168
|
-
expect(cached).toBe('access-1');
|
|
169
|
-
expect(calls.refresh).toBe(0);
|
|
170
|
-
|
|
171
|
-
nowMs = 100_000;
|
|
172
|
-
const refreshed = await session.accessToken();
|
|
173
|
-
expect(refreshed).toBe('access-2');
|
|
174
|
-
expect(calls.refresh).toBe(1);
|
|
175
|
-
} finally {
|
|
176
|
-
nowSpy.mockRestore();
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('falls back to login when refresh returns 401', async () => {
|
|
181
|
-
let nowMs = 0;
|
|
182
|
-
const nowSpy = vi.spyOn(Date, 'now').mockImplementation(() => nowMs);
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
const calls = { nonce: 0, verify: 0, refresh: 0 };
|
|
186
|
-
const fetchMock: FetchFn = async (input, init) => {
|
|
187
|
-
const path = new URL(input.toString()).pathname;
|
|
188
|
-
if (path === '/auth/nonce') {
|
|
189
|
-
calls.nonce += 1;
|
|
190
|
-
const body = JSON.parse(init?.body as string);
|
|
191
|
-
expect(body.address).toBeTruthy();
|
|
192
|
-
return new Response(JSON.stringify(noncePayload), { status: 200 });
|
|
193
|
-
}
|
|
194
|
-
if (path === '/auth/verify') {
|
|
195
|
-
calls.verify += 1;
|
|
196
|
-
const body = JSON.parse(init?.body as string);
|
|
197
|
-
expect(body.address).toBeTruthy();
|
|
198
|
-
return new Response(
|
|
199
|
-
JSON.stringify({
|
|
200
|
-
access_token: `access-${calls.verify}`,
|
|
201
|
-
refresh_token: `refresh-${calls.verify}`,
|
|
202
|
-
expires_in: 5,
|
|
203
|
-
}),
|
|
204
|
-
{ status: 200 }
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
if (path === '/auth/refresh') {
|
|
208
|
-
calls.refresh += 1;
|
|
209
|
-
return new Response('unauthorized', { status: 401 });
|
|
210
|
-
}
|
|
211
|
-
return new Response('not found', { status: 404 });
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const session = new AuthSession({
|
|
215
|
-
authUrl: 'https://auth.example.com',
|
|
216
|
-
signer: createLocalSigner(PRIVATE_KEY),
|
|
217
|
-
refreshMarginSecs: 0,
|
|
218
|
-
fetchFn: fetchMock,
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const first = await session.accessToken();
|
|
222
|
-
expect(first).toBe('access-1');
|
|
223
|
-
|
|
224
|
-
nowMs = 10_000;
|
|
225
|
-
const second = await session.accessToken();
|
|
226
|
-
expect(second).toBe('access-2');
|
|
227
|
-
expect(calls.refresh).toBe(1);
|
|
228
|
-
expect(calls.verify).toBe(2);
|
|
229
|
-
expect(calls.nonce).toBe(2);
|
|
230
|
-
} finally {
|
|
231
|
-
nowSpy.mockRestore();
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('single-flights concurrent accessToken calls', async () => {
|
|
236
|
-
const calls = { nonce: 0, verify: 0 };
|
|
237
|
-
const fetchMock: FetchFn = async (input, init) => {
|
|
238
|
-
const path = new URL(input.toString()).pathname;
|
|
239
|
-
if (path === '/auth/nonce') {
|
|
240
|
-
calls.nonce += 1;
|
|
241
|
-
const body = JSON.parse(init?.body as string);
|
|
242
|
-
expect(body.address).toBeTruthy();
|
|
243
|
-
return new Response(JSON.stringify(noncePayload), { status: 200 });
|
|
244
|
-
}
|
|
245
|
-
if (path === '/auth/verify') {
|
|
246
|
-
calls.verify += 1;
|
|
247
|
-
const body = JSON.parse(init?.body as string);
|
|
248
|
-
expect(body.address).toBeTruthy();
|
|
249
|
-
return new Response(
|
|
250
|
-
JSON.stringify({
|
|
251
|
-
access_token: 'access-1',
|
|
252
|
-
refresh_token: 'refresh-1',
|
|
253
|
-
expires_in: 120,
|
|
254
|
-
}),
|
|
255
|
-
{ status: 200 }
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
return new Response('not found', { status: 404 });
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
const session = new AuthSession({
|
|
262
|
-
authUrl: 'https://auth.example.com',
|
|
263
|
-
signer: createLocalSigner(PRIVATE_KEY),
|
|
264
|
-
fetchFn: fetchMock,
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
const [first, second] = await Promise.all([session.accessToken(), session.accessToken()]);
|
|
268
|
-
expect(first).toBe('access-1');
|
|
269
|
-
expect(second).toBe('access-1');
|
|
270
|
-
expect(calls.nonce).toBe(1);
|
|
271
|
-
expect(calls.verify).toBe(1);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it('rejects invalid private keys and negative refresh margins', () => {
|
|
275
|
-
expect(
|
|
276
|
-
() =>
|
|
277
|
-
new AuthSession({
|
|
278
|
-
authUrl: 'https://auth.example.com',
|
|
279
|
-
signer: createLocalSigner('0x1234'),
|
|
280
|
-
})
|
|
281
|
-
).toThrow(ValidationError);
|
|
282
|
-
|
|
283
|
-
expect(
|
|
284
|
-
() =>
|
|
285
|
-
new AuthSession({
|
|
286
|
-
authUrl: 'https://auth.example.com',
|
|
287
|
-
signer: createLocalSigner(PRIVATE_KEY),
|
|
288
|
-
refreshMarginSecs: -1,
|
|
289
|
-
})
|
|
290
|
-
).toThrow(AuthConfigError);
|
|
291
|
-
});
|
|
292
|
-
});
|
package/tests/config.test.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { ConfigBuilder } from '../src/config';
|
|
3
|
-
import { ConfigError } from '../src/errors';
|
|
4
|
-
|
|
5
|
-
describe('ConfigBuilder', () => {
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
vi.resetModules();
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
delete process.env['4MICA_RPC_URL'];
|
|
12
|
-
delete process.env['4MICA_WALLET_PRIVATE_KEY'];
|
|
13
|
-
delete process.env['4MICA_ETHEREUM_HTTP_RPC_URL'];
|
|
14
|
-
delete process.env['4MICA_CONTRACT_ADDRESS'];
|
|
15
|
-
delete process.env['4MICA_ADMIN_API_KEY'];
|
|
16
|
-
delete process.env['4MICA_AUTH_URL'];
|
|
17
|
-
delete process.env['4MICA_AUTH_REFRESH_MARGIN_SECS'];
|
|
18
|
-
delete process.env['4MICA_BEARER_TOKEN'];
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('reads from env', () => {
|
|
22
|
-
process.env['4MICA_RPC_URL'] = 'https://example.com';
|
|
23
|
-
process.env['4MICA_WALLET_PRIVATE_KEY'] = '11'.repeat(32);
|
|
24
|
-
const cfg = new ConfigBuilder().fromEnv().build();
|
|
25
|
-
expect(cfg.rpcUrl).toBe('https://example.com');
|
|
26
|
-
expect(cfg.walletPrivateKey.startsWith('0x11')).toBe(true);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('requires private key', () => {
|
|
30
|
-
delete process.env['4MICA_WALLET_PRIVATE_KEY'];
|
|
31
|
-
const builder = new ConfigBuilder().fromEnv();
|
|
32
|
-
expect(() => builder.build()).toThrow(ConfigError);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('rejects invalid private key', () => {
|
|
36
|
-
const builder = new ConfigBuilder().walletPrivateKey('0x1234');
|
|
37
|
-
expect(() => builder.build()).toThrow(ConfigError);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('reads auth env defaults', () => {
|
|
41
|
-
process.env['4MICA_RPC_URL'] = 'https://example.com';
|
|
42
|
-
process.env['4MICA_WALLET_PRIVATE_KEY'] = '11'.repeat(32);
|
|
43
|
-
process.env['4MICA_AUTH_REFRESH_MARGIN_SECS'] = '90';
|
|
44
|
-
const cfg = new ConfigBuilder().fromEnv().build();
|
|
45
|
-
expect(cfg.authUrl).toBe('https://example.com');
|
|
46
|
-
expect(cfg.authRefreshMarginSecs).toBe(90);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('reads bearer token', () => {
|
|
50
|
-
process.env['4MICA_RPC_URL'] = 'https://example.com';
|
|
51
|
-
process.env['4MICA_WALLET_PRIVATE_KEY'] = '11'.repeat(32);
|
|
52
|
-
process.env['4MICA_BEARER_TOKEN'] = 'token';
|
|
53
|
-
const cfg = new ConfigBuilder().fromEnv().build();
|
|
54
|
-
expect(cfg.bearerToken).toBe('token');
|
|
55
|
-
});
|
|
56
|
-
});
|
package/tests/guarantee.test.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { decodeGuaranteeClaims, encodeGuaranteeClaims } from '../src/guarantee';
|
|
3
|
-
import { PaymentGuaranteeClaims } from '../src/models';
|
|
4
|
-
|
|
5
|
-
describe('guarantee codec', () => {
|
|
6
|
-
it('round trips guarantee claims', () => {
|
|
7
|
-
const claims: PaymentGuaranteeClaims = {
|
|
8
|
-
domain: new Uint8Array(32),
|
|
9
|
-
userAddress: '0x0000000000000000000000000000000000000001',
|
|
10
|
-
recipientAddress: '0x0000000000000000000000000000000000000002',
|
|
11
|
-
tabId: 1n,
|
|
12
|
-
reqId: 2n,
|
|
13
|
-
amount: 3n,
|
|
14
|
-
totalAmount: 4n,
|
|
15
|
-
assetAddress: '0x0000000000000000000000000000000000000000',
|
|
16
|
-
timestamp: 123456,
|
|
17
|
-
version: 1,
|
|
18
|
-
};
|
|
19
|
-
const encoded = encodeGuaranteeClaims(claims);
|
|
20
|
-
const decoded = decodeGuaranteeClaims(encoded);
|
|
21
|
-
expect(decoded.userAddress).toBe(claims.userAddress);
|
|
22
|
-
expect(decoded.recipientAddress).toBe(claims.recipientAddress);
|
|
23
|
-
expect(decoded.tabId).toBe(claims.tabId);
|
|
24
|
-
expect(decoded.amount).toBe(claims.amount);
|
|
25
|
-
});
|
|
26
|
-
});
|
package/tests/rpc.test.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { RpcProxy } from '../src/rpc';
|
|
3
|
-
import type { FetchFn } from '../src/rpc';
|
|
4
|
-
import { RpcError } from '../src/errors';
|
|
5
|
-
|
|
6
|
-
describe('RpcProxy', () => {
|
|
7
|
-
it('round trips public params', async () => {
|
|
8
|
-
const params = {
|
|
9
|
-
publicKey: [1, 2, 3],
|
|
10
|
-
contractAddress: '0x1234567890abcdef1234567890abcdef12345678',
|
|
11
|
-
ethereumHttpRpcUrl: 'http://localhost:8545',
|
|
12
|
-
eip712Name: '4mica',
|
|
13
|
-
eip712Version: '1',
|
|
14
|
-
chainId: 1337,
|
|
15
|
-
};
|
|
16
|
-
const fetchMock = vi.fn<FetchFn>(async (input) => {
|
|
17
|
-
const url = input.toString();
|
|
18
|
-
expect(url.endsWith('/core/public-params')).toBe(true);
|
|
19
|
-
return new Response(JSON.stringify(params), { status: 200 });
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const proxy = new RpcProxy('http://example.com', undefined, fetchMock);
|
|
23
|
-
const got = await proxy.getPublicParams();
|
|
24
|
-
expect(got.chainId).toBe(1337);
|
|
25
|
-
expect(got.contractAddress).toBe(params.contractAddress);
|
|
26
|
-
expect(got.ethereumHttpRpcUrl).toBe(params.ethereumHttpRpcUrl);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('surfaces api errors', async () => {
|
|
30
|
-
const fetchMock = vi.fn<FetchFn>(async (input) => {
|
|
31
|
-
expect(input.toString()).toContain('settlement_status=unknown');
|
|
32
|
-
return new Response(JSON.stringify({ error: 'invalid settlement status: unknown' }), {
|
|
33
|
-
status: 400,
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
const proxy = new RpcProxy('http://example.com', undefined, fetchMock);
|
|
37
|
-
await expect(proxy.listRecipientTabs('0xdeadbeef', ['unknown'])).rejects.toThrow(RpcError);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('returns decode error on invalid json', async () => {
|
|
41
|
-
const fetchMock = vi.fn<FetchFn>(async () => {
|
|
42
|
-
return new Response('not-json', { status: 200 });
|
|
43
|
-
});
|
|
44
|
-
const proxy = new RpcProxy('http://example.com', undefined, fetchMock);
|
|
45
|
-
await expect(proxy.getPublicParams()).rejects.toThrow(RpcError);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('adds bearer tokens without double prefixing', async () => {
|
|
49
|
-
const params = {
|
|
50
|
-
publicKey: [1, 2, 3],
|
|
51
|
-
contractAddress: '0x1234567890abcdef1234567890abcdef12345678',
|
|
52
|
-
ethereumHttpRpcUrl: 'http://localhost:8545',
|
|
53
|
-
eip712Name: '4mica',
|
|
54
|
-
eip712Version: '1',
|
|
55
|
-
chainId: 1337,
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const fetchRaw = vi.fn<FetchFn>(async (_input, init) => {
|
|
59
|
-
const headers = init?.headers as Record<string, string>;
|
|
60
|
-
expect(headers.Authorization).toBe('Bearer token');
|
|
61
|
-
return new Response(JSON.stringify(params), { status: 200 });
|
|
62
|
-
});
|
|
63
|
-
await new RpcProxy('http://example.com', undefined, fetchRaw)
|
|
64
|
-
.withBearerToken('token')
|
|
65
|
-
.getPublicParams();
|
|
66
|
-
|
|
67
|
-
const fetchPrefixed = vi.fn<FetchFn>(async (_input, init) => {
|
|
68
|
-
const headers = init?.headers as Record<string, string>;
|
|
69
|
-
expect(headers.Authorization).toBe('Bearer token');
|
|
70
|
-
return new Response(JSON.stringify(params), { status: 200 });
|
|
71
|
-
});
|
|
72
|
-
await new RpcProxy('http://example.com', undefined, fetchPrefixed)
|
|
73
|
-
.withBearerToken('Bearer token')
|
|
74
|
-
.getPublicParams();
|
|
75
|
-
});
|
|
76
|
-
});
|
package/tests/signing.test.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { PaymentGuaranteeRequestClaims, SigningScheme } from '../src/models';
|
|
3
|
-
import { CorePublicParameters, createLocalSigner, PaymentSigner } from '../src/signing';
|
|
4
|
-
import { SigningError } from '../src/errors';
|
|
5
|
-
|
|
6
|
-
function buildParams(): CorePublicParameters {
|
|
7
|
-
return new CorePublicParameters(
|
|
8
|
-
new Uint8Array(),
|
|
9
|
-
'0x0000000000000000000000000000000000000000',
|
|
10
|
-
'https://example.com',
|
|
11
|
-
'4Mica',
|
|
12
|
-
'1',
|
|
13
|
-
1
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('PaymentSigner', () => {
|
|
18
|
-
it('rejects address mismatch', async () => {
|
|
19
|
-
const signer = new PaymentSigner(createLocalSigner('11'.repeat(32)));
|
|
20
|
-
const claims = PaymentGuaranteeRequestClaims.new(
|
|
21
|
-
'0x0000000000000000000000000000000000000011',
|
|
22
|
-
'0x0000000000000000000000000000000000000002',
|
|
23
|
-
1,
|
|
24
|
-
5,
|
|
25
|
-
1234,
|
|
26
|
-
null,
|
|
27
|
-
7
|
|
28
|
-
);
|
|
29
|
-
await expect(signer.signRequest(buildParams(), claims, SigningScheme.EIP712)).rejects.toThrow(
|
|
30
|
-
SigningError
|
|
31
|
-
);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('produces eip712 signature', async () => {
|
|
35
|
-
const privateKey = '0x59c6995e998f97a5a0044976f7be35d5ad91c0cfa55b5cfb20b07a1c60f4c5bc';
|
|
36
|
-
const localSigner = createLocalSigner(privateKey);
|
|
37
|
-
const signer = new PaymentSigner(localSigner);
|
|
38
|
-
const accountAddress = localSigner.address.toLowerCase();
|
|
39
|
-
const claims = PaymentGuaranteeRequestClaims.new(
|
|
40
|
-
accountAddress,
|
|
41
|
-
'0x0000000000000000000000000000000000000002',
|
|
42
|
-
42,
|
|
43
|
-
123,
|
|
44
|
-
999,
|
|
45
|
-
null,
|
|
46
|
-
2
|
|
47
|
-
);
|
|
48
|
-
const sig = await signer.signRequest(buildParams(), claims, SigningScheme.EIP712);
|
|
49
|
-
expect(sig.scheme).toBe(SigningScheme.EIP712);
|
|
50
|
-
expect(sig.signature.length).toBe(132);
|
|
51
|
-
});
|
|
52
|
-
});
|
package/tests/utils.test.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
ValidationError,
|
|
4
|
-
normalizeAddress,
|
|
5
|
-
normalizePrivateKey,
|
|
6
|
-
parseU256,
|
|
7
|
-
serializeU256,
|
|
8
|
-
validateUrl,
|
|
9
|
-
} from '../src/utils';
|
|
10
|
-
|
|
11
|
-
describe('utils', () => {
|
|
12
|
-
it('validateUrl rejects bad input', () => {
|
|
13
|
-
expect(() => validateUrl('not-a-url')).toThrow(ValidationError);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('normalizePrivateKey strips prefix and lowercases', () => {
|
|
17
|
-
const key = normalizePrivateKey('0xABCDEF' + '0'.repeat(58));
|
|
18
|
-
expect(key).toBe('0xabcdef' + '0'.repeat(58));
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('parseU256 accepts hex strings and serializes back', () => {
|
|
22
|
-
const value = parseU256('0x10');
|
|
23
|
-
expect(value).toBe(16n);
|
|
24
|
-
expect(serializeU256(value)).toBe('0x10');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('normalizeAddress round trips checksum', () => {
|
|
28
|
-
const addr = '0x0000000000000000000000000000000000000001';
|
|
29
|
-
expect(normalizeAddress(addr)).toBe('0x0000000000000000000000000000000000000001');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('parseU256 rejects negatives', () => {
|
|
33
|
-
expect(() => parseU256(-1)).toThrow(ValidationError);
|
|
34
|
-
});
|
|
35
|
-
});
|