From d31f5bb094efbff87546c8519f5a0963c7382d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Tue, 16 Sep 2025 08:22:29 +0300 Subject: [PATCH 01/27] implement `send_raw_tx*` functions for solana MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- Cargo.lock | 2 ++ mm2src/coins/Cargo.toml | 4 ++++ mm2src/coins/solana/solana_coin.rs | 28 +++++++++++++++++++++++++--- mm2src/mm2_main/Cargo.toml | 2 +- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c96971897e..1d20860aa1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1036,11 +1036,13 @@ dependencies = [ "sha2 0.10.9", "sha3 0.9.1", "sia-rust", + "solana-bincode", "solana-keypair", "solana-pubkey", "solana-rpc-client", "solana-rpc-client-types", "solana-signer", + "solana-transaction", "spl-associated-token-account-client", "spv_validation", "tendermint-rpc", diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index e84ed0e373..75ef3d5ab9 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -10,11 +10,13 @@ enable-sia = [ "dep:sia-rust" ] enable-solana = [ + "dep:solana-bincode", "dep:solana-keypair", "dep:solana-pubkey", "dep:solana-rpc-client-types", "dep:solana-rpc-client", "dep:solana-signer", + "dep:solana-transaction", "dep:spl-associated-token-account-client", ] default = [] @@ -125,11 +127,13 @@ zcash_extras.workspace = true zcash_primitives.workspace = true # Solana +solana-bincode = { version = "2.2", default-features = false, optional = true } solana-keypair = { version = "2.2", default-features = false, optional = true } solana-pubkey = { version = "2.4", default-features = false, optional = true } solana-rpc-client = { version = "2.3", default-features = false, optional = true } solana-rpc-client-types= { version = "2.3", default-features = false, optional = true } solana-signer = { version = "2.2", default-features = false, optional = true } +solana-transaction = { version = "2.2", default-features = false, optional = true } spl-associated-token-account-client = { version = "2.0", default-features = false, optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/mm2src/coins/solana/solana_coin.rs b/mm2src/coins/solana/solana_coin.rs index bb99d0ff87..aa67a69405 100644 --- a/mm2src/coins/solana/solana_coin.rs +++ b/mm2src/coins/solana/solana_coin.rs @@ -21,11 +21,13 @@ use nom::AsBytes; use num_traits::Zero; use parking_lot::Mutex as PaMutex; use rpc::v1::types::{Bytes as RpcBytes, H264 as RpcH264}; +use solana_bincode::limited_deserialize; use solana_keypair::keypair_from_seed; use solana_pubkey::Pubkey as SolanaAddress; use solana_rpc_client::rpc_client::RpcClient; use solana_rpc_client_types::request::TokenAccountsFilter; use solana_signer::Signer; +use solana_transaction::Transaction; use url::Url; use crate::{ @@ -42,6 +44,14 @@ use crate::{ pub const SOLANA_DECIMALS: u8 = 9; +/// Maximum over-the-wire size of a Transaction +/// 1280 is IPv6 minimum MTU +/// 40 bytes is the size of the IPv6 header +/// 8 bytes is the size of the fragment header +/// +/// Ported from: https://github.com/anza-xyz/solana-sdk/blob/ac902c4bdb8b0a1/packet/src/lib.rs#L28-L32 +pub const PACKET_DATA_SIZE: usize = 1280 - 40 - 8; + #[derive(Clone, Deserialize)] pub struct RpcNode { url: Url, @@ -228,7 +238,7 @@ impl MmCoin for SolanaCoin { } fn spawner(&self) -> WeakSpawner { - todo!() + self.abortable_system.weak_spawner() } fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { @@ -402,11 +412,23 @@ impl MarketCoinOps for SolanaCoin { } fn send_raw_tx(&self, tx: &str) -> Box + Send> { - todo!() + let bytes = try_fus!(hex::decode(tx)); + self.send_raw_tx_bytes(&bytes) } fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { - todo!() + let coin = self.clone(); + let bytes = tx.to_vec(); + let fut = async move { + let rpc = coin.rpc_client().await.map_err(|e| e.into_inner())?; + + let tx: Transaction = limited_deserialize(&bytes, PACKET_DATA_SIZE as u64).map_err(|e| e.to_string())?; + let signature = rpc.send_transaction(&tx).map_err(|e| e.to_string())?; + + // TX hash is just the base58 `String` form of the `Signature`. + Ok(signature.to_string()) + }; + Box::new(fut.boxed().compat()) } #[inline(always)] diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 0a4994305d..1abe429b0d 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -18,7 +18,7 @@ native = [] # Deprecated track-ctx-pointer = ["common/track-ctx-pointer"] zhtlc-native-tests = ["coins/zhtlc-native-tests"] run-docker-tests = ["for-tests", "coins/run-docker-tests"] -default = [] +default = [ "enable-solana" ] trezor-udp = ["crypto/trezor-udp"] # use for tests to connect to trezor emulator over udp run-device-tests = [] enable-sia = ["coins/enable-sia", "coins_activation/enable-sia"] From c3f52f2f356d486cc09b316940c7fc3564a98727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Tue, 16 Sep 2025 12:49:29 +0300 Subject: [PATCH 02/27] implement withdraw RPC for Solana MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- Cargo.lock | 17 +++++ mm2src/coins/Cargo.toml | 4 + mm2src/coins/solana/solana_coin.rs | 111 ++++++++++++++++++++++++++-- mm2src/coins/solana/solana_token.rs | 2 +- 4 files changed, 127 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1d20860aa1..b913b6bf8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -946,6 +946,7 @@ dependencies = [ "async-std", "async-trait", "base64 0.21.7", + "bincode", "bip32", "bitcoin", "bitcoin_hashes", @@ -1042,6 +1043,7 @@ dependencies = [ "solana-rpc-client", "solana-rpc-client-types", "solana-signer", + "solana-system-transaction", "solana-transaction", "spl-associated-token-account-client", "spv_validation", @@ -7658,6 +7660,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "solana-system-transaction" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd98a25e5bcba8b6be8bcbb7b84b24c2a6a8178d7fb0e3077a916855ceba91a" +dependencies = [ + "solana-hash", + "solana-keypair", + "solana-message", + "solana-pubkey", + "solana-signer", + "solana-system-interface", + "solana-transaction", +] + [[package]] name = "solana-sysvar" version = "2.3.0" diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 75ef3d5ab9..0c454541d9 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -10,12 +10,14 @@ enable-sia = [ "dep:sia-rust" ] enable-solana = [ + "dep:bincode", "dep:solana-bincode", "dep:solana-keypair", "dep:solana-pubkey", "dep:solana-rpc-client-types", "dep:solana-rpc-client", "dep:solana-signer", + "dep:solana-system-transaction", "dep:solana-transaction", "dep:spl-associated-token-account-client", ] @@ -127,12 +129,14 @@ zcash_extras.workspace = true zcash_primitives.workspace = true # Solana +bincode = { version = "1.3", default-features = false, optional = true } solana-bincode = { version = "2.2", default-features = false, optional = true } solana-keypair = { version = "2.2", default-features = false, optional = true } solana-pubkey = { version = "2.4", default-features = false, optional = true } solana-rpc-client = { version = "2.3", default-features = false, optional = true } solana-rpc-client-types= { version = "2.3", default-features = false, optional = true } solana-signer = { version = "2.2", default-features = false, optional = true } +solana-system-transaction = { version = "2.2", default-features = false, optional = true } solana-transaction = { version = "2.2", default-features = false, optional = true } spl-associated-token-account-client = { version = "2.0", default-features = false, optional = true } diff --git a/mm2src/coins/solana/solana_coin.rs b/mm2src/coins/solana/solana_coin.rs index aa67a69405..4f9b06ab04 100644 --- a/mm2src/coins/solana/solana_coin.rs +++ b/mm2src/coins/solana/solana_coin.rs @@ -18,11 +18,13 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; use nom::AsBytes; +use num_traits::ToPrimitive; use num_traits::Zero; use parking_lot::Mutex as PaMutex; +use rpc::v1::types::Bytes as BytesJson; use rpc::v1::types::{Bytes as RpcBytes, H264 as RpcH264}; use solana_bincode::limited_deserialize; -use solana_keypair::keypair_from_seed; +use solana_keypair::{keypair_from_seed, Keypair}; use solana_pubkey::Pubkey as SolanaAddress; use solana_rpc_client::rpc_client::RpcClient; use solana_rpc_client_types::request::TokenAccountsFilter; @@ -37,9 +39,10 @@ use crate::{ FoundSwapTxSpend, HistorySyncState, MarketCoinOps, MmCoin, NegotiateSwapContractAddrErr, PrivKeyBuildPolicy, RawTransactionFut, RawTransactionRequest, RawTransactionResult, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, TradeFee, TradePreimageFut, - TradePreimageResult, TradePreimageValue, TransactionEnum, TransactionResult, TxMarshalingErr, - UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateOtherPubKeyErr, ValidatePaymentInput, - VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WithdrawFut, WithdrawRequest, + TradePreimageResult, TradePreimageValue, TransactionData, TransactionDetails, TransactionEnum, TransactionResult, + TransactionType, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, + ValidateOtherPubKeyErr, ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, + WithdrawError, WithdrawFut, WithdrawRequest, }; pub const SOLANA_DECIMALS: u8 = 9; @@ -63,6 +66,7 @@ pub struct SolanaCoin(Arc); pub struct SolanaCoinFields { ticker: String, pub(crate) address: SolanaAddress, + keypair: Keypair, pub(crate) abortable_system: AbortableQueue, rpc_clients: AsyncMutex>>, protocol_info: SolanaProtocolInfo, @@ -167,6 +171,7 @@ impl SolanaCoin { let fields = SolanaCoinFields { ticker, address, + keypair, abortable_system, rpc_clients: AsyncMutex::new(rpc_clients), protocol_info, @@ -225,6 +230,40 @@ impl SolanaCoin { unspendable: Default::default(), }) } + + async fn calculate_withdraw_amount(&self, req: &WithdrawRequest) -> MmResult { + let rpc = self + .rpc_client() + .await + .map_err(|e| WithdrawError::Transport(e.into_inner()))?; + + if req.max { + let balance = rpc + .get_balance(&self.address) + .map_err(|e| WithdrawError::Transport(e.to_string()))?; + + let recent_blockhash = rpc + .get_latest_blockhash() + .map_err(|e| WithdrawError::Transport(e.to_string()))?; + + // Dummy TX to estimate the fee. + let tx = solana_system_transaction::transfer(&self.keypair, &self.address, balance, recent_blockhash); + + let fee = rpc + .get_fee_for_message(tx.message()) + .map_err(|e| WithdrawError::Transport(e.to_string()))?; + + Ok(balance.saturating_sub(fee)) + } else { + let big_decimal = &req.amount * &BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32)); + + big_decimal.to_u64().ok_or_else(|| { + MmError::new(WithdrawError::InternalError(format!( + "Couldn't convert {big_decimal} to u64." + ))) + }) + } + } } #[async_trait] @@ -242,7 +281,67 @@ impl MmCoin for SolanaCoin { } fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { - todo!() + let coin = self.clone(); + let fut = async move { + let to = SolanaAddress::from_str(&req.to).map_err(|e| WithdrawError::InvalidAddress(e.to_string()))?; + + let rpc = coin + .rpc_client() + .await + .map_err(|e| WithdrawError::Transport(e.into_inner()))?; + + let lamports = coin.calculate_withdraw_amount(&req).await?; + + if lamports == 0 { + return MmError::err(WithdrawError::AmountTooLow { + amount: req.amount, + threshold: coin.min_tx_amount(), + }); + } + + let recent_blockhash = rpc + .get_latest_blockhash() + .map_err(|e| WithdrawError::Transport(e.to_string()))?; + + // Actual TX + let tx = solana_system_transaction::transfer(&coin.keypair, &to, lamports, recent_blockhash); + + let tx_hash = tx + .signatures + .first() + .map(|s| s.to_string()) + .ok_or_else(|| WithdrawError::InternalError("Couldn't find the TX signature.".to_owned()))?; + + let tx_bytes = + bincode::serialize(&tx).map_err(|e| MmError::new(WithdrawError::InternalError(e.to_string())))?; + + let tx_data = TransactionData::new_signed(BytesJson(tx_bytes), tx_hash.clone()); + + let amount_dec = BigDecimal::from(lamports) / BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32)); + + Ok(TransactionDetails { + tx: tx_data, + from: vec![coin.address.to_string()], + to: vec![to.to_string()], + total_amount: amount_dec.clone(), + spent_by_me: amount_dec.clone(), + // TODO: calculate this field properly. + received_by_me: BigDecimal::from(0), + my_balance_change: -amount_dec, + block_height: 0, + timestamp: 0, + // TODO: handle fee_details here. + fee_details: None, + coin: req.coin, + internal_id: BytesJson(tx_hash.into_bytes()), + kmd_rewards: None, + transaction_type: TransactionType::StandardTransfer, + // TODO: add memo instruction to the TX. + memo: req.memo, + }) + }; + + Box::new(fut.boxed().compat()) } fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut<'_> { @@ -466,7 +565,7 @@ impl MarketCoinOps for SolanaCoin { #[inline] fn min_tx_amount(&self) -> BigDecimal { - todo!() + BigDecimal::from(1) / BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32)) } #[inline] diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index 92939b7dda..95a3d69778 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -324,7 +324,7 @@ impl MarketCoinOps for SolanaToken { #[inline] fn min_tx_amount(&self) -> BigDecimal { - todo!() + self.platform_coin.min_tx_amount() } #[inline] From dbe6033c6ad12705c724bec692f199c2a714b4a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Wed, 17 Sep 2025 07:26:20 +0300 Subject: [PATCH 03/27] handle solana fee details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/lp_coins.rs | 4 ++++ mm2src/coins/solana/mod.rs | 1 + mm2src/coins/solana/solana_coin.rs | 23 +++++++++++++++++++---- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index 9c53c21648..cd430ad9e7 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -303,6 +303,8 @@ use z_coin::{ZCoin, ZcoinProtocolInfo}; #[cfg(feature = "enable-solana")] pub mod solana; +#[cfg(feature = "enable-solana")] +use crate::solana::SolanaFeeDetails; pub type TransactionFut = Box + Send>; pub type TransactionResult = Result; @@ -2440,6 +2442,8 @@ pub enum TxFeeDetails { Qrc20(Qrc20FeeDetails), Slp(SlpFeeDetails), Tendermint(TendermintFeeDetails), + #[cfg(feature = "enable-solana")] + Solana(SolanaFeeDetails), } /// Deserialize the TxFeeDetails as an untagged enum. diff --git a/mm2src/coins/solana/mod.rs b/mm2src/coins/solana/mod.rs index 4aa90e1631..a4796f8754 100644 --- a/mm2src/coins/solana/mod.rs +++ b/mm2src/coins/solana/mod.rs @@ -2,6 +2,7 @@ mod solana_coin; mod solana_token; pub use solana_coin::RpcNode; +pub use solana_coin::SolanaFeeDetails; pub use solana_coin::{SolanaCoin, SolanaProtocolInfo}; pub use solana_coin::{SolanaInitError, SolanaInitErrorKind}; pub use solana_token::{SolanaToken, SolanaTokenProtocolInfo}; diff --git a/mm2src/coins/solana/solana_coin.rs b/mm2src/coins/solana/solana_coin.rs index 4f9b06ab04..f5d98623b1 100644 --- a/mm2src/coins/solana/solana_coin.rs +++ b/mm2src/coins/solana/solana_coin.rs @@ -32,6 +32,7 @@ use solana_signer::Signer; use solana_transaction::Transaction; use url::Url; +use crate::TxFeeDetails; use crate::{ coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}, hd_wallet::HDAddressSelector, @@ -108,6 +109,11 @@ pub enum SolanaInitErrorKind { }, } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct SolanaFeeDetails { + pub amount: BigDecimal, +} + impl SolanaCoin { pub async fn init( ctx: &MmArc, @@ -319,19 +325,28 @@ impl MmCoin for SolanaCoin { let amount_dec = BigDecimal::from(lamports) / BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32)); + let fee = rpc + .get_fee_for_message(tx.message()) + .map_err(|e| WithdrawError::Transport(e.to_string()))?; + let fee = BigDecimal::from(fee) / BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32)); + + let received_by_me = if to == coin.address { + amount_dec.clone() + } else { + BigDecimal::zero() + }; + Ok(TransactionDetails { tx: tx_data, from: vec![coin.address.to_string()], to: vec![to.to_string()], total_amount: amount_dec.clone(), spent_by_me: amount_dec.clone(), - // TODO: calculate this field properly. - received_by_me: BigDecimal::from(0), + received_by_me, my_balance_change: -amount_dec, block_height: 0, timestamp: 0, - // TODO: handle fee_details here. - fee_details: None, + fee_details: Some(TxFeeDetails::Solana(SolanaFeeDetails { amount: fee })), coin: req.coin, internal_id: BytesJson(tx_hash.into_bytes()), kmd_rewards: None, From 0b699ef8c5f57bf85133dff26ca5aa07b355ec54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Wed, 17 Sep 2025 07:27:44 +0300 Subject: [PATCH 04/27] add TODO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_coin.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mm2src/coins/solana/solana_coin.rs b/mm2src/coins/solana/solana_coin.rs index f5d98623b1..a1f7a28bcd 100644 --- a/mm2src/coins/solana/solana_coin.rs +++ b/mm2src/coins/solana/solana_coin.rs @@ -263,6 +263,8 @@ impl SolanaCoin { } else { let big_decimal = &req.amount * &BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32)); + // TODO: Check if user can afford the fee. + big_decimal.to_u64().ok_or_else(|| { MmError::new(WithdrawError::InternalError(format!( "Couldn't convert {big_decimal} to u64." From 751085c89c2a2be046d166bc418bcc5b29a4a1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Wed, 17 Sep 2025 13:08:45 +0300 Subject: [PATCH 05/27] implement withdraw for solana tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- Cargo.lock | 564 +++++++++++++++++- mm2src/coins/Cargo.toml | 2 + mm2src/coins/solana/solana_coin.rs | 6 +- mm2src/coins/solana/solana_token.rs | 193 +++++- .../src/solana_token_activation.rs | 8 +- .../src/solana_with_assets.rs | 12 +- 6 files changed, 739 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b913b6bf8e..e21b3c42c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,9 +181,9 @@ dependencies = [ [[package]] name = "arrayref" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" @@ -193,9 +193,9 @@ checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" [[package]] name = "arrayvec" -version = "0.7.1" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4dc07131ffa69b8072d35f5007352af944213cde02545e2103680baed38fcd" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "asn1_der" @@ -405,6 +405,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + [[package]] name = "base64" version = "0.13.0" @@ -598,7 +604,7 @@ checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" dependencies = [ "arrayref", "arrayvec 0.5.1", - "constant_time_eq", + "constant_time_eq 0.1.5", ] [[package]] @@ -609,7 +615,21 @@ checksum = "ab9e07352b829279624ceb7c64adb4f585dacdb81d35cafae81139ccd617cf44" dependencies = [ "arrayref", "arrayvec 0.5.1", - "constant_time_eq", + "constant_time_eq 0.1.5", +] + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "cc", + "cfg-if 1.0.0", + "constant_time_eq 0.3.1", + "digest 0.10.7", ] [[package]] @@ -683,6 +703,74 @@ dependencies = [ "serde_with", ] +[[package]] +name = "borsh" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115e54d64eb62cdebad391c19efc9dce4981c690c85a33a12199d99bb9546fee" +dependencies = [ + "borsh-derive 0.10.4", + "hashbrown 0.13.2", +] + +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive 1.5.7", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831213f80d9423998dd696e2c5345aba6be7a0bd8cd19e31c5243e13df1cef89" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.95", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote 1.0.37", + "syn 2.0.87", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65d6ba50644c98714aa2a70d13d7df3cd75cd2b523a2b452bf010443800976b3" +dependencies = [ + "proc-macro2", + "quote 1.0.37", + "syn 1.0.95", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276691d96f063427be83e6692b86148e488ebba9f48f77788724ca027ba3b6d4" +dependencies = [ + "proc-macro2", + "quote 1.0.37", + "syn 1.0.95", +] + [[package]] name = "brotli" version = "8.0.2" @@ -1046,6 +1134,7 @@ dependencies = [ "solana-system-transaction", "solana-transaction", "spl-associated-token-account-client", + "spl-token", "spv_validation", "tendermint-rpc", "time", @@ -1186,6 +1275,26 @@ dependencies = [ "crossbeam-utils 0.8.21", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" +dependencies = [ + "log", + "web-sys", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1198,6 +1307,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.4.0" @@ -1355,7 +1470,7 @@ dependencies = [ "crossbeam-utils 0.7.2", "lazy_static", "maybe-uninit", - "memoffset", + "memoffset 0.5.6", "scopeguard", ] @@ -2005,7 +2120,7 @@ dependencies = [ "regex", "serde", "serde_json", - "sha3 0.10.4", + "sha3 0.10.8", "thiserror 1.0.40", "uint", ] @@ -3339,9 +3454,12 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.0" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] [[package]] name = "keccak-hash" @@ -3586,7 +3704,7 @@ dependencies = [ "asn1_der", "bs58 0.5.1", "ed25519-dalek 1.0.1", - "libsecp256k1", + "libsecp256k1 0.7.0", "log", "multihash", "quick-protobuf", @@ -3786,6 +3904,23 @@ dependencies = [ "yamux 0.13.1", ] +[[package]] +name = "libsecp256k1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" +dependencies = [ + "arrayref", + "base64 0.12.3", + "digest 0.9.0", + "libsecp256k1-core 0.2.2", + "libsecp256k1-gen-ecmult 0.2.1", + "libsecp256k1-gen-genmult 0.2.1", + "rand 0.7.3", + "serde", + "sha2 0.9.9", +] + [[package]] name = "libsecp256k1" version = "0.7.0" @@ -3796,15 +3931,26 @@ dependencies = [ "base64 0.13.0", "digest 0.9.0", "hmac-drbg", - "libsecp256k1-core", - "libsecp256k1-gen-ecmult", - "libsecp256k1-gen-genmult", + "libsecp256k1-core 0.3.0", + "libsecp256k1-gen-ecmult 0.3.0", + "libsecp256k1-gen-genmult 0.3.0", "rand 0.8.5", "serde", "sha2 0.9.9", "typenum", ] +[[package]] +name = "libsecp256k1-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + [[package]] name = "libsecp256k1-core" version = "0.3.0" @@ -3816,13 +3962,31 @@ dependencies = [ "subtle", ] +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" +dependencies = [ + "libsecp256k1-core 0.2.2", +] + [[package]] name = "libsecp256k1-gen-ecmult" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" dependencies = [ - "libsecp256k1-core", + "libsecp256k1-core 0.3.0", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" +dependencies = [ + "libsecp256k1-core 0.2.2", ] [[package]] @@ -3831,7 +3995,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" dependencies = [ - "libsecp256k1-core", + "libsecp256k1-core 0.3.0", ] [[package]] @@ -4037,6 +4201,15 @@ dependencies = [ "autocfg 1.1.0", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "memory-db" version = "0.29.0" @@ -4818,11 +4991,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg 1.1.0", "num-integer", "num-traits", "serde", @@ -4847,11 +5019,10 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.43" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg 1.1.0", "num-traits", ] @@ -4887,6 +5058,28 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +dependencies = [ + "proc-macro-crate 1.1.3", + "proc-macro2", + "quote 1.0.37", + "syn 2.0.87", +] + [[package]] name = "object" version = "0.29.0" @@ -4960,7 +5153,7 @@ version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8b44461635bbb1a0300f100a841e571e7d919c81c73075ef5d152ffdb521066" dependencies = [ - "arrayvec 0.7.1", + "arrayvec 0.7.6", "bitvec 1.0.0", "byte-slice-cast", "impl-trait-for-tuples", @@ -4974,7 +5167,7 @@ version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c45ed1f39709f5a89338fab50e59816b2e8815f5bb58276e7ddf9afd495f73f8" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.1.3", "proc-macro2", "quote 1.0.37", "syn 1.0.95", @@ -5269,6 +5462,15 @@ dependencies = [ "uint", ] +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml 0.5.7", +] + [[package]] name = "proc-macro-crate" version = "1.1.3" @@ -5279,6 +5481,15 @@ dependencies = [ "toml 0.5.7", ] +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro-warning" version = "0.4.1" @@ -6428,7 +6639,7 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50e334bb10a245e28e5fd755cabcafd96cfcd167c99ae63a46924ca8d8703a3c" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.1.3", "proc-macro2", "quote 1.0.37", "syn 1.0.95", @@ -6822,9 +7033,9 @@ dependencies = [ [[package]] name = "sha3" -version = "0.10.4" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaedf34ed289ea47c2b741bb72e5357a209512d67bcd4bda44359e5bf0470f56" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ "digest 0.10.7", "keccak", @@ -7058,11 +7269,30 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8f5152a288ef1912300fc6efa6c2d1f9bb55d9398eb6c72326360b8063987da" dependencies = [ + "bincode", + "serde", "solana-program-error", "solana-program-memory", "solana-pubkey", ] +[[package]] +name = "solana-address-lookup-table-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1673f67efe870b64a65cb39e6194be5b26527691ce5922909939961a6e6b395" +dependencies = [ + "bincode", + "bytemuck", + "serde", + "serde_derive", + "solana-clock", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-slot-hashes", +] + [[package]] name = "solana-atomic-u64" version = "2.2.1" @@ -7072,6 +7302,17 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "solana-big-mod-exp" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75db7f2bbac3e62cfd139065d15bcda9e2428883ba61fc8d27ccb251081e7567" +dependencies = [ + "num-bigint", + "num-traits", + "solana-define-syscall", +] + [[package]] name = "solana-bincode" version = "2.2.1" @@ -7083,6 +7324,28 @@ dependencies = [ "solana-instruction", ] +[[package]] +name = "solana-blake3-hasher" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0801e25a1b31a14494fc80882a036be0ffd290efc4c2d640bfcca120a4672" +dependencies = [ + "blake3", + "solana-define-syscall", + "solana-hash", + "solana-sanitize", +] + +[[package]] +name = "solana-borsh" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718333bcd0a1a7aed6655aa66bef8d7fb047944922b2d3a18f49cbc13e73d004" +dependencies = [ + "borsh 0.10.4", + "borsh 1.5.7", +] + [[package]] name = "solana-clock" version = "2.2.2" @@ -7172,16 +7435,44 @@ dependencies = [ "solana-sysvar-id", ] +[[package]] +name = "solana-example-mocks" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84461d56cbb8bb8d539347151e0525b53910102e4bced875d49d5139708e39d3" +dependencies = [ + "serde", + "serde_derive", + "solana-address-lookup-table-interface", + "solana-clock", + "solana-hash", + "solana-instruction", + "solana-keccak-hasher", + "solana-message", + "solana-nonce", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", + "thiserror 2.0.15", +] + [[package]] name = "solana-feature-gate-interface" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43f5c5382b449e8e4e3016fb05e418c53d57782d8b5c30aa372fc265654b956d" dependencies = [ + "bincode", "serde", "serde_derive", + "solana-account", + "solana-account-info", + "solana-instruction", + "solana-program-error", "solana-pubkey", + "solana-rent", "solana-sdk-ids", + "solana-system-interface", ] [[package]] @@ -7201,6 +7492,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b96e9f0300fa287b545613f007dfe20043d7812bee255f418c1eb649c93b63" dependencies = [ + "borsh 1.5.7", "bytemuck", "bytemuck_derive", "five8", @@ -7225,6 +7517,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47298e2ce82876b64f71e9d13a46bc4b9056194e7f9937ad3084385befa50885" dependencies = [ "bincode", + "borsh 1.5.7", "getrandom 0.2.16", "js-sys", "num-traits", @@ -7252,6 +7545,18 @@ dependencies = [ "solana-sysvar-id", ] +[[package]] +name = "solana-keccak-hasher" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7aeb957fbd42a451b99235df4942d96db7ef678e8d5061ef34c9b34cae12f79" +dependencies = [ + "sha3 0.10.8", + "solana-define-syscall", + "solana-hash", + "solana-sanitize", +] + [[package]] name = "solana-keypair" version = "2.2.3" @@ -7281,6 +7586,50 @@ dependencies = [ "solana-sysvar-id", ] +[[package]] +name = "solana-loader-v2-interface" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ab08006dad78ae7cd30df8eea0539e207d08d91eaefb3e1d49a446e1c49654" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solana-loader-v3-interface" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f7162a05b8b0773156b443bccd674ea78bb9aa406325b467ea78c06c99a63a2" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", +] + +[[package]] +name = "solana-loader-v4-interface" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706a777242f1f39a83e2a96a2a6cb034cb41169c6ecbee2cf09cb873d9659e7e" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", +] + [[package]] name = "solana-message" version = "2.4.0" @@ -7288,6 +7637,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1796aabce376ff74bf89b78d268fa5e683d7d7a96a0a4e4813ec34de49d5314b" dependencies = [ "bincode", + "blake3", "lazy_static", "serde", "serde_derive", @@ -7312,6 +7662,106 @@ dependencies = [ "solana-define-syscall", ] +[[package]] +name = "solana-native-token" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61515b880c36974053dd499c0510066783f0cc6ac17def0c7ef2a244874cf4a9" + +[[package]] +name = "solana-nonce" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703e22eb185537e06204a5bd9d509b948f0066f2d1d814a6f475dafb3ddf1325" +dependencies = [ + "serde", + "serde_derive", + "solana-fee-calculator", + "solana-hash", + "solana-pubkey", + "solana-sha256-hasher", +] + +[[package]] +name = "solana-program" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98eca145bd3545e2fbb07166e895370576e47a00a7d824e325390d33bf467210" +dependencies = [ + "bincode", + "blake3", + "borsh 0.10.4", + "borsh 1.5.7", + "bs58 0.5.1", + "bytemuck", + "console_error_panic_hook", + "console_log", + "getrandom 0.2.16", + "lazy_static", + "log", + "memoffset 0.9.1", + "num-bigint", + "num-derive", + "num-traits", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_derive", + "solana-account-info", + "solana-address-lookup-table-interface", + "solana-atomic-u64", + "solana-big-mod-exp", + "solana-bincode", + "solana-blake3-hasher", + "solana-borsh", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-define-syscall", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-example-mocks", + "solana-feature-gate-interface", + "solana-fee-calculator", + "solana-hash", + "solana-instruction", + "solana-instructions-sysvar", + "solana-keccak-hasher", + "solana-last-restart-slot", + "solana-loader-v2-interface", + "solana-loader-v3-interface", + "solana-loader-v4-interface", + "solana-message", + "solana-msg", + "solana-native-token", + "solana-nonce", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-rent", + "solana-sanitize", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-secp256k1-recover", + "solana-serde-varint", + "solana-serialize-utils", + "solana-sha256-hasher", + "solana-short-vec", + "solana-slot-hashes", + "solana-slot-history", + "solana-stable-layout", + "solana-stake-interface", + "solana-system-interface", + "solana-sysvar", + "solana-sysvar-id", + "solana-vote-interface", + "thiserror 2.0.15", + "wasm-bindgen", +] + [[package]] name = "solana-program-entrypoint" version = "2.3.0" @@ -7330,7 +7780,10 @@ version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ee2e0217d642e2ea4bee237f37bd61bb02aec60da3647c48ff88f6556ade775" dependencies = [ + "borsh 1.5.7", "num-traits", + "serde", + "serde_derive", "solana-decode-error", "solana-instruction", "solana-msg", @@ -7346,12 +7799,29 @@ dependencies = [ "solana-define-syscall", ] +[[package]] +name = "solana-program-option" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc677a2e9bc616eda6dbdab834d463372b92848b2bfe4a1ed4e4b4adba3397d0" + +[[package]] +name = "solana-program-pack" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "319f0ef15e6e12dc37c597faccb7d62525a509fec5f6975ecb9419efddeb277b" +dependencies = [ + "solana-program-error", +] + [[package]] name = "solana-pubkey" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b62adb9c3261a052ca1f999398c388f1daf558a1b492f60a6d9e64857db4ff1" dependencies = [ + "borsh 0.10.4", + "borsh 1.5.7", "bytemuck", "bytemuck_derive", "curve25519-dalek 4.1.3", @@ -7507,6 +7977,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "solana-secp256k1-recover" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" +dependencies = [ + "libsecp256k1 0.6.0", + "solana-define-syscall", + "thiserror 2.0.15", +] + [[package]] name = "solana-seed-phrase" version = "2.2.1" @@ -7625,6 +8106,8 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5269e89fde216b4d7e1d1739cf5303f8398a1ff372a81232abbee80e554a838c" dependencies = [ + "borsh 0.10.4", + "borsh 1.5.7", "num-traits", "serde", "serde_derive", @@ -7683,6 +8166,8 @@ checksum = "b8c3595f95069f3d90f275bb9bd235a1973c4d059028b0a7f81baca2703815db" dependencies = [ "base64 0.22.1", "bincode", + "bytemuck", + "bytemuck_derive", "lazy_static", "serde", "serde_derive", @@ -7818,8 +8303,11 @@ version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b80d57478d6599d30acc31cc5ae7f93ec2361a06aefe8ea79bc81739a08af4c3" dependencies = [ + "bincode", "num-derive", "num-traits", + "serde", + "serde_derive", "solana-clock", "solana-decode-error", "solana-hash", @@ -7827,7 +8315,10 @@ dependencies = [ "solana-pubkey", "solana-rent", "solana-sdk-ids", + "solana-serde-varint", "solana-serialize-utils", + "solana-short-vec", + "solana-system-interface", ] [[package]] @@ -7890,7 +8381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ecb916b9664ed9f90abef0ff5a3e61454c1efea5861b2997e03f39b59b955f" dependencies = [ "Inflector", - "proc-macro-crate", + "proc-macro-crate 1.1.3", "proc-macro2", "quote 1.0.37", "syn 1.0.95", @@ -7995,6 +8486,21 @@ dependencies = [ "solana-pubkey", ] +[[package]] +name = "spl-token" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9e171cbcb4b1f72f6d78ed1e975cb467f56825c27d09b8dd2608e4e7fc8b3b" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-program", + "thiserror 1.0.40", +] + [[package]] name = "spv_validation" version = "0.1.0" @@ -9303,7 +9809,7 @@ name = "web3" version = "0.19.0" source = "git+https://github.com/komodoplatform/rust-web3?tag=v0.20.0#01de1d732e61c920cfb2fb1533db7d7110c8a457" dependencies = [ - "arrayvec 0.7.1", + "arrayvec 0.7.6", "base64 0.13.0", "bytes", "derive_more", diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 0c454541d9..baf77fd234 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -20,6 +20,7 @@ enable-solana = [ "dep:solana-system-transaction", "dep:solana-transaction", "dep:spl-associated-token-account-client", + "dep:spl-token", ] default = [] run-docker-tests = [] @@ -139,6 +140,7 @@ solana-signer = { version = "2.2", default-features = false, optional = true } solana-system-transaction = { version = "2.2", default-features = false, optional = true } solana-transaction = { version = "2.2", default-features = false, optional = true } spl-associated-token-account-client = { version = "2.0", default-features = false, optional = true } +spl-token = { version = "4.0", default-features = false, optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] blake2b_simd.workspace = true diff --git a/mm2src/coins/solana/solana_coin.rs b/mm2src/coins/solana/solana_coin.rs index a1f7a28bcd..81e60dd236 100644 --- a/mm2src/coins/solana/solana_coin.rs +++ b/mm2src/coins/solana/solana_coin.rs @@ -67,7 +67,7 @@ pub struct SolanaCoin(Arc); pub struct SolanaCoinFields { ticker: String, pub(crate) address: SolanaAddress, - keypair: Keypair, + pub(crate) keypair: Keypair, pub(crate) abortable_system: AbortableQueue, rpc_clients: AsyncMutex>>, protocol_info: SolanaProtocolInfo, @@ -353,8 +353,8 @@ impl MmCoin for SolanaCoin { internal_id: BytesJson(tx_hash.into_bytes()), kmd_rewards: None, transaction_type: TransactionType::StandardTransfer, - // TODO: add memo instruction to the TX. - memo: req.memo, + // TODO: Add memo instruction to the TX. + memo: None, }) }; diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index 95a3d69778..67ad59aac3 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -8,19 +8,24 @@ use std::sync::Arc; use async_trait::async_trait; use common::executor::abortable_queue::{AbortableQueue, WeakSpawner}; use common::executor::{AbortableSystem, AbortedError}; +use common::Future01CompatExt; use derive_more::Display; use futures::{FutureExt, TryFutureExt}; use futures01::Future; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::{BigDecimal, MmNumber}; +use num_traits::ToPrimitive; +use num_traits::Zero; use rpc::v1::types::{Bytes as RpcBytes, H264 as RpcH264}; use serde::Deserialize; use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; use crate::hd_wallet::HDAddressSelector; +use crate::solana::SolanaFeeDetails; use crate::{ - solana::SolanaCoin, BalanceFut, CoinBalance, RawTransactionFut, RawTransactionRequest, WithdrawFut, WithdrawRequest, + solana::SolanaCoin, BalanceFut, CoinBalance, RawTransactionFut, RawTransactionRequest, TxFeeDetails, WithdrawFut, + WithdrawRequest, }; use crate::{ CheckIfMyPaymentSentArgs, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, HistorySyncState, @@ -28,12 +33,17 @@ use crate::{ SearchForSwapTxSpendInput, SendPaymentArgs, SignRawTransactionRequest, SignatureResult, SpendPaymentArgs, SwapOps, TradeFee, TradePreimageFut, TradePreimageResult, TradePreimageValue, TransactionEnum, TransactionResult, TxMarshalingErr, UnexpectedDerivationMethod, ValidateAddressResult, ValidateFeeArgs, ValidateOtherPubKeyErr, - ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, + ValidatePaymentInput, VerificationResult, WaitForHTLCTxSpendArgs, WatcherOps, WithdrawError, }; use solana_pubkey::Pubkey as SolanaAddress; +use solana_transaction::Transaction; +use spl_associated_token_account_client::address::get_associated_token_address; +use spl_associated_token_account_client::instruction::create_associated_token_account; +use spl_token as spl_token_program; pub struct SolanaTokenFields { pub ticker: String, + address: SolanaAddress, pub platform_coin: SolanaCoin, pub protocol_info: SolanaTokenProtocolInfo, abortable_system: AbortableQueue, @@ -87,6 +97,8 @@ pub enum SolanaTokenInitErrorKind { Internal { reason: String, }, + #[display(fmt = "None of the RPC servers are healthy.")] + UnhealthyRPCs, #[display( fmt = "Expected platform coin is '{expected_platform_coin}' but requested one is '{actual_platform_coin}'." )] @@ -97,7 +109,7 @@ pub enum SolanaTokenInitErrorKind { } impl SolanaToken { - pub fn init( + pub async fn init( ticker: String, platform_coin: SolanaCoin, protocol_info: SolanaTokenProtocolInfo, @@ -110,8 +122,39 @@ impl SolanaToken { kind: SolanaTokenInitErrorKind::Internal { reason: e.to_string() }, })?; + let address = spl_associated_token_account_client::address::get_associated_token_address( + &platform_coin.address, + &protocol_info.mint_address, + ); + + let rpc = platform_coin.rpc_client().await.map_err(|e| SolanaTokenInitError { + ticker: ticker.clone(), + kind: SolanaTokenInitErrorKind::UnhealthyRPCs, + })?; + + let mint_account = rpc + .get_account(&protocol_info.mint_address) + .map_err(|e| SolanaTokenInitError { + ticker: ticker.clone(), + kind: SolanaTokenInitErrorKind::QueryError { reason: e.to_string() }, + })?; + + if mint_account.owner != spl_token_program::id() { + return MmError::err(SolanaTokenInitError { + ticker: ticker.clone(), + kind: SolanaTokenInitErrorKind::QueryError { + reason: format!( + "Unsupported SPL program. Expected Program ID: '{}', Got: '{}'.", + spl_token_program::id(), + mint_account.owner + ), + }, + }); + } + let token_fields = SolanaTokenFields { ticker, + address, platform_coin, protocol_info, abortable_system, @@ -132,11 +175,145 @@ impl MmCoin for SolanaToken { } fn spawner(&self) -> WeakSpawner { - todo!() + self.abortable_system.weak_spawner() } fn withdraw(&self, req: WithdrawRequest) -> WithdrawFut { - todo!() + let token = self.clone(); + let coin = self.platform_coin.clone(); + + let fut = async move { + let rpc = coin + .rpc_client() + .await + .map_err(|e| WithdrawError::Transport(e.into_inner()))?; + + // `to` can be either a Solana address, or a token address. We create + // `to_token_account` regardless to support the both cases. + let to = SolanaAddress::from_str(&req.to).map_err(|e| WithdrawError::InvalidAddress(e.to_string()))?; + let to_token_account = get_associated_token_address(&to, &token.protocol_info.mint_address); + + let amount_u64 = if req.max { + let balance = token + .my_balance() + .compat() + .await + .map_err(|e| WithdrawError::Transport(e.to_string()))?; + + balance.spendable.to_u64().ok_or_else(|| { + MmError::new(WithdrawError::InternalError(format!( + "Couldn't convert {} to u64.", + balance.spendable + ))) + })? + } else { + let scale = BigDecimal::from(10u64.pow(token.protocol_info.decimals as u32)); + let big_decimal = &req.amount * &scale; + + big_decimal.to_u64().ok_or_else(|| { + MmError::new(WithdrawError::InternalError(format!( + "Couldn't convert {big_decimal} to u64." + ))) + })? + }; + + if amount_u64 == 0 { + return MmError::err(WithdrawError::AmountTooLow { + amount: req.amount, + threshold: token.min_tx_amount(), + }); + } + + // Instructions: + // - Create recipient address if missing. + // - Transfer. + let mut instructions = Vec::new(); + + if let Err(e) = rpc.get_account(&to_token_account) { + // TODO: This might be a different kind of error. Check it more + // precisely (like how we do it in `solana_coin`) before sending + // the create instruction. + + instructions.push(create_associated_token_account( + &coin.address, + &to, + &token.protocol_info.mint_address, + &spl_token_program::id(), + )); + }; + + let transfer_ix = spl_token_program::instruction::transfer_checked( + &spl_token_program::id(), + &coin.address, + &token.protocol_info.mint_address, + &to_token_account, + &coin.address, + &[], + amount_u64, + token.protocol_info.decimals, + ) + .map_err(|e| WithdrawError::InternalError(e.to_string()))?; + instructions.push(transfer_ix); + + let recent_blockhash = rpc + .get_latest_blockhash() + .map_err(|e| WithdrawError::Transport(e.to_string()))?; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&coin.address), + &[&coin.keypair], + recent_blockhash, + ); + + // TX hash is the first signature (base58 String). + let tx_hash = tx + .signatures + .first() + .map(|s| s.to_string()) + .ok_or_else(|| WithdrawError::InternalError("Couldn't find the TX signature.".to_owned()))?; + + let tx_bytes = + bincode::serialize(&tx).map_err(|e| MmError::new(WithdrawError::InternalError(e.to_string())))?; + + let tx_data = crate::TransactionData::new_signed(rpc::v1::types::Bytes(tx_bytes), tx_hash.clone()); + + let amount_dec = + BigDecimal::from(amount_u64) / BigDecimal::from(10u64.pow(token.protocol_info.decimals as u32)); + + let fee_lamports = rpc + .get_fee_for_message(tx.message()) + .map_err(|e| WithdrawError::Transport(e.to_string()))?; + let fee_dec = BigDecimal::from(fee_lamports) + / BigDecimal::from(10u64.pow(super::solana_coin::SOLANA_DECIMALS as u32)); + + let received_by_me = if to == coin.address { + amount_dec.clone() + } else { + BigDecimal::zero() + }; + + Ok(crate::TransactionDetails { + tx: tx_data, + from: vec![coin.address.to_string()], + to: vec![to.to_string()], + total_amount: amount_dec.clone(), + spent_by_me: amount_dec.clone(), + received_by_me, + my_balance_change: -amount_dec, + block_height: 0, + timestamp: 0, + fee_details: Some(TxFeeDetails::Solana(SolanaFeeDetails { amount: fee_dec })), + coin: req.coin, + internal_id: rpc::v1::types::Bytes(tx_hash.into_bytes()), + kmd_rewards: None, + transaction_type: crate::TransactionType::StandardTransfer, + // TODO: Add memo instruction to the TX. + memo: None, + }) + }; + + Box::new(fut.boxed().compat()) } fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut<'_> { @@ -249,7 +426,7 @@ impl MarketCoinOps for SolanaToken { } fn my_address(&self) -> MmResult { - self.platform_coin.my_address() + Ok(self.address.to_string()) } fn address_from_pubkey(&self, pubkey: &RpcH264) -> MmResult { @@ -290,11 +467,11 @@ impl MarketCoinOps for SolanaToken { } fn send_raw_tx(&self, tx: &str) -> Box + Send> { - todo!() + self.platform_coin.send_raw_tx(tx) } fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box + Send> { - todo!() + self.platform_coin.send_raw_tx_bytes(tx) } #[inline(always)] diff --git a/mm2src/coins_activation/src/solana_token_activation.rs b/mm2src/coins_activation/src/solana_token_activation.rs index 73665ad98e..4f74304fd6 100644 --- a/mm2src/coins_activation/src/solana_token_activation.rs +++ b/mm2src/coins_activation/src/solana_token_activation.rs @@ -52,6 +52,9 @@ impl From for EnableTokenError { SolanaTokenInitErrorKind::QueryError { reason } => EnableTokenError::Transport(reason.to_string()), SolanaTokenInitErrorKind::Internal { reason } => EnableTokenError::Internal(reason.to_string()), SolanaTokenInitErrorKind::PlatformCoinMismatch { .. } => EnableTokenError::PlatformCoinMismatch, + SolanaTokenInitErrorKind::UnhealthyRPCs => { + EnableTokenError::Transport("None of the RPC servers are healthy.".to_owned()) + }, } } } @@ -62,6 +65,9 @@ impl From for InitTokensAsMmCoinsError { SolanaTokenInitErrorKind::QueryError { reason } => InitTokensAsMmCoinsError::Transport(reason.to_string()), SolanaTokenInitErrorKind::Internal { reason } => InitTokensAsMmCoinsError::Internal(reason.to_string()), SolanaTokenInitErrorKind::PlatformCoinMismatch { .. } => InitTokensAsMmCoinsError::PlatformCoinMismatch, + SolanaTokenInitErrorKind::UnhealthyRPCs => { + InitTokensAsMmCoinsError::Transport("None of the RPC servers are healthy.".to_owned()) + }, } } } @@ -81,7 +87,7 @@ impl TokenActivationOps for SolanaToken { protocol_conf: Self::ProtocolInfo, _is_custom: bool, ) -> Result<(Self, Self::ActivationResult), MmError> { - let token = SolanaToken::init(ticker.clone(), platform_coin, protocol_conf)?; + let token = SolanaToken::init(ticker.clone(), platform_coin, protocol_conf).await?; let address = token.my_address().map_err(|e| SolanaTokenInitError { ticker: ticker.clone(), diff --git a/mm2src/coins_activation/src/solana_with_assets.rs b/mm2src/coins_activation/src/solana_with_assets.rs index d81e703606..c2d1cd12ca 100644 --- a/mm2src/coins_activation/src/solana_with_assets.rs +++ b/mm2src/coins_activation/src/solana_with_assets.rs @@ -12,7 +12,7 @@ use coins::{ CoinBalance, CoinProtocol, MarketCoinOps, MmCoinEnum, PrivKeyBuildPolicy, }; use common::Future01CompatExt; -use futures::future::join_all; +use futures::future::{join_all, try_join_all}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; @@ -251,10 +251,12 @@ impl TokenInitializer for SolanaCoin { &self, params: Vec>, ) -> Result, MmError> { - params - .into_iter() - .map(|param| SolanaToken::init(param.ticker, self.platform_coin().clone(), param.protocol.clone())) - .collect() + try_join_all( + params + .into_iter() + .map(|param| SolanaToken::init(param.ticker, self.platform_coin().clone(), param.protocol.clone())), + ) + .await } fn platform_coin(&self) -> &::PlatformCoin { From e91089ea776a29a9ca0297ff59cb1e1ab26bbcbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Wed, 17 Sep 2025 13:09:00 +0300 Subject: [PATCH 06/27] disable enable-solana feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/mm2_main/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 1abe429b0d..0a4994305d 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -18,7 +18,7 @@ native = [] # Deprecated track-ctx-pointer = ["common/track-ctx-pointer"] zhtlc-native-tests = ["coins/zhtlc-native-tests"] run-docker-tests = ["for-tests", "coins/run-docker-tests"] -default = [ "enable-solana" ] +default = [] trezor-udp = ["crypto/trezor-udp"] # use for tests to connect to trezor emulator over udp run-device-tests = [] enable-sia = ["coins/enable-sia", "coins_activation/enable-sia"] From 076ca39035353d0d34bead54d55e7fcfafa8912d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 18 Sep 2025 08:14:29 +0300 Subject: [PATCH 07/27] handle fee on received_by_me MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_coin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/coins/solana/solana_coin.rs b/mm2src/coins/solana/solana_coin.rs index 81e60dd236..f57f845e46 100644 --- a/mm2src/coins/solana/solana_coin.rs +++ b/mm2src/coins/solana/solana_coin.rs @@ -333,7 +333,7 @@ impl MmCoin for SolanaCoin { let fee = BigDecimal::from(fee) / BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32)); let received_by_me = if to == coin.address { - amount_dec.clone() + &amount_dec - &fee } else { BigDecimal::zero() }; From 4800d4e5a46299d83c5fb1dacb8c757069685fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 18 Sep 2025 08:14:43 +0300 Subject: [PATCH 08/27] disable token check temporarily MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_token.rs | 50 +++++++++++++++-------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index 67ad59aac3..b6129344c1 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -127,30 +127,32 @@ impl SolanaToken { &protocol_info.mint_address, ); - let rpc = platform_coin.rpc_client().await.map_err(|e| SolanaTokenInitError { - ticker: ticker.clone(), - kind: SolanaTokenInitErrorKind::UnhealthyRPCs, - })?; - - let mint_account = rpc - .get_account(&protocol_info.mint_address) - .map_err(|e| SolanaTokenInitError { - ticker: ticker.clone(), - kind: SolanaTokenInitErrorKind::QueryError { reason: e.to_string() }, - })?; - - if mint_account.owner != spl_token_program::id() { - return MmError::err(SolanaTokenInitError { - ticker: ticker.clone(), - kind: SolanaTokenInitErrorKind::QueryError { - reason: format!( - "Unsupported SPL program. Expected Program ID: '{}', Got: '{}'.", - spl_token_program::id(), - mint_account.owner - ), - }, - }); - } + // TODO: Handle non-existent mint accounts, then uncomment this code. + // + // let rpc = platform_coin.rpc_client().await.map_err(|e| SolanaTokenInitError { + // ticker: ticker.clone(), + // kind: SolanaTokenInitErrorKind::UnhealthyRPCs, + // })?; + + // let mint_account = rpc + // .get_account(&protocol_info.mint_address) + // .map_err(|e| SolanaTokenInitError { + // ticker: ticker.clone(), + // kind: SolanaTokenInitErrorKind::QueryError { reason: e.to_string() }, + // })?; + + // if mint_account.owner != spl_token_program::id() { + // return MmError::err(SolanaTokenInitError { + // ticker: ticker.clone(), + // kind: SolanaTokenInitErrorKind::QueryError { + // reason: format!( + // "Unsupported SPL program. Expected Program ID: '{}', Got: '{}'.", + // spl_token_program::id(), + // mint_account.owner + // ), + // }, + // }); + // } let token_fields = SolanaTokenFields { ticker, From 3c7603ab3aa2e88649c60d3a9cdada4afb933ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 18 Sep 2025 08:15:17 +0300 Subject: [PATCH 09/27] rename test module for tendermint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/tendermint/tendermint_coin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 702b028a43..c90af94aac 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -4376,7 +4376,7 @@ pub(crate) fn tendermint_tx_internal_id(bytes: &[u8], token_id: Option Date: Thu, 18 Sep 2025 08:15:32 +0300 Subject: [PATCH 10/27] add coverage on sol activation, withdraw and send_raw_transaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/mm2_main/tests/mm2_tests/mod.rs | 1 + .../mm2_main/tests/mm2_tests/solana_tests.rs | 105 ++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 mm2src/mm2_main/tests/mm2_tests/solana_tests.rs diff --git a/mm2src/mm2_main/tests/mm2_tests/mod.rs b/mm2src/mm2_main/tests/mm2_tests/mod.rs index 793c430166..648b15900b 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mod.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mod.rs @@ -5,6 +5,7 @@ mod lightning_tests; mod lp_bot_tests; mod mm2_tests_inner; mod orderbook_sync_tests; +mod solana_tests; mod z_coin_tests; #[cfg(all(feature = "zhtlc-native-tests", not(target_arch = "wasm32")))] diff --git a/mm2src/mm2_main/tests/mm2_tests/solana_tests.rs b/mm2src/mm2_main/tests/mm2_tests/solana_tests.rs new file mode 100644 index 0000000000..ea3ce06115 --- /dev/null +++ b/mm2src/mm2_main/tests/mm2_tests/solana_tests.rs @@ -0,0 +1,105 @@ +use common::{block_on, log}; +use mm2_number::BigDecimal; +use mm2_test_helpers::for_tests::Mm2TestConf; +use mm2_test_helpers::for_tests::{send_raw_transaction, withdraw_v1, MarketMakerIt}; + +use serde_json::json; + +const SOLANA_DEVNET_RPC_URL: &str = "https://api.devnet.solana.com"; +// TODO: Use different seed here. +const SOLANA_DEVNET_TEST_SEED: &str = "iris test seed"; + +fn solana_coin_config() -> serde_json::Value { + json!({ + "coin": "SOL-DEV", + "name": "solana", + "fname": "Solana", + "required_confirmations": 2, + "avg_blocktime": 3, + "protocol": { + "type": "SOLANA", + "protocol_data": {} + }, + "derivation_path": "m/44'/501'" + }) +} + +fn usdc_token_config() -> serde_json::Value { + json!({ + "coin": "USDC-SOL-DEV", + "name": "usd-coin-devnet", + "fname": "USD Coin (Devnet)", + "required_confirmations": 2, + "avg_blocktime": 3, + "protocol": { + "type": "SOLANATOKEN", + "protocol_data": { + "platform": "SOL-DEV", + "decimals": 6, + "mint_address": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" + } + }, + "derivation_path": "m/44'/501'" + }) +} + +pub async fn enable_solana(mm: &MarketMakerIt, coin: &str, tokens: &[&str], rpc_urls: &[&str]) -> serde_json::Value { + let tokens: Vec<_> = tokens.iter().map(|ticker| json!({ "ticker": ticker })).collect(); + let nodes: Vec<_> = rpc_urls.iter().map(|u| json!({ "url": u })).collect(); + let method = "experimental::enable_solana_with_assets"; + + let request = json!({ + "userpass": mm.userpass, + "method": method, + "mmrpc": "2.0", + "params": { + "ticker": coin, + "tokens_params": tokens, + "nodes": nodes + } + }); + log!("{method} request {}", serde_json::to_string(&request).unwrap()); + + let request = mm.rpc(&request).await.unwrap(); + assert_eq!(request.0, http::StatusCode::OK, "'{method}' failed: {}", request.1); + log!("{method} response {}", request.1); + serde_json::from_str(&request.1).unwrap() +} + +#[test] +fn enable_with_tokens_and_withdraw() { + const MY_ADDRESS: &str = "5dbw8U6zrLFwtYQgy3gxvMFe6PzWRs8yXB7bXShCnbfT"; + + let coins = json!([solana_coin_config(), usdc_token_config()]); + let coin = coins[0]["coin"].as_str().unwrap(); + let usdc_token = coins[1]["coin"].as_str().unwrap(); + + let conf = Mm2TestConf::seednode(SOLANA_DEVNET_TEST_SEED, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let activation_res = block_on(enable_solana(&mm, coin, &[usdc_token], &[SOLANA_DEVNET_RPC_URL])); + log!("Activation {}", serde_json::to_string(&activation_res).unwrap()); + + let to_address = "devwuNsNYACyiEYxRNqMNseBpNnGfnd4ZwNHL7sphqv"; + // Just call withdraw without sending to check response correctness. + let tx_details = block_on(withdraw_v1(&mm, coin, to_address, "0.1", None)); + log!("Withdraw to other {}", serde_json::to_string(&tx_details).unwrap()); + assert_eq!(tx_details.received_by_me, BigDecimal::default()); + assert_eq!(tx_details.to, vec![to_address.to_owned()]); + assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); + + // Withdraw and send transaction to ourselves. + let tx_details = block_on(withdraw_v1(&mm, coin, MY_ADDRESS, "0.1", None)); + let fee: BigDecimal = tx_details.fee_details["amount"].as_str().unwrap().parse().unwrap(); + log!("Withdraw to self {}", serde_json::to_string(&tx_details).unwrap()); + + let expected_received: BigDecimal = "0.1".parse().unwrap(); + // We sent it to ourselves, so the fee value should be extracted. + assert_eq!(tx_details.received_by_me, expected_received - fee); + + assert_eq!(tx_details.to, vec![MY_ADDRESS.to_owned()]); + assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); + + let send_raw_tx = block_on(send_raw_transaction(&mm, coin, &tx_details.tx_hex)); + log!("Send raw tx {}", serde_json::to_string(&send_raw_tx).unwrap()); +} From 853db9169771aaa0f612ab821c494b3c1b7a2184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 18 Sep 2025 08:33:34 +0300 Subject: [PATCH 11/27] feature gate solana_tests module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/mm2_main/tests/mm2_tests/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mm2src/mm2_main/tests/mm2_tests/mod.rs b/mm2src/mm2_main/tests/mm2_tests/mod.rs index 648b15900b..8ba3594263 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mod.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mod.rs @@ -5,9 +5,11 @@ mod lightning_tests; mod lp_bot_tests; mod mm2_tests_inner; mod orderbook_sync_tests; -mod solana_tests; mod z_coin_tests; +#[cfg(feature = "enable-solana")] +mod solana_tests; + #[cfg(all(feature = "zhtlc-native-tests", not(target_arch = "wasm32")))] use mm2_test_helpers::for_tests::MarketMakerIt; #[cfg(all(feature = "zhtlc-native-tests", not(target_arch = "wasm32")))] From 6748484155c129753dca45f72e1c85aa1fc2f617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 18 Sep 2025 08:37:17 +0300 Subject: [PATCH 12/27] add another TODO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_token.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index b6129344c1..0af97188d0 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -190,6 +190,9 @@ impl MmCoin for SolanaToken { .await .map_err(|e| WithdrawError::Transport(e.into_inner()))?; + // TODO: If platform_coin balance is zero, they can't afford the + // fee, so do early-return here. + // `to` can be either a Solana address, or a token address. We create // `to_token_account` regardless to support the both cases. let to = SolanaAddress::from_str(&req.to).map_err(|e| WithdrawError::InvalidAddress(e.to_string()))?; From cc75fad491b4ffff7f76dceb78b1af4aea90fd20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Tue, 7 Oct 2025 08:35:22 +0300 Subject: [PATCH 13/27] extend calculate_withdraw_amount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_coin.rs | 82 ++++++++++++++++++++--------- mm2src/coins/solana/solana_token.rs | 7 ++- 2 files changed, 59 insertions(+), 30 deletions(-) diff --git a/mm2src/coins/solana/solana_coin.rs b/mm2src/coins/solana/solana_coin.rs index f57f845e46..0c6dd1ebbb 100644 --- a/mm2src/coins/solana/solana_coin.rs +++ b/mm2src/coins/solana/solana_coin.rs @@ -243,34 +243,61 @@ impl SolanaCoin { .await .map_err(|e| WithdrawError::Transport(e.into_inner()))?; - if req.max { - let balance = rpc - .get_balance(&self.address) - .map_err(|e| WithdrawError::Transport(e.to_string()))?; + let recent_blockhash = rpc + .get_latest_blockhash() + .map_err(|e| WithdrawError::Transport(e.to_string()))?; - let recent_blockhash = rpc - .get_latest_blockhash() - .map_err(|e| WithdrawError::Transport(e.to_string()))?; + // Dummy TX to estimate the fee. + let tx = solana_system_transaction::transfer(&self.keypair, &self.address, 0, recent_blockhash); + let fee_u64 = rpc + .get_fee_for_message(tx.message()) + .map_err(|e| WithdrawError::Transport(e.to_string()))?; - // Dummy TX to estimate the fee. - let tx = solana_system_transaction::transfer(&self.keypair, &self.address, balance, recent_blockhash); + let balance_u64 = rpc + .get_balance(&self.address) + .map_err(|e| WithdrawError::Transport(e.to_string()))?; - let fee = rpc - .get_fee_for_message(tx.message()) - .map_err(|e| WithdrawError::Transport(e.to_string()))?; + if req.max { + let amount = balance_u64.saturating_sub(fee_u64); + let amount_big_decimal = lamports_to_big_decimal(amount, SOLANA_DECIMALS); + + // Amount must be bigger than min_tx_amount. + if amount_big_decimal < self.min_tx_amount() { + return MmError::err(WithdrawError::AmountTooLow { + amount: amount_big_decimal, + threshold: self.min_tx_amount(), + }); + } - Ok(balance.saturating_sub(fee)) - } else { - let big_decimal = &req.amount * &BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32)); + return Ok(balance_u64.saturating_sub(fee_u64)); + } - // TODO: Check if user can afford the fee. + let requested_amount = &req.amount * &BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32)); - big_decimal.to_u64().ok_or_else(|| { - MmError::new(WithdrawError::InternalError(format!( - "Couldn't convert {big_decimal} to u64." - ))) - }) + // Amount must be bigger than min_tx_amount. + if requested_amount < self.min_tx_amount() { + return MmError::err(WithdrawError::AmountTooLow { + amount: requested_amount, + threshold: self.min_tx_amount(), + }); } + + let requested_amount_u64 = requested_amount.to_u64().ok_or_else(|| { + MmError::new(WithdrawError::InternalError(format!( + "Couldn't convert {requested_amount} to u64." + ))) + })?; + + // User must have enough balance to cover both the send and fee amounts. + if requested_amount_u64 + fee_u64 > balance_u64 { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: self.ticker.to_owned(), + available: lamports_to_big_decimal(balance_u64, SOLANA_DECIMALS), + required: lamports_to_big_decimal(requested_amount_u64 + fee_u64, SOLANA_DECIMALS), + }); + }; + + Ok(requested_amount_u64) } } @@ -325,12 +352,12 @@ impl MmCoin for SolanaCoin { let tx_data = TransactionData::new_signed(BytesJson(tx_bytes), tx_hash.clone()); - let amount_dec = BigDecimal::from(lamports) / BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32)); + let amount_dec = lamports_to_big_decimal(lamports, SOLANA_DECIMALS); let fee = rpc .get_fee_for_message(tx.message()) .map_err(|e| WithdrawError::Transport(e.to_string()))?; - let fee = BigDecimal::from(fee) / BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32)); + let fee = lamports_to_big_decimal(fee, SOLANA_DECIMALS); let received_by_me = if to == coin.address { &amount_dec - &fee @@ -507,8 +534,7 @@ impl MarketCoinOps for SolanaCoin { .get_balance(&coin.address) .map_err(|e| BalanceError::Transport(e.to_string()))?; - let scale = BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32)); - let balance_decimal = BigDecimal::from(balance_u64) / scale; + let balance_decimal = lamports_to_big_decimal(balance_u64, SOLANA_DECIMALS); Ok(CoinBalance { spendable: balance_decimal, @@ -582,7 +608,7 @@ impl MarketCoinOps for SolanaCoin { #[inline] fn min_tx_amount(&self) -> BigDecimal { - BigDecimal::from(1) / BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32)) + lamports_to_big_decimal(1, SOLANA_DECIMALS) } #[inline] @@ -702,3 +728,7 @@ impl SwapOps for SolanaCoin { #[async_trait] impl WatcherOps for SolanaCoin {} + +pub(crate) fn lamports_to_big_decimal>(lamports: u64, decimals: T) -> BigDecimal { + BigDecimal::from(lamports) / BigDecimal::from(10u64.pow(decimals.into())) +} diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index 0af97188d0..a319e2ac2e 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -22,6 +22,7 @@ use serde::Deserialize; use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; use crate::hd_wallet::HDAddressSelector; +use crate::solana::solana_coin::lamports_to_big_decimal; use crate::solana::SolanaFeeDetails; use crate::{ solana::SolanaCoin, BalanceFut, CoinBalance, RawTransactionFut, RawTransactionRequest, TxFeeDetails, WithdrawFut, @@ -283,14 +284,12 @@ impl MmCoin for SolanaToken { let tx_data = crate::TransactionData::new_signed(rpc::v1::types::Bytes(tx_bytes), tx_hash.clone()); - let amount_dec = - BigDecimal::from(amount_u64) / BigDecimal::from(10u64.pow(token.protocol_info.decimals as u32)); + let amount_dec = lamports_to_big_decimal(amount_u64, token.protocol_info.decimals); let fee_lamports = rpc .get_fee_for_message(tx.message()) .map_err(|e| WithdrawError::Transport(e.to_string()))?; - let fee_dec = BigDecimal::from(fee_lamports) - / BigDecimal::from(10u64.pow(super::solana_coin::SOLANA_DECIMALS as u32)); + let fee_dec = lamports_to_big_decimal(fee_lamports, super::solana_coin::SOLANA_DECIMALS); let received_by_me = if to == coin.address { amount_dec.clone() From 7094fcba1bab9ab29d6b5344b48e61576efe79c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Tue, 7 Oct 2025 08:46:54 +0300 Subject: [PATCH 14/27] resolve solana_token todo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_token.rs | 39 ++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index a319e2ac2e..4be6aef072 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -191,21 +191,18 @@ impl MmCoin for SolanaToken { .await .map_err(|e| WithdrawError::Transport(e.into_inner()))?; - // TODO: If platform_coin balance is zero, they can't afford the - // fee, so do early-return here. - // `to` can be either a Solana address, or a token address. We create // `to_token_account` regardless to support the both cases. let to = SolanaAddress::from_str(&req.to).map_err(|e| WithdrawError::InvalidAddress(e.to_string()))?; let to_token_account = get_associated_token_address(&to, &token.protocol_info.mint_address); - let amount_u64 = if req.max { - let balance = token - .my_balance() - .compat() - .await - .map_err(|e| WithdrawError::Transport(e.to_string()))?; + let balance = token + .my_balance() + .compat() + .await + .map_err(|e| WithdrawError::Transport(e.to_string()))?; + let amount_u64 = if req.max { balance.spendable.to_u64().ok_or_else(|| { MmError::new(WithdrawError::InternalError(format!( "Couldn't convert {} to u64.", @@ -230,6 +227,15 @@ impl MmCoin for SolanaToken { }); } + let amount_decimal = lamports_to_big_decimal(amount_u64, token.protocol_info.decimals); + if balance.spendable < amount_decimal { + return MmError::err(WithdrawError::NotSufficientBalance { + coin: token.ticker.to_owned(), + available: balance.spendable, + required: amount_decimal, + }); + } + // Instructions: // - Create recipient address if missing. // - Transfer. @@ -291,6 +297,21 @@ impl MmCoin for SolanaToken { .map_err(|e| WithdrawError::Transport(e.to_string()))?; let fee_dec = lamports_to_big_decimal(fee_lamports, super::solana_coin::SOLANA_DECIMALS); + let platform_coin_balance = coin + .my_balance() + .compat() + .await + .map_err(|e| WithdrawError::Transport(e.to_string()))? + .spendable; + + if fee_dec > platform_coin_balance { + return MmError::err(WithdrawError::NotSufficientPlatformBalanceForFee { + available: platform_coin_balance, + required: fee_dec, + coin: coin.ticker().to_owned(), + }); + } + let received_by_me = if to == coin.address { amount_dec.clone() } else { From 903336d1c91d2ff0e5cf7ed7a6a95c7a768c531e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Tue, 7 Oct 2025 09:11:25 +0300 Subject: [PATCH 15/27] handle non-existent token accounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_token.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index 4be6aef072..6469c85575 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -242,9 +242,9 @@ impl MmCoin for SolanaToken { let mut instructions = Vec::new(); if let Err(e) = rpc.get_account(&to_token_account) { - // TODO: This might be a different kind of error. Check it more - // precisely (like how we do it in `solana_coin`) before sending - // the create instruction. + if !e.kind.to_string().contains("AccountNotFound") { + return MmError::err(WithdrawError::Transport(e.to_string())); + } instructions.push(create_associated_token_account( &coin.address, From 82d7a3a2cd7b73777f75ff5d13dcd818096e2c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Tue, 7 Oct 2025 11:39:08 +0300 Subject: [PATCH 16/27] handle token programs on token init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_token.rs | 56 +++++++++++++++-------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index 6469c85575..190bc04338 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -128,32 +128,36 @@ impl SolanaToken { &protocol_info.mint_address, ); - // TODO: Handle non-existent mint accounts, then uncomment this code. - // - // let rpc = platform_coin.rpc_client().await.map_err(|e| SolanaTokenInitError { - // ticker: ticker.clone(), - // kind: SolanaTokenInitErrorKind::UnhealthyRPCs, - // })?; - - // let mint_account = rpc - // .get_account(&protocol_info.mint_address) - // .map_err(|e| SolanaTokenInitError { - // ticker: ticker.clone(), - // kind: SolanaTokenInitErrorKind::QueryError { reason: e.to_string() }, - // })?; - - // if mint_account.owner != spl_token_program::id() { - // return MmError::err(SolanaTokenInitError { - // ticker: ticker.clone(), - // kind: SolanaTokenInitErrorKind::QueryError { - // reason: format!( - // "Unsupported SPL program. Expected Program ID: '{}', Got: '{}'.", - // spl_token_program::id(), - // mint_account.owner - // ), - // }, - // }); - // } + let rpc = platform_coin.rpc_client().await.map_err(|e| SolanaTokenInitError { + ticker: ticker.clone(), + kind: SolanaTokenInitErrorKind::UnhealthyRPCs, + })?; + + match rpc.get_account(&protocol_info.mint_address) { + Ok(mint_account) => { + if mint_account.owner != spl_token_program::id() { + return MmError::err(SolanaTokenInitError { + ticker: ticker.clone(), + kind: SolanaTokenInitErrorKind::QueryError { + reason: format!( + "Unsupported SPL program. Expected Program ID: '{}', Got: '{}'.", + spl_token_program::id(), + mint_account.owner + ), + }, + }); + } + }, + Err(e) if e.kind.to_string().contains("AccountNotFound") => { + // Nothing to do here. + }, + Err(e) => { + return MmError::err(SolanaTokenInitError { + ticker: ticker.clone(), + kind: SolanaTokenInitErrorKind::QueryError { reason: e.to_string() }, + }) + }, + }; let token_fields = SolanaTokenFields { ticker, From 974ac57d02da1e2287fe34d0ea88fbe1fe8c7b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 9 Oct 2025 08:25:20 +0300 Subject: [PATCH 17/27] bump spl-token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- Cargo.lock | 484 +++------------------------------------- mm2src/coins/Cargo.toml | 2 +- 2 files changed, 28 insertions(+), 458 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9f91996b0..6c9fa26c01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,12 +405,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - [[package]] name = "base64" version = "0.13.0" @@ -604,7 +598,7 @@ checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" dependencies = [ "arrayref", "arrayvec 0.5.1", - "constant_time_eq 0.1.5", + "constant_time_eq", ] [[package]] @@ -615,21 +609,7 @@ checksum = "ab9e07352b829279624ceb7c64adb4f585dacdb81d35cafae81139ccd617cf44" dependencies = [ "arrayref", "arrayvec 0.5.1", - "constant_time_eq 0.1.5", -] - -[[package]] -name = "blake3" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" -dependencies = [ - "arrayref", - "arrayvec 0.7.6", - "cc", - "cfg-if 1.0.0", - "constant_time_eq 0.3.1", - "digest 0.10.7", + "constant_time_eq", ] [[package]] @@ -703,74 +683,6 @@ dependencies = [ "serde_with", ] -[[package]] -name = "borsh" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115e54d64eb62cdebad391c19efc9dce4981c690c85a33a12199d99bb9546fee" -dependencies = [ - "borsh-derive 0.10.4", - "hashbrown 0.13.2", -] - -[[package]] -name = "borsh" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" -dependencies = [ - "borsh-derive 1.5.7", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831213f80d9423998dd696e2c5345aba6be7a0bd8cd19e31c5243e13df1cef89" -dependencies = [ - "borsh-derive-internal", - "borsh-schema-derive-internal", - "proc-macro-crate 0.1.5", - "proc-macro2", - "syn 1.0.95", -] - -[[package]] -name = "borsh-derive" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" -dependencies = [ - "once_cell", - "proc-macro-crate 3.2.0", - "proc-macro2", - "quote 1.0.37", - "syn 2.0.87", -] - -[[package]] -name = "borsh-derive-internal" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65d6ba50644c98714aa2a70d13d7df3cd75cd2b523a2b452bf010443800976b3" -dependencies = [ - "proc-macro2", - "quote 1.0.37", - "syn 1.0.95", -] - -[[package]] -name = "borsh-schema-derive-internal" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276691d96f063427be83e6692b86148e488ebba9f48f77788724ca027ba3b6d4" -dependencies = [ - "proc-macro2", - "quote 1.0.37", - "syn 1.0.95", -] - [[package]] name = "brotli" version = "8.0.2" @@ -1275,26 +1187,6 @@ dependencies = [ "crossbeam-utils 0.8.21", ] -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if 1.0.0", - "wasm-bindgen", -] - -[[package]] -name = "console_log" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" -dependencies = [ - "log", - "web-sys", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -1307,12 +1199,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - [[package]] name = "convert_case" version = "0.4.0" @@ -1470,7 +1356,7 @@ dependencies = [ "crossbeam-utils 0.7.2", "lazy_static", "maybe-uninit", - "memoffset 0.5.6", + "memoffset", "scopeguard", ] @@ -3695,7 +3581,7 @@ dependencies = [ "asn1_der", "bs58 0.5.1", "ed25519-dalek 1.0.1", - "libsecp256k1 0.7.0", + "libsecp256k1", "log", "multihash", "quick-protobuf", @@ -3895,23 +3781,6 @@ dependencies = [ "yamux 0.13.1", ] -[[package]] -name = "libsecp256k1" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" -dependencies = [ - "arrayref", - "base64 0.12.3", - "digest 0.9.0", - "libsecp256k1-core 0.2.2", - "libsecp256k1-gen-ecmult 0.2.1", - "libsecp256k1-gen-genmult 0.2.1", - "rand 0.7.3", - "serde", - "sha2 0.9.9", -] - [[package]] name = "libsecp256k1" version = "0.7.0" @@ -3922,26 +3791,15 @@ dependencies = [ "base64 0.13.0", "digest 0.9.0", "hmac-drbg", - "libsecp256k1-core 0.3.0", - "libsecp256k1-gen-ecmult 0.3.0", - "libsecp256k1-gen-genmult 0.3.0", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", "rand 0.8.5", "serde", "sha2 0.9.9", "typenum", ] -[[package]] -name = "libsecp256k1-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" -dependencies = [ - "crunchy", - "digest 0.9.0", - "subtle", -] - [[package]] name = "libsecp256k1-core" version = "0.3.0" @@ -3953,31 +3811,13 @@ dependencies = [ "subtle", ] -[[package]] -name = "libsecp256k1-gen-ecmult" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" -dependencies = [ - "libsecp256k1-core 0.2.2", -] - [[package]] name = "libsecp256k1-gen-ecmult" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" dependencies = [ - "libsecp256k1-core 0.3.0", -] - -[[package]] -name = "libsecp256k1-gen-genmult" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" -dependencies = [ - "libsecp256k1-core 0.2.2", + "libsecp256k1-core", ] [[package]] @@ -3986,7 +3826,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" dependencies = [ - "libsecp256k1-core 0.3.0", + "libsecp256k1-core", ] [[package]] @@ -4183,15 +4023,6 @@ dependencies = [ "autocfg 1.1.0", ] -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg 1.1.0", -] - [[package]] name = "memory-db" version = "0.29.0" @@ -5055,7 +4886,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate 1.1.3", + "proc-macro-crate 3.2.0", "proc-macro2", "quote 1.0.37", "syn 2.0.87", @@ -5439,15 +5270,6 @@ dependencies = [ "uint", ] -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml 0.5.7", -] - [[package]] name = "proc-macro-crate" version = "1.1.3" @@ -7246,30 +7068,11 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8f5152a288ef1912300fc6efa6c2d1f9bb55d9398eb6c72326360b8063987da" dependencies = [ - "bincode", - "serde", "solana-program-error", "solana-program-memory", "solana-pubkey", ] -[[package]] -name = "solana-address-lookup-table-interface" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1673f67efe870b64a65cb39e6194be5b26527691ce5922909939961a6e6b395" -dependencies = [ - "bincode", - "bytemuck", - "serde", - "serde_derive", - "solana-clock", - "solana-instruction", - "solana-pubkey", - "solana-sdk-ids", - "solana-slot-hashes", -] - [[package]] name = "solana-atomic-u64" version = "2.2.1" @@ -7279,17 +7082,6 @@ dependencies = [ "parking_lot", ] -[[package]] -name = "solana-big-mod-exp" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75db7f2bbac3e62cfd139065d15bcda9e2428883ba61fc8d27ccb251081e7567" -dependencies = [ - "num-bigint", - "num-traits", - "solana-define-syscall", -] - [[package]] name = "solana-bincode" version = "2.2.1" @@ -7301,28 +7093,6 @@ dependencies = [ "solana-instruction", ] -[[package]] -name = "solana-blake3-hasher" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0801e25a1b31a14494fc80882a036be0ffd290efc4c2d640bfcca120a4672" -dependencies = [ - "blake3", - "solana-define-syscall", - "solana-hash", - "solana-sanitize", -] - -[[package]] -name = "solana-borsh" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718333bcd0a1a7aed6655aa66bef8d7fb047944922b2d3a18f49cbc13e73d004" -dependencies = [ - "borsh 0.10.4", - "borsh 1.5.7", -] - [[package]] name = "solana-clock" version = "2.2.2" @@ -7412,44 +7182,16 @@ dependencies = [ "solana-sysvar-id", ] -[[package]] -name = "solana-example-mocks" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84461d56cbb8bb8d539347151e0525b53910102e4bced875d49d5139708e39d3" -dependencies = [ - "serde", - "serde_derive", - "solana-address-lookup-table-interface", - "solana-clock", - "solana-hash", - "solana-instruction", - "solana-keccak-hasher", - "solana-message", - "solana-nonce", - "solana-pubkey", - "solana-sdk-ids", - "solana-system-interface", - "thiserror 2.0.15", -] - [[package]] name = "solana-feature-gate-interface" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43f5c5382b449e8e4e3016fb05e418c53d57782d8b5c30aa372fc265654b956d" dependencies = [ - "bincode", "serde", "serde_derive", - "solana-account", - "solana-account-info", - "solana-instruction", - "solana-program-error", "solana-pubkey", - "solana-rent", "solana-sdk-ids", - "solana-system-interface", ] [[package]] @@ -7469,7 +7211,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b96e9f0300fa287b545613f007dfe20043d7812bee255f418c1eb649c93b63" dependencies = [ - "borsh 1.5.7", "bytemuck", "bytemuck_derive", "five8", @@ -7494,7 +7235,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47298e2ce82876b64f71e9d13a46bc4b9056194e7f9937ad3084385befa50885" dependencies = [ "bincode", - "borsh 1.5.7", "getrandom 0.2.16", "js-sys", "num-traits", @@ -7522,18 +7262,6 @@ dependencies = [ "solana-sysvar-id", ] -[[package]] -name = "solana-keccak-hasher" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7aeb957fbd42a451b99235df4942d96db7ef678e8d5061ef34c9b34cae12f79" -dependencies = [ - "sha3 0.10.8", - "solana-define-syscall", - "solana-hash", - "solana-sanitize", -] - [[package]] name = "solana-keypair" version = "2.2.3" @@ -7563,50 +7291,6 @@ dependencies = [ "solana-sysvar-id", ] -[[package]] -name = "solana-loader-v2-interface" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8ab08006dad78ae7cd30df8eea0539e207d08d91eaefb3e1d49a446e1c49654" -dependencies = [ - "serde", - "serde_bytes", - "serde_derive", - "solana-instruction", - "solana-pubkey", - "solana-sdk-ids", -] - -[[package]] -name = "solana-loader-v3-interface" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f7162a05b8b0773156b443bccd674ea78bb9aa406325b467ea78c06c99a63a2" -dependencies = [ - "serde", - "serde_bytes", - "serde_derive", - "solana-instruction", - "solana-pubkey", - "solana-sdk-ids", - "solana-system-interface", -] - -[[package]] -name = "solana-loader-v4-interface" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "706a777242f1f39a83e2a96a2a6cb034cb41169c6ecbee2cf09cb873d9659e7e" -dependencies = [ - "serde", - "serde_bytes", - "serde_derive", - "solana-instruction", - "solana-pubkey", - "solana-sdk-ids", - "solana-system-interface", -] - [[package]] name = "solana-message" version = "2.4.0" @@ -7614,7 +7298,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1796aabce376ff74bf89b78d268fa5e683d7d7a96a0a4e4813ec34de49d5314b" dependencies = [ "bincode", - "blake3", "lazy_static", "serde", "serde_derive", @@ -7639,106 +7322,6 @@ dependencies = [ "solana-define-syscall", ] -[[package]] -name = "solana-native-token" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61515b880c36974053dd499c0510066783f0cc6ac17def0c7ef2a244874cf4a9" - -[[package]] -name = "solana-nonce" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703e22eb185537e06204a5bd9d509b948f0066f2d1d814a6f475dafb3ddf1325" -dependencies = [ - "serde", - "serde_derive", - "solana-fee-calculator", - "solana-hash", - "solana-pubkey", - "solana-sha256-hasher", -] - -[[package]] -name = "solana-program" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98eca145bd3545e2fbb07166e895370576e47a00a7d824e325390d33bf467210" -dependencies = [ - "bincode", - "blake3", - "borsh 0.10.4", - "borsh 1.5.7", - "bs58 0.5.1", - "bytemuck", - "console_error_panic_hook", - "console_log", - "getrandom 0.2.16", - "lazy_static", - "log", - "memoffset 0.9.1", - "num-bigint", - "num-derive", - "num-traits", - "rand 0.8.5", - "serde", - "serde_bytes", - "serde_derive", - "solana-account-info", - "solana-address-lookup-table-interface", - "solana-atomic-u64", - "solana-big-mod-exp", - "solana-bincode", - "solana-blake3-hasher", - "solana-borsh", - "solana-clock", - "solana-cpi", - "solana-decode-error", - "solana-define-syscall", - "solana-epoch-rewards", - "solana-epoch-schedule", - "solana-example-mocks", - "solana-feature-gate-interface", - "solana-fee-calculator", - "solana-hash", - "solana-instruction", - "solana-instructions-sysvar", - "solana-keccak-hasher", - "solana-last-restart-slot", - "solana-loader-v2-interface", - "solana-loader-v3-interface", - "solana-loader-v4-interface", - "solana-message", - "solana-msg", - "solana-native-token", - "solana-nonce", - "solana-program-entrypoint", - "solana-program-error", - "solana-program-memory", - "solana-program-option", - "solana-program-pack", - "solana-pubkey", - "solana-rent", - "solana-sanitize", - "solana-sdk-ids", - "solana-sdk-macro", - "solana-secp256k1-recover", - "solana-serde-varint", - "solana-serialize-utils", - "solana-sha256-hasher", - "solana-short-vec", - "solana-slot-hashes", - "solana-slot-history", - "solana-stable-layout", - "solana-stake-interface", - "solana-system-interface", - "solana-sysvar", - "solana-sysvar-id", - "solana-vote-interface", - "thiserror 2.0.15", - "wasm-bindgen", -] - [[package]] name = "solana-program-entrypoint" version = "2.3.0" @@ -7757,10 +7340,7 @@ version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ee2e0217d642e2ea4bee237f37bd61bb02aec60da3647c48ff88f6556ade775" dependencies = [ - "borsh 1.5.7", "num-traits", - "serde", - "serde_derive", "solana-decode-error", "solana-instruction", "solana-msg", @@ -7797,8 +7377,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b62adb9c3261a052ca1f999398c388f1daf558a1b492f60a6d9e64857db4ff1" dependencies = [ - "borsh 0.10.4", - "borsh 1.5.7", "bytemuck", "bytemuck_derive", "curve25519-dalek 4.1.3", @@ -7954,17 +7532,6 @@ dependencies = [ "syn 2.0.87", ] -[[package]] -name = "solana-secp256k1-recover" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" -dependencies = [ - "libsecp256k1 0.6.0", - "solana-define-syscall", - "thiserror 2.0.15", -] - [[package]] name = "solana-seed-phrase" version = "2.2.1" @@ -8083,8 +7650,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5269e89fde216b4d7e1d1739cf5303f8398a1ff372a81232abbee80e554a838c" dependencies = [ - "borsh 0.10.4", - "borsh 1.5.7", "num-traits", "serde", "serde_derive", @@ -8143,8 +7708,6 @@ checksum = "b8c3595f95069f3d90f275bb9bd235a1973c4d059028b0a7f81baca2703815db" dependencies = [ "base64 0.22.1", "bincode", - "bytemuck", - "bytemuck_derive", "lazy_static", "serde", "serde_derive", @@ -8280,11 +7843,8 @@ version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b80d57478d6599d30acc31cc5ae7f93ec2361a06aefe8ea79bc81739a08af4c3" dependencies = [ - "bincode", "num-derive", "num-traits", - "serde", - "serde_derive", "solana-clock", "solana-decode-error", "solana-hash", @@ -8292,10 +7852,7 @@ dependencies = [ "solana-pubkey", "solana-rent", "solana-sdk-ids", - "solana-serde-varint", "solana-serialize-utils", - "solana-short-vec", - "solana-system-interface", ] [[package]] @@ -8465,17 +8022,30 @@ dependencies = [ [[package]] name = "spl-token" -version = "4.0.2" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e9e171cbcb4b1f72f6d78ed1e975cb467f56825c27d09b8dd2608e4e7fc8b3b" +checksum = "053067c6a82c705004f91dae058b11b4780407e9ccd6799dc9e7d0fab5f242da" dependencies = [ "arrayref", "bytemuck", "num-derive", "num-traits", "num_enum", - "solana-program", - "thiserror 1.0.40", + "solana-account-info", + "solana-cpi", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-sysvar", + "thiserror 2.0.15", ] [[package]] diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index baf77fd234..bb55483c48 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -140,7 +140,7 @@ solana-signer = { version = "2.2", default-features = false, optional = true } solana-system-transaction = { version = "2.2", default-features = false, optional = true } solana-transaction = { version = "2.2", default-features = false, optional = true } spl-associated-token-account-client = { version = "2.0", default-features = false, optional = true } -spl-token = { version = "4.0", default-features = false, optional = true } +spl-token = { version = "8", default-features = false, optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] blake2b_simd.workspace = true From b13eb91e3f8e753ed07d3ebece1121d8765090f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 9 Oct 2025 08:25:42 +0300 Subject: [PATCH 18/27] use token transfer with proper token id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_token.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index 190bc04338..823e352392 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -6,6 +6,7 @@ use std::str::FromStr; use std::sync::Arc; use async_trait::async_trait; +use bitcrypto::sha256; use common::executor::abortable_queue::{AbortableQueue, WeakSpawner}; use common::executor::{AbortableSystem, AbortedError}; use common::Future01CompatExt; @@ -169,6 +170,10 @@ impl SolanaToken { Ok(SolanaToken(Arc::new(token_fields))) } + + fn token_id(&self) -> RpcBytes { + sha256(self.ticker().to_lowercase().as_bytes()).to_vec().into() + } } #[async_trait] @@ -336,7 +341,7 @@ impl MmCoin for SolanaToken { coin: req.coin, internal_id: rpc::v1::types::Bytes(tx_hash.into_bytes()), kmd_rewards: None, - transaction_type: crate::TransactionType::StandardTransfer, + transaction_type: crate::TransactionType::TokenTransfer(token.token_id()), // TODO: Add memo instruction to the TX. memo: None, }) From 755e71241ba834bef63cea334867c397c3a8cbc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 9 Oct 2025 08:41:18 +0300 Subject: [PATCH 19/27] use token address on token transfer_checked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_token.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index 823e352392..ed7418a1e6 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -265,7 +265,7 @@ impl MmCoin for SolanaToken { let transfer_ix = spl_token_program::instruction::transfer_checked( &spl_token_program::id(), - &coin.address, + &token.address, &token.protocol_info.mint_address, &to_token_account, &coin.address, From 3dbc4b8225cf8ba5784d7ea102db248da51ea94b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Mon, 13 Oct 2025 12:42:07 +0300 Subject: [PATCH 20/27] add missing items to untagged deser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/lp_coins.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mm2src/coins/lp_coins.rs b/mm2src/coins/lp_coins.rs index cd430ad9e7..83b5193156 100644 --- a/mm2src/coins/lp_coins.rs +++ b/mm2src/coins/lp_coins.rs @@ -2458,14 +2458,20 @@ impl<'de> Deserialize<'de> for TxFeeDetails { Utxo(UtxoFeeDetails), Eth(EthTxFeeDetails), Qrc20(Qrc20FeeDetails), + Slp(SlpFeeDetails), Tendermint(TendermintFeeDetails), + #[cfg(feature = "enable-solana")] + Solana(SolanaFeeDetails), } match Deserialize::deserialize(deserializer)? { TxFeeDetailsUnTagged::Utxo(f) => Ok(TxFeeDetails::Utxo(f)), TxFeeDetailsUnTagged::Eth(f) => Ok(TxFeeDetails::Eth(f)), TxFeeDetailsUnTagged::Qrc20(f) => Ok(TxFeeDetails::Qrc20(f)), + TxFeeDetailsUnTagged::Slp(f) => Ok(TxFeeDetails::Slp(f)), TxFeeDetailsUnTagged::Tendermint(f) => Ok(TxFeeDetails::Tendermint(f)), + #[cfg(feature = "enable-solana")] + TxFeeDetailsUnTagged::Solana(f) => Ok(TxFeeDetails::Solana(f)), } } } From 84ac433643600ea7990ef43fd35f8db206443e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Tue, 14 Oct 2025 12:57:59 +0300 Subject: [PATCH 21/27] minor improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_coin.rs | 67 +++++++++++++------ mm2src/coins/solana/solana_token.rs | 27 ++++---- .../src/solana_with_assets.rs | 4 +- 3 files changed, 61 insertions(+), 37 deletions(-) diff --git a/mm2src/coins/solana/solana_coin.rs b/mm2src/coins/solana/solana_coin.rs index 0c6dd1ebbb..1ec9e69aae 100644 --- a/mm2src/coins/solana/solana_coin.rs +++ b/mm2src/coins/solana/solana_coin.rs @@ -26,7 +26,7 @@ use rpc::v1::types::{Bytes as RpcBytes, H264 as RpcH264}; use solana_bincode::limited_deserialize; use solana_keypair::{keypair_from_seed, Keypair}; use solana_pubkey::Pubkey as SolanaAddress; -use solana_rpc_client::rpc_client::RpcClient; +use solana_rpc_client::nonblocking::rpc_client::RpcClient; use solana_rpc_client_types::request::TokenAccountsFilter; use solana_signer::Signer; use solana_transaction::Transaction; @@ -111,7 +111,7 @@ pub enum SolanaInitErrorKind { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct SolanaFeeDetails { - pub amount: BigDecimal, + pub total_amount: BigDecimal, } impl SolanaCoin { @@ -167,7 +167,10 @@ impl SolanaCoin { kind: SolanaInitErrorKind::Internal { reason: e.to_string() }, })?; - let rpc_clients: Vec> = nodes.iter().map(|n| Arc::new(RpcClient::new(&n.url))).collect(); + let rpc_clients: Vec> = nodes + .iter() + .map(|n| Arc::new(RpcClient::new(n.url.to_string()))) + .collect(); let abortable_system = ctx.abortable_system.create_subsystem().map_to_mm(|e| SolanaInitError { ticker: ticker.clone(), @@ -190,11 +193,11 @@ impl SolanaCoin { pub(crate) async fn rpc_client(&self) -> MmResult, String> { let mut rpcs = self.rpc_clients.lock().await; - if let Some(index) = rpcs.iter().position(|rpc| rpc.get_health().is_ok()) { - // Put healthy one to the front. - rpcs.rotate_left(index); - - return Ok(rpcs[0].clone()); + for (index, rpc) in rpcs.iter().enumerate() { + if rpc.get_health().await.is_ok() { + rpcs.rotate_left(index); + return Ok(rpcs[0].clone()); + } } MmError::err("No healthy RPC client found.".to_owned()) @@ -210,7 +213,10 @@ impl SolanaCoin { .map_err(|e| BalanceError::Transport(e.into_inner())) .await?; - if let Err(e) = rpc.get_token_accounts_by_owner(&self.address, TokenAccountsFilter::Mint(*mint_address)) { + if let Err(e) = rpc + .get_token_accounts_by_owner(&self.address, TokenAccountsFilter::Mint(*mint_address)) + .await + { if e.kind.to_string().contains("could not find mint") { return Ok(CoinBalance { spendable: BigDecimal::zero(), @@ -226,6 +232,7 @@ impl SolanaCoin { let balance_string = rpc .get_token_account_balance(&token_account) + .await .map_err(|e| BalanceError::Transport(e.to_string()))? .ui_amount_string; @@ -237,6 +244,11 @@ impl SolanaCoin { }) } + /// Calculates the amount (in lamports) that can be withdrawn based on the + /// user's request. + /// + /// Returns the amount to withdraw in lamports on success or a [`WithdrawError`] + /// if the request is invalid or cannot be processed. async fn calculate_withdraw_amount(&self, req: &WithdrawRequest) -> MmResult { let rpc = self .rpc_client() @@ -245,21 +257,24 @@ impl SolanaCoin { let recent_blockhash = rpc .get_latest_blockhash() + .await .map_err(|e| WithdrawError::Transport(e.to_string()))?; // Dummy TX to estimate the fee. let tx = solana_system_transaction::transfer(&self.keypair, &self.address, 0, recent_blockhash); let fee_u64 = rpc .get_fee_for_message(tx.message()) + .await .map_err(|e| WithdrawError::Transport(e.to_string()))?; let balance_u64 = rpc .get_balance(&self.address) + .await .map_err(|e| WithdrawError::Transport(e.to_string()))?; if req.max { let amount = balance_u64.saturating_sub(fee_u64); - let amount_big_decimal = lamports_to_big_decimal(amount, SOLANA_DECIMALS); + let amount_big_decimal = u64_lamports_to_big_decimal(amount, SOLANA_DECIMALS); // Amount must be bigger than min_tx_amount. if amount_big_decimal < self.min_tx_amount() { @@ -272,7 +287,7 @@ impl SolanaCoin { return Ok(balance_u64.saturating_sub(fee_u64)); } - let requested_amount = &req.amount * &BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32)); + let requested_amount = include_lamports_to_big_decimal(&req.amount, SOLANA_DECIMALS); // Amount must be bigger than min_tx_amount. if requested_amount < self.min_tx_amount() { @@ -292,8 +307,8 @@ impl SolanaCoin { if requested_amount_u64 + fee_u64 > balance_u64 { return MmError::err(WithdrawError::NotSufficientBalance { coin: self.ticker.to_owned(), - available: lamports_to_big_decimal(balance_u64, SOLANA_DECIMALS), - required: lamports_to_big_decimal(requested_amount_u64 + fee_u64, SOLANA_DECIMALS), + available: u64_lamports_to_big_decimal(balance_u64, SOLANA_DECIMALS), + required: u64_lamports_to_big_decimal(requested_amount_u64 + fee_u64, SOLANA_DECIMALS), }); }; @@ -336,6 +351,7 @@ impl MmCoin for SolanaCoin { let recent_blockhash = rpc .get_latest_blockhash() + .await .map_err(|e| WithdrawError::Transport(e.to_string()))?; // Actual TX @@ -352,12 +368,13 @@ impl MmCoin for SolanaCoin { let tx_data = TransactionData::new_signed(BytesJson(tx_bytes), tx_hash.clone()); - let amount_dec = lamports_to_big_decimal(lamports, SOLANA_DECIMALS); + let amount_dec = u64_lamports_to_big_decimal(lamports, SOLANA_DECIMALS); let fee = rpc .get_fee_for_message(tx.message()) + .await .map_err(|e| WithdrawError::Transport(e.to_string()))?; - let fee = lamports_to_big_decimal(fee, SOLANA_DECIMALS); + let fee = u64_lamports_to_big_decimal(fee, SOLANA_DECIMALS); let received_by_me = if to == coin.address { &amount_dec - &fee @@ -371,11 +388,11 @@ impl MmCoin for SolanaCoin { to: vec![to.to_string()], total_amount: amount_dec.clone(), spent_by_me: amount_dec.clone(), + my_balance_change: &received_by_me - &amount_dec, received_by_me, - my_balance_change: -amount_dec, block_height: 0, timestamp: 0, - fee_details: Some(TxFeeDetails::Solana(SolanaFeeDetails { amount: fee })), + fee_details: Some(TxFeeDetails::Solana(SolanaFeeDetails { total_amount: fee })), coin: req.coin, internal_id: BytesJson(tx_hash.into_bytes()), kmd_rewards: None, @@ -532,9 +549,10 @@ impl MarketCoinOps for SolanaCoin { let balance_u64 = rpc_client .get_balance(&coin.address) + .await .map_err(|e| BalanceError::Transport(e.to_string()))?; - let balance_decimal = lamports_to_big_decimal(balance_u64, SOLANA_DECIMALS); + let balance_decimal = u64_lamports_to_big_decimal(balance_u64, SOLANA_DECIMALS); Ok(CoinBalance { spendable: balance_decimal, @@ -565,9 +583,10 @@ impl MarketCoinOps for SolanaCoin { let rpc = coin.rpc_client().await.map_err(|e| e.into_inner())?; let tx: Transaction = limited_deserialize(&bytes, PACKET_DATA_SIZE as u64).map_err(|e| e.to_string())?; - let signature = rpc.send_transaction(&tx).map_err(|e| e.to_string())?; + let signature = rpc.send_transaction(&tx).await.map_err(|e| e.to_string())?; // TX hash is just the base58 `String` form of the `Signature`. + // ref: https://solana.com/docs/references/terminology#transaction-id Ok(signature.to_string()) }; Box::new(fut.boxed().compat()) @@ -596,7 +615,7 @@ impl MarketCoinOps for SolanaCoin { let fut = async move { let rpc_client = try_s!(coin.rpc_client().await); - rpc_client.get_block_height().map_err(|e| e.to_string()) + rpc_client.get_block_height().await.map_err(|e| e.to_string()) }; Box::new(fut.boxed().compat()) @@ -608,7 +627,7 @@ impl MarketCoinOps for SolanaCoin { #[inline] fn min_tx_amount(&self) -> BigDecimal { - lamports_to_big_decimal(1, SOLANA_DECIMALS) + u64_lamports_to_big_decimal(1, SOLANA_DECIMALS) } #[inline] @@ -729,6 +748,10 @@ impl SwapOps for SolanaCoin { #[async_trait] impl WatcherOps for SolanaCoin {} -pub(crate) fn lamports_to_big_decimal>(lamports: u64, decimals: T) -> BigDecimal { +pub(crate) fn u64_lamports_to_big_decimal>(lamports: u64, decimals: T) -> BigDecimal { BigDecimal::from(lamports) / BigDecimal::from(10u64.pow(decimals.into())) } + +pub(crate) fn include_lamports_to_big_decimal>(amount: &BigDecimal, decimals: T) -> BigDecimal { + amount * &BigDecimal::from(10u64.pow(decimals.into())) +} diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index ed7418a1e6..50c1641b2c 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -23,7 +23,7 @@ use serde::Deserialize; use crate::coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult}; use crate::hd_wallet::HDAddressSelector; -use crate::solana::solana_coin::lamports_to_big_decimal; +use crate::solana::solana_coin::{include_lamports_to_big_decimal, u64_lamports_to_big_decimal}; use crate::solana::SolanaFeeDetails; use crate::{ solana::SolanaCoin, BalanceFut, CoinBalance, RawTransactionFut, RawTransactionRequest, TxFeeDetails, WithdrawFut, @@ -40,7 +40,7 @@ use crate::{ use solana_pubkey::Pubkey as SolanaAddress; use solana_transaction::Transaction; use spl_associated_token_account_client::address::get_associated_token_address; -use spl_associated_token_account_client::instruction::create_associated_token_account; +use spl_associated_token_account_client::instruction::create_associated_token_account_idempotent; use spl_token as spl_token_program; pub struct SolanaTokenFields { @@ -134,7 +134,7 @@ impl SolanaToken { kind: SolanaTokenInitErrorKind::UnhealthyRPCs, })?; - match rpc.get_account(&protocol_info.mint_address) { + match rpc.get_account(&protocol_info.mint_address).await { Ok(mint_account) => { if mint_account.owner != spl_token_program::id() { return MmError::err(SolanaTokenInitError { @@ -172,7 +172,7 @@ impl SolanaToken { } fn token_id(&self) -> RpcBytes { - sha256(self.ticker().to_lowercase().as_bytes()).to_vec().into() + sha256(&self.protocol_info.mint_address.to_bytes()).to_vec().into() } } @@ -219,8 +219,7 @@ impl MmCoin for SolanaToken { ))) })? } else { - let scale = BigDecimal::from(10u64.pow(token.protocol_info.decimals as u32)); - let big_decimal = &req.amount * &scale; + let big_decimal = include_lamports_to_big_decimal(&req.amount, token.protocol_info.decimals); big_decimal.to_u64().ok_or_else(|| { MmError::new(WithdrawError::InternalError(format!( @@ -236,7 +235,7 @@ impl MmCoin for SolanaToken { }); } - let amount_decimal = lamports_to_big_decimal(amount_u64, token.protocol_info.decimals); + let amount_decimal = u64_lamports_to_big_decimal(amount_u64, token.protocol_info.decimals); if balance.spendable < amount_decimal { return MmError::err(WithdrawError::NotSufficientBalance { coin: token.ticker.to_owned(), @@ -250,12 +249,12 @@ impl MmCoin for SolanaToken { // - Transfer. let mut instructions = Vec::new(); - if let Err(e) = rpc.get_account(&to_token_account) { + if let Err(e) = rpc.get_account(&to_token_account).await { if !e.kind.to_string().contains("AccountNotFound") { return MmError::err(WithdrawError::Transport(e.to_string())); } - instructions.push(create_associated_token_account( + instructions.push(create_associated_token_account_idempotent( &coin.address, &to, &token.protocol_info.mint_address, @@ -278,6 +277,7 @@ impl MmCoin for SolanaToken { let recent_blockhash = rpc .get_latest_blockhash() + .await .map_err(|e| WithdrawError::Transport(e.to_string()))?; let tx = Transaction::new_signed_with_payer( @@ -299,12 +299,13 @@ impl MmCoin for SolanaToken { let tx_data = crate::TransactionData::new_signed(rpc::v1::types::Bytes(tx_bytes), tx_hash.clone()); - let amount_dec = lamports_to_big_decimal(amount_u64, token.protocol_info.decimals); + let amount_dec = u64_lamports_to_big_decimal(amount_u64, token.protocol_info.decimals); let fee_lamports = rpc .get_fee_for_message(tx.message()) + .await .map_err(|e| WithdrawError::Transport(e.to_string()))?; - let fee_dec = lamports_to_big_decimal(fee_lamports, super::solana_coin::SOLANA_DECIMALS); + let fee_dec = u64_lamports_to_big_decimal(fee_lamports, super::solana_coin::SOLANA_DECIMALS); let platform_coin_balance = coin .my_balance() @@ -333,11 +334,11 @@ impl MmCoin for SolanaToken { to: vec![to.to_string()], total_amount: amount_dec.clone(), spent_by_me: amount_dec.clone(), + my_balance_change: &received_by_me - &amount_dec, received_by_me, - my_balance_change: -amount_dec, block_height: 0, timestamp: 0, - fee_details: Some(TxFeeDetails::Solana(SolanaFeeDetails { amount: fee_dec })), + fee_details: Some(TxFeeDetails::Solana(SolanaFeeDetails { total_amount: fee_dec })), coin: req.coin, internal_id: rpc::v1::types::Bytes(tx_hash.into_bytes()), kmd_rewards: None, diff --git a/mm2src/coins_activation/src/solana_with_assets.rs b/mm2src/coins_activation/src/solana_with_assets.rs index c2d1cd12ca..226f9c60bc 100644 --- a/mm2src/coins_activation/src/solana_with_assets.rs +++ b/mm2src/coins_activation/src/solana_with_assets.rs @@ -12,7 +12,7 @@ use coins::{ CoinBalance, CoinProtocol, MarketCoinOps, MmCoinEnum, PrivKeyBuildPolicy, }; use common::Future01CompatExt; -use futures::future::{join_all, try_join_all}; +use futures::future::try_join_all; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; @@ -207,7 +207,7 @@ impl PlatformCoinWithTokensActivationOps for SolanaCoin { } }); - let tokens_balances: HashMap<_, _> = join_all(tasks).await.into_iter().collect::>()?; + let tokens_balances: HashMap<_, _> = try_join_all(tasks).await?.into_iter().collect(); Ok(SolanaActivationResult { ticker: self.ticker().to_owned(), From dd8eccfc4648b36fc9a8ff3e83b46f879de468de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 16 Oct 2025 10:38:38 +0300 Subject: [PATCH 22/27] update received_by_me and spent_by_me, my_balance_change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_coin.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mm2src/coins/solana/solana_coin.rs b/mm2src/coins/solana/solana_coin.rs index 1ec9e69aae..b14b18ce96 100644 --- a/mm2src/coins/solana/solana_coin.rs +++ b/mm2src/coins/solana/solana_coin.rs @@ -284,7 +284,7 @@ impl SolanaCoin { }); } - return Ok(balance_u64.saturating_sub(fee_u64)); + return Ok(amount); } let requested_amount = include_lamports_to_big_decimal(&req.amount, SOLANA_DECIMALS); @@ -377,18 +377,20 @@ impl MmCoin for SolanaCoin { let fee = u64_lamports_to_big_decimal(fee, SOLANA_DECIMALS); let received_by_me = if to == coin.address { - &amount_dec - &fee + amount_dec.clone() } else { BigDecimal::zero() }; + let spent_by_me = &amount_dec + &fee; + Ok(TransactionDetails { tx: tx_data, from: vec![coin.address.to_string()], to: vec![to.to_string()], - total_amount: amount_dec.clone(), - spent_by_me: amount_dec.clone(), - my_balance_change: &received_by_me - &amount_dec, + my_balance_change: &received_by_me - &spent_by_me, + spent_by_me, + total_amount: amount_dec, received_by_me, block_height: 0, timestamp: 0, From d41ea993ce2ee204749ff1dec5ce512446bbab63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 23 Oct 2025 11:02:29 +0300 Subject: [PATCH 23/27] some nit fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_coin.rs | 3 ++- mm2src/coins/solana/solana_token.rs | 9 +++------ mm2src/mm2_main/tests/mm2_tests/solana_tests.rs | 4 +--- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/mm2src/coins/solana/solana_coin.rs b/mm2src/coins/solana/solana_coin.rs index b14b18ce96..47bbff5982 100644 --- a/mm2src/coins/solana/solana_coin.rs +++ b/mm2src/coins/solana/solana_coin.rs @@ -10,6 +10,7 @@ use common::executor::{ abortable_queue::{AbortableQueue, WeakSpawner}, AbortableSystem, AbortedError, }; +use common::now_sec; use derive_more::Display; use futures::lock::Mutex as AsyncMutex; use futures::{FutureExt, TryFutureExt}; @@ -393,7 +394,7 @@ impl MmCoin for SolanaCoin { total_amount: amount_dec, received_by_me, block_height: 0, - timestamp: 0, + timestamp: now_sec(), fee_details: Some(TxFeeDetails::Solana(SolanaFeeDetails { total_amount: fee })), coin: req.coin, internal_id: BytesJson(tx_hash.into_bytes()), diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index 50c1641b2c..dfbadb98bc 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -9,7 +9,7 @@ use async_trait::async_trait; use bitcrypto::sha256; use common::executor::abortable_queue::{AbortableQueue, WeakSpawner}; use common::executor::{AbortableSystem, AbortedError}; -use common::Future01CompatExt; +use common::{now_sec, Future01CompatExt}; use derive_more::Display; use futures::{FutureExt, TryFutureExt}; use futures01::Future; @@ -124,10 +124,7 @@ impl SolanaToken { kind: SolanaTokenInitErrorKind::Internal { reason: e.to_string() }, })?; - let address = spl_associated_token_account_client::address::get_associated_token_address( - &platform_coin.address, - &protocol_info.mint_address, - ); + let address = get_associated_token_address(&platform_coin.address, &protocol_info.mint_address); let rpc = platform_coin.rpc_client().await.map_err(|e| SolanaTokenInitError { ticker: ticker.clone(), @@ -337,7 +334,7 @@ impl MmCoin for SolanaToken { my_balance_change: &received_by_me - &amount_dec, received_by_me, block_height: 0, - timestamp: 0, + timestamp: now_sec(), fee_details: Some(TxFeeDetails::Solana(SolanaFeeDetails { total_amount: fee_dec })), coin: req.coin, internal_id: rpc::v1::types::Bytes(tx_hash.into_bytes()), diff --git a/mm2src/mm2_main/tests/mm2_tests/solana_tests.rs b/mm2src/mm2_main/tests/mm2_tests/solana_tests.rs index ea3ce06115..89d0d8f8b4 100644 --- a/mm2src/mm2_main/tests/mm2_tests/solana_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/solana_tests.rs @@ -90,12 +90,10 @@ fn enable_with_tokens_and_withdraw() { // Withdraw and send transaction to ourselves. let tx_details = block_on(withdraw_v1(&mm, coin, MY_ADDRESS, "0.1", None)); - let fee: BigDecimal = tx_details.fee_details["amount"].as_str().unwrap().parse().unwrap(); log!("Withdraw to self {}", serde_json::to_string(&tx_details).unwrap()); let expected_received: BigDecimal = "0.1".parse().unwrap(); - // We sent it to ourselves, so the fee value should be extracted. - assert_eq!(tx_details.received_by_me, expected_received - fee); + assert_eq!(tx_details.received_by_me, expected_received); assert_eq!(tx_details.to, vec![MY_ADDRESS.to_owned()]); assert_eq!(tx_details.from, vec![MY_ADDRESS.to_owned()]); From dfe4a0e8d43d8625f39f0f37bef209a4f8caefbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 23 Oct 2025 11:59:45 +0300 Subject: [PATCH 24/27] better program id usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_token.rs | 25 +++++++++++++------ .../mm2_main/tests/mm2_tests/solana_tests.rs | 3 ++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index dfbadb98bc..69b9dd3e35 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -39,7 +39,7 @@ use crate::{ }; use solana_pubkey::Pubkey as SolanaAddress; use solana_transaction::Transaction; -use spl_associated_token_account_client::address::get_associated_token_address; +use spl_associated_token_account_client::address::get_associated_token_address_with_program_id; use spl_associated_token_account_client::instruction::create_associated_token_account_idempotent; use spl_token as spl_token_program; @@ -68,6 +68,8 @@ pub struct SolanaTokenProtocolInfo { pub decimals: u8, #[serde(serialize_with = "serialize_pubkey", deserialize_with = "deserialize_pubkey")] pub mint_address: SolanaAddress, + #[serde(serialize_with = "serialize_pubkey", deserialize_with = "deserialize_pubkey")] + program_id: SolanaAddress, } pub fn serialize_pubkey(public_key: &SolanaAddress, serializer: S) -> Result @@ -124,7 +126,11 @@ impl SolanaToken { kind: SolanaTokenInitErrorKind::Internal { reason: e.to_string() }, })?; - let address = get_associated_token_address(&platform_coin.address, &protocol_info.mint_address); + let address = get_associated_token_address_with_program_id( + &platform_coin.address, + &protocol_info.mint_address, + &protocol_info.program_id, + ); let rpc = platform_coin.rpc_client().await.map_err(|e| SolanaTokenInitError { ticker: ticker.clone(), @@ -133,14 +139,13 @@ impl SolanaToken { match rpc.get_account(&protocol_info.mint_address).await { Ok(mint_account) => { - if mint_account.owner != spl_token_program::id() { + if mint_account.owner != protocol_info.program_id { return MmError::err(SolanaTokenInitError { ticker: ticker.clone(), kind: SolanaTokenInitErrorKind::QueryError { reason: format!( "Unsupported SPL program. Expected Program ID: '{}', Got: '{}'.", - spl_token_program::id(), - mint_account.owner + protocol_info.program_id, mint_account.owner ), }, }); @@ -200,7 +205,11 @@ impl MmCoin for SolanaToken { // `to` can be either a Solana address, or a token address. We create // `to_token_account` regardless to support the both cases. let to = SolanaAddress::from_str(&req.to).map_err(|e| WithdrawError::InvalidAddress(e.to_string()))?; - let to_token_account = get_associated_token_address(&to, &token.protocol_info.mint_address); + let to_token_account = get_associated_token_address_with_program_id( + &to, + &token.protocol_info.mint_address, + &token.protocol_info.program_id, + ); let balance = token .my_balance() @@ -255,12 +264,12 @@ impl MmCoin for SolanaToken { &coin.address, &to, &token.protocol_info.mint_address, - &spl_token_program::id(), + &token.protocol_info.program_id, )); }; let transfer_ix = spl_token_program::instruction::transfer_checked( - &spl_token_program::id(), + &token.protocol_info.program_id, &token.address, &token.protocol_info.mint_address, &to_token_account, diff --git a/mm2src/mm2_main/tests/mm2_tests/solana_tests.rs b/mm2src/mm2_main/tests/mm2_tests/solana_tests.rs index 89d0d8f8b4..2102533ecf 100644 --- a/mm2src/mm2_main/tests/mm2_tests/solana_tests.rs +++ b/mm2src/mm2_main/tests/mm2_tests/solana_tests.rs @@ -36,7 +36,8 @@ fn usdc_token_config() -> serde_json::Value { "protocol_data": { "platform": "SOL-DEV", "decimals": 6, - "mint_address": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" + "mint_address": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", + "program_id": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", } }, "derivation_path": "m/44'/501'" From 6ec2ee76f3b11fbce1019abfe3433620dffa1146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 23 Oct 2025 12:18:04 +0300 Subject: [PATCH 25/27] re-write calculate_withdraw_and_fee_amount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_coin.rs | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/mm2src/coins/solana/solana_coin.rs b/mm2src/coins/solana/solana_coin.rs index 47bbff5982..6f069ca033 100644 --- a/mm2src/coins/solana/solana_coin.rs +++ b/mm2src/coins/solana/solana_coin.rs @@ -245,12 +245,12 @@ impl SolanaCoin { }) } - /// Calculates the amount (in lamports) that can be withdrawn based on the - /// user's request. + /// Calculates the withdraw amount (in lamports) that can be withdrawn based on the + /// user's request along with the network fee (in lamports). /// - /// Returns the amount to withdraw in lamports on success or a [`WithdrawError`] - /// if the request is invalid or cannot be processed. - async fn calculate_withdraw_amount(&self, req: &WithdrawRequest) -> MmResult { + /// Returns the amount to withdraw and network fee in lamports on success or + /// [`WithdrawError`] if the request is invalid or cannot be processed. + async fn calculate_withdraw_and_fee_amount(&self, req: &WithdrawRequest) -> MmResult<(u64, u64), WithdrawError> { let rpc = self .rpc_client() .await @@ -285,7 +285,7 @@ impl SolanaCoin { }); } - return Ok(amount); + return Ok((amount, fee_u64)); } let requested_amount = include_lamports_to_big_decimal(&req.amount, SOLANA_DECIMALS); @@ -313,7 +313,7 @@ impl SolanaCoin { }); }; - Ok(requested_amount_u64) + Ok((requested_amount_u64, fee_u64)) } } @@ -341,9 +341,9 @@ impl MmCoin for SolanaCoin { .await .map_err(|e| WithdrawError::Transport(e.into_inner()))?; - let lamports = coin.calculate_withdraw_amount(&req).await?; + let (withdraw_lamports, fee_lamports) = coin.calculate_withdraw_and_fee_amount(&req).await?; - if lamports == 0 { + if withdraw_lamports == 0 { return MmError::err(WithdrawError::AmountTooLow { amount: req.amount, threshold: coin.min_tx_amount(), @@ -356,7 +356,7 @@ impl MmCoin for SolanaCoin { .map_err(|e| WithdrawError::Transport(e.to_string()))?; // Actual TX - let tx = solana_system_transaction::transfer(&coin.keypair, &to, lamports, recent_blockhash); + let tx = solana_system_transaction::transfer(&coin.keypair, &to, withdraw_lamports, recent_blockhash); let tx_hash = tx .signatures @@ -369,13 +369,9 @@ impl MmCoin for SolanaCoin { let tx_data = TransactionData::new_signed(BytesJson(tx_bytes), tx_hash.clone()); - let amount_dec = u64_lamports_to_big_decimal(lamports, SOLANA_DECIMALS); + let amount_dec = u64_lamports_to_big_decimal(withdraw_lamports, SOLANA_DECIMALS); - let fee = rpc - .get_fee_for_message(tx.message()) - .await - .map_err(|e| WithdrawError::Transport(e.to_string()))?; - let fee = u64_lamports_to_big_decimal(fee, SOLANA_DECIMALS); + let fee = u64_lamports_to_big_decimal(fee_lamports, SOLANA_DECIMALS); let received_by_me = if to == coin.address { amount_dec.clone() From e1d15825e53f092dbc32246c79e62cbe285cef29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Thu, 23 Oct 2025 12:40:32 +0300 Subject: [PATCH 26/27] handle rent amounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_coin.rs | 14 +++++++++++++- mm2src/coins/solana/solana_token.rs | 24 ++++++++++++++++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/mm2src/coins/solana/solana_coin.rs b/mm2src/coins/solana/solana_coin.rs index 6f069ca033..bf74fcddbb 100644 --- a/mm2src/coins/solana/solana_coin.rs +++ b/mm2src/coins/solana/solana_coin.rs @@ -110,8 +110,16 @@ pub enum SolanaInitErrorKind { }, } +/// Fees associated with a Solana transaction. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct SolanaFeeDetails { + /// Network fee in SOL. + pub fee_amount: BigDecimal, + /// Rent in SOL when an associated token account (ATA) is created. + /// + /// This is 0 if no ATA creation is needed. + pub rent_amount: BigDecimal, + /// Sum of the network fee and rent. pub total_amount: BigDecimal, } @@ -391,7 +399,11 @@ impl MmCoin for SolanaCoin { received_by_me, block_height: 0, timestamp: now_sec(), - fee_details: Some(TxFeeDetails::Solana(SolanaFeeDetails { total_amount: fee })), + fee_details: Some(TxFeeDetails::Solana(SolanaFeeDetails { + fee_amount: fee.clone(), + rent_amount: 0.into(), + total_amount: fee, + })), coin: req.coin, internal_id: BytesJson(tx_hash.into_bytes()), kmd_rewards: None, diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index 69b9dd3e35..69ed255475 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -41,7 +41,7 @@ use solana_pubkey::Pubkey as SolanaAddress; use solana_transaction::Transaction; use spl_associated_token_account_client::address::get_associated_token_address_with_program_id; use spl_associated_token_account_client::instruction::create_associated_token_account_idempotent; -use spl_token as spl_token_program; +use spl_token::solana_program::program_pack::Pack; pub struct SolanaTokenFields { pub ticker: String, @@ -254,12 +254,18 @@ impl MmCoin for SolanaToken { // - Create recipient address if missing. // - Transfer. let mut instructions = Vec::new(); + let mut rent_lamports = 0; if let Err(e) = rpc.get_account(&to_token_account).await { if !e.kind.to_string().contains("AccountNotFound") { return MmError::err(WithdrawError::Transport(e.to_string())); } + rent_lamports = rpc + .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN) + .await + .map_err(|e| WithdrawError::Transport(e.to_string()))?; + instructions.push(create_associated_token_account_idempotent( &coin.address, &to, @@ -268,7 +274,7 @@ impl MmCoin for SolanaToken { )); }; - let transfer_ix = spl_token_program::instruction::transfer_checked( + let transfer_ix = spl_token::instruction::transfer_checked( &token.protocol_info.program_id, &token.address, &token.protocol_info.mint_address, @@ -311,7 +317,9 @@ impl MmCoin for SolanaToken { .get_fee_for_message(tx.message()) .await .map_err(|e| WithdrawError::Transport(e.to_string()))?; - let fee_dec = u64_lamports_to_big_decimal(fee_lamports, super::solana_coin::SOLANA_DECIMALS); + let network_fee_dec = u64_lamports_to_big_decimal(fee_lamports, super::solana_coin::SOLANA_DECIMALS); + let rent_dec = u64_lamports_to_big_decimal(rent_lamports, super::solana_coin::SOLANA_DECIMALS); + let total_fee_dec = &network_fee_dec + &rent_dec; let platform_coin_balance = coin .my_balance() @@ -320,10 +328,10 @@ impl MmCoin for SolanaToken { .map_err(|e| WithdrawError::Transport(e.to_string()))? .spendable; - if fee_dec > platform_coin_balance { + if total_fee_dec > platform_coin_balance { return MmError::err(WithdrawError::NotSufficientPlatformBalanceForFee { available: platform_coin_balance, - required: fee_dec, + required: total_fee_dec, coin: coin.ticker().to_owned(), }); } @@ -344,7 +352,11 @@ impl MmCoin for SolanaToken { received_by_me, block_height: 0, timestamp: now_sec(), - fee_details: Some(TxFeeDetails::Solana(SolanaFeeDetails { total_amount: fee_dec })), + fee_details: Some(TxFeeDetails::Solana(SolanaFeeDetails { + fee_amount: network_fee_dec, + rent_amount: rent_dec, + total_amount: total_fee_dec, + })), coin: req.coin, internal_id: rpc::v1::types::Bytes(tx_hash.into_bytes()), kmd_rewards: None, From 03b006cd6aeef55a51c7da730fcb7b913ca5b1ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Mon, 27 Oct 2025 08:02:17 +0300 Subject: [PATCH 27/27] add TODO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- mm2src/coins/solana/solana_token.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/mm2src/coins/solana/solana_token.rs b/mm2src/coins/solana/solana_token.rs index 69ed255475..aa2d55182c 100644 --- a/mm2src/coins/solana/solana_token.rs +++ b/mm2src/coins/solana/solana_token.rs @@ -262,6 +262,7 @@ impl MmCoin for SolanaToken { } rent_lamports = rpc + // TODO: use dynamic account length .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN) .await .map_err(|e| WithdrawError::Transport(e.to_string()))?;