Skip to content
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

add Serai JSON-RPC methods #627

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 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
2 changes: 1 addition & 1 deletion substrate/client/src/serai/validator_sets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ impl<'a> SeraiValidatorSets<'a> {
&self,
network: NetworkId,
) -> Result<Vec<Public>, SeraiError> {
self.0.runtime_api("SeraiRuntimeApi_validators", network).await
self.0.runtime_api("ValidatorSetsApi_validators", network).await
}

// TODO: Store these separately since we almost never need both at once?
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() && coin2 != Coin::native()) ||
(coin2 == Coin::native() && coin1 != 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
7 changes: 7 additions & 0 deletions substrate/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ 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"] }

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 +75,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
Loading