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