Skip to content

Commit 91a2175

Browse files
authored
Merge pull request #10 from pubky/ln_verification_unique_ids
Ln verification do not use payment_hash as verification identifier
2 parents 5b7f093 + f83fb22 commit 91a2175

File tree

9 files changed

+420
-38
lines changed

9 files changed

+420
-38
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,18 @@ hex = "0.4"
2020
regex = "1.11"
2121
reqwest = { version = "0.12", features = ["json"] }
2222
reqwest-websocket = "0.5.1"
23-
sea-query = { version = "0.32.6", features = [ "with-chrono", "postgres-array" ]}
24-
sea-query-binder = { version = "0.7.0", features = [ "with-chrono", "runtime-tokio", "sqlx-postgres", "postgres-array" ] }
23+
sea-query = { version = "0.32.6", features = [ "with-chrono", "postgres-array", "with-uuid" ]}
24+
sea-query-binder = { version = "0.7.0", features = [ "with-chrono", "runtime-tokio", "sqlx-postgres", "postgres-array", "with-uuid" ] }
2525
serde = { version = "1.0.228", features = ["derive"] }
2626
serde_json = "1.0"
27-
sqlx = { version = "0.8.6", features = [ "runtime-tokio", "postgres", "chrono", "tls-rustls" ] }
27+
sqlx = { version = "0.8.6", features = [ "runtime-tokio", "postgres", "chrono", "tls-rustls", "uuid" ] }
2828
thiserror = "2.0.12"
2929
tokio = { version = "1.48.0", features = ["full"] }
3030
tower-http = { version = "0.6.2", features = ["cors", "trace"] }
3131
tracing = "0.1.41"
3232
tracing-subscriber = "0.3.20"
3333
url = "2.5.7"
34-
uuid = { version = "1.18.1", features = ["v4"] }
34+
uuid = { version = "1.18.1", features = ["v4", "serde"] }
3535

3636
[dev-dependencies]
3737
axum-test = "18"

src/infrastructure/sql/migrations/m20251216_create_ln_verification.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,18 @@ impl MigrationTrait for M20251216CreateLnVerification {
1313
let statement = Table::create()
1414
.table("lightning_verifications")
1515
.if_not_exists()
16+
.col(
17+
ColumnDef::new("id")
18+
.uuid()
19+
.not_null()
20+
.primary_key()
21+
.default(sea_query::Expr::cust("gen_random_uuid()")),
22+
)
1623
.col(
1724
ColumnDef::new("payment_hash")
1825
.char_len(64)
1926
.not_null()
20-
.primary_key(),
27+
.unique_key(),
2128
) // 64 hex characters
2229
.col(ColumnDef::new("amount_sat").integer().not_null())
2330
.col(ColumnDef::new("signup_code").text().null())

src/ln_verification/http.rs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ use crate::{
1313
EnvConfig,
1414
infrastructure::http::HttpServerError,
1515
ln_verification::{
16-
app_state::AppState, error::LnVerificationError,
17-
invoice_background_syncer::InvoiceBackgroundSyncer, payment_hash::PaymentHash,
18-
phoenixd_api::PhoenixdAPI, service::LnVerificationService,
16+
VerificationId, app_state::AppState, error::LnVerificationError,
17+
invoice_background_syncer::InvoiceBackgroundSyncer, phoenixd_api::PhoenixdAPI,
18+
service::LnVerificationService,
1919
},
2020
shared::HomeserverAdminAPI,
2121
};
@@ -53,6 +53,7 @@ pub async fn router(
5353
.route("/", post(create_verification_handler))
5454
.route("/{id}", get(get_verification_handler))
5555
.route("/{id}/await", get(await_verification_handler))
56+
.route("/price", get(get_price_handler))
5657
.with_state(state))
5758
}
5859

@@ -61,9 +62,9 @@ async fn create_verification_handler(
6162
State(state): State<AppState>,
6263
) -> Result<Json<CreateVerificationResponse>, LnVerificationError> {
6364
let (verification, invoice) = state.ln_service.create_verification().await?;
64-
tracing::info!("Created verification {}", verification.payment_hash);
65+
tracing::info!("Created verification {}", verification.id);
6566
let response = CreateVerificationResponse {
66-
id: verification.payment_hash,
67+
id: verification.id,
6768
bolt11_invoice: invoice.invoice,
6869
amount_sat: invoice.requested_sat,
6970
expires_at: invoice.expires_at.timestamp_millis(),
@@ -74,7 +75,7 @@ async fn create_verification_handler(
7475
/// Get a Lightning Network verification handler
7576
async fn get_verification_handler(
7677
State(state): State<AppState>,
77-
Path(id): Path<PaymentHash>,
78+
Path(id): Path<VerificationId>,
7879
) -> Response {
7980
let verification = match state.ln_service.get_verification(&id).await {
8081
Ok(Some(verification)) => verification,
@@ -91,7 +92,7 @@ async fn get_verification_handler(
9192
/// Await for a Lightning Network verification to be finalized handler
9293
async fn await_verification_handler(
9394
State(state): State<AppState>,
94-
Path(id): Path<PaymentHash>,
95+
Path(id): Path<VerificationId>,
9596
) -> impl IntoResponse {
9697
let mut verification = match state.ln_service.get_verification(&id).await {
9798
Ok(Some(verification)) => verification,
@@ -115,7 +116,7 @@ async fn await_verification_handler(
115116
}
116117
Err(e) => return e.into_response(),
117118
};
118-
tracing::info!("Awaited verification {}", verification.payment_hash);
119+
tracing::info!("Awaited verification {}", verification.id);
119120
};
120121

121122
Json(GetVerificationResponse::from_entity(
@@ -125,10 +126,17 @@ async fn await_verification_handler(
125126
.into_response()
126127
}
127128

129+
/// Get the configured Lightning invoice price handler
130+
async fn get_price_handler(State(state): State<AppState>) -> Json<GetPriceResponse> {
131+
Json(GetPriceResponse {
132+
amount_sat: state.ln_service.get_price_sat(),
133+
})
134+
}
135+
128136
#[derive(Debug, serde::Serialize, serde::Deserialize)]
129137
#[serde(rename_all = "camelCase")]
130138
pub struct CreateVerificationResponse {
131-
id: PaymentHash,
139+
id: VerificationId,
132140
bolt11_invoice: String,
133141
amount_sat: u64,
134142
expires_at: i64,
@@ -137,7 +145,7 @@ pub struct CreateVerificationResponse {
137145
#[derive(Debug, serde::Serialize, serde::Deserialize)]
138146
#[serde(rename_all = "camelCase")]
139147
pub struct GetVerificationResponse {
140-
id: PaymentHash,
148+
id: VerificationId,
141149
amount_sat: u64,
142150
expires_at: i64,
143151
is_paid: bool,
@@ -153,7 +161,7 @@ impl GetVerificationResponse {
153161
) -> Self {
154162
let is_paid = entity.is_finalised();
155163
Self {
156-
id: entity.payment_hash,
164+
id: entity.id,
157165
amount_sat: entity.amount_sat as u64,
158166
expires_at: entity.expires_at.and_utc().timestamp_millis(),
159167
is_paid,
@@ -164,6 +172,12 @@ impl GetVerificationResponse {
164172
}
165173
}
166174

175+
#[derive(Debug, serde::Serialize, serde::Deserialize)]
176+
#[serde(rename_all = "camelCase")]
177+
pub struct GetPriceResponse {
178+
amount_sat: u64,
179+
}
180+
167181
impl IntoResponse for LnVerificationError {
168182
fn into_response(self) -> Response {
169183
let status = match self {

src/ln_verification/invoice_background_syncer.rs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use std::time::Duration;
22

33
use crate::ln_verification::{
4-
LightningVerificationEntity, error::LnVerificationError, payment_hash::PaymentHash,
5-
phoenixd_api::PhoenixdAPI, service::LnVerificationService,
4+
LightningVerificationEntity, VerificationId, error::LnVerificationError,
5+
payment_hash::PaymentHash, phoenixd_api::PhoenixdAPI, service::LnVerificationService,
66
};
77

88
use futures_util::TryStreamExt;
@@ -13,7 +13,7 @@ use tokio::sync::broadcast;
1313
pub struct InvoiceBackgroundSyncer {
1414
service: LnVerificationService,
1515
phoenixd_api: PhoenixdAPI,
16-
verification_completed_tx: broadcast::Sender<PaymentHash>,
16+
verification_completed_tx: broadcast::Sender<VerificationId>,
1717
}
1818

1919
impl InvoiceBackgroundSyncer {
@@ -29,23 +29,23 @@ impl InvoiceBackgroundSyncer {
2929

3030
/// Subscribe to finalized verification events.
3131
/// Returns a receiver that will receive notifications whenever a verification is finalized.
32-
pub fn subscribe(&self) -> broadcast::Receiver<PaymentHash> {
32+
pub fn subscribe(&self) -> broadcast::Receiver<VerificationId> {
3333
self.verification_completed_tx.subscribe()
3434
}
3535

3636
/// Await the payment to be finalized.
3737
/// Returns Ok(Some(LightningVerificationEntity)) if the payment was finalized, Ok(None) if the timeout was reached.
3838
pub async fn wait_for_payment(
3939
&self,
40-
payment_hash: &PaymentHash,
40+
id: &VerificationId,
4141
timeout: Duration,
4242
) -> Result<Option<LightningVerificationEntity>, LnVerificationError> {
4343
let future = async {
4444
let mut receiver = self.subscribe();
4545
loop {
4646
match receiver.recv().await {
47-
Ok(finalized_payment_hash) if finalized_payment_hash == *payment_hash => break,
48-
Ok(_) => continue, // Different payment hash, keep waiting
47+
Ok(finalized_id) if finalized_id == *id => break,
48+
Ok(_) => continue, // Different verification ID, keep waiting
4949
Err(broadcast::error::RecvError::Lagged(n)) => {
5050
tracing::warn!("Broadcast receiver lagged by {} messages", n);
5151
continue;
@@ -63,7 +63,7 @@ impl InvoiceBackgroundSyncer {
6363
Err(_) => return Ok(None),
6464
};
6565

66-
self.service.get_verification(payment_hash).await
66+
self.service.get_verification(id).await
6767
}
6868

6969
/// Sync an invoice.
@@ -75,10 +75,8 @@ impl InvoiceBackgroundSyncer {
7575
Some(verification) => verification,
7676
None => return Ok(()),
7777
};
78-
tracing::info!("Verification {} finalised", newly_finalised.payment_hash);
79-
let _ = self
80-
.verification_completed_tx
81-
.send(newly_finalised.payment_hash);
78+
tracing::info!("Verification {} finalised", newly_finalised.id);
79+
let _ = self.verification_completed_tx.send(newly_finalised.id);
8280
Ok(())
8381
}
8482

src/ln_verification/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@ mod payment_hash;
66
mod phoenixd_api;
77
mod repository;
88
mod service;
9+
mod verification_id;
910
pub use http::router;
1011
pub use repository::*;
12+
pub use verification_id::*;

src/ln_verification/repository.rs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
use crate::{infrastructure::sql::UnifiedExecutor, ln_verification::payment_hash::PaymentHash};
1+
use crate::{
2+
infrastructure::sql::UnifiedExecutor,
3+
ln_verification::{payment_hash::PaymentHash, verification_id::VerificationId},
4+
};
25
use chrono::NaiveDateTime;
36
use sea_query::{Expr, PostgresQueryBuilder, Query};
47
use sea_query_binder::SqlxBinder;
58

69
#[derive(Debug, Clone, sqlx::FromRow)]
710
pub struct LightningVerificationEntity {
11+
pub id: VerificationId,
12+
#[allow(dead_code)]
13+
// Stored for data integrity and testing. Sync operations use payment_hash from phoenixd API responses
814
pub payment_hash: PaymentHash,
915
pub amount_sat: i32,
1016
pub expires_at: NaiveDateTime,
@@ -65,6 +71,46 @@ impl LnVerificationRepository {
6571
Ok(verification)
6672
}
6773

74+
/// Get a verification by its public ID (for API requests)
75+
///
76+
/// # Arguments
77+
/// * `id` - The verification ID
78+
/// * `executor` - The executor to use to execute the query
79+
///
80+
/// # Returns
81+
/// * `LightningVerificationEntity` - The verification record
82+
///
83+
/// # Errors
84+
/// * `sqlx::Error` - If the query fails
85+
pub async fn get_verification_by_id<'a>(
86+
id: &VerificationId,
87+
executor: &mut UnifiedExecutor<'a>,
88+
) -> Result<Option<LightningVerificationEntity>, sqlx::Error> {
89+
let statement = Query::select()
90+
.columns([
91+
"id",
92+
"payment_hash",
93+
"amount_sat",
94+
"created_at",
95+
"expires_at",
96+
"finalised_at",
97+
"signup_code",
98+
])
99+
.from(TABLE_NAME)
100+
.and_where(Expr::col("id").eq(*id.as_uuid()))
101+
.to_owned();
102+
let (query, values) = statement.build_sqlx(PostgresQueryBuilder);
103+
let con = executor.get_con().await?;
104+
let verification: LightningVerificationEntity =
105+
match sqlx::query_as_with(&query, values).fetch_one(con).await {
106+
Ok(verification) => verification,
107+
Err(sqlx::Error::RowNotFound) => return Ok(None),
108+
Err(e) => return Err(e),
109+
};
110+
111+
Ok(Some(verification))
112+
}
113+
68114
/// Get a verification by payment hash
69115
///
70116
/// # Arguments
@@ -82,6 +128,7 @@ impl LnVerificationRepository {
82128
) -> Result<Option<LightningVerificationEntity>, sqlx::Error> {
83129
let statement = Query::select()
84130
.columns([
131+
"id",
85132
"payment_hash",
86133
"amount_sat",
87134
"created_at",
@@ -316,4 +363,39 @@ mod tests {
316363
.unwrap();
317364
assert_eq!(timestamp, Some(veri1.created_at));
318365
}
366+
367+
#[sqlx::test]
368+
async fn test_get_verification_by_id(pool: PgPool) {
369+
let db = SqlDb::test(pool).await;
370+
let payment_hash = PaymentHash::random();
371+
let expires_at = Utc::now().naive_utc();
372+
let veri = LnVerificationRepository::create_verification(
373+
&payment_hash,
374+
1000,
375+
expires_at,
376+
&mut db.pool().into(),
377+
)
378+
.await
379+
.unwrap();
380+
381+
// Should be able to get by ID
382+
let veri_by_id =
383+
LnVerificationRepository::get_verification_by_id(&veri.id, &mut db.pool().into())
384+
.await
385+
.unwrap()
386+
.unwrap();
387+
388+
assert_eq!(veri_by_id.id, veri.id);
389+
assert_eq!(veri_by_id.payment_hash, payment_hash);
390+
assert_eq!(veri_by_id.amount_sat, 1000);
391+
392+
// Non-existent ID should return None
393+
let non_existent = LnVerificationRepository::get_verification_by_id(
394+
&VerificationId::random(),
395+
&mut db.pool().into(),
396+
)
397+
.await
398+
.unwrap();
399+
assert!(non_existent.is_none());
400+
}
319401
}

0 commit comments

Comments
 (0)