use {
crate::types::{MarginMode, Order, PerpPosition, SpotPosition},
carbon_core::{
account::AccountMetadata,
postgres::{
metadata::AccountRowMetadata,
primitives::{Pubkey, U16, U32, U64, U8},
},
},
};
#[derive(sqlx::FromRow, Debug, Clone)]
pub struct UserRow {
#[sqlx(flatten)]
pub account_metadata: AccountRowMetadata,
pub authority: Pubkey,
pub delegate: Pubkey,
pub name: Vec<u8>,
pub spot_positions: sqlx::types::Json<Vec<SpotPosition>>,
pub perp_positions: sqlx::types::Json<Vec<PerpPosition>>,
pub orders: sqlx::types::Json<Vec<Order>>,
pub last_add_perp_lp_shares_ts: i64,
pub total_deposits: U64,
pub total_withdraws: U64,
pub total_social_loss: U64,
pub settled_perp_pnl: i64,
pub cumulative_spot_fees: i64,
pub cumulative_perp_funding: i64,
pub liquidation_margin_freed: U64,
pub last_active_slot: U64,
pub next_order_id: U32,
pub max_margin_ratio: U32,
pub next_liquidation_id: U16,
pub sub_account_id: U16,
pub status: U8,
pub is_margin_trading_enabled: bool,
pub idle: bool,
pub open_orders: U8,
pub has_open_order: bool,
pub open_auctions: U8,
pub has_open_auction: bool,
pub margin_mode: sqlx::types::Json<MarginMode>,
pub pool_id: U8,
pub padding1: Vec<u8>,
pub last_fuel_bonus_update_ts: U32,
pub padding: Vec<u8>,
}
impl UserRow {
pub fn from_parts(source: crate::accounts::user::User, metadata: AccountMetadata) -> Self {
Self {
account_metadata: metadata.into(),
authority: source.authority.into(),
delegate: source.delegate.into(),
name: source.name.to_vec(),
spot_positions: sqlx::types::Json(source.spot_positions.to_vec()),
perp_positions: sqlx::types::Json(source.perp_positions.to_vec()),
orders: sqlx::types::Json(source.orders.to_vec()),
last_add_perp_lp_shares_ts: source.last_add_perp_lp_shares_ts,
total_deposits: source.total_deposits.into(),
total_withdraws: source.total_withdraws.into(),
total_social_loss: source.total_social_loss.into(),
settled_perp_pnl: source.settled_perp_pnl,
cumulative_spot_fees: source.cumulative_spot_fees,
cumulative_perp_funding: source.cumulative_perp_funding,
liquidation_margin_freed: source.liquidation_margin_freed.into(),
last_active_slot: source.last_active_slot.into(),
next_order_id: source.next_order_id.into(),
max_margin_ratio: source.max_margin_ratio.into(),
next_liquidation_id: source.next_liquidation_id.into(),
sub_account_id: source.sub_account_id.into(),
status: source.status.into(),
is_margin_trading_enabled: source.is_margin_trading_enabled,
idle: source.idle,
open_orders: source.open_orders.into(),
has_open_order: source.has_open_order,
open_auctions: source.open_auctions.into(),
has_open_auction: source.has_open_auction,
margin_mode: sqlx::types::Json(source.margin_mode),
pool_id: source.pool_id.into(),
padding1: source.padding1.to_vec(),
last_fuel_bonus_update_ts: source.last_fuel_bonus_update_ts.into(),
padding: source.padding.to_vec(),
}
}
}
impl TryFrom<UserRow> for crate::accounts::user::User {
type Error = carbon_core::error::Error;
fn try_from(source: UserRow) -> Result<Self, Self::Error> {
Ok(Self {
authority: *source.authority,
delegate: *source.delegate,
name: source.name.as_slice().try_into().map_err(|_| {
carbon_core::error::Error::Custom(
"Failed to convert padding from postgres primitive: expected 32 bytes"
.to_string(),
)
})?,
spot_positions: source
.spot_positions
.0
.into_iter()
.collect::<Vec<_>>()
.try_into()
.map_err(|_| {
carbon_core::error::Error::Custom(
"Failed to convert value from postgres primitive".to_string(),
)
})?,
perp_positions: source
.perp_positions
.0
.into_iter()
.collect::<Vec<_>>()
.try_into()
.map_err(|_| {
carbon_core::error::Error::Custom(
"Failed to convert value from postgres primitive".to_string(),
)
})?,
orders: source
.orders
.0
.into_iter()
.collect::<Vec<_>>()
.try_into()
.map_err(|_| {
carbon_core::error::Error::Custom(
"Failed to convert value from postgres primitive".to_string(),
)
})?,
last_add_perp_lp_shares_ts: source.last_add_perp_lp_shares_ts,
total_deposits: *source.total_deposits,
total_withdraws: *source.total_withdraws,
total_social_loss: *source.total_social_loss,
settled_perp_pnl: source.settled_perp_pnl,
cumulative_spot_fees: source.cumulative_spot_fees,
cumulative_perp_funding: source.cumulative_perp_funding,
liquidation_margin_freed: *source.liquidation_margin_freed,
last_active_slot: *source.last_active_slot,
next_order_id: source.next_order_id.try_into().map_err(|_| {
carbon_core::error::Error::Custom(
"Failed to convert value from postgres primitive".to_string(),
)
})?,
max_margin_ratio: source.max_margin_ratio.try_into().map_err(|_| {
carbon_core::error::Error::Custom(
"Failed to convert value from postgres primitive".to_string(),
)
})?,
next_liquidation_id: source.next_liquidation_id.try_into().map_err(|_| {
carbon_core::error::Error::Custom(
"Failed to convert value from postgres primitive".to_string(),
)
})?,
sub_account_id: source.sub_account_id.try_into().map_err(|_| {
carbon_core::error::Error::Custom(
"Failed to convert value from postgres primitive".to_string(),
)
})?,
status: source.status.try_into().map_err(|_| {
carbon_core::error::Error::Custom(
"Failed to convert value from postgres primitive".to_string(),
)
})?,
is_margin_trading_enabled: source.is_margin_trading_enabled,
idle: source.idle,
open_orders: source.open_orders.try_into().map_err(|_| {
carbon_core::error::Error::Custom(
"Failed to convert value from postgres primitive".to_string(),
)
})?,
has_open_order: source.has_open_order,
open_auctions: source.open_auctions.try_into().map_err(|_| {
carbon_core::error::Error::Custom(
"Failed to convert value from postgres primitive".to_string(),
)
})?,
has_open_auction: source.has_open_auction,
margin_mode: source.margin_mode.0,
pool_id: source.pool_id.try_into().map_err(|_| {
carbon_core::error::Error::Custom(
"Failed to convert value from postgres primitive".to_string(),
)
})?,
padding1: source.padding1.as_slice().try_into().map_err(|_| {
carbon_core::error::Error::Custom(
"Failed to convert padding from postgres primitive: expected 3 bytes"
.to_string(),
)
})?,
last_fuel_bonus_update_ts: source.last_fuel_bonus_update_ts.try_into().map_err(
|_| {
carbon_core::error::Error::Custom(
"Failed to convert value from postgres primitive".to_string(),
)
},
)?,
padding: source.padding.as_slice().try_into().map_err(|_| {
carbon_core::error::Error::Custom(
"Failed to convert padding from postgres primitive: expected 12 bytes"
.to_string(),
)
})?,
})
}
}
impl carbon_core::postgres::operations::Table for crate::accounts::user::User {
fn table() -> &'static str {
"user_account"
}
fn columns() -> Vec<&'static str> {
vec![
"__pubkey",
"__slot",
"authority",
"delegate",
"name",
"spot_positions",
"perp_positions",
"orders",
"last_add_perp_lp_shares_ts",
"total_deposits",
"total_withdraws",
"total_social_loss",
"settled_perp_pnl",
"cumulative_spot_fees",
"cumulative_perp_funding",
"liquidation_margin_freed",
"last_active_slot",
"next_order_id",
"max_margin_ratio",
"next_liquidation_id",
"sub_account_id",
"status",
"is_margin_trading_enabled",
"idle",
"open_orders",
"has_open_order",
"open_auctions",
"has_open_auction",
"margin_mode",
"pool_id",
"padding1",
"last_fuel_bonus_update_ts",
"padding",
]
}
}
#[async_trait::async_trait]
impl carbon_core::postgres::operations::Insert for UserRow {
async fn insert(&self, pool: &sqlx::PgPool) -> carbon_core::error::CarbonResult<()> {
sqlx::query(r#"
INSERT INTO user_account (
"authority",
"delegate",
"name",
"spot_positions",
"perp_positions",
"orders",
"last_add_perp_lp_shares_ts",
"total_deposits",
"total_withdraws",
"total_social_loss",
"settled_perp_pnl",
"cumulative_spot_fees",
"cumulative_perp_funding",
"liquidation_margin_freed",
"last_active_slot",
"next_order_id",
"max_margin_ratio",
"next_liquidation_id",
"sub_account_id",
"status",
"is_margin_trading_enabled",
"idle",
"open_orders",
"has_open_order",
"open_auctions",
"has_open_auction",
"margin_mode",
"pool_id",
"padding1",
"last_fuel_bonus_update_ts",
"padding",
__pubkey, __slot
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33
)"#)
.bind(self.authority)
.bind(self.delegate)
.bind(&self.name)
.bind(&self.spot_positions)
.bind(&self.perp_positions)
.bind(&self.orders)
.bind(self.last_add_perp_lp_shares_ts)
.bind(&self.total_deposits)
.bind(&self.total_withdraws)
.bind(&self.total_social_loss)
.bind(self.settled_perp_pnl)
.bind(self.cumulative_spot_fees)
.bind(self.cumulative_perp_funding)
.bind(&self.liquidation_margin_freed)
.bind(&self.last_active_slot)
.bind(self.next_order_id)
.bind(self.max_margin_ratio)
.bind(self.next_liquidation_id)
.bind(self.sub_account_id)
.bind(self.status)
.bind(self.is_margin_trading_enabled)
.bind(self.idle)
.bind(self.open_orders)
.bind(self.has_open_order)
.bind(self.open_auctions)
.bind(self.has_open_auction)
.bind(&self.margin_mode)
.bind(self.pool_id)
.bind(&self.padding1)
.bind(self.last_fuel_bonus_update_ts)
.bind(&self.padding)
.bind(self.account_metadata.pubkey)
.bind(&self.account_metadata.slot)
.execute(pool).await
.map_err(|e| carbon_core::error::Error::Custom(e.to_string()))?;
Ok(())
}
}
#[async_trait::async_trait]
impl carbon_core::postgres::operations::Upsert for UserRow {
async fn upsert(&self, pool: &sqlx::PgPool) -> carbon_core::error::CarbonResult<()> {
sqlx::query(r#"INSERT INTO user_account (
"authority",
"delegate",
"name",
"spot_positions",
"perp_positions",
"orders",
"last_add_perp_lp_shares_ts",
"total_deposits",
"total_withdraws",
"total_social_loss",
"settled_perp_pnl",
"cumulative_spot_fees",
"cumulative_perp_funding",
"liquidation_margin_freed",
"last_active_slot",
"next_order_id",
"max_margin_ratio",
"next_liquidation_id",
"sub_account_id",
"status",
"is_margin_trading_enabled",
"idle",
"open_orders",
"has_open_order",
"open_auctions",
"has_open_auction",
"margin_mode",
"pool_id",
"padding1",
"last_fuel_bonus_update_ts",
"padding",
__pubkey, __slot
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33
) ON CONFLICT (
__pubkey
) DO UPDATE SET
"authority" = EXCLUDED."authority",
"delegate" = EXCLUDED."delegate",
"name" = EXCLUDED."name",
"spot_positions" = EXCLUDED."spot_positions",
"perp_positions" = EXCLUDED."perp_positions",
"orders" = EXCLUDED."orders",
"last_add_perp_lp_shares_ts" = EXCLUDED."last_add_perp_lp_shares_ts",
"total_deposits" = EXCLUDED."total_deposits",
"total_withdraws" = EXCLUDED."total_withdraws",
"total_social_loss" = EXCLUDED."total_social_loss",
"settled_perp_pnl" = EXCLUDED."settled_perp_pnl",
"cumulative_spot_fees" = EXCLUDED."cumulative_spot_fees",
"cumulative_perp_funding" = EXCLUDED."cumulative_perp_funding",
"liquidation_margin_freed" = EXCLUDED."liquidation_margin_freed",
"last_active_slot" = EXCLUDED."last_active_slot",
"next_order_id" = EXCLUDED."next_order_id",
"max_margin_ratio" = EXCLUDED."max_margin_ratio",
"next_liquidation_id" = EXCLUDED."next_liquidation_id",
"sub_account_id" = EXCLUDED."sub_account_id",
"status" = EXCLUDED."status",
"is_margin_trading_enabled" = EXCLUDED."is_margin_trading_enabled",
"idle" = EXCLUDED."idle",
"open_orders" = EXCLUDED."open_orders",
"has_open_order" = EXCLUDED."has_open_order",
"open_auctions" = EXCLUDED."open_auctions",
"has_open_auction" = EXCLUDED."has_open_auction",
"margin_mode" = EXCLUDED."margin_mode",
"pool_id" = EXCLUDED."pool_id",
"padding1" = EXCLUDED."padding1",
"last_fuel_bonus_update_ts" = EXCLUDED."last_fuel_bonus_update_ts",
"padding" = EXCLUDED."padding",
__slot = EXCLUDED.__slot
"#)
.bind(self.authority)
.bind(self.delegate)
.bind(&self.name)
.bind(&self.spot_positions)
.bind(&self.perp_positions)
.bind(&self.orders)
.bind(self.last_add_perp_lp_shares_ts)
.bind(&self.total_deposits)
.bind(&self.total_withdraws)
.bind(&self.total_social_loss)
.bind(self.settled_perp_pnl)
.bind(self.cumulative_spot_fees)
.bind(self.cumulative_perp_funding)
.bind(&self.liquidation_margin_freed)
.bind(&self.last_active_slot)
.bind(self.next_order_id)
.bind(self.max_margin_ratio)
.bind(self.next_liquidation_id)
.bind(self.sub_account_id)
.bind(self.status)
.bind(self.is_margin_trading_enabled)
.bind(self.idle)
.bind(self.open_orders)
.bind(self.has_open_order)
.bind(self.open_auctions)
.bind(self.has_open_auction)
.bind(&self.margin_mode)
.bind(self.pool_id)
.bind(&self.padding1)
.bind(self.last_fuel_bonus_update_ts)
.bind(&self.padding)
.bind(self.account_metadata.pubkey)
.bind(&self.account_metadata.slot)
.execute(pool).await
.map_err(|e| carbon_core::error::Error::Custom(e.to_string()))?;
Ok(())
}
}
#[async_trait::async_trait]
impl carbon_core::postgres::operations::Delete for UserRow {
type Key = carbon_core::postgres::primitives::Pubkey;
async fn delete(key: Self::Key, pool: &sqlx::PgPool) -> carbon_core::error::CarbonResult<()> {
sqlx::query(
r#"DELETE FROM user_account WHERE
__pubkey = $1
"#,
)
.bind(key)
.execute(pool)
.await
.map_err(|e| carbon_core::error::Error::Custom(e.to_string()))?;
Ok(())
}
}
#[async_trait::async_trait]
impl carbon_core::postgres::operations::Lookup for UserRow {
type Key = carbon_core::postgres::primitives::Pubkey;
async fn lookup(
key: Self::Key,
pool: &sqlx::PgPool,
) -> carbon_core::error::CarbonResult<Option<Self>> {
let row = sqlx::query_as(
r#"SELECT * FROM user_account WHERE
__pubkey = $1
"#,
)
.bind(key)
.fetch_optional(pool)
.await
.map_err(|e| carbon_core::error::Error::Custom(e.to_string()))?;
Ok(row)
}
}
pub struct UserMigrationOperation;
#[async_trait::async_trait]
impl sqlx_migrator::Operation<sqlx::Postgres> for UserMigrationOperation {
async fn up(
&self,
connection: &mut sqlx::PgConnection,
) -> Result<(), sqlx_migrator::error::Error> {
sqlx::query(
r#"CREATE TABLE IF NOT EXISTS user_account (
-- Account data
"authority" BYTEA NOT NULL,
"delegate" BYTEA NOT NULL,
"name" BYTEA NOT NULL,
"spot_positions" JSONB NOT NULL,
"perp_positions" JSONB NOT NULL,
"orders" JSONB NOT NULL,
"last_add_perp_lp_shares_ts" INT8 NOT NULL,
"total_deposits" NUMERIC(20) NOT NULL,
"total_withdraws" NUMERIC(20) NOT NULL,
"total_social_loss" NUMERIC(20) NOT NULL,
"settled_perp_pnl" INT8 NOT NULL,
"cumulative_spot_fees" INT8 NOT NULL,
"cumulative_perp_funding" INT8 NOT NULL,
"liquidation_margin_freed" NUMERIC(20) NOT NULL,
"last_active_slot" NUMERIC(20) NOT NULL,
"next_order_id" INT8 NOT NULL,
"max_margin_ratio" INT8 NOT NULL,
"next_liquidation_id" INT4 NOT NULL,
"sub_account_id" INT4 NOT NULL,
"status" INT2 NOT NULL,
"is_margin_trading_enabled" BOOLEAN NOT NULL,
"idle" BOOLEAN NOT NULL,
"open_orders" INT2 NOT NULL,
"has_open_order" BOOLEAN NOT NULL,
"open_auctions" INT2 NOT NULL,
"has_open_auction" BOOLEAN NOT NULL,
"margin_mode" JSONB NOT NULL,
"pool_id" INT2 NOT NULL,
"padding1" BYTEA NOT NULL,
"last_fuel_bonus_update_ts" INT8 NOT NULL,
"padding" BYTEA NOT NULL,
-- Account metadata
__pubkey BYTEA NOT NULL,
__slot NUMERIC(20),
PRIMARY KEY (__pubkey)
)"#,
)
.execute(connection)
.await?;
Ok(())
}
async fn down(
&self,
connection: &mut sqlx::PgConnection,
) -> Result<(), sqlx_migrator::error::Error> {
sqlx::query(r#"DROP TABLE IF EXISTS user_account"#)
.execute(connection)
.await?;
Ok(())
}
}