import _ from 'lodash'; import debug from 'debug'; // @ts-expect-error https://github.com/microsoft/TypeScript/issues/49721#issuecomment-1319854183 import type { CID as CIDType } from 'multiformats/cid'; // @ts-expect-error TODO: Resolve (Not able to find the type declarations) import * as codec from '@ipld/dag-cbor'; import { BlockProgressInterface, GraphDatabaseInterface, StateInterface, StateKind } from './types'; import { jsonBigIntStringReplacer } from './misc'; import { ResultState } from './indexer'; const log = debug('vulcanize:state-helper'); export interface StateDataMeta { id: string kind: StateKind parent: { '/': string | null }, ethBlock: { cid: { '/': string | null }, num: number } } interface StateData { meta?: StateDataMeta; state: any } export const updateStateForElementaryType = (initialObject: any, stateVariable: string, value: any): any => { const object = _.cloneDeep(initialObject); const path = ['state', stateVariable]; return _.set(object, path, value); }; export const updateStateForMappingType = (initialObject: any, stateVariable: string, keys: string[], value: any): any => { const object = _.cloneDeep(initialObject); keys.unshift('state', stateVariable); // Use _.setWith() with Object as customizer as _.set() treats numeric value in path as an index to an array. return _.setWith(object, keys, value, Object); }; export const verifyCheckpointData = async (database: GraphDatabaseInterface, block: BlockProgressInterface, data: any): Promise => { const { state } = data; for (const [entityName, idEntityMap] of Object.entries(state)) { for (const [id, stateEntity] of Object.entries(idEntityMap as {[key: string]: any})) { const entityData = await database.getEntity(entityName, id, block.blockHash) as any; // Compare entities. const diffFound = Object.keys(stateEntity) .some(key => { let stateValue = stateEntity[key]; if (key === 'blockNumber') { entityData.blockNumber = entityData._blockNumber; } if (key === 'blockHash') { entityData.blockHash = entityData._blockHash; } if (typeof stateEntity[key] === 'object' && stateEntity[key]?.id) { stateValue = stateEntity[key].id; } if ( Array.isArray(stateEntity[key]) && stateEntity[key].length && stateEntity[key][0].id ) { // Map State entity 1 to N relation field array to match DB entity. stateValue = stateEntity[key].map(({ id }: { id: string }) => id); // Sort DB entity 1 to N relation field array. entityData[key] = entityData[key].sort((a: string, b: string) => a.localeCompare(b)); } return JSON.stringify(stateValue) !== JSON.stringify(entityData[key], jsonBigIntStringReplacer); }); if (diffFound) { const message = `Diff found for checkpoint at block ${block.blockNumber} in entity ${entityName} id ${id}`; log(message); throw new Error(message); } } } }; export const getResultState = (state: StateInterface): ResultState => { const block = state.block; const data = codec.decode(Buffer.from(state.data)) as any; return { block: { cid: block.cid, hash: block.blockHash, number: block.blockNumber, timestamp: block.blockTimestamp, parentHash: block.parentHash }, contractAddress: state.contractAddress, cid: state.cid, kind: state.kind, data: JSON.stringify(data) }; }; export const createOrUpdateStateData = async ( data: StateData, meta?: StateDataMeta ): Promise<{ cid: CIDType, data: StateData, bytes: codec.ByteView }> => { if (meta) { data.meta = meta; } // Encoding the data using dag-cbor codec. const bytes = codec.encode(data); const { sha256 } = await import('multiformats/hashes/sha2'); // Calculating sha256 (multi)hash of the encoded data. const hash = await sha256.digest(bytes); const { CID } = await import('multiformats/cid'); // Calculating the CID: v1, code: dag-cbor, hash. const cid = CID.create(1, codec.code, hash); return { cid, data, bytes }; };