forked from Txio-labs/txio-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvalidation.rs
More file actions
177 lines (161 loc) · 5.96 KB
/
Copy pathvalidation.rs
File metadata and controls
177 lines (161 loc) · 5.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
use anyhow::{Result, anyhow};
use reqwest::Url;
use stellar_strkey::Strkey; // Added for Soroban validation
pub fn validate_aptos_address(address: &str) -> Result<String> {
let address = address.trim();
let stripped = address.strip_prefix("0x").unwrap_or(address);
if stripped.is_empty() || stripped.len() > 64 {
return Err(anyhow!(
"Invalid Aptos address: length must be 1-64 hex chars, optionally prefixed with 0x"
));
}
if !stripped.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(anyhow!(
"Invalid Aptos address: must contain only hexadecimal characters"
));
}
Ok(format!("0x{}", stripped.to_lowercase()))
}
/// Validates a Soroban/Stellar address.
/// Performs full Strkey decoding to verify the version byte and CRC16 checksum.
pub fn validate_soroban_address(address: &str) -> Result<String> {
let address = address.trim();
// Fast fail for obviously wrong length/prefix
if address.len() != 56 || !address.starts_with('G') {
return Err(anyhow!(
"Invalid Soroban address: expected 56 characters starting with 'G'"
));
}
// Decode the Strkey (base32 decode + checksum verification)
match Strkey::from_string(address) {
Ok(Strkey::PublicKeyEd25519(_)) => Ok(address.to_string()),
Ok(_) => Err(anyhow!(
"Invalid Soroban address type: only standard 'G' public keys are supported"
)),
Err(e) => Err(anyhow!(
"Invalid Soroban address: checksum or encoding error: {e}"
)),
}
}
pub fn validate_ethereum_address(address: &str) -> Result<String> {
let address = address.trim();
let stripped = address.strip_prefix("0x").unwrap_or(address);
// Ethereum account addresses are exactly 20 bytes (40 hex characters).
if stripped.len() != 40 {
return Err(anyhow!(
"Invalid Ethereum address: must be exactly 40 hex characters (20 bytes), optionally prefixed with 0x"
));
}
if !stripped.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(anyhow!(
"Invalid Ethereum address: must contain only hexadecimal characters"
));
}
Ok(format!("0x{}", stripped.to_lowercase()))
}
pub fn validate_solana_address(address: &str) -> Result<String> {
let address = address.trim();
if address.is_empty() {
return Err(anyhow!("Invalid Solana address: cannot be empty"));
}
// Reject characters outside the base58 alphabet before decoding.
if !address
.chars()
.all(|c| matches!(c, '1'..='9' | 'A'..='H' | 'J'..='N' | 'P'..='Z' | 'a'..='k' | 'm'..='z'))
{
return Err(anyhow!(
"Invalid Solana address: must be a base58-encoded public key"
));
}
// Decode and verify the address produces exactly 32 bytes (Ed25519 public key).
let decoded = bs58::decode(address)
.into_vec()
.map_err(|e| anyhow!("Invalid Solana address: base58 decode failed: {e}"))?;
if decoded.len() != 32 {
return Err(anyhow!(
"Invalid Solana address: expected 32-byte public key, got {} bytes",
decoded.len()
));
}
Ok(address.to_string())
}
pub fn validate_sui_address(address: &str) -> Result<String> {
let address = address.trim();
let stripped = address.strip_prefix("0x").unwrap_or(address);
if stripped.is_empty() || stripped.len() > 64 {
return Err(anyhow!(
"Invalid Sui address: length must be 1-64 hex chars, optionally prefixed with 0x"
));
}
if !stripped.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(anyhow!(
"Invalid Sui address: must contain only hexadecimal characters"
));
}
Ok(format!("0x{}", stripped.to_lowercase()))
}
pub fn build_url(base: &str, segments: &[&str]) -> Result<Url> {
let mut url = Url::parse(base).map_err(|e| anyhow!("Invalid base URL: {e}"))?;
{
let mut path_segments = url
.path_segments_mut()
.map_err(|_| anyhow!("Failed to build URL path segments from base URL"))?;
for segment in segments {
path_segments.push(segment);
}
}
Ok(url)
}
pub fn build_url_with_query(
base: &str,
segments: &[&str],
query: &[(&str, String)],
) -> Result<Url> {
let mut url = build_url(base, segments)?;
{
let mut pairs = url.query_pairs_mut();
for (key, value) in query {
pairs.append_pair(key, value);
}
}
Ok(url)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_soroban_address_success() {
// A valid Stellar 'G' address
let addr = "GDV6S2C7ZALZSVY5QO7W6XN2L654A7V2D65Y5S65K6L673R776S567X6";
assert!(validate_soroban_address(addr).is_ok());
}
#[test]
fn validate_soroban_address_rejects_invalid_checksum() {
// G-prefix, 56 chars, valid charset, but structurally invalid (corrupted checksum)
let addr = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
let result = validate_soroban_address(addr);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("checksum"));
}
#[test]
fn validate_soroban_address_rejects_wrong_prefix() {
let addr = "BDV6S2C7ZALZSVY5QO7W6XN2L654A7V2D65Y5S65K6L673R776S567X6";
assert!(validate_soroban_address(addr).is_err());
}
#[test]
fn validate_soroban_address_rejects_wrong_length() {
assert!(validate_soroban_address("GAAA").is_err());
}
#[test]
fn validate_ethereum_address_accepts_0x_prefixed() {
assert_eq!(
validate_ethereum_address("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045").unwrap(),
"0xd8da6bf26964af9d7eed9e03e53415d37aa96045"
);
}
#[test]
fn build_url_encodes_path_segments() {
let url = build_url("https://example.com/v1", &["accounts", "foo/bar"]).unwrap();
assert_eq!(url.as_str(), "https://example.com/v1/accounts/foo%2Fbar");
}
}