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.
package/agora.py ADDED
@@ -0,0 +1,771 @@
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
+ eCash Agora Plugin
6
+
7
+ Allows users to create UTXOs that can be accepted by anyone if the spending tx
8
+ has outputs enforced by the Script of the UTXO.
9
+
10
+ This allows users to offer UTXOs for other UTXOs in a single, direct, atomic,
11
+ peer-to-peer, non-custodial transaction.
12
+ """
13
+
14
+ import hashlib
15
+ from dataclasses import dataclass
16
+ from io import BytesIO
17
+ from typing import Optional, Union
18
+
19
+ from chronik_plugin.plugin import Plugin, PluginOutput
20
+ from chronik_plugin.script import (
21
+ OP_0,
22
+ OP_0NOTEQUAL,
23
+ OP_2,
24
+ OP_2DUP,
25
+ OP_2OVER,
26
+ OP_2SWAP,
27
+ OP_3,
28
+ OP_3DUP,
29
+ OP_8,
30
+ OP_9,
31
+ OP_12,
32
+ OP_ADD,
33
+ OP_BIN2NUM,
34
+ OP_CAT,
35
+ OP_CHECKDATASIGVERIFY,
36
+ OP_CHECKSIG,
37
+ OP_CHECKSIGVERIFY,
38
+ OP_CODESEPARATOR,
39
+ OP_DIV,
40
+ OP_DROP,
41
+ OP_DUP,
42
+ OP_ELSE,
43
+ OP_ENDIF,
44
+ OP_EQUAL,
45
+ OP_EQUALVERIFY,
46
+ OP_FROMALTSTACK,
47
+ OP_GREATERTHANOREQUAL,
48
+ OP_HASH160,
49
+ OP_HASH256,
50
+ OP_IF,
51
+ OP_MOD,
52
+ OP_NIP,
53
+ OP_NOTIF,
54
+ OP_NUM2BIN,
55
+ OP_OVER,
56
+ OP_PICK,
57
+ OP_PUSHDATA1,
58
+ OP_RESERVED,
59
+ OP_RETURN,
60
+ OP_REVERSEBYTES,
61
+ OP_ROT,
62
+ OP_SHA256,
63
+ OP_SIZE,
64
+ OP_SPLIT,
65
+ OP_SUB,
66
+ OP_SWAP,
67
+ OP_TOALTSTACK,
68
+ OP_TUCK,
69
+ OP_VERIFY,
70
+ CScript,
71
+ )
72
+ from chronik_plugin.slp import slp_send
73
+ from chronik_plugin.token import Token
74
+
75
+ LOKAD_ID = b"AGR0"
76
+ SLP_INT_SIZE = 8
77
+ SLP_INT_PUSHOP = bytes([SLP_INT_SIZE])
78
+
79
+ ALL_ANYONECANPAY_BIP143 = 0x80 | 0x40 | 0x01
80
+
81
+
82
+ def hash160(m):
83
+ ripemd160 = hashlib.new("ripemd160")
84
+ ripemd160.update(hashlib.sha256(m).digest())
85
+ return ripemd160.digest()
86
+
87
+
88
+ def alp_send_intro(token_id: str) -> bytes:
89
+ result = bytearray()
90
+ result.extend(b"SLP2")
91
+ result.append(0)
92
+ result.extend(b"\x04SEND")
93
+ result.extend(bytes.fromhex(token_id)[::-1])
94
+ return bytes(result)
95
+
96
+
97
+ class AgoraPlugin(Plugin):
98
+ def lokad_id(self):
99
+ return LOKAD_ID
100
+
101
+ def version(self):
102
+ return "0.1.0"
103
+
104
+ def run(self, tx):
105
+ return self.run_ad_input(tx) or self.run_ad_empp(tx)
106
+
107
+ def run_ad_input(self, tx):
108
+ """
109
+ Parse the Agora variant that has an "ad" as the first input
110
+ """
111
+ if not tx.inputs:
112
+ return []
113
+ if len(tx.outputs) < 2:
114
+ return []
115
+ if not tx.token_entries:
116
+ return []
117
+
118
+ ad_input = tx.inputs[0]
119
+ token_entry = tx.token_entries[0]
120
+
121
+ pushdata = parse_ad_script_sig(ad_input.script)
122
+ if pushdata is None:
123
+ return []
124
+
125
+ covenant_variant, *pushdata, ad_redeem_bytecode = pushdata
126
+ ad_redeem_script = CScript(ad_redeem_bytecode)
127
+
128
+ if covenant_variant == b"ONESHOT":
129
+ # Offer output is always output 1
130
+ offer_idx = 1
131
+ offer_output = tx.outputs[offer_idx]
132
+ # Offer must have a token
133
+ if offer_output.token is None:
134
+ return []
135
+ agora_oneshot = AgoraOneshot.parse_redeem_script(
136
+ ad_redeem_script,
137
+ offer_output.token,
138
+ )
139
+ if agora_oneshot is None:
140
+ return []
141
+
142
+ expected_agora_script = agora_oneshot.script()
143
+ expected_agora_p2sh = CScript(
144
+ [OP_HASH160, hash160(expected_agora_script), OP_EQUAL]
145
+ )
146
+
147
+ if offer_output.script != expected_agora_p2sh:
148
+ # Offered output doesn't have the advertized P2SH script
149
+ return [
150
+ PluginOutput(
151
+ idx=1,
152
+ data=[b"ERROR", expected_agora_script],
153
+ groups=[],
154
+ )
155
+ ]
156
+ data = agora_oneshot.data()
157
+ pubkey = agora_oneshot.cancel_pk
158
+ elif covenant_variant == b"PARTIAL":
159
+ offer_output, offer_idx = AgoraPartial.find_offered_output(tx)
160
+ # Offer must have a token
161
+ if offer_output.token is None:
162
+ return []
163
+
164
+ agora_partial = AgoraPartial.parse_redeem_script(
165
+ ad_redeem_script, offer_output.token
166
+ )
167
+ if agora_partial is None:
168
+ return []
169
+
170
+ expected_agora_script = agora_partial.script()
171
+ expected_agora_sh = hash160(expected_agora_script)
172
+ expected_agora_p2sh = CScript(
173
+ bytes([OP_HASH160, 20]) + expected_agora_sh + bytes([OP_EQUAL])
174
+ )
175
+
176
+ if offer_output.script != expected_agora_p2sh:
177
+ # Offered output doesn't have the advertized P2SH script
178
+ return [
179
+ PluginOutput(
180
+ idx=offer_idx,
181
+ data=[b"ERROR", expected_agora_script],
182
+ groups=[],
183
+ )
184
+ ]
185
+
186
+ data = agora_partial.data()
187
+ pubkey = agora_partial.maker_pk
188
+ else:
189
+ return []
190
+
191
+ token_id_bytes = bytes.fromhex(token_entry.token_id)
192
+ groups = [
193
+ b"P" + pubkey,
194
+ b"T" + token_id_bytes,
195
+ ]
196
+ if token_entry.group_token_id:
197
+ groups.append(b"G" + bytes.fromhex(token_entry.group_token_id))
198
+ else:
199
+ groups.append(b"F" + token_id_bytes)
200
+
201
+ return [
202
+ PluginOutput(
203
+ idx=offer_idx,
204
+ data=data,
205
+ groups=groups,
206
+ )
207
+ ]
208
+
209
+ def run_ad_empp(self, tx):
210
+ if not tx.empp_data:
211
+ return []
212
+ agr0_data = bytes(tx.empp_data[0])
213
+ if not agr0_data.startswith(LOKAD_ID):
214
+ return []
215
+ if len(tx.outputs) < 2:
216
+ return []
217
+ offer_output, offer_idx = AgoraPartial.find_offered_output(tx)
218
+ if offer_output.token is None:
219
+ return []
220
+ agora_partial = parse_partial(agr0_data, offer_output.token)
221
+ if agora_partial is None:
222
+ return []
223
+ expected_agora_script = agora_partial.script()
224
+ expected_agora_sh = hash160(expected_agora_script)
225
+ expected_agora_p2sh = CScript(
226
+ bytes([OP_HASH160, 20]) + expected_agora_sh + bytes([OP_EQUAL])
227
+ )
228
+ if offer_output.script != expected_agora_p2sh:
229
+ return [
230
+ PluginOutput(
231
+ idx=offer_idx,
232
+ data=[b"ERROR", expected_agora_script],
233
+ groups=[],
234
+ )
235
+ ]
236
+
237
+ token_id_bytes = bytes.fromhex(offer_output.token.token_id)
238
+ return [
239
+ PluginOutput(
240
+ idx=offer_idx,
241
+ data=agora_partial.data(),
242
+ groups=[
243
+ b"P" + agora_partial.maker_pk,
244
+ b"T" + token_id_bytes,
245
+ b"F" + token_id_bytes,
246
+ ],
247
+ )
248
+ ]
249
+
250
+
251
+ MIN_NUM_SCRIPTSIG_PUSHOPS = 3
252
+
253
+
254
+ def parse_ad_script_sig(script) -> Optional[list[Union[bytes, int]]]:
255
+ pushdata = []
256
+ for op in script:
257
+ if not isinstance(op, (bytes, int)):
258
+ return None
259
+ pushdata.append(op)
260
+ if len(pushdata) < MIN_NUM_SCRIPTSIG_PUSHOPS:
261
+ return None
262
+ if pushdata[0] != LOKAD_ID:
263
+ return None
264
+ return pushdata[1:]
265
+
266
+
267
+ @dataclass
268
+ class AgoraOneshot:
269
+ cancel_pk: bytes
270
+ extra_outputs_ser: bytes
271
+ token: Token
272
+
273
+ @classmethod
274
+ def parse_redeem_script(
275
+ cls, redeem_script: CScript, token: Token
276
+ ) -> Optional["AgoraOneshot"]:
277
+ if token.token_protocol != "SLP":
278
+ # Only SLP implemented
279
+ return None
280
+
281
+ ops = list(redeem_script)
282
+
283
+ extra_outputs_ser = ops[0]
284
+ if not isinstance(extra_outputs_ser, bytes):
285
+ # Op 0 expected to be pushop for outputsSer
286
+ return None
287
+
288
+ if ops[1] != OP_DROP:
289
+ # Op 1 expected to be OP_DROP
290
+ return None
291
+
292
+ cancel_pk = ops[2]
293
+ if not isinstance(cancel_pk, bytes) or len(cancel_pk) != 33:
294
+ # Op 2 expected to be pushop for cancelPk and 33 bytes long
295
+ return None
296
+
297
+ if ops[3] != OP_CHECKSIGVERIFY:
298
+ # Op 3 expected to be OP_CHECKSIGVERIFY
299
+ return None
300
+
301
+ covenant_variant = ops[4]
302
+ if not isinstance(covenant_variant, bytes):
303
+ # Op 4 expected to be pushop for covenantVariant
304
+ return None
305
+
306
+ if ops[5] != OP_EQUALVERIFY:
307
+ # Op 5 expected to be OP_EQUALVERIFY
308
+ return None
309
+
310
+ lokad_id = ops[6]
311
+ if not isinstance(lokad_id, bytes):
312
+ # Op 6 expected to be pushop for LOKAD ID
313
+ return None
314
+
315
+ return AgoraOneshot(cancel_pk, extra_outputs_ser, token)
316
+
317
+ def data(self) -> list[bytes]:
318
+ return [b"ONESHOT", self.extra_outputs_ser]
319
+
320
+ def enforced_outputs_ser(self):
321
+ op_return_script = slp_send(
322
+ token_type=self.token.token_type,
323
+ token_id=self.token.token_id,
324
+ amounts=[0, self.token.amount],
325
+ )
326
+ return (
327
+ bytes(8)
328
+ + bytes([len(op_return_script)])
329
+ + op_return_script
330
+ + self.extra_outputs_ser
331
+ )
332
+
333
+ def script(self) -> CScript:
334
+ return CScript(
335
+ [
336
+ OP_IF, # if is_accept
337
+ self.enforced_outputs_ser(), # push enforced_outputs
338
+ OP_SWAP, # swap buyer_outputs, enforced_outputs
339
+ OP_CAT, # outputs = OP_CAT(enforced_outputs, buyer_outputs)
340
+ OP_HASH256, # expected_hash_outputs = OP_HASH256(outputs)
341
+ OP_OVER, # duplicate preimage_4_10,
342
+ # push hash_outputs_idx:
343
+ 36
344
+ + 2 # 4. outpoint
345
+ + 8 # 5. scriptCode, truncated to 01ac via OP_CODESEPARATOR
346
+ + 4, # 6. value # 7. sequence
347
+ OP_SPLIT, # split into preimage_4_7 and preimage_8_10
348
+ OP_NIP, # remove preimage_4_7
349
+ 32, # push 32 onto the stack
350
+ OP_SPLIT, # split into actual_hash_outputs and preimage_9_10
351
+ OP_DROP, # drop preimage_9_10
352
+ OP_EQUALVERIFY, # expected_hash_outputs == actual_hash_outputs
353
+ OP_2, # push tx version
354
+ # length of BIP143 preimage parts 1 to 3
355
+ 4 + 32 + 32,
356
+ # build BIP143 preimage parts 1 to 3 for ANYONECANPAY using OP_NUM2BIN
357
+ OP_NUM2BIN,
358
+ OP_SWAP, # swap preimage_4_10 and preimage_1_3
359
+ OP_CAT, # preimage = OP_CAT(preimage_1_3, preimage_4_10)
360
+ OP_SHA256, # preimage_sha256 = OP_SHA256(preimage)
361
+ OP_3DUP, # OP_3DUP(covenant_pk, covenant_sig, preimage_sha256)
362
+ OP_ROT, # -> covenant_sig | preimage_sha256 | covenant_pk
363
+ OP_CHECKDATASIGVERIFY, # verify preimage matches covenant_sig
364
+ OP_DROP, # drop preimage_sha256
365
+ # push ALL|ANYONECANPAY|BIP143 onto the stack
366
+ bytes([ALL_ANYONECANPAY_BIP143]),
367
+ OP_CAT, # append sighash flags onto covenant_sig
368
+ OP_SWAP, # swap covenant_pk, covenant_sig_flagged
369
+ OP_ELSE, # cancel path
370
+ self.cancel_pk, # pubkey that can cancel the covenant
371
+ OP_ENDIF,
372
+ # cut out everything except the OP_CHECKSIG from the BIP143 scriptCode
373
+ OP_CODESEPARATOR,
374
+ OP_CHECKSIG,
375
+ ]
376
+ )
377
+
378
+
379
+ @dataclass
380
+ class AgoraPartial:
381
+ trunc_tokens: int
382
+ num_token_trunc_bytes: int
383
+ token_scale_factor: int
384
+ scaled_trunc_tokens_per_trunc_sat: int
385
+ num_sats_trunc_bytes: int
386
+ maker_pk: bytes
387
+ min_accepted_scaled_trunc_tokens: int
388
+ token_id: str
389
+ token_type: int
390
+ token_protocol: str
391
+ script_len: int
392
+ dust_amount: int
393
+
394
+ @classmethod
395
+ def parse_redeem_script(cls, redeem_script, token):
396
+ consts = next(iter(redeem_script))
397
+ len_slp_intro = len(
398
+ slp_send(
399
+ token_type=token.token_type,
400
+ token_id=token.token_id,
401
+ amounts=[0],
402
+ )
403
+ )
404
+ ad_pushdata = consts[len_slp_intro:]
405
+ return parse_partial(ad_pushdata, token)
406
+
407
+ @classmethod
408
+ def find_offered_output(cls, tx):
409
+ # Offer output is either output 1 or 2
410
+ offer_idx = 1
411
+ offer_output = tx.outputs[offer_idx]
412
+ if len(tx.outputs) >= 3 and offer_output.token is None:
413
+ offer_idx = 2
414
+ offer_output = tx.outputs[offer_idx]
415
+ return offer_output, offer_idx
416
+
417
+ def ad_pushdata(self):
418
+ pushdata = bytearray()
419
+ if self.token_protocol == "ALP":
420
+ pushdata.extend(b"AGR0")
421
+ pushdata.extend(b"\x07PARTIAL")
422
+ pushdata.append(self.num_token_trunc_bytes)
423
+ pushdata.append(self.num_sats_trunc_bytes)
424
+ pushdata.extend(self.token_scale_factor.to_bytes(8, "little"))
425
+ pushdata.extend(self.scaled_trunc_tokens_per_trunc_sat.to_bytes(8, "little"))
426
+ pushdata.extend(self.min_accepted_scaled_trunc_tokens.to_bytes(8, "little"))
427
+ pushdata.extend(self.maker_pk)
428
+ return bytes(pushdata)
429
+
430
+ def data(self) -> list[bytes]:
431
+ return [
432
+ b"PARTIAL",
433
+ bytes([self.num_token_trunc_bytes]),
434
+ bytes([self.num_sats_trunc_bytes]),
435
+ self.token_scale_factor.to_bytes(8, "little"),
436
+ self.scaled_trunc_tokens_per_trunc_sat.to_bytes(8, "little"),
437
+ self.min_accepted_scaled_trunc_tokens.to_bytes(8, "little"),
438
+ ]
439
+
440
+ def script(self) -> CScript:
441
+ # See partial.ts in ecash-agora for a commented version of this Script
442
+ scaled_trunc_tokens_8le = (
443
+ self.trunc_tokens * self.token_scale_factor
444
+ ).to_bytes(8, "little")
445
+
446
+ ad_pushdata = self.ad_pushdata()
447
+
448
+ # Consts are of slightly different form
449
+ if self.token_protocol == "SLP":
450
+ slp_intro = slp_send(
451
+ token_type=self.token_type,
452
+ token_id=self.token_id,
453
+ amounts=[0],
454
+ )
455
+ covenant_consts = bytes(slp_intro) + ad_pushdata
456
+ token_intro_len = len(slp_intro)
457
+ elif self.token_protocol == "ALP":
458
+ alp_intro = alp_send_intro(self.token_id)
459
+ empp_intro = CScript([OP_RETURN, OP_RESERVED, self.ad_pushdata()])
460
+ covenant_consts = alp_intro + bytes(empp_intro)
461
+ token_intro_len = len(alp_intro)
462
+ else:
463
+ raise NotImplementedError
464
+
465
+ return CScript(
466
+ [
467
+ covenant_consts,
468
+ scaled_trunc_tokens_8le,
469
+ OP_CODESEPARATOR,
470
+ OP_ROT,
471
+ OP_IF,
472
+ OP_BIN2NUM,
473
+ OP_ROT,
474
+ OP_2DUP,
475
+ OP_GREATERTHANOREQUAL,
476
+ OP_VERIFY,
477
+ OP_DUP,
478
+ self.min_accepted_scaled_trunc_tokens,
479
+ OP_GREATERTHANOREQUAL,
480
+ OP_VERIFY,
481
+ OP_DUP,
482
+ self.token_scale_factor,
483
+ OP_MOD,
484
+ OP_0,
485
+ OP_EQUALVERIFY,
486
+ OP_TUCK,
487
+ OP_SUB,
488
+ 2,
489
+ OP_PICK,
490
+ token_intro_len,
491
+ OP_SPLIT,
492
+ OP_DROP,
493
+ OP_OVER,
494
+ OP_0NOTEQUAL,
495
+ *self._script_build_op_return(token_intro_len),
496
+ bytes(self.num_sats_trunc_bytes),
497
+ OP_CAT,
498
+ OP_ROT,
499
+ self.scaled_trunc_tokens_per_trunc_sat - 1,
500
+ OP_ADD,
501
+ self.scaled_trunc_tokens_per_trunc_sat,
502
+ OP_DIV,
503
+ 8 - self.num_sats_trunc_bytes,
504
+ OP_NUM2BIN,
505
+ OP_CAT,
506
+ bytes([25, OP_DUP, OP_HASH160, 20]),
507
+ OP_2OVER,
508
+ OP_DROP,
509
+ len(covenant_consts) - 33,
510
+ OP_SPLIT,
511
+ OP_NIP,
512
+ OP_HASH160,
513
+ OP_CAT,
514
+ bytes([OP_EQUALVERIFY, OP_CHECKSIG]),
515
+ OP_CAT,
516
+ OP_CAT,
517
+ OP_TOALTSTACK,
518
+ OP_TUCK,
519
+ self.dust_amount,
520
+ OP_8,
521
+ OP_NUM2BIN,
522
+ bytes([23, OP_HASH160, 20]),
523
+ OP_CAT,
524
+ bytes(
525
+ [OP_PUSHDATA1, len(covenant_consts)]
526
+ if len(covenant_consts) >= OP_PUSHDATA1
527
+ else [len(covenant_consts)]
528
+ ),
529
+ OP_2SWAP,
530
+ OP_8,
531
+ OP_TUCK,
532
+ OP_NUM2BIN,
533
+ OP_CAT,
534
+ OP_CAT,
535
+ OP_CAT,
536
+ bytes([OP_CODESEPARATOR]),
537
+ OP_CAT,
538
+ 3,
539
+ OP_PICK,
540
+ 36 + (1 if self.script_len < 0xFD else 3),
541
+ OP_SPLIT,
542
+ OP_NIP,
543
+ self.script_len,
544
+ OP_SPLIT,
545
+ OP_12,
546
+ OP_SPLIT,
547
+ OP_NIP,
548
+ 32,
549
+ OP_SPLIT,
550
+ OP_DROP,
551
+ OP_TOALTSTACK,
552
+ OP_CAT,
553
+ OP_HASH160,
554
+ OP_CAT,
555
+ bytes([OP_EQUAL]),
556
+ OP_CAT,
557
+ OP_SWAP,
558
+ OP_0NOTEQUAL,
559
+ OP_NOTIF,
560
+ OP_DROP,
561
+ b"",
562
+ OP_ENDIF,
563
+ OP_ROT,
564
+ OP_SIZE,
565
+ OP_0NOTEQUAL,
566
+ OP_VERIFY,
567
+ OP_CAT,
568
+ OP_FROMALTSTACK,
569
+ OP_FROMALTSTACK,
570
+ OP_ROT,
571
+ OP_CAT,
572
+ OP_HASH256,
573
+ OP_EQUALVERIFY,
574
+ OP_2,
575
+ 4 + 32 + 32,
576
+ OP_NUM2BIN,
577
+ OP_SWAP,
578
+ OP_CAT,
579
+ OP_SHA256,
580
+ OP_3DUP,
581
+ OP_ROT,
582
+ OP_CHECKDATASIGVERIFY,
583
+ OP_DROP,
584
+ bytes([ALL_ANYONECANPAY_BIP143]),
585
+ OP_CAT,
586
+ OP_SWAP,
587
+ OP_ELSE,
588
+ OP_DROP,
589
+ len(covenant_consts) - 33,
590
+ OP_SPLIT,
591
+ OP_NIP,
592
+ OP_ENDIF,
593
+ *self._script_outro(),
594
+ ]
595
+ )
596
+
597
+ def _script_build_op_return(self, token_intro_len):
598
+ if self.token_protocol == "SLP":
599
+ return self._script_build_slp_op_return()
600
+ elif self.token_protocol == "ALP":
601
+ return self._script_build_alp_op_return(token_intro_len)
602
+ else:
603
+ raise NotImplementedError
604
+
605
+ def _script_build_slp_op_return(self):
606
+ return [
607
+ OP_IF,
608
+ OP_8,
609
+ OP_CAT,
610
+ OP_OVER,
611
+ self.token_scale_factor,
612
+ OP_DIV,
613
+ *self._script_ser_trunc_tokens(),
614
+ OP_REVERSEBYTES,
615
+ bytes(self.num_token_trunc_bytes),
616
+ OP_CAT,
617
+ OP_CAT,
618
+ OP_ENDIF,
619
+ OP_8,
620
+ OP_CAT,
621
+ 2,
622
+ OP_PICK,
623
+ self.token_scale_factor,
624
+ OP_DIV,
625
+ *self._script_ser_trunc_tokens(),
626
+ OP_REVERSEBYTES,
627
+ bytes(self.num_token_trunc_bytes),
628
+ OP_CAT,
629
+ OP_CAT,
630
+ OP_SIZE,
631
+ OP_9,
632
+ OP_NUM2BIN,
633
+ OP_REVERSEBYTES,
634
+ OP_SWAP,
635
+ OP_CAT,
636
+ ]
637
+
638
+ def _script_build_alp_op_return(self, token_intro_len):
639
+ return [
640
+ OP_IF,
641
+ OP_3,
642
+ 7 + self.num_token_trunc_bytes,
643
+ OP_NUM2BIN,
644
+ OP_CAT,
645
+ OP_OVER,
646
+ self.token_scale_factor,
647
+ OP_DIV,
648
+ 6,
649
+ OP_ELSE,
650
+ OP_2,
651
+ 7 + self.num_token_trunc_bytes,
652
+ OP_ENDIF,
653
+ OP_NUM2BIN,
654
+ OP_CAT,
655
+ 2,
656
+ OP_PICK,
657
+ self.token_scale_factor,
658
+ OP_DIV,
659
+ *self._script_ser_trunc_tokens(),
660
+ OP_CAT,
661
+ OP_SIZE,
662
+ OP_SWAP,
663
+ OP_CAT,
664
+ 3,
665
+ OP_PICK,
666
+ token_intro_len,
667
+ OP_SPLIT,
668
+ OP_NIP,
669
+ OP_SWAP,
670
+ OP_CAT,
671
+ OP_SIZE,
672
+ OP_9,
673
+ OP_NUM2BIN,
674
+ OP_REVERSEBYTES,
675
+ OP_SWAP,
676
+ OP_CAT,
677
+ ]
678
+
679
+ def _script_ser_trunc_tokens(self):
680
+ if self.token_protocol == "SLP":
681
+ num_bytes_token_amount = 8
682
+ elif self.token_protocol == "ALP":
683
+ num_bytes_token_amount = 6
684
+ else:
685
+ raise NotImplementedError
686
+ if self.num_token_trunc_bytes == num_bytes_token_amount - 3:
687
+ return [
688
+ 4,
689
+ OP_NUM2BIN,
690
+ 3,
691
+ OP_SPLIT,
692
+ OP_DROP,
693
+ ]
694
+ return [
695
+ num_bytes_token_amount - self.num_token_trunc_bytes,
696
+ OP_NUM2BIN,
697
+ ]
698
+
699
+ def _script_outro(self):
700
+ if self.token_protocol == "SLP":
701
+ return [
702
+ OP_CHECKSIGVERIFY,
703
+ b"PARTIAL",
704
+ OP_EQUALVERIFY,
705
+ LOKAD_ID,
706
+ OP_EQUAL,
707
+ ]
708
+ elif self.token_protocol == "ALP":
709
+ return [OP_CHECKSIG]
710
+ else:
711
+ raise NotImplementedError
712
+
713
+
714
+ def parse_partial(pushdata: bytes, token) -> Optional[AgoraPartial]:
715
+ data_reader = BytesIO(pushdata)
716
+ # AGR0 PARTIAL pushdata always has the same length
717
+ if token.token_protocol == "SLP":
718
+ if len(pushdata) != 59:
719
+ return None
720
+ elif token.token_protocol == "ALP":
721
+ if len(pushdata) != 71:
722
+ return None
723
+ if data_reader.read(4) != b"AGR0":
724
+ return None
725
+ if data_reader.read(8) != b"\x07PARTIAL":
726
+ return None
727
+ else:
728
+ raise NotImplementedError
729
+ num_token_trunc_bytes = data_reader.read(1)[0]
730
+ num_sats_trunc_bytes = data_reader.read(1)[0]
731
+ token_scale_factor = int.from_bytes(data_reader.read(8), "little")
732
+ scaled_trunc_tokens_per_trunc_sat = int.from_bytes(data_reader.read(8), "little")
733
+ min_accepted_scaled_trunc_tokens = int.from_bytes(data_reader.read(8), "little")
734
+ maker_pk = data_reader.read(33)
735
+
736
+ token_trunc_factor = 1 << (8 * num_token_trunc_bytes)
737
+
738
+ # Offers must have a losslessly truncatable token amount
739
+ if token.amount % token_trunc_factor != 0:
740
+ return None
741
+
742
+ partial_alp = AgoraPartial(
743
+ trunc_tokens=token.amount // token_trunc_factor,
744
+ num_token_trunc_bytes=num_token_trunc_bytes,
745
+ token_scale_factor=token_scale_factor,
746
+ scaled_trunc_tokens_per_trunc_sat=scaled_trunc_tokens_per_trunc_sat,
747
+ num_sats_trunc_bytes=num_sats_trunc_bytes,
748
+ maker_pk=maker_pk,
749
+ min_accepted_scaled_trunc_tokens=min_accepted_scaled_trunc_tokens,
750
+ token_id=token.token_id,
751
+ token_type=token.token_type,
752
+ token_protocol=token.token_protocol,
753
+ script_len=0x7F,
754
+ dust_amount=546,
755
+ )
756
+ measured_len = len(cut_out_codesep(partial_alp.script()))
757
+ if measured_len > 0x80:
758
+ partial_alp.script_len = measured_len
759
+ measured_len = len(cut_out_codesep(partial_alp.script()))
760
+ partial_alp.script_len = measured_len
761
+ return partial_alp
762
+
763
+
764
+ def cut_out_codesep(script):
765
+ script_iter = iter(script)
766
+ for op in script_iter:
767
+ if op == OP_CODESEPARATOR:
768
+ break
769
+ else:
770
+ return script
771
+ return CScript(script_iter)