Skip to content

Commit 7b5649e

Browse files
authored
Merge pull request #52 from ethena-labs/f/pe-87-add-delegated-l2-integration-examples-to-the-adapters-repo
F/pe 87 add delegated l2 integration examples to the adapters repo
2 parents 28d9065 + f92a520 commit 7b5649e

20 files changed

+3408
-22
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ MODE_NODE_URL='https://mainnet.mode.network'
66
FRAXTAL_NODE_URL='https://rpc.frax.com'
77
LYRA_NODE_URL='https://rpc.derive.xyz'
88
SWELL_NODE_URL='https://rpc.ankr.com/swell'
9+
SOLANA_NODE_URL='https://api.mainnet-beta.solana.com'
910

1011
DERIVE_SUBGRAPH_API_KEY=''

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,4 @@ cython_debug/
163163
# and can be added to the global gitignore or merged into this file. For a more nuclear
164164
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
165165
#.idea/
166+
node_modules

README.md

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,71 @@ For your protocol to be included and your users to receive points, you should su
88

99
1. Make a copy of `.env.example` and name it `.env`.
1010
2. Run `pip install -r requirements.txt` to install the required packages.
11+
- If using TypeScript scripts (recommended for L2 delegated integrations), also run `pnpm install` or `npm install` in the root directory.
1112
3. Add your integration metadata to `integrations/integration_ids.py`.
1213
4. Create a new summary column in `constants/summary_columns.py`.
13-
5. Make a copy of [Template](integrations/template.py), naming the file `[protocol name]_integration.py` and place in the `integrations` directory.
14-
6. Your integration must be a class that inherits from `CachedBalancesIntegration` and implements the `get_block_balances` method.
15-
7. The `get_block_balances` method should return a dict of block numbers to a dict of user addresses to balances at that block. (Note: Not all blocks are queried by this service in production- only a sampling, but your code should be capable of handling any block number.)
16-
8. Write some basic tests at the bottom of the file to ensure your integration is working correctly.
17-
9. Submit a PR to this repo with your integration and ping the Ethena team in Telegram.
14+
5. Choose your integration type:
15+
- For EVM compatible chains: Copy [CachedBalancesTemplate](integrations/template.py)
16+
- For non-EVM chains (e.g. Solana): Copy [L2DelegationTemplate](integrations/l2_delegation_template.py)
17+
- We strongly recommend using a TypeScript script or similar to fetch balances for better reliability and maintainability
18+
- See [KaminoL2DelegationExampleIntegration](integrations/kamino_l2_delegation_example_integration.py) for the recommended TypeScript approach
19+
- Create your TypeScript script in the `ts/` directory
20+
- Use the Kamino example as a reference for calling your script from Python
21+
- API integration is also supported but less preferred (see [RatexL2DelegationExampleIntegration](integrations/ratex_l2_delegation_example_integration.py))
22+
6. Name your file `[protocol name]_integration.py` and place it in the `integrations` directory.
23+
7. Your integration must inherit from either:
24+
- `CachedBalancesIntegration` and implement the `get_block_balances` method
25+
- `L2DelegationIntegration` and implement the `get_l2_block_balances` and `get_participants_data` methods
26+
8. The integration should return user balances:
27+
- For CachedBalances: Return a dict of {block_number: {checksum_address: balance}}
28+
- For L2Delegation: Return a dict of {block_number: {address: balance}}
29+
9. Write some basic tests at the bottom of the file to ensure your integration is working correctly.
30+
10. Submit a PR to this repo with your integration and ping the Ethena team in Telegram.
1831

1932
# Guidelines
2033

2134
- Integrations must follow this architecture and be written in python.
22-
- Pendle integrations are included as examples of functioning integrations. Run `python -m integrations.pendle_lpt_integration` to see the output.
2335
- The `get_block_balances` method should be as efficient as possible. So the use of the cached data from previous blocks if possible is highly encouraged.
2436
- We prefer that on chain RPC calls are used to get information as much as possible due to reliability and trustlessness. Off chain calls to apis or subgraphs are acceptable if necessary. If usage is not reasonable or the external service is not reliable, users may not receive their points.
2537

26-
# Example Integrations
27-
- [ClaimedEnaIntegration](integrations/claimed_ena_example_integration.py): This integration demonstrates how to track ENA token claims using cached balance snapshots for improved performance. It reads from previous balance snapshots to efficiently track user claim history.
28-
- [BeefyCachedBalanceExampleIntegration](integrations/beefy_cached_balance_example_integration.py): This integration is an example of a cached balance integration that is based on API calls.
29-
- [PendleLPTIntegration](integrations/pendle_lpt_integration.py): (Legacy Example) A basic integration showing Pendle LPT staking tracking. Note: This is a non-cached implementation included only for reference - new integrations should use the cached approach for better efficiency.
30-
- [PendleYTIntegration](integrations/pendle_yt_integration.py): (Legacy Example) A basic integration showing Pendle YT staking tracking. Note: This is a non-cached implementation included only for reference - new integrations should use the cached approach for better efficiency.
38+
# L2 Delegation Setup Requirements
39+
40+
For non-EVM chain integrations (like Solana) or users that don't control the same addresses in the L2 and Ethereum, your users must complete an additional delegation step to receive points. This process links their L2 wallet to their Ethereum address:
41+
42+
1. Visit the [Ethena UI](https://app.ethena.fi) and connect your Ethereum wallet
43+
2. Navigate to [Ethena Delegation Section](https://app.ethena.fi/delegation)
44+
3. Click "Select Chain" and select.
45+
<img src="readme_assets/select_chain.png" alt="Select Chain" style="float: left; clear: left;">
46+
47+
4. Click "Signing With" and select your wallet type.
48+
<img src="readme_assets/select_wallet_type.png" alt="Select Wallet Type" style="float: left; clear: left;">
49+
50+
5. Connect your wallet and sign a message to prove ownership
51+
6. Once delegated, your L2 balances will be attributed to your Ethereum address for points calculation
52+
53+
**Important Notes:**
54+
- Users can delegate at any moment and they won't miss any past points.
55+
56+
# Examples
57+
## Cached Balances Integrations (Default)
58+
- [ClaimedEnaIntegration](integrations/claimed_ena_example_integration.py): This integration demonstrates how to track ENA token claims using cached balance
59+
snapshots for improved performance. It reads from previous balance snapshots to efficiently track user claim history.
60+
- [BeefyCachedBalanceExampleIntegration](integrations/beefy_cached_balance_example_integration.py): This integration is an example of a cached balance integration
61+
that is based on API calls.
62+
63+
## L2 Delegation Examples (For non-EVM chains)
64+
- [KaminoL2DelegationExampleIntegration](integrations/kamino_l2_delegation_example_integration.py)
65+
- Example of L2 delegation for Solana chain
66+
- Uses TypeScript script to query Kamino balances
67+
- Demonstrates cross-chain integration pattern
68+
69+
- [RatexL2DelegationExampleIntegration](integrations/ratex_l2_delegation_example_integration.py)
70+
- Example of L2 delegation using API calls
71+
- Shows how to integrate external API data sources
72+
- Demonstrates proper API response handling
73+
74+
## Legacy Integrations (Don't use, just for reference)
75+
- [PendleLPTIntegration](integrations/pendle_lpt_integration.py): (Legacy Example) A basic integration showing Pendle LPT staking tracking. Note: This is a
76+
non-cached implementation included only for reference - new integrations should use the cached approach for better efficiency.
77+
- [PendleYTIntegration](integrations/pendle_yt_integration.py): (Legacy Example) A basic integration showing Pendle YT staking tracking. Note: This is a non-cached
78+
implementation included only for reference - new integrations should use the cached approach for better efficiency.

campaign/campaign.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@
22
from constants.example_integrations import (
33
ACTIVE_ENA_START_BLOCK_EXAMPLE,
44
BEEFY_ARBITRUM_START_BLOCK_EXAMPLE,
5+
KAMINO_SUSDE_COLLATERAL_START_BLOCK_EXAMPLE,
6+
RATEX_EXAMPLE_USDE_START_BLOCK,
57
)
68
from integrations.beefy_cached_balance_example_integration import (
79
BeefyCachedBalanceIntegration,
810
)
911
from integrations.claimed_ena_example_integration import ClaimedEnaIntegration
12+
from integrations.kamino_l2_delegation_example_integration import (
13+
KaminoL2DelegationExampleIntegration,
14+
)
15+
from integrations.ratex_l2_delegation_example_integration import (
16+
RatexL2DelegationExampleIntegration,
17+
)
1018
from utils import pendle
1119
from web3 import Web3
1220

@@ -49,6 +57,26 @@
4957
chain=Chain.ARBITRUM,
5058
reward_multiplier=1,
5159
),
60+
# L2 Delegation example for non EVM chains, based on ts script
61+
KaminoL2DelegationExampleIntegration(
62+
integration_id=IntegrationID.KAMINO_SUSDE_COLLATERAL_EXAMPLE,
63+
start_block=KAMINO_SUSDE_COLLATERAL_START_BLOCK_EXAMPLE,
64+
market_address="BJnbcRHqvppTyGesLzWASGKnmnF1wq9jZu6ExrjT7wvF",
65+
token_address="EwBTjwCXJ3TsKP8dNTYnzRmBWRd6h48FdLFSAGJ3sCtx",
66+
decimals=9,
67+
chain=Chain.SOLANA,
68+
reward_multiplier=1,
69+
),
70+
# L2 Delegation example for non EVM chains, based on API calls
71+
RatexL2DelegationExampleIntegration(
72+
integration_id=IntegrationID.RATEX_USDE_EXAMPLE,
73+
start_block=RATEX_EXAMPLE_USDE_START_BLOCK,
74+
summary_cols=[
75+
SummaryColumn.RATEX_EXAMPLE_PTS,
76+
],
77+
chain=Chain.SOLANA,
78+
reward_multiplier=1,
79+
),
5280
# Simple Integration class examples (outdated),
5381
# don't use these anymore
5482
PendleLPTIntegration(

constants/chains.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ class Chain(Enum):
1010
SCROLL = "Scroll"
1111
MODE = "Mode"
1212
OPTIMISM = "Optimism"
13-
Lyra = "Lyra"
13+
LYRA = "Lyra"
1414
SWELL = "Swell"
15+
SOLANA = "Solana"

constants/example_integrations.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,7 @@
1414
)
1515

1616
BEEFY_ARBITRUM_START_BLOCK_EXAMPLE = 219870802
17+
18+
KAMINO_SUSDE_COLLATERAL_START_BLOCK_EXAMPLE = 20471904
19+
20+
RATEX_EXAMPLE_USDE_START_BLOCK = 21202656

constants/summary_columns.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,16 @@ class SummaryColumn(Enum):
3939
"beefy_cached_balance_example",
4040
SummaryColumnType.ETHENA_PTS,
4141
)
42-
42+
4343
TEMPEST_SWELL_SHARDS = ("tempest_swell_shards", SummaryColumnType.ETHENA_PTS)
4444

45+
KAMINO_DELEGATED_PTS_EXAMPLE = (
46+
"kamino_delegated_pts_example",
47+
SummaryColumnType.ETHENA_PTS,
48+
)
49+
50+
RATEX_EXAMPLE_PTS = ("ratex_example_pts", SummaryColumnType.ETHENA_PTS)
51+
4552
def __init__(self, column_name: str, col_type: SummaryColumnType):
4653
self.column_name = column_name
4754
self.col_type = col_type

integrations/integration.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,20 @@ def __init__(
3939
def get_balance(self, user: str, block: int) -> float:
4040
raise NotImplementedError
4141

42-
# either get_participants OR get_block_balances must be implemented
4342
def get_participants(
4443
self,
4544
blocks: Optional[List[int]],
4645
) -> Set[str]:
4746
raise NotImplementedError
4847

49-
# either get_participants OR get_block_balances must be implemented
5048
def get_block_balances(
5149
self, cached_data: Dict[int, Dict[ChecksumAddress, float]], blocks: List[int]
5250
) -> Dict[int, Dict[ChecksumAddress, float]]:
5351
raise NotImplementedError
52+
53+
def get_l2_block_balances(
54+
self,
55+
cached_data: Dict[int, Dict[str, float]],
56+
blocks: List[int],
57+
) -> Dict[int, Dict[str, float]]:
58+
raise NotImplementedError

integrations/integration_ids.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,18 @@ class IntegrationID(Enum):
394394
"Beefy Cached Balance Example",
395395
Token.USDE,
396396
)
397+
398+
KAMINO_SUSDE_COLLATERAL_EXAMPLE = (
399+
"kamino_susde_collateral_example",
400+
"Kamino sUSDe Collateral Example",
401+
Token.SUSDE,
402+
)
403+
404+
RATEX_USDE_EXAMPLE = ("ratex_usde_example", "Ratex USDe Example", Token.USDE)
405+
397406
# Upshift sUSDe
398407
UPSHIFT_UPSUSDE = ("upshift_upsusde", "Upshift upsUSDe", Token.SUSDE)
399408

400-
401409
# Tempest Finance
402410
TEMPEST_SWELL_USDE = (
403411
"tempest_swell_usde_held",
@@ -412,7 +420,6 @@ class IntegrationID(Enum):
412420
Token.USDE,
413421
)
414422

415-
416423
def __init__(self, column_name: str, description: str, token: Token = Token.USDE):
417424
self.column_name = column_name
418425
self.description = description
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import logging
2+
import os
3+
import subprocess
4+
import json
5+
import time
6+
7+
from typing import Dict, List
8+
from dotenv import load_dotenv
9+
from constants.summary_columns import SummaryColumn
10+
from constants.example_integrations import (
11+
KAMINO_SUSDE_COLLATERAL_START_BLOCK_EXAMPLE,
12+
)
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+
20+
class KaminoL2DelegationExampleIntegration(L2DelegationIntegration):
21+
def __init__(
22+
self,
23+
integration_id: IntID,
24+
start_block: int,
25+
token_address: str,
26+
market_address: str,
27+
decimals: int,
28+
chain: Chain = Chain.SOLANA,
29+
reward_multiplier: int = 1,
30+
):
31+
32+
super().__init__(
33+
integration_id=integration_id,
34+
start_block=start_block,
35+
chain=chain,
36+
summary_cols=[SummaryColumn.KAMINO_DELEGATED_PTS_EXAMPLE],
37+
reward_multiplier=reward_multiplier,
38+
)
39+
self.token_address = token_address
40+
self.market_address = market_address
41+
self.decimals = str(decimals)
42+
self.kamino_ts_location = "ts/kamino_collat.ts"
43+
44+
def get_l2_block_balances(
45+
self, cached_data: Dict[int, Dict[str, float]], blocks: List[int]
46+
) -> Dict[int, Dict[str, float]]:
47+
logging.info("Getting block data for Kamino l2 delegation example...")
48+
block_data: Dict[int, Dict[str, float]] = {}
49+
for block in blocks:
50+
block_data[block] = self.get_participants_data(block)
51+
return block_data
52+
53+
def get_participants_data(self, block: int) -> Dict[str, float]:
54+
logging.info(
55+
f"Getting participants data for Kamino l2 delegation example at block {block}..."
56+
)
57+
max_retries = 3
58+
retry_count = 0
59+
60+
while retry_count < max_retries:
61+
try:
62+
logging.info(
63+
f"Getting participants data for Kamino l2 delegation example at block {block}... (Attempt {retry_count + 1}/{max_retries})"
64+
)
65+
response = subprocess.run(
66+
[
67+
"ts-node",
68+
os.path.join(
69+
os.path.dirname(__file__), "..", self.kamino_ts_location
70+
),
71+
self.market_address,
72+
self.token_address,
73+
self.decimals,
74+
],
75+
capture_output=True,
76+
text=True,
77+
check=True,
78+
env=os.environ.copy(),
79+
)
80+
balances = json.loads(response.stdout)
81+
return balances
82+
except Exception as e:
83+
retry_count += 1
84+
if retry_count == max_retries:
85+
err_msg = f"Error getting participants data for Kamino l2 delegation example after {max_retries} attempts: {e}"
86+
logging.error(err_msg)
87+
return {}
88+
else:
89+
logging.warning(
90+
f"Attempt {retry_count}/{max_retries} failed, retrying..."
91+
)
92+
time.sleep(5) # Add a small delay between retries
93+
return {}
94+
95+
96+
if __name__ == "__main__":
97+
example_integration = KaminoL2DelegationExampleIntegration(
98+
integration_id=IntID.KAMINO_SUSDE_COLLATERAL_EXAMPLE,
99+
start_block=KAMINO_SUSDE_COLLATERAL_START_BLOCK_EXAMPLE,
100+
market_address="BJnbcRHqvppTyGesLzWASGKnmnF1wq9jZu6ExrjT7wvF",
101+
token_address="EwBTjwCXJ3TsKP8dNTYnzRmBWRd6h48FdLFSAGJ3sCtx",
102+
decimals=9,
103+
chain=Chain.SOLANA,
104+
reward_multiplier=5,
105+
)
106+
107+
example_integration_output = example_integration.get_l2_block_balances(
108+
cached_data={}, blocks=[21209856, 21217056]
109+
)
110+
111+
print("=" * 120)
112+
print("Run without cached data", example_integration_output)
113+
print("=" * 120, "\n" * 5)

0 commit comments

Comments
 (0)