Skip to content

Commit 5210f8a

Browse files
authored
Merge pull request #105 from 0xwml/wml/tha-2813-ethena-debug-ech-xlpt-sats-adapter
[echelon-xlpt] add adapter
2 parents 792c974 + 9dee8d4 commit 5210f8a

6 files changed

Lines changed: 247 additions & 1 deletion

File tree

constants/echelon.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
LENDING_CONTRACT_ADDRESS = "0xc6bc659f1649553c1a3fa05d9727433dc03843baac29473c817d06d39e7621ba"
22

3+
XLPT_ORACLE_CONTRACT_ADDRESS = "0xff9c14f0ab345b7804388d93401f5f0651f29dff7082bf386eea9b7a695c769e"
4+
35
SUSDE_MARKET_ADDRESS = "0x778362f04f7904ba0b76913ec7c0c5cc04e469b0b96929c6998b34910690a740"
46

57
SUSDE_TOKEN_ADDRESS = "0xb30a694a344edee467d9f82330bbe7c3b89f440a1ecd2da1f3bca266560fce69"
68

7-
ETHENA_ADDRESS_API_URL = "https://app.echelon.market/api/ethena-addresses"
9+
SUSDE_USDC_XLPT_MARKET_ADDRESS = "0x09c22785c5247e8bc491b2c19f25bbc313c5cd683a23a736bb358195bfbe81f1"
10+
11+
SUSDE_USDC_TOKEN_ADDRESS = "0x35c3e420fa4fd925628366f1977865d62432c8856a2db147a1cb13f7207f6a79"
12+
13+
ETHENA_ADDRESS_API_URL = "https://app.echelon.market/api/ethena-addresses"
14+
15+
ETHENA_SUSDE_USDC_XLPT_ADDRESS_API_URL = "https://app.echelon.market/api/ethena-xlpt-addresses"

constants/example_integrations.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
ECHELON_SUSDE_COLLATERAL_START_BLOCK = 2379805052
2323

24+
ECHELON_SUSDE_USDC_XLPT_COLLATERAL_START_BLOCK = 2626780000
25+
2426
RATEX_EXAMPLE_USDE_START_BLOCK = 21202656
2527

2628
FIVA_EXAMPLE_USDE_START_BLOCK = 22370179

constants/thala.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
ETHENA_ADDRESS_API_URL="https://app.thala.fi/api/ethena-addresses"
66

7+
ETHENA_XLPT_ADDRESS_API_URL="https://app.thala.fi/api/ethena-xlpt-addresses"
8+
79
THALA_FARMING_V1_ADDRESS = "0x6b3720cd988adeaf721ed9d4730da4324d52364871a68eac62b46d21e4d2fa99"
810

911
THALASWAP_V2_ADDRESS = "0x7730cd28ee1cdc9e999336cbc430f99e7c44397c0aa77516f6f23a78559bb5"
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import logging
2+
import subprocess
3+
import json
4+
import requests
5+
6+
from typing import Dict, List, Set
7+
from dotenv import load_dotenv
8+
from constants.summary_columns import SummaryColumn
9+
from constants.example_integrations import (
10+
ECHELON_SUSDE_USDC_XLPT_COLLATERAL_START_BLOCK
11+
)
12+
from constants.echelon import LENDING_CONTRACT_ADDRESS, XLPT_ORACLE_CONTRACT_ADDRESS, SUSDE_USDC_XLPT_MARKET_ADDRESS, SUSDE_USDC_TOKEN_ADDRESS, ETHENA_SUSDE_USDC_XLPT_ADDRESS_API_URL
13+
from constants.chains import Chain
14+
from integrations.integration_ids import IntegrationID as IntID
15+
from integrations.l2_delegation_integration import L2DelegationIntegration
16+
17+
load_dotenv()
18+
19+
class EchelonAptosIntegration(L2DelegationIntegration):
20+
def __init__(
21+
self,
22+
integration_id: IntID,
23+
start_block: int,
24+
token_address: str,
25+
market_address: str,
26+
decimals: int,
27+
chain: Chain = Chain.APTOS,
28+
reward_multiplier: int = 1,
29+
):
30+
super().__init__(
31+
integration_id=integration_id,
32+
start_block=start_block,
33+
chain=chain,
34+
summary_cols=[SummaryColumn.ECHELON_SHARDS],
35+
reward_multiplier=reward_multiplier,
36+
)
37+
self.token_address = token_address
38+
self.market_address = market_address
39+
self.decimals = str(decimals)
40+
self.echelon_ts_location = "ts/echelon_xlpt_balances.ts"
41+
42+
def get_l2_block_balances(
43+
self, cached_data: Dict[int, Dict[str, float]], blocks: List[int]
44+
) -> Dict[int, Dict[str, float]]:
45+
logging.info("Getting block data for Echelon sUSDe/USDC xLPT Collateral...")
46+
# Ensure blocks are sorted smallest to largest
47+
block_data: Dict[int, Dict[str, float]] = {}
48+
sorted_blocks = sorted(blocks)
49+
50+
# Populate block data from smallest to largest
51+
for block in sorted_blocks:
52+
user_addresses = self.get_participants(block)
53+
result = self.get_participants_data(block, user_addresses[0:20])
54+
55+
# Store the balances and cache the exchange rate
56+
block_data[block] = result
57+
58+
return block_data
59+
60+
def get_participants_data(self, block, user_addresses=[]):
61+
print("Getting participants data for block", block)
62+
try:
63+
response = subprocess.run(
64+
[
65+
"ts-node",
66+
self.echelon_ts_location,
67+
LENDING_CONTRACT_ADDRESS,
68+
self.market_address,
69+
str(self.decimals),
70+
str(block),
71+
json.dumps(user_addresses),
72+
XLPT_ORACLE_CONTRACT_ADDRESS,
73+
SUSDE_USDC_TOKEN_ADDRESS,
74+
],
75+
capture_output=True,
76+
text=True,
77+
check=True
78+
)
79+
80+
# Debug output
81+
print("TypeScript stdout:", response.stdout)
82+
print("TypeScript stderr:", response.stderr)
83+
84+
try:
85+
result = json.loads(response.stdout)
86+
return result # Now returns dict with both balances and exchange rate
87+
except json.JSONDecodeError as e:
88+
print(f"JSON Decode Error: {e}")
89+
print(f"Raw output: {response.stdout}")
90+
raise
91+
92+
except subprocess.CalledProcessError as e:
93+
print(f"Process error: {e}")
94+
print(f"stderr: {e.stderr}")
95+
raise
96+
except Exception as e:
97+
print(f"Unexpected error: {e}")
98+
raise
99+
100+
def get_participants(self, block: int) -> List[str]:
101+
try:
102+
response = requests.get(
103+
f"{ETHENA_SUSDE_USDC_XLPT_ADDRESS_API_URL}?block={block}",
104+
timeout=10
105+
)
106+
response.raise_for_status()
107+
108+
data = response.json()['data']
109+
if not isinstance(data, list):
110+
logging.warning(f"Unexpected response format from API: {data}")
111+
return []
112+
113+
return [addr for addr in data if isinstance(addr, str)]
114+
115+
except requests.RequestException as e:
116+
logging.error(f"Request failed for block {block}: {str(e)}")
117+
return []
118+
except Exception as e:
119+
logging.error(f"Error processing participants for block {block}: {str(e)}")
120+
return []
121+
122+
if __name__ == "__main__":
123+
example_integration = EchelonAptosIntegration(
124+
integration_id=IntID.ECHELON_SUSDE_COLLATERAL,
125+
start_block=ECHELON_SUSDE_USDC_XLPT_COLLATERAL_START_BLOCK,
126+
market_address=SUSDE_USDC_XLPT_MARKET_ADDRESS,
127+
token_address=SUSDE_USDC_TOKEN_ADDRESS,
128+
decimals=8,
129+
chain=Chain.APTOS,
130+
reward_multiplier=5,
131+
)
132+
133+
example_integration_output = example_integration.get_l2_block_balances(
134+
cached_data={}, blocks=list(range(ECHELON_SUSDE_USDC_XLPT_COLLATERAL_START_BLOCK, ECHELON_SUSDE_USDC_XLPT_COLLATERAL_START_BLOCK + 40548182, 2000000))
135+
)
136+
137+
print("=" * 120)
138+
print("Run without cached data", example_integration_output)
139+
print("=" * 120, "\n" * 5)

integrations/integration_ids.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ class IntegrationID(Enum):
8181
Token.SUSDE,
8282
)
8383

84+
ECHELON_SUSDE_USDC_XLPT_COLLATERAL = (
85+
"echelon_susde_usdc_xlpt_collateral",
86+
"Echelon sUSDe/USDC xLPT Collateral",
87+
Token.SUSDE,
88+
)
89+
8490
# Stake DAO
8591
STAKEDAO_SUSDE_JULY_LPT = (
8692
"stakedao_susde_july_effective_lpt_held",

ts/echelon_xlpt_balances.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import * as dotenv from "dotenv";
2+
import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk";
3+
4+
dotenv.config();
5+
6+
const config = new AptosConfig({ network: Network.MAINNET });
7+
// Aptos is the main entrypoint for all functions
8+
const client = new Aptos(config);
9+
10+
const args = process.argv.slice(2);
11+
const LENDING_CONTRACT_ADDRESS = args[0];
12+
const market_address = args[1];
13+
const decimals = Number(args[2]);
14+
const block = Number(args[3]);
15+
const user_addresses: string[] = JSON.parse(args[4]);
16+
const XLPT_ORACLE_ADDRESS = args[5];
17+
const SUSDE_USDC_TOKEN_ADDRESS = args[6];
18+
19+
async function getStrategy() {
20+
// iterate over all users and get their susde balance
21+
const user_balances: Record<string, number> = {};
22+
for (const address of user_addresses) {
23+
const susde_usdc_xlpt_balance = await client.view({
24+
payload: {
25+
function: `${LENDING_CONTRACT_ADDRESS}::lending::account_coins`,
26+
functionArguments: [address, market_address],
27+
},
28+
options: { ledgerVersion: block },
29+
});
30+
31+
const susde_usdc_xlpt_price = await client.view({
32+
payload: {
33+
function: `${XLPT_ORACLE_ADDRESS}::oracle::get_price`,
34+
functionArguments: [SUSDE_USDC_TOKEN_ADDRESS],
35+
},
36+
options: { ledgerVersion: block },
37+
});
38+
39+
const susde_usdc_value = Number(susde_usdc_xlpt_balance) * fp64ToFloat(BigInt((susde_usdc_xlpt_price[0] as { v: string }).v));;
40+
41+
user_balances[address] = scaleDownByDecimals(
42+
susde_usdc_value,
43+
decimals
44+
);
45+
}
46+
47+
console.log(JSON.stringify(user_balances));
48+
}
49+
50+
function scaleDownByDecimals(value: number, decimals: number) {
51+
return value / 10 ** decimals;
52+
}
53+
54+
const ZERO = BigInt(0);
55+
const ONE = BigInt(1);
56+
57+
export const fp64ToFloat = (a: bigint): number => {
58+
// avoid large number
59+
let mask = BigInt("0xffffffff000000000000000000000000");
60+
if ((a & mask) != ZERO) {
61+
throw new Error("too large");
62+
}
63+
64+
// integer part
65+
mask = BigInt("0x10000000000000000");
66+
let base = 1;
67+
let result = 0;
68+
for (let i = 0; i < 32; ++i) {
69+
if ((a & mask) != ZERO) {
70+
result += base;
71+
}
72+
base *= 2;
73+
mask = mask << ONE;
74+
}
75+
76+
// fractional part
77+
mask = BigInt("0x8000000000000000");
78+
base = 0.5;
79+
for (let i = 0; i < 32; ++i) {
80+
if ((a & mask) != ZERO) {
81+
result += base;
82+
}
83+
base /= 2;
84+
mask = mask >> ONE;
85+
}
86+
return result;
87+
};
88+
89+
const strategy = getStrategy().catch(console.error);

0 commit comments

Comments
 (0)