javascript-solid-server 0.0.100 → 0.0.102
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/LICENSE +1 -0
- package/README.md +54 -2
- package/bin/jss.js +10 -0
- package/package.json +2 -1
- package/src/config.js +12 -1
- package/src/handlers/pay.js +297 -0
- package/src/mrc20.js +335 -0
- package/src/server.js +23 -0
- package/src/webledger.js +162 -0
- package/test/helpers.js +1 -2
- package/test/idp.test.js +57 -41
- package/test/{live-reload.test.js → live-reload.standalone.js} +1 -0
- package/test/mrc20.test.js +343 -0
- package/test/pay.test.js +231 -0
- package/test/webledger.test.js +174 -0
package/test/idp.test.js
CHANGED
|
@@ -8,28 +8,42 @@ import { createServer } from '../src/server.js';
|
|
|
8
8
|
import fs from 'fs-extra';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
|
|
11
|
-
const TEST_PORT = 3099;
|
|
12
11
|
const TEST_HOST = 'localhost';
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
import { createServer as createNetServer } from 'net';
|
|
13
|
+
|
|
14
|
+
/** Get an available port by briefly binding to port 0 */
|
|
15
|
+
async function getAvailablePort() {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const srv = createNetServer();
|
|
18
|
+
srv.on('error', (err) => reject(err));
|
|
19
|
+
srv.listen(0, TEST_HOST, () => {
|
|
20
|
+
const port = srv.address().port;
|
|
21
|
+
srv.close(() => resolve(port));
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
15
25
|
|
|
16
26
|
describe('Identity Provider', () => {
|
|
17
27
|
let server;
|
|
28
|
+
let baseUrl;
|
|
29
|
+
const DATA_DIR = './test-data-idp';
|
|
18
30
|
|
|
19
31
|
before(async () => {
|
|
20
|
-
// Clean up any existing test data
|
|
21
32
|
await fs.remove(DATA_DIR);
|
|
22
33
|
await fs.ensureDir(DATA_DIR);
|
|
23
34
|
|
|
24
|
-
|
|
35
|
+
const port = await getAvailablePort();
|
|
36
|
+
baseUrl = `http://${TEST_HOST}:${port}`;
|
|
37
|
+
|
|
25
38
|
server = createServer({
|
|
26
39
|
logger: false,
|
|
27
40
|
root: DATA_DIR,
|
|
28
41
|
idp: true,
|
|
29
|
-
idpIssuer:
|
|
42
|
+
idpIssuer: baseUrl,
|
|
43
|
+
forceCloseConnections: true,
|
|
30
44
|
});
|
|
31
45
|
|
|
32
|
-
await server.listen({ port
|
|
46
|
+
await server.listen({ port, host: TEST_HOST });
|
|
33
47
|
});
|
|
34
48
|
|
|
35
49
|
after(async () => {
|
|
@@ -39,19 +53,19 @@ describe('Identity Provider', () => {
|
|
|
39
53
|
|
|
40
54
|
describe('OIDC Discovery', () => {
|
|
41
55
|
it('should serve /.well-known/openid-configuration', async () => {
|
|
42
|
-
const res = await fetch(`${
|
|
56
|
+
const res = await fetch(`${baseUrl}/.well-known/openid-configuration`);
|
|
43
57
|
assert.strictEqual(res.status, 200);
|
|
44
58
|
|
|
45
59
|
const config = await res.json();
|
|
46
60
|
// Issuer has trailing slash for CTH compatibility
|
|
47
|
-
assert.strictEqual(config.issuer,
|
|
61
|
+
assert.strictEqual(config.issuer, baseUrl + '/');
|
|
48
62
|
assert.ok(config.authorization_endpoint);
|
|
49
63
|
assert.ok(config.token_endpoint);
|
|
50
64
|
assert.ok(config.jwks_uri);
|
|
51
65
|
});
|
|
52
66
|
|
|
53
67
|
it('should include required Solid-OIDC endpoints', async () => {
|
|
54
|
-
const res = await fetch(`${
|
|
68
|
+
const res = await fetch(`${baseUrl}/.well-known/openid-configuration`);
|
|
55
69
|
const config = await res.json();
|
|
56
70
|
|
|
57
71
|
assert.ok(config.registration_endpoint, 'should have registration endpoint');
|
|
@@ -60,7 +74,7 @@ describe('Identity Provider', () => {
|
|
|
60
74
|
});
|
|
61
75
|
|
|
62
76
|
it('should serve /.well-known/jwks.json', async () => {
|
|
63
|
-
const res = await fetch(`${
|
|
77
|
+
const res = await fetch(`${baseUrl}/.well-known/jwks.json`);
|
|
64
78
|
assert.strictEqual(res.status, 200);
|
|
65
79
|
|
|
66
80
|
const jwks = await res.json();
|
|
@@ -73,7 +87,7 @@ describe('Identity Provider', () => {
|
|
|
73
87
|
|
|
74
88
|
describe('Pod Creation with IdP', () => {
|
|
75
89
|
it('should require email when IdP is enabled', async () => {
|
|
76
|
-
const res = await fetch(`${
|
|
90
|
+
const res = await fetch(`${baseUrl}/.pods`, {
|
|
77
91
|
method: 'POST',
|
|
78
92
|
headers: { 'Content-Type': 'application/json' },
|
|
79
93
|
body: JSON.stringify({ name: 'noemail' }),
|
|
@@ -85,7 +99,7 @@ describe('Identity Provider', () => {
|
|
|
85
99
|
});
|
|
86
100
|
|
|
87
101
|
it('should require password when IdP is enabled', async () => {
|
|
88
|
-
const res = await fetch(`${
|
|
102
|
+
const res = await fetch(`${baseUrl}/.pods`, {
|
|
89
103
|
method: 'POST',
|
|
90
104
|
headers: { 'Content-Type': 'application/json' },
|
|
91
105
|
body: JSON.stringify({ name: 'nopass', email: 'test@example.com' }),
|
|
@@ -98,7 +112,7 @@ describe('Identity Provider', () => {
|
|
|
98
112
|
|
|
99
113
|
it('should create pod with account', async () => {
|
|
100
114
|
const uniqueId = Date.now();
|
|
101
|
-
const res = await fetch(`${
|
|
115
|
+
const res = await fetch(`${baseUrl}/.pods`, {
|
|
102
116
|
method: 'POST',
|
|
103
117
|
headers: { 'Content-Type': 'application/json' },
|
|
104
118
|
body: JSON.stringify({
|
|
@@ -125,7 +139,7 @@ describe('Identity Provider', () => {
|
|
|
125
139
|
const duplicateEmail = `duplicate${uniqueId}@example.com`;
|
|
126
140
|
|
|
127
141
|
// First user
|
|
128
|
-
await fetch(`${
|
|
142
|
+
await fetch(`${baseUrl}/.pods`, {
|
|
129
143
|
method: 'POST',
|
|
130
144
|
headers: { 'Content-Type': 'application/json' },
|
|
131
145
|
body: JSON.stringify({
|
|
@@ -136,7 +150,7 @@ describe('Identity Provider', () => {
|
|
|
136
150
|
});
|
|
137
151
|
|
|
138
152
|
// Second user with same email
|
|
139
|
-
const res = await fetch(`${
|
|
153
|
+
const res = await fetch(`${baseUrl}/.pods`, {
|
|
140
154
|
method: 'POST',
|
|
141
155
|
headers: { 'Content-Type': 'application/json' },
|
|
142
156
|
body: JSON.stringify({
|
|
@@ -154,15 +168,10 @@ describe('Identity Provider', () => {
|
|
|
154
168
|
|
|
155
169
|
describe('Login Interaction', () => {
|
|
156
170
|
it('should respond to authorization endpoint', async () => {
|
|
157
|
-
|
|
158
|
-
// Various responses are acceptable - 302/303 (redirect), 400 (bad request), 404 (no route)
|
|
159
|
-
// This just verifies the server handles the request
|
|
160
|
-
const res = await fetch(`${BASE_URL}/idp/auth?client_id=test&redirect_uri=http://localhost&response_type=code&scope=openid`, {
|
|
171
|
+
const res = await fetch(`${baseUrl}/idp/auth?client_id=test&redirect_uri=http://localhost&response_type=code&scope=openid`, {
|
|
161
172
|
redirect: 'manual',
|
|
162
173
|
});
|
|
163
174
|
|
|
164
|
-
// oidc-provider mounted via middie may return different status codes
|
|
165
|
-
// The important thing is it doesn't crash and returns a valid HTTP response
|
|
166
175
|
assert.ok(res.status >= 200 && res.status < 600, `got valid HTTP status ${res.status}`);
|
|
167
176
|
});
|
|
168
177
|
});
|
|
@@ -170,20 +179,25 @@ describe('Identity Provider', () => {
|
|
|
170
179
|
|
|
171
180
|
describe('Identity Provider - Accounts', () => {
|
|
172
181
|
let server;
|
|
182
|
+
let accountsUrl;
|
|
173
183
|
const ACCOUNTS_DATA_DIR = './test-data-idp-accounts';
|
|
174
184
|
|
|
175
185
|
before(async () => {
|
|
176
186
|
await fs.remove(ACCOUNTS_DATA_DIR);
|
|
177
187
|
await fs.ensureDir(ACCOUNTS_DATA_DIR);
|
|
178
188
|
|
|
189
|
+
const port = await getAvailablePort();
|
|
190
|
+
accountsUrl = `http://${TEST_HOST}:${port}`;
|
|
191
|
+
|
|
179
192
|
server = createServer({
|
|
180
193
|
logger: false,
|
|
181
194
|
root: ACCOUNTS_DATA_DIR,
|
|
182
195
|
idp: true,
|
|
183
|
-
idpIssuer:
|
|
196
|
+
idpIssuer: accountsUrl,
|
|
197
|
+
forceCloseConnections: true,
|
|
184
198
|
});
|
|
185
199
|
|
|
186
|
-
await server.listen({ port
|
|
200
|
+
await server.listen({ port, host: TEST_HOST });
|
|
187
201
|
});
|
|
188
202
|
|
|
189
203
|
after(async () => {
|
|
@@ -195,7 +209,7 @@ describe('Identity Provider - Accounts', () => {
|
|
|
195
209
|
const uniqueName = `stored${Date.now()}`;
|
|
196
210
|
const uniqueEmail = `stored${Date.now()}@example.com`;
|
|
197
211
|
|
|
198
|
-
const res = await fetch(
|
|
212
|
+
const res = await fetch(`${accountsUrl}/.pods`, {
|
|
199
213
|
method: 'POST',
|
|
200
214
|
headers: { 'Content-Type': 'application/json' },
|
|
201
215
|
body: JSON.stringify({
|
|
@@ -221,7 +235,7 @@ describe('Identity Provider - Accounts', () => {
|
|
|
221
235
|
const uniqueName = `hashed${Date.now()}`;
|
|
222
236
|
const uniqueEmail = `hashed${Date.now()}@example.com`;
|
|
223
237
|
|
|
224
|
-
const res = await fetch(
|
|
238
|
+
const res = await fetch(`${accountsUrl}/.pods`, {
|
|
225
239
|
method: 'POST',
|
|
226
240
|
headers: { 'Content-Type': 'application/json' },
|
|
227
241
|
body: JSON.stringify({
|
|
@@ -248,24 +262,26 @@ describe('Identity Provider - Accounts', () => {
|
|
|
248
262
|
|
|
249
263
|
describe('Identity Provider - Credentials Endpoint', () => {
|
|
250
264
|
let server;
|
|
251
|
-
|
|
265
|
+
let credsUrl;
|
|
252
266
|
const CREDS_DATA_DIR = './data';
|
|
253
|
-
const CREDS_PORT = 3101;
|
|
254
|
-
const CREDS_URL = `http://${TEST_HOST}:${CREDS_PORT}`;
|
|
255
267
|
|
|
256
268
|
before(async () => {
|
|
257
269
|
await fs.emptyDir(CREDS_DATA_DIR);
|
|
258
270
|
|
|
271
|
+
const port = await getAvailablePort();
|
|
272
|
+
credsUrl = `http://${TEST_HOST}:${port}`;
|
|
273
|
+
|
|
259
274
|
server = createServer({
|
|
260
275
|
logger: false,
|
|
261
276
|
idp: true,
|
|
262
|
-
idpIssuer:
|
|
277
|
+
idpIssuer: credsUrl,
|
|
278
|
+
forceCloseConnections: true,
|
|
263
279
|
});
|
|
264
280
|
|
|
265
|
-
await server.listen({ port
|
|
281
|
+
await server.listen({ port, host: TEST_HOST });
|
|
266
282
|
|
|
267
283
|
// Create a test user
|
|
268
|
-
const res = await fetch(`${
|
|
284
|
+
const res = await fetch(`${credsUrl}/.pods`, {
|
|
269
285
|
method: 'POST',
|
|
270
286
|
headers: { 'Content-Type': 'application/json' },
|
|
271
287
|
body: JSON.stringify({
|
|
@@ -286,7 +302,7 @@ describe('Identity Provider - Credentials Endpoint', () => {
|
|
|
286
302
|
|
|
287
303
|
describe('GET /idp/credentials', () => {
|
|
288
304
|
it('should return endpoint info', async () => {
|
|
289
|
-
const res = await fetch(`${
|
|
305
|
+
const res = await fetch(`${credsUrl}/idp/credentials`);
|
|
290
306
|
assert.strictEqual(res.status, 200);
|
|
291
307
|
|
|
292
308
|
const info = await res.json();
|
|
@@ -299,7 +315,7 @@ describe('Identity Provider - Credentials Endpoint', () => {
|
|
|
299
315
|
|
|
300
316
|
describe('POST /idp/credentials', () => {
|
|
301
317
|
it('should return 400 for missing credentials', async () => {
|
|
302
|
-
const res = await fetch(`${
|
|
318
|
+
const res = await fetch(`${credsUrl}/idp/credentials`, {
|
|
303
319
|
method: 'POST',
|
|
304
320
|
headers: { 'Content-Type': 'application/json' },
|
|
305
321
|
body: JSON.stringify({}),
|
|
@@ -311,7 +327,7 @@ describe('Identity Provider - Credentials Endpoint', () => {
|
|
|
311
327
|
});
|
|
312
328
|
|
|
313
329
|
it('should return 401 for wrong password', async () => {
|
|
314
|
-
const res = await fetch(`${
|
|
330
|
+
const res = await fetch(`${credsUrl}/idp/credentials`, {
|
|
315
331
|
method: 'POST',
|
|
316
332
|
headers: { 'Content-Type': 'application/json' },
|
|
317
333
|
body: JSON.stringify({
|
|
@@ -326,7 +342,7 @@ describe('Identity Provider - Credentials Endpoint', () => {
|
|
|
326
342
|
});
|
|
327
343
|
|
|
328
344
|
it('should return 401 for unknown email', async () => {
|
|
329
|
-
const res = await fetch(`${
|
|
345
|
+
const res = await fetch(`${credsUrl}/idp/credentials`, {
|
|
330
346
|
method: 'POST',
|
|
331
347
|
headers: { 'Content-Type': 'application/json' },
|
|
332
348
|
body: JSON.stringify({
|
|
@@ -339,7 +355,7 @@ describe('Identity Provider - Credentials Endpoint', () => {
|
|
|
339
355
|
});
|
|
340
356
|
|
|
341
357
|
it('should return access token for valid credentials', async () => {
|
|
342
|
-
const res = await fetch(`${
|
|
358
|
+
const res = await fetch(`${credsUrl}/idp/credentials`, {
|
|
343
359
|
method: 'POST',
|
|
344
360
|
headers: { 'Content-Type': 'application/json' },
|
|
345
361
|
body: JSON.stringify({
|
|
@@ -358,7 +374,7 @@ describe('Identity Provider - Credentials Endpoint', () => {
|
|
|
358
374
|
});
|
|
359
375
|
|
|
360
376
|
it('should return JWT token with webid claim', async () => {
|
|
361
|
-
const res = await fetch(`${
|
|
377
|
+
const res = await fetch(`${credsUrl}/idp/credentials`, {
|
|
362
378
|
method: 'POST',
|
|
363
379
|
headers: { 'Content-Type': 'application/json' },
|
|
364
380
|
body: JSON.stringify({
|
|
@@ -382,7 +398,7 @@ describe('Identity Provider - Credentials Endpoint', () => {
|
|
|
382
398
|
});
|
|
383
399
|
|
|
384
400
|
it('should work with form-encoded body', async () => {
|
|
385
|
-
const res = await fetch(`${
|
|
401
|
+
const res = await fetch(`${credsUrl}/idp/credentials`, {
|
|
386
402
|
method: 'POST',
|
|
387
403
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
388
404
|
body: 'email=credtest%40example.com&password=testpassword123',
|
|
@@ -395,7 +411,7 @@ describe('Identity Provider - Credentials Endpoint', () => {
|
|
|
395
411
|
|
|
396
412
|
it('should allow using token to access protected resource', async () => {
|
|
397
413
|
// Get access token
|
|
398
|
-
const tokenRes = await fetch(`${
|
|
414
|
+
const tokenRes = await fetch(`${credsUrl}/idp/credentials`, {
|
|
399
415
|
method: 'POST',
|
|
400
416
|
headers: { 'Content-Type': 'application/json' },
|
|
401
417
|
body: JSON.stringify({
|
|
@@ -407,7 +423,7 @@ describe('Identity Provider - Credentials Endpoint', () => {
|
|
|
407
423
|
const { access_token } = await tokenRes.json();
|
|
408
424
|
|
|
409
425
|
// Try to access private resource
|
|
410
|
-
const res = await fetch(`${
|
|
426
|
+
const res = await fetch(`${credsUrl}/credtest/private/`, {
|
|
411
427
|
headers: { 'Authorization': `Bearer ${access_token}` },
|
|
412
428
|
});
|
|
413
429
|
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MRC20 Token Verification tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import {
|
|
8
|
+
jcs,
|
|
9
|
+
sha256Hex,
|
|
10
|
+
verifyStateLink,
|
|
11
|
+
validateMrc20State,
|
|
12
|
+
extractTransfersTo,
|
|
13
|
+
totalTransferredTo,
|
|
14
|
+
verifyMrc20Deposit,
|
|
15
|
+
btAddress,
|
|
16
|
+
verifyMrc20Anchor
|
|
17
|
+
} from '../src/mrc20.js';
|
|
18
|
+
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
19
|
+
|
|
20
|
+
const PROFILE = 'mono.mrc20.v0.1';
|
|
21
|
+
|
|
22
|
+
// Helper: create a valid state chain pair
|
|
23
|
+
function createStatePair(ops, toAddress) {
|
|
24
|
+
const prevState = {
|
|
25
|
+
profile: PROFILE,
|
|
26
|
+
prev: '0'.repeat(64),
|
|
27
|
+
seq: 0,
|
|
28
|
+
ticker: 'TEST',
|
|
29
|
+
name: 'Test Token',
|
|
30
|
+
decimals: 0,
|
|
31
|
+
supply: 1000,
|
|
32
|
+
balances: { creator: 1000 },
|
|
33
|
+
ops: []
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const state = {
|
|
37
|
+
profile: PROFILE,
|
|
38
|
+
prev: sha256Hex(jcs(prevState)),
|
|
39
|
+
seq: 1,
|
|
40
|
+
ticker: 'TEST',
|
|
41
|
+
name: 'Test Token',
|
|
42
|
+
decimals: 0,
|
|
43
|
+
supply: 1000,
|
|
44
|
+
balances: { creator: 900, [toAddress]: 100 },
|
|
45
|
+
ops: ops || [{ op: 'urn:mono:op:transfer', from: 'creator', to: toAddress, amt: 100 }]
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return { prevState, state };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('MRC20 Verification', () => {
|
|
52
|
+
describe('jcs', () => {
|
|
53
|
+
it('should sort keys alphabetically', () => {
|
|
54
|
+
assert.strictEqual(jcs({ b: 1, a: 2 }), '{"a":2,"b":1}');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should handle nested objects', () => {
|
|
58
|
+
assert.strictEqual(jcs({ z: { b: 1, a: 2 }, a: 3 }), '{"a":3,"z":{"a":2,"b":1}}');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should handle arrays', () => {
|
|
62
|
+
assert.strictEqual(jcs([3, 1, 2]), '[3,1,2]');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should handle null and primitives', () => {
|
|
66
|
+
assert.strictEqual(jcs(null), 'null');
|
|
67
|
+
assert.strictEqual(jcs(42), '42');
|
|
68
|
+
assert.strictEqual(jcs('hello'), '"hello"');
|
|
69
|
+
assert.strictEqual(jcs(true), 'true');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should be deterministic', () => {
|
|
73
|
+
const obj = { name: 'TEST', profile: PROFILE, seq: 0, prev: '0'.repeat(64) };
|
|
74
|
+
assert.strictEqual(jcs(obj), jcs(obj));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('sha256Hex', () => {
|
|
79
|
+
it('should return 64-char hex string', () => {
|
|
80
|
+
const hash = sha256Hex('hello');
|
|
81
|
+
assert.strictEqual(hash.length, 64);
|
|
82
|
+
assert.ok(/^[0-9a-f]{64}$/.test(hash));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should be deterministic', () => {
|
|
86
|
+
assert.strictEqual(sha256Hex('test'), sha256Hex('test'));
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('verifyStateLink', () => {
|
|
91
|
+
it('should verify valid state chain link', () => {
|
|
92
|
+
const { prevState, state } = createStatePair();
|
|
93
|
+
const result = verifyStateLink(state, prevState);
|
|
94
|
+
assert.strictEqual(result.valid, true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should reject invalid prev hash', () => {
|
|
98
|
+
const { prevState, state } = createStatePair();
|
|
99
|
+
state.prev = 'bad' + state.prev.slice(3);
|
|
100
|
+
const result = verifyStateLink(state, prevState);
|
|
101
|
+
assert.strictEqual(result.valid, false);
|
|
102
|
+
assert.ok(result.error.includes('State chain break'));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should reject wrong sequence number', () => {
|
|
106
|
+
const { prevState, state } = createStatePair();
|
|
107
|
+
state.seq = 5; // should be 1
|
|
108
|
+
const result = verifyStateLink(state, prevState);
|
|
109
|
+
assert.strictEqual(result.valid, false);
|
|
110
|
+
assert.ok(result.error.includes('Sequence mismatch'));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should reject missing states', () => {
|
|
114
|
+
assert.strictEqual(verifyStateLink(null, {}).valid, false);
|
|
115
|
+
assert.strictEqual(verifyStateLink({}, null).valid, false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('validateMrc20State', () => {
|
|
120
|
+
it('should accept valid MRC20 state', () => {
|
|
121
|
+
const { state } = createStatePair();
|
|
122
|
+
assert.strictEqual(validateMrc20State(state).valid, true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should reject wrong profile', () => {
|
|
126
|
+
const { state } = createStatePair();
|
|
127
|
+
state.profile = 'wrong.profile';
|
|
128
|
+
assert.strictEqual(validateMrc20State(state).valid, false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should reject missing ops', () => {
|
|
132
|
+
const { state } = createStatePair();
|
|
133
|
+
delete state.ops;
|
|
134
|
+
assert.strictEqual(validateMrc20State(state).valid, false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should reject non-object', () => {
|
|
138
|
+
assert.strictEqual(validateMrc20State(null).valid, false);
|
|
139
|
+
assert.strictEqual(validateMrc20State('string').valid, false);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('extractTransfersTo', () => {
|
|
144
|
+
it('should extract transfers to specific address', () => {
|
|
145
|
+
const { state } = createStatePair(
|
|
146
|
+
[
|
|
147
|
+
{ op: 'urn:mono:op:transfer', from: 'alice', to: 'pod', amt: 50 },
|
|
148
|
+
{ op: 'urn:mono:op:transfer', from: 'bob', to: 'other', amt: 30 },
|
|
149
|
+
{ op: 'urn:mono:op:transfer', from: 'carol', to: 'pod', amt: 25 }
|
|
150
|
+
],
|
|
151
|
+
'pod'
|
|
152
|
+
);
|
|
153
|
+
const transfers = extractTransfersTo(state, 'pod');
|
|
154
|
+
assert.strictEqual(transfers.length, 2);
|
|
155
|
+
assert.strictEqual(transfers[0].amt, 50);
|
|
156
|
+
assert.strictEqual(transfers[1].amt, 25);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should return empty for no matching transfers', () => {
|
|
160
|
+
const { state } = createStatePair();
|
|
161
|
+
const transfers = extractTransfersTo(state, 'nobody');
|
|
162
|
+
assert.strictEqual(transfers.length, 0);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should ignore non-transfer ops', () => {
|
|
166
|
+
const { state } = createStatePair(
|
|
167
|
+
[{ op: 'urn:mono:op:mint', to: 'pod', amt: 100 }],
|
|
168
|
+
'pod'
|
|
169
|
+
);
|
|
170
|
+
const transfers = extractTransfersTo(state, 'pod');
|
|
171
|
+
assert.strictEqual(transfers.length, 0);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('totalTransferredTo', () => {
|
|
176
|
+
it('should sum all transfers to address', () => {
|
|
177
|
+
const { state } = createStatePair(
|
|
178
|
+
[
|
|
179
|
+
{ op: 'urn:mono:op:transfer', from: 'a', to: 'pod', amt: 50 },
|
|
180
|
+
{ op: 'urn:mono:op:transfer', from: 'b', to: 'pod', amt: 25 }
|
|
181
|
+
],
|
|
182
|
+
'pod'
|
|
183
|
+
);
|
|
184
|
+
assert.strictEqual(totalTransferredTo(state, 'pod'), 75);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('verifyMrc20Deposit', () => {
|
|
189
|
+
it('should verify valid deposit', () => {
|
|
190
|
+
const { prevState, state } = createStatePair(
|
|
191
|
+
[{ op: 'urn:mono:op:transfer', from: 'user', to: 'mypod', amt: 200 }],
|
|
192
|
+
'mypod'
|
|
193
|
+
);
|
|
194
|
+
const result = verifyMrc20Deposit({ state, prevState, toAddress: 'mypod' });
|
|
195
|
+
assert.strictEqual(result.valid, true);
|
|
196
|
+
assert.strictEqual(result.amount, 200);
|
|
197
|
+
assert.strictEqual(result.ticker, 'TEST');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should reject broken state chain', () => {
|
|
201
|
+
const { prevState, state } = createStatePair(
|
|
202
|
+
[{ op: 'urn:mono:op:transfer', from: 'user', to: 'pod', amt: 100 }],
|
|
203
|
+
'pod'
|
|
204
|
+
);
|
|
205
|
+
state.prev = 'tampered';
|
|
206
|
+
const result = verifyMrc20Deposit({ state, prevState, toAddress: 'pod' });
|
|
207
|
+
assert.strictEqual(result.valid, false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should reject deposit to wrong address', () => {
|
|
211
|
+
const { prevState, state } = createStatePair(
|
|
212
|
+
[{ op: 'urn:mono:op:transfer', from: 'user', to: 'other-pod', amt: 100 }],
|
|
213
|
+
'other-pod'
|
|
214
|
+
);
|
|
215
|
+
const result = verifyMrc20Deposit({ state, prevState, toAddress: 'my-pod' });
|
|
216
|
+
assert.strictEqual(result.valid, false);
|
|
217
|
+
assert.ok(result.error.includes('No transfers'));
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should reject invalid MRC20 profile', () => {
|
|
221
|
+
const { prevState, state } = createStatePair();
|
|
222
|
+
state.profile = 'wrong';
|
|
223
|
+
const result = verifyMrc20Deposit({ state, prevState, toAddress: 'pod' });
|
|
224
|
+
assert.strictEqual(result.valid, false);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should sum multiple transfers in same state', () => {
|
|
228
|
+
const { prevState, state } = createStatePair(
|
|
229
|
+
[
|
|
230
|
+
{ op: 'urn:mono:op:transfer', from: 'a', to: 'pod', amt: 100 },
|
|
231
|
+
{ op: 'urn:mono:op:transfer', from: 'b', to: 'pod', amt: 50 }
|
|
232
|
+
],
|
|
233
|
+
'pod'
|
|
234
|
+
);
|
|
235
|
+
const result = verifyMrc20Deposit({ state, prevState, toAddress: 'pod' });
|
|
236
|
+
assert.strictEqual(result.valid, true);
|
|
237
|
+
assert.strictEqual(result.amount, 150);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('btAddress', () => {
|
|
242
|
+
// Use a known keypair for deterministic tests
|
|
243
|
+
const testPriv = Buffer.alloc(32, 1); // 0x0101...01
|
|
244
|
+
const testPub = Buffer.from(secp256k1.getPublicKey(testPriv, true)).toString('hex');
|
|
245
|
+
|
|
246
|
+
it('should derive a valid testnet bech32m address', () => {
|
|
247
|
+
const addr = btAddress(testPub, ['state0'], 'testnet4');
|
|
248
|
+
assert.ok(addr.startsWith('tb1p'), `Expected tb1p prefix, got ${addr}`);
|
|
249
|
+
assert.ok(addr.length >= 62, `Address too short: ${addr.length}`);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should derive a valid mainnet bech32m address', () => {
|
|
253
|
+
const addr = btAddress(testPub, ['state0'], 'mainnet');
|
|
254
|
+
assert.ok(addr.startsWith('bc1p'), `Expected bc1p prefix, got ${addr}`);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should be deterministic', () => {
|
|
258
|
+
const a1 = btAddress(testPub, ['s1', 's2']);
|
|
259
|
+
const a2 = btAddress(testPub, ['s1', 's2']);
|
|
260
|
+
assert.strictEqual(a1, a2);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should produce different addresses for different states', () => {
|
|
264
|
+
const a1 = btAddress(testPub, ['state-a']);
|
|
265
|
+
const a2 = btAddress(testPub, ['state-b']);
|
|
266
|
+
assert.notStrictEqual(a1, a2);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should produce different addresses for different pubkeys', () => {
|
|
270
|
+
const priv2 = Buffer.alloc(32, 2);
|
|
271
|
+
const pub2 = Buffer.from(secp256k1.getPublicKey(priv2, true)).toString('hex');
|
|
272
|
+
const a1 = btAddress(testPub, ['state']);
|
|
273
|
+
const a2 = btAddress(pub2, ['state']);
|
|
274
|
+
assert.notStrictEqual(a1, a2);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should chain multiple states', () => {
|
|
278
|
+
const a1 = btAddress(testPub, ['s1']);
|
|
279
|
+
const a2 = btAddress(testPub, ['s1', 's2']);
|
|
280
|
+
assert.notStrictEqual(a1, a2);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('verifyMrc20Anchor', () => {
|
|
285
|
+
const testPriv = Buffer.alloc(32, 1);
|
|
286
|
+
const testPub = Buffer.from(secp256k1.getPublicKey(testPriv, true)).toString('hex');
|
|
287
|
+
|
|
288
|
+
it('should reject missing stateStrings', async () => {
|
|
289
|
+
const { prevState, state } = createStatePair(
|
|
290
|
+
[{ op: 'urn:mono:op:transfer', from: 'user', to: 'pod', amt: 100 }],
|
|
291
|
+
'pod'
|
|
292
|
+
);
|
|
293
|
+
const result = await verifyMrc20Anchor({
|
|
294
|
+
state, prevState, toAddress: 'pod',
|
|
295
|
+
pubkey: testPub, stateStrings: []
|
|
296
|
+
});
|
|
297
|
+
assert.strictEqual(result.valid, false);
|
|
298
|
+
assert.ok(result.error.includes('stateStrings'));
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should reject bad pubkey', async () => {
|
|
302
|
+
const { prevState, state } = createStatePair(
|
|
303
|
+
[{ op: 'urn:mono:op:transfer', from: 'user', to: 'pod', amt: 100 }],
|
|
304
|
+
'pod'
|
|
305
|
+
);
|
|
306
|
+
const result = await verifyMrc20Anchor({
|
|
307
|
+
state, prevState, toAddress: 'pod',
|
|
308
|
+
pubkey: 'short', stateStrings: [jcs(state)]
|
|
309
|
+
});
|
|
310
|
+
assert.strictEqual(result.valid, false);
|
|
311
|
+
assert.ok(result.error.includes('pubkey'));
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should reject mismatched last stateString', async () => {
|
|
315
|
+
const { prevState, state } = createStatePair(
|
|
316
|
+
[{ op: 'urn:mono:op:transfer', from: 'user', to: 'pod', amt: 100 }],
|
|
317
|
+
'pod'
|
|
318
|
+
);
|
|
319
|
+
const result = await verifyMrc20Anchor({
|
|
320
|
+
state, prevState, toAddress: 'pod',
|
|
321
|
+
pubkey: testPub, stateStrings: ['wrong-jcs']
|
|
322
|
+
});
|
|
323
|
+
assert.strictEqual(result.valid, false);
|
|
324
|
+
assert.ok(result.error.includes('Last stateString'));
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should reject when no UTXO exists (mempool returns empty)', async () => {
|
|
328
|
+
const { prevState, state } = createStatePair(
|
|
329
|
+
[{ op: 'urn:mono:op:transfer', from: 'user', to: 'pod', amt: 100 }],
|
|
330
|
+
'pod'
|
|
331
|
+
);
|
|
332
|
+
// Use a fake mempool URL that will fail
|
|
333
|
+
const result = await verifyMrc20Anchor({
|
|
334
|
+
state, prevState, toAddress: 'pod',
|
|
335
|
+
pubkey: testPub,
|
|
336
|
+
stateStrings: [jcs(prevState), jcs(state)],
|
|
337
|
+
mempoolUrl: 'http://127.0.0.1:1' // will fail to connect
|
|
338
|
+
});
|
|
339
|
+
assert.strictEqual(result.valid, false);
|
|
340
|
+
assert.ok(result.error.includes('Mempool') || result.error.includes('failed'));
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|