From e9463c52a0bf5e537a859a9e9330568a7b903d77 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Tue, 19 Aug 2025 11:11:12 +0200 Subject: [PATCH 01/31] Fetch orcid, get orcid account id --- docker/start-e2e.sh | 5 +- .../(app)/orcids/[orcidId]/+page.server.ts | 3 + .../[orcidId]/components/fetch-orcid.ts | 58 +++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts 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/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts index 1300bded4..a4e948984 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts @@ -46,3 +46,6 @@ export const load = (async ({ params, fetch }) => { orcidAccount, }; }) 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 new file mode 100644 index 000000000..3bbe411c5 --- /dev/null +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts @@ -0,0 +1,58 @@ +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'; +import { PUBLIC_ORCID_API_URL } from '$env/static/public'; + +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) { + 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` + ${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, + ); +} From f8b10b583ce8c46848d158103db4ddd2ecd1b3d5 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 25 Aug 2025 13:34:26 +0200 Subject: [PATCH 02/31] One time donation support scaffolding for ORCIDs --- .../create-donation/methods/build-one-time-donation-txs.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/flows/create-donation/methods/build-one-time-donation-txs.ts b/src/lib/flows/create-donation/methods/build-one-time-donation-txs.ts index 548f89f78..cc043d0ec 100644 --- a/src/lib/flows/create-donation/methods/build-one-time-donation-txs.ts +++ b/src/lib/flows/create-donation/methods/build-one-time-donation-txs.ts @@ -21,6 +21,12 @@ import type { CreateDonationDetailsStepOrcidFragment, } from '../__generated__/gql.generated'; +// TODO: integrate into SDK +type SdkOrcidReceiver = { + type: 'orcid-account'; + accountId: bigint; +}; + const WAITING_WALLET_ICON = { component: 'Emoji', props: { From 5dc5686d09ace277d42056f00ad7ed5d764f20fc Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 25 Aug 2025 14:29:00 +0200 Subject: [PATCH 03/31] Ensure serialization of ORCID entity --- .../support-card/support-card.svelte | 4 +++ src/lib/reviver.ts | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/lib/reviver.ts diff --git a/src/lib/components/support-card/support-card.svelte b/src/lib/components/support-card/support-card.svelte index b784c7f84..ea95cdf0a 100644 --- a/src/lib/components/support-card/support-card.svelte +++ b/src/lib/components/support-card/support-card.svelte @@ -311,6 +311,10 @@
+ {:else if orcid} +
+ +
{/if} diff --git a/src/lib/reviver.ts b/src/lib/reviver.ts new file mode 100644 index 000000000..b3ff23ad4 --- /dev/null +++ b/src/lib/reviver.ts @@ -0,0 +1,36 @@ +// src/lib/reviver.ts + +import Orcid from "./utils/orcids/entities"; + +// A map of class names to their constructors +const classMap = { + Orcid, + // You could add other classes here in the future + // AnotherClass, +}; + +type ClassMap = typeof classMap; + +/** + * Revives plain objects back into their class instances. + * @param key The current key in the object being processed. + * @param value The value associated with the key. + */ +export function revive(key: string, value: any): T { + if (value && typeof value === 'object' && value.__class) { + const ClassConstructor = classMap[value.__class as keyof ClassMap]; + if (ClassConstructor) { + // Re-instantiate the class with its data + return new ClassConstructor(value.data) as T; + } + } + return value as T; +} + +/** + * A helper to walk through the entire data object from `load`. + * @param data The data object from SvelteKit's `load` function. + */ +export function hydrate(data: any) { + return JSON.parse(JSON.stringify(data), revive); +} \ No newline at end of file From c3eb74e224ebea6dbc3e9f14d5652089e77a3718 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 25 Aug 2025 17:21:15 +0200 Subject: [PATCH 04/31] Refine ORCID badge, icon --- src/lib/components/button/button.svelte | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib/components/button/button.svelte b/src/lib/components/button/button.svelte index 9f789cbf5..d04270e28 100644 --- a/src/lib/components/button/button.svelte +++ b/src/lib/components/button/button.svelte @@ -189,6 +189,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); } From 5e720878328c038c1c6759725ba17233828f4238 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Tue, 26 Aug 2025 14:11:14 +0200 Subject: [PATCH 05/31] Initial support for ORCID iDs in list editor; fix circular import of EDIT_DRIP_LIST_FLOW_DRIP_LIST_FRAGMENT --- .../split-receivers-to-list-editor-config.ts | 1 + .../configure-one-time-donation.svelte | 41 +++++++++++++++++++ src/lib/flows/edit-drip-list/fragments.ts | 15 +++++++ 3 files changed, 57 insertions(+) create mode 100644 src/lib/flows/create-drip-list-flow/steps/configure-one-time-donation/configure-one-time-donation.svelte diff --git a/src/lib/components/list-editor/utils/split-receivers-to-list-editor-config.ts b/src/lib/components/list-editor/utils/split-receivers-to-list-editor-config.ts index e433a286f..82f3d0a6c 100644 --- a/src/lib/components/list-editor/utils/split-receivers-to-list-editor-config.ts +++ b/src/lib/components/list-editor/utils/split-receivers-to-list-editor-config.ts @@ -2,6 +2,7 @@ import { gql } from 'graphql-request'; import { LIST_EDITOR_DRIP_LIST_FRAGMENT, LIST_EDITOR_ECOSYSTEM_FRAGMENT, + // LIST_EDITOR_ORCID_FRAGMENT, LIST_EDITOR_PROJECT_FRAGMENT, LIST_EDITOR_SUB_LIST_FRAGMENT, type ListEditorItem, diff --git a/src/lib/flows/create-drip-list-flow/steps/configure-one-time-donation/configure-one-time-donation.svelte b/src/lib/flows/create-drip-list-flow/steps/configure-one-time-donation/configure-one-time-donation.svelte new file mode 100644 index 000000000..8a077fb0b --- /dev/null +++ b/src/lib/flows/create-drip-list-flow/steps/configure-one-time-donation/configure-one-time-donation.svelte @@ -0,0 +1,41 @@ + + + + + + + + + + + diff --git a/src/lib/flows/edit-drip-list/fragments.ts b/src/lib/flows/edit-drip-list/fragments.ts index 11981ef37..4dd3be477 100644 --- a/src/lib/flows/edit-drip-list/fragments.ts +++ b/src/lib/flows/edit-drip-list/fragments.ts @@ -3,7 +3,10 @@ import { SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_ADDRESS_RECEIVER_FRAGMENT, SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_DRIP_LIST_RECEIVER_FRAGMENT, SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_PROJECT_RECEIVER_FRAGMENT, +<<<<<<< HEAD SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_ORCID_RECEIVER_FRAGMENT, +======= +>>>>>>> c87ec7ab (Initial support for ORCID iDs in list editor; fix circular import of EDIT_DRIP_LIST_FLOW_DRIP_LIST_FRAGMENT) } from '$lib/components/list-editor/utils/split-receivers-to-list-editor-config'; import { DRIP_LIST_BADGE_FRAGMENT } from '$lib/components/drip-list-badge/drip-list-badge.svelte'; @@ -11,7 +14,10 @@ export const EDIT_DRIP_LIST_FLOW_DRIP_LIST_FRAGMENT = gql` ${SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_ADDRESS_RECEIVER_FRAGMENT} ${SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_DRIP_LIST_RECEIVER_FRAGMENT} ${SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_PROJECT_RECEIVER_FRAGMENT} +<<<<<<< HEAD ${SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_ORCID_RECEIVER_FRAGMENT} +======= +>>>>>>> c87ec7ab (Initial support for ORCID iDs in list editor; fix circular import of EDIT_DRIP_LIST_FLOW_DRIP_LIST_FRAGMENT) fragment EditDripListFlowDripList on DripList { name description @@ -29,9 +35,12 @@ export const EDIT_DRIP_LIST_FLOW_DRIP_LIST_FRAGMENT = gql` ... on ProjectReceiver { ...SplitReceiversToListEditorConfigProjectReceiver } +<<<<<<< HEAD ... on LinkedIdentityReceiver { ...SplitReceiversToListEditorConfigOrcidReceiver } +======= +>>>>>>> c87ec7ab (Initial support for ORCID iDs in list editor; fix circular import of EDIT_DRIP_LIST_FLOW_DRIP_LIST_FRAGMENT) } } `; @@ -68,11 +77,14 @@ export const SELECT_DRIP_LIST_STEP_LISTS_FRAGMENT = gql` accountId } } +<<<<<<< HEAD ... on LinkedIdentityReceiver { account { accountId } } +======= +>>>>>>> c87ec7ab (Initial support for ORCID iDs in list editor; fix circular import of EDIT_DRIP_LIST_FLOW_DRIP_LIST_FRAGMENT) } } `; @@ -95,6 +107,7 @@ export const SELECT_DRIP_LIST_DRIP_LIST_TO_ADD_FRAGMENT = gql` } } `; +<<<<<<< HEAD export const SELECT_DRIP_LIST_ORCID_TO_ADD_FRAGMENT = gql` fragment SelectDripListOrcidToAdd on OrcidLinkedIdentity { @@ -103,3 +116,5 @@ export const SELECT_DRIP_LIST_ORCID_TO_ADD_FRAGMENT = gql` } } `; +======= +>>>>>>> c87ec7ab (Initial support for ORCID iDs in list editor; fix circular import of EDIT_DRIP_LIST_FLOW_DRIP_LIST_FRAGMENT) From 8976c59ea69662899d4b0cb0d1969ff4c39dc31e Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Tue, 26 Aug 2025 14:27:44 +0200 Subject: [PATCH 06/31] Ensure list editor item can render ORCID iD --- .../create-drip-list-flow/steps/build-list/build-list.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/flows/create-drip-list-flow/steps/build-list/build-list.svelte b/src/lib/flows/create-drip-list-flow/steps/build-list/build-list.svelte index 1ecc5c3e5..43ee0fb59 100644 --- a/src/lib/flows/create-drip-list-flow/steps/build-list/build-list.svelte +++ b/src/lib/flows/create-drip-list-flow/steps/build-list/build-list.svelte @@ -17,6 +17,7 @@ } from '$lib/flows/import-from-csv/csv-import-helpers'; import importFromCSVSteps from '$lib/flows/import-from-csv/import-from-csv-steps'; import CustodialWarning from '$lib/components/annotation-box/custodial-warning.svelte'; + import type { AccountId } from '$lib/utils/common-types'; const dispatch = createEventDispatcher(); From 58d41d258c1c93a2dff5641ba1a2ca9fdb911e30 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Tue, 26 Aug 2025 16:43:38 +0200 Subject: [PATCH 07/31] Allow csv to theoretically support orcids --- .../steps/configure-maintainers/configure-maintainers.svelte | 1 + src/lib/flows/edit-drip-list/shared/steps/edit-drip-list.svelte | 1 + .../flows/edit-project-splits/steps/edit-maintainer-list.svelte | 1 + 3 files changed, 3 insertions(+) diff --git a/src/lib/flows/claim-project-flow/steps/configure-maintainers/configure-maintainers.svelte b/src/lib/flows/claim-project-flow/steps/configure-maintainers/configure-maintainers.svelte index 158e3948c..e61f4b720 100644 --- a/src/lib/flows/claim-project-flow/steps/configure-maintainers/configure-maintainers.svelte +++ b/src/lib/flows/claim-project-flow/steps/configure-maintainers/configure-maintainers.svelte @@ -19,6 +19,7 @@ import mapFilterUndefined from '$lib/utils/map-filter-undefined'; import FormField from '$lib/components/form-field/form-field.svelte'; import ArrowDown from '$lib/components/icons/ArrowDown.svelte'; + import type { AccountId } from '$lib/utils/common-types'; const dispatch = createEventDispatcher(); diff --git a/src/lib/flows/edit-drip-list/shared/steps/edit-drip-list.svelte b/src/lib/flows/edit-drip-list/shared/steps/edit-drip-list.svelte index 0eead314c..6ffcc60d1 100644 --- a/src/lib/flows/edit-drip-list/shared/steps/edit-drip-list.svelte +++ b/src/lib/flows/edit-drip-list/shared/steps/edit-drip-list.svelte @@ -53,6 +53,7 @@ import { invalidateAll } from '$app/navigation'; import { waitForAccountMetadata } from '$lib/utils/ipfs'; import { buildDripListUpdateTxs } from '$lib/utils/driplist/buildDripListUpdateTxs'; + import type { AccountId } from '$lib/utils/common-types'; const dispatch = createEventDispatcher(); diff --git a/src/lib/flows/edit-project-splits/steps/edit-maintainer-list.svelte b/src/lib/flows/edit-project-splits/steps/edit-maintainer-list.svelte index aefe741a1..4b3334a22 100644 --- a/src/lib/flows/edit-project-splits/steps/edit-maintainer-list.svelte +++ b/src/lib/flows/edit-project-splits/steps/edit-maintainer-list.svelte @@ -20,6 +20,7 @@ import ArrowDown from '$lib/components/icons/ArrowDown.svelte'; import FormField from '$lib/components/form-field/form-field.svelte'; import mapFilterUndefined from '$lib/utils/map-filter-undefined'; + import type { AccountId } from '$lib/utils/common-types'; const dispatch = createEventDispatcher(); From 56d5a13cf3417098aa852800fae0648378f6df94 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Tue, 26 Aug 2025 19:05:11 +0200 Subject: [PATCH 08/31] Solve various type errors relating to splits and orcids --- src/lib/components/splits/types.ts | 21 +++++++++++++++++++ .../methods/build-one-time-donation-txs.ts | 6 ++++-- .../review-voting-round.svelte | 1 + src/lib/flows/edit-drip-list/fragments.ts | 15 ------------- .../[orcidId]/components/orcid-profile.svelte | 2 +- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/lib/components/splits/types.ts b/src/lib/components/splits/types.ts index 4b1b71667..6759d6a17 100644 --- a/src/lib/components/splits/types.ts +++ b/src/lib/components/splits/types.ts @@ -47,6 +47,27 @@ export const SPLITS_COMPONENT_DRIP_LIST_FRAGMENT = gql` } `; +// export const SPLITS_COMPONENT_ORCID_FRAGMENT = gql` +// ${PROJECT_BADGE_FRAGMENT} +// fragment SplitsComponentProject on Project { +// ...ProjectBadge +// source { +// repoName +// ownerName +// } +// isVisible +// chainData { +// ... on ClaimedProjectData { +// chain +// owner { +// address +// } +// color +// } +// } +// } +// `; + export const SPLITS_COMPONENT_PROJECT_RECEIVER_FRAGMENT = gql` ${SPLITS_COMPONENT_PROJECT_FRAGMENT} fragment SplitsComponentProjectReceiver on ProjectReceiver { diff --git a/src/lib/flows/create-donation/methods/build-one-time-donation-txs.ts b/src/lib/flows/create-donation/methods/build-one-time-donation-txs.ts index cc043d0ec..bbf980f77 100644 --- a/src/lib/flows/create-donation/methods/build-one-time-donation-txs.ts +++ b/src/lib/flows/create-donation/methods/build-one-time-donation-txs.ts @@ -23,10 +23,12 @@ import type { // TODO: integrate into SDK type SdkOrcidReceiver = { - type: 'orcid-account'; - accountId: bigint; + type: 'orcid-account'; + accountId: bigint; }; +type SdkReceiverWithOrcid = SdkReceiver | SdkOrcidReceiver; + const WAITING_WALLET_ICON = { component: 'Emoji', props: { diff --git a/src/lib/flows/create-drip-list-flow/steps/review-voting-round/review-voting-round.svelte b/src/lib/flows/create-drip-list-flow/steps/review-voting-round/review-voting-round.svelte index 801871391..bdaf9900c 100644 --- a/src/lib/flows/create-drip-list-flow/steps/review-voting-round/review-voting-round.svelte +++ b/src/lib/flows/create-drip-list-flow/steps/review-voting-round/review-voting-round.svelte @@ -23,6 +23,7 @@ import WhatsNextSection from '$lib/components/whats-next/whats-next-section.svelte'; import WhatsNextCard from '$lib/components/whats-next/whats-next-card.svelte'; import WhatsNextItem from '$lib/components/whats-next/whats-next-item.svelte'; + import getLastPathSegment from '$lib/utils/get-last-path-segment'; const dispatch = createEventDispatcher(); diff --git a/src/lib/flows/edit-drip-list/fragments.ts b/src/lib/flows/edit-drip-list/fragments.ts index 4dd3be477..11981ef37 100644 --- a/src/lib/flows/edit-drip-list/fragments.ts +++ b/src/lib/flows/edit-drip-list/fragments.ts @@ -3,10 +3,7 @@ import { SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_ADDRESS_RECEIVER_FRAGMENT, SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_DRIP_LIST_RECEIVER_FRAGMENT, SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_PROJECT_RECEIVER_FRAGMENT, -<<<<<<< HEAD SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_ORCID_RECEIVER_FRAGMENT, -======= ->>>>>>> c87ec7ab (Initial support for ORCID iDs in list editor; fix circular import of EDIT_DRIP_LIST_FLOW_DRIP_LIST_FRAGMENT) } from '$lib/components/list-editor/utils/split-receivers-to-list-editor-config'; import { DRIP_LIST_BADGE_FRAGMENT } from '$lib/components/drip-list-badge/drip-list-badge.svelte'; @@ -14,10 +11,7 @@ export const EDIT_DRIP_LIST_FLOW_DRIP_LIST_FRAGMENT = gql` ${SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_ADDRESS_RECEIVER_FRAGMENT} ${SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_DRIP_LIST_RECEIVER_FRAGMENT} ${SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_PROJECT_RECEIVER_FRAGMENT} -<<<<<<< HEAD ${SPLIT_RECEIVERS_TO_LIST_EDITOR_CONFIG_ORCID_RECEIVER_FRAGMENT} -======= ->>>>>>> c87ec7ab (Initial support for ORCID iDs in list editor; fix circular import of EDIT_DRIP_LIST_FLOW_DRIP_LIST_FRAGMENT) fragment EditDripListFlowDripList on DripList { name description @@ -35,12 +29,9 @@ export const EDIT_DRIP_LIST_FLOW_DRIP_LIST_FRAGMENT = gql` ... on ProjectReceiver { ...SplitReceiversToListEditorConfigProjectReceiver } -<<<<<<< HEAD ... on LinkedIdentityReceiver { ...SplitReceiversToListEditorConfigOrcidReceiver } -======= ->>>>>>> c87ec7ab (Initial support for ORCID iDs in list editor; fix circular import of EDIT_DRIP_LIST_FLOW_DRIP_LIST_FRAGMENT) } } `; @@ -77,14 +68,11 @@ export const SELECT_DRIP_LIST_STEP_LISTS_FRAGMENT = gql` accountId } } -<<<<<<< HEAD ... on LinkedIdentityReceiver { account { accountId } } -======= ->>>>>>> c87ec7ab (Initial support for ORCID iDs in list editor; fix circular import of EDIT_DRIP_LIST_FLOW_DRIP_LIST_FRAGMENT) } } `; @@ -107,7 +95,6 @@ export const SELECT_DRIP_LIST_DRIP_LIST_TO_ADD_FRAGMENT = gql` } } `; -<<<<<<< HEAD export const SELECT_DRIP_LIST_ORCID_TO_ADD_FRAGMENT = gql` fragment SelectDripListOrcidToAdd on OrcidLinkedIdentity { @@ -116,5 +103,3 @@ export const SELECT_DRIP_LIST_ORCID_TO_ADD_FRAGMENT = gql` } } `; -======= ->>>>>>> c87ec7ab (Initial support for ORCID iDs in list editor; fix circular import of EDIT_DRIP_LIST_FLOW_DRIP_LIST_FRAGMENT) 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 a7f5ba110..82e3d4953 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 @@ -154,7 +154,7 @@ From 944392cc1357f66f75192e96ebdf6ebea8db3800 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Tue, 26 Aug 2025 19:22:36 +0200 Subject: [PATCH 09/31] Allow app host with local development --- .../(pages)/app/(app)/orcids/[orcidId]/+page.server.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 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 a4e948984..c976beb4f 100644 --- a/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts +++ b/src/routes/(pages)/app/(app)/orcids/[orcidId]/+page.server.ts @@ -4,6 +4,10 @@ import { fetchOrcid, fetchOrcidAccount } from '../../../../../../lib/utils/orcid import Orcid from '$lib/utils/orcids/entities'; import isValidOrcidId from '$lib/utils/orcids/is-valid-orcid-id'; +/** + * 0009-0007-5482-8654 me in ORCID prod + * 0009-0007-1106-8413 drips.network in ORCID sandbox + */ export const load = (async ({ params, fetch }) => { if (!isValidOrcidId(params.orcidId)) { return error(400, 'Invalid ORCID iD'); @@ -46,6 +50,3 @@ export const load = (async ({ params, fetch }) => { orcidAccount, }; }) satisfies PageServerLoad; - -// 0009-0007-5482-8654 me in prod -// 0009-0007-1106-8413 drips.network in sandbox From 639fc2a2c319aba6e05618a9d39997f14620b4a1 Mon Sep 17 00:00:00 2001 From: Georgios Jason Efstathiou Date: Thu, 28 Aug 2025 11:16:14 +0200 Subject: [PATCH 10/31] blueprints (wip) --- src/lib/components/list-editor/hydrators.ts | 20 +-- src/lib/components/list-editor/types.ts | 4 +- .../progress-bar/progress-bar.svelte | 16 +- .../stepper/components/await-step.svelte | 28 +++ src/lib/components/stepper/types.ts | 9 + .../create-drip-list-flow.ts | 27 ++- .../create-drip-list-stepper.svelte | 42 ++++- .../choose-creation-mode.svelte | 102 ++++++----- .../populate-blueprint.svelte | 159 ++++++++++++++++++ .../sdk/address-driver/calc-account-id.ts | 19 +++ .../(flows)/funder-onboarding/+page.server.ts | 34 ++++ .../(flows)/funder-onboarding/+page.svelte | 4 +- src/routes/api/list-blueprints/+server.ts | 33 ++++ .../list-blueprints/[blueprintId]/+server.ts | 28 +++ .../api/list-blueprints/blueprintSchema.ts | 43 +++++ 15 files changed, 498 insertions(+), 70 deletions(-) create mode 100644 src/lib/flows/create-drip-list-flow/steps/populate-blueprint/populate-blueprint.svelte create mode 100644 src/lib/utils/sdk/address-driver/calc-account-id.ts create mode 100644 src/routes/(pages)/app/(flows)/funder-onboarding/+page.server.ts create mode 100644 src/routes/api/list-blueprints/+server.ts create mode 100644 src/routes/api/list-blueprints/[blueprintId]/+server.ts create mode 100644 src/routes/api/list-blueprints/blueprintSchema.ts diff --git a/src/lib/components/list-editor/hydrators.ts b/src/lib/components/list-editor/hydrators.ts index 7c4b32923..35cdd0226 100644 --- a/src/lib/components/list-editor/hydrators.ts +++ b/src/lib/components/list-editor/hydrators.ts @@ -14,9 +14,9 @@ import type { GetOrcidQuery, GetOrcidQueryVariables, } from './__generated__/gql.generated'; -import { isAddress } from 'ethers'; import network from '$lib/stores/wallet/network'; import { fetchOrcid, orcidIdToAccountId } from '../../utils/orcids/fetch-orcid'; +import { calcAccountId } from '$lib/utils/sdk/address-driver/calc-account-id'; export const getDripList = async (dripListId: string): Promise => { const res = await query( @@ -117,24 +117,6 @@ export const getOrcid = async (orcidId: string): Promise => { }; }; -function calcAccountId(addr: string): bigint { - if (!isAddress(addr)) { - throw new Error('Invalid Ethereum address format'); - } - - const driverId = 0; - - const addrBigInt = BigInt(addr); - - // Shift left by 224 bits to make space for the address - let accountId = BigInt(driverId) << 224n; - - // Combine the shifted driverId and the address BigInt - accountId |= addrBigInt; - - return accountId; -} - export const getAddress = async (address: string): Promise => { try { const accountId = String(calcAccountId(address)); diff --git a/src/lib/components/list-editor/types.ts b/src/lib/components/list-editor/types.ts index f5ffd07c8..0294c9e1f 100644 --- a/src/lib/components/list-editor/types.ts +++ b/src/lib/components/list-editor/types.ts @@ -60,7 +60,7 @@ type BaseItem = { }; }; -type ProjectItem = BaseItem & { +export type ProjectItem = BaseItem & { type: 'project'; project: ListEditorProjectFragment; }; @@ -70,7 +70,7 @@ export type DripListItem = BaseItem & { dripList: ListEditorDripListFragment; }; -type EthAddressItem = BaseItem & { +export type EthAddressItem = BaseItem & { type: 'address'; address: string; }; diff --git a/src/lib/components/progress-bar/progress-bar.svelte b/src/lib/components/progress-bar/progress-bar.svelte index 98a6488f0..603345270 100644 --- a/src/lib/components/progress-bar/progress-bar.svelte +++ b/src/lib/components/progress-bar/progress-bar.svelte @@ -14,9 +14,14 @@ progressFn: ProgressFn; updateFrequencyMs?: number; errorMessage?: string | undefined; + centeredProgressText?: boolean; } + // export let progressFn: ProgressFn; + // export let updateFrequencyMs = 10; + // export let errorMessage: string | undefined = undefined; + // export let centeredProgressText = true; - let { progressFn, updateFrequencyMs = 10, errorMessage = undefined }: Props = $props(); + let { progressFn, updateFrequencyMs = 10, errorMessage = undefined, centeredProgressText = true }: Props = $props(); let interval: ReturnType | undefined = $state(); @@ -86,13 +91,18 @@ {/if} + {#if remainingText || done}

.progress-bar-wrapper { + min-width: 16rem; color: var(--color-foreground-level-6); + display: flex; + flex-direction: column; + justify-content: center; } .progress-bar-container { diff --git a/src/lib/components/stepper/components/await-step.svelte b/src/lib/components/stepper/components/await-step.svelte index bfb1abd14..0204a436a 100644 --- a/src/lib/components/stepper/components/await-step.svelte +++ b/src/lib/components/stepper/components/await-step.svelte @@ -16,6 +16,8 @@ import { createEventDispatcher, onMount, type Component } from 'svelte'; import type { UpdateAwaitStepFn } from '../types'; import { isHttpError } from '@sveltejs/kit'; + import type { ProgressFn } from '$lib/components/progress-bar/progress-bar.svelte'; + import ProgressBar from '$lib/components/progress-bar/progress-bar.svelte'; const dispatch = createEventDispatcher<{ result: Result }>(); @@ -25,6 +27,10 @@ link?: { url: string; label: string } | undefined; icon?: { component: Component; props: Record } | undefined; promise: (updateFn: UpdateAwaitStepFn) => Promise; + progressBar?: { + progressFn: ProgressFn; + centeredProgressText?: boolean; + } | undefined; } let { @@ -33,8 +39,25 @@ link = $bindable(), icon = $bindable(), promise, + progressBar = $bindable(), }: Props = $props(); + + const dispatch = createEventDispatcher<{ result: Result }>(); + + // export let message: string; + // export let subtitle: string | undefined = undefined; + // export let link: { url: string; label: string } | undefined = undefined; + // export let icon: { component: ComponentType; props: Record } | undefined = + // undefined; + // export let promise: (updateFn: UpdateAwaitStepFn) => Promise; + // export let progressBar: + // | { + // progressFn: ProgressFn; + // centeredProgressText?: boolean; + // } + // | undefined = undefined; + const updateFn: UpdateAwaitStepFn = (params) => { message = params.message ?? message; subtitle = params.subtitle; @@ -87,6 +110,11 @@ {/if}

{message}

{#if subtitle}

{subtitle}

{/if} + + {#if progressBar} + + {/if} + {#if link} {link.label} {/if} diff --git a/src/lib/components/stepper/types.ts b/src/lib/components/stepper/types.ts index d0171b65c..9d5ba1eb5 100644 --- a/src/lib/components/stepper/types.ts +++ b/src/lib/components/stepper/types.ts @@ -3,6 +3,7 @@ import type { SendTransactionsResponse } from '@safe-global/safe-apps-sdk'; import type { TransactionLike, TypedDataDomain, TypedDataField } from 'ethers'; import type { TransactionReceipt } from 'ethers'; import type { Component, ComponentProps } from 'svelte'; +import type { ProgressFn } from '../progress-bar/progress-bar.svelte'; export type TransactionWrapper = { title: string; @@ -93,6 +94,14 @@ export interface AwaitPendingPayload extends UpdateAwaitStepParams { * and text displayed on the await step before the promise resolves. */ promise: (updateFn: UpdateAwaitStepFn) => Promise; + /** + * Optional function to report progress of the awaited promise. + * If provided, a progress bar is shown below the message. + */ + progressBar?: { + progressFn: ProgressFn; + centeredProgressText?: boolean; + }; } export interface MovePayload { diff --git a/src/lib/flows/create-drip-list-flow/create-drip-list-flow.ts b/src/lib/flows/create-drip-list-flow/create-drip-list-flow.ts index bc9d53c58..a5b048385 100644 --- a/src/lib/flows/create-drip-list-flow/create-drip-list-flow.ts +++ b/src/lib/flows/create-drip-list-flow/create-drip-list-flow.ts @@ -13,6 +13,8 @@ import type { AddItemError } from '$lib/components/list-editor/errors'; import walletStore from '$lib/stores/wallet/wallet.store'; import dismissablesStore from '$lib/stores/dismissables/dismissables.store'; import DripList from '$lib/components/illustrations/drip-list.svelte'; +import type { Blueprint } from '../../../routes/api/list-blueprints/blueprintSchema'; +import PopulateBlueprint from './steps/populate-blueprint/populate-blueprint.svelte'; export interface State { dripList: DripListConfig; @@ -53,11 +55,34 @@ const staticHeaderComponent = { }, }; -export const steps = (state: Writable, skipWalletConnect = false, isModal = false) => [ +export const steps = ( + state: Writable, + skipWalletConnect = false, + isModal = false, + blueprintOrBlueprintError: + | { + blueprintError: 'not-found' | 'unknown' | 'invalid' | undefined; + } + | { + blueprint: Blueprint; + } + | undefined, +) => [ + ...(blueprintOrBlueprintError + ? [ + makeStep({ + component: PopulateBlueprint, + props: { + blueprintOrBlueprintError, + }, + }), + ] + : []), makeStep({ component: ChooseCreationMode, props: { canCancel: isModal, + blueprintMode: !!blueprintOrBlueprintError, }, staticHeaderComponent, }), diff --git a/src/lib/flows/create-drip-list-flow/create-drip-list-stepper.svelte b/src/lib/flows/create-drip-list-flow/create-drip-list-stepper.svelte index 8088b0cee..97c1e47ac 100644 --- a/src/lib/flows/create-drip-list-flow/create-drip-list-stepper.svelte +++ b/src/lib/flows/create-drip-list-flow/create-drip-list-stepper.svelte @@ -1,13 +1,49 @@ diff --git a/src/lib/utils/sdk/address-driver/calc-account-id.ts b/src/lib/utils/sdk/address-driver/calc-account-id.ts new file mode 100644 index 000000000..9d7ea7556 --- /dev/null +++ b/src/lib/utils/sdk/address-driver/calc-account-id.ts @@ -0,0 +1,19 @@ +import { isAddress } from 'ethers'; + +export function calcAccountId(addr: string): bigint { + if (!isAddress(addr)) { + throw new Error('Invalid Ethereum address format'); + } + + const driverId = 0; + + const addrBigInt = BigInt(addr); + + // Shift left by 224 bits to make space for the address + let accountId = BigInt(driverId) << 224n; + + // Combine the shifted driverId and the address BigInt + accountId |= addrBigInt; + + return accountId; +} diff --git a/src/routes/(pages)/app/(flows)/funder-onboarding/+page.server.ts b/src/routes/(pages)/app/(flows)/funder-onboarding/+page.server.ts new file mode 100644 index 000000000..3e5cceeb8 --- /dev/null +++ b/src/routes/(pages)/app/(flows)/funder-onboarding/+page.server.ts @@ -0,0 +1,34 @@ +import type z from 'zod'; +import { blueprintSchema } from '../../../../api/list-blueprints/blueprintSchema.js'; + +export const load = async ({ fetch, url }) => { + const blueprintIdParam = url.searchParams.get('blueprintId'); + + let blueprint: z.infer | undefined = undefined; + let blueprintError: 'not-found' | 'unknown' | 'invalid' | undefined = undefined; + + if (blueprintIdParam) { + const blueprintResponse = await fetch(`/api/list-blueprints/${blueprintIdParam}`); + + if (!blueprintResponse.ok) { + blueprintError = blueprintResponse.status === 404 ? 'not-found' : 'unknown'; + } + + const asJson = await blueprintResponse.json().catch(() => null); + const parsedBlueprint = blueprintSchema.safeParse(asJson); + + if (!parsedBlueprint.success) { + blueprintError = 'invalid'; + } else { + blueprint = parsedBlueprint.data; + } + } + + return { + blueprintOrBlueprintError: blueprintError + ? { blueprintError: blueprintError } + : blueprint + ? { blueprint } + : undefined, + }; +}; diff --git a/src/routes/(pages)/app/(flows)/funder-onboarding/+page.svelte b/src/routes/(pages)/app/(flows)/funder-onboarding/+page.svelte index c84b71670..7e9507a4a 100644 --- a/src/routes/(pages)/app/(flows)/funder-onboarding/+page.svelte +++ b/src/routes/(pages)/app/(flows)/funder-onboarding/+page.svelte @@ -4,10 +4,12 @@ import HeadMeta from '$lib/components/head-meta/head-meta.svelte'; import CreateDripListStepper from '$lib/flows/create-drip-list-flow/create-drip-list-stepper.svelte'; + export let data; + onMount(() => browser && (window.onbeforeunload = () => true)); onDestroy(() => browser && (window.onbeforeunload = null)); - + diff --git a/src/routes/api/list-blueprints/+server.ts b/src/routes/api/list-blueprints/+server.ts new file mode 100644 index 000000000..74dca3259 --- /dev/null +++ b/src/routes/api/list-blueprints/+server.ts @@ -0,0 +1,33 @@ +/* +"List Blueprints" allow submitting a list of splits in return for a "blueprint ID". +Then, the user can be sent off to /app/funder-onboarding?blueprintId=XYZ, which will retrieve +the blueprint and pre-fill the splits in the onboarding flow. + +We store blueprints on Redis with a short TTL of 6 hours, since they are meant to be +short-lived and temporary. +*/ + +import { error } from '@sveltejs/kit'; +import { redis } from '../redis'; +import network from '$lib/stores/wallet/network'; +import { blueprintSchema } from './blueprintSchema'; + +export const PUT = async ({ request }) => { + if (!redis) return error(503, 'Redis not available'); + + const body = await request.json(); + + const parsed = blueprintSchema.safeParse(body); + + if (!parsed.success) { + throw error(400, parsed.error); + } + + const id = crypto.randomUUID(); + + await redis.set(`list-blueprint:${network.chainId}:${id}`, JSON.stringify(parsed.data), { + EX: 60 * 60 * 6, // 6 hours + }); + + return new Response(JSON.stringify({ id, expiresAt: Date.now() + 60 * 60 * 6 * 1000 })); +}; diff --git a/src/routes/api/list-blueprints/[blueprintId]/+server.ts b/src/routes/api/list-blueprints/[blueprintId]/+server.ts new file mode 100644 index 000000000..6e82db020 --- /dev/null +++ b/src/routes/api/list-blueprints/[blueprintId]/+server.ts @@ -0,0 +1,28 @@ +import { error } from '@sveltejs/kit'; +import { redis } from '../../redis'; +import network from '$lib/stores/wallet/network'; +import { blueprintSchema } from '../blueprintSchema'; + +export const GET = async ({ params }) => { + if (!redis) return error(503, 'Redis not available'); + + const { blueprintId: id } = params; + + const data = await redis.get(`list-blueprint:${network.chainId}:${id}`); + if (!data) { + throw error(404, 'Blueprint not found or expired'); + } + + try { + const asJson = JSON.parse(data); + const parsed = blueprintSchema.safeParse(asJson); + + if (!parsed.success) { + throw error(500, 'Invalid blueprint stored'); + } + + return new Response(JSON.stringify(parsed.data)); + } catch { + throw error(500, 'Failed to parse blueprint'); + } +}; diff --git a/src/routes/api/list-blueprints/blueprintSchema.ts b/src/routes/api/list-blueprints/blueprintSchema.ts new file mode 100644 index 000000000..d2a7cd271 --- /dev/null +++ b/src/routes/api/list-blueprints/blueprintSchema.ts @@ -0,0 +1,43 @@ +import z from 'zod'; + +const MAX_SPLITS_WEIGHT = 1000000; + +const addressReceiver = z.object({ + type: z.literal('address'), + ethAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/), + weight: z.number().min(0).max(MAX_SPLITS_WEIGHT), +}); + +const projectReceiver = z.object({ + type: z.literal('project'), + // string with repoOwner/repoName + repoName: z.string().regex(/^[^/]+\/[^/]+$/), + weight: z.number().min(0).max(MAX_SPLITS_WEIGHT), +}); + +const dripListReceiver = z.object({ + type: z.literal('drip-list'), + accountId: z.string().regex(/^0x[a-fA-F0-9]{40}$/), + weight: z.number().min(0).max(MAX_SPLITS_WEIGHT), +}); + +const orcidIdReceiver = z.object({ + type: z.literal('orcid-id'), + orcidId: z.string().regex(/^(\d{4}-){3}\d{3}(\d|X)$/), + weight: z.number().min(0).max(MAX_SPLITS_WEIGHT), +}); + +const splitsSchema = z.union([addressReceiver, projectReceiver, dripListReceiver, orcidIdReceiver]); + +const blueprintSchema = z.object({ + listName: z.string().min(1).max(200), + listDescription: z.string().max(1000).optional(), + splits: z.array(splitsSchema).min(1).max(200), +}); + +export type Split = z.infer; +export type AddressReceiver = z.infer; +export type ProjectReceiver = z.infer; +export type DripListReceiver = z.infer; +export type Blueprint = z.infer; +export { blueprintSchema }; From f0fda8836456f1e8f1b9e56c5a44b33c27f6ec82 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Thu, 28 Aug 2025 10:40:29 -0400 Subject: [PATCH 11/31] Adjust e2e temp --- docker/start-e2e.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/start-e2e.sh b/docker/start-e2e.sh index 2a49f73e5..148d3fd05 100755 --- a/docker/start-e2e.sh +++ b/docker/start-e2e.sh @@ -39,9 +39,9 @@ export LOCAL_GID=$(id -g) # TODO: REMOVE! if [ $PROD_BUILD = true ]; then - 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 + docker compose build && APP_USE_LOCAL_TESTNET_WALLET_STORE=true CONTRACTS_TAG=igor-orcid 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 GRAPHQL_API_TAG=jason-ecosystems EVENT_PROCESSOR_TAG=ecosystems docker compose -f docker-compose.yml up --renew-anon-volumes --detach + docker compose build && APP_USE_LOCAL_TESTNET_WALLET_STORE=true CONTRACTS_TAG=igor-orcid docker compose -f docker-compose.yml up --renew-anon-volumes --detach fi rm -rf ./test-data/project-states.json From aa1e398f2df24b57481f142b87d55a77489f854e Mon Sep 17 00:00:00 2001 From: Georgios Jason Efstathiou Date: Fri, 12 Sep 2025 10:09:45 +0200 Subject: [PATCH 12/31] temporarily disable ownership check --- .../steps/add-ethereum-address/add-ethereum-address.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 2e6f171be..e7a163dfd 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 @@ -65,7 +65,8 @@ const { address, dripsAccountId } = $walletStore; assert(address && dripsAccountId); - await verifyOrcidClaim($context.claimableId, address); + // todo: TEMPORARYILY DISABLED FOR TESTING + // await verifyOrcidClaim($context.claimableId, address); $context.linkedToClaimable = true; From 22ab1423864722dfb0d597241ca69867fff1eb2b Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Tue, 16 Dec 2025 16:37:59 +0100 Subject: [PATCH 13/31] Resolves problems raised during execution of dev environment --- .../stepper/components/await-step.svelte | 14 +++-- .../create-drip-list-stepper.svelte | 21 ++----- .../[orcidId]/components/fetch-orcid.ts | 58 ------------------- 3 files changed, 12 insertions(+), 81 deletions(-) delete mode 100644 src/routes/(pages)/app/(app)/orcids/[orcidId]/components/fetch-orcid.ts diff --git a/src/lib/components/stepper/components/await-step.svelte b/src/lib/components/stepper/components/await-step.svelte index 0204a436a..10463bc7e 100644 --- a/src/lib/components/stepper/components/await-step.svelte +++ b/src/lib/components/stepper/components/await-step.svelte @@ -27,10 +27,12 @@ link?: { url: string; label: string } | undefined; icon?: { component: Component; props: Record } | undefined; promise: (updateFn: UpdateAwaitStepFn) => Promise; - progressBar?: { - progressFn: ProgressFn; - centeredProgressText?: boolean; - } | undefined; + progressBar?: + | { + progressFn: ProgressFn; + centeredProgressText?: boolean; + } + | undefined; } let { @@ -42,8 +44,7 @@ progressBar = $bindable(), }: Props = $props(); - - const dispatch = createEventDispatcher<{ result: Result }>(); + // const dispatch = createEventDispatcher<{ result: Result }>(); // export let message: string; // export let subtitle: string | undefined = undefined; @@ -116,6 +117,7 @@ {/if} {#if link} + {link.label} {/if} diff --git a/src/lib/flows/create-drip-list-flow/create-drip-list-stepper.svelte b/src/lib/flows/create-drip-list-flow/create-drip-list-stepper.svelte index 97c1e47ac..41f518557 100644 --- a/src/lib/flows/create-drip-list-flow/create-drip-list-stepper.svelte +++ b/src/lib/flows/create-drip-list-flow/create-drip-list-stepper.svelte @@ -1,15 +1,11 @@ - - - {#snippet left_actions()} - - {/snippet} - - {#snippet actions()} - - {/snippet} - From ed5e50c98d26d1278ff3479c756d56ac36a2d3ea Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Thu, 18 Dec 2025 16:10:22 +0000 Subject: [PATCH 26/31] Update src/routes/api/list-blueprints/+server.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/routes/api/list-blueprints/+server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/api/list-blueprints/+server.ts b/src/routes/api/list-blueprints/+server.ts index 88d450793..189ef5e0b 100644 --- a/src/routes/api/list-blueprints/+server.ts +++ b/src/routes/api/list-blueprints/+server.ts @@ -1,11 +1,11 @@ -/* * +/** * "List Blueprints" allow submitting a list of splits in return for a "blueprint ID". * Then, the user can be sent off to /app/funder-onboarding?blueprintId=XYZ, which will retrieve * the blueprint and pre-fill the splits in the onboarding flow. * * We store blueprints on Redis with a short TTL of 6 hours, since they are meant to be * short-lived and temporary. - * */ + */ import { error } from '@sveltejs/kit'; import { redis } from '../redis'; From f433977f63986441bfc8e1cbe0b63cd8e4461dff Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Thu, 18 Dec 2025 16:14:14 +0000 Subject: [PATCH 27/31] Update src/routes/api/list-blueprints/[blueprintId]/+server.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../list-blueprints/[blueprintId]/+server.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/routes/api/list-blueprints/[blueprintId]/+server.ts b/src/routes/api/list-blueprints/[blueprintId]/+server.ts index dd0f7a99b..de9094a46 100644 --- a/src/routes/api/list-blueprints/[blueprintId]/+server.ts +++ b/src/routes/api/list-blueprints/[blueprintId]/+server.ts @@ -13,16 +13,18 @@ export const GET = async ({ params }) => { throw error(404, 'Blueprint not found or expired'); } + let asJson: unknown; try { - const asJson = JSON.parse(data); - const parsed = blueprintSchema.safeParse(asJson); - - if (!parsed.success) { - throw error(500, 'Invalid blueprint stored'); - } - - return new Response(JSON.stringify(parsed.data)); + asJson = JSON.parse(data); } catch { throw error(500, 'Failed to parse blueprint'); } + + const parsed = blueprintSchema.safeParse(asJson); + + if (!parsed.success) { + throw error(500, 'Invalid blueprint stored'); + } + + return new Response(JSON.stringify(parsed.data)); }; From 4397c8557ef0a76e074db1e52d483b40e65aa9c0 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Thu, 18 Dec 2025 16:14:54 +0000 Subject: [PATCH 28/31] Update src/routes/(pages)/app/(flows)/funder-onboarding/+page.server.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../(flows)/funder-onboarding/+page.server.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/routes/(pages)/app/(flows)/funder-onboarding/+page.server.ts b/src/routes/(pages)/app/(flows)/funder-onboarding/+page.server.ts index 42e5467de..08cb4e76b 100644 --- a/src/routes/(pages)/app/(flows)/funder-onboarding/+page.server.ts +++ b/src/routes/(pages)/app/(flows)/funder-onboarding/+page.server.ts @@ -12,15 +12,15 @@ export const load = async ({ fetch, url }) => { if (!blueprintResponse.ok) { blueprintError = blueprintResponse.status === 404 ? 'not-found' : 'unknown'; - } - - const asJson = await blueprintResponse.json().catch(() => null); - const parsedBlueprint = blueprintSchema.safeParse(asJson); - - if (!parsedBlueprint.success) { - blueprintError = 'invalid'; } else { - blueprint = parsedBlueprint.data; + const asJson = await blueprintResponse.json().catch(() => null); + const parsedBlueprint = blueprintSchema.safeParse(asJson); + + if (!parsedBlueprint.success) { + blueprintError = 'invalid'; + } else { + blueprint = parsedBlueprint.data; + } } } From 52ec249fce62056c83c8c01637e806d5faebaec8 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Thu, 18 Dec 2025 17:27:31 +0100 Subject: [PATCH 29/31] Add errors when we encounter them in the blueprint --- .../populate-blueprint.svelte | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/lib/flows/create-drip-list-flow/steps/populate-blueprint/populate-blueprint.svelte b/src/lib/flows/create-drip-list-flow/steps/populate-blueprint/populate-blueprint.svelte index 0b57aa89d..e03e4923d 100644 --- a/src/lib/flows/create-drip-list-flow/steps/populate-blueprint/populate-blueprint.svelte +++ b/src/lib/flows/create-drip-list-flow/steps/populate-blueprint/populate-blueprint.svelte @@ -11,6 +11,7 @@ import type { StepComponentEvents } from '$lib/components/stepper/types'; import type { Writable } from 'svelte/store'; import type { State } from '../../create-drip-list-flow'; + import { AddItemError, AddItemSuberror } from '$lib/components/list-editor/errors'; const dispatch = createEventDispatcher(); @@ -77,7 +78,9 @@ let splitsToAdd: Items = {}; let weightsToAdd: Weights = {}; - for (const split of blueprintSplits) { + let errors: Array = []; + + for (const [index, split] of blueprintSplits.entries()) { switch (split.type) { case 'address': { const recipientResult = await getAddress(split.ethAddress); @@ -89,6 +92,13 @@ }; weightsToAdd[recipientResult.accountId] = split.weight; + } else { + const error = new AddItemSuberror( + "We couldn't process this address.", + split.ethAddress, + index + 1, + ); + errors.push(error); } break; @@ -104,7 +114,15 @@ }; weightsToAdd[recipientResult.accountId] = split.weight; + } else { + const error = new AddItemSuberror( + "We couldn't process this project.", + split.repoName, + index + 1, + ); + errors.push(error); } + break; } @@ -118,6 +136,13 @@ }; weightsToAdd[recipientResult.accountId] = split.weight; + } else { + const error = new AddItemSuberror( + "We couldn't process this drip list.", + split.accountId, + index + 1, + ); + errors.push(error); } break; @@ -134,6 +159,13 @@ }; weightsToAdd[recipientResult.accountId] = split.weight; + } else { + const error = new AddItemSuberror( + "We couldn't process this ORCID iD.", + split.orcidId, + index + 1, + ); + errors.push(error); } } break; @@ -142,6 +174,19 @@ splitsProcessed++; } + if (errors.length) { + const recipientError = new AddItemError( + 'Some of your blueprint recipients were invalid', + 'error', + 'They won’t be included in your splits.', + errors, + ); + context.update((c) => { + c.recipientErrors = [recipientError]; + return c; + }); + } + $context.dripList.items = splitsToAdd; $context.dripList.weights = weightsToAdd; $context.dripList.title = listName; From b12200639c115aa2a28332340eb00a4368c8a61c Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Thu, 18 Dec 2025 17:54:25 +0100 Subject: [PATCH 30/31] Add errors for invalid blueprint recipients; DRY recipient addition logic --- .../populate-blueprint.svelte | 167 +++++++++--------- tests/create-drip-list.spec.ts | 2 + tests/payloads/create-blueprint-payload.json | 5 + 3 files changed, 93 insertions(+), 81 deletions(-) diff --git a/src/lib/flows/create-drip-list-flow/steps/populate-blueprint/populate-blueprint.svelte b/src/lib/flows/create-drip-list-flow/steps/populate-blueprint/populate-blueprint.svelte index e03e4923d..5a13de797 100644 --- a/src/lib/flows/create-drip-list-flow/steps/populate-blueprint/populate-blueprint.svelte +++ b/src/lib/flows/create-drip-list-flow/steps/populate-blueprint/populate-blueprint.svelte @@ -1,7 +1,7 @@