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

payjoin / rust-payjoin / 29624071391

18 Jul 2026 12:53AM UTC coverage: 85.913% (-0.4%) from 86.298%
29624071391

Pull #1035

github

web-flow
Merge 296295916 into d27b7b137
Pull Request #1035: Payjoin-cli should cache ohttp-keys for re-use

14 of 91 new or added lines in 2 files covered. (15.38%)

13826 of 16093 relevant lines covered (85.91%)

342.72 hits per line

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

58.65
/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 {
36✔
13
    std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64
36✔
14
}
36✔
15

16
/// Approximately three months (90 days).
17
/// This would be dropped when key-rotation goes into effect.
18
#[cfg(feature = "v2")]
19
pub(crate) const OHTTP_KEY_CACHE_TTL_SECS: i64 = 90 * 24 * 60 * 60;
20

21
#[cfg(feature = "v2")]
22
#[derive(Debug, Clone)]
23
pub(crate) struct CachedOhttpKeys {
24
    pub(crate) keys: payjoin::OhttpKeys,
25
    pub(crate) expires_at: i64,
26
}
27

28
#[cfg(feature = "v2")]
29
impl CachedOhttpKeys {
NEW
30
    pub(crate) fn is_expired(&self) -> bool { now() >= self.expires_at }
×
31
}
32

33
pub(crate) const DB_PATH: &str = "payjoin.sqlite";
34

35
#[derive(Debug)]
36
pub(crate) struct Database(Pool<SqliteConnectionManager>);
37

38
impl Database {
39
    pub(crate) fn create(path: impl AsRef<Path>) -> Result<Self> {
24✔
40
        // locking_mode is a per-connection PRAGMA, so it must be set via
41
        // with_init to apply to every connection the pool creates, not only
42
        // the first one used during init_schema.
43
        let manager = SqliteConnectionManager::file(path.as_ref())
24✔
44
            .with_init(|conn| conn.execute_batch("PRAGMA locking_mode = EXCLUSIVE;"));
240✔
45
        let pool = Pool::new(manager)?;
24✔
46

47
        // Initialize database schema
48
        let conn = pool.get()?;
24✔
49
        Self::init_schema(&conn)?;
24✔
50

51
        Ok(Self(pool))
24✔
52
    }
24✔
53

54
    fn init_schema(conn: &Connection) -> Result<()> {
27✔
55
        // Enable foreign keys
56
        conn.execute("PRAGMA foreign_keys = ON", [])?;
27✔
57

58
        conn.execute(
27✔
59
            "CREATE TABLE IF NOT EXISTS send_sessions (
27✔
60
                session_id TEXT PRIMARY KEY,
27✔
61
                pj_uri TEXT NOT NULL,
27✔
62
                receiver_pubkey BLOB NOT NULL,
27✔
63
                completed_at INTEGER
27✔
64
            )",
27✔
65
            [],
27✔
66
        )?;
×
67

68
        conn.execute(
27✔
69
            "CREATE TABLE IF NOT EXISTS receive_sessions (
27✔
70
                session_id TEXT PRIMARY KEY,
27✔
71
                completed_at INTEGER
27✔
72
            )",
27✔
73
            [],
27✔
74
        )?;
×
75

76
        conn.execute(
27✔
77
            "CREATE TABLE IF NOT EXISTS send_session_events (
27✔
78
                id INTEGER PRIMARY KEY AUTOINCREMENT,
27✔
79
                session_id TEXT NOT NULL,
27✔
80
                event_data TEXT NOT NULL,
27✔
81
                created_at INTEGER NOT NULL,
27✔
82
                FOREIGN KEY(session_id) REFERENCES send_sessions(session_id)
27✔
83
            )",
27✔
84
            [],
27✔
85
        )?;
×
86

87
        conn.execute(
27✔
88
            "CREATE TABLE IF NOT EXISTS receive_session_events (
27✔
89
                id INTEGER PRIMARY KEY AUTOINCREMENT,
27✔
90
                session_id TEXT NOT NULL,
27✔
91
                event_data TEXT NOT NULL,
27✔
92
                created_at INTEGER NOT NULL,
27✔
93
                FOREIGN KEY(session_id) REFERENCES receive_sessions(session_id)
27✔
94
            )",
27✔
95
            [],
27✔
96
        )?;
×
97

98
        conn.execute(
27✔
99
            "CREATE TABLE IF NOT EXISTS inputs_seen (
27✔
100
                outpoint BLOB PRIMARY KEY,
27✔
101
                created_at INTEGER NOT NULL
27✔
102
            )",
27✔
103
            [],
27✔
104
        )?;
×
105

106
        conn.execute(
27✔
107
            "CREATE TABLE IF NOT EXISTS ohttp_key_cache (
27✔
108
                 directory_url TEXT PRIMARY KEY,
27✔
109
                 ohttp_keys BLOB NOT NULL,
27✔
110
                 expires_at INTEGER NOT NULL
27✔
111
            )",
27✔
112
            [],
27✔
NEW
113
        )?;
×
114

115
        let has_expires_at: bool = conn.query_row(
27✔
116
            "SELECT COUNT(*) FROM pragma_table_info('ohttp_key_cache') WHERE name = 'expires_at'",
27✔
117
            [],
27✔
118
            |row| row.get::<_, i64>(0),
27✔
NEW
119
        )? > 0;
×
120
        if !has_expires_at {
27✔
NEW
121
            conn.execute(
×
NEW
122
                "ALTER TABLE ohttp_key_cache ADD COLUMN expires_at INTEGER NOT NULL DEFAULT 0",
×
NEW
123
                [],
×
NEW
124
            )?;
×
125
        }
27✔
126

127
        Ok(())
27✔
128
    }
27✔
129

130
    pub(crate) fn get_connection(&self) -> Result<r2d2::PooledConnection<SqliteConnectionManager>> {
82✔
131
        Ok(self.0.get()?)
82✔
132
    }
82✔
133
    /// Inserts the input and returns true if the input was seen before, false otherwise.
134
    pub(crate) fn insert_input_seen_before(&self, input: OutPoint) -> Result<bool> {
4✔
135
        let conn = self.get_connection()?;
4✔
136
        let key = serialize(&input);
4✔
137

138
        let was_seen_before = conn.execute(
4✔
139
            "INSERT OR IGNORE INTO inputs_seen (outpoint, created_at) VALUES (?1, ?2)",
4✔
140
            params![key, now()],
4✔
141
        )? == 0;
4✔
142

143
        Ok(was_seen_before)
4✔
144
    }
4✔
145
    #[cfg(feature = "v2")]
NEW
146
    pub(crate) fn get_cached_ohttp_keys(
×
NEW
147
        &self,
×
NEW
148
        directory_url: &str,
×
NEW
149
    ) -> Result<Option<CachedOhttpKeys>> {
×
NEW
150
        let conn = self.get_connection()?;
×
NEW
151
        let result = conn.query_row(
×
NEW
152
            "SELECT ohttp_keys, expires_at FROM ohttp_key_cache WHERE directory_url = ?1",
×
NEW
153
            params![directory_url],
×
NEW
154
            |row| Ok((row.get::<_, Vec<u8>>(0)?, row.get::<_, i64>(1)?)),
×
155
        );
NEW
156
        match result {
×
NEW
157
            Ok((bytes, expires_at)) => {
×
NEW
158
                let keys = payjoin::OhttpKeys::decode(&bytes)
×
NEW
159
                    .map_err(|e| {
×
NEW
160
                        tracing::error!("Failed to decode ohttp keys: {}", e);
×
NEW
161
                    })
×
NEW
162
                    .ok();
×
NEW
163
                Ok(keys.map(|keys| CachedOhttpKeys { keys, expires_at }))
×
164
            }
NEW
165
            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
×
NEW
166
            Err(e) => Err(Error::Rusqlite(e)),
×
167
        }
NEW
168
    }
×
169

170
    #[cfg(feature = "v2")]
NEW
171
    pub(crate) fn store_ohttp_keys(
×
NEW
172
        &self,
×
NEW
173
        directory_url: &str,
×
NEW
174
        keys: &payjoin::OhttpKeys,
×
NEW
175
    ) -> Result<()> {
×
NEW
176
        let conn = self.get_connection()?;
×
177

NEW
178
        let encoded =
×
NEW
179
            keys.encode().map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
×
NEW
180
        let expires_at = now() + OHTTP_KEY_CACHE_TTL_SECS;
×
181

NEW
182
        conn.execute(
×
NEW
183
            "INSERT OR REPLACE INTO ohttp_key_cache (directory_url, ohttp_keys, expires_at) VALUES (?1, ?2, ?3)",
×
NEW
184
            params![directory_url, encoded, expires_at],
×
NEW
185
        )?;
×
186

NEW
187
        Ok(())
×
NEW
188
    }
×
189

190
    #[cfg(feature = "v2")]
NEW
191
    pub(crate) fn invalidate_ohttp_key_cache(&self, directory_url: &str) -> Result<()> {
×
NEW
192
        let conn = self.get_connection()?;
×
NEW
193
        conn.execute(
×
NEW
194
            "DELETE FROM ohttp_key_cache WHERE directory_url = ?1",
×
NEW
195
            params![directory_url],
×
NEW
196
        )?;
×
NEW
197
        Ok(())
×
NEW
198
    }
×
199
}
200

201
#[cfg(feature = "v2")]
202
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