rpc: Unique addresses
RPC method to show number of unique addresses interracting with cauldron
This commit is contained in:
parent
fea08d2b84
commit
675b3f08f9
8 changed files with 605 additions and 419 deletions
880
Cargo.lock
generated
880
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -15,6 +15,7 @@ use rusqlite::{params, Connection};
|
||||||
const MAX_DOWNLOAD_ATTEMPTS: usize = 100;
|
const MAX_DOWNLOAD_ATTEMPTS: usize = 100;
|
||||||
|
|
||||||
// BCMR auth chain entry, matches auth_chain_entry table
|
// BCMR auth chain entry, matches auth_chain_entry table
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct AuthChainEntry {
|
pub struct AuthChainEntry {
|
||||||
pub utxo: OutPointHash,
|
pub utxo: OutPointHash,
|
||||||
pub txid: Txid,
|
pub txid: Txid,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ pub mod header;
|
||||||
pub mod mempool;
|
pub mod mempool;
|
||||||
pub mod pool;
|
pub mod pool;
|
||||||
pub mod tx;
|
pub mod tx;
|
||||||
|
pub mod user;
|
||||||
pub mod utxo_funding;
|
pub mod utxo_funding;
|
||||||
pub mod utxo_spending;
|
pub mod utxo_spending;
|
||||||
|
|
||||||
|
|
@ -20,6 +21,7 @@ pub fn prepare_tables(conn: &Connection) {
|
||||||
tx::create_table(conn);
|
tx::create_table(conn);
|
||||||
utxo_funding::create_table(conn);
|
utxo_funding::create_table(conn);
|
||||||
utxo_spending::create_table(conn);
|
utxo_spending::create_table(conn);
|
||||||
|
user::create_table(conn);
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"CREATE TABLE config (
|
"CREATE TABLE config (
|
||||||
|
|
|
||||||
107
src/db/cauldron/user.rs
Normal file
107
src/db/cauldron/user.rs
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
// Copyright (C) 2024 Riften Labs AS
|
||||||
|
//
|
||||||
|
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
|
||||||
|
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
||||||
|
|
||||||
|
// Keep track of individual user activity
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use bitcoin_hashes::{hex::ToHex, Hash};
|
||||||
|
use bitcoincash::{PubkeyHash, Transaction};
|
||||||
|
use riftenlabs_defi::cauldron::ParsedContract;
|
||||||
|
use rusqlite::{params, Connection};
|
||||||
|
|
||||||
|
pub fn create_table(conn: &Connection) {
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE user_action (
|
||||||
|
spent_utxo_hash TEXT NOT NULL,
|
||||||
|
user TEXT NOT NULL,
|
||||||
|
address_type TEXT NOT NULL,
|
||||||
|
FOREIGN KEY(spent_utxo_hash) REFERENCES utxo_spending(spent_utxo_hash),
|
||||||
|
PRIMARY KEY (spent_utxo_hash, user)
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_user_action(
|
||||||
|
con: &Connection,
|
||||||
|
cauldrons: &Vec<ParsedContract>,
|
||||||
|
tx: &Transaction,
|
||||||
|
replace: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut statement = con.prepare(&format!(
|
||||||
|
"INSERT OR {} INTO user_action (spent_utxo_hash, user, address_type) VALUES (?, ?, ?)",
|
||||||
|
if replace { "REPLACE" } else { "IGNORE" }
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let p2pkh_output_hashes: HashSet<PubkeyHash> = tx
|
||||||
|
.output
|
||||||
|
.iter()
|
||||||
|
.filter_map(|output| {
|
||||||
|
if output.script_pubkey.is_p2pkh() {
|
||||||
|
Some(output.script_pubkey[3..23].to_vec())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|pubkey| {
|
||||||
|
PubkeyHash::from_inner(
|
||||||
|
pubkey
|
||||||
|
.try_into()
|
||||||
|
.expect("failed to convert into pubkeyhash"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for c in cauldrons {
|
||||||
|
for user in &p2pkh_output_hashes {
|
||||||
|
statement.execute(params![c.spent_utxo_hash.to_hex(), user.to_hex(), "p2pkh"])?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_unique_per_month_accumilating(conn: &Connection) -> Result<Vec<(String, usize)>> {
|
||||||
|
let sql = "WITH first_user_action AS (
|
||||||
|
-- Step 1: Get the first month when each user was active
|
||||||
|
SELECT
|
||||||
|
ua.user,
|
||||||
|
MIN(strftime('%Y-%m', datetime(tx.mtp_timestamp, 'unixepoch'))) AS first_action_month
|
||||||
|
FROM user_action ua
|
||||||
|
JOIN utxo_spending us ON ua.spent_utxo_hash = us.spent_utxo_hash
|
||||||
|
JOIN tx ON us.txid = tx.txid
|
||||||
|
GROUP BY ua.user
|
||||||
|
),
|
||||||
|
-- Step 2: Accumulate unique users by month
|
||||||
|
cumulative_unique_users AS (
|
||||||
|
SELECT
|
||||||
|
f.first_action_month AS month_year,
|
||||||
|
COUNT(DISTINCT f.user) AS new_unique_users
|
||||||
|
FROM first_user_action f
|
||||||
|
GROUP BY f.first_action_month
|
||||||
|
ORDER BY f.first_action_month
|
||||||
|
)
|
||||||
|
-- Step 3: Running total of unique users
|
||||||
|
SELECT
|
||||||
|
month_year,
|
||||||
|
SUM(new_unique_users) OVER (ORDER BY month_year) AS accumulated_unique_users
|
||||||
|
FROM cumulative_unique_users;
|
||||||
|
";
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare(sql)?;
|
||||||
|
let mut rows = stmt.query([])?;
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
while let Some(row) = rows.next()? {
|
||||||
|
let month_year: String = row.get(0)?;
|
||||||
|
let accumulated_unique_users: usize = row.get(1)?;
|
||||||
|
|
||||||
|
results.push((month_year, accumulated_unique_users));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ use crate::{
|
||||||
config::{config_get, config_set},
|
config::{config_get, config_set},
|
||||||
header::{db_get_header, store_headers},
|
header::{db_get_header, store_headers},
|
||||||
tx::insert_block_tx,
|
tx::insert_block_tx,
|
||||||
|
user::insert_user_action,
|
||||||
utxo_funding::insert_utxo_funding,
|
utxo_funding::insert_utxo_funding,
|
||||||
utxo_spending::insert_utxo_spending,
|
utxo_spending::insert_utxo_spending,
|
||||||
},
|
},
|
||||||
|
|
@ -86,6 +87,7 @@ pub fn update_mempool(db: DBPool, electrum: Arc<Mutex<Client>>) -> Result<()> {
|
||||||
|
|
||||||
insert_utxo_funding(&db_tx, &cauldrons, &txid, false)?;
|
insert_utxo_funding(&db_tx, &cauldrons, &txid, false)?;
|
||||||
insert_utxo_spending(&db_tx, &cauldrons, &txid, false)?;
|
insert_utxo_spending(&db_tx, &cauldrons, &txid, false)?;
|
||||||
|
insert_user_action(&db_tx, &cauldrons, &tx, true)?;
|
||||||
|
|
||||||
all_cauldrons.extend(cauldrons);
|
all_cauldrons.extend(cauldrons);
|
||||||
}
|
}
|
||||||
|
|
@ -226,6 +228,8 @@ pub fn index_blocks(
|
||||||
.context("inserting funding utxos")?;
|
.context("inserting funding utxos")?;
|
||||||
insert_utxo_spending(&db_tx, &cauldrons, &txid, true)
|
insert_utxo_spending(&db_tx, &cauldrons, &txid, true)
|
||||||
.context("inserting spending utxos")?;
|
.context("inserting spending utxos")?;
|
||||||
|
insert_user_action(&db_tx, &cauldrons, tx, true).context("inserting user actions")?;
|
||||||
|
|
||||||
total_cauldrons += cauldrons.len();
|
total_cauldrons += cauldrons.len();
|
||||||
|
|
||||||
all_cauldrons.extend(cauldrons);
|
all_cauldrons.extend(cauldrons);
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,8 @@ fn launch() -> _ {
|
||||||
rpc::pool::list_active_pools,
|
rpc::pool::list_active_pools,
|
||||||
rpc::contract::contract_count_token,
|
rpc::contract::contract_count_token,
|
||||||
rpc::contract::contract_count_all,
|
rpc::contract::contract_count_all,
|
||||||
rpc::contract::contract_volume
|
rpc::contract::contract_volume,
|
||||||
|
rpc::user::unique_addresses
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.mount("/bcmr", routes![rpc::bcmr::token_bcmr,])
|
.mount("/bcmr", routes![rpc::bcmr::token_bcmr,])
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ pub mod pool;
|
||||||
pub mod price;
|
pub mod price;
|
||||||
pub mod tokens;
|
pub mod tokens;
|
||||||
pub mod tvl;
|
pub mod tvl;
|
||||||
|
pub mod user;
|
||||||
|
|
||||||
pub type ResponseCache = Arc<Mutex<HashMap<String, (i64 /* timestamp */, Vec<serde_json::Value>)>>>;
|
pub type ResponseCache = Arc<Mutex<HashMap<String, (i64 /* timestamp */, Vec<serde_json::Value>)>>>;
|
||||||
|
|
||||||
|
|
|
||||||
26
src/rpc/user.rs
Normal file
26
src/rpc/user.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright (C) 2024 Riften Labs AS
|
||||||
|
//
|
||||||
|
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
|
||||||
|
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
||||||
|
|
||||||
|
use rocket::get;
|
||||||
|
use rocket::{http::Status, response::status::Custom, serde::json::Json, State};
|
||||||
|
use serde_json::json;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::db::{cauldron::user::get_unique_per_month_accumilating, DB};
|
||||||
|
|
||||||
|
#[get("/user/unique_addresses")]
|
||||||
|
pub fn unique_addresses(conn: &State<DB>) -> Result<Json<Value>, Custom<String>> {
|
||||||
|
let db = conn.cauldron_r.get().map_err(|_| {
|
||||||
|
Custom(
|
||||||
|
Status::InternalServerError,
|
||||||
|
"Failed to get DB connection".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let users = get_unique_per_month_accumilating(&db)
|
||||||
|
.map_err(|e| Custom(Status::InternalServerError, format!("Error: {}", e)))?;
|
||||||
|
|
||||||
|
Ok(Json(json!(users)))
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue