Skip to content

Commit 1931f71

Browse files
authored
Merge pull request #305 from input-output-hk/add_build_estimate_tx
feat: add build_estimate_tx for offline tx building with fee estimation
2 parents 6f2dfba + a2fec90 commit 1931f71

File tree

2 files changed

+307
-18
lines changed

2 files changed

+307
-18
lines changed

cardano_clusterlib/transaction_group.py

Lines changed: 296 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ def build_raw_tx_bare( # noqa: C901
343343
complex_proposals=complex_proposals,
344344
script_withdrawals=script_withdrawals,
345345
script_votes=script_votes,
346-
for_build=False,
346+
with_execution_units=True,
347347
)
348348

349349
grouped_args_str = " ".join(grouped_args)
@@ -954,11 +954,9 @@ def build_tx( # noqa: C901
954954
complex_proposals=complex_proposals,
955955
script_withdrawals=collected_data.script_withdrawals,
956956
script_votes=script_votes,
957-
for_build=True,
957+
with_execution_units=False,
958958
)
959959

960-
misc_args.extend(["--change-address", change_address])
961-
962960
if witness_override is not None:
963961
misc_args.extend(["--witness-override", str(witness_override)])
964962

@@ -994,12 +992,14 @@ def build_tx( # noqa: C901
994992
*helpers._prepend_flag("--metadata-cbor-file", tx_files.metadata_cbor_files),
995993
*helpers._prepend_flag("--withdrawal", withdrawal_strings),
996994
*txtools._get_return_collateral_txout_args(txouts=return_collateral_txouts),
995+
"--change-address",
996+
change_address,
997997
*misc_args,
998998
*self._clusterlib_obj.magic_args,
999999
*self._clusterlib_obj.socket_args,
10001000
]
1001-
stdout = self._clusterlib_obj.cli(cli_args).stdout.strip()
1002-
stdout_dec = stdout.decode("utf-8") if stdout else ""
1001+
out = self._clusterlib_obj.cli(cli_args)
1002+
stdout_dec = out.stdout.strip().decode("utf-8") if out.stdout else ""
10031003

10041004
# Check for the presence of fee information. No fee information was provided in older
10051005
# versions of the `build` command.
@@ -1008,6 +1008,292 @@ def build_tx( # noqa: C901
10081008
estimated_fee = int(stdout_dec.split()[-2])
10091009
elif "transaction fee" in stdout_dec:
10101010
estimated_fee = int(stdout_dec.split()[-1])
1011+
else:
1012+
fee_str = self.view_tx_dict(tx_body_file=out_file).get("fee") or ""
1013+
if fee_str.endswith("Lovelace"):
1014+
estimated_fee = int(fee_str.split()[-2])
1015+
1016+
return structs.TxRawOutput(
1017+
txins=list(collected_data.txins),
1018+
txouts=processed_txouts,
1019+
txouts_count=txouts_count,
1020+
tx_files=tx_files,
1021+
out_file=out_file,
1022+
fee=estimated_fee,
1023+
build_args=cli_args,
1024+
era=self._clusterlib_obj.era_in_use,
1025+
script_txins=script_txins,
1026+
script_withdrawals=collected_data.script_withdrawals,
1027+
script_votes=script_votes,
1028+
complex_certs=complex_certs,
1029+
complex_proposals=complex_proposals,
1030+
mint=mint,
1031+
invalid_hereafter=invalid_hereafter,
1032+
invalid_before=invalid_before,
1033+
treasury_donation=treasury_donation,
1034+
withdrawals=collected_data.withdrawals,
1035+
change_address=change_address or src_address,
1036+
return_collateral_txouts=return_collateral_txouts,
1037+
total_collateral_amount=total_collateral_amount,
1038+
readonly_reference_txins=readonly_reference_txins,
1039+
script_valid=script_valid,
1040+
required_signers=required_signers,
1041+
required_signer_hashes=required_signer_hashes,
1042+
combined_reference_txins=txtools._get_reference_txins(
1043+
readonly_reference_txins=readonly_reference_txins,
1044+
script_txins=script_txins,
1045+
mint=mint,
1046+
complex_certs=complex_certs,
1047+
script_withdrawals=script_withdrawals,
1048+
),
1049+
)
1050+
1051+
def build_estimate_tx( # noqa: C901
1052+
self,
1053+
src_address: str,
1054+
tx_name: str,
1055+
txins: structs.OptionalUTXOData = (),
1056+
txouts: structs.OptionalTxOuts = (),
1057+
readonly_reference_txins: structs.OptionalUTXOData = (),
1058+
script_txins: structs.OptionalScriptTxIn = (),
1059+
return_collateral_txouts: structs.OptionalTxOuts = (),
1060+
total_collateral_amount: int | None = None,
1061+
mint: structs.OptionalMint = (),
1062+
tx_files: structs.TxFiles | None = None,
1063+
complex_certs: structs.OptionalScriptCerts = (),
1064+
complex_proposals: structs.OptionalScriptProposals = (),
1065+
change_address: str = "",
1066+
fee_buffer: int | None = None,
1067+
required_signers: itp.OptionalFiles = (),
1068+
required_signer_hashes: tp.Optional[list[str]] = None,
1069+
withdrawals: structs.OptionalTxOuts = (),
1070+
script_withdrawals: structs.OptionalScriptWithdrawals = (),
1071+
script_votes: structs.OptionalScriptVotes = (),
1072+
deposit: int | None = None,
1073+
current_treasury_value: int | None = None,
1074+
treasury_donation: int | None = None,
1075+
invalid_hereafter: int | None = None,
1076+
invalid_before: int | None = None,
1077+
script_valid: bool = True,
1078+
src_addr_utxos: list[structs.UTXOData] | None = None,
1079+
witness_count_add: int = 0,
1080+
byron_witness_count: int = 0,
1081+
reference_script_size: int = 0,
1082+
join_txouts: bool = True,
1083+
destination_dir: itp.FileType = ".",
1084+
skip_asset_balancing: bool = True,
1085+
) -> structs.TxRawOutput:
1086+
"""Build a balanced transaction without access to a live node.
1087+
1088+
Args:
1089+
src_address: An address used for fee and inputs (if inputs not specified by `txins`).
1090+
tx_name: A name of the transaction.
1091+
txins: An iterable of `structs.UTXOData`, specifying input UTxOs (optional).
1092+
txouts: A list (iterable) of `TxOuts`, specifying transaction outputs (optional).
1093+
readonly_reference_txins: An iterable of `structs.UTXOData`, specifying input
1094+
UTxOs to be referenced and used as readonly (optional).
1095+
script_txins: An iterable of `ScriptTxIn`, specifying input script UTxOs (optional).
1096+
return_collateral_txouts: A list (iterable) of `TxOuts`, specifying transaction outputs
1097+
for excess collateral (optional).
1098+
total_collateral_amount: An integer indicating the total amount of collateral
1099+
(optional).
1100+
mint: An iterable of `Mint`, specifying script minting data (optional).
1101+
tx_files: A `structs.TxFiles` data container containing files needed for the transaction
1102+
(optional).
1103+
complex_certs: An iterable of `ComplexCert`, specifying certificates script data
1104+
(optional).
1105+
complex_proposals: An iterable of `ComplexProposal`, specifying proposals script data
1106+
(optional).
1107+
change_address: A string with address where ADA in excess of the transaction fee
1108+
will go to (`src_address` by default).
1109+
fee_buffer: A buffer for fee amount (optional).
1110+
required_signers: An iterable of filepaths of the signing keys whose signatures
1111+
are required (optional).
1112+
required_signer_hashes: A list of hashes of the signing keys whose signatures
1113+
are required (optional).
1114+
withdrawals: A list (iterable) of `TxOuts`, specifying reward withdrawals (optional).
1115+
script_withdrawals: An iterable of `ScriptWithdrawal`, specifying withdrawal script
1116+
data (optional).
1117+
script_votes: An iterable of `ScriptVote`, specifying vote script data (optional).
1118+
deposit: A deposit amount needed by the transaction (optional).
1119+
current_treasury_value: The current treasury value (optional).
1120+
treasury_donation: A donation to the treasury to perform (optional).
1121+
invalid_hereafter: A last block when the transaction is still valid (optional).
1122+
invalid_before: A first block when the transaction is valid (optional).
1123+
script_valid: A bool indicating that the script is valid (True by default).
1124+
src_addr_utxos: A list of UTxOs for the source address (optional).
1125+
witness_count_add: A number of witnesses to add - workaround to make the fee
1126+
calculation more precise.
1127+
byron_witness_count: A number of Byron witnesses (optional).
1128+
reference_script_size: A size in bytes of transaction reference scripts (optional).
1129+
join_txouts: A bool indicating whether to aggregate transaction outputs
1130+
by payment address (True by default).
1131+
destination_dir: A path to directory for storing artifacts (optional).
1132+
skip_asset_balancing: A bool indicating if assets balancing should be skipped.
1133+
1134+
Returns:
1135+
structs.TxRawOutput: A data container with transaction output details.
1136+
"""
1137+
max_txout = [o for o in txouts if o.amount == -1 and o.coin in ("", consts.DEFAULT_COIN)]
1138+
if max_txout:
1139+
if change_address:
1140+
msg = "Cannot use '-1' amount and change address at the same time."
1141+
raise AssertionError(msg)
1142+
change_address = max_txout[0].address
1143+
else:
1144+
change_address = change_address or src_address
1145+
1146+
if (treasury_donation is not None) != (current_treasury_value is not None):
1147+
msg = (
1148+
"Both `treasury_donation` and `current_treasury_value` must be specified together."
1149+
)
1150+
raise AssertionError(msg)
1151+
1152+
tx_files = tx_files or structs.TxFiles()
1153+
if tx_files.certificate_files and complex_certs:
1154+
LOGGER.warning(
1155+
"Mixing `tx_files.certificate_files` and `complex_certs`, "
1156+
"certs may come in unexpected order."
1157+
)
1158+
1159+
if tx_files.proposal_files and complex_proposals:
1160+
LOGGER.warning(
1161+
"Mixing `tx_files.proposal_files` and `complex_proposals`, "
1162+
"proposals may come in unexpected order."
1163+
)
1164+
1165+
destination_dir = pl.Path(destination_dir).expanduser()
1166+
1167+
out_file = destination_dir / f"{tx_name}_tx.body"
1168+
clusterlib_helpers._check_files_exist(out_file, clusterlib_obj=self._clusterlib_obj)
1169+
1170+
collected_data = txtools.collect_data_for_build(
1171+
clusterlib_obj=self._clusterlib_obj,
1172+
src_address=src_address,
1173+
txins=txins,
1174+
txouts=txouts,
1175+
script_txins=script_txins,
1176+
mint=mint,
1177+
tx_files=tx_files,
1178+
complex_certs=complex_certs,
1179+
complex_proposals=complex_proposals,
1180+
fee=fee_buffer or 0,
1181+
withdrawals=withdrawals,
1182+
script_withdrawals=script_withdrawals,
1183+
deposit=deposit,
1184+
treasury_donation=treasury_donation,
1185+
src_addr_utxos=src_addr_utxos,
1186+
skip_asset_balancing=skip_asset_balancing,
1187+
)
1188+
1189+
required_signer_hashes = required_signer_hashes or []
1190+
1191+
txout_args, processed_txouts, txouts_count = txtools._process_txouts(
1192+
txouts=collected_data.txouts, join_txouts=join_txouts
1193+
)
1194+
1195+
txin_strings = txtools._get_txin_strings(
1196+
txins=collected_data.txins, script_txins=script_txins
1197+
)
1198+
1199+
withdrawal_strings = [f"{x.address}+{x.amount}" for x in collected_data.withdrawals]
1200+
1201+
mint_txouts = list(itertools.chain.from_iterable(m.txouts for m in mint))
1202+
1203+
script_txins_records = list(itertools.chain.from_iterable(r.txins for r in script_txins))
1204+
combined_txins = [
1205+
*collected_data.txins,
1206+
*script_txins_records,
1207+
]
1208+
total_utxo_value = txtools.calculate_utxos_balance(utxos=combined_txins)
1209+
1210+
estimate_args = [
1211+
"--shelley-key-witnesses",
1212+
str(len(tx_files.signing_key_files) + witness_count_add),
1213+
"--byron-key-witnesses",
1214+
str(byron_witness_count),
1215+
"--reference-script-size",
1216+
str(reference_script_size),
1217+
"--total-utxo-value",
1218+
str(total_utxo_value),
1219+
]
1220+
1221+
misc_args = []
1222+
1223+
if invalid_before is not None:
1224+
misc_args.extend(["--invalid-before", str(invalid_before)])
1225+
if invalid_hereafter is not None:
1226+
misc_args.extend(["--invalid-hereafter", str(invalid_hereafter)])
1227+
1228+
if treasury_donation is not None:
1229+
misc_args.extend(["--treasury-donation", str(treasury_donation)])
1230+
1231+
if not script_valid:
1232+
misc_args.append("--script-invalid")
1233+
1234+
# There's allowed just single `--mint` argument, let's aggregate all the outputs
1235+
mint_records = [f"{m.amount} {m.coin}" for m in mint_txouts]
1236+
misc_args.extend(["--mint", "+".join(mint_records)] if mint_records else [])
1237+
1238+
for txin in readonly_reference_txins:
1239+
misc_args.extend(["--read-only-tx-in-reference", f"{txin.utxo_hash}#{txin.utxo_ix}"])
1240+
1241+
grouped_args = txtools._get_script_args(
1242+
script_txins=script_txins,
1243+
mint=mint,
1244+
complex_certs=complex_certs,
1245+
complex_proposals=complex_proposals,
1246+
script_withdrawals=collected_data.script_withdrawals,
1247+
script_votes=script_votes,
1248+
with_execution_units=True,
1249+
)
1250+
1251+
if total_collateral_amount:
1252+
misc_args.extend(["--tx-total-collateral", str(total_collateral_amount)])
1253+
1254+
if tx_files.metadata_json_files and tx_files.metadata_json_detailed_schema:
1255+
misc_args.append("--json-metadata-detailed-schema")
1256+
1257+
self._clusterlib_obj.create_pparams_file()
1258+
1259+
cli_args = [
1260+
"transaction",
1261+
"build-estimate",
1262+
"--out-file",
1263+
str(out_file),
1264+
*estimate_args,
1265+
*grouped_args,
1266+
*helpers._prepend_flag("--tx-in", txin_strings),
1267+
*txout_args,
1268+
*helpers._prepend_flag("--required-signer", required_signers),
1269+
*helpers._prepend_flag("--required-signer-hash", required_signer_hashes),
1270+
*helpers._prepend_flag("--certificate-file", tx_files.certificate_files),
1271+
*helpers._prepend_flag("--proposal-file", tx_files.proposal_files),
1272+
*helpers._prepend_flag("--vote-file", tx_files.vote_files),
1273+
*helpers._prepend_flag("--auxiliary-script-file", tx_files.auxiliary_script_files),
1274+
*helpers._prepend_flag("--metadata-json-file", tx_files.metadata_json_files),
1275+
*helpers._prepend_flag("--metadata-cbor-file", tx_files.metadata_cbor_files),
1276+
*helpers._prepend_flag("--withdrawal", withdrawal_strings),
1277+
*txtools._get_return_collateral_txout_args(txouts=return_collateral_txouts),
1278+
"--change-address",
1279+
change_address,
1280+
"--protocol-params-file",
1281+
str(self._clusterlib_obj.pparams_file),
1282+
*misc_args,
1283+
*self._clusterlib_obj.socket_args,
1284+
]
1285+
out = self._clusterlib_obj.cli(cli_args)
1286+
stdout_dec = out.stdout.strip().decode("utf-8") if out.stdout else ""
1287+
1288+
# Check for the presence of fee information. No fee information was provided in older
1289+
# versions of the `build-estimate` command. Try to get the fee information from the
1290+
# `transaction view` command if not available from the `build-estimate`.
1291+
estimated_fee = -1
1292+
fee_str = stdout_dec
1293+
if not fee_str.endswith("Lovelace"):
1294+
fee_str = self.view_tx_dict(tx_body_file=out_file).get("fee") or ""
1295+
if fee_str.endswith("Lovelace"):
1296+
estimated_fee = int(fee_str.split()[-2])
10111297

10121298
return structs.TxRawOutput(
10131299
txins=list(collected_data.txins),
@@ -1192,7 +1478,10 @@ def submit_tx_bare(self, tx_file: itp.FileType) -> str:
11921478
return txhash
11931479

11941480
def submit_tx(
1195-
self, tx_file: itp.FileType, txins: list[structs.UTXOData], wait_blocks: int | None = None
1481+
self,
1482+
tx_file: itp.FileType,
1483+
txins: list[structs.UTXOData],
1484+
wait_blocks: int | None = None,
11961485
) -> str:
11971486
"""Submit a transaction, resubmit if the transaction didn't make it to the chain.
11981487

0 commit comments

Comments
 (0)