From 204e672a26c066bbbd3b638e28301bef61a7efdd Mon Sep 17 00:00:00 2001 From: youngkidwarrior Date: Sat, 4 Oct 2025 04:21:49 -0700 Subject: [PATCH] profile_lookup_result: re-add is_verified and wire through views Why: profile_result requires is_verified. Add it back to the composite type and include it in profile_lookup and the referrer view. is_verified is computed from verified_at so we avoid redundant logic. Test plan: - Reset DB & generate types; confirm CompositeTypes.profile_lookup_result has is_verified and verified_at - Query: select is_verified, verified_at from profile_lookup('sendid',''); --- docs/real-time-verifications.md | 48 + .../account/components/AccountHeader.tsx | 13 +- .../deposit/DepositCoinbase/screen.tsx | 2 +- .../features/home/InvestmentsBalanceCard.tsx | 2 +- .../app/features/home/StablesBalanceCard.tsx | 2 +- .../shared/components/ChartLineSection.tsx | 6 +- packages/app/features/profile/screen.tsx | 7 +- packages/app/utils/useUser.ts | 2 +- packages/snaplet/.snaplet/dataModel.json | 443 +++++++++ packages/snaplet/.snaplet/snaplet-client.d.ts | 61 ++ packages/snaplet/.snaplet/snaplet.d.ts | 68 ++ supabase/config.toml | 1 + supabase/database-generated.types.ts | 7 + supabase/database.types.ts | 2 + ...erified_at_computed_and_profile_lookup.sql | 316 +++++++ .../schemas/distribution_verifications.sql | 883 ++++++++++++++++++ supabase/schemas/distributions.sql | 798 +--------------- supabase/schemas/referrals.sql | 79 +- supabase/schemas/tags.sql | 66 +- supabase/schemas/types.sql | 3 +- supabase/tests/verification_status_test.sql | 143 +++ 21 files changed, 2051 insertions(+), 901 deletions(-) create mode 100644 docs/real-time-verifications.md create mode 100644 supabase/migrations/20251004125914_verified_at_computed_and_profile_lookup.sql create mode 100644 supabase/schemas/distribution_verifications.sql create mode 100644 supabase/tests/verification_status_test.sql diff --git a/docs/real-time-verifications.md b/docs/real-time-verifications.md new file mode 100644 index 000000000..1b7f0a144 --- /dev/null +++ b/docs/real-time-verifications.md @@ -0,0 +1,48 @@ +# Real-time verifications + +This document explains how Send computes and exposes a profile’s verification state in real time. + +## Summary +- Verified status requires all three conditions simultaneously (for the active distribution): + 1) tag_registration verification present with weight > 0 + 2) send_token_hodler verification weight >= distributions.hodler_min_balance + 3) Earn balance condition: any address owned by the user has send_earn balance >= distributions.earn_min_balance +- We expose two computed values: + - verified_at(profiles) -> timestamptz: the timestamp when the user became currently verified. NULL if not verified now. + - is_verified(profiles) -> boolean: derived as (verified_at IS NOT NULL) + +## Active distribution +Both functions first locate the active distribution using the current UTC time: +- qualification_start <= now() <= qualification_end (UTC) +- If multiple windows match, we choose the latest by qualification_start. + +## verified_at semantics +- If the user is currently verified, verified_at returns the latest time at which all required conditions were satisfied: + - tag_at: earliest tag_registration DV for the active distribution + - hodler_at: earliest send_token_hodler DV meeting the min balance for the active distribution + - earn_at: + - If earn_min_balance = 0, no earn requirement is enforced (earn_at = qualification_start of the active distribution) + - Otherwise, the earliest send_earn_balances_timeline row where assets >= earn_min_balance for any of the user’s addresses +- verified_at = GREATEST(tag_at, hodler_at, earn_at) +- If any required timestamp is NULL, verified_at returns NULL and the user is not currently verified. + +## is_verified +is_verified(profiles) simply returns (verified_at(profiles) IS NOT NULL). This avoids duplicated logic and guarantees consistency. + +## Query points and usage +- profile_lookup returns is_verified (and verified_at is available separately via public.verified_at(p)). +- UI can: + - Show a badge when is_verified = true + - Show the time since verification began using verified_at + +## Edge cases +- Missing any one of the conditions -> verified_at = NULL, is_verified = false +- Earn threshold set to 0 -> earn condition is bypassed, so tag + hodler alone determine verification +- When a condition is later lost (e.g., token holdings drop below hodler_min_balance), verified_at returns NULL again, and is_verified becomes false + +## Performance notes +- The functions operate within a single SELECT using a single active distribution row and narrowly scoped subqueries. +- The logic avoids repeated full scans by joining against specific user- and distribution-scoped data. + +## Testing +- See supabase/tests/verification_status_test.sql for coverage of the happy path and key edge cases (no DVs, single DV, both DVs, and losing a condition). \ No newline at end of file diff --git a/packages/app/features/account/components/AccountHeader.tsx b/packages/app/features/account/components/AccountHeader.tsx index 16dc3e77e..5d9e02f67 100644 --- a/packages/app/features/account/components/AccountHeader.tsx +++ b/packages/app/features/account/components/AccountHeader.tsx @@ -30,7 +30,7 @@ const ShareProfileDialog = lazy(() => ) export const AccountHeader = memo(function AccountHeader(props) { - const { profile, distributionShares } = useUser() + const { profile } = useUser() const [shareDialogOpen, setShareDialogOpen] = useState(false) const hoverStyles = useHoverStyles() @@ -54,11 +54,6 @@ export const AccountHeader = memo(function AccountHeader(props) { [] ) - const isVerified = useMemo( - () => Boolean(distributionShares[0] && distributionShares[0].amount > 0n), - [distributionShares] - ) - const handleSharePress = useCallback(async () => { if (!referralHref) return @@ -77,7 +72,7 @@ export const AccountHeader = memo(function AccountHeader(props) { // Verification status icon component const VerificationIcon = () => { - if (isVerified) { + if (profile?.is_verified) { return ( (function AccountHeader(props) { {name || '---'} - {!isVerified ? ( + {!profile?.is_verified ? ( (function AccountHeader(props) { { - if (isVerified) { + if (profile?.is_verified) { return } e.preventDefault() diff --git a/packages/app/features/deposit/DepositCoinbase/screen.tsx b/packages/app/features/deposit/DepositCoinbase/screen.tsx index 11c3d446c..b27af8bf2 100644 --- a/packages/app/features/deposit/DepositCoinbase/screen.tsx +++ b/packages/app/features/deposit/DepositCoinbase/screen.tsx @@ -85,7 +85,7 @@ export function DepositCoinbaseScreen({ defaultPaymentMethod }: DepositCoinbaseS color={'$lightGrayTextField'} $theme-light={{ color: '$darkGrayTextField' }} > - Turn off "Block Popups" in iOS Safari Settings then try again. + Turn off "Block Popups" in iOS Safari Settings then try again. )} diff --git a/packages/app/features/home/InvestmentsBalanceCard.tsx b/packages/app/features/home/InvestmentsBalanceCard.tsx index 84de79592..cb806cfd0 100644 --- a/packages/app/features/home/InvestmentsBalanceCard.tsx +++ b/packages/app/features/home/InvestmentsBalanceCard.tsx @@ -16,7 +16,7 @@ import { View, } from '@my/ui' import formatAmount, { localizeAmount } from 'app/utils/formatAmount' -import { ChevronLeft, ChevronRight } from '@tamagui/lucide-icons' +import { ChevronRight } from '@tamagui/lucide-icons' import { useIsPriceHidden } from './utils/useIsPriceHidden' import { useSendAccountBalances } from 'app/utils/useSendAccountBalances' import { diff --git a/packages/app/features/home/StablesBalanceCard.tsx b/packages/app/features/home/StablesBalanceCard.tsx index 562114f5e..9803d53c8 100644 --- a/packages/app/features/home/StablesBalanceCard.tsx +++ b/packages/app/features/home/StablesBalanceCard.tsx @@ -13,7 +13,7 @@ import { } from '@my/ui' import formatAmount from 'app/utils/formatAmount' -import { ChevronLeft, ChevronRight } from '@tamagui/lucide-icons' +import { ChevronRight } from '@tamagui/lucide-icons' import { useIsPriceHidden } from 'app/features/home/utils/useIsPriceHidden' import { useSendAccountBalances } from 'app/utils/useSendAccountBalances' import { stableCoins, usdcCoin } from 'app/data/coins' diff --git a/packages/app/features/home/charts/shared/components/ChartLineSection.tsx b/packages/app/features/home/charts/shared/components/ChartLineSection.tsx index a5a8ca9a4..83709194b 100644 --- a/packages/app/features/home/charts/shared/components/ChartLineSection.tsx +++ b/packages/app/features/home/charts/shared/components/ChartLineSection.tsx @@ -30,9 +30,9 @@ export function ChartLineSection({ const native = useScrollNativeGesture() const gesturePad = 24 - const { onScrub, ...restPathProps } = (pathProps ?? {}) as { - onScrub?: (payload: { active: boolean; ox?: number; oy?: number }) => void - } & Record + const restPathProps = Object.fromEntries( + Object.entries((pathProps ?? {}) as Record).filter(([k]) => k !== 'onScrub') + ) as Record const mergedPanProps = { shouldCancelWhenOutside: false, diff --git a/packages/app/features/profile/screen.tsx b/packages/app/features/profile/screen.tsx index df1570404..aa0ee654a 100644 --- a/packages/app/features/profile/screen.tsx +++ b/packages/app/features/profile/screen.tsx @@ -22,12 +22,7 @@ import { // Internal import { useProfileLookup } from 'app/utils/useProfileLookup' import { useProfileScreenParams } from 'app/routers/params' -import { - IconAccount, - IconLinkInBio, - IconCheckCircle, - IconBadgeCheckSolid, -} from 'app/components/icons' +import { IconAccount, IconLinkInBio, IconBadgeCheckSolid } from 'app/components/icons' import { ShareOtherProfileDialog } from './components/ShareOtherProfileDialog' import type { Functions } from '@my/supabase/database.types' import { useTokenPrices } from 'app/utils/useTokenPrices' diff --git a/packages/app/utils/useUser.ts b/packages/app/utils/useUser.ts index 75833333c..2ac9844fe 100644 --- a/packages/app/utils/useUser.ts +++ b/packages/app/utils/useUser.ts @@ -46,7 +46,7 @@ export const useUser = () => { const { data, error } = await supabase .from('profiles') .select( - '*, tags(*), main_tag(*), links_in_bio(*), distribution_shares(*), canton_party_verifications(*)' + '*, is_verified, tags(*), main_tag(*), links_in_bio(*), distribution_shares(*), canton_party_verifications(*)' ) .eq('id', user?.id ?? '') .single() diff --git a/packages/snaplet/.snaplet/dataModel.json b/packages/snaplet/.snaplet/dataModel.json index c62bd8003..46a088099 100644 --- a/packages/snaplet/.snaplet/dataModel.json +++ b/packages/snaplet/.snaplet/dataModel.json @@ -630,6 +630,20 @@ "isId": false, "maxLength": null }, + { + "id": "storage.buckets.type", + "name": "type", + "columnName": "type", + "type": "buckettype", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": false, + "maxLength": null + }, { "name": "objects", "type": "objects", @@ -697,6 +711,112 @@ } ] }, + "buckets_analytics": { + "id": "storage.buckets_analytics", + "schemaName": "storage", + "tableName": "buckets_analytics", + "fields": [ + { + "id": "storage.buckets_analytics.id", + "name": "id", + "columnName": "id", + "type": "text", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": true, + "maxLength": null + }, + { + "id": "storage.buckets_analytics.type", + "name": "type", + "columnName": "type", + "type": "buckettype", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": false, + "maxLength": null + }, + { + "id": "storage.buckets_analytics.format", + "name": "format", + "columnName": "format", + "type": "text", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": false, + "maxLength": null + }, + { + "id": "storage.buckets_analytics.created_at", + "name": "created_at", + "columnName": "created_at", + "type": "timestamptz", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": false, + "maxLength": null + }, + { + "id": "storage.buckets_analytics.updated_at", + "name": "updated_at", + "columnName": "updated_at", + "type": "timestamptz", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": false, + "maxLength": null + }, + { + "name": "iceberg_namespaces", + "type": "iceberg_namespaces", + "isRequired": false, + "kind": "object", + "relationName": "iceberg_namespacesTobuckets_analytics", + "relationFromFields": [], + "relationToFields": [], + "isList": true, + "isId": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false + }, + { + "name": "iceberg_tables", + "type": "iceberg_tables", + "isRequired": false, + "kind": "object", + "relationName": "iceberg_tablesTobuckets_analytics", + "relationFromFields": [], + "relationToFields": [], + "isList": true, + "isId": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false + } + ], + "uniqueConstraints": [] + }, "canton_party_verifications": { "id": "public.canton_party_verifications", "schemaName": "public", @@ -2466,6 +2586,276 @@ ], "uniqueConstraints": [] }, + "iceberg_namespaces": { + "id": "storage.iceberg_namespaces", + "schemaName": "storage", + "tableName": "iceberg_namespaces", + "fields": [ + { + "id": "storage.iceberg_namespaces.id", + "name": "id", + "columnName": "id", + "type": "uuid", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": true, + "maxLength": null + }, + { + "id": "storage.iceberg_namespaces.bucket_id", + "name": "bucket_id", + "columnName": "bucket_id", + "type": "text", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "storage.iceberg_namespaces.name", + "name": "name", + "columnName": "name", + "type": "text", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "storage.iceberg_namespaces.created_at", + "name": "created_at", + "columnName": "created_at", + "type": "timestamptz", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": false, + "maxLength": null + }, + { + "id": "storage.iceberg_namespaces.updated_at", + "name": "updated_at", + "columnName": "updated_at", + "type": "timestamptz", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": false, + "maxLength": null + }, + { + "name": "buckets_analytics", + "type": "buckets_analytics", + "isRequired": true, + "kind": "object", + "relationName": "iceberg_namespacesTobuckets_analytics", + "relationFromFields": [ + "bucket_id" + ], + "relationToFields": [ + "id" + ], + "isList": false, + "isId": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false + }, + { + "name": "iceberg_tables", + "type": "iceberg_tables", + "isRequired": false, + "kind": "object", + "relationName": "iceberg_tablesToiceberg_namespaces", + "relationFromFields": [], + "relationToFields": [], + "isList": true, + "isId": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false + } + ], + "uniqueConstraints": [ + { + "name": "idx_iceberg_namespaces_bucket_id", + "fields": [ + "bucket_id", + "name" + ], + "nullNotDistinct": false + } + ] + }, + "iceberg_tables": { + "id": "storage.iceberg_tables", + "schemaName": "storage", + "tableName": "iceberg_tables", + "fields": [ + { + "id": "storage.iceberg_tables.id", + "name": "id", + "columnName": "id", + "type": "uuid", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": true, + "maxLength": null + }, + { + "id": "storage.iceberg_tables.namespace_id", + "name": "namespace_id", + "columnName": "namespace_id", + "type": "uuid", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "storage.iceberg_tables.bucket_id", + "name": "bucket_id", + "columnName": "bucket_id", + "type": "text", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "storage.iceberg_tables.name", + "name": "name", + "columnName": "name", + "type": "text", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "storage.iceberg_tables.location", + "name": "location", + "columnName": "location", + "type": "text", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "storage.iceberg_tables.created_at", + "name": "created_at", + "columnName": "created_at", + "type": "timestamptz", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": false, + "maxLength": null + }, + { + "id": "storage.iceberg_tables.updated_at", + "name": "updated_at", + "columnName": "updated_at", + "type": "timestamptz", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": false, + "maxLength": null + }, + { + "name": "buckets_analytics", + "type": "buckets_analytics", + "isRequired": true, + "kind": "object", + "relationName": "iceberg_tablesTobuckets_analytics", + "relationFromFields": [ + "bucket_id" + ], + "relationToFields": [ + "id" + ], + "isList": false, + "isId": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false + }, + { + "name": "iceberg_namespaces", + "type": "iceberg_namespaces", + "isRequired": true, + "kind": "object", + "relationName": "iceberg_tablesToiceberg_namespaces", + "relationFromFields": [ + "namespace_id" + ], + "relationToFields": [ + "id" + ], + "isList": false, + "isId": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false + } + ], + "uniqueConstraints": [ + { + "name": "idx_iceberg_tables_namespace_id", + "fields": [ + "name", + "namespace_id" + ], + "nullNotDistinct": false + } + ] + }, "identities": { "id": "auth.identities", "schemaName": "auth", @@ -11052,6 +11442,20 @@ "isId": false, "maxLength": null }, + { + "id": "auth.sso_providers.disabled", + "name": "disabled", + "columnName": "disabled", + "type": "bool", + "isRequired": false, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, { "name": "saml_providers", "type": "saml_providers", @@ -12020,6 +12424,34 @@ "isId": false, "maxLength": 255 }, + { + "id": "_realtime.tenants.max_presence_events_per_second", + "name": "max_presence_events_per_second", + "columnName": "max_presence_events_per_second", + "type": "int4", + "isRequired": false, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": false, + "maxLength": null + }, + { + "id": "_realtime.tenants.max_payload_size_in_kb", + "name": "max_payload_size_in_kb", + "columnName": "max_payload_size_in_kb", + "type": "int4", + "isRequired": false, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": false, + "maxLength": null + }, { "name": "extensions", "type": "extensions", @@ -13540,6 +13972,17 @@ } ] }, + "buckettype": { + "schemaName": "storage", + "values": [ + { + "name": "ANALYTICS" + }, + { + "name": "STANDARD" + } + ] + }, "transfer_status": { "schemaName": "temporal", "values": [ diff --git a/packages/snaplet/.snaplet/snaplet-client.d.ts b/packages/snaplet/.snaplet/snaplet-client.d.ts index 8f6ac2ccb..932421825 100644 --- a/packages/snaplet/.snaplet/snaplet-client.d.ts +++ b/packages/snaplet/.snaplet/snaplet-client.d.ts @@ -38,10 +38,23 @@ type Override = { file_size_limit?: string; allowed_mime_types?: string; owner_id?: string; + type?: string; objects?: string; prefixes?: string; }; } + buckets_analytics?: { + name?: string; + fields?: { + id?: string; + type?: string; + format?: string; + created_at?: string; + updated_at?: string; + iceberg_namespaces?: string; + iceberg_tables?: string; + }; + } canton_party_verifications?: { name?: string; fields?: { @@ -162,6 +175,32 @@ type Override = { request_id?: string; }; } + iceberg_namespaces?: { + name?: string; + fields?: { + id?: string; + bucket_id?: string; + name?: string; + created_at?: string; + updated_at?: string; + buckets_analytics?: string; + iceberg_tables?: string; + }; + } + iceberg_tables?: { + name?: string; + fields?: { + id?: string; + namespace_id?: string; + bucket_id?: string; + name?: string; + location?: string; + created_at?: string; + updated_at?: string; + buckets_analytics?: string; + iceberg_namespaces?: string; + }; + } leaderboard_referrals_all_time?: { name?: string; fields?: { @@ -716,6 +755,8 @@ type Override = { private_only?: string; migrations_ran?: string; broadcast_adapter?: string; + max_presence_events_per_second?: string; + max_payload_size_in_kb?: string; extensions?: string; }; } @@ -840,6 +881,12 @@ export interface Fingerprint { objects?: FingerprintRelationField; prefixes?: FingerprintRelationField; } + bucketsAnalytics?: { + createdAt?: FingerprintDateField; + updatedAt?: FingerprintDateField; + icebergNamespacesByBucketId?: FingerprintRelationField; + icebergTablesByBucketId?: FingerprintRelationField; + } cantonPartyVerifications?: { createdAt?: FingerprintDateField; user?: FingerprintRelationField; @@ -918,6 +965,18 @@ export interface Fingerprint { createdAt?: FingerprintDateField; requestId?: FingerprintNumberField; } + icebergNamespaces?: { + createdAt?: FingerprintDateField; + updatedAt?: FingerprintDateField; + bucket?: FingerprintRelationField; + icebergTablesByNamespaceId?: FingerprintRelationField; + } + icebergTables?: { + createdAt?: FingerprintDateField; + updatedAt?: FingerprintDateField; + bucket?: FingerprintRelationField; + namespace?: FingerprintRelationField; + } leaderboardReferralsAllTimes?: { referrals?: FingerprintNumberField; rewardsUsdc?: FingerprintNumberField; @@ -1200,6 +1259,8 @@ export interface Fingerprint { maxJoinsPerSecond?: FingerprintNumberField; jwtJwks?: FingerprintJsonField; migrationsRan?: FingerprintNumberField; + maxPresenceEventsPerSecond?: FingerprintNumberField; + maxPayloadSizeInKb?: FingerprintNumberField; extensions?: FingerprintRelationField; } tokenBalances?: { diff --git a/packages/snaplet/.snaplet/snaplet.d.ts b/packages/snaplet/.snaplet/snaplet.d.ts index d5bfdca0b..2f5655c4a 100644 --- a/packages/snaplet/.snaplet/snaplet.d.ts +++ b/packages/snaplet/.snaplet/snaplet.d.ts @@ -19,6 +19,7 @@ type Enum_public_verification_type = 'create_passkey' | 'send_ceiling' | 'send_o type Enum_public_verification_value_mode = 'aggregate' | 'individual'; type Enum_realtime_action = 'DELETE' | 'ERROR' | 'INSERT' | 'TRUNCATE' | 'UPDATE'; type Enum_realtime_equality_op = 'eq' | 'gt' | 'gte' | 'in' | 'lt' | 'lte' | 'neq'; +type Enum_storage_buckettype = 'ANALYTICS' | 'STANDARD'; type Enum_temporal_transfer_status = 'cancelled' | 'confirmed' | 'failed' | 'initialized' | 'sent' | 'submitted'; interface Table_net_http_response { id: number | null; @@ -64,6 +65,14 @@ interface Table_storage_buckets { file_size_limit: number | null; allowed_mime_types: string[] | null; owner_id: string | null; + type: Enum_storage_buckettype; +} +interface Table_storage_buckets_analytics { + id: string; + type: Enum_storage_buckettype; + format: string; + created_at: string; + updated_at: string; } interface Table_public_canton_party_verifications { id: string; @@ -181,6 +190,22 @@ interface Table_net_http_request_queue { body: string | null; timeout_milliseconds: number; } +interface Table_storage_iceberg_namespaces { + id: string; + bucket_id: string; + name: string; + created_at: string; + updated_at: string; +} +interface Table_storage_iceberg_tables { + id: string; + namespace_id: string; + bucket_id: string; + name: string; + location: string; + created_at: string; + updated_at: string; +} interface Table_auth_identities { provider_id: string; user_id: string; @@ -759,6 +784,7 @@ interface Table_auth_sso_providers { resource_id: string | null; created_at: string | null; updated_at: string | null; + disabled: boolean | null; } interface Table_realtime_subscription { id: number; @@ -842,6 +868,9 @@ interface Table_realtime_tenants { private_only: boolean; migrations_ran: number | null; broadcast_adapter: string | null; + max_presence_events_per_second: number | null; + max_payload_size_in_kb: number | null; +} interface Table_public_token_balances { id: number; user_id: string; @@ -1003,6 +1032,9 @@ interface Schema_shovel { } interface Schema_storage { buckets: Table_storage_buckets; + buckets_analytics: Table_storage_buckets_analytics; + iceberg_namespaces: Table_storage_iceberg_namespaces; + iceberg_tables: Table_storage_iceberg_tables; migrations: Table_storage_migrations; objects: Table_storage_objects; prefixes: Table_storage_prefixes; @@ -1087,6 +1119,19 @@ interface Tables_relationships { }; parentDestinationsTables: | {}; childDestinationsTables: "storage.objects" | "storage.prefixes" | "storage.s3_multipart_uploads" | "storage.s3_multipart_uploads_parts" | {}; + + }; + "storage.buckets_analytics": { + parent: { + + }; + children: { + iceberg_namespaces_bucket_id_fkey: "storage.iceberg_namespaces"; + iceberg_tables_bucket_id_fkey: "storage.iceberg_tables"; + }; + parentDestinationsTables: | {}; + childDestinationsTables: "storage.iceberg_namespaces" | "storage.iceberg_tables" | {}; + }; "public.canton_party_verifications": { parent: { @@ -1181,6 +1226,29 @@ interface Tables_relationships { parentDestinationsTables: | {}; childDestinationsTables: "auth.saml_relay_states" | {}; + }; + "storage.iceberg_namespaces": { + parent: { + iceberg_namespaces_bucket_id_fkey: "storage.buckets_analytics"; + }; + children: { + iceberg_tables_namespace_id_fkey: "storage.iceberg_tables"; + }; + parentDestinationsTables: "storage.buckets_analytics" | {}; + childDestinationsTables: "storage.iceberg_tables" | {}; + + }; + "storage.iceberg_tables": { + parent: { + iceberg_tables_bucket_id_fkey: "storage.buckets_analytics"; + iceberg_tables_namespace_id_fkey: "storage.iceberg_namespaces"; + }; + children: { + + }; + parentDestinationsTables: "storage.buckets_analytics" | "storage.iceberg_namespaces" | {}; + childDestinationsTables: | {}; + }; "auth.identities": { parent: { diff --git a/supabase/config.toml b/supabase/config.toml index ab77185d7..5779ab29f 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -38,6 +38,7 @@ schema_paths = [ "./schemas/distributions.sql", "./schemas/send_earn.sql", "./schemas/send_scores.sql", + "./schemas/distribution_verifications.sql", # Send account related tables "./schemas/send_account_created.sql", diff --git a/supabase/database-generated.types.ts b/supabase/database-generated.types.ts index db4ef5dea..8588c446e 100644 --- a/supabase/database-generated.types.ts +++ b/supabase/database-generated.types.ts @@ -1734,6 +1734,7 @@ export type Database = { chain_id: number | null id: string | null is_public: boolean | null + is_verified: boolean | null links_in_bio: | Database["public"]["Tables"]["link_in_bio"]["Row"][] | null @@ -1744,6 +1745,7 @@ export type Database = { send_id: number | null sendid: number | null tag: string | null + verified_at: string | null x_username: string | null } Relationships: [] @@ -2190,6 +2192,10 @@ export type Database = { Args: Record Returns: number } + verified_at: { + Args: { p: Database["public"]["Tables"]["profiles"]["Row"] } + Returns: string + } } Enums: { key_type_enum: "ES256" @@ -2257,6 +2263,7 @@ export type Database = { | null banner_url: string | null is_verified: boolean | null + verified_at: string | null } tag_search_result: { avatar_url: string | null diff --git a/supabase/database.types.ts b/supabase/database.types.ts index c13a297d2..06136ea4b 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -73,6 +73,8 @@ export type Database = MergeDeep< main_tag: DatabaseGenerated['public']['Tables']['tags']['Row'] links_in_bio: DatabaseGenerated['public']['Tables']['link_in_bio']['Row'][] distribution_shares: ProfileDistributionShares[] + is_verified: boolean + verified_at: string } } webauthn_credentials: { diff --git a/supabase/migrations/20251004125914_verified_at_computed_and_profile_lookup.sql b/supabase/migrations/20251004125914_verified_at_computed_and_profile_lookup.sql new file mode 100644 index 000000000..c1cbc2997 --- /dev/null +++ b/supabase/migrations/20251004125914_verified_at_computed_and_profile_lookup.sql @@ -0,0 +1,316 @@ +set check_function_bodies = off; + +ALTER TYPE "public"."profile_lookup_result" + ADD ATTRIBUTE "verified_at" timestamptz; + +ALTER TYPE "public"."tag_search_result" + ADD ATTRIBUTE "verified_at" timestamptz; + +CREATE OR REPLACE FUNCTION public.verified_at(p profiles) + RETURNS timestamptz + LANGUAGE sql + STABLE SECURITY DEFINER + SET search_path TO 'public' +AS $function$ +SELECT ( + SELECT CASE + WHEN tag_at IS NOT NULL AND hodler_at IS NOT NULL AND earn_at IS NOT NULL + THEN GREATEST(tag_at, hodler_at, earn_at) + ELSE NULL + END + FROM ( + SELECT + -- Active distribution (current window) + d.id, + -- Earliest time user satisfied tag_registration for this distribution + ( + SELECT MIN(dv.created_at) AS tag_at + FROM distribution_verifications dv + WHERE dv.user_id = p.id + AND dv.distribution_id = d.id + AND dv.type = 'tag_registration'::verification_type + AND dv.weight > 0 + ) AS tag_at, + -- Earliest time user satisfied hodler threshold for this distribution + ( + SELECT MIN(dv.created_at) AS hodler_at + FROM distribution_verifications dv + WHERE dv.user_id = p.id + AND dv.distribution_id = d.id + AND dv.type = 'send_token_hodler'::verification_type + AND dv.weight >= d.hodler_min_balance + ) AS hodler_at, + -- Earliest time any of the user's earn balances met the threshold + ( + SELECT MIN(to_timestamp(ebt.block_time) AT TIME ZONE 'UTC') AS earn_at + FROM send_earn_balances_timeline ebt + JOIN send_accounts sa + ON decode(replace(sa.address::text, ('0x'::citext)::text, ''::text), 'hex') = ebt.owner + WHERE sa.user_id = p.id + AND ebt.assets >= d.earn_min_balance + ) AS earn_at + FROM ( + SELECT id, hodler_min_balance, earn_min_balance, qualification_start + FROM distributions + WHERE qualification_start <= (now() AT TIME ZONE 'UTC') + AND qualification_end >= (now() AT TIME ZONE 'UTC') + ORDER BY qualification_start DESC + LIMIT 1 + ) d + ) s +); +$function$ +; + + + +CREATE OR REPLACE FUNCTION public.profile_lookup(lookup_type lookup_type_enum, identifier text) +RETURNS SETOF profile_lookup_result +LANGUAGE plpgsql +IMMUTABLE SECURITY DEFINER +AS $function$ +begin + if identifier is null or identifier = '' then raise exception 'identifier cannot be null or empty'; end if; + if lookup_type is null then raise exception 'lookup_type cannot be null'; end if; + + RETURN QUERY + WITH current_distribution_id AS ( + SELECT id FROM distributions + WHERE qualification_start <= CURRENT_TIMESTAMP AT TIME ZONE 'UTC' + AND qualification_end >= CURRENT_TIMESTAMP AT TIME ZONE 'UTC' + ORDER BY qualification_start DESC + LIMIT 1 + ) + SELECT + case when p.id = ( select auth.uid() ) then p.id end, + p.avatar_url::text, + p.name::text, + p.about::text, + p.referral_code, + CASE WHEN p.is_public THEN p.x_username ELSE NULL END, + CASE WHEN p.is_public THEN p.birthday ELSE NULL END, + COALESCE(mt.name, t.name), + sa.address, + sa.chain_id, + case when current_setting('role')::text = 'service_role' then p.is_public + when p.is_public then true + else false end, + p.send_id, + ( select array_agg(t2.name::text) + from tags t2 + join send_account_tags sat2 on sat2.tag_id = t2.id + join send_accounts sa2 on sa2.id = sat2.send_account_id + where sa2.user_id = p.id and t2.status = 'confirmed'::tag_status ), + case when p.id = ( select auth.uid() ) then sa.main_tag_id end, + mt.name::text, + CASE WHEN p.is_public THEN +(SELECT array_agg(link_in_bio_row) + FROM ( + SELECT ROW( + CASE WHEN lib.user_id = (SELECT auth.uid()) THEN lib.id ELSE NULL END, + CASE WHEN lib.user_id = (SELECT auth.uid()) THEN lib.user_id ELSE NULL END, + lib.handle, + lib.domain_name, + lib.created_at, + lib.updated_at, + lib.domain + )::link_in_bio as link_in_bio_row + FROM link_in_bio lib + WHERE lib.user_id = p.id AND lib.handle IS NOT NULL + ) sub) + ELSE NULL + END, + p.banner_url::text, + public.verified_at(p) IS NOT NULL AS is_verified, + public.verified_at(p) AS verified_at + from profiles p + join auth.users a on a.id = p.id + left join send_accounts sa on sa.user_id = p.id + left join tags mt on mt.id = sa.main_tag_id + left join send_account_tags sat on sat.send_account_id = sa.id + left join tags t on t.id = sat.tag_id and t.status = 'confirmed'::tag_status + where ((lookup_type = 'sendid' and p.send_id::text = identifier) or + (lookup_type = 'tag' and t.name = identifier::citext) or + (lookup_type = 'refcode' and p.referral_code = identifier) or + (lookup_type = 'address' and sa.address = identifier) or + (p.is_public and lookup_type = 'phone' and a.phone::text = identifier)) + and (p.is_public + or ( select auth.uid() ) is not null + or current_setting('role')::text = 'service_role') + limit 1; +end; +$function$; + +create or replace view "public"."referrer" as WITH referrer AS ( + SELECT p.send_id + FROM (referrals r + JOIN profiles p ON ((r.referrer_id = p.id))) + WHERE (r.referred_id = ( SELECT auth.uid() AS uid)) + ORDER BY r.created_at + LIMIT 1 + ), profile_lookup AS ( + SELECT pl.id, + pl.avatar_url, + pl.name, + pl.about, + pl.refcode, + pl.x_username, + pl.birthday, + pl.tag, + pl.address, + pl.chain_id, + pl.is_public, + pl.sendid, + pl.all_tags, + pl.main_tag_id, + pl.main_tag_name, + pl.links_in_bio, + pl.banner_url, + pl.is_verified, + pl.verified_at, + referrer.send_id + FROM profile_lookup('sendid'::lookup_type_enum, ( SELECT (referrer_1.send_id)::text AS send_id FROM referrer referrer_1)) AS pl + JOIN referrer ON ((referrer.send_id IS NOT NULL)) + ) + SELECT profile_lookup.id, + profile_lookup.avatar_url, + profile_lookup.name, + profile_lookup.about, + profile_lookup.refcode, + profile_lookup.x_username, + profile_lookup.birthday, + profile_lookup.tag, + profile_lookup.address, + profile_lookup.chain_id, + profile_lookup.is_public, + profile_lookup.sendid, + profile_lookup.all_tags, + profile_lookup.main_tag_id, + profile_lookup.main_tag_name, + profile_lookup.links_in_bio, + profile_lookup.send_id, + profile_lookup.banner_url, + profile_lookup.is_verified, + profile_lookup.verified_at + FROM profile_lookup; + + +CREATE OR REPLACE FUNCTION public.tag_search(query text, limit_val integer, offset_val integer) + RETURNS TABLE(send_id_matches tag_search_result[], tag_matches tag_search_result[], phone_matches tag_search_result[]) + LANGUAGE plpgsql + IMMUTABLE SECURITY DEFINER +AS $function$ +BEGIN + IF limit_val IS NULL OR(limit_val <= 0 OR limit_val > 100) THEN + RAISE EXCEPTION 'limit_val must be between 1 and 100'; + END IF; + IF offset_val IS NULL OR offset_val < 0 THEN + RAISE EXCEPTION 'offset_val must be greater than or equal to 0'; + END IF; + RETURN query + SELECT + -- send_id matches +( + SELECT + array_agg(ROW(sub.avatar_url, sub.tag_name, sub.send_id, sub.phone, sub.is_verified, sub.verified_at)::public.tag_search_result) + FROM( + SELECT + p.avatar_url, + t.name AS tag_name, + p.send_id, + NULL::text AS phone, + (public.verified_at(p) IS NOT NULL) AS is_verified, + public.verified_at(p) AS verified_at + FROM + profiles p + LEFT JOIN send_accounts sa ON sa.user_id = p.id + LEFT JOIN send_account_tags sat ON sat.send_account_id = sa.id + LEFT JOIN tags t ON t.id = sat.tag_id + AND t.status = 'confirmed' + WHERE + query SIMILAR TO '\d+' + AND p.send_id::varchar LIKE '%' || query || '%' + ORDER BY + p.send_id + LIMIT limit_val offset offset_val) sub) AS send_id_matches, + -- tag matches + ( + SELECT + array_agg(ROW(sub.avatar_url, sub.tag_name, sub.send_id, sub.phone, sub.is_verified, sub.verified_at)::public.tag_search_result) + FROM ( + SELECT + ranked_matches.avatar_url, + ranked_matches.tag_name, + ranked_matches.send_id, + ranked_matches.phone, + ranked_matches.is_verified, + ranked_matches.verified_at + FROM ( + WITH scores AS ( + -- Aggregate user send scores, summing all scores for cumulative activity + SELECT + user_id, + SUM(score) AS total_score + FROM private.send_scores_history + GROUP BY user_id + ), + tag_matches AS ( + SELECT + p.avatar_url, + t.name AS tag_name, + p.send_id, + NULL::text AS phone, + (public.verified_at(p) IS NOT NULL) AS is_verified, + public.verified_at(p) AS verified_at, + (t.name <-> query) AS distance, -- Trigram distance (kept for debugging/ties) + COALESCE(scores.total_score, 0) AS send_score, + -- Compute exact match flag in CTE + LOWER(t.name) = LOWER(query) AS is_exact, + -- Primary ranking: exact matches (primary_rank=0) always outrank fuzzy matches (primary_rank=1) + CASE WHEN LOWER(t.name) = LOWER(query) THEN 0 ELSE 1 END AS primary_rank + FROM profiles p + JOIN send_accounts sa ON sa.user_id = p.id + JOIN send_account_tags sat ON sat.send_account_id = sa.id + JOIN tags t ON t.id = sat.tag_id + AND t.status = 'confirmed' + LEFT JOIN scores ON scores.user_id = p.id + WHERE + -- Use ILIKE '%' only when NOT exact to avoid excluding true exact matches like 'Ethen_' + LOWER(t.name) = LOWER(query) + OR (NOT (LOWER(t.name) = LOWER(query)) AND (t.name <<-> query < 0.7 OR t.name ILIKE '%' || query || '%')) + ) + SELECT + tm.avatar_url, + tm.tag_name, + tm.send_id, + tm.phone, + tm.is_verified, + tm.distance, + tm.send_score, + tm.is_exact, + tm.primary_rank, + -- Verification bucket for ranking within fuzzy matches: 0 verified, 1 unverified. Exact bucket unaffected. + CASE WHEN tm.is_exact THEN 1 ELSE CASE WHEN tm.is_verified THEN 0 ELSE 1 END END AS verification_rank, + -- Higher score should sort earlier -> negative for ascending order + -tm.send_score AS score_rank, + ROW_NUMBER() OVER ( + PARTITION BY tm.send_id + ORDER BY + tm.primary_rank, + CASE WHEN tm.is_exact THEN 1 ELSE CASE WHEN tm.is_verified THEN 0 ELSE 1 END END, + -tm.send_score + ) AS rn + FROM tag_matches tm + ) ranked_matches + WHERE ranked_matches.rn = 1 + ORDER BY ranked_matches.primary_rank ASC, + ranked_matches.verification_rank ASC, + ranked_matches.score_rank ASC + LIMIT limit_val OFFSET offset_val + ) sub + ) AS tag_matches, + -- phone matches, disabled for now + (null::public.tag_search_result[]) AS phone_matches; +END; +$function$ +; \ No newline at end of file diff --git a/supabase/schemas/distribution_verifications.sql b/supabase/schemas/distribution_verifications.sql new file mode 100644 index 000000000..8c36fc995 --- /dev/null +++ b/supabase/schemas/distribution_verifications.sql @@ -0,0 +1,883 @@ +-- distribution_verifications.sql + +-- Sequences and tables for distribution verifications +CREATE SEQUENCE IF NOT EXISTS "public"."distribution_verifications_id_seq" + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER TABLE "public"."distribution_verifications_id_seq" OWNER TO "postgres"; + +CREATE TABLE IF NOT EXISTS "public"."distribution_verifications" ( + "id" integer NOT NULL, + "distribution_id" integer NOT NULL, + "user_id" "uuid" NOT NULL, + "type" "public"."verification_type" NOT NULL, + "metadata" "jsonb", + "created_at" timestamp with time zone DEFAULT ("now"() AT TIME ZONE 'utc'::"text") NOT NULL, + "weight" numeric DEFAULT 1 NOT NULL +); + +ALTER TABLE "public"."distribution_verifications" OWNER TO "postgres"; + +CREATE TABLE IF NOT EXISTS "public"."distribution_verification_values" ( + "type" "public"."verification_type" NOT NULL, + "fixed_value" numeric NOT NULL, + "bips_value" bigint NOT NULL, + "distribution_id" integer NOT NULL, + "created_at" timestamp with time zone DEFAULT ("now"() AT TIME ZONE 'utc'::"text") NOT NULL, + "updated_at" timestamp with time zone DEFAULT ("now"() AT TIME ZONE 'utc'::"text") NOT NULL, + "multiplier_min" numeric(10,4) DEFAULT 1.0 NOT NULL, + "multiplier_max" numeric(10,4) DEFAULT 1.0 NOT NULL, + "multiplier_step" numeric(10,4) DEFAULT 0.0 NOT NULL +); + +ALTER TABLE "public"."distribution_verification_values" OWNER TO "postgres"; + +-- Sequence ownership and defaults +ALTER SEQUENCE "public"."distribution_verifications_id_seq" OWNED BY "public"."distribution_verifications"."id"; +ALTER TABLE ONLY "public"."distribution_verifications" ALTER COLUMN "id" SET DEFAULT "nextval"('"public"."distribution_verifications_id_seq"'::"regclass"); + +-- Primary Keys +ALTER TABLE ONLY "public"."distribution_verifications" + ADD CONSTRAINT "distribution_verifications_pkey" PRIMARY KEY ("id"); +ALTER TABLE ONLY "public"."distribution_verification_values" + ADD CONSTRAINT "distribution_verification_values_pkey" PRIMARY KEY ("type", "distribution_id"); + +-- Indexes +CREATE INDEX "distribution_verifications_distribution_id_index" ON "public"."distribution_verifications" USING "btree" ("distribution_id"); +CREATE INDEX "distribution_verifications_user_id_index" ON "public"."distribution_verifications" USING "btree" ("user_id"); +CREATE INDEX "idx_distribution_verifications_composite" ON "public"."distribution_verifications" USING "btree" ("distribution_id", "user_id", "type"); + +-- Foreign Keys +ALTER TABLE ONLY "public"."distribution_verification_values" + ADD CONSTRAINT "distribution_verification_values_distribution_id_fkey" FOREIGN KEY ("distribution_id") REFERENCES "public"."distributions"("id") ON DELETE CASCADE; +ALTER TABLE ONLY "public"."distribution_verifications" + ADD CONSTRAINT "distribution_verification_values_fk" FOREIGN KEY ("type", "distribution_id") REFERENCES "public"."distribution_verification_values"("type", "distribution_id"); +ALTER TABLE ONLY "public"."distribution_verifications" + ADD CONSTRAINT "distribution_verifications_distribution_id_fkey" FOREIGN KEY ("distribution_id") REFERENCES "public"."distributions"("id") ON DELETE CASCADE; +ALTER TABLE ONLY "public"."distribution_verifications" + ADD CONSTRAINT "distribution_verifications_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; + +-- Grants for tables and sequence +GRANT ALL ON TABLE "public"."distribution_verifications" TO "anon"; +GRANT ALL ON TABLE "public"."distribution_verifications" TO "authenticated"; +GRANT ALL ON TABLE "public"."distribution_verifications" TO "service_role"; +GRANT ALL ON SEQUENCE "public"."distribution_verifications_id_seq" TO "anon"; +GRANT ALL ON SEQUENCE "public"."distribution_verifications_id_seq" TO "authenticated"; +GRANT ALL ON SEQUENCE "public"."distribution_verifications_id_seq" TO "service_role"; +GRANT ALL ON TABLE "public"."distribution_verification_values" TO "anon"; +GRANT ALL ON TABLE "public"."distribution_verification_values" TO "authenticated"; +GRANT ALL ON TABLE "public"."distribution_verification_values" TO "service_role"; + +-- RLS for DV and DVV +ALTER TABLE ONLY "public"."distribution_verifications" ENABLE ROW LEVEL SECURITY; +CREATE POLICY "Users can see their own distribution verifications" ON "public"."distribution_verifications" FOR SELECT USING ((( SELECT "auth"."uid"() AS "uid") = "user_id")); +ALTER TABLE ONLY "public"."distribution_verification_values" ENABLE ROW LEVEL SECURITY; +CREATE POLICY "Authenticated users can see distribution_verification_values" ON "public"."distribution_verification_values" FOR SELECT USING ((( SELECT "auth"."uid"() AS "uid") IS NOT NULL)); +CREATE POLICY "Enable read access to public" ON "public"."distribution_verification_values" FOR SELECT TO "authenticated" USING (true); + + +-- Computed column: verified_at(public.profiles) +-- Returns the timestamp (UTC) when all three verification conditions were first met +-- 1) tag_registration DV present (weight > 0) for the active distribution +-- 2) send_token_hodler DV weight >= distributions.hodler_min_balance +-- 3) Any send_earn_balances_timeline entry for any of the user's addresses with assets >= distributions.earn_min_balance +-- The result is the GREATEST of the earliest timestamps at which each condition was satisfied. + +CREATE OR REPLACE FUNCTION public.verified_at(p profiles) + RETURNS timestamp with time zone + LANGUAGE sql + STABLE SECURITY DEFINER + SET search_path TO 'public' +AS $function$ +SELECT ( + SELECT CASE + WHEN tag_at IS NOT NULL AND hodler_at IS NOT NULL AND earn_at IS NOT NULL + THEN GREATEST(tag_at, hodler_at, earn_at) + ELSE NULL + END + FROM ( + SELECT + -- Active distribution (current window) + d.id, + -- Earliest time user satisfied tag_registration for this distribution + ( + SELECT MIN(dv.created_at) AS tag_at + FROM distribution_verifications dv + WHERE dv.user_id = p.id + AND dv.distribution_id = d.id + AND dv.type = 'tag_registration'::verification_type + AND dv.weight > 0 + ) AS tag_at, + -- Earliest time user satisfied hodler threshold for this distribution + ( + SELECT MIN(dv.created_at) AS hodler_at + FROM distribution_verifications dv + WHERE dv.user_id = p.id + AND dv.distribution_id = d.id + AND dv.type = 'send_token_hodler'::verification_type + AND dv.weight >= d.hodler_min_balance + ) AS hodler_at, + -- Earliest time any of the user's earn balances met the threshold + ( + SELECT MIN(to_timestamp(ebt.block_time) AT TIME ZONE 'UTC') AS earn_at + FROM send_earn_balances_timeline ebt + JOIN send_accounts sa + ON decode(replace(sa.address::text, ('0x'::citext)::text, ''::text), 'hex') = ebt.owner + WHERE sa.user_id = p.id + AND ebt.assets >= d.earn_min_balance + ) AS earn_at + FROM ( + SELECT id, hodler_min_balance, earn_min_balance, qualification_start + FROM distributions + WHERE qualification_start <= (now() AT TIME ZONE 'UTC') + AND qualification_end >= (now() AT TIME ZONE 'UTC') + ORDER BY qualification_start DESC + LIMIT 1 + ) d + ) s +); +$function$ +; + +ALTER FUNCTION public.verified_at("public"."profiles") OWNER TO postgres; +-- Verification functions migrated from distributions.sql (CREATE OR REPLACE safe) + +CREATE OR REPLACE FUNCTION public.calculate_and_insert_send_ceiling_verification(distribution_number integer) +RETURNS void +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +BEGIN + WITH dist_scores AS ( + SELECT * FROM send_scores ss + WHERE ss.distribution_id = ( + SELECT id FROM distributions WHERE number = $1 + ) + ), + updated_rows AS ( + UPDATE distribution_verifications dv + SET + weight = ds.score, + metadata = jsonb_build_object('value', ds.send_ceiling::text) + FROM dist_scores ds + WHERE dv.user_id = ds.user_id + AND dv.distribution_id = ds.distribution_id + AND dv.type = 'send_ceiling' + RETURNING dv.user_id + ) + INSERT INTO distribution_verifications( + distribution_id, + user_id, + type, + weight, + metadata + ) + SELECT + distribution_id, + user_id, + 'send_ceiling'::public.verification_type, + score, + jsonb_build_object('value', send_ceiling::text) + FROM dist_scores ds + WHERE NOT EXISTS ( + SELECT 1 FROM updated_rows ur + WHERE ur.user_id = ds.user_id + ); +END; +$$; + +CREATE OR REPLACE FUNCTION "public"."insert_create_passkey_verifications"("distribution_num" integer) RETURNS "void" + LANGUAGE "plpgsql" + AS $$ +BEGIN + INSERT INTO public.distribution_verifications( + distribution_id, + user_id, + type, + metadata, + created_at) + SELECT + ( + SELECT + id + FROM + distributions + WHERE + "number" = distribution_num + LIMIT 1) AS distribution_id, + sa.user_id, + 'create_passkey'::public.verification_type AS type, + jsonb_build_object('account_created_at', sa.created_at) AS metadata, + sa.created_at AS created_at + FROM + send_accounts sa + WHERE + sa.created_at >= ( + SELECT + qualification_start + FROM + distributions + WHERE + "number" = distribution_num + LIMIT 1 + ) + AND sa.created_at <= ( + SELECT + qualification_end + FROM + distributions + WHERE + "number" = distribution_num + LIMIT 1 + ); +END; +$$; + +CREATE OR REPLACE FUNCTION "public"."insert_send_streak_verifications"("distribution_num" integer) RETURNS "void" + LANGUAGE "plpgsql" + AS $$ +BEGIN + -- Perform the entire operation within a single function + WITH distribution_info AS ( + SELECT + id, + qualification_start, + qualification_end + FROM + distributions + WHERE + "number" = distribution_num + LIMIT 1 + ), + daily_transfers AS ( + SELECT + sa.user_id, + DATE(to_timestamp(stt.block_time) AT TIME ZONE 'UTC') AS transfer_date, + COUNT(DISTINCT stt.t) AS unique_recipients + FROM + send_token_transfers stt + JOIN send_accounts sa ON sa.address = CONCAT('0x', ENCODE(stt.f, 'hex'))::CITEXT + WHERE + stt.block_time >= EXTRACT(EPOCH FROM ( + SELECT + qualification_start + FROM distribution_info)) + AND stt.block_time < EXTRACT(EPOCH FROM ( + SELECT + qualification_end + FROM distribution_info)) + GROUP BY + sa.user_id, + DATE(to_timestamp(stt.block_time) AT TIME ZONE 'UTC') + ), + streaks AS ( + SELECT + user_id, + transfer_date, + unique_recipients, + transfer_date - (ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY transfer_date))::INTEGER AS streak_group + FROM + daily_transfers + WHERE + unique_recipients > 0 + ), + max_streaks AS ( + SELECT + user_id, + MAX(streak_length) AS max_streak_length + FROM ( + SELECT + user_id, + streak_group, + COUNT(*) AS streak_length + FROM + streaks + GROUP BY + user_id, + streak_group) AS streak_lengths + GROUP BY + user_id + ) + INSERT INTO public.distribution_verifications( + distribution_id, + user_id, + type, + created_at, + weight + ) + SELECT + ( + SELECT + id + FROM + distribution_info), + ms.user_id, + 'send_streak'::public.verification_type, + (SELECT NOW() AT TIME ZONE 'UTC'), + ms.max_streak_length + FROM + max_streaks ms + WHERE + ms.max_streak_length > 0; +END; +$$; + +CREATE OR REPLACE FUNCTION public.insert_send_verifications(distribution_num integer) +RETURNS void +LANGUAGE plpgsql +AS $$ +BEGIN + INSERT INTO public.distribution_verifications( + distribution_id, + user_id, + type, + metadata, + created_at, + weight + ) + SELECT + d.id, + ss.user_id, + type, + JSONB_BUILD_OBJECT('value', ss.unique_sends), + d.qualification_end, + CASE + WHEN type = 'send_ten'::public.verification_type + AND ss.unique_sends >= 10 THEN 1 + WHEN type = 'send_one_hundred'::public.verification_type + AND ss.unique_sends >= 100 THEN 1 + ELSE 0 + END + FROM + distributions d + JOIN send_scores ss ON ss.distribution_id = d.id + CROSS JOIN ( + SELECT 'send_ten'::public.verification_type AS type + UNION ALL + SELECT 'send_one_hundred'::public.verification_type + ) types + WHERE d.number = distribution_num; +END; +$$; + +CREATE OR REPLACE FUNCTION "public"."insert_tag_referral_verifications"("distribution_num" integer) RETURNS "void" + LANGUAGE "plpgsql" + AS $$ +DECLARE + dist_id integer; + prev_dist_id integer; + qual_start timestamp; + qual_end timestamp; +BEGIN + -- Get current distribution data once + SELECT id, qualification_start, qualification_end INTO dist_id, qual_start, qual_end + FROM distributions + WHERE "number" = distribution_num + LIMIT 1; + + -- Get previous distribution ID once + SELECT id INTO prev_dist_id + FROM distributions + WHERE "number" = distribution_num - 1 + LIMIT 1; + + -- Add month referrals to distribution_verifications + INSERT INTO public.distribution_verifications( + distribution_id, + user_id, + type, + metadata, + created_at, + weight) + SELECT + dist_id, + referrer_id, + 'tag_referral'::public.verification_type, + jsonb_build_object('referred_id', referred_id), + referrals.created_at, + CASE + WHEN EXISTS ( + SELECT 1 + FROM distribution_shares ds + WHERE ds.user_id = referrals.referred_id + AND ds.distribution_id = prev_dist_id + ) THEN 1 + ELSE 0 + END + FROM + referrals + WHERE + referrals.created_at < qual_end + AND referrals.created_at > qual_start; +END; +$$; + +CREATE OR REPLACE FUNCTION insert_tag_registration_verifications(distribution_num integer) +RETURNS void AS $$ +BEGIN + -- Idempotent insert: avoid duplicating rows per (distribution_id, user_id, type, tag) + INSERT INTO public.distribution_verifications( + distribution_id, + user_id, + type, + metadata, + weight, + created_at + ) + SELECT + ( + SELECT id + FROM distributions + WHERE "number" = distribution_num + LIMIT 1 + ) AS distribution_id, + t.user_id, + 'tag_registration'::public.verification_type AS type, + jsonb_build_object('tag', t."name") AS metadata, + CASE + WHEN LENGTH(t.name) >= 6 THEN 1 + WHEN LENGTH(t.name) = 5 THEN 2 + WHEN LENGTH(t.name) = 4 THEN 3 -- Increase reward value of shorter tags + WHEN LENGTH(t.name) > 0 THEN 4 + ELSE 0 + END AS weight, + t.created_at AS created_at + FROM tags t + INNER JOIN tag_receipts tr ON t.name = tr.tag_name + WHERE NOT EXISTS ( + SELECT 1 + FROM public.distribution_verifications dv + WHERE dv.distribution_id = ( + SELECT id FROM distributions WHERE "number" = distribution_num LIMIT 1 + ) + AND dv.user_id = t.user_id + AND dv.type = 'tag_registration'::public.verification_type + AND dv.metadata->>'tag' = t.name + ); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION "public"."insert_total_referral_verifications"("distribution_num" integer) RETURNS "void" + LANGUAGE "plpgsql" + AS $$ +DECLARE + dist_id integer; + prev_dist_id integer; + qual_end timestamp; +BEGIN + -- Get current distribution data once + SELECT id, qualification_end INTO dist_id, qual_end + FROM distributions + WHERE "number" = distribution_num + LIMIT 1; + + -- Get previous distribution ID once + SELECT id INTO prev_dist_id + FROM distributions + WHERE "number" = distribution_num - 1 + LIMIT 1; + + -- Add total_tag_referrals to distribution_verifications + INSERT INTO public.distribution_verifications( + distribution_id, + user_id, + type, + created_at, + weight) + WITH total_referrals AS ( + SELECT + r.referrer_id, + COUNT(*) FILTER (WHERE EXISTS ( + SELECT 1 + FROM distribution_shares ds + WHERE ds.user_id = r.referred_id + AND ds.distribution_id = prev_dist_id + )) AS qualified_referrals, + MAX(r.created_at) AS last_referral_date + FROM + referrals r + WHERE + r.created_at <= qual_end + GROUP BY + r.referrer_id + ) + SELECT + dist_id AS distribution_id, + tr.referrer_id AS user_id, + 'total_tag_referrals'::public.verification_type AS type, + LEAST(tr.last_referral_date, qual_end) AS created_at, + tr.qualified_referrals AS weight + FROM + total_referrals tr + WHERE + tr.qualified_referrals > 0; +END; +$$; + +CREATE OR REPLACE FUNCTION "public"."insert_verification_value"("distribution_number" integer, "type" "public"."verification_type", "fixed_value" numeric DEFAULT NULL::numeric, "bips_value" integer DEFAULT NULL::integer, "multiplier_min" numeric DEFAULT NULL::numeric, "multiplier_max" numeric DEFAULT NULL::numeric, "multiplier_step" numeric DEFAULT NULL::numeric) RETURNS "void" + LANGUAGE "plpgsql" + AS $$ +DECLARE + prev_verification_values RECORD; +BEGIN + SELECT * INTO prev_verification_values + FROM public.distribution_verification_values dvv + WHERE distribution_id = (SELECT id FROM distributions WHERE "number" = insert_verification_value.distribution_number - 1 LIMIT 1) + AND dvv.type = insert_verification_value.type + LIMIT 1; + + INSERT INTO public.distribution_verification_values( + type, + fixed_value, + bips_value, + multiplier_min, + multiplier_max, + multiplier_step, + distribution_id + ) VALUES ( + insert_verification_value.type, + COALESCE(insert_verification_value.fixed_value, prev_verification_values.fixed_value, 0), + COALESCE(insert_verification_value.bips_value, prev_verification_values.bips_value, 0), + COALESCE(insert_verification_value.multiplier_min, prev_verification_values.multiplier_min), + COALESCE(insert_verification_value.multiplier_max, prev_verification_values.multiplier_max), + COALESCE(insert_verification_value.multiplier_step, prev_verification_values.multiplier_step), + (SELECT id FROM distributions WHERE "number" = insert_verification_value.distribution_number LIMIT 1) + ); +END; +$$; + +ALTER FUNCTION "public"."insert_verification_value"("distribution_number" integer, "type" "public"."verification_type", "fixed_value" numeric, "bips_value" integer, "multiplier_min" numeric, "multiplier_max" numeric, "multiplier_step" numeric) OWNER TO "postgres"; + + +CREATE OR REPLACE FUNCTION public.update_referral_verifications( + distribution_id INTEGER, + shares distribution_shares[] +) +RETURNS VOID +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path TO 'public' +AS $function$ +BEGIN + -- Create temp table for shares lookup + CREATE TEMPORARY TABLE temp_shares ON COMMIT DROP AS + SELECT DISTINCT user_id + FROM unnest(shares) ds; + + -- Update tag_referral weights - just check if in shares + UPDATE distribution_verifications dv + SET weight = CASE + WHEN ts.user_id IS NOT NULL THEN 1 + ELSE 0 + END + FROM referrals r + LEFT JOIN temp_shares ts ON ts.user_id = r.referred_id + WHERE dv.distribution_id = $1 + AND dv.type = 'tag_referral' + AND dv.user_id = r.referrer_id + AND (dv.metadata->>'referred_id')::uuid = r.referred_id; + + -- Insert total_tag_referrals if doesn't exist + INSERT INTO distribution_verifications (distribution_id, user_id, type, weight) + SELECT + $1, + r.referrer_id, + 'total_tag_referrals', + COUNT(ts.user_id) + FROM referrals r + JOIN temp_shares ts ON ts.user_id = r.referred_id + WHERE NOT EXISTS ( + SELECT 1 FROM distribution_verifications dv + WHERE dv.distribution_id = $1 + AND dv.type = 'total_tag_referrals' + AND dv.user_id = r.referrer_id + ) + GROUP BY r.referrer_id; + + -- Update existing total_tag_referrals + UPDATE distribution_verifications dv + SET weight = rc.referral_count + FROM ( + SELECT + r.referrer_id, + COUNT(ts.user_id) as referral_count + FROM referrals r + JOIN temp_shares ts ON ts.user_id = r.referred_id + GROUP BY r.referrer_id + ) rc + WHERE dv.distribution_id = $1 + AND dv.type = 'total_tag_referrals' + AND dv.user_id = rc.referrer_id; + + DROP TABLE temp_shares; +END; +$function$; + +ALTER FUNCTION "public"."update_referral_verifications"("distribution_id" integer, "shares" "public"."distribution_shares"[]) OWNER TO "postgres"; + + +-- Trigger functions for real-time updates + +CREATE OR REPLACE FUNCTION public."insert_send_streak_verification"() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + curr_distribution_id bigint; + from_user_id uuid; + to_user_id uuid; + unique_recipient_count integer; + current_streak integer; + existing_record_id bigint; + ignored_addresses bytea[] := ARRAY['\x592e1224d203be4214b15e205f6081fbbacfcd2d'::bytea, '\x36f43082d01df4801af2d95aeed1a0200c5510ae'::bytea]; +BEGIN + -- Get the current distribution id + SELECT id INTO curr_distribution_id + FROM distributions + WHERE qualification_start <= CURRENT_TIMESTAMP AT TIME ZONE 'UTC' + AND qualification_end >= CURRENT_TIMESTAMP AT TIME ZONE 'UTC' + ORDER BY qualification_start DESC + LIMIT 1; + + -- Get user_ids from send_accounts + SELECT user_id INTO from_user_id + FROM send_accounts + WHERE address = concat('0x', encode(NEW.f, 'hex'))::citext; + + SELECT user_id INTO to_user_id + FROM send_accounts + WHERE address = concat('0x', encode(NEW.t, 'hex'))::citext; + + IF curr_distribution_id IS NOT NULL AND from_user_id IS NOT NULL AND to_user_id IS NOT NULL THEN + -- Calculate streak with simplified unique recipients per day logic + WITH daily_unique_transfers AS ( + SELECT + DATE(to_timestamp(block_time) at time zone 'UTC') AS transfer_date + FROM send_token_transfers stt + WHERE f = NEW.f + AND NOT (t = ANY (ignored_addresses)) + AND block_time >= ( + SELECT extract(epoch FROM qualification_start) + FROM distributions + WHERE id = curr_distribution_id + ) + GROUP BY DATE(to_timestamp(block_time) at time zone 'UTC') + HAVING COUNT(DISTINCT t) > 0 + ), + streaks AS ( + SELECT + transfer_date, + transfer_date - (ROW_NUMBER() OVER (ORDER BY transfer_date))::integer AS streak_group + FROM daily_unique_transfers + ) + SELECT COUNT(*) INTO current_streak + FROM streaks + WHERE streak_group = ( + SELECT streak_group + FROM streaks + WHERE transfer_date = DATE(to_timestamp(NEW.block_time) at time zone 'UTC') + ); + + -- Handle send_streak verification + SELECT id INTO existing_record_id + FROM public.distribution_verifications + WHERE distribution_id = curr_distribution_id + AND user_id = from_user_id + AND type = 'send_streak'::public.verification_type; + + IF existing_record_id IS NOT NULL THEN + UPDATE public.distribution_verifications + SET weight = GREATEST(current_streak, weight), + created_at = to_timestamp(NEW.block_time) at time zone 'UTC' + WHERE id = existing_record_id; + ELSE + INSERT INTO public.distribution_verifications( + distribution_id, + user_id, + type, + created_at, + weight + ) + VALUES ( + curr_distribution_id, + from_user_id, + 'send_streak'::public.verification_type, + to_timestamp(NEW.block_time) at time zone 'UTC', + current_streak + ); + END IF; + END IF; + + RETURN NEW; +END; +$$; + +CREATE OR REPLACE FUNCTION public.insert_verification_sends() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public' +AS $function$ +BEGIN + -- Update existing verifications + UPDATE public.distribution_verifications dv + SET metadata = jsonb_build_object('value', s.unique_sends), + weight = CASE + WHEN dv.type = 'send_ten' AND s.unique_sends >= 10 THEN 1 + WHEN dv.type = 'send_one_hundred' AND s.unique_sends >= 100 THEN 1 + ELSE 0 + END, + created_at = to_timestamp(NEW.block_time) at time zone 'UTC' + FROM private.get_send_score(NEW.f) s + JOIN send_accounts sa ON sa.address = concat('0x', encode(NEW.f, 'hex'))::citext + WHERE dv.distribution_id = s.distribution_id + AND dv.user_id = sa.user_id + AND dv.type IN ('send_ten', 'send_one_hundred'); + + -- Insert new verifications if they don't exist + INSERT INTO public.distribution_verifications( + distribution_id, + user_id, + type, + metadata, + weight, + created_at + ) + SELECT + s.distribution_id, + sa.user_id, + v.type, + jsonb_build_object('value', s.unique_sends), + CASE + WHEN v.type = 'send_ten' AND s.unique_sends >= 10 THEN 1 + WHEN v.type = 'send_one_hundred' AND s.unique_sends >= 100 THEN 1 + ELSE 0 + END, + to_timestamp(NEW.block_time) at time zone 'UTC' + FROM private.get_send_score(NEW.f) s + JOIN send_accounts sa ON sa.address = concat('0x', encode(NEW.f, 'hex'))::citext + CROSS JOIN ( + VALUES + ('send_ten'::verification_type), + ('send_one_hundred'::verification_type) + ) v(type) + WHERE NOT EXISTS ( + SELECT 1 + FROM distribution_verifications dv + WHERE dv.user_id = sa.user_id + AND dv.distribution_id = s.distribution_id + AND dv.type = v.type + ); + + RETURN NEW; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION public.insert_verification_send_ceiling() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public' +AS $function$ +BEGIN + -- Exit early if value is not positive + IF NOT (NEW.v > 0) THEN + RETURN NEW; + END IF; + + -- Try to update existing verification + UPDATE distribution_verifications dv + SET + weight = s.score, + metadata = jsonb_build_object('value', s.send_ceiling::text) + FROM private.get_send_score(NEW.f) s + CROSS JOIN ( + SELECT user_id + FROM send_accounts + WHERE address = concat('0x', encode(NEW.f, 'hex'))::citext + ) sa + WHERE dv.user_id = sa.user_id + AND dv.distribution_id = s.distribution_id + AND dv.type = 'send_ceiling'; + + -- If no row was updated, insert new verification + IF NOT FOUND THEN + INSERT INTO distribution_verifications( + distribution_id, + user_id, + type, + weight, + metadata + ) + SELECT + s.distribution_id, + sa.user_id, + 'send_ceiling', + s.score, + jsonb_build_object('value', s.send_ceiling::text) + FROM private.get_send_score(NEW.f) s + CROSS JOIN ( + SELECT user_id + FROM send_accounts + WHERE address = concat('0x', encode(NEW.f, 'hex'))::citext + ) sa + WHERE s.score > 0; + END IF; + + RETURN NEW; +EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE 'Error in insert_verification_send_ceiling: %', SQLERRM; + RETURN NEW; +END; +$function$ +; + +-- Function grants for verification functions +REVOKE ALL ON FUNCTION "public"."calculate_and_insert_send_ceiling_verification"("distribution_number" integer) FROM PUBLIC; +GRANT ALL ON FUNCTION "public"."calculate_and_insert_send_ceiling_verification"("distribution_number" integer) TO "service_role"; + +REVOKE ALL ON FUNCTION "public"."insert_create_passkey_verifications"("distribution_num" integer) FROM PUBLIC; +REVOKE ALL ON FUNCTION "public"."insert_create_passkey_verifications"("distribution_num" integer) FROM authenticated; +GRANT ALL ON FUNCTION "public"."insert_create_passkey_verifications"("distribution_num" integer) TO service_role; + +REVOKE ALL ON FUNCTION "public"."insert_send_streak_verifications"("distribution_num" integer) FROM PUBLIC; +REVOKE ALL ON FUNCTION "public"."insert_send_streak_verifications"("distribution_num" integer) FROM authenticated; +GRANT ALL ON FUNCTION "public"."insert_send_streak_verifications"("distribution_num" integer) TO service_role; + +REVOKE ALL ON FUNCTION "public"."insert_send_verifications"("distribution_num" integer) FROM PUBLIC; +REVOKE ALL ON FUNCTION "public"."insert_send_verifications"("distribution_num" integer) FROM authenticated; +GRANT ALL ON FUNCTION "public"."insert_send_verifications"("distribution_num" integer) TO service_role; + +REVOKE ALL ON FUNCTION "public"."insert_tag_referral_verifications"("distribution_num" integer) FROM PUBLIC; +REVOKE ALL ON FUNCTION "public"."insert_tag_referral_verifications"("distribution_num" integer) FROM authenticated; +GRANT ALL ON FUNCTION "public"."insert_tag_referral_verifications"("distribution_num" integer) TO service_role; + +REVOKE ALL ON FUNCTION "public"."insert_tag_registration_verifications"("distribution_num" integer) FROM PUBLIC; +REVOKE ALL ON FUNCTION "public"."insert_tag_registration_verifications"("distribution_num" integer) FROM authenticated; +GRANT ALL ON FUNCTION "public"."insert_tag_registration_verifications"("distribution_num" integer) TO service_role; + +REVOKE ALL ON FUNCTION "public"."insert_total_referral_verifications"("distribution_num" integer) FROM PUBLIC; +REVOKE ALL ON FUNCTION "public"."insert_total_referral_verifications"("distribution_num" integer) FROM authenticated; +GRANT ALL ON FUNCTION "public"."insert_total_referral_verifications"("distribution_num" integer) TO service_role; + +REVOKE ALL ON FUNCTION "public"."update_referral_verifications"("distribution_id" integer, "shares" "public"."distribution_shares"[]) FROM PUBLIC; +REVOKE ALL ON FUNCTION "public"."update_referral_verifications"("distribution_id" integer, "shares" "public"."distribution_shares"[]) FROM authenticated; +GRANT ALL ON FUNCTION "public"."update_referral_verifications"("distribution_id" integer, "shares" "public"."distribution_shares"[]) TO service_role; + +REVOKE ALL ON FUNCTION "public"."insert_verification_sends"() FROM PUBLIC; +REVOKE ALL ON FUNCTION "public"."insert_verification_sends"() FROM authenticated; +GRANT ALL ON FUNCTION "public"."insert_verification_sends"() TO service_role; + +REVOKE ALL ON FUNCTION "public"."insert_verification_send_ceiling"() FROM PUBLIC; +REVOKE ALL ON FUNCTION "public"."insert_verification_send_ceiling"() FROM authenticated; +GRANT ALL ON FUNCTION "public"."insert_verification_send_ceiling"() TO service_role; diff --git a/supabase/schemas/distributions.sql b/supabase/schemas/distributions.sql index b2540fa33..da2e0b324 100644 --- a/supabase/schemas/distributions.sql +++ b/supabase/schemas/distributions.sql @@ -33,15 +33,6 @@ CREATE SEQUENCE IF NOT EXISTS "public"."distribution_shares_id_seq" ALTER TABLE "public"."distribution_shares_id_seq" OWNER TO "postgres"; -CREATE SEQUENCE IF NOT EXISTS "public"."distribution_verifications_id_seq" - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - -ALTER TABLE "public"."distribution_verifications_id_seq" OWNER TO "postgres"; -- Tables (in dependency order) CREATE TABLE IF NOT EXISTS "public"."distributions" ( @@ -87,31 +78,6 @@ CREATE TABLE IF NOT EXISTS "public"."distribution_shares" ( ALTER TABLE "public"."distribution_shares" OWNER TO "postgres"; -CREATE TABLE IF NOT EXISTS "public"."distribution_verifications" ( - "id" integer NOT NULL, - "distribution_id" integer NOT NULL, - "user_id" "uuid" NOT NULL, - "type" "public"."verification_type" NOT NULL, - "metadata" "jsonb", - "created_at" timestamp with time zone DEFAULT ("now"() AT TIME ZONE 'utc'::"text") NOT NULL, - "weight" numeric DEFAULT 1 NOT NULL -); - -ALTER TABLE "public"."distribution_verifications" OWNER TO "postgres"; - -CREATE TABLE IF NOT EXISTS "public"."distribution_verification_values" ( - "type" "public"."verification_type" NOT NULL, - "fixed_value" numeric NOT NULL, - "bips_value" bigint NOT NULL, - "distribution_id" integer NOT NULL, - "created_at" timestamp with time zone DEFAULT ("now"() AT TIME ZONE 'utc'::"text") NOT NULL, - "updated_at" timestamp with time zone DEFAULT ("now"() AT TIME ZONE 'utc'::"text") NOT NULL, - "multiplier_min" numeric(10,4) DEFAULT 1.0 NOT NULL, - "multiplier_max" numeric(10,4) DEFAULT 1.0 NOT NULL, - "multiplier_step" numeric(10,4) DEFAULT 0.0 NOT NULL -); - -ALTER TABLE "public"."distribution_verification_values" OWNER TO "postgres"; CREATE TABLE IF NOT EXISTS "public"."send_slash" ( "distribution_number" integer NOT NULL, @@ -125,11 +91,9 @@ ALTER TABLE "public"."send_slash" OWNER TO "postgres"; -- Sequence ownership and defaults ALTER SEQUENCE "public"."distributions_id_seq" OWNED BY "public"."distributions"."id"; ALTER SEQUENCE "public"."distribution_shares_id_seq" OWNED BY "public"."distribution_shares"."id"; -ALTER SEQUENCE "public"."distribution_verifications_id_seq" OWNED BY "public"."distribution_verifications"."id"; ALTER TABLE ONLY "public"."distributions" ALTER COLUMN "id" SET DEFAULT "nextval"('"public"."distributions_id_seq"'::"regclass"); ALTER TABLE ONLY "public"."distribution_shares" ALTER COLUMN "id" SET DEFAULT "nextval"('"public"."distribution_shares_id_seq"'::"regclass"); -ALTER TABLE ONLY "public"."distribution_verifications" ALTER COLUMN "id" SET DEFAULT "nextval"('"public"."distribution_verifications_id_seq"'::"regclass"); -- Primary Keys and Constraints ALTER TABLE ONLY "public"."distributions" @@ -138,11 +102,7 @@ ALTER TABLE ONLY "public"."distributions" ALTER TABLE ONLY "public"."distributions" ADD CONSTRAINT "distributions_tranche_id_key" UNIQUE ("merkle_drop_addr", "tranche_id"); -ALTER TABLE ONLY "public"."distribution_verifications" - ADD CONSTRAINT "distribution_verifications_pkey" PRIMARY KEY ("id"); -ALTER TABLE ONLY "public"."distribution_verification_values" - ADD CONSTRAINT "distribution_verification_values_pkey" PRIMARY KEY ("type", "distribution_id"); ALTER TABLE ONLY "public"."send_slash" ADD CONSTRAINT "send_slash_pkey" PRIMARY KEY ("distribution_number"); @@ -152,9 +112,6 @@ CREATE UNIQUE INDEX "distribution_shares_address_idx" ON "public"."distribution_ CREATE INDEX "distribution_shares_distribution_id_idx" ON "public"."distribution_shares" USING "btree" ("distribution_id"); CREATE UNIQUE INDEX "distribution_shares_distribution_id_index_uindex" ON "public"."distribution_shares" USING "btree" ("distribution_id", "index"); CREATE UNIQUE INDEX "distribution_shares_user_id_idx" ON "public"."distribution_shares" USING "btree" ("user_id", "distribution_id"); -CREATE INDEX "distribution_verifications_distribution_id_index" ON "public"."distribution_verifications" USING "btree" ("distribution_id"); -CREATE INDEX "distribution_verifications_user_id_index" ON "public"."distribution_verifications" USING "btree" ("user_id"); -CREATE INDEX "idx_distribution_verifications_composite" ON "public"."distribution_verifications" USING "btree" ("distribution_id", "user_id", "type"); CREATE INDEX "idx_distributions_qualification_dates" ON "public"."distributions" USING "btree" ("qualification_start", "qualification_end"); -- Foreign Keys @@ -164,116 +121,16 @@ ALTER TABLE ONLY "public"."distribution_shares" ALTER TABLE ONLY "public"."distribution_shares" ADD CONSTRAINT "distribution_shares_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; -ALTER TABLE ONLY "public"."distribution_verification_values" - ADD CONSTRAINT "distribution_verification_values_distribution_id_fkey" FOREIGN KEY ("distribution_id") REFERENCES "public"."distributions"("id") ON DELETE CASCADE; -ALTER TABLE ONLY "public"."distribution_verifications" - ADD CONSTRAINT "distribution_verification_values_fk" FOREIGN KEY ("type", "distribution_id") REFERENCES "public"."distribution_verification_values"("type", "distribution_id"); -ALTER TABLE ONLY "public"."distribution_verifications" - ADD CONSTRAINT "distribution_verifications_distribution_id_fkey" FOREIGN KEY ("distribution_id") REFERENCES "public"."distributions"("id") ON DELETE CASCADE; -ALTER TABLE ONLY "public"."distribution_verifications" - ADD CONSTRAINT "distribution_verifications_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE CASCADE; ALTER TABLE ONLY "public"."send_slash" ADD CONSTRAINT "send_slash_distribution_id_fkey" FOREIGN KEY ("distribution_id") REFERENCES "public"."distributions"("id") ON DELETE CASCADE; -CREATE OR REPLACE FUNCTION public.calculate_and_insert_send_ceiling_verification(distribution_number integer) -RETURNS void -LANGUAGE plpgsql -SECURITY DEFINER -SET search_path = public -AS $$ -BEGIN - WITH dist_scores AS ( - SELECT * FROM send_scores ss - WHERE ss.distribution_id = ( - SELECT id FROM distributions WHERE number = $1 - ) - ), - updated_rows AS ( - UPDATE distribution_verifications dv - SET - weight = ds.score, - metadata = jsonb_build_object('value', ds.send_ceiling::text) - FROM dist_scores ds - WHERE dv.user_id = ds.user_id - AND dv.distribution_id = ds.distribution_id - AND dv.type = 'send_ceiling' - RETURNING dv.user_id - ) - INSERT INTO distribution_verifications( - distribution_id, - user_id, - type, - weight, - metadata - ) - SELECT - distribution_id, - user_id, - 'send_ceiling'::public.verification_type, - score, - jsonb_build_object('value', send_ceiling::text) - FROM dist_scores ds - WHERE NOT EXISTS ( - SELECT 1 FROM updated_rows ur - WHERE ur.user_id = ds.user_id - ); -END; -$$; -ALTER FUNCTION "public"."calculate_and_insert_send_ceiling_verification"("distribution_number" integer) OWNER TO "postgres"; -CREATE OR REPLACE FUNCTION "public"."insert_create_passkey_verifications"("distribution_num" integer) RETURNS "void" - LANGUAGE "plpgsql" - AS $$ -BEGIN - INSERT INTO public.distribution_verifications( - distribution_id, - user_id, - type, - metadata, - created_at) - SELECT - ( - SELECT - id - FROM - distributions - WHERE - "number" = distribution_num - LIMIT 1) AS distribution_id, - sa.user_id, - 'create_passkey'::public.verification_type AS type, - jsonb_build_object('account_created_at', sa.created_at) AS metadata, - sa.created_at AS created_at - FROM - send_accounts sa - WHERE - sa.created_at >= ( - SELECT - qualification_start - FROM - distributions - WHERE - "number" = distribution_num - LIMIT 1 - ) - AND sa.created_at <= ( - SELECT - qualification_end - FROM - distributions - WHERE - "number" = distribution_num - LIMIT 1 - ); -END; -$$; -ALTER FUNCTION "public"."insert_create_passkey_verifications"("distribution_num" integer) OWNER TO "postgres"; CREATE OR REPLACE FUNCTION "public"."insert_send_slash"("distribution_number" integer, "scaling_divisor" integer DEFAULT NULL::integer, "minimum_sends" integer DEFAULT NULL::integer) RETURNS "void" LANGUAGE "plpgsql" @@ -303,427 +160,13 @@ $$; ALTER FUNCTION "public"."insert_send_slash"("distribution_number" integer, "scaling_divisor" integer, "minimum_sends" integer) OWNER TO "postgres"; -CREATE OR REPLACE FUNCTION "public"."insert_send_streak_verifications"("distribution_num" integer) RETURNS "void" - LANGUAGE "plpgsql" - AS $$ -BEGIN - -- Perform the entire operation within a single function - WITH distribution_info AS ( - SELECT - id, - qualification_start, - qualification_end - FROM - distributions - WHERE - "number" = distribution_num - LIMIT 1 - ), - daily_transfers AS ( - SELECT - sa.user_id, - DATE(to_timestamp(stt.block_time) AT TIME ZONE 'UTC') AS transfer_date, - COUNT(DISTINCT stt.t) AS unique_recipients - FROM - send_token_transfers stt - JOIN send_accounts sa ON sa.address = CONCAT('0x', ENCODE(stt.f, 'hex'))::CITEXT - WHERE - stt.block_time >= EXTRACT(EPOCH FROM ( - SELECT - qualification_start - FROM distribution_info)) - AND stt.block_time < EXTRACT(EPOCH FROM ( - SELECT - qualification_end - FROM distribution_info)) - GROUP BY - sa.user_id, - DATE(to_timestamp(stt.block_time) AT TIME ZONE 'UTC') - ), - streaks AS ( - SELECT - user_id, - transfer_date, - unique_recipients, - transfer_date - (ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY transfer_date))::INTEGER AS streak_group - FROM - daily_transfers - WHERE - unique_recipients > 0 - ), - max_streaks AS ( - SELECT - user_id, - MAX(streak_length) AS max_streak_length - FROM ( - SELECT - user_id, - streak_group, - COUNT(*) AS streak_length - FROM - streaks - GROUP BY - user_id, - streak_group) AS streak_lengths - GROUP BY - user_id - ) - INSERT INTO public.distribution_verifications( - distribution_id, - user_id, - type, - created_at, - weight - ) - SELECT - ( - SELECT - id - FROM - distribution_info), - ms.user_id, - 'send_streak'::public.verification_type, - (SELECT NOW() AT TIME ZONE 'UTC'), - ms.max_streak_length - FROM - max_streaks ms - WHERE - ms.max_streak_length > 0; -END; -$$; -ALTER FUNCTION "public"."insert_send_streak_verifications"("distribution_num" integer) OWNER TO "postgres"; -CREATE OR REPLACE FUNCTION public.insert_send_verifications(distribution_num integer) -RETURNS void -LANGUAGE plpgsql -AS $$ -BEGIN - INSERT INTO public.distribution_verifications( - distribution_id, - user_id, - type, - metadata, - created_at, - weight - ) - SELECT - d.id, - ss.user_id, - type, - JSONB_BUILD_OBJECT('value', ss.unique_sends), - d.qualification_end, - CASE - WHEN type = 'send_ten'::public.verification_type - AND ss.unique_sends >= 10 THEN 1 - WHEN type = 'send_one_hundred'::public.verification_type - AND ss.unique_sends >= 100 THEN 1 - ELSE 0 - END - FROM - distributions d - JOIN send_scores ss ON ss.distribution_id = d.id - CROSS JOIN ( - SELECT 'send_ten'::public.verification_type AS type - UNION ALL - SELECT 'send_one_hundred'::public.verification_type - ) types - WHERE d.number = distribution_num; -END; -$$; -ALTER FUNCTION "public"."insert_send_verifications"("distribution_num" integer) OWNER TO "postgres"; -CREATE OR REPLACE FUNCTION "public"."insert_tag_referral_verifications"("distribution_num" integer) RETURNS "void" - LANGUAGE "plpgsql" - AS $$ -DECLARE - dist_id integer; - prev_dist_id integer; - qual_start timestamp; - qual_end timestamp; -BEGIN - -- Get current distribution data once - SELECT id, qualification_start, qualification_end INTO dist_id, qual_start, qual_end - FROM distributions - WHERE "number" = distribution_num - LIMIT 1; - - -- Get previous distribution ID once - SELECT id INTO prev_dist_id - FROM distributions - WHERE "number" = distribution_num - 1 - LIMIT 1; - - -- Add month referrals to distribution_verifications - INSERT INTO public.distribution_verifications( - distribution_id, - user_id, - type, - metadata, - created_at, - weight) - SELECT - dist_id, - referrer_id, - 'tag_referral'::public.verification_type, - jsonb_build_object('referred_id', referred_id), - referrals.created_at, - CASE - WHEN EXISTS ( - SELECT 1 - FROM distribution_shares ds - WHERE ds.user_id = referrals.referred_id - AND ds.distribution_id = prev_dist_id - ) THEN 1 - ELSE 0 - END - FROM - referrals - WHERE - referrals.created_at < qual_end - AND referrals.created_at > qual_start; -END; -$$; -ALTER FUNCTION "public"."insert_tag_referral_verifications"("distribution_num" integer) OWNER TO "postgres"; -CREATE OR REPLACE FUNCTION insert_tag_registration_verifications(distribution_num integer) -RETURNS void AS $$ -BEGIN - -- Idempotent insert: avoid duplicating rows per (distribution_id, user_id, type, tag) - INSERT INTO public.distribution_verifications( - distribution_id, - user_id, - type, - metadata, - weight, - created_at - ) - SELECT - ( - SELECT id - FROM distributions - WHERE "number" = distribution_num - LIMIT 1 - ) AS distribution_id, - t.user_id, - 'tag_registration'::public.verification_type AS type, - jsonb_build_object('tag', t."name") AS metadata, - CASE - WHEN LENGTH(t.name) >= 6 THEN 1 - WHEN LENGTH(t.name) = 5 THEN 2 - WHEN LENGTH(t.name) = 4 THEN 3 -- Increase reward value of shorter tags - WHEN LENGTH(t.name) > 0 THEN 4 - ELSE 0 - END AS weight, - t.created_at AS created_at - FROM tags t - INNER JOIN tag_receipts tr ON t.name = tr.tag_name - WHERE NOT EXISTS ( - SELECT 1 - FROM public.distribution_verifications dv - WHERE dv.distribution_id = ( - SELECT id FROM distributions WHERE "number" = distribution_num LIMIT 1 - ) - AND dv.user_id = t.user_id - AND dv.type = 'tag_registration'::public.verification_type - AND dv.metadata->>'tag' = t.name - ); -END; -$$ LANGUAGE plpgsql; - -ALTER FUNCTION "public"."insert_tag_registration_verifications"("distribution_num" integer) OWNER TO "postgres"; - -CREATE OR REPLACE FUNCTION "public"."insert_total_referral_verifications"("distribution_num" integer) RETURNS "void" - LANGUAGE "plpgsql" - AS $$ -DECLARE - dist_id integer; - prev_dist_id integer; - qual_end timestamp; -BEGIN - -- Get current distribution data once - SELECT id, qualification_end INTO dist_id, qual_end - FROM distributions - WHERE "number" = distribution_num - LIMIT 1; - - -- Get previous distribution ID once - SELECT id INTO prev_dist_id - FROM distributions - WHERE "number" = distribution_num - 1 - LIMIT 1; - - -- Add total_tag_referrals to distribution_verifications - INSERT INTO public.distribution_verifications( - distribution_id, - user_id, - type, - created_at, - weight) - WITH total_referrals AS ( - SELECT - r.referrer_id, - COUNT(*) FILTER (WHERE EXISTS ( - SELECT 1 - FROM distribution_shares ds - WHERE ds.user_id = r.referred_id - AND ds.distribution_id = prev_dist_id - )) AS qualified_referrals, - MAX(r.created_at) AS last_referral_date - FROM - referrals r - WHERE - r.created_at <= qual_end - GROUP BY - r.referrer_id - ) - SELECT - dist_id AS distribution_id, - tr.referrer_id AS user_id, - 'total_tag_referrals'::public.verification_type AS type, - LEAST(tr.last_referral_date, qual_end) AS created_at, - tr.qualified_referrals AS weight - FROM - total_referrals tr - WHERE - tr.qualified_referrals > 0; -END; -$$; - -ALTER FUNCTION "public"."insert_total_referral_verifications"("distribution_num" integer) OWNER TO "postgres"; - -CREATE OR REPLACE FUNCTION "public"."insert_verification_value"("distribution_number" integer, "type" "public"."verification_type", "fixed_value" numeric DEFAULT NULL::numeric, "bips_value" integer DEFAULT NULL::integer, "multiplier_min" numeric DEFAULT NULL::numeric, "multiplier_max" numeric DEFAULT NULL::numeric, "multiplier_step" numeric DEFAULT NULL::numeric) RETURNS "void" - LANGUAGE "plpgsql" - AS $$ -DECLARE - prev_verification_values RECORD; -BEGIN - SELECT * INTO prev_verification_values - FROM public.distribution_verification_values dvv - WHERE distribution_id = (SELECT id FROM distributions WHERE "number" = insert_verification_value.distribution_number - 1 LIMIT 1) - AND dvv.type = insert_verification_value.type - LIMIT 1; - - INSERT INTO public.distribution_verification_values( - type, - fixed_value, - bips_value, - multiplier_min, - multiplier_max, - multiplier_step, - distribution_id - ) VALUES ( - insert_verification_value.type, - COALESCE(insert_verification_value.fixed_value, prev_verification_values.fixed_value, 0), - COALESCE(insert_verification_value.bips_value, prev_verification_values.bips_value, 0), - COALESCE(insert_verification_value.multiplier_min, prev_verification_values.multiplier_min), - COALESCE(insert_verification_value.multiplier_max, prev_verification_values.multiplier_max), - COALESCE(insert_verification_value.multiplier_step, prev_verification_values.multiplier_step), - (SELECT id FROM distributions WHERE "number" = insert_verification_value.distribution_number LIMIT 1) - ); -END; -$$; -ALTER FUNCTION "public"."insert_verification_value"("distribution_number" integer, "type" "public"."verification_type", "fixed_value" numeric, "bips_value" integer, "multiplier_min" numeric, "multiplier_max" numeric, "multiplier_step" numeric) OWNER TO "postgres"; - --- TODO require recipient to have send earn mininum balance -CREATE OR REPLACE FUNCTION public."insert_send_streak_verification"() -RETURNS trigger -LANGUAGE plpgsql -SECURITY DEFINER -SET search_path = public -AS $$ -DECLARE - curr_distribution_id bigint; - from_user_id uuid; - to_user_id uuid; - unique_recipient_count integer; - current_streak integer; - existing_record_id bigint; - ignored_addresses bytea[] := ARRAY['\x592e1224d203be4214b15e205f6081fbbacfcd2d'::bytea, '\x36f43082d01df4801af2d95aeed1a0200c5510ae'::bytea]; -BEGIN - -- Get the current distribution id - SELECT id INTO curr_distribution_id - FROM distributions - WHERE qualification_start <= CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - AND qualification_end >= CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - ORDER BY qualification_start DESC - LIMIT 1; - - -- Get user_ids from send_accounts - SELECT user_id INTO from_user_id - FROM send_accounts - WHERE address = concat('0x', encode(NEW.f, 'hex'))::citext; - - SELECT user_id INTO to_user_id - FROM send_accounts - WHERE address = concat('0x', encode(NEW.t, 'hex'))::citext; - - IF curr_distribution_id IS NOT NULL AND from_user_id IS NOT NULL AND to_user_id IS NOT NULL THEN - -- Calculate streak with simplified unique recipients per day logic - WITH daily_unique_transfers AS ( - SELECT - DATE(to_timestamp(block_time) at time zone 'UTC') AS transfer_date - FROM send_token_transfers stt - WHERE f = NEW.f - AND NOT (t = ANY (ignored_addresses)) - AND block_time >= ( - SELECT extract(epoch FROM qualification_start) - FROM distributions - WHERE id = curr_distribution_id - ) - GROUP BY DATE(to_timestamp(block_time) at time zone 'UTC') - HAVING COUNT(DISTINCT t) > 0 - ), - streaks AS ( - SELECT - transfer_date, - transfer_date - (ROW_NUMBER() OVER (ORDER BY transfer_date))::integer AS streak_group - FROM daily_unique_transfers - ) - SELECT COUNT(*) INTO current_streak - FROM streaks - WHERE streak_group = ( - SELECT streak_group - FROM streaks - WHERE transfer_date = DATE(to_timestamp(NEW.block_time) at time zone 'UTC') - ); - - -- Handle send_streak verification - SELECT id INTO existing_record_id - FROM public.distribution_verifications - WHERE distribution_id = curr_distribution_id - AND user_id = from_user_id - AND type = 'send_streak'::public.verification_type; - - IF existing_record_id IS NOT NULL THEN - UPDATE public.distribution_verifications - SET weight = GREATEST(current_streak, weight), - created_at = to_timestamp(NEW.block_time) at time zone 'UTC' - WHERE id = existing_record_id; - ELSE - INSERT INTO public.distribution_verifications( - distribution_id, - user_id, - type, - created_at, - weight - ) - VALUES ( - curr_distribution_id, - from_user_id, - 'send_streak'::public.verification_type, - to_timestamp(NEW.block_time) at time zone 'UTC', - current_streak - ); - END IF; - END IF; - - RETURN NEW; -END; -$$; CREATE OR REPLACE FUNCTION "public"."update_distribution_shares"("distribution_id" integer, "shares" "public"."distribution_shares"[]) RETURNS "void" LANGUAGE "plpgsql" SECURITY DEFINER @@ -796,197 +239,8 @@ $_$; ALTER FUNCTION "public"."update_distribution_shares"("distribution_id" integer, "shares" "public"."distribution_shares"[]) OWNER TO "postgres"; -CREATE OR REPLACE FUNCTION public.update_referral_verifications( - distribution_id INTEGER, - shares distribution_shares[] -) -RETURNS VOID -LANGUAGE plpgsql -SECURITY DEFINER -SET search_path TO 'public' -AS $function$ -BEGIN - -- Create temp table for shares lookup - CREATE TEMPORARY TABLE temp_shares ON COMMIT DROP AS - SELECT DISTINCT user_id - FROM unnest(shares) ds; - - -- Update tag_referral weights - just check if in shares - UPDATE distribution_verifications dv - SET weight = CASE - WHEN ts.user_id IS NOT NULL THEN 1 - ELSE 0 - END - FROM referrals r - LEFT JOIN temp_shares ts ON ts.user_id = r.referred_id - WHERE dv.distribution_id = $1 - AND dv.type = 'tag_referral' - AND dv.user_id = r.referrer_id - AND (dv.metadata->>'referred_id')::uuid = r.referred_id; - - -- Insert total_tag_referrals if doesn't exist - INSERT INTO distribution_verifications (distribution_id, user_id, type, weight) - SELECT - $1, - r.referrer_id, - 'total_tag_referrals', - COUNT(ts.user_id) - FROM referrals r - JOIN temp_shares ts ON ts.user_id = r.referred_id - WHERE NOT EXISTS ( - SELECT 1 FROM distribution_verifications dv - WHERE dv.distribution_id = $1 - AND dv.type = 'total_tag_referrals' - AND dv.user_id = r.referrer_id - ) - GROUP BY r.referrer_id; - - -- Update existing total_tag_referrals - UPDATE distribution_verifications dv - SET weight = rc.referral_count - FROM ( - SELECT - r.referrer_id, - COUNT(ts.user_id) as referral_count - FROM referrals r - JOIN temp_shares ts ON ts.user_id = r.referred_id - GROUP BY r.referrer_id - ) rc - WHERE dv.distribution_id = $1 - AND dv.type = 'total_tag_referrals' - AND dv.user_id = rc.referrer_id; - - DROP TABLE temp_shares; -END; -$function$; - -ALTER FUNCTION "public"."update_referral_verifications"("distribution_id" integer, "shares" "public"."distribution_shares"[]) OWNER TO "postgres"; - -CREATE OR REPLACE FUNCTION public.insert_verification_sends() - RETURNS trigger - LANGUAGE plpgsql - SECURITY DEFINER - SET search_path TO 'public' -AS $function$ -BEGIN - -- Update existing verifications - UPDATE public.distribution_verifications dv - SET metadata = jsonb_build_object('value', s.unique_sends), - weight = CASE - WHEN dv.type = 'send_ten' AND s.unique_sends >= 10 THEN 1 - WHEN dv.type = 'send_one_hundred' AND s.unique_sends >= 100 THEN 1 - ELSE 0 - END, - created_at = to_timestamp(NEW.block_time) at time zone 'UTC' - FROM private.get_send_score(NEW.f) s - JOIN send_accounts sa ON sa.address = concat('0x', encode(NEW.f, 'hex'))::citext - WHERE dv.distribution_id = s.distribution_id - AND dv.user_id = sa.user_id - AND dv.type IN ('send_ten', 'send_one_hundred'); - - -- Insert new verifications if they don't exist - INSERT INTO public.distribution_verifications( - distribution_id, - user_id, - type, - metadata, - weight, - created_at - ) - SELECT - s.distribution_id, - sa.user_id, - v.type, - jsonb_build_object('value', s.unique_sends), - CASE - WHEN v.type = 'send_ten' AND s.unique_sends >= 10 THEN 1 - WHEN v.type = 'send_one_hundred' AND s.unique_sends >= 100 THEN 1 - ELSE 0 - END, - to_timestamp(NEW.block_time) at time zone 'UTC' - FROM private.get_send_score(NEW.f) s - JOIN send_accounts sa ON sa.address = concat('0x', encode(NEW.f, 'hex'))::citext - CROSS JOIN ( - VALUES - ('send_ten'::verification_type), - ('send_one_hundred'::verification_type) - ) v(type) - WHERE NOT EXISTS ( - SELECT 1 - FROM distribution_verifications dv - WHERE dv.user_id = sa.user_id - AND dv.distribution_id = s.distribution_id - AND dv.type = v.type - ); - RETURN NEW; -END; -$function$ -; -ALTER FUNCTION "public"."insert_verification_sends"() OWNER TO "postgres"; - -CREATE OR REPLACE FUNCTION public.insert_verification_send_ceiling() - RETURNS trigger - LANGUAGE plpgsql - SECURITY DEFINER - SET search_path TO 'public' -AS $function$ -BEGIN - -- Exit early if value is not positive - IF NOT (NEW.v > 0) THEN - RETURN NEW; - END IF; - - -- Try to update existing verification - UPDATE distribution_verifications dv - SET - weight = s.score, - metadata = jsonb_build_object('value', s.send_ceiling::text) - FROM private.get_send_score(NEW.f) s - CROSS JOIN ( - SELECT user_id - FROM send_accounts - WHERE address = concat('0x', encode(NEW.f, 'hex'))::citext - ) sa - WHERE dv.user_id = sa.user_id - AND dv.distribution_id = s.distribution_id - AND dv.type = 'send_ceiling'; - - -- If no row was updated, insert new verification - IF NOT FOUND THEN - INSERT INTO distribution_verifications( - distribution_id, - user_id, - type, - weight, - metadata - ) - SELECT - s.distribution_id, - sa.user_id, - 'send_ceiling', - s.score, - jsonb_build_object('value', s.send_ceiling::text) - FROM private.get_send_score(NEW.f) s - CROSS JOIN ( - SELECT user_id - FROM send_accounts - WHERE address = concat('0x', encode(NEW.f, 'hex'))::citext - ) sa - WHERE s.score > 0; - END IF; - - RETURN NEW; -EXCEPTION - WHEN OTHERS THEN - RAISE NOTICE 'Error in insert_verification_send_ceiling: %', SQLERRM; - RETURN NEW; -END; -$function$ -; - -ALTER FUNCTION "public"."insert_verification_send_ceiling"() OWNER TO "postgres"; -- Grants for tables GRANT ALL ON TABLE "public"."distributions" TO "anon"; @@ -1005,17 +259,6 @@ GRANT ALL ON SEQUENCE "public"."distribution_shares_id_seq" TO "anon"; GRANT ALL ON SEQUENCE "public"."distribution_shares_id_seq" TO "authenticated"; GRANT ALL ON SEQUENCE "public"."distribution_shares_id_seq" TO "service_role"; -GRANT ALL ON TABLE "public"."distribution_verifications" TO "anon"; -GRANT ALL ON TABLE "public"."distribution_verifications" TO "authenticated"; -GRANT ALL ON TABLE "public"."distribution_verifications" TO "service_role"; - -GRANT ALL ON SEQUENCE "public"."distribution_verifications_id_seq" TO "anon"; -GRANT ALL ON SEQUENCE "public"."distribution_verifications_id_seq" TO "authenticated"; -GRANT ALL ON SEQUENCE "public"."distribution_verifications_id_seq" TO "service_role"; - -GRANT ALL ON TABLE "public"."distribution_verification_values" TO "anon"; -GRANT ALL ON TABLE "public"."distribution_verification_values" TO "authenticated"; -GRANT ALL ON TABLE "public"."distribution_verification_values" TO "service_role"; GRANT ALL ON TABLE "public"."send_slash" TO "anon"; GRANT ALL ON TABLE "public"."send_slash" TO "authenticated"; @@ -1024,20 +267,12 @@ GRANT ALL ON TABLE "public"."send_slash" TO "service_role"; -- RLS Policies -- distributions table ALTER TABLE ONLY "public"."distributions" ENABLE ROW LEVEL SECURITY; -CREATE POLICY "Enable read access to public" ON "public"."distribution_verification_values" FOR SELECT TO "authenticated" USING (true); CREATE POLICY "Enable read access to public" ON "public"."distributions" FOR SELECT TO "authenticated" USING (true); -- distribution_shares table ALTER TABLE ONLY "public"."distribution_shares" ENABLE ROW LEVEL SECURITY; CREATE POLICY "User can see own shares" ON "public"."distribution_shares" FOR SELECT USING ((( SELECT "auth"."uid"() AS "uid") = "user_id")); --- distribution_verifications table -ALTER TABLE ONLY "public"."distribution_verifications" ENABLE ROW LEVEL SECURITY; -CREATE POLICY "Users can see their own distribution verifications" ON "public"."distribution_verifications" FOR SELECT USING ((( SELECT "auth"."uid"() AS "uid") = "user_id")); - --- distribution_verification_values table -ALTER TABLE ONLY "public"."distribution_verification_values" ENABLE ROW LEVEL SECURITY; -CREATE POLICY "Authenticated users can see distribution_verification_values" ON "public"."distribution_verification_values" FOR SELECT USING ((( SELECT "auth"."uid"() AS "uid") IS NOT NULL)); -- send_slash table ALTER TABLE ONLY "public"."send_slash" ENABLE ROW LEVEL SECURITY; @@ -1052,55 +287,24 @@ $_$; ALTER FUNCTION "public"."distribution_shares"("public"."profiles") OWNER TO "postgres"; + -- Function grants -REVOKE ALL ON FUNCTION "public"."calculate_and_insert_send_ceiling_verification"("distribution_number" integer) FROM PUBLIC; -GRANT ALL ON FUNCTION "public"."calculate_and_insert_send_ceiling_verification"("distribution_number" integer) TO "service_role"; -- Revoke all public and authenticated access, grant only to service_role -- For all functions: -REVOKE ALL ON FUNCTION "public"."insert_create_passkey_verifications"("distribution_num" integer) FROM PUBLIC; -REVOKE ALL ON FUNCTION "public"."insert_create_passkey_verifications"("distribution_num" integer) FROM authenticated; -GRANT ALL ON FUNCTION "public"."insert_create_passkey_verifications"("distribution_num" integer) TO service_role; REVOKE ALL ON FUNCTION "public"."insert_send_slash"("distribution_number" integer, "scaling_divisor" integer, "minimum_sends" integer) FROM PUBLIC; REVOKE ALL ON FUNCTION "public"."insert_send_slash"("distribution_number" integer, "scaling_divisor" integer, "minimum_sends" integer) FROM authenticated; GRANT ALL ON FUNCTION "public"."insert_send_slash"("distribution_number" integer, "scaling_divisor" integer, "minimum_sends" integer) TO service_role; -REVOKE ALL ON FUNCTION "public"."insert_send_streak_verifications"("distribution_num" integer) FROM PUBLIC; -REVOKE ALL ON FUNCTION "public"."insert_send_streak_verifications"("distribution_num" integer) FROM authenticated; -GRANT ALL ON FUNCTION "public"."insert_send_streak_verifications"("distribution_num" integer) TO service_role; -REVOKE ALL ON FUNCTION "public"."insert_send_verifications"("distribution_num" integer) FROM PUBLIC; -REVOKE ALL ON FUNCTION "public"."insert_send_verifications"("distribution_num" integer) FROM authenticated; -GRANT ALL ON FUNCTION "public"."insert_send_verifications"("distribution_num" integer) TO service_role; -REVOKE ALL ON FUNCTION "public"."insert_tag_referral_verifications"("distribution_num" integer) FROM PUBLIC; -REVOKE ALL ON FUNCTION "public"."insert_tag_referral_verifications"("distribution_num" integer) FROM authenticated; -GRANT ALL ON FUNCTION "public"."insert_tag_referral_verifications"("distribution_num" integer) TO service_role; -REVOKE ALL ON FUNCTION "public"."insert_tag_registration_verifications"("distribution_num" integer) FROM PUBLIC; -REVOKE ALL ON FUNCTION "public"."insert_tag_registration_verifications"("distribution_num" integer) FROM authenticated; -GRANT ALL ON FUNCTION "public"."insert_tag_registration_verifications"("distribution_num" integer) TO service_role; -REVOKE ALL ON FUNCTION "public"."insert_total_referral_verifications"("distribution_num" integer) FROM PUBLIC; -REVOKE ALL ON FUNCTION "public"."insert_total_referral_verifications"("distribution_num" integer) FROM authenticated; -GRANT ALL ON FUNCTION "public"."insert_total_referral_verifications"("distribution_num" integer) TO service_role; -REVOKE ALL ON FUNCTION "public"."insert_verification_value"("distribution_number" integer, "type" "public"."verification_type", "fixed_value" numeric, "bips_value" integer, "multiplier_min" numeric, "multiplier_max" numeric, "multiplier_step" numeric) FROM PUBLIC; -REVOKE ALL ON FUNCTION "public"."insert_verification_value"("distribution_number" integer, "type" "public"."verification_type", "fixed_value" numeric, "bips_value" integer, "multiplier_min" numeric, "multiplier_max" numeric, "multiplier_step" numeric) FROM authenticated; -GRANT ALL ON FUNCTION "public"."insert_verification_value"("distribution_number" integer, "type" "public"."verification_type", "fixed_value" numeric, "bips_value" integer, "multiplier_min" numeric, "multiplier_max" numeric, "multiplier_step" numeric) TO service_role; -REVOKE ALL ON FUNCTION "public"."update_referral_verifications"("distribution_id" integer, "shares" "public"."distribution_shares"[]) FROM PUBLIC; -REVOKE ALL ON FUNCTION "public"."update_referral_verifications"("distribution_id" integer, "shares" "public"."distribution_shares"[]) FROM authenticated; -GRANT ALL ON FUNCTION "public"."update_referral_verifications"("distribution_id" integer, "shares" "public"."distribution_shares"[]) TO service_role; -REVOKE ALL ON FUNCTION "public"."insert_verification_sends"() FROM PUBLIC; -REVOKE ALL ON FUNCTION "public"."insert_verification_sends"() FROM authenticated; -GRANT ALL ON FUNCTION "public"."insert_verification_sends"() TO service_role; -REVOKE ALL ON FUNCTION "public"."insert_verification_send_ceiling"() FROM PUBLIC; -REVOKE ALL ON FUNCTION "public"."insert_verification_send_ceiling"() FROM authenticated; -GRANT ALL ON FUNCTION "public"."insert_verification_send_ceiling"() TO service_role; REVOKE ALL ON FUNCTION "public"."distribution_shares"("public"."profiles") FROM PUBLIC; GRANT ALL ON FUNCTION "public"."distribution_shares"("public"."profiles") TO "anon"; diff --git a/supabase/schemas/referrals.sql b/supabase/schemas/referrals.sql index 54cad1b92..ea368955c 100644 --- a/supabase/schemas/referrals.sql +++ b/supabase/schemas/referrals.sql @@ -17,15 +17,16 @@ CREATE TYPE "public"."profile_lookup_result" AS ( "main_tag_name" "text", "links_in_bio" link_in_bio[], "banner_url" "text", - "is_verified" boolean + "is_verified" boolean, + "verified_at" timestamptz ); ALTER TYPE "public"."profile_lookup_result" OWNER TO "postgres"; -- Functions CREATE OR REPLACE FUNCTION public.profile_lookup(lookup_type lookup_type_enum, identifier text) - RETURNS SETOF profile_lookup_result - LANGUAGE plpgsql - IMMUTABLE SECURITY DEFINER +RETURNS SETOF profile_lookup_result +LANGUAGE plpgsql +IMMUTABLE SECURITY DEFINER AS $function$ begin if identifier is null or identifier = '' then raise exception 'identifier cannot be null or empty'; end if; @@ -51,7 +52,7 @@ begin sa.address, sa.chain_id, case when current_setting('role')::text = 'service_role' then p.is_public - when p.is_public then true + when p.is_public then true else false end, p.send_id, ( select array_agg(t2.name::text) @@ -79,27 +80,25 @@ begin ELSE NULL END, p.banner_url::text, - CASE WHEN ds.user_id IS NOT NULL THEN true ELSE false END AS is_verified + public.verified_at(p) IS NOT NULL AS is_verified, + public.verified_at(p) AS verified_at from profiles p join auth.users a on a.id = p.id left join send_accounts sa on sa.user_id = p.id left join tags mt on mt.id = sa.main_tag_id left join send_account_tags sat on sat.send_account_id = sa.id left join tags t on t.id = sat.tag_id and t.status = 'confirmed'::tag_status - left join distribution_shares ds on ds.user_id = p.id - and ds.distribution_id = (select id from current_distribution_id) where ((lookup_type = 'sendid' and p.send_id::text = identifier) or - (lookup_type = 'tag' and t.name = identifier::citext) or - (lookup_type = 'refcode' and p.referral_code = identifier) or - (lookup_type = 'address' and sa.address = identifier) or - (p.is_public and lookup_type = 'phone' and a.phone::text = identifier)) - and (p.is_public - or ( select auth.uid() ) is not null - or current_setting('role')::text = 'service_role') + (lookup_type = 'tag' and t.name = identifier::citext) or + (lookup_type = 'refcode' and p.referral_code = identifier) or + (lookup_type = 'address' and sa.address = identifier) or + (p.is_public and lookup_type = 'phone' and a.phone::text = identifier)) + and (p.is_public + or ( select auth.uid() ) is not null + or current_setting('role')::text = 'service_role') limit 1; end; -$function$ -; +$function$; ALTER FUNCTION "public"."profile_lookup"("lookup_type" "public"."lookup_type_enum", "identifier" "text") OWNER TO "postgres"; @@ -525,27 +524,28 @@ create or replace view "public"."referrer" as WITH referrer AS ( ORDER BY r.created_at LIMIT 1 ), profile_lookup AS ( - SELECT p.id, - p.avatar_url, - p.name, - p.about, - p.refcode, - p.x_username, - p.birthday, - p.tag, - p.address, - p.chain_id, - p.is_public, - p.sendid, - p.all_tags, - p.main_tag_id, - p.main_tag_name, - p.links_in_bio, - p.banner_url, + SELECT pl.id, + pl.avatar_url, + pl.name, + pl.about, + pl.refcode, + pl.x_username, + pl.birthday, + pl.tag, + pl.address, + pl.chain_id, + pl.is_public, + pl.sendid, + pl.all_tags, + pl.main_tag_id, + pl.main_tag_name, + pl.links_in_bio, + pl.banner_url, + pl.is_verified, + pl.verified_at, referrer.send_id - FROM (profile_lookup('sendid'::lookup_type_enum, ( SELECT (referrer_1.send_id)::text AS send_id - FROM referrer referrer_1)) p(id, avatar_url, name, about, refcode, x_username, birthday, tag, address, chain_id, is_public, sendid, all_tags, main_tag_id, main_tag_name, links_in_bio, banner_url, is_verified) - JOIN referrer ON ((referrer.send_id IS NOT NULL))) + FROM profile_lookup('sendid'::lookup_type_enum, ( SELECT (referrer_1.send_id)::text AS send_id FROM referrer referrer_1)) AS pl + JOIN referrer ON ((referrer.send_id IS NOT NULL)) ) SELECT profile_lookup.id, profile_lookup.avatar_url, @@ -564,9 +564,12 @@ create or replace view "public"."referrer" as WITH referrer AS ( profile_lookup.main_tag_name, profile_lookup.links_in_bio, profile_lookup.send_id, - profile_lookup.banner_url + profile_lookup.banner_url, + profile_lookup.is_verified, + profile_lookup.verified_at FROM profile_lookup; + ALTER TABLE "public"."referrer" OWNER TO "postgres"; CREATE OR REPLACE FUNCTION public.referrer_lookup(referral_code text DEFAULT NULL::text) diff --git a/supabase/schemas/tags.sql b/supabase/schemas/tags.sql index 044678fa3..346a04dd3 100644 --- a/supabase/schemas/tags.sql +++ b/supabase/schemas/tags.sql @@ -411,34 +411,25 @@ BEGIN RAISE EXCEPTION 'offset_val must be greater than or equal to 0'; END IF; RETURN query - WITH current_distribution_id AS ( - -- Get current distribution once - SELECT id FROM distributions - WHERE qualification_start <= CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - AND qualification_end >= CURRENT_TIMESTAMP AT TIME ZONE 'UTC' - ORDER BY qualification_start DESC - LIMIT 1 - ) SELECT -- send_id matches ( SELECT - array_agg(ROW(sub.avatar_url, sub.tag_name, sub.send_id, sub.phone, sub.is_verified)::public.tag_search_result) + array_agg(ROW(sub.avatar_url, sub.tag_name, sub.send_id, sub.phone, sub.is_verified, sub.verified_at)::public.tag_search_result) FROM( SELECT p.avatar_url, t.name AS tag_name, p.send_id, NULL::text AS phone, - CASE WHEN ds.user_id IS NOT NULL THEN true ELSE false END AS is_verified + (public.verified_at(p) IS NOT NULL) AS is_verified, + public.verified_at(p) AS verified_at FROM profiles p LEFT JOIN send_accounts sa ON sa.user_id = p.id LEFT JOIN send_account_tags sat ON sat.send_account_id = sa.id LEFT JOIN tags t ON t.id = sat.tag_id AND t.status = 'confirmed' - LEFT JOIN distribution_shares ds ON ds.user_id = p.id - AND ds.distribution_id = (SELECT id FROM current_distribution_id) WHERE query SIMILAR TO '\d+' AND p.send_id::varchar LIKE '%' || query || '%' @@ -448,14 +439,15 @@ BEGIN -- tag matches ( SELECT - array_agg(ROW(sub.avatar_url, sub.tag_name, sub.send_id, sub.phone, sub.is_verified)::public.tag_search_result) + array_agg(ROW(sub.avatar_url, sub.tag_name, sub.send_id, sub.phone, sub.is_verified, sub.verified_at)::public.tag_search_result) FROM ( SELECT ranked_matches.avatar_url, ranked_matches.tag_name, ranked_matches.send_id, ranked_matches.phone, - ranked_matches.is_verified + ranked_matches.is_verified, + ranked_matches.verified_at FROM ( WITH scores AS ( -- Aggregate user send scores, summing all scores for cumulative activity @@ -471,8 +463,9 @@ BEGIN t.name AS tag_name, p.send_id, NULL::text AS phone, - CASE WHEN ds.user_id IS NOT NULL THEN true ELSE false END AS is_verified, - (t.name <-> query) AS distance, -- Trigram distance: 0=exact, higher=different + (public.verified_at(p) IS NOT NULL) AS is_verified, + public.verified_at(p) AS verified_at, + (t.name <-> query) AS distance, -- Trigram distance (kept for debugging/ties) COALESCE(scores.total_score, 0) AS send_score, -- Compute exact match flag in CTE LOWER(t.name) = LOWER(query) AS is_exact, @@ -484,8 +477,6 @@ BEGIN JOIN tags t ON t.id = sat.tag_id AND t.status = 'confirmed' LEFT JOIN scores ON scores.user_id = p.id - LEFT JOIN distribution_shares ds ON ds.user_id = p.id - AND ds.distribution_id = (SELECT id FROM current_distribution_id) WHERE -- Use ILIKE '%' only when NOT exact to avoid excluding true exact matches like 'Ethen_' LOWER(t.name) = LOWER(query) @@ -501,34 +492,23 @@ BEGIN tm.send_score, tm.is_exact, tm.primary_rank, - ( - -- Secondary ranking varies by match type: - -- For exact matches (primary_rank=0): use negative send_score (higher score = better/lower secondary rank) - -- For fuzzy matches (primary_rank=1): use old trigram + send_score formula - CASE - WHEN tm.is_exact THEN - -tm.send_score -- Negative for DESC ordering within exact matches - ELSE - -- Old fuzzy ranking formula: distance - (send_score / 1M) - CASE WHEN tm.distance IS NULL THEN 0 ELSE tm.distance END - - (tm.send_score / 1000000.0) - END - ) AS secondary_rank, - ROW_NUMBER() OVER (PARTITION BY tm.send_id ORDER BY ( - -- Deduplication uses same ranking logic as main ordering - tm.primary_rank, -- Primary: exact vs fuzzy - CASE - WHEN tm.is_exact THEN - -tm.send_score -- Secondary: send_score DESC for exact - ELSE - CASE WHEN tm.distance IS NULL THEN 0 ELSE tm.distance END - - (tm.send_score / 1000000.0) -- Secondary: old formula for fuzzy - END - )) AS rn + -- Verification bucket for ranking within fuzzy matches: 0 verified, 1 unverified. Exact bucket unaffected. + CASE WHEN tm.is_exact THEN 1 ELSE CASE WHEN tm.is_verified THEN 0 ELSE 1 END END AS verification_rank, + -- Higher score should sort earlier -> negative for ascending order + -tm.send_score AS score_rank, + ROW_NUMBER() OVER ( + PARTITION BY tm.send_id + ORDER BY + tm.primary_rank, + CASE WHEN tm.is_exact THEN 1 ELSE CASE WHEN tm.is_verified THEN 0 ELSE 1 END END, + -tm.send_score + ) AS rn FROM tag_matches tm ) ranked_matches WHERE ranked_matches.rn = 1 - ORDER BY ranked_matches.primary_rank ASC, ranked_matches.secondary_rank ASC + ORDER BY ranked_matches.primary_rank ASC, + ranked_matches.verification_rank ASC, + ranked_matches.score_rank ASC LIMIT limit_val OFFSET offset_val ) sub ) AS tag_matches, diff --git a/supabase/schemas/types.sql b/supabase/schemas/types.sql index 9cb8ac14f..8a307e165 100644 --- a/supabase/schemas/types.sql +++ b/supabase/schemas/types.sql @@ -36,7 +36,8 @@ CREATE TYPE "public"."tag_search_result" AS ( "tag_name" "text", "send_id" integer, "phone" "text", - "is_verified" boolean + "is_verified" boolean, + "verified_at" timestamptz ); ALTER TYPE "public"."tag_search_result" OWNER TO "postgres"; diff --git a/supabase/tests/verification_status_test.sql b/supabase/tests/verification_status_test.sql new file mode 100644 index 000000000..799703929 --- /dev/null +++ b/supabase/tests/verification_status_test.sql @@ -0,0 +1,143 @@ +BEGIN; +SELECT plan(7); +CREATE EXTENSION IF NOT EXISTS "basejump-supabase_test_helpers"; + +-- Create a user +SELECT tests.create_supabase_user('verif_user'); +SELECT tests.authenticate_as_service_role(); + +-- Create an active test distribution with no earn requirement (earn_min_balance = 0) +DO $$ +DECLARE + v_now timestamptz := (now() AT TIME ZONE 'UTC'); +BEGIN + INSERT INTO public.distributions( + number, + amount, + hodler_pool_bips, + bonus_pool_bips, + fixed_pool_bips, + name, + description, + qualification_start, + qualification_end, + claim_end, + hodler_min_balance, + chain_id, + tranche_id, + earn_min_balance + ) VALUES ( + 990001, + 1000000, + 1000, + 0, + 0, + 'verification test dist', + 'active test dist', + v_now - interval '1 hour', + v_now + interval '1 hour', + v_now + interval '7 days', + 1, + 1, + 990001, + 0 + ); +END $$; + +-- Seed verification value rows for the two types we will use (required by FK) +SELECT public.insert_verification_value(990001, 'tag_registration', 0, 0, 1, 1, 0); +SELECT public.insert_verification_value(990001, 'send_token_hodler', 0, 0, 1, 1, 0); + +-- Helper: fetch active distribution id +CREATE TEMP TABLE __active_dist AS +SELECT id FROM distributions +WHERE qualification_start <= (now() AT TIME ZONE 'UTC') + AND qualification_end >= (now() AT TIME ZONE 'UTC') +ORDER BY qualification_start DESC +LIMIT 1; + +-- Case 1: No verifications => verified_at is NULL, is_verified false +SELECT results_eq( + $$ +SELECT public.verified_at(p) IS NULL, public.is_verified(p) + FROM public.profiles p + WHERE p.id = tests.get_supabase_uid('verif_user') + $$, + $$ VALUES (true, false) $$, + 'No verifications -> not verified' +); + +-- Case 2: Only tag_registration => still not verified +INSERT INTO public.distribution_verifications(distribution_id, user_id, type, metadata, created_at, weight) +SELECT id, tests.get_supabase_uid('verif_user'), 'tag_registration', jsonb_build_object('tag','x'), + (now() AT TIME ZONE 'UTC') - interval '30 min', 1 +FROM __active_dist; + +SELECT results_eq( + $$ +SELECT public.verified_at(p) IS NULL, public.is_verified(p) + FROM public.profiles p + WHERE p.id = tests.get_supabase_uid('verif_user') + $$, + $$ VALUES (true, false) $$, + 'Only tag_registration -> not verified' +); + +-- Case 3: Only hodler => still not verified +DELETE FROM public.distribution_verifications +WHERE user_id = tests.get_supabase_uid('verif_user'); + +INSERT INTO public.distribution_verifications(distribution_id, user_id, type, metadata, created_at, weight) +SELECT id, tests.get_supabase_uid('verif_user'), 'send_token_hodler', NULL, + (now() AT TIME ZONE 'UTC') - interval '20 min', 1 +FROM __active_dist; + +SELECT results_eq( + $$ +SELECT public.verified_at(p) IS NULL, public.is_verified(p) + FROM public.profiles p + WHERE p.id = tests.get_supabase_uid('verif_user') + $$, + $$ VALUES (true, false) $$, + 'Only hodler -> not verified' +); + +-- Case 4: Both tag + hodler (earn requirement is bypassed because earn_min_balance=0) +-- Re-insert tag; keep hodler present +INSERT INTO public.distribution_verifications(distribution_id, user_id, type, metadata, created_at, weight) +SELECT id, tests.get_supabase_uid('verif_user'), 'tag_registration', jsonb_build_object('tag','x'), + (now() AT TIME ZONE 'UTC') - interval '25 min', 1 +FROM __active_dist; + +-- Expect verified_at NOT NULL and is_verified true +SELECT results_eq( + $$ +SELECT (public.verified_at(p) IS NOT NULL), public.is_verified(p) + FROM public.profiles p + WHERE p.id = tests.get_supabase_uid('verif_user') + $$, + $$ VALUES (true, true) $$, + 'Tag + hodler -> verified' +); + +-- Case 5: Losing one condition should return NULL again +-- Remove hodler row +DELETE FROM public.distribution_verifications +WHERE user_id = tests.get_supabase_uid('verif_user') + AND type = 'send_token_hodler'; + +SELECT results_eq( + $$ +SELECT public.verified_at(p) IS NULL, public.is_verified(p) + FROM public.profiles p + WHERE p.id = tests.get_supabase_uid('verif_user') + $$, + $$ VALUES (true, false) $$, + 'Lost hodler -> not verified' +); + +-- Clean up temp +DROP TABLE __active_dist; + +SELECT * FROM finish(); +ROLLBACK;