Skip to content
Draft
5 changes: 5 additions & 0 deletions docker/mine-block.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

printf "Mining a block on the local Anvil chain...\n\n"

exec docker run --rm --network host --entrypoint cast ghcr.io/foundry-rs/foundry:latest rpc evm_mine --rpc-url http://127.0.0.1:8545
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dev:docker": "./docker/start-dev.sh",
"dev:docker:clear": "docker compose -f docker-compose.yml -f docker-compose.dev.yml down -v",
"dev:docker:update-repo-owner": "./docker/update-repo-owner.sh",
"dev:docker:mine-block": "./docker/mine-block.sh",
"dev:docker:sprinkle": "./docker/sprinkle.sh",
"preview": "vite preview",
"test:unit": "PUBLIC_NETWORK=1 PUBLIC_JUNCTION_URL=http://dummy.org PUBLIC_INTERNAL_JUNCTION_URL=http://dummy.org vitest run unit --mode=unit-test",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@
});

if (!res.ok) {
throw new Error('Failed to get ownership signature from Lit. Please try again later.');
throw new Error(
'Failed to get an ownership signature from Lit Protocol. There may be a temporary outage – Please try again later, and reach out to Drips if the issue persists.',
);
}

$context.litOwnerUpdateSignature = await res.json();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
CallerERC2771Domain,
CallSignedERC2771Types,
getCallerNonce,
populateCallerWriteTx,
} from '$lib/utils/sdk/caller/caller';
import txToCallerCall from '$lib/utils/sdk/utils/tx-to-caller-call';
import gaslessStore from '$lib/stores/gasless/gasless.store';
import { orcidIdToSandoxOrcidId } from '$lib/utils/orcids/fetch-orcid';
import {
Expand Down Expand Up @@ -202,10 +204,35 @@
);
}

const callerTx = await populateCallerWriteTx({
functionName: 'callBatched',
args: [[txToCallerCall(updateOwnerByLitTx)]],
});

const { address } = $walletStore;
assert(address, 'Wallet address is not defined');

transactions.push(
{
title: 'Update ORCID iD owner',
transaction: updateOwnerByLitTx,
transaction: callerTx,
gasless: $gaslessStore
? {
nonceGetter: () => getCallerNonce(address),
ERC2771Data: (nonce) => ({
domain: CallerERC2771Domain,
types: CallSignedERC2771Types,
payload: {
sender: $walletStore.address,
target: callerTx.to,
data: callerTx.data,
value: '0',
nonce,
deadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour
},
}),
}
: undefined,
applyGasBuffer: false,
},
{
Expand Down
7 changes: 6 additions & 1 deletion src/lib/flows/claim-project-flow/claim-project-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface State {
linkedToRepo: boolean;
gitUrl: string;
isPartiallyClaimed: boolean;
isReclaiming: boolean;
project: ClaimProjectFlowProjectFragment | undefined;
projectMetadata:
| {
Expand Down Expand Up @@ -90,6 +91,7 @@ export interface State {
export const flowState = () =>
writable<State>({
isPartiallyClaimed: false,
isReclaiming: false,
linkedToRepo: false,
gitUrl: '',
project: undefined,
Expand Down Expand Up @@ -238,7 +240,10 @@ export const steps = (
makeStep({
component: SuccessStep,
props: {
message: 'Your project has been successfully claimed.',
message: () =>
get(state).isReclaiming
? 'Your project has been successfully re-claimed.'
: 'Your project has been successfully claimed.',
action: linkToProjectPageOnSuccess ? 'link' : 'close',
href() {
const context = get(state);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import { Octokit } from '@octokit/rest';
import { env } from '$env/dynamic/public';
import getLitChainName from '$lib/utils/lit/get-lit-chain-name';
import expect from '$lib/utils/expect';
import { populateRepoDriverWriteTx } from '$lib/utils/sdk/repo-driver/repo-driver';

const octokit = new Octokit();
const github = new GitHub(octokit);
Expand Down Expand Up @@ -103,7 +105,7 @@

if (env.PUBLIC_USE_LIT_OWNER_UPDATE === 'true') {
// Use Lit Protocol to get a verifiable ownership signature
const res = await fetch('/api/lit/owner-signature', {
const litRes = await fetch('/api/lit/owner-signature', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
Expand All @@ -113,11 +115,68 @@
}),
});

if (!res.ok) {
if (!litRes.ok) {
throw new Error('Failed to get ownership signature from Lit. Please try again later.');
}

$context.litOwnerUpdateSignature = await res.json();
const litSignature = await litRes.json();

// Build the tx to simulate against the chain before relaying,
// since the Lit signature timestamp may be ahead of the chain's latest block.
const updateOwnerByLitTx = await populateRepoDriverWriteTx({
functionName: 'updateOwnerByLit',
args: [
litSignature.sourceId,
litSignature.name as `0x${string}`,
litSignature.owner as `0x${string}`,
litSignature.timestamp,
litSignature.r as `0x${string}`,
litSignature.vs as `0x${string}`,
],
});

const simulationResult = await expect(
async () => {
try {
await $walletStore.provider.call({
to: updateOwnerByLitTx.to,
data: updateOwnerByLitTx.data,
});
return true;
} catch {
return false;
}
},
(result) => result === true,
60000,
3000,
);

if (simulationResult.failed) {
throw new Error(
'The owner update transaction simulation failed after multiple attempts. ' +
'The chain may not have caught up with the signature timestamp yet. Please try again.',
);
}

// Submit the Lit owner update gaslessly in the background via Gelato
const relayRes = await fetch('/api/gasless/call/lit-owner-update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...litSignature,
chainId: $walletStore.network.chainId,
}),
});

if (relayRes.ok) {
const { taskId } = await relayRes.json();
$context.gaslessOwnerUpdateTaskId = taskId === null ? undefined : taskId;
} else {
// If gasless relay fails, fall back to having the user submit via their wallet
$context.litOwnerUpdateSignature = litSignature;
}

return;
}

Expand Down Expand Up @@ -157,7 +216,7 @@
},
message: 'Verifying...',
subtitle:
'We’re scanning your repo’s main branch for a FUNDING.json file with your Ethereum address, and computing a cryptographic proof.',
'We’re scanning your repo’s main branch for a FUNDING.json file with your Ethereum address and computing a cryptographic proof of its contents. This may take up to a minute.',
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
tokenAddress
}
}
... on ClaimedProjectData {
owner {
address
}
verificationStatus
}
}
}
`;
Expand Down Expand Up @@ -165,6 +171,8 @@
}

async function fetchProject() {
$context.isReclaiming = false;
$context.isPartiallyClaimed = false;
$context.linkedToRepo = false;

try {
Expand Down Expand Up @@ -200,7 +208,17 @@
const projectChainData = filterCurrentChainData(project.chainData);

if (projectChainData.__typename === 'ClaimedProjectData') {
throw new InvalidUrlError('Project already claimed');
if (!$walletStore.connected) {
throw new InvalidUrlError(
'Project already claimed. To re-claim with a new wallet address, please first connect that wallet and try again.',
);
}

if (projectChainData.owner.address.toLowerCase() === $walletStore.address?.toLowerCase()) {
throw new InvalidUrlError('Project already claimed by your connected wallet');
}

$context.isReclaiming = true;
}
Comment thread
efstajas marked this conversation as resolved.

if (
Expand Down Expand Up @@ -239,6 +257,7 @@
$context.project = undefined;
$context.linkedToRepo = false;
$context.isPartiallyClaimed = false;
$context.isReclaiming = false;
$context.projectMetadata = undefined;

claimingRenamedRepoOriginalName = undefined;
Expand Down Expand Up @@ -281,8 +300,10 @@
</script>

<StandaloneFlowStepLayout
headline="Claim your project"
description="Enter your project’s GitHub URL to see if it has claimable funds and start the registration. Your repository must be public."
headline={$context.isReclaiming ? 'Re-claim your project' : 'Claim your project'}
description={$context.isReclaiming
? 'This project has already been claimed on Drips. You can re-claim it to update the owner to your currently-connected wallet.'
: 'Enter your project’s GitHub URL to see if it has claimable funds and start the registration. Your repository must be public.'}
>
<TextInput
bind:value={$context.gitUrl}
Expand All @@ -296,20 +317,27 @@
onpaste={onPaste}
/>
{#if $context.project && validationState.type === 'valid'}
<UnclaimedProjectCard
project={$context.project}
projectMetadata={$context.projectMetadata}
claimableTokensKey="Claimable tokens"
/>
{#if claimingRenamedRepoOriginalName}
<AnnotationBox>
You're claiming a project that has been renamed to {claimingRenamedRepoOriginalName.replace(
'https://github.com/',
'',
)} on GitHub. Please ensure that the repository URL you entered matches the old name of your
repo exactly (including casing), and validate that any funds you're expecting to claim are displayed
above.
{#if $context.isReclaiming}
<AnnotationBox type="info">
This project has already been claimed on Drips. You can re-claim it from your
currently-connected account by updating the FUNDING.json file in the repository.
</AnnotationBox>
{:else}
<UnclaimedProjectCard
project={$context.project}
projectMetadata={$context.projectMetadata}
claimableTokensKey="Claimable tokens"
/>
{#if claimingRenamedRepoOriginalName}
<AnnotationBox>
You're claiming a project that has been renamed to {claimingRenamedRepoOriginalName.replace(
'https://github.com/',
'',
)} on GitHub. Please ensure that the repository URL you entered matches the old name of your
repo exactly (including casing), and validate that any funds you're expecting to claim are
displayed above.
</AnnotationBox>
{/if}
{/if}
{/if}
{#snippet actions()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,18 @@
if (!res.projectByUrl?.chainData) return false;
const projectChainData = filterCurrentChainData(res.projectByUrl.chainData);

return (
projectChainData.verificationStatus === ProjectVerificationStatus.PendingMetadata &&
projectChainData.owner.address.toLowerCase() === $walletStore.address?.toLowerCase()
);
const isCorrectOwner =
projectChainData.owner.address.toLowerCase() === $walletStore.address?.toLowerCase();

// For fresh claims, the project transitions to PendingMetadata after the owner update.
// For re-claims of already-claimed projects, the project stays Claimed (since metadata
// already exists), but with the new owner.
const isExpectedStatus =
projectChainData.verificationStatus === ProjectVerificationStatus.PendingMetadata ||
($context.isReclaiming &&
projectChainData.verificationStatus === ProjectVerificationStatus.Claimed);

return isCorrectOwner && isExpectedStatus;
}

async function waitForRepoOwnerUpdate(gasless: boolean) {
Expand Down Expand Up @@ -218,10 +226,35 @@
);
}

const callerTx = await populateCallerWriteTx({
functionName: 'callBatched',
args: [[txToCallerCall(updateOwnerByLitTx)]],
});

const { address } = $walletStore;
assert(address, 'Wallet address is not defined');

transactions.push(
{
title: 'Update repository owner',
transaction: updateOwnerByLitTx,
transaction: callerTx,
gasless: $gaslessStore
? {
nonceGetter: () => getCallerNonce(address),
ERC2771Data: (nonce) => ({
domain: CallerERC2771Domain,
types: CallSignedERC2771Types,
payload: {
sender: $walletStore.address,
target: callerTx.to,
data: callerTx.data,
value: '0',
nonce,
deadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour
},
}),
}
: undefined,
applyGasBuffer: false,
},
{
Expand Down Expand Up @@ -286,7 +319,7 @@
dispatch(
'transact',
makeTransactPayload({
headline: 'Claim your project',
headline: $context.isReclaiming ? 'Re-claim your project' : 'Claim your project',

before: async () => {
const gitProjectService = await GitProjectService.new();
Expand Down
Loading
Loading