ecash-agora 0.1.1-rc

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.
@@ -0,0 +1,586 @@
1
+ // Copyright (c) 2024 The Bitcoin developers
2
+ // Distributed under the MIT software license, see the accompanying
3
+ // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
+
5
+ import { expect, use } from 'chai';
6
+ import chaiAsPromised from 'chai-as-promised';
7
+ import { ChronikClient } from 'chronik-client';
8
+ import {
9
+ ALL_BIP143,
10
+ ALP_STANDARD,
11
+ DEFAULT_DUST_LIMIT,
12
+ Ecc,
13
+ P2PKHSignatory,
14
+ Script,
15
+ TxBuilderInput,
16
+ alpSend,
17
+ emppScript,
18
+ fromHex,
19
+ initWasm,
20
+ shaRmd160,
21
+ toHex,
22
+ } from 'ecash-lib';
23
+ import { TestRunner } from 'ecash-lib/dist/test/testRunner.js';
24
+
25
+ import { AgoraPartial } from '../src/partial.js';
26
+ import { makeAlpOffer, takeAlpOffer } from './partial-helper-alp.js';
27
+ import { Agora } from '../src/agora.js';
28
+
29
+ use(chaiAsPromised);
30
+
31
+ // This test needs a lot of sats
32
+ const NUM_COINS = 500;
33
+ const COIN_VALUE = 1100000000;
34
+
35
+ const BASE_PARAMS_ALP = {
36
+ tokenId: '00'.repeat(32), // filled in later
37
+ tokenType: ALP_STANDARD,
38
+ tokenProtocol: 'ALP' as const,
39
+ dustAmount: DEFAULT_DUST_LIMIT,
40
+ };
41
+
42
+ describe('AgoraPartial ALP', () => {
43
+ let runner: TestRunner;
44
+ let chronik: ChronikClient;
45
+ let ecc: Ecc;
46
+
47
+ let makerSk: Uint8Array;
48
+ let makerPk: Uint8Array;
49
+ let makerPkh: Uint8Array;
50
+ let makerScript: Script;
51
+ let makerScriptHex: string;
52
+ let takerSk: Uint8Array;
53
+ let takerPk: Uint8Array;
54
+ let takerPkh: Uint8Array;
55
+ let takerScript: Script;
56
+ let takerScriptHex: string;
57
+
58
+ async function makeBuilderInputs(
59
+ values: number[],
60
+ ): Promise<TxBuilderInput[]> {
61
+ const txid = await runner.sendToScript(values, makerScript);
62
+ return values.map((value, outIdx) => ({
63
+ input: {
64
+ prevOut: {
65
+ txid,
66
+ outIdx,
67
+ },
68
+ signData: {
69
+ value,
70
+ outputScript: makerScript,
71
+ },
72
+ },
73
+ signatory: P2PKHSignatory(makerSk, makerPk, ALL_BIP143),
74
+ }));
75
+ }
76
+
77
+ before(async () => {
78
+ await initWasm();
79
+ runner = await TestRunner.setup('setup_scripts/ecash-agora_base');
80
+ chronik = runner.chronik;
81
+ ecc = runner.ecc;
82
+ await runner.setupCoins(NUM_COINS, COIN_VALUE);
83
+
84
+ makerSk = fromHex('33'.repeat(32));
85
+ makerPk = ecc.derivePubkey(makerSk);
86
+ makerPkh = shaRmd160(makerPk);
87
+ makerScript = Script.p2pkh(makerPkh);
88
+ makerScriptHex = toHex(makerScript.bytecode);
89
+ takerSk = fromHex('44'.repeat(32));
90
+ takerPk = ecc.derivePubkey(takerSk);
91
+ takerPkh = shaRmd160(takerPk);
92
+ takerScript = Script.p2pkh(takerPkh);
93
+ takerScriptHex = toHex(takerScript.bytecode);
94
+ });
95
+
96
+ after(() => {
97
+ runner.stop();
98
+ });
99
+
100
+ interface TestCase {
101
+ offeredTokens: bigint;
102
+ info: string;
103
+ priceNanoSatsPerToken: bigint;
104
+ acceptedTokens: bigint;
105
+ askedSats: number;
106
+ }
107
+ const TEST_CASES: TestCase[] = [
108
+ {
109
+ offeredTokens: 1000n,
110
+ info: '1sat/token, full accept',
111
+ priceNanoSatsPerToken: 1000000000n,
112
+ acceptedTokens: 1000n,
113
+ askedSats: 1000,
114
+ },
115
+ {
116
+ offeredTokens: 1000n,
117
+ info: '1sat/token, dust accept',
118
+ priceNanoSatsPerToken: 1000000000n,
119
+ acceptedTokens: 546n,
120
+ askedSats: 546,
121
+ },
122
+ {
123
+ offeredTokens: 1000n,
124
+ info: '1000sat/token, full accept',
125
+ priceNanoSatsPerToken: 1000n * 1000000000n,
126
+ acceptedTokens: 1000n,
127
+ askedSats: 1000225,
128
+ },
129
+ {
130
+ offeredTokens: 1000n,
131
+ info: '1000sat/token, half accept',
132
+ priceNanoSatsPerToken: 1000n * 1000000000n,
133
+ acceptedTokens: 500n,
134
+ askedSats: 500113,
135
+ },
136
+ {
137
+ offeredTokens: 1000n,
138
+ info: '1000sat/token, 1 accept',
139
+ priceNanoSatsPerToken: 1000n * 1000000000n,
140
+ acceptedTokens: 1n,
141
+ askedSats: 1001,
142
+ },
143
+ {
144
+ offeredTokens: 1000n,
145
+ info: '1000000sat/token, full accept',
146
+ priceNanoSatsPerToken: 1000000n * 1000000000n,
147
+ acceptedTokens: 1000n,
148
+ askedSats: 1000013824,
149
+ },
150
+ {
151
+ offeredTokens: 1000n,
152
+ info: '1000000sat/token, half accept',
153
+ priceNanoSatsPerToken: 1000000n * 1000000000n,
154
+ acceptedTokens: 500n,
155
+ askedSats: 500039680,
156
+ },
157
+ {
158
+ offeredTokens: 1000n,
159
+ info: '1000000sat/token, 1 accept',
160
+ priceNanoSatsPerToken: 1000000n * 1000000000n,
161
+ acceptedTokens: 1n,
162
+ askedSats: 1048576,
163
+ },
164
+ {
165
+ offeredTokens: 1000n,
166
+ info: '1000000000sat/token, 1 accept',
167
+ priceNanoSatsPerToken: 1000000000n * 1000000000n,
168
+ acceptedTokens: 1n,
169
+ askedSats: 1006632960,
170
+ },
171
+ {
172
+ offeredTokens: 1000000n,
173
+ info: '0.001sat/token, full accept',
174
+ priceNanoSatsPerToken: 1000000n,
175
+ acceptedTokens: 1000000n,
176
+ askedSats: 1000,
177
+ },
178
+ {
179
+ offeredTokens: 1000000n,
180
+ info: '1sat/token, full accept',
181
+ priceNanoSatsPerToken: 1000000000n,
182
+ acceptedTokens: 1000000n,
183
+ askedSats: 1000000,
184
+ },
185
+ {
186
+ offeredTokens: 1000000n,
187
+ info: '1sat/token, half accept',
188
+ priceNanoSatsPerToken: 1000000000n,
189
+ acceptedTokens: 500000n,
190
+ askedSats: 500000,
191
+ },
192
+ {
193
+ offeredTokens: 1000000n,
194
+ info: '1sat/token, dust accept',
195
+ priceNanoSatsPerToken: 1000000000n,
196
+ acceptedTokens: 546n,
197
+ askedSats: 546,
198
+ },
199
+ {
200
+ offeredTokens: 1000000n,
201
+ info: '1000sat/token, full accept',
202
+ priceNanoSatsPerToken: 1000n * 1000000000n,
203
+ acceptedTokens: 1000000n,
204
+ askedSats: 1001151232,
205
+ },
206
+ {
207
+ offeredTokens: 1000000n,
208
+ info: '1000sat/token, half accept',
209
+ priceNanoSatsPerToken: 1000n * 1000000000n,
210
+ acceptedTokens: 500000n,
211
+ askedSats: 500575744,
212
+ },
213
+ {
214
+ offeredTokens: 1000000n,
215
+ info: '1000sat/token, 1 accept',
216
+ priceNanoSatsPerToken: 1000n * 1000000000n,
217
+ acceptedTokens: 1n,
218
+ askedSats: 1024,
219
+ },
220
+ {
221
+ offeredTokens: 1000000n,
222
+ info: '1000000sat/token, 1000 accept',
223
+ priceNanoSatsPerToken: 1000000n * 1000000000n,
224
+ acceptedTokens: 1000n,
225
+ askedSats: 1005060096,
226
+ },
227
+ {
228
+ offeredTokens: 1000000n,
229
+ info: '1000000sat/token, 1 accept',
230
+ priceNanoSatsPerToken: 1000000n * 1000000000n,
231
+ acceptedTokens: 1n,
232
+ askedSats: 1048576,
233
+ },
234
+ {
235
+ offeredTokens: 1000000n,
236
+ info: '1000000sat/token, 1 accept',
237
+ priceNanoSatsPerToken: 1000000000n * 1000000000n,
238
+ acceptedTokens: 1n,
239
+ askedSats: 1006632960,
240
+ },
241
+ {
242
+ offeredTokens: 1000000000n,
243
+ info: '0.001sat/token, full accept',
244
+ priceNanoSatsPerToken: 1000000n,
245
+ acceptedTokens: 1000000000n,
246
+ askedSats: 1000000,
247
+ },
248
+ {
249
+ offeredTokens: 1000000000n,
250
+ info: '0.001sat/token, half accept',
251
+ priceNanoSatsPerToken: 1000000n,
252
+ acceptedTokens: 500000000n,
253
+ askedSats: 500000,
254
+ },
255
+ {
256
+ offeredTokens: 1000000000n,
257
+ info: '0.001sat/token, dust accept',
258
+ priceNanoSatsPerToken: 1000000n,
259
+ acceptedTokens: 546000n,
260
+ askedSats: 546,
261
+ },
262
+ {
263
+ offeredTokens: 1000000000n,
264
+ info: '1sat/token, full accept',
265
+ priceNanoSatsPerToken: 1000000000n,
266
+ acceptedTokens: 1000000000n,
267
+ askedSats: 1000000000,
268
+ },
269
+ {
270
+ offeredTokens: 1000000000n,
271
+ info: '1sat/token, half accept',
272
+ priceNanoSatsPerToken: 1000000000n,
273
+ acceptedTokens: 500000000n,
274
+ askedSats: 500000000,
275
+ },
276
+ {
277
+ offeredTokens: 1000000000n,
278
+ info: '1sat/token, dust accept',
279
+ priceNanoSatsPerToken: 1000000000n,
280
+ acceptedTokens: 546n,
281
+ askedSats: 546,
282
+ },
283
+ {
284
+ offeredTokens: 1000000000n,
285
+ info: '1000sat/token, 983040 accept',
286
+ priceNanoSatsPerToken: 1000n * 1000000000n,
287
+ acceptedTokens: 983040n,
288
+ askedSats: 989855744,
289
+ },
290
+ {
291
+ offeredTokens: 1000000000n,
292
+ info: '1000sat/token, 65536 accept',
293
+ priceNanoSatsPerToken: 1000n * 1000000000n,
294
+ acceptedTokens: 65536n,
295
+ askedSats: 67108864,
296
+ },
297
+ {
298
+ offeredTokens: 1000000000000n,
299
+ info: '0.000001sat/token, full accept',
300
+ priceNanoSatsPerToken: 1000n,
301
+ acceptedTokens: 999999995904n,
302
+ askedSats: 1000108,
303
+ },
304
+ {
305
+ offeredTokens: 1000000000000n,
306
+ info: '0.000001sat/token, half accept',
307
+ priceNanoSatsPerToken: 1000n,
308
+ acceptedTokens: 546045952n,
309
+ askedSats: 547,
310
+ },
311
+ {
312
+ offeredTokens: 1000000000000n,
313
+ info: '0.001sat/token, full accept',
314
+ priceNanoSatsPerToken: 1000000n,
315
+ acceptedTokens: 999999995904n,
316
+ askedSats: 1068115230,
317
+ },
318
+ {
319
+ offeredTokens: 1000000000000n,
320
+ info: '0.001sat/token, dust accept',
321
+ priceNanoSatsPerToken: 1000000n,
322
+ acceptedTokens: 589824n,
323
+ askedSats: 630,
324
+ },
325
+ {
326
+ offeredTokens: 0x7fffffffffffn,
327
+ info: '0.000000001sat/token, full accept',
328
+ priceNanoSatsPerToken: 1n,
329
+ acceptedTokens: 0x7fffc4660000n,
330
+ askedSats: 140744,
331
+ },
332
+ {
333
+ offeredTokens: 0x7fffffffffffn,
334
+ info: '0.000000001sat/token, dust accept',
335
+ priceNanoSatsPerToken: 1n,
336
+ acceptedTokens: 0x7f1e660000n,
337
+ askedSats: 546,
338
+ },
339
+ {
340
+ offeredTokens: 0x7fffffffffffn,
341
+ info: '0.000001sat/token, full accept',
342
+ priceNanoSatsPerToken: 1000n,
343
+ acceptedTokens: 0x7ffffff10000n,
344
+ askedSats: 143165576,
345
+ },
346
+ {
347
+ offeredTokens: 0x7fffffffffffn,
348
+ info: '0.000001sat/token, dust accept',
349
+ priceNanoSatsPerToken: 1000n,
350
+ acceptedTokens: 0x1ffd0000n,
351
+ askedSats: 546,
352
+ },
353
+ {
354
+ offeredTokens: 0x7fffffffffffn,
355
+ info: '0.001sat/token, max sats accept',
356
+ priceNanoSatsPerToken: 1000000n,
357
+ acceptedTokens: 799999983616n,
358
+ askedSats: 1041666816,
359
+ },
360
+ {
361
+ offeredTokens: 0x7fffffffffffn,
362
+ info: '0.001sat/token, dust accept',
363
+ priceNanoSatsPerToken: 1000000n,
364
+ acceptedTokens: 0x70000n,
365
+ askedSats: 768,
366
+ },
367
+ {
368
+ offeredTokens: 0x7fffffffffffn,
369
+ info: '1sat/token, max sats accept',
370
+ priceNanoSatsPerToken: 1000000000n,
371
+ acceptedTokens: 999948288n,
372
+ askedSats: 999948288,
373
+ },
374
+ {
375
+ offeredTokens: 0x7fffffffffffn,
376
+ info: '1sat/token, min accept',
377
+ priceNanoSatsPerToken: 1000000000n,
378
+ acceptedTokens: 0x10000n,
379
+ askedSats: 0x10000,
380
+ },
381
+ {
382
+ offeredTokens: 0xffffffffffffn,
383
+ info: '0.000000001sat/token, full accept',
384
+ priceNanoSatsPerToken: 1n,
385
+ acceptedTokens: 0xffffff000000n,
386
+ askedSats: 281505,
387
+ },
388
+ {
389
+ offeredTokens: 0xffffffffffffn,
390
+ info: '0.000000001sat/token, dust accept',
391
+ priceNanoSatsPerToken: 1n,
392
+ acceptedTokens: 0x7f1c000000n,
393
+ askedSats: 546,
394
+ },
395
+ {
396
+ offeredTokens: 0xffffffffffffn,
397
+ info: '0.000001sat/token, full accept',
398
+ priceNanoSatsPerToken: 1000n,
399
+ acceptedTokens: 0xffffff000000n,
400
+ askedSats: 306783360,
401
+ },
402
+ {
403
+ offeredTokens: 0xffffffffffffn,
404
+ info: '0.000001sat/token, dust accept',
405
+ priceNanoSatsPerToken: 1000n,
406
+ acceptedTokens: 0x1e000000n,
407
+ askedSats: 549,
408
+ },
409
+ {
410
+ offeredTokens: 0xffffffffffffn,
411
+ info: '0.001sat/token, max sats accept',
412
+ priceNanoSatsPerToken: 1000000n,
413
+ acceptedTokens: 0x8000000000n,
414
+ askedSats: 1073741824,
415
+ },
416
+ {
417
+ offeredTokens: 0xffffffffffffn,
418
+ info: '0.001sat/token, min accept',
419
+ priceNanoSatsPerToken: 1000000n,
420
+ acceptedTokens: 0x1000000n,
421
+ askedSats: 32768,
422
+ },
423
+ {
424
+ offeredTokens: 0xffffffffffffn,
425
+ info: '1sat/token, max sats accept',
426
+ priceNanoSatsPerToken: 1000000000n,
427
+ acceptedTokens: 989855744n,
428
+ askedSats: 989855744,
429
+ },
430
+ {
431
+ offeredTokens: 0xffffffffffffn,
432
+ info: '1sat/token, min accept',
433
+ priceNanoSatsPerToken: 1000000000n,
434
+ acceptedTokens: 0x1000000n,
435
+ askedSats: 0x1000000,
436
+ },
437
+ ];
438
+
439
+ for (const testCase of TEST_CASES) {
440
+ it(`AgoraPartial ALP ${testCase.offeredTokens} for ${testCase.info}`, async () => {
441
+ const agoraPartial = AgoraPartial.approximateParams({
442
+ offeredTokens: testCase.offeredTokens,
443
+ priceNanoSatsPerToken: testCase.priceNanoSatsPerToken,
444
+ minAcceptedTokens: testCase.acceptedTokens,
445
+ makerPk,
446
+ ...BASE_PARAMS_ALP,
447
+ });
448
+ const askedSats = agoraPartial.askedSats(testCase.acceptedTokens);
449
+ const requiredSats = askedSats + 2000n;
450
+ const [fuelInput, takerInput] = await makeBuilderInputs([
451
+ 4000,
452
+ Number(requiredSats),
453
+ ]);
454
+
455
+ const offer = await makeAlpOffer({
456
+ chronik,
457
+ ecc,
458
+ agoraPartial,
459
+ makerSk,
460
+ fuelInput,
461
+ });
462
+ const acceptTxid = await takeAlpOffer({
463
+ chronik,
464
+ ecc,
465
+ takerSk,
466
+ offer,
467
+ takerInput,
468
+ acceptedTokens: testCase.acceptedTokens,
469
+ });
470
+ const acceptTx = await chronik.tx(acceptTxid);
471
+ const offeredTokens = agoraPartial.offeredTokens();
472
+ const agora = new Agora(chronik);
473
+ if (testCase.acceptedTokens == offeredTokens) {
474
+ // FULL ACCEPT
475
+ // 0th output is OP_RETURN eMPP AGR0 ad + ALP SEND
476
+ expect(acceptTx.outputs[0].outputScript).to.equal(
477
+ toHex(
478
+ emppScript([
479
+ agoraPartial.adPushdata(),
480
+ alpSend(
481
+ agoraPartial.tokenId,
482
+ agoraPartial.tokenType,
483
+ [0, agoraPartial.offeredTokens()],
484
+ ),
485
+ ]).bytecode,
486
+ ),
487
+ );
488
+ expect(acceptTx.outputs[0].value).to.equal(0);
489
+ expect(acceptTx.outputs[0].token).to.equal(undefined);
490
+ // 1st output is sats to maker
491
+ expect(acceptTx.outputs[1].token).to.equal(undefined);
492
+ expect(acceptTx.outputs[1].value).to.equal(testCase.askedSats);
493
+ expect(acceptTx.outputs[1].outputScript).to.equal(
494
+ makerScriptHex,
495
+ );
496
+ // 2nd output is tokens to taker
497
+ expect(acceptTx.outputs[2].token?.amount).to.equal(
498
+ offeredTokens.toString(),
499
+ );
500
+ expect(acceptTx.outputs[2].value).to.equal(DEFAULT_DUST_LIMIT);
501
+ expect(acceptTx.outputs[2].outputScript).to.equal(
502
+ takerScriptHex,
503
+ );
504
+ // Offer is now gone
505
+ const newOffers = await agora.activeOffersByTokenId(
506
+ offer.token.tokenId,
507
+ );
508
+ expect(newOffers).to.deep.equal([]);
509
+ return;
510
+ }
511
+
512
+ // PARTIAL ACCEPT
513
+ const leftoverTokens = offeredTokens - testCase.acceptedTokens;
514
+ const leftoverTruncTokens =
515
+ leftoverTokens >> BigInt(8 * agoraPartial.numTokenTruncBytes);
516
+ // 0th output is OP_RETURN eMPP AGR0 ad + ALP SEND
517
+ expect(acceptTx.outputs[0].outputScript).to.equal(
518
+ toHex(
519
+ emppScript([
520
+ agoraPartial.adPushdata(),
521
+ alpSend(agoraPartial.tokenId, agoraPartial.tokenType, [
522
+ 0,
523
+ leftoverTokens,
524
+ testCase.acceptedTokens,
525
+ ]),
526
+ ]).bytecode,
527
+ ),
528
+ );
529
+ expect(acceptTx.outputs[0].value).to.equal(0);
530
+ expect(acceptTx.outputs[0].token).to.equal(undefined);
531
+ // 1st output is sats to maker
532
+ expect(acceptTx.outputs[1].token).to.equal(undefined);
533
+ expect(acceptTx.outputs[1].value).to.equal(testCase.askedSats);
534
+ expect(acceptTx.outputs[1].outputScript).to.equal(makerScriptHex);
535
+ // 2nd output is back to the P2SH Script
536
+ expect(acceptTx.outputs[2].token?.amount).to.equal(
537
+ leftoverTokens.toString(),
538
+ );
539
+ expect(acceptTx.outputs[2].value).to.equal(DEFAULT_DUST_LIMIT);
540
+ expect(acceptTx.outputs[2].outputScript.slice(0, 4)).to.equal(
541
+ 'a914',
542
+ );
543
+ // 3rd output is tokens to taker
544
+ expect(acceptTx.outputs[3].token?.amount).to.equal(
545
+ testCase.acceptedTokens.toString(),
546
+ );
547
+ expect(acceptTx.outputs[3].value).to.equal(DEFAULT_DUST_LIMIT);
548
+ expect(acceptTx.outputs[3].outputScript).to.equal(takerScriptHex);
549
+ // Offer is now modified
550
+ const newOffers = await agora.activeOffersByTokenId(
551
+ offer.token.tokenId,
552
+ );
553
+ expect(newOffers.length).to.equal(1);
554
+ const newOffer = newOffers[0];
555
+ expect(newOffer.variant).to.deep.equal({
556
+ type: 'PARTIAL',
557
+ params: new AgoraPartial({
558
+ ...agoraPartial,
559
+ truncTokens: leftoverTruncTokens,
560
+ }),
561
+ });
562
+
563
+ // Cancel leftover offer
564
+ const cancelFeeSats = newOffer.cancelFeeSats({
565
+ recipientScript: makerScript,
566
+ extraInputs: [fuelInput], // dummy input for measuring
567
+ });
568
+ const cancelTxSer = newOffer
569
+ .cancelTx({
570
+ ecc,
571
+ cancelSk: makerSk,
572
+ fuelInputs: await makeBuilderInputs([
573
+ Number(cancelFeeSats),
574
+ ]),
575
+ recipientScript: makerScript,
576
+ })
577
+ .ser();
578
+ const cancelTxid = (await chronik.broadcastTx(cancelTxSer)).txid;
579
+ const cancelTx = await chronik.tx(cancelTxid);
580
+ expect(cancelTx.outputs[1].token?.amount).to.equal(
581
+ leftoverTokens.toString(),
582
+ );
583
+ expect(cancelTx.outputs[1].outputScript).to.equal(makerScriptHex);
584
+ });
585
+ }
586
+ });