Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 29 additions & 23 deletions ddl/functions/handle_associated_wallet.sql
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
CREATE OR REPLACE FUNCTION handle_associated_wallets()
RETURNS TRIGGER AS $$
DECLARE
v_mint varchar;
v_mint varchar;
v_user_id int;
v_wallet text;
v_chain text;
BEGIN
-- For INSERT, always run
IF TG_OP = 'INSERT' THEN
FOR v_mint IN
SELECT DISTINCT mint FROM sol_token_account_balances WHERE owner = NEW.wallet
LOOP
PERFORM update_sol_user_balance_mint(NEW.user_id, v_mint);
END LOOP;
IF TG_OP = 'DELETE' THEN
v_user_id := OLD.user_id;
v_wallet := OLD.wallet;
v_chain := OLD.chain;
ELSE
v_user_id := NEW.user_id;
v_wallet := NEW.wallet;
v_chain := NEW.chain;
END IF;

-- For UPDATE, only run if is_delete changed
IF TG_OP = 'UPDATE' AND (NEW.is_delete IS DISTINCT FROM OLD.is_delete) THEN
FOR v_mint IN
SELECT DISTINCT mint FROM sol_token_account_balances WHERE owner = NEW.wallet
LOOP
PERFORM update_sol_user_balance_mint(NEW.user_id, v_mint);
END LOOP;
-- Only act on INSERT, DELETE, or an is_delete flip on UPDATE — a no-op
-- metadata UPDATE shouldn't recompute balances (preserves prior behavior).
IF TG_OP = 'UPDATE' AND (NEW.is_delete IS NOT DISTINCT FROM OLD.is_delete) THEN
RETURN NULL;
END IF;

-- For DELETE, always run
IF TG_OP = 'DELETE' THEN
FOR v_mint IN
SELECT DISTINCT mint FROM sol_token_account_balances WHERE owner = OLD.wallet
LOOP
PERFORM update_sol_user_balance_mint(OLD.user_id, v_mint);
END LOOP;
-- wAUDIO / artist-coin (sol) balances: recompute every sol mint this wallet
-- holds. For a chain=eth wallet this loop simply finds nothing.
FOR v_mint IN
SELECT DISTINCT mint FROM sol_token_account_balances WHERE owner = v_wallet
LOOP
PERFORM update_sol_user_balance_mint(v_user_id, v_mint);
END LOOP;

-- AUDIO (ETH-side) balance: linking / unlinking a chain=eth wallet changes
-- the user's aggregated eth_user_balances total.
IF v_chain = 'eth' THEN
PERFORM update_eth_user_balance(v_user_id);
END IF;

RETURN NULL;
Expand All @@ -46,4 +52,4 @@ BEGIN
EXCEPTION
WHEN others THEN NULL;
END $$;
COMMENT ON TRIGGER on_associated_wallets ON associated_wallets IS 'Updates sol_user_balances when associated_wallets are added and removed';
COMMENT ON TRIGGER on_associated_wallets ON associated_wallets IS 'Updates sol_user_balances and eth_user_balances when associated_wallets are added and removed';
52 changes: 52 additions & 0 deletions ddl/functions/handle_eth_wallet_balance_change.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-- Keeps eth_user_balances fresh when the eth-indexer writes a wallet balance.
-- A single eth_wallet_balances row can back a user's primary wallet and/or any
-- number of users' linked chain=eth associated_wallets, so we recompute every
-- user the changed wallet maps to. NEW.wallet is already lowercase hex (the
-- eth_wallet_balances PK); users.wallet is lowered to match.
CREATE OR REPLACE FUNCTION handle_eth_wallet_balance_change()
RETURNS TRIGGER AS $$
DECLARE
v_user_id int;
BEGIN
-- Skip metadata-only updates (e.g. an updated_at touch with no balance
-- delta) so we don't churn eth_user_balances for no reason.
IF TG_OP = 'UPDATE' AND NEW.balance IS NOT DISTINCT FROM OLD.balance THEN
RETURN NULL;
END IF;

FOR v_user_id IN
SELECT user_id
FROM users
WHERE LOWER(wallet) = NEW.wallet
AND is_current = TRUE

UNION

SELECT user_id
FROM associated_wallets
WHERE LOWER(wallet) = NEW.wallet
AND chain = 'eth'
AND is_current = TRUE
AND is_delete = FALSE
LOOP
PERFORM update_eth_user_balance(v_user_id);
END LOOP;

RETURN NULL;
EXCEPTION
WHEN OTHERS THEN
RAISE WARNING 'An error occurred in %: %', TG_NAME, SQLERRM;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;

DO $$
BEGIN
CREATE TRIGGER on_eth_wallet_balance_changes
AFTER INSERT OR UPDATE ON eth_wallet_balances
FOR EACH ROW EXECUTE PROCEDURE handle_eth_wallet_balance_change();
EXCEPTION
WHEN others THEN NULL;
END $$;
COMMENT ON TRIGGER on_eth_wallet_balance_changes ON eth_wallet_balances IS
'Recomputes eth_user_balances for affected users whenever an eth wallet balance changes.';
40 changes: 40 additions & 0 deletions ddl/functions/update_eth_user_balance.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
-- Recompute one user's aggregated ETH-side AUDIO balance (wei) into
-- eth_user_balances. ETH analog of update_sol_user_balance_mint: sums
-- eth_wallet_balances across the user's primary wallet + linked chain=eth
-- associated_wallets. Always writes exactly one row (no GROUP BY) so unlinking
-- the last wallet correctly drives the balance to 0 rather than leaving a stale
-- value.
CREATE OR REPLACE FUNCTION update_eth_user_balance(p_user_id int)
RETURNS VOID AS $$
BEGIN
INSERT INTO eth_user_balances (user_id, balance, updated_at, created_at)
SELECT
p_user_id,
COALESCE(SUM(ewb.balance), 0),
NOW(),
NOW()
FROM eth_wallet_balances ewb
WHERE ewb.wallet IN (
-- eth_wallet_balances PK is lowercase hex; users.wallet can be
-- mixed-case, associated_wallets are canonical lowercase (0207).
SELECT LOWER(wallet)
FROM users
WHERE user_id = p_user_id
AND is_current = TRUE
AND wallet IS NOT NULL

UNION ALL

SELECT LOWER(wallet)
FROM associated_wallets
WHERE user_id = p_user_id
AND chain = 'eth'
AND is_current = TRUE
AND is_delete = FALSE
)
ON CONFLICT (user_id)
DO UPDATE SET
balance = EXCLUDED.balance,
updated_at = NOW();
END;
$$ LANGUAGE plpgsql;
50 changes: 50 additions & 0 deletions ddl/migrations/0213_add_eth_user_balances.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
BEGIN;
SET LOCAL statement_timeout = 0;

-- Pre-aggregated AUDIO (ETH-side, in wei) balance per user — the ETH analog of
-- sol_user_balances. Replaces the per-user LATERAL aggregate that
-- v_user_balances ran over eth_wallet_balances on every read (the GetUsers hot
-- path): summing a user's wallets live, per row, made GetUsers scale with the
-- whole user base whenever the planner materialized the view instead of
-- parameterizing it. This table turns that into a single PK lookup, exactly
-- like the sol side.
--
-- Kept fresh by triggers:
-- * handle_eth_wallet_balance_change (eth_wallet_balances writes)
-- * handle_associated_wallets (chain=eth wallet link / unlink)
-- and recomputed for one user via update_eth_user_balance(user_id).
CREATE TABLE IF NOT EXISTS eth_user_balances (
user_id INT PRIMARY KEY,
balance NUMERIC NOT NULL DEFAULT 0,
updated_at TIMESTAMP NOT NULL DEFAULT now(),
created_at TIMESTAMP NOT NULL DEFAULT now()
);
COMMENT ON TABLE eth_user_balances IS 'Per-user AUDIO ERC-20 balance (wei), summed across users.wallet + chain=eth associated_wallets. Pre-aggregated mirror of eth_wallet_balances, maintained by triggers (handle_eth_wallet_balance_change / handle_associated_wallets) and recomputed by update_eth_user_balance(user_id). ETH-side analog of sol_user_balances.';

-- Backfill from eth_wallet_balances. eth_wallet_balances.wallet is lowercase
-- hex (PK); associated_wallets.wallet is already lowercased for chain=eth
-- (migration 0207), and users.wallet is lowered to match. UNION ALL (not
-- UNION) mirrors the previous view semantics exactly — if a wallet is both a
-- user's primary and one of their own linked wallets it counts the same way it
-- did before.
INSERT INTO eth_user_balances (user_id, balance, updated_at)
SELECT user_id, SUM(balance), MAX(updated_at)
FROM (
SELECT u.user_id, ewb.balance, ewb.updated_at
FROM users u
JOIN eth_wallet_balances ewb ON ewb.wallet = LOWER(u.wallet)
WHERE u.is_current = TRUE

UNION ALL

SELECT aw.user_id, ewb.balance, ewb.updated_at
FROM associated_wallets aw
JOIN eth_wallet_balances ewb ON ewb.wallet = LOWER(aw.wallet)
WHERE aw.chain = 'eth'
AND aw.is_current = TRUE
AND aw.is_delete = FALSE
) b
GROUP BY user_id
ON CONFLICT (user_id) DO NOTHING;

COMMIT;
41 changes: 12 additions & 29 deletions ddl/views/v_user_balances.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,30 @@ SELECT
u.user_id,

-- Total ETH-side AUDIO balance in wei: primary users.wallet + all linked
-- chain=eth associated_wallets, summed server-side. eth_wallet_balances
-- already includes ERC-20 balanceOf + staking + delegation (Multicall3 sum
-- in eth/indexer/multicall.go), matching the legacy Python
-- cache_user_balance.py semantic. The IN-list values are LOWER()'d
-- because eth_wallet_balances stores wallets lowercased (eth-indexer
-- normalizes via lowerHex()) while associated_wallets historically
-- accepted mixed-case input. LOWER() is on the *values* being looked
-- up, not on ewb.wallet, so eth_wallet_balances_pkey still drives the
-- lookup — no seq scan.
COALESCE(eth.total_balance, 0)::varchar AS eth_balance,
-- chain=eth associated_wallets, pre-aggregated into eth_user_balances and
-- maintained by handle_eth_wallet_balance_change / handle_associated_wallets
-- triggers (recomputed via update_eth_user_balance). A single PK lookup —
-- it replaces the per-user LATERAL aggregate over eth_wallet_balances that
-- previously made GetUsers scan the whole user base whenever the planner
-- materialized this view instead of parameterizing it.
COALESCE(eub.balance, 0)::varchar AS eth_balance,

-- wAUDIO total for the user — sol_user_balances already pre-aggregates
-- user_bank PDAs + linked Solana wallets per (user_id, mint), maintained
-- by handle_sol_claimable_accounts / update_sol_user_balance triggers.
-- One PK lookup; replaces the two LATERAL subqueries the previous shape
-- of this view used. wAUDIO base units (8 decimals).
-- One PK lookup. wAUDIO base units (8 decimals).
COALESCE(sub.balance, 0)::varchar AS sol_balance,

GREATEST(
COALESCE(eth.updated_at, '1970-01-01'::timestamp),
COALESCE(eub.updated_at, '1970-01-01'::timestamp),
COALESCE(sub.updated_at, '1970-01-01'::timestamp)
) AS updated_at
FROM users u
LEFT JOIN sol_user_balances sub
ON sub.user_id = u.user_id
AND sub.mint = '9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM'
LEFT JOIN LATERAL (
SELECT SUM(ewb.balance) AS total_balance,
MAX(ewb.updated_at) AS updated_at
FROM eth_wallet_balances ewb
WHERE ewb.wallet IN (
SELECT LOWER(u.wallet)
UNION ALL
SELECT LOWER(aw.wallet)
FROM associated_wallets aw
WHERE aw.user_id = u.user_id
AND aw.chain = 'eth'
AND aw.is_current = TRUE
AND aw.is_delete = FALSE
)
) eth ON TRUE
LEFT JOIN eth_user_balances eub
ON eub.user_id = u.user_id
WHERE u.is_current = TRUE;

COMMENT ON VIEW v_user_balances IS 'Per-user AUDIO/wAUDIO balance totals. One row per current user with eth_balance (wei) and sol_balance (wAUDIO base units, 8 decimals — multiply by 10^10 to compare to wei). eth_balance sums eth_wallet_balances across users.wallet + chain=eth associated_wallets (current, not deleted). sol_balance is sol_user_balances for the wAUDIO mint, already pre-aggregated across user_bank PDAs + linked Solana wallets by handle_sol_claimable_accounts / update_sol_user_balance triggers.';
COMMENT ON VIEW v_user_balances IS 'Per-user AUDIO/wAUDIO balance totals. One row per current user with eth_balance (wei) and sol_balance (wAUDIO base units, 8 decimals — multiply by 10^10 to compare to wei). eth_balance is eth_user_balances (pre-aggregated across users.wallet + chain=eth associated_wallets, maintained by handle_eth_wallet_balance_change / handle_associated_wallets). sol_balance is sol_user_balances for the wAUDIO mint, pre-aggregated across user_bank PDAs + linked Solana wallets by handle_sol_claimable_accounts / update_sol_user_balance triggers.';
Loading
Loading