From 726147cf46cb53c3a6c9d88b79758e9da2d74645 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Mon, 10 Jul 2023 14:27:25 -0230 Subject: [PATCH 1/5] only hide project if the removal request is resolved --- .../src/OptimisticRecipientRegistryMapping.ts | 3 ++- .../src/api/recipient-registry-optimistic.ts | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/subgraph/src/OptimisticRecipientRegistryMapping.ts b/subgraph/src/OptimisticRecipientRegistryMapping.ts index feeb4e29f..f02ba444b 100644 --- a/subgraph/src/OptimisticRecipientRegistryMapping.ts +++ b/subgraph/src/OptimisticRecipientRegistryMapping.ts @@ -41,7 +41,8 @@ export function handleRequestResolved(event: RequestResolved): void { recipient.requester = event.transaction.from.toHexString() recipient.submissionTime = event.params._timestamp.toString() recipient.rejected = event.params._rejected - recipient.verified = !event.params._rejected + // verified means the request is resolved + recipient.verified = true recipient.recipientRegistry = recipientRegistryId recipient.recipientIndex = event.params._recipientIndex recipient.requestResolvedHash = event.transaction.hash diff --git a/vue-app/src/api/recipient-registry-optimistic.ts b/vue-app/src/api/recipient-registry-optimistic.ts index fcfd04a2c..a2493568b 100644 --- a/vue-app/src/api/recipient-registry-optimistic.ts +++ b/vue-app/src/api/recipient-registry-optimistic.ts @@ -245,14 +245,16 @@ export async function getProjects(registryAddress: string, startTime?: number, e } if (requestType === RequestTypeCode.Removal) { - const removedAt = submissionTime - if (!startTime || removedAt <= startTime) { - // Start time not specified - // or recipient had been removed before start time - project.isHidden = true - } else { - // Disallow contributions to removed recipient, but don't hide it - project.isLocked = true + if (recipient.verified) { + const removedAt = submissionTime + if (!startTime || removedAt <= startTime) { + // Start time not specified + // or recipient had been removed before start time + project.isHidden = true + } else { + // Disallow contributions to removed recipient, but don't hide it + project.isLocked = true + } } } From 36ae4ac7b70fead52aa388adba0b72aef4394990 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Thu, 13 Jul 2023 00:54:50 -0300 Subject: [PATCH 2/5] do not clear project index, metadata until removal is approved --- .../src/OptimisticRecipientRegistryMapping.ts | 41 ++++++++++++++----- .../src/api/recipient-registry-optimistic.ts | 19 +++++++-- vue-app/src/views/RecipientRegistry.vue | 6 +-- 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/subgraph/src/OptimisticRecipientRegistryMapping.ts b/subgraph/src/OptimisticRecipientRegistryMapping.ts index f02ba444b..506cc9c83 100644 --- a/subgraph/src/OptimisticRecipientRegistryMapping.ts +++ b/subgraph/src/OptimisticRecipientRegistryMapping.ts @@ -57,17 +57,36 @@ export function handleRequestSubmitted(event: RequestSubmitted): void { //TODO: create RecipientRegistry entity here if it does not exist. let recipientId = event.params._recipientId.toHexString() - let recipient = new Recipient(recipientId) - recipient.recipientRegistry = recipientRegistryId - recipient.recipientAddress = event.params._recipient - recipient.requestType = BigInt.fromI32(event.params._type).toString() - recipient.requester = event.transaction.from.toHexString() - recipient.submissionTime = event.params._timestamp.toString() - recipient.deposit = event.transaction.value - recipient.recipientMetadata = event.params._metadata - recipient.verified = false - recipient.requestSubmittedHash = event.transaction.hash + if (event.params._type == 0) { + // add recipient request + let recipient = new Recipient(recipientId) + recipient.recipientRegistry = recipientRegistryId + recipient.recipientAddress = event.params._recipient + recipient.requester = event.transaction.from.toHexString() + recipient.deposit = event.transaction.value + recipient.recipientMetadata = event.params._metadata - recipient.save() + // requestSubmittedHash stores the transaction hash for add recipient request + // the UI uses it to look up the newly added recipient record + recipient.requestSubmittedHash = event.transaction.hash + recipient.requestType = BigInt.fromI32(event.params._type).toString() + recipient.submissionTime = event.params._timestamp.toString() + recipient.verified = false + recipient.save() + } else { + // deleteRecipient request + // no need to update metadata and recipient address as they are not available for delete requests + let recipient = Recipient.load(recipientId) + if (recipient == null) { + // this should not happen, record the transaction hash for troubleshooting + recipient = new Recipient(recipientId) + recipient.requestSubmittedHash = event.transaction.hash + } + recipient.recipientRegistry = recipientRegistryId + recipient.requestType = BigInt.fromI32(event.params._type).toString() + recipient.submissionTime = event.params._timestamp.toString() + recipient.verified = false + recipient.save() + } } diff --git a/vue-app/src/api/recipient-registry-optimistic.ts b/vue-app/src/api/recipient-registry-optimistic.ts index a2493568b..6550092cf 100644 --- a/vue-app/src/api/recipient-registry-optimistic.ts +++ b/vue-app/src/api/recipient-registry-optimistic.ts @@ -52,6 +52,7 @@ export enum RequestStatus { Accepted = 'Accepted', Rejected = 'Rejected', Executed = 'Live', + PendingRemoval = 'Pending removal', Removed = 'Removed', } @@ -131,6 +132,10 @@ export async function getRequests(registryInfo: RegistryInfo, registryAddress: s if (recipient.verified) { request.status = requestType === RequestTypeCode.Removal ? RequestStatus.Removed : RequestStatus.Executed + } else { + if (requestType === RequestTypeCode.Removal) { + request.status = RequestStatus.PendingRemoval + } } // In case there are two requests submissions events, we always prioritize @@ -255,6 +260,9 @@ export async function getProjects(registryAddress: string, startTime?: number, e // Disallow contributions to removed recipient, but don't hide it project.isLocked = true } + } else { + // project is not removed yet, keep the index so that it can still receive contributions + project.index = recipient.recipientIndex } } @@ -309,9 +317,14 @@ export async function getProject(recipientId: string, filter = true): Promise -
+
From 8479cf9064a27b75f4e47ac15426ebdc936788bf Mon Sep 17 00:00:00 2001 From: yuetloo Date: Wed, 26 Jul 2023 14:40:28 -0400 Subject: [PATCH 3/5] recipient registry mapping changes for delete request edge cases --- .../src/OptimisticRecipientRegistryMapping.ts | 74 +++++++++++++------ vue-app/src/views/RecipientRegistry.vue | 6 +- 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/subgraph/src/OptimisticRecipientRegistryMapping.ts b/subgraph/src/OptimisticRecipientRegistryMapping.ts index 506cc9c83..dc7c5280b 100644 --- a/subgraph/src/OptimisticRecipientRegistryMapping.ts +++ b/subgraph/src/OptimisticRecipientRegistryMapping.ts @@ -37,15 +37,33 @@ export function handleRequestResolved(event: RequestResolved): void { let recipientId = event.params._recipientId.toHexString() let recipient = new Recipient(recipientId) - recipient.requestType = BigInt.fromI32(event.params._type).toString() - recipient.requester = event.transaction.from.toHexString() - recipient.submissionTime = event.params._timestamp.toString() - recipient.rejected = event.params._rejected - // verified means the request is resolved - recipient.verified = true recipient.recipientRegistry = recipientRegistryId - recipient.recipientIndex = event.params._recipientIndex recipient.requestResolvedHash = event.transaction.hash + // verified means the request is resolved + recipient.verified = true + + if (event.params._rejected) { + // this is a challengeRequest + if (event.params._type == 0) { + // reject add request + recipient.rejected = event.params._rejected + recipient.recipientIndex = event.params._recipientIndex + recipient.requester = event.transaction.from.toHexString() + recipient.submissionTime = event.params._timestamp.toString() + } else { + // reject delete request - revert request type back to 'Add' + // to clear the 'Pending removal' status + recipient.requestType = '0' + } + } else { + // this is a executeRequest + recipient.requestType = BigInt.fromI32(event.params._type).toString() + recipient.recipientIndex = event.params._recipientIndex + // reject the recipient if it was a 'delete recipient request' + recipient.rejected = event.params._rejected + recipient.requester = event.transaction.from.toHexString() + recipient.submissionTime = event.params._timestamp.toString() + } recipient.save() } @@ -57,11 +75,15 @@ export function handleRequestSubmitted(event: RequestSubmitted): void { //TODO: create RecipientRegistry entity here if it does not exist. let recipientId = event.params._recipientId.toHexString() + let recipient = new Recipient(recipientId) + recipient.recipientRegistry = recipientRegistryId + recipient.submissionTime = event.params._timestamp.toString() + recipient.lastUpdatedAt = event.block.timestamp.toString() if (event.params._type == 0) { // add recipient request - let recipient = new Recipient(recipientId) - recipient.recipientRegistry = recipientRegistryId + recipient.requestType = BigInt.fromI32(event.params._type).toString() + recipient.recipientIndex = null recipient.recipientAddress = event.params._recipient recipient.requester = event.transaction.from.toHexString() recipient.deposit = event.transaction.value @@ -70,23 +92,29 @@ export function handleRequestSubmitted(event: RequestSubmitted): void { // requestSubmittedHash stores the transaction hash for add recipient request // the UI uses it to look up the newly added recipient record recipient.requestSubmittedHash = event.transaction.hash + + // reset these fields in case the same recipient was added and removed + // in which case these fields could hold values for previous record + recipient.requestResolvedHash = null + recipient.verified = false + recipient.rejected = false + recipient.createdAt = event.block.timestamp.toString() + } else if (event.params._type == 1) { + // mark the record as pending delete recipient.requestType = BigInt.fromI32(event.params._type).toString() - recipient.submissionTime = event.params._timestamp.toString() recipient.verified = false - recipient.save() + log.info('handleRequestSubmitted - delete id {}', [recipientId]) } else { - // deleteRecipient request - // no need to update metadata and recipient address as they are not available for delete requests - let recipient = Recipient.load(recipientId) - if (recipient == null) { - // this should not happen, record the transaction hash for troubleshooting - recipient = new Recipient(recipientId) - recipient.requestSubmittedHash = event.transaction.hash - } - recipient.recipientRegistry = recipientRegistryId + // don't know how to process this request recipient.requestType = BigInt.fromI32(event.params._type).toString() - recipient.submissionTime = event.params._timestamp.toString() - recipient.verified = false - recipient.save() + log.warning( + 'handleRequestSubmitted - ignore unknown type {} from txHash {}', + [ + BigInt.fromI32(event.params._type).toString(), + event.transaction.hash.toString(), + ] + ) } + + recipient.save() } diff --git a/vue-app/src/views/RecipientRegistry.vue b/vue-app/src/views/RecipientRegistry.vue index d6b06174f..3f7d94345 100644 --- a/vue-app/src/views/RecipientRegistry.vue +++ b/vue-app/src/views/RecipientRegistry.vue @@ -192,8 +192,12 @@ function isExecuted(request: Request): boolean { return request.status === RequestStatus.Executed } +function isPendingRemoval(request: Request): boolean { + return request.status === RequestStatus.PendingRemoval +} + function hasProjectLink(request: Request): boolean { - return request.type === RequestType.Registration && request.status === RequestStatus.Executed + return isExecuted(request) || isPendingRemoval(request) } async function approve(request: Request): Promise { From f4873a04cddcd73584c62d0f08f15d74f59b2b0d Mon Sep 17 00:00:00 2001 From: yuetloo Date: Thu, 27 Jul 2023 20:13:55 -0400 Subject: [PATCH 4/5] get recipientId from transaction hash instead of query the subgraph --- .../src/OptimisticRecipientRegistryMapping.ts | 22 ++++++++++-- vue-app/src/api/projects.ts | 35 ++++++++++--------- vue-app/src/api/subgraph.ts | 17 +++++++++ vue-app/src/utils/contracts.ts | 4 +-- vue-app/src/views/JoinView.vue | 7 ++-- vue-app/src/views/ProjectAdded.vue | 8 ++--- 6 files changed, 64 insertions(+), 29 deletions(-) create mode 100644 vue-app/src/api/subgraph.ts diff --git a/subgraph/src/OptimisticRecipientRegistryMapping.ts b/subgraph/src/OptimisticRecipientRegistryMapping.ts index dc7c5280b..201c7f2f2 100644 --- a/subgraph/src/OptimisticRecipientRegistryMapping.ts +++ b/subgraph/src/OptimisticRecipientRegistryMapping.ts @@ -5,7 +5,7 @@ import { RequestSubmitted, } from '../generated/OptimisticRecipientRegistry/OptimisticRecipientRegistry' -import { Recipient } from '../generated/schema' +import { Recipient, RecipientRegistry } from '../generated/schema' // It is also possible to access smart contracts from mappings. For // example, the contract that has emitted the event can be connected to @@ -31,9 +31,18 @@ export function handleOwnershipTransferred(event: OwnershipTransferred): void { } export function handleRequestResolved(event: RequestResolved): void { + log.info('handleRequestResolved', []) + let recipientRegistryId = event.address.toHexString() + let recipientRegistry = RecipientRegistry.load(recipientRegistryId) + if (!recipientRegistry) { + log.warning( + 'handleRequestResolved - ignore unknown recipient registry {} hash {}', + [event.address.toHexString(), event.transaction.hash.toHex()] + ) + return + } - log.info('handleRequestResolved', []) let recipientId = event.params._recipientId.toHexString() let recipient = new Recipient(recipientId) @@ -72,7 +81,14 @@ export function handleRequestSubmitted(event: RequestSubmitted): void { log.info('handleRequestSubmitted', []) let recipientRegistryId = event.address.toHexString() - //TODO: create RecipientRegistry entity here if it does not exist. + let recipientRegistery = RecipientRegistry.load(recipientRegistryId) + if (!recipientRegistery) { + log.warning( + 'handleRequestSubmitted - ignore unknown recipient registry {} hash {}', + [event.address.toHexString(), event.transaction.hash.toHex()] + ) + return + } let recipientId = event.params._recipientId.toHexString() let recipient = new Recipient(recipientId) diff --git a/vue-app/src/api/projects.ts b/vue-app/src/api/projects.ts index c6a85de33..7bcc19164 100644 --- a/vue-app/src/api/projects.ts +++ b/vue-app/src/api/projects.ts @@ -1,6 +1,6 @@ import { BigNumber, Contract, Signer } from 'ethers' import type { TransactionResponse } from '@ethersproject/abstract-provider' -import { FundingRound } from './abi' +import { FundingRound, OptimisticRecipientRegistry } from './abi' import { factory, provider, recipientRegistryType, ipfsGatewayUrl } from './core' import SimpleRegistry from './recipient-registry-simple' @@ -9,6 +9,7 @@ import KlerosRegistry from './recipient-registry-kleros' import sdk from '@/graphql/sdk' import { getLeaderboardData } from '@/api/leaderboard' import type { RecipientApplicationData } from '@/api/types' +import { getEventArg } from '@/utils/contracts' export interface LeaderboardProject { id: string // Address or another ID depending on registry implementation @@ -164,28 +165,28 @@ export async function getProjectByIndex( } /** - * Check if the recipient with the submission hash exists in the subgraph + * Return the recipientId for the given transaction hash * @param transactionHash recipient submission hash - * @returns true if recipients with the submission hash was found + * @returns recipientId or null for not found */ -export async function recipientExists(transactionHash: string): Promise { - const data = await sdk.GetRecipientBySubmitHash({ transactionHash }) - return data.recipients.length > 0 -} - -/** - * Return the recipient for the given submission hash - * @param transactionHash recipient submission hash - * @returns project or null for not found - */ -export async function getRecipientBySubmitHash(transactionHash: string): Promise { +export async function getRecipientIdByHash(transactionHash: string): Promise { try { - const data = await sdk.GetRecipientBySubmitHash({ transactionHash }) - const exists = data.recipients.length > 0 - return exists ? OptimisticRegistry.decodeProject(data.recipients[0]) : null + const receipt = await provider.getTransactionReceipt(transactionHash) + + // should only have 1 event, just in case, return the first matching event + for (const log of receipt.logs) { + const registry = new Contract(log.address, OptimisticRecipientRegistry, provider) + try { + const recipientId = getEventArg(receipt, registry, 'RequestSubmitted', '_recipientId') + return recipientId + } catch { + // try next log + } + } } catch { return null } + return null } export function toLeaderboardProject(project: any): LeaderboardProject { diff --git a/vue-app/src/api/subgraph.ts b/vue-app/src/api/subgraph.ts new file mode 100644 index 000000000..db01532a2 --- /dev/null +++ b/vue-app/src/api/subgraph.ts @@ -0,0 +1,17 @@ +import type { TransactionReceipt } from '@ethersproject/abstract-provider' +import sdk from '@/graphql/sdk' + +/** + * Check if the transaction in the receipt exists in the subgraph + * @param receipt transaction receipt + * @returns true if the latest block number in subgraph is greater than the transaction block number + */ +export async function isTransactionInSubgraph(receipt: TransactionReceipt): Promise { + const data = await sdk.GetLatestBlockNumber() + + if (!data._meta?.block.number) { + return false + } + + return data._meta.block.number > receipt.blockNumber +} diff --git a/vue-app/src/utils/contracts.ts b/vue-app/src/utils/contracts.ts index e7cfeb67b..92a71c594 100644 --- a/vue-app/src/utils/contracts.ts +++ b/vue-app/src/utils/contracts.ts @@ -43,14 +43,14 @@ export async function waitForTransaction( */ export async function waitForTransactionAndCheck( pendingTransaction: Promise, - checkFn: (hash: string) => Promise, + checkFn: (receipt: TransactionReceipt) => Promise, onTransactionHash?: (hash: string) => void, ): Promise { const receipt = await waitForTransaction(pendingTransaction, onTransactionHash) return new Promise(resolve => { async function checkAndWait(depth = 0) { - if (await checkFn(receipt.transactionHash)) { + if (await checkFn(receipt)) { resolve(receipt) } else { if (depth > MAX_WAIT_DEPTH) { diff --git a/vue-app/src/views/JoinView.vue b/vue-app/src/views/JoinView.vue index 868187de2..57169cd16 100644 --- a/vue-app/src/views/JoinView.vue +++ b/vue-app/src/views/JoinView.vue @@ -717,7 +717,8 @@ import { useVuelidate } from '@vuelidate/core' import { required, requiredIf, email, maxLength, url, helpers } from '@vuelidate/validators' import type { RecipientApplicationData } from '@/api/types' import type { Project } from '@/api/projects' -import { recipientExists, formToProjectInterface } from '@/api/projects' +import { isTransactionInSubgraph } from '@/api/subgraph' +import { formToProjectInterface } from '@/api/projects' import { chain, showComplianceRequirement, isOptimisticRecipientRegistry } from '@/api/core' import { DateTime } from 'luxon' import { useRecipientStore, useAppStore, useUserStore } from '@/stores' @@ -948,8 +949,8 @@ async function addRecipient() { recipientRegistryInfo.value.deposit, currentUser.value.walletProvider.getSigner(), ), - hash => { - return isOptimisticRecipientRegistry ? recipientExists(hash) : Promise.resolve(true) + receipt => { + return isOptimisticRecipientRegistry ? isTransactionInSubgraph(receipt) : Promise.resolve(true) }, hash => (txHash.value = hash), ) diff --git a/vue-app/src/views/ProjectAdded.vue b/vue-app/src/views/ProjectAdded.vue index abfbbf3ce..59fe6290c 100644 --- a/vue-app/src/views/ProjectAdded.vue +++ b/vue-app/src/views/ProjectAdded.vue @@ -49,7 +49,7 @@ import { useAppStore } from '@/stores' import { useRoute } from 'vue-router' import { storeToRefs } from 'pinia' import { isOptimisticRecipientRegistry } from '@/api/core' -import { getRecipientBySubmitHash } from '@/api/projects' +import { getRecipientIdByHash } from '@/api/projects' const route = useRoute() const appStore = useAppStore() @@ -59,9 +59,9 @@ const hash = computed(() => route.params.hash as string) const recipientId = ref('') onMounted(async () => { - const recipient = await getRecipientBySubmitHash(hash.value) - if (recipient) { - recipientId.value = recipient.id + const id = await getRecipientIdByHash(hash.value) + if (id) { + recipientId.value = id } }) From 70347bdcbaba029ecbd6b2422466af64738922c6 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Thu, 27 Jul 2023 20:18:31 -0400 Subject: [PATCH 5/5] remove irrelevant comment --- subgraph/src/OptimisticRecipientRegistryMapping.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/subgraph/src/OptimisticRecipientRegistryMapping.ts b/subgraph/src/OptimisticRecipientRegistryMapping.ts index 201c7f2f2..24394f231 100644 --- a/subgraph/src/OptimisticRecipientRegistryMapping.ts +++ b/subgraph/src/OptimisticRecipientRegistryMapping.ts @@ -104,9 +104,6 @@ export function handleRequestSubmitted(event: RequestSubmitted): void { recipient.requester = event.transaction.from.toHexString() recipient.deposit = event.transaction.value recipient.recipientMetadata = event.params._metadata - - // requestSubmittedHash stores the transaction hash for add recipient request - // the UI uses it to look up the newly added recipient record recipient.requestSubmittedHash = event.transaction.hash // reset these fields in case the same recipient was added and removed