Skip to content
Merged
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
21 changes: 1 addition & 20 deletions migrations/1_start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ heroku pg:psql --app "$APP_NAME" DATABASE_URL <<EOF
DROP TABLE IF EXISTS bundles;
DROP TABLE IF EXISTS cids;
DROP TABLE IF EXISTS balances;
DROP TABLE IF EXISTS nonces;
DROP TABLE IF EXISTS proposers;

-- Create the bundles table
Expand Down Expand Up @@ -61,15 +60,6 @@ CREATE TABLE IF NOT EXISTS balances (
UNIQUE (vault, token)
);

-- Create the nonces table
CREATE TABLE IF NOT EXISTS nonces (
id SERIAL PRIMARY KEY,
vault TEXT NOT NULL,
nonce INTEGER NOT NULL,
timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE (vault)
);

-- Create the proposers table
CREATE TABLE IF NOT EXISTS proposers (
id SERIAL PRIMARY KEY,
Expand All @@ -78,24 +68,15 @@ CREATE TABLE IF NOT EXISTS proposers (
);
EOF

echo "Tables 'bundles', 'cids', 'balances', 'nonces', and 'proposers' created successfully."
echo "Tables 'bundles', 'cids', 'balances', and 'proposers' created successfully."

echo "Adding case-insensitive indexes and updating existing data to lowercase..."
# Create a unique index on nonces (lowercase vault)
heroku pg:psql --app "$APP_NAME" DATABASE_URL -c "
CREATE UNIQUE INDEX IF NOT EXISTS unique_lower_vault_nonces ON nonces (LOWER(vault));
"

# Create a unique index on balances (lowercase vault and token)
heroku pg:psql --app "$APP_NAME" DATABASE_URL -c "
CREATE UNIQUE INDEX IF NOT EXISTS unique_lower_vault_token_balances ON balances (LOWER(vault), LOWER(token));
"

# Update existing vault values in nonces to lowercase
heroku pg:psql --app "$APP_NAME" DATABASE_URL -c "
UPDATE nonces SET vault = LOWER(vault);
"

# Update existing vault and token values in balances to lowercase
heroku pg:psql --app "$APP_NAME" DATABASE_URL -c "
UPDATE balances SET vault = LOWER(vault), token = LOWER(token);
Expand Down
74 changes: 74 additions & 0 deletions migrations/4_merge_nonces_into_vaults.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/bin/bash

# This script merges the nonces table into the vaults table by:
# 1) Adding a NOT NULL DEFAULT 0 nonce column to vaults
# 2) Enforcing non-empty controllers array via a CHECK constraint
# 3) Backfilling nonce values from the legacy nonces table

APP_NAME=""

# Function to display usage
usage() {
echo "Usage: $0 --app-name oya-api"
exit 1
}

# Parse command-line arguments
while [[ "$#" -gt 0 ]]; do
case $1 in
--app-name) APP_NAME="$2"; shift ;;
*) echo "Unknown parameter passed: $1"; usage; exit 1 ;;
esac
shift
done

# Verify required arguments
if [ -z "$APP_NAME" ]; then
usage
fi

echo "Merging nonces into vaults (app: $APP_NAME)..."
heroku pg:psql --app "$APP_NAME" DATABASE_URL <<EOF
-- 1) Add nonce column to vaults with NOT NULL DEFAULT 0 (safe & idempotent)
ALTER TABLE IF EXISTS vaults
ADD COLUMN IF NOT EXISTS nonce INTEGER NOT NULL DEFAULT 0;

-- 2) Enforce non-empty controllers array (CHECK constraint)
DO
\$\$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'vaults_controllers_nonempty'
) THEN
ALTER TABLE vaults
ADD CONSTRAINT vaults_controllers_nonempty
CHECK (array_length(controllers, 1) IS NOT NULL AND array_length(controllers, 1) > 0);
END IF;
END
\$\$;

-- 3) Backfill nonce from legacy nonces table if present
DO
\$\$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'nonces'
) THEN
UPDATE vaults v
SET nonce = n.nonce
FROM nonces n
WHERE v.vault = n.vault;
END IF;
END
\$\$;
-- 4) Drop legacy index and table if they exist (cleanup)
DROP INDEX IF EXISTS unique_lower_vault_nonces;
DROP TABLE IF EXISTS nonces;
EOF

echo "Merge complete: vaults.nonce added, controllers non-empty enforced, nonces backfilled, legacy table dropped."


3 changes: 1 addition & 2 deletions scripts/setup-db.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,8 @@ ${chalk.yellow('Tables Created:')}
- bundles : Stores bundle data with IPFS CIDs
- cids : Tracks submitted CIDs
- balances : Manages vault token balances
- nonces : Tracks vault nonces
- proposers : Records block proposers
- vaults : Maps vault IDs to controllers and rules
- vaults : Maps vault IDs to controllers and rules; stores nonce

${chalk.red('Warning:')} Using --drop-existing will DELETE ALL EXISTING DATA!
`)
Expand Down
3 changes: 1 addition & 2 deletions scripts/setup-test-db.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ ${chalk.yellow('Tables Created:')}
- bundles : Stores bundle data with IPFS CIDs
- cids : Tracks submitted CIDs
- balances : Manages vault token balances
- nonces : Tracks vault nonces
- proposers : Records block proposers
- vaults : Maps vault IDs to controllers and rules
- vaults : Maps vault IDs to controllers and rules; stores nonce

${chalk.red('Warning:')} Using --drop-existing will DELETE ALL EXISTING DATA!
`)
Expand Down
31 changes: 9 additions & 22 deletions scripts/shared/db-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,6 @@ CREATE TABLE IF NOT EXISTS balances (
UNIQUE (vault, token)
);

-- Create the nonces table (tracks vault nonces)
CREATE TABLE IF NOT EXISTS nonces (
id SERIAL PRIMARY KEY,
vault TEXT NOT NULL,
nonce INTEGER NOT NULL,
timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE (vault)
);

-- Create the proposers table (records block proposers)
CREATE TABLE IF NOT EXISTS proposers (
id SERIAL PRIMARY KEY,
Expand All @@ -71,6 +62,7 @@ CREATE TABLE IF NOT EXISTS proposers (
CREATE TABLE IF NOT EXISTS vaults (
vault TEXT PRIMARY KEY,
controllers TEXT[] NOT NULL,
nonce INTEGER NOT NULL DEFAULT 0,
rules TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
Expand Down Expand Up @@ -102,7 +94,6 @@ CREATE TABLE IF NOT EXISTS deposit_assignment_events (
*/
export const createIndexesSql = `
-- Create case-insensitive unique indexes for vault/token lookups
CREATE UNIQUE INDEX IF NOT EXISTS unique_lower_vault_nonces ON nonces (LOWER(vault));
CREATE UNIQUE INDEX IF NOT EXISTS unique_lower_vault_token_balances ON balances (LOWER(vault), LOWER(token));

-- Create indexes for Filecoin tracking
Expand Down Expand Up @@ -131,7 +122,6 @@ DROP TABLE IF EXISTS deposits CASCADE;
DROP TABLE IF EXISTS bundles CASCADE;
DROP TABLE IF EXISTS cids CASCADE;
DROP TABLE IF EXISTS balances CASCADE;
DROP TABLE IF EXISTS nonces CASCADE;
DROP TABLE IF EXISTS proposers CASCADE;
DROP TABLE IF EXISTS vaults CASCADE;
`
Expand Down Expand Up @@ -199,11 +189,11 @@ export async function setupDatabase(options) {

// Verify tables were created
console.log(chalk.yellow('\nVerifying database schema...'))
const result = await pool.query(`
const result = await pool.query(`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('bundles', 'cids', 'balances', 'nonces', 'proposers', 'vaults', 'deposits', 'deposit_assignment_events')
AND table_name IN ('bundles', 'cids', 'balances', 'proposers', 'vaults', 'deposits', 'deposit_assignment_events')
ORDER BY table_name
`)

Expand All @@ -214,15 +204,12 @@ export async function setupDatabase(options) {
})

// Check if we need to update existing data to lowercase
const balancesCount = await pool.query('SELECT COUNT(*) FROM balances')
const noncesCount = await pool.query('SELECT COUNT(*) FROM nonces')

if (parseInt(balancesCount.rows[0].count) > 0 || parseInt(noncesCount.rows[0].count) > 0) {
console.log(chalk.yellow('\nUpdating existing data to lowercase...'))
await pool.query('UPDATE nonces SET vault = LOWER(vault)')
await pool.query('UPDATE balances SET vault = LOWER(vault), token = LOWER(token)')
console.log(chalk.green('✓ Existing data updated'))
}
const balancesCount = await pool.query('SELECT COUNT(*) FROM balances')
if (parseInt(balancesCount.rows[0].count) > 0) {
console.log(chalk.yellow('\nUpdating existing data to lowercase...'))
await pool.query('UPDATE balances SET vault = LOWER(vault), token = LOWER(token)')
console.log(chalk.green('✓ Existing data updated'))
}

console.log(chalk.green(`\n✅ ${environment} database is ready for use!\n`))

Expand Down
1 change: 0 additions & 1 deletion src/config/dbSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export const REQUIRED_TABLES = [
'bundles',
'cids',
'balances',
'nonces',
'proposers',
'vaults',
'deposits',
Expand Down
27 changes: 16 additions & 11 deletions src/controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ export const getVaultNonce = async (req: Request, res: Response) => {
const { vault } = req.params
try {
const result = await pool.query(
'SELECT nonce FROM nonces WHERE LOWER(vault) = LOWER($1)',
'SELECT nonce FROM vaults WHERE LOWER(vault) = LOWER($1)',
[vault]
)
logger.info('Getting vault nonce:', result.rows)
Expand All @@ -320,14 +320,13 @@ export const setVaultNonce = async (req: Request, res: Response) => {

try {
const result = await pool.query(
`INSERT INTO nonces (vault, nonce)
VALUES (LOWER($1), $2)
ON CONFLICT (LOWER(vault))
DO UPDATE SET nonce = EXCLUDED.nonce
RETURNING *`,
`UPDATE vaults SET nonce = $2 WHERE LOWER(vault) = LOWER($1) RETURNING vault, nonce`,
[vault, nonce]
)
res.status(201).json(result.rows[0])
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Vault not found' })
}
res.status(200).json(result.rows[0])
} catch (err) {
logger.error(err)
res.status(500).json({ error: 'Internal Server Error' })
Expand Down Expand Up @@ -397,10 +396,8 @@ export const getMetrics = async (req: Request, res: Response) => {
const latestCIDNonce =
latestCIDResult.rows.length > 0 ? latestCIDResult.rows[0].nonce : null

// Get unique vault count
const vaultCountResult = await pool.query(
'SELECT COUNT(DISTINCT vault) FROM nonces'
)
// Get vault count
const vaultCountResult = await pool.query('SELECT COUNT(*) FROM vaults')
const totalVaults = parseInt(vaultCountResult.rows[0].count)

// Get latest bundle nonce
Expand Down Expand Up @@ -756,6 +753,14 @@ export const removeControllerFromVault = async (
if (inner instanceof Error && inner.message === 'Vault not found') {
return res.status(404).json({ error: 'Vault not found' })
}
if (
inner instanceof Error &&
inner.message === 'Controllers cannot be empty'
) {
return res
.status(400)
.json({ error: 'Cannot remove the last controller from a vault' })
}
throw inner
}
} catch (error) {
Expand Down
14 changes: 8 additions & 6 deletions src/proposer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ async function getLatestNonce(): Promise<number> {
* Returns 0 if no nonce is found for the vault.
*/
async function getVaultNonce(vaultId: number | string): Promise<number> {
const result = await pool.query('SELECT nonce FROM nonces WHERE vault = $1', [
const result = await pool.query('SELECT nonce FROM vaults WHERE vault = $1', [
String(vaultId),
])
if (result.rows.length === 0) {
Expand Down Expand Up @@ -605,13 +605,15 @@ async function saveBundleData(
for (const execution of bundleData.bundle) {
const vaultNonce = execution.intention.nonce
const vault = execution.from
await pool.query(
`INSERT INTO nonces (vault, nonce)
VALUES ($1, $2)
ON CONFLICT (vault)
DO UPDATE SET nonce = EXCLUDED.nonce`,
const updateResult = await pool.query(
`UPDATE vaults SET nonce = $2 WHERE vault = $1`,
[String(vault), vaultNonce]
)
if (updateResult.rowCount === 0) {
logger.warn(
`Nonce update skipped: vault ${String(vault)} does not exist`
)
}
}
}
}
Expand Down
18 changes: 17 additions & 1 deletion src/utils/vaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export async function updateVaultControllers(
vaultId: number,
controllers: string[]
): Promise<void> {
if (!Array.isArray(controllers) || controllers.length === 0) {
throw new Error('Controllers cannot be empty')
}
const lowercasedControllers = controllers.map((c) => c.toLowerCase())
try {
const result = await pool.query(
Expand Down Expand Up @@ -184,7 +187,20 @@ export async function removeControllerFromVault(
if (result.rows.length === 0) {
throw new Error('Vault not found')
}
return (result.rows[0].controllers as string[]).map((c) => c.toLowerCase())
const updated = (result.rows[0].controllers as string[]).map((c) =>
c.toLowerCase()
)
if (updated.length === 0) {
// Revert removal to satisfy invariant and report an error
await pool.query(
`UPDATE vaults
SET controllers = ARRAY[LOWER($2)] || controllers
WHERE vault = $1`,
[String(vaultId), lower]
)
throw new Error('Controllers cannot be empty')
}
return updated
} catch (error) {
logger.error(
`Failed to remove controller ${controller} from vault ${vaultId}:`,
Expand Down
2 changes: 1 addition & 1 deletion test/helpers/testServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export async function clearTestDatabase(pool: Pool): Promise<void> {
try {
// Clear application tables
await client.query(
'TRUNCATE TABLE bundles, cids, balances, nonces, proposers, vaults CASCADE'
'TRUNCATE TABLE bundles, cids, balances, proposers, vaults CASCADE'
)
} finally {
client.release()
Expand Down
31 changes: 28 additions & 3 deletions test/integration/vaults.db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,35 @@ describe('Vault utils (DB)', () => {
)
})

test('removeControllerFromVault: update-only', async () => {
test('removeControllerFromVault: cannot remove last controller', async () => {
await createVaultRow(TEST_VAULT_1, CTRL_1, null)
const removed = await removeControllerFromVault(TEST_VAULT_1, CTRL_1)
expect(removed).toEqual([])
let threw = false
try {
await removeControllerFromVault(TEST_VAULT_1, CTRL_1)
} catch (e) {
threw = e instanceof Error && e.message === 'Controllers cannot be empty'
}
expect(threw).toBe(true)
const controllers = await getControllersForVault(TEST_VAULT_1)
expect(controllers).toEqual([CTRL_1.toLowerCase()])
})

test('vault nonce defaults to 0 and can be updated', async () => {
await createVaultRow(TEST_VAULT_1, CTRL_1, null)
const initial = await pool.query(
'SELECT nonce FROM vaults WHERE vault = $1',
[String(TEST_VAULT_1)]
)
expect(initial.rows[0].nonce).toBe(0)
await pool.query('UPDATE vaults SET nonce = $2 WHERE vault = $1', [
String(TEST_VAULT_1),
7,
])
const updated = await pool.query(
'SELECT nonce FROM vaults WHERE vault = $1',
[String(TEST_VAULT_1)]
)
expect(updated.rows[0].nonce).toBe(7)
})

test('setRulesForVault and getRulesForVault: update-only', async () => {
Expand Down