bitrouter_core/jwt/
keys.rs1use alloy_primitives::Address;
10use alloy_signer::SignerSync;
11use alloy_signer_local::PrivateKeySigner;
12use base64::Engine;
13use base64::engine::general_purpose::URL_SAFE_NO_PAD;
14use serde::{Deserialize, Serialize};
15use solana_keypair::{Keypair as SolanaKeypair, Signer as SolanaSigner};
16use solana_pubkey::Pubkey;
17
18use crate::jwt::JwtError;
19use crate::jwt::chain::{Caip10, Chain};
20
21#[derive(Clone)]
27pub struct MasterKeypair {
28 seed: [u8; 32],
29}
30
31impl MasterKeypair {
32 pub fn generate() -> Self {
34 Self {
35 seed: rand::random(),
36 }
37 }
38
39 pub fn from_seed(seed: [u8; 32]) -> Self {
41 Self { seed }
42 }
43
44 pub fn seed(&self) -> &[u8; 32] {
46 &self.seed
47 }
48
49 fn solana_keypair(&self) -> SolanaKeypair {
53 SolanaKeypair::new_from_array(self.seed)
54 }
55
56 pub fn solana_pubkey_b58(&self) -> String {
58 self.solana_keypair().pubkey().to_string()
59 }
60
61 pub fn evm_signer(&self) -> Result<PrivateKeySigner, JwtError> {
65 PrivateKeySigner::from_slice(&self.seed).map_err(|e| JwtError::Secp256k1(e.to_string()))
66 }
67
68 pub fn evm_address(&self) -> Result<Address, JwtError> {
70 Ok(self.evm_signer()?.address())
71 }
72
73 pub fn evm_address_string(&self) -> Result<String, JwtError> {
75 Ok(self.evm_address()?.to_checksum(None))
76 }
77
78 pub fn caip10(&self, chain: &Chain) -> Result<Caip10, JwtError> {
82 let address = match chain {
83 Chain::Solana { .. } => self.solana_pubkey_b58(),
84 Chain::Evm { .. } => self.evm_address_string()?,
85 };
86 Ok(Caip10 {
87 chain: chain.clone(),
88 address,
89 })
90 }
91
92 pub fn public_key_prefix(&self) -> String {
98 let b58 = self.solana_pubkey_b58();
99 b58[..16.min(b58.len())].to_string()
100 }
101
102 pub fn sign_ed25519(&self, message: &[u8]) -> Vec<u8> {
108 self.solana_keypair()
109 .sign_message(message)
110 .as_ref()
111 .to_vec()
112 }
113
114 pub fn sign_eip191(&self, message: &[u8]) -> Result<Vec<u8>, JwtError> {
121 let signer = self.evm_signer()?;
122 let sig = signer
123 .sign_message_sync(message)
124 .map_err(|e| JwtError::Signing(e.to_string()))?;
125 Ok(sig.as_bytes().to_vec())
126 }
127
128 pub fn to_json(&self) -> MasterKeyJson {
132 MasterKeyJson {
133 algorithm: "web3".to_string(),
134 seed: URL_SAFE_NO_PAD.encode(self.seed),
135 }
136 }
137
138 pub fn from_json(json: &MasterKeyJson) -> Result<Self, JwtError> {
140 if json.algorithm != "web3" {
141 return Err(JwtError::InvalidKeypair);
142 }
143 let bytes = URL_SAFE_NO_PAD
144 .decode(&json.seed)
145 .map_err(|_| JwtError::InvalidKeypair)?;
146 let seed: [u8; 32] = bytes.try_into().map_err(|_| JwtError::InvalidKeypair)?;
147 Ok(Self::from_seed(seed))
148 }
149}
150
151pub fn decode_solana_pubkey(b58: &str) -> Result<Pubkey, JwtError> {
155 b58.parse::<Pubkey>()
156 .map_err(|_| JwtError::InvalidPublicKey)
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct MasterKeyJson {
164 pub algorithm: String,
166 pub seed: String,
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn roundtrip_seed() {
176 let kp = MasterKeypair::generate();
177 let seed = *kp.seed();
178 let kp2 = MasterKeypair::from_seed(seed);
179 assert_eq!(kp.solana_pubkey_b58(), kp2.solana_pubkey_b58());
180 }
181
182 #[test]
183 fn roundtrip_json() {
184 let kp = MasterKeypair::generate();
185 let json = kp.to_json();
186 let kp2 = MasterKeypair::from_json(&json).expect("valid json");
187 assert_eq!(kp.solana_pubkey_b58(), kp2.solana_pubkey_b58());
188 }
189
190 #[test]
191 fn same_seed_same_addresses() {
192 let kp1 = MasterKeypair::generate();
193 let kp2 = MasterKeypair::from_seed(*kp1.seed());
194 assert_eq!(kp1.solana_pubkey_b58(), kp2.solana_pubkey_b58());
195 assert_eq!(
196 kp1.evm_address_string().expect("evm"),
197 kp2.evm_address_string().expect("evm")
198 );
199 }
200
201 #[test]
202 fn solana_and_evm_addresses_differ() {
203 let kp = MasterKeypair::generate();
204 let sol = kp.solana_pubkey_b58();
205 let evm = kp.evm_address_string().expect("evm");
206 assert_ne!(sol, evm);
207 }
208
209 #[test]
210 fn public_key_prefix_length() {
211 let kp = MasterKeypair::generate();
212 assert_eq!(kp.public_key_prefix().len(), 16);
213 }
214
215 #[test]
216 fn caip10_solana() {
217 let kp = MasterKeypair::generate();
218 let chain = Chain::solana_mainnet();
219 let id = kp.caip10(&chain).expect("caip10");
220 assert!(
221 id.format()
222 .starts_with("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:")
223 );
224 assert_eq!(id.address, kp.solana_pubkey_b58());
225 }
226
227 #[test]
228 fn caip10_evm() {
229 let kp = MasterKeypair::generate();
230 let chain = Chain::base();
231 let id = kp.caip10(&chain).expect("caip10");
232 assert!(id.format().starts_with("eip155:8453:0x"));
233 assert_eq!(id.address, kp.evm_address_string().expect("evm"));
234 }
235
236 #[test]
237 fn decode_solana_pubkey_roundtrip() {
238 let kp = MasterKeypair::generate();
239 let b58 = kp.solana_pubkey_b58();
240 let pk = decode_solana_pubkey(&b58).expect("decode");
241 assert_eq!(bs58::encode(pk.to_bytes()).into_string(), b58);
242 }
243
244 #[test]
245 fn sign_ed25519_produces_64_bytes() {
246 let kp = MasterKeypair::generate();
247 let sig = kp.sign_ed25519(b"test message");
248 assert_eq!(sig.len(), 64);
249 }
250
251 #[test]
252 fn sign_eip191_produces_65_bytes() {
253 let kp = MasterKeypair::generate();
254 let sig = kp.sign_eip191(b"test message").expect("sign");
255 assert_eq!(sig.len(), 65);
256 }
257
258 #[test]
259 fn from_json_rejects_wrong_algorithm() {
260 let kp = MasterKeypair::generate();
261 let mut json = kp.to_json();
262 json.algorithm = "rsa".to_string();
263 assert!(MasterKeypair::from_json(&json).is_err());
264 }
265
266 #[test]
267 fn eip191_signature_recovers_correct_address() {
268 use alloy_primitives::Signature as EvmSignature;
269
270 let kp = MasterKeypair::generate();
271 let message = b"hello web3";
272 let sig_bytes = kp.sign_eip191(message).expect("sign");
273
274 let sig = EvmSignature::try_from(sig_bytes.as_slice()).expect("parse sig");
275 let recovered = sig.recover_address_from_msg(message).expect("recover");
276 assert_eq!(recovered, kp.evm_address().expect("address"));
277 }
278}