cashscript-py 1.0.0__py3-none-any.whl
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.
- cashscript_py/__init__.py +36 -0
- cashscript_py/contract.py +210 -0
- cashscript_py/helpers/__init__.py +1 -0
- cashscript_py/helpers/address.py +95 -0
- cashscript_py/helpers/argument_encoding.py +161 -0
- cashscript_py/helpers/bch_opcodes.py +217 -0
- cashscript_py/helpers/bech32.py +97 -0
- cashscript_py/helpers/cashaddress.py +355 -0
- cashscript_py/helpers/cashtoken.py +100 -0
- cashscript_py/helpers/crypto.py +15 -0
- cashscript_py/helpers/data_encoding.py +46 -0
- cashscript_py/helpers/schnorr.py +137 -0
- cashscript_py/helpers/script.py +327 -0
- cashscript_py/helpers/transaction.py +310 -0
- cashscript_py/interfaces.py +230 -0
- cashscript_py/network/__init__.py +1 -0
- cashscript_py/network/electrum_network_provider.py +156 -0
- cashscript_py/network/network_provider.py +112 -0
- cashscript_py/signature_template.py +152 -0
- cashscript_py/transaction_builder.py +287 -0
- cashscript_py-1.0.0.dist-info/METADATA +98 -0
- cashscript_py-1.0.0.dist-info/RECORD +24 -0
- cashscript_py-1.0.0.dist-info/WHEEL +4 -0
- cashscript_py-1.0.0.dist-info/licenses/LICENSE +7 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Public API for CashScript-Py.
|
|
2
|
+
|
|
3
|
+
Exports the most commonly used classes and helpers:
|
|
4
|
+
- Contract
|
|
5
|
+
- Output, Utxo
|
|
6
|
+
- ElectrumNetworkProvider, NetworkProvider
|
|
7
|
+
- SignatureTemplate
|
|
8
|
+
- TransactionBuilder
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from cashscript_py.contract import Contract
|
|
12
|
+
from cashscript_py.helpers.argument_encoding import ConstructorArgument, FunctionArgument
|
|
13
|
+
from cashscript_py.helpers.cashaddress import LockingBytecodeType
|
|
14
|
+
from cashscript_py.interfaces import HashType, NftCapability, Output, SignatureAlgorithm, TokenDetails, Utxo
|
|
15
|
+
from cashscript_py.network.electrum_network_provider import ElectrumNetworkProvider
|
|
16
|
+
from cashscript_py.network.network_provider import Network, NetworkProvider
|
|
17
|
+
from cashscript_py.signature_template import SignatureTemplate
|
|
18
|
+
from cashscript_py.transaction_builder import TransactionBuilder
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Contract",
|
|
22
|
+
"Output",
|
|
23
|
+
"Utxo",
|
|
24
|
+
"ElectrumNetworkProvider",
|
|
25
|
+
"Network",
|
|
26
|
+
"NetworkProvider",
|
|
27
|
+
"SignatureTemplate",
|
|
28
|
+
"TransactionBuilder",
|
|
29
|
+
"LockingBytecodeType",
|
|
30
|
+
"ConstructorArgument",
|
|
31
|
+
"FunctionArgument",
|
|
32
|
+
"HashType",
|
|
33
|
+
"SignatureAlgorithm",
|
|
34
|
+
"TokenDetails",
|
|
35
|
+
"NftCapability",
|
|
36
|
+
]
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Contract API.
|
|
2
|
+
|
|
3
|
+
Provides loading and interaction with CashScript-contract artifacts, including
|
|
4
|
+
constructor validation/encoding, address generation, and ABI-based unlockers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import semver
|
|
13
|
+
|
|
14
|
+
from cashscript_py.helpers.address import script_to_address
|
|
15
|
+
from cashscript_py.helpers.argument_encoding import (
|
|
16
|
+
ConstructorArgument,
|
|
17
|
+
FunctionArgument,
|
|
18
|
+
encode_constructor_arguments,
|
|
19
|
+
encode_function_argument,
|
|
20
|
+
)
|
|
21
|
+
from cashscript_py.helpers.cashaddress import LockingBytecodeType, address_string_to_locking_bytecode
|
|
22
|
+
from cashscript_py.helpers.crypto import hash256
|
|
23
|
+
from cashscript_py.helpers.script import (
|
|
24
|
+
asm_to_script,
|
|
25
|
+
calculate_bytesize,
|
|
26
|
+
count_opcodes,
|
|
27
|
+
create_input_script,
|
|
28
|
+
generate_redeem_script,
|
|
29
|
+
serialize_script,
|
|
30
|
+
)
|
|
31
|
+
from cashscript_py.helpers.transaction import create_preimage
|
|
32
|
+
from cashscript_py.interfaces import ContractUnlocker, Output, Transaction, Utxo
|
|
33
|
+
from cashscript_py.network.network_provider import NetworkProvider
|
|
34
|
+
from cashscript_py.signature_template import SignatureTemplate
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Contract:
|
|
38
|
+
"""High-level interface to a CashScript contract.
|
|
39
|
+
|
|
40
|
+
A Contract is constructed from a compiled cashc JSON artifact and constructor
|
|
41
|
+
arguments. It exposes ABI functions via the `unlock` mapping and provides
|
|
42
|
+
helper methods for querying UTXOs and balances.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
name: The contract name from the artifact.
|
|
46
|
+
address: The contract address (token-unaware).
|
|
47
|
+
token_address: The contract address (token-aware).
|
|
48
|
+
bytecode: Contract redeem script bytecode (hex).
|
|
49
|
+
bytesize: Size of the redeem script in bytes.
|
|
50
|
+
opcount: Count of non-push opcodes in the redeem script.
|
|
51
|
+
provider: Network provider used for blockchain operations.
|
|
52
|
+
address_type: Locking bytecode type (e.g., P2SH32).
|
|
53
|
+
unlock: Mapping of function name to unlocker factory.
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
artifact: dict[str, Any],
|
|
60
|
+
constructor_args: list[ConstructorArgument],
|
|
61
|
+
provider: NetworkProvider,
|
|
62
|
+
address_type: LockingBytecodeType = LockingBytecodeType.P2SH32,
|
|
63
|
+
):
|
|
64
|
+
"""Initialize a Contract instance.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
artifact: The compiled cashc JSON artifact.
|
|
68
|
+
constructor_args: Constructor arguments in ABI order.
|
|
69
|
+
provider: Network provider for queries and broadcasts.
|
|
70
|
+
address_type: Locking bytecode type to use for addresses (Default: P2SH32).
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
ValueError: If the artifact is incomplete/invalid, the compiler
|
|
74
|
+
version is unsupported (< 0.7.0), or the constructor argument
|
|
75
|
+
count/types do not match the artifact.
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
self.artifact = artifact
|
|
79
|
+
self.constructor_args: list[ConstructorArgument] = constructor_args
|
|
80
|
+
self.provider = provider
|
|
81
|
+
self.address_type = address_type
|
|
82
|
+
|
|
83
|
+
required_fields = ["abi", "bytecode", "constructorInputs", "contractName", "compiler"]
|
|
84
|
+
for field in required_fields:
|
|
85
|
+
if field not in artifact or artifact[field] is None:
|
|
86
|
+
raise ValueError(f"Missing {field}")
|
|
87
|
+
|
|
88
|
+
version = artifact["compiler"]["version"]
|
|
89
|
+
if not semver.VersionInfo.parse(version).match(">=0.7.0"):
|
|
90
|
+
raise ValueError(f"Artifact compiled with unsupported compiler version: {version}, required >=0.7.0")
|
|
91
|
+
|
|
92
|
+
if len(artifact["constructorInputs"]) != len(constructor_args):
|
|
93
|
+
expected_types = [input["type"] for input in artifact["constructorInputs"]]
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"Incorrect number of arguments passed to {artifact['contractName']} constructor. "
|
|
96
|
+
f"Expected {len(artifact['constructorInputs'])} arguments ({expected_types}) "
|
|
97
|
+
f"but got {len(constructor_args)}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
self.encoded_constructor_args: list[bytes] = encode_constructor_arguments(
|
|
101
|
+
constructor_args, artifact["constructorInputs"]
|
|
102
|
+
)
|
|
103
|
+
self.redeem_script = generate_redeem_script(asm_to_script(artifact["bytecode"]), self.encoded_constructor_args)
|
|
104
|
+
|
|
105
|
+
self.name = artifact["contractName"]
|
|
106
|
+
self.address = script_to_address(self.redeem_script, self.provider.network, self.address_type, False)
|
|
107
|
+
self.token_address = script_to_address(self.redeem_script, self.provider.network, self.address_type, True)
|
|
108
|
+
self.bytecode = serialize_script(self.redeem_script).hex()
|
|
109
|
+
self.bytesize = calculate_bytesize(self.redeem_script)
|
|
110
|
+
self.opcount = count_opcodes(self.redeem_script)
|
|
111
|
+
self.unlock: dict[str, Callable[..., ContractUnlocker]] = {}
|
|
112
|
+
|
|
113
|
+
if len(artifact["abi"]) == 1:
|
|
114
|
+
f = artifact["abi"][0]
|
|
115
|
+
self.unlock[f["name"]] = self._create_unlocker(f)
|
|
116
|
+
else:
|
|
117
|
+
for i, f in enumerate(artifact["abi"]):
|
|
118
|
+
self.unlock[f["name"]] = self._create_unlocker(f, i)
|
|
119
|
+
|
|
120
|
+
def _create_unlocker(
|
|
121
|
+
self, abi_function: dict[str, Any], selector: int | None = None
|
|
122
|
+
) -> Callable[..., ContractUnlocker]:
|
|
123
|
+
def unlocker(*args: FunctionArgument) -> ContractUnlocker:
|
|
124
|
+
if len(abi_function["inputs"]) != len(args):
|
|
125
|
+
expected_types = [inp["type"] for inp in abi_function["inputs"]]
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"Incorrect number of arguments passed to function {abi_function['name']}. "
|
|
128
|
+
f"Expected {len(abi_function['inputs'])} arguments ({expected_types}) "
|
|
129
|
+
f"but got {len(args)}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
encoded_args = [
|
|
133
|
+
encode_function_argument(arg, abi_function["inputs"][i]["type"]) for i, arg in enumerate(args)
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
def generate_unlocking_bytecode(
|
|
137
|
+
transaction: Transaction, source_outputs: list[Output], input_index: int
|
|
138
|
+
) -> bytes:
|
|
139
|
+
complete_args = []
|
|
140
|
+
|
|
141
|
+
for arg in encoded_args:
|
|
142
|
+
if isinstance(arg, SignatureTemplate):
|
|
143
|
+
# Build preimage and sighash (SIGHASH_UTXOS path handled by create_preimage)
|
|
144
|
+
hashtype_full = arg.get_hash_type().value
|
|
145
|
+
covered_script = serialize_script(self.redeem_script)
|
|
146
|
+
preimage = create_preimage(
|
|
147
|
+
transaction, source_outputs, input_index, covered_script, hashtype_full
|
|
148
|
+
)
|
|
149
|
+
sighash = hash256(preimage)
|
|
150
|
+
|
|
151
|
+
# Generate signature (sig + hashtype byte) for CHECKSIG
|
|
152
|
+
signature = arg.generate_signature(sighash)
|
|
153
|
+
|
|
154
|
+
# Debugging info
|
|
155
|
+
if os.environ.get("CASHSCRIPT_PY_DEBUG") == "1":
|
|
156
|
+
print(f"[contract-debug] function={abi_function.get('name')}")
|
|
157
|
+
print(
|
|
158
|
+
f"[contract-debug] hashtype_byte=0x{(hashtype_full & 0xFF):02x} "
|
|
159
|
+
f"algo={arg.get_signature_algorithm().name}"
|
|
160
|
+
)
|
|
161
|
+
print(f"[contract-debug] preimage: {preimage.hex()}")
|
|
162
|
+
print(f"[contract-debug] sighash: {sighash.hex()}")
|
|
163
|
+
print(f"[contract-debug] signature: {signature.hex()}")
|
|
164
|
+
|
|
165
|
+
complete_args.append(signature)
|
|
166
|
+
else:
|
|
167
|
+
complete_args.append(arg)
|
|
168
|
+
|
|
169
|
+
# Create input script
|
|
170
|
+
input_script = create_input_script(self.redeem_script, complete_args, selector)
|
|
171
|
+
|
|
172
|
+
# Debugging info
|
|
173
|
+
if os.environ.get("CASHSCRIPT_PY_DEBUG") == "1":
|
|
174
|
+
redeem_bc = serialize_script(self.redeem_script)
|
|
175
|
+
print(f"[contract-debug] redeem_script_bytecode: {redeem_bc.hex()}")
|
|
176
|
+
print(f"[contract-debug] input_script_bytecode: {input_script.hex()}")
|
|
177
|
+
|
|
178
|
+
return input_script
|
|
179
|
+
|
|
180
|
+
def generate_locking_bytecode() -> bytes:
|
|
181
|
+
return address_string_to_locking_bytecode(self.address)
|
|
182
|
+
|
|
183
|
+
return ContractUnlocker(
|
|
184
|
+
generate_locking_bytecode,
|
|
185
|
+
generate_unlocking_bytecode,
|
|
186
|
+
self,
|
|
187
|
+
list(args),
|
|
188
|
+
abi_function,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return unlocker
|
|
192
|
+
|
|
193
|
+
async def get_balance(self) -> int:
|
|
194
|
+
"""Return the total balance (sum of UTXO satoshis) at the contract address.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
The sum of satoshis across all UTXOs at the contract address.
|
|
198
|
+
|
|
199
|
+
"""
|
|
200
|
+
utxos = await self.get_utxos()
|
|
201
|
+
return sum(utxo.satoshis for utxo in utxos)
|
|
202
|
+
|
|
203
|
+
async def get_utxos(self) -> list[Utxo]:
|
|
204
|
+
"""Fetch all UTXOs (confirmed/unconfirmed) for the contract address.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
A list of UTXOs spendable by this contract.
|
|
208
|
+
|
|
209
|
+
"""
|
|
210
|
+
return await self.provider.get_utxos(self.address)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Helper subpackage: address, script, encoding, crypto, and token utilities."""
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Utility functions for address computations, conversions, and transformations."""
|
|
2
|
+
|
|
3
|
+
from cashscript_py.helpers.cashaddress import (
|
|
4
|
+
LockingBytecodeType,
|
|
5
|
+
address_contents_to_locking_bytecode,
|
|
6
|
+
get_network_prefix,
|
|
7
|
+
locking_bytecode_to_cash_address,
|
|
8
|
+
payload_to_cash_address,
|
|
9
|
+
)
|
|
10
|
+
from cashscript_py.helpers.crypto import hash160, hash256
|
|
11
|
+
from cashscript_py.helpers.script import Script, serialize_script
|
|
12
|
+
from cashscript_py.network.network_provider import Network
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def script_to_address(
|
|
16
|
+
script: Script, network: Network, address_type: LockingBytecodeType, token_aware: bool = False
|
|
17
|
+
) -> str:
|
|
18
|
+
"""Convert a script to a CashAddress for the given network/type.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
script: Script list to encode.
|
|
22
|
+
network: Target network.
|
|
23
|
+
address_type: Locking bytecode type (P2SH20/P2SH32/P2PKH/P2PK).
|
|
24
|
+
token_aware: If True, encode token-aware address version.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
CashAddress string.
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
locking_bytecode = script_to_locking_bytecode(script, address_type)
|
|
31
|
+
prefix = get_network_prefix(network)
|
|
32
|
+
return locking_bytecode_to_cash_address(locking_bytecode, prefix, token_aware)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def script_to_locking_bytecode(script: Script, address_type: LockingBytecodeType) -> bytes:
|
|
36
|
+
"""Hash a script into its P2SH20/P2SH32 (or P2PKH/P2PK) locking bytecode.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
script: Script list to hash.
|
|
40
|
+
address_type: Desired locking type.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Locking bytecode.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If the address_type is unsupported.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
if address_type != LockingBytecodeType.P2SH20 and address_type != LockingBytecodeType.P2SH32:
|
|
50
|
+
raise ValueError("address_type needs to be either 'p2sh32' or 'p2sh20'")
|
|
51
|
+
script_bytecode = serialize_script(script)
|
|
52
|
+
script_hash = hash256(script_bytecode) if address_type == LockingBytecodeType.P2SH32 else hash160(script_bytecode)
|
|
53
|
+
return address_contents_to_locking_bytecode(script_hash, address_type)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def public_key_to_pkh(public_key: bytes) -> bytes:
|
|
57
|
+
"""Return the HASH160 of a public key (pkh)."""
|
|
58
|
+
return hash160(public_key)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def public_key_to_p2pkh_locking_bytecode(public_key: bytes) -> bytes:
|
|
62
|
+
"""Build a standard P2PKH locking bytecode from a public key.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
public_key: Raw public key (compressed or uncompressed).
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
P2PKH locking bytecode.
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
pubkey_hash: bytes = hash160(public_key)
|
|
72
|
+
return address_contents_to_locking_bytecode(pubkey_hash, LockingBytecodeType.P2PKH)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def public_key_to_cash_address(
|
|
76
|
+
public_key: bytes,
|
|
77
|
+
network: Network,
|
|
78
|
+
address_type: LockingBytecodeType = LockingBytecodeType.P2PKH,
|
|
79
|
+
token_aware: bool = False,
|
|
80
|
+
) -> str:
|
|
81
|
+
"""Convert a public key to a CashAddress.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
public_key: Raw public key (compressed or uncompressed).
|
|
85
|
+
network: Target network.
|
|
86
|
+
address_type: Address type (default P2PKH).
|
|
87
|
+
token_aware: If True, uses token-aware address version.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
CashAddress string.
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
prefix = get_network_prefix(network)
|
|
94
|
+
public_key_hash = hash160(public_key)
|
|
95
|
+
return payload_to_cash_address(prefix, public_key_hash, address_type, token_aware)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""CashScript ABI argument encoding.
|
|
2
|
+
|
|
3
|
+
Key behavior:
|
|
4
|
+
- 'int' encodes to Script Number (little-endian, minimal-length).
|
|
5
|
+
- 'bool' encodes to 0x00/0x01.
|
|
6
|
+
- 'string' encodes to UTF-8 bytes.
|
|
7
|
+
- 'pubkey' accepts raw bytes or hex string (33/65 bytes).
|
|
8
|
+
- 'bytes'/'bytesN' accept raw bytes or hex string; bytesN enforces exact length.
|
|
9
|
+
- 'sig' accepts SignatureTemplate, or raw bytes with valid lengths {0,65,71,72,73}.
|
|
10
|
+
- 'datasig' accepts raw bytes with valid lengths {0,64,70,71,72}.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import cast
|
|
14
|
+
|
|
15
|
+
from cashscript_py.helpers.script import encode_int as encode_script_number
|
|
16
|
+
from cashscript_py.signature_template import SignatureTemplate
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ArgumentTypeError(Exception):
|
|
20
|
+
"""Raised when an ABI argument's Python type does not match the expected CashScript type."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, actual_type: str, expected_type: str):
|
|
23
|
+
"""Initialize the type error with actual and expected type names."""
|
|
24
|
+
super().__init__(f"Expected type `{expected_type}` but got `{actual_type}`")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
ConstructorArgument = bool | int | str | bytes
|
|
28
|
+
FunctionArgument = ConstructorArgument | SignatureTemplate
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_type(type_str: str) -> tuple[str, int | None]:
|
|
32
|
+
"""Parse a CashScript type string.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
type_str: Type string (e.g., "int", "bytes", "bytes20", "pubkey", "sig").
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
A tuple of (base_type, bound) where bound is the integer size for bytesN or None.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
import re
|
|
42
|
+
|
|
43
|
+
m = re.match(r"bytes(\d+)$", type_str)
|
|
44
|
+
if m:
|
|
45
|
+
return ("bytes", int(m.group(1)))
|
|
46
|
+
return (type_str, None)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _encode_bool(value: bool) -> bytes:
|
|
50
|
+
return b"\x01" if value else b"\x00"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _encode_string(value: str) -> bytes:
|
|
54
|
+
return value.encode("utf-8")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _encode_pubkey(value: bytes | str) -> bytes:
|
|
58
|
+
return value if isinstance(value, bytes) else bytes.fromhex(value)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def encode_constructor_arguments(
|
|
62
|
+
contract_params: list[ConstructorArgument], constructor_inputs: list[dict[str, str]]
|
|
63
|
+
) -> list[bytes]:
|
|
64
|
+
"""Encode constructor arguments according to the artifact's constructor types.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
contract_params: Values provided to the contract constructor.
|
|
68
|
+
constructor_inputs: Type descriptors (in declaration order) from the artifact.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Encoded arguments as raw bytes (ABI format).
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
ValueError: If a signature type is used in the constructor (disallowed) or types mismatch.
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
if "sig" in [i["type"] for i in constructor_inputs]:
|
|
78
|
+
raise ValueError("Cannot use signatures in constructor")
|
|
79
|
+
return [
|
|
80
|
+
cast(bytes, encode_function_argument(value, arg["type"]))
|
|
81
|
+
for value, arg in zip(contract_params, constructor_inputs, strict=False)
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def encode_function_argument(argument: FunctionArgument, type_str: str) -> bytes | SignatureTemplate:
|
|
86
|
+
"""Encode a single function argument for the CashScript ABI.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
argument: The argument value (bytes/hex/string/int/bool/SigTemplate).
|
|
90
|
+
type_str: The ABI type string (e.g., "int", "bool", "string", "bytesN", "pubkey", "sig", "datasig").
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Encoded bytes or a SignatureTemplate (for "sig").
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ArgumentTypeError: If the value type does not match the expected ABI type.
|
|
97
|
+
ValueError: If bounded bytesN lengths, signature lengths, or unknown types are invalid.
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
# Normalize to bytes (convert hex string to bytes)
|
|
102
|
+
def _normalize_bytes(value: FunctionArgument) -> bool | int | bytes | SignatureTemplate:
|
|
103
|
+
if isinstance(value, str):
|
|
104
|
+
value = value[2:] if value.startswith("0x") else value
|
|
105
|
+
value = bytes.fromhex(value)
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
t, bound = _parse_type(type_str)
|
|
109
|
+
|
|
110
|
+
# Special handling for signature-like types
|
|
111
|
+
if t == "sig":
|
|
112
|
+
if isinstance(argument, SignatureTemplate):
|
|
113
|
+
return argument
|
|
114
|
+
argument = _normalize_bytes(argument)
|
|
115
|
+
if not isinstance(argument, bytes):
|
|
116
|
+
raise ArgumentTypeError(type(argument).__name__, "sig")
|
|
117
|
+
# Allowed lengths: NULLFAIL (0), Schnorr+hashtype (65), ECDSA DER+hashtype (71/72/73)
|
|
118
|
+
if len(argument) not in {0, 65, 71, 72, 73}:
|
|
119
|
+
raise ValueError(f"Expected sig length in {{0,65,71,72,73}} but got {len(argument)}")
|
|
120
|
+
return argument
|
|
121
|
+
|
|
122
|
+
if t == "datasig":
|
|
123
|
+
argument = _normalize_bytes(argument)
|
|
124
|
+
if not isinstance(argument, bytes):
|
|
125
|
+
raise ArgumentTypeError(type(argument).__name__, "datasig")
|
|
126
|
+
# Allowed lengths: NULLFAIL (0), Schnorr (64), ECDSA DER (70/71/72)
|
|
127
|
+
if len(argument) not in {0, 64, 70, 71, 72}:
|
|
128
|
+
raise ValueError(f"Expected datasig length in {{0,64,70,71,72}} but got {len(argument)}")
|
|
129
|
+
return argument
|
|
130
|
+
|
|
131
|
+
if t == "int":
|
|
132
|
+
if not isinstance(argument, int):
|
|
133
|
+
raise ArgumentTypeError(type(argument).__name__, "int")
|
|
134
|
+
return bytes(encode_script_number(argument))
|
|
135
|
+
|
|
136
|
+
if t == "bool":
|
|
137
|
+
if not isinstance(argument, bool):
|
|
138
|
+
raise ArgumentTypeError(type(argument).__name__, "bool")
|
|
139
|
+
return _encode_bool(argument)
|
|
140
|
+
|
|
141
|
+
if t == "string":
|
|
142
|
+
if not isinstance(argument, str):
|
|
143
|
+
raise ArgumentTypeError(type(argument).__name__, "string")
|
|
144
|
+
return _encode_string(argument)
|
|
145
|
+
|
|
146
|
+
if t == "pubkey":
|
|
147
|
+
if not isinstance(argument, (bytes, str)):
|
|
148
|
+
raise ArgumentTypeError(type(argument).__name__, "pubkey")
|
|
149
|
+
return _encode_pubkey(argument)
|
|
150
|
+
|
|
151
|
+
if t == "bytes":
|
|
152
|
+
# Ensure correct type first – use the full type_str (e.g., 'bytes20') in error for clarity
|
|
153
|
+
argument = _normalize_bytes(argument)
|
|
154
|
+
if not isinstance(argument, bytes):
|
|
155
|
+
raise ArgumentTypeError(type(argument).__name__, type_str)
|
|
156
|
+
# Enforce bounded length for bytesN
|
|
157
|
+
if bound is not None and len(argument) != bound:
|
|
158
|
+
raise ValueError(f"Expected bytes of length {bound} for type `{type_str}` but got {len(argument)}")
|
|
159
|
+
return argument
|
|
160
|
+
|
|
161
|
+
raise ValueError(f"Unknown type: {type_str}")
|