1use std::str::FromStr;
5use std::sync::Arc;
6
7use axum::extract::State;
8use axum::{Extension, Json};
9use axum_extra::extract::WithRejection;
10use fastcrypto::encoding::{Encoding, Hex};
11use fastcrypto::hash::HashFunction;
12use prost_types::FieldMask;
13use sui_rpc::field::FieldMaskUtil;
14use sui_rpc::proto::sui::rpc::v2::{
15 ExecuteTransactionRequest, SimulateTransactionRequest, UserSignature,
16 simulate_transaction_request::TransactionChecks,
17};
18
19use shared_crypto::intent::{Intent, IntentMessage};
20use sui_types::base_types::SuiAddress;
21use sui_types::crypto::{DefaultHash, SignatureScheme, ToFromBytes};
22use sui_types::digests::TransactionDigest;
23use sui_types::signature::{GenericSignature, VerifyParams};
24use sui_types::signature_verification::{
25 VerifiedDigestCache, verify_sender_signed_data_message_signatures,
26};
27use sui_types::transaction::TransactionDataAPI;
28
29use crate::errors::Error;
30use crate::operations::reconstruct_operations;
31use crate::types::internal_operation::{PayCoin, TransactionObjectData, TryConstructTransaction};
32use crate::types::transaction_envelope;
33use crate::types::{
34 Amount, AuxData, ConstructionCombineRequest, ConstructionCombineResponse,
35 ConstructionDeriveRequest, ConstructionDeriveResponse, ConstructionHashRequest,
36 ConstructionMetadata, ConstructionMetadataRequest, ConstructionMetadataResponse,
37 ConstructionParseRequest, ConstructionParseResponse, ConstructionPayloadsRequest,
38 ConstructionPayloadsResponse, ConstructionPreprocessRequest, ConstructionPreprocessResponse,
39 ConstructionSubmitRequest, InternalOperation, MetadataOptions, RosettaTransaction,
40 SignatureType, SigningPayload, TransactionIdentifier, TransactionIdentifierResponse,
41};
42use crate::{OnlineServerContext, SuiEnv};
43use move_core_types::language_storage::TypeTag;
44
45pub async fn derive(
51 Extension(env): Extension<SuiEnv>,
52 WithRejection(Json(request), _): WithRejection<Json<ConstructionDeriveRequest>, Error>,
53) -> Result<ConstructionDeriveResponse, Error> {
54 env.check_network_identifier(&request.network_identifier)?;
55 let address: SuiAddress = request.public_key.try_into()?;
56 Ok(ConstructionDeriveResponse {
57 account_identifier: address.into(),
58 })
59}
60
61pub async fn payloads(
67 Extension(env): Extension<SuiEnv>,
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
73 let internal = request.operations.into_internal()?;
74 let data = internal.clone().try_into_data(metadata)?;
75 let wrapper = RosettaTransaction {
76 transaction: transaction_envelope::encode_inner_proto(&data),
77 signatures: vec![],
78 aux: internal.aux(),
79 };
80 let unsigned = transaction_envelope::encode(&wrapper)?;
81
82 let sender = data.sender();
83 let intent_msg = IntentMessage::new(Intent::sui_transaction(), data);
84 let mut hasher = DefaultHash::default();
85 bcs::serialize_into(&mut hasher, &intent_msg).expect("Message serialization should not fail");
86 let digest = hasher.finalize().digest;
87
88 Ok(ConstructionPayloadsResponse {
89 unsigned_transaction: unsigned,
90 payloads: vec![SigningPayload {
91 account_identifier: sender.into(),
92 hex_bytes: Hex::encode(digest),
93 signature_type: Some(SignatureType::Ed25519),
94 }],
95 })
96}
97
98pub async fn combine(
103 Extension(env): Extension<SuiEnv>,
104 WithRejection(Json(request), _): WithRejection<Json<ConstructionCombineRequest>, Error>,
105) -> Result<ConstructionCombineResponse, Error> {
106 env.check_network_identifier(&request.network_identifier)?;
107
108 let unsigned = transaction_envelope::decode(&request.unsigned_transaction)?;
109 let proto = transaction_envelope::decode_inner_proto(&unsigned.transaction)?;
110 let data = transaction_envelope::proto_to_transaction_data(proto)?;
111
112 let sig = request
113 .signatures
114 .first()
115 .ok_or_else(|| Error::MissingInput("Signature".to_string()))?;
116 let sig_bytes = sig.hex_bytes.to_vec()?;
117 let pub_key = sig.public_key.hex_bytes.to_vec()?;
118 let flag = vec![
119 match sig.signature_type {
120 SignatureType::Ed25519 => SignatureScheme::ED25519,
121 SignatureType::Ecdsa => SignatureScheme::Secp256k1,
122 }
123 .flag(),
124 ];
125 let generic_sig_bytes = [&*flag, &*sig_bytes, &*pub_key].concat();
126 let generic_sig = GenericSignature::from_bytes(&generic_sig_bytes)?;
127
128 let signed_tx =
129 sui_types::transaction::Transaction::from_generic_sig_data(data, vec![generic_sig]);
130 let place_holder_epoch = 0;
133 verify_sender_signed_data_message_signatures(
134 &signed_tx,
135 place_holder_epoch,
136 &VerifyParams::default(),
137 Arc::new(VerifiedDigestCache::new_empty()), vec![],
140 )?;
141
142 let signed_wrapper = RosettaTransaction {
146 transaction: unsigned.transaction,
147 signatures: vec![Hex::from_bytes(&generic_sig_bytes)],
148 aux: unsigned.aux,
149 };
150
151 Ok(ConstructionCombineResponse {
152 signed_transaction: transaction_envelope::encode(&signed_wrapper)?,
153 })
154}
155
156pub async fn submit(
160 State(context): State<OnlineServerContext>,
161 Extension(env): Extension<SuiEnv>,
162 WithRejection(Json(request), _): WithRejection<Json<ConstructionSubmitRequest>, Error>,
163) -> Result<TransactionIdentifierResponse, Error> {
164 env.check_network_identifier(&request.network_identifier)?;
165
166 let wrapper = transaction_envelope::decode(&request.signed_transaction)?;
167 if wrapper.signatures.is_empty() {
168 return Err(Error::DataError(
169 "cannot submit an unsigned transaction: wrapper carries no signatures".to_string(),
170 ));
171 }
172 let proto_transaction = transaction_envelope::decode_inner_proto(&wrapper.transaction)?;
175
176 let signatures = wrapper
178 .signatures
179 .iter()
180 .map(|sig_hex| {
181 let bytes = sig_hex.to_vec()?;
182 let generic = GenericSignature::from_bytes(&bytes)?;
183 Ok::<UserSignature, Error>(UserSignature::from(&generic))
184 })
185 .collect::<Result<Vec<_>, Error>>()?;
186
187 let request = SimulateTransactionRequest::new(proto_transaction.clone())
196 .with_read_mask(FieldMask::from_paths([
197 "transaction.effects.status",
198 "transaction.balance_changes",
199 ]))
200 .with_checks(TransactionChecks::Enabled)
201 .with_do_gas_selection(false);
202
203 let response = context
204 .client
205 .clone()
206 .execution_client()
207 .simulate_transaction(request)
208 .await?
209 .into_inner();
210
211 let effects = response.transaction().effects();
212
213 if !effects.status().success() {
214 return Err(Error::TransactionDryRunError(Box::new(
215 effects.status().error().clone(),
216 )));
217 };
218
219 verify_pay_coin_currency(
227 &wrapper.aux,
228 response
229 .transaction()
230 .balance_changes()
231 .iter()
232 .map(|bc| bc.coin_type()),
233 )?;
234
235 let mut client = context.client.clone();
236 let mut execution_client = client.execution_client();
237
238 let exec_request = ExecuteTransactionRequest::default()
239 .with_transaction(proto_transaction)
240 .with_signatures(signatures)
241 .with_read_mask(FieldMask::from_paths(["*"]));
242
243 let grpc_response = execution_client
244 .execute_transaction(exec_request)
245 .await?
246 .into_inner();
247
248 let transaction = grpc_response.transaction();
249 let effects = transaction.effects();
250 if !effects.status().success() {
251 return Err(Error::TransactionExecutionError(Box::new(
252 effects.status().error().clone(),
253 )));
254 }
255
256 let digest = transaction
257 .digest()
258 .parse::<TransactionDigest>()
259 .map_err(|e| Error::DataError(format!("Invalid transaction digest: {}", e)))?;
260
261 Ok(TransactionIdentifierResponse {
262 transaction_identifier: TransactionIdentifier { hash: digest },
263 metadata: None,
264 })
265}
266
267pub async fn preprocess(
272 Extension(env): Extension<SuiEnv>,
273 WithRejection(Json(request), _): WithRejection<Json<ConstructionPreprocessRequest>, Error>,
274) -> Result<ConstructionPreprocessResponse, Error> {
275 env.check_network_identifier(&request.network_identifier)?;
276
277 let internal_operation = request.operations.into_internal()?;
278 let sender = internal_operation.sender();
279 let budget = request.metadata.and_then(|m| m.budget);
280 Ok(ConstructionPreprocessResponse {
281 options: Some(MetadataOptions {
282 internal_operation,
283 budget,
284 }),
285 required_public_keys: vec![sender.into()],
286 })
287}
288
289pub async fn hash(
293 Extension(env): Extension<SuiEnv>,
294 WithRejection(Json(request), _): WithRejection<Json<ConstructionHashRequest>, Error>,
295) -> Result<TransactionIdentifierResponse, Error> {
296 env.check_network_identifier(&request.network_identifier)?;
297
298 let wrapper = transaction_envelope::decode(&request.signed_transaction)?;
299 if wrapper.signatures.is_empty() {
300 return Err(Error::DataError(
301 "cannot hash an unsigned transaction: wrapper carries no signatures".to_string(),
302 ));
303 }
304 let proto = transaction_envelope::decode_inner_proto(&wrapper.transaction)?;
305 let data = transaction_envelope::proto_to_transaction_data(proto)?;
306
307 let signatures = wrapper
312 .signatures
313 .iter()
314 .map(|sig_hex| {
315 let bytes = sig_hex.to_vec()?;
316 GenericSignature::from_bytes(&bytes).map_err(Error::from)
317 })
318 .collect::<Result<Vec<_>, Error>>()?;
319 let tx = sui_types::transaction::Transaction::from_generic_sig_data(data, signatures);
320
321 Ok(TransactionIdentifierResponse {
322 transaction_identifier: TransactionIdentifier { hash: *tx.digest() },
323 metadata: None,
324 })
325}
326
327pub async fn metadata(
333 State(mut context): State<OnlineServerContext>,
334 Extension(env): Extension<SuiEnv>,
335 WithRejection(Json(request), _): WithRejection<Json<ConstructionMetadataRequest>, Error>,
336) -> Result<ConstructionMetadataResponse, Error> {
337 env.check_network_identifier(&request.network_identifier)?;
338 let option = request.options.ok_or(Error::MissingMetadata)?;
339 let budget = option.budget;
340 let sender = option.internal_operation.sender();
341 let currency = match &option.internal_operation {
342 InternalOperation::PayCoin(PayCoin { currency, .. }) => Some(currency.clone()),
343 _ => None,
344 };
345
346 let mut gas_price = context.client.get_reference_gas_price().await?;
347 gas_price += 100;
349
350 let is_pay_sui_or_stake = matches!(
352 &option.internal_operation,
353 InternalOperation::PaySui(_) | InternalOperation::Stake(_)
354 );
355
356 let needed_objects = option
357 .internal_operation
358 .try_fetch_needed_objects(&mut context.client.clone(), Some(gas_price), budget)
359 .await?;
360
361 if needed_objects.is_gasless() {
365 gas_price = 0;
366 }
367
368 let TransactionObjectData {
369 gas_coins,
370 objects,
371 party_objects,
372 total_sui_balance,
373 budget,
374 address_balance_withdrawal,
375 fss_object_count,
376 redeem_token_amount,
377 redeem_plan,
378 bind_epoch,
379 } = needed_objects;
380
381 let extra_gas_coins = if is_pay_sui_or_stake {
386 objects.clone()
387 } else {
388 vec![]
389 };
390
391 let needs_address_balance_metadata = gas_coins.is_empty() || address_balance_withdrawal > 0;
401 let (epoch, chain_id) = if needs_address_balance_metadata {
402 let epoch = match bind_epoch {
403 Some(e) => e,
404 None => crate::get_current_epoch(&mut context.client.clone()).await?,
405 };
406 let chain_id_str =
407 sui_types::digests::CheckpointDigest::new(*context.chain_id.as_bytes()).base58_encode();
408 (Some(epoch), Some(chain_id_str))
409 } else {
410 (None, None)
411 };
412
413 Ok(ConstructionMetadataResponse {
414 metadata: ConstructionMetadata {
415 sender,
416 gas_coins,
417 extra_gas_coins,
418 objects,
419 party_objects,
420 total_coin_value: total_sui_balance,
421 gas_price,
422 budget,
423 currency,
424 address_balance_withdrawal,
425 epoch,
426 chain_id,
427 fss_object_count,
428 redeem_token_amount,
429 redeem_plan,
430 bind_epoch,
431 },
432 suggested_fee: vec![Amount::new(budget as i128, None)],
433 })
434}
435
436pub async fn parse(
441 Extension(env): Extension<SuiEnv>,
442 WithRejection(Json(request), _): WithRejection<Json<ConstructionParseRequest>, Error>,
443) -> Result<ConstructionParseResponse, Error> {
444 env.check_network_identifier(&request.network_identifier)?;
445
446 let wrapper = transaction_envelope::decode(&request.transaction)?;
452 let (aux, transaction, signed) = (wrapper.aux, wrapper.transaction, request.signed);
453
454 let proto = transaction_envelope::decode_inner_proto(&transaction)?;
455 let operations = reconstruct_operations(&proto, &aux, None)?;
456
457 let account_identifier_signers = if signed {
459 vec![
460 SuiAddress::from_str(proto.sender())
461 .map_err(|e| Error::DataError(format!("invalid transaction sender: {e}")))?
462 .into(),
463 ]
464 } else {
465 vec![]
466 };
467
468 let _ = transaction_envelope::proto_to_transaction_data(proto)?;
474
475 Ok(ConstructionParseResponse {
476 operations,
477 account_identifier_signers,
478 metadata: None,
479 })
480}
481
482fn verify_pay_coin_currency<'a>(
489 aux: &AuxData,
490 moved_coin_types: impl IntoIterator<Item = &'a str>,
491) -> Result<(), Error> {
492 let AuxData::PayCoin { currency } = aux else {
493 return Ok(());
494 };
495 let want = TypeTag::from_str(¤cy.metadata.coin_type)
496 .map_err(|e| Error::DataError(format!("invalid PayCoin currency coin_type: {e}")))?;
497 let sui = TypeTag::from_str("0x2::sui::SUI").expect("0x2::sui::SUI is a valid type tag");
498 let matched = moved_coin_types.into_iter().any(|ct| {
499 TypeTag::from_str(ct)
500 .map(|t| t == want && t != sui)
501 .unwrap_or(false)
502 });
503 if matched {
504 Ok(())
505 } else {
506 Err(Error::DataError(format!(
507 "PayCoin currency {} does not match any non-SUI balance change in the simulated \
508 transaction",
509 currency.metadata.coin_type
510 )))
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517 use crate::types::{Currency, CurrencyMetadata};
518
519 fn pay_coin(coin_type: &str) -> AuxData {
520 AuxData::PayCoin {
521 currency: Currency {
522 symbol: "USDC".to_string(),
523 decimals: 6,
524 metadata: CurrencyMetadata {
525 coin_type: coin_type.to_string(),
526 },
527 },
528 }
529 }
530
531 #[test]
535 fn test_verify_pay_coin_currency() {
536 let usdc = "0x5::usdc::USDC";
537 let aux = pay_coin(usdc);
538
539 assert!(verify_pay_coin_currency(&aux, ["0x2::sui::SUI", usdc]).is_ok());
541
542 let err = verify_pay_coin_currency(&aux, ["0x2::sui::SUI", "0x9::other::OTHER"])
545 .expect_err("mismatched currency must be rejected");
546 assert!(format!("{err:?}").contains("does not match"));
547
548 assert!(verify_pay_coin_currency(&aux, ["0x2::sui::SUI"]).is_err());
550
551 assert!(verify_pay_coin_currency(&pay_coin("0x2::sui::SUI"), ["0x2::sui::SUI"]).is_err());
554
555 assert!(verify_pay_coin_currency(&AuxData::None, std::iter::empty()).is_ok());
557 }
558}