1use sha2::{Digest, Sha256};
2use stellar_xdr::{
3 self as xdr, Hash, LedgerFootprint, Limits, OperationBody, ReadXdr, SorobanAuthorizationEntry,
4 SorobanAuthorizedFunction, SorobanResources, SorobanTransactionData, Transaction,
5 TransactionEnvelope, TransactionExt, TransactionSignaturePayload,
6 TransactionSignaturePayloadTaggedTransaction, TransactionV1Envelope, VecM, WriteXdr,
7};
8
9use soroban_rpc::{
10 AuthMode, Error, LogEvents, LogResources, ResourceConfig, SimulateTransactionResponse,
11};
12
13use crate::utils::XDR_DEPTH_LIMIT;
14
15pub async fn simulate_and_assemble_transaction(
16 client: &soroban_rpc::Client,
17 tx: &Transaction,
18 resource_config: Option<ResourceConfig>,
19 resource_fee: Option<i64>,
20 auth_mode: Option<AuthMode>,
21) -> Result<Assembled, Error> {
22 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
23 tx: tx.clone(),
24 signatures: VecM::default(),
25 });
26
27 tracing::trace!(
28 "Simulation transaction envelope: {}",
29 envelope.to_xdr_base64(Limits::depth(XDR_DEPTH_LIMIT))?
30 );
31
32 let record_auth = matches!(
35 auth_mode,
36 Some(AuthMode::Record | AuthMode::RecordAllowNonRoot)
37 );
38
39 let sim_res = client
40 .next_simulate_transaction_envelope(&envelope, auth_mode, resource_config)
41 .await?;
42 tracing::trace!("{sim_res:#?}");
43
44 if let Some(e) = &sim_res.error {
45 crate::log::event::all(&sim_res.events()?);
46 Err(Error::TransactionSimulationFailed(e.clone()))
47 } else {
48 Ok(Assembled::new(tx, sim_res, resource_fee, record_auth)?)
49 }
50}
51
52pub struct Assembled {
53 pub(crate) txn: Transaction,
54 pub(crate) sim_res: SimulateTransactionResponse,
55 pub(crate) fee_bump_fee: Option<i64>,
56}
57
58impl Assembled {
60 pub fn new(
76 txn: &Transaction,
77 sim_res: SimulateTransactionResponse,
78 resource_fee: Option<i64>,
79 record_auth: bool,
80 ) -> Result<Self, Error> {
81 assemble(txn, sim_res, resource_fee, record_auth)
82 }
83
84 pub fn hash(&self, network_passphrase: &str) -> Result<[u8; 32], xdr::Error> {
95 let signature_payload = TransactionSignaturePayload {
96 network_id: Hash(Sha256::digest(network_passphrase).into()),
97 tagged_transaction: TransactionSignaturePayloadTaggedTransaction::Tx(self.txn.clone()),
98 };
99 Ok(Sha256::digest(signature_payload.to_xdr(Limits::depth(XDR_DEPTH_LIMIT))?).into())
100 }
101
102 #[must_use]
104 pub fn transaction(&self) -> &Transaction {
105 &self.txn
106 }
107
108 #[must_use]
110 pub fn sim_response(&self) -> &SimulateTransactionResponse {
111 &self.sim_res
112 }
113
114 #[must_use]
115 pub fn fee_bump_fee(&self) -> Option<i64> {
116 self.fee_bump_fee
117 }
118
119 #[must_use]
120 pub fn bump_seq_num(mut self) -> Self {
121 self.txn.seq_num.0 += 1;
122 self
123 }
124
125 #[must_use]
128 pub fn auth_entries(&self) -> VecM<SorobanAuthorizationEntry> {
129 self.txn
130 .operations
131 .first()
132 .and_then(|op| match op.body {
133 OperationBody::InvokeHostFunction(ref body) => (matches!(
134 body.auth.first().map(|x| &x.root_invocation.function),
135 Some(&SorobanAuthorizedFunction::ContractFn(_))
136 ))
137 .then_some(body.auth.clone()),
138 _ => None,
139 })
140 .unwrap_or_default()
141 }
142
143 pub fn log(
146 &self,
147 log_events: Option<LogEvents>,
148 log_resources: Option<LogResources>,
149 ) -> Result<(), Error> {
150 if let TransactionExt::V1(SorobanTransactionData {
151 resources: resources @ SorobanResources { footprint, .. },
152 ..
153 }) = &self.txn.ext
154 {
155 if let Some(log) = log_resources {
156 log(resources);
157 }
158
159 if let Some(log) = log_events {
160 log(footprint, &[self.auth_entries()], &self.sim_res.events()?);
161 }
162 }
163 Ok(())
164 }
165
166 #[must_use]
167 pub fn requires_fee_bump(&self) -> bool {
168 self.fee_bump_fee.is_some()
169 }
170
171 #[must_use]
172 pub fn is_view(&self) -> bool {
173 let TransactionExt::V1(SorobanTransactionData {
174 resources:
175 SorobanResources {
176 footprint: LedgerFootprint { read_write, .. },
177 ..
178 },
179 ..
180 }) = &self.txn.ext
181 else {
182 return false;
183 };
184 read_write.is_empty()
185 }
186
187 #[must_use]
189 pub fn set_max_instructions(mut self, instructions: u32) -> Self {
190 if let TransactionExt::V1(SorobanTransactionData {
191 resources:
192 SorobanResources {
193 instructions: ref mut i,
194 ..
195 },
196 ..
197 }) = &mut self.txn.ext
198 {
199 tracing::trace!("setting max instructions to {instructions} from {i}");
200 *i = instructions;
201 }
202 self
203 }
204}
205
206fn assemble(
211 raw: &Transaction,
212 simulation: SimulateTransactionResponse,
213 resource_fee: Option<i64>,
214 record_auth: bool,
215) -> Result<Assembled, Error> {
216 let mut tx = raw.clone();
217
218 if tx.operations.len() != 1 {
223 return Err(Error::UnexpectedOperationCount {
224 count: tx.operations.len(),
225 });
226 }
227
228 let mut transaction_data = simulation.transaction_data()?;
229 let min_resource_fee = match resource_fee {
230 Some(rf) => {
231 tracing::trace!(
232 "overriding resource fee to {rf} (simulation suggested {})",
233 simulation.min_resource_fee
234 );
235 transaction_data.resource_fee = rf;
236 u64::try_from(rf).map_err(|_| {
240 Error::TransactionSubmissionFailed(String::from(
241 "TxMalformed - negative resource fee",
242 ))
243 })?
244 }
245 None => simulation.min_resource_fee,
247 };
248
249 let mut op = tx.operations[0].clone();
250 if let OperationBody::InvokeHostFunction(ref mut body) = &mut op.body {
251 if body.auth.is_empty() || record_auth {
255 if simulation.results.len() != 1 {
256 return Err(Error::UnexpectedSimulateTransactionResultSize {
257 length: simulation.results.len(),
258 });
259 }
260
261 let auths = simulation
262 .results
263 .iter()
264 .map(|r| {
265 VecM::try_from(
266 r.auth
267 .iter()
268 .map(|v| {
269 SorobanAuthorizationEntry::from_xdr_base64(
270 v,
271 Limits::depth(XDR_DEPTH_LIMIT),
272 )
273 })
274 .collect::<Result<Vec<_>, _>>()?,
275 )
276 })
277 .collect::<Result<Vec<_>, _>>()?;
278 if !auths.is_empty() {
279 body.auth = auths[0].clone();
280 }
281 }
282 }
283
284 let mut inclusion_fee = u64::from(raw.fee);
289 if let TransactionExt::V1(SorobanTransactionData {
290 resource_fee: existing_resource_fee,
291 ..
292 }) = &raw.ext
293 {
294 if let Ok(existing_resource_fee) = u64::try_from(*existing_resource_fee) {
295 if inclusion_fee > existing_resource_fee {
296 inclusion_fee -= existing_resource_fee;
297 }
298 }
299 }
300
301 let total_fee: u64 = inclusion_fee + min_resource_fee;
304 let mut fee_bump_fee: Option<i64> = None;
305 if let Ok(tx_fee) = u32::try_from(total_fee) {
306 tx.fee = tx_fee;
307 } else {
308 tx.fee = 0;
312 let fee_bump_fee_u64 = total_fee + inclusion_fee;
313 fee_bump_fee =
314 Some(i64::try_from(fee_bump_fee_u64).map_err(|_| Error::LargeFee(fee_bump_fee_u64))?);
315 }
316
317 tx.operations = vec![op].try_into()?;
318 tx.ext = TransactionExt::V1(transaction_data);
319 Ok(Assembled {
320 txn: tx,
321 sim_res: simulation,
322 fee_bump_fee,
323 })
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 use soroban_rpc::SimulateHostFunctionResultRaw;
331 use stellar_strkey::ed25519::PublicKey as Ed25519PublicKey;
332 use stellar_xdr::{
333 AccountId, ChangeTrustAsset, ChangeTrustOp, Hash, HostFunction, InvokeContractArgs,
334 InvokeHostFunctionOp, LedgerFootprint, Memo, MuxedAccount, Operation, Preconditions,
335 PublicKey, ScAddress, ScSymbol, ScVal, SequenceNumber, SorobanAuthorizedFunction,
336 SorobanAuthorizedInvocation, SorobanResources, SorobanTransactionData, Uint256, WriteXdr,
337 };
338
339 const SOURCE: &str = "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI";
340
341 fn transaction_data() -> SorobanTransactionData {
342 SorobanTransactionData {
343 resources: SorobanResources {
344 footprint: LedgerFootprint {
345 read_only: VecM::default(),
346 read_write: VecM::default(),
347 },
348 instructions: 0,
349 disk_read_bytes: 5,
350 write_bytes: 0,
351 },
352 resource_fee: 0,
353 ext: xdr::SorobanTransactionDataExt::V0,
354 }
355 }
356
357 fn simulation_response() -> SimulateTransactionResponse {
358 let source_bytes = Ed25519PublicKey::from_string(SOURCE).unwrap().0;
359 let fn_auth = &SorobanAuthorizationEntry {
360 credentials: xdr::SorobanCredentials::Address(xdr::SorobanAddressCredentials {
361 address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
362 source_bytes,
363 )))),
364 nonce: 0,
365 signature_expiration_ledger: 0,
366 signature: ScVal::Void,
367 }),
368 root_invocation: SorobanAuthorizedInvocation {
369 function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs {
370 contract_address: ScAddress::Contract(stellar_xdr::ContractId(Hash([0; 32]))),
371 function_name: ScSymbol("fn".try_into().unwrap()),
372 args: VecM::default(),
373 }),
374 sub_invocations: VecM::default(),
375 },
376 };
377
378 SimulateTransactionResponse {
379 min_resource_fee: 115,
380 latest_ledger: 3,
381 results: vec![SimulateHostFunctionResultRaw {
382 auth: vec![fn_auth
383 .to_xdr_base64(Limits::depth(XDR_DEPTH_LIMIT))
384 .unwrap()],
385 xdr: ScVal::U32(0)
386 .to_xdr_base64(Limits::depth(XDR_DEPTH_LIMIT))
387 .unwrap(),
388 }],
389 transaction_data: transaction_data()
390 .to_xdr_base64(Limits::depth(XDR_DEPTH_LIMIT))
391 .unwrap(),
392 ..Default::default()
393 }
394 }
395
396 fn contract_fn_auth(function_name: &str) -> SorobanAuthorizationEntry {
399 let source_bytes = Ed25519PublicKey::from_string(SOURCE).unwrap().0;
400 SorobanAuthorizationEntry {
401 credentials: xdr::SorobanCredentials::Address(xdr::SorobanAddressCredentials {
402 address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(
403 source_bytes,
404 )))),
405 nonce: 0,
406 signature_expiration_ledger: 0,
407 signature: ScVal::Void,
408 }),
409 root_invocation: SorobanAuthorizedInvocation {
410 function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs {
411 contract_address: ScAddress::Contract(stellar_xdr::ContractId(Hash([0; 32]))),
412 function_name: ScSymbol(function_name.try_into().unwrap()),
413 args: VecM::default(),
414 }),
415 sub_invocations: VecM::default(),
416 },
417 }
418 }
419
420 fn first_auth_fn_name(txn: &Transaction) -> String {
422 let OperationBody::InvokeHostFunction(ref op) = txn.operations[0].body else {
423 panic!("expected InvokeHostFunction operation");
424 };
425 let SorobanAuthorizedFunction::ContractFn(InvokeContractArgs {
426 ref function_name, ..
427 }) = op.auth[0].root_invocation.function
428 else {
429 panic!("expected ContractFn auth");
430 };
431 format!("{}", function_name.0)
432 }
433
434 fn set_auth(txn: &mut Transaction, auth: Vec<SorobanAuthorizationEntry>) {
435 let mut op = txn.operations[0].clone();
436 let OperationBody::InvokeHostFunction(ref mut body) = op.body else {
437 panic!("expected InvokeHostFunction operation");
438 };
439 body.auth = auth.try_into().unwrap();
440 txn.operations = vec![op].try_into().unwrap();
441 }
442
443 fn single_contract_fn_transaction() -> Transaction {
444 let source_bytes = Ed25519PublicKey::from_string(SOURCE).unwrap().0;
445 Transaction {
446 source_account: MuxedAccount::Ed25519(Uint256(source_bytes)),
447 fee: 100,
448 seq_num: SequenceNumber(0),
449 cond: Preconditions::None,
450 memo: Memo::None,
451 operations: vec![Operation {
452 source_account: None,
453 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
454 host_function: HostFunction::InvokeContract(InvokeContractArgs {
455 contract_address: ScAddress::Contract(stellar_xdr::ContractId(Hash(
456 [0x0; 32],
457 ))),
458 function_name: ScSymbol::default(),
459 args: VecM::default(),
460 }),
461 auth: VecM::default(),
462 }),
463 }]
464 .try_into()
465 .unwrap(),
466 ext: TransactionExt::V0,
467 }
468 }
469
470 #[test]
471 fn test_assemble_transaction_updates_tx_data_from_simulation_response() {
472 let sim = simulation_response();
473 let txn = single_contract_fn_transaction();
474 let Ok(result) = assemble(&txn, sim, None, false) else {
475 panic!("assemble failed");
476 };
477
478 assert_eq!(215, result.txn.fee);
481
482 assert_eq!(TransactionExt::V1(transaction_data()), result.txn.ext);
484 }
485
486 #[test]
487 fn test_assemble_transaction_adds_the_auth_to_the_host_function() {
488 let sim = simulation_response();
489 let txn = single_contract_fn_transaction();
490 let Ok(result) = assemble(&txn, sim, None, false) else {
491 panic!("assemble failed");
492 };
493
494 assert_eq!(1, result.txn.operations.len());
495 let OperationBody::InvokeHostFunction(ref op) = result.txn.operations[0].body else {
496 panic!("unexpected operation type: {:#?}", result.txn.operations[0]);
497 };
498
499 assert_eq!(1, op.auth.len());
500 let auth = &op.auth[0];
501
502 let xdr::SorobanAuthorizedFunction::ContractFn(xdr::InvokeContractArgs {
503 ref function_name,
504 ..
505 }) = auth.root_invocation.function
506 else {
507 panic!("unexpected function type");
508 };
509 assert_eq!("fn".to_string(), format!("{}", function_name.0));
510
511 let xdr::SorobanCredentials::Address(xdr::SorobanAddressCredentials {
512 address:
513 xdr::ScAddress::Account(xdr::AccountId(xdr::PublicKey::PublicKeyTypeEd25519(address))),
514 ..
515 }) = &auth.credentials
516 else {
517 panic!("unexpected credentials type");
518 };
519 assert_eq!(
520 SOURCE.to_string(),
521 format!("{}", stellar_strkey::ed25519::PublicKey(address.0))
522 );
523 }
524
525 #[test]
526 fn test_assemble_transaction_errors_for_non_invokehostfn_ops() {
527 let source_bytes = Ed25519PublicKey::from_string(SOURCE).unwrap().0;
528 let txn = Transaction {
529 source_account: MuxedAccount::Ed25519(Uint256(source_bytes)),
530 fee: 100,
531 seq_num: SequenceNumber(0),
532 cond: Preconditions::None,
533 memo: Memo::None,
534 operations: vec![Operation {
535 source_account: None,
536 body: OperationBody::ChangeTrust(ChangeTrustOp {
537 line: ChangeTrustAsset::Native,
538 limit: 0,
539 }),
540 }]
541 .try_into()
542 .unwrap(),
543 ext: TransactionExt::V0,
544 };
545
546 let result = assemble(
547 &txn,
548 SimulateTransactionResponse {
549 min_resource_fee: 115,
550 transaction_data: transaction_data()
551 .to_xdr_base64(Limits::depth(XDR_DEPTH_LIMIT))
552 .unwrap(),
553 latest_ledger: 3,
554 ..Default::default()
555 },
556 None,
557 false,
558 );
559
560 match result {
561 Ok(_) => {}
562 Err(e) => panic!("expected assembled operation, got: {e:#?}"),
563 }
564 }
565
566 #[test]
567 fn test_assemble_transaction_errors_for_errors_for_mismatched_simulation() {
568 let txn = single_contract_fn_transaction();
569
570 let result = assemble(
571 &txn,
572 SimulateTransactionResponse {
573 min_resource_fee: 115,
574 transaction_data: transaction_data()
575 .to_xdr_base64(Limits::depth(XDR_DEPTH_LIMIT))
576 .unwrap(),
577 latest_ledger: 3,
578 ..Default::default()
579 },
580 None,
581 false,
582 );
583
584 match result {
585 Err(Error::UnexpectedSimulateTransactionResultSize { length }) => {
586 assert_eq!(0, length);
587 }
588 Ok(_) => panic!("expected error, got success"),
589 Err(e) => panic!("expected UnexpectedSimulateTransactionResultSize error, got: {e:#?}"),
590 }
591 }
592
593 #[test]
594 fn test_assemble_transaction_calcs_fee() {
595 let mut sim = simulation_response();
596 sim.min_resource_fee = 12345;
597 let mut txn = single_contract_fn_transaction();
598 txn.fee = 10000;
599 let Ok(result) = assemble(&txn, sim, None, false) else {
600 panic!("assemble failed");
601 };
602
603 assert_eq!(12345 + 10000, result.txn.fee);
604 assert_eq!(None, result.fee_bump_fee);
605
606 let expected_tx_data = transaction_data();
608 assert_eq!(TransactionExt::V1(expected_tx_data), result.txn.ext);
609 }
610
611 #[test]
612 fn test_assemble_transaction_fee_bump_fee_behavior() {
613 let mut txn = single_contract_fn_transaction();
622 let mut response = simulation_response();
623
624 let inclusion_fee: u32 = 500;
625 let inclusion_fee_i64: i64 = i64::from(inclusion_fee);
626 txn.fee = inclusion_fee;
627
628 response.min_resource_fee = (u32::MAX - inclusion_fee).into();
630
631 match assemble(&txn, response.clone(), None, false) {
632 Ok(assembled) => {
633 assert_eq!(assembled.txn.fee, u32::MAX);
634 assert_eq!(assembled.fee_bump_fee, None);
635 }
636 Err(e) => panic!("expected success, got error: {e:#?}"),
637 }
638
639 response.min_resource_fee = (u32::MAX - inclusion_fee + 1).into();
641 match assemble(&txn, response.clone(), None, false) {
642 Ok(assembled) => {
643 assert_eq!(assembled.txn.fee, 0);
644 assert_eq!(
645 assembled.fee_bump_fee,
646 Some(i64::try_from(response.min_resource_fee).unwrap() + inclusion_fee_i64 * 2)
647 );
648 }
649 Err(e) => panic!("expected success, got error: {e:#?}"),
650 }
651
652 response.min_resource_fee = u64::try_from(i64::MAX - (2 * inclusion_fee_i64) + 1).unwrap();
654 match assemble(&txn, response, None, false) {
655 Err(Error::LargeFee(fee)) => {
656 let expected = i64::MAX as u64 + 1;
657 assert_eq!(expected, fee, "expected {expected} != {fee} actual");
658 }
659 Ok(_) => panic!("expected error, got success"),
660 Err(e) => panic!("expected LargeFee error, got different error: {e:#?}"),
661 }
662 }
663
664 #[test]
665 fn test_assemble_transaction_with_resource_fee() {
666 let sim = simulation_response();
667 let mut txn = single_contract_fn_transaction();
668 txn.fee = 500;
669 let resource_fee = 12345i64;
670 let Ok(result) = assemble(&txn, sim, Some(resource_fee), false) else {
671 panic!("assemble failed");
672 };
673
674 assert_eq!(12345 + 500, result.txn.fee);
677 assert_eq!(None, result.fee_bump_fee);
678
679 let mut expected_tx_data = transaction_data();
681 expected_tx_data.resource_fee = resource_fee;
682 assert_eq!(TransactionExt::V1(expected_tx_data), result.txn.ext);
683 }
684
685 #[test]
688 fn test_assemble_transaction_input_resource_fee_negative_errors() {
689 let mut sim = simulation_response();
690 sim.min_resource_fee = 12345;
691 let mut txn = single_contract_fn_transaction();
692 txn.fee = 500;
693 let resource_fee = -1;
694 let result = assemble(&txn, sim, Some(resource_fee), false);
695
696 assert!(result.is_err());
697 }
698
699 #[test]
700 fn test_assemble_transaction_with_resource_fee_fee_bump_behavior() {
701 let mut txn = single_contract_fn_transaction();
710 let response = simulation_response();
711
712 let inclusion_fee: u32 = 500;
713 let inclusion_fee_i64: i64 = i64::from(inclusion_fee);
714 txn.fee = inclusion_fee;
715
716 let resource_fee: i64 = (u32::MAX - inclusion_fee).into();
718 match assemble(&txn, response.clone(), Some(resource_fee), false) {
719 Ok(assembled) => {
720 assert_eq!(assembled.txn.fee, u32::MAX);
721 assert_eq!(assembled.fee_bump_fee, None);
722 }
723 Err(e) => panic!("expected success, got error: {e:#?}"),
724 }
725
726 let resource_fee: i64 = (u32::MAX - inclusion_fee + 1).into();
728 match assemble(&txn, response.clone(), Some(resource_fee), false) {
729 Ok(assembled) => {
730 assert_eq!(assembled.txn.fee, 0);
731 assert_eq!(
732 assembled.fee_bump_fee,
733 Some(resource_fee + inclusion_fee_i64 * 2)
734 );
735 }
736 Err(e) => panic!("expected success, got error: {e:#?}"),
737 }
738
739 let resource_fee: i64 = i64::MAX - (2 * inclusion_fee_i64) + 1;
741 match assemble(&txn, response, Some(resource_fee), false) {
742 Err(Error::LargeFee(fee)) => {
743 let expected = i64::MAX as u64 + 1;
744 assert_eq!(expected, fee, "expected {expected} != {fee} actual");
745 }
746 Ok(_) => panic!("expected error, got success"),
747 Err(e) => panic!("expected LargeFee error, got: {e:#?}"),
748 }
749 }
750
751 #[test]
752 fn test_assemble_transaction_replaces_auth_when_recording() {
753 let sim = simulation_response();
757 let mut txn = single_contract_fn_transaction();
758 set_auth(&mut txn, vec![contract_fn_auth("old_fn")]);
759
760 let Ok(result) = assemble(&txn, sim, None, true) else {
761 panic!("assemble failed");
762 };
763
764 assert_eq!(1, result.txn.operations.len());
765 let OperationBody::InvokeHostFunction(ref op) = result.txn.operations[0].body else {
766 panic!("unexpected operation type: {:#?}", result.txn.operations[0]);
767 };
768 assert_eq!(1, op.auth.len());
769 assert_eq!("fn", first_auth_fn_name(&result.txn));
770 }
771
772 #[test]
773 fn test_assemble_transaction_preserves_auth_when_not_recording() {
774 let sim = simulation_response();
777 let mut txn = single_contract_fn_transaction();
778 set_auth(&mut txn, vec![contract_fn_auth("old_fn")]);
779
780 let Ok(result) = assemble(&txn, sim, None, false) else {
781 panic!("assemble failed");
782 };
783
784 assert_eq!("old_fn", first_auth_fn_name(&result.txn));
785 }
786
787 #[test]
788 fn test_assemble_transaction_does_not_double_count_resource_fee() {
789 let mut sim = simulation_response();
794 sim.min_resource_fee = 115;
795 let mut txn = single_contract_fn_transaction();
796
797 let existing_resource_fee = 100;
799 txn.fee = 500;
800 let mut existing_data = transaction_data();
801 existing_data.resource_fee = existing_resource_fee;
802 txn.ext = TransactionExt::V1(existing_data);
803
804 let Ok(result) = assemble(&txn, sim, None, false) else {
805 panic!("assemble failed");
806 };
807
808 assert_eq!(400 + 115, result.txn.fee);
810 assert_eq!(None, result.fee_bump_fee);
811 }
812}