use crate::{
utils::fee_bump_transaction_hash,
xdr::{
self, AccountId, DecoratedSignature, FeeBumpTransactionEnvelope, Hash, HashIdPreimage,
HashIdPreimageSorobanAuthorization, InvokeHostFunctionOp, Limits, Operation, OperationBody,
PublicKey, ScAddress, ScMap, ScSymbol, ScVal, Signature, SignatureHint,
SorobanAddressCredentials, SorobanAuthorizationEntry, SorobanAuthorizedFunction,
SorobanCredentials, Transaction, TransactionEnvelope, TransactionV1Envelope, Uint256, VecM,
WriteXdr,
},
};
use ed25519_dalek::{ed25519::signature::Signer as _, Signature as Ed25519Signature};
use sha2::{Digest, Sha256};
use crate::{config::network::Network, print::Print, utils::transaction_hash};
pub mod ledger;
#[cfg(feature = "additional-libs")]
mod keyring;
pub mod secure_store;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Contract addresses are not supported to sign auth entries {address}")]
ContractAddressAreNotSupported { address: String },
#[error(transparent)]
Ed25519(#[from] ed25519_dalek::SignatureError),
#[error("Missing signing key for account {address}")]
MissingSignerForAddress { address: String },
#[error(transparent)]
TryFromSlice(#[from] std::array::TryFromSliceError),
#[error("User cancelled signing, perhaps need to add -y")]
UserCancelledSigning,
#[error(transparent)]
Xdr(#[from] xdr::Error),
#[error("Transaction envelope type not supported")]
UnsupportedTransactionEnvelopeType,
#[error(transparent)]
Url(#[from] url::ParseError),
#[error(transparent)]
Open(#[from] std::io::Error),
#[error("Returning a signature from Lab is not yet supported; Transaction can be found and submitted in lab")]
ReturningSignatureFromLab,
#[error(transparent)]
SecureStore(#[from] secure_store::Error),
#[error(transparent)]
Ledger(#[from] ledger::Error),
#[error(transparent)]
Decode(#[from] stellar_strkey::DecodeError),
}
fn requires_auth(txn: &Transaction) -> Option<xdr::Operation> {
let [op @ Operation {
body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { auth, .. }),
..
}] = txn.operations.as_slice()
else {
return None;
};
matches!(
auth.first().map(|x| &x.root_invocation.function),
Some(&SorobanAuthorizedFunction::ContractFn(_))
)
.then(move || op.clone())
}
pub fn sign_soroban_authorizations(
raw: &Transaction,
source_signer: &Signer,
signers: &[Signer],
signature_expiration_ledger: u32,
network_passphrase: &str,
) -> Result<Option<Transaction>, Error> {
let mut tx = raw.clone();
let Some(mut op) = requires_auth(&tx) else {
return Ok(None);
};
let Operation {
body: OperationBody::InvokeHostFunction(ref mut body),
..
} = op
else {
return Ok(None);
};
let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into());
let mut signed_auths = Vec::with_capacity(body.auth.len());
for raw_auth in body.auth.as_slice() {
let mut auth = raw_auth.clone();
let SorobanAuthorizationEntry {
credentials: SorobanCredentials::Address(ref mut credentials),
..
} = auth
else {
signed_auths.push(auth);
continue;
};
let SorobanAddressCredentials { ref address, .. } = credentials;
let needle: &[u8; 32] = match address {
ScAddress::MuxedAccount(_) => todo!("muxed accounts are not supported"),
ScAddress::ClaimableBalance(_) => todo!("claimable balance not supported"),
ScAddress::LiquidityPool(_) => todo!("liquidity pool not supported"),
ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(ref a)))) => a,
ScAddress::Contract(stellar_xdr::curr::ContractId(Hash(c))) => {
return Err(Error::MissingSignerForAddress {
address: stellar_strkey::Strkey::Contract(stellar_strkey::Contract(*c))
.to_string(),
});
}
};
let mut signer: Option<&Signer> = None;
for s in signers {
if needle == &s.get_public_key()?.0 {
signer = Some(s);
}
}
if needle == &source_signer.get_public_key()?.0 {
signer = Some(source_signer);
}
match signer {
Some(signer) => {
let signed_entry = sign_soroban_authorization_entry(
raw_auth,
signer,
signature_expiration_ledger,
&network_id,
)?;
signed_auths.push(signed_entry);
}
None => {
return Err(Error::MissingSignerForAddress {
address: stellar_strkey::Strkey::PublicKeyEd25519(
stellar_strkey::ed25519::PublicKey(*needle),
)
.to_string(),
});
}
}
}
body.auth = signed_auths.try_into()?;
tx.operations = vec![op].try_into()?;
Ok(Some(tx))
}
fn sign_soroban_authorization_entry(
raw: &SorobanAuthorizationEntry,
signer: &Signer,
signature_expiration_ledger: u32,
network_id: &Hash,
) -> Result<SorobanAuthorizationEntry, Error> {
let mut auth = raw.clone();
let SorobanAuthorizationEntry {
credentials: SorobanCredentials::Address(ref mut credentials),
..
} = auth
else {
return Ok(auth);
};
let SorobanAddressCredentials { nonce, .. } = credentials;
let preimage = HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization {
network_id: network_id.clone(),
invocation: auth.root_invocation.clone(),
nonce: *nonce,
signature_expiration_ledger,
})
.to_xdr(Limits::none())?;
let payload = Sha256::digest(preimage);
let p: [u8; 32] = payload.as_slice().try_into()?;
let signature = signer.sign_payload(p)?;
let public_key_vec = signer.get_public_key()?.0.to_vec();
let map = ScMap::sorted_from(vec![
(
ScVal::Symbol(ScSymbol("public_key".try_into()?)),
ScVal::Bytes(public_key_vec.try_into().map_err(Error::Xdr)?),
),
(
ScVal::Symbol(ScSymbol("signature".try_into()?)),
ScVal::Bytes(
signature
.to_bytes()
.to_vec()
.try_into()
.map_err(Error::Xdr)?,
),
),
])
.map_err(Error::Xdr)?;
credentials.signature = ScVal::Vec(Some(
vec![ScVal::Map(Some(map))].try_into().map_err(Error::Xdr)?,
));
credentials.signature_expiration_ledger = signature_expiration_ledger;
auth.credentials = SorobanCredentials::Address(credentials.clone());
Ok(auth)
}
pub struct Signer {
pub kind: SignerKind,
pub print: Print,
}
#[allow(clippy::module_name_repetitions, clippy::large_enum_variant)]
pub enum SignerKind {
Local(LocalKey),
Ledger(ledger::LedgerType),
Lab,
SecureStore(SecureStoreEntry),
}
impl Signer {
pub async fn sign_tx(
&self,
tx: Transaction,
network: &Network,
) -> Result<TransactionEnvelope, Error> {
let tx_env = TransactionEnvelope::Tx(TransactionV1Envelope {
tx,
signatures: VecM::default(),
});
self.sign_tx_env(&tx_env, network).await
}
pub async fn sign_tx_env(
&self,
tx_env: &TransactionEnvelope,
network: &Network,
) -> Result<TransactionEnvelope, Error> {
match &tx_env {
TransactionEnvelope::Tx(TransactionV1Envelope { tx, signatures }) => {
let tx_hash = transaction_hash(tx, &network.network_passphrase)?;
self.print
.infoln(format!("Signing transaction: {}", hex::encode(tx_hash),));
let decorated_signature = self.sign_tx_hash(tx_hash, tx_env, network).await?;
let mut sigs = signatures.clone().into_vec();
sigs.push(decorated_signature);
Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
tx: tx.clone(),
signatures: sigs.try_into()?,
}))
}
TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope { tx, signatures }) => {
let tx_hash = fee_bump_transaction_hash(tx, &network.network_passphrase)?;
self.print.infoln(format!(
"Signing fee bump transaction: {}",
hex::encode(tx_hash),
));
let decorated_signature = self.sign_tx_hash(tx_hash, tx_env, network).await?;
let mut sigs = signatures.clone().into_vec();
sigs.push(decorated_signature);
Ok(TransactionEnvelope::TxFeeBump(FeeBumpTransactionEnvelope {
tx: tx.clone(),
signatures: sigs.try_into()?,
}))
}
TransactionEnvelope::TxV0(_) => Err(Error::UnsupportedTransactionEnvelopeType),
}
}
pub fn get_public_key(&self) -> Result<stellar_strkey::ed25519::PublicKey, Error> {
match &self.kind {
SignerKind::Local(local_key) => Ok(stellar_strkey::ed25519::PublicKey::from_payload(
local_key.key.verifying_key().as_bytes(),
)?),
SignerKind::Ledger(_ledger) => todo!("ledger device is not implemented"),
SignerKind::Lab => Err(Error::ReturningSignatureFromLab),
SignerKind::SecureStore(secure_store_entry) => secure_store_entry.get_public_key(),
}
}
pub fn sign_payload(&self, payload: [u8; 32]) -> Result<Ed25519Signature, Error> {
match &self.kind {
SignerKind::Local(local_key) => local_key.sign_payload(payload),
SignerKind::Ledger(_ledger) => todo!("ledger device is not implemented"),
SignerKind::Lab => Err(Error::ReturningSignatureFromLab),
SignerKind::SecureStore(secure_store_entry) => secure_store_entry.sign_payload(payload),
}
}
async fn sign_tx_hash(
&self,
tx_hash: [u8; 32],
tx_env: &TransactionEnvelope,
network: &Network,
) -> Result<DecoratedSignature, Error> {
match &self.kind {
SignerKind::Local(key) => key.sign_tx_hash(tx_hash),
SignerKind::Lab => Lab::sign_tx_env(tx_env, network, &self.print),
SignerKind::Ledger(ledger) => ledger
.sign_transaction_hash(&tx_hash)
.await
.map_err(Error::from),
SignerKind::SecureStore(entry) => entry.sign_tx_hash(tx_hash),
}
}
}
pub struct LocalKey {
pub key: ed25519_dalek::SigningKey,
}
impl LocalKey {
pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result<DecoratedSignature, Error> {
let hint = SignatureHint(self.key.verifying_key().to_bytes()[28..].try_into()?);
let signature = Signature(self.key.sign(&tx_hash).to_bytes().to_vec().try_into()?);
Ok(DecoratedSignature { hint, signature })
}
pub fn sign_payload(&self, payload: [u8; 32]) -> Result<Ed25519Signature, Error> {
Ok(self.key.sign(&payload))
}
}
pub struct Lab;
impl Lab {
const URL: &str = "https://lab.stellar.org/transaction/cli-sign";
pub fn sign_tx_env(
tx_env: &TransactionEnvelope,
network: &Network,
printer: &Print,
) -> Result<DecoratedSignature, Error> {
let xdr = tx_env.to_xdr_base64(Limits::none())?;
let mut url = url::Url::parse(Self::URL)?;
url.query_pairs_mut()
.append_pair("networkPassphrase", &network.network_passphrase)
.append_pair("xdr", &xdr);
let url = url.to_string();
printer.globeln(format!("Opening lab to sign transaction: {url}"));
open::that(url)?;
Err(Error::ReturningSignatureFromLab)
}
}
pub struct SecureStoreEntry {
pub name: String,
pub hd_path: Option<usize>,
}
impl SecureStoreEntry {
pub fn get_public_key(&self) -> Result<stellar_strkey::ed25519::PublicKey, Error> {
Ok(secure_store::get_public_key(&self.name, self.hd_path)?)
}
pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result<DecoratedSignature, Error> {
let hint = SignatureHint(
secure_store::get_public_key(&self.name, self.hd_path)?.0[28..].try_into()?,
);
let signed_tx_hash = secure_store::sign_tx_data(&self.name, self.hd_path, &tx_hash)?;
let signature = Signature(signed_tx_hash.clone().try_into()?);
Ok(DecoratedSignature { hint, signature })
}
pub fn sign_payload(&self, payload: [u8; 32]) -> Result<Ed25519Signature, Error> {
let signed_bytes = secure_store::sign_tx_data(&self.name, self.hd_path, &payload)?;
let sig = Ed25519Signature::from_bytes(signed_bytes.as_slice().try_into()?);
Ok(sig)
}
}