mirror of
https://github.com/cerc-io/watcher-ts
synced 2025-07-04 02:28:01 +00:00
* Handle BigNumber event params in watchers * Customize decimal according to limits of IEEE-754 decimal128 * Add definition for custom scalar BigDecimal
589 lines
16 KiB
TypeScript
589 lines
16 KiB
TypeScript
import { BigNumber, utils } from 'ethers';
|
|
import path from 'path';
|
|
import fs from 'fs-extra';
|
|
import debug from 'debug';
|
|
import yaml from 'js-yaml';
|
|
import Decimal from 'decimal.js';
|
|
import { ColumnType } from 'typeorm';
|
|
import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
|
|
|
|
import { TypeId, EthereumValueKind, ValueKind } from './types';
|
|
|
|
const log = debug('vulcanize:utils');
|
|
|
|
// Customize Decimal according the limits of IEEE-754 decimal128.
|
|
// Reference: https://github.com/graphprotocol/graph-node/blob/v0.24.2/graph/src/data/store/scalar.rs#L42
|
|
export const GraphDecimal = Decimal.clone({ minE: -6143, maxE: 6144, precision: 34 });
|
|
|
|
// Constant used in function digitsToString.
|
|
const LOG_BASE = 7;
|
|
|
|
interface Transaction {
|
|
hash: string;
|
|
index: number;
|
|
from: string;
|
|
to: string;
|
|
}
|
|
|
|
export interface Block {
|
|
blockHash: string;
|
|
blockNumber: string;
|
|
timestamp: string;
|
|
parentHash: string;
|
|
stateRoot: string;
|
|
td: string;
|
|
txRoot: string;
|
|
receiptRoot: string;
|
|
uncleHash: string;
|
|
difficulty: string;
|
|
gasLimit: string;
|
|
gasUsed: string;
|
|
}
|
|
|
|
export interface EventData {
|
|
block: Block;
|
|
tx: Transaction;
|
|
inputs: utils.ParamType[];
|
|
event: { [key: string]: any }
|
|
eventIndex: number;
|
|
}
|
|
|
|
/**
|
|
* Method to get value from graph-ts ethereum.Value wasm instance.
|
|
* @param instanceExports
|
|
* @param value
|
|
* @returns
|
|
*/
|
|
export const fromEthereumValue = async (instanceExports: any, value: any): Promise<any> => {
|
|
const {
|
|
__getString,
|
|
BigInt,
|
|
Address
|
|
} = instanceExports;
|
|
|
|
const kind = await value.kind;
|
|
|
|
switch (kind) {
|
|
case EthereumValueKind.ADDRESS: {
|
|
const addressPtr = await value.toAddress();
|
|
const address = Address.wrap(addressPtr);
|
|
const addressStringPtr = await address.toHexString();
|
|
return __getString(addressStringPtr);
|
|
}
|
|
|
|
case EthereumValueKind.BOOL: {
|
|
const bool = await value.toBoolean();
|
|
return Boolean(bool);
|
|
}
|
|
|
|
case EthereumValueKind.BYTES:
|
|
case EthereumValueKind.FIXED_BYTES: {
|
|
const bytes = await value.toBytes();
|
|
const bytesStringPtr = await bytes.toHexString();
|
|
return __getString(bytesStringPtr);
|
|
}
|
|
|
|
case EthereumValueKind.INT:
|
|
case EthereumValueKind.UINT: {
|
|
const bigIntPtr = await value.toBigInt();
|
|
const bigInt = BigInt.wrap(bigIntPtr);
|
|
const bigIntStringPtr = await bigInt.toString();
|
|
const bigIntString = __getString(bigIntStringPtr);
|
|
return BigNumber.from(bigIntString);
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Method to get ethereum value for passing to wasm instance.
|
|
* @param instanceExports
|
|
* @param value
|
|
* @param type
|
|
* @returns
|
|
*/
|
|
export const toEthereumValue = async (instanceExports: any, output: utils.ParamType, value: any): Promise<any> => {
|
|
const {
|
|
__newString,
|
|
__newArray,
|
|
ByteArray,
|
|
Bytes,
|
|
Address,
|
|
ethereum,
|
|
BigInt,
|
|
id_of_type: getIdOfType
|
|
} = instanceExports;
|
|
|
|
const { type } = output;
|
|
|
|
// For tuple type.
|
|
if (type === 'tuple') {
|
|
const arrayEthereumValueId = await getIdOfType(TypeId.ArrayEthereumValue);
|
|
|
|
// Get values for struct elements.
|
|
const ethereumValuePromises = output.components
|
|
.map(
|
|
async (component: utils.ParamType) => toEthereumValue(
|
|
instanceExports,
|
|
component,
|
|
value[component.name]
|
|
)
|
|
);
|
|
|
|
const ethereumValues: any[] = await Promise.all(ethereumValuePromises);
|
|
const ethereumValuesArrayPtr = await __newArray(arrayEthereumValueId, ethereumValues);
|
|
const ethereumTuple = await ethereum.Tuple.wrap(ethereumValuesArrayPtr);
|
|
|
|
return ethereum.Value.fromTuple(ethereumTuple);
|
|
}
|
|
|
|
// For boolean type.
|
|
if (type === 'bool') {
|
|
return ethereum.Value.fromBoolean(value ? 1 : 0);
|
|
}
|
|
|
|
const [isIntegerOrEnum, isInteger, isUnsigned] = type.match(/^enum|((u?)int([0-9]+))/) || [false];
|
|
|
|
// For uint/int type or enum type.
|
|
if (isIntegerOrEnum) {
|
|
const valueStringPtr = await __newString(value.toString());
|
|
const bigInt = await BigInt.fromString(valueStringPtr);
|
|
let ethereumValue = await ethereum.Value.fromUnsignedBigInt(bigInt);
|
|
|
|
if (Boolean(isInteger) && !isUnsigned) {
|
|
ethereumValue = await ethereum.Value.fromSignedBigInt(bigInt);
|
|
}
|
|
|
|
return ethereumValue;
|
|
}
|
|
|
|
if (type.startsWith('address')) {
|
|
const valueStringPtr = await __newString(value);
|
|
const addressPtr = await Address.fromString(valueStringPtr);
|
|
|
|
return ethereum.Value.fromAddress(addressPtr);
|
|
}
|
|
|
|
// TODO: Check between fixed bytes and dynamic bytes.
|
|
if (type.startsWith('bytes')) {
|
|
const valueStringPtr = await __newString(value);
|
|
const byteArray = await ByteArray.fromHexString(valueStringPtr);
|
|
const bytes = await Bytes.fromByteArray(byteArray);
|
|
return ethereum.Value.fromBytes(bytes);
|
|
}
|
|
|
|
// For string type.
|
|
const valueStringPtr = await __newString(value);
|
|
return ethereum.Value.fromString(valueStringPtr);
|
|
};
|
|
|
|
/**
|
|
* Method to create ethereum event.
|
|
* @param instanceExports
|
|
* @param contractAddress
|
|
* @param eventParamsData
|
|
* @returns
|
|
*/
|
|
export const createEvent = async (instanceExports: any, contractAddress: string, eventData: EventData): Promise<any> => {
|
|
const {
|
|
tx,
|
|
eventIndex,
|
|
inputs,
|
|
event,
|
|
block: blockData
|
|
} = eventData;
|
|
|
|
const {
|
|
__newString,
|
|
__newArray,
|
|
Address,
|
|
BigInt,
|
|
ethereum,
|
|
Bytes,
|
|
ByteArray,
|
|
id_of_type: idOfType
|
|
} = instanceExports;
|
|
|
|
const block = await createBlock(instanceExports, blockData);
|
|
|
|
// Fill transaction data.
|
|
const txHashStringPtr = await __newString(tx.hash);
|
|
const txHashByteArray = await ByteArray.fromHexString(txHashStringPtr);
|
|
const txHash = await Bytes.fromByteArray(txHashByteArray);
|
|
|
|
const txIndex = await BigInt.fromI32(tx.index);
|
|
|
|
const txFromStringPtr = await __newString(tx.from);
|
|
const txFrom = await Address.fromString(txFromStringPtr);
|
|
|
|
const txToStringPtr = await __newString(tx.to);
|
|
const txTo = tx.to && await Address.fromString(txToStringPtr);
|
|
|
|
const txValuePtr = await BigInt.fromI32(0);
|
|
const txGasLimitPtr = await BigInt.fromI32(0);
|
|
const txGasPricePtr = await BigInt.fromI32(0);
|
|
const txinputPtr = await Bytes.empty();
|
|
|
|
// Missing fields from watcher in transaction data:
|
|
// value
|
|
// gasLimit
|
|
// gasPrice
|
|
// input
|
|
const transaction = await ethereum.Transaction.__new(
|
|
txHash,
|
|
txIndex,
|
|
txFrom,
|
|
txTo,
|
|
txValuePtr,
|
|
txGasLimitPtr,
|
|
txGasPricePtr,
|
|
txinputPtr
|
|
);
|
|
|
|
const eventParamArrayPromise = inputs.map(async input => {
|
|
const { name } = input;
|
|
|
|
const ethValue = await toEthereumValue(instanceExports, input, event[name]);
|
|
const namePtr = await __newString(name);
|
|
|
|
return ethereum.EventParam.__new(
|
|
namePtr,
|
|
ethValue
|
|
);
|
|
});
|
|
|
|
const eventParamArray = await Promise.all(eventParamArrayPromise);
|
|
const arrayEventParamId = await idOfType(TypeId.ArrayEventParam);
|
|
const eventParams = await __newArray(arrayEventParamId, eventParamArray);
|
|
|
|
const addStrPtr = await __newString(contractAddress);
|
|
const eventAddressPtr = await Address.fromString(addStrPtr);
|
|
|
|
const eventIndexPtr = await BigInt.fromI32(eventIndex);
|
|
const transactionLogIndexPtr = await BigInt.fromI32(0);
|
|
|
|
// Create event to be passed to handler.
|
|
return ethereum.Event.__new(
|
|
eventAddressPtr,
|
|
eventIndexPtr,
|
|
transactionLogIndexPtr,
|
|
null,
|
|
block,
|
|
transaction,
|
|
eventParams
|
|
);
|
|
};
|
|
|
|
export const createBlock = async (instanceExports: any, blockData: Block): Promise<any> => {
|
|
const {
|
|
__newString,
|
|
Address,
|
|
BigInt,
|
|
ethereum,
|
|
Bytes,
|
|
ByteArray
|
|
} = instanceExports;
|
|
|
|
// Fill block data.
|
|
const blockHashStringPtr = await __newString(blockData.blockHash);
|
|
const blockHashByteArray = await ByteArray.fromHexString(blockHashStringPtr);
|
|
const blockHash = await Bytes.fromByteArray(blockHashByteArray);
|
|
|
|
const parentHashStringPtr = await __newString(blockData.parentHash);
|
|
const parentHashByteArray = await ByteArray.fromHexString(parentHashStringPtr);
|
|
const parentHash = await Bytes.fromByteArray(parentHashByteArray);
|
|
|
|
const uncleHashStringPtr = await __newString(blockData.uncleHash);
|
|
const uncleHashByteArray = await ByteArray.fromHexString(uncleHashStringPtr);
|
|
const uncleHash = await Bytes.fromByteArray(uncleHashByteArray);
|
|
|
|
const blockNumberStringPtr = await __newString(blockData.blockNumber);
|
|
const blockNumber = await BigInt.fromString(blockNumberStringPtr);
|
|
|
|
const gasUsedStringPtr = await __newString(blockData.gasUsed);
|
|
const gasUsed = await BigInt.fromString(gasUsedStringPtr);
|
|
|
|
const gasLimitStringPtr = await __newString(blockData.gasLimit);
|
|
const gasLimit = await BigInt.fromString(gasLimitStringPtr);
|
|
|
|
const timestampStringPtr = await __newString(blockData.timestamp);
|
|
const blockTimestamp = await BigInt.fromString(timestampStringPtr);
|
|
|
|
const stateRootStringPtr = await __newString(blockData.stateRoot);
|
|
const stateRootByteArray = await ByteArray.fromHexString(stateRootStringPtr);
|
|
const stateRoot = await Bytes.fromByteArray(stateRootByteArray);
|
|
|
|
const txRootStringPtr = await __newString(blockData.txRoot);
|
|
const transactionsRootByteArray = await ByteArray.fromHexString(txRootStringPtr);
|
|
const transactionsRoot = await Bytes.fromByteArray(transactionsRootByteArray);
|
|
|
|
const receiptRootStringPtr = await __newString(blockData.receiptRoot);
|
|
const receiptsRootByteArray = await ByteArray.fromHexString(receiptRootStringPtr);
|
|
const receiptsRoot = await Bytes.fromByteArray(receiptsRootByteArray);
|
|
|
|
const difficultyStringPtr = await __newString(blockData.difficulty);
|
|
const difficulty = await BigInt.fromString(difficultyStringPtr);
|
|
|
|
const tdStringPtr = await __newString(blockData.td);
|
|
const totalDifficulty = await BigInt.fromString(tdStringPtr);
|
|
|
|
const authorPtr = await Address.zero();
|
|
|
|
const sizePtr = await __newString('0');
|
|
const size = await BigInt.fromString(sizePtr);
|
|
|
|
// Missing fields from watcher in block data:
|
|
// author
|
|
// size
|
|
return await ethereum.Block.__new(
|
|
blockHash,
|
|
parentHash,
|
|
uncleHash,
|
|
authorPtr,
|
|
stateRoot,
|
|
transactionsRoot,
|
|
receiptsRoot,
|
|
blockNumber,
|
|
gasUsed,
|
|
gasLimit,
|
|
blockTimestamp,
|
|
difficulty,
|
|
totalDifficulty,
|
|
size
|
|
);
|
|
};
|
|
|
|
export const getSubgraphConfig = async (subgraphPath: string): Promise<any> => {
|
|
const configFilePath = path.resolve(path.join(subgraphPath, 'subgraph.yaml'));
|
|
const fileExists = await fs.pathExists(configFilePath);
|
|
|
|
if (!fileExists) {
|
|
throw new Error(`Config file not found: ${configFilePath}`);
|
|
}
|
|
|
|
const configFile = await fs.readFile(configFilePath, 'utf8');
|
|
const config = yaml.load(configFile);
|
|
log('config', JSON.stringify(config, null, 2));
|
|
|
|
return config;
|
|
};
|
|
|
|
export const toEntityValue = async (instanceExports: any, entityInstance: any, data: any, field: ColumnMetadata) => {
|
|
const { __newString, Value } = instanceExports;
|
|
const { type, isArray, propertyName } = field;
|
|
|
|
const entityKey = await __newString(propertyName);
|
|
const entityValuePtr = await entityInstance.get(entityKey);
|
|
const subgraphValue = Value.wrap(entityValuePtr);
|
|
const value = data[propertyName];
|
|
|
|
const entityValue = await formatEntityValue(instanceExports, subgraphValue, type, value, isArray);
|
|
|
|
return entityInstance.set(entityKey, entityValue);
|
|
};
|
|
|
|
export const fromEntityValue = async (instanceExports: any, entityInstance: any, key: string): Promise<any> => {
|
|
const { __newString } = instanceExports;
|
|
const entityKey = await __newString(key);
|
|
const entityValuePtr = await entityInstance.get(entityKey);
|
|
|
|
return parseEntityValue(instanceExports, entityValuePtr);
|
|
};
|
|
|
|
const parseEntityValue = async (instanceExports: any, valuePtr: number) => {
|
|
const {
|
|
__getString,
|
|
__getArray,
|
|
BigInt: ExportBigInt,
|
|
Bytes,
|
|
BigDecimal,
|
|
Value
|
|
} = instanceExports;
|
|
|
|
const value = Value.wrap(valuePtr);
|
|
const kind = await value.kind;
|
|
|
|
switch (kind) {
|
|
case ValueKind.STRING: {
|
|
const stringValue = await value.toString();
|
|
return __getString(stringValue);
|
|
}
|
|
|
|
case ValueKind.BYTES: {
|
|
const bytesPtr = await value.toBytes();
|
|
const bytes = await Bytes.wrap(bytesPtr);
|
|
const bytesStringPtr = await bytes.toHexString();
|
|
|
|
return __getString(bytesStringPtr);
|
|
}
|
|
|
|
case ValueKind.BOOL: {
|
|
const bool = await value.toBoolean();
|
|
|
|
return Boolean(bool);
|
|
}
|
|
|
|
case ValueKind.INT: {
|
|
return value.toI32();
|
|
}
|
|
|
|
case ValueKind.BIGINT: {
|
|
const bigIntPtr = await value.toBigInt();
|
|
const bigInt = ExportBigInt.wrap(bigIntPtr);
|
|
const bigIntStringPtr = await bigInt.toString();
|
|
const bigIntString = __getString(bigIntStringPtr);
|
|
|
|
return BigInt(bigIntString);
|
|
}
|
|
|
|
case ValueKind.BIGDECIMAL: {
|
|
const bigDecimalPtr = await value.toBigDecimal();
|
|
const bigDecimal = BigDecimal.wrap(bigDecimalPtr);
|
|
const bigDecimalStringPtr = await bigDecimal.toString();
|
|
|
|
return new GraphDecimal(__getString(bigDecimalStringPtr)).toFixed();
|
|
}
|
|
|
|
case ValueKind.ARRAY: {
|
|
const arrayPtr = await value.toArray();
|
|
const arr = await __getArray(arrayPtr);
|
|
const arrDataPromises = arr.map((arrValuePtr: any) => parseEntityValue(instanceExports, arrValuePtr));
|
|
|
|
return Promise.all(arrDataPromises);
|
|
}
|
|
|
|
case ValueKind.NULL: {
|
|
return null;
|
|
}
|
|
|
|
default:
|
|
throw new Error(`Unsupported value kind: ${kind}`);
|
|
}
|
|
};
|
|
|
|
const formatEntityValue = async (instanceExports: any, subgraphValue: any, type: ColumnType, value: any, isArray: boolean): Promise<any> => {
|
|
const { __newString, __newArray, BigInt: ExportBigInt, Value, ByteArray, Bytes, BigDecimal, id_of_type: getIdOfType } = instanceExports;
|
|
|
|
if (isArray) {
|
|
// TODO: Implement handling array of Bytes type field.
|
|
const dataArrayPromises = value.map((el: any) => formatEntityValue(instanceExports, subgraphValue, type, el, false));
|
|
const dataArray = await Promise.all(dataArrayPromises);
|
|
const arrayStoreValueId = await getIdOfType(TypeId.ArrayStoreValue);
|
|
const valueArray = await __newArray(arrayStoreValueId, dataArray);
|
|
|
|
return Value.fromArray(valueArray);
|
|
}
|
|
|
|
switch (type) {
|
|
case 'varchar': {
|
|
const entityValue = await __newString(value);
|
|
const kind = await subgraphValue.kind;
|
|
|
|
switch (kind) {
|
|
case ValueKind.BYTES: {
|
|
const byteArray = await ByteArray.fromHexString(entityValue);
|
|
const bytes = await Bytes.fromByteArray(byteArray);
|
|
|
|
return Value.fromBytes(bytes);
|
|
}
|
|
|
|
default:
|
|
return Value.fromString(entityValue);
|
|
}
|
|
}
|
|
|
|
case 'integer': {
|
|
return Value.fromI32(value);
|
|
}
|
|
|
|
case 'bigint': {
|
|
const valueStringPtr = await __newString(value.toString());
|
|
const bigInt = await ExportBigInt.fromString(valueStringPtr);
|
|
|
|
return Value.fromBigInt(bigInt);
|
|
}
|
|
|
|
case 'boolean': {
|
|
return Value.fromBoolean(value ? 1 : 0);
|
|
}
|
|
|
|
case 'enum': {
|
|
const entityValue = await __newString(value);
|
|
|
|
return Value.fromString(entityValue);
|
|
}
|
|
|
|
case 'numeric': {
|
|
const valueStringPtr = await __newString(value.toString());
|
|
const bigDecimal = await BigDecimal.fromString(valueStringPtr);
|
|
|
|
return Value.fromBigDecimal(bigDecimal);
|
|
}
|
|
|
|
// TODO: Support more types.
|
|
default:
|
|
throw new Error(`Unsupported type: ${type}`);
|
|
}
|
|
};
|
|
|
|
export const resolveEntityFieldConflicts = (entity: any): any => {
|
|
if (entity) {
|
|
// Remove fields blockHash and blockNumber from the entity.
|
|
delete entity.blockHash;
|
|
delete entity.blockNumber;
|
|
|
|
// Rename _blockHash -> blockHash.
|
|
if ('_blockHash' in entity) {
|
|
entity.blockHash = entity._blockHash;
|
|
delete entity._blockHash;
|
|
}
|
|
|
|
// Rename _blockNumber -> blockNumber.
|
|
if ('_blockNumber' in entity) {
|
|
entity.blockNumber = entity._blockNumber;
|
|
delete entity._blockNumber;
|
|
}
|
|
}
|
|
|
|
return entity;
|
|
};
|
|
|
|
// Get digits in a string from an array of digit numbers (Decimal().d)
|
|
// https://github.com/MikeMcl/decimal.js/blob/master/decimal.mjs#L2516
|
|
export function digitsToString (d: any) {
|
|
let i, k, ws;
|
|
const indexOfLastWord = d.length - 1;
|
|
let str = '';
|
|
let w = d[0];
|
|
|
|
if (indexOfLastWord > 0) {
|
|
str += w;
|
|
for (i = 1; i < indexOfLastWord; i++) {
|
|
ws = d[i] + '';
|
|
k = LOG_BASE - ws.length;
|
|
if (k) str += getZeroString(k);
|
|
str += ws;
|
|
}
|
|
|
|
w = d[i];
|
|
ws = w + '';
|
|
k = LOG_BASE - ws.length;
|
|
if (k) str += getZeroString(k);
|
|
} else if (w === 0) {
|
|
return '0';
|
|
}
|
|
|
|
// Remove trailing zeros of last w.
|
|
for (; w % 10 === 0;) w /= 10;
|
|
|
|
return str + w;
|
|
}
|
|
|
|
function getZeroString (k: any) {
|
|
let zs = '';
|
|
for (; k--;) zs += '0';
|
|
return zs;
|
|
}
|