Implement method for storage based access in subgraph mapping code (#162)

* Implement storage call in subgraph mapping code

* Add test for mapping type variable storage call

* Use vulcanize graph-ts

* Revert to graph-ts version 0.22.1
This commit is contained in:
nikugogoi 2022-08-17 16:25:49 +05:30 committed by GitHub
parent 80682e2755
commit ec56de057f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 503 additions and 17 deletions

View File

@ -12,7 +12,7 @@ import { JsonFragment } from '@ethersproject/abi';
import { BaseProvider } from '@ethersproject/providers';
import * as codec from '@ipld/dag-cbor';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { StorageLayout } from '@vulcanize/solidity-mapper';
import { MappingKey, StorageLayout } from '@vulcanize/solidity-mapper';
import {
IPLDIndexer as BaseIndexer,
IPLDIndexerInterface,
@ -171,6 +171,10 @@ export class Indexer implements IPLDIndexerInterface {
return this._serverConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
async init (): Promise<void> {
await this._baseIndexer.fetchContracts();
await this._baseIndexer.fetchIPLDStatus();
@ -317,6 +321,16 @@ export class Indexer implements IPLDIndexerInterface {
}
{{/each}}
async getStorageValue (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: MappingKey[]): Promise<ValueResult> {
return this._baseIndexer.getStorageValue(
storageLayout,
blockHash,
contractAddress,
variable,
...mappingKeys
);
}
async pushToIPFS (data: any): Promise<void> {
await this._baseIndexer.pushToIPFS(data);
}

View File

@ -12,7 +12,7 @@ import { JsonFragment } from '@ethersproject/abi';
import { BaseProvider } from '@ethersproject/providers';
import * as codec from '@ipld/dag-cbor';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { StorageLayout } from '@vulcanize/solidity-mapper';
import { MappingKey, StorageLayout } from '@vulcanize/solidity-mapper';
import {
IPLDIndexer as BaseIndexer,
UNKNOWN_EVENT_NAME,
@ -24,7 +24,8 @@ import {
IPFSClient,
StateKind,
IPLDIndexerInterface,
IpldStatus as IpldStatusInterface
IpldStatus as IpldStatusInterface,
ValueResult
} from '@vulcanize/util';
import { GraphWatcher } from '@vulcanize/graph-node';
@ -170,6 +171,10 @@ export class Indexer implements IPLDIndexerInterface {
return this._serverConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
async init (): Promise<void> {
await this._baseIndexer.fetchContracts();
await this._baseIndexer.fetchIPLDStatus();
@ -230,6 +235,16 @@ export class Indexer implements IPLDIndexerInterface {
};
}
async getStorageValue (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: MappingKey[]): Promise<ValueResult> {
return this._baseIndexer.getStorageValue(
storageLayout,
blockHash,
contractAddress,
variable,
...mappingKeys
);
}
async pushToIPFS (data: any): Promise<void> {
await this._baseIndexer.pushToIPFS(data);
}

View File

@ -11,7 +11,7 @@ import { ethers } from 'ethers';
import { BaseProvider } from '@ethersproject/providers';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { StorageLayout } from '@vulcanize/solidity-mapper';
import { MappingKey, StorageLayout } from '@vulcanize/solidity-mapper';
import { IndexerInterface, Indexer as BaseIndexer, ValueResult, UNKNOWN_EVENT_NAME, JobQueue, Where, QueryOptions, ServerConfig } from '@vulcanize/util';
import { Database } from './database';
@ -80,6 +80,10 @@ export class Indexer implements IndexerInterface {
return this._serverConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return new Map([['ERC20', this._storageLayout]]);
}
async init (): Promise<void> {
await this._baseIndexer.fetchContracts();
}
@ -234,6 +238,16 @@ export class Indexer implements IndexerInterface {
return result;
}
async getStorageValue (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: MappingKey[]): Promise<ValueResult> {
return this._baseIndexer.getStorageValue(
storageLayout,
blockHash,
contractAddress,
variable,
...mappingKeys
);
}
async triggerIndexingOnEvent (event: Event): Promise<void> {
const { eventName, eventInfo, contract: token, block: { blockHash } } = event;
const eventFields = JSON.parse(eventInfo);

View File

@ -12,7 +12,7 @@ import { JsonFragment } from '@ethersproject/abi';
import { BaseProvider } from '@ethersproject/providers';
import * as codec from '@ipld/dag-cbor';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { StorageLayout } from '@vulcanize/solidity-mapper';
import { MappingKey, StorageLayout } from '@vulcanize/solidity-mapper';
import {
IPLDIndexer as BaseIndexer,
IPLDIndexerInterface,
@ -128,6 +128,10 @@ export class Indexer implements IPLDIndexerInterface {
return this._serverConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
async init (): Promise<void> {
await this._baseIndexer.fetchContracts();
await this._baseIndexer.fetchIPLDStatus();
@ -673,6 +677,16 @@ export class Indexer implements IPLDIndexerInterface {
return result;
}
async getStorageValue (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: MappingKey[]): Promise<ValueResult> {
return this._baseIndexer.getStorageValue(
storageLayout,
blockHash,
contractAddress,
variable,
...mappingKeys
);
}
async pushToIPFS (data: any): Promise<void> {
await this._baseIndexer.pushToIPFS(data);
}

View File

@ -25,7 +25,8 @@ import {
toEthereumValue,
resolveEntityFieldConflicts,
getEthereumTypes,
jsonFromBytes
jsonFromBytes,
getStorageValueType
} from './utils';
import { Database } from './database';
@ -236,6 +237,41 @@ export const instantiate = async (
const [decoded] = utils.defaultAbiCoder.decode([typesString], dataString);
return toEthereumValue(instanceExports, utils.ParamType.from(typesString), decoded);
},
'ethereum.storageValue': async (contractName: number, contractAddress: number, variable: number, mappingKeys: number) => {
const contractNameString = __getString(contractName);
const address = await Address.wrap(contractAddress);
const addressStringPtr = await address.toHexString();
const addressString = __getString(addressStringPtr);
const variableString = __getString(variable);
const mappingKeyPtrs = __getArray(mappingKeys);
const mappingKeyPromises = mappingKeyPtrs.map(async mappingKeyPtr => {
const ethereumValue = await ethereum.Value.wrap(mappingKeyPtr);
return fromEthereumValue(instanceExports, ethereumValue);
});
const mappingKeyValues = await Promise.all(mappingKeyPromises);
const storageLayout = indexer.storageLayoutMap.get(contractNameString);
assert(storageLayout);
assert(context.block);
const result = await indexer.getStorageValue(
storageLayout,
context.block.blockHash,
addressString,
variableString,
...mappingKeyValues
);
const storageValueType = getStorageValueType(storageLayout, variableString, mappingKeyValues);
return toEthereumValue(
instanceExports,
storageValueType,
result.value
);
}
},
conversion: {

View File

@ -0,0 +1,80 @@
//
// Copyright 2022 Vulcanize, Inc.
//
import assert from 'assert';
import path from 'path';
import { BaseProvider } from '@ethersproject/providers';
import { instantiate } from './loader';
import exampleAbi from '../test/subgraph/example1/build/Example1/abis/Example1.json';
import { storageLayout } from '../test/artifacts/Example1.json';
import { getTestDatabase, getTestIndexer, getTestProvider, getDummyEventData } from '../test/utils';
import { Database } from './database';
import { Indexer } from '../test/utils/indexer';
import { EventData } from './utils';
xdescribe('storage-call wasm tests', () => {
let exports: any;
let db: Database;
let indexer: Indexer;
let provider: BaseProvider;
const contractAddress = process.env.EXAMPLE_CONTRACT_ADDRESS;
assert(contractAddress);
const data = {
abis: {
Example1: exampleAbi
},
dataSource: {
address: contractAddress,
network: 'mainnet'
}
};
let dummyEventData: EventData;
before(async () => {
db = getTestDatabase();
indexer = getTestIndexer(new Map([['Example1', storageLayout]]));
provider = getTestProvider();
// Create dummy test data.
dummyEventData = await getDummyEventData();
});
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,
{
block: dummyEventData.block,
contractAddress
},
filePath,
data
);
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 execute contract getStorageValue function', async () => {
const { testGetStorageValue } = exports;
await testGetStorageValue();
});
it('should execute getStorageValue function for mapping type variable', async () => {
const { testMapStorageValue } = exports;
await testMapStorageValue();
});
});

View File

@ -4,10 +4,12 @@ import fs from 'fs-extra';
import debug from 'debug';
import yaml from 'js-yaml';
import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
import assert from 'assert';
import { GraphDecimal } from '@vulcanize/util';
import { TypeId, EthereumValueKind, ValueKind } from './types';
import { MappingKey, StorageLayout } from '@vulcanize/solidity-mapper';
const log = debug('vulcanize:utils');
@ -767,3 +769,32 @@ export const jsonFromBytes = async (instanceExports: any, bytesPtr: number): Pro
return jsonValue;
};
export const getStorageValueType = (storageLayout: StorageLayout, variableString: string, mappingKeys: MappingKey[]): utils.ParamType => {
const storage = storageLayout.storage.find(({ label }) => label === variableString);
assert(storage);
return getEthereumType(storageLayout.types, storage.type, mappingKeys);
};
const getEthereumType = (storageTypes: StorageLayout['types'], type: string, mappingKeys: MappingKey[]): utils.ParamType => {
const { label, encoding, members, value } = storageTypes[type];
if (encoding === 'mapping') {
assert(value);
return getEthereumType(storageTypes, value, mappingKeys.slice(1));
}
// Struct type contains members field.
if (members) {
const mappingKey = mappingKeys.shift();
const member = members.find(({ label }) => label === mappingKey);
assert(member);
const { type } = member;
return getEthereumType(storageTypes, type, mappingKeys);
}
return utils.ParamType.from(label);
};

1
packages/graph-node/test/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
!artifacts

View File

@ -0,0 +1,183 @@
{
"abi": [
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "string",
"name": "param1",
"type": "string"
},
{
"indexed": false,
"internalType": "uint8",
"name": "param2",
"type": "uint8"
},
{
"indexed": false,
"internalType": "uint256",
"name": "param3",
"type": "uint256"
}
],
"name": "Test",
"type": "event"
},
{
"inputs": [
{
"internalType": "uint128",
"name": "bidAmount1",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "bidAmount2",
"type": "uint128"
}
],
"name": "addMethod",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "addressUintMap",
"outputs": [
{
"internalType": "uint128",
"name": "",
"type": "uint128"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "emitEvent",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "getMethod",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint128",
"name": "bidAmount1",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "bidAmount2",
"type": "uint128"
}
],
"name": "structMethod",
"outputs": [
{
"components": [
{
"internalType": "uint128",
"name": "bidAmount1",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "bidAmount2",
"type": "uint128"
}
],
"internalType": "struct Example.Bid",
"name": "",
"type": "tuple"
}
],
"stateMutability": "pure",
"type": "function"
}
],
"storageLayout": {
"storage": [
{
"astId": 3,
"contract": "Example.sol:Example",
"label": "_test",
"offset": 0,
"slot": "0",
"type": "t_uint256"
},
{
"astId": 7,
"contract": "Example.sol:Example",
"label": "addressUintMap",
"offset": 0,
"slot": "1",
"type": "t_mapping(t_address,t_uint128)"
}
],
"types": {
"t_address": {
"encoding": "inplace",
"label": "address",
"numberOfBytes": "20"
},
"t_mapping(t_address,t_uint128)": {
"encoding": "mapping",
"key": "t_address",
"label": "mapping(address => uint128)",
"numberOfBytes": "32",
"value": "t_uint128"
},
"t_uint128": {
"encoding": "inplace",
"label": "uint128",
"numberOfBytes": "16"
},
"t_uint256": {
"encoding": "inplace",
"label": "uint256",
"numberOfBytes": "32"
}
}
}
}

View File

@ -5,6 +5,8 @@ pragma solidity ^0.8.0;
contract Example {
uint256 private _test;
mapping (address => uint128) public addressUintMap;
struct Bid {
uint128 bidAmount1;
uint128 bidAmount2;
@ -12,6 +14,11 @@ contract Example {
event Test(string param1, uint8 param2, uint256 param3);
constructor() {
_test = 1;
addressUintMap[address(0)] = 123;
}
function getMethod() public view virtual returns (string memory)
{
return 'test';

View File

@ -0,0 +1 @@
@graphprotocol:registry=https://npm.pkg.github.com

View File

@ -10,7 +10,7 @@
"deploy-local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 example1"
},
"dependencies": {
"@graphprotocol/graph-ts": "^0.22.1",
"@graphprotocol/graph-ts": "npm:@vulcanize/graph-ts@0.22.1",
"@vulcanize/graph-cli": "0.22.5"
}
}

View File

@ -159,6 +159,27 @@ export function testStructEthCall (): void {
}
}
export function testGetStorageValue (): void {
log.debug('In test get storage value', []);
// Bind the contract to the address.
const contractAddress = dataSource.address();
const contract = Example1.bind(contractAddress);
const res = contract.getStorageValue('_test', []);
log.debug('Storage call result: {}', [res!.toBigInt().toString()]);
}
export function testMapStorageValue (): void {
log.debug('In test map storage value', []);
// Bind the contract to the address.
const contractAddress = dataSource.address();
const contract = Example1.bind(contractAddress);
const addressValue = ethereum.Value.fromAddress(Address.zero());
const res = contract.getStorageValue('addressUintMap', [addressValue]);
log.debug('Storage call result: {}', [res!.toBigInt().toString()]);
}
export function testBytesToHex (): string {
log.debug('In test bytesToHex', []);

View File

@ -23,10 +23,10 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
"@graphprotocol/graph-ts@^0.22.1":
"@graphprotocol/graph-ts@npm:@vulcanize/graph-ts@0.22.1":
version "0.22.1"
resolved "https://registry.yarnpkg.com/@graphprotocol/graph-ts/-/graph-ts-0.22.1.tgz#3189b2495b33497280f617316cce68074d48e236"
integrity sha512-T5xrHN0tHJwd7ZnSTLhk5hAL3rCIp6rJ40kBCrETnv1mfK9hVyoojJK6VtBQXTbLsYtKe4SYjjD0cdOsAR9QiA==
resolved "https://npm.pkg.github.com/download/@vulcanize/graph-ts/0.22.1/7a14baaab8b99d4a88e19620dc7200aa501fbecf#7a14baaab8b99d4a88e19620dc7200aa501fbecf"
integrity sha512-0CoKeFezskYjAsLmqfdxmS7q+gWy1V1wFgiNB4tMJSa2EiPTVG62qlPKkqTduApK2gZX9//rmE5Vb2xcF/v2+w==
dependencies:
assemblyscript "0.19.10"

View File

@ -4,12 +4,15 @@
import { BaseProvider } from '@ethersproject/providers';
import { getCustomProvider } from '@vulcanize/util';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { StorageLayout } from '@vulcanize/solidity-mapper';
import { EventData } from '../../src/utils';
import { Database } from '../../src/database';
import { Indexer } from './indexer';
const NETWORK_URL = 'http://127.0.0.1:8081';
const IPLD_ETH_SERVER_GQL_URL = 'http://127.0.0.1:8082/graphql';
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
export const ZERO_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000';
@ -71,8 +74,13 @@ export const getTestDatabase = (): Database => {
return new Database({ type: 'postgres' }, '');
};
export const getTestIndexer = (): Indexer => {
return new Indexer();
export const getTestIndexer = (storageLayout?: Map<string, StorageLayout>): Indexer => {
const ethClient = new EthClient({
gqlEndpoint: IPLD_ETH_SERVER_GQL_URL,
cache: undefined
});
return new Indexer(ethClient, storageLayout);
};
export const getTestProvider = (): BaseProvider => {

View File

@ -6,14 +6,43 @@ import {
BlockProgressInterface,
EventInterface,
SyncStatusInterface,
ServerConfig as ServerConfigInterface
ServerConfig as ServerConfigInterface,
ValueResult
} from '@vulcanize/util';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { GetStorageAt, getStorageValue, MappingKey, StorageLayout } from '@vulcanize/solidity-mapper';
export class Indexer implements IndexerInterface {
_getStorageAt: GetStorageAt;
_storageLayoutMap: Map<string, StorageLayout> = new Map()
constructor (ethClient: EthClient, storageLayoutMap?: Map<string, StorageLayout>) {
this._getStorageAt = ethClient.getStorageAt.bind(ethClient);
if (storageLayoutMap) {
this._storageLayoutMap = storageLayoutMap;
}
}
get serverConfig () {
return new ServerConfig();
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
async getStorageValue (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: MappingKey[]): Promise<ValueResult> {
return getStorageValue(
storageLayout,
this._getStorageAt,
blockHash,
contractAddress,
variable,
...mappingKeys
);
}
async getBlockProgress (blockHash: string): Promise<BlockProgressInterface | undefined> {
assert(blockHash);

View File

@ -12,7 +12,7 @@ import { JsonFragment } from '@ethersproject/abi';
import { BaseProvider } from '@ethersproject/providers';
import * as codec from '@ipld/dag-cbor';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { StorageLayout } from '@vulcanize/solidity-mapper';
import { StorageLayout, MappingKey } from '@vulcanize/solidity-mapper';
import {
IPLDIndexer as BaseIndexer,
ValueResult,
@ -141,6 +141,10 @@ export class Indexer implements IPLDIndexerInterface {
return this._serverConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
async init (): Promise<void> {
await this._baseIndexer.fetchContracts();
await this._baseIndexer.fetchIPLDStatus();
@ -266,6 +270,16 @@ export class Indexer implements IPLDIndexerInterface {
return result;
}
async getStorageValue (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: MappingKey[]): Promise<ValueResult> {
return this._baseIndexer.getStorageValue(
storageLayout,
blockHash,
contractAddress,
variable,
...mappingKeys
);
}
async pushToIPFS (data: any): Promise<void> {
await this._baseIndexer.pushToIPFS(data);
}

View File

@ -12,7 +12,7 @@ import { JsonFragment } from '@ethersproject/abi';
import { JsonRpcProvider } from '@ethersproject/providers';
import * as codec from '@ipld/dag-cbor';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { StorageLayout } from '@vulcanize/solidity-mapper';
import { MappingKey, StorageLayout } from '@vulcanize/solidity-mapper';
import {
IPLDIndexer as BaseIndexer,
IPLDIndexerInterface,
@ -133,6 +133,10 @@ export class Indexer implements IPLDIndexerInterface {
return this._serverConfig;
}
get storageLayoutMap (): Map<string, StorageLayout> {
return this._storageLayoutMap;
}
async init (): Promise<void> {
await this._baseIndexer.fetchContracts();
await this._baseIndexer.fetchIPLDStatus();
@ -400,6 +404,16 @@ export class Indexer implements IPLDIndexerInterface {
} as any;
}
async getStorageValue (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: MappingKey[]): Promise<ValueResult> {
return this._baseIndexer.getStorageValue(
storageLayout,
blockHash,
contractAddress,
variable,
...mappingKeys
);
}
async pushToIPFS (data: any): Promise<void> {
await this._baseIndexer.pushToIPFS(data);
}

View File

@ -2,6 +2,6 @@
// Copyright 2021 Vulcanize, Inc.
//
export { getStorageValue, getStorageInfo, getValueByType, StorageLayout, GetStorageAt } from './storage';
export { getStorageValue, getStorageInfo, getValueByType, StorageLayout, GetStorageAt, MappingKey } from './storage';
export { getEventNameTopics } from './logs';

View File

@ -23,7 +23,7 @@ interface Types {
};
}
type MappingKey = string | boolean | number;
export type MappingKey = string | boolean | number;
export interface StorageLayout {
storage: Storage[];

View File

@ -3,10 +3,12 @@
//
import { Connection, DeepPartial, FindConditions, FindManyOptions, QueryRunner } from 'typeorm';
import { MappingKey, StorageLayout } from '@vulcanize/solidity-mapper';
import { ServerConfig } from './config';
import { Where, QueryOptions } from './database';
import { IpldStatus } from './ipld-indexer';
import { ValueResult } from './indexer';
export enum StateKind {
Diff = 'diff',
@ -80,6 +82,7 @@ export interface IPLDBlockInterface {
export interface IndexerInterface {
readonly serverConfig: ServerConfig
readonly storageLayoutMap: Map<string, StorageLayout>
getBlockProgress (blockHash: string): Promise<BlockProgressInterface | undefined>
getBlockProgressEntities (where: FindConditions<BlockProgressInterface>, options: FindManyOptions<BlockProgressInterface>): Promise<BlockProgressInterface[]>
getEvent (id: string): Promise<EventInterface | undefined>
@ -108,6 +111,7 @@ export interface IndexerInterface {
processStateCheckpoint?: (contractAddress: string, blockHash: string) => Promise<boolean>
processBlock?: (blockHash: string, blockNumber: number) => Promise<void>
processBlockAfterEvents?: (blockHash: string) => Promise<void>
getStorageValue (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: MappingKey[]): Promise<ValueResult>
}
export interface IPLDIndexerInterface extends IndexerInterface {