1use std::sync::Arc;
8
9use axum::{Extension, Json, extract::State};
10use axum_extra::extract::WithRejection;
11use fastcrypto::{
12 encoding::{Encoding, Hex},
13 hash::HashFunction,
14};
15use futures::StreamExt;
16use iota_json_rpc_types::{
17 IotaObjectDataOptions, IotaTransactionBlockEffectsAPI, IotaTransactionBlockResponseOptions,
18 StakeStatus,
19};
20use iota_sdk::rpc_types::IotaExecutionStatus;
21use iota_types::{
22 base_types::IotaAddress,
23 crypto::{DefaultHash, SignatureScheme, ToFromBytes},
24 error::IotaError,
25 signature::{GenericSignature, VerifyParams},
26 signature_verification::{VerifiedDigestCache, verify_sender_signed_data_message_signatures},
27 transaction::{Transaction, TransactionData, TransactionDataAPI},
28};
29use shared_crypto::intent::{Intent, IntentMessage};
30
31use crate::{
32 IotaEnv, OnlineServerContext,
33 errors::Error,
34 operations::Operations,
35 types::{
36 Amount, ConstructionCombineRequest, ConstructionCombineResponse, ConstructionDeriveRequest,
37 ConstructionDeriveResponse, ConstructionHashRequest, ConstructionMetadata,
38 ConstructionMetadataRequest, ConstructionMetadataResponse, ConstructionParseRequest,
39 ConstructionParseResponse, ConstructionPayloadsRequest, ConstructionPayloadsResponse,
40 ConstructionPreprocessRequest, ConstructionPreprocessResponse, ConstructionSubmitRequest,
41 InternalOperation, MetadataOptions, SignatureType, SigningPayload, TransactionIdentifier,
42 TransactionIdentifierResponse,
43 },
44};
45
46pub async fn derive(
50 Extension(env): Extension<IotaEnv>,
51 WithRejection(Json(request), _): WithRejection<Json<ConstructionDeriveRequest>, Error>,
52) -> Result<ConstructionDeriveResponse, Error> {
53 env.check_network_identifier(&request.network_identifier)?;
54 let address: IotaAddress = request.public_key.try_into()?;
55 Ok(ConstructionDeriveResponse {
56 account_identifier: address.into(),
57 })
58}
59
60pub async fn payloads(
67 Extension(env): Extension<IotaEnv>,
68 WithRejection(Json(request), _): WithRejection<Json<ConstructionPayloadsRequest>, Error>,
69) -> Result<ConstructionPayloadsResponse, Error> {
70 env.check_network_identifier(&request.network_identifier)?;
71 let metadata = request.metadata.ok_or(Error::MissingMetadata)?;
72 let address = metadata.sender;
73
74 let data = request
75 .operations
76 .into_internal()?
77 .try_into_data(metadata)?;
78 let intent_msg = IntentMessage::new(Intent::iota_transaction(), data);
79 let intent_msg_bytes = bcs::to_bytes(&intent_msg)?;
80
81 let mut hasher = DefaultHash::default();
82 hasher.update(bcs::to_bytes(&intent_msg).expect("Message serialization should not fail"));
83 let digest = hasher.finalize().digest;
84
85 Ok(ConstructionPayloadsResponse {
86 unsigned_transaction: Hex::from_bytes(&intent_msg_bytes),
87 payloads: vec![SigningPayload {
88 account_identifier: address.into(),
89 hex_bytes: Hex::encode(digest),
90 signature_type: Some(SignatureType::Ed25519),
91 }],
92 })
93}
94
95pub async fn combine(
100 Extension(env): Extension<IotaEnv>,
101 WithRejection(Json(request), _): WithRejection<Json<ConstructionCombineRequest>, Error>,
102) -> Result<ConstructionCombineResponse, Error> {
103 env.check_network_identifier(&request.network_identifier)?;
104 let unsigned_tx = request.unsigned_transaction.to_vec()?;
105 let intent_msg: IntentMessage<TransactionData> = bcs::from_bytes(&unsigned_tx)?;
106 let sig = request
107 .signatures
108 .first()
109 .ok_or_else(|| Error::MissingInput("Signature".to_string()))?;
110 let sig_bytes = sig.hex_bytes.to_vec()?;
111 let pub_key = sig.public_key.hex_bytes.to_vec()?;
112 let flag = vec![
113 match sig.signature_type {
114 SignatureType::Ed25519 => SignatureScheme::ED25519,
115 SignatureType::Ecdsa => SignatureScheme::Secp256k1,
116 }
117 .flag(),
118 ];
119
120 let signed_tx = Transaction::from_generic_sig_data(
121 intent_msg.value,
122 vec![GenericSignature::from_bytes(
123 &[&*flag, &*sig_bytes, &*pub_key].concat(),
124 )?],
125 );
126 let place_holder_epoch = 0;
130 verify_sender_signed_data_message_signatures(
131 &signed_tx,
132 place_holder_epoch,
133 &VerifyParams::default(),
134 Arc::new(VerifiedDigestCache::new_empty()), )?;
136 let signed_tx_bytes = bcs::to_bytes(&signed_tx)?;
137
138 Ok(ConstructionCombineResponse {
139 signed_transaction: Hex::from_bytes(&signed_tx_bytes),
140 })
141}
142
143pub async fn submit(
147 State(context): State<OnlineServerContext>,
148 Extension(env): Extension<IotaEnv>,
149 WithRejection(Json(request), _): WithRejection<Json<ConstructionSubmitRequest>, Error>,
150) -> Result<TransactionIdentifierResponse, Error> {
151 env.check_network_identifier(&request.network_identifier)?;
152 let signed_tx: Transaction = bcs::from_bytes(&request.signed_transaction.to_vec()?)?;
153
154 let tx_data = signed_tx.data().transaction_data().clone();
159 let dry_run = context
160 .client
161 .read_api()
162 .dry_run_transaction_block(tx_data)
163 .await?;
164 if let IotaExecutionStatus::Failure { error } = dry_run.effects.status() {
165 return Err(Error::TransactionDryRun(error.clone()));
166 };
167
168 let response = context
169 .client
170 .quorum_driver_api()
171 .execute_transaction_block(
172 signed_tx,
173 IotaTransactionBlockResponseOptions::new()
174 .with_input()
175 .with_effects()
176 .with_balance_changes(),
177 None,
178 )
179 .await?;
180
181 if let IotaExecutionStatus::Failure { error } = response
182 .effects
183 .expect("Execute transaction should return effects")
184 .status()
185 {
186 return Err(Error::TransactionExecution(error.to_string()));
187 }
188
189 Ok(TransactionIdentifierResponse {
190 transaction_identifier: TransactionIdentifier {
191 hash: response.digest,
192 },
193 metadata: None,
194 })
195}
196
197pub async fn preprocess(
203 Extension(env): Extension<IotaEnv>,
204 WithRejection(Json(request), _): WithRejection<Json<ConstructionPreprocessRequest>, Error>,
205) -> Result<ConstructionPreprocessResponse, Error> {
206 env.check_network_identifier(&request.network_identifier)?;
207
208 let internal_operation = request.operations.into_internal()?;
209 let sender = internal_operation.sender();
210 let budget = request.metadata.and_then(|m| m.budget);
211
212 Ok(ConstructionPreprocessResponse {
213 options: Some(MetadataOptions {
214 internal_operation,
215 budget,
216 }),
217 required_public_keys: vec![sender.into()],
218 })
219}
220
221pub async fn hash(
226 Extension(env): Extension<IotaEnv>,
227 WithRejection(Json(request), _): WithRejection<Json<ConstructionHashRequest>, Error>,
228) -> Result<TransactionIdentifierResponse, Error> {
229 env.check_network_identifier(&request.network_identifier)?;
230 let tx_bytes = request.signed_transaction.to_vec()?;
231 let tx: Transaction = bcs::from_bytes(&tx_bytes)?;
232
233 Ok(TransactionIdentifierResponse {
234 transaction_identifier: TransactionIdentifier { hash: *tx.digest() },
235 metadata: None,
236 })
237}
238
239pub async fn metadata(
245 State(context): State<OnlineServerContext>,
246 Extension(env): Extension<IotaEnv>,
247 WithRejection(Json(request), _): WithRejection<Json<ConstructionMetadataRequest>, Error>,
248) -> Result<ConstructionMetadataResponse, Error> {
249 env.check_network_identifier(&request.network_identifier)?;
250 let option = request.options.ok_or(Error::MissingMetadata)?;
251 let budget = option.budget;
252 let sender = option.internal_operation.sender();
253 let mut gas_price = context
254 .client
255 .governance_api()
256 .get_reference_gas_price()
257 .await?;
258 gas_price += 100;
260
261 let (total_required_amount, objects) = match &option.internal_operation {
263 InternalOperation::PayIota { amounts, .. } => {
264 let amount = amounts.iter().sum::<u64>();
265 (Some(amount), vec![])
266 }
267 InternalOperation::Stake { amount, .. } => (*amount, vec![]),
268 InternalOperation::WithdrawStake { sender, stake_ids } => {
269 let stake_ids = if stake_ids.is_empty() {
270 context
272 .client
273 .governance_api()
274 .get_stakes(*sender)
275 .await?
276 .into_iter()
277 .flat_map(|s| {
278 s.stakes.into_iter().filter_map(|s| {
279 if let StakeStatus::Active { .. } = s.status {
280 Some(s.staked_iota_id)
281 } else {
282 None
283 }
284 })
285 })
286 .collect()
287 } else {
288 stake_ids.clone()
289 };
290
291 if stake_ids.is_empty() {
292 return Err(Error::InvalidInput("No active stake to withdraw".into()));
293 }
294
295 let responses = context
296 .client
297 .read_api()
298 .multi_get_object_with_options(stake_ids, IotaObjectDataOptions::default())
299 .await?;
300 let stake_refs = responses
301 .into_iter()
302 .map(|stake| stake.into_object().map(|o| o.object_ref()))
303 .collect::<Result<Vec<_>, _>>()
304 .map_err(IotaError::from)?;
305
306 (Some(0), stake_refs)
307 }
308 };
309
310 let budget = match budget {
312 Some(budget) => budget,
313 None => {
314 let data = option
318 .internal_operation
319 .try_into_data(ConstructionMetadata {
320 sender,
321 coins: vec![],
322 objects: objects.clone(),
323 total_coin_value: 1_000_000_000 * 1_000_000_000,
325 gas_price,
326 budget: 50_000_000_000,
328 })?;
329
330 let dry_run = context
331 .client
332 .read_api()
333 .dry_run_transaction_block(data)
334 .await?;
335 let effects = dry_run.effects;
336
337 if let IotaExecutionStatus::Failure { error } = effects.status() {
338 return Err(Error::TransactionDryRun(error.to_string()));
339 }
340 effects.gas_cost_summary().computation_cost + effects.gas_cost_summary().storage_cost
341 }
342 };
343
344 let coins = if let Some(amount) = total_required_amount {
346 let total_amount = amount + budget;
347 context
348 .client
349 .coin_read_api()
350 .select_coins(sender, None, total_amount.into(), vec![])
351 .await
352 .ok()
353 } else {
354 None
355 };
356
357 let coins = if let Some(coins) = coins {
360 coins
361 } else {
362 context
363 .client
364 .coin_read_api()
365 .get_coins_stream(sender, None)
366 .collect::<Vec<_>>()
367 .await
368 };
369
370 let total_coin_value = coins.iter().fold(0, |sum, coin| sum + coin.balance);
371
372 let coins = coins
373 .into_iter()
374 .map(|c| c.object_ref())
375 .collect::<Vec<_>>();
376
377 Ok(ConstructionMetadataResponse {
378 metadata: ConstructionMetadata {
379 sender,
380 coins,
381 objects,
382 total_coin_value,
383 gas_price,
384 budget,
385 },
386 suggested_fee: vec![Amount::new(budget as i128)],
387 })
388}
389
390pub async fn parse(
395 Extension(env): Extension<IotaEnv>,
396 WithRejection(Json(request), _): WithRejection<Json<ConstructionParseRequest>, Error>,
397) -> Result<ConstructionParseResponse, Error> {
398 env.check_network_identifier(&request.network_identifier)?;
399
400 let (tx_data, tx_digest) = if request.signed {
401 let tx: Transaction = bcs::from_bytes(&request.transaction.to_vec()?)?;
402 let tx_digest = *tx.digest();
403
404 (
405 tx.into_data().intent_message().value.clone(),
406 Some(tx_digest),
407 )
408 } else {
409 let intent: IntentMessage<TransactionData> =
410 bcs::from_bytes(&request.transaction.to_vec()?)?;
411
412 (intent.value, None)
413 };
414
415 let account_identifier_signers = if request.signed {
416 vec![tx_data.sender().into()]
417 } else {
418 vec![]
419 };
420
421 let operations = Operations::from_transaction_data(tx_data, tx_digest)?;
422
423 Ok(ConstructionParseResponse {
424 operations,
425 account_identifier_signers,
426 metadata: None,
427 })
428}