* clear

* feat: Add everything

* fix: Commenht
This commit is contained in:
Prad Nukala
2025-10-03 14:45:52 -04:00
committed by GitHub
parent 43b4a11c06
commit 13e6c3e84d
1935 changed files with 655061 additions and 40058 deletions
@@ -0,0 +1,44 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE accounts (
id TEXT PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITH TIME ZONE,
number INTEGER NOT NULL,
sequence INTEGER NOT NULL DEFAULT 0,
address TEXT NOT NULL,
public_key JSONB NOT NULL,
chain_id TEXT NOT NULL,
block_created INTEGER NOT NULL,
controller TEXT NOT NULL,
label TEXT NOT NULL,
handle TEXT NOT NULL,
is_subsidiary BOOLEAN NOT NULL DEFAULT FALSE,
is_validator BOOLEAN NOT NULL DEFAULT FALSE,
is_delegator BOOLEAN NOT NULL DEFAULT FALSE,
is_accountable BOOLEAN NOT NULL DEFAULT TRUE
);
-- Unique constraints
CREATE UNIQUE INDEX idx_accounts_address_unique ON accounts(address) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX idx_accounts_handle_controller_unique ON accounts(handle, controller) WHERE deleted_at IS NULL;
-- Regular indexes for query performance
CREATE INDEX idx_accounts_chain_id ON accounts(chain_id);
CREATE INDEX idx_accounts_block_created ON accounts(block_created);
CREATE INDEX idx_accounts_label ON accounts(label);
CREATE INDEX idx_accounts_controller ON accounts(controller);
CREATE INDEX idx_accounts_number ON accounts(number);
CREATE INDEX idx_accounts_is_subsidiary ON accounts(is_subsidiary) WHERE is_subsidiary = TRUE;
CREATE INDEX idx_accounts_is_validator ON accounts(is_validator) WHERE is_validator = TRUE;
CREATE INDEX idx_accounts_is_delegator ON accounts(is_delegator) WHERE is_delegator = TRUE;
CREATE INDEX idx_accounts_deleted_at ON accounts(deleted_at) WHERE deleted_at IS NOT NULL;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE accounts;
-- +goose StatementEnd
@@ -0,0 +1,36 @@
-- +goose Up
-- +goose StatementBegin
-- Credentials store WebAuthn credentials
CREATE TABLE credentials (
id TEXT PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITH TIME ZONE,
handle TEXT NOT NULL,
raw_id TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'public-key',
authenticator_attachment TEXT,
transports TEXT[], -- Array of transport strings
client_extension_results JSONB,
attestation_object TEXT NOT NULL, -- Base64URL encoded
client_data_json TEXT NOT NULL, -- Base64URL encoded
public_key_alg INTEGER,
public_key BYTEA
);
-- Unique constraints
CREATE UNIQUE INDEX idx_credentials_id_unique ON credentials(id) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX idx_credentials_raw_id_unique ON credentials(raw_id) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX idx_credentials_handle_unique ON credentials(handle) WHERE deleted_at IS NULL;
-- Regular indexes for query performance
CREATE INDEX idx_credentials_handle ON credentials(handle);
CREATE INDEX idx_credentials_type ON credentials(type);
CREATE INDEX idx_credentials_authenticator_attachment ON credentials(authenticator_attachment);
CREATE INDEX idx_credentials_deleted_at ON credentials(deleted_at) WHERE deleted_at IS NULL;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE credentials;
-- +goose StatementEnd
@@ -0,0 +1,30 @@
-- +goose Up
-- +goose StatementBegin
-- Profiles represent user identities
CREATE TABLE profiles (
id TEXT PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITH TIME ZONE,
address TEXT NOT NULL,
handle TEXT NOT NULL,
origin TEXT NOT NULL,
name TEXT NOT NULL
);
-- Unique constraints
CREATE UNIQUE INDEX idx_profiles_handle_unique ON profiles(handle) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX idx_profiles_address_origin_unique ON profiles(address, origin) WHERE deleted_at IS NULL;
-- Regular indexes for query performance
CREATE INDEX idx_profiles_address ON profiles(address);
CREATE INDEX idx_profiles_origin ON profiles(origin);
CREATE INDEX idx_profiles_name ON profiles(name);
CREATE INDEX idx_profiles_deleted_at ON profiles(deleted_at) WHERE deleted_at IS NOT NULL;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE profiles;
-- +goose StatementEnd
+33
View File
@@ -0,0 +1,33 @@
-- +goose Up
-- +goose StatementBegin
-- Vaults store encrypted data
CREATE TABLE vaults (
id TEXT PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITH TIME ZONE,
handle TEXT NOT NULL,
origin TEXT NOT NULL,
address TEXT NOT NULL,
cid TEXT NOT NULL,
config JSONB NOT NULL,
session_id TEXT NOT NULL,
redirect_uri TEXT NOT NULL
);
-- Unique constraints
CREATE UNIQUE INDEX idx_vaults_cid_unique ON vaults(cid) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX idx_vaults_handle_origin_address_unique ON vaults(handle, origin, address) WHERE deleted_at IS NULL;
-- Regular indexes for query performance
CREATE INDEX idx_vaults_handle ON vaults(handle);
CREATE INDEX idx_vaults_origin ON vaults(origin);
CREATE INDEX idx_vaults_address ON vaults(address);
CREATE INDEX idx_vaults_session_id ON vaults(session_id);
CREATE INDEX idx_vaults_deleted_at ON vaults(deleted_at) WHERE deleted_at IS NOT NULL;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE vaults;
-- +goose StatementEnd
@@ -0,0 +1,368 @@
-- +goose Up
-- +goose StatementBegin
-- Create the http extension required for making HTTP requests
CREATE EXTENSION IF NOT EXISTS http;
CREATE TABLE IF NOT EXISTS chains (
chain_id VARCHAR(100) PRIMARY KEY,
name VARCHAR(100),
path VARCHAR(100),
chain_name VARCHAR(100),
network_type VARCHAR(50),
pretty_name VARCHAR(200),
status VARCHAR(50),
bech32_prefix VARCHAR(20),
slip44 INTEGER,
symbol VARCHAR(20),
display VARCHAR(20),
denom VARCHAR(50),
decimals INTEGER,
image VARCHAR(500),
website VARCHAR(500),
height BIGINT,
proxy_status JSONB,
keywords TEXT[],
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS chain_apis (
id SERIAL PRIMARY KEY,
chain_id VARCHAR(100) REFERENCES chains(chain_id) ON DELETE CASCADE,
api_type VARCHAR(20), -- 'rest', 'rpc', 'grpc'
address VARCHAR(500),
provider VARCHAR(200),
archive BOOLEAN DEFAULT FALSE,
is_best_api BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS chain_explorers (
id SERIAL PRIMARY KEY,
chain_id VARCHAR(100) REFERENCES chains(chain_id) ON DELETE CASCADE,
kind VARCHAR(50),
url VARCHAR(500),
tx_page VARCHAR(500),
account_page VARCHAR(500),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 4. Create versions table
CREATE TABLE IF NOT EXISTS chain_versions (
id SERIAL PRIMARY KEY,
chain_id VARCHAR(100) REFERENCES chains(chain_id) ON DELETE CASCADE,
application_name VARCHAR(100),
application_version VARCHAR(50),
consensus_name VARCHAR(100),
consensus_version VARCHAR(50),
cosmos_sdk_version VARCHAR(50),
tendermint_version VARCHAR(50),
ibc_go_version VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 5. Create assets table
CREATE TABLE IF NOT EXISTS chain_assets (
id SERIAL PRIMARY KEY,
chain_id VARCHAR(100) REFERENCES chains(chain_id) ON DELETE CASCADE,
asset_name VARCHAR(100),
description VARCHAR(1000),
denom_units JSONB,
base VARCHAR(100),
display VARCHAR(100),
symbol VARCHAR(20),
logo_uri VARCHAR(500),
keywords TEXT[],
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 6. Create parameters table
CREATE TABLE IF NOT EXISTS chain_params (
id SERIAL PRIMARY KEY,
chain_id VARCHAR(100) REFERENCES chains(chain_id) ON DELETE CASCADE,
actual_block_time NUMERIC,
actual_blocks_per_year NUMERIC,
bonded_ratio NUMERIC,
inflation JSONB,
staking_apr NUMERIC,
unbonding_time INTEGER,
max_validators INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 7. Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_chains_status ON chains(status);
CREATE INDEX IF NOT EXISTS idx_chains_network_type ON chains(network_type);
CREATE INDEX IF NOT EXISTS idx_chain_apis_type ON chain_apis(api_type);
CREATE INDEX IF NOT EXISTS idx_chain_apis_chain_id ON chain_apis(chain_id);
CREATE INDEX IF NOT EXISTS idx_chain_explorers_chain_id ON chain_explorers(chain_id);
CREATE INDEX IF NOT EXISTS idx_chain_versions_chain_id ON chain_versions(chain_id);
CREATE INDEX IF NOT EXISTS idx_chain_assets_chain_id ON chain_assets(chain_id);
CREATE INDEX IF NOT EXISTS idx_chain_params_chain_id ON chain_params(chain_id);
-- +goose StatementEnd
-- +goose StatementBegin
-- 8. Main function to fetch and parse Cosmos chain data
CREATE OR REPLACE FUNCTION fetch_and_parse_cosmos_chains()
RETURNS TABLE(
operation VARCHAR,
chains_inserted INTEGER,
apis_inserted INTEGER,
explorers_inserted INTEGER,
versions_inserted INTEGER,
assets_inserted INTEGER,
params_inserted INTEGER,
total_processing_time INTERVAL
) AS $$
DECLARE
start_time TIMESTAMP;
cosmos_data JSONB;
chains_count INTEGER := 0;
apis_count INTEGER := 0;
explorers_count INTEGER := 0;
versions_count INTEGER := 0;
assets_count INTEGER := 0;
params_count INTEGER := 0;
BEGIN
start_time := clock_timestamp();
-- Fetch data from the Cosmos chain directory
SELECT content::JSONB INTO cosmos_data
FROM http_get('https://chains.cosmos.directory');
-- Clear existing data (optional - comment out if you want to preserve data)
DELETE FROM chain_params;
DELETE FROM chain_assets;
DELETE FROM chain_versions;
DELETE FROM chain_explorers;
DELETE FROM chain_apis;
DELETE FROM chains;
-- Insert chains data
INSERT INTO chains (
chain_id, name, path, chain_name, network_type, pretty_name,
status, bech32_prefix, slip44, symbol, display, denom,
decimals, image, website, height, proxy_status, keywords
)
SELECT
(chain->>'chain_id')::VARCHAR(100),
(chain->>'name')::VARCHAR(100),
(chain->>'path')::VARCHAR(100),
(chain->>'chain_name')::VARCHAR(100),
(chain->>'network_type')::VARCHAR(50),
(chain->>'pretty_name')::VARCHAR(200),
(chain->>'status')::VARCHAR(50),
(chain->>'bech32_prefix')::VARCHAR(20),
(chain->>'slip44')::INTEGER,
(chain->>'symbol')::VARCHAR(20),
(chain->>'display')::VARCHAR(20),
(chain->>'denom')::VARCHAR(50),
(chain->>'decimals')::INTEGER,
(chain->>'image')::VARCHAR(500),
(chain->>'website')::VARCHAR(500),
(chain->>'height')::BIGINT,
chain->'proxy_status',
CASE
WHEN chain->'keywords' IS NOT NULL
THEN ARRAY(SELECT jsonb_array_elements_text(chain->'keywords'))
ELSE NULL
END
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain;
GET DIAGNOSTICS chains_count = ROW_COUNT;
-- Insert REST API endpoints
INSERT INTO chain_apis (chain_id, api_type, address, provider, archive, is_best_api)
SELECT
(chain->>'chain_id')::VARCHAR(100),
'rest'::VARCHAR(20),
(api->>'address')::VARCHAR(500),
(api->>'provider')::VARCHAR(200),
COALESCE((api->>'archive')::BOOLEAN, false),
true
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain,
JSONB_ARRAY_ELEMENTS(chain->'best_apis'->'rest') AS api
WHERE chain->'best_apis'->'rest' IS NOT NULL;
-- Insert RPC API endpoints
INSERT INTO chain_apis (chain_id, api_type, address, provider, archive, is_best_api)
SELECT
(chain->>'chain_id')::VARCHAR(100),
'rpc'::VARCHAR(20),
(api->>'address')::VARCHAR(500),
(api->>'provider')::VARCHAR(200),
COALESCE((api->>'archive')::BOOLEAN, false),
true
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain,
JSONB_ARRAY_ELEMENTS(chain->'best_apis'->'rpc') AS api
WHERE chain->'best_apis'->'rpc' IS NOT NULL;
-- Insert GRPC API endpoints
INSERT INTO chain_apis (chain_id, api_type, address, provider, archive, is_best_api)
SELECT
(chain->>'chain_id')::VARCHAR(100),
'grpc'::VARCHAR(20),
(api->>'address')::VARCHAR(500),
(api->>'provider')::VARCHAR(200),
COALESCE((api->>'archive')::BOOLEAN, false),
true
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain,
JSONB_ARRAY_ELEMENTS(chain->'best_apis'->'grpc') AS api
WHERE chain->'best_apis'->'grpc' IS NOT NULL;
GET DIAGNOSTICS apis_count = ROW_COUNT;
-- Insert explorers
INSERT INTO chain_explorers (chain_id, kind, url, tx_page, account_page)
SELECT
(chain->>'chain_id')::VARCHAR(100),
(explorer->>'kind')::VARCHAR(50),
(explorer->>'url')::VARCHAR(500),
(explorer->>'tx_page')::VARCHAR(500),
(explorer->>'account_page')::VARCHAR(500)
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain,
JSONB_ARRAY_ELEMENTS(chain->'explorers') AS explorer
WHERE chain->'explorers' IS NOT NULL;
GET DIAGNOSTICS explorers_count = ROW_COUNT;
-- Insert version information
INSERT INTO chain_versions (
chain_id, application_name, application_version,
consensus_name, consensus_version, cosmos_sdk_version,
tendermint_version, ibc_go_version
)
SELECT
(chain->>'chain_id')::VARCHAR(100),
(chain->'versions'->>'name')::VARCHAR(100),
(chain->'versions'->>'version')::VARCHAR(50),
(chain->'versions'->'consensus'->>'name')::VARCHAR(100),
(chain->'versions'->'consensus'->>'version')::VARCHAR(50),
(chain->'versions'->>'cosmos_sdk_version')::VARCHAR(50),
(chain->'versions'->>'tendermint_version')::VARCHAR(50),
(chain->'versions'->>'ibc_go_version')::VARCHAR(50)
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain
WHERE chain->'versions' IS NOT NULL;
GET DIAGNOSTICS versions_count = ROW_COUNT;
-- Insert assets
INSERT INTO chain_assets (
chain_id, asset_name, description, denom_units,
base, display, symbol, logo_uri, keywords
)
SELECT
(chain->>'chain_id')::VARCHAR(100),
(asset->>'name')::VARCHAR(100),
(asset->>'description')::VARCHAR(1000),
asset->'denom_units',
(asset->>'base')::VARCHAR(100),
(asset->>'display')::VARCHAR(100),
(asset->>'symbol')::VARCHAR(20),
(asset->'logo_URIs'->>'png')::VARCHAR(500),
CASE
WHEN asset->'keywords' IS NOT NULL
THEN ARRAY(SELECT jsonb_array_elements_text(asset->'keywords'))
ELSE NULL
END
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain,
JSONB_ARRAY_ELEMENTS(chain->'assets') AS asset
WHERE chain->'assets' IS NOT NULL;
GET DIAGNOSTICS assets_count = ROW_COUNT;
-- Insert parameters
INSERT INTO chain_params (
chain_id, actual_block_time, actual_blocks_per_year,
bonded_ratio, inflation, staking_apr, unbonding_time, max_validators
)
SELECT
(chain->>'chain_id')::VARCHAR(100),
(chain->'params'->>'actual_block_time')::NUMERIC,
(chain->'params'->>'actual_blocks_per_year')::NUMERIC,
(chain->'params'->>'bonded_ratio')::NUMERIC,
chain->'params'->'inflation',
(chain->'params'->>'staking_apr')::NUMERIC,
(chain->'params'->>'unbonding_time')::INTEGER,
(chain->'params'->>'max_validators')::INTEGER
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain
WHERE chain->'params' IS NOT NULL;
GET DIAGNOSTICS params_count = ROW_COUNT;
-- Return summary
RETURN QUERY SELECT
'SUCCESS'::VARCHAR as operation,
chains_count,
apis_count,
explorers_count,
versions_count,
assets_count,
params_count,
clock_timestamp() - start_time as total_processing_time;
EXCEPTION
WHEN OTHERS THEN
-- Return error information
RETURN QUERY SELECT
'ERROR'::VARCHAR as operation,
0::INTEGER,
0::INTEGER,
0::INTEGER,
0::INTEGER,
0::INTEGER,
0::INTEGER,
clock_timestamp() - start_time as total_processing_time;
RAISE;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
-- +goose StatementBegin
-- 9. Convenience function to refresh data
CREATE OR REPLACE FUNCTION refresh_cosmos_data()
RETURNS VOID AS $$
BEGIN
PERFORM fetch_and_parse_cosmos_chains();
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
-- +goose StatementBegin
-- 10. Function to get chain summary statistics
CREATE OR REPLACE FUNCTION get_cosmos_chain_stats()
RETURNS TABLE(
total_chains INTEGER,
live_chains INTEGER,
testnet_chains INTEGER,
total_apis INTEGER,
total_explorers INTEGER,
chains_with_staking_info INTEGER,
avg_staking_apr NUMERIC
) AS $$
BEGIN
RETURN QUERY
SELECT
(SELECT COUNT(*)::INTEGER FROM chains) as total_chains,
(SELECT COUNT(*)::INTEGER FROM chains WHERE status = 'live') as live_chains,
(SELECT COUNT(*)::INTEGER FROM chains WHERE network_type = 'testnet') as testnet_chains,
(SELECT COUNT(*)::INTEGER FROM chain_apis) as total_apis,
(SELECT COUNT(*)::INTEGER FROM chain_explorers) as total_explorers,
(SELECT COUNT(*)::INTEGER FROM chain_params WHERE staking_apr IS NOT NULL) as chains_with_staking_info,
(SELECT AVG(staking_apr) FROM chain_params WHERE staking_apr IS NOT NULL) as avg_staking_apr;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP FUNCTION IF EXISTS get_cosmos_chain_stats();
DROP FUNCTION IF EXISTS refresh_cosmos_data();
DROP FUNCTION IF EXISTS fetch_and_parse_cosmos_chains();
DROP TABLE IF EXISTS chain_params CASCADE;
DROP TABLE IF EXISTS chain_assets CASCADE;
DROP TABLE IF EXISTS chain_versions CASCADE;
DROP TABLE IF EXISTS chain_explorers CASCADE;
DROP TABLE IF EXISTS chain_apis CASCADE;
DROP TABLE IF EXISTS chains CASCADE;
-- +goose StatementEnd
@@ -0,0 +1,16 @@
-- +goose Up
-- +goose StatementBegin
-- Step 1: Execute the main function to fetch and parse data
SELECT * FROM fetch_and_parse_cosmos_chains();
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
-- Clean up data from all tables
DELETE FROM chain_params;
DELETE FROM chain_assets;
DELETE FROM chain_versions;
DELETE FROM chain_explorers;
DELETE FROM chain_apis;
DELETE FROM chains;
-- +goose StatementEnd
@@ -0,0 +1,29 @@
-- +goose Up
-- +goose StatementBegin
CREATE OR REPLACE FUNCTION convert_webauthn_to_vc(
webauthn_credential JSONB
) RETURNS JSONB AS $$
DECLARE
vc_result JSONB;
BEGIN
-- Create the Verifiable Credential structure based on W3C standard
vc_result = jsonb_build_object(
'@context', jsonb_build_array('https://www.w3.org/2018/credentials/v1'),
'type', jsonb_build_array('VerifiableCredential', 'WebAuthnCredential'),
'issuer', jsonb_build_object(
'id', 'did:example:' || encode(digest(webauthn_credential->>'rpId', 'sha256'), 'hex')
),
'issuanceDate', to_char(now() at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"'),
'credentialSubject', jsonb_build_object(
'id', 'did:key:' || encode(digest(webauthn_credential->>'id', 'sha256'), 'hex'),
'webauthn', webauthn_credential
)
);
RETURN vc_result;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
-- +goose Down
DROP FUNCTION convert_webauthn_to_vc;
@@ -0,0 +1,486 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS crypto_global_market_data (
id SERIAL PRIMARY KEY,
market_cap_usd NUMERIC(20, 2),
volume_24h_usd NUMERIC(20, 2),
bitcoin_dominance_percentage NUMERIC(5, 2),
cryptocurrencies_number INTEGER,
market_cap_ath_value NUMERIC(20, 2),
market_cap_ath_date TIMESTAMP WITH TIME ZONE,
volume_24h_ath_value NUMERIC(20, 2),
volume_24h_ath_date TIMESTAMP WITH TIME ZONE,
market_cap_change_24h NUMERIC(10, 2),
volume_24h_change_24h NUMERIC(10, 2),
last_updated INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_crypto_global_market_data_last_updated ON crypto_global_market_data(last_updated DESC);
CREATE INDEX IF NOT EXISTS idx_crypto_global_market_data_created_at ON crypto_global_market_data(created_at DESC);
CREATE TABLE IF NOT EXISTS crypto_coins (
id VARCHAR(100) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
symbol VARCHAR(20) NOT NULL,
rank INTEGER,
is_new BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
type VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_crypto_coins_symbol ON crypto_coins(symbol);
CREATE INDEX IF NOT EXISTS idx_crypto_coins_rank ON crypto_coins(rank);
CREATE INDEX IF NOT EXISTS idx_crypto_coins_is_active ON crypto_coins(is_active);
CREATE INDEX IF NOT EXISTS idx_crypto_coins_type ON crypto_coins(type);
CREATE TABLE IF NOT EXISTS crypto_coin_details (
id VARCHAR(100) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
symbol VARCHAR(20) NOT NULL,
parent_id VARCHAR(100),
parent_name VARCHAR(255),
parent_symbol VARCHAR(20),
rank INTEGER,
is_new BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
type VARCHAR(50),
logo VARCHAR(500),
tags JSONB,
team JSONB,
description TEXT,
message TEXT,
open_source BOOLEAN,
hardware_wallet BOOLEAN,
started_at TIMESTAMP WITH TIME ZONE,
development_status VARCHAR(100),
proof_type VARCHAR(100),
org_structure VARCHAR(100),
hash_algorithm VARCHAR(100),
contract VARCHAR(255),
platform VARCHAR(255),
contracts JSONB,
links JSONB,
links_extended JSONB,
whitepaper JSONB,
first_data_at TIMESTAMP WITH TIME ZONE,
last_data_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_crypto_coin_details_symbol ON crypto_coin_details(symbol);
CREATE INDEX IF NOT EXISTS idx_crypto_coin_details_rank ON crypto_coin_details(rank);
CREATE INDEX IF NOT EXISTS idx_crypto_coin_details_type ON crypto_coin_details(type);
CREATE INDEX IF NOT EXISTS idx_crypto_coin_details_started_at ON crypto_coin_details(started_at);
-- +goose StatementEnd
-- +goose StatementBegin
CREATE OR REPLACE FUNCTION fetch_coinpaprika_global_market_data()
RETURNS TABLE(
operation VARCHAR,
records_inserted INTEGER,
last_market_cap NUMERIC,
last_volume NUMERIC,
processing_time INTERVAL
) AS $$
DECLARE
start_time TIMESTAMP;
market_data JSONB;
insert_count INTEGER := 0;
last_cap NUMERIC;
last_vol NUMERIC;
BEGIN
start_time := clock_timestamp();
SELECT content::JSONB INTO market_data
FROM http_get('https://api.coinpaprika.com/v1/global');
INSERT INTO crypto_global_market_data (
market_cap_usd,
volume_24h_usd,
bitcoin_dominance_percentage,
cryptocurrencies_number,
market_cap_ath_value,
market_cap_ath_date,
volume_24h_ath_value,
volume_24h_ath_date,
market_cap_change_24h,
volume_24h_change_24h,
last_updated
)
SELECT
(market_data->>'market_cap_usd')::NUMERIC,
(market_data->>'volume_24h_usd')::NUMERIC,
(market_data->>'bitcoin_dominance_percentage')::NUMERIC,
(market_data->>'cryptocurrencies_number')::INTEGER,
(market_data->>'market_cap_ath_value')::NUMERIC,
(market_data->>'market_cap_ath_date')::TIMESTAMP WITH TIME ZONE,
(market_data->>'volume_24h_ath_value')::NUMERIC,
(market_data->>'volume_24h_ath_date')::TIMESTAMP WITH TIME ZONE,
(market_data->>'market_cap_change_24h')::NUMERIC,
(market_data->>'volume_24h_change_24h')::NUMERIC,
(market_data->>'last_updated')::INTEGER
RETURNING market_cap_usd, volume_24h_usd INTO last_cap, last_vol;
GET DIAGNOSTICS insert_count = ROW_COUNT;
RETURN QUERY SELECT
'SUCCESS'::VARCHAR as operation,
insert_count as records_inserted,
last_cap as last_market_cap,
last_vol as last_volume,
clock_timestamp() - start_time as processing_time;
EXCEPTION
WHEN OTHERS THEN
RETURN QUERY SELECT
'ERROR: ' || SQLERRM::VARCHAR as operation,
0::INTEGER as records_inserted,
0::NUMERIC as last_market_cap,
0::NUMERIC as last_volume,
clock_timestamp() - start_time as processing_time;
RAISE;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
-- +goose StatementBegin
CREATE OR REPLACE FUNCTION refresh_market_data()
RETURNS VOID AS $$
BEGIN
PERFORM fetch_coinpaprika_global_market_data();
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
-- +goose StatementBegin
CREATE OR REPLACE FUNCTION fetch_coinpaprika_coins()
RETURNS TABLE(
operation VARCHAR,
records_upserted INTEGER,
new_coins INTEGER,
updated_coins INTEGER,
processing_time INTERVAL
) AS $$
DECLARE
start_time TIMESTAMP;
coins_data JSONB;
coin_record JSONB;
upsert_count INTEGER := 0;
new_count INTEGER := 0;
update_count INTEGER := 0;
existing_id VARCHAR;
BEGIN
start_time := clock_timestamp();
SELECT content::JSONB INTO coins_data
FROM http_get('https://api.coinpaprika.com/v1/coins');
FOR coin_record IN SELECT * FROM jsonb_array_elements(coins_data)
LOOP
SELECT id INTO existing_id FROM crypto_coins WHERE id = coin_record->>'id';
INSERT INTO crypto_coins (
id,
name,
symbol,
rank,
is_new,
is_active,
type,
updated_at
)
VALUES (
coin_record->>'id',
coin_record->>'name',
coin_record->>'symbol',
(coin_record->>'rank')::INTEGER,
(coin_record->>'is_new')::BOOLEAN,
(coin_record->>'is_active')::BOOLEAN,
coin_record->>'type',
CURRENT_TIMESTAMP
)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
symbol = EXCLUDED.symbol,
rank = EXCLUDED.rank,
is_new = EXCLUDED.is_new,
is_active = EXCLUDED.is_active,
type = EXCLUDED.type,
updated_at = CURRENT_TIMESTAMP;
upsert_count := upsert_count + 1;
IF existing_id IS NULL THEN
new_count := new_count + 1;
ELSE
update_count := update_count + 1;
END IF;
END LOOP;
RETURN QUERY SELECT
'SUCCESS'::VARCHAR as operation,
upsert_count as records_upserted,
new_count as new_coins,
update_count as updated_coins,
clock_timestamp() - start_time as processing_time;
EXCEPTION
WHEN OTHERS THEN
RETURN QUERY SELECT
'ERROR: ' || SQLERRM::VARCHAR as operation,
0::INTEGER as records_upserted,
0::INTEGER as new_coins,
0::INTEGER as updated_coins,
clock_timestamp() - start_time as processing_time;
RAISE;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
-- +goose StatementBegin
CREATE OR REPLACE FUNCTION refresh_all_coinpaprika_data()
RETURNS TABLE(
operation VARCHAR,
market_data_result VARCHAR,
coins_result VARCHAR,
total_processing_time INTERVAL
) AS $$
DECLARE
start_time TIMESTAMP;
market_result RECORD;
coins_result RECORD;
BEGIN
start_time := clock_timestamp();
-- Fetch market data
SELECT * INTO market_result FROM fetch_coinpaprika_global_market_data();
-- Fetch coins data
SELECT * INTO coins_result FROM fetch_coinpaprika_coins();
-- Return combined results
RETURN QUERY SELECT
'COMPLETE'::VARCHAR as operation,
market_result.operation as market_data_result,
coins_result.operation as coins_result,
clock_timestamp() - start_time as total_processing_time;
EXCEPTION
WHEN OTHERS THEN
RETURN QUERY SELECT
'ERROR'::VARCHAR as operation,
'ERROR: ' || SQLERRM::VARCHAR as market_data_result,
'ERROR: ' || SQLERRM::VARCHAR as coins_result,
clock_timestamp() - start_time as total_processing_time;
RAISE;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
-- +goose StatementBegin
-- Function to fetch and store detailed coin information
CREATE OR REPLACE FUNCTION fetch_coinpaprika_coin_details(coin_id VARCHAR)
RETURNS TABLE(
operation VARCHAR,
coin_name VARCHAR,
coin_symbol VARCHAR,
processing_time INTERVAL
) AS $$
DECLARE
start_time TIMESTAMP;
coin_data JSONB;
parent_data JSONB;
BEGIN
start_time := clock_timestamp();
-- Fetch data from CoinPaprika coin details endpoint
SELECT content::JSONB INTO coin_data
FROM http_get('https://api.coinpaprika.com/v1/coins/' || coin_id);
-- Extract parent data if exists
parent_data := coin_data->'parent';
-- Insert or update the coin details
INSERT INTO crypto_coin_details (
id,
name,
symbol,
parent_id,
parent_name,
parent_symbol,
rank,
is_new,
is_active,
type,
logo,
tags,
team,
description,
message,
open_source,
hardware_wallet,
started_at,
development_status,
proof_type,
org_structure,
hash_algorithm,
contract,
platform,
contracts,
links,
links_extended,
whitepaper,
first_data_at,
last_data_at,
updated_at
)
VALUES (
coin_data->>'id',
coin_data->>'name',
coin_data->>'symbol',
CASE WHEN parent_data IS NOT NULL THEN parent_data->>'id' ELSE NULL END,
CASE WHEN parent_data IS NOT NULL THEN parent_data->>'name' ELSE NULL END,
CASE WHEN parent_data IS NOT NULL THEN parent_data->>'symbol' ELSE NULL END,
(coin_data->>'rank')::INTEGER,
(coin_data->>'is_new')::BOOLEAN,
(coin_data->>'is_active')::BOOLEAN,
coin_data->>'type',
coin_data->>'logo',
coin_data->'tags',
coin_data->'team',
coin_data->>'description',
coin_data->>'message',
(coin_data->>'open_source')::BOOLEAN,
(coin_data->>'hardware_wallet')::BOOLEAN,
(coin_data->>'started_at')::TIMESTAMP WITH TIME ZONE,
coin_data->>'development_status',
coin_data->>'proof_type',
coin_data->>'org_structure',
coin_data->>'hash_algorithm',
coin_data->>'contract',
coin_data->>'platform',
coin_data->'contracts',
coin_data->'links',
coin_data->'links_extended',
coin_data->'whitepaper',
(coin_data->>'first_data_at')::TIMESTAMP WITH TIME ZONE,
(coin_data->>'last_data_at')::TIMESTAMP WITH TIME ZONE,
CURRENT_TIMESTAMP
)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
symbol = EXCLUDED.symbol,
parent_id = EXCLUDED.parent_id,
parent_name = EXCLUDED.parent_name,
parent_symbol = EXCLUDED.parent_symbol,
rank = EXCLUDED.rank,
is_new = EXCLUDED.is_new,
is_active = EXCLUDED.is_active,
type = EXCLUDED.type,
logo = EXCLUDED.logo,
tags = EXCLUDED.tags,
team = EXCLUDED.team,
description = EXCLUDED.description,
message = EXCLUDED.message,
open_source = EXCLUDED.open_source,
hardware_wallet = EXCLUDED.hardware_wallet,
started_at = EXCLUDED.started_at,
development_status = EXCLUDED.development_status,
proof_type = EXCLUDED.proof_type,
org_structure = EXCLUDED.org_structure,
hash_algorithm = EXCLUDED.hash_algorithm,
contract = EXCLUDED.contract,
platform = EXCLUDED.platform,
contracts = EXCLUDED.contracts,
links = EXCLUDED.links,
links_extended = EXCLUDED.links_extended,
whitepaper = EXCLUDED.whitepaper,
first_data_at = EXCLUDED.first_data_at,
last_data_at = EXCLUDED.last_data_at,
updated_at = CURRENT_TIMESTAMP;
-- Return summary
RETURN QUERY SELECT
'SUCCESS'::VARCHAR as operation,
coin_data->>'name' as coin_name,
coin_data->>'symbol' as coin_symbol,
clock_timestamp() - start_time as processing_time;
EXCEPTION
WHEN OTHERS THEN
-- Return error information
RETURN QUERY SELECT
'ERROR: ' || SQLERRM::VARCHAR as operation,
NULL::VARCHAR as coin_name,
NULL::VARCHAR as coin_symbol,
clock_timestamp() - start_time as processing_time;
RAISE;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
-- +goose StatementBegin
-- Function to fetch details for top N coins
CREATE OR REPLACE FUNCTION fetch_top_coins_details(limit_count INTEGER DEFAULT 10)
RETURNS TABLE(
operation VARCHAR,
coins_processed INTEGER,
successful_fetches INTEGER,
failed_fetches INTEGER,
processing_time INTERVAL
) AS $$
DECLARE
start_time TIMESTAMP;
coin_rec RECORD;
success_count INTEGER := 0;
fail_count INTEGER := 0;
total_count INTEGER := 0;
fetch_result RECORD;
BEGIN
start_time := clock_timestamp();
-- Fetch details for each top coin
FOR coin_rec IN
SELECT id FROM crypto_coins
WHERE is_active = TRUE AND rank IS NOT NULL
ORDER BY rank
LIMIT limit_count
LOOP
total_count := total_count + 1;
-- Try to fetch coin details
BEGIN
SELECT * INTO fetch_result FROM fetch_coinpaprika_coin_details(coin_rec.id);
IF fetch_result.operation = 'SUCCESS' THEN
success_count := success_count + 1;
ELSE
fail_count := fail_count + 1;
END IF;
EXCEPTION
WHEN OTHERS THEN
fail_count := fail_count + 1;
END;
END LOOP;
-- Return summary
RETURN QUERY SELECT
'COMPLETE'::VARCHAR as operation,
total_count as coins_processed,
success_count as successful_fetches,
fail_count as failed_fetches,
clock_timestamp() - start_time as processing_time;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP FUNCTION IF EXISTS fetch_top_coins_details(INTEGER);
DROP FUNCTION IF EXISTS fetch_coinpaprika_coin_details(VARCHAR);
DROP FUNCTION IF EXISTS refresh_all_coinpaprika_data();
DROP FUNCTION IF EXISTS fetch_coinpaprika_coins();
DROP FUNCTION IF EXISTS refresh_market_data();
DROP FUNCTION IF EXISTS fetch_coinpaprika_global_market_data();
DROP TABLE IF EXISTS crypto_coin_details CASCADE;
DROP TABLE IF EXISTS crypto_coins CASCADE;
DROP TABLE IF EXISTS crypto_global_market_data CASCADE;
-- +goose StatementEnd
@@ -0,0 +1,139 @@
-- +goose Up
-- +goose StatementBegin
-- Enable pgcrypto extension for gen_random_bytes if not already enabled
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Function to generate WebAuthn registration options with largeBlob and payment extensions
CREATE OR REPLACE FUNCTION generate_webauthn_register_options(
session_id UUID,
vault_id UUID
) RETURNS JSONB AS $$
DECLARE
challenge_bytes BYTEA;
challenge_base64 TEXT;
rp_id TEXT;
rp_name TEXT;
user_name TEXT;
user_display_name TEXT;
BEGIN
-- Generate a random challenge based on session_id
challenge_bytes = gen_random_bytes(32);
challenge_base64 = encode(challenge_bytes, 'base64');
-- Set default user information
user_name = 'user@' || session_id::TEXT;
user_display_name = 'User';
-- Set relying party information
rp_id = 'localhost'; -- This should be configurable
rp_name = 'Sonr Highway';
-- Build and return the registration options
RETURN jsonb_build_object(
'challenge', challenge_base64,
'rp', jsonb_build_object(
'id', rp_id,
'name', rp_name
),
'user', jsonb_build_object(
'id', encode(session_id::TEXT::BYTEA, 'base64'),
'name', user_name,
'displayName', user_display_name
),
'pubKeyCredParams', jsonb_build_array(
jsonb_build_object('type', 'public-key', 'alg', -7), -- ES256
jsonb_build_object('type', 'public-key', 'alg', -257) -- RS256
),
'timeout', 60000,
'attestation', 'direct',
'authenticatorSelection', jsonb_build_object(
'authenticatorAttachment', 'platform',
'userVerification', 'required',
'residentKey', 'required',
'requireResidentKey', true
),
'extensions', jsonb_build_object(
'largeBlob', jsonb_build_object(
'support', 'required'
),
'payment', jsonb_build_object(
'isPayment', true
)
)
);
END;
$$ LANGUAGE plpgsql;
-- Function to generate WebAuthn login options with largeBlob and payment extensions
CREATE OR REPLACE FUNCTION generate_webauthn_login_options(
session_id UUID,
vault_id UUID
) RETURNS JSONB AS $$
DECLARE
challenge_bytes BYTEA;
challenge_base64 TEXT;
allow_credentials JSONB;
rp_id TEXT;
BEGIN
-- Generate a random challenge based on session_id
challenge_bytes = gen_random_bytes(32);
challenge_base64 = encode(challenge_bytes, 'base64');
-- Set relying party information
rp_id = 'localhost'; -- This should be configurable
-- Set empty allowed credentials (no vault_id column exists)
allow_credentials = '[]'::JSONB;
-- Build and return the login options
RETURN jsonb_build_object(
'challenge', challenge_base64,
'timeout', 60000,
'rpId', rp_id,
'allowCredentials', allow_credentials,
'userVerification', 'required',
'extensions', jsonb_build_object(
'largeBlob', jsonb_build_object(
'support', 'required',
'read', true,
'write', true
),
'payment', jsonb_build_object(
'isPayment', true,
'payeeName', 'Sonr Network',
'payeeOrigin', 'https://sonr.io',
'total', jsonb_build_object(
'currency', 'USD',
'value', '0.00'
),
'instrument', jsonb_build_object(
'displayName', 'Sonr Wallet',
'icon', 'https://sonr.io/icon.png'
)
)
)
);
END;
$$ LANGUAGE plpgsql;
-- Add comments for documentation
COMMENT ON FUNCTION generate_webauthn_register_options(UUID, UUID) IS
'Generates WebAuthn registration options with largeBlob and payment extensions.
Parameters:
- session_id: UUID for generating unique challenge
- vault_id: UUID to associate with user information
Returns: JSONB object with WebAuthn registration options including required extensions';
COMMENT ON FUNCTION generate_webauthn_login_options(UUID, UUID) IS
'Generates WebAuthn login options with largeBlob and payment extensions.
Parameters:
- session_id: UUID for generating unique challenge
- vault_id: UUID to lookup allowed credentials
Returns: JSONB object with WebAuthn login options including read/write largeBlob and SPC payment extensions';
-- +goose StatementEnd
-- +goose Down
DROP FUNCTION IF EXISTS generate_webauthn_login_options(UUID, UUID);
DROP FUNCTION IF EXISTS generate_webauthn_register_options(UUID, UUID);
@@ -0,0 +1,176 @@
-- +goose Up
-- +goose StatementBegin
-- Create a junction table to link chain assets with crypto coins by symbol
CREATE TABLE IF NOT EXISTS asset_symbol_links (
id SERIAL PRIMARY KEY,
chain_asset_id INTEGER REFERENCES chain_assets(id) ON DELETE CASCADE,
crypto_coin_id VARCHAR(100) REFERENCES crypto_coins(id) ON DELETE CASCADE,
symbol VARCHAR(20) NOT NULL,
match_confidence VARCHAR(20) DEFAULT 'exact', -- 'exact', 'case_insensitive', 'manual'
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(chain_asset_id, crypto_coin_id)
);
-- Create indexes for efficient lookups
CREATE INDEX IF NOT EXISTS idx_asset_symbol_links_symbol ON asset_symbol_links(symbol);
CREATE INDEX IF NOT EXISTS idx_asset_symbol_links_chain_asset ON asset_symbol_links(chain_asset_id);
CREATE INDEX IF NOT EXISTS idx_asset_symbol_links_crypto_coin ON asset_symbol_links(crypto_coin_id);
CREATE INDEX IF NOT EXISTS idx_asset_symbol_links_confidence ON asset_symbol_links(match_confidence);
-- Create a composite index on both tables' symbol columns for faster joins
CREATE INDEX IF NOT EXISTS idx_chain_assets_symbol_lower ON chain_assets(LOWER(symbol));
CREATE INDEX IF NOT EXISTS idx_crypto_coins_symbol_lower ON crypto_coins(LOWER(symbol));
-- Function to populate asset symbol links based on matching symbols
CREATE OR REPLACE FUNCTION populate_asset_symbol_links()
RETURNS TABLE(
operation VARCHAR,
exact_matches INTEGER,
case_insensitive_matches INTEGER,
total_links_created INTEGER,
processing_time INTERVAL
) AS $$
DECLARE
start_time TIMESTAMP;
exact_count INTEGER := 0;
case_count INTEGER := 0;
BEGIN
start_time := clock_timestamp();
-- First, clear existing automatic matches (preserve manual matches)
DELETE FROM asset_symbol_links WHERE match_confidence IN ('exact', 'case_insensitive');
-- Insert exact symbol matches
INSERT INTO asset_symbol_links (chain_asset_id, crypto_coin_id, symbol, match_confidence)
SELECT DISTINCT
ca.id,
cc.id,
ca.symbol,
'exact'
FROM chain_assets ca
INNER JOIN crypto_coins cc ON ca.symbol = cc.symbol
WHERE ca.symbol IS NOT NULL
AND cc.symbol IS NOT NULL
AND cc.is_active = TRUE
ON CONFLICT (chain_asset_id, crypto_coin_id) DO NOTHING;
GET DIAGNOSTICS exact_count = ROW_COUNT;
-- Insert case-insensitive matches (excluding already matched)
INSERT INTO asset_symbol_links (chain_asset_id, crypto_coin_id, symbol, match_confidence)
SELECT DISTINCT
ca.id,
cc.id,
UPPER(ca.symbol),
'case_insensitive'
FROM chain_assets ca
INNER JOIN crypto_coins cc ON LOWER(ca.symbol) = LOWER(cc.symbol)
WHERE ca.symbol IS NOT NULL
AND cc.symbol IS NOT NULL
AND cc.is_active = TRUE
AND NOT EXISTS (
SELECT 1 FROM asset_symbol_links asl
WHERE asl.chain_asset_id = ca.id
AND asl.crypto_coin_id = cc.id
)
ON CONFLICT (chain_asset_id, crypto_coin_id) DO NOTHING;
GET DIAGNOSTICS case_count = ROW_COUNT;
RETURN QUERY SELECT
'SUCCESS'::VARCHAR as operation,
exact_count as exact_matches,
case_count as case_insensitive_matches,
exact_count + case_count as total_links_created,
clock_timestamp() - start_time as processing_time;
EXCEPTION
WHEN OTHERS THEN
RETURN QUERY SELECT
'ERROR: ' || SQLERRM::VARCHAR as operation,
0::INTEGER as exact_matches,
0::INTEGER as case_insensitive_matches,
0::INTEGER as total_links_created,
clock_timestamp() - start_time as processing_time;
RAISE;
END;
$$ LANGUAGE plpgsql;
-- View to easily query linked assets with their market data
CREATE OR REPLACE VIEW v_linked_crypto_assets AS
SELECT
ca.chain_id,
ca.asset_name,
ca.symbol AS chain_symbol,
ca.description AS chain_description,
ca.logo_uri AS chain_logo,
cc.id AS crypto_coin_id,
cc.name AS crypto_coin_name,
cc.symbol AS crypto_symbol,
cc.rank AS market_rank,
cc.type AS crypto_type,
asl.match_confidence,
asl.created_at AS link_created_at
FROM asset_symbol_links asl
INNER JOIN chain_assets ca ON asl.chain_asset_id = ca.id
INNER JOIN crypto_coins cc ON asl.crypto_coin_id = cc.id
ORDER BY cc.rank;
-- Function to get market data for a specific chain asset
CREATE OR REPLACE FUNCTION get_chain_asset_market_data(p_chain_id VARCHAR, p_symbol VARCHAR)
RETURNS TABLE(
chain_id VARCHAR,
asset_name VARCHAR,
symbol VARCHAR,
crypto_coin_id VARCHAR,
crypto_coin_name VARCHAR,
market_rank INTEGER,
coin_type VARCHAR,
match_confidence VARCHAR
) AS $$
BEGIN
RETURN QUERY
SELECT
ca.chain_id,
ca.asset_name,
ca.symbol,
cc.id AS crypto_coin_id,
cc.name AS crypto_coin_name,
cc.rank AS market_rank,
cc.type AS coin_type,
asl.match_confidence
FROM chain_assets ca
INNER JOIN asset_symbol_links asl ON ca.id = asl.chain_asset_id
INNER JOIN crypto_coins cc ON asl.crypto_coin_id = cc.id
WHERE ca.chain_id = p_chain_id
AND ca.symbol = p_symbol;
END;
$$ LANGUAGE plpgsql;
-- Trigger to update the updated_at timestamp
CREATE OR REPLACE FUNCTION update_asset_symbol_links_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_asset_symbol_links_updated_at
BEFORE UPDATE ON asset_symbol_links
FOR EACH ROW
EXECUTE FUNCTION update_asset_symbol_links_updated_at();
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TRIGGER IF EXISTS trigger_update_asset_symbol_links_updated_at ON asset_symbol_links;
DROP FUNCTION IF EXISTS update_asset_symbol_links_updated_at();
DROP FUNCTION IF EXISTS get_chain_asset_market_data(VARCHAR, VARCHAR);
DROP VIEW IF EXISTS v_linked_crypto_assets;
DROP FUNCTION IF EXISTS populate_asset_symbol_links();
DROP INDEX IF EXISTS idx_crypto_coins_symbol_lower;
DROP INDEX IF EXISTS idx_chain_assets_symbol_lower;
DROP TABLE IF EXISTS asset_symbol_links CASCADE;
-- +goose StatementEnd
@@ -0,0 +1,21 @@
-- +goose Up
-- +goose StatementBegin
-- Create a common function to update the updated_at column
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
-- Drop the function
DROP FUNCTION IF EXISTS update_updated_at_column();
-- +goose StatementEnd
@@ -0,0 +1,507 @@
-- +goose Up
-- +goose StatementBegin
-- Enable TimescaleDB extension
CREATE EXTENSION IF NOT EXISTS timescaledb;
-- Enable pg_cron extension for scheduled jobs
CREATE EXTENSION IF NOT EXISTS pg_cron;
-- Grant usage on pg_cron schema to postgres user
GRANT USAGE ON SCHEMA cron TO postgres;
-- Create crypto_coin_price table for storing historical price data
CREATE TABLE IF NOT EXISTS crypto_coin_price (
id SERIAL,
coin_id VARCHAR(255) NOT NULL REFERENCES crypto_coins(id) ON DELETE CASCADE,
time_open TIMESTAMPTZ NOT NULL,
time_close TIMESTAMPTZ NOT NULL,
open DECIMAL(20, 8),
high DECIMAL(20, 8),
low DECIMAL(20, 8),
close DECIMAL(20, 8),
volume DECIMAL(30, 2),
market_cap DECIMAL(30, 2),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(coin_id, time_open, time_close)
);
-- Convert to hypertable partitioned by time_close
SELECT create_hypertable('crypto_coin_price', 'time_close',
chunk_time_interval => INTERVAL '7 days',
if_not_exists => TRUE
);
-- Create indexes optimized for TimescaleDB
CREATE INDEX idx_crypto_coin_price_time_close_coin_id
ON crypto_coin_price (time_close DESC, coin_id)
WHERE time_close IS NOT NULL;
CREATE INDEX idx_crypto_coin_price_coin_id_time_close
ON crypto_coin_price (coin_id, time_close DESC)
WHERE coin_id IS NOT NULL;
-- Enable compression on older chunks
ALTER TABLE crypto_coin_price SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'coin_id',
timescaledb.compress_orderby = 'time_close DESC'
);
-- Create a policy to automatically compress chunks older than specified interval
-- Default is 30 days, but can be configured via app.timescaledb_compress_after_days
DO $$
DECLARE
compress_days INTEGER;
compress_interval INTERVAL;
BEGIN
-- Try to get compression days from config, default to 30 days
compress_days := COALESCE(
current_setting('app.timescaledb_compress_after_days', true)::INTEGER,
30
);
compress_interval := compress_days || ' days';
PERFORM add_compression_policy('crypto_coin_price', compress_interval);
RAISE NOTICE 'Compression policy set to compress chunks older than % days', compress_days;
END $$;
-- Add retention policy to drop chunks older than specified interval
-- Default is 1 year, but can be configured via app.timescaledb_retention_days
DO $$
DECLARE
retention_days INTEGER;
retention_interval INTERVAL;
BEGIN
-- Try to get retention days from config, default to 365 days (1 year)
retention_days := COALESCE(
current_setting('app.timescaledb_retention_days', true)::INTEGER,
365
);
retention_interval := retention_days || ' days';
PERFORM add_retention_policy('crypto_coin_price', retention_interval);
RAISE NOTICE 'Retention policy set to % days', retention_days;
END $$;
-- Create update trigger for updated_at
CREATE TRIGGER update_crypto_coin_price_updated_at
BEFORE UPDATE ON crypto_coin_price
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Create HTTP function to fetch OHLCV data from CoinPaprika
CREATE OR REPLACE FUNCTION http_get_coinpaprika_ohlcv(coin_id TEXT, start_date TEXT DEFAULT NULL, end_date TEXT DEFAULT NULL)
RETURNS TABLE (
time_open TIMESTAMPTZ,
time_close TIMESTAMPTZ,
open NUMERIC,
high NUMERIC,
low NUMERIC,
close NUMERIC,
volume NUMERIC,
market_cap NUMERIC
) AS $$
DECLARE
api_url TEXT;
response JSON;
api_key TEXT;
BEGIN
-- Get API key from environment or use default
api_key := COALESCE(current_setting('app.coinpaprika_api_key', true), 'dummy-key-for-free-tier');
-- Build URL based on parameters
IF start_date IS NULL AND end_date IS NULL THEN
-- Get today's data
api_url := 'https://api.coinpaprika.com/v1/coins/' || coin_id || '/ohlcv/today';
ELSIF start_date IS NOT NULL AND end_date IS NOT NULL THEN
-- Get historical data for date range
api_url := 'https://api.coinpaprika.com/v1/coins/' || coin_id || '/ohlcv/historical?start=' || start_date || '&end=' || end_date;
ELSE
-- Get latest data (last 24 hours)
api_url := 'https://api.coinpaprika.com/v1/coins/' || coin_id || '/ohlcv/latest';
END IF;
-- Make HTTP request
response := http_get(
api_url,
JSONB_BUILD_OBJECT(
'User-Agent', 'PostgreSQL/HTTP',
'Accept', 'application/json'
)
)::JSON;
-- Parse and return the response
RETURN QUERY
SELECT
(item->>'time_open')::TIMESTAMPTZ,
(item->>'time_close')::TIMESTAMPTZ,
(item->>'open')::NUMERIC,
(item->>'high')::NUMERIC,
(item->>'low')::NUMERIC,
(item->>'close')::NUMERIC,
(item->>'volume')::NUMERIC,
(item->>'market_cap')::NUMERIC
FROM json_array_elements(response) AS item;
END;
$$ LANGUAGE plpgsql;
-- Function to fetch and store today's price data for a coin
CREATE OR REPLACE FUNCTION refresh_coin_price_today(p_coin_id TEXT)
RETURNS VOID AS $$
BEGIN
-- Insert or update today's price data
INSERT INTO crypto_coin_price (
coin_id, time_open, time_close, open, high, low, close, volume, market_cap
)
SELECT
p_coin_id, time_open, time_close, open, high, low, close, volume, market_cap
FROM http_get_coinpaprika_ohlcv(p_coin_id)
ON CONFLICT (coin_id, time_open, time_close)
DO UPDATE SET
open = EXCLUDED.open,
high = EXCLUDED.high,
low = EXCLUDED.low,
close = EXCLUDED.close,
volume = EXCLUDED.volume,
market_cap = EXCLUDED.market_cap,
updated_at = NOW();
END;
$$ LANGUAGE plpgsql;
-- Create continuous aggregates for different time intervals
-- 1 hour aggregates
CREATE MATERIALIZED VIEW crypto_coin_price_1h
WITH (timescaledb.continuous) AS
SELECT
coin_id,
time_bucket('1 hour', time_close) AS bucket,
FIRST(open, time_open) AS open,
MAX(high) AS high,
MIN(low) AS low,
LAST(close, time_close) AS close,
SUM(volume) AS volume,
AVG(market_cap) AS market_cap,
COUNT(*) AS num_records
FROM crypto_coin_price
GROUP BY coin_id, bucket
WITH NO DATA;
-- 4 hour aggregates
CREATE MATERIALIZED VIEW crypto_coin_price_4h
WITH (timescaledb.continuous) AS
SELECT
coin_id,
time_bucket('4 hours', time_close) AS bucket,
FIRST(open, time_open) AS open,
MAX(high) AS high,
MIN(low) AS low,
LAST(close, time_close) AS close,
SUM(volume) AS volume,
AVG(market_cap) AS market_cap,
COUNT(*) AS num_records
FROM crypto_coin_price
GROUP BY coin_id, bucket
WITH NO DATA;
-- 1 day aggregates
CREATE MATERIALIZED VIEW crypto_coin_price_1d
WITH (timescaledb.continuous) AS
SELECT
coin_id,
time_bucket('1 day', time_close) AS bucket,
FIRST(open, time_open) AS open,
MAX(high) AS high,
MIN(low) AS low,
LAST(close, time_close) AS close,
SUM(volume) AS volume,
AVG(market_cap) AS market_cap,
COUNT(*) AS num_records
FROM crypto_coin_price
GROUP BY coin_id, bucket
WITH NO DATA;
-- 1 week aggregates
CREATE MATERIALIZED VIEW crypto_coin_price_1w
WITH (timescaledb.continuous) AS
SELECT
coin_id,
time_bucket('1 week', time_close) AS bucket,
FIRST(open, time_open) AS open,
MAX(high) AS high,
MIN(low) AS low,
LAST(close, time_close) AS close,
SUM(volume) AS volume,
AVG(market_cap) AS market_cap,
COUNT(*) AS num_records
FROM crypto_coin_price
GROUP BY coin_id, bucket
WITH NO DATA;
-- Create refresh policies for continuous aggregates
-- The refresh window must be larger than the bucket interval
-- For 1h buckets: window must be > 1 hour
SELECT add_continuous_aggregate_policy('crypto_coin_price_1h',
start_offset => INTERVAL '4 hours',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '30 minutes');
-- For 4h buckets: window must be > 4 hours
SELECT add_continuous_aggregate_policy('crypto_coin_price_4h',
start_offset => INTERVAL '24 hours',
end_offset => INTERVAL '4 hours',
schedule_interval => INTERVAL '2 hours');
-- For 1d buckets: window must be > 1 day
SELECT add_continuous_aggregate_policy('crypto_coin_price_1d',
start_offset => INTERVAL '7 days',
end_offset => INTERVAL '1 day',
schedule_interval => INTERVAL '6 hours');
-- For 1w buckets: window must be > 1 week
SELECT add_continuous_aggregate_policy('crypto_coin_price_1w',
start_offset => INTERVAL '4 weeks',
end_offset => INTERVAL '1 week',
schedule_interval => INTERVAL '1 day');
-- Create indexes on continuous aggregates
CREATE INDEX idx_crypto_coin_price_1h_coin_bucket ON crypto_coin_price_1h(coin_id, bucket DESC);
CREATE INDEX idx_crypto_coin_price_4h_coin_bucket ON crypto_coin_price_4h(coin_id, bucket DESC);
CREATE INDEX idx_crypto_coin_price_1d_coin_bucket ON crypto_coin_price_1d(coin_id, bucket DESC);
CREATE INDEX idx_crypto_coin_price_1w_coin_bucket ON crypto_coin_price_1w(coin_id, bucket DESC);
-- Function to get latest price for a coin
CREATE OR REPLACE FUNCTION get_latest_coin_price(p_coin_id TEXT)
RETURNS TABLE (
coin_id VARCHAR(255),
price DECIMAL(20, 8),
volume DECIMAL(30, 2),
market_cap DECIMAL(30, 2),
high_24h DECIMAL(20, 8),
low_24h DECIMAL(20, 8),
change_24h DECIMAL(20, 8),
last_updated TIMESTAMPTZ
) AS $$
BEGIN
RETURN QUERY
SELECT
cp.coin_id,
cp.close as price,
cp.volume,
cp.market_cap,
cp.high as high_24h,
cp.low as low_24h,
CASE
WHEN cp.open > 0 THEN ((cp.close - cp.open) / cp.open * 100)
ELSE 0
END as change_24h,
cp.time_close as last_updated
FROM crypto_coin_price cp
WHERE cp.coin_id = p_coin_id
ORDER BY cp.time_close DESC
LIMIT 1;
END;
$$ LANGUAGE plpgsql;
-- Function to update TimescaleDB policies dynamically
CREATE OR REPLACE FUNCTION update_timescaledb_policies(
p_retention_days INTEGER DEFAULT NULL,
p_compress_after_days INTEGER DEFAULT NULL
)
RETURNS TABLE (
policy_type TEXT,
old_value TEXT,
new_value TEXT,
status TEXT
) AS $$
DECLARE
current_retention INTERVAL;
current_compression INTERVAL;
new_retention INTERVAL;
new_compression INTERVAL;
BEGIN
-- Update retention policy if provided
IF p_retention_days IS NOT NULL THEN
-- Get current retention policy
SELECT schedule_interval INTO current_retention
FROM timescaledb_information.policies
WHERE hypertable = 'crypto_coin_price'::regclass
AND policy_name LIKE '%retention%'
LIMIT 1;
new_retention := p_retention_days || ' days';
-- Remove old policy and add new one
PERFORM remove_retention_policy('crypto_coin_price', if_exists => TRUE);
PERFORM add_retention_policy('crypto_coin_price', new_retention);
RETURN QUERY SELECT
'retention'::TEXT,
COALESCE(current_retention::TEXT, 'none'),
new_retention::TEXT,
'updated'::TEXT;
END IF;
-- Update compression policy if provided
IF p_compress_after_days IS NOT NULL THEN
-- Get current compression policy
SELECT schedule_interval INTO current_compression
FROM timescaledb_information.policies
WHERE hypertable = 'crypto_coin_price'::regclass
AND policy_name LIKE '%compress%'
LIMIT 1;
new_compression := p_compress_after_days || ' days';
-- Remove old policy and add new one
PERFORM remove_compression_policy('crypto_coin_price', if_exists => TRUE);
PERFORM add_compression_policy('crypto_coin_price', new_compression);
RETURN QUERY SELECT
'compression'::TEXT,
COALESCE(current_compression::TEXT, 'none'),
new_compression::TEXT,
'updated'::TEXT;
END IF;
-- Save configuration to database settings
IF p_retention_days IS NOT NULL THEN
PERFORM set_config('app.timescaledb_retention_days', p_retention_days::TEXT, FALSE);
END IF;
IF p_compress_after_days IS NOT NULL THEN
PERFORM set_config('app.timescaledb_compress_after_days', p_compress_after_days::TEXT, FALSE);
END IF;
END;
$$ LANGUAGE plpgsql;
-- Create function to refresh price data for all active coins
CREATE OR REPLACE FUNCTION refresh_all_coin_prices()
RETURNS VOID AS $$
DECLARE
coin RECORD;
success_count INT := 0;
error_count INT := 0;
BEGIN
-- Iterate through all coins that have been linked to Cosmos assets
FOR coin IN
SELECT DISTINCT cc.id as coin_id
FROM crypto_coins cc
JOIN asset_symbol_links asl ON asl.crypto_coin_id = cc.id
WHERE cc.is_active = TRUE
LIMIT 100 -- Process in batches to avoid overwhelming the API
LOOP
BEGIN
PERFORM refresh_coin_price_today(coin.coin_id);
success_count := success_count + 1;
EXCEPTION WHEN OTHERS THEN
error_count := error_count + 1;
RAISE NOTICE 'Error refreshing price for coin %: %', coin.coin_id, SQLERRM;
END;
END LOOP;
RAISE NOTICE 'Price refresh completed. Success: %, Errors: %', success_count, error_count;
END;
$$ LANGUAGE plpgsql;
-- Create pg_cron jobs for automated data fetching
-- Refresh prices every 5 minutes during market hours
SELECT cron.schedule(
'refresh-coin-prices-frequent',
'*/5 * * * *', -- Every 5 minutes
$$SELECT refresh_all_coin_prices();$$
);
-- Refresh continuous aggregates every hour
SELECT cron.schedule(
'refresh-continuous-aggregates-1h',
'0 * * * *', -- Every hour at minute 0
$$CALL refresh_continuous_aggregate('crypto_coin_price_1h', NULL, NULL);$$
);
-- Function to get cosmos asset price through symbol linking
CREATE OR REPLACE FUNCTION get_cosmos_asset_price(p_chain_name TEXT, p_base_denom TEXT)
RETURNS TABLE (
chain_name TEXT,
base_denom TEXT,
symbol TEXT,
coin_id VARCHAR(255),
price DECIMAL(20, 8),
volume DECIMAL(30, 2),
market_cap DECIMAL(30, 2),
high_24h DECIMAL(20, 8),
low_24h DECIMAL(20, 8),
change_24h DECIMAL(20, 8),
last_updated TIMESTAMPTZ
) AS $$
BEGIN
RETURN QUERY
SELECT
ca.chain_id as chain_name,
ca.base as base_denom,
asl.symbol,
lcp.coin_id,
lcp.price,
lcp.volume,
lcp.market_cap,
lcp.high_24h,
lcp.low_24h,
lcp.change_24h,
lcp.last_updated
FROM asset_symbol_links asl
JOIN chain_assets ca ON asl.chain_asset_id = ca.id
JOIN LATERAL get_latest_coin_price(asl.crypto_coin_id) lcp ON TRUE
WHERE ca.chain_id = p_chain_name
AND ca.base = p_base_denom;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
-- Drop cron jobs
SELECT cron.unschedule('refresh-coin-prices-frequent');
SELECT cron.unschedule('refresh-continuous-aggregates-1h');
-- Drop functions
DROP FUNCTION IF EXISTS get_cosmos_asset_price(TEXT, TEXT);
DROP FUNCTION IF EXISTS get_latest_coin_price(TEXT);
DROP FUNCTION IF EXISTS update_timescaledb_policies(INTEGER, INTEGER);
DROP FUNCTION IF EXISTS refresh_all_coin_prices();
DROP FUNCTION IF EXISTS refresh_coin_price_today(TEXT);
DROP FUNCTION IF EXISTS http_get_coinpaprika_ohlcv(TEXT, TEXT, TEXT);
-- Drop continuous aggregate policies
SELECT remove_continuous_aggregate_policy('crypto_coin_price_1h', if_exists => TRUE);
SELECT remove_continuous_aggregate_policy('crypto_coin_price_4h', if_exists => TRUE);
SELECT remove_continuous_aggregate_policy('crypto_coin_price_1d', if_exists => TRUE);
SELECT remove_continuous_aggregate_policy('crypto_coin_price_1w', if_exists => TRUE);
-- Drop continuous aggregates
DROP MATERIALIZED VIEW IF EXISTS crypto_coin_price_1h CASCADE;
DROP MATERIALIZED VIEW IF EXISTS crypto_coin_price_4h CASCADE;
DROP MATERIALIZED VIEW IF EXISTS crypto_coin_price_1d CASCADE;
DROP MATERIALIZED VIEW IF EXISTS crypto_coin_price_1w CASCADE;
-- Drop TimescaleDB policies
SELECT remove_retention_policy('crypto_coin_price', if_exists => TRUE);
SELECT remove_compression_policy('crypto_coin_price', if_exists => TRUE);
-- Drop triggers
DROP TRIGGER IF EXISTS update_crypto_coin_price_updated_at ON crypto_coin_price;
-- Drop hypertable (this will drop all associated objects)
DROP TABLE IF EXISTS crypto_coin_price CASCADE;
-- Drop extensions
DROP EXTENSION IF EXISTS pg_cron CASCADE;
DROP EXTENSION IF EXISTS timescaledb CASCADE;
-- +goose StatementEnd
@@ -0,0 +1,420 @@
-- +goose Up
-- +goose StatementBegin
-- Add status and quality fields to chain_assets table
ALTER TABLE chain_assets
ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT true,
ADD COLUMN IF NOT EXISTS is_verified BOOLEAN DEFAULT false,
ADD COLUMN IF NOT EXISTS verification_score INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();
-- Create trigger to update updated_at
CREATE TRIGGER update_chain_assets_updated_at
BEFORE UPDATE ON chain_assets
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Create indexes for efficient filtering
CREATE INDEX IF NOT EXISTS idx_chain_assets_active ON chain_assets(is_active) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_chain_assets_verified ON chain_assets(is_verified) WHERE is_verified = true;
CREATE INDEX IF NOT EXISTS idx_chain_assets_score ON chain_assets(verification_score) WHERE verification_score > 0;
-- Function to calculate verification score for assets
CREATE OR REPLACE FUNCTION calculate_asset_verification_score(p_asset_id INTEGER)
RETURNS INTEGER AS $$
DECLARE
v_score INTEGER := 0;
v_chain_status VARCHAR(50);
v_has_symbol BOOLEAN;
v_has_logo BOOLEAN;
v_has_description BOOLEAN;
v_linked_coin_rank INTEGER;
v_linked_coin_active BOOLEAN;
v_match_confidence VARCHAR(20);
BEGIN
-- Get asset details
SELECT
c.status,
ca.symbol IS NOT NULL AND ca.symbol != '',
ca.logo_uri IS NOT NULL AND ca.logo_uri != '',
ca.description IS NOT NULL AND ca.description != ''
INTO v_chain_status, v_has_symbol, v_has_logo, v_has_description
FROM chain_assets ca
JOIN chains c ON ca.chain_id = c.chain_id
WHERE ca.id = p_asset_id;
-- Base scoring
IF v_chain_status = 'live' THEN
v_score := v_score + 30;
ELSIF v_chain_status = 'upcoming' THEN
v_score := v_score + 10;
END IF;
IF v_has_symbol THEN
v_score := v_score + 20;
END IF;
IF v_has_logo THEN
v_score := v_score + 10;
END IF;
IF v_has_description THEN
v_score := v_score + 10;
END IF;
-- Check for linked market data
SELECT
cc.rank,
cc.is_active,
asl.match_confidence
INTO v_linked_coin_rank, v_linked_coin_active, v_match_confidence
FROM asset_symbol_links asl
JOIN crypto_coins cc ON asl.crypto_coin_id = cc.id
WHERE asl.chain_asset_id = p_asset_id
LIMIT 1;
IF v_linked_coin_rank IS NOT NULL THEN
-- Has market data
v_score := v_score + 30;
-- Bonus for high rank coins
IF v_linked_coin_rank <= 100 THEN
v_score := v_score + 20;
ELSIF v_linked_coin_rank <= 500 THEN
v_score := v_score + 10;
END IF;
-- Match confidence bonus
IF v_match_confidence = 'exact' THEN
v_score := v_score + 10;
ELSIF v_match_confidence = 'manual' THEN
v_score := v_score + 15; -- Manual verification is most trusted
END IF;
IF v_linked_coin_active THEN
v_score := v_score + 10;
END IF;
END IF;
RETURN v_score;
END;
$$ LANGUAGE plpgsql;
-- Function to update asset quality flags
CREATE OR REPLACE FUNCTION update_asset_quality_flags()
RETURNS TABLE(
total_assets INTEGER,
deactivated_count INTEGER,
verified_count INTEGER,
high_quality_count INTEGER
) AS $$
DECLARE
v_total INTEGER;
v_deactivated INTEGER := 0;
v_verified INTEGER := 0;
v_high_quality INTEGER := 0;
BEGIN
-- Count total assets
SELECT COUNT(*) INTO v_total FROM chain_assets;
-- Update verification scores
UPDATE chain_assets
SET verification_score = calculate_asset_verification_score(id);
-- Mark as inactive: assets with very low scores or from non-live chains
UPDATE chain_assets ca
SET is_active = false
WHERE ca.verification_score < 20
OR EXISTS (
SELECT 1 FROM chains c
WHERE c.chain_id = ca.chain_id
AND c.status NOT IN ('live', 'upcoming')
);
GET DIAGNOSTICS v_deactivated = ROW_COUNT;
-- Mark as verified: high quality assets with market data
UPDATE chain_assets ca
SET is_verified = true
WHERE ca.verification_score >= 70
AND EXISTS (
SELECT 1 FROM asset_symbol_links asl
JOIN crypto_coins cc ON asl.crypto_coin_id = cc.id
WHERE asl.chain_asset_id = ca.id
AND cc.is_active = true
AND cc.rank IS NOT NULL
);
GET DIAGNOSTICS v_verified = ROW_COUNT;
-- Count high quality assets
SELECT COUNT(*) INTO v_high_quality
FROM chain_assets
WHERE verification_score >= 50;
RETURN QUERY SELECT v_total, v_deactivated, v_verified, v_high_quality;
END;
$$ LANGUAGE plpgsql;
-- Update the populate_asset_symbol_links function to respect filters
CREATE OR REPLACE FUNCTION populate_asset_symbol_links()
RETURNS TABLE(
operation VARCHAR,
exact_matches INTEGER,
case_insensitive_matches INTEGER,
total_links_created INTEGER,
processing_time INTERVAL
) AS $$
DECLARE
start_time TIMESTAMP;
exact_count INTEGER := 0;
case_count INTEGER := 0;
BEGIN
start_time := clock_timestamp();
-- First, update asset quality flags
PERFORM update_asset_quality_flags();
-- Clear existing automatic matches (preserve manual matches)
DELETE FROM asset_symbol_links WHERE match_confidence IN ('exact', 'case_insensitive');
-- Insert exact symbol matches (only for active assets)
INSERT INTO asset_symbol_links (chain_asset_id, crypto_coin_id, symbol, match_confidence)
SELECT DISTINCT
ca.id,
cc.id,
ca.symbol,
'exact'
FROM chain_assets ca
INNER JOIN crypto_coins cc ON ca.symbol = cc.symbol
INNER JOIN chains c ON ca.chain_id = c.chain_id
WHERE ca.symbol IS NOT NULL
AND cc.symbol IS NOT NULL
AND cc.is_active = TRUE
AND ca.is_active = TRUE
AND c.status IN ('live', 'upcoming')
AND ca.verification_score >= 30
ON CONFLICT (chain_asset_id, crypto_coin_id) DO NOTHING;
GET DIAGNOSTICS exact_count = ROW_COUNT;
-- Insert case-insensitive matches (excluding already matched, only for quality assets)
INSERT INTO asset_symbol_links (chain_asset_id, crypto_coin_id, symbol, match_confidence)
SELECT DISTINCT
ca.id,
cc.id,
UPPER(ca.symbol),
'case_insensitive'
FROM chain_assets ca
INNER JOIN crypto_coins cc ON LOWER(ca.symbol) = LOWER(cc.symbol)
INNER JOIN chains c ON ca.chain_id = c.chain_id
WHERE ca.symbol IS NOT NULL
AND cc.symbol IS NOT NULL
AND cc.is_active = TRUE
AND ca.is_active = TRUE
AND c.status IN ('live', 'upcoming')
AND ca.verification_score >= 30
AND NOT EXISTS (
SELECT 1 FROM asset_symbol_links asl
WHERE asl.chain_asset_id = ca.id
AND asl.crypto_coin_id = cc.id
)
ON CONFLICT (chain_asset_id, crypto_coin_id) DO NOTHING;
GET DIAGNOSTICS case_count = ROW_COUNT;
RETURN QUERY SELECT
'SUCCESS'::VARCHAR as operation,
exact_count as exact_matches,
case_count as case_insensitive_matches,
exact_count + case_count as total_links_created,
clock_timestamp() - start_time as processing_time;
EXCEPTION
WHEN OTHERS THEN
RETURN QUERY SELECT
'ERROR: ' || SQLERRM::VARCHAR as operation,
0::INTEGER as exact_matches,
0::INTEGER as case_insensitive_matches,
0::INTEGER as total_links_created,
clock_timestamp() - start_time as processing_time;
RAISE;
END;
$$ LANGUAGE plpgsql;
-- Create view for quality assets only
CREATE OR REPLACE VIEW v_quality_linked_assets AS
SELECT
ca.chain_id,
ca.asset_name,
ca.symbol,
ca.description,
ca.logo_uri,
ca.is_verified,
ca.verification_score,
cc.id as crypto_coin_id,
cc.name as crypto_coin_name,
cc.rank as market_rank,
cc.type as coin_type,
asl.match_confidence,
cp.close as current_price,
cp.volume as volume_24h,
cp.market_cap,
CASE
WHEN cp.open > 0 THEN ((cp.close - cp.open) / cp.open * 100)
ELSE 0
END as change_24h
FROM chain_assets ca
INNER JOIN asset_symbol_links asl ON ca.id = asl.chain_asset_id
INNER JOIN crypto_coins cc ON asl.crypto_coin_id = cc.id
LEFT JOIN LATERAL (
SELECT * FROM crypto_coin_price
WHERE coin_id = cc.id
ORDER BY time_close DESC
LIMIT 1
) cp ON true
WHERE ca.is_active = true
AND ca.verification_score >= 50
AND cc.is_active = true
ORDER BY cc.rank NULLS LAST;
-- Function to manually mark an asset as junk/inactive
CREATE OR REPLACE FUNCTION mark_asset_as_junk(p_chain_id TEXT, p_base_denom TEXT)
RETURNS VOID AS $$
BEGIN
UPDATE chain_assets
SET
is_active = false,
is_verified = false,
verification_score = 0,
updated_at = NOW()
WHERE chain_id = p_chain_id AND base = p_base_denom;
-- Remove any symbol links for this asset
DELETE FROM asset_symbol_links
WHERE chain_asset_id IN (
SELECT id FROM chain_assets
WHERE chain_id = p_chain_id AND base = p_base_denom
);
END;
$$ LANGUAGE plpgsql;
-- Function to manually verify an asset
CREATE OR REPLACE FUNCTION verify_asset(p_chain_id TEXT, p_base_denom TEXT)
RETURNS VOID AS $$
BEGIN
UPDATE chain_assets
SET
is_active = true,
is_verified = true,
verification_score = GREATEST(verification_score, 80),
updated_at = NOW()
WHERE chain_id = p_chain_id AND base = p_base_denom;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
-- Drop views
DROP VIEW IF EXISTS v_quality_linked_assets;
-- Drop functions
DROP FUNCTION IF EXISTS verify_asset(TEXT, TEXT);
DROP FUNCTION IF EXISTS mark_asset_as_junk(TEXT, TEXT);
DROP FUNCTION IF EXISTS update_asset_quality_flags();
DROP FUNCTION IF EXISTS calculate_asset_verification_score(INTEGER);
-- Restore original populate_asset_symbol_links function
CREATE OR REPLACE FUNCTION populate_asset_symbol_links()
RETURNS TABLE(
operation VARCHAR,
exact_matches INTEGER,
case_insensitive_matches INTEGER,
total_links_created INTEGER,
processing_time INTERVAL
) AS $$
DECLARE
start_time TIMESTAMP;
exact_count INTEGER := 0;
case_count INTEGER := 0;
BEGIN
start_time := clock_timestamp();
-- First, clear existing automatic matches (preserve manual matches)
DELETE FROM asset_symbol_links WHERE match_confidence IN ('exact', 'case_insensitive');
-- Insert exact symbol matches
INSERT INTO asset_symbol_links (chain_asset_id, crypto_coin_id, symbol, match_confidence)
SELECT DISTINCT
ca.id,
cc.id,
ca.symbol,
'exact'
FROM chain_assets ca
INNER JOIN crypto_coins cc ON ca.symbol = cc.symbol
WHERE ca.symbol IS NOT NULL
AND cc.symbol IS NOT NULL
AND cc.is_active = TRUE
ON CONFLICT (chain_asset_id, crypto_coin_id) DO NOTHING;
GET DIAGNOSTICS exact_count = ROW_COUNT;
-- Insert case-insensitive matches (excluding already matched)
INSERT INTO asset_symbol_links (chain_asset_id, crypto_coin_id, symbol, match_confidence)
SELECT DISTINCT
ca.id,
cc.id,
UPPER(ca.symbol),
'case_insensitive'
FROM chain_assets ca
INNER JOIN crypto_coins cc ON LOWER(ca.symbol) = LOWER(cc.symbol)
WHERE ca.symbol IS NOT NULL
AND cc.symbol IS NOT NULL
AND cc.is_active = TRUE
AND NOT EXISTS (
SELECT 1 FROM asset_symbol_links asl
WHERE asl.chain_asset_id = ca.id
AND asl.crypto_coin_id = cc.id
)
ON CONFLICT (chain_asset_id, crypto_coin_id) DO NOTHING;
GET DIAGNOSTICS case_count = ROW_COUNT;
RETURN QUERY SELECT
'SUCCESS'::VARCHAR as operation,
exact_count as exact_matches,
case_count as case_insensitive_matches,
exact_count + case_count as total_links_created,
clock_timestamp() - start_time as processing_time;
EXCEPTION
WHEN OTHERS THEN
RETURN QUERY SELECT
'ERROR: ' || SQLERRM::VARCHAR as operation,
0::INTEGER as exact_matches,
0::INTEGER as case_insensitive_matches,
0::INTEGER as total_links_created,
clock_timestamp() - start_time as processing_time;
RAISE;
END;
$$ LANGUAGE plpgsql;
-- Drop triggers
DROP TRIGGER IF EXISTS update_chain_assets_updated_at ON chain_assets;
-- Drop indexes
DROP INDEX IF EXISTS idx_chain_assets_score;
DROP INDEX IF EXISTS idx_chain_assets_verified;
DROP INDEX IF EXISTS idx_chain_assets_active;
-- Drop columns
ALTER TABLE chain_assets
DROP COLUMN IF EXISTS updated_at,
DROP COLUMN IF EXISTS verification_score,
DROP COLUMN IF EXISTS is_verified,
DROP COLUMN IF EXISTS is_active;
-- +goose StatementEnd
@@ -0,0 +1,26 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
account_id TEXT NOT NULL,
user_agent TEXT,
ip_address TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
session_data JSONB,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
-- Indexes for performance
CREATE INDEX idx_sessions_account_id ON sessions(account_id);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
CREATE INDEX idx_sessions_is_active ON sessions(is_active) WHERE is_active = TRUE;
CREATE INDEX idx_sessions_created_at ON sessions(created_at);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE sessions;
-- +goose StatementEnd