From 683a674edb7e5abfd06d0acb34e2ae7e1ee30f4f Mon Sep 17 00:00:00 2001 From: Joey Kraut Date: Fri, 28 Jun 2024 13:24:08 -0700 Subject: [PATCH 1/2] compliance: compliance-server: db: Define migrations and query db --- compliance/compliance-api/src/lib.rs | 11 +++- compliance/compliance-server/Cargo.toml | 3 + compliance/compliance-server/diesel.toml | 9 +++ compliance/compliance-server/migrations/.keep | 0 .../down.sql | 6 ++ .../up.sql | 36 +++++++++++ .../down.sql | 2 + .../up.sql | 8 +++ compliance/compliance-server/src/db.rs | 59 +++++++++++++++++++ compliance/compliance-server/src/error.rs | 9 ++- compliance/compliance-server/src/main.rs | 46 +++++++++++++-- compliance/compliance-server/src/schema.rs | 11 ++++ 12 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 compliance/compliance-server/diesel.toml create mode 100644 compliance/compliance-server/migrations/.keep create mode 100644 compliance/compliance-server/migrations/00000000000000_diesel_initial_setup/down.sql create mode 100644 compliance/compliance-server/migrations/00000000000000_diesel_initial_setup/up.sql create mode 100644 compliance/compliance-server/migrations/2024-06-28-231756_create_compliance_table/down.sql create mode 100644 compliance/compliance-server/migrations/2024-06-28-231756_create_compliance_table/up.sql create mode 100644 compliance/compliance-server/src/db.rs create mode 100644 compliance/compliance-server/src/schema.rs diff --git a/compliance/compliance-api/src/lib.rs b/compliance/compliance-api/src/lib.rs index 4e933d2..5957068 100644 --- a/compliance/compliance-api/src/lib.rs +++ b/compliance/compliance-api/src/lib.rs @@ -1,3 +1,11 @@ +//! The API for the compliance server + +#![deny(missing_docs)] +#![deny(clippy::missing_docs_in_private_items)] +#![deny(unsafe_code)] +#![deny(clippy::needless_pass_by_value)] +#![deny(clippy::needless_pass_by_ref_mut)] + use serde::{Deserialize, Serialize}; /// The response type for a compliance check @@ -13,5 +21,6 @@ pub enum ComplianceStatus { /// The wallet is compliant Compliant, /// The wallet is not compliant - NotCompliant, + #[allow(missing_docs)] + NotCompliant { reason: String }, } diff --git a/compliance/compliance-server/Cargo.toml b/compliance/compliance-server/Cargo.toml index 83d55c9..68a06c6 100644 --- a/compliance/compliance-server/Cargo.toml +++ b/compliance/compliance-server/Cargo.toml @@ -9,6 +9,9 @@ http-body-util = "0.1.0" warp = "0.3" compliance-api = { path = "../compliance-api" } +# === Database === # +diesel = { version = "2.2", features = ["postgres", "r2d2"] } + # === Renegade Dependencies === # renegade-util = { workspace = true } diff --git a/compliance/compliance-server/diesel.toml b/compliance/compliance-server/diesel.toml new file mode 100644 index 0000000..83d15a9 --- /dev/null +++ b/compliance/compliance-server/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] + +[migrations_directory] +dir = "./migrations" diff --git a/compliance/compliance-server/migrations/.keep b/compliance/compliance-server/migrations/.keep new file mode 100644 index 0000000..e69de29 diff --git a/compliance/compliance-server/migrations/00000000000000_diesel_initial_setup/down.sql b/compliance/compliance-server/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 0000000..a9f5260 --- /dev/null +++ b/compliance/compliance-server/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/compliance/compliance-server/migrations/00000000000000_diesel_initial_setup/up.sql b/compliance/compliance-server/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 0000000..d68895b --- /dev/null +++ b/compliance/compliance-server/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/compliance/compliance-server/migrations/2024-06-28-231756_create_compliance_table/down.sql b/compliance/compliance-server/migrations/2024-06-28-231756_create_compliance_table/down.sql new file mode 100644 index 0000000..6e2f36e --- /dev/null +++ b/compliance/compliance-server/migrations/2024-06-28-231756_create_compliance_table/down.sql @@ -0,0 +1,2 @@ +-- Drop the wallet compliance table +DROP TABLE IF EXISTS wallet_compliance; \ No newline at end of file diff --git a/compliance/compliance-server/migrations/2024-06-28-231756_create_compliance_table/up.sql b/compliance/compliance-server/migrations/2024-06-28-231756_create_compliance_table/up.sql new file mode 100644 index 0000000..975a99b --- /dev/null +++ b/compliance/compliance-server/migrations/2024-06-28-231756_create_compliance_table/up.sql @@ -0,0 +1,8 @@ +-- Create a table for caching wallet compliance information +CREATE TABLE IF NOT EXISTS wallet_compliance ( + address TEXT PRIMARY KEY, + is_compliant BOOLEAN NOT NULL, + reason TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + expires_at TIMESTAMP NOT NULL DEFAULT NOW() + INTERVAL '1 year' +); diff --git a/compliance/compliance-server/src/db.rs b/compliance/compliance-server/src/db.rs new file mode 100644 index 0000000..e165538 --- /dev/null +++ b/compliance/compliance-server/src/db.rs @@ -0,0 +1,59 @@ +//! Database helpers for the server + +use std::time::SystemTime; + +use compliance_api::ComplianceStatus; +use diesel::{ExpressionMethods, Insertable, PgConnection, QueryDsl, Queryable, RunQueryDsl}; +use renegade_util::err_str; + +use crate::{ + error::ComplianceServerError, + schema::{ + wallet_compliance, + wallet_compliance::dsl::{address as address_col, wallet_compliance as compliance_table}, + }, +}; + +// ---------- +// | Models | +// ---------- + +/// A compliance entry for a wallet +#[derive(Debug, Clone, Queryable, Insertable)] +#[table_name = "wallet_compliance"] +#[allow(missing_docs)] +pub struct ComplianceEntry { + pub address: String, + pub is_compliant: bool, + pub reason: String, + pub created_at: SystemTime, + pub expires_at: SystemTime, +} + +impl ComplianceEntry { + /// Get the compliance status for an entry + pub fn compliance_status(&self) -> ComplianceStatus { + if self.is_compliant { + ComplianceStatus::Compliant + } else { + ComplianceStatus::NotCompliant { reason: self.reason.clone() } + } + } +} + +// ----------- +// | Queries | +// ----------- + +/// Get a compliance entry by address +pub fn get_compliance_entry( + address: &str, + conn: &mut PgConnection, +) -> Result, ComplianceServerError> { + let query = compliance_table + .filter(address_col.eq(address)) + .load::(conn) + .map_err(err_str!(ComplianceServerError::Db))?; + + Ok(query.first().cloned()) +} diff --git a/compliance/compliance-server/src/error.rs b/compliance/compliance-server/src/error.rs index dd8daef..b822b5a 100644 --- a/compliance/compliance-server/src/error.rs +++ b/compliance/compliance-server/src/error.rs @@ -6,11 +6,16 @@ use warp::reject::Reject; /// The error type emitted by the compliance server #[derive(Debug, Clone)] -pub enum ComplianceServerError {} +pub enum ComplianceServerError { + /// An error with a database query + Db(String), +} impl Display for ComplianceServerError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "ComplianceServerError") + match self { + ComplianceServerError::Db(e) => write!(f, "Database error: {}", e), + } } } impl Error for ComplianceServerError {} diff --git a/compliance/compliance-server/src/main.rs b/compliance/compliance-server/src/main.rs index 8841715..9d97560 100644 --- a/compliance/compliance-server/src/main.rs +++ b/compliance/compliance-server/src/main.rs @@ -1,10 +1,31 @@ +//! The server that serves wallet compliance information + +#![deny(missing_docs)] +#![deny(clippy::missing_docs_in_private_items)] +#![deny(unsafe_code)] +#![deny(clippy::needless_pass_by_value)] +#![deny(clippy::needless_pass_by_ref_mut)] + +use std::sync::Arc; + use clap::Parser; use compliance_api::{ComplianceCheckResponse, ComplianceStatus}; +use diesel::pg::PgConnection; +use diesel::r2d2::{ConnectionManager, Pool}; use error::ComplianceServerError; +use renegade_util::err_str; use renegade_util::telemetry::{setup_system_logger, LevelFilter}; use warp::{reply::Json, Filter}; +use crate::db::get_compliance_entry; + +pub mod db; pub mod error; +#[allow(missing_docs, clippy::missing_docs_in_private_items)] +pub mod schema; + +/// The type of the connection pool +type ConnectionPool = Arc>>; /// The CLI for the compliance server #[derive(Debug, Clone, Parser)] @@ -16,6 +37,9 @@ struct Cli { /// The Chainalysis API key #[arg(long)] chainalysis_api_key: String, + /// The url of the compliance database + #[arg(long)] + db_url: String, } #[tokio::main] @@ -23,6 +47,11 @@ async fn main() { setup_system_logger(LevelFilter::INFO); let cli = Cli::parse(); + // Create the connection pool + let manager = ConnectionManager::::new(cli.db_url.clone()); + let pool = Pool::builder().build(manager).expect("Failed to create pool"); + let pool = Arc::new(pool); + // Get compliance information for a wallet let chainalysis_key = cli.chainalysis_api_key.clone(); let compliance_check = warp::get() @@ -31,8 +60,10 @@ async fn main() { .and(warp::path::param::()) // wallet_address .and_then(move |wallet_address| { let key = chainalysis_key.clone(); + let pool = pool.clone(); + async move { - handle_req(wallet_address, &key).await + handle_req(wallet_address, &key, pool).await } }); @@ -49,8 +80,10 @@ async fn main() { async fn handle_req( wallet_address: String, chainalysis_api_key: &str, + pool: ConnectionPool, ) -> Result { - let compliance_status = check_wallet_compliance(wallet_address, chainalysis_api_key).await?; + let compliance_status = + check_wallet_compliance(wallet_address, chainalysis_api_key, pool).await?; let resp = ComplianceCheckResponse { compliance_status }; Ok(warp::reply::json(&resp)) } @@ -59,10 +92,15 @@ async fn handle_req( async fn check_wallet_compliance( wallet_address: String, chainalysis_api_key: &str, + pool: ConnectionPool, ) -> Result { // 1. Check the DB first + let mut conn = pool.get().map_err(err_str!(ComplianceServerError::Db))?; + let compliance_entry = get_compliance_entry(&wallet_address, &mut conn)?; + if let Some(compliance_entry) = compliance_entry { + return Ok(compliance_entry.compliance_status()); + } // 2. If not present, check the chainalysis API - - todo!() + Ok(ComplianceStatus::Compliant) } diff --git a/compliance/compliance-server/src/schema.rs b/compliance/compliance-server/src/schema.rs new file mode 100644 index 0000000..8e02bf7 --- /dev/null +++ b/compliance/compliance-server/src/schema.rs @@ -0,0 +1,11 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + wallet_compliance (address) { + address -> Text, + is_compliant -> Bool, + reason -> Text, + created_at -> Timestamp, + expires_at -> Timestamp, + } +} From 2b08225bee62ed277cf85854b01f53bc8ed9d9e5 Mon Sep 17 00:00:00 2001 From: Joey Kraut Date: Sat, 29 Jun 2024 12:44:01 -0700 Subject: [PATCH 2/2] compliance: compliance-server: Fallback to chainalysis API if not cached --- Cargo.toml | 3 + compliance/compliance-api/src/lib.rs | 3 + compliance/compliance-server/Cargo.toml | 4 + .../up.sql | 1 + .../compliance-server/src/chainalysis_api.rs | 107 ++++++++++++++++++ compliance/compliance-server/src/db.rs | 26 ++++- compliance/compliance-server/src/error.rs | 9 ++ compliance/compliance-server/src/main.rs | 12 +- compliance/compliance-server/src/schema.rs | 1 + 9 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 compliance/compliance-server/src/chainalysis_api.rs diff --git a/Cargo.toml b/Cargo.toml index ed5608a..91f38e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,6 @@ renegade-circuits = { package = "circuits", git = "https://github.com/renegade-f renegade-circuit-types = { package = "circuit-types", git = "https://github.com/renegade-fi/renegade.git" } renegade-crypto = { git = "https://github.com/renegade-fi/renegade.git" } renegade-util = { package = "util", git = "https://github.com/renegade-fi/renegade.git" } + +# === Misc Dependencies === # +tracing = "0.1" diff --git a/compliance/compliance-api/src/lib.rs b/compliance/compliance-api/src/lib.rs index 5957068..1613290 100644 --- a/compliance/compliance-api/src/lib.rs +++ b/compliance/compliance-api/src/lib.rs @@ -8,6 +8,9 @@ use serde::{Deserialize, Serialize}; +/// The API endpoint for screening an address for compliance +pub const WALLET_SCREEN_PATH: &str = "/v0/check-compliance"; + /// The response type for a compliance check #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ComplianceCheckResponse { diff --git a/compliance/compliance-server/Cargo.toml b/compliance/compliance-server/Cargo.toml index 68a06c6..bc367c6 100644 --- a/compliance/compliance-server/Cargo.toml +++ b/compliance/compliance-server/Cargo.toml @@ -17,4 +17,8 @@ renegade-util = { workspace = true } # === Misc === # clap = { version = "4.5", features = ["derive"] } +reqwest = { version = "0.12", features = ["json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" tokio = { version = "1.37", features = ["full"] } +tracing = { workspace = true } diff --git a/compliance/compliance-server/migrations/2024-06-28-231756_create_compliance_table/up.sql b/compliance/compliance-server/migrations/2024-06-28-231756_create_compliance_table/up.sql index 975a99b..da22847 100644 --- a/compliance/compliance-server/migrations/2024-06-28-231756_create_compliance_table/up.sql +++ b/compliance/compliance-server/migrations/2024-06-28-231756_create_compliance_table/up.sql @@ -2,6 +2,7 @@ CREATE TABLE IF NOT EXISTS wallet_compliance ( address TEXT PRIMARY KEY, is_compliant BOOLEAN NOT NULL, + risk_level TEXT NOT NULL, reason TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT NOW(), expires_at TIMESTAMP NOT NULL DEFAULT NOW() + INTERVAL '1 year' diff --git a/compliance/compliance-server/src/chainalysis_api.rs b/compliance/compliance-server/src/chainalysis_api.rs new file mode 100644 index 0000000..3052c4f --- /dev/null +++ b/compliance/compliance-server/src/chainalysis_api.rs @@ -0,0 +1,107 @@ +//! Helpers for interacting with the chainalysis API + +use serde::{Deserialize, Serialize}; +use tracing::warn; + +use crate::{db::ComplianceEntry, error::ComplianceServerError}; + +// ------------- +// | API Types | +// ------------- + +/// The base URL for the chainalysis entities API +const CHAINALYSIS_API_BASE: &str = "https://api.chainalysis.com/api/risk/v2/entities"; +/// The header name for the auth token +const TOKEN_HEADER: &str = "Token"; + +/// The register address request body +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RegisterAddressRequest { + /// The address to register + pub address: String, +} + +/// The response to a risk assessment query +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RiskAssessmentResponse { + /// The address that the assessment is for + pub address: String, + /// The risk assessment status + pub risk: String, + /// The reason for the risk assessment + #[serde(rename = "riskReason")] + pub risk_reason: Option, +} + +impl RiskAssessmentResponse { + /// Get a compliance entry from the risk assessment + pub fn as_compliance_entry(self) -> ComplianceEntry { + // We allow low and medium risk entries, high and severe are marked + // non-compliant + let compliant = match self.risk.as_str() { + "Low" | "Medium" => true, + "High" | "Severe" => false, + x => { + // For now we don't block on an unknown assessment, this should be unreachable + warn!("Unexpected risk assessment: {x}"); + true + }, + }; + + let risk_reason = self.risk_reason.unwrap_or_default(); + ComplianceEntry::new(self.address, compliant, self.risk, risk_reason) + } +} + +// --------------- +// | Client Impl | +// --------------- + +/// Query chainalysis for the compliance status of a wallet +pub async fn query_chainalysis( + wallet_address: &str, + chainalysis_api_key: &str, +) -> Result { + // 1. Register the wallet + register_addr(wallet_address, chainalysis_api_key).await?; + + // 2. Query the risk assessment + let risk_assessment = query_risk_assessment(wallet_address, chainalysis_api_key).await?; + Ok(risk_assessment.as_compliance_entry()) +} + +/// Register a wallet with chainalysis +async fn register_addr( + wallet_address: &str, + chainalysis_api_key: &str, +) -> Result<(), ComplianceServerError> { + let body = RegisterAddressRequest { address: wallet_address.to_string() }; + let client = reqwest::Client::new(); + client + .post(CHAINALYSIS_API_BASE) + .header(TOKEN_HEADER, chainalysis_api_key) + .json(&body) + .send() + .await? + .error_for_status()?; + + Ok(()) +} + +/// Query the risk assessment from chainalysis +async fn query_risk_assessment( + wallet_address: &str, + chainalysis_api_key: &str, +) -> Result { + let url = format!("{CHAINALYSIS_API_BASE}/{wallet_address}"); + let client = reqwest::Client::new(); + let resp = client + .get(url) + .header(TOKEN_HEADER, chainalysis_api_key) + .send() + .await? + .error_for_status()?; + + let risk_assessment: RiskAssessmentResponse = resp.json().await?; + Ok(risk_assessment) +} diff --git a/compliance/compliance-server/src/db.rs b/compliance/compliance-server/src/db.rs index e165538..b447f93 100644 --- a/compliance/compliance-server/src/db.rs +++ b/compliance/compliance-server/src/db.rs @@ -1,6 +1,6 @@ //! Database helpers for the server -use std::time::SystemTime; +use std::time::{Duration, SystemTime}; use compliance_api::ComplianceStatus; use diesel::{ExpressionMethods, Insertable, PgConnection, QueryDsl, Queryable, RunQueryDsl}; @@ -14,6 +14,9 @@ use crate::{ }, }; +/// The default expiration duration for a compliance entry +const DEFAULT_EXPIRATION_DURATION: Duration = Duration::from_days(365); + // ---------- // | Models | // ---------- @@ -25,12 +28,20 @@ use crate::{ pub struct ComplianceEntry { pub address: String, pub is_compliant: bool, + pub risk_level: String, pub reason: String, pub created_at: SystemTime, pub expires_at: SystemTime, } impl ComplianceEntry { + /// Create a new entry from a risk assessment + pub fn new(address: String, is_compliant: bool, risk_level: String, reason: String) -> Self { + let created_at = SystemTime::now(); + let expires_at = created_at + DEFAULT_EXPIRATION_DURATION; + ComplianceEntry { address, is_compliant, risk_level, reason, created_at, expires_at } + } + /// Get the compliance status for an entry pub fn compliance_status(&self) -> ComplianceStatus { if self.is_compliant { @@ -57,3 +68,16 @@ pub fn get_compliance_entry( Ok(query.first().cloned()) } + +/// Insert a compliance entry into the database +pub fn insert_compliance_entry( + entry: ComplianceEntry, + conn: &mut PgConnection, +) -> Result<(), ComplianceServerError> { + diesel::insert_into(compliance_table) + .values(entry) + .execute(conn) + .map_err(err_str!(ComplianceServerError::Db))?; + + Ok(()) +} diff --git a/compliance/compliance-server/src/error.rs b/compliance/compliance-server/src/error.rs index b822b5a..4d56a83 100644 --- a/compliance/compliance-server/src/error.rs +++ b/compliance/compliance-server/src/error.rs @@ -9,14 +9,23 @@ use warp::reject::Reject; pub enum ComplianceServerError { /// An error with a database query Db(String), + /// An error with the chainalysis API + Chainalysis(String), } impl Display for ComplianceServerError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ComplianceServerError::Db(e) => write!(f, "Database error: {}", e), + ComplianceServerError::Chainalysis(e) => write!(f, "Chainalysis error: {}", e), } } } impl Error for ComplianceServerError {} impl Reject for ComplianceServerError {} + +impl From for ComplianceServerError { + fn from(e: reqwest::Error) -> Self { + ComplianceServerError::Chainalysis(e.to_string()) + } +} diff --git a/compliance/compliance-server/src/main.rs b/compliance/compliance-server/src/main.rs index 9d97560..187018c 100644 --- a/compliance/compliance-server/src/main.rs +++ b/compliance/compliance-server/src/main.rs @@ -5,20 +5,25 @@ #![deny(unsafe_code)] #![deny(clippy::needless_pass_by_value)] #![deny(clippy::needless_pass_by_ref_mut)] +#![feature(duration_constructors)] use std::sync::Arc; +use chainalysis_api::query_chainalysis; use clap::Parser; use compliance_api::{ComplianceCheckResponse, ComplianceStatus}; +use db::insert_compliance_entry; use diesel::pg::PgConnection; use diesel::r2d2::{ConnectionManager, Pool}; use error::ComplianceServerError; use renegade_util::err_str; use renegade_util::telemetry::{setup_system_logger, LevelFilter}; +use tracing::info; use warp::{reply::Json, Filter}; use crate::db::get_compliance_entry; +pub mod chainalysis_api; pub mod db; pub mod error; #[allow(missing_docs, clippy::missing_docs_in_private_items)] @@ -102,5 +107,10 @@ async fn check_wallet_compliance( } // 2. If not present, check the chainalysis API - Ok(ComplianceStatus::Compliant) + info!("address not cached in DB, querying Chainalysis"); + let compliance_entry = query_chainalysis(&wallet_address, chainalysis_api_key).await?; + + // 3. Cache in the DB + insert_compliance_entry(compliance_entry.clone(), &mut conn)?; + Ok(compliance_entry.compliance_status()) } diff --git a/compliance/compliance-server/src/schema.rs b/compliance/compliance-server/src/schema.rs index 8e02bf7..6748bec 100644 --- a/compliance/compliance-server/src/schema.rs +++ b/compliance/compliance-server/src/schema.rs @@ -4,6 +4,7 @@ diesel::table! { wallet_compliance (address) { address -> Text, is_compliant -> Bool, + risk_level -> Text, reason -> Text, created_at -> Timestamp, expires_at -> Timestamp,