1use std::sync::Arc;
5
6use axum::extract::State;
7use axum::{Extension, Json};
8use axum_extra::extract::WithRejection;
9use fastcrypto::encoding::{Encoding, Hex};
10use fastcrypto::hash::HashFunction;
11use prost_types::FieldMask;
12use sui_rpc::field::FieldMaskUtil;
13use sui_rpc::proto::sui::rpc::v2::{
14 Bcs, ExecuteTransactionRequest, SimulateTransactionRequest, Transaction, UserSignature,
15 simulate_transaction_request::TransactionChecks,
16};
17
18use shared_crypto::intent::{Intent, IntentMessage};
19use sui_types::base_types::SuiAddress;
20use sui_types::crypto::{DefaultHash, SignatureScheme, ToFromBytes};
21use sui_types::digests::TransactionDigest;
22use sui_types::signature::{GenericSignature, VerifyParams};
23use sui_types::signature_verification::{
24 VerifiedDigestCache, verify_sender_signed_data_message_signatures,
25};
26use sui_types::transaction::{TransactionData, TransactionDataAPI};
27
28use crate::errors::Error;
29use crate::operations::Operations;
30use crate::types::internal_operation::{PayCoin, TransactionObjectData, TryConstructTransaction};
31use crate::types::{
32 Amount, ConstructionCombineRequest, ConstructionCombineResponse, ConstructionDeriveRequest,
33 ConstructionDeriveResponse, ConstructionHashRequest, ConstructionMetadata,
34 ConstructionMetadataRequest, ConstructionMetadataResponse, ConstructionParseRequest,
35 ConstructionParseResponse, ConstructionPayloadsRequest, ConstructionPayloadsResponse,
36 ConstructionPreprocessRequest, ConstructionPreprocessResponse, ConstructionSubmitRequest,
37 InternalOperation, MetadataOptions, SignatureType, SigningPayload, TransactionIdentifier,
38 TransactionIdentifierResponse,
39};
40use crate::{OnlineServerContext, SuiEnv};
41
42pub async fn derive(
48 Extension(env): Extension<SuiEnv>,
49 WithRejection(Json(request), _): WithRejection<Json<ConstructionDeriveRequest>, Error>,
50) -> Result<ConstructionDeriveResponse, Error> {
51 env.check_network_identifier(&request.network_identifier)?;
52 let address: SuiAddress = request.public_key.try_into()?;
53 Ok(ConstructionDeriveResponse {
54 account_identifier: address.into(),
55 })
56}
57
58pub async fn payloads(
64 Extension(env): Extension<SuiEnv>,
65 WithRejection(Json(request), _): WithRejection<Json<ConstructionPayloadsRequest>, Error>,
66) -> Result<ConstructionPayloadsResponse, Error> {
67 env.check_network_identifier(&request.network_identifier)?;
68 let metadata = request.metadata.ok_or(Error::MissingMetadata)?;
69 let address = metadata.sender;
70
71 let data = request
72 .operations
73 .into_internal()?
74 .try_into_data(metadata)?;
75 let intent_msg = IntentMessage::new(Intent::sui_transaction(), data);
76 let intent_msg_bytes = bcs::to_bytes(&intent_msg)?;
77
78 let mut hasher = DefaultHash::default();
79 hasher.update(bcs::to_bytes(&intent_msg).expect("Message serialization should not fail"));
80 let digest = hasher.finalize().digest;
81
82 Ok(ConstructionPayloadsResponse {
83 unsigned_transaction: Hex::from_bytes(&intent_msg_bytes),
84 payloads: vec![SigningPayload {
85 account_identifier: address.into(),
86 hex_bytes: Hex::encode(digest),
87 signature_type: Some(SignatureType::Ed25519),
88 }],
89 })
90}
91
92pub async fn combine(
97 Extension(env): Extension<SuiEnv>,
98 WithRejection(Json(request), _): WithRejection<Json<ConstructionCombineRequest>, Error>,
99) -> Result<ConstructionCombineResponse, Error> {
100 env.check_network_identifier(&request.network_identifier)?;
101 let unsigned_tx = request.unsigned_transaction.to_vec()?;
102 let intent_msg: IntentMessage<TransactionData> = bcs::from_bytes(&unsigned_tx)?;
103 let sig = request
104 .signatures
105 .first()
106 .ok_or_else(|| Error::MissingInput("Signature".to_string()))?;
107 let sig_bytes = sig.hex_bytes.to_vec()?;
108 let pub_key = sig.public_key.hex_bytes.to_vec()?;
109 let flag = vec![
110 match sig.signature_type {
111 SignatureType::Ed25519 => SignatureScheme::ED25519,
112 SignatureType::Ecdsa => SignatureScheme::Secp256k1,
113 }
114 .flag(),
115 ];
116
117 let signed_tx = sui_types::transaction::Transaction::from_generic_sig_data(
118 intent_msg.value,
119 vec![GenericSignature::from_bytes(
120 &[&*flag, &*sig_bytes, &*pub_key].concat(),
121 )?],
122 );
123 let place_holder_epoch = 0;
126 verify_sender_signed_data_message_signatures(
127 &signed_tx,
128 place_holder_epoch,
129 &VerifyParams::default(),
130 Arc::new(VerifiedDigestCache::new_empty()), vec![],
133 )?;
134 let signed_tx_bytes = bcs::to_bytes(&signed_tx)?;
135
136 Ok(ConstructionCombineResponse {
137 signed_transaction: Hex::from_bytes(&signed_tx_bytes),
138 })
139}
140
141pub async fn submit(
145 State(context): State<OnlineServerContext>,
146 Extension(env): Extension<SuiEnv>,
147 WithRejection(Json(request), _): WithRejection<Json<ConstructionSubmitRequest>, Error>,
148) -> Result<TransactionIdentifierResponse, Error> {
149 env.check_network_identifier(&request.network_identifier)?;
150 let signed_tx: sui_types::transaction::Transaction =
151 bcs::from_bytes(&request.signed_transaction.to_vec()?)?;
152
153 let signatures = signed_tx
154 .tx_signatures()
155 .iter()
156 .map(UserSignature::from)
157 .collect();
158
159 let tx_data = signed_tx.into_data().into_inner().intent_message.value;
160 let proto_transaction =
161 Transaction::default().with_bcs(Bcs::default().with_value(bcs::to_bytes(&tx_data)?));
162
163 let request = SimulateTransactionRequest::new(proto_transaction.clone())
167 .with_read_mask(FieldMask::from_paths(["transaction.effects.status"]))
168 .with_checks(TransactionChecks::Enabled)
169 .with_do_gas_selection(false);
170
171 let response = context
172 .client
173 .clone()
174 .execution_client()
175 .simulate_transaction(request)
176 .await?
177 .into_inner();
178
179 let effects = response.transaction().effects();
180
181 if !effects.status().success() {
182 return Err(Error::TransactionDryRunError(Box::new(
183 effects.status().error().clone(),
184 )));
185 };
186
187 let mut client = context.client.clone();
188 let mut execution_client = client.execution_client();
189
190 let exec_request = ExecuteTransactionRequest::default()
191 .with_transaction(proto_transaction)
192 .with_signatures(signatures)
193 .with_read_mask(FieldMask::from_paths(["*"]));
194
195 let grpc_response = execution_client
196 .execute_transaction(exec_request)
197 .await?
198 .into_inner();
199
200 let transaction = grpc_response.transaction();
201 let effects = transaction.effects();
202 if !effects.status().success() {
203 return Err(Error::TransactionExecutionError(Box::new(
204 effects.status().error().clone(),
205 )));
206 }
207
208 let digest = transaction
209 .digest()
210 .parse::<TransactionDigest>()
211 .map_err(|e| Error::DataError(format!("Invalid transaction digest: {}", e)))?;
212
213 Ok(TransactionIdentifierResponse {
214 transaction_identifier: TransactionIdentifier { hash: digest },
215 metadata: None,
216 })
217}
218
219pub async fn preprocess(
224 Extension(env): Extension<SuiEnv>,
225 WithRejection(Json(request), _): WithRejection<Json<ConstructionPreprocessRequest>, Error>,
226) -> Result<ConstructionPreprocessResponse, Error> {
227 env.check_network_identifier(&request.network_identifier)?;
228
229 let internal_operation = request.operations.into_internal()?;
230 let sender = internal_operation.sender();
231 let budget = request.metadata.and_then(|m| m.budget);
232 Ok(ConstructionPreprocessResponse {
233 options: Some(MetadataOptions {
234 internal_operation,
235 budget,
236 }),
237 required_public_keys: vec![sender.into()],
238 })
239}
240
241pub async fn hash(
245 Extension(env): Extension<SuiEnv>,
246 WithRejection(Json(request), _): WithRejection<Json<ConstructionHashRequest>, Error>,
247) -> Result<TransactionIdentifierResponse, Error> {
248 env.check_network_identifier(&request.network_identifier)?;
249 let tx_bytes = request.signed_transaction.to_vec()?;
250 let tx: sui_types::transaction::Transaction = bcs::from_bytes(&tx_bytes)?;
251
252 Ok(TransactionIdentifierResponse {
253 transaction_identifier: TransactionIdentifier { hash: *tx.digest() },
254 metadata: None,
255 })
256}
257
258pub async fn metadata(
264 State(mut context): State<OnlineServerContext>,
265 Extension(env): Extension<SuiEnv>,
266 WithRejection(Json(request), _): WithRejection<Json<ConstructionMetadataRequest>, Error>,
267) -> Result<ConstructionMetadataResponse, Error> {
268 env.check_network_identifier(&request.network_identifier)?;
269 let option = request.options.ok_or(Error::MissingMetadata)?;
270 let budget = option.budget;
271 let sender = option.internal_operation.sender();
272 let currency = match &option.internal_operation {
273 InternalOperation::PayCoin(PayCoin { currency, .. }) => Some(currency.clone()),
274 _ => None,
275 };
276
277 let mut gas_price = context.client.get_reference_gas_price().await?;
278 gas_price += 100;
280
281 let is_pay_sui_or_stake = matches!(
283 &option.internal_operation,
284 InternalOperation::PaySui(_) | InternalOperation::Stake(_)
285 );
286
287 let TransactionObjectData {
288 gas_coins,
289 objects,
290 party_objects,
291 total_sui_balance,
292 budget,
293 } = option
294 .internal_operation
295 .try_fetch_needed_objects(&mut context.client.clone(), Some(gas_price), budget)
296 .await?;
297
298 let extra_gas_coins = if is_pay_sui_or_stake {
303 objects.clone()
304 } else {
305 vec![]
306 };
307
308 Ok(ConstructionMetadataResponse {
309 metadata: ConstructionMetadata {
310 sender,
311 gas_coins,
312 extra_gas_coins,
313 objects,
314 party_objects,
315 total_coin_value: total_sui_balance,
316 gas_price,
317 budget,
318 currency,
319 },
320 suggested_fee: vec![Amount::new(budget as i128, None)],
321 })
322}
323
324pub async fn parse(
329 Extension(env): Extension<SuiEnv>,
330 WithRejection(Json(request), _): WithRejection<Json<ConstructionParseRequest>, Error>,
331) -> Result<ConstructionParseResponse, Error> {
332 env.check_network_identifier(&request.network_identifier)?;
333
334 let (data, sender) = if request.signed {
335 let tx: sui_types::transaction::Transaction =
336 bcs::from_bytes(&request.transaction.to_vec()?)?;
337 let intent = tx.into_data().intent_message().value.clone();
338 let sender = intent.sender();
339 (intent, sender)
340 } else {
341 let intent: IntentMessage<TransactionData> =
342 bcs::from_bytes(&request.transaction.to_vec()?)?;
343 let sender = intent.value.sender();
344 (intent.value, sender)
345 };
346 let account_identifier_signers = if request.signed {
347 vec![sender.into()]
348 } else {
349 vec![]
350 };
351 let proto_tx: Transaction = data.into();
352 let tx_kind = proto_tx
353 .kind
354 .ok_or_else(|| Error::DataError("Transaction missing kind".to_string()))?;
355 let operations = Operations::new(Operations::from_transaction(tx_kind, sender, None)?);
356 Ok(ConstructionParseResponse {
357 operations,
358 account_identifier_signers,
359 metadata: None,
360 })
361}