mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user