Merge pull request 'feat: institutional membership tiers and corrected member directory' (#16) from devin/1778358341-institutional-membership-tiers into master
All checks were successful
phoenix-deploy Deployed to explorer-live
Deploy Explorer Live / deploy (push) Successful in 2m58s
All checks were successful
phoenix-deploy Deployed to explorer-live
Deploy Explorer Live / deploy (push) Successful in 2m58s
This commit was merged in pull request #16.
This commit is contained in:
89
backend/api/rest/membership.go
Normal file
89
backend/api/rest/membership.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/explorer/backend/auth"
|
||||
)
|
||||
|
||||
// handleMembershipTiers returns the canonical set of institutional tiers
|
||||
// with their labels and default explorer access tracks.
|
||||
// GET /api/v1/membership/tiers
|
||||
func (s *Server) handleMembershipTiers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
|
||||
tiers := auth.ListTiers()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"tiers": tiers,
|
||||
})
|
||||
}
|
||||
|
||||
// handleMembershipMembers returns all active institutional members.
|
||||
// GET /api/v1/membership/members
|
||||
func (s *Server) handleMembershipMembers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.requireDB(w) {
|
||||
return
|
||||
}
|
||||
|
||||
store := auth.NewMembershipStore(s.db)
|
||||
members, err := store.ListMembers(r.Context())
|
||||
if err != nil {
|
||||
writeInternalError(w, err.Error())
|
||||
return
|
||||
}
|
||||
if members == nil {
|
||||
members = []auth.InstitutionalMember{}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"members": members,
|
||||
"total": len(members),
|
||||
})
|
||||
}
|
||||
|
||||
// handleMembershipMemberDetail returns a single member by slug.
|
||||
// GET /api/v1/membership/members/{slug}
|
||||
func (s *Server) handleMembershipMemberDetail(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.requireDB(w) {
|
||||
return
|
||||
}
|
||||
|
||||
slug := strings.TrimPrefix(r.URL.Path, "/api/v1/membership/members/")
|
||||
if slug == "" {
|
||||
writeError(w, http.StatusBadRequest, "MISSING_SLUG", "Member slug is required")
|
||||
return
|
||||
}
|
||||
|
||||
store := auth.NewMembershipStore(s.db)
|
||||
member, err := store.GetMemberBySlug(r.Context(), slug)
|
||||
if err != nil {
|
||||
writeInternalError(w, err.Error())
|
||||
return
|
||||
}
|
||||
if member == nil {
|
||||
writeNotFound(w, "Member")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"member": member,
|
||||
})
|
||||
}
|
||||
@@ -67,6 +67,11 @@ func (s *Server) SetupRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/access/usage", s.handleAccessUsage)
|
||||
mux.HandleFunc("/api/v1/access/audit", s.handleAccessAudit)
|
||||
|
||||
// Institutional membership directory (public, read-only)
|
||||
mux.HandleFunc("/api/v1/membership/tiers", s.handleMembershipTiers)
|
||||
mux.HandleFunc("/api/v1/membership/members", s.handleMembershipMembers)
|
||||
mux.HandleFunc("/api/v1/membership/members/", s.handleMembershipMemberDetail)
|
||||
|
||||
// Track 1 routes (public, optional auth)
|
||||
// Note: Track 1 endpoints should be registered with OptionalAuth middleware
|
||||
// mux.HandleFunc("/api/v1/track1/blocks/latest", s.track1Server.handleLatestBlocks)
|
||||
|
||||
213
backend/auth/membership.go
Normal file
213
backend/auth/membership.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// InstitutionalTier represents a DBIS institutional membership tier.
|
||||
// These are the canonical tiers from d-bis.org/members#tiers.
|
||||
type InstitutionalTier string
|
||||
|
||||
const (
|
||||
TierSovereignCentralBank InstitutionalTier = "sovereign_central_bank"
|
||||
TierGlobalFamilyOffice InstitutionalTier = "global_family_office"
|
||||
TierSettlementMember InstitutionalTier = "settlement_member"
|
||||
TierInfrastructureOp InstitutionalTier = "infrastructure_operator"
|
||||
TierOversightJudicial InstitutionalTier = "oversight_judicial"
|
||||
TierDelegatedAuthority InstitutionalTier = "delegated_authority"
|
||||
TierStandardsBody InstitutionalTier = "standards_body"
|
||||
)
|
||||
|
||||
// InstitutionalTierLabel returns the human-readable label for a tier.
|
||||
func InstitutionalTierLabel(t InstitutionalTier) string {
|
||||
switch t {
|
||||
case TierSovereignCentralBank:
|
||||
return "Sovereign Central Bank"
|
||||
case TierGlobalFamilyOffice:
|
||||
return "Global Family Office"
|
||||
case TierSettlementMember:
|
||||
return "Settlement Member"
|
||||
case TierInfrastructureOp:
|
||||
return "Infrastructure Operator"
|
||||
case TierOversightJudicial:
|
||||
return "Oversight & Judicial"
|
||||
case TierDelegatedAuthority:
|
||||
return "Delegated Authority"
|
||||
case TierStandardsBody:
|
||||
return "Standards Body"
|
||||
default:
|
||||
return string(t)
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultTrackForTier maps an institutional membership tier to the default
|
||||
// explorer access track. Higher tracks inherit all lower-track permissions.
|
||||
//
|
||||
// Track 1 — Public explorer (read blocks, txs, basic address)
|
||||
// Track 2 — Enhanced explorer (full address, tokens, tx history, search)
|
||||
// Track 3 — Analytics (flows, bridge analytics, risk, distribution)
|
||||
// Track 4 — Operator (bridge control, validators, protocol config)
|
||||
func DefaultTrackForTier(tier InstitutionalTier) int {
|
||||
switch tier {
|
||||
case TierSovereignCentralBank:
|
||||
return 3 // analytics access; operator granted per-address
|
||||
case TierGlobalFamilyOffice:
|
||||
return 3
|
||||
case TierSettlementMember:
|
||||
return 2
|
||||
case TierInfrastructureOp:
|
||||
return 4
|
||||
case TierOversightJudicial:
|
||||
return 3
|
||||
case TierDelegatedAuthority:
|
||||
return 3
|
||||
case TierStandardsBody:
|
||||
return 2
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// InstitutionalMember represents an entity in the DBIS member directory.
|
||||
type InstitutionalMember struct {
|
||||
ID int `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Abbreviation string `json:"abbreviation"`
|
||||
Name string `json:"name"`
|
||||
Tier InstitutionalTier `json:"tier"`
|
||||
Description string `json:"description"`
|
||||
Jurisdiction string `json:"jurisdiction,omitempty"`
|
||||
LEI string `json:"lei,omitempty"`
|
||||
Latitude float64 `json:"latitude,omitempty"`
|
||||
Longitude float64 `json:"longitude,omitempty"`
|
||||
MapLabel string `json:"map_label,omitempty"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
// MembershipStore provides read/write access to the institutional members table.
|
||||
type MembershipStore struct {
|
||||
db *pgxpool.Pool
|
||||
}
|
||||
|
||||
// NewMembershipStore creates a new MembershipStore.
|
||||
func NewMembershipStore(db *pgxpool.Pool) *MembershipStore {
|
||||
return &MembershipStore{db: db}
|
||||
}
|
||||
|
||||
func isMissingMembershipTableError(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), `relation "institutional_members" does not exist`)
|
||||
}
|
||||
|
||||
// ListMembers returns all active institutional members.
|
||||
func (s *MembershipStore) ListMembers(ctx context.Context) ([]InstitutionalMember, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, slug, abbreviation, name, tier, description,
|
||||
COALESCE(jurisdiction, ''), COALESCE(lei, ''),
|
||||
COALESCE(latitude, 0), COALESCE(longitude, 0),
|
||||
COALESCE(map_label, ''), active
|
||||
FROM institutional_members
|
||||
WHERE active = TRUE
|
||||
ORDER BY tier, name
|
||||
`)
|
||||
if err != nil {
|
||||
if isMissingMembershipTableError(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("list members: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var members []InstitutionalMember
|
||||
for rows.Next() {
|
||||
var m InstitutionalMember
|
||||
if err := rows.Scan(
|
||||
&m.ID, &m.Slug, &m.Abbreviation, &m.Name, &m.Tier, &m.Description,
|
||||
&m.Jurisdiction, &m.LEI, &m.Latitude, &m.Longitude, &m.MapLabel, &m.Active,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan member: %w", err)
|
||||
}
|
||||
members = append(members, m)
|
||||
}
|
||||
return members, rows.Err()
|
||||
}
|
||||
|
||||
// GetMemberBySlug returns a single member by URL slug.
|
||||
func (s *MembershipStore) GetMemberBySlug(ctx context.Context, slug string) (*InstitutionalMember, error) {
|
||||
var m InstitutionalMember
|
||||
err := s.db.QueryRow(ctx, `
|
||||
SELECT id, slug, abbreviation, name, tier, description,
|
||||
COALESCE(jurisdiction, ''), COALESCE(lei, ''),
|
||||
COALESCE(latitude, 0), COALESCE(longitude, 0),
|
||||
COALESCE(map_label, ''), active
|
||||
FROM institutional_members
|
||||
WHERE slug = $1
|
||||
`, slug).Scan(
|
||||
&m.ID, &m.Slug, &m.Abbreviation, &m.Name, &m.Tier, &m.Description,
|
||||
&m.Jurisdiction, &m.LEI, &m.Latitude, &m.Longitude, &m.MapLabel, &m.Active,
|
||||
)
|
||||
if err != nil {
|
||||
if isMissingMembershipTableError(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("get member by slug: %w", err)
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// GetMemberByAddress looks up the institutional member linked to a wallet
|
||||
// address via the institutional_member_wallets junction table.
|
||||
func (s *MembershipStore) GetMemberByAddress(ctx context.Context, address string) (*InstitutionalMember, error) {
|
||||
var m InstitutionalMember
|
||||
err := s.db.QueryRow(ctx, `
|
||||
SELECT m.id, m.slug, m.abbreviation, m.name, m.tier, m.description,
|
||||
COALESCE(m.jurisdiction, ''), COALESCE(m.lei, ''),
|
||||
COALESCE(m.latitude, 0), COALESCE(m.longitude, 0),
|
||||
COALESCE(m.map_label, ''), m.active
|
||||
FROM institutional_members m
|
||||
JOIN institutional_member_wallets w ON w.member_id = m.id
|
||||
WHERE LOWER(w.address) = LOWER($1) AND w.active = TRUE AND m.active = TRUE
|
||||
`, address).Scan(
|
||||
&m.ID, &m.Slug, &m.Abbreviation, &m.Name, &m.Tier, &m.Description,
|
||||
&m.Jurisdiction, &m.LEI, &m.Latitude, &m.Longitude, &m.MapLabel, &m.Active,
|
||||
)
|
||||
if err != nil {
|
||||
if isMissingMembershipTableError(err) || strings.Contains(err.Error(), "no rows") {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("get member by address: %w", err)
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// ListTiers returns the canonical set of institutional membership tiers
|
||||
// with their labels and default access tracks.
|
||||
func ListTiers() []struct {
|
||||
Tier InstitutionalTier `json:"tier"`
|
||||
Label string `json:"label"`
|
||||
DefaultTrack int `json:"default_track"`
|
||||
} {
|
||||
tiers := []InstitutionalTier{
|
||||
TierSovereignCentralBank,
|
||||
TierGlobalFamilyOffice,
|
||||
TierSettlementMember,
|
||||
TierInfrastructureOp,
|
||||
TierOversightJudicial,
|
||||
TierDelegatedAuthority,
|
||||
TierStandardsBody,
|
||||
}
|
||||
result := make([]struct {
|
||||
Tier InstitutionalTier `json:"tier"`
|
||||
Label string `json:"label"`
|
||||
DefaultTrack int `json:"default_track"`
|
||||
}, len(tiers))
|
||||
for i, t := range tiers {
|
||||
result[i].Tier = t
|
||||
result[i].Label = InstitutionalTierLabel(t)
|
||||
result[i].DefaultTrack = DefaultTrackForTier(t)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -102,10 +102,12 @@ type WalletAuthRequest struct {
|
||||
|
||||
// WalletAuthResponse represents a wallet authentication response
|
||||
type WalletAuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
Track int `json:"track"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Token string `json:"token"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
Track int `json:"track"`
|
||||
Permissions []string `json:"permissions"`
|
||||
InstitutionalTier *InstitutionalTier `json:"institutional_tier,omitempty"`
|
||||
InstitutionName string `json:"institution_name,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateNonce generates a random nonce for wallet authentication
|
||||
@@ -223,17 +225,30 @@ func (w *WalletAuth) AuthenticateWallet(ctx context.Context, req *WalletAuthRequ
|
||||
// Get permissions for track
|
||||
permissions := getPermissionsForTrack(track)
|
||||
|
||||
return &WalletAuthResponse{
|
||||
resp := &WalletAuthResponse{
|
||||
Token: token,
|
||||
ExpiresAt: expiresAt,
|
||||
Track: track,
|
||||
Permissions: permissions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Attach institutional membership info if present
|
||||
store := NewMembershipStore(w.db)
|
||||
if member, err := store.GetMemberByAddress(ctx, normalizedAddr); err == nil && member != nil {
|
||||
resp.InstitutionalTier = &member.Tier
|
||||
resp.InstitutionName = member.Name
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// getUserTrack gets the track level for a user address
|
||||
// getUserTrack gets the track level for a user address.
|
||||
// Resolution order:
|
||||
// 1. Explicit per-address assignment in operator_roles (highest priority).
|
||||
// 2. Institutional membership via institutional_member_wallets → tier default.
|
||||
// 3. Fallback to Track 1 (public).
|
||||
func (w *WalletAuth) getUserTrack(ctx context.Context, address string) (int, error) {
|
||||
// Check if user exists in operator_roles (Track 4)
|
||||
// 1. Check explicit per-address assignment in operator_roles
|
||||
var track int
|
||||
var approved bool
|
||||
query := `SELECT track_level, approved FROM operator_roles WHERE address = $1`
|
||||
@@ -242,9 +257,20 @@ func (w *WalletAuth) getUserTrack(ctx context.Context, address string) (int, err
|
||||
return track, nil
|
||||
}
|
||||
|
||||
// Check if user is approved for Track 2 or 3
|
||||
// For now, default to Track 1 (public)
|
||||
// In production, you'd have an approval table
|
||||
// 2. Check institutional membership
|
||||
var tier string
|
||||
memberQuery := `
|
||||
SELECT m.tier
|
||||
FROM institutional_members m
|
||||
JOIN institutional_member_wallets w ON w.member_id = m.id
|
||||
WHERE LOWER(w.address) = LOWER($1) AND w.active = TRUE AND m.active = TRUE
|
||||
`
|
||||
err = w.db.QueryRow(ctx, memberQuery, address).Scan(&tier)
|
||||
if err == nil {
|
||||
return DefaultTrackForTier(InstitutionalTier(tier)), nil
|
||||
}
|
||||
|
||||
// 3. Default to Track 1 (public)
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- 0017_institutional_membership.down.sql
|
||||
DROP TRIGGER IF EXISTS update_institutional_members_updated_at ON institutional_members;
|
||||
DROP TABLE IF EXISTS institutional_member_wallets;
|
||||
DROP TABLE IF EXISTS institutional_members;
|
||||
DROP TYPE IF EXISTS institutional_tier;
|
||||
178
backend/database/migrations/0017_institutional_membership.up.sql
Normal file
178
backend/database/migrations/0017_institutional_membership.up.sql
Normal file
@@ -0,0 +1,178 @@
|
||||
-- 0017_institutional_membership.up.sql
|
||||
--
|
||||
-- Adds institutional membership tables and seeds the canonical DBIS member
|
||||
-- directory. The tier taxonomy comes from https://d-bis.org/members#tiers
|
||||
-- with corrections per institutional review (2026-04).
|
||||
|
||||
-- Tier enum
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE institutional_tier AS ENUM (
|
||||
'sovereign_central_bank',
|
||||
'global_family_office',
|
||||
'settlement_member',
|
||||
'infrastructure_operator',
|
||||
'oversight_judicial',
|
||||
'delegated_authority',
|
||||
'standards_body'
|
||||
);
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- Members directory
|
||||
CREATE TABLE IF NOT EXISTS institutional_members (
|
||||
id SERIAL PRIMARY KEY,
|
||||
slug VARCHAR(64) NOT NULL UNIQUE,
|
||||
abbreviation VARCHAR(16) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
tier institutional_tier NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
jurisdiction TEXT,
|
||||
lei VARCHAR(20),
|
||||
latitude DOUBLE PRECISION,
|
||||
longitude DOUBLE PRECISION,
|
||||
map_label TEXT,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_institutional_members_tier ON institutional_members(tier);
|
||||
CREATE INDEX IF NOT EXISTS idx_institutional_members_active ON institutional_members(active);
|
||||
|
||||
-- Junction: wallet addresses linked to institutional members
|
||||
CREATE TABLE IF NOT EXISTS institutional_member_wallets (
|
||||
id SERIAL PRIMARY KEY,
|
||||
member_id INTEGER NOT NULL REFERENCES institutional_members(id) ON DELETE CASCADE,
|
||||
address VARCHAR(42) NOT NULL,
|
||||
label TEXT,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE(member_id, address)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_imw_address ON institutional_member_wallets(address);
|
||||
CREATE INDEX IF NOT EXISTS idx_imw_member_id ON institutional_member_wallets(member_id);
|
||||
|
||||
-- Triggers
|
||||
CREATE TRIGGER update_institutional_members_updated_at
|
||||
BEFORE UPDATE ON institutional_members
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- ============================================================
|
||||
-- Seed data — canonical member directory
|
||||
-- ============================================================
|
||||
-- Corrections from 2026-04 review:
|
||||
-- • MLFO is a Global Family Office, NOT a central bank
|
||||
-- • BIS Innovation Hub removed from directory entirely
|
||||
-- • Added: ICCC, SAID, PANDA, Order of Hospitallers (XOM)
|
||||
-- • Placeholder rows for BRICS founding central banks
|
||||
|
||||
INSERT INTO institutional_members
|
||||
(slug, abbreviation, name, tier, description, jurisdiction, lei, latitude, longitude, map_label)
|
||||
VALUES
|
||||
-- Existing (corrected)
|
||||
('omnl', 'OMNL', 'Organisation Mondiale du Numérique',
|
||||
'sovereign_central_bank',
|
||||
'Participating central bank — OMNL Head Office ledger and journal operator (Fineract / OMNL tenant). ARIN OrgId: OMNL.',
|
||||
'International / participating monetary union', '98450070C57395F6B906',
|
||||
39.61, -104.89, 'Greenwood Village, CO'),
|
||||
|
||||
('mlfo', 'MLFO', 'Mann Li Family Office',
|
||||
'global_family_office',
|
||||
'Founding family office (L.P.B.C., Colorado Entity 20241969162). Capital structure sponsor and BIS debit performance beneficiary. Registered agent: Pandora C. Walker.',
|
||||
'US-CO (Colorado)', NULL,
|
||||
40.02, -105.27, 'Boulder, CO'),
|
||||
|
||||
('defi-oracle', 'DFO', 'DeFi Oracle',
|
||||
'infrastructure_operator',
|
||||
'Infrastructure operator for Chain 138 ecosystem. Manages smart contract deployment (131 contracts), cross-chain bridges, PMM pools, and wallet integrations (MetaMask Snap, Ledger Live).',
|
||||
'US-CO (Colorado)', NULL,
|
||||
39.61, -104.89, 'Greenwood Village, CO'),
|
||||
|
||||
-- Added entities
|
||||
('iccc', 'ICCC', 'International Criminal Court of Commerce',
|
||||
'oversight_judicial',
|
||||
'International court with oversight authority over DBIS ecosystem commercial disputes and enforcement.',
|
||||
'International', NULL,
|
||||
NULL, NULL, NULL),
|
||||
|
||||
('said', 'SAID', 'SAID',
|
||||
'standards_body',
|
||||
'Standards and identity body within the DBIS institutional framework.',
|
||||
'International', NULL,
|
||||
NULL, NULL, NULL),
|
||||
|
||||
('panda', 'PANDA', 'PANDA',
|
||||
'standards_body',
|
||||
'Standards and coordination body within the DBIS institutional framework.',
|
||||
'International', NULL,
|
||||
NULL, NULL, NULL),
|
||||
|
||||
('xom', 'XOM', 'Sovereign Military Hospitaller Order of St. John of Jerusalem of Rhodes and of Malta',
|
||||
'delegated_authority',
|
||||
'The sovereign entity (Order of Hospitallers) extending DBIS the agency authority under which it operates. Recognised UN observer state.',
|
||||
'International (Rome)', NULL,
|
||||
41.90, 12.48, 'Rome, Italy'),
|
||||
|
||||
-- BRICS founding member central banks (representative set)
|
||||
('cb-brazil', 'BCB', 'Banco Central do Brasil',
|
||||
'sovereign_central_bank',
|
||||
'Central Bank of Brazil — BRICS founding member.',
|
||||
'Brazil', NULL,
|
||||
-15.79, -47.88, 'Brasília, Brazil'),
|
||||
|
||||
('cb-russia', 'CBR', 'Central Bank of the Russian Federation',
|
||||
'sovereign_central_bank',
|
||||
'Bank of Russia — BRICS founding member.',
|
||||
'Russia', NULL,
|
||||
55.76, 37.62, 'Moscow, Russia'),
|
||||
|
||||
('cb-india', 'RBI', 'Reserve Bank of India',
|
||||
'sovereign_central_bank',
|
||||
'Reserve Bank of India — BRICS founding member.',
|
||||
'India', NULL,
|
||||
18.93, 72.83, 'Mumbai, India'),
|
||||
|
||||
('cb-china', 'PBOC', 'People''s Bank of China',
|
||||
'sovereign_central_bank',
|
||||
'People''s Bank of China — BRICS founding member.',
|
||||
'China', NULL,
|
||||
39.91, 116.39, 'Beijing, China'),
|
||||
|
||||
('cb-south-africa', 'SARB', 'South African Reserve Bank',
|
||||
'sovereign_central_bank',
|
||||
'South African Reserve Bank — BRICS founding member.',
|
||||
'South Africa', NULL,
|
||||
-25.75, 28.19, 'Pretoria, South Africa'),
|
||||
|
||||
-- BRICS expanded members (2024+)
|
||||
('cb-egypt', 'CBE', 'Central Bank of Egypt',
|
||||
'sovereign_central_bank',
|
||||
'Central Bank of Egypt — BRICS member (2024).',
|
||||
'Egypt', NULL,
|
||||
30.04, 31.24, 'Cairo, Egypt'),
|
||||
|
||||
('cb-ethiopia', 'NBE', 'National Bank of Ethiopia',
|
||||
'sovereign_central_bank',
|
||||
'National Bank of Ethiopia — BRICS member (2024).',
|
||||
'Ethiopia', NULL,
|
||||
9.02, 38.75, 'Addis Ababa, Ethiopia'),
|
||||
|
||||
('cb-iran', 'CBI', 'Central Bank of the Islamic Republic of Iran',
|
||||
'sovereign_central_bank',
|
||||
'Central Bank of Iran — BRICS member (2024).',
|
||||
'Iran', NULL,
|
||||
35.70, 51.42, 'Tehran, Iran'),
|
||||
|
||||
('cb-uae', 'CBUAE', 'Central Bank of the UAE',
|
||||
'sovereign_central_bank',
|
||||
'Central Bank of the UAE — BRICS member (2024).',
|
||||
'United Arab Emirates', NULL,
|
||||
24.45, 54.65, 'Abu Dhabi, UAE'),
|
||||
|
||||
('cb-saudi-arabia', 'SAMA', 'Saudi Central Bank',
|
||||
'sovereign_central_bank',
|
||||
'Saudi Central Bank (formerly SAMA) — BRICS member (2024).',
|
||||
'Saudi Arabia', NULL,
|
||||
24.71, 46.68, 'Riyadh, Saudi Arabia')
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
@@ -118,3 +118,19 @@ func GetAllFeatures() map[string]FeatureFlag {
|
||||
return FeatureFlags
|
||||
}
|
||||
|
||||
// TrackLabel returns a human-readable label for an access track number.
|
||||
func TrackLabel(track int) string {
|
||||
switch track {
|
||||
case 1:
|
||||
return "Explorer"
|
||||
case 2:
|
||||
return "Enhanced Explorer"
|
||||
case 3:
|
||||
return "Analytics"
|
||||
case 4:
|
||||
return "Operator"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { type ReactNode, useEffect, useId, useMemo, useRef, useState } from 'react'
|
||||
import { accessApi, type WalletAccessSession } from '@/services/api/access'
|
||||
import { accessApi, institutionalTierLabels, type WalletAccessSession } from '@/services/api/access'
|
||||
import BrandLockup from './BrandLockup'
|
||||
import HeaderCommandPalette, { type HeaderCommandItem } from './HeaderCommandPalette'
|
||||
import { useUiMode } from './UiModeContext'
|
||||
@@ -310,6 +310,9 @@ function SearchControl({
|
||||
}
|
||||
|
||||
function getAccessTier(walletSession: WalletAccessSession) {
|
||||
if (walletSession.institutionalTier) {
|
||||
return institutionalTierLabels[walletSession.institutionalTier] ?? walletSession.institutionalTier
|
||||
}
|
||||
const permissions = walletSession.permissions || []
|
||||
if (permissions.some((permission) => permission.startsWith('operator.'))) {
|
||||
return 'Operator Tier'
|
||||
@@ -326,10 +329,11 @@ function getAccessTier(walletSession: WalletAccessSession) {
|
||||
function getSessionSummary(walletSession: WalletAccessSession) {
|
||||
const permissionCount = walletSession.permissions?.length || 0
|
||||
const tierLabel = getAccessTier(walletSession)
|
||||
const institutionSuffix = walletSession.institutionName ? ` (${walletSession.institutionName})` : ''
|
||||
if (permissionCount > 0) {
|
||||
return `${tierLabel} · ${permissionCount} permission${permissionCount === 1 ? '' : 's'}`
|
||||
return `${tierLabel}${institutionSuffix} · ${permissionCount} permission${permissionCount === 1 ? '' : 's'}`
|
||||
}
|
||||
return `${tierLabel} · Explorer access active`
|
||||
return `${tierLabel}${institutionSuffix} · Explorer access active`
|
||||
}
|
||||
|
||||
function UiModeToggle({ mobile = false }: { mobile?: boolean }) {
|
||||
|
||||
@@ -21,12 +21,33 @@ export interface AccessSession {
|
||||
expires_at: string
|
||||
}
|
||||
|
||||
export type InstitutionalTier =
|
||||
| 'sovereign_central_bank'
|
||||
| 'global_family_office'
|
||||
| 'settlement_member'
|
||||
| 'infrastructure_operator'
|
||||
| 'oversight_judicial'
|
||||
| 'delegated_authority'
|
||||
| 'standards_body'
|
||||
|
||||
export const institutionalTierLabels: Record<InstitutionalTier, string> = {
|
||||
sovereign_central_bank: 'Sovereign Central Bank',
|
||||
global_family_office: 'Global Family Office',
|
||||
settlement_member: 'Settlement Member',
|
||||
infrastructure_operator: 'Infrastructure Operator',
|
||||
oversight_judicial: 'Oversight & Judicial',
|
||||
delegated_authority: 'Delegated Authority',
|
||||
standards_body: 'Standards Body',
|
||||
}
|
||||
|
||||
export interface WalletAccessSession {
|
||||
token: string
|
||||
expiresAt: string
|
||||
track: string
|
||||
permissions: string[]
|
||||
address: string
|
||||
institutionalTier?: InstitutionalTier
|
||||
institutionName?: string
|
||||
}
|
||||
|
||||
export interface AccessProduct {
|
||||
@@ -220,6 +241,8 @@ export const accessApi = {
|
||||
expires_at: string
|
||||
track: string
|
||||
permissions: string[]
|
||||
institutional_tier?: InstitutionalTier
|
||||
institution_name?: string
|
||||
}>(`${ACCESS_API_PREFIX}/auth/wallet`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ address, signature, nonce }),
|
||||
@@ -230,6 +253,8 @@ export const accessApi = {
|
||||
track: response.track,
|
||||
permissions: response.permissions || [],
|
||||
address,
|
||||
institutionalTier: response.institutional_tier,
|
||||
institutionName: response.institution_name,
|
||||
}
|
||||
setStoredWalletSession(session)
|
||||
return session
|
||||
|
||||
Reference in New Issue
Block a user