// 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, 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 = 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> { 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) }