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;
11
12use shared_crypto::intent::{Intent, IntentMessage};
13use sui_json_rpc_types::{SuiTransactionBlockEffectsAPI, SuiTransactionBlockResponseOptions};
14use sui_sdk::rpc_types::SuiExecutionStatus;
15use sui_types::base_types::SuiAddress;
16use sui_types::crypto::{DefaultHash, SignatureScheme, ToFromBytes};
17use sui_types::signature::{GenericSignature, VerifyParams};
18use sui_types::signature_verification::{
19 VerifiedDigestCache, verify_sender_signed_data_message_signatures,
20};
21use sui_types::transaction::{Transaction, TransactionData, TransactionDataAPI};
22
23use crate::errors::Error;
24use crate::types::internal_operation::{PayCoin, TransactionObjectData, TryConstructTransaction};
25use crate::types::{
26 Amount, ConstructionCombineRequest, ConstructionCombineResponse, ConstructionDeriveRequest,
27 ConstructionDeriveResponse, ConstructionHashRequest, ConstructionMetadata,
28 ConstructionMetadataRequest, ConstructionMetadataResponse, ConstructionParseRequest,
29 ConstructionParseResponse, ConstructionPayloadsRequest, ConstructionPayloadsResponse,
30 ConstructionPreprocessRequest, ConstructionPreprocessResponse, ConstructionSubmitRequest,
31 InternalOperation, MetadataOptions, SignatureType, SigningPayload, TransactionIdentifier,
32 TransactionIdentifierResponse,
33};
34use crate::{OnlineServerContext, SuiEnv};
35
36pub async fn derive(
42 Extension(env): Extension<SuiEnv>,
43 WithRejection(Json(request), _): WithRejection<Json<ConstructionDeriveRequest>, Error>,
44) -> Result<ConstructionDeriveResponse, Error> {
45 env.check_network_identifier(&request.network_identifier)?;
46 let address: SuiAddress = request.public_key.try_into()?;
47 Ok(ConstructionDeriveResponse {
48 account_identifier: address.into(),
49 })
50}
51
52pub async fn payloads(
58 Extension(env): Extension<SuiEnv>,
59 WithRejection(Json(request), _): WithRejection<Json<ConstructionPayloadsRequest>, Error>,
60) -> Result<ConstructionPayloadsResponse, Error> {
61 env.check_network_identifier(&request.network_identifier)?;
62 let metadata = request.metadata.ok_or(Error::MissingMetadata)?;
63 let address = metadata.sender;
64
65 let data = request
66 .operations
67 .into_internal()?
68 .try_into_data(metadata)?;
69 let intent_msg = IntentMessage::new(Intent::sui_transaction(), data);
70 let intent_msg_bytes = bcs::to_bytes(&intent_msg)?;
71
72 let mut hasher = DefaultHash::default();
73 hasher.update(bcs::to_bytes(&intent_msg).expect("Message serialization should not fail"));
74 let digest = hasher.finalize().digest;
75
76 Ok(ConstructionPayloadsResponse {
77 unsigned_transaction: Hex::from_bytes(&intent_msg_bytes),
78 payloads: vec![SigningPayload {
79 account_identifier: address.into(),
80 hex_bytes: Hex::encode(digest),
81 signature_type: Some(SignatureType::Ed25519),
82 }],
83 })
84}
85
86pub async fn combine(
91 Extension(env): Extension<SuiEnv>,
92 WithRejection(Json(request), _): WithRejection<Json<ConstructionCombineRequest>, Error>,
93) -> Result<ConstructionCombineResponse, Error> {
94 env.check_network_identifier(&request.network_identifier)?;
95 let unsigned_tx = request.unsigned_transaction.to_vec()?;
96 let intent_msg: IntentMessage<TransactionData> = bcs::from_bytes(&unsigned_tx)?;
97 let sig = request
98 .signatures
99 .first()
100 .ok_or_else(|| Error::MissingInput("Signature".to_string()))?;
101 let sig_bytes = sig.hex_bytes.to_vec()?;
102 let pub_key = sig.public_key.hex_bytes.to_vec()?;
103 let flag = vec![
104 match sig.signature_type {
105 SignatureType::Ed25519 => SignatureScheme::ED25519,
106 SignatureType::Ecdsa => SignatureScheme::Secp256k1,
107 }
108 .flag(),
109 ];
110
111 let signed_tx = Transaction::from_generic_sig_data(
112 intent_msg.value,
113 vec![GenericSignature::from_bytes(
114 &[&*flag, &*sig_bytes, &*pub_key].concat(),
115 )?],
116 );
117 let place_holder_epoch = 0;
120 verify_sender_signed_data_message_signatures(
121 &signed_tx,
122 place_holder_epoch,
123 &VerifyParams::default(),
124 Arc::new(VerifiedDigestCache::new_empty()), )?;
126 let signed_tx_bytes = bcs::to_bytes(&signed_tx)?;
127
128 Ok(ConstructionCombineResponse {
129 signed_transaction: Hex::from_bytes(&signed_tx_bytes),
130 })
131}
132
133pub async fn submit(
137 State(context): State<OnlineServerContext>,
138 Extension(env): Extension<SuiEnv>,
139 WithRejection(Json(request), _): WithRejection<Json<ConstructionSubmitRequest>, Error>,
140) -> Result<TransactionIdentifierResponse, Error> {
141 env.check_network_identifier(&request.network_identifier)?;
142 let signed_tx: Transaction = bcs::from_bytes(&request.signed_transaction.to_vec()?)?;
143
144 let tx_data = signed_tx.data().transaction_data().clone();
148 let dry_run = context
149 .client
150 .read_api()
151 .dry_run_transaction_block(tx_data)
152 .await?;
153 if let SuiExecutionStatus::Failure { error } = dry_run.effects.status() {
154 return Err(Error::TransactionDryRunError(error.clone()));
155 };
156
157 let response = context
158 .client
159 .quorum_driver_api()
160 .execute_transaction_block(
161 signed_tx,
162 SuiTransactionBlockResponseOptions::new()
163 .with_input()
164 .with_effects()
165 .with_balance_changes(),
166 None,
167 )
168 .await?;
169
170 if let SuiExecutionStatus::Failure { error } = response
171 .effects
172 .expect("Execute transaction should return effects")
173 .status()
174 {
175 return Err(Error::TransactionExecutionError(error.to_string()));
176 }
177
178 Ok(TransactionIdentifierResponse {
179 transaction_identifier: TransactionIdentifier {
180 hash: response.digest,
181 },
182 metadata: None,
183 })
184}
185
186pub async fn preprocess(
191 Extension(env): Extension<SuiEnv>,
192 WithRejection(Json(request), _): WithRejection<Json<ConstructionPreprocessRequest>, Error>,
193) -> Result<ConstructionPreprocessResponse, Error> {
194 env.check_network_identifier(&request.network_identifier)?;
195
196 let internal_operation = request.operations.into_internal()?;
197 let sender = internal_operation.sender();
198 let budget = request.metadata.and_then(|m| m.budget);
199 Ok(ConstructionPreprocessResponse {
200 options: Some(MetadataOptions {
201 internal_operation,
202 budget,
203 }),
204 required_public_keys: vec![sender.into()],
205 })
206}
207
208pub async fn hash(
212 Extension(env): Extension<SuiEnv>,
213 WithRejection(Json(request), _): WithRejection<Json<ConstructionHashRequest>, Error>,
214) -> Result<TransactionIdentifierResponse, Error> {
215 env.check_network_identifier(&request.network_identifier)?;
216 let tx_bytes = request.signed_transaction.to_vec()?;
217 let tx: Transaction = bcs::from_bytes(&tx_bytes)?;
218
219 Ok(TransactionIdentifierResponse {
220 transaction_identifier: TransactionIdentifier { hash: *tx.digest() },
221 metadata: None,
222 })
223}
224
225pub async fn metadata(
231 State(context): State<OnlineServerContext>,
232 Extension(env): Extension<SuiEnv>,
233 WithRejection(Json(request), _): WithRejection<Json<ConstructionMetadataRequest>, Error>,
234) -> Result<ConstructionMetadataResponse, Error> {
235 env.check_network_identifier(&request.network_identifier)?;
236 let option = request.options.ok_or(Error::MissingMetadata)?;
237 let budget = option.budget;
238 let sender = option.internal_operation.sender();
239 let currency = match &option.internal_operation {
240 InternalOperation::PayCoin(PayCoin { currency, .. }) => Some(currency.clone()),
241 _ => None,
242 };
243
244 let mut gas_price = context
245 .client
246 .governance_api()
247 .get_reference_gas_price()
248 .await?;
249 gas_price += 100;
251
252 let TransactionObjectData {
253 gas_coins,
254 extra_gas_coins,
255 objects,
256 total_sui_balance,
257 budget,
258 } = option
259 .internal_operation
260 .try_fetch_needed_objects(&context.client, Some(gas_price), budget)
261 .await?;
262 Ok(ConstructionMetadataResponse {
263 metadata: ConstructionMetadata {
264 sender,
265 gas_coins,
266 extra_gas_coins,
267 objects,
268 total_coin_value: total_sui_balance,
269 gas_price,
270 budget,
271 currency,
272 },
273 suggested_fee: vec![Amount::new(budget as i128, None)],
274 })
275}
276
277pub async fn parse(
282 Extension(env): Extension<SuiEnv>,
283 WithRejection(Json(request), _): WithRejection<Json<ConstructionParseRequest>, Error>,
284) -> Result<ConstructionParseResponse, Error> {
285 env.check_network_identifier(&request.network_identifier)?;
286
287 let data = if request.signed {
288 let tx: Transaction = bcs::from_bytes(&request.transaction.to_vec()?)?;
289 tx.into_data().intent_message().value.clone()
290 } else {
291 let intent: IntentMessage<TransactionData> =
292 bcs::from_bytes(&request.transaction.to_vec()?)?;
293 intent.value
294 };
295 let account_identifier_signers = if request.signed {
296 vec![data.sender().into()]
297 } else {
298 vec![]
299 };
300 let operations = data.try_into()?;
301 Ok(ConstructionParseResponse {
302 operations,
303 account_identifier_signers,
304 metadata: None,
305 })
306}