Skip to content
Closed
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
28 changes: 28 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"xtask",
"mempool",
"oracle",
"perpetuals",
"rpc",
"mcp",
"examples/bridge-chain",
Expand Down Expand Up @@ -44,6 +45,7 @@ nunchi-crypto = { version = "2026.5.0", path = "crypto" }
nunchi-dkg = { version = "2026.5.0", path = "dkg" }
nunchi-mempool = { version = "2026.5.0", path = "mempool" }
nunchi-oracle = { version = "2026.5.0", path = "oracle" }
nunchi-perpetuals = { version = "2026.5.0", path = "perpetuals" }
nunchi-rpc = { version = "2026.5.0", path = "rpc" }
nunchi-coins-chain = { version = "2026.5.0", path = "examples/coins/chain" }
nunchi-bridge-chain = { version = "2026.5.0", path = "examples/bridge-chain" }
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ This repository will contain modules for building public and private blockchains
* `nunchi-securities` - Non-synthetic perps contracts (delivery of tokenized stock)
* `nunchi-vaults` - a module for running vaults composed of many types of capital, traded by an authorised offchain party
* `nunchi-clob` - used on the global chain, provides liquidity between local chain tokens
* [`nunchi-perpetuals`](perpetuals/) - oracle-backed isolated-margin perpetual futures
* `nunchi-derivatives` - ingests a price feed and creates derivatives products
* `nunchi-stablecoin` - a wrapper of coins special for the needs of stablecoins

Expand Down
1 change: 1 addition & 0 deletions examples/coins/chain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ nunchi-crypto = { workspace = true }
nunchi-dkg = { workspace = true }
nunchi-mempool = { workspace = true }
nunchi-oracle = { workspace = true }
nunchi-perpetuals = { workspace = true, features = ["mempool"] }
nunchi-rpc = { workspace = true }
commonware-actor = { workspace = true }
commonware-broadcast = { workspace = true }
Expand Down
34 changes: 32 additions & 2 deletions examples/coins/chain/src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ use jsonrpsee::core::async_trait;
use nunchi_coins::{rpc::CoinQuery, Address, CoinId, Ledger, LedgerError, TokenDefinition};
use nunchi_common::QmdbReader;
use nunchi_mempool::MempoolHandle;
use nunchi_perpetuals::{
rpc::PerpetualQuery, Market, MarketId, PerpetualError, PerpetualLedger, Position, PositionId,
};

pub use nunchi_chain::SharedAppliedHeight;

Expand Down Expand Up @@ -39,8 +42,8 @@ where
}
}

/// A read-only coin query backend over this node's committed databases, suitable for
/// serving the coin RPC (see [`crate::rpc::module`]).
/// A read-only query backend over this node's committed databases, suitable for
/// serving the chain RPC (see [`crate::rpc::module`]).
pub fn query(&self) -> StatefulQuery<E> {
StatefulQuery::new(self.stateful.clone())
}
Expand Down Expand Up @@ -76,6 +79,10 @@ where
async fn ledger(&self) -> Ledger<QmdbReader<E>> {
Ledger::new(QmdbReader::new(self.stateful.subscribe_databases().await))
}

async fn perpetuals(&self) -> PerpetualLedger<QmdbReader<E>> {
PerpetualLedger::new(QmdbReader::new(self.stateful.subscribe_databases().await))
}
}

#[async_trait]
Expand All @@ -100,3 +107,26 @@ where
Ok(QmdbReader::new(db).root().await)
}
}

#[async_trait]
impl<E> PerpetualQuery for StatefulQuery<E>
where
E: Context + Spawner + Metrics + Clock + rand::Rng + Send + Sync + 'static,
{
async fn nonce(&self, account: Address) -> Result<u64, PerpetualError> {
self.perpetuals().await.nonce(&account).await
}

async fn market(&self, market: MarketId) -> Result<Option<Market>, PerpetualError> {
self.perpetuals().await.market(&market).await
}

async fn position(&self, position: PositionId) -> Result<Option<Position>, PerpetualError> {
self.perpetuals().await.position(&position).await
}

async fn state_root(&self) -> Result<Digest, PerpetualError> {
let db = self.stateful.subscribe_databases().await;
Ok(QmdbReader::new(db).root().await)
}
}
10 changes: 10 additions & 0 deletions examples/coins/chain/src/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use nunchi_common::{
CommitState, Namespace, Overlay, QmdbConfig, QmdbState, StateError, StateStore,
};
use nunchi_oracle::{OracleGenesis, OracleLedger};
use nunchi_perpetuals::{PerpetualLedger, PerpetualsGenesis};
use serde::{Deserialize, Serialize};
use std::{fs, path::Path};
use thiserror::Error;
Expand All @@ -34,6 +35,8 @@ pub struct ChainGenesis {
pub coins: Option<CoinsGenesis>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub oracle: Option<OracleGenesis>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub perpetuals: Option<PerpetualsGenesis>,
}

#[derive(Debug, Error)]
Expand All @@ -48,6 +51,8 @@ pub enum GenesisError {
Coins(#[from] nunchi_coins::LedgerError),
#[error("oracle genesis error: {0}")]
Oracle(#[from] nunchi_oracle::OracleError),
#[error("perpetuals genesis error: {0}")]
Perpetuals(#[from] nunchi_perpetuals::PerpetualError),
#[error("state error: {0}")]
State(#[from] StateError),
#[error("existing chain state was initialized with a different genesis")]
Expand Down Expand Up @@ -105,6 +110,11 @@ impl ChainGenesis {
ledger.apply_genesis(oracle).await?;
overlay = ledger.into_inner();
}
if let Some(perpetuals) = &self.perpetuals {
let mut ledger = PerpetualLedger::new(overlay);
ledger.apply_genesis(perpetuals).await?;
overlay = ledger.into_inner();
}
set_genesis_marker(&mut overlay, fingerprint);
overlay.commit();
state.commit().await?;
Expand Down
39 changes: 27 additions & 12 deletions examples/coins/chain/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ use jsonrpsee::{
core::{RegisterMethodError, RpcResult},
RpcModule,
};
use nunchi_coins::rpc::{CoinQuery, CoinsMempoolRpc, CoinsRpc, MempoolIngress};
use nunchi_coins::rpc::{
CoinQuery, CoinsMempoolRpc, CoinsRpc, MempoolIngress as CoinMempoolIngress,
};
use nunchi_coins::Transaction as CoinTransaction;
use nunchi_mempool::{AdmissionError, MempoolHandle, TxStatus};
use nunchi_perpetuals::rpc::{
MempoolIngress as PerpetualMempoolIngress, PerpetualQuery, PerpetualsMempoolRpc, PerpetualsRpc,
};
use nunchi_perpetuals::Transaction as PerpetualTransaction;
use nunchi_rpc::{encode_hex, module_error, RpcBuildError, RpcRouter};
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -59,7 +65,7 @@ impl ChainMempoolIngress {
}

#[jsonrpsee::core::async_trait]
impl MempoolIngress for ChainMempoolIngress {
impl CoinMempoolIngress for ChainMempoolIngress {
async fn submit(&self, transaction: CoinTransaction) -> Result<Digest, AdmissionError> {
self.mempool.submit(transaction.into()).await
}
Expand All @@ -69,6 +75,17 @@ impl MempoolIngress for ChainMempoolIngress {
}
}

#[jsonrpsee::core::async_trait]
impl PerpetualMempoolIngress for ChainMempoolIngress {
async fn submit(&self, transaction: PerpetualTransaction) -> Result<Digest, AdmissionError> {
self.mempool.submit(transaction.into()).await
}

async fn status(&self, digest: Digest) -> Option<TxStatus> {
self.mempool.status(digest).await
}
}

/// Build the complete coins-chain RPC module.
///
/// Downstream applications can follow this pattern: create one router over their node context,
Expand All @@ -80,14 +97,14 @@ pub fn module<Q>(
applied_height: SharedAppliedHeight,
) -> Result<RpcModule<RpcContext<Q>>, RpcBuildError>
where
Q: CoinQuery,
Q: CoinQuery + PerpetualQuery,
{
let ingress = ChainMempoolIngress::new(mempool);
let mut router = RpcRouter::new(RpcContext::new(query.clone(), applied_height));
nunchi_coins::rpc::register(&mut router, CoinsRpc::new(query))?;
nunchi_coins::rpc::register_mempool(
&mut router,
CoinsMempoolRpc::new(ChainMempoolIngress::new(mempool)),
)?;
nunchi_coins::rpc::register(&mut router, CoinsRpc::new(query.clone()))?;
nunchi_coins::rpc::register_mempool(&mut router, CoinsMempoolRpc::new(ingress.clone()))?;
nunchi_perpetuals::rpc::register(&mut router, PerpetualsRpc::new(query))?;
nunchi_perpetuals::rpc::register_mempool(&mut router, PerpetualsMempoolRpc::new(ingress))?;
router.merge(chain_module(router.context())?)?;
Ok(router.into_module())
}
Expand All @@ -96,13 +113,11 @@ fn chain_module<Q>(
context: std::sync::Arc<RpcContext<Q>>,
) -> Result<RpcModule<RpcContext<Q>>, RegisterMethodError>
where
Q: CoinQuery,
Q: CoinQuery + PerpetualQuery,
{
let mut module = RpcModule::from_arc(context);
module.register_async_method("chain.status", |_raw, context, _| async move {
let state_root = context
.query
.state_root()
let state_root = CoinQuery::state_root(&context.query)
.await
.map_err(|err| module_error(format!("failed to read state root: {err}")))?;
let applied_height = *context.applied_height.lock().await;
Expand Down
11 changes: 11 additions & 0 deletions examples/coins/chain/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use nunchi_authority::{AuthorityError, AuthorityLedger};
use nunchi_coins::{Ledger, LedgerError};
use nunchi_common::{EventSink, NoopEventSink, Runtime, RuntimeContext, StateStore};
use nunchi_oracle::{OracleError, OracleLedger};
use nunchi_perpetuals::{PerpetualError, PerpetualLedger};

use crate::Transaction;

Expand All @@ -18,6 +19,8 @@ pub enum RuntimeError {
Authority(#[from] AuthorityError),
#[error("oracle module error: {0}")]
Oracle(#[from] OracleError),
#[error("perpetuals module error: {0}")]
Perpetuals(#[from] PerpetualError),
}

impl RuntimeError {
Expand All @@ -27,6 +30,8 @@ impl RuntimeError {
Self::Coins(LedgerError::Storage(_))
| Self::Authority(AuthorityError::Storage(_))
| Self::Oracle(OracleError::Storage(_))
| Self::Perpetuals(PerpetualError::Storage(_))
| Self::Perpetuals(PerpetualError::Coin(LedgerError::Storage(_)))
)
}
}
Expand Down Expand Up @@ -87,6 +92,10 @@ where
let mut ledger = OracleLedger::new(state);
ledger.apply_transaction(transaction, context).await?;
}
Transaction::Perpetual(transaction) => {
let mut ledger = PerpetualLedger::new(state);
ledger.apply_transaction(transaction, context).await?;
}
}
Ok(())
}
Expand All @@ -100,9 +109,11 @@ mod tests {
assert!(RuntimeError::Coins(LedgerError::Storage("disk".into())).is_storage());
assert!(RuntimeError::Authority(AuthorityError::Storage("disk".into())).is_storage());
assert!(RuntimeError::Oracle(OracleError::Storage("disk".into())).is_storage());
assert!(RuntimeError::Perpetuals(PerpetualError::Storage("disk".into())).is_storage());

assert!(!RuntimeError::Authority(AuthorityError::NotConfigured).is_storage());
assert!(!RuntimeError::Coins(LedgerError::InvalidTokenSpec("bad")).is_storage());
assert!(!RuntimeError::Oracle(OracleError::PayloadTooLarge).is_storage());
assert!(!RuntimeError::Perpetuals(PerpetualError::Unauthorized).is_storage());
}
}
1 change: 1 addition & 0 deletions examples/coins/chain/src/tests/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ fn sample_genesis() -> ChainGenesis {
}],
}),
oracle: Some(OracleGenesis {}),
perpetuals: None,
}
}

Expand Down
19 changes: 19 additions & 0 deletions examples/coins/chain/src/tests/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use commonware_cryptography::{ed25519, Hasher, Sha256, Signer as _};
use nunchi_authority::MultisigPolicy;
use nunchi_coins::{CoinSpec, PrivateKey, TokenName, TokenSymbol};
use nunchi_oracle::{IntervalKey, NamespaceId, OracleOperation, Transaction as OracleTransaction};
use nunchi_perpetuals::{PerpetualOperation, Side, Transaction as PerpetualTransaction};

use crate::transaction::*;

Expand Down Expand Up @@ -59,22 +60,40 @@ fn oracle_transaction(seed: u64, nonce: u64) -> OracleTransaction {
)
}

fn perpetual_transaction(seed: u64, nonce: u64) -> PerpetualTransaction {
let signer = nunchi_crypto::PrivateKey::ed25519_from_seed(seed);
PerpetualTransaction::sign(
&signer,
nonce,
PerpetualOperation::OpenPosition {
market: commonware_cryptography::Sha256::hash(b"btc-usd-perp"),
side: Side::Long,
collateral: 1_000,
leverage_bps: 50_000,
},
)
}

#[test]
fn transaction_codec_uses_stable_tags() {
let coin = Transaction::from(coin_transaction(1, 3));
let authority = Transaction::from(authority_transaction(2, 4));
let oracle = Transaction::from(oracle_transaction(3, 5));
let perpetual = Transaction::from(perpetual_transaction(4, 6));

let coin_encoded = coin.encode();
let authority_encoded = authority.encode();
let oracle_encoded = oracle.encode();
let perpetual_encoded = perpetual.encode();

assert_eq!(coin_encoded[0], TX_COIN);
assert_eq!(authority_encoded[0], TX_AUTHORITY);
assert_eq!(oracle_encoded[0], TX_ORACLE);
assert_eq!(perpetual_encoded[0], TX_PERPETUAL);
assert_eq!(Transaction::decode(coin_encoded).unwrap(), coin);
assert_eq!(Transaction::decode(authority_encoded).unwrap(), authority);
assert_eq!(Transaction::decode(oracle_encoded).unwrap(), oracle);
assert_eq!(Transaction::decode(perpetual_encoded).unwrap(), perpetual);
assert!(Transaction::decode([99].as_slice()).is_err());
}

Expand Down
7 changes: 7 additions & 0 deletions examples/coins/chain/src/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use nunchi_authority::{AuthorityOperation, Transaction as AuthorityTransaction};
use nunchi_coins::{CoinOperation, Transaction as CoinTransaction};
use nunchi_oracle::{OracleOperation, Transaction as OracleTransaction};
use nunchi_perpetuals::{PerpetualOperation, Transaction as PerpetualTransaction};

pub(crate) const TX_COIN: u8 = 0;
pub(crate) const TX_AUTHORITY: u8 = 1;
pub(crate) const TX_ORACLE: u8 = 2;
pub(crate) const TX_PERPETUAL: u8 = 3;

nunchi_chain::transaction_wrapper! {
pub enum Transaction {
Expand All @@ -23,5 +25,10 @@ nunchi_chain::transaction_wrapper! {
transaction: OracleTransaction,
operation: OracleOperation,
},
Perpetual {
tag: TX_PERPETUAL,
transaction: PerpetualTransaction,
operation: PerpetualOperation,
},
}
}
Loading
Loading