-
Notifications
You must be signed in to change notification settings - Fork 379
Commit
- Loading branch information
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { gql } from 'graphql-request'; | ||
|
||
export const SPOOL_ADDRESS = '0x40803cea2b2a32bda1be61d3604af6a814e70976'; | ||
export const VOSPOOL_ADDRESS = '0xaf56d16a7fe479f2fcd48ff567ff589cb2d2a0e9'; | ||
export const STAKING_ADDRESS = '0xc3160c5cc63b6116dd182faa8393d3ad9313e213'; | ||
export const SUBGRAPH_API_BASE_URL = 'https://api.studio.thegraph.com/query/41372/spool-v2_mainnet/version/latest'; | ||
export const SUBGRAPH_V1_API_BASE_URL = "https://api.thegraph.com/subgraphs/name/spoolfi/spool?source=zapper" | ||
export const ANALYTICS_API_BASE_URL = 'https://subgraph-api.58sa2u0osmd2c.eu-west-1.cs.amazonlightsail.com/mainnet'; | ||
export const SPOOL_STAKED_QUERY = gql` | ||
query getStaking($address: String!) { | ||
userSpoolStaking(id: $address) { | ||
id | ||
spoolStaked | ||
} | ||
} | ||
`; | ||
|
||
export const REWARDS_QUERY = gql` | ||
query { | ||
stakingRewardTokens { | ||
id | ||
token { | ||
id | ||
name | ||
symbol | ||
decimals | ||
} | ||
isRemoved | ||
endTime | ||
startTime | ||
} | ||
} | ||
`; | ||
|
||
|
||
export const FEES_QUERY = gql` | ||
query { | ||
globals { | ||
treasuryFee | ||
ecosystemFee | ||
} | ||
} | ||
`; | ||
|
||
export const VAULTS_ANALYTICS_QUERY = gql` | ||
query { | ||
smartVaults { | ||
id | ||
tvr | ||
baseApy | ||
rewardsApy | ||
adjustedApy | ||
} | ||
} | ||
`; | ||
export const VAULTS_QUERY = gql` | ||
query { | ||
smartVaults { | ||
id | ||
assetGroup { | ||
assetGroupTokens { | ||
token { | ||
id | ||
} | ||
} | ||
} | ||
id | ||
name | ||
riskTolerance | ||
riskProvider { | ||
id | ||
} | ||
smartVaultFees { | ||
id | ||
depositFeePercentage | ||
managementFeePercentage | ||
performanceFeePercentage | ||
} | ||
smartVaultStrategies { | ||
id | ||
isRemoved | ||
allocation | ||
strategy { | ||
id | ||
riskScores { | ||
riskScore | ||
riskProvider { | ||
id | ||
} | ||
} | ||
} | ||
} | ||
smartVaultRewardTokens { | ||
id | ||
token { | ||
id | ||
name | ||
symbol | ||
decimals | ||
} | ||
rewardRate | ||
startTime | ||
endTime | ||
totalAmount | ||
isRemoved | ||
} | ||
} | ||
} | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import { Inject } from '@nestjs/common'; | ||
import { BigNumber } from 'ethers'; | ||
import { parseEther } from 'ethers/lib/utils'; | ||
import _ from 'lodash'; | ||
|
||
import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface'; | ||
import { PositionTemplate } from '~app-toolkit/decorators/position-template.decorator'; | ||
import { buildDollarDisplayItem, buildNumberDisplayItem } from '~app-toolkit/helpers/presentation/display-item.present'; | ||
import { getLabelFromToken } from '~app-toolkit/helpers/presentation/image.present'; | ||
import { gqlFetch } from '~app-toolkit/helpers/the-graph.helper'; | ||
import { SpoolV2ContractFactory, SpoolStaking } from '~apps/spool-v2/contracts'; | ||
import { | ||
REWARDS_QUERY, | ||
SPOOL_ADDRESS, | ||
SPOOL_STAKED_QUERY, | ||
STAKING_ADDRESS, | ||
SUBGRAPH_V1_API_BASE_URL, | ||
VOSPOOL_ADDRESS, | ||
} from '~apps/spool-v2/ethereum/spool-v2.constants'; | ||
import { StakingReward, UserSpoolStaking } from '~apps/spool-v2/ethereum/spool-v2.types'; | ||
import { MetaType } from '~position/position.interface'; | ||
import { isClaimable, isSupplied } from '~position/position.utils'; | ||
import { ContractPositionTemplatePositionFetcher } from '~position/template/contract-position.template.position-fetcher'; | ||
import { | ||
GetDisplayPropsParams, | ||
GetTokenBalancesParams, | ||
GetDataPropsParams, | ||
} from '~position/template/contract-position.template.types'; | ||
|
||
type SpoolStakingDataProps = { | ||
tvl: number; | ||
spoolStaked: number; | ||
totalAccVoSpool: number; | ||
}; | ||
|
||
@PositionTemplate() | ||
export class EthereumSpoolV2StakingContractPositionFetcher extends ContractPositionTemplatePositionFetcher< | ||
SpoolStaking, | ||
SpoolStakingDataProps | ||
> { | ||
groupLabel = 'Staking'; | ||
|
||
constructor( | ||
@Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit, | ||
@Inject(SpoolV2ContractFactory) protected readonly contractFactory: SpoolV2ContractFactory, | ||
) { | ||
super(appToolkit); | ||
} | ||
|
||
getContract(address: string): SpoolStaking { | ||
return this.contractFactory.spoolStaking({ address, network: this.network }); | ||
} | ||
|
||
async getDefinitions() { | ||
return [{ address: STAKING_ADDRESS }]; | ||
} | ||
|
||
async getTokenDefinitions() { | ||
const now = parseInt(String(new Date().getTime() / 1000)); | ||
const stakingRewards = await gqlFetch<StakingReward>({ | ||
endpoint: SUBGRAPH_V1_API_BASE_URL, | ||
query: REWARDS_QUERY, | ||
}); | ||
|
||
// SpoolStaking can emit arbitrary tokens; VoSpoolRewards always emits SPOOL | ||
const rewardAddresses = _.uniq( | ||
stakingRewards.stakingRewardTokens | ||
.filter(reward => !reward.isRemoved && parseInt(reward.startTime) >= now && parseInt(reward.endTime) <= now) | ||
.map(reward => reward.token.id) | ||
.concat([SPOOL_ADDRESS]), | ||
); | ||
|
||
return [ | ||
{ metaType: MetaType.SUPPLIED, address: SPOOL_ADDRESS, network: this.network }, | ||
{ metaType: MetaType.VESTING, address: VOSPOOL_ADDRESS, network: this.network }, | ||
...rewardAddresses.map(address => ({ metaType: MetaType.CLAIMABLE, address, network: this.network })), | ||
]; | ||
} | ||
|
||
async getDataProps({ multicall, contractPosition }: GetDataPropsParams<SpoolStaking, SpoolStakingDataProps>) { | ||
const [spoolToken, voSpoolToken] = contractPosition.tokens; | ||
const spoolContract = this.contractFactory.erc20(spoolToken); | ||
const voSpoolContract = this.contractFactory.spoolVospool(voSpoolToken); | ||
|
||
const totalStaked = await multicall.wrap(spoolContract).balanceOf(STAKING_ADDRESS); | ||
Check failure on line 85 in src/apps/spool-v2/ethereum/spool-v2.staking.contract-position-fetcher.ts GitHub Actions / Build Check
|
||
const votingPowerRaw = await multicall.wrap(voSpoolContract).getTotalGradualVotingPower(); | ||
Check failure on line 86 in src/apps/spool-v2/ethereum/spool-v2.staking.contract-position-fetcher.ts GitHub Actions / Build Check
|
||
|
||
const spoolStaked = totalStaked.div(BigNumber.from(10).pow(spoolToken.decimals)).toNumber(); | ||
const totalAccVoSpool = votingPowerRaw.div(BigNumber.from(10).pow(voSpoolToken.decimals)).toNumber(); | ||
const pricePrecision = 10 ** 10; | ||
const tvl = BigNumber.from((pricePrecision * spoolToken.price).toFixed(0)) | ||
.mul(totalStaked) | ||
.div(pricePrecision) | ||
.div(BigNumber.from(10).pow(spoolToken.decimals)) | ||
.toNumber(); | ||
|
||
return { tvl, spoolStaked, totalAccVoSpool }; | ||
} | ||
|
||
async getLabel({ contractPosition }: GetDisplayPropsParams<SpoolStaking>) { | ||
return `${getLabelFromToken(contractPosition.tokens[0])} Staking`; | ||
} | ||
|
||
async getStatsItems({ contractPosition }: GetDisplayPropsParams<SpoolStaking, SpoolStakingDataProps>) { | ||
return [ | ||
{ label: 'TVL', value: buildDollarDisplayItem(contractPosition.dataProps.tvl) }, | ||
{ label: 'SPOOL Staked', value: buildNumberDisplayItem(contractPosition.dataProps.spoolStaked) }, | ||
{ label: 'voSPOOL Accumulated', value: buildNumberDisplayItem(contractPosition.dataProps.totalAccVoSpool) }, | ||
]; | ||
} | ||
|
||
async getTokenBalancesPerPosition({ | ||
address, | ||
contractPosition, | ||
multicall, | ||
}: GetTokenBalancesParams<SpoolStaking, SpoolStakingDataProps>) { | ||
const suppliedToken = contractPosition.tokens.find(isSupplied)!; | ||
const rewardTokens = contractPosition.tokens.filter(isClaimable); | ||
|
||
const staking = this.contractFactory.spoolStaking({ address: STAKING_ADDRESS, network: this.network }); | ||
const voSpool = this.contractFactory.spoolVospool({ address: VOSPOOL_ADDRESS, network: this.network }); | ||
|
||
const [votingPowerRaw, voSpoolRewards, ...tokenRewards] = await Promise.all([ | ||
multicall.wrap(voSpool).getUserGradualVotingPower(address), | ||
Check failure on line 124 in src/apps/spool-v2/ethereum/spool-v2.staking.contract-position-fetcher.ts GitHub Actions / Build Check
|
||
multicall.wrap(staking).callStatic.getUpdatedVoSpoolRewardAmount({ from: address }), | ||
Check failure on line 125 in src/apps/spool-v2/ethereum/spool-v2.staking.contract-position-fetcher.ts GitHub Actions / Build Check
|
||
...rewardTokens.map(reward => multicall.wrap(staking).earned(reward.address, address)), | ||
Check failure on line 126 in src/apps/spool-v2/ethereum/spool-v2.staking.contract-position-fetcher.ts GitHub Actions / Build Check
|
||
]); | ||
|
||
const stakedSpool = await gqlFetch<UserSpoolStaking>({ | ||
endpoint: SUBGRAPH_V1_API_BASE_URL, | ||
query: SPOOL_STAKED_QUERY, | ||
variables: { address }, | ||
}); | ||
|
||
const stakedAmount = parseEther(stakedSpool?.userSpoolStaking?.spoolStaked || '0').toString(); | ||
|
||
const rewardBalances = rewardTokens.map((token, idx) => { | ||
// Add voSPOOL rewards (always in SPOOL) | ||
const rewards = | ||
token.address.toLowerCase() == suppliedToken.address.toLowerCase() | ||
? tokenRewards[idx].add(voSpoolRewards) | ||
: tokenRewards[idx]; | ||
|
||
return rewards.toString(); | ||
}); | ||
|
||
return [stakedAmount, votingPowerRaw.toString(), ...rewardBalances]; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import { IMulticallWrapper } from '~multicall'; | ||
import { ContractPosition } from '~position/position.interface'; | ||
|
||
export type SpoolVaults = { | ||
smartVaults: VaultDetails[]; | ||
}; | ||
|
||
export type VaultDetails = { | ||
id: string; | ||
name: string; | ||
riskTolerance: string; | ||
riskProvider: { | ||
id: string; | ||
}; | ||
assetGroup: { | ||
assetGroupTokens: { | ||
token: { | ||
id: string; | ||
}; | ||
}[]; | ||
}; | ||
smartVaultFees: { | ||
depositFeePercentage: string; | ||
managementFeePercentage: string; | ||
performanceFeePercentage: string; | ||
}; | ||
smartVaultStrategies: { | ||
id: string; | ||
allocation: string; | ||
isRemoved: boolean; | ||
strategy: { | ||
id: string; | ||
riskScores: { | ||
riskScore: string; | ||
riskProvider: { | ||
id: string; | ||
}; | ||
}[]; | ||
}; | ||
}[]; | ||
smartVaultRewardTokens: { | ||
id: string; | ||
token: { | ||
id: string; | ||
name: string; | ||
symbol: string; | ||
decimals: number; | ||
}; | ||
rewardRate: number; | ||
totalAmount: string; | ||
isRemoved: number; | ||
}[]; | ||
}; | ||
|
||
export type Globals = { | ||
globals: { | ||
treasuryFee: string; | ||
ecosystemFee: string; | ||
}; | ||
}; | ||
|
||
export type VaultsAnalytics = { | ||
smartVaults: { | ||
id: string; | ||
tvr: string; | ||
baseApy: string; | ||
rewardsApy: string[]; | ||
adjustedApy: string; | ||
incentivizedApy: string; | ||
}[]; | ||
}; | ||
|
||
export type Platform = { | ||
platform: { | ||
id: string; | ||
treasuryFeeSize: string; | ||
ecosystemFeeSize: string; | ||
}; | ||
}; | ||
|
||
export type StakingReward = { | ||
stakingRewardTokens: { | ||
id: string; | ||
token: { | ||
id: string; | ||
name: string; | ||
symbol: string; | ||
decimals: number; | ||
}; | ||
isRemoved: boolean; | ||
endTime: string; | ||
startTime: string; | ||
}[]; | ||
}; | ||
|
||
export type UserSpoolStaking = { | ||
userSpoolStaking: { | ||
id: string; | ||
spoolStaked: string; | ||
}; | ||
}; | ||
|
||
export type VaultDataProps = { | ||
strategies: string[]; | ||
}; | ||
|
||
export type VaultPosition = ContractPosition<VaultDataProps>; | ||
|
||
export type SpoolVaultDefinition = { | ||
suppliedTokenAddresses: string[]; | ||
address: string; | ||
name: string; | ||
riskModel: string; | ||
rewardTokenAddresses: string[]; | ||
strategyAddresses: string[]; | ||
stats: { | ||
riskTolerance: number; | ||
adjustedApy: number; | ||
incentivizedApy: number; | ||
apy: number; | ||
tvr: number; | ||
fees: number; | ||
}; | ||
}; | ||
|
||
export type SpoolVaultDataProps = { | ||
totalValueRouted: number; | ||
apy: number; | ||
adjustedApy: number; | ||
incentivizedApy: number; | ||
fees: number; | ||
strategies: string[]; | ||
riskScore: string; | ||
riskTolerance: number; | ||
}; | ||
|
||
export type ResolveBalancesProps = { address: string; contractPosition: VaultPosition; multicall: IMulticallWrapper }; |