From d3bbf4ddf245780475ac55eeb59609dc365a02fc Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 18 Aug 2025 14:56:40 +0200 Subject: [PATCH 001/155] Create method to check if string is ORCID --- src/lib/utils/is-orcid/is-orcid.ts | 50 ++++++++++++++++++++ src/lib/utils/is-orcid/is-orcid.unit.test.ts | 24 ++++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/lib/utils/is-orcid/is-orcid.ts create mode 100644 src/lib/utils/is-orcid/is-orcid.unit.test.ts diff --git a/src/lib/utils/is-orcid/is-orcid.ts b/src/lib/utils/is-orcid/is-orcid.ts new file mode 100644 index 000000000..a3e4ef3dc --- /dev/null +++ b/src/lib/utils/is-orcid/is-orcid.ts @@ -0,0 +1,50 @@ +/** + * Validates an ORCID iD. + * + * An ORCID iD is a 16-character string that follows a specific structure: + * - It consists of 15 digits followed by a check digit (0-9 or 'X'). + * - It is often formatted with hyphens, e.g., "0000-0002-1825-0097". + * - The validation uses the ISO 7064 11,2 checksum algorithm. + * + * @param {string} orcid The ORCID iD string to validate. + * @returns {boolean} True if the ORCID iD is valid, false otherwise. + */ +export default function isValidOrcid(orcid: string): boolean { + if (typeof orcid !== 'string') { + return false; + } + + // Remove hyphens and whitespace to get the base 16 characters. + const baseStr: string = orcid.replace(/[-\s]/g, ''); + + // An ORCID must be 16 characters long and match the pattern: + // 15 digits followed by a final character that is a digit or 'X'. + const orcidPattern: RegExp = /^\d{15}[\dX]$/; + if (!orcidPattern.test(baseStr.toUpperCase())) { + return false; + } + + // --- Checksum Calculation (ISO 7064 11,2) --- + + let total: number = 0; + // Iterate over the first 15 digits of the ORCID. + for (let i = 0; i < 15; i++) { + const digit: number = parseInt(baseStr[i], 10); + total = (total + digit) * 2; + } + + // Calculate the remainder when divided by 11. + const remainder: number = total % 11; + // Subtract the remainder from 12. + const result: number = (12 - remainder) % 11; + + // Determine the correct check digit from the result. + // If the result is 10, the check digit is 'X'. Otherwise, it's the digit itself. + const calculatedCheckDigit: string = result === 10 ? 'X' : String(result); + + // Get the actual check digit from the input string. + const actualCheckDigit: string = baseStr.charAt(15).toUpperCase(); + + // Compare the calculated check digit with the actual one. + return calculatedCheckDigit === actualCheckDigit; +} diff --git a/src/lib/utils/is-orcid/is-orcid.unit.test.ts b/src/lib/utils/is-orcid/is-orcid.unit.test.ts new file mode 100644 index 000000000..8e5c2ff55 --- /dev/null +++ b/src/lib/utils/is-orcid/is-orcid.unit.test.ts @@ -0,0 +1,24 @@ +import isValidOrcid from './is-orcid'; + +describe('is-orcid.ts', () => { + it('returns false for non-string input', () => { + expect(isValidOrcid(5 as unknown as string)).toBe(false); + }); + + it('returns false invalid formats', () => { + expect(isValidOrcid('Hello World')).toBe(false); + }); + + it('returns false for incorrect X position', () => { + expect(isValidOrcid('0000-0001-5109-370X')).toBe(false); + }); + + it('returns false for incorrect checksums', () => { + expect(isValidOrcid('0000-0002-1825-0098')).toBe(false); + }); + + it('returns true for valid ORCIDs', () => { + expect(isValidOrcid('0000-0002-1825-0097')).toBe(true); + expect(isValidOrcid('0000-0002-9079-593X')).toBe(true); + }); +}); From 50a9fb5eeb8becbe27f8ced43ee9ff8509406ad6 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 18 Aug 2025 15:26:35 +0200 Subject: [PATCH 002/155] Base logic for fetching orcid info; renaming to orcidId --- .../is-orcid-id.ts} | 6 +- .../is-orcid-id/is-orcid-id.unit.test.ts | 24 ++ src/lib/utils/is-orcid/is-orcid.unit.test.ts | 24 -- .../utils/sdk/repo-driver/repo-driver-abi.ts | 276 ++++++++++++------ src/lib/utils/sdk/sdk-types.ts | 1 + .../(app)/orcids/[orcidId]/+page.server.ts | 18 ++ .../app/(app)/orcids/[orcidId]/+page.svelte | 0 .../[orcidId]/components/fetch-orcid.ts | 43 +++ .../[orcidId]/components/orcid-profile.svelte | 54 ++++ 9 files changed, 333 insertions(+), 113 deletions(-) rename src/lib/utils/{is-orcid/is-orcid.ts => is-orcid-id/is-orcid-id.ts} (91%) create mode 100644 src/lib/utils/is-orcid-id/is-orcid-id.unit.test.ts delete mode 100644 src/lib/utils/is-orcid/is-orcid.unit.test.ts create mode 100644 src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts create mode 100644 src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.svelte create mode 100644 src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts create mode 100644 src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte diff --git a/src/lib/utils/is-orcid/is-orcid.ts b/src/lib/utils/is-orcid-id/is-orcid-id.ts similarity index 91% rename from src/lib/utils/is-orcid/is-orcid.ts rename to src/lib/utils/is-orcid-id/is-orcid-id.ts index a3e4ef3dc..a2880390a 100644 --- a/src/lib/utils/is-orcid/is-orcid.ts +++ b/src/lib/utils/is-orcid-id/is-orcid-id.ts @@ -9,13 +9,13 @@ * @param {string} orcid The ORCID iD string to validate. * @returns {boolean} True if the ORCID iD is valid, false otherwise. */ -export default function isValidOrcid(orcid: string): boolean { - if (typeof orcid !== 'string') { +export default function isValidOrcidId(orcidId: string): boolean { + if (typeof orcidId !== 'string') { return false; } // Remove hyphens and whitespace to get the base 16 characters. - const baseStr: string = orcid.replace(/[-\s]/g, ''); + const baseStr: string = orcidId.replace(/[-\s]/g, ''); // An ORCID must be 16 characters long and match the pattern: // 15 digits followed by a final character that is a digit or 'X'. diff --git a/src/lib/utils/is-orcid-id/is-orcid-id.unit.test.ts b/src/lib/utils/is-orcid-id/is-orcid-id.unit.test.ts new file mode 100644 index 000000000..d74f995d3 --- /dev/null +++ b/src/lib/utils/is-orcid-id/is-orcid-id.unit.test.ts @@ -0,0 +1,24 @@ +import isValidOrcidId from './is-orcid-id'; + +describe('is-orcid.ts', () => { + it('returns false for non-string input', () => { + expect(isValidOrcidId(5 as unknown as string)).toBe(false); + }); + + it('returns false invalid formats', () => { + expect(isValidOrcidId('Hello World')).toBe(false); + }); + + it('returns false for incorrect X position', () => { + expect(isValidOrcidId('0000-0001-5109-370X')).toBe(false); + }); + + it('returns false for incorrect checksums', () => { + expect(isValidOrcidId('0000-0002-1825-0098')).toBe(false); + }); + + it('returns true for valid ORCIDs', () => { + expect(isValidOrcidId('0000-0002-1825-0097')).toBe(true); + expect(isValidOrcidId('0000-0002-9079-593X')).toBe(true); + }); +}); diff --git a/src/lib/utils/is-orcid/is-orcid.unit.test.ts b/src/lib/utils/is-orcid/is-orcid.unit.test.ts deleted file mode 100644 index 8e5c2ff55..000000000 --- a/src/lib/utils/is-orcid/is-orcid.unit.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import isValidOrcid from './is-orcid'; - -describe('is-orcid.ts', () => { - it('returns false for non-string input', () => { - expect(isValidOrcid(5 as unknown as string)).toBe(false); - }); - - it('returns false invalid formats', () => { - expect(isValidOrcid('Hello World')).toBe(false); - }); - - it('returns false for incorrect X position', () => { - expect(isValidOrcid('0000-0001-5109-370X')).toBe(false); - }); - - it('returns false for incorrect checksums', () => { - expect(isValidOrcid('0000-0002-1825-0098')).toBe(false); - }); - - it('returns true for valid ORCIDs', () => { - expect(isValidOrcid('0000-0002-1825-0097')).toBe(true); - expect(isValidOrcid('0000-0002-9079-593X')).toBe(true); - }); -}); diff --git a/src/lib/utils/sdk/repo-driver/repo-driver-abi.ts b/src/lib/utils/sdk/repo-driver/repo-driver-abi.ts index c35230ab0..4a1b2dacd 100644 --- a/src/lib/utils/sdk/repo-driver/repo-driver-abi.ts +++ b/src/lib/utils/sdk/repo-driver/repo-driver-abi.ts @@ -4,16 +4,15 @@ export const repoDriverAbi = [ { internalType: 'contract Drips', name: 'drips_', type: 'address' }, { internalType: 'address', name: 'forwarder', type: 'address' }, { internalType: 'uint32', name: 'driverId_', type: 'uint32' }, + { + internalType: 'contract IAutomate', + name: 'gelatoAutomate_', + type: 'address', + }, ], stateMutability: 'nonpayable', type: 'constructor', }, - { inputs: [], name: 'InvalidShortString', type: 'error' }, - { - inputs: [{ internalType: 'string', name: 'str', type: 'string' }], - name: 'StringTooLong', - type: 'error', - }, { anonymous: false, inputs: [ @@ -38,37 +37,74 @@ export const repoDriverAbi = [ inputs: [ { indexed: true, - internalType: 'contract OperatorInterface', - name: 'operator', + internalType: 'address', + name: 'beacon', type: 'address', }, + ], + name: 'BeaconUpgraded', + type: 'event', + }, + { + anonymous: false, + inputs: [ { indexed: true, - internalType: 'bytes32', - name: 'jobId', - type: 'bytes32', + internalType: 'address', + name: 'user', + type: 'address', }, { indexed: false, - internalType: 'uint96', - name: 'defaultFee', - type: 'uint96', + internalType: 'uint256', + name: 'userFundsUsed', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'commonFundsUsed', + type: 'uint256', }, ], - name: 'AnyApiOperatorUpdated', + name: 'GelatoFeePaid', type: 'event', }, { anonymous: false, inputs: [ { - indexed: true, - internalType: 'address', - name: 'beacon', + indexed: false, + internalType: 'contract GelatoTasksOwner', + name: 'gelatoTasksOwner', type: 'address', }, + { + indexed: false, + internalType: 'bytes32', + name: 'taskId', + type: 'bytes32', + }, + { + indexed: false, + internalType: 'string', + name: 'ipfsCid', + type: 'string', + }, + { + indexed: false, + internalType: 'uint32', + name: 'maxRequestsPerBlock', + type: 'uint32', + }, + { + indexed: false, + internalType: 'uint32', + name: 'maxRequestsPer31Days', + type: 'uint32', + }, ], - name: 'BeaconUpgraded', + name: 'GelatoTaskUpdated', type: 'event', }, { @@ -111,6 +147,12 @@ export const repoDriverAbi = [ name: 'name', type: 'bytes', }, + { + indexed: false, + internalType: 'address', + name: 'payer', + type: 'address', + }, ], name: 'OwnerUpdateRequested', type: 'event', @@ -211,6 +253,50 @@ export const repoDriverAbi = [ name: 'Upgraded', type: 'event', }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'user', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'UserFundsDeposited', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'user', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'address payable', + name: 'receiver', + type: 'address', + }, + ], + name: 'UserFundsWithdrawn', + type: 'event', + }, { inputs: [], name: 'acceptAdmin', @@ -238,21 +324,6 @@ export const repoDriverAbi = [ stateMutability: 'view', type: 'function', }, - { - inputs: [], - name: 'anyApiOperator', - outputs: [ - { - internalType: 'contract OperatorInterface', - name: 'operator', - type: 'address', - }, - { internalType: 'bytes32', name: 'jobId', type: 'bytes32' }, - { internalType: 'uint96', name: 'defaultFee', type: 'uint96' }, - ], - stateMutability: 'view', - type: 'function', - }, { inputs: [ { internalType: 'enum Forge', name: 'forge', type: 'uint8' }, @@ -274,6 +345,20 @@ export const repoDriverAbi = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [], + name: 'commonFunds', + outputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'user', type: 'address' }], + name: 'depositUserFunds', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, { inputs: [], name: 'drips', @@ -306,6 +391,26 @@ export const repoDriverAbi = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [], + name: 'gelatoAutomate', + outputs: [{ internalType: 'contract IAutomate', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'gelatoTasksOwner', + outputs: [ + { + internalType: 'contract GelatoTasksOwner', + name: 'tasksOwner', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [ { internalType: 'uint256', name: 'accountId', type: 'uint256' }, @@ -332,21 +437,6 @@ export const repoDriverAbi = [ stateMutability: 'view', type: 'function', }, - { - inputs: [ - { - internalType: 'contract OperatorInterface', - name: 'operator', - type: 'address', - }, - { internalType: 'bytes32', name: 'jobId', type: 'bytes32' }, - { internalType: 'uint96', name: 'defaultFee', type: 'uint96' }, - ], - name: 'initializeAnyApiOperator', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, { inputs: [], name: 'isPaused', @@ -368,30 +458,6 @@ export const repoDriverAbi = [ stateMutability: 'view', type: 'function', }, - { - inputs: [], - name: 'linkToken', - outputs: [ - { - internalType: 'contract LinkTokenInterface', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: '', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - { internalType: 'bytes', name: 'data', type: 'bytes' }, - ], - name: 'onTokenTransfer', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, { inputs: [{ internalType: 'uint256', name: 'accountId', type: 'uint256' }], name: 'ownerOf', @@ -440,10 +506,17 @@ export const repoDriverAbi = [ { internalType: 'bytes', name: 'name', type: 'bytes' }, ], name: 'requestUpdateOwner', - outputs: [{ internalType: 'uint256', name: 'accountId', type: 'uint256' }], + outputs: [], stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [], + name: 'requestUpdateOwnerGasPenalty', + outputs: [{ internalType: 'uint256', name: 'gasPenalty', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, { inputs: [{ internalType: 'address', name: 'pauser', type: 'address' }], name: 'revokePauser', @@ -518,25 +591,30 @@ export const repoDriverAbi = [ }, { inputs: [ + { internalType: 'string', name: 'ipfsCid', type: 'string' }, { - internalType: 'contract OperatorInterface', - name: 'operator', - type: 'address', + internalType: 'uint32', + name: 'maxRequestsPerBlock', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'maxRequestsPer31Days', + type: 'uint32', }, - { internalType: 'bytes32', name: 'jobId', type: 'bytes32' }, - { internalType: 'uint96', name: 'defaultFee', type: 'uint96' }, ], - name: 'updateAnyApiOperator', + name: 'updateGelatoTask', outputs: [], stateMutability: 'nonpayable', type: 'function', }, { inputs: [ - { internalType: 'bytes32', name: 'requestId', type: 'bytes32' }, - { internalType: 'bytes', name: 'ownerRaw', type: 'bytes' }, + { internalType: 'uint256', name: 'accountId', type: 'uint256' }, + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'payer', type: 'address' }, ], - name: 'updateOwnerByAnyApi', + name: 'updateOwnerByGelato', outputs: [], stateMutability: 'nonpayable', type: 'function', @@ -568,6 +646,32 @@ export const repoDriverAbi = [ stateMutability: 'payable', type: 'function', }, -] as const; - -export type RepoDriverAbi = typeof repoDriverAbi; + { + inputs: [{ internalType: 'address', name: 'user', type: 'address' }], + name: 'userFunds', + outputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { + internalType: 'address payable', + name: 'receiver', + type: 'address', + }, + ], + name: 'withdrawUserFunds', + outputs: [ + { + internalType: 'uint256', + name: 'withdrawnAmount', + type: 'uint256', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { stateMutability: 'payable', type: 'receive' }, +]; diff --git a/src/lib/utils/sdk/sdk-types.ts b/src/lib/utils/sdk/sdk-types.ts index a25a8366f..e714414f1 100644 --- a/src/lib/utils/sdk/sdk-types.ts +++ b/src/lib/utils/sdk/sdk-types.ts @@ -1,6 +1,7 @@ export enum Forge { gitHub = 0, gitLab = 1, + orcidId = 2, } export type OxString = `0x${string}`; diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts new file mode 100644 index 000000000..dbe1d8608 --- /dev/null +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts @@ -0,0 +1,18 @@ +import type { PageServerLoad } from './$types'; +import { fetchOrcid, fetchOrcidChainData, orcidIdToAccountId } from './components/fetch-orcid'; + +export const load = (async ({ params, fetch }) => { + // TODO: allow fetching of account id or orcid! + // if it's an actual orcId, then calc the account id here + // if it's an account id, well... + + const orcid = await fetchOrcid(params.orcidId, fetch); + + const accountId = await orcidIdToAccountId(params.orcidId); + const orcidChainData = await fetchOrcidChainData(accountId, fetch); + + return { + orcid, + orcidChainData, + }; +}) satisfies PageServerLoad; diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.svelte new file mode 100644 index 000000000..e69de29bb diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts new file mode 100644 index 000000000..7af43c2e3 --- /dev/null +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts @@ -0,0 +1,43 @@ +import { gql } from 'graphql-request'; +import { ORCID_PROFILE_FRAGMENT } from './orcid-profile.svelte'; +import network from '$lib/stores/wallet/network'; +import query from '$lib/graphql/dripsQL'; +import type { + OrcidByAccountIdQuery, + OrcidByAccountIdQueryVariables, +} from './__generated__/gql.generated'; +import { executeRepoDriverReadMethod } from '$lib/utils/sdk/repo-driver/repo-driver'; +import { hexlify, toUtf8Bytes } from 'ethers'; +import { Forge, type OxString } from '$lib/utils/sdk/sdk-types'; + +export function orcidIdToAccountId(orcidId: string) { + return executeRepoDriverReadMethod({ + functionName: 'calcAccountId', + args: [Forge.orcidId, hexlify(toUtf8Bytes(orcidId)) as OxString], + }); +} + +export async function fetchOrcid(orcidId: string, fetch: typeof global.fetch) { + // TODO: his the ORCID API endpoint + return [orcidId, fetch]; +} + +const getOrcidQuery = gql` + ${ORCID_PROFILE_FRAGMENT} + query OrcidByAccountId($accountId: ID!, $chains: [SupportedChain!]) { + orcidAccountById(id: $accountId, chains: $chains) { + ...OrcidProfile + } + } +`; + +export async function fetchOrcidChainData(accountId: string, fetch: typeof global.fetch) { + return query( + getOrcidQuery, + { + accountId, + chains: [network.gqlName], + }, + fetch, + ); +} diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte new file mode 100644 index 000000000..a85451266 --- /dev/null +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte @@ -0,0 +1,54 @@ + + + + +
Hello World
From b271521e770616c1127a31b3979b5e93231bca15 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Tue, 19 Aug 2025 11:11:12 +0200 Subject: [PATCH 003/155] Fetch orcid, get orcid account id --- .env.template | 3 +++ Dockerfile | 2 ++ docker-compose.yml | 1 + docker/start-e2e.sh | 5 +++-- .../utils/sdk/repo-driver/repo-driver-abi.ts | 4 +++- .../(app)/orcids/[orcidId]/+page.server.ts | 14 +++++++++++++- .../[orcidId]/components/fetch-orcid.ts | 19 +++++++++++++++++-- 7 files changed, 42 insertions(+), 6 deletions(-) diff --git a/.env.template b/.env.template index b40d59b6e..4e8ef30b0 100644 --- a/.env.template +++ b/.env.template @@ -66,3 +66,6 @@ PUBLIC_FARO_ENVIRONMENT=string # The environment string to set in the Faro SDK. # Ecosystems host and API key. ECOSYSTEM_API_URL=string ECOSYSTEM_API_ACCESS_TOKEN=string + +# The ORCID API is used to look up basic information about ORCIDs +PUBLIC_ORCID_API_URL=string diff --git a/Dockerfile b/Dockerfile index 8b64517e4..d2cd820e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -78,6 +78,8 @@ ARG FARO_UPLOAD_SOURCE_MAPS_KEY ARG PUBLIC_DRIPS_RPGF_URL ARG PUBLIC_INTERNAL_DRIPS_RPGF_URL +ARG PUBLIC_ORCID_API_URL + RUN apt-get update \ && apt-get install -y chromium \ fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \ diff --git a/docker-compose.yml b/docker-compose.yml index e72f67fa5..9a7a04d95 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,7 @@ services: - GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN} - PUBLIC_DRIPS_RPGF_URL=http://localhost:5293 - PUBLIC_INTERNAL_DRIPS_RPGF_URL=http://rpgf:5000 + - PUBLIC_ORCID_API_URL=https://pub.sandbox.orcid.org volumes: - .:/app - /app/node_modules/ diff --git a/docker/start-e2e.sh b/docker/start-e2e.sh index 3d7948a6c..2a49f73e5 100755 --- a/docker/start-e2e.sh +++ b/docker/start-e2e.sh @@ -37,10 +37,11 @@ export ARCH export LOCAL_UID=$(id -u) export LOCAL_GID=$(id -g) +# TODO: REMOVE! if [ $PROD_BUILD = true ]; then - docker compose build && APP_USE_LOCAL_TESTNET_WALLET_STORE=true docker compose -f docker-compose.yml -f docker-compose.e2e.yml up --renew-anon-volumes --detach + docker compose build && APP_USE_LOCAL_TESTNET_WALLET_STORE=true GRAPHQL_API_TAG=jason-ecosystems EVENT_PROCESSOR_TAG=ecosystems docker compose -f docker-compose.yml -f docker-compose.e2e.yml up --renew-anon-volumes --detach else - docker compose build && APP_USE_LOCAL_TESTNET_WALLET_STORE=true docker compose -f docker-compose.yml up --renew-anon-volumes --detach + docker compose build && APP_USE_LOCAL_TESTNET_WALLET_STORE=true GRAPHQL_API_TAG=jason-ecosystems EVENT_PROCESSOR_TAG=ecosystems docker compose -f docker-compose.yml up --renew-anon-volumes --detach fi rm -rf ./test-data/project-states.json diff --git a/src/lib/utils/sdk/repo-driver/repo-driver-abi.ts b/src/lib/utils/sdk/repo-driver/repo-driver-abi.ts index 4a1b2dacd..78c5f22f4 100644 --- a/src/lib/utils/sdk/repo-driver/repo-driver-abi.ts +++ b/src/lib/utils/sdk/repo-driver/repo-driver-abi.ts @@ -674,4 +674,6 @@ export const repoDriverAbi = [ type: 'function', }, { stateMutability: 'payable', type: 'receive' }, -]; +] as const; + +export type RepoDriverAbi = typeof repoDriverAbi; diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts index dbe1d8608..629ff6ab5 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts @@ -1,3 +1,5 @@ +import isValidOrcidId from '$lib/utils/is-orcid-id/is-orcid-id'; +import { error } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; import { fetchOrcid, fetchOrcidChainData, orcidIdToAccountId } from './components/fetch-orcid'; @@ -6,13 +8,23 @@ export const load = (async ({ params, fetch }) => { // if it's an actual orcId, then calc the account id here // if it's an account id, well... + if (!isValidOrcidId(params.orcidId)) { + return error(404); + } + const orcid = await fetchOrcid(params.orcidId, fetch); + if (!orcid) { + return error(404); + } const accountId = await orcidIdToAccountId(params.orcidId); - const orcidChainData = await fetchOrcidChainData(accountId, fetch); + const orcidChainData = await fetchOrcidChainData(String(accountId), fetch); return { orcid, orcidChainData, }; }) satisfies PageServerLoad; + +// 0009-0007-5482-8654 me in prod +// 0009-0007-1106-8413 drips.network in sandbox diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts index 7af43c2e3..3bbe411c5 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts @@ -9,6 +9,7 @@ import type { import { executeRepoDriverReadMethod } from '$lib/utils/sdk/repo-driver/repo-driver'; import { hexlify, toUtf8Bytes } from 'ethers'; import { Forge, type OxString } from '$lib/utils/sdk/sdk-types'; +import { PUBLIC_ORCID_API_URL } from '$env/static/public'; export function orcidIdToAccountId(orcidId: string) { return executeRepoDriverReadMethod({ @@ -18,8 +19,22 @@ export function orcidIdToAccountId(orcidId: string) { } export async function fetchOrcid(orcidId: string, fetch: typeof global.fetch) { - // TODO: his the ORCID API endpoint - return [orcidId, fetch]; + const orcidResponse = await fetch(`${PUBLIC_ORCID_API_URL}/v3.0/${orcidId}/record`, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }); + + if (!orcidResponse.ok) { + // eslint-disable-next-line no-console + console.error('ORCID API returned non-ok response', await orcidResponse.text()); + return null; + } + + // TODO: parse valid response + + return orcidResponse.json(); } const getOrcidQuery = gql` From 310f06fb498bc1d82c5d55c94690bb368ae3025b Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Thu, 21 Aug 2025 17:07:48 +0200 Subject: [PATCH 004/155] Parse orcid api response with zod --- src/lib/utils/orcids/schemas.ts | 172 ++++++++++++++++++ .../(app)/orcids/[orcidId]/+page.server.ts | 12 +- .../[orcidId]/components/fetch-orcid.ts | 6 +- 3 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 src/lib/utils/orcids/schemas.ts diff --git a/src/lib/utils/orcids/schemas.ts b/src/lib/utils/orcids/schemas.ts new file mode 100644 index 000000000..ddcf12ab0 --- /dev/null +++ b/src/lib/utils/orcids/schemas.ts @@ -0,0 +1,172 @@ +import { z } from 'zod'; + +/** + * Zod schema for validating the response from the ORCID Public API v3.0. + * This schema covers the main sections of an ORCID record, including + * personal details and activity summaries. It can be extended to cover + * more specific fields as needed. + * + */ + +// --- Reusable Common Schemas --- + +const StringValueSchema = z.object({ + value: z.string().nullable(), +}); + +const TimestampSchema = z.object({ + value: z.number(), +}); + +// const VisibilitySchema = z.enum(['PUBLIC', 'LIMITED', 'PRIVATE']); + +const OrcidIdentifierSchema = z.object({ + uri: z.string().url(), + path: z.string(), // This is the actual ORCID iD + host: z.string(), +}); + +// const SourceSchema = z.object({ +// 'source-orcid': OrcidIdentifierSchema.nullable(), +// 'source-client-id': z.unknown().nullable(), +// 'source-name': StringValueSchema.nullable(), +// }); + +// --- Person Details --- + +const NameSchema = z.object({ + 'given-names': StringValueSchema.nullable(), + 'family-name': StringValueSchema.nullable(), + 'credit-name': StringValueSchema.nullable(), + // source: SourceSchema.nullable(), + // visibility: VisibilitySchema, + // path: z.string(), +}); + +const BiographySchema = z.object({ + content: z.string(), + // visibility: VisibilitySchema, + // path: z.string(), + 'created-date': TimestampSchema, + 'last-modified-date': TimestampSchema, +}); + +const OtherNameSchema = z.object({ + content: z.string(), + // visibility: VisibilitySchema, + // path: z.string(), + // 'put-code': z.number(), + // 'display-index': z.number(), + // source: SourceSchema, + 'created-date': TimestampSchema, + 'last-modified-date': TimestampSchema, +}); + +const ResearcherUrlSchema = z.object({ + 'url-name': z.string().nullable(), + url: StringValueSchema, + // visibility: VisibilitySchema, + // path: z.string(), + // 'put-code': z.number(), + // 'display-index': z.number(), + // source: SourceSchema, + 'created-date': TimestampSchema, + 'last-modified-date': TimestampSchema, +}); + +// const KeywordSchema = z.object({ +// content: z.string(), +// visibility: VisibilitySchema, +// path: z.string(), +// 'put-code': z.number(), +// 'display-index': z.number(), +// source: SourceSchema, +// 'created-date': TimestampSchema, +// 'last-modified-date': TimestampSchema, +// }); + +// const EmailSchema = z.object({ +// email: z.string().email(), +// path: z.string(), +// visibility: VisibilitySchema, +// verified: z.boolean(), +// primary: z.boolean(), +// 'put-code': z.number().nullable(), +// source: SourceSchema, +// 'created-date': TimestampSchema, +// 'last-modified-date': TimestampSchema, +// }); + +const PersonSchema = z.object({ + 'last-modified-date': TimestampSchema, + name: NameSchema, + biography: BiographySchema.nullable(), + 'researcher-urls': z.object({ 'researcher-url': z.array(ResearcherUrlSchema) }).nullable(), + // emails: z.object({ email: z.array(EmailSchema) }).nullable(), + 'other-names': z.object({ 'other-name': z.array(OtherNameSchema) }).nullable(), + // keywords: z.object({ keyword: z.array(KeywordSchema) }).nullable(), + // path: z.string(), +}); + +// --- Activity Summaries (Works, Employments, etc.) --- + +// A generic schema for a summary item (like a single job or publication) +// const ActivitySummarySchema = z.object({ +// 'created-date': TimestampSchema, +// 'last-modified-date': TimestampSchema, +// source: SourceSchema, +// 'put-code': z.number(), +// 'path': z.string(), +// 'visibility': VisibilitySchema, +// 'display-index': z.string(), +// }); + +// const WorkTitleSchema = z.object({ +// title: StringValueSchema, +// subtitle: StringValueSchema.nullable(), +// }); + +// const WorkSummarySchema = ActivitySummarySchema.extend({ +// type: z.string(), +// 'journal-title': StringValueSchema.nullable(), +// title: WorkTitleSchema, +// }); + +// + +// const AffiliationSummarySchema = ActivitySummarySchema.extend({ +// 'department-name': z.string().nullable(), +// 'role-title': z.string().nullable(), +// 'start-date': z.unknown().nullable(), // Date structure is complex +// 'end-date': z.unknown().nullable(), +// organization: OrganizationSchema, +// }); + +// const ActivitiesSummarySchema = z.object({ +// 'last-modified-date': TimestampSchema, +// // Note the nested structure for affiliations and works +// educations: z.object({ +// 'affiliation-group': z.array(z.object({ summaries: z.array(AffiliationSummarySchema) })), +// }), +// employments: z.object({ +// 'affiliation-group': z.array(z.object({ summaries: z.array(AffiliationSummarySchema) })), +// }), +// works: z.object({ +// group: z.array(z.object({ 'work-summary': z.array(WorkSummarySchema) })), +// }), +// path: z.string(), +// // Other sections like 'fundings', 'peer-reviews' can be added here. +// }); + +// --- Main Schema for the Entire API Response --- + +export const OrcidApiResponseSchema = z.object({ + 'orcid-identifier': OrcidIdentifierSchema, + person: PersonSchema, + // 'activities-summary': ActivitiesSummarySchema, + // 'last-modified-date': TimestampSchema, + // path: z.string(), +}); + +// You can infer a TypeScript type directly from the schema +export type OrcidApiResponse = z.infer; diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts index 629ff6ab5..827a988ea 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts @@ -1,7 +1,7 @@ import isValidOrcidId from '$lib/utils/is-orcid-id/is-orcid-id'; import { error } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; -import { fetchOrcid, fetchOrcidChainData, orcidIdToAccountId } from './components/fetch-orcid'; +import { fetchOrcid, fetchOrcidChainData } from './components/fetch-orcid'; export const load = (async ({ params, fetch }) => { // TODO: allow fetching of account id or orcid! @@ -17,8 +17,14 @@ export const load = (async ({ params, fetch }) => { return error(404); } - const accountId = await orcidIdToAccountId(params.orcidId); - const orcidChainData = await fetchOrcidChainData(String(accountId), fetch); + // TODO: I think there's a problem here, I thought we were supposed to fetch the orcid + // by accountId... We will probably want to support urls that include accountid as well. + // const accountId = await orcidIdToAccountId(params.orcidId); + let orcidChainData = undefined; + const orcidGqlResponse = await fetchOrcidChainData(params.orcidId, fetch); + if (orcidGqlResponse.orcidAccountById) { + orcidChainData = orcidGqlResponse.orcidAccountById; + } return { orcid, diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts index 3bbe411c5..58650ce92 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts @@ -10,6 +10,7 @@ import { executeRepoDriverReadMethod } from '$lib/utils/sdk/repo-driver/repo-dri import { hexlify, toUtf8Bytes } from 'ethers'; import { Forge, type OxString } from '$lib/utils/sdk/sdk-types'; import { PUBLIC_ORCID_API_URL } from '$env/static/public'; +import { OrcidApiResponseSchema } from '$lib/utils/orcids/schemas'; export function orcidIdToAccountId(orcidId: string) { return executeRepoDriverReadMethod({ @@ -32,9 +33,10 @@ export async function fetchOrcid(orcidId: string, fetch: typeof global.fetch) { return null; } - // TODO: parse valid response + const responseJson = await orcidResponse.json(); + const orcid = OrcidApiResponseSchema.parse(responseJson); - return orcidResponse.json(); + return orcid; } const getOrcidQuery = gql` From db69a0e20852a362037008e1aea9268b7f914111 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Fri, 22 Aug 2025 18:22:50 +0200 Subject: [PATCH 005/155] ORCID profile component scaffolding --- src/lib/components/icons/Orcid.svelte | 20 ++ .../components/support-buttons.svelte | 2 +- .../support-card/support-card.svelte | 42 +++- src/lib/utils/get-last-path-segment.ts | 15 ++ src/lib/utils/orcids/build-orcid-url.ts | 3 + src/lib/utils/orcids/entities.ts | 34 +++ .../utils/orcids/filter-current-chain-data.ts | 47 ++++ src/lib/utils/orcids/is-claimed.ts | 23 ++ .../(app)/orcids/[orcidId]/+page.server.ts | 38 +++- .../app/(app)/orcids/[orcidId]/+page.svelte | 8 + .../[orcidId]/components/fetch-orcid.ts | 5 +- .../[orcidId]/components/orcid-avatar.svelte | 48 ++++ .../[orcidId]/components/orcid-badge.svelte | 121 ++++++++++ .../[orcidId]/components/orcid-name.svelte | 31 +++ .../components/orcid-profile-header.svelte | 86 +++++++ .../[orcidId]/components/orcid-profile.svelte | 210 +++++++++++++++++- .../[orcidId]/components/orcid-tooltip.svelte | 118 ++++++++++ 17 files changed, 835 insertions(+), 16 deletions(-) create mode 100644 src/lib/components/icons/Orcid.svelte create mode 100644 src/lib/utils/get-last-path-segment.ts create mode 100644 src/lib/utils/orcids/build-orcid-url.ts create mode 100644 src/lib/utils/orcids/entities.ts create mode 100644 src/lib/utils/orcids/filter-current-chain-data.ts create mode 100644 src/lib/utils/orcids/is-claimed.ts create mode 100644 src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-avatar.svelte create mode 100644 src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-badge.svelte create mode 100644 src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-name.svelte create mode 100644 src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte create mode 100644 src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-tooltip.svelte diff --git a/src/lib/components/icons/Orcid.svelte b/src/lib/components/icons/Orcid.svelte new file mode 100644 index 000000000..a7bee59f6 --- /dev/null +++ b/src/lib/components/icons/Orcid.svelte @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/src/lib/components/support-card/components/support-buttons.svelte b/src/lib/components/support-card/components/support-buttons.svelte index bdead1dd6..e62c7d393 100644 --- a/src/lib/components/support-card/components/support-buttons.svelte +++ b/src/lib/components/support-card/components/support-buttons.svelte @@ -8,7 +8,7 @@ import Wallet from '$lib/components/icons/Wallet.svelte'; import { fade } from 'svelte/transition'; - export let type: 'dripList' | 'project' | 'ecosystem'; + export let type: 'dripList' | 'project' | 'ecosystem' | 'orcid'; export let transitions = true; diff --git a/src/lib/components/support-card/support-card.svelte b/src/lib/components/support-card/support-card.svelte index 1b574c362..83eddd406 100644 --- a/src/lib/components/support-card/support-card.svelte +++ b/src/lib/components/support-card/support-card.svelte @@ -48,6 +48,25 @@ } } `; + + export const SUPPORT_CARD_ORCID_FRAGEMENT = gql` + fragment SupportCardOrcid on OrcidAccount { + source { + url + } + account { + accountId + driver + } + chainData { + ... on ClaimedOrcidAccountData { + linkedTo { + accountId + } + } + } + } + `; + + diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts index 58650ce92..0c5b15ef3 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts @@ -11,6 +11,7 @@ import { hexlify, toUtf8Bytes } from 'ethers'; import { Forge, type OxString } from '$lib/utils/sdk/sdk-types'; import { PUBLIC_ORCID_API_URL } from '$env/static/public'; import { OrcidApiResponseSchema } from '$lib/utils/orcids/schemas'; +import Orcid from '$lib/utils/orcids/entities'; export function orcidIdToAccountId(orcidId: string) { return executeRepoDriverReadMethod({ @@ -36,7 +37,7 @@ export async function fetchOrcid(orcidId: string, fetch: typeof global.fetch) { const responseJson = await orcidResponse.json(); const orcid = OrcidApiResponseSchema.parse(responseJson); - return orcid; + return new Orcid(orcid); } const getOrcidQuery = gql` @@ -48,7 +49,7 @@ const getOrcidQuery = gql` } `; -export async function fetchOrcidChainData(accountId: string, fetch: typeof global.fetch) { +export async function fetchOrcidAccount(accountId: string, fetch: typeof global.fetch) { return query( getOrcidQuery, { diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-avatar.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-avatar.svelte new file mode 100644 index 000000000..674d2e898 --- /dev/null +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-avatar.svelte @@ -0,0 +1,48 @@ + + +
+ +
+ + + diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-badge.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-badge.svelte new file mode 100644 index 000000000..5f3f313ef --- /dev/null +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-badge.svelte @@ -0,0 +1,121 @@ + + + + + + + + {#if !hideAvatar} +
+ {#if !forceUnclaimed && isClaimed(chainData)} +
+ +
+ {/if} +
+
+ {/if} +
+ +
+
+ + + +
+
+ + diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-name.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-name.svelte new file mode 100644 index 000000000..3fa8214a4 --- /dev/null +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-name.svelte @@ -0,0 +1,31 @@ + + + + +{orcidId} diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte new file mode 100644 index 000000000..f873d2db0 --- /dev/null +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte @@ -0,0 +1,86 @@ + + + + +
+
+
+ +
+
+

{orcid.name}

+
+ {#if isClaimed(orcidChainData)} + + {/if} + +
+ {#if orcid.bio} + {@html twemoji(orcid.bio)} + + {/if} +
+ {#if editButton || shareButton} +
+ {#if shareButton} + + {/if} + {#if editButton} + + {/if} +
+ {/if} +
+
+ + diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte index a85451266..62cfb3dcc 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte @@ -2,14 +2,16 @@ // TODO: may have to differentiate these import { SUPPORTER_PILE_FRAGMENT } from '$lib/components/drip-list-card/methods/get-supporters-pile'; import { SUPPORTERS_SECTION_SUPPORT_ITEM_FRAGMENT } from '$lib/components/supporters-section/supporters.section.svelte'; - import { MERGE_WITHDRAWABLE_BALANCES_FRAGMENT } from '$lib/utils/merge-withdrawable-balances'; + import mergeWithdrawableBalances, { MERGE_WITHDRAWABLE_BALANCES_FRAGMENT } from '$lib/utils/merge-withdrawable-balances'; import { gql } from 'graphql-request'; export const ORCID_PROFILE_FRAGMENT = gql` ${SUPPORTERS_SECTION_SUPPORT_ITEM_FRAGMENT} ${SUPPORTER_PILE_FRAGMENT} ${MERGE_WITHDRAWABLE_BALANCES_FRAGMENT} + ${ORCID_PROFILE_HEADER_FRAGMENT} fragment OrcidProfile on OrcidAccount { + ...OrcidProfileHeader account { accountId driver @@ -19,6 +21,7 @@ } chainData { ... on UnClaimedOrcidAccountData { + chain linkedTo { accountId } @@ -31,6 +34,7 @@ } } ... on ClaimedOrcidAccountData { + chain maybeLinkedTo: linkedTo { accountId } @@ -49,6 +53,208 @@ -
Hello World
+ + + + + + + + + + + diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-tooltip.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-tooltip.svelte new file mode 100644 index 000000000..6689da36c --- /dev/null +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-tooltip.svelte @@ -0,0 +1,118 @@ + + + + +
+
+
+ + {#if orcidId} + + {/if} + {#if isClaimed(chainData)} +
+ Owned by + +
+ {/if} +
+ {#if orcid.source.url} + View ORCID + {/if} +
+ + From 96cac674aa8302978eb9edf76d90f81384159d30 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 25 Aug 2025 13:34:26 +0200 Subject: [PATCH 006/155] One time donation support scaffolding for ORCIDs --- .../components/drip-visual/drip-visual.svelte | 8 +++ .../identity-card/identity-card.svelte | 32 ++++++++- .../create-donation-flow-steps.ts | 20 +++++- .../create-donation/input-details.svelte | 19 +++++- .../methods/build-one-time-donation-txs.ts | 20 +++++- .../methods/create-donation.ts | 65 ++++++++++++++++++- .../(app)/orcids/[orcidId]/+page.server.ts | 7 +- 7 files changed, 160 insertions(+), 11 deletions(-) diff --git a/src/lib/components/drip-visual/drip-visual.svelte b/src/lib/components/drip-visual/drip-visual.svelte index b394cb7ef..d0607403d 100644 --- a/src/lib/components/drip-visual/drip-visual.svelte +++ b/src/lib/components/drip-visual/drip-visual.svelte @@ -46,6 +46,13 @@ ...IdentityCardEcosystem } `; + + export const DRIP_VISUAL_ORCID_FRAGMENT = gql` + ${IDENTITY_CARD_ORCID_FRAGMENT} + fragment DripVisualOrcid on OrcidAccount { + ...IdentityCardOrcid + } + ` @@ -152,6 +172,16 @@ {/if}{project.source.repoName}
+ {:else if orcid} +
+
+ +
+ +
+ {getLastPathSegment(orcid.source.url)} +
+
{:else if loading}
{:else} diff --git a/src/lib/flows/create-donation/create-donation-flow-steps.ts b/src/lib/flows/create-donation/create-donation-flow-steps.ts index 1885521f4..27b8382f8 100644 --- a/src/lib/flows/create-donation/create-donation-flow-steps.ts +++ b/src/lib/flows/create-donation/create-donation-flow-steps.ts @@ -6,11 +6,14 @@ import createDonationFlowState from './create-donation-flow-state'; import InputDetails, { CREATE_DONATION_DETAILS_STEP_NFT_DRIVER_ACCOUNT_FRAGMENT, CREATE_DONATION_DETAILS_STEP_PROJECT_FRAGMENT, + CREATE_DONATION_DETAILS_STEP_ORCID_FRAGMENT, + CREATE_DONATION_DETAILS_STEP_ECOSYSTEM_FRAGMENT } from './input-details.svelte'; import type { CreateDonationDetailsStepNftDriverAccountFragment, CreateDonationDetailsStepProjectFragment, CreateDonationDetailsStepEcosystemFragment, + CreateDonationDetailsStepOrcidFragment } from './__generated__/gql.generated'; import { gql } from 'graphql-request'; @@ -30,11 +33,26 @@ export const CREATE_DONATION_FLOW_PROJECT_FRAGMENT = gql` } `; +export const CREATE_DONATION_FLOW_ECOSYSTEM_FRAGMENT = gql` + ${CREATE_DONATION_DETAILS_STEP_ECOSYSTEM_FRAGMENT} + fragment CreateDonationFlowEcosystem on EcosystemMainAccount { + ...CreateDonationDetailsStepEcosystem + } +`; + +export const CREATE_DONATION_FLOW_ORCID_FRAGMENT = gql` + ${CREATE_DONATION_DETAILS_STEP_ORCID_FRAGMENT} + fragment CreateDonationFlowOrcid on OrcidAccount { + ...CreateDonationDetailsStepOrcid + } +`; + export default ( receiver: | CreateDonationDetailsStepNftDriverAccountFragment | CreateDonationDetailsStepProjectFragment - | CreateDonationDetailsStepEcosystemFragment, + | CreateDonationDetailsStepEcosystemFragment + | CreateDonationDetailsStepOrcidFragment ) => ({ context: createDonationFlowState, steps: [ diff --git a/src/lib/flows/create-donation/input-details.svelte b/src/lib/flows/create-donation/input-details.svelte index f89e9d91e..709da23e0 100644 --- a/src/lib/flows/create-donation/input-details.svelte +++ b/src/lib/flows/create-donation/input-details.svelte @@ -4,6 +4,7 @@ DRIP_VISUAL_ECOSYSTEM_FRAGMENT, DRIP_VISUAL_NFT_DRIVER_ACCOUNT_FRAGMENT, DRIP_VISUAL_PROJECT_FRAGMENT, + DRIP_VISUAL_ORCID_FRAGMENT, } from '$lib/components/drip-visual/drip-visual.svelte'; export const CREATE_DONATION_DETAILS_STEP_ADDRESS_DRIVER_ACCOUNT_FRAGMENT = gql` @@ -40,6 +41,16 @@ } } `; + + export const CREATE_DONATION_DETAILS_STEP_ORCID_FRAGMENT = gql` + ${DRIP_VISUAL_ORCID_FRAGMENT} + fragment CreateDonationDetailsStepOrcid on OrcidAccount { + ...DripVisualOrcid + account { + accountId + } + } + ` - + diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte index 62cfb3dcc..3a1f23d65 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte @@ -80,7 +80,6 @@ export let orcid: Orcid; export let orcidAccount: OrcidProfileFragment; - let supportersSectionSkeleton: SectionSkeleton | undefined; $: imageBaseUrl = `/api/share-images/orcid/${encodeURIComponent(orcid.id)}.png`; @@ -145,7 +144,7 @@ From 495e8a1ffc7533c163ef0700b2d76495f6933c4c Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 25 Aug 2025 17:21:15 +0200 Subject: [PATCH 008/155] Refine ORCID badge, icon --- src/lib/components/button/button.svelte | 6 +- src/lib/components/icons/IconWrapper.svelte | 7 +- src/lib/components/icons/Orcid.svelte | 10 +-- src/lib/utils/orcids/entities.ts | 2 +- .../[orcidId]/components/orcid-avatar.svelte | 18 +++-- .../[orcidId]/components/orcid-badge.svelte | 65 ++++++++++++++++--- .../components/orcid-profile-header.svelte | 24 ++++--- 7 files changed, 93 insertions(+), 39 deletions(-) diff --git a/src/lib/components/button/button.svelte b/src/lib/components/button/button.svelte index 83dc322b0..d118bffff 100644 --- a/src/lib/components/button/button.svelte +++ b/src/lib/components/button/button.svelte @@ -4,7 +4,7 @@ import Spinner from '../spinner/spinner.svelte'; import { fade } from 'svelte/transition'; - export let variant: 'normal' | 'primary' | 'destructive' | 'destructive-outline' | 'ghost' = + export let variant: 'normal' | 'primary' | 'destructive' | 'destructive-outline' | 'ghost' | 'muted' = 'normal'; export let icon: ComponentType | undefined = undefined; export let disabled = false; @@ -156,6 +156,10 @@ box-shadow: 0px 0px 0px 1px var(--color-foreground-level-3); } + .button .inner.muted { + box-shadow: 0px 0px 0px 1px var(--color-foreground-level-3); + } + .button:not(.loading) .inner.primary { background-color: var(--color-primary); } diff --git a/src/lib/components/icons/IconWrapper.svelte b/src/lib/components/icons/IconWrapper.svelte index 8a13bee45..f2f14db7f 100644 --- a/src/lib/components/icons/IconWrapper.svelte +++ b/src/lib/components/icons/IconWrapper.svelte @@ -1,15 +1,16 @@ diff --git a/src/lib/components/icons/Orcid.svelte b/src/lib/components/icons/Orcid.svelte index a7bee59f6..ca7ab4f23 100644 --- a/src/lib/components/icons/Orcid.svelte +++ b/src/lib/components/icons/Orcid.svelte @@ -4,17 +4,9 @@ export let style: string | undefined = undefined; - + - - - - diff --git a/src/lib/utils/orcids/entities.ts b/src/lib/utils/orcids/entities.ts index 69a96c958..54276dc0f 100644 --- a/src/lib/utils/orcids/entities.ts +++ b/src/lib/utils/orcids/entities.ts @@ -15,7 +15,7 @@ export default class Orcid { get name(): string { const name = this.data.person.name - return name['credit-name']?.value ?? '' + return name['given-names']?.value || name['credit-name']?.value || name['family-name']?.value || '' } get bio(): string { diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-avatar.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-avatar.svelte index 674d2e898..d9a03ea71 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-avatar.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-avatar.svelte @@ -16,24 +16,32 @@ huge: '8rem', }; $: containerSize = CONTAINER_SIZES[size]; + + $: dimensionCss = { + tiny: '100%', + small: '65%', + medium: '65%', + large: '65%', + huge: '50%', + }[size];
diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte index f873d2db0..310e0c8e0 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte @@ -32,6 +32,9 @@ import Button from '$lib/components/button/button.svelte'; import ShareButton from '$lib/components/share-button/share-button.svelte'; import IdentityBadge from '$lib/components/identity-badge/identity-badge.svelte'; + // import CopyLinkButton from '$lib/components/copy-link-button/copy-link-button.svelte'; + // import buildOrcidUrl from '$lib/utils/orcids/build-orcid-url'; + // import { browser } from '$app/environment'; export let orcid: Orcid; export let orcidAccount: OrcidProfileHeaderFragment; @@ -39,6 +42,7 @@ export let shareButton: ComponentProps | undefined = undefined; $: orcidChainData = filterCurrentChainData(orcidAccount.chainData); + // $: currentDomain = browser && window ? window.location.origin : ''; const dispatch = createEventDispatcher<{ editButtonClick: void }>(); @@ -46,24 +50,24 @@
- +

{orcid.name}

-
- {#if isClaimed(orcidChainData)} - - {/if} - -
{#if orcid.bio} {@html twemoji(orcid.bio)} {/if} +
+ {#if isClaimed(orcidChainData)} + + {/if} + + +
{#if editButton || shareButton}
From b81b6353e6a29e484cbb1970e9320addb46e3fe4 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 25 Aug 2025 17:49:56 +0200 Subject: [PATCH 009/155] Add share to ORCID profile header --- .../components/orcid-profile-header.svelte | 28 +++---------------- .../[orcidId]/components/orcid-profile.svelte | 16 +++++------ 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte index 310e0c8e0..a54b5158d 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte @@ -19,8 +19,7 @@
@@ -66,25 +57,14 @@ {/if} - -
-
- {#if editButton || shareButton} -
{#if shareButton} - - {/if} - {#if editButton} - + {/if}
- {/if} +
diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte index 3a1f23d65..b12c242ac 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte @@ -58,33 +58,28 @@ import SectionSkeleton from '$lib/components/section-skeleton/section-skeleton.svelte'; import KeyValuePair from '$lib/components/key-value-pair/key-value-pair.svelte'; import HeadMeta from '$lib/components/head-meta/head-meta.svelte'; - // import EcosystemProfileHeader from './orcid-profile-header.svelte'; - // import EcosystemCardInteractive from './ecosystem-graph-card.svelte'; - // import EcosystemMetadata from './ecosystem-metadata.svelte'; - // import EcosystemDistribution from './ecosystem-distribution/ecosystem-distribution.svelte'; import SupportersSection from '$lib/components/supporters-section/supporters.section.svelte'; - // import type { Ecosystem } from '$lib/utils/ecosystems/schemas'; import type { OrcidProfileFragment } from './__generated__/gql.generated'; import getSupportersPile from '$lib/components/drip-list-card/methods/get-supporters-pile'; import Pile from '$lib/components/pile/pile.svelte'; - // import { STREAM_STATE_STREAM_FRAGMENT } from '$lib/utils/stream-state'; - // import { CURRENT_AMOUNTS_TIMELINE_ITEM_FRAGMENT } from '$lib/utils/current-amounts'; import AggregateFiatEstimate from '$lib/components/aggregate-fiat-estimate/aggregate-fiat-estimate.svelte'; - // import formatNumber from '$lib/utils/format-number'; import Developer from '$lib/components/developer-section/developer.section.svelte'; import type Orcid from '$lib/utils/orcids/entities'; import filterCurrentChainData from '$lib/utils/orcids/filter-current-chain-data'; import OrcidProfileHeader, { ORCID_PROFILE_HEADER_FRAGMENT } from './orcid-profile-header.svelte'; import isClaimed from '$lib/utils/orcids/is-claimed'; + import buildOrcidUrl from '$lib/utils/orcids/build-orcid-url'; export let orcid: Orcid; export let orcidAccount: OrcidProfileFragment; let supportersSectionSkeleton: SectionSkeleton | undefined; + // TODO: implement $: imageBaseUrl = `/api/share-images/orcid/${encodeURIComponent(orcid.id)}.png`; $: chainData = filterCurrentChainData(orcidAccount.chainData); $: orcidSupport = chainData?.support || []; + $: origin = typeof window !== 'undefined' && window ? window.location.origin : 'https://drips.network';
- +
From 8eb18236efc6f753406d9c568eaa9b2b3644758f Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 25 Aug 2025 18:07:38 +0200 Subject: [PATCH 010/155] Add claiming notice --- src/lib/utils/orcids/build-orcid-url.ts | 9 ++- .../[orcidId]/components/orcid-profile.svelte | 68 ++++++++++++++----- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/lib/utils/orcids/build-orcid-url.ts b/src/lib/utils/orcids/build-orcid-url.ts index 82a444cfb..4562b0fb9 100644 --- a/src/lib/utils/orcids/build-orcid-url.ts +++ b/src/lib/utils/orcids/build-orcid-url.ts @@ -1,3 +1,8 @@ -export default function buildOrcidUrl(orcidId: string) { - return `/app/orcids/${orcidId}` +export default function buildOrcidUrl(orcidId: string, { absolute = false }: { absolute?: boolean } = {}): string { + let origin = '' + if (absolute && typeof window !== 'undefined' && window) { + origin = window.location.origin + } + + return `${origin}/app/orcids/${orcidId}` } \ No newline at end of file diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte index b12c242ac..d98243838 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte @@ -69,6 +69,11 @@ import OrcidProfileHeader, { ORCID_PROFILE_HEADER_FRAGMENT } from './orcid-profile-header.svelte'; import isClaimed from '$lib/utils/orcids/is-claimed'; import buildOrcidUrl from '$lib/utils/orcids/build-orcid-url'; + import AnnotationBox from '$lib/components/annotation-box/annotation-box.svelte'; + import network from '$lib/stores/wallet/network'; + import Button from '$lib/components/button/button.svelte'; + import Registered from '$lib/components/icons/Registered.svelte'; + import CopyLinkButton from '$lib/components/copy-link-button/copy-link-button.svelte'; export let orcid: Orcid; export let orcidAccount: OrcidProfileFragment; @@ -79,7 +84,11 @@ $: imageBaseUrl = `/api/share-images/orcid/${encodeURIComponent(orcid.id)}.png`; $: chainData = filterCurrentChainData(orcidAccount.chainData); $: orcidSupport = chainData?.support || []; - $: origin = typeof window !== 'undefined' && window ? window.location.origin : 'https://drips.network'; + + function launchClaimOrcid() { + // eslint-disable-next-line no-console + console.log('Launch claim ORCID flow'); + } + {#if !isClaimed(chainData)} +
+ + {#if chainData.withdrawableBalances.length > 0}This ORCID iD has in claimable funds. The owner can collect by claiming their ORCID iD.{:else}This + ORCID iD is unclaimed on {network.label}, but can still receive funds that the owner can + collect later.{/if} + +
+ + +
+
+
+
+ {/if} +
-
@@ -129,11 +170,9 @@ {/if}
-
- -
+ Date: Mon, 25 Aug 2025 20:54:37 +0200 Subject: [PATCH 011/155] Add claimable funds section --- .../supporters.section.svelte | 3 + .../[orcidId]/components/orcid-profile.svelte | 86 +++++++ .../components/unclaimed-orcid-card.svelte | 212 ++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 src/routes/(pages)/app/(app)/orcids/[orcidId]/components/unclaimed-orcid-card.svelte diff --git a/src/lib/components/supporters-section/supporters.section.svelte b/src/lib/components/supporters-section/supporters.section.svelte index cb4bc257e..c4439fb82 100644 --- a/src/lib/components/supporters-section/supporters.section.svelte +++ b/src/lib/components/supporters-section/supporters.section.svelte @@ -132,6 +132,8 @@ export let collapsed = false; export let collapsable = false; + export let iconPrimary = true; + let emptyStateText: string; $: { switch (type) { @@ -162,6 +164,7 @@ bind:collapsable header={{ icon: Heart, + iconPrimary, label: headline, infoTooltip, }} diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte index d98243838..0fdaa117b 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte @@ -74,6 +74,11 @@ import Button from '$lib/components/button/button.svelte'; import Registered from '$lib/components/icons/Registered.svelte'; import CopyLinkButton from '$lib/components/copy-link-button/copy-link-button.svelte'; + import SectionHeader from '$lib/components/section-header/section-header.svelte'; + import Wallet from '$lib/components/icons/Wallet.svelte'; + import UnclaimedOrcidCard from './unclaimed-orcid-card.svelte'; + import { goto } from '$app/navigation'; + import buildUrl from '$lib/utils/build-url'; export let orcid: Orcid; export let orcidAccount: OrcidProfileFragment; @@ -171,12 +176,93 @@ + {#if isClaimed(chainData)} + + {:else if chainData.withdrawableBalances.length > 0} +
+ + +
+ 0} + showClaimButton={!isClaimed(chainData)} + on:claimButtonClick={() => + goto(buildUrl('/app/claim-orcid', { orcidToClaim: orcid.id }))} + /> +
+
+
+ {/if} +
diff --git a/src/routes/(pages)/app/(app)/[accountId]/components/linked-identities-card.svelte b/src/routes/(pages)/app/(app)/[accountId]/components/linked-identities-card.svelte index 7ad5758f6..18ddeecc6 100644 --- a/src/routes/(pages)/app/(app)/[accountId]/components/linked-identities-card.svelte +++ b/src/routes/(pages)/app/(app)/[accountId]/components/linked-identities-card.svelte @@ -18,12 +18,19 @@ -
+
{#if linkedIdentities.length}
Linked Identities
    @@ -40,7 +47,9 @@ Learn more {/if} {#if canLinkIdentity} - +
    + +
    {/if}
@@ -59,4 +68,13 @@ color: var(--color-foreground); margin-bottom: 0.5rem; } + + .actions { + display: flex; + flex-direction: column; + } + + .can-link-identity .actions { + flex-direction: row; + } From 17e425a63e61fdb1fe925d94fe5ee6c3a8fc3622 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Wed, 17 Sep 2025 17:58:35 -0700 Subject: [PATCH 056/155] A little cleanup for claim orcid flow --- .../claim-orcid-flow/claim-orcid-flow.ts | 99 +------------------ .../claim-orcid-stepper.svelte | 2 +- .../steps/choose-identity.svelte | 20 ++-- .../app/(app)/[accountId]/+page.server.ts | 1 + 4 files changed, 19 insertions(+), 103 deletions(-) diff --git a/src/lib/flows/claim-orcid-flow/claim-orcid-flow.ts b/src/lib/flows/claim-orcid-flow/claim-orcid-flow.ts index b54716f64..4c96b3928 100644 --- a/src/lib/flows/claim-orcid-flow/claim-orcid-flow.ts +++ b/src/lib/flows/claim-orcid-flow/claim-orcid-flow.ts @@ -1,8 +1,4 @@ -import { - // get, - writable, - type Writable -} from 'svelte/store'; +import { writable } from 'svelte/store'; import type { Slots } from '../../components/standalone-flow-slots/standalone-flow-slots.svelte'; import { makeStep } from '$lib/components/stepper/types'; import ConnectWallet from './steps/connect-wallet/connect-wallet.svelte'; @@ -13,9 +9,6 @@ import AddEthereumAddress, { ADD_ETHEREUM_ADDRESS_STEP_ORCID_FRAGMENT, } from './steps/add-ethereum-address/add-ethereum-address.svelte'; import OrcidSlot from './slots/orcid-slot.svelte'; -// import SplitYourFunds from './steps/split-your-funds/split-your-funds.svelte'; -// import ConfigureMaintainers from './steps/configure-maintainers/configure-maintainers.svelte'; -// import ConfigureDependencies from './steps/configure-dependencies/configure-dependencies.svelte'; import Review, { REVIEW_STEP_UNCLAIMED_ORCID_FRAGMENT } from './steps/review/review.svelte'; import SetSplitsAndEmitMetadata from './steps/set-splits-and-emit-metadata/set-splits-and-emit-metadata.svelte'; import LinkedOrcid from './slots/linked-orcid.svelte'; @@ -23,11 +16,7 @@ import Success from './steps/success/success.svelte'; import WalletSlot from '$lib/components/slots/wallet-slot.svelte'; import { gql } from 'graphql-request'; import type { ClaimOrcidFlowOrcidFragment } from './__generated__/gql.generated'; -// import type { Items, Weights } from '$lib/components/list-editor/types'; import ChooseNetwork from './steps/choose-network/choose-network.svelte'; -// import type { FundingJson } from '$lib/utils/github/GitHub'; -// import type { TemplateHighlight } from './steps/add-ethereum-address/drips-json-template'; -import type { AddItemError } from '$lib/components/list-editor/errors'; import type Orcid from '$lib/utils/orcids/entities'; import type { ListEditorConfig } from '$lib/components/list-editor/types'; @@ -42,14 +31,13 @@ export const CLAIM_ORCID_FLOW_ORCID_FRAGMENT = gql` } `; -type OrcidMetadata = Orcid +type OrcidMetadata = Orcid; export interface State { - // giturl or ORCID claimableId: string; claimableAccount: ClaimOrcidFlowOrcidFragment | undefined; claimableMetadata: OrcidMetadata | undefined; - claimableContext: Record| undefined; + claimableContext: Record | undefined; claimableProof: unknown; linkedToClaimable: boolean; @@ -59,41 +47,6 @@ export interface State { highLevelPercentages: { [key: string]: number }; maintainerSplits: ListEditorConfig; dependencySplits: ListEditorConfig; - - // or this is part of a project's claimableContext - // recipientErrors: Array; - - // aka linked to ORCID profile? - // linkedToRepo: boolean; - // aka ORCID profile URL? - // entityUrl: string; - // gitUrl: string; - // isPartiallyClaimed: boolean; - // entityAccount - // orcidAccount: ClaimProjectFlowOrcidFragment | undefined; - // entityMetadata:; - // orcidMetadata: OrcidMetadata | undefined; - // highLevelPercentages: { [key: string]: number }; - // maintainerSplits: ListEditorConfig; - // dependencySplits: ListEditorConfig; - // dependenciesAutoImported: boolean; - // gaslessOwnerUpdateTaskId: string | undefined; - // avatar: - // | { - // type: 'emoji'; - // emoji: string; - // } - // | { - // type: 'image'; - // cid: string; - // }; - // projectColor: string; - // funding: { - // json: string; - // object: FundingJson; - // highlight: TemplateHighlight; - // }; - // recipientErrors: Array; } export const state = () => @@ -115,30 +68,6 @@ export const state = () => items: {}, weights: {}, }, - // recipientErrors: [], - - // claimableContext: { - // highLevelPercentages: { maintainers: 60, dependencies: 40 }, - // maintainerSplits: { - // items: {}, - // weights: {}, - // }, - // dependencySplits: { - // items: {}, - // weights: {}, - // }, - // dependenciesAutoImported: false, - // avatar: { - // type: 'emoji', - // emoji: '💧', - // }, - // projectColor: '#000000', - // funding: { - // json: '{}', - // object: {}, - // highlight: [null, null], - // }, - // }, }); export function slotsTemplate(state: State, stepIndex: number): Slots { @@ -184,12 +113,7 @@ export function slotsTemplate(state: State, stepIndex: number): Slots { } } -export const steps = ( - state: Writable, - skipWalletConnect = false, - isModal = false, - claimableId: string | undefined = undefined, -) => [ +export const steps = (claimableId: string | undefined = undefined, skipWalletConnect = false) => [ makeStep({ component: ChooseNetwork, props: undefined, @@ -213,25 +137,10 @@ export const steps = ( component: AddEthereumAddress, props: undefined, }), - // makeStep({ - // component: SplitYourFunds, - // props: undefined, - // }), - // makeStep({ - // component: ConfigureMaintainers, - // props: undefined, - // condition: () => get(state).highLevelPercentages.maintainers > 0, - // }), - // makeStep({ - // component: ConfigureDependencies, - // props: undefined, - // condition: () => get(state).highLevelPercentages.dependencies > 0, - // }), makeStep({ component: Review, props: { canEditWalletConnection: !skipWalletConnect, - // isModal, }, }), makeStep({ diff --git a/src/lib/flows/claim-orcid-flow/claim-orcid-stepper.svelte b/src/lib/flows/claim-orcid-flow/claim-orcid-stepper.svelte index 9e51ae397..413fd7aaf 100644 --- a/src/lib/flows/claim-orcid-flow/claim-orcid-stepper.svelte +++ b/src/lib/flows/claim-orcid-flow/claim-orcid-stepper.svelte @@ -31,7 +31,7 @@ bind:currentStepIndex on:stepChange={() => window.scrollTo({ top: 0 })} context={() => myState} - steps={steps(myState, skipWalletConnect, true, orcidId)} + steps={steps(orcidId, skipWalletConnect)} minHeightPx={0} /> diff --git a/src/lib/flows/link-identity-flow/steps/choose-identity.svelte b/src/lib/flows/link-identity-flow/steps/choose-identity.svelte index b8b1de4f2..4f73aef2b 100644 --- a/src/lib/flows/link-identity-flow/steps/choose-identity.svelte +++ b/src/lib/flows/link-identity-flow/steps/choose-identity.svelte @@ -5,7 +5,9 @@ import StepHeader from '$lib/components/step-header/step-header.svelte'; import StepLayout from '$lib/components/step-layout/step-layout.svelte'; import type { StepComponentEvents } from '$lib/components/stepper/types'; - import { state, steps } from '$lib/flows/claim-orcid-flow/claim-orcid-flow'; + import { steps } from '$lib/flows/claim-orcid-flow/claim-orcid-flow'; + import walletStore from '$lib/stores/wallet/wallet.store'; + import launchClaimOrcid from '$lib/utils/launch-claim-orcid'; import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); @@ -14,13 +16,17 @@ const description = 'Select an identity type:'; function launchSpecificIdentityFlow() { - const myState = state(); - const skipWalletConnect = false; - const isModal = true; + const walletConnected = $walletStore.connected; + // TODO; will this ever not be connected? + // If a wallet is connected, sidestep for a nice transition + if (walletConnected) { + return dispatch('sidestep', { + steps: steps(undefined, !walletConnected), + }); + } - dispatch('sidestep', { - steps: steps(myState, skipWalletConnect, isModal), - }); + // otherwise, launch the flow from its own page + launchClaimOrcid(); } diff --git a/src/routes/(pages)/app/(app)/[accountId]/+page.server.ts b/src/routes/(pages)/app/(app)/[accountId]/+page.server.ts index 9148cccf7..d742abb9f 100644 --- a/src/routes/(pages)/app/(app)/[accountId]/+page.server.ts +++ b/src/routes/(pages)/app/(app)/[accountId]/+page.server.ts @@ -31,6 +31,7 @@ const PROFILE_PAGE_QUERY = gql` query ProfilePage($address: String!, $chains: [SupportedChain!]) { userByAddress(address: $address, chains: $chains) { account { + driver address accountId } From 00bfa5b3edcdc974e1b3625a072d5780a5e6364f Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Wed, 17 Sep 2025 21:45:39 -0700 Subject: [PATCH 057/155] Make orcid share image work poorly --- .../[orcidId]/components/orcid-profile.svelte | 2 +- .../orcids/[orcidId].png/+server.ts | 142 ++++++++++++++++++ 2 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 src/routes/api/share-images/orcids/[orcidId].png/+server.ts diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte index 5f42e9355..b70f2225a 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte @@ -32,7 +32,7 @@ let supportersSectionSkeleton: SectionSkeleton | undefined; // TODO: implement - $: imageBaseUrl = `/api/share-images/orcid/${encodeURIComponent(orcid.id)}.png`; + $: imageBaseUrl = `/api/share-images/orcids/${encodeURIComponent(orcid.id)}.png`; $: withdrawableBalances = orcidAccount.withdrawableBalances ?? []; $: support = orcidAccount.support ?? []; diff --git a/src/routes/api/share-images/orcids/[orcidId].png/+server.ts b/src/routes/api/share-images/orcids/[orcidId].png/+server.ts new file mode 100644 index 000000000..83fc5f72f --- /dev/null +++ b/src/routes/api/share-images/orcids/[orcidId].png/+server.ts @@ -0,0 +1,142 @@ +import type { RequestHandler } from './$types'; +import assert from '$lib/utils/assert'; +import { error } from '@sveltejs/kit'; +import loadImage from '../../loadImage'; +import getContrastColor from '$lib/utils/get-contrast-text-color'; +import satori from 'satori'; +import { html as toReactElement } from 'satori-html'; +import loadFonts from '../../loadFonts'; +import { Resvg } from '@resvg/resvg-js'; +import getBackgroundImage from '../../getBackgroundImage'; +import { gql } from 'graphql-request'; +import query from '$lib/graphql/dripsQL'; +import type { OrcidQuery, OrcidQueryVariables } from './__generated__/gql.generated'; +import network from '$lib/stores/wallet/network'; + +export const GET: RequestHandler = async ({ url, fetch, params }) => { + const { orcidId } = params; + assert(orcidId, 'Missing orcidId param'); + + const projectQuery = gql` + query Orcid($orcid: String!, $chain: SupportedChain!) { + orcidLinkedIdentityByOrcid(orcid: $orcid, chain: $chain) { + chain + areSplitsValid + isClaimed + support { + __typename + } + } + } + `; + + const res = await query( + projectQuery, + { orcid: orcidId, chain: network.gqlName }, + fetch, + ); + const { orcidLinkedIdentityByOrcid: orcidAccount } = res; + try { + assert(orcidAccount); + } catch { + error(404); + } + + const orcidName = `Unknown`; + + // const projectData = filterCurrentChainData(project.chainData); + + // const emoji = + // isClaimed(projectData) && projectData.avatar.__typename === 'EmojiAvatar' + // ? sanitize(projectData.avatar.emoji, { + // allowedTags: [], + // allowedAttributes: {}, + // }) + // : 'none'; + + // const cid = + // isClaimed(projectData) && projectData.avatar.__typename === 'ImageAvatar' + // ? projectData.avatar.cid + // : 'none'; + const supportersCount = orcidAccount.support.length; + + // const dependenciesCount = isClaimed(projectData) + // ? projectData.splits.dependencies.length.toString() + // : '0'; + + // const color = isClaimed(projectData) ? projectData.color : 'none'; + const target = url.searchParams.get('target'); + + try { + assert(target === 'twitter' || target === 'og'); + } catch { + error(400, 'Invalid or missing query params'); + } + + const height = target === 'twitter' ? 600 : 675; + + // const bgColor = color === 'none' ? '#5555FF' : color; + const bgColor = 'rgba(85, 85, 255, 1)'; + const contrastColor = getContrastColor(bgColor); + + const bgTheme = contrastColor === 'black' ? 'dark' : 'light'; + const boxIconDataURI = await loadImage(`/assets/share/box-${bgTheme}.png`, fetch); + + const textColor = '#FFFFFF'; + + const supportersString = supportersCount === 0 ? 'Support' : 'Supporters'; + + // const twemojiElem = (emoji !== 'none' && twemoji(emoji)) ?? undefined; + // const twemojiSrc = (twemojiElem && /src\s*=\s*"(.+?)"/g.exec(twemojiElem)?.[1]) ?? undefined; + + // const twemojiImg = twemojiSrc && (await (await fetch(twemojiSrc)).text()); + + // const resizedTwemojImg = + // typeof twemojiImg === 'string' + // ? twemojiImg.replace(' + TODO +
`; + + const svg = await satori( + toReactElement(`
+ ${getBackgroundImage(bgColor, textColor, target)} +
+ ORCID iD +
+ ${avatarHtml} + ${orcidName} +
+
+ + ${supportersCount} ${supportersString} +
+
+
`), + { + width: 1200, + height: height, + fonts: await loadFonts(fetch), + }, + ); + + const resvg = new Resvg(svg, { + fitTo: { + mode: 'width', + value: 1200, + }, + }); + + const image = resvg.render(); + + return new Response(new Uint8Array(image.asPng()), { + headers: { + 'content-type': 'image/png', + 'cache-control': 'public, max-age=86400', // 24 hours + }, + }); +}; From 1cf39f29c06fb5118c59a22e3fe190ae9d1ed9e7 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Thu, 18 Sep 2025 16:13:41 -0700 Subject: [PATCH 058/155] Update one time donation language for ORCIDs --- src/lib/flows/create-donation/input-details.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/flows/create-donation/input-details.svelte b/src/lib/flows/create-donation/input-details.svelte index 41d4d3047..95102d133 100644 --- a/src/lib/flows/create-donation/input-details.svelte +++ b/src/lib/flows/create-donation/input-details.svelte @@ -208,9 +208,9 @@ After your donation... {#if receiver.__typename === 'OrcidLinkedIdentity'} - - Funds can be collected on {nextSettlementDate === 'daily' ? 'today' : formatDate(nextSettlementDate())} Date: Thu, 18 Sep 2025 17:09:23 -0700 Subject: [PATCH 059/155] Refine ORCID share image, add icon and ORCID iD --- .../orcids/[orcidId].png/+server.ts | 54 +++--------------- static/assets/share/orcid.png | Bin 0 -> 2943 bytes 2 files changed, 9 insertions(+), 45 deletions(-) create mode 100644 static/assets/share/orcid.png diff --git a/src/routes/api/share-images/orcids/[orcidId].png/+server.ts b/src/routes/api/share-images/orcids/[orcidId].png/+server.ts index 83fc5f72f..529646552 100644 --- a/src/routes/api/share-images/orcids/[orcidId].png/+server.ts +++ b/src/routes/api/share-images/orcids/[orcidId].png/+server.ts @@ -2,7 +2,6 @@ import type { RequestHandler } from './$types'; import assert from '$lib/utils/assert'; import { error } from '@sveltejs/kit'; import loadImage from '../../loadImage'; -import getContrastColor from '$lib/utils/get-contrast-text-color'; import satori from 'satori'; import { html as toReactElement } from 'satori-html'; import loadFonts from '../../loadFonts'; @@ -35,6 +34,7 @@ export const GET: RequestHandler = async ({ url, fetch, params }) => { { orcid: orcidId, chain: network.gqlName }, fetch, ); + const { orcidLinkedIdentityByOrcid: orcidAccount } = res; try { assert(orcidAccount); @@ -43,76 +43,40 @@ export const GET: RequestHandler = async ({ url, fetch, params }) => { } const orcidName = `Unknown`; - - // const projectData = filterCurrentChainData(project.chainData); - - // const emoji = - // isClaimed(projectData) && projectData.avatar.__typename === 'EmojiAvatar' - // ? sanitize(projectData.avatar.emoji, { - // allowedTags: [], - // allowedAttributes: {}, - // }) - // : 'none'; - - // const cid = - // isClaimed(projectData) && projectData.avatar.__typename === 'ImageAvatar' - // ? projectData.avatar.cid - // : 'none'; const supportersCount = orcidAccount.support.length; - // const dependenciesCount = isClaimed(projectData) - // ? projectData.splits.dependencies.length.toString() - // : '0'; - - // const color = isClaimed(projectData) ? projectData.color : 'none'; const target = url.searchParams.get('target'); - try { assert(target === 'twitter' || target === 'og'); } catch { error(400, 'Invalid or missing query params'); } - const height = target === 'twitter' ? 600 : 675; - // const bgColor = color === 'none' ? '#5555FF' : color; - const bgColor = 'rgba(85, 85, 255, 1)'; - const contrastColor = getContrastColor(bgColor); - - const bgTheme = contrastColor === 'black' ? 'dark' : 'light'; - const boxIconDataURI = await loadImage(`/assets/share/box-${bgTheme}.png`, fetch); + const [orcidDataURI, heartDataURI] = await Promise.all([ + loadImage(`/assets/share/orcid.png`, fetch), + loadImage(`/assets/share/heart.png`, fetch), + ]); + const bgColor = 'rgba(85, 85, 255, 1)'; const textColor = '#FFFFFF'; const supportersString = supportersCount === 0 ? 'Support' : 'Supporters'; - // const twemojiElem = (emoji !== 'none' && twemoji(emoji)) ?? undefined; - // const twemojiSrc = (twemojiElem && /src\s*=\s*"(.+?)"/g.exec(twemojiElem)?.[1]) ?? undefined; - - // const twemojiImg = twemojiSrc && (await (await fetch(twemojiSrc)).text()); - - // const resizedTwemojImg = - // typeof twemojiImg === 'string' - // ? twemojiImg.replace(' - TODO - `; - const svg = await satori( toReactElement(`
${getBackgroundImage(bgColor, textColor, target)}
ORCID iD
- ${avatarHtml} ${orcidName}
- + + ${orcidId} + ${supportersCount} ${supportersString}
diff --git a/static/assets/share/orcid.png b/static/assets/share/orcid.png new file mode 100644 index 0000000000000000000000000000000000000000..b93c468afbf3090d9467c1d6b2b1c362995666db GIT binary patch literal 2943 zcmZ`*c{~#iAKvEL+;bKs#vIL2Q3=r&xmCzqbF&ngIVQ9mbKiGFu230~`?ip)@N*Mo zB6qeZBl7yaf4}b^&+~kbzn{EX-J>F%kf%PU1LpWB~v;1oX9$<^jO1yrhhA zRlW~#LBarG$^pz$5oE-SgN}#-YoJE_Kwb(b6!706)RTfZLOa2(vR zx2qBVsn+Je_TKsaRImhz7xlj=b4XG`AMomZ(q7Vlr|j|Z{y)XK70I#!3lOBs?e4gZ zJ=%`*zLm$5`#102CHgOy3&{-GNN7#h0>T&UqJ?b~x*{gCELu-##RxB~32;?1EpaX5EtP>Gnl#O=c*}OgY>#MYUi-diZ6Vd& z!*QXQmWWa}&ZItCOU=i zuBAp5!SKs(bX_|zmjg@NFKU78O^Z|9ChxBgl!f?G1{y6o|fnyT_$$~?Y`3BYCnBPRQwbCs(|Ck z?>8znUssE=gp_W2(4w<(1PWi4#N|)rm*U!1P3^jl7rG8DjW& z&xB`lX z!hp03ZQy&N%RujXoMcTn5RqGtE}Valf!Vd%MtoF(K@RQgKE z^x%sl^v{%ayQf(R{F6ERU5(m34V!G2DKOWwtSIX+^*<|$r>83eW~l6&0c7O&FG8(0 z`i?S7Y=uK)JGxyacyZE8hUG?Z*iEKVXCmmgZ2zSSaoFT-cA$Ero5*({?{@O0tG-ll zVV;0gaJ%M&y$OlHV%=`yP&Sb1$|7kW+2yGWcT&k5lhiExCD|2vmP66mm-+Vvo{F#W z$W~ojLpKAdB72b)Zj_s$o=JzYV)k*EE_BZZl6{$GNc5#=eaw#jlcy317JTgEd(^l4 zg^8HAK9MCoHzmqQd2Zmwx~bn)_fc@VsUy#bxp}2EVZFcbSS{l9Tql;>EoD9A=*oOB zmh3G3LwNq%^KcB6~;utWvP=`u_kDWe^-VJ z468IPvUniH!Ah+6hmYND!bj{RehI!Z@z3d#};MJC?OEL~@6=^(FaUV|0 zg>CmI6RQp9`j(U*JM@NH1#ULkR^7X3Az!WU+l^F+N2sI!Di9w8i|%{PE~rgar>-&F za?9aNW?T`WtitBMiOn*)svs(Pe7DQGfeq~=X#`RaD zrOWU=li%lxy?k7Qx&zLp=426I}M~VztH&<4ZK<7krZZ zde=5%w)u8|6*5Z^@@YPrD%-Zj$5?hc3)y=m+@+KRP`G-?J{TN9I|DMO9IcUrH=rw} zCm!?b=m}}7r`fTGU;0A|?)0^#a`K#?d^fDTS@9zFDfHV8o6@sHh9d3Sos=oObXDe) z7tpT+e;F1?{_*k`?D~=UB@p9q`tr}hJzu8d&fLo?$nJxT<_=v`Z~*$V;7EO?01r8Q z#82Rs%kLU<4&#d)Q3LNFzW1uY@D1{9REvTWTU2}QgbM-71%gT%L8*Q6{d ztiC;1VEqQ>c1!1vMTBn;f3csKds-eDGF>cbpy0yC$N7FqJy(dWy}`9<@t+tcJ~q0Y zlEr84s;E_gvuzFCT|^a#Em&YpI)Rma!%u+;SS@*S-b3RkB=g zL9Q^ZI?dHmM=(c`^=~!JuCvC0AEe=0>g3U5(`+35BmEPECcv;#v3?Qd3?CbbCA@9k zTI)^yTsf3y%t6zXc%%_)s_^n}ODgrf$FgGEj>6%es>y&yU;zhMA>m1mX;Z$P`|Tu~ i`Un4~rGjUKj+sP|w<4leOQTNuGC*I)M7t8@5cVIe$BS(M literal 0 HcmV?d00001 From 9ef234df8f8049a3fa3d9f64d5c0b324a529a125 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Thu, 18 Sep 2025 18:05:24 -0700 Subject: [PATCH 060/155] Further cleanup of ORCID claiming; add withdrawable balances --- .../add-ethereum-address.svelte | 4 +-- .../choose-network/choose-network.svelte | 2 +- .../enter-orcid-id/enter-orcid-id.svelte | 15 ++-------- .../steps/enter-orcid-id/enter-orcid-id.ts | 5 ++-- .../steps/review/review.svelte | 3 +- .../set-splits-and-emit-metadata.svelte | 8 ++--- .../steps/success/success.svelte | 24 +++++++-------- .../(app)/orcids/[orcidId]/+page.server.ts | 1 + .../components/unclaimed-orcid-card.svelte | 29 +++++++------------ 9 files changed, 35 insertions(+), 56 deletions(-) diff --git a/src/lib/flows/claim-orcid-flow/steps/add-ethereum-address/add-ethereum-address.svelte b/src/lib/flows/claim-orcid-flow/steps/add-ethereum-address/add-ethereum-address.svelte index d742c12a7..b1874f18c 100644 --- a/src/lib/flows/claim-orcid-flow/steps/add-ethereum-address/add-ethereum-address.svelte +++ b/src/lib/flows/claim-orcid-flow/steps/add-ethereum-address/add-ethereum-address.svelte @@ -36,9 +36,9 @@ $: link = `http://0.0.0.0/?ethereum_owned_by=${$walletStore.address}&orcid=${$context.claimableId}`; $: editing = !!$context.claimableProof; $: description = editing - ? `To verify you are the owner of this ORCID iD, please add the funding URL to the Websites & social links section of your ORCID profile.` + ? `To verify you are the owner of this ORCID iD, please add or edit the funding URL to the Websites & social links section of your ORCID profile.` : `To verify you are the owner of this ORCID iD, please add the funding URL to the Websites & social links section of your ORCID profile.`; - $: checkboxLabel = editing ? 'I edited link' : 'I added this to my ORCID profile'; + $: checkboxLabel = editing ? 'I added or edited the URL.' : 'I added this to my ORCID profile'; onMount(() => { $context.linkedToClaimable = false; diff --git a/src/lib/flows/claim-orcid-flow/steps/choose-network/choose-network.svelte b/src/lib/flows/claim-orcid-flow/steps/choose-network/choose-network.svelte index 1023df380..5e44a2d51 100644 --- a/src/lib/flows/claim-orcid-flow/steps/choose-network/choose-network.svelte +++ b/src/lib/flows/claim-orcid-flow/steps/choose-network/choose-network.svelte @@ -28,7 +28,7 @@
diff --git a/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.svelte b/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.svelte index 53d200466..4e7d8ef0a 100644 --- a/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.svelte +++ b/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.svelte @@ -1,17 +1,5 @@ diff --git a/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.ts b/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.ts index 5322baaa0..ca589f28d 100644 --- a/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.ts +++ b/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.ts @@ -3,7 +3,6 @@ import { get, type Writable } from 'svelte/store'; import walletStore from '$lib/stores/wallet/wallet.store'; import { fetchOrcid } from '../../../../utils/orcids/fetch-orcid'; -// TODO: load orcid info export async function loadFundingInfo(context: Writable): Promise { const $walletStore = get(walletStore); const address = $walletStore.address ?? ''; @@ -13,13 +12,13 @@ export async function loadFundingInfo(context: Writable): Promise { : $walletStore.network.name : ''; - // We can't make a useful FUNDING.json without an address or network. + // We can't make a useful claim URL without an address or network. if (!address || !network) { return; } const $context = get(context); - const orcidInfo = await fetchOrcid($context.claimableId, fetch) + const orcidInfo = await fetchOrcid($context.claimableId, fetch); // TODO: handle, this is bad if (!orcidInfo) { throw new Error('ORCID not found'); diff --git a/src/lib/flows/claim-orcid-flow/steps/review/review.svelte b/src/lib/flows/claim-orcid-flow/steps/review/review.svelte index c29c7ddd6..b20d381d2 100644 --- a/src/lib/flows/claim-orcid-flow/steps/review/review.svelte +++ b/src/lib/flows/claim-orcid-flow/steps/review/review.svelte @@ -63,7 +63,8 @@ dispatch('goForward'); } - let withdrawableBalances: MergeWithdrawableBalancesFragment[] = []; + let withdrawableBalances: MergeWithdrawableBalancesFragment[] = + $context.claimableAccount?.withdrawableBalances ?? []; $: hasCollectableAmount = withdrawableBalances.filter((wb) => BigInt(wb.collectableAmount) > 0n).length > 0; diff --git a/src/lib/flows/claim-orcid-flow/steps/set-splits-and-emit-metadata/set-splits-and-emit-metadata.svelte b/src/lib/flows/claim-orcid-flow/steps/set-splits-and-emit-metadata/set-splits-and-emit-metadata.svelte index 6d5fd6369..b66650cad 100644 --- a/src/lib/flows/claim-orcid-flow/steps/set-splits-and-emit-metadata/set-splits-and-emit-metadata.svelte +++ b/src/lib/flows/claim-orcid-flow/steps/set-splits-and-emit-metadata/set-splits-and-emit-metadata.svelte @@ -201,17 +201,17 @@ // and skip the step that waits for everything to be in the right state if so. // We already kick off the gasless owner update after the user confirms the funding.json step, // so it could be that everything already resolved by the time we get here. - const projectAlreadyReadyForClaimTx = await checkOrcidInExpectedStateForClaiming(); + const orcidAlreadyReadyForClaimTx = await checkOrcidInExpectedStateForClaiming(); - return { tx, projectAlreadyReadyForClaimTx }; + return { tx, orcidAlreadyReadyForClaimTx }; }, messages: { duringBefore: 'Preparing to claim ORCID...', }, - transactions: async ({ tx, projectAlreadyReadyForClaimTx }) => { - const ownerUpdateTransactionSteps = projectAlreadyReadyForClaimTx + transactions: async ({ tx, orcidAlreadyReadyForClaimTx }) => { + const ownerUpdateTransactionSteps = orcidAlreadyReadyForClaimTx ? [] : await generateOwnerUpdateTransactions( $context.gaslessOwnerUpdateTaskId, diff --git a/src/lib/flows/claim-orcid-flow/steps/success/success.svelte b/src/lib/flows/claim-orcid-flow/steps/success/success.svelte index 16b38beb3..6db510726 100644 --- a/src/lib/flows/claim-orcid-flow/steps/success/success.svelte +++ b/src/lib/flows/claim-orcid-flow/steps/success/success.svelte @@ -11,6 +11,7 @@ import { createEventDispatcher } from 'svelte'; import type { StepComponentEvents } from '$lib/components/stepper/types'; import buildOrcidUrl from '$lib/utils/orcids/build-orcid-url'; + import mergeAmounts from '$lib/utils/amounts/merge-amounts'; export let context: Writable; @@ -23,23 +24,19 @@ async function viewOrcid() { loading = true; - const collectedFunds = false - // const collectedFunds = - // mergeAmounts( - // projectChainData?.withdrawableBalances.map((wb) => ({ - // tokenAddress: wb.tokenAddress, - // amount: BigInt(wb.collectableAmount) + BigInt(wb.splittableAmount), - // })) ?? [], - // ).length > 0; + const collectedFunds = + mergeAmounts( + $context.claimableAccount?.withdrawableBalances.map((wb) => ({ + tokenAddress: wb.tokenAddress, + amount: BigInt(wb.collectableAmount) + BigInt(wb.splittableAmount), + })) ?? [], + ).length > 0; const ownAccountId = $walletStore.dripsAccountId; assert(ownAccountId); await goto( - buildUrl( - buildOrcidUrl($context.claimableId), - collectedFunds ? { collectHint: 'true' } : {}, - ), + buildUrl(buildOrcidUrl($context.claimableId), collectedFunds ? { collectHint: 'true' } : {}), ).then(() => { loading = false; dispatch('conclude'); @@ -62,8 +59,7 @@ {:else}

Congratulations!

Youʼve successfully claimed your ORCID.

- View ORCID profile {/if}
diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts index 4af474c70..93f2db252 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts @@ -11,6 +11,7 @@ import type { OrcidProfileFragment } from './components/__generated__/gql.genera /** * 0009-0007-5482-8654 me in ORCID prod * 0009-0007-1106-8413 drips.network in ORCID sandbox + * 0000-0002-2677-7622 random other sandbox */ export const load = (async ({ params, fetch }) => { if (!isValidOrcidId(params.orcidId)) { diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/unclaimed-orcid-card.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/unclaimed-orcid-card.svelte index dffa9552b..b0299169c 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/unclaimed-orcid-card.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/unclaimed-orcid-card.svelte @@ -1,22 +1,13 @@ - - @@ -30,7 +21,7 @@ import { gql } from 'graphql-request'; import type { UnclaimedOrcidCardFragment } from './__generated__/gql.generated'; import { - // MERGE_WITHDRAWABLE_BALANCES_FRAGMENT, + MERGE_WITHDRAWABLE_BALANCES_FRAGMENT, mergeCollectableFunds, mergeSplittableFunds, } from '$lib/utils/merge-withdrawable-balances'; @@ -47,9 +38,8 @@ export let orcidAccount: UnclaimedOrcidCardFragment; - // TODO: where are the funds? - $: collectableFunds = mergeCollectableFunds([]); - $: splittableFunds = mergeSplittableFunds([]); + $: collectableFunds = mergeCollectableFunds(orcidAccount.withdrawableBalances); + $: splittableFunds = mergeSplittableFunds(orcidAccount.withdrawableBalances); $: mergedUnclaimedFunds = mergeAmounts(collectableFunds, splittableFunds); @@ -132,7 +122,8 @@
{/if} - {#if splittableFunds.length > 0} + + {:else}
From ea88b2cd352dd70b2d1864cb1ee9a0a2281bbd5c Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Fri, 19 Sep 2025 17:11:42 -0700 Subject: [PATCH 061/155] Add given name and family name to ORCID share image --- .../api/share-images/orcids/[orcidId].png/+server.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/routes/api/share-images/orcids/[orcidId].png/+server.ts b/src/routes/api/share-images/orcids/[orcidId].png/+server.ts index 529646552..0dc3ea4e3 100644 --- a/src/routes/api/share-images/orcids/[orcidId].png/+server.ts +++ b/src/routes/api/share-images/orcids/[orcidId].png/+server.ts @@ -22,6 +22,10 @@ export const GET: RequestHandler = async ({ url, fetch, params }) => { chain areSplitsValid isClaimed + orcidMetadata { + givenName + familyName + } support { __typename } @@ -42,7 +46,9 @@ export const GET: RequestHandler = async ({ url, fetch, params }) => { error(404); } - const orcidName = `Unknown`; + const firstName = orcidAccount.orcidMetadata?.givenName ?? ''; + const lastname = orcidAccount.orcidMetadata?.familyName ?? ''; + const orcidName = `${firstName} ${lastname}`; const supportersCount = orcidAccount.support.length; const target = url.searchParams.get('target'); From c3fa50424ed67de52123831d6bd44604880e97f2 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Fri, 19 Sep 2025 17:26:15 -0700 Subject: [PATCH 062/155] Change ORCID name component so that it uses new orcidMetadata --- src/lib/utils/orcids/display-name.ts | 9 ++++ .../utils/orcids/filter-current-chain-data.ts | 47 ------------------- .../components/linked-identities-card.svelte | 4 ++ .../[orcidId]/components/orcid-badge.svelte | 4 ++ .../[orcidId]/components/orcid-name.svelte | 11 +++-- .../orcids/[orcidId].png/+server.ts | 6 +-- 6 files changed, 28 insertions(+), 53 deletions(-) create mode 100644 src/lib/utils/orcids/display-name.ts delete mode 100644 src/lib/utils/orcids/filter-current-chain-data.ts diff --git a/src/lib/utils/orcids/display-name.ts b/src/lib/utils/orcids/display-name.ts new file mode 100644 index 000000000..95d187f10 --- /dev/null +++ b/src/lib/utils/orcids/display-name.ts @@ -0,0 +1,9 @@ +import type { OrcidNameFragment } from '../../../routes/(pages)/app/(app)/orcids/[orcidId]/components/__generated__/gql.generated'; + +export default function getOrcidDisplayName(orcidAccount: OrcidNameFragment) { + const firstName = orcidAccount.orcidMetadata?.givenName ?? ''; + const lastName = orcidAccount.orcidMetadata?.familyName ?? ''; + + const fullName = `${firstName} ${lastName}`; + return !fullName.trim() ? orcidAccount.orcid : fullName; +} diff --git a/src/lib/utils/orcids/filter-current-chain-data.ts b/src/lib/utils/orcids/filter-current-chain-data.ts deleted file mode 100644 index 4001bf78d..000000000 --- a/src/lib/utils/orcids/filter-current-chain-data.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { SupportedChain } from '$lib/graphql/__generated__/base-types'; -import network from '$lib/stores/wallet/network'; -import assert from '$lib/utils/assert'; -import isClaimed, { isUnclaimed } from './is-claimed'; - -function isOrcidData(data: { - __typename: string; -}): data is { __typename: 'ClaimedOrcidAccountData' | 'UnClaimedOrcidAccountData' } { - return data.__typename === 'ClaimedOrcidAccountData' || data.__typename === 'UnClaimedOrcidAccountData'; -} - -export default function filterCurrentChainData< - T extends - | { __typename: string; chain: SupportedChain } - | { __typename: string; chain: SupportedChain }, - /** If filtering ORCID chain data, use this to enforce a claimed / unclaimed status. Must be undefined if not ORCID data. */ - CT extends 'claimed' | 'unclaimed' | undefined, ->( - items: T[], - expectedOrcidStatus?: CT, - chainOverride?: SupportedChain, -): CT extends 'claimed' - ? T & { __typename: 'ClaimedOrcidAccountData' } - : CT extends 'unclaimed' - ? T & { __typename: 'UnClaimedOrcidAccountData' } - : T { - const expectedChain = chainOverride ?? network.gqlName; - - const filteredItems = items.filter((item) => item.chain === expectedChain); - const item = filteredItems[0]; - - assert(item, `Expected ORCID data for chain ${expectedChain}, ${JSON.stringify(items[0])}`); - - if (expectedOrcidStatus) { - assert(isOrcidData(item), 'Expected ORCID data'); - assert( - expectedOrcidStatus === 'unclaimed' ? isUnclaimed(item) : isClaimed(item), - `Expected ${expectedOrcidStatus} ORCID data`, - ); - } - - return item as CT extends 'claimed' - ? T & { __typename: 'ClaimedOrcidAccountData' } - : CT extends 'unclaimed' - ? T & { __typename: 'UnClaimedOrcidAccountData' } - : T; -} diff --git a/src/routes/(pages)/app/(app)/[accountId]/components/linked-identities-card.svelte b/src/routes/(pages)/app/(app)/[accountId]/components/linked-identities-card.svelte index 18ddeecc6..f3d7a6310 100644 --- a/src/routes/(pages)/app/(app)/[accountId]/components/linked-identities-card.svelte +++ b/src/routes/(pages)/app/(app)/[accountId]/components/linked-identities-card.svelte @@ -10,6 +10,10 @@ chain isClaimed areSplitsValid + orcidMetadata { + givenName + familyName + } } } `; diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-badge.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-badge.svelte index 4625ce97a..318b1c18a 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-badge.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-badge.svelte @@ -10,6 +10,10 @@ orcid isClaimed areSplitsValid + orcidMetadata { + givenName + familyName + } } `; diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-name.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-name.svelte index 78bffa0f1..aa29c078b 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-name.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-name.svelte @@ -4,24 +4,29 @@ export const ORCID_NAME_FRAGMENT = gql` fragment OrcidName on OrcidLinkedIdentity { orcid + orcidMetadata { + givenName + familyName + } } `; {orcid.orcid}{displayName} diff --git a/src/routes/api/share-images/orcids/[orcidId].png/+server.ts b/src/routes/api/share-images/orcids/[orcidId].png/+server.ts index 0dc3ea4e3..73647e041 100644 --- a/src/routes/api/share-images/orcids/[orcidId].png/+server.ts +++ b/src/routes/api/share-images/orcids/[orcidId].png/+server.ts @@ -11,6 +11,7 @@ import { gql } from 'graphql-request'; import query from '$lib/graphql/dripsQL'; import type { OrcidQuery, OrcidQueryVariables } from './__generated__/gql.generated'; import network from '$lib/stores/wallet/network'; +import getOrcidDisplayName from '$lib/utils/orcids/display-name'; export const GET: RequestHandler = async ({ url, fetch, params }) => { const { orcidId } = params; @@ -22,6 +23,7 @@ export const GET: RequestHandler = async ({ url, fetch, params }) => { chain areSplitsValid isClaimed + orcid orcidMetadata { givenName familyName @@ -46,9 +48,7 @@ export const GET: RequestHandler = async ({ url, fetch, params }) => { error(404); } - const firstName = orcidAccount.orcidMetadata?.givenName ?? ''; - const lastname = orcidAccount.orcidMetadata?.familyName ?? ''; - const orcidName = `${firstName} ${lastname}`; + const orcidName = getOrcidDisplayName(orcidAccount); const supportersCount = orcidAccount.support.length; const target = url.searchParams.get('target'); From 3241d370a20bd7917d61c0a6685094cac76d5c8c Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 22 Sep 2025 14:05:55 -0700 Subject: [PATCH 063/155] Ensure ORCID profile gets name from graphql API, update docker-compost.yml with graph ql api env vars --- docker-compose.yml | 4 ++++ src/lib/utils/orcids/display-name.ts | 10 ++++++++-- src/lib/utils/orcids/entities.ts | 11 ++++++++--- .../app/(app)/orcids/[orcidId]/+page.server.ts | 2 -- .../[orcidId]/components/orcid-profile-fragments.ts | 4 ++++ .../[orcidId]/components/orcid-profile-header.svelte | 9 ++++++++- .../orcids/[orcidId]/components/orcid-profile.svelte | 7 ++++--- 7 files changed, 36 insertions(+), 11 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 11bac1a36..98b5f3dc8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -98,6 +98,10 @@ services: - DRIPS_API_KEY=123 - GITHUB_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN} - REDIS_URL=redis://redis:6379 + - ORCID_API_ENDPOINT=${ORCID_API_ENDPOINT-https://pub.sandbox.orcid.org/v3.0} + - ORCID_CLIENT_ID=${ORCID_CLIENT_ID} + - ORCID_CLIENT_SECRET=${ORCID_CLIENT_SECRET} + - ORCID_TOKEN_ENDPOINT=${ORCID_TOKEN_ENDPOINT-https://sandbox.orcid.org/oauth/token} ports: - '8080:8080' depends_on: diff --git a/src/lib/utils/orcids/display-name.ts b/src/lib/utils/orcids/display-name.ts index 95d187f10..cdf689e48 100644 --- a/src/lib/utils/orcids/display-name.ts +++ b/src/lib/utils/orcids/display-name.ts @@ -1,6 +1,12 @@ -import type { OrcidNameFragment } from '../../../routes/(pages)/app/(app)/orcids/[orcidId]/components/__generated__/gql.generated'; +type orcidWithMetadata = { + orcid: string; + orcidMetadata?: { + givenName?: string | null; + familyName?: string | null; + } | null; +}; -export default function getOrcidDisplayName(orcidAccount: OrcidNameFragment) { +export default function getOrcidDisplayName(orcidAccount: orcidWithMetadata) { const firstName = orcidAccount.orcidMetadata?.givenName ?? ''; const lastName = orcidAccount.orcidMetadata?.familyName ?? ''; diff --git a/src/lib/utils/orcids/entities.ts b/src/lib/utils/orcids/entities.ts index 0fb172cd9..0104bb6a9 100644 --- a/src/lib/utils/orcids/entities.ts +++ b/src/lib/utils/orcids/entities.ts @@ -1,3 +1,4 @@ +import getOrcidDisplayName from './display-name'; import type { OrcidApiResponse } from './schemas'; export const CLAIMING_URL_NAME = 'DRIPS_OWNERSHIP_CLAIM'; @@ -19,9 +20,13 @@ export default class Orcid { get name(): string { const name = this.data.person.name; - return ( - name['given-names']?.value || name['credit-name']?.value || name['family-name']?.value || '' - ); + return getOrcidDisplayName({ + orcid: this.id, + orcidMetadata: { + givenName: name['given-names']?.value, + familyName: name['family-name']?.value, + }, + }); } get bio(): string { diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts index 93f2db252..7fe82d827 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts @@ -23,8 +23,6 @@ export const load = (async ({ params, fetch }) => { return error(404); } - // TODO: I think there's a problem here, I thought we were supposed to fetch the orcid - // by accountId... We will probably want to support urls that include accountid as well. let orcidAccount = undefined; const orcidGqlResponse = await fetchOrcidAccount(params.orcidId, fetch); if (orcidGqlResponse.orcidLinkedIdentityByOrcid) { diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-fragments.ts b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-fragments.ts index 50577b9f4..d8cc3acec 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-fragments.ts +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-fragments.ts @@ -30,5 +30,9 @@ export const ORCID_PROFILE_FRAGMENT = gql` tokenAddress amount } + orcidMetadata { + givenName + familyName + } } `; diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte index 756b80124..7c83a49fd 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile-header.svelte @@ -10,6 +10,10 @@ } isClaimed areSplitsValid + orcidMetadata { + givenName + familyName + } } `; @@ -24,10 +28,13 @@ import type Orcid from '$lib/utils/orcids/entities'; import ShareButton from '$lib/components/share-button/share-button.svelte'; import IdentityBadge from '$lib/components/identity-badge/identity-badge.svelte'; + import getOrcidDisplayName from '$lib/utils/orcids/display-name'; export let orcid: Orcid; export let orcidAccount: OrcidProfileHeaderFragment; export let shareButton: ComponentProps | undefined = undefined; + + const orcidName = getOrcidDisplayName(orcidAccount);
@@ -36,7 +43,7 @@
-

{orcid.name}

+

{orcidName}

{#if orcid.bio} From c7186e263730ed38881e2b3bb3e9070b028610f6 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 22 Sep 2025 16:29:09 -0700 Subject: [PATCH 064/155] Minimize use of orcid profile data in orcid profile component --- .../app/(app)/orcids/[orcidId]/+page.server.ts | 5 ----- .../[orcidId]/components/orcid-profile.svelte | 15 ++++++++++----- .../[orcidId]/components/orcid-tooltip.svelte | 4 ++++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts index 7fe82d827..8c71dc5f2 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts @@ -8,11 +8,6 @@ import { } from '../../../../../../lib/utils/orcids/fetch-orcid'; import type { OrcidProfileFragment } from './components/__generated__/gql.generated'; -/** - * 0009-0007-5482-8654 me in ORCID prod - * 0009-0007-1106-8413 drips.network in ORCID sandbox - * 0000-0002-2677-7622 random other sandbox - */ export const load = (async ({ params, fetch }) => { if (!isValidOrcidId(params.orcidId)) { return error(404); diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte index ddea45e1b..43162273c 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte @@ -27,7 +27,9 @@ import launchClaimOrcid from '$lib/utils/launch-claim-orcid'; import getOrcidDisplayName from '$lib/utils/orcids/display-name'; + // for bio export let orcid: Orcid; + // for everything else export let orcidAccount: OrcidProfileFragment; let supportersSectionSkeleton: SectionSkeleton | undefined; @@ -38,7 +40,7 @@ $: support = orcidAccount.support ?? []; function claimOrcid() { - launchClaimOrcid(orcid.id); + launchClaimOrcid(orcidAccount.orcid); } @@ -50,7 +52,7 @@ /> - + @@ -66,7 +68,10 @@ later.{/if}
- + @@ -83,7 +88,7 @@ {orcid} {orcidAccount} shareButton={{ - url: buildOrcidUrl(orcid.id, { absolute: true }), + url: buildOrcidUrl(orcidAccount.orcid, { absolute: true }), downloadableImageUrl: `${imageBaseUrl}?target=og`, }} /> @@ -127,7 +132,7 @@ unclaimedTokensExpanded={withdrawableBalances.length > 0} showClaimButton={!orcidAccount.isClaimed} on:claimButtonClick={() => - goto(buildUrl('/app/claim-orcid', { orcidToClaim: orcid.id }))} + goto(buildUrl('/app/claim-orcid', { orcidToClaim: orcidAccount.orcid }))} />
diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-tooltip.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-tooltip.svelte index 88c18d005..f59e88051 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-tooltip.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-tooltip.svelte @@ -10,6 +10,10 @@ } isClaimed areSplitsValid + orcidMetadata { + givenName + familyName + } } `; From 4effeb98f99d434896313e810011bcf702fe0536 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 22 Sep 2025 17:04:39 -0700 Subject: [PATCH 065/155] Fix up some ORCID utility functions --- .../enter-orcid-id/enter-orcid-id.svelte | 6 +++-- src/lib/utils/orcids/display-name.ts | 14 +++++----- src/lib/utils/orcids/is-claimed.ts | 26 ++++--------------- 3 files changed, 15 insertions(+), 31 deletions(-) diff --git a/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.svelte b/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.svelte index 4e7d8ef0a..868fa582b 100644 --- a/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.svelte +++ b/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.svelte @@ -44,6 +44,7 @@ } from '../../../../../routes/(pages)/app/(app)/orcids/[orcidId]/components/unclaimed-orcid-card.svelte'; import { fetchOrcid } from '../../../../utils/orcids/fetch-orcid'; import isValidOrcidId from '$lib/utils/is-orcid-id/is-orcid-id'; + import isClaimed from '$lib/utils/orcids/is-claimed'; export let context: Writable; export let orcidId: string | undefined = undefined; @@ -95,7 +96,7 @@ const orcidAccount = response.orcidLinkedIdentityByOrcid; if (orcidAccount) { - if (orcidAccount.isClaimed && orcidAccount.areSplitsValid) { + if (isClaimed(orcidAccount)) { throw new InvalidUrlError('ORCID already claimed'); } @@ -119,7 +120,8 @@ orcid: orcidInfo.id, isClaimed: false, areSplitsValid: false, - chain: network.gqlName, // Add the required 'chain' property + chain: network.gqlName, + withdrawableBalances: [], }; } diff --git a/src/lib/utils/orcids/display-name.ts b/src/lib/utils/orcids/display-name.ts index cdf689e48..f2a96ddc5 100644 --- a/src/lib/utils/orcids/display-name.ts +++ b/src/lib/utils/orcids/display-name.ts @@ -1,12 +1,10 @@ -type orcidWithMetadata = { - orcid: string; - orcidMetadata?: { - givenName?: string | null; - familyName?: string | null; - } | null; -}; +import type { OrcidLinkedIdentity } from '$lib/graphql/__generated__/base-types'; -export default function getOrcidDisplayName(orcidAccount: orcidWithMetadata) { +export default function getOrcidDisplayName( + orcidAccount: Pick & { + orcidMetadata?: Omit, '__typename'> | null; + }, +) { const firstName = orcidAccount.orcidMetadata?.givenName ?? ''; const lastName = orcidAccount.orcidMetadata?.familyName ?? ''; diff --git a/src/lib/utils/orcids/is-claimed.ts b/src/lib/utils/orcids/is-claimed.ts index 89c75c96e..0c8eddf08 100644 --- a/src/lib/utils/orcids/is-claimed.ts +++ b/src/lib/utils/orcids/is-claimed.ts @@ -1,23 +1,7 @@ -export default function isClaimed< - IT extends - | { - __typename: 'ClaimedOrcidAccountData'; - } - | { - __typename: 'UnClaimedOrcidAccountData'; - }, ->(chainData: IT): chainData is IT & { __typename: 'ClaimedOrcidAccountData' } { - return chainData.__typename === 'ClaimedOrcidAccountData'; -} +import type { OrcidLinkedIdentity } from '$lib/graphql/__generated__/base-types'; -export function isUnclaimed( - chainData: - | { - __typename: 'ClaimedOrcidAccountData'; - } - | { - __typename: 'UnClaimedOrcidAccountData'; - }, -): chainData is { __typename: 'UnClaimedOrcidAccountData' } { - return chainData.__typename === 'UnClaimedOrcidAccountData'; +export default function isClaimed( + orcidAccount: Pick, +) { + return orcidAccount.isClaimed && orcidAccount.areSplitsValid; } From 0fc153bf32348cd51981488d6dbce078e1ae49a2 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 22 Sep 2025 18:46:47 -0700 Subject: [PATCH 066/155] First pass orcid owner update endpoint --- src/lib/utils/orcids/verify-orcid.ts | 27 +-- .../call/orcid-owner-update/+server.ts | 179 ++++++++++++++++++ 2 files changed, 195 insertions(+), 11 deletions(-) create mode 100644 src/routes/api/gasless/call/orcid-owner-update/+server.ts diff --git a/src/lib/utils/orcids/verify-orcid.ts b/src/lib/utils/orcids/verify-orcid.ts index b8e18c995..52cbcc0a3 100644 --- a/src/lib/utils/orcids/verify-orcid.ts +++ b/src/lib/utils/orcids/verify-orcid.ts @@ -1,11 +1,20 @@ -import { fetchOrcid } from "./fetch-orcid"; +import { fetchOrcid } from './fetch-orcid'; + +export function getClaimingUrlAddress(claimingUrl: string) { + let claimingUrlParsed; + try { + claimingUrlParsed = new URL(claimingUrl); + } catch { + return null; + } + + return claimingUrlParsed.searchParams.get('ethereum_owned_by') ?? ''; +} export default async function verifyOrcidClaim(orcidId: string, expectedAddress: string) { const orcidProfile = await fetchOrcid(orcidId, fetch); if (!orcidProfile) { - throw new Error( - 'Unable to find ORCID profile. Is it public?' - ); + throw new Error('Unable to find ORCID profile. Is it public?'); } if (!orcidProfile.claimingUrl) { @@ -14,21 +23,17 @@ export default async function verifyOrcidClaim(orcidId: string, expectedAddress: ); } - let claimingUrl; - try { - claimingUrl = new URL(orcidProfile.claimingUrl) - } catch(error) { - console.error('ORCID claiming URL invalid', error) + const urlAddress = getClaimingUrlAddress(orcidProfile.claimingUrl); + if (urlAddress === null) { // TODO: refine throw new Error( 'The link in your ORCID profile is invalid. Ensure that it matches http://0.0.0.0/?ethereum_owned_by={eth address}&orcid={ORCID iD}', ); } - const urlAddress = claimingUrl.searchParams.get('ethereum_owned_by') || '' if (urlAddress.toLowerCase() !== expectedAddress.toLowerCase()) { throw new Error( 'Expected Ethereum address not found in link. If you just edited the file, it may take a few moments for GitHub to process your changes, so please try again in a minute.', ); } -} \ No newline at end of file +} diff --git a/src/routes/api/gasless/call/orcid-owner-update/+server.ts b/src/routes/api/gasless/call/orcid-owner-update/+server.ts new file mode 100644 index 000000000..9f7f9862d --- /dev/null +++ b/src/routes/api/gasless/call/orcid-owner-update/+server.ts @@ -0,0 +1,179 @@ +import { z } from 'zod'; +import type { RequestHandler } from './$types'; +import { error } from '@sveltejs/kit'; +import { ethers, toUtf8Bytes } from 'ethers'; +import unreachable from '$lib/utils/unreachable'; +import { GelatoRelay, type SponsoredCallRequest } from '@gelatonetwork/relay-sdk'; +import assert from '$lib/utils/assert'; +import network from '$lib/stores/wallet/network'; +import { gql } from 'graphql-request'; +import query from '$lib/graphql/dripsQL'; +import type { + IsOrcidUnclaimedQuery, + IsOrcidUnclaimedQueryVariables, +} from './__generated__/gql.generated'; +import { redis } from '../../../redis'; +import getOptionalEnvVar from '$lib/utils/get-optional-env-var/private'; +import { JsonRpcProvider } from 'ethers'; +import isClaimed from '$lib/utils/orcids/is-claimed'; +import { fetchOrcid } from '$lib/utils/orcids/fetch-orcid'; +import { getClaimingUrlAddress } from '$lib/utils/orcids/verify-orcid'; +import { Forge } from '$lib/utils/sdk/sdk-types'; + +const GELATO_API_KEY = getOptionalEnvVar( + 'GELATO_API_KEY', + true, + "Gasless transactions won't work." + + "This means that claiming a project won't and collecting funds (on networks supporting gasless TXs and with gasless TXs enabled in settings) won't work.", +); + +const payloadSchema = z.object({ + orcid: z.string(), + chainId: z.number(), +}); + +const REPO_DRIVER_ABI = `[ + { + "inputs": [ + { "internalType": "enum Forge", "name": "forge", "type": "uint8" }, + { "internalType": "bytes", "name": "name", "type": "bytes" } + ], + "name": "requestUpdateOwner", + "outputs": [{ "internalType": "uint256", "name": "accountId", "type": "uint256" }], + "stateMutability": "nonpayable", + "type": "function" + }, +]`; + +const orcidUnclaimedQuery = gql` + query isOrcidUnclaimed($orcid: String!, $chain: SupportedChain!) { + orcidLinkedIdentityByOrcid(orcid: $orcid, chain: $chain) { + chain + isClaimed + areSplitsValid + owner { + address + } + } + } +`; + +export const POST: RequestHandler = async ({ request, fetch }) => { + assert( + GELATO_API_KEY, + 'GELATO_API_KEY is required. Gasless transactions will not work without it.', + ); + + assert( + redis, + 'This endpoint requires a connected Redis instance. Ensure CACHE_REDIS_CONNECTION_STRING is set in env', + ); + + let payload: z.infer; + + try { + const body = await request.text(); + payload = payloadSchema.parse(JSON.parse(body)); + } catch { + error(400, 'Invalid payload'); + } + + // eslint-disable-next-line no-console + console.log('REPO_OWNER_UPDATE', payload); + + const { orcid, chainId } = payload; + + assert(network.chainId === chainId, 'Unsupported chain id'); + + const isOrcidUnclaimedQueryResponse = await query< + IsOrcidUnclaimedQuery, + IsOrcidUnclaimedQueryVariables + >( + orcidUnclaimedQuery, + { + orcid: orcid, + chain: network.gqlName, + }, + fetch, + ); + + const orcidAccount = isOrcidUnclaimedQueryResponse.orcidLinkedIdentityByOrcid; + if (!orcidAccount) { + return error(400, 'ORCID not found'); + } + + if (isClaimed(orcidAccount)) { + return error(400, 'Orcid already claimed'); + } + + const orcidProfile = await fetchOrcid(orcid, fetch); + // TODO: yowza, do we need to think about this more? If we can fetch the account, but not + // the profile, what's going on? + if (!orcidProfile) { + return error(400, 'Orcid unfetchable'); + } + + const urlAddress = getClaimingUrlAddress(orcidProfile.claimingUrl); + if ( + orcidAccount.owner?.address && + orcidAccount.owner.address.toLowerCase() === urlAddress?.toLowerCase() + ) { + return new Response('{ "taskId": null }'); + } + + const blockKey = `${network.name}-ownerUpdateRequest-${orcid}`; + const blockRecordTaskId = await redis.get(blockKey); + + if (blockRecordTaskId) { + const taskStatusRes = await fetch(`/api/gasless/track/${blockRecordTaskId}`); + if (!taskStatusRes.ok) + throw new Error(`Failed to fetch task status: ${await taskStatusRes.text()}`); + + const { task } = await taskStatusRes.json(); + assert(typeof task === 'object', 'Invalid task'); + const { taskState } = task; + assert(typeof taskState === 'string', 'Invalid task state'); + + if (['CheckPending', 'ExecPending', 'WaitingForConfirmation'].includes(taskState)) { + // A request is already in-flight + return new Response(JSON.stringify({ taskId: blockRecordTaskId })); + } else { + await redis.del(blockKey); + } + } + + const provider = new JsonRpcProvider(network.rpcUrl); + const contract = new ethers.Contract(network.contracts.REPO_DRIVER, REPO_DRIVER_ABI, provider); + + const tx = await contract.requestUpdateOwner.populateTransaction( + Forge.orcidId, + ethers.hexlify(toUtf8Bytes(orcid)), + ); + + const relayRequest: SponsoredCallRequest = { + chainId: BigInt(chainId), + target: tx.to ?? unreachable(), + data: tx.data ?? unreachable(), + }; + + const relay = new GelatoRelay(); + + try { + const relayResponse = await relay.sponsoredCall(relayRequest, GELATO_API_KEY); + const { taskId } = relayResponse; + + // eslint-disable-next-line no-console + console.log('RELAY_RESPONSE', payload, relayResponse); + + redis.set(blockKey, taskId, { + // 4 hours + EX: 4 * 60 * 60, + }); + + return new Response(JSON.stringify(relayResponse)); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + return error(500, e instanceof Error ? e : 'Unknown error'); + } +}; From e9fc2a8b99cc497de03fa1d267870a6718f45933 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Tue, 23 Sep 2025 14:39:22 -0700 Subject: [PATCH 067/155] Reverse ORCID profile dependency so that we're failing when the graphql api has nothing to give us --- .../(app)/orcids/[orcidId]/+page.server.ts | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts index 8c71dc5f2..efd2a3934 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts @@ -1,42 +1,44 @@ import isValidOrcidId from '$lib/utils/is-orcid-id/is-orcid-id'; import { error } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; -import { - fetchOrcid, - fetchOrcidAccount, - orcidIdToAccountId, -} from '../../../../../../lib/utils/orcids/fetch-orcid'; -import type { OrcidProfileFragment } from './components/__generated__/gql.generated'; +import { fetchOrcid, fetchOrcidAccount } from '../../../../../../lib/utils/orcids/fetch-orcid'; +import Orcid from '$lib/utils/orcids/entities'; export const load = (async ({ params, fetch }) => { if (!isValidOrcidId(params.orcidId)) { return error(404); } - const orcid = await fetchOrcid(params.orcidId, fetch); - if (!orcid) { - return error(404); - } - - let orcidAccount = undefined; const orcidGqlResponse = await fetchOrcidAccount(params.orcidId, fetch); - if (orcidGqlResponse.orcidLinkedIdentityByOrcid) { - orcidAccount = orcidGqlResponse.orcidLinkedIdentityByOrcid; + const orcidAccount = orcidGqlResponse.orcidLinkedIdentityByOrcid; + // If the backend can't get the ORCID info, then something is deeply wrong + // and we cannot proceed + if (!orcidAccount) { + return error(404); } - if (!orcidAccount) { - const accountId = await orcidIdToAccountId(params.orcidId); - orcidAccount = { - __typename: 'OrcidLinkedIdentity', - account: { - __typename: 'RepoDriverAccount', - accountId: String(accountId), - driver: 'REPO' as const, + // If the frontend can't the ORCID info, then there might be a rate + // limiting issue or an API authroization issue. Fake it till you make it. + let orcid = await fetchOrcid(params.orcidId, fetch); + if (!orcid) { + orcid = new Orcid({ + 'orcid-identifier': { + uri: `https://orcid.org/${params.orcidId}`, + path: params.orcidId, + host: 'orcid.org', + }, + person: { + 'last-modified-date': null, + name: { + 'given-names': null, + 'family-name': null, + 'credit-name': null, + }, + biography: null, + 'researcher-urls': null, + 'other-names': null, }, - orcid: orcid.id, - isClaimed: false, - areSplitsValid: false, - } as OrcidProfileFragment; + }); } return { From 5ced58a5a6ffe62635b22496f7f84d4ec133aca4 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Wed, 24 Sep 2025 13:40:16 -0700 Subject: [PATCH 068/155] Rename is-orcid utils --- package-lock.json | 8 ++++---- src/lib/components/list-editor/classifiers.ts | 4 ++-- .../list-editor/components/list-editor-input.svelte | 2 +- .../steps/enter-orcid-id/enter-orcid-id.svelte | 2 +- .../is-orcid-id.ts => orcids/is-valid-orcid-id.ts} | 0 .../is-valid-orcid-id.unit.test.ts} | 4 ++-- .../(pages)/app/(app)/orcids/[orcidId]/+page.server.ts | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) rename src/lib/utils/{is-orcid-id/is-orcid-id.ts => orcids/is-valid-orcid-id.ts} (100%) rename src/lib/utils/{is-orcid-id/is-orcid-id.unit.test.ts => orcids/is-valid-orcid-id.unit.test.ts} (88%) diff --git a/package-lock.json b/package-lock.json index e60fb41bf..24d9327b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1051,7 +1051,7 @@ }, "node_modules/@drips-network/sdk": { "version": "0.1.0-alpha.11", - "resolved": "git+ssh://git@github.com/drips-network/sdk.git#0a0b30af57a3f1aea32dee15f0b65879a565be75", + "resolved": "git+ssh://git@github.com/drips-network/sdk.git#153494eec334bb8989a46b2588fa02a94050d3ae", "license": "GPL-3.0-or-later", "dependencies": { "@efstajas/versioned-parser": "^0.1.4", @@ -1071,9 +1071,9 @@ } }, "node_modules/@drips-network/sdk/node_modules/pinata": { - "version": "2.4.9", - "resolved": "https://registry.npmjs.org/pinata/-/pinata-2.4.9.tgz", - "integrity": "sha512-94fMvBJXnFgDyZkYRd7SCaXsHZERtDAjIcPIpYlUXAorv77xav3UMU0fipX/EKZwl3Uv8P7Mbul12KdbqpLRdA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/pinata/-/pinata-2.5.0.tgz", + "integrity": "sha512-zjjrEUbEqeAE3gqIOy1K8f4xq3CDDefCgRj6tMqn14gbclSYo014+HQcfcKAAstMUjYNDb1yYQjxmBImW2kxCw==", "license": "MIT", "engines": { "node": ">=20" diff --git a/src/lib/components/list-editor/classifiers.ts b/src/lib/components/list-editor/classifiers.ts index 949c14de0..b54d6675c 100644 --- a/src/lib/components/list-editor/classifiers.ts +++ b/src/lib/components/list-editor/classifiers.ts @@ -11,7 +11,7 @@ import { getAddress, getDripList, getProject, getOrcid } from './hydrators'; import { buildRepositoryURL, isDripsProjectUrl } from '../../utils/build-repo-url'; import type { RecipientClassification } from './types'; import { isAddress } from 'ethers'; -import isValidOrcidId from '$lib/utils/is-orcid-id/is-orcid-id'; +import isValidOrcidId from '$lib/utils/orcids/is-valid-orcid-id'; export const classifyRecipient = ( input: string, @@ -105,7 +105,7 @@ export const classifyRecipient = ( fetch() { return getOrcid(this.value); }, - } + }; } return null; diff --git a/src/lib/components/list-editor/components/list-editor-input.svelte b/src/lib/components/list-editor/components/list-editor-input.svelte index 1779e6df0..a39a4d375 100644 --- a/src/lib/components/list-editor/components/list-editor-input.svelte +++ b/src/lib/components/list-editor/components/list-editor-input.svelte @@ -18,7 +18,7 @@ import { AddItemError } from '../errors'; import { classifyRecipient } from '$lib/components/list-editor/classifiers'; import { isAddress } from 'ethers'; - import isValidOrcidId from '$lib/utils/is-orcid-id/is-orcid-id'; + import isValidOrcidId from '$lib/utils/orcids/is-valid-orcid-id'; const dispatch = createEventDispatcher<{ addAddress: { accountId: string; address: string }; diff --git a/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.svelte b/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.svelte index 868fa582b..f3c21d3ba 100644 --- a/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.svelte +++ b/src/lib/flows/claim-orcid-flow/steps/enter-orcid-id/enter-orcid-id.svelte @@ -43,7 +43,7 @@ UNCLAIMED_ORCID_CARD_FRAGMENT, } from '../../../../../routes/(pages)/app/(app)/orcids/[orcidId]/components/unclaimed-orcid-card.svelte'; import { fetchOrcid } from '../../../../utils/orcids/fetch-orcid'; - import isValidOrcidId from '$lib/utils/is-orcid-id/is-orcid-id'; + import isValidOrcidId from '$lib/utils/orcids/is-valid-orcid-id'; import isClaimed from '$lib/utils/orcids/is-claimed'; export let context: Writable; diff --git a/src/lib/utils/is-orcid-id/is-orcid-id.ts b/src/lib/utils/orcids/is-valid-orcid-id.ts similarity index 100% rename from src/lib/utils/is-orcid-id/is-orcid-id.ts rename to src/lib/utils/orcids/is-valid-orcid-id.ts diff --git a/src/lib/utils/is-orcid-id/is-orcid-id.unit.test.ts b/src/lib/utils/orcids/is-valid-orcid-id.unit.test.ts similarity index 88% rename from src/lib/utils/is-orcid-id/is-orcid-id.unit.test.ts rename to src/lib/utils/orcids/is-valid-orcid-id.unit.test.ts index d74f995d3..392000865 100644 --- a/src/lib/utils/is-orcid-id/is-orcid-id.unit.test.ts +++ b/src/lib/utils/orcids/is-valid-orcid-id.unit.test.ts @@ -1,6 +1,6 @@ -import isValidOrcidId from './is-orcid-id'; +import isValidOrcidId from './is-valid-orcid-id'; -describe('is-orcid.ts', () => { +describe('is-valid-orcid.ts', () => { it('returns false for non-string input', () => { expect(isValidOrcidId(5 as unknown as string)).toBe(false); }); diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts index efd2a3934..0154c3cb5 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts @@ -1,8 +1,8 @@ -import isValidOrcidId from '$lib/utils/is-orcid-id/is-orcid-id'; import { error } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; import { fetchOrcid, fetchOrcidAccount } from '../../../../../../lib/utils/orcids/fetch-orcid'; import Orcid from '$lib/utils/orcids/entities'; +import isValidOrcidId from '$lib/utils/orcids/is-valid-orcid-id'; export const load = (async ({ params, fetch }) => { if (!isValidOrcidId(params.orcidId)) { From c3839bdcdc767c6ca000ab1078c81a8fcc224828 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Wed, 24 Sep 2025 13:40:50 -0700 Subject: [PATCH 069/155] Remove unused DripListService --- src/lib/utils/driplist/DripListService.ts | 353 ---------------------- 1 file changed, 353 deletions(-) delete mode 100644 src/lib/utils/driplist/DripListService.ts diff --git a/src/lib/utils/driplist/DripListService.ts b/src/lib/utils/driplist/DripListService.ts deleted file mode 100644 index bebbbf46b..000000000 --- a/src/lib/utils/driplist/DripListService.ts +++ /dev/null @@ -1,353 +0,0 @@ -import NftDriverMetadataManager from '../metadata/NftDriverMetadataManager'; -import MetadataManagerBase from '../metadata/MetadataManagerBase'; -import { MaxUint256, type Signer, toBigInt } from 'ethers'; -import GitProjectService from '../project/GitProjectService'; -import assert from '$lib/utils/assert'; -import type { Address, IpfsHash } from '../common-types'; -import wallet from '$lib/stores/wallet/wallet.store'; -import { get } from 'svelte/store'; -import Emoji from '$lib/components/emoji/emoji.svelte'; -import type { nftDriverAccountMetadataParser } from '../metadata/schemas'; -import type { LatestVersion } from '@efstajas/versioned-parser'; -import type { Items, Weights } from '$lib/components/list-editor/types'; -import { buildStreamCreateBatchTx } from '../streams/streams'; -import { - executeNftDriverReadMethod, - executeNftDriverWriteMethod, - populateNftDriverWriteTx, -} from '../sdk/nft-driver/nft-driver'; -import type { OxString, SplitsReceiver } from '../sdk/sdk-types'; -import { - getAddressDriverAllowance, - populateAddressDriverWriteTx, -} from '../sdk/address-driver/address-driver'; -import type { ContractTransaction } from 'ethers'; -import { populateErc20WriteTx } from '../sdk/erc20/erc20'; -import { formatSplitReceivers } from '../sdk/utils/format-split-receivers'; -import keyValueToMetatada from '../sdk/utils/key-value-to-metadata'; -import { populateCallerWriteTx } from '../sdk/caller/caller'; -import txToCallerCall from '../sdk/utils/tx-to-caller-call'; -import network from '$lib/stores/wallet/network'; -import calculateRandomSalt from '../calc-salt'; - -type AccountId = string; - -const WAITING_WALLET_ICON = { - component: Emoji, - props: { - emoji: '👛', - size: 'huge', - }, -}; - -/** - * A class for managing `DripList`s. - * - * **Important**: This class assumes that *all* clients and factories are connected to the *same* signer. - */ -export default class DripListService { - private readonly SEED_CONSTANT = 'Drips App'; - - private _owner!: Signer | undefined; - private _ownerAddress!: Address | undefined; - private _nftDriverMetadataManager!: NftDriverMetadataManager; - - private constructor() {} - - /** - * Creates a new `DripListService` instance. - * @returns A new `DripListService` instance. - */ - public static async new(): Promise { - const dripListService = new DripListService(); - - const { connected, signer } = get(wallet); - - if (connected) { - assert(signer, 'Signer address is undefined.'); - dripListService._owner = signer; - dripListService._ownerAddress = await signer.getAddress(); - - dripListService._nftDriverMetadataManager = new NftDriverMetadataManager( - executeNftDriverWriteMethod, - ); - } else { - dripListService._nftDriverMetadataManager = new NftDriverMetadataManager(); - } - - return dripListService; - } - - public async buildTransactContext(config: { - listTitle: string; - listDescription?: string; - weights: Weights; - items: Items; - support?: - | { - type: 'continuous'; - tokenAddress: string; - amountPerSec: bigint; - topUpAmount: bigint; - } - | { - type: 'one-time'; - tokenAddress: string; - donationAmount: bigint; - }; - latestVotingRoundId?: string; - isVisible: boolean; - }) { - assert(this._ownerAddress, `This function requires an active wallet connection.`); - - const { listTitle, listDescription, weights, items, support, latestVotingRoundId, isVisible } = - config; - - const { recipientsMetadata, receivers } = await this.getProjectsSplitMetadataAndReceivers( - weights, - items, - ); - - const salt = calculateRandomSalt(); - - const listId = ( - await executeNftDriverReadMethod({ - functionName: 'calcTokenIdWithSalt', - args: [this._ownerAddress as OxString, salt], - }) - ).toString(); - - const ipfsHash = await this._publishMetadataToIpfs( - listId, - recipientsMetadata, - isVisible, - listTitle, - listDescription, - latestVotingRoundId, - ); - - const createDripListTx = await this._buildCreateDripListTx(salt, ipfsHash); - - const setDripListSplitsTx = await populateNftDriverWriteTx({ - functionName: 'setSplits', - args: [toBigInt(listId), formatSplitReceivers(receivers)], - }); - - let needsApprovalForToken: string | undefined; - let txs: ContractTransaction[]; - - if (support?.type === 'continuous') { - const { tokenAddress, amountPerSec, topUpAmount } = support; - - const allowance = await getAddressDriverAllowance(tokenAddress as OxString); - const needsApproval = allowance < topUpAmount; - - if (needsApproval) { - needsApprovalForToken = tokenAddress; - } - - const setStreamTx = await this._buildSetDripListStreamTxs( - tokenAddress, - listId, - topUpAmount, - amountPerSec, - ); - - txs = [createDripListTx, setDripListSplitsTx, ...setStreamTx.batch]; - } else if (support?.type === 'one-time') { - const { tokenAddress, donationAmount } = support; - - const allowance = await getAddressDriverAllowance(tokenAddress as OxString); - const needsApproval = allowance < donationAmount; - - if (needsApproval) { - needsApprovalForToken = tokenAddress; - } - - const giveTx = await populateAddressDriverWriteTx({ - functionName: 'give', - args: [toBigInt(listId), tokenAddress as OxString, donationAmount], - }); - - txs = [createDripListTx, setDripListSplitsTx, giveTx]; - } else { - // No support - txs = [createDripListTx, setDripListSplitsTx]; - } - - const batch = await populateCallerWriteTx({ - functionName: 'callBatched', - args: [txs.map(txToCallerCall)], - }); - - return { - txs: [ - ...(needsApprovalForToken - ? [ - { - title: `Approve Drips to withdraw ${needsApprovalForToken}`, - transaction: await this._buildTokenApprovalTx(needsApprovalForToken), - waitingSignatureMessage: { - message: `Waiting for you to approve Drips access to the ERC-20 token in your wallet...`, - subtitle: 'You only have to do this once per token.', - icon: WAITING_WALLET_ICON, - }, - applyGasBuffer: false, - }, - ] - : []), - { - title: 'Creating the Drip List', - transaction: batch, - applyGasBuffer: support?.type === 'continuous', - }, - ], - dripListId: listId, - }; - } - - public async getProjectsSplitMetadataAndReceivers(weights: Weights, items: Items) { - const projectsInput = Object.entries(weights); - - const receivers: SplitsReceiver[] = []; - - const recipientsMetadata: Extract< - LatestVersion, - { type: 'dripList' } - >['recipients'] = []; - - for (const [accountId, weight] of projectsInput) { - const item = items[accountId]; - if (weight <= 0) continue; - - switch (item.type) { - case 'address': { - const receiver = { - type: 'address' as const, - weight, - accountId, - }; - - recipientsMetadata.push(receiver); - receivers.push(receiver); - - break; - } - case 'project': { - const { forge, ownerName, repoName } = item.project.source; - - const receiver = { - type: 'repoDriver' as const, - weight, - accountId, - }; - - recipientsMetadata.push({ - ...receiver, - source: GitProjectService.populateSource(forge, repoName, ownerName), - }); - - receivers.push(receiver); - - break; - } - case 'drip-list': { - const receiver = { - type: 'dripList' as const, - weight, - accountId, - }; - - recipientsMetadata.push(receiver); - receivers.push(receiver); - - break; - } - } - } - - return { - recipientsMetadata, - receivers: receivers, - }; - } - - private async _buildCreateDripListTx(salt: bigint, ipfsHash: IpfsHash) { - assert(this._ownerAddress, `This function requires an active wallet connection.`); - - const createDripListTx = await populateNftDriverWriteTx({ - functionName: 'safeMintWithSalt', - args: [ - salt, - this._ownerAddress as OxString, - [ - { - key: MetadataManagerBase.USER_METADATA_KEY, - value: ipfsHash, - }, - ].map(keyValueToMetatada), - ], - }); - - return createDripListTx; - } - - private async _buildSetDripListStreamTxs( - token: Address, - dripListId: AccountId, - topUpAmount: bigint, - amountPerSec: bigint, - ) { - assert(this._owner, `This function requires an active wallet connection.`); - - return await buildStreamCreateBatchTx( - this._owner, - { - tokenAddress: token, - amountPerSecond: amountPerSec, - recipientAccountId: dripListId, - name: undefined, - }, - topUpAmount, - ); - } - - private async _buildTokenApprovalTx(token: Address): Promise { - assert(this._owner, `This function requires an active wallet connection.`); - - const tokenApprovalTx = await populateErc20WriteTx({ - token: token as OxString, - functionName: 'approve', - args: [network.contracts.ADDRESS_DRIVER as OxString, MaxUint256], - }); - - return tokenApprovalTx; - } - - private async _publishMetadataToIpfs( - dripListId: string, - recipients: Extract< - LatestVersion, - { type: 'dripList' } - >['recipients'], - isVisible: boolean, - name?: string, - description?: string, - latestVotingRoundId?: string, - ): Promise { - assert(this._ownerAddress, `This function requires an active wallet connection.`); - - const dripListMetadata = this._nftDriverMetadataManager.buildAccountMetadata({ - forAccountId: dripListId, - recipients, - name, - description, - latestVotingRoundId, - isVisible, - }); - - const ipfsHash = await this._nftDriverMetadataManager.pinAccountMetadata(dripListMetadata); - - return ipfsHash; - } -} From a8a2749abecff7da0b2a52d15411c0acd9bc8e85 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Wed, 24 Sep 2025 14:35:30 -0700 Subject: [PATCH 070/155] Begin to integrate SDK into ORCID claiming process; revamp ORCID recipient classification --- src/lib/components/list-editor/classifiers.ts | 7 +- src/lib/components/list-editor/validators.ts | 36 ++- .../add-ethereum-address.svelte | 1 - .../set-splits-and-emit-metadata.svelte | 289 +++++++++--------- src/lib/flows/import-from-csv/upload.svelte | 2 +- .../utils/orcids/build-orcid-claiming-txs.ts | 26 ++ src/lib/utils/orcids/build-orcid-url.ts | 18 +- src/lib/utils/orcids/fetch-orcid.ts | 7 +- 8 files changed, 230 insertions(+), 156 deletions(-) create mode 100644 src/lib/utils/orcids/build-orcid-claiming-txs.ts diff --git a/src/lib/components/list-editor/classifiers.ts b/src/lib/components/list-editor/classifiers.ts index b54d6675c..b535270e3 100644 --- a/src/lib/components/list-editor/classifiers.ts +++ b/src/lib/components/list-editor/classifiers.ts @@ -5,6 +5,7 @@ import { reformatUrl, validateAddress, validateDripList, + validateOrcid, validateProject, } from './validators'; import { getAddress, getDripList, getProject, getOrcid } from './hydrators'; @@ -97,10 +98,8 @@ export const classifyRecipient = ( return { type: 'orcid', value: input, - async validate() { - // No complex validation for ORCID IDs, just check format - // BUT we could check if the ORCID is fetchable / not private? - return true; + validate() { + return validateOrcid(this.value); }, fetch() { return getOrcid(this.value); diff --git a/src/lib/components/list-editor/validators.ts b/src/lib/components/list-editor/validators.ts index e1a04a937..3b81a1699 100644 --- a/src/lib/components/list-editor/validators.ts +++ b/src/lib/components/list-editor/validators.ts @@ -1,5 +1,13 @@ import { isAddress } from 'ethers'; import ensStore from '../../stores/ens/ens.store'; +import type { RecipientClassification } from './types'; +import { gql } from 'graphql-request'; +import query from '$lib/graphql/dripsQL'; +import network from '$lib/stores/wallet/network'; +import type { + ValidateOrcidQuery, + ValidateOrcidQueryVariables, +} from './__generated__/gql.generated'; export const reformatUrl = (url: string): string => { if (!url.startsWith('http://') && !url.startsWith('https://')) { @@ -51,7 +59,29 @@ export const validateAddress = async ( } }; -export const createInvalidMessage = (type: string): string => { +const validateOrcidQuery = gql` + query ValidateOrcid($orcid: String!, $chain: SupportedChain!) { + orcidLinkedIdentityByOrcid(orcid: $orcid, chain: $chain) { + orcid + } + } +`; + +export const validateOrcid = async (orcidId: string) => { + const orcidQueryResponse = await query( + validateOrcidQuery, + { + orcid: orcidId, + chain: network.gqlName, + }, + fetch, + ); + return !!orcidQueryResponse.orcidLinkedIdentityByOrcid; +}; + +export const createInvalidMessage = ( + type: NonNullable['type'], +): string => { switch (type) { case 'address': return "This isn't a valid wallet address"; @@ -59,7 +89,7 @@ export const createInvalidMessage = (type: string): string => { return "This isn't a GitHub repo or isn't public"; case 'drip-list': return "This isn't a recognized Drip List"; - default: - return "This isn't valid"; + case 'orcid': + return 'This ORCID iD is invalid or inaccessible.'; } }; diff --git a/src/lib/flows/claim-orcid-flow/steps/add-ethereum-address/add-ethereum-address.svelte b/src/lib/flows/claim-orcid-flow/steps/add-ethereum-address/add-ethereum-address.svelte index b1874f18c..04c22ba8b 100644 --- a/src/lib/flows/claim-orcid-flow/steps/add-ethereum-address/add-ethereum-address.svelte +++ b/src/lib/flows/claim-orcid-flow/steps/add-ethereum-address/add-ethereum-address.svelte @@ -87,7 +87,6 @@ } try { - // TODO: this endpoint doesn't exist! // Kick off repo owner update using gasless TX const gaslessCall = await fetch('/api/gasless/call/orcid-owner-update', { method: 'POST', diff --git a/src/lib/flows/claim-orcid-flow/steps/set-splits-and-emit-metadata/set-splits-and-emit-metadata.svelte b/src/lib/flows/claim-orcid-flow/steps/set-splits-and-emit-metadata/set-splits-and-emit-metadata.svelte index b66650cad..37511174c 100644 --- a/src/lib/flows/claim-orcid-flow/steps/set-splits-and-emit-metadata/set-splits-and-emit-metadata.svelte +++ b/src/lib/flows/claim-orcid-flow/steps/set-splits-and-emit-metadata/set-splits-and-emit-metadata.svelte @@ -3,7 +3,7 @@ import { makeTransactPayload, type StepComponentEvents, - type TransactionWrapperOrExternalTransaction, + // type TransactionWrapperOrExternalTransaction, } from '$lib/components/stepper/types'; import type { Writable } from 'svelte/store'; import type { State } from '../../claim-orcid-flow'; @@ -18,16 +18,17 @@ } from './__generated__/gql.generated'; import expect from '$lib/utils/expect'; import invalidateAccountCache from '$lib/utils/cache/remote/invalidate-account-cache'; - import { populateCallerWriteTx } from '$lib/utils/sdk/caller/caller'; - import txToCallerCall from '$lib/utils/sdk/utils/tx-to-caller-call'; + // import { populateCallerWriteTx } from '$lib/utils/sdk/caller/caller'; + // import txToCallerCall from '$lib/utils/sdk/utils/tx-to-caller-call'; import network from '$lib/stores/wallet/network'; import { invalidateAll } from '$lib/stores/fetched-data-cache/invalidate'; - import assert from '$lib/utils/assert'; + // import assert from '$lib/utils/assert'; import walletStore from '$lib/stores/wallet/wallet.store'; - import gaslessStore from '$lib/stores/gasless/gasless.store'; - import { populateRepoDriverWriteTx } from '$lib/utils/sdk/repo-driver/repo-driver'; - import { hexlify, toUtf8Bytes } from 'ethers'; - import OrcidTransactionService from '$lib/utils/orcids/OrcidTransactionService'; + // import gaslessStore from '$lib/stores/gasless/gasless.store'; + // import { populateRepoDriverWriteTx } from '$lib/utils/sdk/repo-driver/repo-driver'; + // import { hexlify, toUtf8Bytes } from 'ethers'; + // import OrcidTransactionService from '$lib/utils/orcids/OrcidTransactionService'; + import { buildOrcidClaimingTxs } from '$lib/utils/orcids/build-orcid-claiming-txs'; const dispatch = createEventDispatcher(); @@ -66,120 +67,120 @@ ); } - async function waitForOrcidOwnerUpdate(gasless: boolean) { - if (gasless) { - // First, wait for Gelato Relay to resolve the update task. - const gaslessOwnerUpdateExpectation = await expect( - async () => { - const res = await fetch(`/api/gasless/track/${$context.gaslessOwnerUpdateTaskId}`); - if (!res.ok) throw new Error('Failed to track gasless owner update task'); + // async function waitForOrcidOwnerUpdate(gasless: boolean) { + // if (gasless) { + // // First, wait for Gelato Relay to resolve the update task. + // const gaslessOwnerUpdateExpectation = await expect( + // async () => { + // const res = await fetch(`/api/gasless/track/${$context.gaslessOwnerUpdateTaskId}`); + // if (!res.ok) throw new Error('Failed to track gasless owner update task'); - const { task } = await res.json(); - assert(typeof task === 'object', 'Invalid task'); - const { taskState } = task; - assert(typeof taskState === 'string', 'Invalid task state'); + // const { task } = await res.json(); + // assert(typeof task === 'object', 'Invalid task'); + // const { taskState } = task; + // assert(typeof taskState === 'string', 'Invalid task state'); - return taskState; - }, - (taskState) => { - switch (taskState) { - case 'ExecSuccess': - return true; - case 'Cancelled': - throw new Error( - 'Failed to gaslessly update the repository owner on-chain. There may be a temporary issue with our Transaction Relay provider. Please try again later.', - ); - default: - return false; - } - }, - 600000, - 2000, - ); - - if (gaslessOwnerUpdateExpectation.failed) { - throw new Error( - "The gasless owner update transaction didn't resolve in the expected timeframe. There may be an issue with our Transaction Relay provider. Please try again later, or disable gasless transactions in the Drips application settings.", - ); - } - } + // return taskState; + // }, + // (taskState) => { + // switch (taskState) { + // case 'ExecSuccess': + // return true; + // case 'Cancelled': + // throw new Error( + // 'Failed to gaslessly update the repository owner on-chain. There may be a temporary issue with our Transaction Relay provider. Please try again later.', + // ); + // default: + // return false; + // } + // }, + // 600000, + // 2000, + // ); - // Next, wait for the new owner to be indexed by our infra. - const ownerIndexedExpectation = await expect( - () => checkOrcidInExpectedStateForClaiming(), - (response) => response, - 600000, - 2000, - ); + // if (gaslessOwnerUpdateExpectation.failed) { + // throw new Error( + // "The gasless owner update transaction didn't resolve in the expected timeframe. There may be an issue with our Transaction Relay provider. Please try again later, or disable gasless transactions in the Drips application settings.", + // ); + // } + // } - if (ownerIndexedExpectation.failed) { - throw new Error( - 'The new owner was not indexed in the expected timeframe. There may be a temporary issue with our oracle provider. Please try again later.', - ); - } - } + // // Next, wait for the new owner to be indexed by our infra. + // const ownerIndexedExpectation = await expect( + // () => checkOrcidInExpectedStateForClaiming(), + // (response) => response, + // 600000, + // 2000, + // ); - async function generateOwnerUpdateTransactions( - gasslessOwnerUpdateTaskId: string | undefined, - orcid: string, - ) { - let transactions: TransactionWrapperOrExternalTransaction[] = []; - let fakeProgressBarConfig: { expectedDurationMs: number; expectedDurationText: string }; - - switch (network.chainId) { - case 1: { - fakeProgressBarConfig = { - expectedDurationMs: 100000, - expectedDurationText: 'Usually less than a minute', - }; - break; - } - case 314: { - fakeProgressBarConfig = { - expectedDurationMs: 500000, - expectedDurationText: 'Usually less than 5 minutes', - }; - break; - } - default: { - fakeProgressBarConfig = { - expectedDurationMs: 100000, - expectedDurationText: 'Usually less than a minute', - }; - } - } + // if (ownerIndexedExpectation.failed) { + // throw new Error( + // 'The new owner was not indexed in the expected timeframe. There may be a temporary issue with our oracle provider. Please try again later.', + // ); + // } + // } - if (gasslessOwnerUpdateTaskId) { - transactions.push({ - external: true, - title: 'Finalizing verification...', - ...fakeProgressBarConfig, - promise: () => waitForOrcidOwnerUpdate(true), - }); - } else { - const ownerUpdateTx = await populateRepoDriverWriteTx({ - functionName: 'requestUpdateOwner', - args: [0, hexlify(toUtf8Bytes(orcid)) as `0x${string}`], - }); - - transactions.push( - { - title: 'Request update of ORCID owner', - transaction: ownerUpdateTx, - gasless: false, - applyGasBuffer: false, - }, - { - external: true, - title: 'Finalizing verification...', - ...fakeProgressBarConfig, - promise: () => waitForOrcidOwnerUpdate(false), - }, - ); - } + // async function generateOwnerUpdateTransactions( + // gasslessOwnerUpdateTaskId: string | undefined, + // orcid: string, + // ) { + // let transactions: TransactionWrapperOrExternalTransaction[] = []; + // let fakeProgressBarConfig: { expectedDurationMs: number; expectedDurationText: string }; - return transactions; - } + // switch (network.chainId) { + // case 1: { + // fakeProgressBarConfig = { + // expectedDurationMs: 100000, + // expectedDurationText: 'Usually less than a minute', + // }; + // break; + // } + // case 314: { + // fakeProgressBarConfig = { + // expectedDurationMs: 500000, + // expectedDurationText: 'Usually less than 5 minutes', + // }; + // break; + // } + // default: { + // fakeProgressBarConfig = { + // expectedDurationMs: 100000, + // expectedDurationText: 'Usually less than a minute', + // }; + // } + // } + + // if (gasslessOwnerUpdateTaskId) { + // transactions.push({ + // external: true, + // title: 'Finalizing verification...', + // ...fakeProgressBarConfig, + // promise: () => waitForOrcidOwnerUpdate(true), + // }); + // } else { + // const ownerUpdateTx = await populateRepoDriverWriteTx({ + // functionName: 'requestUpdateOwner', + // args: [0, hexlify(toUtf8Bytes(orcid)) as `0x${string}`], + // }); + + // transactions.push( + // { + // title: 'Request update of ORCID owner', + // transaction: ownerUpdateTx, + // gasless: false, + // applyGasBuffer: false, + // }, + // { + // external: true, + // title: 'Finalizing verification...', + // ...fakeProgressBarConfig, + // promise: () => waitForOrcidOwnerUpdate(false), + // }, + // ); + // } + + // return transactions; + // } onMount(() => dispatch( @@ -188,14 +189,16 @@ headline: 'Claim your ORCID', before: async () => { - const orcidProjectService = await OrcidTransactionService.new(); + // const orcidProjectService = await OrcidTransactionService.new(); + + // const setSplitsAndEmitMetadataBatch = await orcidProjectService.buildBatchTx($context); - const setSplitsAndEmitMetadataBatch = await orcidProjectService.buildBatchTx($context); + // const tx = await populateCallerWriteTx({ + // functionName: 'callBatched', + // args: [setSplitsAndEmitMetadataBatch.map(txToCallerCall)], + // }); - const tx = await populateCallerWriteTx({ - functionName: 'callBatched', - args: [setSplitsAndEmitMetadataBatch.map(txToCallerCall)], - }); + const tx = await buildOrcidClaimingTxs($context); // Check once if the project is already in the expected state for the final claim TX, // and skip the step that waits for everything to be in the right state if so. @@ -210,22 +213,32 @@ duringBefore: 'Preparing to claim ORCID...', }, - transactions: async ({ tx, orcidAlreadyReadyForClaimTx }) => { - const ownerUpdateTransactionSteps = orcidAlreadyReadyForClaimTx - ? [] - : await generateOwnerUpdateTransactions( - $context.gaslessOwnerUpdateTaskId, - $context.claimableId, - ); - - const setSplitsAndMetadataTransactionStep = { - transaction: tx, - gasless: $gaslessStore, - applyGasBuffer: false, - title: 'Set ORCID splits and metadata', - }; - - return [...ownerUpdateTransactionSteps, setSplitsAndMetadataTransactionStep]; + transactions: async ({ tx }) => { + // if(gasless) { + // return generateOwnerUpdateTransactions( + // $context.gaslessOwnerUpdateTaskId, + // $context.claimableId, + // ); + // } + + // otherwise, do the claiming via the SDK? + + // const ownerUpdateTransactionSteps = orcidAlreadyReadyForClaimTx + // ? [] + // : await generateOwnerUpdateTransactions( + // $context.gaslessOwnerUpdateTaskId, + // $context.claimableId, + // ); + + // const setSplitsAndMetadataTransactionStep = { + // transaction: tx, + // gasless: $gaslessStore, + // applyGasBuffer: false, + // title: 'Set ORCID splits and metadata', + // }; + + // return [...ownerUpdateTransactionSteps, setSplitsAndMetadataTransactionStep]; + return tx.txs; }, after: async () => { @@ -257,7 +270,7 @@ 2000, ); - // Invalidate cached project page (if any). This should happen automatically, but without + // Invalidate cached ORCID page (if any). This should happen automatically, but without // awaiting it here in addition, there could be a race condition. Better safe than sorry! await invalidateAccountCache(orcidAccountId); await invalidateAll(); diff --git a/src/lib/flows/import-from-csv/upload.svelte b/src/lib/flows/import-from-csv/upload.svelte index ccd6cb48c..52a9dac7d 100644 --- a/src/lib/flows/import-from-csv/upload.svelte +++ b/src/lib/flows/import-from-csv/upload.svelte @@ -101,7 +101,7 @@ // can't classify this input as something we recognize if (!classification) { - const error = new AddItemSuberror(createInvalidMessage('unknown'), recipient, index + 1); + const error = new AddItemSuberror("This isn't valid", recipient, index + 1); errors.push(error); continue; } diff --git a/src/lib/utils/orcids/build-orcid-claiming-txs.ts b/src/lib/utils/orcids/build-orcid-claiming-txs.ts new file mode 100644 index 000000000..debdb87d9 --- /dev/null +++ b/src/lib/utils/orcids/build-orcid-claiming-txs.ts @@ -0,0 +1,26 @@ +import type { TransactionWrapper } from '$lib/components/stepper/types'; +import type { State } from '$lib/flows/claim-orcid-flow/claim-orcid-flow'; +import { sdkManager } from '$lib/utils/sdk/sdk-manager'; + +export async function buildOrcidClaimingTxs(context: State): Promise<{ + txs: TransactionWrapper[]; +}> { + const sdk = sdkManager.sdk; + if (!sdk) throw new Error('SDK not initialized'); + + const preparedTx = await sdk.linkedIdentities.prepareClaimOrcid({ + orcidId: context.claimableId, + }); + + return { + txs: [ + { + title: 'Claim ORCID', + transaction: preparedTx, + // TODO: what should this be? + gasless: false, + applyGasBuffer: true, + }, + ], + }; +} diff --git a/src/lib/utils/orcids/build-orcid-url.ts b/src/lib/utils/orcids/build-orcid-url.ts index 039067f17..0da5c665e 100644 --- a/src/lib/utils/orcids/build-orcid-url.ts +++ b/src/lib/utils/orcids/build-orcid-url.ts @@ -1,14 +1,18 @@ -import { PUBLIC_ORCID_API_URL } from "$env/static/public" +import { PUBLIC_ORCID_API_URL } from '$env/static/public'; -export default function buildOrcidUrl(orcidId: string, { absolute = false, external = false }: { absolute?: boolean, external?: boolean } = {}): string { +export default function buildOrcidUrl( + orcidId: string, + { absolute = false, external = false }: { absolute?: boolean; external?: boolean } = {}, +): string { if (external) { - return `${PUBLIC_ORCID_API_URL}/${orcidId}` + const webDomain = PUBLIC_ORCID_API_URL.replace('pub.', ''); + return `${webDomain}/${orcidId}`; } - let origin = '' + let origin = ''; if (absolute && typeof window !== 'undefined' && window) { - origin = window.location.origin + origin = window.location.origin; } - return `${origin}/app/orcids/${orcidId}` -} \ No newline at end of file + return `${origin}/app/orcids/${orcidId}`; +} diff --git a/src/lib/utils/orcids/fetch-orcid.ts b/src/lib/utils/orcids/fetch-orcid.ts index 72c05a150..90f245064 100644 --- a/src/lib/utils/orcids/fetch-orcid.ts +++ b/src/lib/utils/orcids/fetch-orcid.ts @@ -8,7 +8,10 @@ import { Forge, type OxString } from '$lib/utils/sdk/sdk-types'; import { PUBLIC_ORCID_API_URL } from '$env/static/public'; import { OrcidApiResponseSchema } from '$lib/utils/orcids/schemas'; import Orcid from '$lib/utils/orcids/entities'; -import type { OrcidByAccountIdQuery, OrcidByAccountIdQueryVariables } from './__generated__/gql.generated'; +import type { + OrcidByAccountIdQuery, + OrcidByAccountIdQueryVariables, +} from './__generated__/gql.generated'; export function orcidIdToAccountId(orcidId: string) { return executeRepoDriverReadMethod({ @@ -46,7 +49,7 @@ const getOrcidQuery = gql` } `; -export async function fetchOrcidAccount(accountId: string, fetch: typeof global.fetch) { +export async function fetchOrcidAccount(accountId: string, fetch?: typeof global.fetch) { return query( getOrcidQuery, { From c356f262a28128b658bb804ac4a099ef78825942 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Fri, 26 Sep 2025 09:05:37 -0700 Subject: [PATCH 071/155] Start integrating and understanding gasless with regards to ORCID claiming --- .../set-splits-and-emit-metadata.svelte | 242 +++++++++--------- .../utils/orcids/build-orcid-claiming-txs.ts | 6 +- .../call/orcid-owner-update/+server.ts | 51 ++-- 3 files changed, 155 insertions(+), 144 deletions(-) diff --git a/src/lib/flows/claim-orcid-flow/steps/set-splits-and-emit-metadata/set-splits-and-emit-metadata.svelte b/src/lib/flows/claim-orcid-flow/steps/set-splits-and-emit-metadata/set-splits-and-emit-metadata.svelte index 37511174c..e0323772a 100644 --- a/src/lib/flows/claim-orcid-flow/steps/set-splits-and-emit-metadata/set-splits-and-emit-metadata.svelte +++ b/src/lib/flows/claim-orcid-flow/steps/set-splits-and-emit-metadata/set-splits-and-emit-metadata.svelte @@ -3,6 +3,7 @@ import { makeTransactPayload, type StepComponentEvents, + type TransactionWrapperOrExternalTransaction, // type TransactionWrapperOrExternalTransaction, } from '$lib/components/stepper/types'; import type { Writable } from 'svelte/store'; @@ -67,120 +68,123 @@ ); } - // async function waitForOrcidOwnerUpdate(gasless: boolean) { - // if (gasless) { - // // First, wait for Gelato Relay to resolve the update task. - // const gaslessOwnerUpdateExpectation = await expect( - // async () => { - // const res = await fetch(`/api/gasless/track/${$context.gaslessOwnerUpdateTaskId}`); - // if (!res.ok) throw new Error('Failed to track gasless owner update task'); - - // const { task } = await res.json(); - // assert(typeof task === 'object', 'Invalid task'); - // const { taskState } = task; - // assert(typeof taskState === 'string', 'Invalid task state'); - - // return taskState; - // }, - // (taskState) => { - // switch (taskState) { - // case 'ExecSuccess': - // return true; - // case 'Cancelled': - // throw new Error( - // 'Failed to gaslessly update the repository owner on-chain. There may be a temporary issue with our Transaction Relay provider. Please try again later.', - // ); - // default: - // return false; - // } - // }, - // 600000, - // 2000, - // ); - - // if (gaslessOwnerUpdateExpectation.failed) { - // throw new Error( - // "The gasless owner update transaction didn't resolve in the expected timeframe. There may be an issue with our Transaction Relay provider. Please try again later, or disable gasless transactions in the Drips application settings.", - // ); - // } - // } - - // // Next, wait for the new owner to be indexed by our infra. - // const ownerIndexedExpectation = await expect( - // () => checkOrcidInExpectedStateForClaiming(), - // (response) => response, - // 600000, - // 2000, - // ); - - // if (ownerIndexedExpectation.failed) { - // throw new Error( - // 'The new owner was not indexed in the expected timeframe. There may be a temporary issue with our oracle provider. Please try again later.', - // ); - // } - // } - - // async function generateOwnerUpdateTransactions( - // gasslessOwnerUpdateTaskId: string | undefined, - // orcid: string, - // ) { - // let transactions: TransactionWrapperOrExternalTransaction[] = []; - // let fakeProgressBarConfig: { expectedDurationMs: number; expectedDurationText: string }; - - // switch (network.chainId) { - // case 1: { - // fakeProgressBarConfig = { - // expectedDurationMs: 100000, - // expectedDurationText: 'Usually less than a minute', - // }; - // break; - // } - // case 314: { - // fakeProgressBarConfig = { - // expectedDurationMs: 500000, - // expectedDurationText: 'Usually less than 5 minutes', - // }; - // break; - // } - // default: { - // fakeProgressBarConfig = { - // expectedDurationMs: 100000, - // expectedDurationText: 'Usually less than a minute', - // }; - // } - // } - - // if (gasslessOwnerUpdateTaskId) { - // transactions.push({ - // external: true, - // title: 'Finalizing verification...', - // ...fakeProgressBarConfig, - // promise: () => waitForOrcidOwnerUpdate(true), - // }); - // } else { - // const ownerUpdateTx = await populateRepoDriverWriteTx({ - // functionName: 'requestUpdateOwner', - // args: [0, hexlify(toUtf8Bytes(orcid)) as `0x${string}`], - // }); - - // transactions.push( - // { - // title: 'Request update of ORCID owner', - // transaction: ownerUpdateTx, - // gasless: false, - // applyGasBuffer: false, - // }, - // { - // external: true, - // title: 'Finalizing verification...', - // ...fakeProgressBarConfig, - // promise: () => waitForOrcidOwnerUpdate(false), - // }, - // ); - // } - - // return transactions; - // } + async function waitForOrcidOwnerUpdate(gasless: boolean) { + if (gasless) { + // First, wait for Gelato Relay to resolve the update task. + const gaslessOwnerUpdateExpectation = await expect( + async () => { + const res = await fetch(`/api/gasless/track/${$context.gaslessOwnerUpdateTaskId}`); + if (!res.ok) throw new Error('Failed to track gasless owner update task'); + + const { task } = await res.json(); + assert(typeof task === 'object', 'Invalid task'); + const { taskState } = task; + assert(typeof taskState === 'string', 'Invalid task state'); + + return taskState; + }, + (taskState) => { + switch (taskState) { + case 'ExecSuccess': + return true; + case 'Cancelled': + throw new Error( + 'Failed to gaslessly update the repository owner on-chain. There may be a temporary issue with our Transaction Relay provider. Please try again later.', + ); + default: + return false; + } + }, + 600000, + 2000, + ); + + if (gaslessOwnerUpdateExpectation.failed) { + throw new Error( + "The gasless owner update transaction didn't resolve in the expected timeframe. There may be an issue with our Transaction Relay provider. Please try again later, or disable gasless transactions in the Drips application settings.", + ); + } + } + + // Next, wait for the new owner to be indexed by our infra. + const ownerIndexedExpectation = await expect( + () => checkOrcidInExpectedStateForClaiming(), + (response) => response, + 600000, + 2000, + ); + + if (ownerIndexedExpectation.failed) { + throw new Error( + 'The new owner was not indexed in the expected timeframe. There may be a temporary issue with our oracle provider. Please try again later.', + ); + } + } + + async function generateOwnerUpdateTransactions( + gasslessOwnerUpdateTaskId: string | undefined, + orcid: string, + ) { + let transactions: TransactionWrapperOrExternalTransaction[] = []; + let fakeProgressBarConfig: { expectedDurationMs: number; expectedDurationText: string }; + + switch (network.chainId) { + case 1: { + fakeProgressBarConfig = { + expectedDurationMs: 100000, + expectedDurationText: 'Usually less than a minute', + }; + break; + } + case 314: { + fakeProgressBarConfig = { + expectedDurationMs: 500000, + expectedDurationText: 'Usually less than 5 minutes', + }; + break; + } + default: { + fakeProgressBarConfig = { + expectedDurationMs: 100000, + expectedDurationText: 'Usually less than a minute', + }; + } + } + + if (gasslessOwnerUpdateTaskId) { + transactions.push({ + external: true, + title: 'Finalizing verification...', + ...fakeProgressBarConfig, + promise: () => waitForOrcidOwnerUpdate(true), + }); + } else { + // const ownerUpdateTx = await populateRepoDriverWriteTx({ + // functionName: 'requestUpdateOwner', + // args: [0, hexlify(toUtf8Bytes(orcid)) as `0x${string}`], + // }); + + const tx = await buildOrcidClaimingTxs(orcid); + + transactions.push( + ...tx.txs, + // { + // title: 'Request update of ORCID owner', + // transaction: ownerUpdateTx, + // gasless: false, + // applyGasBuffer: false, + // }, + { + external: true, + title: 'Finalizing verification...', + ...fakeProgressBarConfig, + promise: () => waitForOrcidOwnerUpdate(false), + }, + ); + } + + return transactions; + } onMount(() => dispatch( @@ -198,7 +202,7 @@ // args: [setSplitsAndEmitMetadataBatch.map(txToCallerCall)], // }); - const tx = await buildOrcidClaimingTxs($context); + const tx = await buildOrcidClaimingTxs($context.claimableId); // Check once if the project is already in the expected state for the final claim TX, // and skip the step that waits for everything to be in the right state if so. @@ -213,7 +217,8 @@ duringBefore: 'Preparing to claim ORCID...', }, - transactions: async ({ tx }) => { + // TODO: what do we do with this? + transactions: async (/*{ tx }*/) => { // if(gasless) { // return generateOwnerUpdateTransactions( // $context.gaslessOwnerUpdateTaskId, @@ -238,7 +243,10 @@ // }; // return [...ownerUpdateTransactionSteps, setSplitsAndMetadataTransactionStep]; - return tx.txs; + return generateOwnerUpdateTransactions( + $context.gaslessOwnerUpdateTaskId, + $context.claimableId, + ); }, after: async () => { diff --git a/src/lib/utils/orcids/build-orcid-claiming-txs.ts b/src/lib/utils/orcids/build-orcid-claiming-txs.ts index debdb87d9..395a727ef 100644 --- a/src/lib/utils/orcids/build-orcid-claiming-txs.ts +++ b/src/lib/utils/orcids/build-orcid-claiming-txs.ts @@ -1,15 +1,14 @@ import type { TransactionWrapper } from '$lib/components/stepper/types'; -import type { State } from '$lib/flows/claim-orcid-flow/claim-orcid-flow'; import { sdkManager } from '$lib/utils/sdk/sdk-manager'; -export async function buildOrcidClaimingTxs(context: State): Promise<{ +export async function buildOrcidClaimingTxs(orcidId: string): Promise<{ txs: TransactionWrapper[]; }> { const sdk = sdkManager.sdk; if (!sdk) throw new Error('SDK not initialized'); const preparedTx = await sdk.linkedIdentities.prepareClaimOrcid({ - orcidId: context.claimableId, + orcidId, }); return { @@ -17,7 +16,6 @@ export async function buildOrcidClaimingTxs(context: State): Promise<{ { title: 'Claim ORCID', transaction: preparedTx, - // TODO: what should this be? gasless: false, applyGasBuffer: true, }, diff --git a/src/routes/api/gasless/call/orcid-owner-update/+server.ts b/src/routes/api/gasless/call/orcid-owner-update/+server.ts index 9f7f9862d..907dd5639 100644 --- a/src/routes/api/gasless/call/orcid-owner-update/+server.ts +++ b/src/routes/api/gasless/call/orcid-owner-update/+server.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import type { RequestHandler } from './$types'; import { error } from '@sveltejs/kit'; -import { ethers, toUtf8Bytes } from 'ethers'; +// import { ethers, toUtf8Bytes } from 'ethers'; import unreachable from '$lib/utils/unreachable'; import { GelatoRelay, type SponsoredCallRequest } from '@gelatonetwork/relay-sdk'; import assert from '$lib/utils/assert'; @@ -14,11 +14,12 @@ import type { } from './__generated__/gql.generated'; import { redis } from '../../../redis'; import getOptionalEnvVar from '$lib/utils/get-optional-env-var/private'; -import { JsonRpcProvider } from 'ethers'; +// import { JsonRpcProvider } from 'ethers'; import isClaimed from '$lib/utils/orcids/is-claimed'; import { fetchOrcid } from '$lib/utils/orcids/fetch-orcid'; import { getClaimingUrlAddress } from '$lib/utils/orcids/verify-orcid'; -import { Forge } from '$lib/utils/sdk/sdk-types'; +// import { Forge } from '$lib/utils/sdk/sdk-types'; +import { buildOrcidClaimingTxs } from '$lib/utils/orcids/build-orcid-claiming-txs'; const GELATO_API_KEY = getOptionalEnvVar( 'GELATO_API_KEY', @@ -32,18 +33,18 @@ const payloadSchema = z.object({ chainId: z.number(), }); -const REPO_DRIVER_ABI = `[ - { - "inputs": [ - { "internalType": "enum Forge", "name": "forge", "type": "uint8" }, - { "internalType": "bytes", "name": "name", "type": "bytes" } - ], - "name": "requestUpdateOwner", - "outputs": [{ "internalType": "uint256", "name": "accountId", "type": "uint256" }], - "stateMutability": "nonpayable", - "type": "function" - }, -]`; +// const REPO_DRIVER_ABI = `[ +// { +// "inputs": [ +// { "internalType": "enum Forge", "name": "forge", "type": "uint8" }, +// { "internalType": "bytes", "name": "name", "type": "bytes" } +// ], +// "name": "requestUpdateOwner", +// "outputs": [{ "internalType": "uint256", "name": "accountId", "type": "uint256" }], +// "stateMutability": "nonpayable", +// "type": "function" +// }, +// ]`; const orcidUnclaimedQuery = gql` query isOrcidUnclaimed($orcid: String!, $chain: SupportedChain!) { @@ -142,18 +143,22 @@ export const POST: RequestHandler = async ({ request, fetch }) => { } } - const provider = new JsonRpcProvider(network.rpcUrl); - const contract = new ethers.Contract(network.contracts.REPO_DRIVER, REPO_DRIVER_ABI, provider); + const { + txs: [{ transaction: claimOrcidTx }], + } = await buildOrcidClaimingTxs(orcid); - const tx = await contract.requestUpdateOwner.populateTransaction( - Forge.orcidId, - ethers.hexlify(toUtf8Bytes(orcid)), - ); + // const provider = new JsonRpcProvider(network.rpcUrl); + // const contract = new ethers.Contract(network.contracts.REPO_DRIVER, REPO_DRIVER_ABI, provider); + + // const tx = await contract.requestUpdateOwner.populateTransaction( + // Forge.orcidId, + // ethers.hexlify(toUtf8Bytes(orcid)), + // ); const relayRequest: SponsoredCallRequest = { chainId: BigInt(chainId), - target: tx.to ?? unreachable(), - data: tx.data ?? unreachable(), + target: claimOrcidTx.to ?? unreachable(), + data: claimOrcidTx.data ?? unreachable(), }; const relay = new GelatoRelay(); From 0890aa62da894f7a58c80f966b4b39e28f3596a3 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Fri, 26 Sep 2025 13:16:19 -0700 Subject: [PATCH 072/155] Fix rebase problems --- src/lib/components/splits/types.ts | 31 +- .../supporters.section.svelte | 3 - src/lib/utils/driplist/DripListService.ts | 353 ++++++++++++++++++ .../utils/orcids/build-orcid-claiming-txs.ts | 1 - .../[orcidId]/components/orcid-profile.svelte | 1 - 5 files changed, 371 insertions(+), 18 deletions(-) create mode 100644 src/lib/utils/driplist/DripListService.ts diff --git a/src/lib/components/splits/types.ts b/src/lib/components/splits/types.ts index d304846cd..8ad16500a 100644 --- a/src/lib/components/splits/types.ts +++ b/src/lib/components/splits/types.ts @@ -99,6 +99,20 @@ export const SPLITS_COMPONENT_SUB_LIST_RECEIVER_FRAGMENT = gql` } `; +export const SPLITS_COMPONENT_ORCID_RECEIVER_FRAGMENT = gql` + fragment SplitsComponentOrcidReceiver on LinkedIdentityReceiver { + weight + linkedIdentity { + ... on OrcidLinkedIdentity { + account { + accountId + } + orcid + } + } + } +`; + export const SPLITS_COMPONENT_PROJECT_SPLITS_FRAGMENT = gql` ${PROJECT_AVATAR_FRAGMENT} ${SPLITS_COMPONENT_PROJECT_RECEIVER_FRAGMENT} @@ -106,6 +120,7 @@ export const SPLITS_COMPONENT_PROJECT_SPLITS_FRAGMENT = gql` ${SPLITS_COMPONENT_ADDRESS_RECEIVER_FRAGMENT} ${SPLITS_COMPONENT_ECOSYSTEM_RECEIVER_FRAGMENT} ${SPLITS_COMPONENT_SUB_LIST_RECEIVER_FRAGMENT} + ${SPLITS_COMPONENT_ORCID_RECEIVER_FRAGMENT} fragment SplitsComponentProjectSplits on ProjectData { ... on ClaimedProjectData { splits { @@ -125,6 +140,9 @@ export const SPLITS_COMPONENT_PROJECT_SPLITS_FRAGMENT = gql` ... on SubListReceiver { ...SplitsComponentSubListReceiver } + ... on LinkedIdentityReceiver { + ...SplitsComponentOrcidReceiver + } } maintainers { ... on AddressReceiver { @@ -135,19 +153,6 @@ export const SPLITS_COMPONENT_PROJECT_SPLITS_FRAGMENT = gql` } } `; -export const SPLITS_COMPONENT_ORCID_RECEIVER_FRAGMENT = gql` - fragment SplitsComponentOrcidReceiver on LinkedIdentityReceiver { - weight - linkedIdentity { - ... on OrcidLinkedIdentity { - account { - accountId - } - orcid - } - } - } -`; export type SplitsComponentSplitsReceiver = | SplitsComponentAddressReceiverFragment diff --git a/src/lib/components/supporters-section/supporters.section.svelte b/src/lib/components/supporters-section/supporters.section.svelte index c4439fb82..cb4bc257e 100644 --- a/src/lib/components/supporters-section/supporters.section.svelte +++ b/src/lib/components/supporters-section/supporters.section.svelte @@ -132,8 +132,6 @@ export let collapsed = false; export let collapsable = false; - export let iconPrimary = true; - let emptyStateText: string; $: { switch (type) { @@ -164,7 +162,6 @@ bind:collapsable header={{ icon: Heart, - iconPrimary, label: headline, infoTooltip, }} diff --git a/src/lib/utils/driplist/DripListService.ts b/src/lib/utils/driplist/DripListService.ts new file mode 100644 index 000000000..26480e7e5 --- /dev/null +++ b/src/lib/utils/driplist/DripListService.ts @@ -0,0 +1,353 @@ +import NftDriverMetadataManager from '../metadata/NftDriverMetadataManager'; +import MetadataManagerBase from '../metadata/MetadataManagerBase'; +import { MaxUint256, type Signer, toBigInt } from 'ethers'; +import GitProjectService from '../project/GitProjectService'; +import assert from '$lib/utils/assert'; +import type { Address, IpfsHash } from '../common-types'; +import wallet from '$lib/stores/wallet/wallet.store'; +import { get } from 'svelte/store'; +import Emoji from '$lib/components/emoji/emoji.svelte'; +import type { nftDriverAccountMetadataParser } from '../metadata/schemas'; +import type { LatestVersion } from '@efstajas/versioned-parser'; +import type { Items, Weights } from '$lib/components/list-editor/types'; +import { buildStreamCreateBatchTx } from '../streams/streams'; +import { + executeNftDriverReadMethod, + executeNftDriverWriteMethod, + populateNftDriverWriteTx, +} from '../sdk/nft-driver/nft-driver'; +import type { OxString, SplitsReceiver } from '../sdk/sdk-types'; +import { + getAddressDriverAllowance, + populateAddressDriverWriteTx, +} from '../sdk/address-driver/address-driver'; +import type { ContractTransaction } from 'ethers'; +import { populateErc20WriteTx } from '../sdk/erc20/erc20'; +import { formatSplitReceivers } from '../sdk/utils/format-split-receivers'; +import keyValueToMetatada from '../sdk/utils/key-value-to-metadata'; +import { populateCallerWriteTx } from '../sdk/caller/caller'; +import txToCallerCall from '../sdk/utils/tx-to-caller-call'; +import network from '$lib/stores/wallet/network'; +import calculateRandomSalt from '../calc-salt'; + +type AccountId = string; + +const WAITING_WALLET_ICON = { + component: Emoji, + props: { + emoji: '👛', + size: 'huge', + }, +}; + +/** + * A class for managing `DripList`s. + * + * **Important**: This class assumes that *all* clients and factories are connected to the *same* signer. + */ +export default class DripListService { + private readonly SEED_CONSTANT = 'Drips App'; + + private _owner!: Signer | undefined; + private _ownerAddress!: Address | undefined; + private _nftDriverMetadataManager!: NftDriverMetadataManager; + + private constructor() {} + + /** + * Creates a new `DripListService` instance. + * @returns A new `DripListService` instance. + */ + public static async new(): Promise { + const dripListService = new DripListService(); + + const { connected, signer } = get(wallet); + + if (connected) { + assert(signer, 'Signer address is undefined.'); + dripListService._owner = signer; + dripListService._ownerAddress = await signer.getAddress(); + + dripListService._nftDriverMetadataManager = new NftDriverMetadataManager( + executeNftDriverWriteMethod, + ); + } else { + dripListService._nftDriverMetadataManager = new NftDriverMetadataManager(); + } + + return dripListService; + } + + public async buildTransactContext(config: { + listTitle: string; + listDescription?: string; + weights: Weights; + items: Items; + support?: + | { + type: 'continuous'; + tokenAddress: string; + amountPerSec: bigint; + topUpAmount: bigint; + } + | { + type: 'one-time'; + tokenAddress: string; + donationAmount: bigint; + }; + latestVotingRoundId?: string; + isVisible: boolean; + }) { + assert(this._ownerAddress, `This function requires an active wallet connection.`); + + const { listTitle, listDescription, weights, items, support, latestVotingRoundId, isVisible } = + config; + + const { recipientsMetadata, receivers } = await this.getProjectsSplitMetadataAndReceivers( + weights, + items, + ); + + const salt = calculateRandomSalt(); + + const listId = ( + await executeNftDriverReadMethod({ + functionName: 'calcTokenIdWithSalt', + args: [this._ownerAddress as OxString, salt], + }) + ).toString(); + + const ipfsHash = await this._publishMetadataToIpfs( + listId, + recipientsMetadata, + isVisible, + listTitle, + listDescription, + latestVotingRoundId, + ); + + const createDripListTx = await this._buildCreateDripListTx(salt, ipfsHash); + + const setDripListSplitsTx = await populateNftDriverWriteTx({ + functionName: 'setSplits', + args: [toBigInt(listId), formatSplitReceivers(receivers)], + }); + + let needsApprovalForToken: string | undefined; + let txs: ContractTransaction[]; + + if (support?.type === 'continuous') { + const { tokenAddress, amountPerSec, topUpAmount } = support; + + const allowance = await getAddressDriverAllowance(tokenAddress as OxString); + const needsApproval = allowance < topUpAmount; + + if (needsApproval) { + needsApprovalForToken = tokenAddress; + } + + const setStreamTx = await this._buildSetDripListStreamTxs( + tokenAddress, + listId, + topUpAmount, + amountPerSec, + ); + + txs = [createDripListTx, setDripListSplitsTx, ...setStreamTx.batch]; + } else if (support?.type === 'one-time') { + const { tokenAddress, donationAmount } = support; + + const allowance = await getAddressDriverAllowance(tokenAddress as OxString); + const needsApproval = allowance < donationAmount; + + if (needsApproval) { + needsApprovalForToken = tokenAddress; + } + + const giveTx = await populateAddressDriverWriteTx({ + functionName: 'give', + args: [toBigInt(listId), tokenAddress as OxString, donationAmount], + }); + + txs = [createDripListTx, setDripListSplitsTx, giveTx]; + } else { + // No support + txs = [createDripListTx, setDripListSplitsTx]; + } + + const batch = await populateCallerWriteTx({ + functionName: 'callBatched', + args: [txs.map(txToCallerCall)], + }); + + return { + txs: [ + ...(needsApprovalForToken + ? [ + { + title: `Approve Drips to withdraw ${needsApprovalForToken}`, + transaction: await this._buildTokenApprovalTx(needsApprovalForToken), + waitingSignatureMessage: { + message: `Waiting for you to approve Drips access to the ERC-20 token in your wallet...`, + subtitle: 'You only have to do this once per token.', + icon: WAITING_WALLET_ICON, + }, + applyGasBuffer: false, + }, + ] + : []), + { + title: 'Creating the Drip List', + transaction: batch, + applyGasBuffer: true, + }, + ], + dripListId: listId, + }; + } + + public async getProjectsSplitMetadataAndReceivers(weights: Weights, items: Items) { + const projectsInput = Object.entries(weights); + + const receivers: SplitsReceiver[] = []; + + const recipientsMetadata: Extract< + LatestVersion, + { type: 'dripList' } + >['recipients'] = []; + + for (const [accountId, weight] of projectsInput) { + const item = items[accountId]; + if (weight <= 0) continue; + + switch (item.type) { + case 'address': { + const receiver = { + type: 'address' as const, + weight, + accountId, + }; + + recipientsMetadata.push(receiver); + receivers.push(receiver); + + break; + } + case 'project': { + const { forge, ownerName, repoName } = item.project.source; + + const receiver = { + type: 'repoDriver' as const, + weight, + accountId, + }; + + recipientsMetadata.push({ + ...receiver, + source: GitProjectService.populateSource(forge, repoName, ownerName), + }); + + receivers.push(receiver); + + break; + } + case 'drip-list': { + const receiver = { + type: 'dripList' as const, + weight, + accountId, + }; + + recipientsMetadata.push(receiver); + receivers.push(receiver); + + break; + } + } + } + + return { + recipientsMetadata, + receivers: receivers, + }; + } + + private async _buildCreateDripListTx(salt: bigint, ipfsHash: IpfsHash) { + assert(this._ownerAddress, `This function requires an active wallet connection.`); + + const createDripListTx = await populateNftDriverWriteTx({ + functionName: 'safeMintWithSalt', + args: [ + salt, + this._ownerAddress as OxString, + [ + { + key: MetadataManagerBase.USER_METADATA_KEY, + value: ipfsHash, + }, + ].map(keyValueToMetatada), + ], + }); + + return createDripListTx; + } + + private async _buildSetDripListStreamTxs( + token: Address, + dripListId: AccountId, + topUpAmount: bigint, + amountPerSec: bigint, + ) { + assert(this._owner, `This function requires an active wallet connection.`); + + return await buildStreamCreateBatchTx( + this._owner, + { + tokenAddress: token, + amountPerSecond: amountPerSec, + recipientAccountId: dripListId, + name: undefined, + }, + topUpAmount, + ); + } + + private async _buildTokenApprovalTx(token: Address): Promise { + assert(this._owner, `This function requires an active wallet connection.`); + + const tokenApprovalTx = await populateErc20WriteTx({ + token: token as OxString, + functionName: 'approve', + args: [network.contracts.ADDRESS_DRIVER as OxString, MaxUint256], + }); + + return tokenApprovalTx; + } + + private async _publishMetadataToIpfs( + dripListId: string, + recipients: Extract< + LatestVersion, + { type: 'dripList' } + >['recipients'], + isVisible: boolean, + name?: string, + description?: string, + latestVotingRoundId?: string, + ): Promise { + assert(this._ownerAddress, `This function requires an active wallet connection.`); + + const dripListMetadata = this._nftDriverMetadataManager.buildAccountMetadata({ + forAccountId: dripListId, + recipients, + name, + description, + latestVotingRoundId, + isVisible, + }); + + const ipfsHash = await this._nftDriverMetadataManager.pinAccountMetadata(dripListMetadata); + + return ipfsHash; + } +} diff --git a/src/lib/utils/orcids/build-orcid-claiming-txs.ts b/src/lib/utils/orcids/build-orcid-claiming-txs.ts index 395a727ef..71611e454 100644 --- a/src/lib/utils/orcids/build-orcid-claiming-txs.ts +++ b/src/lib/utils/orcids/build-orcid-claiming-txs.ts @@ -16,7 +16,6 @@ export async function buildOrcidClaimingTxs(orcidId: string): Promise<{ { title: 'Claim ORCID', transaction: preparedTx, - gasless: false, applyGasBuffer: true, }, ], diff --git a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte index 43162273c..ccf3b118f 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/orcid-profile.svelte @@ -145,7 +145,6 @@ bind:sectionSkeleton={supportersSectionSkeleton} type="ecosystem" supportItems={support} - iconPrimary={false} />