@cfxlabsinc/b2b-services
    Preparing search index...

    Read model over solana_program_event — the decoded mintfx emit_cpi! events, one row per event.

    Reads the projection rather than solana_transaction.parsed_events: the array is still the source of truth, but expanding it per query meant a CROSS JOIN LATERAL … WITH ORDINALITY and a jsonb destructure that no index could serve. Every filter here is an ordinary indexed column, and the account filter is a GIN array overlap.

    Read-only and derived: SolanaAccountTransactionService owns the decode that writes both the column and these rows, in one transaction.

    Deliberately not a method on SolanaAccountTransactionQueryService: that service's grain is (account, mint, transaction), so every event would be duplicated once per token account the transaction touched — two to three times over for a transfer. Same data, different question.

    Index
    • Events matching the filters, newest first by default.

      total is the window count over the expanded set, which is exactly the event count — the LATERAL is the grain rather than a fan-out join, so the cardinality caveat on window counts does not bite here. It rides back only on a row that was returned, so a page past the last one reports zero and the caller is the one that has to notice.

      No index serves the jsonb predicates, so a filtered search is a scan of solana_transaction (~81k rows on prod). That is the shape of the table rather than a defect in the query: parsed_events carries no expression index, and adding one needs a migration this read model does not own.

      Parameters

      Returns Promise<
          {
              ok: true;
              value: {
                  hasNext: boolean;
                  items: SolanaProgramEvent[];
                  total: number;
                  wallets: Record<string, SolanaWalletLabel>;
              };
          },
      >

    • Everything that describes the set a search is looking at, in one call.

      The two facets and the decode coverage were three methods and three awaits at every call site, and a page that renders them separately can show counts from one filter beside rows from another. One method makes that impossible to get wrong, and the three reads go out together.

      Each facet holds its OWN dimension open — counting names underneath the name filter would only ever report the names already picked — so the three queries genuinely differ in their WHERE and cannot collapse into one pass.

      Parameters

      Returns Promise<
          {
              ok: true;
              value: {
                  decodedThrough: Date
                  | null;
                  eventNames: { count: number; name: string }[];
                  tokenIds: string[];
                  undecoded: number;
              };
          },
      >