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
21 changes: 21 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 = [
"rpc",
"examples/template",
"examples/coins-chain",
"perpetuals",
]
resolver = "2"

Expand All @@ -28,6 +29,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-rpc = { version = "2026.5.0", path = "rpc" }
nunchi-perpetuals = { version = "2026.5.0", path = "perpetuals" }
nunchi-template = { version = "2026.5.0", path = "examples/template" }
nunchi-coins-chain = { version = "2026.5.0", path = "examples/coins-chain" }
commonware-actor = "2026.5.0"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ This repository will contain modules for building public and private blockchains

### Financial Primitives

* [`perpetuals`](perpetuals/) - defines perpetual swap markets, leveraged positions, liquidation checks, and RPC queries
* `margin` - user has BTC + nunchi and doesn't want to sell, and deposits BTC+nunchi and gets a stablecoin. Could be backed by other coins, not just btc and nunchi.
* `securities` - Non-synthetic perps contracts (delivery of tokenized stock)
* `vaults` - a module for running vaults composed of many types of capital, traded by an authorised offchain party
* `clob` - used on the global chain, provides liquidity between local chain tokens
* `derivatives` - ingests a price feed and creates derivatives products
* `stablecoin` - a wrapper of coins special for the needs of stablecoins

28 changes: 28 additions & 0 deletions perpetuals/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "nunchi-perpetuals"
version.workspace = true
edition.workspace = true
license.workspace = true

[features]
default = ["rpc"]
rpc = ["dep:nunchi-rpc", "dep:jsonrpsee", "dep:futures"]

[dependencies]
async-trait = { workspace = true }
bytes = { workspace = true }
thiserror = { workspace = true }
nunchi-coins = { workspace = true }
nunchi-common = { workspace = true }
nunchi-crypto = { workspace = true }
nunchi-rpc = { workspace = true, optional = true }
commonware-codec = { workspace = true }
commonware-cryptography = { workspace = true }
commonware-formatting = { workspace = true }
jsonrpsee = { workspace = true, optional = true }
serde = { workspace = true }
futures = { workspace = true, optional = true }

[dev-dependencies]
commonware-runtime = { workspace = true }
serde_json = { workspace = true }
149 changes: 149 additions & 0 deletions perpetuals/src/db.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//! Persistence layer for the perpetuals module.

use crate::ledger::LedgerError;
use crate::{Market, MarketId, Position, PositionId, PERPETUALS_NAMESPACE};
use async_trait::async_trait;
use commonware_codec::{Encode, Read, ReadExt};
use commonware_cryptography::sha256::Digest;
use nunchi_common::state_db::{Namespace, StateStore};
use nunchi_common::Address;

const NS: Namespace = Namespace::new(PERPETUALS_NAMESPACE);

#[repr(u8)]
#[derive(Clone, Copy)]
enum Table {
Account = 0,
MarketNonce = 1,
PositionNonce = 2,
Market = 3,
Position = 4,
}

impl From<Table> for u8 {
fn from(table: Table) -> Self {
table as Self
}
}

fn encoded<T: Encode>(value: &T) -> Vec<u8> {
value.encode().as_ref().to_vec()
}

fn decoded<T: Read<Cfg = ()>>(bytes: &[u8]) -> Result<T, LedgerError> {
let mut buf = bytes;
T::read(&mut buf).map_err(|err| LedgerError::Storage(err.to_string()))
}

#[async_trait]
pub trait PerpetualDB {
async fn nonce(&self, id: &Address) -> Result<u64, LedgerError>;

fn set_nonce(&mut self, id: &Address, nonce: u64);

async fn market_nonce(&self) -> Result<u64, LedgerError>;

fn set_market_nonce(&mut self, nonce: u64);

async fn position_nonce(&self) -> Result<u64, LedgerError>;

fn set_position_nonce(&mut self, nonce: u64);

async fn market(&self, market: &MarketId) -> Result<Option<Market>, LedgerError>;

fn set_market(&mut self, market: &Market);

async fn position(&self, position: &PositionId) -> Result<Option<Position>, LedgerError>;

fn set_position(&mut self, position: &Position);

fn remove_position(&mut self, position: &PositionId);
}

#[async_trait]
impl<S: StateStore + Send + Sync> PerpetualDB for S {
async fn nonce(&self, id: &Address) -> Result<u64, LedgerError> {
let key = NS.key(Table::Account, &encoded(id));
match StateStore::get(self, &key)
.await
.map_err(|err| LedgerError::Storage(err.to_string()))?
{
Some(bytes) => decoded::<u64>(&bytes),
None => Ok(0),
}
}

fn set_nonce(&mut self, id: &Address, nonce: u64) {
let key = NS.key(Table::Account, &encoded(id));
StateStore::set(self, key, encoded(&nonce));
}

async fn market_nonce(&self) -> Result<u64, LedgerError> {
let key = NS.key(Table::MarketNonce, &[]);
match StateStore::get(self, &key)
.await
.map_err(|err| LedgerError::Storage(err.to_string()))?
{
Some(bytes) => decoded::<u64>(&bytes),
None => Ok(0),
}
}

fn set_market_nonce(&mut self, nonce: u64) {
let key = NS.key(Table::MarketNonce, &[]);
StateStore::set(self, key, encoded(&nonce));
}

async fn position_nonce(&self) -> Result<u64, LedgerError> {
let key = NS.key(Table::PositionNonce, &[]);
match StateStore::get(self, &key)
.await
.map_err(|err| LedgerError::Storage(err.to_string()))?
{
Some(bytes) => decoded::<u64>(&bytes),
None => Ok(0),
}
}

fn set_position_nonce(&mut self, nonce: u64) {
let key = NS.key(Table::PositionNonce, &[]);
StateStore::set(self, key, encoded(&nonce));
}

async fn market(&self, market: &MarketId) -> Result<Option<Market>, LedgerError> {
let key = NS.key(Table::Market, &encoded(market));
match StateStore::get(self, &key)
.await
.map_err(|err| LedgerError::Storage(err.to_string()))?
{
Some(bytes) => Ok(Some(decoded::<Market>(&bytes)?)),
None => Ok(None),
}
}

fn set_market(&mut self, market: &Market) {
let key = NS.key(Table::Market, &encoded(&market.id));
StateStore::set(self, key, encoded(market));
}

async fn position(&self, position: &PositionId) -> Result<Option<Position>, LedgerError> {
let key = NS.key(Table::Position, &encoded(position));
match StateStore::get(self, &key)
.await
.map_err(|err| LedgerError::Storage(err.to_string()))?
{
Some(bytes) => Ok(Some(decoded::<Position>(&bytes)?)),
None => Ok(None),
}
}

fn set_position(&mut self, position: &Position) {
let key = NS.key(Table::Position, &encoded(&position.id));
StateStore::set(self, key, encoded(position));
}

fn remove_position(&mut self, position: &PositionId) {
let key: Digest = NS.key(Table::Position, &encoded(position));
StateStore::remove(self, key);
}
}
Loading
Loading