Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d31f5bb
implement `send_raw_tx*` functions for solana
onur-ozkan Sep 16, 2025
c3f52f2
implement withdraw RPC for Solana
onur-ozkan Sep 16, 2025
dbe6033
handle solana fee details
onur-ozkan Sep 17, 2025
0b699ef
add TODO
onur-ozkan Sep 17, 2025
751085c
implement withdraw for solana tokens
onur-ozkan Sep 17, 2025
e91089e
disable enable-solana feature
onur-ozkan Sep 17, 2025
076ca39
handle fee on received_by_me
onur-ozkan Sep 18, 2025
4800d4e
disable token check temporarily
onur-ozkan Sep 18, 2025
3c7603a
rename test module for tendermint
onur-ozkan Sep 18, 2025
ab1f3a5
add coverage on sol activation, withdraw and send_raw_transaction
onur-ozkan Sep 18, 2025
853db91
feature gate solana_tests module
onur-ozkan Sep 18, 2025
6748484
add another TODO
onur-ozkan Sep 18, 2025
cc75fad
extend calculate_withdraw_amount
onur-ozkan Oct 7, 2025
7094fcb
resolve solana_token todo
onur-ozkan Oct 7, 2025
ab565df
Merge branch 'dev' of github.com:KomodoPlatform/komodo-defi-framework…
onur-ozkan Oct 7, 2025
903336d
handle non-existent token accounts
onur-ozkan Oct 7, 2025
82d7a3a
handle token programs on token init
onur-ozkan Oct 7, 2025
974ac57
bump spl-token
onur-ozkan Oct 9, 2025
b13eb91
use token transfer with proper token id
onur-ozkan Oct 9, 2025
755e712
use token address on token transfer_checked
onur-ozkan Oct 9, 2025
3dbc4b8
add missing items to untagged deser
onur-ozkan Oct 13, 2025
84ac433
minor improvements
onur-ozkan Oct 14, 2025
dd8eccf
update received_by_me and spent_by_me, my_balance_change
onur-ozkan Oct 16, 2025
d41ea99
some nit fixes
onur-ozkan Oct 23, 2025
dfe4a0e
better program id usage
onur-ozkan Oct 23, 2025
6ec2ee7
re-write calculate_withdraw_and_fee_amount
onur-ozkan Oct 23, 2025
e1d1582
handle rent amounts
onur-ozkan Oct 23, 2025
03b006c
add TODO
onur-ozkan Oct 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
583 changes: 554 additions & 29 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions mm2src/coins/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ 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",
"dep:spl-token",
]
default = []
run-docker-tests = []
Expand Down Expand Up @@ -125,12 +130,17 @@ 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 }
spl-token = { version = "4.0", default-features = false, optional = true }

[target.'cfg(target_arch = "wasm32")'.dependencies]
blake2b_simd.workspace = true
Expand Down
4 changes: 4 additions & 0 deletions mm2src/coins/lp_coins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Future<Item = TransactionEnum, Error = TransactionErr> + Send>;
pub type TransactionResult = Result<TransactionEnum, TransactionErr>;
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions mm2src/coins/solana/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
156 changes: 147 additions & 9 deletions mm2src/coins/solana/solana_coin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,44 @@ 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_keypair::keypair_from_seed;
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_types::request::TokenAccountsFilter;
use solana_signer::Signer;
use solana_transaction::Transaction;
use url::Url;

use crate::TxFeeDetails;
use crate::{
coin_errors::{AddressFromPubkeyError, MyAddressError, ValidatePaymentResult},
hd_wallet::HDAddressSelector,
BalanceError, BalanceFut, CheckIfMyPaymentSentArgs, CoinBalance, ConfirmPaymentInput, DexFee, FeeApproxStage,
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;

/// 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,
Expand All @@ -53,6 +67,7 @@ pub struct SolanaCoin(Arc<SolanaCoinFields>);
pub struct SolanaCoinFields {
ticker: String,
pub(crate) address: SolanaAddress,
pub(crate) keypair: Keypair,
pub(crate) abortable_system: AbortableQueue,
rpc_clients: AsyncMutex<Vec<Arc<RpcClient>>>,
protocol_info: SolanaProtocolInfo,
Expand Down Expand Up @@ -94,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,
Expand Down Expand Up @@ -157,6 +177,7 @@ impl SolanaCoin {
let fields = SolanaCoinFields {
ticker,
address,
keypair,
abortable_system,
rpc_clients: AsyncMutex::new(rpc_clients),
protocol_info,
Expand Down Expand Up @@ -215,6 +236,42 @@ impl SolanaCoin {
unspendable: Default::default(),
})
}

async fn calculate_withdraw_amount(&self, req: &WithdrawRequest) -> MmResult<u64, WithdrawError> {
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));

// 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."
)))
})
}
}
}

#[async_trait]
Expand All @@ -228,11 +285,80 @@ impl MmCoin for SolanaCoin {
}

fn spawner(&self) -> WeakSpawner {
todo!()
self.abortable_system.weak_spawner()
}

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(),
});
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it won't == 0, we do that check above in calculate-withdraw_amount (checking with coin.min_tx_amount())

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but it's hard to track (if we change calculate_withdraw_amount we will most likely forget to add the check in withdraw). So for the sake of safety/explicitly I would still keep it here to ensure everything is guaranteed.


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));

let fee = rpc
.get_fee_for_message(tx.message())
.map_err(|e| WithdrawError::Transport(e.to_string()))?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cant we calculate the tx fee internally without invoking the rpc?
also im not sure if this method does an estimation or query the fee for this specific transaction from the blockchain?

nvm, i thought that solana_system_transaction::transfer sends the transaction on-chain.
but then i got a Q: does this mean that the tx fee is calculated dynamically based on the blockchain conditions? do we have any control/limit over the fees that could be spent from out account at the time of tx publishing?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: does this mean that the tx fee is calculated dynamically based on the blockchain conditions?

Yes.

Do we have any control/limit over the fees that could be spent from out account at the time of tx publishing?

Well, we see the fee before we broadcasting it, so there is a bit of control.

let fee = BigDecimal::from(fee) / BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32));

let received_by_me = if to == coin.address {
&amount_dec - &fee
} 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(),
received_by_me,
my_balance_change: -amount_dec,
block_height: 0,
timestamp: 0,
fee_details: Some(TxFeeDetails::Solana(SolanaFeeDetails { amount: fee })),
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: None,
})
};

Box::new(fut.boxed().compat())
}

fn get_raw_transaction(&self, req: RawTransactionRequest) -> RawTransactionFut<'_> {
Expand Down Expand Up @@ -402,11 +528,23 @@ impl MarketCoinOps for SolanaCoin {
}

fn send_raw_tx(&self, tx: &str) -> Box<dyn Future<Item = String, Error = String> + Send> {
todo!()
let bytes = try_fus!(hex::decode(tx));
self.send_raw_tx_bytes(&bytes)
}

fn send_raw_tx_bytes(&self, tx: &[u8]) -> Box<dyn Future<Item = String, Error = String> + 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)]
Expand Down Expand Up @@ -444,7 +582,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]
Expand Down
Loading
Loading