-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
295 additions
and
4 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
PRAGMA foreign_keys=OFF;--> statement-breakpoint | ||
CREATE TABLE `__new_links` ( | ||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, | ||
`url` text NOT NULL, | ||
`used` integer DEFAULT false NOT NULL, | ||
`claimed` integer DEFAULT false NOT NULL | ||
); | ||
--> statement-breakpoint | ||
INSERT INTO `__new_links`("id", "url", "used", "claimed") SELECT "id", "url", "used", "claimed" FROM `links`;--> statement-breakpoint | ||
DROP TABLE `links`;--> statement-breakpoint | ||
ALTER TABLE `__new_links` RENAME TO `links`;--> statement-breakpoint | ||
PRAGMA foreign_keys=ON; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
{ | ||
"version": "6", | ||
"dialect": "sqlite", | ||
"id": "38267cf7-0850-41d1-8206-8c8278ec9898", | ||
"prevId": "34a4f4db-562d-405f-bc7f-0ed2208242c1", | ||
"tables": { | ||
"links": { | ||
"name": "links", | ||
"columns": { | ||
"id": { | ||
"name": "id", | ||
"type": "integer", | ||
"primaryKey": true, | ||
"notNull": true, | ||
"autoincrement": true | ||
}, | ||
"url": { | ||
"name": "url", | ||
"type": "text", | ||
"primaryKey": false, | ||
"notNull": true, | ||
"autoincrement": false | ||
}, | ||
"used": { | ||
"name": "used", | ||
"type": "integer", | ||
"primaryKey": false, | ||
"notNull": true, | ||
"autoincrement": false, | ||
"default": false | ||
}, | ||
"claimed": { | ||
"name": "claimed", | ||
"type": "integer", | ||
"primaryKey": false, | ||
"notNull": true, | ||
"autoincrement": false, | ||
"default": false | ||
} | ||
}, | ||
"indexes": {}, | ||
"foreignKeys": {}, | ||
"compositePrimaryKeys": {}, | ||
"uniqueConstraints": {}, | ||
"checkConstraints": {} | ||
} | ||
}, | ||
"views": {}, | ||
"enums": {}, | ||
"_meta": { | ||
"schemas": {}, | ||
"tables": {}, | ||
"columns": {} | ||
}, | ||
"internal": { | ||
"indexes": {} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import { and, eq } from "drizzle-orm"; | ||
import { db } from "./db"; | ||
import { links, links as linksTable } from "./db/schema"; | ||
import { batcher } from "./util/batch"; | ||
import { isClaimed, type ClaimResponse } from "./util/poap"; | ||
import { logger as baseLogger } from "./util/logger"; | ||
import { err, fromThrowable, ok, type Result } from "neverthrow"; | ||
|
||
const extractId = (link: string): Result<string, Error> => { | ||
// error if not matching /mint/ID | ||
const url = fromThrowable( | ||
() => new URL(link), | ||
(cause) => new Error("Failed to parse link", { cause }) | ||
)(); | ||
|
||
if (url.isErr()) { | ||
return err(url.error); | ||
} | ||
|
||
const match = url.value.pathname.match(/^\/mint\/(.+)$/); | ||
if (!match || match.length < 2) { | ||
return err(new Error(`Invalid link: ${link}`)); | ||
} | ||
|
||
// Return matching group 1 | ||
return ok(match[1]); | ||
}; | ||
|
||
export const checkClaims = async ( | ||
logger: typeof baseLogger = baseLogger, | ||
emitter?: (message: string) => void | ||
) => { | ||
const emit = emitter ?? (() => {}); | ||
|
||
logger.info("Checking POAP claims"); | ||
emit("Checking POAP claims"); | ||
|
||
const usedLinks = await db.query.links.findMany({ | ||
where: eq(linksTable.claimed, false), | ||
}); | ||
|
||
const batches = batcher(usedLinks, 10); | ||
const results: { | ||
linkId: number; | ||
poapId: string | null; | ||
result: Result<ClaimResponse, Error>; | ||
}[] = []; | ||
|
||
let i = 0; | ||
for (const batch of batches) { | ||
i++; | ||
logger.trace( | ||
{ | ||
batch: i, | ||
total: batches.length, | ||
}, | ||
`Checking batch` | ||
); | ||
emit(`Checking batch ${i} of ${batches.length}`); | ||
|
||
const batchResults = await Promise.all( | ||
batch.map(async (link) => { | ||
const poapId = extractId(link.url); | ||
if (poapId.isErr()) { | ||
return { | ||
linkId: link.id, | ||
poapId: null, | ||
result: err(poapId.error), | ||
}; | ||
} | ||
const result = await isClaimed(poapId.value); | ||
return { | ||
linkId: link.id, | ||
poapId: poapId.value, | ||
result, | ||
}; | ||
}) | ||
); | ||
|
||
results.push(...batchResults); | ||
|
||
logger.trace( | ||
{ | ||
batch: i, | ||
total: batches.length, | ||
failed: batchResults.filter(({ result }) => result.isErr()) | ||
.length, | ||
}, | ||
`Batch completed` | ||
); | ||
emit(`Batch ${i} completed`); | ||
} | ||
|
||
logger.info( | ||
{ | ||
total: results.length, | ||
failed: results.filter(({ result }) => result.isErr()).length, | ||
}, | ||
"All batches completed" | ||
); | ||
emit(`All batches completed`); | ||
|
||
let unclaimed: number[] = []; | ||
|
||
for (const { linkId, poapId, result } of results) { | ||
if (result.isErr()) { | ||
logger.error( | ||
{ linkId, poapId, error: `${result.error}` }, | ||
"Failed to check claim" | ||
); | ||
emit(`${linkId}: Failed to check claim: ${result.error}`); | ||
} else { | ||
if (result.value.claimed) { | ||
logger.trace({ linkId, poapId }, "POAP has been Claimed"); | ||
emit(`${linkId}: POAP has been Claimed`); | ||
|
||
await db | ||
.update(linksTable) | ||
.set({ claimed: true }) | ||
.where(eq(linksTable.id, linkId)); | ||
} else { | ||
logger.trace({ linkId, poapId }, "POAP has not been Claimed"); | ||
unclaimed.push(linkId); | ||
|
||
await db | ||
.update(linksTable) | ||
.set({ claimed: false, used: false }) | ||
.where(eq(linksTable.id, linkId)); | ||
} | ||
} | ||
} | ||
|
||
emit(`Unclaimed POAPs (${unclaimed.length}): ${unclaimed.join(", ")}`); | ||
|
||
emit(`All POAPs checked`); | ||
|
||
return results; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export const batcher = <T>(items: T[], batchSize: number) => { | ||
const batches = []; | ||
for (let i = 0; i < items.length; i += batchSize) { | ||
batches.push(items.slice(i, i + batchSize)); | ||
} | ||
return batches; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { errAsync, fromPromise, type ResultAsync } from "neverthrow"; | ||
|
||
export type ClaimResponse = { | ||
claimed: boolean; | ||
}; | ||
|
||
export const isClaimed = (id: string): ResultAsync<ClaimResponse, Error> => { | ||
return fromPromise( | ||
fetch(`https://frontend.poap.tech/actions/claim-qr?qr_hash=${id}`, { | ||
headers: { | ||
origin: "https://collectors.poap.xyz", | ||
}, | ||
}), | ||
(e) => | ||
new Error(`Failed to fetch POAP claim status`, { | ||
cause: e, | ||
}) | ||
).andThen((response) => { | ||
if (!response.ok) { | ||
return errAsync( | ||
new Error( | ||
`Failed to fetch POAP claim status: ${response.statusText}` | ||
) | ||
); | ||
} | ||
|
||
return fromPromise( | ||
response.json() as Promise<ClaimResponse>, | ||
(e) => | ||
new Error(`Failed to parse POAP claim status`, { | ||
cause: e, | ||
}) | ||
); | ||
}); | ||
}; |