bullet_rust_sdk/
transaction_builder.rs1use base64::Engine;
33use base64::engine::general_purpose::STANDARD as BASE64;
34use bon::bon;
35use bullet_exchange_interface::transaction::{
36 Amount, Gas, PriorityFeeBips, RuntimeCall, Transaction as SignedTransaction, TxDetails,
37 UniquenessData, UnsignedTransaction as RawUnsignedTransaction, Version0,
38};
39use web_time::{SystemTime, UNIX_EPOCH};
40
41use crate::codegen::Error::ErrorResponse;
42use crate::generated::types::{SubmitTxRequest, SubmitTxResponse};
43use crate::types::CallMessage;
44use crate::{Client, Keypair, SDKError, SDKResult};
45
46pub struct UnsignedTransaction {
53 inner: RawUnsignedTransaction,
54 chain_hash: [u8; 32],
55}
56
57#[bon]
58impl UnsignedTransaction {
59 pub fn to_bytes(&self) -> SDKResult<Vec<u8>> {
64 let mut data =
65 borsh::to_vec(&self.inner).map_err(|e| SDKError::SerializationError(e.to_string()))?;
66 data.extend_from_slice(&self.chain_hash);
67 Ok(data)
68 }
69
70 #[builder]
91 pub fn new(
92 call_message: CallMessage,
93 max_fee: u128,
94 priority_fee_bips: u64,
95 gas_limit: Option<Gas>,
96 client: &Client,
97 ) -> SDKResult<UnsignedTransaction> {
98 if let Some(user_actions) = client.user_actions()
100 && let CallMessage::User(ref call) = call_message
101 && !user_actions.contains(&call.into())
102 {
103 return Err(SDKError::UnsupportedCallMessage(call_message.msg_type()));
104 }
105
106 let runtime_call = RuntimeCall::Exchange(call_message);
107 let timestamp = SystemTime::now()
108 .duration_since(UNIX_EPOCH)
109 .map_err(|_| SDKError::SystemTimeError)?
110 .as_millis() as u64;
111 let uniqueness = UniquenessData::Generation(timestamp);
112 let details = TxDetails {
113 chain_id: client.chain_id(),
114 max_fee: Amount(max_fee),
115 gas_limit,
116 max_priority_fee_bips: PriorityFeeBips(priority_fee_bips),
117 };
118
119 Ok(UnsignedTransaction {
120 inner: RawUnsignedTransaction { runtime_call, uniqueness, details },
121 chain_hash: client.chain_hash(),
122 })
123 }
124}
125
126pub struct Transaction;
133
134#[bon]
135impl Transaction {
136 #[builder]
154 pub fn new(
155 call_message: CallMessage,
156 max_fee: Option<u128>,
157 priority_fee_bips: Option<u64>,
158 gas_limit: Option<Gas>,
159 signer: Option<&Keypair>,
160 client: &Client,
161 ) -> SDKResult<SignedTransaction> {
162 let signer = signer.or_else(|| client.keypair()).ok_or(SDKError::MissingKeypair)?;
163
164 let max_fee = max_fee.unwrap_or_else(|| client.max_fee().0);
165 let priority_fee_bips =
166 priority_fee_bips.unwrap_or_else(|| client.max_priority_fee_bips().0);
167 let gas_limit = gas_limit.or_else(|| client.gas_limit());
168
169 let unsigned = UnsignedTransaction::builder()
170 .call_message(call_message)
171 .max_fee(max_fee)
172 .priority_fee_bips(priority_fee_bips)
173 .maybe_gas_limit(gas_limit)
174 .client(client)
175 .build()?;
176
177 let data = unsigned.to_bytes()?;
178 let sig_bytes: [u8; 64] = signer
179 .sign(&data)
180 .try_into()
181 .map_err(|v: Vec<u8>| SDKError::InvalidSignatureLength(v.len()))?;
182 let pub_key: [u8; 32] = signer
183 .public_key()
184 .try_into()
185 .map_err(|v: Vec<u8>| SDKError::InvalidPublicKeyLength(v.len()))?;
186
187 Ok(Self::from_parts(unsigned, sig_bytes, pub_key))
188 }
189
190 pub fn from_parts(
195 tx: UnsignedTransaction,
196 signature: [u8; 64],
197 pub_key: [u8; 32],
198 ) -> SignedTransaction {
199 let RawUnsignedTransaction { runtime_call, uniqueness, details } = tx.inner;
200 SignedTransaction::V0(Version0 { runtime_call, uniqueness, details, pub_key, signature })
201 }
202
203 pub fn to_bytes(signed: &SignedTransaction) -> SDKResult<Vec<u8>> {
207 borsh::to_vec(signed).map_err(|e| SDKError::SerializationError(e.to_string()))
208 }
209
210 pub fn to_base64(signed: &SignedTransaction) -> SDKResult<String> {
212 let bytes = Self::to_bytes(signed)?;
213 Ok(BASE64.encode(&bytes))
214 }
215}
216
217impl Client {
219 pub async fn send_transaction(
223 &self,
224 signed: &SignedTransaction,
225 ) -> SDKResult<SubmitTxResponse> {
226 let body = Transaction::to_base64(signed)?;
227 let response = self.client().submit_tx(&SubmitTxRequest { body }).await;
228 match response {
229 Err(ErrorResponse(response)) if response.status() == 401 => {
230 let inner = response.into_inner();
231 if inner.message.contains("Invalid signature") {
232 self.update_schema().await?;
233 return Err(SDKError::TransactionOutdated);
236 }
237 Err(SDKError::ApiError(inner))
238 }
239 Ok(r) => Ok(r.into_inner()),
240 Err(e) => Err(e.into()),
241 }
242 }
243}
244
245#[cfg(test)]
248mod tests {
249 use bullet_exchange_interface::message::PublicAction;
250 use bullet_exchange_interface::transaction::{
251 Amount, PriorityFeeBips, RuntimeCall, TxDetails, UniquenessData,
252 };
253
254 use super::*;
255
256 fn test_unsigned_tx() -> UnsignedTransaction {
257 let inner = RawUnsignedTransaction {
258 runtime_call: RuntimeCall::Exchange(CallMessage::Public(PublicAction::ApplyFunding {
259 addresses: vec![],
260 })),
261 uniqueness: UniquenessData::Generation(12345),
262 details: TxDetails {
263 chain_id: 1,
264 max_fee: Amount(10_000_000),
265 gas_limit: None,
266 max_priority_fee_bips: PriorityFeeBips(0),
267 },
268 };
269 UnsignedTransaction { inner, chain_hash: [42u8; 32] }
270 }
271
272 #[test]
273 fn to_bytes_is_borsh_plus_chain_hash() {
274 let unsigned = test_unsigned_tx();
275 let bytes = unsigned.to_bytes().unwrap();
276
277 let mut expected = borsh::to_vec(&unsigned.inner).unwrap();
278 expected.extend_from_slice(&unsigned.chain_hash);
279 assert_eq!(bytes, expected);
280 }
281
282 #[test]
283 fn from_parts_matches_direct_construction() {
284 let keypair = Keypair::generate();
285 let unsigned = test_unsigned_tx();
286
287 let signable = unsigned.to_bytes().unwrap();
288 let sig: [u8; 64] = keypair.sign(&signable).try_into().unwrap();
289 let pk: [u8; 32] = keypair.public_key().try_into().unwrap();
290
291 let chain_hash = unsigned.chain_hash;
293 let inner_clone = RawUnsignedTransaction {
294 runtime_call: unsigned.inner.runtime_call.clone(),
295 uniqueness: unsigned.inner.uniqueness.clone(),
296 details: unsigned.inner.details.clone(),
297 };
298 let assembled = Transaction::from_parts(unsigned, sig, pk);
299
300 let mut data = borsh::to_vec(&inner_clone).unwrap();
302 data.extend_from_slice(&chain_hash);
303 let sig2: [u8; 64] = keypair.sign(&data).try_into().unwrap();
304 let direct = SignedTransaction::V0(Version0 {
305 runtime_call: inner_clone.runtime_call,
306 uniqueness: inner_clone.uniqueness,
307 details: inner_clone.details,
308 pub_key: pk,
309 signature: sig2,
310 });
311
312 assert_eq!(assembled, direct);
313 assert_eq!(
314 Transaction::to_bytes(&assembled).unwrap(),
315 Transaction::to_bytes(&direct).unwrap(),
316 );
317 }
318
319 #[test]
320 fn signed_to_bytes_roundtrips() {
321 let keypair = Keypair::generate();
322 let unsigned = test_unsigned_tx();
323
324 let signable = unsigned.to_bytes().unwrap();
325 let sig: [u8; 64] = keypair.sign(&signable).try_into().unwrap();
326 let pk: [u8; 32] = keypair.public_key().try_into().unwrap();
327 let signed = Transaction::from_parts(unsigned, sig, pk);
328
329 let bytes = Transaction::to_bytes(&signed).unwrap();
330 assert!(!bytes.is_empty());
331
332 let deserialized: SignedTransaction =
333 borsh::from_slice(&bytes).expect("should deserialize");
334 assert_eq!(bytes, Transaction::to_bytes(&deserialized).unwrap());
335 }
336
337 #[test]
338 fn to_base64_is_nonempty() {
339 let keypair = Keypair::generate();
340 let unsigned = test_unsigned_tx();
341
342 let signable = unsigned.to_bytes().unwrap();
343 let sig: [u8; 64] = keypair.sign(&signable).try_into().unwrap();
344 let pk: [u8; 32] = keypair.public_key().try_into().unwrap();
345 let signed = Transaction::from_parts(unsigned, sig, pk);
346
347 assert!(!Transaction::to_base64(&signed).unwrap().is_empty());
348 }
349
350 #[cfg(feature = "integration")]
351 mod integration {
352 use bullet_exchange_interface::message::PublicAction;
353
354 use super::*;
355 use crate::Network;
356
357 #[tokio::test]
358 async fn test_builder_build() {
359 let network = std::env::var("BULLET_API_ENDPOINT")
360 .map(|e| Network::from(e.as_str()))
361 .unwrap_or(Network::Mainnet);
362
363 let client =
364 Client::builder().network(network).build().await.expect("could not connect");
365 let keypair = Keypair::generate();
366
367 let call_msg = CallMessage::Public(PublicAction::ApplyFunding { addresses: vec![] });
368
369 let signed = Transaction::builder()
370 .call_message(call_msg)
371 .max_fee(10_000_000)
372 .signer(&keypair)
373 .client(&client)
374 .build()
375 .expect("Failed to build transaction");
376
377 assert!(!Transaction::to_base64(&signed).unwrap().is_empty());
378 }
379 }
380}