|
|
@@ -47,7 +47,13 @@ impl SqlStore {
|
|
|
.await
|
|
|
.map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
|
|
|
- let migrations: &[(&str, &str)] = &[("001_init", include_str!("migrations/001_init.sql"))];
|
|
|
+ let migrations: &[(&str, &str)] = &[
|
|
|
+ ("001_init", include_str!("migrations/001_init.sql")),
|
|
|
+ (
|
|
|
+ "002_subaccounts",
|
|
|
+ include_str!("migrations/002_subaccounts.sql"),
|
|
|
+ ),
|
|
|
+ ];
|
|
|
|
|
|
for (name, sql) in migrations {
|
|
|
let applied = sqlx::query("SELECT 1 FROM _migrations WHERE name = $1")
|
|
|
@@ -160,6 +166,9 @@ fn row_to_account(row: &sqlx::any::AnyRow) -> Result<Account, StoreError> {
|
|
|
let id: i64 = row
|
|
|
.try_get("id")
|
|
|
.map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
+ let subaccount: i64 = row
|
|
|
+ .try_get("subaccount")
|
|
|
+ .map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
let version: i64 = row
|
|
|
.try_get("version")
|
|
|
.map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
@@ -180,7 +189,7 @@ fn row_to_account(row: &sqlx::any::AnyRow) -> Result<Account, StoreError> {
|
|
|
.map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
|
|
|
Ok(Account {
|
|
|
- id: AccountId::new(id),
|
|
|
+ id: AccountId::with_sub(id, subaccount),
|
|
|
version: version as u64,
|
|
|
policy: deserialize_policy(&policy_str)?,
|
|
|
flags: AccountFlags::from_bits_truncate(flags_bits as u32),
|
|
|
@@ -200,6 +209,9 @@ fn row_to_posting(row: &sqlx::any::AnyRow) -> Result<Posting, StoreError> {
|
|
|
let owner: i64 = row
|
|
|
.try_get("owner")
|
|
|
.map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
+ let subaccount: i64 = row
|
|
|
+ .try_get("subaccount")
|
|
|
+ .map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
let asset: i32 = row
|
|
|
.try_get("asset")
|
|
|
.map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
@@ -219,7 +231,7 @@ fn row_to_posting(row: &sqlx::any::AnyRow) -> Result<Posting, StoreError> {
|
|
|
transfer: envelope_id_from_hex(&transfer_id)?,
|
|
|
index: idx as u16,
|
|
|
},
|
|
|
- owner: AccountId::new(owner),
|
|
|
+ owner: AccountId::with_sub(owner, subaccount),
|
|
|
asset: AssetId::new(asset as u32),
|
|
|
value,
|
|
|
status: status_from_i16(status)?,
|
|
|
@@ -234,12 +246,15 @@ fn row_to_posting(row: &sqlx::any::AnyRow) -> Result<Posting, StoreError> {
|
|
|
#[async_trait]
|
|
|
impl AccountStore for SqlStore {
|
|
|
async fn get_account(&self, id: &AccountId) -> Result<Account, StoreError> {
|
|
|
- let row = sqlx::query("SELECT * FROM accounts WHERE id = $1 ORDER BY version DESC LIMIT 1")
|
|
|
- .bind(id.0)
|
|
|
- .fetch_optional(&self.pool)
|
|
|
- .await
|
|
|
- .map_err(|e| StoreError::Internal(e.to_string()))?
|
|
|
- .ok_or_else(|| StoreError::NotFound(format!("account {id:?}")))?;
|
|
|
+ let row = sqlx::query(
|
|
|
+ "SELECT * FROM accounts WHERE id = $1 AND subaccount = $2 ORDER BY version DESC LIMIT 1",
|
|
|
+ )
|
|
|
+ .bind(id.id)
|
|
|
+ .bind(id.sub)
|
|
|
+ .fetch_optional(&self.pool)
|
|
|
+ .await
|
|
|
+ .map_err(|e| StoreError::Internal(e.to_string()))?
|
|
|
+ .ok_or_else(|| StoreError::NotFound(format!("account {id:?}")))?;
|
|
|
row_to_account(&row)
|
|
|
}
|
|
|
|
|
|
@@ -252,11 +267,13 @@ impl AccountStore for SqlStore {
|
|
|
}
|
|
|
|
|
|
async fn create_account(&self, account: Account) -> Result<(), StoreError> {
|
|
|
- let exists = sqlx::query("SELECT 1 FROM accounts WHERE id = $1 LIMIT 1")
|
|
|
- .bind(account.id.0)
|
|
|
- .fetch_optional(&self.pool)
|
|
|
- .await
|
|
|
- .map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
+ let exists =
|
|
|
+ sqlx::query("SELECT 1 FROM accounts WHERE id = $1 AND subaccount = $2 LIMIT 1")
|
|
|
+ .bind(account.id.id)
|
|
|
+ .bind(account.id.sub)
|
|
|
+ .fetch_optional(&self.pool)
|
|
|
+ .await
|
|
|
+ .map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
if exists.is_some() {
|
|
|
return Err(StoreError::AlreadyExists(format!(
|
|
|
"account {:?}",
|
|
|
@@ -265,9 +282,10 @@ impl AccountStore for SqlStore {
|
|
|
}
|
|
|
|
|
|
sqlx::query(
|
|
|
- "INSERT INTO accounts (id, version, policy, flags, book, user_data, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7)"
|
|
|
+ "INSERT INTO accounts (id, subaccount, version, policy, flags, book, user_data, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)"
|
|
|
)
|
|
|
- .bind(account.id.0)
|
|
|
+ .bind(account.id.id)
|
|
|
+ .bind(account.id.sub)
|
|
|
.bind(account.version as i64)
|
|
|
.bind(serialize_policy(&account.policy)?)
|
|
|
.bind(account.flags.bits() as i32)
|
|
|
@@ -281,13 +299,15 @@ impl AccountStore for SqlStore {
|
|
|
}
|
|
|
|
|
|
async fn append_account_version(&self, account: Account) -> Result<(), StoreError> {
|
|
|
- let current =
|
|
|
- sqlx::query("SELECT version FROM accounts WHERE id = $1 ORDER BY version DESC LIMIT 1")
|
|
|
- .bind(account.id.0)
|
|
|
- .fetch_optional(&self.pool)
|
|
|
- .await
|
|
|
- .map_err(|e| StoreError::Internal(e.to_string()))?
|
|
|
- .ok_or_else(|| StoreError::NotFound(format!("account {:?}", account.id)))?;
|
|
|
+ let current = sqlx::query(
|
|
|
+ "SELECT version FROM accounts WHERE id = $1 AND subaccount = $2 ORDER BY version DESC LIMIT 1",
|
|
|
+ )
|
|
|
+ .bind(account.id.id)
|
|
|
+ .bind(account.id.sub)
|
|
|
+ .fetch_optional(&self.pool)
|
|
|
+ .await
|
|
|
+ .map_err(|e| StoreError::Internal(e.to_string()))?
|
|
|
+ .ok_or_else(|| StoreError::NotFound(format!("account {:?}", account.id)))?;
|
|
|
|
|
|
let current_version: i64 = current
|
|
|
.try_get("version")
|
|
|
@@ -305,9 +325,10 @@ impl AccountStore for SqlStore {
|
|
|
}
|
|
|
|
|
|
sqlx::query(
|
|
|
- "INSERT INTO accounts (id, version, policy, flags, book, user_data, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7)"
|
|
|
+ "INSERT INTO accounts (id, subaccount, version, policy, flags, book, user_data, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)"
|
|
|
)
|
|
|
- .bind(account.id.0)
|
|
|
+ .bind(account.id.id)
|
|
|
+ .bind(account.id.sub)
|
|
|
.bind(account.version as i64)
|
|
|
.bind(serialize_policy(&account.policy)?)
|
|
|
.bind(account.flags.bits() as i32)
|
|
|
@@ -321,11 +342,14 @@ impl AccountStore for SqlStore {
|
|
|
}
|
|
|
|
|
|
async fn get_account_history(&self, id: &AccountId) -> Result<Vec<Account>, StoreError> {
|
|
|
- let rows = sqlx::query("SELECT * FROM accounts WHERE id = $1 ORDER BY version ASC")
|
|
|
- .bind(id.0)
|
|
|
- .fetch_all(&self.pool)
|
|
|
- .await
|
|
|
- .map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
+ let rows = sqlx::query(
|
|
|
+ "SELECT * FROM accounts WHERE id = $1 AND subaccount = $2 ORDER BY version ASC",
|
|
|
+ )
|
|
|
+ .bind(id.id)
|
|
|
+ .bind(id.sub)
|
|
|
+ .fetch_all(&self.pool)
|
|
|
+ .await
|
|
|
+ .map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
if rows.is_empty() {
|
|
|
return Err(StoreError::NotFound(format!("account {id:?}")));
|
|
|
}
|
|
|
@@ -333,7 +357,7 @@ impl AccountStore for SqlStore {
|
|
|
}
|
|
|
|
|
|
async fn list_accounts(&self) -> Result<Vec<Account>, StoreError> {
|
|
|
- let rows = sqlx::query("SELECT * FROM accounts ORDER BY id, version DESC")
|
|
|
+ let rows = sqlx::query("SELECT * FROM accounts ORDER BY id, subaccount, version DESC")
|
|
|
.fetch_all(&self.pool)
|
|
|
.await
|
|
|
.map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
@@ -367,44 +391,43 @@ impl PostingStore for SqlStore {
|
|
|
|
|
|
async fn get_postings_by_account(
|
|
|
&self,
|
|
|
- account: &AccountId,
|
|
|
+ id: i64,
|
|
|
+ sub: Option<i64>,
|
|
|
asset: Option<&AssetId>,
|
|
|
status: Option<PostingStatus>,
|
|
|
) -> Result<Vec<Posting>, StoreError> {
|
|
|
- let rows = match (asset, status) {
|
|
|
- (Some(a), Some(s)) => {
|
|
|
- sqlx::query(
|
|
|
- "SELECT * FROM postings WHERE owner = $1 AND asset = $2 AND status = $3",
|
|
|
- )
|
|
|
- .bind(account.0)
|
|
|
- .bind(a.0 as i32)
|
|
|
- .bind(status_to_i16(s))
|
|
|
- .fetch_all(&self.pool)
|
|
|
- .await
|
|
|
- }
|
|
|
- (Some(a), None) => {
|
|
|
- sqlx::query("SELECT * FROM postings WHERE owner = $1 AND asset = $2")
|
|
|
- .bind(account.0)
|
|
|
- .bind(a.0 as i32)
|
|
|
- .fetch_all(&self.pool)
|
|
|
- .await
|
|
|
- }
|
|
|
- (None, Some(s)) => {
|
|
|
- sqlx::query("SELECT * FROM postings WHERE owner = $1 AND status = $2")
|
|
|
- .bind(account.0)
|
|
|
- .bind(status_to_i16(s))
|
|
|
- .fetch_all(&self.pool)
|
|
|
- .await
|
|
|
- }
|
|
|
- (None, None) => {
|
|
|
- sqlx::query("SELECT * FROM postings WHERE owner = $1")
|
|
|
- .bind(account.0)
|
|
|
- .fetch_all(&self.pool)
|
|
|
- .await
|
|
|
- }
|
|
|
+ // Build the predicate dynamically: `sub == None` spans every subaccount
|
|
|
+ // of `id`, `Some(s)` restricts to one. The subaccount is compared only
|
|
|
+ // for equality, never as a magnitude.
|
|
|
+ let mut sql = String::from("SELECT * FROM postings WHERE owner = $1");
|
|
|
+ let mut placeholder = 2u32;
|
|
|
+ if sub.is_some() {
|
|
|
+ sql.push_str(&format!(" AND subaccount = ${placeholder}"));
|
|
|
+ placeholder += 1;
|
|
|
+ }
|
|
|
+ if asset.is_some() {
|
|
|
+ sql.push_str(&format!(" AND asset = ${placeholder}"));
|
|
|
+ placeholder += 1;
|
|
|
+ }
|
|
|
+ if status.is_some() {
|
|
|
+ sql.push_str(&format!(" AND status = ${placeholder}"));
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut q = sqlx::query(&sql).bind(id);
|
|
|
+ if let Some(s) = sub {
|
|
|
+ q = q.bind(s);
|
|
|
+ }
|
|
|
+ if let Some(a) = asset {
|
|
|
+ q = q.bind(a.0 as i32);
|
|
|
+ }
|
|
|
+ if let Some(s) = status {
|
|
|
+ q = q.bind(status_to_i16(s));
|
|
|
}
|
|
|
- .map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
|
|
|
+ let rows = q
|
|
|
+ .fetch_all(&self.pool)
|
|
|
+ .await
|
|
|
+ .map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
rows.iter().map(row_to_posting).collect()
|
|
|
}
|
|
|
|
|
|
@@ -412,6 +435,10 @@ impl PostingStore for SqlStore {
|
|
|
let (where_clause, count_clause) = {
|
|
|
let mut w = String::from("WHERE owner = $1");
|
|
|
let mut idx = 2u32;
|
|
|
+ if query.sub.is_some() {
|
|
|
+ w.push_str(&format!(" AND subaccount = ${idx}"));
|
|
|
+ idx += 1;
|
|
|
+ }
|
|
|
if query.asset.is_some() {
|
|
|
w.push_str(&format!(" AND asset = ${idx}"));
|
|
|
idx += 1;
|
|
|
@@ -427,7 +454,10 @@ impl PostingStore for SqlStore {
|
|
|
};
|
|
|
|
|
|
// Build count query
|
|
|
- let mut count_q = sqlx::query(&count_clause).bind(query.account.0);
|
|
|
+ let mut count_q = sqlx::query(&count_clause).bind(query.account);
|
|
|
+ if let Some(s) = query.sub {
|
|
|
+ count_q = count_q.bind(s);
|
|
|
+ }
|
|
|
if let Some(ref a) = query.asset {
|
|
|
count_q = count_q.bind(a.0 as i32);
|
|
|
}
|
|
|
@@ -443,7 +473,10 @@ impl PostingStore for SqlStore {
|
|
|
.map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
|
|
|
// Build data query
|
|
|
- let mut data_q = sqlx::query(&where_clause).bind(query.account.0);
|
|
|
+ let mut data_q = sqlx::query(&where_clause).bind(query.account);
|
|
|
+ if let Some(s) = query.sub {
|
|
|
+ data_q = data_q.bind(s);
|
|
|
+ }
|
|
|
if let Some(ref a) = query.asset {
|
|
|
data_q = data_q.bind(a.0 as i32);
|
|
|
}
|
|
|
@@ -583,11 +616,12 @@ impl PostingStore for SqlStore {
|
|
|
let mut inserted: u64 = 0;
|
|
|
for posting in postings {
|
|
|
let res = sqlx::query(
|
|
|
- "INSERT INTO postings (transfer_id, idx, owner, asset, value, status) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (transfer_id, idx) DO NOTHING"
|
|
|
+ "INSERT INTO postings (transfer_id, idx, owner, subaccount, asset, value, status) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (transfer_id, idx) DO NOTHING"
|
|
|
)
|
|
|
.bind(envelope_id_to_hex(&posting.id.transfer))
|
|
|
.bind(posting.id.index as i16)
|
|
|
- .bind(posting.owner.0)
|
|
|
+ .bind(posting.owner.id)
|
|
|
+ .bind(posting.owner.sub)
|
|
|
.bind(posting.asset.0 as i32)
|
|
|
.bind(posting.value.to_string())
|
|
|
.bind(status_to_i16(posting.status))
|
|
|
@@ -667,9 +701,10 @@ impl TransferStore for SqlStore {
|
|
|
// Index every involved account (caller supplies the set; storage does no
|
|
|
// computation). Idempotent so a replay is harmless.
|
|
|
for account in involved {
|
|
|
- sqlx::query("INSERT INTO transfer_accounts (transfer_id, account_id) VALUES ($1, $2) ON CONFLICT (transfer_id, account_id) DO NOTHING")
|
|
|
+ sqlx::query("INSERT INTO transfer_accounts (transfer_id, account_id, subaccount) VALUES ($1, $2, $3) ON CONFLICT (transfer_id, account_id, subaccount) DO NOTHING")
|
|
|
.bind(&tid_hex)
|
|
|
- .bind(account.0)
|
|
|
+ .bind(account.id)
|
|
|
+ .bind(account.sub)
|
|
|
.execute(&mut *tx)
|
|
|
.await
|
|
|
.map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
@@ -683,12 +718,24 @@ impl TransferStore for SqlStore {
|
|
|
|
|
|
async fn get_transfers_for_account(
|
|
|
&self,
|
|
|
- account: &AccountId,
|
|
|
+ id: i64,
|
|
|
+ sub: Option<i64>,
|
|
|
) -> Result<Vec<EnvelopeRecord>, StoreError> {
|
|
|
- let rows = sqlx::query(
|
|
|
- "SELECT t.id, t.transfer, t.receipt, t.created_at FROM transfers t INNER JOIN transfer_accounts ta ON t.id = ta.transfer_id WHERE ta.account_id = $1 ORDER BY t.created_at"
|
|
|
- )
|
|
|
- .bind(account.0)
|
|
|
+ // `sub == None` spans every subaccount of `id`; `Some(s)` restricts to
|
|
|
+ // one. The subaccount is matched only for equality.
|
|
|
+ let mut sql = String::from(
|
|
|
+ "SELECT t.id, t.transfer, t.receipt, t.created_at FROM transfers t INNER JOIN transfer_accounts ta ON t.id = ta.transfer_id WHERE ta.account_id = $1",
|
|
|
+ );
|
|
|
+ if sub.is_some() {
|
|
|
+ sql.push_str(" AND ta.subaccount = $2");
|
|
|
+ }
|
|
|
+ sql.push_str(" ORDER BY t.created_at");
|
|
|
+
|
|
|
+ let mut q = sqlx::query(&sql).bind(id);
|
|
|
+ if let Some(s) = sub {
|
|
|
+ q = q.bind(s);
|
|
|
+ }
|
|
|
+ let rows = q
|
|
|
.fetch_all(&self.pool)
|
|
|
.await
|
|
|
.map_err(|e| StoreError::Internal(e.to_string()))?;
|
|
|
@@ -718,8 +765,8 @@ impl TransferStore for SqlStore {
|
|
|
query: &TransferQuery,
|
|
|
) -> Result<Page<EnvelopeRecord>, StoreError> {
|
|
|
// Load base records, using the account join when available.
|
|
|
- let base_records = if let Some(ref account) = query.account {
|
|
|
- self.get_transfers_for_account(account).await?
|
|
|
+ let base_records = if let Some(account) = query.account {
|
|
|
+ self.get_transfers_for_account(account, query.sub).await?
|
|
|
} else {
|
|
|
let rows = sqlx::query(
|
|
|
"SELECT transfer, receipt, created_at FROM transfers ORDER BY created_at",
|