Skip to content

Commit

Permalink
feat: add propertiesAggregatorService (#289)
Browse files Browse the repository at this point in the history
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
jstashh committed May 31, 2022
1 parent ad79b72 commit 1db57e4
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export enum ContractAddressId {
zapperZapIn = "ZAPPER_ZAP_IN",
zapperZapOut = "ZAPPER_ZAP_OUT",
pickleZapIn = "PICKLE_ZAP_IN",
propertiesAggregator = "PROPERTIES_AGGREGATOR",
unused = "UNUSED",
}

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export { RegistryV2Adapter } from "./services/adapters/registry";
export { AssetService } from "./services/assets";
export { LensService } from "./services/lens";
export { OracleService } from "./services/oracle";
export { PropertiesAggregatorService } from "./services/propertiesAggregator";
export { SubgraphService } from "./services/subgraph";
export { TelegramService } from "./services/telegram";
export { VisionService } from "./services/vision";
Expand Down
82 changes: 82 additions & 0 deletions src/services/propertiesAggregator.spec.ts
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);
});
});
});
59 changes: 59 additions & 0 deletions src/services/propertiesAggregator.ts
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);
}
}
3 changes: 3 additions & 0 deletions src/yearn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { MetaService } from "./services/meta";
import { OracleService } from "./services/oracle";
import { PartnerService } from "./services/partner";
import { PickleService } from "./services/partners/pickle";
import { PropertiesAggregatorService } from "./services/propertiesAggregator";
import { SubgraphService } from "./services/subgraph";
import { TelegramService } from "./services/telegram";
import { TransactionService } from "./services/transaction";
Expand Down Expand Up @@ -47,6 +48,7 @@ type ServicesType<T extends ChainId> = {
pickle: PickleService;
helper: HelperService<T>;
partner?: PartnerService<T>;
propertiesAggregator: PropertiesAggregatorService<T>;
};

/**
Expand Down Expand Up @@ -163,6 +165,7 @@ export class Yearn<T extends ChainId> {
allowList: allowlistService,
transaction: new TransactionService(chainId, ctx, allowlistService),
partner: ctx.partnerId ? new PartnerService(chainId, ctx, addressProvider, ctx.partnerId) : undefined,
propertiesAggregator: new PropertiesAggregatorService(chainId, ctx, addressProvider),
};
}

Expand Down

0 comments on commit 1db57e4

Please sign in to comment.