forked from MudwoodLabs/pyrxd
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdmint_v1_deploy_demo.py
More file actions
697 lines (590 loc) · 28.1 KB
/
Copy pathdmint_v1_deploy_demo.py
File metadata and controls
697 lines (590 loc) · 28.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
"""dMint V1 deploy demo — multi-contract permissionless-mint token.
This demo deploys a V1 dMint token (the only dMint format on Radiant
mainnet today). Unlike a plain Glyph FT deploy, dMint emits *N parallel
contract UTXOs* in the reveal — every contract can be mined from
independently, so claims race in parallel. Total supply equals
``num_contracts * max_height * reward_photons``.
The on-chain reference deployment is Radiant Glyph Protocol (GLYPH) at
mainnet commit ``a443d9df…878b`` / reveal ``b965b32d…9dd6``. This demo
emits byte-identical contract scripts when configured with the same
params (the M2 byte-equal golden vector tests pin this).
Run modes
---------
::
# 1. Dry-run (builds txs but does NOT broadcast — the SAFE default):
DRY_RUN=1 GLYPH_WIF=<wif> python examples/dmint_v1_deploy_demo.py
# 2. Resume reveal phase after commit was broadcast:
DRY_RUN=0 \\
I_UNDERSTAND_THIS_IS_REAL=yes \\
GLYPH_WIF=<wif> \\
COMMIT_TXID=<txid> COMMIT_VOUT=<n> COMMIT_VALUE=<photons> \\
python examples/dmint_v1_deploy_demo.py
# 3. Full real broadcast (commit + reveal, BOTH on mainnet):
DRY_RUN=0 \\
I_UNDERSTAND_THIS_IS_REAL=yes \\
GLYPH_WIF=<wif> \\
python examples/dmint_v1_deploy_demo.py
Environment variables
---------------------
``DRY_RUN`` Default ``1``; ``0`` = build+broadcast.
Refuses to broadcast unless ``I_UNDERSTAND_THIS_IS_REAL=yes``.
``GLYPH_WIF`` Funded WIF private key.
``TOKEN_NAME`` Default ``"pyrxd V1 demo"``.
``TOKEN_TICKER`` Default ``"PXD"``.
``TOKEN_DESC`` Default ``"V1 dMint demo deploy via pyrxd"``.
``NUM_CONTRACTS`` Default ``4`` (keep small for the demo — real deploys
use 32+).
``MAX_HEIGHT`` Default ``100``.
``REWARD_PHOTONS`` Default ``1000``.
``DIFFICULTY`` Default ``1`` (very easy — anyone can mine).
``COMMIT_TXID`` Resume after broadcast: skip commit, go to reveal.
``COMMIT_VOUT`` Required when ``COMMIT_TXID`` is set (typically 0).
``COMMIT_VALUE`` Required when ``COMMIT_TXID`` is set.
Threat-model notes
------------------
* The three-key handshake (``DRY_RUN=0`` plus
``I_UNDERSTAND_THIS_IS_REAL=yes`` plus a real ``GLYPH_WIF``) is a
deliberate footgun guard. A typo or leaked env var alone is not
enough to broadcast.
* The demo refuses to spend token-bearing UTXOs as funding (mirrors
``find_dmint_funding_utxo`` from ``pyrxd.glyph.dmint``). A wallet
that accidentally spends an FT/NFT/dMint UTXO destroys the token.
* Signing is atomic: all input preimages are computed first, then
signed in a separate pass, then attached in a final pass. Partial
failure leaves the tx untouched.
See ``docs/DMINT_RESEARCH.md`` for the byte-by-byte
chain truth this demo emits against.
"""
from __future__ import annotations
import asyncio
import json
import os
import sys
import websockets
from pyrxd.fee_models import SatoshisPerKilobyte
from pyrxd.glyph import GlyphBuilder, GlyphMetadata, GlyphProtocol
from pyrxd.glyph.builder import DmintV1DeployParams
from pyrxd.glyph.dmint import is_token_bearing_script
from pyrxd.hash import sha256
from pyrxd.keys import PrivateKey
from pyrxd.script.script import Script
from pyrxd.script.type import P2PKH, encode_pushdata, to_unlock_script_template
from pyrxd.security.types import Hex20
from pyrxd.transaction.transaction import Transaction, TransactionInput, TransactionOutput
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
ELECTRUMX_WS_URL = os.environ.get("PYRXD_ELECTRUMX", "wss://electrumx.radiant4people.com:50022/")
DRY_RUN: bool = os.environ.get("DRY_RUN", "1") != "0"
I_UNDERSTAND: bool = os.environ.get("I_UNDERSTAND_THIS_IS_REAL", "").strip().lower() == "yes"
GLYPH_WIF: str = os.environ.get("GLYPH_WIF", "")
TOKEN_NAME = os.environ.get("TOKEN_NAME", "pyrxd V1 demo")
TOKEN_TICKER = os.environ.get("TOKEN_TICKER", "PXD")
TOKEN_DESC = os.environ.get("TOKEN_DESC", "V1 dMint demo deploy via pyrxd")
NUM_CONTRACTS = int(os.environ.get("NUM_CONTRACTS", "4"))
MAX_HEIGHT = int(os.environ.get("MAX_HEIGHT", "100"))
REWARD_PHOTONS = int(os.environ.get("REWARD_PHOTONS", "1000"))
DIFFICULTY = int(os.environ.get("DIFFICULTY", "1"))
# Resume after commit-broadcast (skip commit phase).
RESUME_COMMIT_TXID = os.environ.get("COMMIT_TXID", "").lower()
RESUME_COMMIT_VOUT = int(os.environ.get("COMMIT_VOUT", "0"))
RESUME_COMMIT_VALUE = int(os.environ.get("COMMIT_VALUE", "0")) if RESUME_COMMIT_TXID else 0
RESUME_FILE = ".dmint_v1_deploy_demo.resume.json"
MIN_FEE_RATE = 10_000 # photons per byte (post-V2 mainnet)
# ---------------------------------------------------------------------------
# ElectrumX client (minimal — matches ft_deploy_premine.py's helpers)
# ---------------------------------------------------------------------------
async def _ws_call(method: str, params: list) -> object:
"""Single round-trip ElectrumX JSON-RPC call."""
async with websockets.connect(ELECTRUMX_WS_URL) as ws:
await ws.send(json.dumps({"id": 1, "method": method, "params": params}))
resp = json.loads(await asyncio.wait_for(ws.recv(), timeout=30.0))
if resp.get("error"):
raise RuntimeError(f"ElectrumX error for {method}: {resp['error']}")
return resp["result"]
async def fetch_utxos(address: str) -> list:
locking = P2PKH().lock(address)
sh = sha256(locking.serialize()).hex()
rev = "".join(reversed([sh[i : i + 2] for i in range(0, len(sh), 2)]))
raw = await _ws_call("blockchain.scripthash.listunspent", [rev])
if not isinstance(raw, list):
return []
return raw
async def fetch_raw_tx(txid: str) -> bytes:
raw_hex = await _ws_call("blockchain.transaction.get", [txid, False])
if not isinstance(raw_hex, str):
raise RuntimeError(f"unexpected raw-tx response type for {txid}")
return bytes.fromhex(raw_hex)
async def broadcast(tx_hex: str) -> str:
result = await _ws_call("blockchain.transaction.broadcast", [tx_hex])
if not isinstance(result, str):
raise RuntimeError(f"unexpected broadcast response: {result!r}")
return result
# ---------------------------------------------------------------------------
# Token-burn defense
# ---------------------------------------------------------------------------
async def _filter_plain_funding_utxos(utxos: list, address: str) -> list:
"""Remove any UTXO whose source script is token-bearing.
Mirrors `find_dmint_funding_utxo`'s opcode-aware classifier from
`pyrxd.glyph.dmint`: never spend an FT/NFT/dMint UTXO as funding —
that destroys the token. Naive byte-substring scans misclassify
legitimate P2PKHs (compound doc: funding-utxo-byte-scan-dos.md).
Use the same `is_token_bearing_script` helper the library uses.
"""
safe: list = []
for u in utxos:
try:
raw = await fetch_raw_tx(u["tx_hash"])
except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as e:
# Narrow set of failure modes from the local _ws_call shim:
# OSError covers connection drops, ValueError covers protocol
# decode failures, RuntimeError covers ElectrumX RPC errors,
# JSONDecodeError covers malformed responses. A broader catch
# would mask programming errors (e.g. a typo'd attribute on
# the response shape) that should crash loudly.
print(f" [skip] could not fetch source tx for {u['tx_hash']}: {e}")
continue
tx = Transaction.from_hex(raw)
if tx is None or u["tx_pos"] >= len(tx.outputs):
continue
script = tx.outputs[u["tx_pos"]].locking_script.serialize()
if is_token_bearing_script(script):
print(
f" [skip] token-bearing UTXO {u['tx_hash'][:12]}…:{u['tx_pos']} "
f"({u['value']:,} photons) — would destroy the token"
)
continue
safe.append(u)
return safe
# ---------------------------------------------------------------------------
# Atomic multi-input signing
# ---------------------------------------------------------------------------
def _sign_p2pkh_inputs(
tx: Transaction,
indices: list[int],
private_key: PrivateKey,
*,
suffix_by_index: dict[int, bytes] | None = None,
) -> None:
"""Sign multiple inputs atomically (three-pass: build, sign, attach).
Why three-pass: per security S4 in the M2 plan, a build-sign-attach
loop that mutates the tx mid-iteration would leave a half-signed tx
on partial failure (key access failure, OOM, etc.). The three-pass
pattern builds all preimages first, signs all preimages second, and
attaches all unlocking scripts in a final pass — if any step raises,
the tx is left untouched.
:param tx: The transaction to sign in place.
:param indices: Input indices to sign (all P2PKH).
:param private_key: Private key for every input (single-key
demo; production would pass per-input keys).
:param suffix_by_index: Optional dict mapping input index to a
scriptSig suffix appended after the
``<sig> <pubkey>`` push (used for the FT
and NFT reveal inputs that carry the
Glyph payload).
"""
suffix_by_index = suffix_by_index or {}
# Pass 1: build every preimage. If any input's metadata is malformed
# this raises before we sign anything.
preimages: list[bytes] = []
for idx in indices:
preimages.append(tx.preimage(idx))
# Pass 2: produce every signature. Key access failures surface
# here, still without mutating the tx.
pub = private_key.public_key().serialize()
sigs: list[bytes] = []
for preimage in preimages:
sigs.append(private_key.sign(preimage))
# Pass 3: build full unlocking scripts and attach in one sweep.
unlocks: list[bytes] = []
for idx, sig in zip(indices, sigs, strict=True):
sighash = tx.inputs[idx].sighash
unlock = encode_pushdata(sig + sighash.to_bytes(1, "little")) + encode_pushdata(pub)
if idx in suffix_by_index:
unlock += suffix_by_index[idx]
unlocks.append(unlock)
for idx, unlock in zip(indices, unlocks, strict=True):
tx.inputs[idx].unlocking_script = Script(unlock)
# ---------------------------------------------------------------------------
# Unlock-script templates (used by tx.sign() during fee estimation)
# ---------------------------------------------------------------------------
def _p2pkh_unlock_template(private_key: PrivateKey):
"""Standard P2PKH unlock template — pass-through to library helper."""
def sign(tx, idx):
sig = private_key.sign(tx.preimage(idx))
sighash = tx.inputs[idx].sighash
pub = private_key.public_key().serialize()
return Script(encode_pushdata(sig + sighash.to_bytes(1, "little")) + encode_pushdata(pub))
def estimated_len():
return 107
return to_unlock_script_template(sign, estimated_len)
def _reveal_input_unlock_template(private_key: PrivateKey, scriptsig_suffix: bytes):
"""Unlock template for a Glyph commit-hashlock spend: P2PKH-sig + suffix."""
def sign(tx, idx):
sig = private_key.sign(tx.preimage(idx))
sighash = tx.inputs[idx].sighash
pub = private_key.public_key().serialize()
p2pkh_part = encode_pushdata(sig + sighash.to_bytes(1, "little")) + encode_pushdata(pub)
return Script(p2pkh_part + scriptsig_suffix)
def estimated_len():
return 107 + len(scriptsig_suffix)
return to_unlock_script_template(sign, estimated_len)
# ---------------------------------------------------------------------------
# Commit tx builder (1 FT-commit + N ref-seeds + 1 change)
# ---------------------------------------------------------------------------
def _build_commit_tx(
utxos: list,
private_key: PrivateKey,
commit_script: bytes,
num_contracts: int,
address: str,
pkh: Hex20,
) -> Transaction:
"""Build the V1 dMint deploy commit tx.
Output layout (mirrors the on-chain GLYPH commit at
``a443d9df…878b`` documented in
``docs/DMINT_RESEARCH.md`` §2):
* vout 0: the 75-byte FT-commit hashlock
* vouts 1..N: ``num_contracts`` × 1-photon P2PKH ref-seeds
* vout N+1: P2PKH change
Note: the on-chain GLYPH commit also had a 75-byte *NFT-commit*
hashlock at vout N+1 (for the auth NFT). The pyrxd M2 demo skips
this — see ``docs/DMINT_RESEARCH.md`` §6 for the
"mint-fresh vs forward-prior" decision: M2 picks mint-fresh
*without* the auth NFT to keep the demo focused on the dMint
machinery itself. A future milestone can layer the auth NFT on top.
"""
p2pkh_lock = P2PKH().lock(address)
inputs: list[TransactionInput] = []
total_in = 0
# Commit needs to cover, conservatively:
# - 1 photon for the FT-commit hashlock output (vout 0)
# - 1 photon × num_contracts for the ref-seeds (vouts 1..N)
# - commit-tx fee (~300 bytes × 10K photons/byte ≈ 3M photons)
# - reveal-tx fee + funding for the reveal's external input
# (~700 bytes × 10K photons/byte ≈ 7M photons for small N)
# - dust-margin so the commit's change output isn't < 546 photons
# (pyrxd's fee model drops sub-dust change to miners)
# Use 20M photons of headroom — cheap insurance against an under-sized
# wallet. Operator can override by setting funding bigger than this.
target_value = 1 + num_contracts + 20_000_000
for u in utxos:
src_out = TransactionOutput(p2pkh_lock, u["value"])
class _SrcTx:
def __init__(self, out, pos):
self.outputs = {pos: out}
inp = TransactionInput(
source_txid=u["tx_hash"],
source_output_index=u["tx_pos"],
unlocking_script_template=_p2pkh_unlock_template(private_key),
)
inp.satoshis = u["value"]
inp.locking_script = p2pkh_lock
inp.source_transaction = _SrcTx(src_out, u["tx_pos"])
inputs.append(inp)
total_in += u["value"]
if total_in >= target_value:
break
if total_in < target_value:
raise ValueError(
f"Insufficient funds: have {total_in:,}, need ≥{target_value:,} photons "
f"(1 commit + {num_contracts} ref-seeds + reveal budget). Top up the wallet."
)
outputs: list[TransactionOutput] = [
TransactionOutput(Script(commit_script), 1), # vout 0: FT-commit
]
for _ in range(num_contracts):
outputs.append(TransactionOutput(p2pkh_lock, 1)) # vouts 1..N: ref-seeds
outputs.append(TransactionOutput(p2pkh_lock, change=True))
tx = Transaction(tx_inputs=inputs, tx_outputs=outputs)
tx.fee(SatoshisPerKilobyte(MIN_FEE_RATE * 1000))
tx.sign()
return tx
# ---------------------------------------------------------------------------
# Reveal tx builder (N dMint contracts + change)
# ---------------------------------------------------------------------------
def _build_reveal_tx(
commit_txid: str,
commit_script: bytes,
num_contracts: int,
scriptsig_suffix: bytes,
contract_scripts: tuple[bytes, ...],
op_return_script: bytes | None,
funding_utxo: dict,
funding_pkh_lock: bytes,
private_key: PrivateKey,
address: str,
) -> Transaction:
"""Build the V1 dMint deploy reveal tx.
Reveal layout (simplified from GLYPH — no auth NFT):
* vin 0: spends commit vout 0 (FT-commit hashlock) with
``<sig> <pubkey> <gly> <CBOR>``
* vins 1..N: spend commit vouts 1..N (ref-seeds, P2PKH)
* vin N+1: plain funding input for reveal fees
* vouts 0..N-1: ``num_contracts`` V1 dMint contract UTXOs
* vout N: optional OP_RETURN (omitted if not set)
* vout N | N+1: P2PKH change to deployer
Signing is delegated to ``tx.sign()`` which invokes the unlock
templates. Each input's ``locking_script`` MUST be the actual
on-chain script of the UTXO being spent — the BIP143 sighash
preimage hashes ``tx_input.locking_script`` (Radiant
transaction_preimage.py line 130), so a wrong value here
produces a signature the validator will reject.
The two distinct locking-script shapes:
* vin 0: the 75-byte FT-commit hashlock (``commit_script`` arg)
* vins 1..N + funding: standard 25-byte P2PKH
"""
p2pkh_lock = P2PKH().lock(address)
commit_lock = Script(commit_script)
class _SrcTx:
def __init__(self, outs):
self.outputs = outs
# Reconstruct the commit's outputs so each TransactionInput can resolve
# satoshis + locking_script for sighash preimage construction. The
# commit script for vout 0 MUST be the actual 75-byte FT-commit
# hashlock; vouts 1..N are 1-photon P2PKH ref-seeds.
commit_outs = {0: TransactionOutput(commit_lock, 1)}
for i in range(1, num_contracts + 1):
commit_outs[i] = TransactionOutput(p2pkh_lock, 1)
src_commit = _SrcTx(commit_outs)
inputs: list[TransactionInput] = []
# vin 0: FT-commit hashlock — needs the reveal suffix.
inp0 = TransactionInput(
source_txid=commit_txid,
source_output_index=0,
unlocking_script_template=_reveal_input_unlock_template(private_key, scriptsig_suffix),
)
inp0.satoshis = 1
inp0.locking_script = commit_lock # the actual 75-byte FT-commit hashlock
inp0.source_transaction = src_commit
inputs.append(inp0)
# vins 1..N: ref-seed P2PKHs at the deployer's PKH.
for i in range(1, num_contracts + 1):
inp = TransactionInput(
source_txid=commit_txid,
source_output_index=i,
unlocking_script_template=_p2pkh_unlock_template(private_key),
)
inp.satoshis = 1
inp.locking_script = p2pkh_lock
inp.source_transaction = src_commit
inputs.append(inp)
# vin N+1: external plain-RXD funding input for the reveal's fee.
# Note: funding_pkh_lock comes from the deployer's own address (we
# currently assume single-key deploys); for a multi-key deployer
# the funding key would be separate.
funding_lock = Script(funding_pkh_lock)
funding_src = _SrcTx({funding_utxo["tx_pos"]: TransactionOutput(funding_lock, funding_utxo["value"])})
fund = TransactionInput(
source_txid=funding_utxo["tx_hash"],
source_output_index=funding_utxo["tx_pos"],
unlocking_script_template=_p2pkh_unlock_template(private_key),
)
fund.satoshis = funding_utxo["value"]
fund.locking_script = funding_lock
fund.source_transaction = funding_src
inputs.append(fund)
outputs: list[TransactionOutput] = [TransactionOutput(Script(s), 1) for s in contract_scripts]
if op_return_script is not None:
outputs.append(TransactionOutput(Script(op_return_script), 0))
outputs.append(TransactionOutput(p2pkh_lock, change=True))
tx = Transaction(tx_inputs=inputs, tx_outputs=outputs)
tx.fee(SatoshisPerKilobyte(MIN_FEE_RATE * 1000))
tx.sign()
return tx
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
async def main() -> None:
if not GLYPH_WIF:
print("ERROR: Set GLYPH_WIF to a funded WIF private key.", file=sys.stderr)
sys.exit(1)
if not DRY_RUN and not I_UNDERSTAND:
print(
"ERROR: DRY_RUN=0 requires I_UNDERSTAND_THIS_IS_REAL=yes (broadcasts a real mainnet tx).",
file=sys.stderr,
)
sys.exit(1)
private_key = PrivateKey(GLYPH_WIF)
pub = private_key.public_key()
address = pub.address()
pkh = Hex20(pub.hash160())
total_supply = NUM_CONTRACTS * MAX_HEIGHT * REWARD_PHOTONS
# Conservative cost estimate. Real fees depend on tx serialized size,
# which varies with NUM_CONTRACTS (more contracts → bigger reveal).
# At MIN_FEE_RATE=10K photons/byte and the demo's typical sizes:
# - commit: ~300 bytes → ~3M photons fee
# - reveal: (250 bytes per contract + ~200 bytes overhead) × 10K
estimated_commit_fee = 3_000_000
estimated_reveal_fee = (NUM_CONTRACTS * 250 + 200) * MIN_FEE_RATE
print(f"Deployer wallet: {address}")
print(f"Token name: {TOKEN_NAME}")
print(f"Ticker: {TOKEN_TICKER}")
print(f"num_contracts: {NUM_CONTRACTS}")
print(f"max_height: {MAX_HEIGHT}")
print(f"reward_photons: {REWARD_PHOTONS}")
print(f"difficulty: {DIFFICULTY}")
print(f"Total supply: {total_supply:,} photons ({NUM_CONTRACTS} × {MAX_HEIGHT} × {REWARD_PHOTONS})")
print(f"Commit fee (est): ~{estimated_commit_fee:,} photons")
print(f"Reveal fee (est): ~{estimated_reveal_fee:,} photons")
print(
f"Total cost (est): ~{estimated_commit_fee + estimated_reveal_fee + NUM_CONTRACTS + 1:,} photons "
f"(plus dust margin for change)"
)
print(f"DRY_RUN: {DRY_RUN}")
print()
metadata = GlyphMetadata(
protocol=[GlyphProtocol.FT, GlyphProtocol.DMINT],
name=TOKEN_NAME,
ticker=TOKEN_TICKER,
description=TOKEN_DESC,
)
builder = GlyphBuilder()
params = DmintV1DeployParams(
metadata=metadata,
owner_pkh=pkh,
num_contracts=NUM_CONTRACTS,
max_height=MAX_HEIGHT,
reward_photons=REWARD_PHOTONS,
difficulty=DIFFICULTY,
)
result = builder.prepare_dmint_deploy(params)
print(f"Payload hash: {result.commit_result.payload_hash.hex()}")
print(f"CBOR ({len(result.cbor_bytes)} bytes): {result.cbor_bytes.hex()}")
print(f"Placeholder contract script length: {len(result.placeholder_contract_scripts[0])} bytes")
print()
# Commit phase.
if RESUME_COMMIT_TXID:
commit_txid = RESUME_COMMIT_TXID
print(f"Resuming from commit {commit_txid} (vout {RESUME_COMMIT_VOUT}, {RESUME_COMMIT_VALUE:,} photons)")
# Param-drift guard: if a resume file exists, verify the env-derived
# params reproduce the same commit script. A mismatch means the user
# changed TOKEN_NAME / NUM_CONTRACTS / etc. between commit and resume
# — the reveal would build against the wrong commit script and the
# signed sighash would be wrong on every input.
try:
with open(RESUME_FILE) as f:
saved = json.load(f)
except FileNotFoundError:
print(
f" ⚠️ No resume file at {RESUME_FILE} — cannot verify env "
f"params match the on-chain commit. Proceed only if you are "
f"sure none of TOKEN_NAME, TOKEN_TICKER, NUM_CONTRACTS, "
f"MAX_HEIGHT, REWARD_PHOTONS, DIFFICULTY changed since commit."
)
else:
saved_script_hex = saved.get("commit_script_hex")
current_script_hex = result.commit_result.commit_script.hex()
if saved_script_hex and saved_script_hex != current_script_hex:
print(
"ERROR: resume-file commit script does not match the "
"script the current env vars would build. One of the "
"deploy params changed since the commit was broadcast. "
"The reveal would fail on chain (sighash mismatch).",
file=sys.stderr,
)
print(f" saved: {saved_script_hex[:60]}...", file=sys.stderr)
print(f" current: {current_script_hex[:60]}...", file=sys.stderr)
sys.exit(1)
elif saved.get("commit_txid") != commit_txid:
print(
f" ⚠️ Resume file's commit_txid ({saved.get('commit_txid')}) "
f"differs from COMMIT_TXID env ({commit_txid}). Proceeding "
f"with env-supplied txid."
)
else:
print(" ✓ Resume file matches current env params.")
else:
print("Fetching UTXOs and filtering token-bearing...")
utxos = await fetch_utxos(address)
if not utxos:
print("No UTXOs found. Fund the address and retry.", file=sys.stderr)
sys.exit(1)
utxos = await _filter_plain_funding_utxos(utxos, address)
if not utxos:
print("No plain (non-token-bearing) UTXOs available.", file=sys.stderr)
sys.exit(1)
total = sum(u["value"] for u in utxos)
print(f" → {len(utxos)} plain UTXO(s), {total:,} photons")
commit_tx = _build_commit_tx(
utxos=utxos,
private_key=private_key,
commit_script=result.commit_result.commit_script,
num_contracts=NUM_CONTRACTS,
address=address,
pkh=pkh,
)
commit_txid = commit_tx.txid()
print(f"Commit tx: {commit_txid}")
print(f" size: {commit_tx.byte_length()} bytes")
print(f" fee: {commit_tx.get_fee():,} photons")
print()
resume_info = {
"commit_txid": commit_txid,
"num_contracts": NUM_CONTRACTS,
"cbor_hex": result.cbor_bytes.hex(),
# Save the commit script so the resume path can verify nothing
# drifted between commit and reveal. See param-drift guard above.
"commit_script_hex": result.commit_result.commit_script.hex(),
}
with open(RESUME_FILE, "w") as f:
json.dump(resume_info, f)
print(f"Resume info saved to {RESUME_FILE}")
if DRY_RUN:
print("[DRY RUN] Commit tx NOT broadcast.")
print(f" Resume env: COMMIT_TXID={commit_txid} COMMIT_VOUT=0 COMMIT_VALUE=1")
print(f"\nCommit hex (for review):\n{commit_tx.hex()}")
return
print("Broadcasting commit tx...")
txid = await broadcast(commit_tx.hex())
print(f"Broadcast: {txid}")
print("Waiting 90s for commit to confirm before reveal...")
await asyncio.sleep(90)
# Reveal phase — rebuild contract scripts with the real commit txid.
reveal_scripts = result.build_reveal_outputs(commit_txid)
print(
f"Contract scripts built (N={len(reveal_scripts.contract_scripts)}, "
f"each {len(reveal_scripts.contract_scripts[0])} bytes)"
)
# Need a separate plain funding UTXO for reveal fees (the commit
# already consumed its inputs; the ref-seeds + FT-commit are all
# 1-photon outputs, not enough for fee). Fetch + filter.
print("Fetching reveal-fee funding UTXO...")
utxos = await fetch_utxos(address)
utxos = await _filter_plain_funding_utxos(utxos, address)
# Pick the largest.
utxos.sort(key=lambda u: u["value"], reverse=True)
if not utxos:
print("ERROR: no plain UTXO available to fund reveal fees.", file=sys.stderr)
sys.exit(1)
funding = utxos[0]
print(f" → using {funding['tx_hash'][:12]}…:{funding['tx_pos']} ({funding['value']:,} photons)")
reveal_tx = _build_reveal_tx(
commit_txid=commit_txid,
commit_script=result.commit_result.commit_script,
num_contracts=NUM_CONTRACTS,
scriptsig_suffix=reveal_scripts.scriptsig_suffix,
contract_scripts=reveal_scripts.contract_scripts,
op_return_script=reveal_scripts.op_return_script,
funding_utxo=funding,
funding_pkh_lock=bytes(P2PKH().lock(address).serialize()),
private_key=private_key,
address=address,
)
print(f"Reveal tx: {reveal_tx.txid()}")
print(f" size: {reveal_tx.byte_length()} bytes")
print(f" fee: {reveal_tx.get_fee():,} photons")
if DRY_RUN:
print("\n[DRY RUN] Reveal tx NOT broadcast.")
print(f"\nReveal hex (for review):\n{reveal_tx.hex()[:200]}…")
return
print("\nBroadcasting reveal tx...")
rev_txid = await broadcast(reveal_tx.hex())
print(f"Broadcast: {rev_txid}")
print()
print("Token deployed!")
print(f" Token ref: {commit_txid}:0")
print(f" Contracts: {NUM_CONTRACTS} parallel UTXOs at {rev_txid}:0..{NUM_CONTRACTS - 1}")
print(f" Total supply: {total_supply:,} photons")
if __name__ == "__main__":
asyncio.run(main())