Skip to main content

alibi_sqlx/store/
wallets.rs

1use super::SqlxStore;
2use super::entities::wallet_address::{self, WalletChainId};
3use crate::model::{self, ActiveRow, SqlxModel};
4use crate::pool::{Exec, SqlxTransaction};
5use crate::schema::{AuthSchema, SqlxUserModel};
6use crate::sql::Sql;
7use crate::value::SqlxValue;
8use alibi_core::error::{AuthError, AuthResult};
9use alibi_core::store::WalletAddressStore;
10use alibi_core::{AuthUser, CreateWalletAddress, WalletAddress};
11use async_trait::async_trait;
12use chrono::Utc;
13use uuid::Uuid;
14
15#[async_trait]
16impl<S> WalletAddressStore for SqlxStore<S>
17where
18    S: AuthSchema + Send + Sync,
19    S::User: SqlxUserModel,
20{
21    async fn get_wallet_address(
22        &self,
23        address: &str,
24        chain_id: Option<f64>,
25    ) -> AuthResult<Option<WalletAddress>> {
26        let mut sql = model::select_model::<wallet_address::Model>(self.exec());
27        sql.push(" WHERE ");
28        sql.compare(wallet_address::Model::TABLE, "address", " = ", address);
29        if let Some(chain_id) = chain_id {
30            sql.push(" AND ");
31            sql.compare(wallet_address::Model::TABLE, "chain_id", " = ", chain_id);
32        }
33        model::limit_one(&mut sql);
34        // The upstream adapter uses findOne without ordering. In SQLite this
35        // preserves insertion order for an address-only scan; a chain/address
36        // index or creation-time sort would change the selected owner.
37        Ok(self
38            .exec()
39            .fetch_optional::<wallet_address::Model>(sql)
40            .await?
41            .map(Into::into))
42    }
43
44    async fn create_wallet_address(&self, data: CreateWalletAddress) -> AuthResult<WalletAddress> {
45        if !data.chain_id.is_finite() {
46            return Err(AuthError::bad_request("Wallet chain ID must be finite"));
47        }
48        self.in_transaction(true, async move |tx| {
49            let exec = Exec::tx(tx);
50            let owner = super::users::find_user_by_id::<S::User>(
51                exec,
52                &data.user_id,
53                super::users::Lock::Shared,
54            )
55            .await?
56            .ok_or(AuthError::UserNotFound)?;
57            let mut active = ActiveRow::new();
58            active.set(
59                "id",
60                self.generated_id(self.exec(), "walletAddress", "wallet_address", "id")
61                    .await?
62                    .unwrap_or_else(|| Uuid::new_v4().to_string()),
63            );
64            active.set("user_id", owner.id().into_owned());
65            active.set("address", data.address);
66            active.set(
67                "chain_id",
68                WalletChainId::from(data.chain_id).into_sql_value(),
69            );
70            active.set("is_primary", data.is_primary);
71            active.set("created_at", Utc::now());
72            let created = model::insert::<wallet_address::Model>(exec, &active).await?;
73            Ok(created.into())
74        })
75        .await
76    }
77}
78
79pub(super) async fn remove_owned_wallets(tx: &SqlxTransaction, user_id: &str) -> AuthResult<()> {
80    let exec = Exec::tx(tx);
81    if !super::migrator::has_table(exec, "wallet_address").await? {
82        return Ok(());
83    }
84    let mut sql = Sql::with(exec.engine(), "DELETE FROM ");
85    sql.ident(wallet_address::Model::TABLE);
86    sql.push(" WHERE ");
87    sql.compare(wallet_address::Model::TABLE, "user_id", " = ", user_id);
88    _ = exec.execute(sql).await?;
89    Ok(())
90}