Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions crates/cli/src/output/renderers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,88 @@ pub fn render_fee_breakdown(fee: &FeeBreakdown) -> String {
out
}

/// Render a human-readable summary of an [`AuthCredential`], distinguishing
/// Ed25519 accounts from Smart Wallet contracts.
///
/// * **Ed25519** — shows the label, public key, and any decoded signature hexes.
/// * **Smart Wallet** — shows the label and the contract ID.
/// * **SourceAccount** — shows a brief note that the transaction source account authorized it.
pub fn render_auth_credential(
credential: &prism_core::AuthCredential,
signatures: &[String],
) -> String {
use prism_core::AuthCredential;
use prism_core::SignatureKind;

let palette = ColorPalette::default();
let mut out = String::new();

match credential {
AuthCredential::SourceAccount => {
out.push_str(&palette.metadata_text("Authorization Type: SourceAccount"));
out.push('\n');
out.push_str(" (Authorized by the transaction source account — no separate signature)\n");
}
AuthCredential::Address(addr_cred) => {
let kind = addr_cred.signature_kind();
let type_label = kind.label();

out.push_str(&palette.accent_text(&format!("Authorization Type: {type_label}")));
out.push('\n');

match kind {
SignatureKind::Ed25519 => {
out.push_str(&format!(
" Public Key: {}\n",
palette.success_text(&addr_cred.address)
));
if signatures.is_empty() {
out.push_str(&format!(
" Signature: {}\n",
palette.muted_text("(none)")
));
} else {
for sig in signatures {
out.push_str(&format!(
" Signature: {}\n",
palette.metadata_text(sig)
));
}
}
}
SignatureKind::SmartWallet => {
out.push_str(&format!(
" Contract ID: {}\n",
palette.warning_text(&addr_cred.address)
));
if !signatures.is_empty() {
for sig in signatures {
out.push_str(&format!(
" Auth Data: {}\n",
palette.metadata_text(sig)
));
}
}
}
SignatureKind::Unknown => {
out.push_str(&format!(
" Address: {}\n",
palette.error_text(&addr_cred.address)
));
}
}

out.push_str(&format!(" Nonce: {}\n", addr_cred.nonce));
out.push_str(&format!(
" Expires: ledger {}\n",
addr_cred.signature_expiration_ledger
));
}
}

out
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
65 changes: 65 additions & 0 deletions crates/core/src/decode/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use crate::error::PrismResult;
use crate::xdr::codec::XdrCodec;
use crate::decode::auth_signature::SignatureKind;
use serde::{Deserialize, Serialize};
use stellar_xdr::curr::{
AccountId, Hash, PublicKey, ScAddress, ScVal, SorobanAddressCredentials,
Expand Down Expand Up @@ -41,6 +42,18 @@ pub struct AddressCredential {
pub signed: bool,
}

impl AddressCredential {
/// Detect whether this credential is an Ed25519 account or a Smart Wallet
/// contract by inspecting the address strkey prefix.
///
/// * `G...` → [`SignatureKind::Ed25519`]
/// * `C...` → [`SignatureKind::SmartWallet`]
/// * anything else → [`SignatureKind::Unknown`]
pub fn signature_kind(&self) -> SignatureKind {
SignatureKind::from_address(&self.address)
}
}

/// The kind of host function a single invocation step authorizes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
Expand Down Expand Up @@ -421,4 +434,56 @@ mod tests {
fn invalid_base64_is_an_error() {
assert!(AuthChain::from_xdr_base64("!!!not-valid!!!").is_err());
}

// --- Issue #271: signature_kind() on AddressCredential ---

#[test]
fn address_credential_ed25519_kind_from_account_address() {
use crate::decode::auth_signature::SignatureKind;
let entry = SorobanAuthorizationEntry {
credentials: SorobanCredentials::Address(SorobanAddressCredentials {
address: account_address(1),
nonce: 0,
signature_expiration_ledger: 0,
signature: ScVal::Void,
}),
root_invocation: invocation(
contract_fn(contract_address(2), "fn", vec![]),
empty_subs(),
),
};
let chain = AuthChain::from_entry(&entry);
match chain.credential {
AuthCredential::Address(cred) => {
assert_eq!(cred.signature_kind(), SignatureKind::Ed25519);
assert!(cred.address.starts_with('G'));
}
other => panic!("expected Address credential, got {other:?}"),
}
}

#[test]
fn address_credential_smart_wallet_kind_from_contract_address() {
use crate::decode::auth_signature::SignatureKind;
let entry = SorobanAuthorizationEntry {
credentials: SorobanCredentials::Address(SorobanAddressCredentials {
address: contract_address(5),
nonce: 0,
signature_expiration_ledger: 0,
signature: ScVal::Void,
}),
root_invocation: invocation(
contract_fn(contract_address(6), "fn", vec![]),
empty_subs(),
),
};
let chain = AuthChain::from_entry(&entry);
match chain.credential {
AuthCredential::Address(cred) => {
assert_eq!(cred.signature_kind(), SignatureKind::SmartWallet);
assert!(cred.address.starts_with('C'));
}
other => panic!("expected Address credential, got {other:?}"),
}
}
}
Loading