-
Notifications
You must be signed in to change notification settings - Fork 115
feat(protocol): [2] solana support #2622
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 17 commits
d31f5bb
c3f52f2
dbe6033
0b699ef
751085c
e91089e
076ca39
4800d4e
3c7603a
ab1f3a5
853db91
6748484
cc75fad
7094fcb
ab565df
903336d
82d7a3a
974ac57
b13eb91
755e712
3dbc4b8
84ac433
dd8eccf
d41ea99
dfe4a0e
6ec2ee7
e1d1582
03b006c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
|
@@ -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, | ||
|
|
@@ -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, | ||
|
|
@@ -157,6 +177,7 @@ impl SolanaCoin { | |
| let fields = SolanaCoinFields { | ||
| ticker, | ||
| address, | ||
| keypair, | ||
| abortable_system, | ||
| rpc_clients: AsyncMutex::new(rpc_clients), | ||
| protocol_info, | ||
|
|
@@ -215,6 +236,69 @@ impl SolanaCoin { | |
| unspendable: Default::default(), | ||
| }) | ||
| } | ||
|
|
||
| async fn calculate_withdraw_amount(&self, req: &WithdrawRequest) -> MmResult<u64, WithdrawError> { | ||
onur-ozkan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let rpc = self | ||
| .rpc_client() | ||
| .await | ||
| .map_err(|e| WithdrawError::Transport(e.into_inner()))?; | ||
|
|
||
| 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()))?; | ||
|
|
||
| let balance_u64 = rpc | ||
| .get_balance(&self.address) | ||
| .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(), | ||
| }); | ||
| } | ||
|
|
||
| return Ok(balance_u64.saturating_sub(fee_u64)); | ||
| } | ||
|
|
||
| let requested_amount = &req.amount * &BigDecimal::from(10u64.pow(SOLANA_DECIMALS as u32)); | ||
onur-ozkan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // 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) | ||
| } | ||
| } | ||
|
|
||
| #[async_trait] | ||
|
|
@@ -228,11 +312,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 | ||
onur-ozkan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .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 = lamports_to_big_decimal(lamports, SOLANA_DECIMALS); | ||
|
|
||
| let fee = rpc | ||
| .get_fee_for_message(tx.message()) | ||
shamardy marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| .map_err(|e| WithdrawError::Transport(e.to_string()))?; | ||
|
||
| let fee = lamports_to_big_decimal(fee, SOLANA_DECIMALS); | ||
|
|
||
| let received_by_me = if to == coin.address { | ||
| &amount_dec - &fee | ||
shamardy marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } 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, | ||
onur-ozkan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| block_height: 0, | ||
| timestamp: 0, | ||
shamardy marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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<'_> { | ||
|
|
@@ -381,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, | ||
|
|
@@ -402,11 +554,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()) | ||
onur-ozkan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }; | ||
| Box::new(fut.boxed().compat()) | ||
| } | ||
|
|
||
| #[inline(always)] | ||
|
|
@@ -444,7 +608,7 @@ impl MarketCoinOps for SolanaCoin { | |
|
|
||
| #[inline] | ||
| fn min_tx_amount(&self) -> BigDecimal { | ||
| todo!() | ||
| lamports_to_big_decimal(1, SOLANA_DECIMALS) | ||
| } | ||
|
|
||
| #[inline] | ||
|
|
@@ -564,3 +728,7 @@ impl SwapOps for SolanaCoin { | |
|
|
||
| #[async_trait] | ||
| impl WatcherOps for SolanaCoin {} | ||
|
|
||
| pub(crate) fn lamports_to_big_decimal<T: Into<u32>>(lamports: u64, decimals: T) -> BigDecimal { | ||
| BigDecimal::from(lamports) / BigDecimal::from(10u64.pow(decimals.into())) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.