add Serai JSON-RPC methods ()

* add serai rpc methods

* fix machete & dex quote price api

* fix validators api

---------

Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
This commit is contained in:
akildemir 2025-01-30 12:23:03 +03:00 committed by GitHub
parent e4cc23b72d
commit 11d48d0685
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 393 additions and 42 deletions
Cargo.lock
substrate
client
dex/pallet/src
node
primitives/src
runtime/src
validator-sets/pallet

3
Cargo.lock generated
View file

@ -9421,7 +9421,9 @@ dependencies = [
"jsonrpsee",
"libp2p 0.52.4",
"log",
"monero-wallet",
"pallet-transaction-payment-rpc",
"parity-scale-codec",
"rand_core",
"sc-authority-discovery",
"sc-basic-authorship",
@ -9893,7 +9895,6 @@ dependencies = [
"serai-dex-pallet",
"serai-primitives",
"serai-validator-sets-primitives",
"serde",
"sp-application-crypto",
"sp-consensus-babe",
"sp-core",

View file

@ -50,7 +50,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" }

View file

@ -16,7 +16,8 @@ pub use abi::{primitives, Transaction};
use abi::*;
pub use primitives::{SeraiAddress, Signature, Amount};
use primitives::{Header, ExternalNetworkId};
use primitives::{Header, NetworkId, ExternalNetworkId, QuotePriceParams};
use crate::in_instructions::primitives::Shorthand;
pub mod coins;
pub use coins::SeraiCoins;
@ -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 TemporalSerai<'_> {

View file

@ -179,7 +179,7 @@ impl SeraiValidatorSets<'_> {
&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?

View file

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

View file

@ -999,6 +999,25 @@ 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,
@ -1006,20 +1025,24 @@ 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_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.
@ -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.

View file

@ -52,6 +52,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" }
@ -77,6 +79,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" }

View file

@ -1,4 +1,4 @@
use std::{sync::Arc, collections::HashSet};
use std::{sync::Arc, ops::Deref, collections::HashSet};
use rand_core::{RngCore, OsRng};
@ -7,13 +7,17 @@ use sp_block_builder::BlockBuilder;
use sp_api::ProvideRuntimeApi;
use serai_runtime::{
primitives::{NetworkId, SubstrateAmount, PublicKey},
Nonce, Block, SeraiRuntimeApi,
in_instructions::primitives::Shorthand,
primitives::{ExternalNetworkId, NetworkId, PublicKey, SubstrateAmount, QuotePriceParams},
validator_sets::ValidatorSetsApi,
dex::DexApi,
Block, Nonce,
};
use tokio::sync::RwLock;
use jsonrpsee::RpcModule;
use jsonrpsee::{RpcModule, core::Error};
use scale::Encode;
pub use sc_rpc_api::DenyUnsafe;
use sc_transaction_pool_api::TransactionPool;
@ -40,11 +44,14 @@ pub fn create_full<
where
C::Api: substrate_frame_rpc_system::AccountNonceApi<Block, PublicKey, Nonce>
+ pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi<Block, SubstrateAmount>
+ SeraiRuntimeApi<Block>
+ ValidatorSetsApi<Block>
+ DexApi<Block>
+ BlockBuilder<Block>,
{
use substrate_frame_rpc_system::{System, SystemApiServer};
use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer};
use ciphersuite::{Ciphersuite, Ed25519, Secp256k1};
use bitcoin_serai::{bitcoin, crypto::x_only};
let mut module = RpcModule::new(());
let FullDeps { id, client, pool, deny_unsafe, authority_discovery } = deps;
@ -54,7 +61,7 @@ where
if let Some(authority_discovery) = authority_discovery {
let mut authority_discovery_module =
RpcModule::new((id, client, RwLock::new(authority_discovery)));
RpcModule::new((id, client.clone(), RwLock::new(authority_discovery)));
authority_discovery_module.register_async_method(
"p2p_validators",
|params, context| async move {
@ -63,7 +70,7 @@ where
let latest_block = client.info().best_hash;
let validators = client.runtime_api().validators(latest_block, network).map_err(|_| {
jsonrpsee::core::Error::to_call_error(std::io::Error::other(format!(
Error::to_call_error(std::io::Error::other(format!(
"couldn't get validators from the latest block, which is likely a fatal bug. {}",
"please report this at https://github.com/serai-dex/serai/issues",
)))
@ -99,5 +106,95 @@ where
module.merge(authority_discovery_module)?;
}
let mut serai_json_module = RpcModule::new(client);
// add network address rpc
serai_json_module.register_async_method(
"external_network_address",
|params, context| async move {
let network: ExternalNetworkId = params.parse()?;
let client = &*context;
let latest_block = client.info().best_hash;
let external_key = client
.runtime_api()
.external_network_key(latest_block, network)
.map_err(|_| Error::Custom("api call error".to_string()))?
.ok_or(Error::Custom("no address for the network".to_string()))?;
match network {
ExternalNetworkId::Bitcoin => {
let key = <Secp256k1 as Ciphersuite>::read_G::<&[u8]>(&mut external_key.as_slice())
.map_err(|_| Error::Custom("invalid key stored in db".to_string()))?;
let addr = bitcoin::Address::p2tr_tweaked(
bitcoin::key::TweakedPublicKey::dangerous_assume_tweaked(x_only(&key)),
bitcoin::address::KnownHrp::Mainnet,
);
Ok(addr.to_string())
}
// We don't know the eth address before the smart contract is deployed.
ExternalNetworkId::Ethereum => Ok(String::new()),
ExternalNetworkId::Monero => {
let view_private = zeroize::Zeroizing::new(
<Ed25519 as Ciphersuite>::hash_to_F(
b"Serai DEX Additional Key",
&["Monero".as_bytes(), &0u64.to_le_bytes()].concat(),
)
.0,
);
let spend = <Ed25519 as Ciphersuite>::read_G::<&[u8]>(&mut external_key.as_slice())
.map_err(|_| Error::Custom("invalid key stored in db".to_string()))?;
let addr = monero_wallet::address::MoneroAddress::new(
monero_wallet::address::Network::Mainnet,
monero_wallet::address::AddressType::Featured {
subaddress: false,
payment_id: None,
guaranteed: true,
},
*spend,
view_private.deref() * curve25519_dalek::constants::ED25519_BASEPOINT_TABLE,
);
Ok(addr.to_string())
}
}
},
)?;
// add shorthand encoding rpc
serai_json_module.register_async_method("encoded_shorthand", |params, _| async move {
// decode using serde and encode back using scale
let shorthand: Shorthand = params.parse()?;
Ok(shorthand.encode())
})?;
// add simulating a swap path rpc
serai_json_module.register_async_method("quote_price", |params, context| async move {
let client = &*context;
let latest_block = client.info().best_hash;
let QuotePriceParams { coin1, coin2, amount, include_fee, exact_in } = params.parse()?;
let amount = if exact_in {
client
.runtime_api()
.quote_price_exact_tokens_for_tokens(latest_block, coin1, coin2, amount, include_fee)
.map_err(|_| Error::Custom("api call error".to_string()))?
.ok_or(Error::Custom("invalid params or empty pool".to_string()))?
} else {
client
.runtime_api()
.quote_price_tokens_for_exact_tokens(latest_block, coin1, coin2, amount, include_fee)
.map_err(|_| Error::Custom("api call error".to_string()))?
.ok_or(Error::Custom("invalid params or empty pool".to_string()))?
};
Ok(amount)
})?;
module.merge(serai_json_module)?;
Ok(module)
}

View file

@ -0,0 +1,19 @@
#[cfg(feature = "borsh")]
use borsh::{BorshSerialize, BorshDeserialize};
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
use scale::{Encode, Decode, MaxEncodedLen};
use crate::{Coin, SubstrateAmount};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen)]
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct QuotePriceParams {
pub coin1: Coin,
pub coin2: Coin,
pub amount: SubstrateAmount,
pub include_fee: bool,
pub exact_in: bool,
}

View file

@ -40,6 +40,10 @@ pub use account::*;
mod constants;
pub use constants::*;
mod dex;
#[allow(unused_imports)]
pub use dex::*;
pub type BlockNumber = u64;
pub type Header = sp_runtime::generic::Header<BlockNumber, sp_runtime::traits::BlakeTwo256>;

View file

@ -53,7 +53,7 @@ use sp_runtime::{
#[allow(unused_imports)]
use primitives::{
NetworkId, PublicKey, AccountLookup, SubstrateAmount, Coin, EXTERNAL_NETWORKS,
NetworkId, ExternalNetworkId, PublicKey, AccountLookup, SubstrateAmount, Coin, EXTERNAL_NETWORKS,
MEDIAN_PRICE_WINDOW_LENGTH, HOURS, DAYS, MINUTES, TARGET_BLOCK_TIME, BLOCK_SIZE,
FAST_EPOCH_DURATION,
};
@ -374,13 +374,6 @@ mod benches {
);
}
sp_api::decl_runtime_apis! {
#[api_version(1)]
pub trait SeraiRuntimeApi {
fn validators(network_id: NetworkId) -> Vec<PublicKey>;
}
}
sp_api::impl_runtime_apis! {
impl sp_api::Core<Block> for Runtime {
fn version() -> RuntimeVersion {
@ -589,7 +582,7 @@ sp_api::impl_runtime_apis! {
}
}
impl crate::SeraiRuntimeApi<Block> for Runtime {
impl validator_sets::ValidatorSetsApi<Block> for Runtime {
fn validators(network_id: NetworkId) -> Vec<PublicKey> {
if network_id == NetworkId::Serai {
Babe::authorities()
@ -604,6 +597,10 @@ sp_api::impl_runtime_apis! {
)
}
}
fn external_network_key(network: ExternalNetworkId) -> Option<Vec<u8>> {
ValidatorSets::external_network_key(network)
}
}
impl dex::DexApi<Block> for Runtime {

View file

@ -26,6 +26,7 @@ serde = { version = "1", default-features = false, features = ["derive", "alloc"
sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-io = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-api = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-application-crypto = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-session = { git = "https://github.com/serai-dex/substrate", default-features = false }
@ -65,6 +66,7 @@ std = [
"sp-core/std",
"sp-io/std",
"sp-std/std",
"sp-api/std",
"sp-application-crypto/std",
"sp-runtime/std",
"sp-session/std",

View file

@ -994,6 +994,14 @@ pub mod pallet {
false
}
}
/// Returns the external network key for a given external network
pub fn external_network_key(network: ExternalNetworkId) -> Option<Vec<u8>> {
let current_session = Self::session(NetworkId::from(network))?;
let keys = Keys::<T>::get(ExternalValidatorSet { network, session: current_session })?;
Some(keys.1.into_inner())
}
}
#[pallet::call]
@ -1365,4 +1373,17 @@ pub mod pallet {
}
}
sp_api::decl_runtime_apis! {
#[api_version(1)]
pub trait ValidatorSetsApi {
/// Returns the validator set for a given network.
fn validators(network_id: NetworkId) -> Vec<PublicKey>;
/// Returns the external network key for a given external network.
fn external_network_key(
network: ExternalNetworkId,
) -> Option<Vec<u8>>;
}
}
pub use pallet::*;