partner_chains_cardano_offchain/
multisig.rs1use crate::{
2 await_tx::AwaitTx,
3 cardano_keys::CardanoPaymentSigningKey,
4 csl::{
5 Costs, OgmiosUtxoExt, TransactionBuilderExt, TransactionContext, get_builder_config,
6 key_hash_address,
7 },
8 governance::GovernanceData,
9};
10use cardano_serialization_lib::{
11 Address, JsError, NetworkIdKind, PrivateKey, Transaction, TransactionBody, TransactionBuilder,
12 TransactionOutput, Value,
13};
14use ogmios_client::{
15 query_ledger_state::{QueryLedgerState, QueryUtxoByUtxoId},
16 query_network::QueryNetwork,
17 transactions::Transactions,
18};
19use serde::{Serialize, Serializer};
20use sidechain_domain::{McTxHash, UtxoId, UtxoIndex, crypto::blake2b};
21
22#[derive(Clone, Debug, Serialize)]
24#[serde(rename_all = "snake_case")]
25pub enum MultiSigSmartContractResult {
26 TransactionSubmitted(McTxHash),
28 TransactionToSign(MultiSigTransactionData),
30}
31
32impl MultiSigSmartContractResult {
33 pub fn tx_submitted(hash: [u8; 32]) -> Self {
35 Self::TransactionSubmitted(McTxHash(hash))
36 }
37}
38
39#[derive(Clone, Debug, Serialize)]
42pub struct MultiSigTransactionData {
43 pub tx_name: String,
45 pub temporary_wallet: TemporaryWalletData,
47 #[serde(serialize_with = "serialize_as_conway_tx")]
48 pub tx: Vec<u8>,
50}
51
52#[derive(Clone, Debug, Serialize)]
54pub struct TemporaryWalletData {
55 pub address: String,
57 pub public_key_hash: String,
59}
60
61pub(crate) struct TemporaryWallet {
62 pub address: cardano_serialization_lib::Address,
63 pub private_key: CardanoPaymentSigningKey,
64}
65
66impl TemporaryWallet {
67 pub(crate) fn address_bech32(&self) -> String {
68 self.address.to_bech32(None).expect("to_bech32 is safe with None prefix")
69 }
70}
71
72impl From<TemporaryWallet> for TemporaryWalletData {
73 fn from(value: TemporaryWallet) -> Self {
74 TemporaryWalletData {
75 address: value.address_bech32(),
76 public_key_hash: hex::encode(&value.private_key.to_pub_key_hash().0),
77 }
78 }
79}
80
81fn serialize_as_conway_tx<S>(tx_bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
82where
83 S: Serializer,
84{
85 let json = serde_json::json!({
86 "type": "Tx ConwayEra",
87 "description": "",
88 "cborHex": hex::encode(tx_bytes)
89 });
90 json.serialize(serializer)
91}
92
93pub(crate) async fn fund_temporary_wallet<
94 F: Fn(Costs, &TransactionContext) -> anyhow::Result<Transaction>,
95 T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
96 A: AwaitTx,
97>(
98 make_tx: &F,
99 payment_ctx: TransactionContext,
100 client: &T,
101 await_tx: &A,
102) -> anyhow::Result<TemporaryWallet> {
103 let wallet = create_temporary_wallet(payment_ctx.network)?;
104 let tx_to_estimate_costs = Costs::calculate_costs(|c| make_tx(c, &payment_ctx), client).await?;
105 let value = estimate_required_value(tx_to_estimate_costs.body(), &payment_ctx)?;
106 save_wallet_file(&wallet)?;
107 transfer_to_temporary_wallet(payment_ctx, &wallet.address, &value, client, await_tx).await?;
108 Ok(wallet)
109}
110
111fn create_temporary_wallet(network: NetworkIdKind) -> Result<TemporaryWallet, JsError> {
112 let private_key = CardanoPaymentSigningKey(PrivateKey::generate_ed25519()?);
113 let address = key_hash_address(&private_key.0.to_public().hash(), network);
114 Ok(TemporaryWallet { private_key, address })
115}
116
117fn estimate_required_value(
121 tx: TransactionBody,
122 ctx: &TransactionContext,
123) -> Result<Value, JsError> {
124 let mut change = Value::new(&0u32.into());
125 for output in tx.outputs().into_iter() {
126 if output.address() == ctx.change_address {
127 change = change.checked_add(&output.amount())?;
128 }
129 }
130
131 let mut total_input = Value::new(&0u32.into());
132 for input in tx.inputs().into_iter() {
133 if let Some(utxo) =
134 ctx.payment_key_utxos.iter().find(|utxo| utxo.to_csl_tx_input() == *input)
135 {
136 total_input = total_input.checked_add(&utxo.to_csl()?.output().amount())?;
137 }
138 }
139
140 total_input.clamped_sub(&change).checked_add(&Value::new(&5_000_000u32.into()))
141}
142
143fn save_wallet_file(wallet: &TemporaryWallet) -> Result<(), anyhow::Error> {
144 let key_bytes = wallet.private_key.to_bytes();
145 let cbor_hex = format!("5820{}", hex::encode(key_bytes));
147 let json = serde_json::json!({
148 "type": "PaymentSigningKeyShelley_ed25519",
149 "description": "Temporary wallet key generated for a MultiSigTransaction",
150 "cborHex": cbor_hex
151 });
152 let file_name = format!("{}.skey", wallet.address_bech32());
153 let file = std::fs::File::create(file_name)?;
154 serde_json::to_writer_pretty(file, &json)?;
155 Ok(())
156}
157
158async fn transfer_to_temporary_wallet<T: Transactions + QueryUtxoByUtxoId, A: AwaitTx>(
159 payment_ctx: TransactionContext,
160 address: &Address,
161 value: &Value,
162 client: &T,
163 await_tx: &A,
164) -> Result<(), anyhow::Error> {
165 let mut funding_tx_builder = TransactionBuilder::new(&get_builder_config(&payment_ctx)?);
166 funding_tx_builder.add_output(&TransactionOutput::new(address, value))?;
167 let funding_tx = funding_tx_builder.balance_update_and_build(&payment_ctx)?;
168 let tx_hash: [u8; 32] = blake2b(funding_tx.body().to_bytes().as_ref());
169 log::info!(
170 "Founding temporary wallet {} with {} in transaction: {}",
171 &address.to_bech32(None)?,
172 serde_json::to_string(&value)?,
173 &hex::encode(tx_hash)
174 );
175 client.submit_transaction(&payment_ctx.sign(&funding_tx).to_bytes()).await?;
176 await_tx.await_tx_output(client, UtxoId::new(tx_hash, 0)).await?;
177 Ok(())
178}
179
180pub(crate) async fn submit_or_create_tx_to_sign<F, T, A>(
187 governance_data: &GovernanceData,
188 payment_ctx: TransactionContext,
189 make_tx: F,
190 tx_name: &str,
191 client: &T,
192 await_tx: &A,
193) -> anyhow::Result<MultiSigSmartContractResult>
194where
195 F: Fn(Costs, &TransactionContext) -> anyhow::Result<Transaction>,
196 T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
197 A: AwaitTx,
198{
199 Ok(if governance_data.policy.is_single_key_policy_for(&payment_ctx.payment_key_hash()) {
200 MultiSigSmartContractResult::TransactionSubmitted(
201 submit_single_governance_key_tx(payment_ctx, make_tx, tx_name, client, await_tx)
202 .await?,
203 )
204 } else {
205 MultiSigSmartContractResult::TransactionToSign(
206 create_transaction_to_sign(payment_ctx, make_tx, tx_name, client, await_tx).await?,
207 )
208 })
209}
210
211async fn submit_single_governance_key_tx<F, T, A>(
212 payment_ctx: TransactionContext,
213 make_tx: F,
214 tx_name: &str,
215 client: &T,
216 await_tx: &A,
217) -> anyhow::Result<McTxHash>
218where
219 F: Fn(Costs, &TransactionContext) -> anyhow::Result<Transaction>,
220 T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
221 A: AwaitTx,
222{
223 let tx = Costs::calculate_costs(|c| make_tx(c, &payment_ctx), client).await?;
224 let signed_tx = payment_ctx.sign(&tx).to_bytes();
225 let res = client.submit_transaction(&signed_tx).await.map_err(|e| {
226 anyhow::anyhow!(
227 "Submit '{}' transaction request failed: {}, bytes: {}",
228 tx_name,
229 e,
230 hex::encode(signed_tx)
231 )
232 })?;
233 let tx_id = McTxHash(res.transaction.id);
234 log::info!("'{}' transaction submitted: {}", tx_name, hex::encode(tx_id.0));
235 await_tx
236 .await_tx_output(
237 client,
238 UtxoId { tx_hash: McTxHash(res.transaction.id), index: UtxoIndex(0) },
239 )
240 .await?;
241 Ok(tx_id)
242}
243
244async fn create_transaction_to_sign<F, T, A>(
245 payment_ctx: TransactionContext,
246 make_tx: F,
247 tx_name: &str,
248 client: &T,
249 await_tx: &A,
250) -> anyhow::Result<MultiSigTransactionData>
251where
252 F: Fn(Costs, &TransactionContext) -> anyhow::Result<Transaction>,
253 T: QueryLedgerState + Transactions + QueryNetwork + QueryUtxoByUtxoId,
254 A: AwaitTx,
255{
256 let original_ctx = payment_ctx.clone();
257 let temporary_wallet = fund_temporary_wallet(&make_tx, payment_ctx, client, await_tx)
258 .await
259 .map_err(|e| {
260 anyhow::anyhow!("Failed to create temporary wallet for '{}': {}", tx_name, e)
261 })?;
262 let temp_wallet_ctx =
263 TransactionContext::for_payment_key(&temporary_wallet.private_key, client)
264 .await?
265 .with_change_address(&original_ctx.change_address);
266 let tx =
267 Costs::calculate_costs(|c| make_tx(c, &temp_wallet_ctx), client)
268 .await
269 .map_err(|e| {
270 anyhow::anyhow!(
271 "Failed to create '{}' transaction using temporary wallet: {}",
272 tx_name,
273 e
274 )
275 })?;
276 let signed_tx_by_caller = original_ctx.sign(&tx);
277 let signed_tx = temp_wallet_ctx.sign(&signed_tx_by_caller);
278 Ok(MultiSigTransactionData {
279 tx_name: tx_name.to_owned(),
280 temporary_wallet: temporary_wallet.into(),
281 tx: signed_tx.to_bytes(),
282 })
283}