@1001-digital/ponder-artifacts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 1001.digital
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,433 @@
1
+ import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
2
+ import { Hono } from 'hono';
3
+
4
+ declare const offchainSchema: drizzle_orm_pg_core.PgSchema<"offchain">;
5
+ declare const artifactToken: drizzle_orm_pg_core.PgTableWithColumns<{
6
+ name: "artifact_token";
7
+ schema: "offchain";
8
+ columns: {
9
+ collection: drizzle_orm_pg_core.PgColumn<{
10
+ name: "collection";
11
+ tableName: "artifact_token";
12
+ dataType: "string";
13
+ columnType: "PgText";
14
+ data: string;
15
+ driverParam: string;
16
+ notNull: true;
17
+ hasDefault: false;
18
+ isPrimaryKey: false;
19
+ isAutoincrement: false;
20
+ hasRuntimeDefault: false;
21
+ enumValues: [string, ...string[]];
22
+ baseColumn: never;
23
+ identity: undefined;
24
+ generated: undefined;
25
+ }, {}, {}>;
26
+ tokenId: drizzle_orm_pg_core.PgColumn<{
27
+ name: "token_id";
28
+ tableName: "artifact_token";
29
+ dataType: "string";
30
+ columnType: "PgText";
31
+ data: string;
32
+ driverParam: string;
33
+ notNull: true;
34
+ hasDefault: false;
35
+ isPrimaryKey: false;
36
+ isAutoincrement: false;
37
+ hasRuntimeDefault: false;
38
+ enumValues: [string, ...string[]];
39
+ baseColumn: never;
40
+ identity: undefined;
41
+ generated: undefined;
42
+ }, {}, {}>;
43
+ tokenStandard: drizzle_orm_pg_core.PgColumn<{
44
+ name: "token_standard";
45
+ tableName: "artifact_token";
46
+ dataType: "string";
47
+ columnType: "PgText";
48
+ data: string;
49
+ driverParam: string;
50
+ notNull: true;
51
+ hasDefault: false;
52
+ isPrimaryKey: false;
53
+ isAutoincrement: false;
54
+ hasRuntimeDefault: false;
55
+ enumValues: [string, ...string[]];
56
+ baseColumn: never;
57
+ identity: undefined;
58
+ generated: undefined;
59
+ }, {}, {}>;
60
+ tokenUri: drizzle_orm_pg_core.PgColumn<{
61
+ name: "token_uri";
62
+ tableName: "artifact_token";
63
+ dataType: "string";
64
+ columnType: "PgText";
65
+ data: string;
66
+ driverParam: string;
67
+ notNull: false;
68
+ hasDefault: false;
69
+ isPrimaryKey: false;
70
+ isAutoincrement: false;
71
+ hasRuntimeDefault: false;
72
+ enumValues: [string, ...string[]];
73
+ baseColumn: never;
74
+ identity: undefined;
75
+ generated: undefined;
76
+ }, {}, {}>;
77
+ name: drizzle_orm_pg_core.PgColumn<{
78
+ name: "name";
79
+ tableName: "artifact_token";
80
+ dataType: "string";
81
+ columnType: "PgText";
82
+ data: string;
83
+ driverParam: string;
84
+ notNull: false;
85
+ hasDefault: false;
86
+ isPrimaryKey: false;
87
+ isAutoincrement: false;
88
+ hasRuntimeDefault: false;
89
+ enumValues: [string, ...string[]];
90
+ baseColumn: never;
91
+ identity: undefined;
92
+ generated: undefined;
93
+ }, {}, {}>;
94
+ description: drizzle_orm_pg_core.PgColumn<{
95
+ name: "description";
96
+ tableName: "artifact_token";
97
+ dataType: "string";
98
+ columnType: "PgText";
99
+ data: string;
100
+ driverParam: string;
101
+ notNull: false;
102
+ hasDefault: false;
103
+ isPrimaryKey: false;
104
+ isAutoincrement: false;
105
+ hasRuntimeDefault: false;
106
+ enumValues: [string, ...string[]];
107
+ baseColumn: never;
108
+ identity: undefined;
109
+ generated: undefined;
110
+ }, {}, {}>;
111
+ image: drizzle_orm_pg_core.PgColumn<{
112
+ name: "image";
113
+ tableName: "artifact_token";
114
+ dataType: "string";
115
+ columnType: "PgText";
116
+ data: string;
117
+ driverParam: string;
118
+ notNull: false;
119
+ hasDefault: false;
120
+ isPrimaryKey: false;
121
+ isAutoincrement: false;
122
+ hasRuntimeDefault: false;
123
+ enumValues: [string, ...string[]];
124
+ baseColumn: never;
125
+ identity: undefined;
126
+ generated: undefined;
127
+ }, {}, {}>;
128
+ animationUrl: drizzle_orm_pg_core.PgColumn<{
129
+ name: "animation_url";
130
+ tableName: "artifact_token";
131
+ dataType: "string";
132
+ columnType: "PgText";
133
+ data: string;
134
+ driverParam: string;
135
+ notNull: false;
136
+ hasDefault: false;
137
+ isPrimaryKey: false;
138
+ isAutoincrement: false;
139
+ hasRuntimeDefault: false;
140
+ enumValues: [string, ...string[]];
141
+ baseColumn: never;
142
+ identity: undefined;
143
+ generated: undefined;
144
+ }, {}, {}>;
145
+ data: drizzle_orm_pg_core.PgColumn<{
146
+ name: "data";
147
+ tableName: "artifact_token";
148
+ dataType: "json";
149
+ columnType: "PgJson";
150
+ data: Record<string, unknown>;
151
+ driverParam: unknown;
152
+ notNull: false;
153
+ hasDefault: false;
154
+ isPrimaryKey: false;
155
+ isAutoincrement: false;
156
+ hasRuntimeDefault: false;
157
+ enumValues: undefined;
158
+ baseColumn: never;
159
+ identity: undefined;
160
+ generated: undefined;
161
+ }, {}, {
162
+ $type: Record<string, unknown>;
163
+ }>;
164
+ updatedAt: drizzle_orm_pg_core.PgColumn<{
165
+ name: "updated_at";
166
+ tableName: "artifact_token";
167
+ dataType: "number";
168
+ columnType: "PgInteger";
169
+ data: number;
170
+ driverParam: string | number;
171
+ notNull: true;
172
+ hasDefault: false;
173
+ isPrimaryKey: false;
174
+ isAutoincrement: false;
175
+ hasRuntimeDefault: false;
176
+ enumValues: undefined;
177
+ baseColumn: never;
178
+ identity: undefined;
179
+ generated: undefined;
180
+ }, {}, {}>;
181
+ };
182
+ dialect: "pg";
183
+ }>;
184
+ declare const artifactCollection: drizzle_orm_pg_core.PgTableWithColumns<{
185
+ name: "artifact_collection";
186
+ schema: "offchain";
187
+ columns: {
188
+ collection: drizzle_orm_pg_core.PgColumn<{
189
+ name: "collection";
190
+ tableName: "artifact_collection";
191
+ dataType: "string";
192
+ columnType: "PgText";
193
+ data: string;
194
+ driverParam: string;
195
+ notNull: true;
196
+ hasDefault: false;
197
+ isPrimaryKey: true;
198
+ isAutoincrement: false;
199
+ hasRuntimeDefault: false;
200
+ enumValues: [string, ...string[]];
201
+ baseColumn: never;
202
+ identity: undefined;
203
+ generated: undefined;
204
+ }, {}, {}>;
205
+ tokenStandard: drizzle_orm_pg_core.PgColumn<{
206
+ name: "token_standard";
207
+ tableName: "artifact_collection";
208
+ dataType: "string";
209
+ columnType: "PgText";
210
+ data: string;
211
+ driverParam: string;
212
+ notNull: true;
213
+ hasDefault: false;
214
+ isPrimaryKey: false;
215
+ isAutoincrement: false;
216
+ hasRuntimeDefault: false;
217
+ enumValues: [string, ...string[]];
218
+ baseColumn: never;
219
+ identity: undefined;
220
+ generated: undefined;
221
+ }, {}, {}>;
222
+ name: drizzle_orm_pg_core.PgColumn<{
223
+ name: "name";
224
+ tableName: "artifact_collection";
225
+ dataType: "string";
226
+ columnType: "PgText";
227
+ data: string;
228
+ driverParam: string;
229
+ notNull: false;
230
+ hasDefault: false;
231
+ isPrimaryKey: false;
232
+ isAutoincrement: false;
233
+ hasRuntimeDefault: false;
234
+ enumValues: [string, ...string[]];
235
+ baseColumn: never;
236
+ identity: undefined;
237
+ generated: undefined;
238
+ }, {}, {}>;
239
+ symbol: drizzle_orm_pg_core.PgColumn<{
240
+ name: "symbol";
241
+ tableName: "artifact_collection";
242
+ dataType: "string";
243
+ columnType: "PgText";
244
+ data: string;
245
+ driverParam: string;
246
+ notNull: false;
247
+ hasDefault: false;
248
+ isPrimaryKey: false;
249
+ isAutoincrement: false;
250
+ hasRuntimeDefault: false;
251
+ enumValues: [string, ...string[]];
252
+ baseColumn: never;
253
+ identity: undefined;
254
+ generated: undefined;
255
+ }, {}, {}>;
256
+ owner: drizzle_orm_pg_core.PgColumn<{
257
+ name: "owner";
258
+ tableName: "artifact_collection";
259
+ dataType: "string";
260
+ columnType: "PgText";
261
+ data: string;
262
+ driverParam: string;
263
+ notNull: false;
264
+ hasDefault: false;
265
+ isPrimaryKey: false;
266
+ isAutoincrement: false;
267
+ hasRuntimeDefault: false;
268
+ enumValues: [string, ...string[]];
269
+ baseColumn: never;
270
+ identity: undefined;
271
+ generated: undefined;
272
+ }, {}, {}>;
273
+ contractUri: drizzle_orm_pg_core.PgColumn<{
274
+ name: "contract_uri";
275
+ tableName: "artifact_collection";
276
+ dataType: "string";
277
+ columnType: "PgText";
278
+ data: string;
279
+ driverParam: string;
280
+ notNull: false;
281
+ hasDefault: false;
282
+ isPrimaryKey: false;
283
+ isAutoincrement: false;
284
+ hasRuntimeDefault: false;
285
+ enumValues: [string, ...string[]];
286
+ baseColumn: never;
287
+ identity: undefined;
288
+ generated: undefined;
289
+ }, {}, {}>;
290
+ description: drizzle_orm_pg_core.PgColumn<{
291
+ name: "description";
292
+ tableName: "artifact_collection";
293
+ dataType: "string";
294
+ columnType: "PgText";
295
+ data: string;
296
+ driverParam: string;
297
+ notNull: false;
298
+ hasDefault: false;
299
+ isPrimaryKey: false;
300
+ isAutoincrement: false;
301
+ hasRuntimeDefault: false;
302
+ enumValues: [string, ...string[]];
303
+ baseColumn: never;
304
+ identity: undefined;
305
+ generated: undefined;
306
+ }, {}, {}>;
307
+ image: drizzle_orm_pg_core.PgColumn<{
308
+ name: "image";
309
+ tableName: "artifact_collection";
310
+ dataType: "string";
311
+ columnType: "PgText";
312
+ data: string;
313
+ driverParam: string;
314
+ notNull: false;
315
+ hasDefault: false;
316
+ isPrimaryKey: false;
317
+ isAutoincrement: false;
318
+ hasRuntimeDefault: false;
319
+ enumValues: [string, ...string[]];
320
+ baseColumn: never;
321
+ identity: undefined;
322
+ generated: undefined;
323
+ }, {}, {}>;
324
+ data: drizzle_orm_pg_core.PgColumn<{
325
+ name: "data";
326
+ tableName: "artifact_collection";
327
+ dataType: "json";
328
+ columnType: "PgJson";
329
+ data: Record<string, unknown>;
330
+ driverParam: unknown;
331
+ notNull: false;
332
+ hasDefault: false;
333
+ isPrimaryKey: false;
334
+ isAutoincrement: false;
335
+ hasRuntimeDefault: false;
336
+ enumValues: undefined;
337
+ baseColumn: never;
338
+ identity: undefined;
339
+ generated: undefined;
340
+ }, {}, {
341
+ $type: Record<string, unknown>;
342
+ }>;
343
+ updatedAt: drizzle_orm_pg_core.PgColumn<{
344
+ name: "updated_at";
345
+ tableName: "artifact_collection";
346
+ dataType: "number";
347
+ columnType: "PgInteger";
348
+ data: number;
349
+ driverParam: string | number;
350
+ notNull: true;
351
+ hasDefault: false;
352
+ isPrimaryKey: false;
353
+ isAutoincrement: false;
354
+ hasRuntimeDefault: false;
355
+ enumValues: undefined;
356
+ baseColumn: never;
357
+ identity: undefined;
358
+ generated: undefined;
359
+ }, {}, {}>;
360
+ };
361
+ dialect: "pg";
362
+ }>;
363
+
364
+ type TokenStandard = "erc721" | "erc1155" | "unknown";
365
+ type ArtifactToken = {
366
+ collection: string;
367
+ tokenId: string;
368
+ tokenStandard: TokenStandard;
369
+ tokenUri: string | null;
370
+ name: string | null;
371
+ description: string | null;
372
+ image: string | null;
373
+ animationUrl: string | null;
374
+ data: Record<string, unknown> | null;
375
+ updatedAt: number;
376
+ };
377
+ type ArtifactCollection = {
378
+ collection: string;
379
+ tokenStandard: TokenStandard;
380
+ name: string | null;
381
+ symbol: string | null;
382
+ owner: string | null;
383
+ contractUri: string | null;
384
+ description: string | null;
385
+ image: string | null;
386
+ data: Record<string, unknown> | null;
387
+ updatedAt: number;
388
+ };
389
+ type ArtifactPluginConfig = {
390
+ /** Viem public client for on-chain reads. */
391
+ client: {
392
+ readContract: (args: {
393
+ address: `0x${string}`;
394
+ abi: readonly unknown[];
395
+ functionName: string;
396
+ args?: readonly unknown[];
397
+ }) => Promise<unknown>;
398
+ };
399
+ /** Drizzle DB instance. Use createOffchainDb() or provide your own. */
400
+ db: any;
401
+ /** Cache TTL in milliseconds. Defaults to 30 days. */
402
+ cacheTtl?: number;
403
+ /** IPFS gateway URL. Defaults to https://ipfs.io/ipfs/ */
404
+ ipfsGateway?: string;
405
+ /** Arweave gateway URL. Defaults to https://arweave.net/ */
406
+ arweaveGateway?: string;
407
+ };
408
+
409
+ declare function createArtifactService(config: ArtifactPluginConfig): {
410
+ fetchToken: (collection: string, tokenId: string) => Promise<ArtifactToken | null>;
411
+ updateToken: (collection: string, tokenId: string) => Promise<void>;
412
+ fetchCollection: (address: string) => Promise<ArtifactCollection | null>;
413
+ updateCollection: (address: string) => Promise<void>;
414
+ isFresh: (timestamp: number | null) => boolean;
415
+ };
416
+
417
+ declare function createArtifactRoutes(config: ArtifactPluginConfig): Hono;
418
+
419
+ declare function createOffchainDb(options?: {
420
+ databaseUrl?: string;
421
+ dataDir?: string;
422
+ }): Promise<{
423
+ db: any;
424
+ }>;
425
+
426
+ interface ResolveUriOptions {
427
+ ipfsGateway?: string;
428
+ arweaveGateway?: string;
429
+ }
430
+ declare function resolveUri(uri?: string | null, options?: ResolveUriOptions): string;
431
+ declare function fetchJson(uri: string, options?: ResolveUriOptions): Promise<Record<string, unknown>>;
432
+
433
+ export { type ArtifactCollection, type ArtifactPluginConfig, type ArtifactToken, type TokenStandard, artifactCollection, artifactToken, createArtifactRoutes, createArtifactService, createOffchainDb, fetchJson, offchainSchema, resolveUri };
package/dist/index.js ADDED
@@ -0,0 +1,376 @@
1
+ // src/schema.ts
2
+ import { pgSchema, text, integer, json, primaryKey } from "drizzle-orm/pg-core";
3
+ var offchainSchema = pgSchema("offchain");
4
+ var artifactToken = offchainSchema.table(
5
+ "artifact_token",
6
+ {
7
+ collection: text("collection").notNull(),
8
+ tokenId: text("token_id").notNull(),
9
+ tokenStandard: text("token_standard").notNull(),
10
+ tokenUri: text("token_uri"),
11
+ name: text("name"),
12
+ description: text("description"),
13
+ image: text("image"),
14
+ animationUrl: text("animation_url"),
15
+ data: json("data").$type(),
16
+ updatedAt: integer("updated_at").notNull()
17
+ },
18
+ (table) => [primaryKey({ columns: [table.collection, table.tokenId] })]
19
+ );
20
+ var artifactCollection = offchainSchema.table(
21
+ "artifact_collection",
22
+ {
23
+ collection: text("collection").primaryKey(),
24
+ tokenStandard: text("token_standard").notNull(),
25
+ name: text("name"),
26
+ symbol: text("symbol"),
27
+ owner: text("owner"),
28
+ contractUri: text("contract_uri"),
29
+ description: text("description"),
30
+ image: text("image"),
31
+ data: json("data").$type(),
32
+ updatedAt: integer("updated_at").notNull()
33
+ }
34
+ );
35
+
36
+ // src/service.ts
37
+ import { eq, and } from "drizzle-orm";
38
+ import { parseAbi } from "viem";
39
+
40
+ // src/uri.ts
41
+ var DEFAULT_IPFS_GATEWAY = "https://ipfs.io/ipfs/";
42
+ var DEFAULT_ARWEAVE_GATEWAY = "https://arweave.net/";
43
+ function resolveUri(uri, options) {
44
+ if (!uri) return "";
45
+ const ipfs = options?.ipfsGateway || DEFAULT_IPFS_GATEWAY;
46
+ const ar = options?.arweaveGateway || DEFAULT_ARWEAVE_GATEWAY;
47
+ if (uri.startsWith("data:")) return uri;
48
+ if (uri.startsWith("ipfs://")) return ipfs + uri.replace("ipfs://", "");
49
+ if (uri.startsWith("ar://")) return ar + uri.replace("ar://", "");
50
+ if (uri.startsWith("Qm") || uri.startsWith("baf")) return ipfs + uri;
51
+ return uri;
52
+ }
53
+ async function fetchJson(uri, options) {
54
+ const resolved = resolveUri(uri, options);
55
+ if (resolved.startsWith("data:application/json;base64,")) {
56
+ const base64Data = resolved.split(",")[1];
57
+ return JSON.parse(atob(base64Data));
58
+ }
59
+ if (resolved.startsWith("data:application/json")) {
60
+ const jsonStr = decodeURIComponent(resolved.split(",")[1]);
61
+ return JSON.parse(jsonStr);
62
+ }
63
+ const response = await fetch(resolved);
64
+ if (!response.ok) {
65
+ throw new Error(`Failed to fetch ${resolved}: ${response.status}`);
66
+ }
67
+ return response.json();
68
+ }
69
+
70
+ // src/service.ts
71
+ var DEFAULT_CACHE_TTL = 30 * 24 * 60 * 60 * 1e3;
72
+ var ERC721_ABI = parseAbi([
73
+ "function tokenURI(uint256 tokenId) view returns (string)",
74
+ "function supportsInterface(bytes4 interfaceId) view returns (bool)"
75
+ ]);
76
+ var ERC1155_ABI = parseAbi([
77
+ "function uri(uint256) view returns (string)",
78
+ "function supportsInterface(bytes4 interfaceId) view returns (bool)"
79
+ ]);
80
+ var COLLECTION_ABI = parseAbi([
81
+ "function name() view returns (string)",
82
+ "function symbol() view returns (string)",
83
+ "function owner() view returns (address)",
84
+ "function contractURI() view returns (string)"
85
+ ]);
86
+ var ERC721_INTERFACE_ID = "0x80ac58cd";
87
+ var ERC1155_INTERFACE_ID = "0xd9b67a26";
88
+ function createArtifactService(config) {
89
+ const { client, db } = config;
90
+ const cacheTtl = config.cacheTtl ?? DEFAULT_CACHE_TTL;
91
+ const uriOptions = {
92
+ ipfsGateway: config.ipfsGateway,
93
+ arweaveGateway: config.arweaveGateway
94
+ };
95
+ function isFresh(timestamp) {
96
+ if (!timestamp) return false;
97
+ return Date.now() - timestamp * 1e3 < cacheTtl;
98
+ }
99
+ async function detectStandard(address) {
100
+ const isERC721 = await client.readContract({
101
+ address,
102
+ abi: ERC721_ABI,
103
+ functionName: "supportsInterface",
104
+ args: [ERC721_INTERFACE_ID]
105
+ }).catch(() => false);
106
+ if (isERC721) return "erc721";
107
+ const isERC1155 = await client.readContract({
108
+ address,
109
+ abi: ERC1155_ABI,
110
+ functionName: "supportsInterface",
111
+ args: [ERC1155_INTERFACE_ID]
112
+ }).catch(() => false);
113
+ if (isERC1155) return "erc1155";
114
+ return "unknown";
115
+ }
116
+ async function fetchTokenUri(address, tokenId, standard) {
117
+ if (standard === "erc1155") {
118
+ const uri = await client.readContract({
119
+ address,
120
+ abi: ERC1155_ABI,
121
+ functionName: "uri",
122
+ args: [tokenId]
123
+ });
124
+ const tokenIdHex = tokenId.toString(16).padStart(64, "0");
125
+ return uri.replace("{id}", tokenIdHex);
126
+ }
127
+ return await client.readContract({
128
+ address,
129
+ abi: ERC721_ABI,
130
+ functionName: "tokenURI",
131
+ args: [tokenId]
132
+ });
133
+ }
134
+ async function fetchToken(collection, tokenId) {
135
+ const result = await db.select().from(artifactToken).where(
136
+ and(
137
+ eq(artifactToken.collection, collection.toLowerCase()),
138
+ eq(artifactToken.tokenId, tokenId)
139
+ )
140
+ ).limit(1);
141
+ return result[0] ?? null;
142
+ }
143
+ async function updateToken(collection, tokenId) {
144
+ const address = collection.toLowerCase();
145
+ const tokenIdBigInt = BigInt(tokenId);
146
+ const tokenStandard = await detectStandard(address);
147
+ const tokenUri = await fetchTokenUri(
148
+ address,
149
+ tokenIdBigInt,
150
+ tokenStandard
151
+ ).catch(() => null);
152
+ let name = null;
153
+ let description = null;
154
+ let image = null;
155
+ let animationUrl = null;
156
+ let data = null;
157
+ if (tokenUri) {
158
+ try {
159
+ const metadata = await fetchJson(tokenUri, uriOptions);
160
+ data = metadata;
161
+ name = metadata.name ?? null;
162
+ description = metadata.description ?? null;
163
+ image = metadata.image ?? null;
164
+ animationUrl = metadata.animation_url ?? null;
165
+ } catch {
166
+ }
167
+ }
168
+ const row = {
169
+ tokenStandard,
170
+ tokenUri,
171
+ name,
172
+ description,
173
+ image,
174
+ animationUrl,
175
+ data,
176
+ updatedAt: Math.floor(Date.now() / 1e3)
177
+ };
178
+ await db.insert(artifactToken).values({
179
+ collection: address,
180
+ tokenId,
181
+ ...row
182
+ }).onConflictDoUpdate({
183
+ target: [artifactToken.collection, artifactToken.tokenId],
184
+ set: row
185
+ });
186
+ }
187
+ async function fetchCollection(address) {
188
+ const result = await db.select().from(artifactCollection).where(eq(artifactCollection.collection, address.toLowerCase())).limit(1);
189
+ return result[0] ?? null;
190
+ }
191
+ async function updateCollection(address) {
192
+ const normalized = address.toLowerCase();
193
+ const [name, symbol, owner, contractUri, tokenStandard] = await Promise.all([
194
+ client.readContract({
195
+ address: normalized,
196
+ abi: COLLECTION_ABI,
197
+ functionName: "name"
198
+ }).catch(() => null),
199
+ client.readContract({
200
+ address: normalized,
201
+ abi: COLLECTION_ABI,
202
+ functionName: "symbol"
203
+ }).catch(() => null),
204
+ client.readContract({
205
+ address: normalized,
206
+ abi: COLLECTION_ABI,
207
+ functionName: "owner"
208
+ }).catch(() => null),
209
+ client.readContract({
210
+ address: normalized,
211
+ abi: COLLECTION_ABI,
212
+ functionName: "contractURI"
213
+ }).catch(() => null),
214
+ detectStandard(normalized)
215
+ ]);
216
+ let description = null;
217
+ let image = null;
218
+ let data = null;
219
+ if (contractUri) {
220
+ try {
221
+ const metadata = await fetchJson(contractUri, uriOptions);
222
+ data = metadata;
223
+ description = metadata.description ?? null;
224
+ image = metadata.image ?? null;
225
+ } catch {
226
+ }
227
+ }
228
+ const row = {
229
+ tokenStandard,
230
+ name: name ?? data?.name ?? null,
231
+ symbol: symbol ?? data?.symbol ?? null,
232
+ owner: owner?.toLowerCase() ?? null,
233
+ contractUri,
234
+ description: description ?? null,
235
+ image: image ?? null,
236
+ data,
237
+ updatedAt: Math.floor(Date.now() / 1e3)
238
+ };
239
+ await db.insert(artifactCollection).values({
240
+ collection: normalized,
241
+ ...row
242
+ }).onConflictDoUpdate({
243
+ target: artifactCollection.collection,
244
+ set: row
245
+ });
246
+ }
247
+ return {
248
+ fetchToken,
249
+ updateToken,
250
+ fetchCollection,
251
+ updateCollection,
252
+ isFresh
253
+ };
254
+ }
255
+
256
+ // src/routes.ts
257
+ import { Hono } from "hono";
258
+ function createArtifactRoutes(config) {
259
+ const {
260
+ fetchToken,
261
+ updateToken,
262
+ fetchCollection,
263
+ updateCollection,
264
+ isFresh
265
+ } = createArtifactService(config);
266
+ const app = new Hono();
267
+ app.get("/token/:collection/:tokenId", async (c) => {
268
+ const collection = c.req.param("collection");
269
+ const tokenId = c.req.param("tokenId");
270
+ const cached = await fetchToken(collection, tokenId);
271
+ if (cached && isFresh(cached.updatedAt)) {
272
+ return c.json(cached);
273
+ }
274
+ try {
275
+ await updateToken(collection, tokenId);
276
+ return c.json(await fetchToken(collection, tokenId));
277
+ } catch {
278
+ if (cached) return c.json(cached);
279
+ return c.json({ error: "Failed to fetch token metadata" }, 500);
280
+ }
281
+ });
282
+ app.post("/token/:collection/:tokenId", async (c) => {
283
+ const collection = c.req.param("collection");
284
+ const tokenId = c.req.param("tokenId");
285
+ try {
286
+ await updateToken(collection, tokenId);
287
+ return c.json(await fetchToken(collection, tokenId));
288
+ } catch {
289
+ const cached = await fetchToken(collection, tokenId);
290
+ if (cached) return c.json(cached);
291
+ return c.json({ error: "Failed to fetch token metadata" }, 500);
292
+ }
293
+ });
294
+ app.get("/collection/:address", async (c) => {
295
+ const address = c.req.param("address");
296
+ const cached = await fetchCollection(address);
297
+ if (cached && isFresh(cached.updatedAt)) {
298
+ return c.json(cached);
299
+ }
300
+ try {
301
+ await updateCollection(address);
302
+ return c.json(await fetchCollection(address));
303
+ } catch {
304
+ if (cached) return c.json(cached);
305
+ return c.json({ error: "Failed to fetch collection metadata" }, 500);
306
+ }
307
+ });
308
+ app.post("/collection/:address", async (c) => {
309
+ const address = c.req.param("address");
310
+ try {
311
+ await updateCollection(address);
312
+ return c.json(await fetchCollection(address));
313
+ } catch {
314
+ const cached = await fetchCollection(address);
315
+ if (cached) return c.json(cached);
316
+ return c.json({ error: "Failed to fetch collection metadata" }, 500);
317
+ }
318
+ });
319
+ return app;
320
+ }
321
+
322
+ // src/db.ts
323
+ var INIT_SQL = `
324
+ CREATE SCHEMA IF NOT EXISTS offchain;
325
+ CREATE TABLE IF NOT EXISTS offchain.artifact_token (
326
+ collection TEXT NOT NULL,
327
+ token_id TEXT NOT NULL,
328
+ token_standard TEXT NOT NULL,
329
+ token_uri TEXT,
330
+ name TEXT,
331
+ description TEXT,
332
+ image TEXT,
333
+ animation_url TEXT,
334
+ data JSON,
335
+ updated_at INTEGER NOT NULL,
336
+ PRIMARY KEY (collection, token_id)
337
+ );
338
+ CREATE TABLE IF NOT EXISTS offchain.artifact_collection (
339
+ collection TEXT PRIMARY KEY,
340
+ token_standard TEXT NOT NULL,
341
+ name TEXT,
342
+ symbol TEXT,
343
+ owner TEXT,
344
+ contract_uri TEXT,
345
+ description TEXT,
346
+ image TEXT,
347
+ data JSON,
348
+ updated_at INTEGER NOT NULL
349
+ );
350
+ `;
351
+ async function createOffchainDb(options) {
352
+ const databaseUrl = options?.databaseUrl ?? process.env.DATABASE_PRIVATE_URL ?? process.env.DATABASE_URL;
353
+ if (databaseUrl) {
354
+ const { default: pg } = await import("pg");
355
+ const { drizzle: drizzle2 } = await import("drizzle-orm/node-postgres");
356
+ const pool = new pg.Pool({ connectionString: databaseUrl });
357
+ await pool.query(INIT_SQL);
358
+ return { db: drizzle2(pool) };
359
+ }
360
+ const dataDir = options?.dataDir ?? ".ponder/artifacts";
361
+ const { PGlite } = await import("@electric-sql/pglite");
362
+ const { drizzle } = await import("drizzle-orm/pglite");
363
+ const client = new PGlite(dataDir);
364
+ await client.exec(INIT_SQL);
365
+ return { db: drizzle(client) };
366
+ }
367
+ export {
368
+ artifactCollection,
369
+ artifactToken,
370
+ createArtifactRoutes,
371
+ createArtifactService,
372
+ createOffchainDb,
373
+ fetchJson,
374
+ offchainSchema,
375
+ resolveUri
376
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@1001-digital/ponder-artifacts",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./dist/index.js",
9
+ "types": "./dist/index.d.ts"
10
+ }
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "peerDependencies": {
16
+ "drizzle-orm": ">=0.38.0",
17
+ "hono": ">=4.0.0",
18
+ "viem": ">=2.0.0",
19
+ "pg": ">=8.0.0",
20
+ "@electric-sql/pglite": ">=0.2.0"
21
+ },
22
+ "peerDependenciesMeta": {
23
+ "pg": {
24
+ "optional": true
25
+ },
26
+ "@electric-sql/pglite": {
27
+ "optional": true
28
+ }
29
+ },
30
+ "devDependencies": {
31
+ "@electric-sql/pglite": "^0.2.13",
32
+ "@types/pg": "^8.11.0",
33
+ "drizzle-orm": "^0.38.0",
34
+ "hono": "^4.5.0",
35
+ "pg": "^8.13.0",
36
+ "tsup": "^8.0.0",
37
+ "typescript": "^5.2.0",
38
+ "viem": "^2.21.0"
39
+ },
40
+ "scripts": {
41
+ "build": "tsup src/index.ts --format esm --dts",
42
+ "dev": "tsup src/index.ts --format esm --dts --watch",
43
+ "typecheck": "tsc --noEmit"
44
+ }
45
+ }