107 lines
3.2 KiB
Rust
107 lines
3.2 KiB
Rust
// 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) ON DELETE CASCADE,
|
|
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)
|
|
}
|