Skip to content

Commit

Permalink
add serai rpc methods
Browse files Browse the repository at this point in the history
  • Loading branch information
akildemir committed Oct 26, 2024
1 parent f3d20e6 commit 2158993
Show file tree
Hide file tree
Showing 12 changed files with 398 additions and 40 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion substrate/client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ hex = "0.4"

blake2 = "0.10"

ciphersuite = { path = "../../crypto/ciphersuite", features = ["ristretto"] }
ciphersuite = { path = "../../crypto/ciphersuite", features = ["ristretto", "secp256k1"] }
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] }
schnorrkel = { path = "../../crypto/schnorrkel", package = "frost-schnorrkel" }

Expand Down
21 changes: 20 additions & 1 deletion substrate/client/src/serai/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ pub use abi::{primitives, Transaction};
use abi::*;

pub use primitives::{SeraiAddress, Signature, Amount};
use primitives::{Header, NetworkId};
use primitives::{Header, NetworkId, ExternalNetworkId, QuotePriceParams};
use crate::in_instructions::primitives::Shorthand;

pub mod coins;
pub use coins::SeraiCoins;
Expand Down Expand Up @@ -317,6 +318,24 @@ impl Serai {
) -> Result<Vec<multiaddr::Multiaddr>, SeraiError> {
self.call("p2p_validators", network).await
}

// TODO: move this to SeraiValidatorSets?
pub async fn external_network_address(
&self,
network: ExternalNetworkId,
) -> Result<String, SeraiError> {
self.call("external_network_address", network).await
}

// TODO: move this to SeraiInInstructions?
pub async fn encoded_shorthand(&self, shorthand: Shorthand) -> Result<Vec<u8>, SeraiError> {
self.call("encoded_shorthand", shorthand).await
}

// TODO: move this to SeraiDex?
pub async fn quote_price(&self, params: QuotePriceParams) -> Result<u64, SeraiError> {
self.call("quote_price", params).await
}
}

impl<'a> TemporalSerai<'a> {
Expand Down
158 changes: 158 additions & 0 deletions substrate/client/tests/serai-rpc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use std::str::FromStr;

use scale::Decode;
use zeroize::Zeroizing;

use ciphersuite::{
group::{ff::Field, GroupEncoding},
Ciphersuite, Ed25519, Secp256k1,
};

use sp_core::{
Pair as PairTrait,
sr25519::{Public, Pair},
};

use serai_abi::{
in_instructions::primitives::Shorthand,
primitives::{
insecure_pair_from_name, ExternalBalance, ExternalCoin, ExternalNetworkId, QuotePriceParams,
Amount,
},
validator_sets::primitives::{ExternalValidatorSet, KeyPair, Session},
};
use serai_client::{Serai, SeraiAddress};

use rand_core::{RngCore, OsRng};

mod common;
use common::{validator_sets::set_keys, in_instructions::mint_coin, dex::add_liquidity};

serai_test!(
external_address: (|serai: Serai| async move {
test_external_address(serai).await;
})

encoded_shorthand: (|serai: Serai| async move {
test_encoded_shorthand(serai).await;
})

dex_quote_price: (|serai: Serai| async move {
test_dex_quote_price(serai).await;
})
);

async fn set_network_keys<C: Ciphersuite>(
serai: &Serai,
set: ExternalValidatorSet,
pairs: &[Pair],
) {
// Ristretto key
let mut ristretto_key = [0; 32];
OsRng.fill_bytes(&mut ristretto_key);

// network key
let network_priv_key = Zeroizing::new(C::F::random(&mut OsRng));
let network_key = (C::generator() * *network_priv_key).to_bytes().as_ref().to_vec();

let key_pair = KeyPair(Public(ristretto_key), network_key.try_into().unwrap());
let _ = set_keys(serai, set, key_pair, pairs).await;
}

async fn test_external_address(serai: Serai) {
let pair = insecure_pair_from_name("Alice");

// set btc keys
let network = ExternalNetworkId::Bitcoin;
set_network_keys::<Secp256k1>(
&serai,
ExternalValidatorSet { session: Session(0), network },
&[pair.clone()],
)
.await;

// get the address from the node
let btc_address: String = serai.external_network_address(network).await.unwrap();

// make sure it is a valid address
let _ = bitcoin::Address::from_str(&btc_address)
.unwrap()
.require_network(bitcoin::Network::Bitcoin)
.unwrap();

// set monero keys
let network = ExternalNetworkId::Monero;
set_network_keys::<Ed25519>(
&serai,
ExternalValidatorSet { session: Session(0), network },
&[pair],
)
.await;

// get the address from the node
let xmr_address: String = serai.external_network_address(network).await.unwrap();

// make sure it is a valid address
let _ = monero_wallet::address::MoneroAddress::from_str(
monero_wallet::address::Network::Mainnet,
&xmr_address,
)
.unwrap();
}

async fn test_encoded_shorthand(serai: Serai) {
let shorthand = Shorthand::transfer(None, SeraiAddress::new([0u8; 32]));
let encoded = serai.encoded_shorthand(shorthand.clone()).await.unwrap();

assert_eq!(Shorthand::decode::<&[u8]>(&mut encoded.as_slice()).unwrap(), shorthand);
}

async fn test_dex_quote_price(serai: Serai) {
// make a liquid pool to get the quote on
let coin1 = ExternalCoin::Bitcoin;
let coin2 = ExternalCoin::Monero;
let amount1 = Amount(10u64.pow(coin1.decimals()));
let amount2 = Amount(10u64.pow(coin2.decimals()));
let pair = insecure_pair_from_name("Ferdie");

// mint sriBTC in the account so that we can add liq.
// Ferdie account is already pre-funded with SRI.
mint_coin(
&serai,
ExternalBalance { coin: coin1, amount: amount1 },
0,
pair.clone().public().into(),
)
.await;

// add liquidity
let coin_amount = Amount(amount1.0 / 2);
let sri_amount = Amount(amount1.0 / 2);
let _ = add_liquidity(&serai, coin1, coin_amount, sri_amount, 0, pair.clone()).await;

// same for xmr
mint_coin(
&serai,
ExternalBalance { coin: coin2, amount: amount2 },
0,
pair.clone().public().into(),
)
.await;

// add liquidity
let coin_amount = Amount(amount2.0 / 2);
let sri_amount = Amount(amount2.0 / 2);
let _ = add_liquidity(&serai, coin2, coin_amount, sri_amount, 1, pair.clone()).await;

// price for BTC -> SRI -> XMR path
let params = QuotePriceParams {
coin1: coin1.into(),
coin2: coin2.into(),
amount: coin_amount.0 / 2,
include_fee: true,
exact_in: true,
};

let res = serai.quote_price(params).await.unwrap();
assert!(res > 0);
}
70 changes: 48 additions & 22 deletions substrate/dex/pallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -999,27 +999,50 @@ pub mod pallet {
Ok(amounts)
}

fn get_swap_path_from_coins(
coin1: Coin,
coin2: Coin,
) -> Option<BoundedVec<Coin, T::MaxSwapPathLength>> {
if coin1 == coin2 {
return None;
}

let path = if coin1 == Coin::native() {
vec![coin2, coin1]
} else if coin2 == Coin::native() {
vec![coin1, coin2]
} else {
vec![coin1, Coin::native(), coin2]
};

Some(path.try_into().unwrap())
}

/// Used by the RPC service to provide current prices.
pub fn quote_price_exact_tokens_for_tokens(
coin1: Coin,
coin2: Coin,
amount: SubstrateAmount,
include_fee: bool,
) -> Option<SubstrateAmount> {
let pool_id = Self::get_pool_id(coin1, coin2).ok()?;
let pool_account = Self::get_pool_account(pool_id);
let path = Self::get_swap_path_from_coins(coin1, coin2)?;

let balance1 = Self::get_balance(&pool_account, coin1);
let balance2 = Self::get_balance(&pool_account, coin2);
if balance1 != 0 {
if include_fee {
Self::get_amount_out(amount, balance1, balance2).ok()
} else {
Self::quote(amount, balance1, balance2).ok()
let mut amounts: Vec<SubstrateAmount> = vec![amount];
for coins_pair in path.windows(2) {
if let [coin1, coin2] = coins_pair {
let (reserve_in, reserve_out) = Self::get_reserves(coin1, coin2).ok()?;
let prev_amount = amounts.last().expect("Always has at least one element");
let amount_out = if include_fee {
Self::get_amount_out(*prev_amount, reserve_in, reserve_out).ok()?
} else {
Self::quote(*prev_amount, reserve_in, reserve_out).ok()?
};

amounts.push(amount_out);
}
} else {
None
}

Some(*amounts.last().unwrap())
}

/// Used by the RPC service to provide current prices.
Expand All @@ -1029,20 +1052,23 @@ pub mod pallet {
amount: SubstrateAmount,
include_fee: bool,
) -> Option<SubstrateAmount> {
let pool_id = Self::get_pool_id(coin1, coin2).ok()?;
let pool_account = Self::get_pool_account(pool_id);
let path = Self::get_swap_path_from_coins(coin1, coin2)?;

let balance1 = Self::get_balance(&pool_account, coin1);
let balance2 = Self::get_balance(&pool_account, coin2);
if balance1 != 0 {
if include_fee {
Self::get_amount_in(amount, balance1, balance2).ok()
} else {
Self::quote(amount, balance2, balance1).ok()
let mut amounts: Vec<SubstrateAmount> = vec![amount];
for coins_pair in path.windows(2).rev() {
if let [coin1, coin2] = coins_pair {
let (reserve_in, reserve_out) = Self::get_reserves(coin1, coin2).ok()?;
let prev_amount = amounts.last().expect("Always has at least one element");
let amount_in = if include_fee {
Self::get_amount_in(*prev_amount, reserve_in, reserve_out).ok()?
} else {
Self::quote(*prev_amount, reserve_out, reserve_in).ok()?
};
amounts.push(amount_in);
}
} else {
None
}

Some(*amounts.last().unwrap())
}

/// Calculates the optimal amount from the reserves.
Expand Down
8 changes: 8 additions & 0 deletions substrate/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ futures-util = "0.3"
tokio = { version = "1", features = ["sync", "rt-multi-thread"] }
jsonrpsee = { version = "0.16", features = ["server"] }

scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
serde = { version = "1", default-features = false, features = ["derive", "alloc"] }

sc-offchain = { git = "https://github.com/serai-dex/substrate" }
sc-transaction-pool = { git = "https://github.com/serai-dex/substrate" }
sc-transaction-pool-api = { git = "https://github.com/serai-dex/substrate" }
Expand All @@ -73,6 +76,11 @@ pallet-transaction-payment-rpc = { git = "https://github.com/serai-dex/substrate

serai-env = { path = "../../common/env" }

bitcoin-serai = { path = "../../networks/bitcoin", default-features = false, features = ["std", "hazmat"] }
monero-wallet = { path = "../../networks/monero/wallet", default-features = false, features = ["std"] }
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["ed25519", "secp256k1"] }
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }

[build-dependencies]
substrate-build-script-utils = { git = "https://github.com/serai-dex/substrate" }

Expand Down
Loading

0 comments on commit 2158993

Please sign in to comment.