Skip to content

add Solana network support for legacy and versioned transactions #108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 18, 2025
Merged
Show file tree
Hide file tree
Changes from 7 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ it as a premier choice for constructing secure and high-performing blockchain so
- [x] Optimism (OP)
- [x] Polygon zkEVM
- [x] Stolz
- [x] Solana (SOL)

#### Coming soon | Under development:

Expand All @@ -60,7 +61,6 @@ it as a premier choice for constructing secure and high-performing blockchain so
- Cudos
- Aura
- Internet Computer (ICP)
- Solana (SOL)
- BNB Chain
- Bitcoin Cash (BCH)
- Cardano (ADA)
Expand Down
22 changes: 21 additions & 1 deletion packages/kos/src/chains/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ pub enum ChainError {
InvalidHex,
DecodeRawTx,
DecodeHash,
InvalidTransactionHeader,
InvalidAccountLength,
InvalidBlockhash,
InvalidSignatureLength,
}

impl Display for ChainError {
Expand Down Expand Up @@ -135,6 +139,18 @@ impl Display for ChainError {
ChainError::DecodeHash => {
write!(f, "decode hash")
}
ChainError::InvalidTransactionHeader => {
write!(f, "invalid transaction header")
}
ChainError::InvalidAccountLength => {
write!(f, "invalid account length")
}
ChainError::InvalidBlockhash => {
write!(f, "invalid block hash")
}
ChainError::InvalidSignatureLength => {
write!(f, "invalid signature length")
}
}
}
}
Expand Down Expand Up @@ -228,6 +244,10 @@ impl ChainError {
ChainError::InvalidHex => 23,
ChainError::DecodeRawTx => 24,
ChainError::DecodeHash => 25,
ChainError::InvalidTransactionHeader => 26,
ChainError::InvalidAccountLength => 27,
ChainError::InvalidBlockhash => 28,
ChainError::InvalidSignatureLength => 29,
}
}
}
Expand Down Expand Up @@ -542,7 +562,7 @@ impl ChainRegistry {
constants::SOL,
ChainInfo {
factory: || Box::new(sol::SOL {}),
supported: false,
supported: true,
},
),
(
Expand Down
193 changes: 184 additions & 9 deletions packages/kos/src/chains/sol/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod models;

use crate::chains::util::private_key_from_vec;
use crate::chains::{Chain, ChainError, Transaction, TxInfo};
use crate::crypto::b58::b58enc;
Expand All @@ -24,7 +26,7 @@ impl Chain for SOL {
}

fn get_decimals(&self) -> u32 {
todo!()
9
}

fn mnemonic_to_seed(&self, mnemonic: String, password: String) -> Result<Vec<u8>, ChainError> {
Expand Down Expand Up @@ -52,16 +54,44 @@ impl Chain for SOL {
Ok(String::from_utf8(addr)?)
}

fn sign_tx(&self, _private_key: Vec<u8>, _tx: Transaction) -> Result<Transaction, ChainError> {
Err(ChainError::NotSupported)
fn sign_tx(
&self,
private_key: Vec<u8>,
mut tx: Transaction,
) -> Result<Transaction, ChainError> {
let mut sol_tx = models::SolanaTransaction::decode(&tx.raw_data)?;

if sol_tx.message.header.num_required_signatures as usize != 1 {
return Err(ChainError::InvalidTransactionHeader);
}
if sol_tx.message.account_keys.is_empty() {
return Err(ChainError::InvalidAccountLength);
}
if sol_tx.message.recent_blockhash.iter().all(|&x| x == 0)
|| sol_tx.message.recent_blockhash.iter().all(|&x| x == 1)
{
return Err(ChainError::InvalidBlockhash);
}

let message_bytes = sol_tx.message.encode()?;

let signature = self.sign_raw(private_key, message_bytes)?;
if signature.len() != 64 {
return Err(ChainError::InvalidSignatureLength);
}
sol_tx.signatures = vec![signature.clone()];

tx.tx_hash = sol_tx.signatures[0].clone();

let signed_tx = sol_tx.encode()?;

tx.raw_data = signed_tx;
tx.signature = signature;
Ok(tx)
}

fn sign_message(
&self,
_private_key: Vec<u8>,
_message: Vec<u8>,
) -> Result<Vec<u8>, ChainError> {
Err(ChainError::NotSupported)
fn sign_message(&self, private_key: Vec<u8>, message: Vec<u8>) -> Result<Vec<u8>, ChainError> {
self.sign_raw(private_key, message)
}

fn sign_raw(&self, private_key: Vec<u8>, payload: Vec<u8>) -> Result<Vec<u8>, ChainError> {
Expand All @@ -79,6 +109,7 @@ impl Chain for SOL {
#[cfg(test)]
mod test {
use super::*;
use crate::crypto::base64::simple_base64_decode;
use alloc::string::ToString;

#[test]
Expand All @@ -93,4 +124,148 @@ mod test {
let addr = sol.get_address(pbk).unwrap();
assert_eq!(addr, "B9sVeu4rJU12oUrUtzjc6BSNuEXdfvurZkdcaTVkP2LY");
}

fn create_test_transaction() -> Vec<u8> {
let tx = models::SolanaTransaction {
message: models::Message {
version: "legacy".to_string(),
header: models::MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 0,
},
account_keys: vec![
vec![1; 32], // Sender
vec![2; 32], // Recipient
vec![3; 32], // Program ID
],
recent_blockhash: [42; 32],
instructions: vec![models::CompiledInstruction {
program_id_index: 2,
accounts: vec![0, 1],
data: vec![2, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0], // Transfer 100 lamports
}],
address_table_lookups: vec![],
},
signatures: vec![],
};
tx.encode().unwrap()
}

#[test]
fn test_derive_and_sign_tx() {
let sol = SOL {};
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
let seed = sol.mnemonic_to_seed(mnemonic, "".to_string()).unwrap();
let pvk = sol.derive(seed, "m/44'/501'/0'/0'/0'".to_string()).unwrap();

let raw_tx = create_test_transaction();
let tx = Transaction {
raw_data: raw_tx,
tx_hash: vec![],
signature: vec![],
options: Option::None,
};

let result = sol.sign_tx(pvk, tx).unwrap();

assert_eq!(result.signature.len(), 64);

// Verify tx_hash is not all ones and matches signature
assert!(!result.tx_hash.iter().all(|&x| x == 1));
assert_eq!(result.tx_hash.len(), 64);
assert!(!result.tx_hash.iter().all(|&x| x == 1));
assert!(!result.tx_hash.iter().all(|&x| x == 0));

let decoded = models::SolanaTransaction::decode(&result.raw_data).unwrap();
assert_eq!(decoded.signatures.len(), 1);
assert_eq!(decoded.signatures[0], result.signature);
assert_eq!(decoded.message.header.num_required_signatures, 1);
}

#[test]
fn test_sign_tx_consistent_hash() {
let sol = SOL {};
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
let seed = sol.mnemonic_to_seed(mnemonic, "".to_string()).unwrap();
let pvk = sol.derive(seed, "m/44'/501'/0'/0'/0'".to_string()).unwrap();

let raw_tx = create_test_transaction();
let tx1 = Transaction {
raw_data: raw_tx.clone(),
tx_hash: vec![],
signature: vec![],
options: Option::None,
};

let tx2 = Transaction {
raw_data: raw_tx,
tx_hash: vec![],
signature: vec![],
options: Option::None,
};

let result1 = sol.sign_tx(pvk.clone(), tx1).unwrap();
let result2 = sol.sign_tx(pvk, tx2).unwrap();

// Same transaction signed with same key should produce same signature and hash
assert_eq!(result1.signature, result2.signature);
assert_eq!(result1.tx_hash, result2.tx_hash);
}

#[test]
fn test_sign_tx_legacy() {
let sol = SOL {};
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
let seed = sol.mnemonic_to_seed(mnemonic, "".to_string()).unwrap();
let pvk = sol.derive(seed, "m/44'/501'/0'/0'/0'".to_string()).unwrap();

let raw_tx = simple_base64_decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAIEmjxocK65Bo8r+e3cj7GbPVedpCwx+DCZJ57Tw3fMN0e5dTAYLc651CwBwFga8GLJTsriJc/FAP3GlbhfEGOidAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwZGb+UhFzL/7K26csOb57yM5bvF9xJrLEObOkAAAACg2vm5+lhfRud/PKY6hEMgdKkQ8I7jtpxDFjknIKRXGQMDAAUCSQIAAAMACQOAlpgAAAAAAAICAAEUAgAAAAEAAAAAAAAAsmBySL6HLBg=").unwrap();

let tx1 = Transaction {
raw_data: raw_tx.clone(),
tx_hash: vec![],
signature: vec![],
options: Option::None,
};

let result = sol.sign_tx(pvk.clone(), tx1).unwrap();

// Same transaction signed with same key should produce same signature and hash
assert_eq!(hex::encode(&result.signature), "b079c666c9ff53bb26d7606d10131ebbc8d398dac9fd1285d5138bbdd521758d7a6b6bdb2876730637704eb1511f3f7d842343b9e406bb3e3583d6588949a904");
}

#[test]
fn test_sign_tx_v0() {
let sol = SOL {};
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
let seed = sol.mnemonic_to_seed(mnemonic, "".to_string()).unwrap();
let pvk = sol.derive(seed, "m/44'/501'/0'/0'/0'".to_string()).unwrap();

let raw_tx = simple_base64_decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAGCpo8aHCuuQaPK/nt3I+xmz1XnaQsMfgwmSee08N3zDdHWO9nf7VjXmRzcktw4WtkBVQDTqR6HHs/zYiFPEFdMlR2uAUKvCmGoT5EOvm/TqTTENr0znYcEsWsViKudXw20rGZQgJtALiRcUwlRMT2kZt8QRbvckZEPIiyFe59326vAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACsH4P9uc5VDeldVYzceVRhzPQ3SsaI7BOphAAiCnjaBgMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAtD/6J/XX9kp0wJsfKVh53ksJqzbfyd1RSzIap7OM5egEedVb8jHAbu50xW7OaBUH/bGy3qP0jlECsc2iVrwTjwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpheXoR6gYqo7X4aA9Sx2/Qcpf6TpzF6ddVuj771s5eWQFBgAFAua+AQAGAAkDRJEGAAAAAAAIBQMAEwkECZPxe2T0hK52/wgYCQACAwgTAQcIDxELAAIDDgoNDAkSEhAFI+UXy5d6460qAQAAABlkAAH4LgEAAAAAAMGtCQAAAAAAKwAFCQMDAAABCQEP5d+hcffknhCj1qkbVbtXFKZDtelOHlry/os01b5PsgXi4ePoyQXn5ODlRQ==").unwrap();
let tx1 = Transaction {
raw_data: raw_tx.clone(),
tx_hash: vec![],
signature: vec![],
options: Option::None,
};

let result = sol.sign_tx(pvk.clone(), tx1).unwrap();
// Same transaction signed with same key should produce same signature and hash
assert_eq!(hex::encode(&result.signature), "40098643a37209b2e0984c2f55872ccf150c44a1100a16a985b1bc04b13c31f9d9d1b070229241df5aaa21af22e0e4f88b6371106766fd95096b67f1066f8701");
}

#[test]
fn test_sign_message() {
let sol = SOL {};
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
let seed = sol.mnemonic_to_seed(mnemonic, "".to_string()).unwrap();
let pvk = sol.derive(seed, "m/44'/501'/0'/0'/0'".to_string()).unwrap();

let message = "Hello, World!".as_bytes().to_vec();
let result = sol.sign_message(pvk.clone(), message.into()).unwrap();

// Same transaction signed with same key should produce same signature and hash
assert_eq!(hex::encode(&result), "e8ebd3bf665fe5b57e421c477fa4187ef5f1275ddc8dbf693dd684a0164f11aef22bb98416e2e765d39dbb38451d8996fea135baa9e9fd13890286e8be0e8200");
}
}
Loading