Skip to content
This repository has been archived by the owner on Jan 24, 2024. It is now read-only.

Commit

Permalink
feat(angle): Add vaults on Arbitrum (#3158)
Browse files Browse the repository at this point in the history
  • Loading branch information
wpoulin authored Dec 18, 2023
1 parent b344f29 commit f2494ac
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 137 deletions.
7 changes: 5 additions & 2 deletions src/apps/angle/angle.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Module } from '@nestjs/common';

import { AbstractApp } from '~app/app.dynamic-module';

import { AngleApiHelper } from './common/angle.api';
import { ArbitrumAngleVaultsContractPositionFetcher } from './arbitrum/angle.vault.contract-position-fetcher';
import { AnglePositionResolver } from './common/angle.position-resolver';
import { AngleViemContractFactory } from './contracts';
import { EthereumAngleSanTokenTokenFetcher } from './ethereum/angle.san-token.token-fetcher';
import { EthereumAngleVaultsContractPositionFetcher } from './ethereum/angle.vault.contract-position-fetcher';
Expand All @@ -11,7 +12,9 @@ import { EthereumAngleVeAngleContractPositionFetcher } from './ethereum/angle.vo
@Module({
providers: [
AngleViemContractFactory,
AngleApiHelper,
AnglePositionResolver,
// Arbitrum
ArbitrumAngleVaultsContractPositionFetcher,
// Ethereum
EthereumAngleSanTokenTokenFetcher,
EthereumAngleVeAngleContractPositionFetcher,
Expand Down
86 changes: 86 additions & 0 deletions src/apps/angle/arbitrum/angle.vault.contract-position-fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Inject } from '@nestjs/common';
import { BigNumberish } from 'ethers';
import { sum } from 'lodash';

import { IAppToolkit, APP_TOOLKIT } from '~app-toolkit/app-toolkit.interface';
import { PositionTemplate } from '~app-toolkit/decorators/position-template.decorator';
import { getLabelFromToken } from '~app-toolkit/helpers/presentation/image.present';
import { MetaType } from '~position/position.interface';
import { ContractPositionTemplatePositionFetcher } from '~position/template/contract-position.template.position-fetcher';
import {
DefaultContractPositionDefinition,
GetDisplayPropsParams,
GetTokenBalancesParams,
GetTokenDefinitionsParams,
} from '~position/template/contract-position.template.types';

import { AnglePositionResolver } from '../common/angle.position-resolver';
import { AngleViemContractFactory } from '../contracts';
import { AngleVaultManager } from '../contracts/viem';

@PositionTemplate()
export class ArbitrumAngleVaultsContractPositionFetcher extends ContractPositionTemplatePositionFetcher<AngleVaultManager> {
groupLabel = 'Vaults';

constructor(
@Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit,
@Inject(AngleViemContractFactory) protected readonly contractFactory: AngleViemContractFactory,
@Inject(AnglePositionResolver) protected readonly anglePositionResolver: AnglePositionResolver,
) {
super(appToolkit);
}

getContract(address: string) {
return this.contractFactory.angleVaultManager({ address, network: this.network });
}

async getDefinitions(): Promise<DefaultContractPositionDefinition[]> {
const vaultAddresses = await this.anglePositionResolver.getVaultManagers(this.network);
return vaultAddresses.map(address => {
return {
address,
};
});
}

async getTokenDefinitions({ contract }: GetTokenDefinitionsParams<AngleVaultManager>) {
return [
{ metaType: MetaType.SUPPLIED, address: await contract.read.collateral(), network: this.network },
{ metaType: MetaType.BORROWED, address: await contract.read.stablecoin(), network: this.network },
];
}

async getLabel({ contractPosition }: GetDisplayPropsParams<AngleVaultManager>): Promise<string> {
return `${getLabelFromToken(contractPosition.tokens[0])} - ${getLabelFromToken(contractPosition.tokens[1])}`;
}

async getTokenBalancesPerPosition({
address,
contract,
}: GetTokenBalancesParams<AngleVaultManager>): Promise<BigNumberish[]> {
const userBalanceCount = await contract.read.balanceOf([address]);

if (Number(userBalanceCount) < 1) return [0, 0];

const vaultIds = Object.values(await this.anglePositionResolver.getVaultIds(address, this.network));

const balances = await Promise.all(
vaultIds.map(async vaultId => {
const [collateralData, vaultDebt] = await Promise.all([
contract.read.vaultData([BigInt(vaultId)]),
contract.read.getVaultDebt([BigInt(vaultId)]),
]);

return {
supplied: collateralData[0],
borrowed: vaultDebt,
};
}),
);

const supplied = sum(balances.map(x => Number(x.supplied)));
const borrowed = sum(balances.map(x => Number(x.borrowed)));

return [supplied, borrowed];
}
}
111 changes: 0 additions & 111 deletions src/apps/angle/common/angle.api.ts

This file was deleted.

66 changes: 66 additions & 0 deletions src/apps/angle/common/angle.position-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import querystring from 'node:querystring';

import { Injectable } from '@nestjs/common';
import Axios from 'axios';

import { Cache } from '~cache/cache.decorator';
import { NETWORK_IDS, Network } from '~types/network.interface';

const BASE_URL = 'https://api.angle.money/v1';

type TVaultManager = {
address: string;
};

type TVault = {
address: string;
id: number;
};

@Injectable()
export class AnglePositionResolver {
private async callAngleApi<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
let url = `${BASE_URL}/${endpoint}`;
if (params) {
url += `?${querystring.stringify(params)}`;
}
const data = await Axios.get<T>(url).then(v => v.data);
return data;
}

@Cache({
key: (network: Network) => `studio:angle:vaultmanagers:${network}:angle`,
ttl: 15 * 60,
})
async getVaultManagers(network: Network) {
const vaultManagers = await this.callAngleApi<Record<string, TVaultManager>>('vaultManagers', {
chainId: NETWORK_IDS[network],
});

return Object.values(vaultManagers).map(x => x.address);
}

@Cache({
key: (network: Network) => `studio:angle:vaults:${network}:angle`,
ttl: 15 * 60,
})
async getVaultIds(address: string, network: Network) {
const userVaultDataRaw = await this.callAngleApi<Record<string, TVault>>('vaults', {
chainId: NETWORK_IDS[network],
user: address,
});

return Object.values(userVaultDataRaw).map(userVault => userVault.id);
}

@Cache({
key: (network: Network) => `studio:angle:rewardsdata:${network}:angle`,
ttl: 30 * 60,
})
async getRewardsData(address: string, network: Network) {
return this.callAngleApi<{ rewardsData: { totalClaimable: number } }>('dao', {
chainId: NETWORK_IDS[network],
user: address,
});
}
}
2 changes: 0 additions & 2 deletions src/apps/angle/ethereum/angle.san-token.token-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
GetUnderlyingTokensParams,
} from '~position/template/app-token.template.types';

import { AngleApiHelper } from '../common/angle.api';
import { AngleViemContractFactory } from '../contracts';
import { AngleSanToken } from '../contracts/viem';

Expand All @@ -23,7 +22,6 @@ export class EthereumAngleSanTokenTokenFetcher extends AppTokenTemplatePositionF
constructor(
@Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit,
@Inject(AngleViemContractFactory) protected readonly contractFactory: AngleViemContractFactory,
@Inject(AngleApiHelper) protected readonly angleApiHelper: AngleApiHelper,
) {
super(appToolkit);
}
Expand Down
32 changes: 13 additions & 19 deletions src/apps/angle/ethereum/angle.vault.contract-position-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,27 @@ import { sum } from 'lodash';
import { IAppToolkit, APP_TOOLKIT } from '~app-toolkit/app-toolkit.interface';
import { PositionTemplate } from '~app-toolkit/decorators/position-template.decorator';
import { getLabelFromToken } from '~app-toolkit/helpers/presentation/image.present';
import { DefaultDataProps } from '~position/display.interface';
import { MetaType } from '~position/position.interface';
import { ContractPositionTemplatePositionFetcher } from '~position/template/contract-position.template.position-fetcher';
import {
DefaultContractPositionDefinition,
GetDisplayPropsParams,
GetTokenBalancesParams,
GetTokenDefinitionsParams,
} from '~position/template/contract-position.template.types';

import { AngleApiHelper } from '../common/angle.api';
import { AnglePositionResolver } from '../common/angle.position-resolver';
import { AngleViemContractFactory } from '../contracts';
import { AngleVaultManager } from '../contracts/viem';

export type AngleVaultDefinition = {
address: string;
collateral: string;
stablecoin: string;
};

@PositionTemplate()
export class EthereumAngleVaultsContractPositionFetcher extends ContractPositionTemplatePositionFetcher<
AngleVaultManager,
DefaultDataProps,
AngleVaultDefinition
> {
export class EthereumAngleVaultsContractPositionFetcher extends ContractPositionTemplatePositionFetcher<AngleVaultManager> {
groupLabel = 'Vaults';

constructor(
@Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit,
@Inject(AngleViemContractFactory) protected readonly contractFactory: AngleViemContractFactory,
@Inject(AngleApiHelper) protected readonly angleApiHelper: AngleApiHelper,
@Inject(AnglePositionResolver) protected readonly anglePositionResolver: AnglePositionResolver,
) {
super(appToolkit);
}
Expand All @@ -44,12 +34,16 @@ export class EthereumAngleVaultsContractPositionFetcher extends ContractPosition
return this.contractFactory.angleVaultManager({ address, network: this.network });
}

async getDefinitions() {
const vaultManagers = Object.values(await this.angleApiHelper.getVaultManagers(this.network));
return vaultManagers;
async getDefinitions(): Promise<DefaultContractPositionDefinition[]> {
const vaultAddresses = await this.anglePositionResolver.getVaultManagers(this.network);
return vaultAddresses.map(address => {
return {
address,
};
});
}

async getTokenDefinitions({ contract }: GetTokenDefinitionsParams<AngleVaultManager, AngleVaultDefinition>) {
async getTokenDefinitions({ contract }: GetTokenDefinitionsParams<AngleVaultManager>) {
return [
{ metaType: MetaType.SUPPLIED, address: await contract.read.collateral(), network: this.network },
{ metaType: MetaType.BORROWED, address: await contract.read.stablecoin(), network: this.network },
Expand All @@ -68,7 +62,7 @@ export class EthereumAngleVaultsContractPositionFetcher extends ContractPosition

if (Number(userBalanceCount) < 1) return [0, 0];

const vaultIds = Object.values(await this.angleApiHelper.getVaultIds(address, this.network));
const vaultIds = Object.values(await this.anglePositionResolver.getVaultIds(address, this.network));

const balances = await Promise.all(
vaultIds.map(async vaultId => {
Expand Down
Loading

0 comments on commit f2494ac

Please sign in to comment.