From 5b5e0cf06995a895931078e38b9022f9d3af9bf4 Mon Sep 17 00:00:00 2001 From: Juan Date: Tue, 17 Dec 2024 17:11:15 -0300 Subject: [PATCH 01/12] Rename drops collections --- src/hooks/useEventsOwnersAndMetrics.ts | 6 +++--- src/loaders/collector.ts | 2 +- src/loaders/event.ts | 4 ++-- src/pages/Addresses.tsx | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/hooks/useEventsOwnersAndMetrics.ts b/src/hooks/useEventsOwnersAndMetrics.ts index 69a4102..091c696 100644 --- a/src/hooks/useEventsOwnersAndMetrics.ts +++ b/src/hooks/useEventsOwnersAndMetrics.ts @@ -2,7 +2,7 @@ import { useCallback, useState } from 'react' import { filterInvalidOwners } from 'models/address' import { AbortedError } from 'models/error' import { EventAndOwners, InCommon } from 'models/api' -import { fetchDropCollectors } from 'loaders/collector' +import { fetchDropsCollectors } from 'loaders/collector' import { getEventAndOwners, getEventMetrics, @@ -156,11 +156,11 @@ function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record { removeError(eventId) addLoading(eventId) - let eventCollectorsResult: PromiseSettledResult>> + let eventCollectorsResult: PromiseSettledResult>> let eventMetricsResult: PromiseSettledResult>> try { [eventCollectorsResult, eventMetricsResult] = await Promise.allSettled([ - fetchDropCollectors([eventId], abortSignal), + fetchDropsCollectors([eventId], abortSignal), getEventMetrics(eventId, abortSignal, force), ]) } catch (err: unknown) { diff --git a/src/loaders/collector.ts b/src/loaders/collector.ts index 624d849..1714252 100644 --- a/src/loaders/collector.ts +++ b/src/loaders/collector.ts @@ -6,7 +6,7 @@ import { DEFAULT_POAP_LIMIT, parsePOAP, POAP } from 'models/poap' import { Drop } from 'models/drop' import { queryAllCompass } from 'loaders/compass' -export async function fetchDropCollectors( +export async function fetchDropsCollectors( dropIds: number[], abortSignal?: AbortSignal, limit = Math.min(DEFAULT_COLLECTOR_LIMIT, DEFAULT_COMPASS_LIMIT), diff --git a/src/loaders/event.ts b/src/loaders/event.ts index be9ccef..842bcf5 100644 --- a/src/loaders/event.ts +++ b/src/loaders/event.ts @@ -3,7 +3,7 @@ import { Drop } from 'models/drop' import { HttpError } from 'models/error' import { getEventAndOwners, getEventMetrics, getEvents } from 'loaders/api' import { fetchDrop, fetchDropsOrErrors } from 'loaders/drop' -import { fetchDropCollectors } from 'loaders/collector' +import { fetchDropsCollectors } from 'loaders/collector' export async function eventLoader({ params, request }) { const force = new URL(request.url).searchParams.get('force') === 'true' @@ -38,7 +38,7 @@ export async function eventLoader({ params, request }) { } const [collectorsSettled, metricsSettled] = await Promise.allSettled([ - fetchDropCollectors([params.eventId]), + fetchDropsCollectors([params.eventId]), getEventMetrics(params.eventId, null, /*refresh*/force), ]) diff --git a/src/pages/Addresses.tsx b/src/pages/Addresses.tsx index 2a5a811..0bc5b06 100644 --- a/src/pages/Addresses.tsx +++ b/src/pages/Addresses.tsx @@ -10,7 +10,7 @@ import { EnsByAddress } from 'models/ethereum' import { HTMLContext } from 'stores/html' import { ResolverEnsContext, ReverseEnsContext } from 'stores/ethereum' import { getEventsOwners } from 'loaders/api' -import { fetchCollectorDrops, fetchDropCollectors } from 'loaders/collector' +import { fetchCollectorDrops, fetchDropsCollectors } from 'loaders/collector' import AddressesForm from 'components/AddressesForm' import Card from 'components/Card' import CenterPage from 'components/CenterPage' @@ -321,7 +321,7 @@ function Addresses() { setLoadingEventsOwners(true) if (force) { const controller = new AbortController() - fetchDropCollectors(searchEvents, controller.signal).then( + fetchDropsCollectors(searchEvents, controller.signal).then( (collectors) => { let addresses: ParsedAddress[] | undefined try { From 9c44edd4507e178c45ff0242fcfdcabffebf3308 Mon Sep 17 00:00:00 2001 From: Juan Date: Tue, 17 Dec 2024 17:11:43 -0300 Subject: [PATCH 02/12] Query --- src/loaders/collector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loaders/collector.ts b/src/loaders/collector.ts index 1714252..7c0dfb5 100644 --- a/src/loaders/collector.ts +++ b/src/loaders/collector.ts @@ -15,7 +15,7 @@ export async function fetchDropsCollectors( `poaps`, parseCollector, ` - query FetchDropCollectors( + query FetchDropsCollectors( $dropIds: [bigint!] $offset: Int! $limit: Int! From f94b12b3b5391be90fb2b4f7a9b7bc6d042b1a70 Mon Sep 17 00:00:00 2001 From: Juan Date: Wed, 18 Dec 2024 11:25:34 -0300 Subject: [PATCH 03/12] Fetch drop metrics --- src/loaders/drop.ts | 36 ++++++++++++++- src/loaders/event.ts | 27 +++++++---- src/models/drop.ts | 104 ++++++++++++++++++++++++++++++++++--------- 3 files changed, 136 insertions(+), 31 deletions(-) diff --git a/src/loaders/drop.ts b/src/loaders/drop.ts index f441cb9..6fac715 100644 --- a/src/loaders/drop.ts +++ b/src/loaders/drop.ts @@ -1,9 +1,10 @@ import { DEFAULT_COMPASS_LIMIT } from 'models/compass' import { DEFAULT_DROP_LIMIT, DEFAULT_SEARCH_LIMIT } from 'models/event' -import { Drop, parseDrop } from 'models/drop' +import { Drop, DropMetrics, parseDrop, parseDropMetrics } from 'models/drop' import { HttpError } from 'models/error' import { queryAggregateCountCompass, + queryCompass, queryFirstCompass, queryManyCompass, } from 'loaders/compass' @@ -200,3 +201,36 @@ export async function fetchDrop( throw err } } + +export async function fetchDropMetrics( + dropId: number, + abortSignal?: AbortSignal, +): Promise { + return await queryCompass( + 'drops_by_pk', + parseDropMetrics, + ` + query DropMetrics($dropId: Int!) { + drops_by_pk(id: $dropId) { + email_claims_stats { + minted + reserved + total + } + moments_stats { + moments_uploaded + } + collections_items_aggregate { + aggregate { + count(columns: collection_id, distinct: true) + } + } + } + } + `, + { + dropId, + }, + abortSignal, + ) +} diff --git a/src/loaders/event.ts b/src/loaders/event.ts index 842bcf5..a7c4035 100644 --- a/src/loaders/event.ts +++ b/src/loaders/event.ts @@ -1,16 +1,24 @@ import { parseEventIds } from 'models/event' import { Drop } from 'models/drop' import { HttpError } from 'models/error' -import { getEventAndOwners, getEventMetrics, getEvents } from 'loaders/api' -import { fetchDrop, fetchDropsOrErrors } from 'loaders/drop' +import { getEventAndOwners, getEvents } from 'loaders/api' +import { fetchDrop, fetchDropMetrics, fetchDropsOrErrors } from 'loaders/drop' import { fetchDropsCollectors } from 'loaders/collector' export async function eventLoader({ params, request }) { const force = new URL(request.url).searchParams.get('force') === 'true' + const dropId = parseInt(String(params.eventId)) + + if (isNaN(dropId)) { + throw new Response('', { + status: 400, + statusText: 'Invalid drop id', + }) + } try { const eventAndOwners = await getEventAndOwners( - params.eventId, + dropId, /*abortSignal*/undefined, /*includeDescription*/true, /*includeMetrics*/true, @@ -28,7 +36,7 @@ export async function eventLoader({ params, request }) { console.error(err) } - const event = await fetchDrop(params.eventId, /*includeDescription*/true) + const event = await fetchDrop(dropId, /*includeDescription*/true) if (!event) { throw new Response('', { @@ -38,8 +46,8 @@ export async function eventLoader({ params, request }) { } const [collectorsSettled, metricsSettled] = await Promise.allSettled([ - fetchDropsCollectors([params.eventId]), - getEventMetrics(params.eventId, null, /*refresh*/force), + fetchDropsCollectors([dropId]), + fetchDropMetrics(dropId, /*abortSignal*/undefined), ]) if (collectorsSettled.status === 'rejected') { @@ -51,14 +59,15 @@ export async function eventLoader({ params, request }) { } const owners = collectorsSettled.value + const metrics = metricsSettled.status === 'fulfilled' + ? metricsSettled.value + : null return { event, owners, ts: null, - metrics: metricsSettled.status === 'fulfilled' - ? metricsSettled.value - : null, + metrics, } } diff --git a/src/models/drop.ts b/src/models/drop.ts index 8464656..b9384c2 100644 --- a/src/models/drop.ts +++ b/src/models/drop.ts @@ -140,30 +140,92 @@ export function parseDropMetrics(eventMetrics: unknown): DropMetrics | null { return null } if ( - typeof eventMetrics !== 'object' || - !('emailReservations' in eventMetrics) || - typeof eventMetrics.emailReservations !== 'number' || - !('emailClaimsMinted' in eventMetrics) || - typeof eventMetrics.emailClaimsMinted !== 'number' || - !('emailClaims' in eventMetrics) || - typeof eventMetrics.emailClaims !== 'number' || - !('momentsUploaded' in eventMetrics) || - typeof eventMetrics.momentsUploaded !== 'number' || - !('collectionsIncludes' in eventMetrics) || - typeof eventMetrics.collectionsIncludes !== 'number' || - !('ts' in eventMetrics) || - typeof eventMetrics.ts !== 'number' + typeof eventMetrics === 'object' && + eventMetrics != null && + 'emailReservations' in eventMetrics && + eventMetrics.emailReservations != null && + typeof eventMetrics.emailReservations === 'number' && + 'emailClaimsMinted' in eventMetrics && + eventMetrics.emailClaimsMinted != null && + typeof eventMetrics.emailClaimsMinted === 'number' && + 'emailClaims' in eventMetrics && + eventMetrics.emailClaims != null && + typeof eventMetrics.emailClaims === 'number' && + 'momentsUploaded' in eventMetrics && + eventMetrics.momentsUploaded != null && + typeof eventMetrics.momentsUploaded === 'number' && + 'collectionsIncludes' in eventMetrics && + eventMetrics.collectionsIncludes != null && + typeof eventMetrics.collectionsIncludes === 'number' && + 'ts' in eventMetrics && + eventMetrics.ts != null && + typeof eventMetrics.ts === 'number' ) { - throw new Error('Malformed drop metrics') + return { + emailReservations: eventMetrics.emailReservations, + emailClaimsMinted: eventMetrics.emailClaimsMinted, + emailClaims: eventMetrics.emailClaims, + momentsUploaded: eventMetrics.momentsUploaded, + collectionsIncludes: eventMetrics.collectionsIncludes, + ts: eventMetrics.ts, + } } - return { - emailReservations: eventMetrics.emailReservations, - emailClaimsMinted: eventMetrics.emailClaimsMinted, - emailClaims: eventMetrics.emailClaims, - momentsUploaded: eventMetrics.momentsUploaded, - collectionsIncludes: eventMetrics.collectionsIncludes, - ts: eventMetrics.ts, + if ( + typeof eventMetrics === 'object' && + eventMetrics != null && + 'email_claims_stats' in eventMetrics && + 'moments_stats' in eventMetrics && + 'collections_items_aggregate' in eventMetrics && + eventMetrics.collections_items_aggregate != null && + typeof eventMetrics.collections_items_aggregate === 'object' && + 'aggregate' in eventMetrics.collections_items_aggregate && + eventMetrics.collections_items_aggregate.aggregate != null && + typeof eventMetrics.collections_items_aggregate.aggregate === 'object' && + 'count' in eventMetrics.collections_items_aggregate.aggregate && + eventMetrics.collections_items_aggregate.aggregate.count != null && + typeof eventMetrics.collections_items_aggregate.aggregate.count === 'number' + ) { + let emailReservations = 0; + let emailClaimsMinted = 0; + let emailClaims = 0; + if ( + eventMetrics.email_claims_stats != null && + typeof eventMetrics.email_claims_stats === 'object' && + 'minted' in eventMetrics.email_claims_stats && + eventMetrics.email_claims_stats.minted != null && + typeof eventMetrics.email_claims_stats.minted === 'number' && + 'reserved' in eventMetrics.email_claims_stats && + eventMetrics.email_claims_stats.reserved != null && + typeof eventMetrics.email_claims_stats.reserved === 'number' && + 'total' in eventMetrics.email_claims_stats && + eventMetrics.email_claims_stats.total != null && + typeof eventMetrics.email_claims_stats.total === 'number' + ) { + emailReservations = eventMetrics.email_claims_stats.reserved; + emailClaimsMinted = eventMetrics.email_claims_stats.minted; + emailClaims = eventMetrics.email_claims_stats.total; + } + let momentsUploaded = 0; + if ( + eventMetrics.moments_stats != null && + typeof eventMetrics.moments_stats === 'object' && + 'moments_uploaded' in eventMetrics.moments_stats && + eventMetrics.moments_stats.moments_uploaded != null && + typeof eventMetrics.moments_stats.moments_uploaded === 'number' + ) { + momentsUploaded = eventMetrics.moments_stats.moments_uploaded; + } + return { + emailReservations, + emailClaimsMinted, + emailClaims, + momentsUploaded, + collectionsIncludes: + eventMetrics.collections_items_aggregate.aggregate.count, + ts: Date.now(), + } } + throw new Error('Malformed drop metrics') } export function parseDropData( From 7573c6f3ec37f958e50b1d9780a1a1dc0c01335d Mon Sep 17 00:00:00 2001 From: Juan Date: Wed, 18 Dec 2024 11:27:11 -0300 Subject: [PATCH 04/12] Make cached time null when loading from Compass --- src/models/drop.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/drop.ts b/src/models/drop.ts index b9384c2..d8c3667 100644 --- a/src/models/drop.ts +++ b/src/models/drop.ts @@ -132,7 +132,7 @@ export interface DropMetrics { emailClaims: number momentsUploaded: number collectionsIncludes: number - ts: number + ts: number | null } export function parseDropMetrics(eventMetrics: unknown): DropMetrics | null { @@ -222,7 +222,7 @@ export function parseDropMetrics(eventMetrics: unknown): DropMetrics | null { momentsUploaded, collectionsIncludes: eventMetrics.collections_items_aggregate.aggregate.count, - ts: Date.now(), + ts: null, } } throw new Error('Malformed drop metrics') From a582beecb1bd73b63bf71e51417c2189a6e192ae Mon Sep 17 00:00:00 2001 From: Juan Date: Wed, 18 Dec 2024 11:31:11 -0300 Subject: [PATCH 05/12] Use it in hooks --- src/hooks/useEventsOwnersAndMetrics.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/hooks/useEventsOwnersAndMetrics.ts b/src/hooks/useEventsOwnersAndMetrics.ts index 091c696..102997f 100644 --- a/src/hooks/useEventsOwnersAndMetrics.ts +++ b/src/hooks/useEventsOwnersAndMetrics.ts @@ -1,11 +1,12 @@ import { useCallback, useState } from 'react' import { filterInvalidOwners } from 'models/address' +import { DropMetrics } from 'models/drop' import { AbortedError } from 'models/error' import { EventAndOwners, InCommon } from 'models/api' import { fetchDropsCollectors } from 'loaders/collector' +import { fetchDropMetrics } from 'loaders/drop' import { getEventAndOwners, - getEventMetrics, getEventsMetrics, getEventsOwners, } from 'loaders/api' @@ -16,7 +17,7 @@ function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record eventsOwnersAndMetricsErrors: Record eventsOwners: InCommon - eventsMetrics: Record + eventsMetrics: Record fetchEventsOwnersAndMetrics: () => () => void retryEventOwnersAndMetrics: (eventId: number) => () => void } { @@ -25,7 +26,7 @@ function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record>({}) const [errors, setErrors] = useState>({}) const [owners, setOwners] = useState({}) - const [metrics, setMetrics] = useState>({}) + const [metrics, setMetrics] = useState>({}) function addLoading(eventId: number): void { setLoading((alsoLoading) => ({ @@ -157,11 +158,11 @@ function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record>> - let eventMetricsResult: PromiseSettledResult>> + let eventMetricsResult: PromiseSettledResult>> try { [eventCollectorsResult, eventMetricsResult] = await Promise.allSettled([ fetchDropsCollectors([eventId], abortSignal), - getEventMetrics(eventId, abortSignal, force), + fetchDropMetrics(eventId, abortSignal), ]) } catch (err: unknown) { removeLoading(eventId) @@ -202,7 +203,7 @@ function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record Date: Wed, 18 Dec 2024 11:31:48 -0300 Subject: [PATCH 06/12] Remove API loader --- src/loaders/api.ts | 59 ---------------------------------------------- 1 file changed, 59 deletions(-) diff --git a/src/loaders/api.ts b/src/loaders/api.ts index b430675..b2f2a2a 100644 --- a/src/loaders/api.ts +++ b/src/loaders/api.ts @@ -844,65 +844,6 @@ export async function getEventsOwners( ) } -export async function getEventMetrics( - eventId: number, - abortSignal?: AbortSignal, - refresh: boolean = false, -): Promise { - if (!FAMILY_API_KEY) { - throw new Error( - `Drop ${eventId} metrics could not be fetched, ` + - `configure Family API key` - ) - } - - let response: Response - - try { - response = await fetch( - `${FAMILY_API_URL}/event/${eventId}` + - `/metrics${refresh ? '?refresh=true' : ''}`, - { - signal: abortSignal instanceof AbortSignal ? abortSignal : null, - headers: { - 'x-api-key': FAMILY_API_KEY, - }, - } - ) - } catch (err: unknown) { - if (err instanceof Error && err.name === 'AbortError') { - throw new AbortedError( - `Fetch drop ${eventId} metrics aborted`, - { cause: err } - ) - } - throw new Error( - `Cannot fetch drop ${eventId} metrics: ` + - `response was not success (network error)`, - { cause: err } - ) - } - - if (response.status === 404) { - return null - } - - if (response.status !== 200) { - throw new HttpError( - `Drop ${eventId} failed to fetch metrics (status ${response.status})`, - { status: response.status } - ) - } - - const body: unknown = await response.json() - - if (body == null || typeof body !== 'object') { - throw new Error(`Malformed drops metrics (type ${typeof body})`) - } - - return parseDropMetrics(body) -} - export async function getEventsMetrics( eventIds: number[], abortSignal?: AbortSignal, From e5110284aba82276e84b59e25d529a93b74480b1 Mon Sep 17 00:00:00 2001 From: Juan Date: Wed, 18 Dec 2024 12:05:29 -0300 Subject: [PATCH 07/12] Fetch drops metrics --- src/hooks/useEventsOwnersAndMetrics.ts | 14 ++-- src/loaders/api.ts | 91 ++------------------------ src/loaders/drop.ts | 76 +++++++++++++++++++++ 3 files changed, 88 insertions(+), 93 deletions(-) diff --git a/src/hooks/useEventsOwnersAndMetrics.ts b/src/hooks/useEventsOwnersAndMetrics.ts index 102997f..5697357 100644 --- a/src/hooks/useEventsOwnersAndMetrics.ts +++ b/src/hooks/useEventsOwnersAndMetrics.ts @@ -4,12 +4,8 @@ import { DropMetrics } from 'models/drop' import { AbortedError } from 'models/error' import { EventAndOwners, InCommon } from 'models/api' import { fetchDropsCollectors } from 'loaders/collector' -import { fetchDropMetrics } from 'loaders/drop' -import { - getEventAndOwners, - getEventsMetrics, - getEventsOwners, -} from 'loaders/api' +import { fetchDropMetrics, fetchDropsMetrics } from 'loaders/drop' +import { getEventAndOwners, getEventsOwners } from 'loaders/api' function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record, force: boolean = false): { completedEventsOwnersAndMetrics: boolean @@ -95,7 +91,7 @@ function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record): void { + function updateEventsMetrics(eventsMetrics: Record): void { setMetrics((prevMetrics) => ({ ...prevMetrics, ...Object.fromEntries( @@ -237,7 +233,7 @@ function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record { updateEventsOwners( Object.fromEntries( diff --git a/src/loaders/api.ts b/src/loaders/api.ts index b2f2a2a..7b56db5 100644 --- a/src/loaders/api.ts +++ b/src/loaders/api.ts @@ -10,7 +10,13 @@ import { EventAndOwners, } from 'models/api' import { encodeExpiryDates } from 'models/event' -import { parseDrop, parseDropMetrics, parseDropOwners, Drop, DropMetrics, DropOwners } from 'models/drop' +import { + parseDrop, + parseDropMetrics, + parseDropOwners, + Drop, + DropOwners, +} from 'models/drop' import { AbortedError, HttpError } from 'models/error' import { DownloadProgress } from 'models/http' @@ -844,89 +850,6 @@ export async function getEventsOwners( ) } -export async function getEventsMetrics( - eventIds: number[], - abortSignal?: AbortSignal, - expiryDates?: Record, -): Promise> { - if (!FAMILY_API_KEY) { - throw new Error( - `Drops (${eventIds.length}) metrics could not be fetched, ` + - `configure Family API key` - ) - } - - const encodedExpiryDates = expiryDates ? encodeExpiryDates(expiryDates) : '' - - let response: Response - - try { - response = await fetch( - `${FAMILY_API_URL}/events` + - `/${eventIds.map((eventId) => encodeURIComponent(eventId)).join(',')}` + - `/metrics${encodedExpiryDates ? `?${encodedExpiryDates}` : ''}`, - { - signal: abortSignal instanceof AbortSignal ? abortSignal : null, - headers: { - 'x-api-key': FAMILY_API_KEY, - }, - } - ) - } catch (err: unknown) { - if (err instanceof Error && err.name === 'AbortError') { - throw new AbortedError(`Fetch drops metrics aborted`, { cause: err }) - } - throw new Error( - `Cannot fetch drops metrics: response was not success (network error)`, - { cause: err } - ) - } - - if (response.status !== 200) { - let message: string = 'Unknown error' - try { - const data = await response.json() - if ( - data != null && - typeof data === 'object' && - 'message' in data && - data.message != null && - typeof data.message === 'string' - ) { - message = data.message - } - } catch (err: unknown) { - console.error(err) - } - - if (message) { - throw new HttpError( - `Drops (${eventIds.length}) failed to fetch metrics ` + - `(status ${response.status}): ${message}`, - { status: response.status } - ) - } - - throw new HttpError( - `Drops (${eventIds.length}) failed to fetch metrics ` + - `(status ${response.status})`, - { status: response.status } - ) - } - - const body: unknown = await response.json() - - if (body == null || typeof body !== 'object') { - throw new Error(`Malformed drops metrics (type ${typeof body})`) - } - - return Object.fromEntries( - Object.entries(body).map( - ([eventId, event]) => [eventId, parseDropMetrics(event)] - ) - ) -} - export async function auth(passphrase: string): Promise { const response = await fetch(`${FAMILY_API_URL}/auth`, { method: 'POST', diff --git a/src/loaders/drop.ts b/src/loaders/drop.ts index 6fac715..6f93265 100644 --- a/src/loaders/drop.ts +++ b/src/loaders/drop.ts @@ -234,3 +234,79 @@ export async function fetchDropMetrics( abortSignal, ) } + +export async function fetchDropsMetrics( + dropIds: number[], + abortSignal?: AbortSignal, + limit: number = Math.min(DEFAULT_DROP_LIMIT, DEFAULT_COMPASS_LIMIT), +): Promise> { + const dropsMetrics: Record = {} + + for (let i = 0; i < dropIds.length; i += limit) { + const ids = dropIds.slice(i, i + limit) + + if (ids.length === 0) { + break + } + + const drops = await queryManyCompass( + 'drops', + (data: unknown): DropMetrics & { id: number } => { + if ( + data == null || + typeof data !== 'object' || + !('id' in data) || + data.id == null || + typeof data.id !== 'number' + ) { + throw new Error('Invalid drop id') + } + + return { + ...parseDropMetrics(data), + id: data.id, + } + }, + ` + query DropsMetrics($dropIds: [Int!]) { + drops( + where: { + id: { _in: $dropIds } + } + ) { + email_claims_stats { + minted + reserved + total + } + moments_stats { + moments_uploaded + } + collections_items_aggregate { + aggregate { + count(columns: collection_id, distinct: true) + } + } + id + } + } + `, + { + dropIds: ids, + }, + ) + + for (const drop of drops) { + dropsMetrics[drop.id] = { + emailReservations: drop.emailReservations, + emailClaimsMinted: drop.emailClaimsMinted, + emailClaims: drop.emailClaims, + momentsUploaded: drop.momentsUploaded, + collectionsIncludes: drop.collectionsIncludes, + ts: drop.ts, + } + } + } + + return dropsMetrics +} From 6cd4cb536b5c41787d179d799c9624c749ba6065 Mon Sep 17 00:00:00 2001 From: Juan Date: Wed, 18 Dec 2024 12:10:36 -0300 Subject: [PATCH 08/12] Rename fetch drops collections --- src/hooks/useEventsCollections.ts | 10 +++++----- src/loaders/collection.ts | 32 ++++++++++++++++++------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/hooks/useEventsCollections.ts b/src/hooks/useEventsCollections.ts index c4efcc2..a565835 100644 --- a/src/hooks/useEventsCollections.ts +++ b/src/hooks/useEventsCollections.ts @@ -1,23 +1,23 @@ import { useCallback, useState } from 'react' import { AbortedError } from 'models/error' -import { findEventsCollections } from 'loaders/collection' +import { fetchDropsCollections } from 'loaders/collection' function useEventsCollections(eventIds: number[]): { loadingCollections: boolean collectionsError: Error | null - collections: Awaited>['collections'] | null - relatedCollections: Awaited>['related'] | null + collections: Awaited>['collections'] | null + relatedCollections: Awaited>['related'] | null fetchEventsCollections: () => () => void } { const [loading, setLoading] = useState(false) const [error, setError] = useState(null) - const [result, setResult] = useState> | null>(null) + const [result, setResult] = useState> | null>(null) const fetchEventsCollections = useCallback( () => { const controller = new AbortController() setLoading(true) - findEventsCollections( + fetchDropsCollections( eventIds, controller.signal ).then((result) => { diff --git a/src/loaders/collection.ts b/src/loaders/collection.ts index 4daeddf..69ee5b9 100644 --- a/src/loaders/collection.ts +++ b/src/loaders/collection.ts @@ -1,4 +1,10 @@ -import { Collection, CollectionWithDrops, parseCollection, parseCollectionWithDrops } from 'models/collection' +import { + Collection, + COLLECTIONS_LIMIT, + CollectionWithDrops, + parseCollection, + parseCollectionWithDrops, +} from 'models/collection' import { DEFAULT_COMPASS_LIMIT } from 'models/compass' import { queryAggregateCountCompass, @@ -10,10 +16,10 @@ import { * Retrieve collections that has all given drops and collections that only have * some of the given drops (related) when more than one is given. */ -export async function findEventsCollections( - eventIds: number[], +export async function fetchDropsCollections( + dropIds: number[], abortSignal: AbortSignal, - limit: number = DEFAULT_COMPASS_LIMIT, + limit: number = Math.min(COLLECTIONS_LIMIT, DEFAULT_COMPASS_LIMIT), ): Promise<{ collections: Collection[] related: Collection[] @@ -23,18 +29,18 @@ export async function findEventsCollections( 'collections', parseCollection, ` - query EventsCollections( + query DropsCollections( $offset: Int! $limit: Int! ) { collections( where: { _and: [ - ${eventIds.map((eventId) => ` + ${dropIds.map((dropId) => ` { collections_items: { drop_id: { - _eq: ${eventId} + _eq: ${dropId} } } } @@ -59,12 +65,12 @@ export async function findEventsCollections( limit, abortSignal ), - eventIds.length < 2 ? Promise.resolve([]) : queryAllCompass( + dropIds.length < 2 ? Promise.resolve([]) : queryAllCompass( 'collections', parseCollection, ` - query EventsRelatedCollections( - $eventIds: [bigint!] + query DropsRelatedCollections( + $dropIds: [bigint!] $offset: Int! $limit: Int! ) { @@ -72,7 +78,7 @@ export async function findEventsCollections( where: { collections_items: { drop_id: { - _in: $eventIds + _in: $dropIds } } } @@ -88,7 +94,7 @@ export async function findEventsCollections( } `, { - eventIds, + dropIds, limit, }, 'offset', @@ -114,7 +120,7 @@ export async function searchCollections( search: string, abortSignal: AbortSignal, offset: number = 0, - limit: number = DEFAULT_COMPASS_LIMIT, + limit: number = Math.min(COLLECTIONS_LIMIT, DEFAULT_COMPASS_LIMIT), ): Promise<{ total: number | null items: CollectionWithDrops[] From 791db6b1478113308fd37a925f54589a16bb6aba Mon Sep 17 00:00:00 2001 From: Juan Date: Thu, 19 Dec 2024 12:01:53 -0300 Subject: [PATCH 09/12] Fetch collectors by drops --- src/hooks/useEventsOwnersAndMetrics.ts | 26 ++--- src/loaders/api.ts | 150 +------------------------ src/loaders/collector.ts | 82 ++++++++++++++ src/pages/Addresses.tsx | 16 +-- 4 files changed, 95 insertions(+), 179 deletions(-) diff --git a/src/hooks/useEventsOwnersAndMetrics.ts b/src/hooks/useEventsOwnersAndMetrics.ts index 5697357..38d4325 100644 --- a/src/hooks/useEventsOwnersAndMetrics.ts +++ b/src/hooks/useEventsOwnersAndMetrics.ts @@ -2,17 +2,17 @@ import { useCallback, useState } from 'react' import { filterInvalidOwners } from 'models/address' import { DropMetrics } from 'models/drop' import { AbortedError } from 'models/error' -import { EventAndOwners, InCommon } from 'models/api' -import { fetchDropsCollectors } from 'loaders/collector' +import { EventAndOwners } from 'models/api' +import { fetchCollectorsByDrops, fetchDropsCollectors } from 'loaders/collector' import { fetchDropMetrics, fetchDropsMetrics } from 'loaders/drop' -import { getEventAndOwners, getEventsOwners } from 'loaders/api' +import { getEventAndOwners } from 'loaders/api' function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record, force: boolean = false): { completedEventsOwnersAndMetrics: boolean loadingEventsOwnersAndMetrics: boolean loadingOwnersAndMetricsEvents: Record eventsOwnersAndMetricsErrors: Record - eventsOwners: InCommon + eventsOwners: Record eventsMetrics: Record fetchEventsOwnersAndMetrics: () => () => void retryEventOwnersAndMetrics: (eventId: number) => () => void @@ -21,7 +21,7 @@ function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record(false) const [loading, setLoading] = useState>({}) const [errors, setErrors] = useState>({}) - const [owners, setOwners] = useState({}) + const [owners, setOwners] = useState>({}) const [metrics, setMetrics] = useState>({}) function addLoading(eventId: number): void { @@ -75,7 +75,7 @@ function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record): void { setOwners((prevOwners) => ({ ...prevOwners, ...Object.fromEntries( @@ -232,19 +232,10 @@ function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record { - updateEventsOwners( - Object.fromEntries( - Object.entries(eventsOwners).map( - ([rawEventId, eventOwners]) => [ - rawEventId, - eventOwners.owners, - ] - ) - ) - ) + updateEventsOwners(eventsOwners) updateEventsMetrics(eventsMetrics) setLoadingCache(false) setCompleted(true) @@ -282,7 +273,6 @@ function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record { - if (!FAMILY_API_KEY) { - throw new Error( - `Drop ${eventId} owners could not be fetched, ` + - `configure Family API key` - ) - } - - let response: Response - - try { - response = await fetch( - `${FAMILY_API_URL}/event/${eventId}/owners${refresh ? '?refresh=true' : ''}`, - { - signal: abortSignal instanceof AbortSignal ? abortSignal : null, - headers: { - 'x-api-key': FAMILY_API_KEY, - }, - } - ) - } catch (err: unknown) { - if (err instanceof Error && err.name === 'AbortError') { - throw new AbortedError( - `Fetch drop ${eventId} owners aborted`, - { cause: err } - ) - } - throw new Error( - `Cannot fetch drop ${eventId} owners: ` + - `response was not success (network error)`, - { cause: err } - ) - } - - if (response.status === 404) { - return null - } - - if (response.status !== 200) { - throw new HttpError( - `Drop ${eventId} failed to fetch owners (status ${response.status})`, - { status: response.status } - ) - } - - const body: unknown = await response.json() - - if (body == null || typeof body !== 'object') { - throw new Error(`Malformed drop owners (type ${typeof body})`) - } - - return parseDropOwners(body) -} - -export async function getEventsOwners( - eventIds: number[], - abortSignal?: AbortSignal, - expiryDates?: Record, -): Promise> { - if (!FAMILY_API_KEY) { - throw new Error( - `Drops (${eventIds.length}) owners could not be fetched, ` + - `configure Family API key` - ) - } - - const encodedExpiryDates = expiryDates ? encodeExpiryDates(expiryDates) : '' - - let response: Response - - try { - response = await fetch( - `${FAMILY_API_URL}/events` + - `/${eventIds.map((eventId) => encodeURIComponent(eventId)).join(',')}` + - `/owners${encodedExpiryDates ? `?${encodedExpiryDates}` : ''}`, - { - signal: abortSignal instanceof AbortSignal ? abortSignal : null, - headers: { - 'x-api-key': FAMILY_API_KEY, - }, - } - ) - } catch (err: unknown) { - if (err instanceof Error && err.name === 'AbortError') { - throw new AbortedError(`Fetch drops owners aborted`, { cause: err }) - } - throw new Error( - `Cannot fetch drops owners: response was not success (network error)`, - { cause: err } - ) - } - - if (response.status !== 200) { - let message: string = 'Unknown error' - try { - const data = await response.json() - if ( - data != null && - typeof data === 'object' && - 'message' in data && - data.message != null && - typeof data.message === 'string' - ) { - message = data.message - } - } catch (err: unknown) { - console.error(err) - } - - if (message) { - throw new HttpError( - `Drops (${eventIds.length}) failed to fetch owners ` + - `(status ${response.status}): ${message}`, - { status: response.status } - ) - } - - throw new HttpError( - `Drops (${eventIds.length}) failed to fetch owners ` + - `(status ${response.status})`, - { status: response.status } - ) - } - - const body: unknown = await response.json() - - if (body == null || typeof body !== 'object') { - throw new Error(`Malformed drops owners (type ${typeof body})`) - } - - return Object.fromEntries( - Object.entries(body).map( - ([eventId, event]) => [eventId, parseDropOwners(event)] - ) - ) -} - export async function auth(passphrase: string): Promise { const response = await fetch(`${FAMILY_API_URL}/auth`, { method: 'POST', diff --git a/src/loaders/collector.ts b/src/loaders/collector.ts index 7c0dfb5..9b22c8e 100644 --- a/src/loaders/collector.ts +++ b/src/loaders/collector.ts @@ -6,6 +6,88 @@ import { DEFAULT_POAP_LIMIT, parsePOAP, POAP } from 'models/poap' import { Drop } from 'models/drop' import { queryAllCompass } from 'loaders/compass' +export async function fetchCollectorsByDrops( + dropIds: number[], + abortSignal?: AbortSignal, + dropsLimit: number = Math.min(DEFAULT_DROP_LIMIT, DEFAULT_COMPASS_LIMIT), + collectorsLimit: number = Math.min(DEFAULT_COLLECTOR_LIMIT, DEFAULT_COMPASS_LIMIT), +): Promise> { + const collectorsByDrops: Record> = {} + + for (let i = 0; i < dropIds.length; i += dropsLimit) { + const ids = dropIds.slice(i, i + dropsLimit) + + if (ids.length === 0) { + break + } + + const dropsCollectors = await queryAllCompass( + 'poaps', + (data: unknown): { dropId: number; address: string } => { + if ( + data == null || + typeof data !== 'object' || + !('drop_id' in data) || + data.drop_id == null || + typeof data.drop_id !== 'number' || + !('collector_address' in data) || + data.collector_address == null || + typeof data.collector_address !== 'string' || + !data.collector_address.startsWith('0x') || + data.collector_address.length !== 42 + ) { + throw new Error('Malformed drop collector') + } + return { + dropId: data.drop_id, + address: data.collector_address, + } + }, + ` + query FetchCollectorsByDrops( + $offset: Int! + $limit: Int! + $dropIds: [bigint!] + ) { + poaps( + where: { + drop_id: { _in: $dropIds } + collector_address: { + _nin: ["${IGNORED_OWNERS.join('", "')}"] + } + } + offset: $offset + limit: $limit + ) { + collector_address + drop_id + } + } + `, + { + dropIds: ids, + limit: collectorsLimit, + }, + 'offset', + collectorsLimit, + abortSignal + ) + + for (const dropCollector of dropsCollectors) { + if (!(dropCollector.dropId in collectorsByDrops)) { + collectorsByDrops[dropCollector.dropId] = new Set() + } + collectorsByDrops[dropCollector.dropId].add(dropCollector.address) + } + } + + return Object.fromEntries( + Object.entries(collectorsByDrops).map( + ([rawDropId, collectorsSet]) => [rawDropId, [...collectorsSet]] + ) + ) +} + export async function fetchDropsCollectors( dropIds: number[], abortSignal?: AbortSignal, diff --git a/src/pages/Addresses.tsx b/src/pages/Addresses.tsx index 0bc5b06..bdc320b 100644 --- a/src/pages/Addresses.tsx +++ b/src/pages/Addresses.tsx @@ -9,7 +9,6 @@ import { InCommon } from 'models/api' import { EnsByAddress } from 'models/ethereum' import { HTMLContext } from 'stores/html' import { ResolverEnsContext, ReverseEnsContext } from 'stores/ethereum' -import { getEventsOwners } from 'loaders/api' import { fetchCollectorDrops, fetchDropsCollectors } from 'loaders/collector' import AddressesForm from 'components/AddressesForm' import Card from 'components/Card' @@ -351,18 +350,11 @@ function Addresses() { controllers.push(controller) } else { const controller = new AbortController() - getEventsOwners(searchEvents, controller.signal).then( - (ownersMap) => { + fetchDropsCollectors(searchEvents, controller.signal).then( + (owners) => { setLoadingEventsOwners(false) - if (ownersMap) { - const uniqueOwners = Object.values(ownersMap).reduce( - (allOwners, eventOwners) => new Set([ - ...allOwners, - ...(eventOwners?.owners ?? []), - ]), - new Set() - ) - const addresses = [...uniqueOwners].map( + if (owners) { + const addresses = [...owners].map( (owner) => parseAddress(owner) ) updateAddresses(addresses) From e0518981716e497768efb255dec55df4e43f5bbe Mon Sep 17 00:00:00 2001 From: Juan Date: Thu, 19 Dec 2024 12:25:23 -0300 Subject: [PATCH 10/12] Fill empty drops collectors --- src/hooks/useEventsOwnersAndMetrics.ts | 9 ++++++++- src/utils/object.ts | 9 +++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 src/utils/object.ts diff --git a/src/hooks/useEventsOwnersAndMetrics.ts b/src/hooks/useEventsOwnersAndMetrics.ts index 38d4325..fb22336 100644 --- a/src/hooks/useEventsOwnersAndMetrics.ts +++ b/src/hooks/useEventsOwnersAndMetrics.ts @@ -6,6 +6,7 @@ import { EventAndOwners } from 'models/api' import { fetchCollectorsByDrops, fetchDropsCollectors } from 'loaders/collector' import { fetchDropMetrics, fetchDropsMetrics } from 'loaders/drop' import { getEventAndOwners } from 'loaders/api' +import { fillNull } from 'utils/object' function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record, force: boolean = false): { completedEventsOwnersAndMetrics: boolean @@ -235,7 +236,13 @@ function useEventsOwnersAndMetrics(eventIds: number[], expiryDates: Record { - updateEventsOwners(eventsOwners) + updateEventsOwners( + fillNull( + eventsOwners, + eventIds.map((eventId) => String(eventId)), + [] + ) + ) updateEventsMetrics(eventsMetrics) setLoadingCache(false) setCompleted(true) diff --git a/src/utils/object.ts b/src/utils/object.ts new file mode 100644 index 0000000..7ee0138 --- /dev/null +++ b/src/utils/object.ts @@ -0,0 +1,9 @@ +export function fillNull(obj: T, keys: string[], value: T[keyof T]): T { + const cloned = Object.assign({}, obj) + for (const key of keys) { + if (!(key in obj) || obj[key] == null) { + cloned[key] = value + } + } + return cloned +} From e108e6af6f653d4a663c64ac0f26314432bcdde5 Mon Sep 17 00:00:00 2001 From: Juan Date: Thu, 19 Dec 2024 12:30:18 -0300 Subject: [PATCH 11/12] Remove addresses force --- src/models/event.ts | 18 ---------- src/pages/Addresses.tsx | 75 +++++++++++++---------------------------- 2 files changed, 23 insertions(+), 70 deletions(-) diff --git a/src/models/event.ts b/src/models/event.ts index 815470e..3c35e98 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -33,21 +33,3 @@ export function parseExpiryDates(events: Record): Record endOfDayDate != null) ) } - -export function encodeExpiryDates(expiryDates: Record): string { - if (typeof expiryDates !== 'object') { - return '' - } - return Object.entries(expiryDates) - .map( - ([eventId, expiryDate]) => { - if (!(expiryDate instanceof Date) || isNaN(expiryDate.getTime())) { - return null - } - const ts = Math.trunc(expiryDate.getTime() / 1000) - return `expiry[${encodeURIComponent(eventId)}]=${encodeURIComponent(ts)}` - } - ) - .filter((param) => param != null) - .join('&') -} diff --git a/src/pages/Addresses.tsx b/src/pages/Addresses.tsx index bdc320b..4540578 100644 --- a/src/pages/Addresses.tsx +++ b/src/pages/Addresses.tsx @@ -286,11 +286,6 @@ function Addresses() { [searchParams] ) - const force = useMemo( - () => searchParams.get('force') === 'true', - [searchParams] - ) - const updateCollectionFromHash = useCallback( (initial = false) => { const controllers: AbortController[] = [] @@ -318,66 +313,42 @@ function Addresses() { } } else if (searchEvents.length > 0) { setLoadingEventsOwners(true) - if (force) { - const controller = new AbortController() - fetchDropsCollectors(searchEvents, controller.signal).then( - (collectors) => { - let addresses: ParsedAddress[] | undefined - try { - addresses = collectors.map((owner) => parseAddress(owner)) - } catch (err: unknown) { - setLoadingEventsOwners(false) - addError( - new Error('Cannot parse collectors', { - cause: err, - }) - ) - return - } - - setLoadingEventsOwners(false) - updateAddresses(addresses) - }, - (err) => { + const controller = new AbortController() + fetchDropsCollectors(searchEvents, controller.signal).then( + (collectors) => { + let addresses: ParsedAddress[] | undefined + try { + addresses = collectors.map((owner) => parseAddress(owner)) + } catch (err: unknown) { setLoadingEventsOwners(false) addError( - new Error(`Cannot load drops ${searchEvents.join(', ')}`, { + new Error('Cannot parse collectors', { cause: err, }) ) + return } - ) - controllers.push(controller) - } else { - const controller = new AbortController() - fetchDropsCollectors(searchEvents, controller.signal).then( - (owners) => { - setLoadingEventsOwners(false) - if (owners) { - const addresses = [...owners].map( - (owner) => parseAddress(owner) - ) - updateAddresses(addresses) - } else { - addError(new Error(`Drop owners could not be loaded`)) - } - }, - (err) => { - setLoadingEventsOwners(false) - addError(new Error(`Drop owners could not be loaded`, { + + setLoadingEventsOwners(false) + updateAddresses(addresses) + }, + (err) => { + setLoadingEventsOwners(false) + addError( + new Error(`Cannot load drops ${searchEvents.join(', ')}`, { cause: err, - })) - } - ) - controllers.push(controller) - } + }) + ) + } + ) + controllers.push(controller) } else if (!initial) { setAddresses(null) setCollectors({}) } return controllers }, - [searchEvents, force] + [searchEvents] ) useEffect( From 17a3dea45ade5dae4587c6fa17f84f0290af4840 Mon Sep 17 00:00:00 2001 From: Juan Date: Thu, 19 Dec 2024 12:42:43 -0300 Subject: [PATCH 12/12] Version 1.21.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 475d117..aafb424 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@poap-xyz/poap-family", - "version": "1.20.1", + "version": "1.21.0", "author": { "name": "POAP", "url": "https://poap.xyz"