1use crate::xdr::{
2 self, AccountId, DecoratedSignature, Hash, HashIdPreimage, HashIdPreimageSorobanAuthorization,
3 InvokeHostFunctionOp, Limits, Operation, OperationBody, PublicKey, ScAddress, ScMap, ScSymbol,
4 ScVal, Signature, SignatureHint, SorobanAddressCredentials, SorobanAuthorizationEntry,
5 SorobanAuthorizedFunction, SorobanCredentials, Transaction, TransactionEnvelope,
6 TransactionV1Envelope, Uint256, VecM, WriteXdr,
7};
8use ed25519_dalek::ed25519::signature::Signer as _;
9use sha2::{Digest, Sha256};
10
11use crate::{config::network::Network, print::Print, utils::transaction_hash};
12
13pub mod ledger;
14
15#[cfg(feature = "additional-libs")]
16mod keyring;
17pub mod secure_store;
18
19#[derive(thiserror::Error, Debug)]
20pub enum Error {
21 #[error("Contract addresses are not supported to sign auth entries {address}")]
22 ContractAddressAreNotSupported { address: String },
23 #[error(transparent)]
24 Ed25519(#[from] ed25519_dalek::SignatureError),
25 #[error("Missing signing key for account {address}")]
26 MissingSignerForAddress { address: String },
27 #[error(transparent)]
28 TryFromSlice(#[from] std::array::TryFromSliceError),
29 #[error("User cancelled signing, perhaps need to add -y")]
30 UserCancelledSigning,
31 #[error(transparent)]
32 Xdr(#[from] xdr::Error),
33 #[error("Only Transaction envelope V1 type is supported")]
34 UnsupportedTransactionEnvelopeType,
35 #[error(transparent)]
36 Url(#[from] url::ParseError),
37 #[error(transparent)]
38 Open(#[from] std::io::Error),
39 #[error("Returning a signature from Lab is not yet supported; Transaction can be found and submitted in lab")]
40 ReturningSignatureFromLab,
41 #[error(transparent)]
42 SecureStore(#[from] secure_store::Error),
43 #[error(transparent)]
44 Ledger(#[from] ledger::Error),
45}
46
47fn requires_auth(txn: &Transaction) -> Option<xdr::Operation> {
48 let [op @ Operation {
49 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { auth, .. }),
50 ..
51 }] = txn.operations.as_slice()
52 else {
53 return None;
54 };
55 matches!(
56 auth.first().map(|x| &x.root_invocation.function),
57 Some(&SorobanAuthorizedFunction::ContractFn(_))
58 )
59 .then(move || op.clone())
60}
61
62pub fn sign_soroban_authorizations(
65 raw: &Transaction,
66 source_key: &ed25519_dalek::SigningKey,
67 signers: &[ed25519_dalek::SigningKey],
68 signature_expiration_ledger: u32,
69 network_passphrase: &str,
70) -> Result<Option<Transaction>, Error> {
71 let mut tx = raw.clone();
72 let Some(mut op) = requires_auth(&tx) else {
73 return Ok(None);
74 };
75
76 let Operation {
77 body: OperationBody::InvokeHostFunction(ref mut body),
78 ..
79 } = op
80 else {
81 return Ok(None);
82 };
83
84 let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into());
85
86 let verification_key = source_key.verifying_key();
87 let source_address = verification_key.as_bytes();
88
89 let signed_auths = body
90 .auth
91 .as_slice()
92 .iter()
93 .map(|raw_auth| {
94 let mut auth = raw_auth.clone();
95 let SorobanAuthorizationEntry {
96 credentials: SorobanCredentials::Address(ref mut credentials),
97 ..
98 } = auth
99 else {
100 return Ok(auth);
102 };
103 let SorobanAddressCredentials { ref address, .. } = credentials;
104
105 let needle = match address {
108 ScAddress::MuxedAccount(_) => todo!("muxed accounts are not supported"),
109 ScAddress::ClaimableBalance(_) => todo!("claimable balance not supported"),
110 ScAddress::LiquidityPool(_) => todo!("liquidity pool not supported"),
111 ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(ref a)))) => a,
112 ScAddress::Contract(stellar_xdr::curr::ContractId(Hash(c))) => {
113 return Err(Error::MissingSignerForAddress {
116 address: stellar_strkey::Strkey::Contract(stellar_strkey::Contract(*c))
117 .to_string(),
118 });
119 }
120 };
121 let signer = if let Some(s) = signers
122 .iter()
123 .find(|s| needle == s.verifying_key().as_bytes())
124 {
125 s
126 } else if needle == source_address {
127 source_key
129 } else {
130 return Err(Error::MissingSignerForAddress {
132 address: stellar_strkey::Strkey::PublicKeyEd25519(
133 stellar_strkey::ed25519::PublicKey(*needle),
134 )
135 .to_string(),
136 });
137 };
138
139 sign_soroban_authorization_entry(
140 raw_auth,
141 signer,
142 signature_expiration_ledger,
143 &network_id,
144 )
145 })
146 .collect::<Result<Vec<_>, Error>>()?;
147
148 body.auth = signed_auths.try_into()?;
149 tx.operations = vec![op].try_into()?;
150 Ok(Some(tx))
151}
152
153fn sign_soroban_authorization_entry(
154 raw: &SorobanAuthorizationEntry,
155 signer: &ed25519_dalek::SigningKey,
156 signature_expiration_ledger: u32,
157 network_id: &Hash,
158) -> Result<SorobanAuthorizationEntry, Error> {
159 let mut auth = raw.clone();
160 let SorobanAuthorizationEntry {
161 credentials: SorobanCredentials::Address(ref mut credentials),
162 ..
163 } = auth
164 else {
165 return Ok(auth);
167 };
168 let SorobanAddressCredentials { nonce, .. } = credentials;
169
170 let preimage = HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization {
171 network_id: network_id.clone(),
172 invocation: auth.root_invocation.clone(),
173 nonce: *nonce,
174 signature_expiration_ledger,
175 })
176 .to_xdr(Limits::none())?;
177
178 let payload = Sha256::digest(preimage);
179 let signature = signer.sign(&payload);
180
181 let map = ScMap::sorted_from(vec![
182 (
183 ScVal::Symbol(ScSymbol("public_key".try_into()?)),
184 ScVal::Bytes(
185 signer
186 .verifying_key()
187 .to_bytes()
188 .to_vec()
189 .try_into()
190 .map_err(Error::Xdr)?,
191 ),
192 ),
193 (
194 ScVal::Symbol(ScSymbol("signature".try_into()?)),
195 ScVal::Bytes(
196 signature
197 .to_bytes()
198 .to_vec()
199 .try_into()
200 .map_err(Error::Xdr)?,
201 ),
202 ),
203 ])
204 .map_err(Error::Xdr)?;
205 credentials.signature = ScVal::Vec(Some(
206 vec![ScVal::Map(Some(map))].try_into().map_err(Error::Xdr)?,
207 ));
208 credentials.signature_expiration_ledger = signature_expiration_ledger;
209 auth.credentials = SorobanCredentials::Address(credentials.clone());
210 Ok(auth)
211}
212
213pub struct Signer {
214 pub kind: SignerKind,
215 pub print: Print,
216}
217
218#[allow(clippy::module_name_repetitions, clippy::large_enum_variant)]
219pub enum SignerKind {
220 Local(LocalKey),
221 Ledger(ledger::LedgerType),
222 Lab,
223 SecureStore(SecureStoreEntry),
224}
225
226impl Signer {
229 pub async fn sign_tx(
230 &self,
231 tx: Transaction,
232 network: &Network,
233 ) -> Result<TransactionEnvelope, Error> {
234 let tx_env = TransactionEnvelope::Tx(TransactionV1Envelope {
235 tx,
236 signatures: VecM::default(),
237 });
238 self.sign_tx_env(&tx_env, network).await
239 }
240
241 pub async fn sign_tx_env(
242 &self,
243 tx_env: &TransactionEnvelope,
244 network: &Network,
245 ) -> Result<TransactionEnvelope, Error> {
246 match &tx_env {
247 TransactionEnvelope::Tx(TransactionV1Envelope { tx, signatures }) => {
248 let tx_hash = transaction_hash(tx, &network.network_passphrase)?;
249 self.print
250 .infoln(format!("Signing transaction: {}", hex::encode(tx_hash),));
251 let decorated_signature = match &self.kind {
252 SignerKind::Local(key) => key.sign_tx_hash(tx_hash)?,
253 SignerKind::Lab => Lab::sign_tx_env(tx_env, network, &self.print)?,
254 SignerKind::Ledger(ledger) => ledger.sign_transaction_hash(&tx_hash).await?,
255 SignerKind::SecureStore(entry) => entry.sign_tx_hash(tx_hash)?,
256 };
257 let mut sigs = signatures.clone().into_vec();
258 sigs.push(decorated_signature);
259 Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
260 tx: tx.clone(),
261 signatures: sigs.try_into()?,
262 }))
263 }
264 _ => Err(Error::UnsupportedTransactionEnvelopeType),
265 }
266 }
267}
268
269pub struct LocalKey {
270 pub key: ed25519_dalek::SigningKey,
271}
272
273impl LocalKey {
274 pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result<DecoratedSignature, Error> {
275 let hint = SignatureHint(self.key.verifying_key().to_bytes()[28..].try_into()?);
276 let signature = Signature(self.key.sign(&tx_hash).to_bytes().to_vec().try_into()?);
277 Ok(DecoratedSignature { hint, signature })
278 }
279}
280
281pub struct Lab;
282
283impl Lab {
284 const URL: &str = "https://lab.stellar.org/transaction/cli-sign";
285
286 pub fn sign_tx_env(
287 tx_env: &TransactionEnvelope,
288 network: &Network,
289 printer: &Print,
290 ) -> Result<DecoratedSignature, Error> {
291 let xdr = tx_env.to_xdr_base64(Limits::none())?;
292
293 let mut url = url::Url::parse(Self::URL)?;
294 url.query_pairs_mut()
295 .append_pair("networkPassphrase", &network.network_passphrase)
296 .append_pair("xdr", &xdr);
297 let url = url.to_string();
298
299 printer.globeln(format!("Opening lab to sign transaction: {url}"));
300 open::that(url)?;
301
302 Err(Error::ReturningSignatureFromLab)
303 }
304}
305
306pub struct SecureStoreEntry {
307 pub name: String,
308 pub hd_path: Option<usize>,
309}
310
311impl SecureStoreEntry {
312 pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result<DecoratedSignature, Error> {
313 let hint = SignatureHint(
314 secure_store::get_public_key(&self.name, self.hd_path)?.0[28..].try_into()?,
315 );
316
317 let signed_tx_hash = secure_store::sign_tx_data(&self.name, self.hd_path, &tx_hash)?;
318
319 let signature = Signature(signed_tx_hash.clone().try_into()?);
320 Ok(DecoratedSignature { hint, signature })
321 }
322}