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

Commit f2494ac

Browse files
author
William Poulin
authored
feat(angle): Add vaults on Arbitrum (#3158)
1 parent b344f29 commit f2494ac

7 files changed

Lines changed: 173 additions & 137 deletions

src/apps/angle/angle.module.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { Module } from '@nestjs/common';
22

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

5-
import { AngleApiHelper } from './common/angle.api';
5+
import { ArbitrumAngleVaultsContractPositionFetcher } from './arbitrum/angle.vault.contract-position-fetcher';
6+
import { AnglePositionResolver } from './common/angle.position-resolver';
67
import { AngleViemContractFactory } from './contracts';
78
import { EthereumAngleSanTokenTokenFetcher } from './ethereum/angle.san-token.token-fetcher';
89
import { EthereumAngleVaultsContractPositionFetcher } from './ethereum/angle.vault.contract-position-fetcher';
@@ -11,7 +12,9 @@ import { EthereumAngleVeAngleContractPositionFetcher } from './ethereum/angle.vo
1112
@Module({
1213
providers: [
1314
AngleViemContractFactory,
14-
AngleApiHelper,
15+
AnglePositionResolver,
16+
// Arbitrum
17+
ArbitrumAngleVaultsContractPositionFetcher,
1518
// Ethereum
1619
EthereumAngleSanTokenTokenFetcher,
1720
EthereumAngleVeAngleContractPositionFetcher,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { Inject } from '@nestjs/common';
2+
import { BigNumberish } from 'ethers';
3+
import { sum } from 'lodash';
4+
5+
import { IAppToolkit, APP_TOOLKIT } from '~app-toolkit/app-toolkit.interface';
6+
import { PositionTemplate } from '~app-toolkit/decorators/position-template.decorator';
7+
import { getLabelFromToken } from '~app-toolkit/helpers/presentation/image.present';
8+
import { MetaType } from '~position/position.interface';
9+
import { ContractPositionTemplatePositionFetcher } from '~position/template/contract-position.template.position-fetcher';
10+
import {
11+
DefaultContractPositionDefinition,
12+
GetDisplayPropsParams,
13+
GetTokenBalancesParams,
14+
GetTokenDefinitionsParams,
15+
} from '~position/template/contract-position.template.types';
16+
17+
import { AnglePositionResolver } from '../common/angle.position-resolver';
18+
import { AngleViemContractFactory } from '../contracts';
19+
import { AngleVaultManager } from '../contracts/viem';
20+
21+
@PositionTemplate()
22+
export class ArbitrumAngleVaultsContractPositionFetcher extends ContractPositionTemplatePositionFetcher<AngleVaultManager> {
23+
groupLabel = 'Vaults';
24+
25+
constructor(
26+
@Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit,
27+
@Inject(AngleViemContractFactory) protected readonly contractFactory: AngleViemContractFactory,
28+
@Inject(AnglePositionResolver) protected readonly anglePositionResolver: AnglePositionResolver,
29+
) {
30+
super(appToolkit);
31+
}
32+
33+
getContract(address: string) {
34+
return this.contractFactory.angleVaultManager({ address, network: this.network });
35+
}
36+
37+
async getDefinitions(): Promise<DefaultContractPositionDefinition[]> {
38+
const vaultAddresses = await this.anglePositionResolver.getVaultManagers(this.network);
39+
return vaultAddresses.map(address => {
40+
return {
41+
address,
42+
};
43+
});
44+
}
45+
46+
async getTokenDefinitions({ contract }: GetTokenDefinitionsParams<AngleVaultManager>) {
47+
return [
48+
{ metaType: MetaType.SUPPLIED, address: await contract.read.collateral(), network: this.network },
49+
{ metaType: MetaType.BORROWED, address: await contract.read.stablecoin(), network: this.network },
50+
];
51+
}
52+
53+
async getLabel({ contractPosition }: GetDisplayPropsParams<AngleVaultManager>): Promise<string> {
54+
return `${getLabelFromToken(contractPosition.tokens[0])} - ${getLabelFromToken(contractPosition.tokens[1])}`;
55+
}
56+
57+
async getTokenBalancesPerPosition({
58+
address,
59+
contract,
60+
}: GetTokenBalancesParams<AngleVaultManager>): Promise<BigNumberish[]> {
61+
const userBalanceCount = await contract.read.balanceOf([address]);
62+
63+
if (Number(userBalanceCount) < 1) return [0, 0];
64+
65+
const vaultIds = Object.values(await this.anglePositionResolver.getVaultIds(address, this.network));
66+
67+
const balances = await Promise.all(
68+
vaultIds.map(async vaultId => {
69+
const [collateralData, vaultDebt] = await Promise.all([
70+
contract.read.vaultData([BigInt(vaultId)]),
71+
contract.read.getVaultDebt([BigInt(vaultId)]),
72+
]);
73+
74+
return {
75+
supplied: collateralData[0],
76+
borrowed: vaultDebt,
77+
};
78+
}),
79+
);
80+
81+
const supplied = sum(balances.map(x => Number(x.supplied)));
82+
const borrowed = sum(balances.map(x => Number(x.borrowed)));
83+
84+
return [supplied, borrowed];
85+
}
86+
}

src/apps/angle/common/angle.api.ts

Lines changed: 0 additions & 111 deletions
This file was deleted.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import querystring from 'node:querystring';
2+
3+
import { Injectable } from '@nestjs/common';
4+
import Axios from 'axios';
5+
6+
import { Cache } from '~cache/cache.decorator';
7+
import { NETWORK_IDS, Network } from '~types/network.interface';
8+
9+
const BASE_URL = 'https://api.angle.money/v1';
10+
11+
type TVaultManager = {
12+
address: string;
13+
};
14+
15+
type TVault = {
16+
address: string;
17+
id: number;
18+
};
19+
20+
@Injectable()
21+
export class AnglePositionResolver {
22+
private async callAngleApi<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
23+
let url = `${BASE_URL}/${endpoint}`;
24+
if (params) {
25+
url += `?${querystring.stringify(params)}`;
26+
}
27+
const data = await Axios.get<T>(url).then(v => v.data);
28+
return data;
29+
}
30+
31+
@Cache({
32+
key: (network: Network) => `studio:angle:vaultmanagers:${network}:angle`,
33+
ttl: 15 * 60,
34+
})
35+
async getVaultManagers(network: Network) {
36+
const vaultManagers = await this.callAngleApi<Record<string, TVaultManager>>('vaultManagers', {
37+
chainId: NETWORK_IDS[network],
38+
});
39+
40+
return Object.values(vaultManagers).map(x => x.address);
41+
}
42+
43+
@Cache({
44+
key: (network: Network) => `studio:angle:vaults:${network}:angle`,
45+
ttl: 15 * 60,
46+
})
47+
async getVaultIds(address: string, network: Network) {
48+
const userVaultDataRaw = await this.callAngleApi<Record<string, TVault>>('vaults', {
49+
chainId: NETWORK_IDS[network],
50+
user: address,
51+
});
52+
53+
return Object.values(userVaultDataRaw).map(userVault => userVault.id);
54+
}
55+
56+
@Cache({
57+
key: (network: Network) => `studio:angle:rewardsdata:${network}:angle`,
58+
ttl: 30 * 60,
59+
})
60+
async getRewardsData(address: string, network: Network) {
61+
return this.callAngleApi<{ rewardsData: { totalClaimable: number } }>('dao', {
62+
chainId: NETWORK_IDS[network],
63+
user: address,
64+
});
65+
}
66+
}

src/apps/angle/ethereum/angle.san-token.token-fetcher.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
GetUnderlyingTokensParams,
1313
} from '~position/template/app-token.template.types';
1414

15-
import { AngleApiHelper } from '../common/angle.api';
1615
import { AngleViemContractFactory } from '../contracts';
1716
import { AngleSanToken } from '../contracts/viem';
1817

@@ -23,7 +22,6 @@ export class EthereumAngleSanTokenTokenFetcher extends AppTokenTemplatePositionF
2322
constructor(
2423
@Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit,
2524
@Inject(AngleViemContractFactory) protected readonly contractFactory: AngleViemContractFactory,
26-
@Inject(AngleApiHelper) protected readonly angleApiHelper: AngleApiHelper,
2725
) {
2826
super(appToolkit);
2927
}

src/apps/angle/ethereum/angle.vault.contract-position-fetcher.ts

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,27 @@ import { sum } from 'lodash';
55
import { IAppToolkit, APP_TOOLKIT } from '~app-toolkit/app-toolkit.interface';
66
import { PositionTemplate } from '~app-toolkit/decorators/position-template.decorator';
77
import { getLabelFromToken } from '~app-toolkit/helpers/presentation/image.present';
8-
import { DefaultDataProps } from '~position/display.interface';
98
import { MetaType } from '~position/position.interface';
109
import { ContractPositionTemplatePositionFetcher } from '~position/template/contract-position.template.position-fetcher';
1110
import {
11+
DefaultContractPositionDefinition,
1212
GetDisplayPropsParams,
1313
GetTokenBalancesParams,
1414
GetTokenDefinitionsParams,
1515
} from '~position/template/contract-position.template.types';
1616

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

21-
export type AngleVaultDefinition = {
22-
address: string;
23-
collateral: string;
24-
stablecoin: string;
25-
};
26-
2721
@PositionTemplate()
28-
export class EthereumAngleVaultsContractPositionFetcher extends ContractPositionTemplatePositionFetcher<
29-
AngleVaultManager,
30-
DefaultDataProps,
31-
AngleVaultDefinition
32-
> {
22+
export class EthereumAngleVaultsContractPositionFetcher extends ContractPositionTemplatePositionFetcher<AngleVaultManager> {
3323
groupLabel = 'Vaults';
3424

3525
constructor(
3626
@Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit,
3727
@Inject(AngleViemContractFactory) protected readonly contractFactory: AngleViemContractFactory,
38-
@Inject(AngleApiHelper) protected readonly angleApiHelper: AngleApiHelper,
28+
@Inject(AnglePositionResolver) protected readonly anglePositionResolver: AnglePositionResolver,
3929
) {
4030
super(appToolkit);
4131
}
@@ -44,12 +34,16 @@ export class EthereumAngleVaultsContractPositionFetcher extends ContractPosition
4434
return this.contractFactory.angleVaultManager({ address, network: this.network });
4535
}
4636

47-
async getDefinitions() {
48-
const vaultManagers = Object.values(await this.angleApiHelper.getVaultManagers(this.network));
49-
return vaultManagers;
37+
async getDefinitions(): Promise<DefaultContractPositionDefinition[]> {
38+
const vaultAddresses = await this.anglePositionResolver.getVaultManagers(this.network);
39+
return vaultAddresses.map(address => {
40+
return {
41+
address,
42+
};
43+
});
5044
}
5145

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

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

71-
const vaultIds = Object.values(await this.angleApiHelper.getVaultIds(address, this.network));
65+
const vaultIds = Object.values(await this.anglePositionResolver.getVaultIds(address, this.network));
7266

7367
const balances = await Promise.all(
7468
vaultIds.map(async vaultId => {

0 commit comments

Comments
 (0)