diff --git a/db/migrations/1735226891776-AddNo Affiliation.js b/db/migrations/1735226891776-AddNo Affiliation.js new file mode 100644 index 0000000..2df9803 --- /dev/null +++ b/db/migrations/1735226891776-AddNo Affiliation.js @@ -0,0 +1,28 @@ +const ethers = require("ethers"); +const easSdk = require("@ethereum-attestation-service/eas-sdk"); + +const ZERO_UID = easSdk.ZERO_BYTES32; +module.exports = class AddNoAffiliation1735226891776 { + name = "AddNoAffiliation1735226891776"; + + async up(db) { + // Add organisation with name "No Affiliation" + await db.query( + `INSERT INTO "organisation" ("id", "name", "issuer", "color", "start_block") + VALUES ( + '${ZERO_UID}', + 'No Affiliation', + '${ethers.ZeroAddress}', + '#0049b7', + null + )` + ); + } + + async down(db) { + // Remove organisation with name "No Affiliation" + await db.query( + `DELETE FROM "organisation" WHERE "id" = '${ZERO_UID}'` + ); + } +}; diff --git a/src/constants.ts b/src/constants.ts index 664cf61..b01a53c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,5 @@ +import { ZERO_BYTES32 } from "@ethereum-attestation-service/eas-sdk"; import { assertNotNull } from "@subsquid/evm-processor"; -import { map } from "zod"; const SQUID_NETWORK = process.env.SQUID_NETWORK || "eth-sepolia"; @@ -39,3 +39,5 @@ export const DESCRIPTION_SUMMARY_LENGTH = Number( ); export const AGORA_API_KEY = process.env.AGORA_API_KEY; + +export const ZERO_UID = ZERO_BYTES32; diff --git a/src/controllers/projectVerificationAttestation.ts b/src/controllers/projectVerificationAttestation.ts index 631a56a..dfe5c9f 100644 --- a/src/controllers/projectVerificationAttestation.ts +++ b/src/controllers/projectVerificationAttestation.ts @@ -5,8 +5,10 @@ import { getAttestationData, removeDuplicateProjectAttestations, } from "./utils/easHelper"; -import { AttestorOrganisation, ProjectAttestation } from "../model"; +import { ProjectAttestation } from "../model"; import { + getAttestor, + getOrCreateAttestorOrganisation, getProject, updateProjectAttestationCounts, } from "./utils/modelHelper"; @@ -16,12 +18,10 @@ export const handleProjectAttestation = async ( ctx: DataHandlerContext, log: Log ): Promise => { - const { - uid, - schema: schemaUid, - attestor: issuer, - recipient, - } = EASContract.events.Attested.decode(log); + const attestedEvent = EASContract.events.Attested.decode(log); + + const { uid, schema: schemaUid, recipient } = attestedEvent; + const issuer = attestedEvent.attestor.toLowerCase(); const { decodedData, refUID } = await getAttestationData( ctx, @@ -30,15 +30,14 @@ export const handleProjectAttestation = async ( schemaUid ); - const attestorOrganisation = await ctx.store.get(AttestorOrganisation, { - where: { - id: refUID.toLowerCase(), - }, - relations: { - organisation: true, - attestor: true, - }, - }); + const attestor = await getAttestor(ctx, issuer); + + const attestorOrganisation = await getOrCreateAttestorOrganisation( + ctx, + attestor, + refUID, + new Date(log.block.timestamp) + ); if (!attestorOrganisation) { ctx.log.debug( diff --git a/src/controllers/utils/modelHelper.ts b/src/controllers/utils/modelHelper.ts index ce26bd0..768b33d 100644 --- a/src/controllers/utils/modelHelper.ts +++ b/src/controllers/utils/modelHelper.ts @@ -2,12 +2,14 @@ import { DataHandlerContext } from "@subsquid/evm-processor"; import { Store } from "@subsquid/typeorm-store"; import { Attestor, + AttestorOrganisation, Organisation, OrganisationProject, Project, } from "../../model"; import { getEntityMangerByContext } from "./databaseHelper"; import { ProjectStats } from "./types"; +import { ZERO_UID } from "../../constants"; export const upsertOrganisatoinProject = async ( ctx: DataHandlerContext, @@ -182,3 +184,52 @@ export const getProjectStats = async ( return ProjectStats.parse(result[0]); }; + +export const getOrCreateAttestorOrganisation = async ( + ctx: DataHandlerContext, + attestor: Attestor, + refUID?: string, + attestTimestamp?: Date +): Promise => { + // Check if refUID is valid and not a placeholder for an empty reference + if (refUID && refUID !== ZERO_UID) { + // Attempt to find existing AttestorOrganisation + const attestorOrganisation = await ctx.store.get(AttestorOrganisation, { + where: { id: refUID.toLowerCase() }, + relations: { organisation: true, attestor: true }, + }); + + if (attestorOrganisation) { + ctx.log.debug( + `Found existing attestorOrganisation: ${attestorOrganisation}` + ); + return attestorOrganisation; + } + } + + // Attempt to retrieve default organisation + let organisation = await ctx.store.get(Organisation, ZERO_UID); + + if (!organisation) { + ctx.log.error("No Affiliation organisation not found"); + return undefined; + } + + // Ensure timestamp is set + const timestamp = attestTimestamp || new Date(); + + // Generate unique key + const key = `NO_AFFILIATION${attestor.id}`; + + const newAttestorOrganisation = new AttestorOrganisation({ + id: key, + attestor, + organisation, + attestTimestamp: timestamp, + }); + + await ctx.store.upsert(newAttestorOrganisation); + ctx.log.debug(`Created new attestorOrganisation: ${newAttestorOrganisation}`); + + return newAttestorOrganisation; +}; diff --git a/src/controllers/utils/types.test.ts b/src/controllers/utils/types.test.ts index 24b8c28..f54a267 100644 --- a/src/controllers/utils/types.test.ts +++ b/src/controllers/utils/types.test.ts @@ -1,6 +1,6 @@ import { orgCountTuplesTypes } from "./types"; -describe.only("parse database query", () => { +describe("parse database query", () => { it("should parse project stats query", () => { const raw = `{"(0x2e22df9a11e06c306ed8f64ca45ceae02efcf8a443371395a78371bc4fb6f722,1)","(0xf63f2a7159ee674aa6fce42196a8bb0605eafcf20c19e91a7eafba8d39fa0404,1)"}`; diff --git a/src/test/store.test.ts b/src/test/store.test.ts index 4d4ac09..194363f 100644 --- a/src/test/store.test.ts +++ b/src/test/store.test.ts @@ -1,13 +1,17 @@ import { describe, expect, test, afterAll } from "@jest/globals"; -import { closeConnection, getTestCtx } from "./utils"; +import { closeConnection, getTestCtx, getTestEntityManager } from "./utils"; import { Organisation } from "../model"; describe("simple storage", () => { + beforeAll(async () => { + const em = await getTestEntityManager(); + await em.getRepository(Organisation).delete({}); + }); afterAll(async () => { await closeConnection(); }); - test("sample authorized attest", async () => { + test("should save sample authorized attest", async () => { const ctx = await getTestCtx(); const organization = new Organisation({ id: "schemaUid", @@ -24,4 +28,23 @@ describe("simple storage", () => { expect(fetchOrganization).toBeDefined(); expect(fetchOrganization?.name).toBe("name"); }); + + test("should not fail on upserting the same entity", async () => { + const ctx = await getTestCtx(); + const organization = new Organisation({ + id: "schemaUid", + name: "name", + issuer: "issuer", + }); + + await ctx.store.upsert(organization); + await ctx.store.upsert(organization); + + const fetchOrganization = await ctx.store.findOneBy(Organisation, { + name: "name", + }); + + expect(fetchOrganization).toBeDefined(); + expect(fetchOrganization?.name).toBe("name"); + }); });