diff --git a/backend/src/accounts/entities/storage-item.entity.ts b/backend/src/accounts/entities/storage-item.entity.ts index 36ae58a8..98ca5ba5 100644 --- a/backend/src/accounts/entities/storage-item.entity.ts +++ b/backend/src/accounts/entities/storage-item.entity.ts @@ -11,10 +11,12 @@ import { ensurePrefixedAddress } from "../../utils"; @Entity({ name: "storage" }) export class AccountStorageItemEntity extends PollingEntity { - @PrimaryColumn() - id: string; + // This is a deprecated column, but removing it would cause a database migration error. + // To completely remove this we would need to write a manual migration script. + @PrimaryColumn({ name: "id" }) + _id: string = ""; - @Column() + @PrimaryColumn() pathIdentifier: string; @PrimaryColumn() @@ -29,6 +31,12 @@ export class AccountStorageItemEntity extends PollingEntity { @ManyToOne(() => AccountEntity, (account) => account.storage) account: AccountEntity; + get id() { + return `${this.accountAddress}/${this.getLowerCasedPathDomain()}/${ + this.pathIdentifier + }`; + } + toProto(): AccountStorageItem { return { id: this.id, @@ -40,53 +48,6 @@ export class AccountStorageItemEntity extends PollingEntity { }; } - static create( - flowStorageDomain: FlowAccountStorageDomain, - flowStorageIdentifier: FlowStorageIdentifier, - flowAccountStorage: FlowAccountStorage - ) { - const storageData = - flowAccountStorage[flowStorageDomain][flowStorageIdentifier]; - - const storageItem = new AccountStorageItemEntity(); - storageItem.pathIdentifier = flowStorageIdentifier; - storageItem.pathDomain = this.convertFlowStorageDomain(flowStorageDomain); - - // TODO(milestone-x): For now we will just show plain (unparsed) storage data - // But in the future we will want to parse it so that we can extract info - // This will be possible after storage API implements proper deserialization of storage data - if (typeof storageData !== "object") { - // In case the data is a simple value (string, number, boolean,...) - // we need to store it in object form (e.g. under "value" key). - // Otherwise it won't get properly encoded/decoded by protocol buffers. - storageItem.data = { value: storageData }; - } else { - storageItem.data = storageData; - } - storageItem.accountAddress = ensurePrefixedAddress( - flowAccountStorage.Address - ); - storageItem.id = `${ - storageItem.accountAddress - }/${storageItem.getLowerCasedPathDomain()}/${storageItem.pathIdentifier}`; - return storageItem; - } - - private static convertFlowStorageDomain( - flowStorageDomain: FlowAccountStorageDomain - ): AccountStorageDomain { - switch (flowStorageDomain) { - case "Public": - return AccountStorageDomain.STORAGE_DOMAIN_PUBLIC; - case "Private": - return AccountStorageDomain.STORAGE_DOMAIN_PRIVATE; - case "Storage": - return AccountStorageDomain.STORAGE_DOMAIN_STORAGE; - default: - return AccountStorageDomain.STORAGE_DOMAIN_UNKNOWN; - } - } - getLowerCasedPathDomain() { switch (this.pathDomain) { case AccountStorageDomain.STORAGE_DOMAIN_PUBLIC: diff --git a/backend/src/accounts/services/storage.service.ts b/backend/src/accounts/services/storage.service.ts index c5441dc0..27e6471b 100644 --- a/backend/src/accounts/services/storage.service.ts +++ b/backend/src/accounts/services/storage.service.ts @@ -40,7 +40,7 @@ export class AccountStorageService { ) { const oldStorageItems = await this.findStorageByAccount(address); const entitiesDiff = computeEntitiesDiff({ - primaryKey: "id", + primaryKey: ["pathIdentifier", "pathDomain", "accountAddress"], newEntities: newStorageItems, oldEntities: oldStorageItems, deepCompare: true, diff --git a/backend/src/core/services/cache-removal.service.ts b/backend/src/core/services/cache-removal.service.ts index 41fb3af9..d0ab1a17 100644 --- a/backend/src/core/services/cache-removal.service.ts +++ b/backend/src/core/services/cache-removal.service.ts @@ -42,7 +42,7 @@ export class CacheRemovalService { this.blocksService.removeAll(), ]); } catch (e) { - this.logger.error("Failed to remove other data", e); + this.logger.error("Failed to remove cached data", e); throw e; } } @@ -62,7 +62,7 @@ export class CacheRemovalService { this.blocksService.removeByBlockIds(blockIds), ]); } catch (e) { - this.logger.error("Failed to remove other data", e); + this.logger.error("Failed to remove cached data", e); throw e; } } diff --git a/backend/src/data-processing/processor.service.ts b/backend/src/data-processing/processor.service.ts index d087016a..f546a01f 100644 --- a/backend/src/data-processing/processor.service.ts +++ b/backend/src/data-processing/processor.service.ts @@ -6,6 +6,7 @@ import { FlowEvent, FlowGatewayService, FlowKey, + FlowSignableObject, FlowTransaction, FlowTransactionStatus, } from "../flow/services/gateway.service"; @@ -31,6 +32,7 @@ import { FlowCoreEventType, ManagedProcessState, ServiceStatus, + SignableObject, TransactionStatus, } from "@flowser/shared"; import { @@ -305,9 +307,9 @@ export class ProcessorService implements ProjectContextLifecycle { const transactionPromises = Promise.all( data.transactions.map((transaction) => this.processNewTransaction({ - block: data.block, - transaction, - transactionStatus: transaction.status, + flowBlock: data.block, + flowTransaction: transaction, + flowTransactionStatus: transaction.status, }).catch((e) => this.logger.error(`transaction save error: ${e.message}`, e.stack) ) @@ -451,7 +453,7 @@ export class ProcessorService implements ProjectContextLifecycle { case FlowCoreEventType.ACCOUNT_CONTRACT_ADDED: case FlowCoreEventType.ACCOUNT_CONTRACT_UPDATED: case FlowCoreEventType.ACCOUNT_CONTRACT_REMOVED: - // TODO: Use event.data.address & event.data.contract to determine updated/created/removed contract + // TODO: Stop re-fetching all contracts and instead use event.data.contract to get the removed/updated/added contract return this.updateStoredAccountContracts({ address, flowBlock }); // For now keep listening for monotonic and non-monotonic addresses, // although I think only non-monotonic ones are used for contract deployment. @@ -480,22 +482,13 @@ export class ProcessorService implements ProjectContextLifecycle { } private async processNewTransaction(options: { - block: FlowBlock; - transaction: FlowTransaction; - transactionStatus: FlowTransactionStatus; + flowBlock: FlowBlock; + flowTransaction: FlowTransaction; + flowTransactionStatus: FlowTransactionStatus; }) { - // TODO: Should we also mark all tx.authorizers as updated? - const payerAddress = ensurePrefixedAddress(options.transaction.payer); - return Promise.all([ - this.transactionService.createOrUpdate( - TransactionEntity.create( - options.block, - options.transaction, - options.transactionStatus - ) - ), - this.accountService.markUpdated(payerAddress), - ]); + this.transactionService.createOrUpdate( + this.createTransactionEntity(options) + ); } private async storeNewAccountWithContractsAndKeys(options: { @@ -750,7 +743,50 @@ export class ProcessorService implements ProjectContextLifecycle { contract.accountAddress = ensurePrefixedAddress(flowAccount.address); contract.name = name; contract.code = code; - // contract.updateId(); return contract; } + + private createTransactionEntity(options: { + flowBlock: FlowBlock; + flowTransaction: FlowTransaction; + flowTransactionStatus: FlowTransactionStatus; + }): TransactionEntity { + const { flowBlock, flowTransaction, flowTransactionStatus } = options; + const transaction = new TransactionEntity(); + transaction.id = flowTransaction.id; + transaction.script = flowTransaction.script; + transaction.payerAddress = ensurePrefixedAddress(flowTransaction.payer); + transaction.blockId = flowBlock.id; + transaction.referenceBlockId = flowTransaction.referenceBlockId; + transaction.gasLimit = flowTransaction.gasLimit; + transaction.authorizers = flowTransaction.authorizers.map((address) => + ensurePrefixedAddress(address) + ); + transaction.args = flowTransaction.args; + transaction.proposalKey = { + ...flowTransaction.proposalKey, + address: ensurePrefixedAddress(flowTransaction.proposalKey.address), + }; + transaction.envelopeSignatures = this.deserializeSignableObjects( + flowTransaction.envelopeSignatures + ); + transaction.payloadSignatures = this.deserializeSignableObjects( + flowTransaction.payloadSignatures + ); + transaction.status = TransactionStatus.fromJSON({ + errorMessage: flowTransactionStatus.errorMessage, + grcpStatus: flowTransactionStatus.statusCode, + executionStatus: flowTransactionStatus.status, + }); + return transaction; + } + + private deserializeSignableObjects(signableObjects: FlowSignableObject[]) { + return signableObjects.map((signable) => + SignableObject.fromJSON({ + ...signable, + address: ensurePrefixedAddress(signable.address), + }) + ); + } } diff --git a/backend/src/flow/services/storage.service.ts b/backend/src/flow/services/storage.service.ts index 59378256..61ae8c2d 100644 --- a/backend/src/flow/services/storage.service.ts +++ b/backend/src/flow/services/storage.service.ts @@ -2,6 +2,8 @@ import { Injectable, HttpException } from "@nestjs/common"; import axios from "axios"; import { FlowAccount } from "./gateway.service"; import { AccountStorageItemEntity } from "../../accounts/entities/storage-item.entity"; +import { ensurePrefixedAddress } from "../../utils"; +import { AccountStorageDomain } from "@flowser/shared"; /** * For more info on the account storage model and API, see: @@ -47,14 +49,27 @@ export class FlowAccountStorageService { ); const storageIdentifiers = Object.keys(flowAccountStorage.Storage ?? {}); - const privateItems = privateStorageIdentifiers.map((identifier) => - AccountStorageItemEntity.create("Private", identifier, flowAccountStorage) + const privateItems = privateStorageIdentifiers.map( + (flowStorageIdentifier) => + this.createStorageEntity({ + flowStorageDomain: "Private", + flowStorageIdentifier, + flowAccountStorage, + }) ); - const publicItems = publicStorageIdentifiers.map((identifier) => - AccountStorageItemEntity.create("Public", identifier, flowAccountStorage) + const publicItems = publicStorageIdentifiers.map((flowStorageIdentifier) => + this.createStorageEntity({ + flowStorageDomain: "Public", + flowStorageIdentifier, + flowAccountStorage, + }) ); - const storageItems = storageIdentifiers.map((identifier) => - AccountStorageItemEntity.create("Storage", identifier, flowAccountStorage) + const storageItems = storageIdentifiers.map((flowStorageIdentifier) => + this.createStorageEntity({ + flowStorageDomain: "Storage", + flowStorageIdentifier, + flowAccountStorage, + }) ); return { privateItems, publicItems, storageItems }; @@ -72,4 +87,50 @@ export class FlowAccountStorageService { return response.data as FlowAccountStorage; } + + private createStorageEntity(options: { + flowStorageDomain: FlowAccountStorageDomain; + flowStorageIdentifier: FlowStorageIdentifier; + flowAccountStorage: FlowAccountStorage; + }) { + const { flowAccountStorage, flowStorageIdentifier, flowStorageDomain } = + options; + const storageData = + flowAccountStorage[flowStorageDomain][flowStorageIdentifier]; + + const storageItem = new AccountStorageItemEntity(); + storageItem.pathIdentifier = flowStorageIdentifier; + storageItem.pathDomain = this.flowStorageDomainToEnum(flowStorageDomain); + + // TODO(milestone-x): For now we will just show plain (unparsed) storage data + // But in the future we will want to parse it so that we can extract info + // This will be possible after storage API implements proper deserialization of storage data + if (typeof storageData !== "object") { + // In case the data is a simple value (string, number, boolean,...) + // we need to store it in object form (e.g. under "value" key). + // Otherwise, it won't get properly encoded/decoded by protocol buffers. + storageItem.data = { value: storageData }; + } else { + storageItem.data = storageData; + } + storageItem.accountAddress = ensurePrefixedAddress( + flowAccountStorage.Address + ); + return storageItem; + } + + private flowStorageDomainToEnum( + flowStorageDomain: FlowAccountStorageDomain + ): AccountStorageDomain { + switch (flowStorageDomain) { + case "Public": + return AccountStorageDomain.STORAGE_DOMAIN_PUBLIC; + case "Private": + return AccountStorageDomain.STORAGE_DOMAIN_PRIVATE; + case "Storage": + return AccountStorageDomain.STORAGE_DOMAIN_STORAGE; + default: + return AccountStorageDomain.STORAGE_DOMAIN_UNKNOWN; + } + } } diff --git a/backend/src/transactions/transaction.entity.ts b/backend/src/transactions/transaction.entity.ts index 3ff2e473..515b2e06 100644 --- a/backend/src/transactions/transaction.entity.ts +++ b/backend/src/transactions/transaction.entity.ts @@ -88,47 +88,4 @@ export class TransactionEntity updatedAt: this.updatedAt.toISOString(), }; } - - static create( - flowBlock: FlowBlock, - flowTransaction: FlowTransaction, - flowTransactionStatus: FlowTransactionStatus - ): TransactionEntity { - const transaction = new TransactionEntity(); - transaction.id = flowTransaction.id; - transaction.script = flowTransaction.script; - transaction.payerAddress = ensurePrefixedAddress(flowTransaction.payer); - transaction.blockId = flowBlock.id; - transaction.referenceBlockId = flowTransaction.referenceBlockId; - transaction.gasLimit = flowTransaction.gasLimit; - transaction.authorizers = flowTransaction.authorizers.map((address) => - ensurePrefixedAddress(address) - ); - transaction.args = flowTransaction.args; - transaction.proposalKey = { - ...flowTransaction.proposalKey, - address: ensurePrefixedAddress(flowTransaction.proposalKey.address), - }; - transaction.envelopeSignatures = deserializeSignableObjects( - flowTransaction.envelopeSignatures - ); - transaction.payloadSignatures = deserializeSignableObjects( - flowTransaction.payloadSignatures - ); - transaction.status = TransactionStatus.fromJSON({ - errorMessage: flowTransactionStatus.errorMessage, - grcpStatus: flowTransactionStatus.statusCode, - executionStatus: flowTransactionStatus.status, - }); - return transaction; - } -} - -function deserializeSignableObjects(signableObjects: FlowSignableObject[]) { - return signableObjects.map((signable) => - SignableObject.fromJSON({ - ...signable, - address: ensurePrefixedAddress(signable.address), - }) - ); } diff --git a/frontend/src/hooks/use-api.ts b/frontend/src/hooks/use-api.ts index 8a9dea54..da21cede 100644 --- a/frontend/src/hooks/use-api.ts +++ b/frontend/src/hooks/use-api.ts @@ -130,8 +130,12 @@ export function useGetPollingContracts(): TimeoutPollingHook { } export function useGetContract(contractId: string) { - return useQuery(`/contract/${contractId}`, () => - contractsService.getSingle(contractId) + return useQuery( + `/contract/${contractId}`, + () => contractsService.getSingle(contractId), + { + refetchInterval: 2000, + } ); } diff --git a/frontend/src/pages/contracts/details/Details.tsx b/frontend/src/pages/contracts/details/Details.tsx index d615de88..ac66e1a1 100644 --- a/frontend/src/pages/contracts/details/Details.tsx +++ b/frontend/src/pages/contracts/details/Details.tsx @@ -51,6 +51,10 @@ const Details: FunctionComponent = () => { ), }, + { + label: "Updated date", + value: TextUtils.longDate(contract.updatedAt), + }, { label: "Created date", value: TextUtils.longDate(contract.createdAt), diff --git a/frontend/src/pages/contracts/main/Main.tsx b/frontend/src/pages/contracts/main/Main.tsx index b39ce25d..5dcad2f4 100644 --- a/frontend/src/pages/contracts/main/Main.tsx +++ b/frontend/src/pages/contracts/main/Main.tsx @@ -37,6 +37,14 @@ const columns = [ ), }), + columnHelper.accessor("updatedAt", { + header: () => , + cell: (info) => ( + + + + ), + }), columnHelper.accessor("createdAt", { header: () => , cell: (info) => (