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/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
- const BASE_URL = `http://${TEST_HOST}:${TEST_PORT}`;
14
- const DATA_DIR = './test-data-idp';
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
- // Create server with IdP enabled
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: BASE_URL,
42
+ idpIssuer: baseUrl,
43
+ forceCloseConnections: true,
30
44
  });
31
45
 
32
- await server.listen({ port: TEST_PORT, host: TEST_HOST });
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(`${BASE_URL}/.well-known/openid-configuration`);
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, BASE_URL + '/');
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(`${BASE_URL}/.well-known/openid-configuration`);
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(`${BASE_URL}/.well-known/jwks.json`);
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(`${BASE_URL}/.pods`, {
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(`${BASE_URL}/.pods`, {
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(`${BASE_URL}/.pods`, {
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(`${BASE_URL}/.pods`, {
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(`${BASE_URL}/.pods`, {
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
- // Start an authorization flow
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: `http://${TEST_HOST}:${TEST_PORT + 1}`,
196
+ idpIssuer: accountsUrl,
197
+ forceCloseConnections: true,
184
198
  });
185
199
 
186
- await server.listen({ port: TEST_PORT + 1, host: TEST_HOST });
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(`http://${TEST_HOST}:${TEST_PORT + 1}/.pods`, {
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(`http://${TEST_HOST}:${TEST_PORT + 1}/.pods`, {
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
- // Use same data dir as other tests (DATA_ROOT is cached at module load)
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: CREDS_URL,
277
+ idpIssuer: credsUrl,
278
+ forceCloseConnections: true,
263
279
  });
264
280
 
265
- await server.listen({ port: CREDS_PORT, host: TEST_HOST });
281
+ await server.listen({ port, host: TEST_HOST });
266
282
 
267
283
  // Create a test user
268
- const res = await fetch(`${CREDS_URL}/.pods`, {
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(`${CREDS_URL}/idp/credentials`);
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(`${CREDS_URL}/idp/credentials`, {
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(`${CREDS_URL}/idp/credentials`, {
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(`${CREDS_URL}/idp/credentials`, {
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(`${CREDS_URL}/idp/credentials`, {
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(`${CREDS_URL}/idp/credentials`, {
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(`${CREDS_URL}/idp/credentials`, {
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(`${CREDS_URL}/idp/credentials`, {
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(`${CREDS_URL}/credtest/private/`, {
426
+ const res = await fetch(`${credsUrl}/credtest/private/`, {
411
427
  headers: { 'Authorization': `Bearer ${access_token}` },
412
428
  });
413
429
 
@@ -255,6 +255,7 @@ async function runTests() {
255
255
  console.log('Test 2 result: PASSED');
256
256
 
257
257
  console.log('\n=== All tests passed ===');
258
+ process.exit(0);
258
259
  } catch (err) {
259
260
  console.error('\n=== Test FAILED ===');
260
261
  console.error(err.message);
@@ -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
+ });