From 14fbafd73d98d1fa74b09fb47661550c0be2f748 Mon Sep 17 00:00:00 2001 From: Georgios Jason Efstathiou Date: Wed, 10 Sep 2025 10:19:34 +0200 Subject: [PATCH 1/3] wip --- src/metadata/schemas/index.ts | 2 ++ src/metadata/schemas/nft-driver/v6.ts | 2 +- src/metadata/schemas/nft-driver/v7.ts | 20 ++++++++++++++++++++ src/metadata/schemas/repo-driver/v6.ts | 12 ++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/metadata/schemas/nft-driver/v7.ts create mode 100644 src/metadata/schemas/repo-driver/v6.ts diff --git a/src/metadata/schemas/index.ts b/src/metadata/schemas/index.ts index 1f3c0fa..49b8d2f 100644 --- a/src/metadata/schemas/index.ts +++ b/src/metadata/schemas/index.ts @@ -12,8 +12,10 @@ import { repoDriverAccountMetadataSchemaV5 } from './repo-driver/v5'; import { nftDriverAccountMetadataSchemaV5 } from './nft-driver/v5'; import { subListMetadataSchemaV1 } from './immutable-splits-driver/v1'; import { nftDriverAccountMetadataSchemaV6 } from './nft-driver/v6'; +import { nftDriverAccountMetadataSchemaV7 } from './nft-driver/v7'; export const nftDriverAccountMetadataParser = createVersionedParser([ + nftDriverAccountMetadataSchemaV7.parse, nftDriverAccountMetadataSchemaV6.parse, nftDriverAccountMetadataSchemaV5.parse, nftDriverAccountMetadataSchemaV4.parse, diff --git a/src/metadata/schemas/nft-driver/v6.ts b/src/metadata/schemas/nft-driver/v6.ts index 2d6a3ed..ba8f699 100644 --- a/src/metadata/schemas/nft-driver/v6.ts +++ b/src/metadata/schemas/nft-driver/v6.ts @@ -31,7 +31,7 @@ const ecosystemVariant = base.extend({ avatar: emojiAvatarSchema, }); -const dripListVariant = base.extend({ +export const dripListVariant = base.extend({ type: z.literal('dripList'), recipients: z.array( z.union([ diff --git a/src/metadata/schemas/nft-driver/v7.ts b/src/metadata/schemas/nft-driver/v7.ts new file mode 100644 index 0000000..6870e98 --- /dev/null +++ b/src/metadata/schemas/nft-driver/v7.ts @@ -0,0 +1,20 @@ +import { z } from 'zod'; +import { + dripListVariant as dripListVariantV6, + nftDriverAccountMetadataSchemaV6, +} from './v6'; +import { orcidSplitReceiverSchema } from '../repo-driver/v6'; + +export const dripListVariantV7 = dripListVariantV6.extend({ + recipients: z.array( + z.union([ + ...dripListVariantV6.shape.recipients._def.type.options, + orcidSplitReceiverSchema, + ]), + ), +}); + +export const nftDriverAccountMetadataSchemaV7 = z.discriminatedUnion('type', [ + nftDriverAccountMetadataSchemaV6._def.options[0], + dripListVariantV7, +]); diff --git a/src/metadata/schemas/repo-driver/v6.ts b/src/metadata/schemas/repo-driver/v6.ts new file mode 100644 index 0000000..b5acd88 --- /dev/null +++ b/src/metadata/schemas/repo-driver/v6.ts @@ -0,0 +1,12 @@ +import z from 'zod'; + +export const orcidSplitReceiverSchema = z.object({ + type: z.literal('orcid'), + weight: z.number(), + accountId: z.string(), + orcidId: z.string(), +}); + +// TODO: actually export new version +// should allow orcidSplitReceiverSchema as a dependency +// for repoDriverAccountSplitsSchema From 80f871b6401e1e4373d543cbf7a568b27869ad37 Mon Sep 17 00:00:00 2001 From: Georgios Jason Efstathiou Date: Wed, 10 Sep 2025 12:51:37 +0200 Subject: [PATCH 2/3] handle orcid receiver --- .../handlers/handleDripListMetadata.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index 8b694cb..63bf1de 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -256,24 +256,24 @@ async function createNewSplitReceivers({ // 3. Persist receivers. const receiverPromises = splitReceivers.map(async (receiver) => { switch (receiver.type) { - case 'repoDriver': + case 'orcid': assertIsRepoDriverId(receiver.accountId); + return createSplitReceiver({ + scopedLogger, + transaction, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'drip_list', + receiverAccountId: receiver.accountId, + receiverAccountType: 'linked_identity', + relationshipType: 'drip_list_receiver', + weight: receiver.weight, + blockTimestamp, + }, + }); - if (receiver.source.forge === 'orcid') { - return createSplitReceiver({ - scopedLogger, - transaction, - splitReceiverShape: { - senderAccountId: emitterAccountId, - senderAccountType: 'drip_list', - receiverAccountId: receiver.accountId, - receiverAccountType: 'linked_identity', - relationshipType: 'drip_list_receiver', - weight: receiver.weight, - blockTimestamp, - }, - }); - } + case 'repoDriver': + assertIsRepoDriverId(receiver.accountId); await ProjectModel.findOrCreate({ transaction, From 6fc14cd2ce67aaf322ba7822e83c1edb9c32b524 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Wed, 10 Sep 2025 15:15:56 +0300 Subject: [PATCH 3/3] refactor: support unclaimed linked identities --- ...-make-linked-identities-owners-nullable.ts | 26 +++++++++++ .../handlers/handleDripListMetadata.ts | 8 ++++ .../handleEcosystemMainAccountMetadata.ts | 8 ++++ .../handlers/handleProjectMetadata.ts | 8 ++++ .../handlers/handleSubListMetadata.ts | 8 ++++ .../processLinkedIdentitySplits.ts | 18 ++++++++ src/models/LinkedIdentityModel.ts | 21 ++++----- src/utils/linkedIdentityUtils.ts | 45 +++++++++++++++++++ 8 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 src/db/migrations/20250910120000-make-linked-identities-owners-nullable.ts create mode 100644 src/utils/linkedIdentityUtils.ts diff --git a/src/db/migrations/20250910120000-make-linked-identities-owners-nullable.ts b/src/db/migrations/20250910120000-make-linked-identities-owners-nullable.ts new file mode 100644 index 0000000..2a10256 --- /dev/null +++ b/src/db/migrations/20250910120000-make-linked-identities-owners-nullable.ts @@ -0,0 +1,26 @@ +import type { QueryInterface } from 'sequelize'; +import getSchema from '../../utils/getSchema'; + +export async function up({ context: sequelize }: any): Promise { + const schema = getSchema(); + const qi: QueryInterface = sequelize.getQueryInterface(); + + await qi.sequelize.query(` + ALTER TABLE ${schema}.linked_identities + ALTER COLUMN owner_address DROP NOT NULL, + ALTER COLUMN owner_account_id DROP NOT NULL, + ALTER COLUMN is_linked SET DEFAULT false; + `); +} + +export async function down({ context: sequelize }: any): Promise { + const schema = getSchema(); + const qi: QueryInterface = sequelize.getQueryInterface(); + + await qi.sequelize.query(` + ALTER TABLE ${schema}.linked_identities + ALTER COLUMN is_linked DROP DEFAULT, + ALTER COLUMN owner_address SET NOT NULL, + ALTER COLUMN owner_account_id SET NOT NULL; + `); +} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index 63bf1de..47d8606 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -31,6 +31,7 @@ import { nftDriverContract, } from '../../../core/contractClients'; import { ProjectModel } from '../../../models'; +import { ensureLinkedIdentityExists } from '../../../utils/linkedIdentityUtils'; type Params = { ipfsHash: IpfsHash; @@ -258,6 +259,13 @@ async function createNewSplitReceivers({ switch (receiver.type) { case 'orcid': assertIsRepoDriverId(receiver.accountId); + await ensureLinkedIdentityExists( + receiver.accountId, + { blockNumber, logIndex }, + transaction, + scopedLogger, + ); + return createSplitReceiver({ scopedLogger, transaction, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index 8a049c0..79572d0 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -35,6 +35,7 @@ import { } from '../../../utils/lastProcessedVersion'; import type { repoSubAccountDriverSplitReceiverSchema } from '../../../metadata/schemas/common/repoSubAccountDriverSplitReceiverSchema'; import type { gitHubSourceSchema } from '../../../metadata/schemas/common/sources'; +import { ensureLinkedIdentityExists } from '../../../utils/linkedIdentityUtils'; type Params = { ipfsHash: IpfsHash; @@ -229,6 +230,13 @@ async function createNewSplitReceivers({ const repoDriverId = await calcParentRepoDriverId(receiver.accountId); if (receiver.source.forge === 'orcid') { + await ensureLinkedIdentityExists( + repoDriverId, + { blockNumber, logIndex }, + transaction, + scopedLogger, + ); + return createSplitReceiver({ scopedLogger, transaction, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts index ea72c4c..84153d3 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts @@ -26,6 +26,7 @@ import { import { makeVersion } from '../../../utils/lastProcessedVersion'; import RecoverableError from '../../../utils/recoverableError'; import type { gitHubSourceSchema } from '../../../metadata/schemas/common/sources'; +import { ensureLinkedIdentityExists } from '../../../utils/linkedIdentityUtils'; type Params = { logIndex: number; @@ -251,6 +252,13 @@ async function createNewSplitReceivers({ } if (dependency.source.forge === 'orcid') { + await ensureLinkedIdentityExists( + dependency.accountId, + { blockNumber, logIndex }, + transaction, + scopedLogger, + ); + return createSplitReceiver({ scopedLogger, transaction, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index bc02688..4554c9e 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -34,6 +34,7 @@ import { import { makeVersion } from '../../../utils/lastProcessedVersion'; import type { repoSubAccountDriverSplitReceiverSchema } from '../../../metadata/schemas/common/repoSubAccountDriverSplitReceiverSchema'; import type { gitHubSourceSchema } from '../../../metadata/schemas/common/sources'; +import { ensureLinkedIdentityExists } from '../../../utils/linkedIdentityUtils'; type Params = { logIndex: number; @@ -205,6 +206,13 @@ async function createNewSplitReceivers({ const repoDriverId = await calcParentRepoDriverId(receiver.accountId); if (receiver.source.forge === 'orcid') { + await ensureLinkedIdentityExists( + repoDriverId, + { blockNumber, logIndex }, + transaction, + scopedLogger, + ); + return createSplitReceiver({ scopedLogger, transaction, diff --git a/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts b/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts index 1944d48..71c3a5c 100644 --- a/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts +++ b/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts @@ -41,6 +41,24 @@ export async function processLinkedIdentitySplits( ); } + if (!linkedIdentity.ownerAccountId) { + scopedLogger.bufferMessage( + `ORCID account ${accountId} has no owner set yet. Skipping validation and splits creation.`, + ); + + linkedIdentity.isLinked = false; + + scopedLogger.bufferUpdate({ + type: LinkedIdentityModel, + id: linkedIdentity.accountId, + input: linkedIdentity, + }); + + await linkedIdentity.save({ transaction }); + + return; + } + const isLinked = await validateLinkedIdentity( accountId, linkedIdentity.ownerAccountId, diff --git a/src/models/LinkedIdentityModel.ts b/src/models/LinkedIdentityModel.ts index 30a5c26..aa09ac1 100644 --- a/src/models/LinkedIdentityModel.ts +++ b/src/models/LinkedIdentityModel.ts @@ -20,14 +20,14 @@ export default class LinkedIdentityModel extends Model< InferAttributes, InferCreationAttributes > { - declare public accountId: RepoDriverId; - declare public identityType: LinkedIdentityType; - declare public ownerAddress: Address; - declare public ownerAccountId: AddressDriverId; - declare public isLinked: boolean; - declare public lastProcessedVersion: string; - declare public createdAt: CreationOptional; - declare public updatedAt: CreationOptional; + public declare accountId: RepoDriverId; + public declare identityType: LinkedIdentityType; + public declare ownerAddress: Address | null; + public declare ownerAccountId: AddressDriverId | null; + public declare isLinked: boolean; + public declare lastProcessedVersion: string; + public declare createdAt: CreationOptional; + public declare updatedAt: CreationOptional; public static initialize(sequelize: Sequelize): void { this.init( @@ -41,16 +41,17 @@ export default class LinkedIdentityModel extends Model< type: DataTypes.ENUM(...LINKED_IDENTITY_TYPES), }, ownerAddress: { - allowNull: false, + allowNull: true, type: DataTypes.STRING, }, ownerAccountId: { - allowNull: false, + allowNull: true, type: DataTypes.STRING, }, isLinked: { allowNull: false, type: DataTypes.BOOLEAN, + defaultValue: false, }, lastProcessedVersion: { allowNull: false, diff --git a/src/utils/linkedIdentityUtils.ts b/src/utils/linkedIdentityUtils.ts new file mode 100644 index 0000000..929af4b --- /dev/null +++ b/src/utils/linkedIdentityUtils.ts @@ -0,0 +1,45 @@ +import type { Transaction } from 'sequelize'; +import type ScopedLogger from '../core/ScopedLogger'; +import LinkedIdentityModel from '../models/LinkedIdentityModel'; +import type { RepoDriverId } from '../core/types'; +import { isOrcidAccount } from './accountIdUtils'; +import { makeVersion } from './lastProcessedVersion'; + +export async function ensureLinkedIdentityExists( + accountId: RepoDriverId, + ctx: { blockNumber: number; logIndex: number }, + transaction: Transaction, + scopedLogger: ScopedLogger, +): Promise { + if (!isOrcidAccount(accountId)) { + throw new Error( + `${ensureLinkedIdentityExists.name} called with non-ORCID accountId: ${accountId}`, + ); + } + + const [identity, isCreation] = await LinkedIdentityModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { accountId }, + // Creates an "unclaimed" linked identity. + defaults: { + accountId, + identityType: 'orcid', + ownerAddress: null, + ownerAccountId: null, + isLinked: false, + lastProcessedVersion: makeVersion( + ctx.blockNumber, + ctx.logIndex, + ).toString(), + }, + }); + + if (isCreation) { + scopedLogger.bufferCreation({ + type: LinkedIdentityModel, + input: identity, + id: identity.accountId, + }); + } +}