diff --git a/constants/example_integrations.py b/constants/example_integrations.py index 42e6944..f4e4705 100644 --- a/constants/example_integrations.py +++ b/constants/example_integrations.py @@ -19,4 +19,6 @@ ECHELON_SUSDE_COLLATERAL_START_BLOCK = 273913668 +THALA_SUSDE_START_BLOCK = 276439225 + RATEX_EXAMPLE_USDE_START_BLOCK = 21202656 diff --git a/constants/summary_columns.py b/constants/summary_columns.py index c850cd0..3227e7d 100644 --- a/constants/summary_columns.py +++ b/constants/summary_columns.py @@ -21,6 +21,8 @@ class SummaryColumn(Enum): ECHELON_SHARDS = ("echelon_shards", SummaryColumnType.ETHENA_PTS) + THALA_SHARDS = ("thala_shards", SummaryColumnType.ETHENA_PTS) + NURI_SHARDS = ("nuri_shards", SummaryColumnType.ETHENA_PTS) LENDLE_MANTLE_SHARDS = ("lendle_mantle_shards", SummaryColumnType.ETHENA_PTS) diff --git a/constants/thala.py b/constants/thala.py new file mode 100644 index 0000000..86b853d --- /dev/null +++ b/constants/thala.py @@ -0,0 +1 @@ +SUSDE_LPT_METADATA = "0x99d34f16193e251af236d5a5c3114fa54e22ca512280317eda2f8faf1514c395" # TODO: APT/USDT Address for now diff --git a/integrations/integration_ids.py b/integrations/integration_ids.py index c19bf7d..c2d5b6e 100644 --- a/integrations/integration_ids.py +++ b/integrations/integration_ids.py @@ -73,6 +73,14 @@ class IntegrationID(Enum): "Echelon sUSDe Collateral", Token.SUSDE, ) + + # Thala + THALA_SUSDE_LP = ( + "thala_susde_lp", + "Thala sUSDe LP", + Token.SUSDE, + ) + # Stake DAO STAKEDAO_SUSDE_JULY_LPT = ( "stakedao_susde_july_effective_lpt_held", diff --git a/integrations/thala_integration.py b/integrations/thala_integration.py new file mode 100644 index 0000000..3e94f06 --- /dev/null +++ b/integrations/thala_integration.py @@ -0,0 +1,112 @@ +import logging +import subprocess +import json + +from typing import Dict, List +from dotenv import load_dotenv +from constants.summary_columns import SummaryColumn +from constants.example_integrations import ( + THALA_SUSDE_START_BLOCK, +) +from constants.thala import THALASWAP_CONTRACT_ADDRESS, SUSDE_LPT_METADATA, SUSDE_TOKEN_ADDRESS +from constants.chains import Chain +from integrations.integration_ids import IntegrationID as IntID +from integrations.l2_delegation_integration import L2DelegationIntegration + +load_dotenv() + +class ThalaAptosIntegration(L2DelegationIntegration): + def __init__( + self, + integration_id: IntID, + start_block: int, + token_address: str, + decimals: int, + chain: Chain = Chain.APTOS, + reward_multiplier: int = 1, + ): + super().__init__( + integration_id=integration_id, + start_block=start_block, + chain=chain, + summary_cols=[SummaryColumn.THALA_SHARDS], + reward_multiplier=reward_multiplier, + ) + self.token_address = token_address + self.decimals = str(decimals) + self.thala_ts_location = "ts/thala_balances.ts" + + def get_l2_block_balances( + self, cached_data: Dict[int, Dict[str, float]], blocks: List[int] + ) -> Dict[int, Dict[str, float]]: + logging.info("Getting block data for Thala sUSDe LP...") + # Ensure blocks are sorted smallest to largest + block_data: Dict[int, Dict[str, float]] = {} + sorted_blocks = sorted(blocks) + + # Populate block data from smallest to largest + for block in sorted_blocks: + # Check block_data first, then cached_data for previous block balances + prev_block_user_balances = block_data.get(block - 1, cached_data.get(block - 1, {})) + result = self.get_participants_data(block, prev_block_user_balances) + + # Store the balances + block_data[block] = result['balances'] + + return block_data + + def get_participants_data(self, block, prev_block_user_balances=None): + print("Getting participants data for block", block) + try: + response = subprocess.run( + [ + "ts-node", + self.thala_ts_location, + SUSDE_LPT_METADATA, + str(self.decimals), + str(block), + json.dumps(prev_block_user_balances or {}), + ], + capture_output=True, + text=True, + check=True + ) + + # Debug output + print("TypeScript stdout:", response.stdout) + print("TypeScript stderr:", response.stderr) + + try: + result = json.loads(response.stdout) + return result + except json.JSONDecodeError as e: + print(f"JSON Decode Error: {e}") + print(f"Raw output: {response.stdout}") + raise + + except subprocess.CalledProcessError as e: + print(f"Process error: {e}") + print(f"stderr: {e.stderr}") + raise + except Exception as e: + print(f"Unexpected error: {e}") + raise + + +if __name__ == "__main__": + example_integration = ThalaAptosIntegration( + integration_id=IntID.THALA_SUSDE_LP, + start_block=THALA_SUSDE_START_BLOCK, + token_address=SUSDE_LPT_METADATA, + decimals=8, + chain=Chain.APTOS, + reward_multiplier=5, + ) + + example_integration_output = example_integration.get_l2_block_balances( + cached_data={}, blocks=list(range(THALA_SUSDE_START_BLOCK, THALA_SUSDE_START_BLOCK + 300)) + ) + + print("=" * 120) + print("Run without cached data", example_integration_output) + print("=" * 120, "\n" * 5) \ No newline at end of file diff --git a/ts/thala_balances.ts b/ts/thala_balances.ts new file mode 100644 index 0000000..e022fa5 --- /dev/null +++ b/ts/thala_balances.ts @@ -0,0 +1,127 @@ +import * as dotenv from "dotenv"; +import { + Aptos, + AptosConfig, + Network, + WriteSetChangeWriteResource, + isUserTransactionResponse, + MoveResource, +} from "@aptos-labs/ts-sdk"; + +dotenv.config(); + +const config = new AptosConfig({ network: Network.MAINNET }); +// Aptos is the main entrypoint for all functions +const client = new Aptos(config); + +const args = process.argv.slice(2); +const SUSDE_LPT_METADATA = args[0]; +const decimals = Number(args[1]); +const block = Number(args[2]); +const user_balances: Record = new Proxy( + args[3] ? JSON.parse(args[3]) : {}, + { + get: (target, prop) => target[prop] || 0, + } +); + +type FungibleStoreResource = WriteSetChangeWriteResource & { + data: MoveResource; +}; + +type ObjectCoreResource = WriteSetChangeWriteResource & { + data: MoveResource; +}; + +type FungibleStoreData = { + balance: string; + metadata: { + inner: string; + }; +}; + +type ObjectCoreData = { + owner: string; +}; + +async function getStrategy() { + const block_data = await client.getBlockByHeight({ + blockHeight: block, + options: { + withTransactions: true, + }, + }); + + if (!block_data) { + throw new Error(`Block ${block} not found`); + } + + if (!block_data.transactions) { + throw new Error(`No transactions found in Block ${block}`); + } + + const user_transactions = block_data.transactions.filter( + isUserTransactionResponse + ); + + for (const transaction of user_transactions) { + // First, collect all ObjectCore and LPT changes + const objectCoreChanges = new Map(); + const lptChanges: Array<{ store: string; balance: string }> = []; + + // Collect all relevant changes + for (const change of transaction.changes as WriteSetChangeWriteResource[]) { + if (isObjectCoreChange(change)) { + objectCoreChanges.set(change.address, change.data.data.owner); + } else if (isLPTFungibleStoreChange(change)) { + lptChanges.push({ + store: change.address, + balance: change.data.data.balance, + }); + } + } + + // Process LPT changes after we have all the data + for (const { store, balance } of lptChanges) { + const userAddress = objectCoreChanges.get(store); + if (userAddress) { + user_balances[userAddress] = scaleDownByDecimals( + Number(balance), + decimals + ); + } + } + } + + console.log( + JSON.stringify({ + balances: user_balances, + }) + ); +} + +function scaleDownByDecimals(value: number, decimals: number) { + return value / 10 ** decimals; +} + +function isLPTFungibleStoreChange( + change: WriteSetChangeWriteResource +): change is FungibleStoreResource { + return ( + change.type === "write_resource" && + change.data.type === "0x1::fungible_asset::FungibleStore" && + (change as FungibleStoreResource).data.data.metadata.inner === + SUSDE_LPT_METADATA + ); +} + +function isObjectCoreChange( + change: WriteSetChangeWriteResource +): change is ObjectCoreResource { + return ( + change.type === "write_resource" && + change.data.type === "0x1::object::ObjectCore" + ); +} + +const strategy = getStrategy().catch(console.error);