Skip to content

Commit f327bd4

Browse files
moose-codeclaude
andcommitted
Add TokenScope plugin (Phase 6)
Track ENS NFT transfers (ERC721/ERC1155) across all subregistries and Seaport secondary market sales. Implements 9-type NFT transfer state machine with CAIP-19 asset IDs for 6 contracts across 4 chains. New files: tokenscope-helpers.ts, TokenScope.ts, Seaport1_5.json New entities: NameToken, NameSale New contract: Seaport 1.5 on mainnet New events: ThreeDNSToken TransferSingle/TransferBatch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 67179ae commit f327bd4

File tree

11 files changed

+1039
-56
lines changed

11 files changed

+1039
-56
lines changed

MIGRATION_PLAN.md

Lines changed: 157 additions & 54 deletions
Large diffs are not rendered by default.

abis/Seaport1_5.json

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
[
2+
{
3+
"anonymous": false,
4+
"inputs": [
5+
{
6+
"indexed": false,
7+
"internalType": "bytes32",
8+
"name": "orderHash",
9+
"type": "bytes32"
10+
},
11+
{
12+
"indexed": true,
13+
"internalType": "address",
14+
"name": "offerer",
15+
"type": "address"
16+
},
17+
{
18+
"indexed": true,
19+
"internalType": "address",
20+
"name": "zone",
21+
"type": "address"
22+
},
23+
{
24+
"indexed": false,
25+
"internalType": "address",
26+
"name": "recipient",
27+
"type": "address"
28+
},
29+
{
30+
"components": [
31+
{
32+
"internalType": "enum ItemType",
33+
"name": "itemType",
34+
"type": "uint8"
35+
},
36+
{
37+
"internalType": "address",
38+
"name": "token",
39+
"type": "address"
40+
},
41+
{
42+
"internalType": "uint256",
43+
"name": "identifier",
44+
"type": "uint256"
45+
},
46+
{
47+
"internalType": "uint256",
48+
"name": "amount",
49+
"type": "uint256"
50+
}
51+
],
52+
"indexed": false,
53+
"internalType": "struct SpentItem[]",
54+
"name": "offer",
55+
"type": "tuple[]"
56+
},
57+
{
58+
"components": [
59+
{
60+
"internalType": "enum ItemType",
61+
"name": "itemType",
62+
"type": "uint8"
63+
},
64+
{
65+
"internalType": "address",
66+
"name": "token",
67+
"type": "address"
68+
},
69+
{
70+
"internalType": "uint256",
71+
"name": "identifier",
72+
"type": "uint256"
73+
},
74+
{
75+
"internalType": "uint256",
76+
"name": "amount",
77+
"type": "uint256"
78+
},
79+
{
80+
"internalType": "address payable",
81+
"name": "recipient",
82+
"type": "address"
83+
}
84+
],
85+
"indexed": false,
86+
"internalType": "struct ReceivedItem[]",
87+
"name": "consideration",
88+
"type": "tuple[]"
89+
}
90+
],
91+
"name": "OrderFulfilled",
92+
"type": "event"
93+
}
94+
]

config.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,15 @@ contracts:
230230
field_selection:
231231
transaction_fields:
232232
- hash
233+
# ─── ERC1155 Transfer events (for TokenScope plugin) ──────────────────────
234+
- event: "TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value)"
235+
field_selection:
236+
transaction_fields:
237+
- hash
238+
- event: "TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values)"
239+
field_selection:
240+
transaction_fields:
241+
- hash
233242

234243
# ─── Resolver (dynamically registered via Registry.NewResolver) ─────────────
235244
- name: Resolver
@@ -301,6 +310,16 @@ contracts:
301310
- hash
302311
- from
303312

313+
# ─── Seaport (TokenScope plugin) ───────────────────────────────────────────
314+
- name: Seaport
315+
abi_file_path: ./abis/Seaport1_5.json
316+
handler: ./src/handlers/TokenScope.ts
317+
events:
318+
- event: "OrderFulfilled(bytes32 orderHash, address indexed offerer, address indexed zone, address recipient, (uint8,address,uint256,uint256)[] offer, (uint8,address,uint256,uint256,address)[] consideration)"
319+
field_selection:
320+
transaction_fields:
321+
- hash
322+
304323
# ─── StandaloneReverseRegistrar (PA plugin) ────────────────────────────────
305324
- name: StandaloneReverseRegistrar
306325
events:
@@ -345,6 +364,10 @@ chains:
345364
address:
346365
- "0xf55575bde5953ee4272d5ce7cdd924c74d8fa81a"
347366
start_block: 23784217
367+
- name: Seaport
368+
address:
369+
- "0x00000000000000adc04c56bf30ac9d3c0aaf14dc"
370+
start_block: 17129405
348371
- name: Resolver
349372
address:
350373
# Static reverse resolver addresses (PA plugin)

schema.graphql

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,35 @@ type RegistrarActionMetadata {
348348
logicalEventKey: String! # "{node}:{transactionHash}"
349349
logicalEventId: String! # ID of registrarAction being built
350350
}
351+
352+
# ─── TokenScope Plugin Entities ──────────────────────────────────────────
353+
354+
type NameToken {
355+
id: ID! # CAIP-19 Asset ID
356+
domainId: String! # namehash (node) of the ENS domain
357+
chainId: Int! # chain managing the token
358+
contractAddress: String! # NFT contract address
359+
tokenId: BigInt! # token ID in the contract
360+
assetNamespace: String! # "erc721" or "erc1155"
361+
owner: String! # current owner (zeroAddress if burned)
362+
mintStatus: String! # "minted" or "burned"
363+
}
364+
365+
type NameSale {
366+
id: ID! # "{chainId}-{blockNumber}-{logIndex}"
367+
chainId: Int!
368+
blockNumber: Int!
369+
logIndex: Int!
370+
transactionHash: String!
371+
orderHash: String!
372+
contractAddress: String!
373+
tokenId: BigInt!
374+
assetNamespace: String!
375+
assetId: String! # CAIP-19 Asset ID
376+
domainId: String! # namehash (node)
377+
buyer: String!
378+
seller: String!
379+
currency: String! # "ETH", "USDC", "DAI"
380+
amount: BigInt! # smallest unit
381+
timestamp: BigInt!
382+
}

src/handlers/BaseRegistrar.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ import {
2626
handleRegistrarControllerEvent,
2727
} from "../lib/registrar-helpers";
2828

29+
import {
30+
handleNFTTransfer,
31+
buildDomainAssetId,
32+
AssetNamespaces,
33+
} from "../lib/tokenscope-helpers";
34+
2935
// ─── Constants ──────────────────────────────────────────────────────────────
3036

3137
const managedNode = BASE_ETH_NODE;
@@ -254,6 +260,16 @@ BaseRegistrar_Base.Transfer.handler(async ({ event, context }) => {
254260
registration_id: registrationId,
255261
newOwner_id: to,
256262
});
263+
264+
// TokenScope: track ERC721 transfer
265+
const nft = buildDomainAssetId(
266+
event.chainId,
267+
event.srcAddress,
268+
event.params.tokenId,
269+
AssetNamespaces.ERC721,
270+
(tokenId) => makeSubdomainNode(tokenIdToLabelHash(tokenId), managedNode),
271+
);
272+
await handleNFTTransfer(context, event.params.from, to, false, nft);
257273
});
258274

259275
// ─── EAController_Base.NameRegistered ───────────────────────────────────────

src/handlers/LineaRegistrar.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ import {
2424
handleRegistrarControllerEvent,
2525
} from "../lib/registrar-helpers";
2626

27+
import {
28+
handleNFTTransfer,
29+
buildDomainAssetId,
30+
AssetNamespaces,
31+
} from "../lib/tokenscope-helpers";
32+
2733
// ─── Constants ──────────────────────────────────────────────────────────────
2834

2935
const managedNode = LINEA_ETH_NODE;
@@ -178,6 +184,16 @@ BaseRegistrar_Linea.Transfer.handler(async ({ event, context }) => {
178184
registration_id: registrationId,
179185
newOwner_id: to,
180186
});
187+
188+
// TokenScope: track ERC721 transfer
189+
const nft = buildDomainAssetId(
190+
event.chainId,
191+
event.srcAddress,
192+
event.params.tokenId,
193+
AssetNamespaces.ERC721,
194+
(tokenId) => makeSubdomainNode(tokenIdToLabelHash(tokenId), managedNode),
195+
);
196+
await handleNFTTransfer(context, event.params.from, to, false, nft);
181197
});
182198

183199
// ─── EthController_Linea.NameRegistered (paid registration) ─────────────────

src/handlers/NameWrapper.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,15 @@ import {
1111
upsertAccount,
1212
bigintMax,
1313
MANAGED_NODES,
14+
tokenIdToLabelHash,
1415
} from "../lib/helpers";
1516

17+
import {
18+
handleERC1155Transfer,
19+
buildDomainAssetId,
20+
AssetNamespaces,
21+
} from "../lib/tokenscope-helpers";
22+
1623
// ─── Constants ──────────────────────────────────────────────────────────────
1724

1825
/**
@@ -130,7 +137,7 @@ async function handleTransfer(
130137
// ─── TransferSingle ─────────────────────────────────────────────────────────
131138

132139
NameWrapper.TransferSingle.handler(async ({ event, context }) => {
133-
const { id: tokenId, to } = event.params;
140+
const { id: tokenId, to, from, value } = event.params;
134141

135142
await handleTransfer(
136143
event,
@@ -139,22 +146,43 @@ NameWrapper.TransferSingle.handler(async ({ event, context }) => {
139146
tokenId,
140147
to,
141148
);
149+
150+
// TokenScope: track ERC1155 transfer
151+
const nft = buildDomainAssetId(
152+
event.chainId,
153+
event.srcAddress,
154+
tokenId,
155+
AssetNamespaces.ERC1155,
156+
(tid) => "0x" + tid.toString(16).padStart(64, "0"),
157+
);
158+
await handleERC1155Transfer(context, from, to, false, nft, value);
142159
});
143160

144161
// ─── TransferBatch ──────────────────────────────────────────────────────────
145162

146163
NameWrapper.TransferBatch.handler(async ({ event, context }) => {
147-
const { ids: tokenIds, to } = event.params;
164+
const { ids: tokenIds, values, to, from } = event.params;
148165

149166
for (let i = 0; i < tokenIds.length; i++) {
150167
const tokenId = tokenIds[i]!;
168+
const value = values[i]!;
151169
await handleTransfer(
152170
event,
153171
context,
154172
makeEventId(event.chainId, event.block.number, event.logIndex, i),
155173
tokenId,
156174
to,
157175
);
176+
177+
// TokenScope: track ERC1155 transfer for each token
178+
const nft = buildDomainAssetId(
179+
event.chainId,
180+
event.srcAddress,
181+
tokenId,
182+
AssetNamespaces.ERC1155,
183+
(tid) => "0x" + tid.toString(16).padStart(64, "0"),
184+
);
185+
await handleERC1155Transfer(context, from, to, false, nft, value);
158186
}
159187
});
160188

src/handlers/Registrar.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ import {
2929
decodeEncodedReferrer,
3030
} from "../lib/registrar-helpers";
3131

32+
import {
33+
handleNFTTransfer,
34+
buildDomainAssetId,
35+
AssetNamespaces,
36+
} from "../lib/tokenscope-helpers";
37+
3238
// ─── Constants ──────────────────────────────────────────────────────────────
3339

3440
const managedNode = ETH_NODE;
@@ -199,6 +205,16 @@ BaseRegistrar.Transfer.handler(async ({ event, context }) => {
199205
registration_id: registrationId,
200206
newOwner_id: to,
201207
});
208+
209+
// TokenScope: track ERC721 transfer
210+
const nft = buildDomainAssetId(
211+
event.chainId,
212+
event.srcAddress,
213+
event.params.tokenId,
214+
AssetNamespaces.ERC721,
215+
(tokenId) => makeSubdomainNode(tokenIdToLabelHash(tokenId), managedNode),
216+
);
217+
await handleNFTTransfer(context, event.params.from, to, false, nft);
202218
});
203219

204220
// ─── LegacyController Handlers ─────────────────────────────────────────────

src/handlers/ThreeDNS.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ import {
2323

2424
import { upsertDomainResolverRelation } from "../lib/protocol-acceleration";
2525

26+
import {
27+
handleERC1155Transfer,
28+
buildDomainAssetId,
29+
AssetNamespaces,
30+
} from "../lib/tokenscope-helpers";
31+
2632
// ─── Root Node Tracking ─────────────────────────────────────────────────────
2733
// ThreeDNS lives on chains that may not have a Registry (e.g. Optimism).
2834
// Ensure the root domain exists on the first event per chain.
@@ -263,3 +269,50 @@ ThreeDNSToken.RegistrationExtended.handler(async ({ event, context }) => {
263269
expiryDate: newExpiry,
264270
});
265271
});
272+
273+
// ─── ThreeDNSToken.TransferSingle (TokenScope: ERC1155 NFT tracking) ──────
274+
275+
ThreeDNSToken.TransferSingle.handler(async ({ event, context }) => {
276+
const { id: tokenId, from, to, value } = event.params;
277+
278+
// 3DNS allows non-standard minted remint (mint over already-minted token)
279+
const allowMintedRemint = true;
280+
281+
const nft = buildDomainAssetId(
282+
event.chainId,
283+
event.srcAddress,
284+
tokenId,
285+
AssetNamespaces.ERC1155,
286+
(tid) => "0x" + tid.toString(16).padStart(64, "0"),
287+
);
288+
await handleERC1155Transfer(context, from, to, allowMintedRemint, nft, value);
289+
});
290+
291+
// ─── ThreeDNSToken.TransferBatch (TokenScope: ERC1155 NFT tracking) ───────
292+
293+
ThreeDNSToken.TransferBatch.handler(async ({ event, context }) => {
294+
const { ids: tokenIds, values, from, to } = event.params;
295+
296+
if (tokenIds.length !== values.length) {
297+
throw new Error(
298+
`ERC1155 transfer batch ids and values must have the same length, got ${tokenIds.length} and ${values.length}.`,
299+
);
300+
}
301+
302+
// 3DNS allows non-standard minted remint (mint over already-minted token)
303+
const allowMintedRemint = true;
304+
305+
for (let i = 0; i < tokenIds.length; i++) {
306+
const tokenId = tokenIds[i]!;
307+
const value = values[i]!;
308+
309+
const nft = buildDomainAssetId(
310+
event.chainId,
311+
event.srcAddress,
312+
tokenId,
313+
AssetNamespaces.ERC1155,
314+
(tid) => "0x" + tid.toString(16).padStart(64, "0"),
315+
);
316+
await handleERC1155Transfer(context, from, to, allowMintedRemint, nft, value);
317+
}
318+
});

0 commit comments

Comments
 (0)