javascript-solid-server 0.0.101 → 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/README.md +1 -1
- package/package.json +1 -1
- package/src/handlers/pay.js +58 -9
- package/src/mrc20.js +189 -0
- package/test/mrc20.test.js +107 -1
package/README.md
CHANGED
package/package.json
CHANGED
package/src/handlers/pay.js
CHANGED
|
@@ -21,10 +21,35 @@
|
|
|
21
21
|
|
|
22
22
|
import { getNostrPubkey, pubkeyToDidNostr } from '../auth/nostr.js';
|
|
23
23
|
import { readLedger, writeLedger, getBalance, credit, debit } from '../webledger.js';
|
|
24
|
-
import { verifyMrc20Deposit } from '../mrc20.js';
|
|
24
|
+
import { verifyMrc20Deposit, verifyMrc20Anchor, jcs } from '../mrc20.js';
|
|
25
|
+
import fs from 'fs-extra';
|
|
26
|
+
import path from 'path';
|
|
25
27
|
|
|
26
28
|
const DEFAULT_COST = 1; // satoshis per request
|
|
27
29
|
|
|
30
|
+
// --- Replay protection ---
|
|
31
|
+
const replayFile = () => path.join(process.env.DATA_ROOT || './data', '.well-known/webledgers/replay.json');
|
|
32
|
+
|
|
33
|
+
async function loadReplaySet() {
|
|
34
|
+
try {
|
|
35
|
+
const data = await fs.readFile(replayFile(), 'utf8');
|
|
36
|
+
return new Set(JSON.parse(data));
|
|
37
|
+
} catch { return new Set(); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function saveReplaySet(set) {
|
|
41
|
+
await fs.ensureDir(path.dirname(replayFile()));
|
|
42
|
+
await fs.writeFile(replayFile(), JSON.stringify([...set]));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function checkAndRecordState(stateHash) {
|
|
46
|
+
const seen = await loadReplaySet();
|
|
47
|
+
if (seen.has(stateHash)) return false; // replay!
|
|
48
|
+
seen.add(stateHash);
|
|
49
|
+
await saveReplaySet(seen);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
28
53
|
// --- Deposit verification via mempool API ---
|
|
29
54
|
|
|
30
55
|
async function verifySatsDeposit(txoUri, mempoolUrl) {
|
|
@@ -88,11 +113,11 @@ function parseDepositBody(body) {
|
|
|
88
113
|
function classifyDepositObject(obj) {
|
|
89
114
|
// Explicit type field
|
|
90
115
|
if (obj.type === 'mrc20' && obj.state && obj.prevState) {
|
|
91
|
-
return { type: 'mrc20', state: obj.state, prevState: obj.prevState };
|
|
116
|
+
return { type: 'mrc20', state: obj.state, prevState: obj.prevState, anchor: obj.anchor };
|
|
92
117
|
}
|
|
93
118
|
// Auto-detect: if it has state + prevState with MRC20 profile
|
|
94
119
|
if (obj.state?.profile === 'mono.mrc20.v0.1' && obj.prevState) {
|
|
95
|
-
return { type: 'mrc20', state: obj.state, prevState: obj.prevState };
|
|
120
|
+
return { type: 'mrc20', state: obj.state, prevState: obj.prevState, anchor: obj.anchor };
|
|
96
121
|
}
|
|
97
122
|
// Fall back to TXO URI in .txo field
|
|
98
123
|
if (obj.txo) {
|
|
@@ -160,11 +185,34 @@ export function createPayHandler(options = {}) {
|
|
|
160
185
|
});
|
|
161
186
|
}
|
|
162
187
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
188
|
+
// Replay protection: reject duplicate state hashes
|
|
189
|
+
const stateHash = jcs(deposit.state);
|
|
190
|
+
const isNew = await checkAndRecordState(stateHash);
|
|
191
|
+
if (!isNew) {
|
|
192
|
+
return reply.code(400).send({ error: 'Replay: this state has already been used for a deposit' });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let result;
|
|
196
|
+
|
|
197
|
+
// Anchor verification (if anchor data provided)
|
|
198
|
+
if (deposit.anchor && deposit.anchor.pubkey && deposit.anchor.stateStrings) {
|
|
199
|
+
result = await verifyMrc20Anchor({
|
|
200
|
+
state: deposit.state,
|
|
201
|
+
prevState: deposit.prevState,
|
|
202
|
+
toAddress: payAddress,
|
|
203
|
+
pubkey: deposit.anchor.pubkey,
|
|
204
|
+
stateStrings: deposit.anchor.stateStrings,
|
|
205
|
+
mempoolUrl,
|
|
206
|
+
network: deposit.anchor.network || 'testnet4'
|
|
207
|
+
});
|
|
208
|
+
} else {
|
|
209
|
+
// Fallback: verify chain integrity only (no anchor check)
|
|
210
|
+
result = verifyMrc20Deposit({
|
|
211
|
+
state: deposit.state,
|
|
212
|
+
prevState: deposit.prevState,
|
|
213
|
+
toAddress: payAddress
|
|
214
|
+
});
|
|
215
|
+
}
|
|
168
216
|
|
|
169
217
|
if (!result.valid) {
|
|
170
218
|
return reply.code(400).send({ error: result.error });
|
|
@@ -180,7 +228,8 @@ export function createPayHandler(options = {}) {
|
|
|
180
228
|
deposited: result.amount,
|
|
181
229
|
ticker: result.ticker,
|
|
182
230
|
balance: newBalance,
|
|
183
|
-
unit: 'token'
|
|
231
|
+
unit: 'token',
|
|
232
|
+
...(result.address ? { anchor: result.address } : {})
|
|
184
233
|
});
|
|
185
234
|
}
|
|
186
235
|
|
package/src/mrc20.js
CHANGED
|
@@ -13,10 +13,16 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import crypto from 'crypto';
|
|
16
|
+
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
16
17
|
|
|
17
18
|
const MRC20_PROFILE = 'mono.mrc20.v0.1';
|
|
18
19
|
const TRANSFER_OP = 'urn:mono:op:transfer';
|
|
19
20
|
|
|
21
|
+
// --- BIP-341 key chaining constants ---
|
|
22
|
+
const SECP_N = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141');
|
|
23
|
+
const BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
|
24
|
+
const BECH32M = 0x2bc830a3;
|
|
25
|
+
|
|
20
26
|
/**
|
|
21
27
|
* JSON Canonicalization Scheme (RFC 8785)
|
|
22
28
|
* Produces deterministic JSON — sorted keys, no whitespace.
|
|
@@ -144,3 +150,186 @@ export function verifyMrc20Deposit(params) {
|
|
|
144
150
|
ticker: state.ticker || 'UNKNOWN'
|
|
145
151
|
};
|
|
146
152
|
}
|
|
153
|
+
|
|
154
|
+
// --- BIP-341 key chaining (blocktrails anchor verification) ---
|
|
155
|
+
|
|
156
|
+
function sha256Bytes(data) {
|
|
157
|
+
const buf = data instanceof Uint8Array ? Buffer.from(data) : Buffer.from(data, 'utf8');
|
|
158
|
+
return new Uint8Array(crypto.createHash('sha256').update(buf).digest());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function concatBytes(...arrays) {
|
|
162
|
+
const total = arrays.reduce((s, a) => s + a.length, 0);
|
|
163
|
+
const result = new Uint8Array(total);
|
|
164
|
+
let off = 0;
|
|
165
|
+
for (const a of arrays) { result.set(a, off); off += a.length; }
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function bytesToBigInt(bytes) {
|
|
170
|
+
let r = 0n;
|
|
171
|
+
for (const b of bytes) r = (r << 8n) | BigInt(b);
|
|
172
|
+
return r;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function hexToU8(hex) {
|
|
176
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
177
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
178
|
+
return bytes;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function bytesToHex(bytes) {
|
|
182
|
+
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function taggedHash(tag, ...msgs) {
|
|
186
|
+
const tagHash = sha256Bytes(Buffer.from(tag, 'utf8'));
|
|
187
|
+
return sha256Bytes(concatBytes(tagHash, tagHash, ...msgs));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function btScalar(pubkeyCompressed, state) {
|
|
191
|
+
const xOnly = pubkeyCompressed.slice(1);
|
|
192
|
+
const stateBytes = typeof state === 'string' ? Buffer.from(state, 'utf8') : state;
|
|
193
|
+
const sh = sha256Bytes(stateBytes);
|
|
194
|
+
return bytesToBigInt(taggedHash('TapTweak', xOnly, sh)) % SECP_N;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function btDeriveChainedPubkey(pubkeyBase, states) {
|
|
198
|
+
let P = secp256k1.ProjectivePoint.fromHex(bytesToHex(pubkeyBase));
|
|
199
|
+
let cur = pubkeyBase;
|
|
200
|
+
for (const s of states) {
|
|
201
|
+
const t = btScalar(cur, s);
|
|
202
|
+
P = P.add(secp256k1.ProjectivePoint.BASE.multiply(t));
|
|
203
|
+
cur = new Uint8Array(P.toRawBytes(true));
|
|
204
|
+
}
|
|
205
|
+
return cur;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- Bech32m encoding ---
|
|
209
|
+
|
|
210
|
+
function convertBits(data, from, to, pad) {
|
|
211
|
+
let acc = 0, bits = 0;
|
|
212
|
+
const ret = [], maxv = (1 << to) - 1;
|
|
213
|
+
for (const v of data) {
|
|
214
|
+
acc = (acc << from) | v;
|
|
215
|
+
bits += from;
|
|
216
|
+
while (bits >= to) { bits -= to; ret.push((acc >> bits) & maxv); }
|
|
217
|
+
}
|
|
218
|
+
if (pad && bits > 0) ret.push((acc << (to - bits)) & maxv);
|
|
219
|
+
return ret;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function hrpExpand(hrp) {
|
|
223
|
+
const r = [];
|
|
224
|
+
for (const c of hrp) r.push(c.charCodeAt(0) >> 5);
|
|
225
|
+
r.push(0);
|
|
226
|
+
for (const c of hrp) r.push(c.charCodeAt(0) & 31);
|
|
227
|
+
return r;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function polymod(values) {
|
|
231
|
+
const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
232
|
+
let chk = 1;
|
|
233
|
+
for (const v of values) {
|
|
234
|
+
const b = chk >> 25;
|
|
235
|
+
chk = ((chk & 0x1ffffff) << 5) ^ v;
|
|
236
|
+
for (let i = 0; i < 5; i++) if ((b >> i) & 1) chk ^= GEN[i];
|
|
237
|
+
}
|
|
238
|
+
return chk;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function bech32mEncode(hrp, version, program) {
|
|
242
|
+
const conv = convertBits(program, 8, 5, true);
|
|
243
|
+
const values = [version, ...conv];
|
|
244
|
+
const enc = [...hrpExpand(hrp), ...values, 0, 0, 0, 0, 0, 0];
|
|
245
|
+
const mod = polymod(enc) ^ BECH32M;
|
|
246
|
+
const checksum = [0, 1, 2, 3, 4, 5].map(i => (mod >> (5 * (5 - i))) & 31);
|
|
247
|
+
let result = hrp + '1';
|
|
248
|
+
for (const v of [...values, ...checksum]) result += BECH32_CHARSET[v];
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Derive the taproot address for a state chain
|
|
254
|
+
* @param {string} pubkeyHex - Issuer's compressed pubkey (66-char hex)
|
|
255
|
+
* @param {string[]} states - JCS-encoded state strings
|
|
256
|
+
* @param {string} [network='testnet4'] - 'testnet4' or 'mainnet'
|
|
257
|
+
* @returns {string} Bech32m taproot address
|
|
258
|
+
*/
|
|
259
|
+
export function btAddress(pubkeyHex, states, network = 'testnet4') {
|
|
260
|
+
const pubkeyBase = hexToU8(pubkeyHex);
|
|
261
|
+
const P = btDeriveChainedPubkey(pubkeyBase, states);
|
|
262
|
+
const xOnly = P.slice(1);
|
|
263
|
+
const hrp = network === 'mainnet' ? 'bc' : 'tb';
|
|
264
|
+
return bech32mEncode(hrp, 1, xOnly);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Verify that an MRC20 state is anchored to a Bitcoin UTXO
|
|
269
|
+
* @param {object} params
|
|
270
|
+
* @param {object} params.state - Current state with transfer ops
|
|
271
|
+
* @param {object} params.prevState - Previous state (chain verification)
|
|
272
|
+
* @param {string} params.toAddress - Pod's address for transfer verification
|
|
273
|
+
* @param {string} params.pubkey - Issuer's compressed pubkey (66-char hex)
|
|
274
|
+
* @param {string[]} params.stateStrings - All JCS state strings (genesis to current)
|
|
275
|
+
* @param {string} [params.mempoolUrl] - Mempool API base URL
|
|
276
|
+
* @param {string} [params.network] - 'testnet4' or 'mainnet'
|
|
277
|
+
* @returns {Promise<{valid: boolean, amount?: number, ticker?: string, address?: string, error?: string}>}
|
|
278
|
+
*/
|
|
279
|
+
export async function verifyMrc20Anchor(params) {
|
|
280
|
+
const {
|
|
281
|
+
state, prevState, toAddress, pubkey, stateStrings,
|
|
282
|
+
mempoolUrl = 'https://mempool.space/testnet4',
|
|
283
|
+
network = 'testnet4'
|
|
284
|
+
} = params;
|
|
285
|
+
|
|
286
|
+
// 1. Standard deposit verification (chain integrity + transfers)
|
|
287
|
+
const depositCheck = verifyMrc20Deposit({ state, prevState, toAddress });
|
|
288
|
+
if (!depositCheck.valid) return depositCheck;
|
|
289
|
+
|
|
290
|
+
// 2. Verify stateStrings is provided and non-empty
|
|
291
|
+
if (!Array.isArray(stateStrings) || stateStrings.length === 0) {
|
|
292
|
+
return { valid: false, amount: 0, error: 'stateStrings required for anchor verification' };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 3. Verify pubkey is provided
|
|
296
|
+
if (!pubkey || typeof pubkey !== 'string' || pubkey.length !== 66) {
|
|
297
|
+
return { valid: false, amount: 0, error: 'pubkey must be a 66-char compressed pubkey hex' };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 4. Verify the last stateString matches the current state
|
|
301
|
+
const currentJcs = jcs(state);
|
|
302
|
+
const lastStateString = stateStrings[stateStrings.length - 1];
|
|
303
|
+
if (currentJcs !== lastStateString) {
|
|
304
|
+
return { valid: false, amount: 0, error: 'Last stateString does not match JCS(state)' };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 5. Derive expected taproot address
|
|
308
|
+
let address;
|
|
309
|
+
try {
|
|
310
|
+
address = btAddress(pubkey, stateStrings, network);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
return { valid: false, amount: 0, error: `Key derivation failed: ${err.message}` };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 6. Query mempool for UTXO at derived address
|
|
316
|
+
try {
|
|
317
|
+
const resp = await fetch(`${mempoolUrl}/api/address/${address}/utxo`);
|
|
318
|
+
if (!resp.ok) {
|
|
319
|
+
return { valid: false, amount: 0, error: `Mempool API error: ${resp.status}` };
|
|
320
|
+
}
|
|
321
|
+
const utxos = await resp.json();
|
|
322
|
+
if (!Array.isArray(utxos) || utxos.length === 0) {
|
|
323
|
+
return { valid: false, amount: 0, error: `No UTXO at derived address ${address}` };
|
|
324
|
+
}
|
|
325
|
+
} catch (err) {
|
|
326
|
+
return { valid: false, amount: 0, error: `Mempool lookup failed: ${err.message}` };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
valid: true,
|
|
331
|
+
amount: depositCheck.amount,
|
|
332
|
+
ticker: depositCheck.ticker,
|
|
333
|
+
address
|
|
334
|
+
};
|
|
335
|
+
}
|
package/test/mrc20.test.js
CHANGED
|
@@ -11,8 +11,11 @@ import {
|
|
|
11
11
|
validateMrc20State,
|
|
12
12
|
extractTransfersTo,
|
|
13
13
|
totalTransferredTo,
|
|
14
|
-
verifyMrc20Deposit
|
|
14
|
+
verifyMrc20Deposit,
|
|
15
|
+
btAddress,
|
|
16
|
+
verifyMrc20Anchor
|
|
15
17
|
} from '../src/mrc20.js';
|
|
18
|
+
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
16
19
|
|
|
17
20
|
const PROFILE = 'mono.mrc20.v0.1';
|
|
18
21
|
|
|
@@ -234,4 +237,107 @@ describe('MRC20 Verification', () => {
|
|
|
234
237
|
assert.strictEqual(result.amount, 150);
|
|
235
238
|
});
|
|
236
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
|
+
});
|
|
237
343
|
});
|