@@ -65,7 +65,25 @@ const SPLIT_PRODUCT: Record<string, InflowSource> = {
6565 * the DAO actually earns from, not a transaction taxonomy. `splits` is the
6666 * explicit "a split paid this but we cannot say which product" bucket.
6767 */
68- export type InflowSource = "auction" | "subnet" | "swap" | "splits" | "transfer" ;
68+ export type InflowSource = "auction" | "subnet" | "swap" | "splits" | "secondary" | "transfer" ;
69+
70+ /**
71+ * Seaport 1.6 (the canonical vanity deployment). Secondary sales of Gnars NFTs
72+ * settle through it in two shapes, and only ONE of them is detectable by the
73+ * sender:
74+ *
75+ * - ETH listings: Seaport itself pays the treasury, so `from` IS this address.
76+ * - Accepted WETH offers: the WETH leaves EACH BUYER's wallet (via conduit),
77+ * so `from` varies per row — dozens of senders were dozens of buyers, not a
78+ * mystery. Do NOT "fix" that path by filtering on a sender; the only
79+ * reliable marker is Seaport appearing among the transaction's log emitters
80+ * (verified: to = Seaport, emitters = Seaport + WETH + the Gnars token).
81+ *
82+ * The tag is "secondary", never a platform name: Seaport is an open protocol
83+ * (OpenSea, aggregators, bots), and the dissected fulfilment used no OpenSea
84+ * conduit — the platform is not provable from the chain.
85+ */
86+ const SEAPORT = "0x0000000000000068f116a894984e2db1123eb395" ;
6987
7088/** Assets this panel reports. Everything else the treasury receives is noise here. */
7189const TRACKED = new Set ( [ USDC , WETH ] ) ;
@@ -109,7 +127,7 @@ const ALCHEMY_KEY = process.env.ALCHEMY_API_KEY;
109127 * there, and the answer is the generic `splits`, not a guess. A failed
110128 * receipt read degrades the same way: `splits` asserts nothing false.
111129 */
112- async function splitSourceForTx ( hash : string ) : Promise < InflowSource > {
130+ async function txLogEmitters ( hash : string ) : Promise < Set < string > | null > {
113131 try {
114132 const res = await fetch ( `https://base-mainnet.g.alchemy.com/v2/${ ALCHEMY_KEY } ` , {
115133 method : "POST" ,
@@ -123,18 +141,32 @@ async function splitSourceForTx(hash: string): Promise<InflowSource> {
123141 // Receipts are immutable; cache them as long as Next allows.
124142 next : { revalidate : 86400 } ,
125143 } ) ;
126- if ( ! res . ok ) return "splits" ;
144+ if ( ! res . ok ) return null ;
127145 const json = ( await res . json ( ) ) as { result ?: { logs ?: { address ?: string } [ ] } } ;
128- for ( const log of json . result ?. logs ?? [ ] ) {
129- const product = SPLIT_PRODUCT [ ( log . address ?? "" ) . toLowerCase ( ) ] ;
130- if ( product ) return product ;
131- }
132- return "splits" ;
146+ return new Set ( ( json . result ?. logs ?? [ ] ) . map ( ( l ) => ( l . address ?? "" ) . toLowerCase ( ) ) ) ;
133147 } catch {
134- return "splits" ;
148+ return null ;
135149 }
136150}
137151
152+ async function splitSourceForTx ( hash : string ) : Promise < InflowSource > {
153+ const emitters = await txLogEmitters ( hash ) ;
154+ if ( ! emitters ) return "splits" ;
155+ for ( const e of emitters ) {
156+ const product = SPLIT_PRODUCT [ e ] ;
157+ if ( product ) return product ;
158+ }
159+ return "splits" ;
160+ }
161+
162+ /** A WETH credit is a secondary sale iff Seaport emitted in its transaction —
163+ * see the SEAPORT note for why the sender can never be the marker here. A
164+ * failed receipt read stays "transfer": asserting a sale needs the evidence. */
165+ async function secondaryOrTransfer ( hash : string ) : Promise < InflowSource > {
166+ const emitters = await txLogEmitters ( hash ) ;
167+ return emitters ?. has ( SEAPORT ) ? "secondary" : "transfer" ;
168+ }
169+
138170/** One page of inflows plus the cursor to the next. `nextPageKey: null` means
139171 * the history is genuinely exhausted — a different fact from "not fetched yet",
140172 * and the UI keeps the two apart. */
@@ -217,7 +249,11 @@ export const loadTreasuryInflowsPage = cache(async (pageKey?: string): Promise<I
217249 ? "auction"
218250 : from === SPLITS_WAREHOUSE
219251 ? await splitSourceForTx ( t . hash )
220- : "transfer" ;
252+ : from === SEAPORT
253+ ? "secondary"
254+ : asset === "WETH"
255+ ? await secondaryOrTransfer ( t . hash )
256+ : "transfer" ;
221257
222258 return [
223259 {
0 commit comments