mirror of
https://github.com/cerc-io/watcher-ts
synced 2025-07-26 18:32:06 +00:00
Implement ethereum ABI encode and decode in subgraph (#103)
* Implement ethereum ABI encode in subgraph * Implement ethereum ABI decoding host API * Implement ABI encode decode for array type * Implement ABI encode decode for bytes type
This commit is contained in:
parent
9b1aa29afd
commit
8b913af93f
@ -71,7 +71,7 @@ describe('call handler in mapping code', () => {
|
||||
db,
|
||||
indexer,
|
||||
provider,
|
||||
{ event: { block: dummyEventData.block } },
|
||||
{ block: dummyEventData.block },
|
||||
filePath,
|
||||
dummyGraphData
|
||||
);
|
||||
|
@ -88,7 +88,7 @@ describe('eden wasm loader tests', async () => {
|
||||
db,
|
||||
indexer,
|
||||
provider,
|
||||
{ event: { block: dummyEventData.block } },
|
||||
{ block: dummyEventData.block },
|
||||
filePath,
|
||||
data
|
||||
));
|
||||
@ -203,7 +203,7 @@ describe('eden wasm loader tests', async () => {
|
||||
({ exports } = await instantiate(db,
|
||||
indexer,
|
||||
provider,
|
||||
{ event: { block: dummyEventData.block } },
|
||||
{ block: dummyEventData.block },
|
||||
filePath,
|
||||
data
|
||||
));
|
||||
@ -316,7 +316,7 @@ describe('eden wasm loader tests', async () => {
|
||||
db,
|
||||
indexer,
|
||||
provider,
|
||||
{ event: { block: dummyEventData.block } },
|
||||
{ block: dummyEventData.block },
|
||||
filePath,
|
||||
data
|
||||
));
|
||||
|
70
packages/graph-node/src/eth-abi.test.ts
Normal file
70
packages/graph-node/src/eth-abi.test.ts
Normal file
@ -0,0 +1,70 @@
|
||||
//
|
||||
// Copyright 2021 Vulcanize, Inc.
|
||||
//
|
||||
|
||||
import path from 'path';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { BaseProvider } from '@ethersproject/providers';
|
||||
|
||||
import { instantiate } from './loader';
|
||||
import { getTestDatabase, getTestIndexer, getTestProvider } from '../test/utils';
|
||||
import { Database } from './database';
|
||||
import { Indexer } from '../test/utils/indexer';
|
||||
|
||||
describe('ethereum ABI encode decode', () => {
|
||||
let exports: any;
|
||||
let db: Database;
|
||||
let indexer: Indexer;
|
||||
let provider: BaseProvider;
|
||||
let encoded: string;
|
||||
|
||||
before(async () => {
|
||||
db = getTestDatabase();
|
||||
indexer = getTestIndexer();
|
||||
provider = getTestProvider();
|
||||
});
|
||||
|
||||
it('should load the subgraph example wasm', async () => {
|
||||
const filePath = path.resolve(__dirname, '../test/subgraph/example1/build/Example1/Example1.wasm');
|
||||
const instance = await instantiate(
|
||||
db,
|
||||
indexer,
|
||||
provider,
|
||||
{},
|
||||
filePath
|
||||
);
|
||||
exports = instance.exports;
|
||||
const { _start } = exports;
|
||||
|
||||
// Important to call _start for built subgraphs on instantiation!
|
||||
// TODO: Check api version https://github.com/graphprotocol/graph-node/blob/6098daa8955bdfac597cec87080af5449807e874/runtime/wasm/src/module/mod.rs#L533
|
||||
_start();
|
||||
});
|
||||
|
||||
it('should encode data', async () => {
|
||||
const { testEthereumEncode, __getString } = exports;
|
||||
|
||||
const encodedPtr = await testEthereumEncode();
|
||||
encoded = __getString(encodedPtr);
|
||||
|
||||
expect(encoded)
|
||||
.to
|
||||
.equal('0x0000000000000000000000000000000000000000000000000000000000000420583bc7e1bc4799a225663353b82eb36d925399e6ef2799a6a95909f5ab8ac945000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000003f0000000000000000000000000000000000000000000000000000000000000001');
|
||||
});
|
||||
|
||||
it('should decode data', async () => {
|
||||
const { testEthereumDecode, __getString, __getArray, __newString } = exports;
|
||||
|
||||
const encodedString = await __newString(encoded);
|
||||
const decodedArrayPtr = await testEthereumDecode(encodedString);
|
||||
const decodedArray = __getArray(decodedArrayPtr);
|
||||
const [addressString, bytesString, bigInt1String, bigInt2String, boolString] = decodedArray.map((value: any) => __getString(value));
|
||||
|
||||
expect(addressString).to.equal('0x0000000000000000000000000000000000000420');
|
||||
expect(bytesString).to.equal('0x583bc7e1bc4799a225663353b82eb36d925399e6ef2799a6a95909f5ab8ac945');
|
||||
expect(bigInt1String).to.equal('62');
|
||||
expect(bigInt2String).to.equal('63');
|
||||
expect(boolString).to.equal('true');
|
||||
});
|
||||
});
|
@ -49,7 +49,7 @@ describe('eth-call wasm tests', () => {
|
||||
db,
|
||||
indexer,
|
||||
provider,
|
||||
{ event: { block: dummyEventData.block } },
|
||||
{ block: dummyEventData.block },
|
||||
filePath,
|
||||
data
|
||||
);
|
||||
|
@ -32,7 +32,7 @@ describe('wasm loader tests', () => {
|
||||
db,
|
||||
indexer,
|
||||
provider,
|
||||
{ event: {} },
|
||||
{},
|
||||
filePath
|
||||
);
|
||||
|
||||
@ -109,7 +109,7 @@ describe('wasm loader tests', () => {
|
||||
db,
|
||||
indexer,
|
||||
provider,
|
||||
{ event: {} },
|
||||
{},
|
||||
module
|
||||
);
|
||||
|
||||
|
@ -23,7 +23,8 @@ import {
|
||||
Block,
|
||||
fromEthereumValue,
|
||||
toEthereumValue,
|
||||
resolveEntityFieldConflicts
|
||||
resolveEntityFieldConflicts,
|
||||
getEthereumTypes
|
||||
} from './utils';
|
||||
import { Database } from './database';
|
||||
|
||||
@ -194,6 +195,27 @@ export const instantiate = async (
|
||||
console.log('eth_call error', err);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
'ethereum.encode': async (token: number) => {
|
||||
const ethValue = await ethereum.Value.wrap(token);
|
||||
|
||||
const data = await fromEthereumValue(instanceExports, ethValue);
|
||||
const type = await getEthereumTypes(instanceExports, ethValue);
|
||||
|
||||
const encoded = utils.defaultAbiCoder.encode([type], [data]);
|
||||
const encodedString = await __newString(encoded);
|
||||
|
||||
return ByteArray.fromHexString(encodedString);
|
||||
},
|
||||
'ethereum.decode': async (types: number, data: number) => {
|
||||
const typesString = __getString(types);
|
||||
const byteArray = await ByteArray.wrap(data);
|
||||
const bytesHex = await byteArray.toHex();
|
||||
const dataString = __getString(bytesHex);
|
||||
|
||||
const [decoded] = utils.defaultAbiCoder.decode([typesString], dataString);
|
||||
|
||||
return toEthereumValue(instanceExports, utils.ParamType.from(typesString), decoded);
|
||||
}
|
||||
},
|
||||
conversion: {
|
||||
@ -593,6 +615,7 @@ export const instantiate = async (
|
||||
const Address: any = instanceExports.Address as any;
|
||||
const ethereum: any = instanceExports.ethereum as any;
|
||||
const Entity: any = instanceExports.Entity as any;
|
||||
const ByteArray: any = instanceExports.ByteArray as any;
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
@ -42,7 +42,7 @@ describe('numbers wasm tests', () => {
|
||||
db,
|
||||
indexer,
|
||||
provider,
|
||||
{ event: {} },
|
||||
{},
|
||||
filePath
|
||||
);
|
||||
exports = instance.exports;
|
||||
|
@ -31,7 +31,7 @@ describe('typeConversion wasm tests', () => {
|
||||
db,
|
||||
indexer,
|
||||
provider,
|
||||
{ event: {} },
|
||||
{},
|
||||
filePath
|
||||
);
|
||||
exports = instance.exports;
|
||||
|
@ -63,6 +63,80 @@ export interface EventData {
|
||||
eventIndex: number;
|
||||
}
|
||||
|
||||
export const getEthereumTypes = async (instanceExports: any, value: any): Promise<any> => {
|
||||
const {
|
||||
__getArray,
|
||||
Bytes,
|
||||
ethereum
|
||||
} = instanceExports;
|
||||
|
||||
const kind = await value.kind;
|
||||
|
||||
switch (kind) {
|
||||
case EthereumValueKind.ADDRESS:
|
||||
return 'address';
|
||||
|
||||
case EthereumValueKind.BOOL:
|
||||
return 'bool';
|
||||
|
||||
case EthereumValueKind.STRING:
|
||||
return 'string';
|
||||
|
||||
case EthereumValueKind.BYTES:
|
||||
return 'bytes';
|
||||
|
||||
case EthereumValueKind.FIXED_BYTES: {
|
||||
const bytesPtr = await value.toBytes();
|
||||
const bytes = await Bytes.wrap(bytesPtr);
|
||||
const length = await bytes.length;
|
||||
|
||||
return `bytes${length}`;
|
||||
}
|
||||
|
||||
case EthereumValueKind.INT:
|
||||
return 'int256';
|
||||
|
||||
case EthereumValueKind.UINT: {
|
||||
return 'uint256';
|
||||
}
|
||||
|
||||
case EthereumValueKind.ARRAY: {
|
||||
const valuesPtr = await value.toArray();
|
||||
const [firstValuePtr] = await __getArray(valuesPtr);
|
||||
const firstValue = await ethereum.Value.wrap(firstValuePtr);
|
||||
const type = await getEthereumTypes(instanceExports, firstValue);
|
||||
|
||||
return `${type}[]`;
|
||||
}
|
||||
|
||||
case EthereumValueKind.FIXED_ARRAY: {
|
||||
const valuesPtr = await value.toArray();
|
||||
const values = await __getArray(valuesPtr);
|
||||
const firstValue = await ethereum.Value.wrap(values[0]);
|
||||
const type = await getEthereumTypes(instanceExports, firstValue);
|
||||
|
||||
return `${type}[${values.length}]`;
|
||||
}
|
||||
|
||||
case EthereumValueKind.TUPLE: {
|
||||
let values = await value.toTuple();
|
||||
values = await __getArray(values);
|
||||
|
||||
const typePromises = values.map(async (value: any) => {
|
||||
value = await ethereum.Value.wrap(value);
|
||||
return getEthereumTypes(instanceExports, value);
|
||||
});
|
||||
|
||||
const types = await Promise.all(typePromises);
|
||||
|
||||
return `tuple(${types.join(',')})`;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Method to get value from graph-ts ethereum.Value wasm instance.
|
||||
* @param instanceExports
|
||||
@ -71,9 +145,12 @@ export interface EventData {
|
||||
*/
|
||||
export const fromEthereumValue = async (instanceExports: any, value: any): Promise<any> => {
|
||||
const {
|
||||
__getArray,
|
||||
__getString,
|
||||
BigInt,
|
||||
Address
|
||||
Address,
|
||||
Bytes,
|
||||
ethereum
|
||||
} = instanceExports;
|
||||
|
||||
const kind = await value.kind;
|
||||
@ -91,9 +168,15 @@ export const fromEthereumValue = async (instanceExports: any, value: any): Promi
|
||||
return Boolean(bool);
|
||||
}
|
||||
|
||||
case EthereumValueKind.STRING: {
|
||||
const stringPtr = await value.toString();
|
||||
return __getString(stringPtr);
|
||||
}
|
||||
|
||||
case EthereumValueKind.BYTES:
|
||||
case EthereumValueKind.FIXED_BYTES: {
|
||||
const bytes = await value.toBytes();
|
||||
const bytesPtr = await value.toBytes();
|
||||
const bytes = await Bytes.wrap(bytesPtr);
|
||||
const bytesStringPtr = await bytes.toHexString();
|
||||
return __getString(bytesStringPtr);
|
||||
}
|
||||
@ -107,6 +190,31 @@ export const fromEthereumValue = async (instanceExports: any, value: any): Promi
|
||||
return BigNumber.from(bigIntString);
|
||||
}
|
||||
|
||||
case EthereumValueKind.ARRAY:
|
||||
case EthereumValueKind.FIXED_ARRAY: {
|
||||
const valuesPtr = await value.toArray();
|
||||
const values = __getArray(valuesPtr);
|
||||
|
||||
const valuePromises = values.map(async (value: any) => {
|
||||
value = await ethereum.Value.wrap(value);
|
||||
return fromEthereumValue(instanceExports, value);
|
||||
});
|
||||
|
||||
return Promise.all(valuePromises);
|
||||
}
|
||||
|
||||
case EthereumValueKind.TUPLE: {
|
||||
let values = await value.toTuple();
|
||||
values = await __getArray(values);
|
||||
|
||||
const valuePromises = values.map(async (value: any) => {
|
||||
value = await ethereum.Value.wrap(value);
|
||||
return fromEthereumValue(instanceExports, value);
|
||||
});
|
||||
|
||||
return Promise.all(valuePromises);
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -131,7 +239,26 @@ export const toEthereumValue = async (instanceExports: any, output: utils.ParamT
|
||||
id_of_type: getIdOfType
|
||||
} = instanceExports;
|
||||
|
||||
const { type } = output;
|
||||
const { type, baseType, arrayChildren } = output;
|
||||
|
||||
// For array type.
|
||||
if (baseType === 'array') {
|
||||
const arrayEthereumValueId = await getIdOfType(TypeId.ArrayEthereumValue);
|
||||
|
||||
// Get values for array elements.
|
||||
const ethereumValuePromises = value.map(
|
||||
async (value: any) => toEthereumValue(
|
||||
instanceExports,
|
||||
arrayChildren,
|
||||
value
|
||||
)
|
||||
);
|
||||
|
||||
const ethereumValues: any[] = await Promise.all(ethereumValuePromises);
|
||||
const ethereumValuesArray = await __newArray(arrayEthereumValueId, ethereumValues);
|
||||
|
||||
return ethereum.Value.fromArray(ethereumValuesArray);
|
||||
}
|
||||
|
||||
// For tuple type.
|
||||
if (type === 'tuple') {
|
||||
@ -140,10 +267,10 @@ export const toEthereumValue = async (instanceExports: any, output: utils.ParamT
|
||||
// Get values for struct elements.
|
||||
const ethereumValuePromises = output.components
|
||||
.map(
|
||||
async (component: utils.ParamType) => toEthereumValue(
|
||||
async (component: utils.ParamType, index) => toEthereumValue(
|
||||
instanceExports,
|
||||
component,
|
||||
value[component.name]
|
||||
value[index]
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Address, log, BigInt, BigDecimal, ByteArray, dataSource, ethereum } from '@graphprotocol/graph-ts';
|
||||
import { Address, log, BigInt, BigDecimal, ByteArray, dataSource, ethereum, Bytes } from '@graphprotocol/graph-ts';
|
||||
|
||||
import {
|
||||
Example1,
|
||||
@ -440,3 +440,67 @@ export function testBigIntToHex (value: string): string[] {
|
||||
|
||||
return [res1, res2];
|
||||
}
|
||||
|
||||
export function testEthereumEncode (): string {
|
||||
const address = ethereum.Value.fromAddress(Address.fromString('0x0000000000000000000000000000000000000420'));
|
||||
const bigInt1 = ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(62));
|
||||
const bigInt2 = ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(63));
|
||||
const bool = ethereum.Value.fromBoolean(true);
|
||||
|
||||
const bytes = ethereum.Value.fromFixedBytes(
|
||||
Bytes.fromByteArray(
|
||||
ByteArray.fromHexString('0x583bc7e1bc4799a225663353b82eb36d925399e6ef2799a6a95909f5ab8ac945')
|
||||
)
|
||||
);
|
||||
|
||||
const fixedSizedArray = ethereum.Value.fromFixedSizedArray([
|
||||
bigInt1,
|
||||
bigInt2
|
||||
]);
|
||||
|
||||
const tupleArray: Array<ethereum.Value> = [
|
||||
fixedSizedArray,
|
||||
bool
|
||||
];
|
||||
|
||||
const tuple = ethereum.Value.fromTuple(changetype<ethereum.Tuple>(tupleArray));
|
||||
|
||||
const token: Array<ethereum.Value> = [
|
||||
address,
|
||||
bytes,
|
||||
tuple
|
||||
];
|
||||
|
||||
const encoded = ethereum.encode(ethereum.Value.fromTuple(changetype<ethereum.Tuple>(token)))!;
|
||||
|
||||
log.debug('encoded: {}', [encoded.toHex()]);
|
||||
|
||||
return encoded.toHex();
|
||||
}
|
||||
|
||||
export function testEthereumDecode (encoded: string): string[] {
|
||||
const decoded = ethereum.decode('(address,bytes32,(uint256[2],bool))', Bytes.fromByteArray(ByteArray.fromHexString(encoded)));
|
||||
const tupleValues = decoded!.toTuple();
|
||||
|
||||
const decodedAddress = tupleValues[0].toAddress();
|
||||
const decodedBytes = tupleValues[1].toBytes();
|
||||
const decodedTuple = tupleValues[2].toTuple();
|
||||
const decodedFixedSizedArray = decodedTuple[0].toArray();
|
||||
const decodedBigInt1 = decodedFixedSizedArray[0].toBigInt();
|
||||
const decodedBigInt2 = decodedFixedSizedArray[1].toBigInt();
|
||||
const decodedBool = decodedTuple[1].toBoolean();
|
||||
|
||||
log.debug('decoded address: {}', [decodedAddress.toHex()]);
|
||||
log.debug('decoded bytes: {}', [decodedBytes.toHex()]);
|
||||
log.debug('decoded bigInt1: {}', [decodedBigInt1.toString()]);
|
||||
log.debug('decoded bigInt2: {}', [decodedBigInt2.toString()]);
|
||||
log.debug('decoded bool: {}', [decodedBool.toString()]);
|
||||
|
||||
return [
|
||||
decodedAddress.toHex(),
|
||||
decodedBytes.toHex(),
|
||||
decodedBigInt1.toString(),
|
||||
decodedBigInt2.toString(),
|
||||
decodedBool.toString()
|
||||
];
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ export const getDummyEventData = async (): Promise<EventData> => {
|
||||
const ethersBlock = await provider.getBlock(blockNumber);
|
||||
|
||||
const block = {
|
||||
headerId: 0,
|
||||
blockHash: ethersBlock.hash,
|
||||
blockNumber: ethersBlock.number.toString(),
|
||||
timestamp: '0',
|
||||
@ -41,7 +42,11 @@ export const getDummyEventData = async (): Promise<EventData> => {
|
||||
hash: ZERO_HASH,
|
||||
index: 0,
|
||||
from: ZERO_ADDRESS,
|
||||
to: ZERO_ADDRESS
|
||||
to: ZERO_ADDRESS,
|
||||
value: '0',
|
||||
gasLimit: '0',
|
||||
gasPrice: '0',
|
||||
input: ZERO_HASH
|
||||
};
|
||||
|
||||
return {
|
||||
|
Loading…
Reference in New Issue
Block a user