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/.eslintignore +8 -0
- package/.nycrc +4 -0
- package/README.md +122 -0
- package/agora.py +771 -0
- package/dist/ad.d.ts +15 -0
- package/dist/ad.d.ts.map +1 -0
- package/dist/ad.js +111 -0
- package/dist/ad.js.map +1 -0
- package/dist/agora.d.ts +178 -0
- package/dist/agora.d.ts.map +1 -0
- package/dist/agora.js +432 -0
- package/dist/agora.js.map +1 -0
- package/dist/consts.d.ts +5 -0
- package/dist/consts.d.ts.map +1 -0
- package/dist/consts.js +9 -0
- package/dist/consts.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/oneshot.d.ts +34 -0
- package/dist/oneshot.d.ts.map +1 -0
- package/dist/oneshot.js +204 -0
- package/dist/oneshot.js.map +1 -0
- package/dist/partial.d.ts +256 -0
- package/dist/partial.d.ts.map +1 -0
- package/dist/partial.js +955 -0
- package/dist/partial.js.map +1 -0
- package/eslint.config.js +16 -0
- package/package.json +52 -0
- package/tests/oneshot.test.ts +569 -0
- package/tests/partial-helper-alp.ts +131 -0
- package/tests/partial-helper-slp.ts +154 -0
- package/tests/partial.alp.bigsats.test.ts +694 -0
- package/tests/partial.alp.test.ts +586 -0
- package/tests/partial.slp.bigsats.test.ts +681 -0
- package/tests/partial.slp.test.ts +630 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.json +19 -0
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)
|