"""Builders for the structured records SIGN_TX streams to the device. Wire layouts are documented at the top of `src/handlers/sign_tx.rs`; the covenant item layouts mirror `shd/Sources/Covenants/CovenantData.swift`. """ from hashlib import sha3_256 from typing import List, Sequence, Tuple from ragger.bip import pack_derivation_path SCRIPT_KIND_P2WPKH: int = 0x00 SCRIPT_KIND_P2WSH: int = 0x01 SIGHASH_ALL: int = 0x01 SIGHASH_NONE: int = 0x02 SIGHASH_SINGLE: int = 0x03 SIGHASH_SINGLEREVERSE: int = 0x04 SIGHASH_NOINPUT: int = 0x40 SIGHASH_ANYONECANPAY: int = 0x80 # Covenant types, matching shd/Sources/Protocol/CovenantType.swift. COVENANT_NONE: int = 0 COVENANT_CLAIM: int = 1 COVENANT_OPEN: int = 2 COVENANT_BID: int = 3 COVENANT_REVEAL: int = 4 COVENANT_REDEEM: int = 5 COVENANT_REGISTER: int = 6 COVENANT_UPDATE: int = 7 COVENANT_RENEW: int = 8 COVENANT_TRANSFER: int = 9 COVENANT_FINALIZE: int = 10 COVENANT_REVOKE: int = 11 def encode_varint(n: int) -> bytes: if n < 0xFD: return bytes([n]) if n <= 0xFFFF: return b"\xfd" + n.to_bytes(2, "little") if n <= 0xFFFFFFFF: return b"\xfe" + n.to_bytes(4, "little") return b"\xff" + n.to_bytes(8, "little") def name_hash(name: bytes) -> bytes: """`shd` NameRules.hashName: SHA3-256, not Keccak.""" return sha3_256(name).digest() def begin_payload(n_in: int, n_out: int, version: int = 0, locktime: int = 0) -> bytes: return (version.to_bytes(4, "little") + encode_varint(n_in) + encode_varint(n_out) + locktime.to_bytes(4, "little")) def input_payload(path: str, value: int, txid: bytes = b"\x11" * 32, vout: int = 0, sequence: int = 0xFFFFFFFF, sighash: int = SIGHASH_ALL, script_kind: int = SCRIPT_KIND_P2WPKH) -> bytes: assert len(txid) == 32 return (txid + vout.to_bytes(4, "little") + sequence.to_bytes(4, "little") + value.to_bytes(8, "little") + bytes([sighash]) + pack_derivation_path(path) + bytes([script_kind])) def output_payload(value: int, addr_hash: bytes = b"\x22" * 20, addr_version: int = 0, covenant_kind: int = COVENANT_NONE, covenant_items: Sequence[bytes] = ()) -> bytes: items = b"".join(encode_varint(len(i)) + i for i in covenant_items) return (value.to_bytes(8, "little") + bytes([addr_version, len(addr_hash)]) + addr_hash + bytes([covenant_kind]) + encode_varint(len(covenant_items)) + items) def uint32_le(n: int) -> bytes: return n.to_bytes(4, "little") def open_items(name: bytes) -> List[bytes]: """OPEN: [nameHash, height, name] (CovenantData.makeOpen).""" return [name_hash(name), uint32_le(0), name] def bid_items(name: bytes, height: int = 0, blind: bytes = b"\x33" * 32) -> List[bytes]: """BID: [nameHash, startHeight, name, blind] (CovenantData.makeBid).""" return [name_hash(name), uint32_le(height), name, blind] def reveal_items(name: bytes, height: int = 0, nonce: bytes = b"\x44" * 32) -> List[bytes]: """REVEAL: [nameHash, startHeight, nonce]. Carries no plaintext name.""" return [name_hash(name), uint32_le(height), nonce] def transfer_items(name: bytes, dest_hash: bytes, height: int = 0, dest_version: int = 0) -> List[bytes]: """TRANSFER: [nameHash, startHeight, [version], addressHash]. `items[3]` is the address the name is handed to, which is the whole point of the covenant and appears nowhere else in the output. """ return [name_hash(name), uint32_le(height), bytes([dest_version]), dest_hash]