Skip to content

Commit ee4edd3

Browse files
authored
feat(treasury): tag Seaport secondary sales as their own source (#272)
Two shapes of the same income, only one detectable by sender: ETH listings arrive from Seaport 1.6 itself, while accepted WETH offers leave EACH BUYER's wallet — dozens of senders were dozens of buyers, so the sender can never be the marker there. WETH credits check the receipt for Seaport among the log emitters, reusing the immutable- receipt path the split attribution already uses. The tag says Secondary, never a platform: Seaport is an open protocol and the dissected fulfilment used no OpenSea conduit — the platform is not provable from the chain. Sixth source: the count-derived tile grid resolves to 3+3 on its own, which is what the formula existed for.
1 parent 347e22c commit ee4edd3

4 files changed

Lines changed: 55 additions & 13 deletions

File tree

messages/en/treasury.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@
9191
"colFrom": "From",
9292
"colSource": "Source",
9393
"colAmount": "Amount",
94-
"colAge": "Age"
94+
"colAge": "Age",
95+
"secondary": "Secondary"
9596
},
9697
"allocation": {
9798
"title": "Allocation",

messages/pt-br/treasury.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@
9191
"colFrom": "De",
9292
"colSource": "Origem",
9393
"colAmount": "Valor",
94-
"colAge": "Idade"
94+
"colAge": "Idade",
95+
"secondary": "Secundária"
9596
},
9697
"allocation": {
9798
"title": "Alocação",

src/components/treasury/TreasuryInflowsList.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const SOURCE_DOT: Record<InflowSource, string> = {
3030
auction: "bg-amber-500",
3131
subnet: "bg-emerald-500",
3232
swap: "bg-sky-500",
33+
secondary: "bg-rose-500",
3334
// Generic splits is the "a split we have not mapped" bucket — it must never
3435
// borrow a product's colour, least of all the subnet green it used to share.
3536
splits: "bg-foreground/50",
@@ -40,6 +41,7 @@ const SOURCE_TONE: Record<InflowSource, string> = {
4041
auction: "border-amber-500/25 bg-amber-500/10 text-amber-600 dark:text-amber-400",
4142
subnet: "border-emerald-500/25 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
4243
swap: "border-sky-500/25 bg-sky-500/10 text-sky-600 dark:text-sky-400",
44+
secondary: "border-rose-500/25 bg-rose-500/10 text-rose-600 dark:text-rose-400",
4345
// Unmapped split: tinted just enough to say "known mechanism, unknown
4446
// product" without wearing any product's brand colour.
4547
splits: "border-foreground/20 bg-foreground/5 text-foreground/70",
@@ -214,7 +216,9 @@ export function TreasuryInflowsList({
214216
// arrive as ETH, WETH and USDC — and there is no price feed in this component,
215217
// so each asset keeps its own line. A single blended figure would require a
216218
// conversion this component cannot honestly make.
217-
const summary = (["auction", "subnet", "swap", "splits", "transfer"] as InflowSource[])
219+
const summary = (
220+
["auction", "secondary", "subnet", "swap", "splits", "transfer"] as InflowSource[]
221+
)
218222
.map((source) => {
219223
const rowsFor = all.filter((f) => f.source === source);
220224
const byAsset = new Map<InflowAsset, number>();

src/services/treasury-inflows.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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. */
7189
const 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

Comments
 (0)