From e8784fc37b0e9321660aa5a9b374d02e7ba1baf8 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:43:05 -0600 Subject: [PATCH 1/2] handle affiliate revenue by denom --- cli/affiliateRevs.txt | 13 +++++++++++ cli/sample.env | 4 +++- cli/src/client.ts | 51 ++++++++++++++++++++++++++++++++++++++----- cli/src/index.ts | 25 ++++++++++++++------- cli/src/ipfs.ts | 2 +- cli/src/types.ts | 2 ++ cli/src/wallet.ts | 7 +++++- 7 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 cli/affiliateRevs.txt diff --git a/cli/affiliateRevs.txt b/cli/affiliateRevs.txt new file mode 100644 index 0000000..b69d83e --- /dev/null +++ b/cli/affiliateRevs.txt @@ -0,0 +1,13 @@ +Affiliate Revs: + +- Thorchain (done) +- Mayachain (clone thorchain logic) + +- Relay (determine if there is a set fee pay address(s) for detection) - Monthly manual +- ChainFlip (try to get access to graphql api docs to query broker overview by timestamp/block range) - Monthly manual + +PER TX +- 0x: https://api.0x.org/trade-analytics/swap -H "0x-api-key: 5db0d1cb-f3a3-4c38-9ff2-14347eb4ff84" -H "0x-version: v2" +- Portals: api endpoint? + - https://etherscan.io/address/0xbf5a7f3629fb325e2a8453d595ab103465f75e62 +- CowSwap \ No newline at end of file diff --git a/cli/sample.env b/cli/sample.env index 8b82066..acc23a1 100644 --- a/cli/sample.env +++ b/cli/sample.env @@ -1,4 +1,6 @@ INFURA_API_KEY= PINATA_JWT= PINATA_GATEWAY_URL= -PINATA_GATEWAY_API_KEY= \ No newline at end of file +PINATA_GATEWAY_API_KEY= +THORNODE_URL=https://daemon.thorchain.shapeshift.com +UNCHAINED_URL=https://api.thorchain.shapeshift.com \ No newline at end of file diff --git a/cli/src/client.ts b/cli/src/client.ts index 0ab9b24..d66abf4 100644 --- a/cli/src/client.ts +++ b/cli/src/client.ts @@ -10,12 +10,23 @@ import { error, info, warn } from './logging' import { CalculateRewardsArgs, RewardDistribution } from './types' const INFURA_API_KEY = process.env['INFURA_API_KEY'] - if (!INFURA_API_KEY) { error('INFURA_API_KEY not set. Please make sure you copied the sample.env and filled out your .env file.') process.exit(1) } +const THORNODE_URL = process.env['THORNODE_URL'] +if (!THORNODE_URL) { + error('THORNODE_URL not set. Please make sure you copied the sample.env and filled out your .env file.') + process.exit(1) +} + +const UNCHAINED_URL = process.env['UNCHAINED_URL'] +if (!UNCHAINED_URL) { + error('UNCHAINED_URL not set. Please make sure you copied the sample.env and filled out your .env file.') + process.exit(1) +} + const AVERAGE_BLOCK_TIME_BLOCKS = 1000 const THORCHAIN_PRECISION = 8 const TOKEN_PRECISION = 18 @@ -32,6 +43,7 @@ export const stakingContracts = [ type Revenue = { addresses: string[] amount: string + revenue: Record } type Pool = { @@ -145,7 +157,7 @@ export class Client { async getRevenue(startTimestamp: number, endTimestamp: number): Promise { try { const { data } = await axios.get( - `https://api.thorchain.shapeshift.com/api/v1/affiliate/revenue?start=${startTimestamp}&end=${endTimestamp}`, + `${UNCHAINED_URL}/api/v1/affiliate/revenue?start=${startTimestamp}&end=${endTimestamp}`, ) return data } catch (err) { @@ -161,14 +173,41 @@ export class Client { } } + async getTotalRevenueInRune(revenue: Record): Promise { + try { + let total = new BigNumber(0) + + for (const [denom, amount] of Object.entries(revenue)) { + if (denom === 'THOR.RUNE') { + total = total.plus(amount) + continue + } + + const { data: pool } = await axios.get(`${THORNODE_URL}/lcd/thorchain/pool/${denom}`) + + const assetInRune = new BigNumber(pool.balance_rune).div(pool.balance_asset) + + total = total.plus(new BigNumber(amount).times(assetInRune).toFixed(0)) + } + + return total.toFixed() + } catch (err) { + if (isAxiosError(err)) { + error(`Failed to get total revenue in rune: ${err.message}, exiting.`) + } else { + error('Failed to get total revenue in rune, exiting.') + } + + process.exit(1) + } + } + async getPrice(): Promise { try { - const { data: ethPool } = await axios.get( - 'https://daemon.thorchain.shapeshift.com/lcd/thorchain/pool/ETH.ETH', - ) + const { data: ethPool } = await axios.get(`${THORNODE_URL}/lcd/thorchain/pool/ETH.ETH`) const { data: foxPool } = await axios.get( - 'https://daemon.thorchain.shapeshift.com/lcd/thorchain/pool/ETH.FOX-0XC770EEFAD204B5180DF6A14EE197D99D808EE52D', + `${THORNODE_URL}/lcd/thorchain/pool/ETH.FOX-0XC770EEFAD204B5180DF6A14EE197D99D808EE52D`, ) const ethPriceUsd = toPrecision(ethPool.asset_tor_price, THORCHAIN_PRECISION).toFixed(THORCHAIN_PRECISION) diff --git a/cli/src/index.ts b/cli/src/index.ts index c1c6bf5..6b80cfd 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -44,13 +44,21 @@ const processEpoch = async () => { const revenue = await client.getRevenue(metadata.epochStartTimestamp, metadata.epochEndTimestamp) - info( - `Total ${month} revenue earned by ${revenue.addresses}: ${BigNumber(revenue.amount).div(100000000).toFixed(8)} RUNE`, - ) + info(`Affilate addresses:`) + revenue.addresses.forEach(address => info(`\t- ${address}`)) + + info(`Total revenue earned by denom:`) + Object.entries(revenue.revenue).forEach(([denom, amount]) => { + info(`\t- ${BigNumber(amount).div(100000000).toFixed(8)} ${denom}`) + }) + + const totalRevenueInRune = await client.getTotalRevenueInRune(revenue.revenue) + + info(`Total revenue in RUNE: ${BigNumber(totalRevenueInRune).div(100000000).toFixed(8)}`) const totalDistributionRate = Object.entries(metadata.distributionRateByStakingContract).reduce( (prev, [stakingContract, distributionRate]) => { - const totalDistribution = BigNumber(BigNumber(revenue.amount).times(distributionRate).toFixed(0)) + const totalDistribution = BigNumber(BigNumber(totalRevenueInRune).times(distributionRate).toFixed(0)) info( `Staking Contract ${stakingContract}:\n\t- Share of total revenue to be distributed as rewards: ${distributionRate * 100}%\n\t- Total rewards to be distributed: ${totalDistribution.div(100000000).toFixed()} RUNE`, ) @@ -60,11 +68,11 @@ const processEpoch = async () => { ) info(`Share of total revenue to be distributed as rewards: ${totalDistributionRate * 100}%`) - const totalDistribution = BigNumber(BigNumber(revenue.amount).times(totalDistributionRate).toFixed(0)) + const totalDistribution = BigNumber(BigNumber(totalRevenueInRune).times(totalDistributionRate).toFixed(0)) info(`Total rewards to be distributed: ${totalDistribution.div(100000000).toFixed()} RUNE`) info(`Share of total revenue to buy back fox and burn: ${metadata.burnRate * 100}%`) - const totalBurn = BigNumber(BigNumber(revenue.amount).times(metadata.burnRate).toFixed(0)) + const totalBurn = BigNumber(BigNumber(totalRevenueInRune).times(metadata.burnRate).toFixed(0)) info(`Total amount to be used to buy back fox and burn: ${totalBurn.div(100000000).toFixed()} RUNE`) const spinner = ora('Detecting epoch start and end blocks...').start() @@ -100,7 +108,7 @@ const processEpoch = async () => { endBlock, secondsInEpoch, distributionRate, - totalRevenue: revenue.amount, + totalRevenue: totalRevenueInRune, }) detailsByStakingContract[stakingContract] = { @@ -118,7 +126,8 @@ const processEpoch = async () => { startBlock: Number(startBlock), endBlock: Number(endBlock), treasuryAddress: metadata.treasuryAddress, - totalRevenue: revenue.amount, + totalRevenue: totalRevenueInRune, + revenue: revenue.revenue, burnRate: metadata.burnRate, runePriceUsd, distributionStatus: 'pending', diff --git a/cli/src/ipfs.ts b/cli/src/ipfs.ts index b1926fc..db84dab 100644 --- a/cli/src/ipfs.ts +++ b/cli/src/ipfs.ts @@ -1,6 +1,6 @@ import * as prompts from '@inquirer/prompts' import { PinataSDK } from 'pinata' -import axios, { isAxiosError } from 'axios' +import { isAxiosError } from 'axios' import BigNumber from 'bignumber.js' import { error, info } from './logging' import { Epoch, EpochDetails, RFOXMetadata, RewardDistribution } from './types' diff --git a/cli/src/types.ts b/cli/src/types.ts index 4380fda..06e7855 100644 --- a/cli/src/types.ts +++ b/cli/src/types.ts @@ -80,6 +80,8 @@ export type Epoch = { treasuryAddress: string /** The total revenue (RUNE) earned by the treasury for this epoch */ totalRevenue: string + /** The revenue earned (by denom) by the treasury for this epoch */ + revenue: Record /** The percentage of revenue (RUNE) accumulated by the treasury to be used to buy FOX from the open market and subsequently burned for this epoch */ burnRate: number /** The spot price of rune in USD */ diff --git a/cli/src/wallet.ts b/cli/src/wallet.ts index 2bd12ee..43b3432 100644 --- a/cli/src/wallet.ts +++ b/cli/src/wallet.ts @@ -11,9 +11,14 @@ import { error, info, success } from './logging' import { Epoch } from './types' import { RFOX_DIR } from '.' +const THORNODE_URL = process.env['THORNODE_URL'] +if (!THORNODE_URL) { + error('THORNODE_URL not set. Please make sure you copied the sample.env and filled out your .env file.') + process.exit(1) +} + const BIP32_PATH = `m/44'/931'/0'/0/0` const SHAPESHIFT_MULTISIG_ADDRESS = 'thor122h9hlrugzdny9ct95z6g7afvpzu34s73uklju' -const THORNODE_URL = 'https://daemon.thorchain.shapeshift.com' const addressNList = bip32ToAddressNList(BIP32_PATH) From eb55df0a7103f0c6914cda9e38fa8edb9095419e Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:08:05 -0600 Subject: [PATCH 2/2] use pre calculated total revs in rune --- cli/affiliateRevs.txt | 13 ------------- cli/src/client.ts | 29 ----------------------------- cli/src/index.ts | 14 ++++++-------- 3 files changed, 6 insertions(+), 50 deletions(-) delete mode 100644 cli/affiliateRevs.txt diff --git a/cli/affiliateRevs.txt b/cli/affiliateRevs.txt deleted file mode 100644 index b69d83e..0000000 --- a/cli/affiliateRevs.txt +++ /dev/null @@ -1,13 +0,0 @@ -Affiliate Revs: - -- Thorchain (done) -- Mayachain (clone thorchain logic) - -- Relay (determine if there is a set fee pay address(s) for detection) - Monthly manual -- ChainFlip (try to get access to graphql api docs to query broker overview by timestamp/block range) - Monthly manual - -PER TX -- 0x: https://api.0x.org/trade-analytics/swap -H "0x-api-key: 5db0d1cb-f3a3-4c38-9ff2-14347eb4ff84" -H "0x-version: v2" -- Portals: api endpoint? - - https://etherscan.io/address/0xbf5a7f3629fb325e2a8453d595ab103465f75e62 -- CowSwap \ No newline at end of file diff --git a/cli/src/client.ts b/cli/src/client.ts index d66abf4..edf0c91 100644 --- a/cli/src/client.ts +++ b/cli/src/client.ts @@ -173,35 +173,6 @@ export class Client { } } - async getTotalRevenueInRune(revenue: Record): Promise { - try { - let total = new BigNumber(0) - - for (const [denom, amount] of Object.entries(revenue)) { - if (denom === 'THOR.RUNE') { - total = total.plus(amount) - continue - } - - const { data: pool } = await axios.get(`${THORNODE_URL}/lcd/thorchain/pool/${denom}`) - - const assetInRune = new BigNumber(pool.balance_rune).div(pool.balance_asset) - - total = total.plus(new BigNumber(amount).times(assetInRune).toFixed(0)) - } - - return total.toFixed() - } catch (err) { - if (isAxiosError(err)) { - error(`Failed to get total revenue in rune: ${err.message}, exiting.`) - } else { - error('Failed to get total revenue in rune, exiting.') - } - - process.exit(1) - } - } - async getPrice(): Promise { try { const { data: ethPool } = await axios.get(`${THORNODE_URL}/lcd/thorchain/pool/ETH.ETH`) diff --git a/cli/src/index.ts b/cli/src/index.ts index 6b80cfd..34e6351 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -52,13 +52,11 @@ const processEpoch = async () => { info(`\t- ${BigNumber(amount).div(100000000).toFixed(8)} ${denom}`) }) - const totalRevenueInRune = await client.getTotalRevenueInRune(revenue.revenue) - - info(`Total revenue in RUNE: ${BigNumber(totalRevenueInRune).div(100000000).toFixed(8)}`) + info(`Total revenue in RUNE: ${BigNumber(revenue.amount).div(100000000).toFixed(8)}`) const totalDistributionRate = Object.entries(metadata.distributionRateByStakingContract).reduce( (prev, [stakingContract, distributionRate]) => { - const totalDistribution = BigNumber(BigNumber(totalRevenueInRune).times(distributionRate).toFixed(0)) + const totalDistribution = BigNumber(BigNumber(revenue.amount).times(distributionRate).toFixed(0)) info( `Staking Contract ${stakingContract}:\n\t- Share of total revenue to be distributed as rewards: ${distributionRate * 100}%\n\t- Total rewards to be distributed: ${totalDistribution.div(100000000).toFixed()} RUNE`, ) @@ -68,11 +66,11 @@ const processEpoch = async () => { ) info(`Share of total revenue to be distributed as rewards: ${totalDistributionRate * 100}%`) - const totalDistribution = BigNumber(BigNumber(totalRevenueInRune).times(totalDistributionRate).toFixed(0)) + const totalDistribution = BigNumber(BigNumber(revenue.amount).times(totalDistributionRate).toFixed(0)) info(`Total rewards to be distributed: ${totalDistribution.div(100000000).toFixed()} RUNE`) info(`Share of total revenue to buy back fox and burn: ${metadata.burnRate * 100}%`) - const totalBurn = BigNumber(BigNumber(totalRevenueInRune).times(metadata.burnRate).toFixed(0)) + const totalBurn = BigNumber(BigNumber(revenue.amount).times(metadata.burnRate).toFixed(0)) info(`Total amount to be used to buy back fox and burn: ${totalBurn.div(100000000).toFixed()} RUNE`) const spinner = ora('Detecting epoch start and end blocks...').start() @@ -108,7 +106,7 @@ const processEpoch = async () => { endBlock, secondsInEpoch, distributionRate, - totalRevenue: totalRevenueInRune, + totalRevenue: revenue.amount, }) detailsByStakingContract[stakingContract] = { @@ -126,7 +124,7 @@ const processEpoch = async () => { startBlock: Number(startBlock), endBlock: Number(endBlock), treasuryAddress: metadata.treasuryAddress, - totalRevenue: totalRevenueInRune, + totalRevenue: revenue.amount, revenue: revenue.revenue, burnRate: metadata.burnRate, runePriceUsd,