Skip to main content

alibi_sqlx/store/entities/
wallet_address.rs

1use crate::SqlxModel;
2use crate::value::{ColumnKind, SqlValue, SqlxValue, ValueTypeError};
3use alibi_core::WalletAddress;
4use chrono::{DateTime, Utc};
5use sqlx::error::BoxDynError;
6#[cfg(feature = "postgres")]
7use sqlx::postgres::{PgTypeInfo, PgValueRef, Postgres};
8#[cfg(feature = "sqlite")]
9use sqlx::sqlite::{Sqlite, SqliteTypeInfo, SqliteValueRef};
10use sqlx::{Decode, Type, ValueRef};
11
12/// Better Auth's number columns use SQLite INTEGER affinity.
13///
14/// SQLite may return either an integer or a real for the same column, including large JS chain
15/// IDs. PostgreSQL returns its INTEGER as i32. Retain the value as a JS Number without requiring a
16/// different persisted column type.
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct WalletChainId(pub f64);
19
20impl From<f64> for WalletChainId {
21    fn from(value: f64) -> Self {
22        Self(value)
23    }
24}
25
26impl SqlxValue for WalletChainId {
27    const KIND: ColumnKind = ColumnKind::Other;
28    fn null() -> SqlValue {
29        SqlValue::Double(None)
30    }
31    #[expect(
32        clippy::as_conversions,
33        clippy::cast_possible_truncation,
34        clippy::cast_precision_loss,
35        reason = "JavaScript-compatible numbers deliberately retain IEEE754 rounding and guarded integer coercion at the wire or adapter boundary"
36    )]
37    fn into_sql_value(self) -> SqlValue {
38        if self.0.fract() == 0.0 && self.0 >= i64::MIN as f64 && self.0 < i64::MAX as f64 {
39            SqlValue::BigInt(Some(self.0 as i64))
40        } else {
41            SqlValue::Double(Some(self.0))
42        }
43    }
44    #[expect(
45        clippy::as_conversions,
46        clippy::cast_precision_loss,
47        reason = "JavaScript-compatible numbers deliberately retain IEEE754 rounding and guarded integer coercion at the wire or adapter boundary"
48    )]
49    fn from_sql_value(value: SqlValue) -> Result<Self, ValueTypeError> {
50        match value {
51            SqlValue::Double(Some(value)) => Ok(Self(value)),
52            SqlValue::BigInt(Some(value)) => Ok(Self(value as f64)),
53            SqlValue::Int(Some(value)) => Ok(Self(f64::from(value))),
54            _ => Err(ValueTypeError),
55        }
56    }
57}
58
59#[cfg(feature = "sqlite")]
60impl Type<Sqlite> for WalletChainId {
61    fn type_info() -> SqliteTypeInfo {
62        <f64 as Type<Sqlite>>::type_info()
63    }
64    fn compatible(ty: &SqliteTypeInfo) -> bool {
65        <f64 as Type<Sqlite>>::compatible(ty)
66            || <i64 as Type<Sqlite>>::compatible(ty)
67            || <i32 as Type<Sqlite>>::compatible(ty)
68    }
69}
70
71#[cfg(feature = "sqlite")]
72impl<'r> Decode<'r, Sqlite> for WalletChainId {
73    #[expect(
74        clippy::as_conversions,
75        clippy::cast_precision_loss,
76        reason = "JavaScript-compatible numbers deliberately retain IEEE754 rounding and guarded integer coercion at the wire or adapter boundary"
77    )]
78    fn decode(value: SqliteValueRef<'r>) -> Result<Self, BoxDynError> {
79        // Try f64, then i64, then i32, as the typed column decoders would.
80        let ty = value.type_info().into_owned();
81        if <f64 as Type<Sqlite>>::compatible(&ty) {
82            <f64 as Decode<'r, Sqlite>>::decode(value).map(Self)
83        } else if <i64 as Type<Sqlite>>::compatible(&ty) {
84            <i64 as Decode<'r, Sqlite>>::decode(value).map(|value| Self(value as f64))
85        } else {
86            <i32 as Decode<'r, Sqlite>>::decode(value).map(|value| Self(f64::from(value)))
87        }
88    }
89}
90
91#[cfg(feature = "postgres")]
92impl Type<Postgres> for WalletChainId {
93    fn type_info() -> PgTypeInfo {
94        <f64 as Type<Postgres>>::type_info()
95    }
96    fn compatible(ty: &PgTypeInfo) -> bool {
97        <f64 as Type<Postgres>>::compatible(ty)
98            || <i64 as Type<Postgres>>::compatible(ty)
99            || <i32 as Type<Postgres>>::compatible(ty)
100    }
101}
102
103#[cfg(feature = "postgres")]
104impl<'r> Decode<'r, Postgres> for WalletChainId {
105    #[expect(
106        clippy::as_conversions,
107        clippy::cast_precision_loss,
108        reason = "JavaScript-compatible numbers deliberately retain IEEE754 rounding and guarded integer coercion at the wire or adapter boundary"
109    )]
110    fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
111        let ty = value.type_info().into_owned();
112        if <f64 as Type<Postgres>>::compatible(&ty) {
113            <f64 as Decode<'r, Postgres>>::decode(value).map(Self)
114        } else if <i64 as Type<Postgres>>::compatible(&ty) {
115            <i64 as Decode<'r, Postgres>>::decode(value).map(|value| Self(value as f64))
116        } else {
117            <i32 as Decode<'r, Postgres>>::decode(value).map(|value| Self(f64::from(value)))
118        }
119    }
120}
121
122#[derive(Clone, Debug, PartialEq, sqlx::FromRow, SqlxModel)]
123#[auth(table = "wallet_address")]
124pub struct Model {
125    pub id: String,
126    pub user_id: String,
127    pub address: String,
128    pub chain_id: WalletChainId,
129    pub is_primary: bool,
130    pub created_at: DateTime<Utc>,
131}
132
133impl From<Model> for WalletAddress {
134    fn from(row: Model) -> Self {
135        Self {
136            id: row.id,
137            user_id: row.user_id,
138            address: row.address,
139            chain_id: row.chain_id.0,
140            is_primary: row.is_primary,
141            created_at: row.created_at,
142        }
143    }
144}