import { PrizeInfo, TokenWithSupply, TxOverrides } from '@shared/types'
import {
calculatePercentageOfBigInt,
drawManagerABI,
getPrizePoolAllPrizeInfo,
getPrizePoolContributionAmounts,
getPrizePoolContributionPercentages,
getPrizePoolId,
getPrizePoolTotalSupplyTwabs,
getTokenInfo,
prizePoolABI,
validateAddress,
validateClientNetwork
} from '@shared/utilities'
import { Address, formatEther, PublicClient, WalletClient } from 'viem'
/**
* This class provides read and write functions to interact with a prize pool
*/
export class PrizePool {
readonly id: string
walletClient: WalletClient | undefined
prizeTokenAddress: Address | undefined
drawManagerAddress: Address | undefined
twabControllerAddress: Address | undefined
drawPeriodInSeconds: number | undefined
drawAuctionDurationInSeconds: number | undefined
tierShares: number | undefined
reserveShares: number | undefined
tierLiquidityUtilizationRate: bigint | undefined
grandPrizePeriodDraws: number | undefined
/**
* Creates an instance of a Prize Pool with a given public and optional wallet client
*
* NOTE: If initialized without a wallet Viem client, write functions will not be available
* @param chainId the prize pool's chain ID
* @param address the prize pool's address
* @param publicClient a public Viem client for the network the prize pool is deployed on
* @param options optional parameters (including wallet client)
*/
constructor(
public chainId: number,
public address: Address,
public publicClient: PublicClient,
options?: {
walletClient?: WalletClient
prizeTokenAddress?: Address
drawManagerAddress?: Address
twabControllerAddress?: Address
drawPeriodInSeconds?: number
drawAuctionDurationInSeconds?: number
tierShares?: number
reserveShares?: number
tierLiquidityUtilizationRate?: bigint
grandPrizePeriodDraws?: number
}
) {
this.id = getPrizePoolId(chainId, address)
this.walletClient = options?.walletClient
this.prizeTokenAddress = options?.prizeTokenAddress
this.drawManagerAddress = options?.drawManagerAddress
this.twabControllerAddress = options?.twabControllerAddress
this.drawPeriodInSeconds = options?.drawPeriodInSeconds
this.drawAuctionDurationInSeconds = options?.drawAuctionDurationInSeconds
this.tierShares = options?.tierShares
this.reserveShares = options?.reserveShares
this.tierLiquidityUtilizationRate = options?.tierLiquidityUtilizationRate
this.grandPrizePeriodDraws = options?.grandPrizePeriodDraws
}
/* ============================== Read Functions ============================== */
/**
* Returns the address of the token awarded by the prize pool
* @returns
*/
async getPrizeTokenAddress(): Promise
{
if (this.prizeTokenAddress !== undefined) return this.prizeTokenAddress
const source = 'Prize Pool [getPrizeTokenAddress]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const prizeTokenAddress = await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'prizeToken'
})
this.prizeTokenAddress = prizeTokenAddress
return this.prizeTokenAddress
}
/**
* Returns the address of prize pool's draw manager
* @returns
*/
async getDrawManagerAddress(): Promise {
if (this.drawManagerAddress !== undefined) return this.drawManagerAddress
const source = 'Prize Pool [getDrawManagerAddress]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const drawManagerAddress = await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'drawManager'
})
this.drawManagerAddress = drawManagerAddress
return this.drawManagerAddress
}
/**
* Returns the address of the prize pool's TWAB controller
* @returns
*/
async getTwabControllerAddress(): Promise {
if (this.twabControllerAddress !== undefined) return this.twabControllerAddress
const source = 'Prize Pool [getTwabControllerAddress]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const twabControllerAddress = await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'twabController'
})
this.twabControllerAddress = twabControllerAddress
return this.twabControllerAddress
}
/**
* Returns basic data about the token awarded by the prize pool
* @returns
*/
async getPrizeTokenData(): Promise {
const source = 'Prize Pool [getPrizeTokenData]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const prizeTokenAddress = await this.getPrizeTokenAddress()
const prizeTokenInfo = await getTokenInfo(this.publicClient, [prizeTokenAddress])
return prizeTokenInfo[prizeTokenAddress]
}
/**
* Returns the duration of a draw in seconds
* @returns
*/
async getDrawPeriodInSeconds(): Promise {
if (this.drawPeriodInSeconds !== undefined) return this.drawPeriodInSeconds
const source = 'Prize Pool [getDrawPeriodInSeconds]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const drawPeriodInSeconds = await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'drawPeriodSeconds'
})
this.drawPeriodInSeconds = drawPeriodInSeconds
return drawPeriodInSeconds
}
/**
* Returns the duration of a draw auction in seconds
* @returns
*/
async getDrawAuctionDurationInSeconds(): Promise {
if (this.drawAuctionDurationInSeconds !== undefined) return this.drawAuctionDurationInSeconds
const source = 'Prize Pool [getDrawAuctionDurationInSeconds]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const drawManagerAddress = await this.getDrawManagerAddress()
const drawAuctionDurationInSeconds = await this.publicClient.readContract({
address: drawManagerAddress,
abi: drawManagerABI,
functionName: 'auctionDuration'
})
this.drawAuctionDurationInSeconds = drawAuctionDurationInSeconds
return drawAuctionDurationInSeconds
}
/**
* Returns the number of shares allocated to each prize tier
* @returns
*/
async getTierShares(): Promise {
if (this.tierShares !== undefined) return this.tierShares
const source = 'Prize Pool [getTierShares]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const tierShares = await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'tierShares'
})
this.tierShares = tierShares
return tierShares
}
/**
* Returns the number of shares allocated to the reserve
* @returns
*/
async getReserveShares(): Promise {
if (this.reserveShares !== undefined) return this.reserveShares
const source = 'Prize Pool [getReserveShares]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const reserveShares = await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'reserveShares'
})
this.reserveShares = reserveShares
return reserveShares
}
/**
* Returns the number of prize tiers in the prize pool
*
* NOTE: Includes the canary tiers
* @returns
*/
async getNumberOfTiers(): Promise {
const source = 'Prize Pool [getNumberOfTiers]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const numberOfTiers = await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'numberOfTiers'
})
return numberOfTiers
}
/**
* Returns the prize pool's last awarded draw ID
* @returns
*/
async getLastAwardedDrawId(): Promise {
const source = 'Prize Pool [getLastAwardedDrawId]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const lastDrawId = Number(
await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'getLastAwardedDrawId'
})
)
return lastDrawId
}
/**
* Returns the total token amount contributed by all vaults for the given draw IDs
* @param startDrawId start draw ID (inclusive)
* @param endDrawId end draw ID (inclusive)
* @returns
*/
async getTotalContributedAmount(startDrawId: number, endDrawId: number): Promise {
const source = 'Prize Pool [getTotalContributedAmount]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const totalContributedAmount = await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'getTotalContributedBetween',
args: [startDrawId, endDrawId]
})
return totalContributedAmount
}
/**
* Returns the token amounts contributed by any vaults for the given draw IDs
* @param vaultAddresses vault addresses to get contributions from
* @param startDrawId start draw ID (inclusive)
* @param endDrawId end draw ID (inclusive)
* @returns
*/
async getVaultContributedAmounts(
vaultAddresses: Address[],
startDrawId: number,
endDrawId: number
): Promise<{ [vaultId: string]: bigint }> {
const source = 'Prize Pool [getVaultContributedAmounts]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const contributedAmounts = await getPrizePoolContributionAmounts(
this.publicClient,
this.address,
vaultAddresses,
startDrawId,
endDrawId
)
return contributedAmounts
}
/**
* Returns the percentage of the total prize pool contributions for any vaults for the given draw IDs
*
* NOTE: Percentage value from 0 to 1 (eg: 0.25 representing 25%)
* @param vaultAddresses vault addresses to get contributions from
* @param startDrawId start draw ID (inclusive)
* @param endDrawId end draw ID (inclusive)
* @returns
*/
async getVaultContributedPercentages(
vaultAddresses: Address[],
startDrawId: number,
endDrawId: number
): Promise<{ [vaultId: string]: number }> {
const source = 'Prize Pool [getVaultContributedPercentages]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const contributedPercentages = await getPrizePoolContributionPercentages(
this.publicClient,
this.address,
vaultAddresses,
startDrawId,
endDrawId
)
return contributedPercentages
}
/**
* Returns the total supply TWAB for any vaults over the last N draws
* @param vaultAddresses vault addresses to get total supply TWAB for
* @param numDraws the number of draws to look back on
* @returns
*/
async getVaultTotalSupplyTwabs(
vaultAddresses: Address[],
numDraws: number
): Promise<{ [vaultId: string]: bigint }> {
const source = 'Prize Pool [getVaultTotalSupplyTwab]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const lastDrawId = (await this.getLastAwardedDrawId()) || 1
const startDrawId = numDraws > lastDrawId ? 1 : lastDrawId - Math.floor(numDraws) + 1
const totalSupplyTwabs = await getPrizePoolTotalSupplyTwabs(
this.publicClient,
this.address,
vaultAddresses,
startDrawId,
lastDrawId
)
return totalSupplyTwabs
}
/**
* Returns the opened at timestamp of the first ever draw (in seconds)
* @returns
*/
async getFirstDrawOpenedAt(): Promise {
const source = 'Prize Pool [getFirstDrawOpenedAt]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const openedAt = Number(
await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'firstDrawOpensAt'
})
)
return openedAt
}
/**
* Returns the opened at timestamp of the last awarded draw (in seconds)
* @returns
*/
async getLastAwardedDrawOpenedAt(): Promise {
const source = 'Prize Pool [getLastAwardedDrawOpenedAt]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const lastDrawId = await this.getLastAwardedDrawId()
if (lastDrawId === 0) {
return 0
}
const openedAt = Number(
await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'drawOpensAt',
args: [lastDrawId]
})
)
return openedAt
}
/**
* Checks if a user has won a specific prize tier and index while deposited in a given vault
* @param vaultAddress vault address to check
* @param userAddress user address to check prizes for
* @param tier prize tier to check
* @param prizeIndex prize index to check
* @returns
*/
async isWinner(
vaultAddress: Address,
userAddress: Address,
tier: number,
prizeIndex: number
): Promise {
const source = 'Prize Pool [isWinner]'
validateAddress(vaultAddress, source)
validateAddress(userAddress, source)
await validateClientNetwork(this.chainId, this.publicClient, source)
const isWinner = await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'isWinner',
args: [vaultAddress, userAddress, tier, prizeIndex]
})
return isWinner
}
/**
* Returns the prize size of a given tier
* @param tier prize tier
* @returns
*/
async getTierPrizeSize(tier: number): Promise {
const source = 'Prize Pool [getTierPrizeSize]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const tierPrizeSize = await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'getTierPrizeSize',
args: [tier]
})
return tierPrizeSize
}
/**
* Returns the estimated time to award a given tier (in seconds)
* @param tier prize tier
* @returns
*/
async getEstimatedTierAwardTime(tier: number): Promise {
const source = 'Prize Pool [getEstimatedTierAwardTime]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const estimatedDraws = await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'getTierAccrualDurationInDraws',
args: [tier]
})
const drawPeriod = await this.getDrawPeriodInSeconds()
return estimatedDraws * drawPeriod
}
// TODO: this function should emulate the logic in the contract and return a number with more decimals (less rounding)
/**
* Returns the estimated number of prizes awarded based on number of tiers active
* @param options optional settings
* @returns
*/
async getEstimatedPrizeCount(options?: { includeCanary?: boolean }): Promise {
const source = 'Prize Pool [getEstimatedPrizeCount]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const numberOfTiers = await this.getNumberOfTiers()
const estimatedPrizeCount = await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'estimatedPrizeCountWithBothCanaries',
args: [!!options?.includeCanary ? numberOfTiers : numberOfTiers - 2]
})
return estimatedPrizeCount
}
/**
* Returns current and estimated prize amounts and frequency for all prize tiers
* @param options optional settings
* @returns
*/
async getAllPrizeInfo(options?: { considerPastDraws?: number }): Promise {
const source = 'Prize Pool [getAllPrizeInfo]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const numberOfTiers = await this.getNumberOfTiers()
const tiers = Array.from(Array(numberOfTiers).keys())
const allPrizeInfo = await getPrizePoolAllPrizeInfo(
this.publicClient,
this.address,
tiers,
options?.considerPastDraws
)
return allPrizeInfo
}
/**
* Returns the utilization rate for all tiers' liquidity
* @returns
*/
async getTierLiquidityUtilizationRate(): Promise {
if (this.tierLiquidityUtilizationRate !== undefined) return this.tierLiquidityUtilizationRate
const source = 'Prize Pool [getTierLiquidityUtilizationRate]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const tierLiquidityUtilizationRate = await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'tierLiquidityUtilizationRate'
})
this.tierLiquidityUtilizationRate = tierLiquidityUtilizationRate
return tierLiquidityUtilizationRate
}
/**
* Returns the total amount of prizes available on the prize pool
* @returns
*/
async getTotalPrizesAvailable(): Promise {
const source = 'Prize Pool [getTotalPrizesAvailable]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const tierLiquidityUtilizationRate = await this.getTierLiquidityUtilizationRate()
const utilizationRateMultiplier = 1 / parseFloat(formatEther(tierLiquidityUtilizationRate))
const allPrizeInfo = await this.getAllPrizeInfo()
let total = 0n
allPrizeInfo.forEach((prizeInfo, i) => {
const prizeSize = calculatePercentageOfBigInt(
prizeInfo.amount.current,
utilizationRateMultiplier
)
const numPrizes = BigInt(this.getTierPrizeCount(i))
total += prizeSize * numPrizes
})
return total
}
/**
* Returns the number of draws targeted to distribute the grand prize
* @returns
*/
async getGrandPrizePeriodDraws(): Promise {
if (this.grandPrizePeriodDraws !== undefined) return this.grandPrizePeriodDraws
const source = 'Prize Pool [getGrandPrizePeriodDraws]'
await validateClientNetwork(this.chainId, this.publicClient, source)
const grandPrizePeriodDraws = await this.publicClient.readContract({
address: this.address,
abi: prizePoolABI,
functionName: 'grandPrizePeriodDraws'
})
this.grandPrizePeriodDraws = grandPrizePeriodDraws
return grandPrizePeriodDraws
}
/* ============================== Write Functions ============================== */
/**
* Submits a transaction to claim a prize from the prize pool
* @param winner the wallet address that won a given tier's prize
* @param tier the prize tier to claim
* @param prizeIndex the prize index to claim
* @param options optional settings and overrides for this transaction
* @returns
*/
async claimPrize(
winner: Address,
tier: number,
prizeIndex: number,
options?: {
recipient?: Address
fee?: { amount: bigint; recipient: Address }
overrides?: TxOverrides
}
) {
const source = 'Prize Pool [claimPrize]'
if (!this.walletClient?.account) {
throw new Error(`${source} | Invalid/Unavailable Viem Wallet Client`)
}
const { request } = await this.publicClient.simulateContract({
account: this.walletClient.account,
address: this.address,
abi: prizePoolABI,
functionName: 'claimPrize',
args: [
winner,
tier,
prizeIndex,
options?.recipient ?? winner,
options?.fee?.amount ?? 0n,
options?.fee?.recipient ?? this.walletClient.account.address
],
chain: this.walletClient.chain,
...options?.overrides
})
const txHash = await this.walletClient.writeContract(request)
return txHash
}
/**
* Submits a transaction to close the currently open draw
* @param winningRandomNumber randomly generated winning number
* @param overrides optional overrides for this transaction
* @returns
*/
async closeDraw(winningRandomNumber: bigint, overrides?: TxOverrides) {
const source = 'Prize Pool [closeDraw]'
if (!this.walletClient?.account) {
throw new Error(`${source} | Invalid/Unavailable Viem Wallet Client`)
}
const { request } = await this.publicClient.simulateContract({
account: this.walletClient.account,
address: this.address,
abi: prizePoolABI,
functionName: 'awardDraw',
args: [winningRandomNumber],
chain: this.walletClient.chain,
...overrides
})
const txHash = await this.walletClient.writeContract(request)
return txHash
}
/* ============================== Other Functions ============================== */
/**
* Returns the number of prizes in a given prize tier
* @param tier prize tier
* @returns
*/
getTierPrizeCount(tier: number): number {
return 4 ** tier
}
}