-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add propertiesAggregatorService (#289)
Description Add a new PropertiesAggregatorService which can aggregate multiple property queries on a contract into a single call, with the limitation that there can be no arguments in the properties. This can be used, for example, for fetching multiple properties of a vault at once, while being flexible for adding/removing the properties queried in the future. Corresponding PR of where the smart contract functionality has been added to lens - yearn/yearn-lens#55 Motivation and Context Adding more functionality to the SDK for rendering data about vaults/strategies. This will be followed by adding a function to the vaults interface that supports fetching information about a vault in one call, e.g. name, fees, totalAssets etc How Has This Been Tested? Added unit tests
- Loading branch information
Showing
5 changed files
with
146 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { ParamType } from "@ethersproject/abi"; | ||
|
||
import { ChainId } from "../chain"; | ||
import { ContractAddressId } from "../common"; | ||
import { Context } from "../context"; | ||
import { AddressProvider } from "./addressProvider"; | ||
import { PropertiesAggregatorService } from "./propertiesAggregator"; | ||
|
||
const targetAddress = "0x5D7201c10AfD0Ed1a1F408E321Ef0ebc7314B086"; | ||
|
||
jest.mock("./addressProvider", () => ({ | ||
AddressProvider: jest.fn().mockImplementation(() => ({ | ||
addressById: jest.fn().mockResolvedValue(targetAddress), | ||
})), | ||
})); | ||
|
||
jest.mock("@ethersproject/abi", () => { | ||
const original = jest.requireActual("@ethersproject/abi"); | ||
return { | ||
...original, | ||
defaultAbiCoder: { | ||
decode: jest.fn().mockReturnValue("decoded"), | ||
}, | ||
}; | ||
}); | ||
|
||
describe("PropertiesAggregatorService", () => { | ||
let propertiesAggregatorService: PropertiesAggregatorService<1>; | ||
let mockedAddressProvider: AddressProvider<ChainId>; | ||
let context: Context; | ||
let getPropertyMock: jest.Mock; | ||
let getPropertiesMock: jest.Mock; | ||
|
||
beforeEach(() => { | ||
mockedAddressProvider = new (AddressProvider as unknown as jest.Mock<AddressProvider<ChainId>>)(); | ||
context = new Context({}); | ||
propertiesAggregatorService = new PropertiesAggregatorService(1, context, mockedAddressProvider); | ||
getPropertyMock = jest.fn(); | ||
getPropertiesMock = jest.fn(); | ||
Object.defineProperty(propertiesAggregatorService, "_getContract", { | ||
value: jest.fn().mockResolvedValue({ | ||
read: { getProperty: getPropertyMock, getProperties: getPropertiesMock }, | ||
}), | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
describe("address provider type", () => { | ||
it("should be properties aggregator service contract id", () => { | ||
expect(PropertiesAggregatorService.contractId).toEqual(ContractAddressId.propertiesAggregator); | ||
}); | ||
}); | ||
|
||
describe("getProperty", () => { | ||
it("should call the properties aggregator contract's getProperty method", async () => { | ||
const propertyName = "name"; | ||
const paramType = ParamType.from(`string ${propertyName}`); | ||
await propertiesAggregatorService.getProperty(targetAddress, paramType); | ||
|
||
expect(getPropertyMock).toHaveBeenCalledTimes(1); | ||
expect(getPropertyMock).toHaveBeenCalledWith(targetAddress, propertyName); | ||
}); | ||
}); | ||
|
||
describe("getProperties", () => { | ||
beforeEach(() => { | ||
getPropertiesMock.mockReturnValue(["firstResult", "secondResult"]); | ||
}); | ||
|
||
it("should call the properties aggregator contract's getProperties method", async () => { | ||
const paramTypes = [ParamType.from("string name"), ParamType.from("uint256 totalAssets")]; | ||
await propertiesAggregatorService.getProperties(targetAddress, paramTypes); | ||
const propertyNames = paramTypes.map((type) => type.name); | ||
|
||
expect(getPropertiesMock).toHaveBeenCalledTimes(1); | ||
expect(getPropertiesMock).toHaveBeenCalledWith(targetAddress, propertyNames); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { defaultAbiCoder, ParamType } from "@ethersproject/abi"; | ||
import { BigNumber } from "@ethersproject/bignumber"; | ||
|
||
import { ChainId } from "../chain"; | ||
import { ContractAddressId, ContractService, WrappedContract } from "../common"; | ||
import { Address } from "../types"; | ||
|
||
type DecodingType = string | BigNumber; | ||
|
||
/** | ||
* [[PropertiesAggregatorService]] allows queries of a contract's methods to be aggregated into one | ||
* call, with the limitation that none of the methods can have arguments. Method names are dynamically provided | ||
* in order to provide flexibility for easily adding or removing property queries in the future | ||
*/ | ||
export class PropertiesAggregatorService<T extends ChainId> extends ContractService<T> { | ||
static abi = [ | ||
"function getProperty(address target, string calldata name) public view returns (bytes memory)", | ||
"function getProperties(address target, string[] calldata names) public view returns (bytes[] memory)", | ||
]; | ||
static contractId = ContractAddressId.propertiesAggregator; | ||
|
||
get contract(): Promise<WrappedContract> { | ||
return this._getContract(PropertiesAggregatorService.abi, PropertiesAggregatorService.contractId, this.ctx); | ||
} | ||
|
||
/** | ||
* Fetches a single property from the target contract, assuming no arguments are used for the property | ||
* @param target The target contract to perform the call on | ||
* @param paramType Ethers' `ParamType` object that contains data about the method to call e.g. ParamType.from("string name") | ||
* @returns The decoded result of the property query | ||
*/ | ||
async getProperty(target: Address, paramType: ParamType): Promise<DecodingType> { | ||
const contract = await this.contract; | ||
const data = await contract.read.getProperty(target, paramType.name); | ||
const decoded = defaultAbiCoder.decode([paramType.type], data)[0]; | ||
|
||
return Promise.resolve(decoded); | ||
} | ||
|
||
/** | ||
* Simultaneously fetches multiple properties from the target contract, assuming no arguments are used for each property | ||
* @param target The target contract to perform the call on | ||
* @param paramTypes An array of Ethers' `ParamType` object that contains data about the method to call e.g. ParamType.from("string name") | ||
* @returns An object with the inputted property names as keys, and corresponding decoded data as values | ||
*/ | ||
async getProperties(target: Address, paramTypes: ParamType[]): Promise<Record<string, DecodingType>> { | ||
const contract = await this.contract; | ||
const names = paramTypes.map((param) => param.name); | ||
const data = await contract.read.getProperties(target, names); | ||
|
||
const result: Record<string, DecodingType> = {}; | ||
paramTypes.forEach((paramType, index) => { | ||
const datum = data[index]; | ||
const decoded = defaultAbiCoder.decode([paramType.type], datum)[0]; | ||
result[paramType.name] = decoded; | ||
}); | ||
return Promise.resolve(result); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters