1use crate::DecodeError;
7use base64::engine::general_purpose::STANDARD;
8use base64::Engine;
9use sha2::{Digest, Sha256};
10use stellar_strkey::Strkey;
11use stellar_xdr::{
12 AccountId, BytesM, ContractExecutable, ContractId, ContractIdPreimage,
13 ContractIdPreimageFromAddress, CreateContractArgs, CreateContractArgsV2, ExtendFootprintTtlOp,
14 ExtensionPoint, Hash, HashIdPreimage, HashIdPreimageContractId, HostFunction,
15 InvokeContractArgs, InvokeHostFunctionOp, LedgerFootprint, LedgerKey, Memo, MuxedAccount,
16 Operation, OperationBody, Preconditions, PublicKey, ReadXdr, RestoreFootprintOp, ScAddress,
17 ScSymbol, ScVal, SequenceNumber, SorobanAuthorizationEntry, SorobanResources,
18 SorobanTransactionData, SorobanTransactionDataExt, Transaction, TransactionEnvelope,
19 TransactionExt, TransactionV1Envelope, Uint256, VecM, WriteXdr,
20};
21
22#[derive(Debug, Clone)]
24pub struct InvokeTransactionParams {
25 pub source_account: String,
27 pub sequence: i64,
29 pub fee: u32,
31 pub contract_id: String,
33 pub function: String,
35 pub args: Vec<String>,
37 pub memo: Option<Memo>,
39}
40
41pub fn memo_text(text: &str) -> Result<Memo, DecodeError> {
47 let bytes = text.as_bytes();
48 let vec_m = bytes.try_into().map_err(|_| {
49 DecodeError::Extraction(format!(
50 "memo text must be at most 28 bytes (got {})",
51 bytes.len()
52 ))
53 })?;
54 Ok(Memo::Text(vec_m))
55}
56
57pub fn memo_id(id: u64) -> Memo {
59 Memo::Id(id)
60}
61
62pub fn decode_account_id(pubkey: &str) -> Result<AccountId, DecodeError> {
64 let key = Strkey::from_string(pubkey)
65 .map_err(|e| DecodeError::Extraction(format!("Invalid public key: {}", e)))?;
66
67 match key {
68 Strkey::PublicKeyEd25519(pk) => {
69 Ok(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(pk.0))))
70 }
71 _ => Err(DecodeError::Extraction(
72 "Expected ED25519 Public Key".into(),
73 )),
74 }
75}
76
77pub fn decode_contract_id(contract_id: &str) -> Result<Hash, DecodeError> {
79 let key = Strkey::from_string(contract_id)
80 .map_err(|e| DecodeError::Extraction(format!("Invalid contract ID: {}", e)))?;
81
82 match key {
83 Strkey::Contract(c) => Ok(Hash(c.0)),
84 _ => Err(DecodeError::Extraction("Expected Contract ID".into())),
85 }
86}
87
88pub fn build_invoke_transaction(params: &InvokeTransactionParams) -> Result<String, DecodeError> {
90 let source_account = decode_account_id(¶ms.source_account)?;
91 let contract_hash = decode_contract_id(¶ms.contract_id)?;
92
93 let mut scval_args = Vec::new();
95 for arg_b64 in ¶ms.args {
96 let raw = STANDARD.decode(arg_b64)?;
97 let mut cursor = std::io::Cursor::new(&raw);
98 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
99 let val = stellar_xdr::ScVal::read_xdr(&mut l)
100 .map_err(|e| DecodeError::XdrParse("ScVal arg".into(), e))?;
101 scval_args.push(val);
102 }
103 let args_vec = VecM::try_from(scval_args)
104 .map_err(|_| DecodeError::Extraction("Too many arguments".into()))?;
105
106 let function_name = ScSymbol(
107 params
108 .function
109 .as_bytes()
110 .try_into()
111 .map_err(|_| DecodeError::Extraction("Function name too long".into()))?,
112 );
113
114 let invoke_op = InvokeHostFunctionOp {
115 host_function: HostFunction::InvokeContract(InvokeContractArgs {
116 contract_address: ScAddress::Contract(ContractId(contract_hash)),
117 function_name,
118 args: args_vec,
119 }),
120 auth: VecM::default(),
121 };
122
123 let op = Operation {
124 source_account: None,
125 body: OperationBody::InvokeHostFunction(invoke_op),
126 };
127
128 let tx = Transaction {
129 source_account: MuxedAccount::Ed25519(match source_account.0 {
130 PublicKey::PublicKeyTypeEd25519(u) => u,
131 }),
132 fee: params.fee,
133 seq_num: SequenceNumber(params.sequence),
134 cond: Preconditions::None,
135 memo: params.memo.clone().unwrap_or(Memo::None),
136 operations: VecM::try_from(vec![op]).unwrap(),
137 ext: TransactionExt::V0,
138 };
139
140 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
141 tx,
142 signatures: VecM::default(), });
144
145 let mut buf = Vec::new();
146 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
147 envelope.write_xdr(&mut l).map_err(DecodeError::XdrWrite)?;
148
149 Ok(STANDARD.encode(&buf))
150}
151
152pub fn build_invoke_transaction_with_data(
156 params: &InvokeTransactionParams,
157 soroban_data: SorobanTransactionData,
158 auth_entries: Vec<SorobanAuthorizationEntry>,
159) -> Result<String, DecodeError> {
160 let source_account = decode_account_id(¶ms.source_account)?;
161 let contract_hash = decode_contract_id(¶ms.contract_id)?;
162
163 let mut scval_args = Vec::new();
164 for arg_b64 in ¶ms.args {
165 let raw = STANDARD.decode(arg_b64)?;
166 let mut cursor = std::io::Cursor::new(&raw);
167 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
168 let val = stellar_xdr::ScVal::read_xdr(&mut l)
169 .map_err(|e| DecodeError::XdrParse("ScVal arg".into(), e))?;
170 scval_args.push(val);
171 }
172 let args_vec = VecM::try_from(scval_args)
173 .map_err(|_| DecodeError::Extraction("Too many arguments".into()))?;
174
175 let function_name = ScSymbol(
176 params
177 .function
178 .as_bytes()
179 .try_into()
180 .map_err(|_| DecodeError::Extraction("Function name too long".into()))?,
181 );
182
183 let auth_vec_m = VecM::try_from(auth_entries)
184 .map_err(|_| DecodeError::Extraction("Too many auth entries".into()))?;
185
186 let invoke_op = InvokeHostFunctionOp {
187 host_function: HostFunction::InvokeContract(InvokeContractArgs {
188 contract_address: ScAddress::Contract(ContractId(contract_hash)),
189 function_name,
190 args: args_vec,
191 }),
192 auth: auth_vec_m,
193 };
194
195 let op = Operation {
196 source_account: None,
197 body: OperationBody::InvokeHostFunction(invoke_op),
198 };
199
200 let tx = Transaction {
201 source_account: MuxedAccount::Ed25519(match source_account.0 {
202 PublicKey::PublicKeyTypeEd25519(u) => u,
203 }),
204 fee: params.fee,
205 seq_num: SequenceNumber(params.sequence),
206 cond: Preconditions::None,
207 memo: params.memo.clone().unwrap_or(Memo::None),
208 operations: VecM::try_from(vec![op]).unwrap(),
209 ext: TransactionExt::V1(soroban_data),
210 };
211
212 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
213 tx,
214 signatures: VecM::default(),
215 });
216
217 let mut buf = Vec::new();
218 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
219 envelope.write_xdr(&mut l).map_err(DecodeError::XdrWrite)?;
220
221 Ok(STANDARD.encode(&buf))
222}
223
224pub struct UploadWasmParams {
226 pub source_account: String,
228 pub sequence: i64,
230 pub fee: u32,
232 pub wasm_bytes: Vec<u8>,
234}
235
236#[derive(Debug, Clone)]
238pub struct CreateContractParams {
239 pub source_account: String,
241 pub sequence: i64,
243 pub fee: u32,
245 pub wasm_hash: [u8; 32],
247 pub deployer_address: String,
249 pub salt: [u8; 20],
251}
252
253#[derive(Debug, Clone)]
255pub struct CreateContractV2Params {
256 pub source_account: String,
258 pub sequence: i64,
260 pub fee: u32,
262 pub wasm_hash: [u8; 32],
264 pub deployer_address: String,
266 pub salt: [u8; 20],
268 pub constructor_args: Vec<String>,
270}
271
272pub fn build_upload_wasm_tx(params: &UploadWasmParams) -> Result<String, DecodeError> {
275 let source_account = decode_account_id(¶ms.source_account)?;
276
277 let wasm_bytes_m = BytesM::try_from(params.wasm_bytes.as_slice())
278 .map_err(|_| DecodeError::Extraction("WASM bytes exceed max length".into()))?;
279
280 let upload_op = InvokeHostFunctionOp {
281 host_function: HostFunction::UploadContractWasm(wasm_bytes_m),
282 auth: VecM::default(),
283 };
284
285 let op = Operation {
286 source_account: None,
287 body: OperationBody::InvokeHostFunction(upload_op),
288 };
289
290 let tx = Transaction {
291 source_account: MuxedAccount::Ed25519(match source_account.0 {
292 PublicKey::PublicKeyTypeEd25519(u) => u,
293 }),
294 fee: params.fee,
295 seq_num: SequenceNumber(params.sequence),
296 cond: Preconditions::None,
297 memo: Memo::None,
298 operations: VecM::try_from(vec![op]).unwrap(),
299 ext: TransactionExt::V0,
300 };
301
302 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
303 tx,
304 signatures: VecM::default(),
305 });
306
307 let mut buf = Vec::new();
308 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
309 envelope.write_xdr(&mut l).map_err(DecodeError::XdrWrite)?;
310
311 Ok(STANDARD.encode(&buf))
312}
313
314pub fn build_upload_wasm_tx_with_data(
316 params: &UploadWasmParams,
317 soroban_data: SorobanTransactionData,
318) -> Result<String, DecodeError> {
319 let source_account = decode_account_id(¶ms.source_account)?;
320
321 let wasm_bytes_m = BytesM::try_from(params.wasm_bytes.as_slice())
322 .map_err(|_| DecodeError::Extraction("WASM bytes exceed max length".into()))?;
323
324 let upload_op = InvokeHostFunctionOp {
325 host_function: HostFunction::UploadContractWasm(wasm_bytes_m),
326 auth: VecM::default(),
327 };
328
329 let op = Operation {
330 source_account: None,
331 body: OperationBody::InvokeHostFunction(upload_op),
332 };
333
334 let tx = Transaction {
335 source_account: MuxedAccount::Ed25519(match source_account.0 {
336 PublicKey::PublicKeyTypeEd25519(u) => u,
337 }),
338 fee: params.fee,
339 seq_num: SequenceNumber(params.sequence),
340 cond: Preconditions::None,
341 memo: Memo::None,
342 operations: VecM::try_from(vec![op]).unwrap(),
343 ext: TransactionExt::V1(soroban_data),
344 };
345
346 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
347 tx,
348 signatures: VecM::default(),
349 });
350
351 let mut buf = Vec::new();
352 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
353 envelope.write_xdr(&mut l).map_err(DecodeError::XdrWrite)?;
354
355 Ok(STANDARD.encode(&buf))
356}
357
358pub fn build_create_contract_tx(params: &CreateContractParams) -> Result<String, DecodeError> {
361 let source_account = decode_account_id(¶ms.source_account)?;
362 let deployer = decode_account_id(¶ms.deployer_address)?;
363
364 let mut salt_bytes = [0u8; 32];
366 salt_bytes[..params.salt.len()].copy_from_slice(¶ms.salt);
367
368 let preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
369 address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(
370 match deployer.0 {
371 PublicKey::PublicKeyTypeEd25519(u) => u,
372 },
373 ))),
374 salt: Uint256(salt_bytes),
375 });
376
377 let create_op = InvokeHostFunctionOp {
378 host_function: HostFunction::CreateContract(CreateContractArgs {
379 contract_id_preimage: preimage,
380 executable: ContractExecutable::Wasm(Hash(params.wasm_hash)),
381 }),
382 auth: VecM::default(),
383 };
384
385 let op = Operation {
386 source_account: None,
387 body: OperationBody::InvokeHostFunction(create_op),
388 };
389
390 let tx = Transaction {
391 source_account: MuxedAccount::Ed25519(match source_account.0 {
392 PublicKey::PublicKeyTypeEd25519(u) => u,
393 }),
394 fee: params.fee,
395 seq_num: SequenceNumber(params.sequence),
396 cond: Preconditions::None,
397 memo: Memo::None,
398 operations: VecM::try_from(vec![op]).unwrap(),
399 ext: TransactionExt::V0,
400 };
401
402 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
403 tx,
404 signatures: VecM::default(),
405 });
406
407 let mut buf = Vec::new();
408 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
409 envelope.write_xdr(&mut l).map_err(DecodeError::XdrWrite)?;
410
411 Ok(STANDARD.encode(&buf))
412}
413
414pub fn build_create_contract_tx_with_data(
416 params: &CreateContractParams,
417 soroban_data: SorobanTransactionData,
418) -> Result<String, DecodeError> {
419 let source_account = decode_account_id(¶ms.source_account)?;
420 let deployer = decode_account_id(¶ms.deployer_address)?;
421
422 let mut salt_bytes = [0u8; 32];
424 salt_bytes[..params.salt.len()].copy_from_slice(¶ms.salt);
425
426 let preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
427 address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(
428 match deployer.0 {
429 PublicKey::PublicKeyTypeEd25519(u) => u,
430 },
431 ))),
432 salt: Uint256(salt_bytes),
433 });
434
435 let create_op = InvokeHostFunctionOp {
436 host_function: HostFunction::CreateContract(CreateContractArgs {
437 contract_id_preimage: preimage,
438 executable: ContractExecutable::Wasm(Hash(params.wasm_hash)),
439 }),
440 auth: VecM::default(),
441 };
442
443 let op = Operation {
444 source_account: None,
445 body: OperationBody::InvokeHostFunction(create_op),
446 };
447
448 let tx = Transaction {
449 source_account: MuxedAccount::Ed25519(match source_account.0 {
450 PublicKey::PublicKeyTypeEd25519(u) => u,
451 }),
452 fee: params.fee,
453 seq_num: SequenceNumber(params.sequence),
454 cond: Preconditions::None,
455 memo: Memo::None,
456 operations: VecM::try_from(vec![op]).unwrap(),
457 ext: TransactionExt::V1(soroban_data),
458 };
459
460 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
461 tx,
462 signatures: VecM::default(),
463 });
464
465 let mut buf = Vec::new();
466 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
467 envelope.write_xdr(&mut l).map_err(DecodeError::XdrWrite)?;
468
469 Ok(STANDARD.encode(&buf))
470}
471
472pub fn build_upload_wasm_tx_with_data_and_auth(
474 params: &UploadWasmParams,
475 soroban_data: SorobanTransactionData,
476 auth_entries: Vec<SorobanAuthorizationEntry>,
477) -> Result<String, DecodeError> {
478 let source_account = decode_account_id(¶ms.source_account)?;
479
480 let wasm_bytes_m = BytesM::try_from(params.wasm_bytes.as_slice())
481 .map_err(|_| DecodeError::Extraction("WASM bytes exceed max length".into()))?;
482
483 let auth_vec_m = VecM::try_from(auth_entries)
484 .map_err(|_| DecodeError::Extraction("Too many auth entries".into()))?;
485
486 let upload_op = InvokeHostFunctionOp {
487 host_function: HostFunction::UploadContractWasm(wasm_bytes_m),
488 auth: auth_vec_m,
489 };
490
491 let op = Operation {
492 source_account: None,
493 body: OperationBody::InvokeHostFunction(upload_op),
494 };
495
496 let tx = Transaction {
497 source_account: MuxedAccount::Ed25519(match source_account.0 {
498 PublicKey::PublicKeyTypeEd25519(u) => u,
499 }),
500 fee: params.fee,
501 seq_num: SequenceNumber(params.sequence),
502 cond: Preconditions::None,
503 memo: Memo::None,
504 operations: VecM::try_from(vec![op]).unwrap(),
505 ext: TransactionExt::V1(soroban_data),
506 };
507
508 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
509 tx,
510 signatures: VecM::default(),
511 });
512
513 let mut buf = Vec::new();
514 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
515 envelope.write_xdr(&mut l).map_err(DecodeError::XdrWrite)?;
516
517 Ok(STANDARD.encode(&buf))
518}
519
520pub fn build_create_contract_tx_with_data_and_auth(
522 params: &CreateContractParams,
523 soroban_data: SorobanTransactionData,
524 auth_entries: Vec<SorobanAuthorizationEntry>,
525) -> Result<String, DecodeError> {
526 let source_account = decode_account_id(¶ms.source_account)?;
527 let deployer = decode_account_id(¶ms.deployer_address)?;
528
529 let mut salt_bytes = [0u8; 32];
531 salt_bytes[..params.salt.len()].copy_from_slice(¶ms.salt);
532
533 let preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
534 address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(
535 match deployer.0 {
536 PublicKey::PublicKeyTypeEd25519(u) => u,
537 },
538 ))),
539 salt: Uint256(salt_bytes),
540 });
541
542 let auth_vec_m = VecM::try_from(auth_entries)
543 .map_err(|_| DecodeError::Extraction("Too many auth entries".into()))?;
544
545 let create_op = InvokeHostFunctionOp {
546 host_function: HostFunction::CreateContract(CreateContractArgs {
547 contract_id_preimage: preimage,
548 executable: ContractExecutable::Wasm(Hash(params.wasm_hash)),
549 }),
550 auth: auth_vec_m,
551 };
552
553 let op = Operation {
554 source_account: None,
555 body: OperationBody::InvokeHostFunction(create_op),
556 };
557
558 let tx = Transaction {
559 source_account: MuxedAccount::Ed25519(match source_account.0 {
560 PublicKey::PublicKeyTypeEd25519(u) => u,
561 }),
562 fee: params.fee,
563 seq_num: SequenceNumber(params.sequence),
564 cond: Preconditions::None,
565 memo: Memo::None,
566 operations: VecM::try_from(vec![op]).unwrap(),
567 ext: TransactionExt::V1(soroban_data),
568 };
569
570 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
571 tx,
572 signatures: VecM::default(),
573 });
574
575 let mut buf = Vec::new();
576 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
577 envelope.write_xdr(&mut l).map_err(DecodeError::XdrWrite)?;
578
579 Ok(STANDARD.encode(&buf))
580}
581
582pub fn parse_scval_args(args: &[String]) -> Result<VecM<ScVal>, DecodeError> {
584 let mut scval_args = Vec::new();
585 for arg_b64 in args {
586 let raw = STANDARD.decode(arg_b64)?;
587 let mut cursor = std::io::Cursor::new(&raw);
588 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
589 let val =
590 ScVal::read_xdr(&mut l).map_err(|e| DecodeError::XdrParse("ScVal arg".into(), e))?;
591 scval_args.push(val);
592 }
593 VecM::try_from(scval_args).map_err(|_| DecodeError::Extraction("Too many arguments".into()))
594}
595
596pub fn build_create_contract_v2_tx(params: &CreateContractV2Params) -> Result<String, DecodeError> {
599 let source_account = decode_account_id(¶ms.source_account)?;
600 let deployer = decode_account_id(¶ms.deployer_address)?;
601
602 let mut salt_bytes = [0u8; 32];
604 salt_bytes[..params.salt.len()].copy_from_slice(¶ms.salt);
605
606 let preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
607 address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(
608 match deployer.0 {
609 PublicKey::PublicKeyTypeEd25519(u) => u,
610 },
611 ))),
612 salt: Uint256(salt_bytes),
613 });
614
615 let constructor_args = parse_scval_args(¶ms.constructor_args)?;
616
617 let create_op = InvokeHostFunctionOp {
618 host_function: HostFunction::CreateContractV2(CreateContractArgsV2 {
619 contract_id_preimage: preimage,
620 executable: ContractExecutable::Wasm(Hash(params.wasm_hash)),
621 constructor_args,
622 }),
623 auth: VecM::default(),
624 };
625
626 let op = Operation {
627 source_account: None,
628 body: OperationBody::InvokeHostFunction(create_op),
629 };
630
631 let tx = Transaction {
632 source_account: MuxedAccount::Ed25519(match source_account.0 {
633 PublicKey::PublicKeyTypeEd25519(u) => u,
634 }),
635 fee: params.fee,
636 seq_num: SequenceNumber(params.sequence),
637 cond: Preconditions::None,
638 memo: Memo::None,
639 operations: VecM::try_from(vec![op]).unwrap(),
640 ext: TransactionExt::V0,
641 };
642
643 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
644 tx,
645 signatures: VecM::default(),
646 });
647
648 let mut buf = Vec::new();
649 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
650 envelope.write_xdr(&mut l).map_err(DecodeError::XdrWrite)?;
651
652 Ok(STANDARD.encode(&buf))
653}
654
655pub fn build_create_contract_v2_tx_with_data(
657 params: &CreateContractV2Params,
658 soroban_data: SorobanTransactionData,
659) -> Result<String, DecodeError> {
660 let source_account = decode_account_id(¶ms.source_account)?;
661 let deployer = decode_account_id(¶ms.deployer_address)?;
662
663 let mut salt_bytes = [0u8; 32];
665 salt_bytes[..params.salt.len()].copy_from_slice(¶ms.salt);
666
667 let preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
668 address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(
669 match deployer.0 {
670 PublicKey::PublicKeyTypeEd25519(u) => u,
671 },
672 ))),
673 salt: Uint256(salt_bytes),
674 });
675
676 let constructor_args = parse_scval_args(¶ms.constructor_args)?;
677
678 let create_op = InvokeHostFunctionOp {
679 host_function: HostFunction::CreateContractV2(CreateContractArgsV2 {
680 contract_id_preimage: preimage,
681 executable: ContractExecutable::Wasm(Hash(params.wasm_hash)),
682 constructor_args,
683 }),
684 auth: VecM::default(),
685 };
686
687 let op = Operation {
688 source_account: None,
689 body: OperationBody::InvokeHostFunction(create_op),
690 };
691
692 let tx = Transaction {
693 source_account: MuxedAccount::Ed25519(match source_account.0 {
694 PublicKey::PublicKeyTypeEd25519(u) => u,
695 }),
696 fee: params.fee,
697 seq_num: SequenceNumber(params.sequence),
698 cond: Preconditions::None,
699 memo: Memo::None,
700 operations: VecM::try_from(vec![op]).unwrap(),
701 ext: TransactionExt::V1(soroban_data),
702 };
703
704 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
705 tx,
706 signatures: VecM::default(),
707 });
708
709 let mut buf = Vec::new();
710 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
711 envelope.write_xdr(&mut l).map_err(DecodeError::XdrWrite)?;
712
713 Ok(STANDARD.encode(&buf))
714}
715
716pub fn build_create_contract_v2_tx_with_data_and_auth(
718 params: &CreateContractV2Params,
719 soroban_data: SorobanTransactionData,
720 auth_entries: Vec<SorobanAuthorizationEntry>,
721) -> Result<String, DecodeError> {
722 let source_account = decode_account_id(¶ms.source_account)?;
723 let deployer = decode_account_id(¶ms.deployer_address)?;
724
725 let mut salt_bytes = [0u8; 32];
727 salt_bytes[..params.salt.len()].copy_from_slice(¶ms.salt);
728
729 let preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
730 address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(
731 match deployer.0 {
732 PublicKey::PublicKeyTypeEd25519(u) => u,
733 },
734 ))),
735 salt: Uint256(salt_bytes),
736 });
737
738 let auth_vec_m = VecM::try_from(auth_entries)
739 .map_err(|_| DecodeError::Extraction("Too many auth entries".into()))?;
740
741 let constructor_args = parse_scval_args(¶ms.constructor_args)?;
742
743 let create_op = InvokeHostFunctionOp {
744 host_function: HostFunction::CreateContractV2(CreateContractArgsV2 {
745 contract_id_preimage: preimage,
746 executable: ContractExecutable::Wasm(Hash(params.wasm_hash)),
747 constructor_args,
748 }),
749 auth: auth_vec_m,
750 };
751
752 let op = Operation {
753 source_account: None,
754 body: OperationBody::InvokeHostFunction(create_op),
755 };
756
757 let tx = Transaction {
758 source_account: MuxedAccount::Ed25519(match source_account.0 {
759 PublicKey::PublicKeyTypeEd25519(u) => u,
760 }),
761 fee: params.fee,
762 seq_num: SequenceNumber(params.sequence),
763 cond: Preconditions::None,
764 memo: Memo::None,
765 operations: VecM::try_from(vec![op]).unwrap(),
766 ext: TransactionExt::V1(soroban_data),
767 };
768
769 let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
770 tx,
771 signatures: VecM::default(),
772 });
773
774 let mut buf = Vec::new();
775 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
776 envelope.write_xdr(&mut l).map_err(DecodeError::XdrWrite)?;
777
778 Ok(STANDARD.encode(&buf))
779}
780
781pub fn parse_soroban_authorization_entry(
783 base64_data: &str,
784) -> Result<SorobanAuthorizationEntry, DecodeError> {
785 let raw = STANDARD.decode(base64_data).map_err(|e| {
786 DecodeError::Extraction(format!("Failed to decode SorobanAuthorizationEntry: {}", e))
787 })?;
788 let mut cursor = std::io::Cursor::new(&raw);
789 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
790 SorobanAuthorizationEntry::read_xdr(&mut l)
791 .map_err(|e| DecodeError::XdrParse("SorobanAuthorizationEntry".into(), e))
792}
793
794pub fn parse_soroban_authorization_entries(
796 auth_b64_list: &[String],
797) -> Result<Vec<SorobanAuthorizationEntry>, DecodeError> {
798 auth_b64_list
799 .iter()
800 .map(|s| parse_soroban_authorization_entry(s))
801 .collect::<Result<Vec<_>, _>>()
802}
803
804pub fn parse_soroban_transaction_data(
806 base64_data: &str,
807) -> Result<SorobanTransactionData, DecodeError> {
808 let raw = STANDARD.decode(base64_data).map_err(|e| {
809 DecodeError::Extraction(format!("Failed to decode SorobanTransactionData: {}", e))
810 })?;
811 let mut cursor = std::io::Cursor::new(&raw);
812 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
813 SorobanTransactionData::read_xdr(&mut l)
814 .map_err(|e| DecodeError::XdrParse("SorobanTransactionData".into(), e))
815}
816
817pub struct ExtendFootprintParams {
819 pub source_account: String,
821 pub sequence: i64,
823 pub fee: u32,
825 pub extend_to: u32,
830 pub footprint_keys: Vec<String>,
833}
834
835pub fn decode_ledger_key(encoded: &str) -> Result<LedgerKey, DecodeError> {
837 let trimmed = encoded.trim();
838 if trimmed.is_empty() {
839 return Err(DecodeError::Extraction("empty LedgerKey".into()));
840 }
841 let raw = if let Ok(bytes) = STANDARD.decode(trimmed) {
842 bytes
843 } else if trimmed.len().is_multiple_of(2) && trimmed.chars().all(|c| c.is_ascii_hexdigit()) {
844 hex::decode(trimmed).map_err(DecodeError::Hex)?
845 } else {
846 return Err(DecodeError::Extraction(
847 "LedgerKey must be base64 XDR or even-length hex".into(),
848 ));
849 };
850 if raw.is_empty() {
851 return Err(DecodeError::Extraction("empty LedgerKey".into()));
852 }
853 let mut cursor = std::io::Cursor::new(&raw);
854 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
855 LedgerKey::read_xdr(&mut l).map_err(|e| DecodeError::XdrParse("LedgerKey".into(), e))
856}
857
858pub fn merge_footprint_keys(keys: &[String]) -> Result<Vec<String>, DecodeError> {
860 let mut seen = std::collections::HashSet::new();
861 let mut out = Vec::new();
862 for k in keys {
863 let decoded = decode_ledger_key(k)?;
864 let mut buf = Vec::new();
865 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
866 decoded.write_xdr(&mut l).map_err(DecodeError::XdrWrite)?;
867 let canonical = STANDARD.encode(&buf);
868 if seen.insert(canonical.clone()) {
869 out.push(canonical);
870 }
871 }
872 if out.is_empty() {
873 return Err(DecodeError::Extraction(
874 "footprint must contain at least one LedgerKey".into(),
875 ));
876 }
877 Ok(out)
878}
879
880fn muxed_from_account(source_account: AccountId) -> MuxedAccount {
881 MuxedAccount::Ed25519(match source_account.0 {
882 PublicKey::PublicKeyTypeEd25519(u) => u,
883 })
884}
885
886fn encode_envelope(envelope: TransactionEnvelope) -> Result<String, DecodeError> {
887 let mut buf = Vec::new();
888 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
889 envelope.write_xdr(&mut l).map_err(DecodeError::XdrWrite)?;
890 Ok(STANDARD.encode(&buf))
891}
892
893fn empty_soroban_data_with_keys(keys: &[LedgerKey]) -> Result<SorobanTransactionData, DecodeError> {
894 let read_only = VecM::try_from(keys.to_vec())
895 .map_err(|_| DecodeError::Extraction("Too many footprint keys".into()))?;
896 Ok(SorobanTransactionData {
897 ext: SorobanTransactionDataExt::V0,
898 resources: SorobanResources {
899 footprint: LedgerFootprint {
900 read_only,
901 read_write: VecM::default(),
902 },
903 instructions: 0,
904 disk_read_bytes: 0,
905 write_bytes: 0,
906 },
907 resource_fee: 0,
908 })
909}
910
911fn build_extend_envelope(
912 params: &ExtendFootprintParams,
913 soroban_data: SorobanTransactionData,
914) -> Result<String, DecodeError> {
915 if params.extend_to == 0 {
916 return Err(DecodeError::Extraction(
917 "extend_to must be greater than 0".into(),
918 ));
919 }
920 let source_account = decode_account_id(¶ms.source_account)?;
921 let op = Operation {
922 source_account: None,
923 body: OperationBody::ExtendFootprintTtl(ExtendFootprintTtlOp {
924 ext: ExtensionPoint::V0,
925 extend_to: params.extend_to,
926 }),
927 };
928 let tx = Transaction {
929 source_account: muxed_from_account(source_account),
930 fee: params.fee,
931 seq_num: SequenceNumber(params.sequence),
932 cond: Preconditions::None,
933 memo: Memo::None,
934 operations: VecM::try_from(vec![op]).unwrap(),
935 ext: TransactionExt::V1(soroban_data),
936 };
937 encode_envelope(TransactionEnvelope::Tx(TransactionV1Envelope {
938 tx,
939 signatures: VecM::default(),
940 }))
941}
942
943pub fn build_extend_footprint_tx(params: &ExtendFootprintParams) -> Result<String, DecodeError> {
948 let merged = merge_footprint_keys(¶ms.footprint_keys)?;
949 let decoded: Result<Vec<LedgerKey>, _> = merged.iter().map(|k| decode_ledger_key(k)).collect();
950 let data = empty_soroban_data_with_keys(&decoded?)?;
951 build_extend_envelope(params, data)
952}
953
954pub fn build_extend_footprint_tx_with_data(
957 params: &ExtendFootprintParams,
958 soroban_data: SorobanTransactionData,
959) -> Result<String, DecodeError> {
960 build_extend_envelope(params, soroban_data)
961}
962
963#[derive(Debug, Clone)]
965pub struct RestoreFootprintParams {
966 pub source_account: String,
968 pub sequence: i64,
970 pub fee: u32,
972 pub soroban_data: SorobanTransactionData,
974}
975
976pub fn build_restore_footprint_tx(params: &RestoreFootprintParams) -> Result<String, DecodeError> {
984 let footprint = ¶ms.soroban_data.resources.footprint;
985 if footprint.read_only.is_empty() && footprint.read_write.is_empty() {
986 return Err(DecodeError::Extraction(
987 "restore footprint is empty: no archived entries to restore".into(),
988 ));
989 }
990 if i64::from(params.fee) < params.soroban_data.resource_fee {
991 return Err(DecodeError::Extraction(format!(
992 "restore fee {} is below the preamble resource fee {}",
993 params.fee, params.soroban_data.resource_fee
994 )));
995 }
996 let source_account = decode_account_id(¶ms.source_account)?;
997 let op = Operation {
998 source_account: None,
999 body: OperationBody::RestoreFootprint(RestoreFootprintOp {
1000 ext: ExtensionPoint::V0,
1001 }),
1002 };
1003 let tx = Transaction {
1004 source_account: muxed_from_account(source_account),
1005 fee: params.fee,
1006 seq_num: SequenceNumber(params.sequence),
1007 cond: Preconditions::None,
1008 memo: Memo::None,
1009 operations: VecM::try_from(vec![op]).unwrap(),
1010 ext: TransactionExt::V1(params.soroban_data.clone()),
1011 };
1012 encode_envelope(TransactionEnvelope::Tx(TransactionV1Envelope {
1013 tx,
1014 signatures: VecM::default(),
1015 }))
1016}
1017
1018pub fn derive_contract_id(
1028 network_id: &[u8; 32],
1029 deployer_address: &str,
1030 salt: &[u8; 20],
1031 wasm_hash: &[u8; 32],
1032) -> Result<String, DecodeError> {
1033 let deployer = decode_account_id(deployer_address)?;
1034
1035 let mut salt_bytes = [0u8; 32];
1037 salt_bytes[..salt.len()].copy_from_slice(salt);
1038
1039 let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
1040 address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(
1041 match deployer.0 {
1042 PublicKey::PublicKeyTypeEd25519(u) => u,
1043 },
1044 ))),
1045 salt: Uint256(salt_bytes),
1046 });
1047
1048 let full_preimage = HashIdPreimage::ContractId(HashIdPreimageContractId {
1050 network_id: Hash(*network_id),
1051 contract_id_preimage,
1052 });
1053
1054 let mut buf = Vec::new();
1055 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
1056 full_preimage
1057 .write_xdr(&mut l)
1058 .map_err(DecodeError::XdrWrite)?;
1059
1060 let hash = Sha256::digest(&buf);
1061 let _ = wasm_hash;
1062 Ok(hex::encode(hash))
1063}
1064
1065#[cfg(test)]
1066fn realistic_soroban_transaction_data() -> SorobanTransactionData {
1067 use stellar_xdr::{
1068 ContractDataDurability, LedgerFootprint, LedgerKey, LedgerKeyContractData, ScVal,
1069 SorobanResources, SorobanTransactionDataExt,
1070 };
1071
1072 let contract_key = LedgerKey::ContractData(LedgerKeyContractData {
1073 contract: ScAddress::Contract(ContractId(Hash([0; 32]))),
1074 key: ScVal::LedgerKeyContractInstance,
1075 durability: ContractDataDurability::Persistent,
1076 });
1077
1078 SorobanTransactionData {
1079 ext: SorobanTransactionDataExt::V0,
1080 resources: SorobanResources {
1081 footprint: LedgerFootprint {
1082 read_only: VecM::try_from(vec![contract_key]).unwrap(),
1083 read_write: VecM::default(),
1084 },
1085 instructions: 100_000_000,
1086 disk_read_bytes: 100_000,
1087 write_bytes: 100_000,
1088 },
1089 resource_fee: 50_000,
1090 }
1091}
1092
1093#[cfg(test)]
1095fn test_auth_entry() -> SorobanAuthorizationEntry {
1096 use stellar_xdr::{SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanCredentials};
1097
1098 SorobanAuthorizationEntry {
1099 credentials: SorobanCredentials::SourceAccount,
1100 root_invocation: SorobanAuthorizedInvocation {
1101 function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs {
1102 contract_address: ScAddress::Contract(ContractId(Hash([0; 32]))),
1103 function_name: ScSymbol::try_from("hello").unwrap(),
1104 args: VecM::default(),
1105 }),
1106 sub_invocations: VecM::default(),
1107 },
1108 }
1109}
1110
1111#[cfg(test)]
1112mod tests {
1113 use super::*;
1114 use stellar_xdr::{OperationBody, SorobanCredentials};
1115
1116 const TEST_SOURCE: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
1117 const TEST_CONTRACT: &str = "CAAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQC526";
1118
1119 #[test]
1120 fn test_decode_account_id_valid() {
1121 let acc = decode_account_id(TEST_SOURCE).unwrap();
1122 match acc.0 {
1123 PublicKey::PublicKeyTypeEd25519(_) => {}
1124 }
1125 }
1126
1127 #[test]
1128 fn test_decode_account_id_invalid() {
1129 assert!(
1130 decode_account_id("GBZXLHQZGOWBZY6W3U4Z7GZGGXYVQBZWYM3XEQZ7W5Z4QXYZ5Z3XYY").is_err()
1131 ); assert!(
1133 decode_account_id("CAZXLHQZGOWBZY6W3U4Z7GZGGXYVQBZWYM3XEQZ7W5Z4QXYZ5Z3XYYYY").is_err()
1134 ); assert!(decode_account_id("not-a-key").is_err());
1136 }
1137
1138 #[test]
1139 fn test_decode_contract_id_valid() {
1140 let hash = decode_contract_id(TEST_CONTRACT).unwrap();
1141 assert_eq!(hash.0.len(), 32);
1142 }
1143
1144 #[test]
1145 fn test_decode_contract_id_invalid() {
1146 assert!(decode_contract_id(TEST_SOURCE).is_err()); }
1148
1149 #[test]
1150 fn test_build_invoke_transaction() {
1151 let params = InvokeTransactionParams {
1152 source_account: TEST_SOURCE.to_string(),
1153 sequence: 12345,
1154 fee: 100,
1155 contract_id: TEST_CONTRACT.to_string(),
1156 function: "hello".to_string(),
1157 args: vec![],
1158 memo: None,
1159 };
1160
1161 let envelope = build_invoke_transaction(¶ms).unwrap();
1162
1163 let raw = STANDARD.decode(&envelope).unwrap();
1165
1166 let mut cursor = std::io::Cursor::new(&raw);
1168 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
1169 let env = TransactionEnvelope::read_xdr(&mut l).unwrap();
1170
1171 match env {
1172 TransactionEnvelope::Tx(v1) => {
1173 assert_eq!(v1.tx.fee, 100);
1174 assert_eq!(v1.tx.seq_num.0, 12345);
1175 }
1176 _ => panic!("Expected V1 envelope"),
1177 }
1178 }
1179
1180 #[test]
1181 fn test_build_invoke_transaction_with_args() {
1182 let arg_b64 = "AAAABAAAACo=";
1184
1185 let params = InvokeTransactionParams {
1186 source_account: TEST_SOURCE.to_string(),
1187 sequence: 1,
1188 fee: 100,
1189 contract_id: TEST_CONTRACT.to_string(),
1190 function: "add".to_string(),
1191 args: vec![arg_b64.to_string()],
1192 memo: None,
1193 };
1194
1195 let envelope = build_invoke_transaction(¶ms).unwrap();
1196 assert!(!envelope.is_empty());
1197 }
1198
1199 fn envelope_memo(envelope_b64: &str) -> Memo {
1200 let raw = STANDARD.decode(envelope_b64).unwrap();
1201 let mut cursor = std::io::Cursor::new(&raw);
1202 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
1203 match TransactionEnvelope::read_xdr(&mut l).unwrap() {
1204 TransactionEnvelope::Tx(v1) => v1.tx.memo,
1205 _ => panic!("Expected V1 envelope"),
1206 }
1207 }
1208
1209 #[test]
1210 fn test_build_invoke_transaction_with_memo_text() {
1211 let params = InvokeTransactionParams {
1212 source_account: TEST_SOURCE.to_string(),
1213 sequence: 7,
1214 fee: 100,
1215 contract_id: TEST_CONTRACT.to_string(),
1216 function: "transfer".to_string(),
1217 args: vec![],
1218 memo: Some(memo_text("deposit-123").unwrap()),
1219 };
1220
1221 let envelope = build_invoke_transaction(¶ms).unwrap();
1222 let memo = envelope_memo(&envelope);
1223
1224 assert!(matches!(memo, Memo::Text(_)));
1225 assert_eq!(memo, memo_text("deposit-123").unwrap());
1226 }
1227
1228 #[test]
1229 fn test_build_invoke_transaction_with_memo_id() {
1230 let params = InvokeTransactionParams {
1231 source_account: TEST_SOURCE.to_string(),
1232 sequence: 7,
1233 fee: 100,
1234 contract_id: TEST_CONTRACT.to_string(),
1235 function: "transfer".to_string(),
1236 args: vec![],
1237 memo: Some(memo_id(42)),
1238 };
1239
1240 let envelope = build_invoke_transaction(¶ms).unwrap();
1241 let memo = envelope_memo(&envelope);
1242
1243 assert!(matches!(memo, Memo::Id(_)));
1244 assert_eq!(memo, Memo::Id(42));
1245 }
1246
1247 #[test]
1248 fn test_build_invoke_transaction_without_memo_is_none() {
1249 let params = InvokeTransactionParams {
1250 source_account: TEST_SOURCE.to_string(),
1251 sequence: 7,
1252 fee: 100,
1253 contract_id: TEST_CONTRACT.to_string(),
1254 function: "transfer".to_string(),
1255 args: vec![],
1256 memo: None,
1257 };
1258
1259 let envelope = build_invoke_transaction(¶ms).unwrap();
1260 assert_eq!(envelope_memo(&envelope), Memo::None);
1261 }
1262
1263 #[test]
1264 fn test_memo_text_accepts_max_length_and_rejects_over_long() {
1265 let max = "x".repeat(28);
1266 assert!(memo_text(&max).is_ok());
1267
1268 let too_long = "x".repeat(29);
1269 let err = memo_text(&too_long).unwrap_err();
1270 assert!(matches!(err, DecodeError::Extraction(msg) if msg.contains("28 bytes")));
1271 }
1272
1273 #[test]
1274 fn test_build_upload_wasm_tx_produces_valid_xdr() {
1275 let params = UploadWasmParams {
1276 source_account: TEST_SOURCE.to_string(),
1277 sequence: 1,
1278 fee: 100,
1279 wasm_bytes: vec![0x00, 0x61, 0x73, 0x6d], };
1281
1282 let envelope = build_upload_wasm_tx(¶ms).unwrap();
1283 assert!(!envelope.is_empty());
1284
1285 let raw = STANDARD.decode(&envelope).unwrap();
1287 let mut cursor = std::io::Cursor::new(&raw);
1288 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
1289 let env = TransactionEnvelope::read_xdr(&mut l).unwrap();
1290
1291 match env {
1292 TransactionEnvelope::Tx(v1) => {
1293 assert_eq!(v1.tx.fee, 100);
1294 assert_eq!(v1.tx.seq_num.0, 1);
1295 }
1296 _ => panic!("Expected V1 envelope"),
1297 }
1298 }
1299
1300 #[test]
1301 fn test_build_create_contract_tx_produces_valid_xdr() {
1302 let wasm_hash = [1u8; 32];
1303 let salt = [2u8; 20];
1304
1305 let params = CreateContractParams {
1306 source_account: TEST_SOURCE.to_string(),
1307 sequence: 2,
1308 fee: 100,
1309 wasm_hash,
1310 deployer_address: TEST_SOURCE.to_string(),
1311 salt,
1312 };
1313
1314 let envelope = build_create_contract_tx(¶ms).unwrap();
1315 assert!(!envelope.is_empty());
1316
1317 let raw = STANDARD.decode(&envelope).unwrap();
1319 let mut cursor = std::io::Cursor::new(&raw);
1320 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
1321 let env = TransactionEnvelope::read_xdr(&mut l).unwrap();
1322
1323 match env {
1324 TransactionEnvelope::Tx(v1) => {
1325 assert_eq!(v1.tx.fee, 100);
1326 assert_eq!(v1.tx.seq_num.0, 2);
1327 }
1328 _ => panic!("Expected V1 envelope"),
1329 }
1330 }
1331
1332 #[test]
1333 fn test_derive_contract_id_matches_stellar_formula() {
1334 let mut network_id = [0u8; 32];
1350 network_id.copy_from_slice(
1351 &hex::decode("cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472")
1352 .unwrap(),
1353 );
1354
1355 let source = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
1356 let salt = [3u8; 20];
1357 let wasm_hash = [4u8; 32];
1358
1359 let result = derive_contract_id(&network_id, source, &salt, &wasm_hash);
1360 assert!(result.is_ok());
1361
1362 let contract_id = result.unwrap();
1363 assert_eq!(
1364 contract_id,
1365 "e52f714abcb6555da317cb8d4265ffce88401c06e40e60c2d1f721445f683c3c"
1366 );
1367 }
1368
1369 #[test]
1370 fn test_derive_contract_id_different_networks_produce_different_ids() {
1371 let mut testnet_network_id = [0u8; 32];
1373 testnet_network_id.copy_from_slice(
1374 &hex::decode("cee0302d59844d32bdca915c8203dd44b33fbb7edc19051ea37abedf28ecd472")
1375 .unwrap(),
1376 );
1377
1378 let mut mainnet_network_id = [0u8; 32];
1379 mainnet_network_id.copy_from_slice(
1380 &hex::decode("7ac33997544e3264d040315e569b3cd71dbbc6eb9a5aa6f2b7ecf1df4b49e5e1")
1381 .unwrap(),
1382 );
1383
1384 let source = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
1385 let salt = [3u8; 20];
1386 let wasm_hash = [4u8; 32];
1387
1388 let testnet_id =
1389 derive_contract_id(&testnet_network_id, source, &salt, &wasm_hash).unwrap();
1390 let mainnet_id =
1391 derive_contract_id(&mainnet_network_id, source, &salt, &wasm_hash).unwrap();
1392
1393 assert_ne!(testnet_id, mainnet_id);
1394 }
1395
1396 #[test]
1397 fn test_build_upload_wasm_tx_with_data() {
1398 let params = UploadWasmParams {
1399 source_account: TEST_SOURCE.to_string(),
1400 sequence: 1,
1401 fee: 50_100, wasm_bytes: vec![0x00, 0x61, 0x73, 0x6d],
1403 };
1404
1405 let soroban_data = realistic_soroban_transaction_data();
1406 let envelope = build_upload_wasm_tx_with_data(¶ms, soroban_data).unwrap();
1407 assert!(!envelope.is_empty());
1408
1409 let raw = STANDARD.decode(&envelope).unwrap();
1411 let mut cursor = std::io::Cursor::new(&raw);
1412 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
1413 let env = TransactionEnvelope::read_xdr(&mut l).unwrap();
1414
1415 match env {
1416 TransactionEnvelope::Tx(v1) => {
1417 assert_eq!(v1.tx.fee, 50_100);
1418 match v1.tx.ext {
1419 TransactionExt::V1(data) => {
1420 assert_eq!(data.resource_fee, 50_000);
1421 assert_eq!(data.resources.instructions, 100_000_000);
1422 assert_eq!(data.resources.disk_read_bytes, 100_000);
1423 assert_eq!(data.resources.write_bytes, 100_000);
1424 assert_eq!(data.resources.footprint.read_only.len(), 1);
1425 }
1426 _ => panic!("Expected V1 extension"),
1427 }
1428 }
1429 _ => panic!("Expected V1 envelope"),
1430 }
1431 }
1432
1433 #[test]
1434 fn test_build_create_contract_tx_with_data() {
1435 let wasm_hash = [1u8; 32];
1436 let salt = [2u8; 20];
1437
1438 let params = CreateContractParams {
1439 source_account: TEST_SOURCE.to_string(),
1440 sequence: 2,
1441 fee: 50_100,
1442 wasm_hash,
1443 deployer_address: TEST_SOURCE.to_string(),
1444 salt,
1445 };
1446
1447 let soroban_data = realistic_soroban_transaction_data();
1448 let envelope = build_create_contract_tx_with_data(¶ms, soroban_data).unwrap();
1449 assert!(!envelope.is_empty());
1450
1451 let raw = STANDARD.decode(&envelope).unwrap();
1453 let mut cursor = std::io::Cursor::new(&raw);
1454 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
1455 let env = TransactionEnvelope::read_xdr(&mut l).unwrap();
1456
1457 match env {
1458 TransactionEnvelope::Tx(v1) => {
1459 assert_eq!(v1.tx.fee, 50_100);
1460 match v1.tx.ext {
1461 TransactionExt::V1(data) => {
1462 assert_eq!(data.resource_fee, 50_000);
1463 assert_eq!(data.resources.instructions, 100_000_000);
1464 }
1465 _ => panic!("Expected V1 extension"),
1466 }
1467 }
1468 _ => panic!("Expected V1 envelope"),
1469 }
1470 }
1471
1472 #[test]
1473 fn test_parse_soroban_transaction_data() {
1474 let data = realistic_soroban_transaction_data();
1476 let mut buf = Vec::new();
1477 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
1478 data.write_xdr(&mut l).unwrap();
1479 let encoded = STANDARD.encode(&buf);
1480
1481 let result = parse_soroban_transaction_data(&encoded);
1483 assert!(result.is_ok());
1484 let parsed = result.unwrap();
1485 assert_eq!(parsed.resource_fee, 50_000);
1486 assert_eq!(parsed.resources.instructions, 100_000_000);
1487 assert_eq!(parsed.resources.footprint.read_only.len(), 1);
1488 }
1489
1490 #[test]
1491 fn test_build_upload_wasm_tx_with_auth() {
1492 let params = UploadWasmParams {
1493 source_account: TEST_SOURCE.to_string(),
1494 sequence: 1,
1495 fee: 50_100,
1496 wasm_bytes: vec![0x00, 0x61, 0x73, 0x6d],
1497 };
1498
1499 let soroban_data = realistic_soroban_transaction_data();
1500 let auth_entries = vec![test_auth_entry()];
1501 let envelope =
1502 build_upload_wasm_tx_with_data_and_auth(¶ms, soroban_data, auth_entries).unwrap();
1503 assert!(!envelope.is_empty());
1504
1505 let raw = STANDARD.decode(&envelope).unwrap();
1507 let mut cursor = std::io::Cursor::new(&raw);
1508 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
1509 let env = TransactionEnvelope::read_xdr(&mut l).unwrap();
1510
1511 match env {
1512 TransactionEnvelope::Tx(v1) => match v1.tx.ext {
1513 TransactionExt::V1(data) => {
1514 assert_eq!(data.resource_fee, 50_000);
1515 let op = &v1.tx.operations[0];
1517 match &op.body {
1518 OperationBody::InvokeHostFunction(host_fn) => {
1519 assert_eq!(host_fn.auth.len(), 1);
1520 assert_eq!(
1521 host_fn.auth[0].credentials,
1522 SorobanCredentials::SourceAccount
1523 );
1524 }
1525 _ => panic!("Expected InvokeHostFunction"),
1526 }
1527 }
1528 _ => panic!("Expected V1 extension"),
1529 },
1530 _ => panic!("Expected V1 envelope"),
1531 }
1532 }
1533
1534 #[test]
1535 fn test_build_create_contract_tx_with_auth() {
1536 let wasm_hash = [1u8; 32];
1537 let salt = [2u8; 20];
1538
1539 let params = CreateContractParams {
1540 source_account: TEST_SOURCE.to_string(),
1541 sequence: 2,
1542 fee: 50_100,
1543 wasm_hash,
1544 deployer_address: TEST_SOURCE.to_string(),
1545 salt,
1546 };
1547
1548 let soroban_data = realistic_soroban_transaction_data();
1549 let auth_entries = vec![test_auth_entry()];
1550 let envelope =
1551 build_create_contract_tx_with_data_and_auth(¶ms, soroban_data, auth_entries)
1552 .unwrap();
1553 assert!(!envelope.is_empty());
1554
1555 let raw = STANDARD.decode(&envelope).unwrap();
1557 let mut cursor = std::io::Cursor::new(&raw);
1558 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
1559 let env = TransactionEnvelope::read_xdr(&mut l).unwrap();
1560
1561 match env {
1562 TransactionEnvelope::Tx(v1) => match v1.tx.ext {
1563 TransactionExt::V1(data) => {
1564 assert_eq!(data.resource_fee, 50_000);
1565 let op = &v1.tx.operations[0];
1567 match &op.body {
1568 OperationBody::InvokeHostFunction(host_fn) => {
1569 assert_eq!(host_fn.auth.len(), 1);
1570 assert_eq!(
1571 host_fn.auth[0].credentials,
1572 SorobanCredentials::SourceAccount
1573 );
1574 }
1575 _ => panic!("Expected InvokeHostFunction"),
1576 }
1577 }
1578 _ => panic!("Expected V1 extension"),
1579 },
1580 _ => panic!("Expected V1 envelope"),
1581 }
1582 }
1583
1584 #[test]
1585 fn test_parse_invalid_auth_entry() {
1586 let result = parse_soroban_authorization_entry("invalid-base64!!!");
1587 assert!(result.is_err());
1588 }
1589
1590 #[test]
1591 fn test_parse_empty_auth_list() {
1592 let auth_list: Vec<String> = vec![];
1593 let result = parse_soroban_authorization_entries(&auth_list);
1594 assert!(result.is_ok());
1595 assert_eq!(result.unwrap().len(), 0);
1596 }
1597
1598 #[test]
1599 fn test_build_create_contract_v2_tx_produces_valid_xdr() {
1600 use crate::scval_to_base64;
1601 let wasm_hash = [1u8; 32];
1602 let salt = [2u8; 20];
1603
1604 let arg_b64 = scval_to_base64(&ScVal::U32(42)).unwrap();
1605 let params = CreateContractV2Params {
1606 source_account: TEST_SOURCE.to_string(),
1607 sequence: 2,
1608 fee: 100,
1609 wasm_hash,
1610 deployer_address: TEST_SOURCE.to_string(),
1611 salt,
1612 constructor_args: vec![arg_b64],
1613 };
1614
1615 let envelope = build_create_contract_v2_tx(¶ms).unwrap();
1616 assert!(!envelope.is_empty());
1617
1618 let raw = STANDARD.decode(&envelope).unwrap();
1619 let mut cursor = std::io::Cursor::new(&raw);
1620 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
1621 let env = TransactionEnvelope::read_xdr(&mut l).unwrap();
1622
1623 match env {
1624 TransactionEnvelope::Tx(v1) => {
1625 assert_eq!(v1.tx.fee, 100);
1626 assert_eq!(v1.tx.seq_num.0, 2);
1627 let op = &v1.tx.operations[0];
1628 match &op.body {
1629 OperationBody::InvokeHostFunction(host_fn) => match &host_fn.host_function {
1630 HostFunction::CreateContractV2(args) => {
1631 assert_eq!(args.constructor_args.len(), 1);
1632 assert_eq!(args.constructor_args[0], ScVal::U32(42));
1633 assert_eq!(args.executable, ContractExecutable::Wasm(Hash(wasm_hash)));
1634 }
1635 _ => panic!("Expected CreateContractV2 host function"),
1636 },
1637 _ => panic!("Expected InvokeHostFunction operation"),
1638 }
1639 }
1640 _ => panic!("Expected V1 envelope"),
1641 }
1642 }
1643
1644 #[test]
1645 fn test_build_create_contract_v2_tx_with_data() {
1646 use crate::scval_to_base64;
1647 let wasm_hash = [1u8; 32];
1648 let salt = [2u8; 20];
1649
1650 let arg_b64 = scval_to_base64(&ScVal::I32(-7)).unwrap();
1651 let params = CreateContractV2Params {
1652 source_account: TEST_SOURCE.to_string(),
1653 sequence: 3,
1654 fee: 100,
1655 wasm_hash,
1656 deployer_address: TEST_SOURCE.to_string(),
1657 salt,
1658 constructor_args: vec![arg_b64],
1659 };
1660
1661 let soroban_data = realistic_soroban_transaction_data();
1662 let envelope = build_create_contract_v2_tx_with_data(¶ms, soroban_data).unwrap();
1663 assert!(!envelope.is_empty());
1664
1665 let raw = STANDARD.decode(&envelope).unwrap();
1666 let mut cursor = std::io::Cursor::new(&raw);
1667 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
1668 let env = TransactionEnvelope::read_xdr(&mut l).unwrap();
1669
1670 match env {
1671 TransactionEnvelope::Tx(v1) => match v1.tx.ext {
1672 TransactionExt::V1(data) => {
1673 assert_eq!(data.resource_fee, 50_000);
1674 let op = &v1.tx.operations[0];
1675 match &op.body {
1676 OperationBody::InvokeHostFunction(host_fn) => {
1677 match &host_fn.host_function {
1678 HostFunction::CreateContractV2(args) => {
1679 assert_eq!(args.constructor_args.len(), 1);
1680 assert_eq!(args.constructor_args[0], ScVal::I32(-7));
1681 }
1682 _ => panic!("Expected CreateContractV2"),
1683 }
1684 }
1685 _ => panic!("Expected InvokeHostFunction"),
1686 }
1687 }
1688 _ => panic!("Expected V1 extension"),
1689 },
1690 _ => panic!("Expected V1 envelope"),
1691 }
1692 }
1693
1694 #[test]
1695 fn test_build_create_contract_v2_tx_with_auth() {
1696 use crate::scval_to_base64;
1697 let wasm_hash = [1u8; 32];
1698 let salt = [2u8; 20];
1699
1700 let arg_b64 = scval_to_base64(&ScVal::U32(99)).unwrap();
1701 let params = CreateContractV2Params {
1702 source_account: TEST_SOURCE.to_string(),
1703 sequence: 4,
1704 fee: 50_100,
1705 wasm_hash,
1706 deployer_address: TEST_SOURCE.to_string(),
1707 salt,
1708 constructor_args: vec![arg_b64],
1709 };
1710
1711 let soroban_data = realistic_soroban_transaction_data();
1712 let auth_entries = vec![test_auth_entry()];
1713 let envelope =
1714 build_create_contract_v2_tx_with_data_and_auth(¶ms, soroban_data, auth_entries)
1715 .unwrap();
1716 assert!(!envelope.is_empty());
1717
1718 let raw = STANDARD.decode(&envelope).unwrap();
1719 let mut cursor = std::io::Cursor::new(&raw);
1720 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
1721 let env = TransactionEnvelope::read_xdr(&mut l).unwrap();
1722
1723 match env {
1724 TransactionEnvelope::Tx(v1) => {
1725 let op = &v1.tx.operations[0];
1726 match &op.body {
1727 OperationBody::InvokeHostFunction(host_fn) => {
1728 assert_eq!(host_fn.auth.len(), 1);
1729 assert_eq!(
1730 host_fn.auth[0].credentials,
1731 SorobanCredentials::SourceAccount
1732 );
1733 match &host_fn.host_function {
1734 HostFunction::CreateContractV2(args) => {
1735 assert_eq!(args.constructor_args.len(), 1);
1736 assert_eq!(args.constructor_args[0], ScVal::U32(99));
1737 }
1738 _ => panic!("Expected CreateContractV2"),
1739 }
1740 }
1741 _ => panic!("Expected InvokeHostFunction"),
1742 }
1743 }
1744 _ => panic!("Expected V1 envelope"),
1745 }
1746 }
1747
1748 #[test]
1749 fn test_contract_id_derivation_identical_between_v1_and_v2() {
1750 use crate::scval_to_base64;
1751
1752 let network_id = [42u8; 32];
1753 let deployer = TEST_SOURCE;
1754 let salt = [7u8; 20];
1755 let wasm_hash = [99u8; 32];
1756
1757 let v1_params = CreateContractParams {
1758 source_account: deployer.to_string(),
1759 sequence: 100,
1760 fee: 50_000,
1761 wasm_hash,
1762 deployer_address: deployer.to_string(),
1763 salt,
1764 };
1765 let v1_envelope = build_create_contract_tx(&v1_params).unwrap();
1766
1767 let arg_b64 = scval_to_base64(&ScVal::U32(42)).unwrap();
1768 let v2_params = CreateContractV2Params {
1769 source_account: deployer.to_string(),
1770 sequence: 100,
1771 fee: 50_000,
1772 wasm_hash,
1773 deployer_address: deployer.to_string(),
1774 salt,
1775 constructor_args: vec![arg_b64],
1776 };
1777 let v2_envelope = build_create_contract_v2_tx(&v2_params).unwrap();
1778
1779 let decode_env = |b64: &str| -> TransactionEnvelope {
1780 let raw = STANDARD.decode(b64).unwrap();
1781 let mut cursor = std::io::Cursor::new(&raw);
1782 let mut l = stellar_xdr::Limited::new(&mut cursor, stellar_xdr::Limits::none());
1783 TransactionEnvelope::read_xdr(&mut l).unwrap()
1784 };
1785
1786 let v1_preimage = match decode_env(&v1_envelope) {
1787 TransactionEnvelope::Tx(v1) => match &v1.tx.operations[0].body {
1788 OperationBody::InvokeHostFunction(hf) => match &hf.host_function {
1789 HostFunction::CreateContract(args) => args.contract_id_preimage.clone(),
1790 _ => panic!("Expected CreateContract in V1"),
1791 },
1792 _ => panic!("Expected InvokeHostFunction"),
1793 },
1794 _ => panic!("Expected V1 envelope"),
1795 };
1796
1797 let v2_preimage = match decode_env(&v2_envelope) {
1798 TransactionEnvelope::Tx(v1) => match &v1.tx.operations[0].body {
1799 OperationBody::InvokeHostFunction(hf) => match &hf.host_function {
1800 HostFunction::CreateContractV2(args) => args.contract_id_preimage.clone(),
1801 _ => panic!("Expected CreateContractV2 in V2"),
1802 },
1803 _ => panic!("Expected InvokeHostFunction"),
1804 },
1805 _ => panic!("Expected V1 envelope"),
1806 };
1807
1808 assert_eq!(v1_preimage, v2_preimage);
1809
1810 let id1 = derive_contract_id(&network_id, deployer, &salt, &wasm_hash).unwrap();
1812 let id2 = derive_contract_id(&network_id, deployer, &salt, &wasm_hash).unwrap();
1813 assert_eq!(id1, id2);
1814 }
1815
1816 #[test]
1817 fn test_auth_entry_create_contract_v2_host_fn() {
1818 use stellar_xdr::{
1819 SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanCredentials,
1820 };
1821
1822 let deployer = decode_account_id(TEST_SOURCE).unwrap();
1823 let preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress {
1824 address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(
1825 match deployer.0 {
1826 PublicKey::PublicKeyTypeEd25519(u) => u,
1827 },
1828 ))),
1829 salt: Uint256([0; 32]),
1830 });
1831
1832 let constructor_args = VecM::try_from(vec![ScVal::U32(100)]).unwrap();
1833
1834 let auth_entry = SorobanAuthorizationEntry {
1835 credentials: SorobanCredentials::SourceAccount,
1836 root_invocation: SorobanAuthorizedInvocation {
1837 function: SorobanAuthorizedFunction::CreateContractV2HostFn(CreateContractArgsV2 {
1838 contract_id_preimage: preimage,
1839 executable: ContractExecutable::Wasm(Hash([1; 32])),
1840 constructor_args,
1841 }),
1842 sub_invocations: VecM::default(),
1843 },
1844 };
1845
1846 let mut buf = Vec::new();
1847 let mut l = stellar_xdr::Limited::new(&mut buf, stellar_xdr::Limits::none());
1848 auth_entry.write_xdr(&mut l).unwrap();
1849 let b64 = STANDARD.encode(&buf);
1850
1851 let parsed = parse_soroban_authorization_entry(&b64).unwrap();
1852 match parsed.root_invocation.function {
1853 SorobanAuthorizedFunction::CreateContractV2HostFn(args) => {
1854 assert_eq!(args.executable, ContractExecutable::Wasm(Hash([1; 32])));
1855 assert_eq!(args.constructor_args.len(), 1);
1856 assert_eq!(args.constructor_args[0], ScVal::U32(100));
1857 }
1858 _ => panic!("Expected CreateContractV2HostFn"),
1859 }
1860 }
1861}
1862
1863#[cfg(test)]
1864mod extract_tests {
1865 use crate::extract_contract_data_value;
1866 use base64::Engine;
1867 use stellar_xdr::{
1868 ContractDataDurability, ContractDataEntry, ContractId, ExtensionPoint, Hash, LedgerEntry,
1869 LedgerEntryData, LedgerEntryExt, Limited, Limits, ScAddress, ScVal, WriteXdr,
1870 };
1871
1872 #[test]
1873 fn test_extract_contract_data_value_returns_val() {
1874 let entry = LedgerEntry {
1875 last_modified_ledger_seq: 1,
1876 data: LedgerEntryData::ContractData(ContractDataEntry {
1877 ext: ExtensionPoint::V0,
1878 contract: ScAddress::Contract(ContractId(Hash([0; 32]))),
1879 key: ScVal::U32(1),
1880 durability: ContractDataDurability::Persistent,
1881 val: ScVal::U32(42),
1882 }),
1883 ext: LedgerEntryExt::V0,
1884 };
1885 let mut buf = Vec::new();
1886 let mut l = Limited::new(&mut buf, Limits::none());
1887 entry.write_xdr(&mut l).unwrap();
1888 let b64 = base64::engine::general_purpose::STANDARD.encode(&buf);
1889
1890 let cd = extract_contract_data_value(&b64).unwrap();
1891 assert!(matches!(cd.val, ScVal::U32(42)));
1892 assert_eq!(cd.key, ScVal::U32(1));
1893 assert_eq!(cd.durability, ContractDataDurability::Persistent);
1894 }
1895
1896 #[test]
1897 fn test_extract_contract_data_value_rejects_non_contract_data() {
1898 let result = extract_contract_data_value("AAAABAAAAAE=");
1899 assert!(result.is_err());
1900 }
1901}
1902
1903#[cfg(test)]
1904mod extend_tests {
1905 use super::*;
1906 use crate::builder::{
1907 build_extend_footprint_tx, build_extend_footprint_tx_with_data,
1908 realistic_soroban_transaction_data, ExtendFootprintParams,
1909 };
1910 use stellar_xdr::{
1911 ContractDataDurability, Hash, LedgerKey, LedgerKeyContractData, Limited, Limits,
1912 OperationBody, ScAddress, ScVal, TransactionEnvelope, TransactionExt, WriteXdr,
1913 };
1914
1915 #[test]
1916 fn test_build_extend_footprint_tx_round_trip() {
1917 let k = LedgerKey::ContractData(LedgerKeyContractData {
1918 contract: ScAddress::Contract(ContractId(Hash([0; 32]))),
1919 key: ScVal::LedgerKeyContractInstance,
1920 durability: ContractDataDurability::Persistent,
1921 });
1922 let mut buf = Vec::new();
1923 let mut l = Limited::new(&mut buf, Limits::none());
1924 k.write_xdr(&mut l).unwrap();
1925 let key = STANDARD.encode(&buf);
1926
1927 let params = ExtendFootprintParams {
1928 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".into(),
1929 sequence: 7,
1930 fee: 100,
1931 extend_to: 100_000,
1932 footprint_keys: vec![key],
1933 };
1934 let envelope = build_extend_footprint_tx(¶ms).unwrap();
1935 let raw = STANDARD.decode(&envelope).unwrap();
1936 let mut cursor = std::io::Cursor::new(&raw);
1937 let mut l = Limited::new(&mut cursor, Limits::none());
1938 let env = TransactionEnvelope::read_xdr(&mut l).unwrap();
1939 match env {
1940 TransactionEnvelope::Tx(v1) => {
1941 assert_eq!(v1.tx.seq_num.0, 7);
1942 match &v1.tx.operations[0].body {
1943 OperationBody::ExtendFootprintTtl(op) => {
1944 assert_eq!(op.extend_to, 100_000);
1945 }
1946 other => panic!("expected ExtendFootprintTtl, got {other:?}"),
1947 }
1948 match v1.tx.ext {
1949 TransactionExt::V1(data) => {
1950 assert_eq!(data.resources.footprint.read_only.len(), 1);
1951 assert!(data.resources.footprint.read_write.is_empty());
1952 }
1953 other => panic!("expected V1 extension, got {other:?}"),
1954 }
1955 }
1956 _ => panic!("Expected V1 envelope"),
1957 }
1958 }
1959
1960 #[test]
1961 fn test_build_extend_footprint_tx_rejects_zero_ledgers() {
1962 let k = LedgerKey::ContractData(LedgerKeyContractData {
1963 contract: ScAddress::Contract(ContractId(Hash([0; 32]))),
1964 key: ScVal::LedgerKeyContractInstance,
1965 durability: ContractDataDurability::Persistent,
1966 });
1967 let mut buf = Vec::new();
1968 let mut l = Limited::new(&mut buf, Limits::none());
1969 k.write_xdr(&mut l).unwrap();
1970 let key = STANDARD.encode(&buf);
1971
1972 let params = ExtendFootprintParams {
1973 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".into(),
1974 sequence: 1,
1975 fee: 100,
1976 extend_to: 0,
1977 footprint_keys: vec![key],
1978 };
1979 let err = build_extend_footprint_tx(¶ms).unwrap_err();
1980 assert!(err.to_string().contains("greater than 0"));
1981 }
1982
1983 #[test]
1984 fn test_build_extend_footprint_tx_with_data_preserves_simulation_resources() {
1985 let k = LedgerKey::ContractData(LedgerKeyContractData {
1986 contract: ScAddress::Contract(ContractId(Hash([0; 32]))),
1987 key: ScVal::LedgerKeyContractInstance,
1988 durability: ContractDataDurability::Persistent,
1989 });
1990 let mut buf = Vec::new();
1991 let mut l = Limited::new(&mut buf, Limits::none());
1992 k.write_xdr(&mut l).unwrap();
1993 let key = STANDARD.encode(&buf);
1994
1995 let params = ExtendFootprintParams {
1996 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".into(),
1997 sequence: 2,
1998 fee: 100,
1999 extend_to: 50_000,
2000 footprint_keys: vec![key],
2001 };
2002 let data = realistic_soroban_transaction_data();
2003 let envelope = build_extend_footprint_tx_with_data(¶ms, data).unwrap();
2004 let raw = STANDARD.decode(&envelope).unwrap();
2005 let mut cursor = std::io::Cursor::new(&raw);
2006 let mut l = Limited::new(&mut cursor, Limits::none());
2007 let env = TransactionEnvelope::read_xdr(&mut l).unwrap();
2008 match env {
2009 TransactionEnvelope::Tx(v1) => match v1.tx.ext {
2010 TransactionExt::V1(data) => {
2011 assert_eq!(data.resource_fee, 50_000);
2012 assert_eq!(data.resources.instructions, 100_000_000);
2013 }
2014 _ => panic!("Expected V1 extension"),
2015 },
2016 _ => panic!("Expected V1 envelope"),
2017 }
2018 }
2019}
2020
2021#[cfg(test)]
2022mod restore_tests {
2023 use super::*;
2024 use crate::builder::{build_restore_footprint_tx, RestoreFootprintParams};
2025 use stellar_xdr::{
2026 ContractDataDurability, Hash, LedgerFootprint, LedgerKey, LedgerKeyContractData, Limited,
2027 Limits, OperationBody, ScAddress, ScVal, SorobanResources, SorobanTransactionData,
2028 SorobanTransactionDataExt, TransactionEnvelope, TransactionExt, VecM,
2029 };
2030
2031 fn create_test_soroban_data() -> SorobanTransactionData {
2032 let k = LedgerKey::ContractData(LedgerKeyContractData {
2033 contract: ScAddress::Contract(ContractId(Hash([1; 32]))),
2034 key: ScVal::LedgerKeyContractInstance,
2035 durability: ContractDataDurability::Persistent,
2036 });
2037 SorobanTransactionData {
2038 ext: SorobanTransactionDataExt::V0,
2039 resources: SorobanResources {
2040 footprint: LedgerFootprint {
2041 read_only: VecM::try_from(vec![k]).unwrap(),
2042 read_write: VecM::default(),
2043 },
2044 instructions: 1000,
2045 disk_read_bytes: 500,
2046 write_bytes: 200,
2047 },
2048 resource_fee: 10000,
2049 }
2050 }
2051
2052 #[test]
2053 fn test_build_restore_footprint_tx_round_trip() {
2054 let soroban_data = create_test_soroban_data();
2055 let params = RestoreFootprintParams {
2056 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".into(),
2057 sequence: 10,
2058 fee: 10_200,
2059 soroban_data,
2060 };
2061 let envelope = build_restore_footprint_tx(¶ms).unwrap();
2062 let raw = STANDARD.decode(&envelope).unwrap();
2063 let mut cursor = std::io::Cursor::new(&raw);
2064 let mut l = Limited::new(&mut cursor, Limits::none());
2065 let env = TransactionEnvelope::read_xdr(&mut l).unwrap();
2066 match env {
2067 TransactionEnvelope::Tx(v1) => {
2068 assert_eq!(v1.tx.seq_num.0, 10);
2069 assert_eq!(v1.tx.fee, 10_200);
2070 match &v1.tx.operations[0].body {
2071 OperationBody::RestoreFootprint(_) => {
2072 }
2074 other => panic!("expected RestoreFootprint, got {other:?}"),
2075 }
2076 match &v1.tx.ext {
2078 TransactionExt::V1(soroban_data) => {
2079 assert_eq!(soroban_data.resource_fee, 10000);
2080 assert_eq!(soroban_data.resources.instructions, 1000);
2081 assert!(!soroban_data.resources.footprint.read_only.is_empty());
2082 }
2083 other => panic!("expected V1 extension, got {other:?}"),
2084 }
2085 }
2086 other => panic!("expected v1 envelope, got {other:?}"),
2087 }
2088 }
2089
2090 #[test]
2091 fn test_build_restore_footprint_tx_preserves_preamble_footprint() {
2092 let soroban_data = create_test_soroban_data();
2093 let original_footprint_keys = soroban_data.resources.footprint.read_only.clone();
2094
2095 let params = RestoreFootprintParams {
2096 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".into(),
2097 sequence: 11,
2098 fee: 10_300,
2099 soroban_data,
2100 };
2101 let envelope = build_restore_footprint_tx(¶ms).unwrap();
2102 let raw = STANDARD.decode(&envelope).unwrap();
2103 let mut cursor = std::io::Cursor::new(&raw);
2104 let mut l = Limited::new(&mut cursor, Limits::none());
2105 let env = TransactionEnvelope::read_xdr(&mut l).unwrap();
2106 match env {
2107 TransactionEnvelope::Tx(v1) => match &v1.tx.ext {
2108 TransactionExt::V1(soroban_data) => {
2109 assert_eq!(
2110 soroban_data.resources.footprint.read_only,
2111 original_footprint_keys
2112 );
2113 }
2114 other => panic!("expected V1 extension, got {other:?}"),
2115 },
2116 other => panic!("expected v1 envelope, got {other:?}"),
2117 }
2118 }
2119
2120 #[test]
2121 fn test_build_restore_footprint_tx_fee_floor_enforcement() {
2122 let soroban_data = create_test_soroban_data();
2123 let min_fee = soroban_data.resource_fee as u32;
2124
2125 let params = RestoreFootprintParams {
2127 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".into(),
2128 sequence: 12,
2129 fee: min_fee,
2130 soroban_data: soroban_data.clone(),
2131 };
2132 assert!(build_restore_footprint_tx(¶ms).is_ok());
2133
2134 let params = RestoreFootprintParams {
2136 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".into(),
2137 sequence: 13,
2138 fee: min_fee + 100,
2139 soroban_data: soroban_data.clone(),
2140 };
2141 assert!(build_restore_footprint_tx(¶ms).is_ok());
2142
2143 let params = RestoreFootprintParams {
2145 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".into(),
2146 sequence: 14,
2147 fee: min_fee - 1,
2148 soroban_data,
2149 };
2150 let err = build_restore_footprint_tx(¶ms).unwrap_err();
2151 assert!(err.to_string().contains("below the preamble resource fee"));
2152 }
2153
2154 #[test]
2155 fn test_build_restore_footprint_tx_rejects_empty_footprint() {
2156 let mut soroban_data = create_test_soroban_data();
2157 soroban_data.resources.footprint = LedgerFootprint {
2158 read_only: VecM::default(),
2159 read_write: VecM::default(),
2160 };
2161 let params = RestoreFootprintParams {
2162 source_account: "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF".into(),
2163 sequence: 15,
2164 fee: 20_000,
2165 soroban_data,
2166 };
2167 let err = build_restore_footprint_tx(¶ms).unwrap_err();
2168 assert!(err.to_string().contains("footprint is empty"));
2169 }
2170}