• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

payjoin / rust-payjoin / 26917175140

03 Jun 2026 10:32PM UTC coverage: 84.79% (-0.5%) from 85.31%
26917175140

Pull #1035

github

web-flow
Merge 841a2d222 into fec44af10
Pull Request #1035: Payjoin-cli should cache ohttp-keys for re-use

7 of 101 new or added lines in 2 files covered. (6.93%)

96 existing lines in 4 files now uncovered.

11835 of 13958 relevant lines covered (84.79%)

389.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.54
/payjoin-cli/src/db/mod.rs
1
use std::path::Path;
2

3
use payjoin::bitcoin::consensus::encode::serialize;
4
use payjoin::bitcoin::OutPoint;
5
use r2d2::Pool;
6
use r2d2_sqlite::SqliteConnectionManager;
7
use rusqlite::{params, Connection};
8

9
pub(crate) mod error;
10
use error::*;
11

12
pub(crate) fn now() -> i64 {
28✔
13
    std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64
28✔
14
}
28✔
15

16
pub(crate) const DB_PATH: &str = "payjoin.sqlite";
17

18
#[derive(Debug)]
19
pub(crate) struct Database(Pool<SqliteConnectionManager>);
20

21
impl Database {
22
    pub(crate) fn create(path: impl AsRef<Path>) -> Result<Self> {
17✔
23
        // locking_mode is a per-connection PRAGMA, so it must be set via
24
        // with_init to apply to every connection the pool creates, not only
25
        // the first one used during init_schema.
26
        let manager = SqliteConnectionManager::file(path.as_ref())
17✔
27
            .with_init(|conn| conn.execute_batch("PRAGMA locking_mode = EXCLUSIVE;"));
170✔
28
        let pool = Pool::new(manager)?;
17✔
29

30
        // Initialize database schema
31
        let conn = pool.get()?;
17✔
32
        Self::init_schema(&conn)?;
17✔
33

34
        Ok(Self(pool))
17✔
35
    }
17✔
36

37
    fn init_schema(conn: &Connection) -> Result<()> {
20✔
38
        // Enable foreign keys
39
        conn.execute("PRAGMA foreign_keys = ON", [])?;
20✔
40

41
        conn.execute(
20✔
42
            "CREATE TABLE IF NOT EXISTS send_sessions (
20✔
43
                session_id INTEGER PRIMARY KEY AUTOINCREMENT,
20✔
44
                pj_uri TEXT NOT NULL,
20✔
45
                receiver_pubkey BLOB NOT NULL,
20✔
46
                completed_at INTEGER
20✔
47
            )",
20✔
48
            [],
20✔
49
        )?;
×
50

51
        conn.execute(
20✔
52
            "CREATE TABLE IF NOT EXISTS receive_sessions (
20✔
53
                session_id INTEGER PRIMARY KEY AUTOINCREMENT,
20✔
54
                completed_at INTEGER
20✔
55
            )",
20✔
56
            [],
20✔
57
        )?;
×
58

59
        conn.execute(
20✔
60
            "CREATE TABLE IF NOT EXISTS send_session_events (
20✔
61
                id INTEGER PRIMARY KEY AUTOINCREMENT,
20✔
62
                session_id INTEGER NOT NULL,
20✔
63
                event_data TEXT NOT NULL,
20✔
64
                created_at INTEGER NOT NULL,
20✔
65
                FOREIGN KEY(session_id) REFERENCES send_sessions(session_id)
20✔
66
            )",
20✔
67
            [],
20✔
68
        )?;
×
69

70
        conn.execute(
20✔
71
            "CREATE TABLE IF NOT EXISTS receive_session_events (
20✔
72
                id INTEGER PRIMARY KEY AUTOINCREMENT,
20✔
73
                session_id INTEGER NOT NULL,
20✔
74
                event_data TEXT NOT NULL,
20✔
75
                created_at INTEGER NOT NULL,
20✔
76
                FOREIGN KEY(session_id) REFERENCES receive_sessions(session_id)
20✔
77
            )",
20✔
78
            [],
20✔
79
        )?;
×
80

81
        conn.execute(
20✔
82
            "CREATE TABLE IF NOT EXISTS inputs_seen (
20✔
83
                outpoint BLOB PRIMARY KEY,
20✔
84
                created_at INTEGER NOT NULL
20✔
85
            )",
20✔
86
            [],
20✔
87
        )?;
×
88

89
        conn.execute(
20✔
90
            "CREATE TABLE IF NOT EXISTS ohttp_cache (
20✔
91
                directory_url TEXT PRIMARY KEY,
20✔
92
                ohttp_keys BLOB NOT NULL,
20✔
93
                expires_at INTEGER NOT NULL
20✔
94
            )",
20✔
95
            [],
20✔
NEW
96
        )?;
×
97

98
        Ok(())
20✔
99
    }
20✔
100

101
    pub(crate) fn get_connection(&self) -> Result<r2d2::PooledConnection<SqliteConnectionManager>> {
57✔
102
        Ok(self.0.get()?)
57✔
103
    }
57✔
104
    /// Inserts the input and returns true if the input was seen before, false otherwise.
105
    pub(crate) fn insert_input_seen_before(&self, input: OutPoint) -> Result<bool> {
4✔
106
        let conn = self.get_connection()?;
4✔
107
        let key = serialize(&input);
4✔
108

109
        let was_seen_before = conn.execute(
4✔
110
            "INSERT OR IGNORE INTO inputs_seen (outpoint, created_at) VALUES (?1, ?2)",
4✔
111
            params![key, now()],
4✔
112
        )? == 0;
4✔
113

114
        Ok(was_seen_before)
4✔
115
    }
4✔
116

NEW
117
    pub(crate) fn get_cached_ohttp_keys(
×
NEW
118
        &self,
×
NEW
119
        directory_url: &str,
×
NEW
120
    ) -> Result<Option<payjoin::OhttpKeys>> {
×
NEW
121
        let conn = self.get_connection()?;
×
NEW
122
        let result = conn.query_row(
×
NEW
123
            "SELECT ohttp_keys FROM ohttp_cache WHERE directory_url = ?1 AND expires_at > ?2",
×
NEW
124
            params![directory_url, now()],
×
NEW
125
            |row| row.get::<_, Vec<u8>>(0),
×
126
        );
127

NEW
128
        match result {
×
NEW
129
            Ok(bytes) => {
×
NEW
130
                let keys = payjoin::OhttpKeys::decode(&bytes)
×
NEW
131
                    .map_err(|e| {
×
NEW
132
                        tracing::error!("Failed to decode OHTTP keys: {:?}", e);
×
NEW
133
                    })
×
NEW
134
                    .ok();
×
NEW
135
                Ok(keys)
×
136
            }
NEW
137
            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
×
NEW
138
            Err(e) => Err(Error::Rusqlite(e)),
×
139
        }
NEW
140
    }
×
141

NEW
142
    pub(crate) fn store_ohttp_keys(
×
NEW
143
        &self,
×
NEW
144
        directory_url: &str,
×
NEW
145
        keys: &payjoin::OhttpKeys,
×
NEW
146
        expires_at: i64,
×
NEW
147
    ) -> Result<()> {
×
NEW
148
        let conn = self.get_connection()?;
×
NEW
149
        let encoded =
×
NEW
150
            keys.encode().map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
×
151

NEW
152
        conn.execute(
×
NEW
153
            "INSERT OR REPLACE INTO ohttp_cache (directory_url, ohttp_keys, expires_at) VALUES (?1, ?2, ?3)
×
NEW
154
            ON CONFLICT(directory_url) DO UPDATE SET
×
NEW
155
            ohttp_keys = excluded.ohttp_keys,
×
NEW
156
            expires_at = excluded.expires_at
×
NEW
157
            ",
×
NEW
158
            params![directory_url, encoded, expires_at],
×
NEW
159
        )?;
×
160

NEW
161
        Ok(())
×
NEW
162
    }
×
163
}
164

165
#[cfg(feature = "v2")]
166
pub(crate) mod v2;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc