1use crate::{
2 log::format_auth_entry,
3 signer::ledger::LedgerEntry,
4 utils::fee_bump_transaction_hash,
5 xdr::{
6 self, AccountId, DecoratedSignature, FeeBumpTransactionEnvelope, Hash, HashIdPreimage,
7 HashIdPreimageSorobanAuthorization, Limits, MuxedAccount, Operation, OperationBody,
8 PublicKey, ScAddress, ScMap, ScSymbol, ScVal, Signature, SignatureHint,
9 SorobanAddressCredentials, SorobanAuthorizationEntry, SorobanCredentials, Transaction,
10 TransactionEnvelope, TransactionV1Envelope, Uint256, VecM, WriteXdr,
11 },
12};
13use ed25519_dalek::{ed25519::signature::Signer as _, Signature as Ed25519Signature};
14use sha2::{Digest, Sha256};
15
16use crate::{config::network::Network, print::Print, utils::transaction_hash};
17use std::io::{self, BufRead, IsTerminal};
18
19pub mod ledger;
20pub mod validation;
21
22#[cfg(feature = "additional-libs")]
23mod keyring;
24pub mod secure_store;
25
26#[derive(thiserror::Error, Debug)]
27pub enum Error {
28 #[error("Contract addresses are not supported to sign auth entries {address}")]
29 ContractAddressAreNotSupported { address: String },
30 #[error(transparent)]
31 Ed25519(#[from] ed25519_dalek::SignatureError),
32 #[error("Missing signing key for account {address}")]
33 MissingSignerForAddress { address: String },
34 #[error(transparent)]
35 TryFromSlice(#[from] std::array::TryFromSliceError),
36 #[error("Invalid Soroban authorization entry - {reason}:\n{auth_entry_str}")]
37 InvalidAuthEntry {
38 reason: String,
39 auth_entry_str: String,
40 },
41 #[error("An authorization entry requires confirmation, but stdin is not interactive. Rerun with --auto-sign to sign anyway.")]
42 AuthEntryRequiresConfirmation,
43 #[error("signing cancelled by user")]
44 AuthRejected,
45 #[error(transparent)]
46 Xdr(#[from] xdr::Error),
47 #[error("Transaction envelope type not supported")]
48 UnsupportedTransactionEnvelopeType,
49 #[error(transparent)]
50 Url(#[from] url::ParseError),
51 #[error(transparent)]
52 Open(#[from] std::io::Error),
53 #[error("Returning a signature from Lab is not yet supported; Transaction can be found and submitted in lab")]
54 ReturningSignatureFromLab,
55 #[error(transparent)]
56 SecureStore(#[from] secure_store::Error),
57 #[error(transparent)]
58 Ledger(#[from] ledger::Error),
59 #[error(transparent)]
60 Decode(#[from] stellar_strkey::DecodeError),
61}
62
63pub async fn sign_soroban_authorizations(
71 raw: &Transaction,
72 signers: &[Signer],
73 signature_expiration_ledger: u32,
74 network_passphrase: &str,
75 skip_approval: bool,
76 print: &Print,
77) -> Result<Option<Transaction>, Error> {
78 let [op @ Operation {
80 body: OperationBody::InvokeHostFunction(body),
81 ..
82 }] = raw.operations.as_slice()
83 else {
84 return Ok(None);
85 };
86
87 let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into());
88 let source_bytes = muxed_account_bytes(&raw.source_account);
89
90 let mut auths_modified = false;
91 let mut signed_auths = Vec::with_capacity(body.auth.len());
92 for raw_auth in body.auth.as_slice() {
93 let SorobanAuthorizationEntry {
94 credentials: SorobanCredentials::Address(credentials),
95 ..
96 } = raw_auth
97 else {
98 signed_auths.push(raw_auth.clone());
100 continue;
101 };
102 let SorobanAddressCredentials { address, .. } = credentials;
103
104 match validation::classify_auth_invocation(&body.host_function, &raw_auth.root_invocation) {
106 validation::AuthStyle::Strict => {}
107 validation::AuthStyle::NonStrict => {
108 handle_non_strict_authorization(raw_auth, skip_approval, print)?;
109 }
110 validation::AuthStyle::Invalid => {
111 return Err(Error::InvalidAuthEntry {
112 reason: "authorization entry is not expected for the transaction".to_string(),
113 auth_entry_str: format_auth_entry(raw_auth),
114 });
115 }
116 }
117
118 let auth_address_bytes: &[u8; 32] = match address {
121 ScAddress::MuxedAccount(_) => todo!("muxed accounts are not supported"),
122 ScAddress::ClaimableBalance(_) => todo!("claimable balance not supported"),
123 ScAddress::LiquidityPool(_) => todo!("liquidity pool not supported"),
124 ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(ref a)))) => a,
125 ScAddress::Contract(stellar_xdr::curr::ContractId(Hash(c))) => {
126 return Err(Error::MissingSignerForAddress {
129 address: format!(
130 "{}",
131 stellar_strkey::Strkey::Contract(stellar_strkey::Contract(*c))
132 ),
133 });
134 }
135 };
136
137 if auth_address_bytes == source_bytes {
139 return Err(Error::InvalidAuthEntry {
140 reason: "transaction source account is used as credentials".to_string(),
141 auth_entry_str: format_auth_entry(raw_auth),
142 });
143 }
144
145 let mut signer: Option<&Signer> = None;
146 for s in signers {
147 if auth_address_bytes == &s.get_public_key()?.0 {
148 signer = Some(s);
149 break;
150 }
151 }
152
153 match signer {
154 Some(signer) => {
155 let signed_entry = sign_soroban_authorization_entry(
156 raw_auth,
157 signer,
158 signature_expiration_ledger,
159 &network_id,
160 )
161 .await?;
162 signed_auths.push(signed_entry);
163 auths_modified = true;
164 }
165 None => {
166 return Err(Error::MissingSignerForAddress {
167 address: format!(
168 "{}",
169 stellar_strkey::Strkey::PublicKeyEd25519(
170 stellar_strkey::ed25519::PublicKey(*auth_address_bytes),
171 )
172 ),
173 });
174 }
175 }
176 }
177
178 if !auths_modified {
180 return Ok(None);
181 }
182
183 let mut tx = raw.clone();
185 let mut new_body = body.clone();
186 new_body.auth = signed_auths.try_into()?;
187 tx.operations = vec![Operation {
188 source_account: op.source_account.clone(),
189 body: OperationBody::InvokeHostFunction(new_body),
190 }]
191 .try_into()?;
192 Ok(Some(tx))
193}
194
195fn handle_non_strict_authorization(
200 auth: &SorobanAuthorizationEntry,
201 skip_approval: bool,
202 print: &Print,
203) -> Result<(), Error> {
204 if skip_approval {
205 print.warnln("Signing authorization entry without approval (--auto-sign):");
206 print.println(format_auth_entry(auth));
207 Ok(())
208 } else {
209 confirm_non_strict_authorization(auth)
210 }
211}
212
213fn confirm_non_strict_authorization(auth: &SorobanAuthorizationEntry) -> Result<(), Error> {
214 let print = Print::new(false);
216 print.warnln(
217 "Authorization entry does not match the current contract call, and needs approval:",
218 );
219 print.println(format_auth_entry(auth));
220
221 let stdin = io::stdin();
222 if !stdin.is_terminal() {
223 return Err(Error::AuthEntryRequiresConfirmation);
224 }
225
226 print.warnln("Sign this authorization entry? (y/N)");
227 let mut response = String::new();
228 stdin.lock().read_line(&mut response)?;
229 if response.trim().eq_ignore_ascii_case("y") {
230 Ok(())
231 } else {
232 Err(Error::AuthRejected)
233 }
234}
235
236async fn sign_soroban_authorization_entry(
237 raw: &SorobanAuthorizationEntry,
238 signer: &Signer,
239 signature_expiration_ledger: u32,
240 network_id: &Hash,
241) -> Result<SorobanAuthorizationEntry, Error> {
242 let mut auth = raw.clone();
243 let SorobanAuthorizationEntry {
244 credentials: SorobanCredentials::Address(ref mut credentials),
245 ..
246 } = auth
247 else {
248 return Ok(auth);
250 };
251 let SorobanAddressCredentials { nonce, .. } = credentials;
252
253 let preimage = HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization {
254 network_id: network_id.clone(),
255 invocation: auth.root_invocation.clone(),
256 nonce: *nonce,
257 signature_expiration_ledger,
258 })
259 .to_xdr(Limits::none())?;
260
261 let payload = Sha256::digest(preimage);
262 let p: [u8; 32] = payload.as_slice().try_into()?;
263 let signature = signer.sign_payload(p).await?;
264 let public_key_vec = signer.get_public_key()?.0.to_vec();
265
266 let map = ScMap::sorted_from(vec![
267 (
268 ScVal::Symbol(ScSymbol("public_key".try_into()?)),
269 ScVal::Bytes(public_key_vec.try_into().map_err(Error::Xdr)?),
270 ),
271 (
272 ScVal::Symbol(ScSymbol("signature".try_into()?)),
273 ScVal::Bytes(
274 signature
275 .to_bytes()
276 .to_vec()
277 .try_into()
278 .map_err(Error::Xdr)?,
279 ),
280 ),
281 ])
282 .map_err(Error::Xdr)?;
283 credentials.signature = ScVal::Vec(Some(
284 vec![ScVal::Map(Some(map))].try_into().map_err(Error::Xdr)?,
285 ));
286 credentials.signature_expiration_ledger = signature_expiration_ledger;
287 auth.credentials = SorobanCredentials::Address(credentials.clone());
288 Ok(auth)
289}
290
291pub struct Signer {
292 pub kind: SignerKind,
293 pub print: Print,
294}
295
296#[allow(clippy::module_name_repetitions, clippy::large_enum_variant)]
297pub enum SignerKind {
298 Local(LocalKey),
299 Ledger(LedgerEntry),
300 Lab,
301 SecureStore(SecureStoreEntry),
302}
303
304impl Signer {
306 pub async fn sign_tx(
307 &self,
308 tx: Transaction,
309 network: &Network,
310 ) -> Result<TransactionEnvelope, Error> {
311 let tx_env = TransactionEnvelope::Tx(TransactionV1Envelope {
312 tx,
313 signatures: VecM::default(),
314 });
315 self.sign_tx_env(&tx_env, network).await
316 }
317
318 pub async fn sign_tx_env(
319 &self,
320 tx_env: &TransactionEnvelope,
321 network: &Network,
322 ) -> Result<TransactionEnvelope, Error> {
323 match &tx_env {
324 TransactionEnvelope::Tx(TransactionV1Envelope { tx, signatures }) => {
325 let tx_hash = transaction_hash(tx, &network.network_passphrase)?;
326 self.print
327 .infoln(format!("Signing transaction: {}", hex::encode(tx_hash)));
328 let decorated_signature = self.sign_tx_hash(tx_hash, tx_env, network).await?;
329 let mut sigs = signatures.clone().into_vec();
330 sigs.push(decorated_signature);
331 Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
332 tx: tx.clone(),
333 signatures: sigs.try_into()?,
334 }))
335 }
336 TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope { tx, signatures }) => {
337 let tx_hash = fee_bump_transaction_hash(tx, &network.network_passphrase)?;
338 self.print.infoln(format!(
339 "Signing fee bump transaction: {}",
340 hex::encode(tx_hash),
341 ));
342 let decorated_signature = self.sign_tx_hash(tx_hash, tx_env, network).await?;
343 let mut sigs = signatures.clone().into_vec();
344 sigs.push(decorated_signature);
345 Ok(TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope {
346 tx: tx.clone(),
347 signatures: sigs.try_into()?,
348 }))
349 }
350 TransactionEnvelope::TxV0(_) => Err(Error::UnsupportedTransactionEnvelopeType),
351 }
352 }
353
354 pub fn get_public_key(&self) -> Result<stellar_strkey::ed25519::PublicKey, Error> {
355 match &self.kind {
356 SignerKind::Local(local_key) => Ok(stellar_strkey::ed25519::PublicKey::from_payload(
357 local_key.key.verifying_key().as_bytes(),
358 )?),
359 SignerKind::Ledger(ledger) => Ok(ledger
360 .public_key
361 .expect("Ledger signers reachable here are built from Secret::Ledger and always carry a cached public key")),
362 SignerKind::Lab => Err(Error::ReturningSignatureFromLab),
363 SignerKind::SecureStore(secure_store_entry) => secure_store_entry.get_public_key(),
364 }
365 }
366
367 pub async fn sign_payload(&self, payload: [u8; 32]) -> Result<Ed25519Signature, Error> {
368 match &self.kind {
369 SignerKind::Local(local_key) => local_key.sign_payload(payload),
370 SignerKind::Ledger(ledger) => Ok(ledger.sign_payload(payload).await?),
371 SignerKind::Lab => Err(Error::ReturningSignatureFromLab),
372 SignerKind::SecureStore(secure_store_entry) => secure_store_entry.sign_payload(payload),
373 }
374 }
375
376 async fn sign_tx_hash(
377 &self,
378 tx_hash: [u8; 32],
379 tx_env: &TransactionEnvelope,
380 network: &Network,
381 ) -> Result<DecoratedSignature, Error> {
382 match &self.kind {
383 SignerKind::Local(key) => key.sign_tx_hash(tx_hash),
384 SignerKind::Lab => Lab::sign_tx_env(tx_env, network, &self.print),
385 SignerKind::Ledger(ledger) => ledger.sign_tx_hash(tx_hash).await.map_err(Error::from),
386 SignerKind::SecureStore(entry) => entry.sign_tx_hash(tx_hash),
387 }
388 }
389}
390
391pub struct LocalKey {
392 pub key: ed25519_dalek::SigningKey,
393}
394
395impl LocalKey {
396 pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result<DecoratedSignature, Error> {
397 let hint = SignatureHint(self.key.verifying_key().to_bytes()[28..].try_into()?);
398 let signature = Signature(self.key.sign(&tx_hash).to_bytes().to_vec().try_into()?);
399 Ok(DecoratedSignature { hint, signature })
400 }
401
402 pub fn sign_payload(&self, payload: [u8; 32]) -> Result<Ed25519Signature, Error> {
403 Ok(self.key.sign(&payload))
404 }
405}
406
407pub struct Lab;
408
409impl Lab {
410 const URL: &str = "https://lab.stellar.org/transaction/cli-sign";
411
412 pub fn sign_tx_env(
413 tx_env: &TransactionEnvelope,
414 network: &Network,
415 printer: &Print,
416 ) -> Result<DecoratedSignature, Error> {
417 let xdr = tx_env.to_xdr_base64(Limits::none())?;
418
419 let mut url = url::Url::parse(Self::URL)?;
420 url.query_pairs_mut()
421 .append_pair("networkPassphrase", &network.network_passphrase)
422 .append_pair("xdr", &xdr);
423 let url = url.to_string();
424
425 printer.globeln(format!("Opening lab to sign transaction: {url}"));
426 open::that(url)?;
427
428 Err(Error::ReturningSignatureFromLab)
429 }
430}
431
432pub struct SecureStoreEntry {
433 pub name: String,
434 pub hd_path: Option<u32>,
435 pub public_key: Option<stellar_strkey::ed25519::PublicKey>,
436}
437
438impl SecureStoreEntry {
439 pub fn get_public_key(&self) -> Result<stellar_strkey::ed25519::PublicKey, Error> {
440 if let Some(pk) = &self.public_key {
441 return Ok(*pk);
442 }
443 Ok(secure_store::get_public_key(&self.name, self.hd_path)?)
444 }
445
446 pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result<DecoratedSignature, Error> {
447 let hint = SignatureHint(self.get_public_key()?.0[28..].try_into()?);
448
449 let signed_tx_hash = secure_store::sign_tx_data(&self.name, self.hd_path, &tx_hash)?;
450
451 let signature = Signature(signed_tx_hash.clone().try_into()?);
452 Ok(DecoratedSignature { hint, signature })
453 }
454
455 pub fn sign_payload(&self, payload: [u8; 32]) -> Result<Ed25519Signature, Error> {
456 let signed_bytes = secure_store::sign_tx_data(&self.name, self.hd_path, &payload)?;
457 let sig = Ed25519Signature::from_bytes(signed_bytes.as_slice().try_into()?);
458 Ok(sig)
459 }
460}
461
462fn muxed_account_bytes(source: &MuxedAccount) -> &[u8; 32] {
464 match source {
465 MuxedAccount::Ed25519(Uint256(bytes)) => bytes,
466 MuxedAccount::MuxedEd25519(muxed) => &muxed.ed25519.0,
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473 use crate::signer::ledger::LedgerEntry;
474 use crate::xdr::{
475 BytesM, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Memo, Preconditions,
476 SequenceNumber, SorobanAuthorizedFunction, SorobanAuthorizedInvocation, TransactionExt,
477 };
478
479 const NETWORK: &str = "Test SDF Network ; September 2015";
480 const EXPIRATION_LEDGER: u32 = 100;
481
482 fn local_signer(seed: [u8; 32]) -> Signer {
483 Signer {
484 kind: SignerKind::Local(LocalKey {
485 key: ed25519_dalek::SigningKey::from_bytes(&seed),
486 }),
487 print: Print::new(true),
488 }
489 }
490
491 fn signer_pubkey(signer: &Signer) -> [u8; 32] {
492 signer.get_public_key().unwrap().0
493 }
494
495 fn ed25519_address(bytes: [u8; 32]) -> ScAddress {
496 ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(bytes))))
497 }
498
499 fn invoke_args(contract: [u8; 32], fn_name: &str) -> InvokeContractArgs {
500 InvokeContractArgs {
501 contract_address: ScAddress::Contract(stellar_xdr::curr::ContractId(Hash(contract))),
502 function_name: ScSymbol(fn_name.try_into().unwrap()),
503 args: VecM::default(),
504 }
505 }
506
507 fn invocation(contract: [u8; 32], fn_name: &str) -> SorobanAuthorizedInvocation {
508 SorobanAuthorizedInvocation {
509 function: SorobanAuthorizedFunction::ContractFn(invoke_args(contract, fn_name)),
510 sub_invocations: VecM::default(),
511 }
512 }
513
514 fn address_auth(
515 address: ScAddress,
516 invocation: SorobanAuthorizedInvocation,
517 ) -> SorobanAuthorizationEntry {
518 SorobanAuthorizationEntry {
519 credentials: SorobanCredentials::Address(SorobanAddressCredentials {
520 address,
521 nonce: 0,
522 signature_expiration_ledger: 0,
523 signature: ScVal::Void,
524 }),
525 root_invocation: invocation,
526 }
527 }
528
529 fn build_tx(
530 source: MuxedAccount,
531 host_function: HostFunction,
532 auth: Vec<SorobanAuthorizationEntry>,
533 ) -> Transaction {
534 Transaction {
535 source_account: source,
536 fee: 100,
537 seq_num: SequenceNumber(1),
538 cond: Preconditions::None,
539 memo: Memo::None,
540 operations: vec![Operation {
541 source_account: None,
542 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
543 host_function,
544 auth: auth.try_into().unwrap(),
545 }),
546 }]
547 .try_into()
548 .unwrap(),
549 ext: TransactionExt::V0,
550 }
551 }
552
553 fn extract_signed_pubkey(creds: &SorobanAddressCredentials) -> [u8; 32] {
555 let ScVal::Vec(Some(outer)) = &creds.signature else {
556 panic!("expected ScVal::Vec signature");
557 };
558 let Some(ScVal::Map(Some(map))) = outer.first() else {
559 panic!("expected ScVal::Map inside signature vec");
560 };
561 map.iter()
562 .find_map(|e| match (&e.key, &e.val) {
563 (ScVal::Symbol(s), ScVal::Bytes(b)) if s.0.as_slice() == b"public_key" => {
564 Some(b.as_slice().try_into().unwrap())
565 }
566 _ => None,
567 })
568 .expect("public_key entry")
569 }
570
571 #[tokio::test]
572 async fn test_signs_address_auth_entry_with_matching_signer() {
573 let signer = local_signer([1u8; 32]);
574 let signer_unused = local_signer([2u8; 32]);
575 let signer_pk = signer_pubkey(&signer);
576 let source = MuxedAccount::Ed25519(Uint256([9u8; 32]));
577 let contract = [42u8; 32];
578
579 let entry = address_auth(ed25519_address(signer_pk), invocation(contract, "hello"));
580 let host_fn = HostFunction::InvokeContract(invoke_args(contract, "hello"));
581 let tx = build_tx(source, host_fn, vec![entry]);
582
583 let signed_auth_tx = sign_soroban_authorizations(
584 &tx,
585 &[signer_unused, signer],
586 EXPIRATION_LEDGER,
587 NETWORK,
588 false,
589 &Print::new(true),
590 )
591 .await
592 .unwrap()
593 .expect("signing modifies the transaction");
594
595 let OperationBody::InvokeHostFunction(body) = &signed_auth_tx.operations[0].body else {
596 panic!("expected InvokeHostFunction");
597 };
598 let SorobanCredentials::Address(creds) = &body.auth[0].credentials else {
599 panic!("expected Address credentials");
600 };
601 assert!(
602 !matches!(creds.signature, ScVal::Void),
603 "signature should be filled in"
604 );
605 assert_eq!(creds.signature_expiration_ledger, EXPIRATION_LEDGER);
606 assert_eq!(
607 extract_signed_pubkey(creds),
608 signer_pk,
609 "embedded public_key should match the signer"
610 );
611 }
612
613 #[tokio::test]
614 async fn test_non_strict_auth_signs_when_allowed() {
615 let signer = local_signer([1u8; 32]);
616 let signer_pk = signer_pubkey(&signer);
617 let source = MuxedAccount::Ed25519(Uint256([9u8; 32]));
618 let contract = [42u8; 32];
619 let other_contract = [99u8; 32];
620
621 let entry = address_auth(
622 ed25519_address(signer_pk),
623 invocation(other_contract, "hello"),
624 );
625 let host_fn = HostFunction::InvokeContract(invoke_args(contract, "hello"));
626 let tx = build_tx(source, host_fn, vec![entry]);
627
628 let signed_auth_tx = sign_soroban_authorizations(
629 &tx,
630 &[signer],
631 EXPIRATION_LEDGER,
632 NETWORK,
633 true,
634 &Print::new(true),
635 )
636 .await
637 .unwrap()
638 .expect("signing modifies the transaction");
639
640 let OperationBody::InvokeHostFunction(body) = &signed_auth_tx.operations[0].body else {
641 panic!("expected InvokeHostFunction");
642 };
643 let SorobanCredentials::Address(creds) = &body.auth[0].credentials else {
644 panic!("expected Address credentials");
645 };
646 assert!(!matches!(creds.signature, ScVal::Void));
647 }
648
649 #[tokio::test]
650 async fn test_upload_wasm_with_auth_returns_invalid() {
651 let signer = local_signer([1u8; 32]);
652 let signer_pk = signer_pubkey(&signer);
653 let source = MuxedAccount::Ed25519(Uint256([9u8; 32]));
654 let wasm: BytesM = [0u8; 32].try_into().unwrap();
655
656 let entry = address_auth(ed25519_address(signer_pk), invocation([42u8; 32], "hello"));
657 let host_fn = HostFunction::UploadContractWasm(wasm);
658 let tx = build_tx(source, host_fn, vec![entry]);
659
660 let result = sign_soroban_authorizations(
661 &tx,
662 &[signer],
663 EXPIRATION_LEDGER,
664 NETWORK,
665 false,
666 &Print::new(true),
667 )
668 .await;
669 assert!(matches!(result, Err(Error::InvalidAuthEntry { .. })));
670 }
671
672 #[tokio::test]
673 async fn test_source_account_as_address_returns_invalid() {
674 let signer = local_signer([1u8; 32]);
675 let signer_pk = signer_pubkey(&signer);
676 let source = MuxedAccount::Ed25519(Uint256(signer_pk));
677 let contract = [42u8; 32];
678
679 let entry = address_auth(ed25519_address(signer_pk), invocation(contract, "hello"));
680 let host_fn = HostFunction::InvokeContract(invoke_args(contract, "hello"));
681 let tx = build_tx(source, host_fn, vec![entry]);
682
683 let result = sign_soroban_authorizations(
684 &tx,
685 &[signer],
686 EXPIRATION_LEDGER,
687 NETWORK,
688 false,
689 &Print::new(true),
690 )
691 .await;
692 assert!(matches!(result, Err(Error::InvalidAuthEntry { .. })));
693 }
694
695 #[tokio::test]
696 async fn test_missing_signer_returns_error() {
697 let source = MuxedAccount::Ed25519(Uint256([9u8; 32]));
698 let contract = [42u8; 32];
699 let unknown = [77u8; 32];
700
701 let entry = address_auth(ed25519_address(unknown), invocation(contract, "hello"));
702 let host_fn = HostFunction::InvokeContract(invoke_args(contract, "hello"));
703 let tx = build_tx(source, host_fn, vec![entry]);
704
705 let result = sign_soroban_authorizations(
706 &tx,
707 &[],
708 EXPIRATION_LEDGER,
709 NETWORK,
710 false,
711 &Print::new(true),
712 )
713 .await;
714 assert!(matches!(result, Err(Error::MissingSignerForAddress { .. })));
715 }
716
717 #[test]
718 fn ledger_signer_get_public_key_returns_cached_without_device() {
719 const TEST_PUBLIC_KEY: &str = "GAREAZZQWHOCBJS236KIE3AWYBVFLSBK7E5UW3ICI3TCRWQKT5LNLCEZ";
720 let pk = stellar_strkey::ed25519::PublicKey::from_string(TEST_PUBLIC_KEY).unwrap();
721 let signer = Signer {
722 kind: SignerKind::Ledger(LedgerEntry {
723 hd_path: 0,
724 public_key: Some(pk),
725 }),
726 print: Print::new(true),
727 };
728 assert_eq!(
729 signer.get_public_key().unwrap().to_string(),
730 TEST_PUBLIC_KEY
731 );
732 }
733}