Skip to content

Commit 7800dbc

Browse files
committed
feat(starknet): portfolio provider
1 parent a8bafc7 commit 7800dbc

File tree

4 files changed

+252
-309
lines changed

4 files changed

+252
-309
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import {
2+
elizaLogger,
3+
IAgentRuntime,
4+
Memory,
5+
Provider,
6+
State,
7+
} from "@ai16z/eliza";
8+
9+
import { fetchWithRetry, getStarknetAccount } from "../utils";
10+
import { ERC20Token } from "../utils/ERC20Token";
11+
12+
const CONFIG = {
13+
// Coingecko IDs src:
14+
// https://api.coingecko.com/api/v3/coins/list
15+
// https://docs.google.com/spreadsheets/d/1wTTuxXt8n9q7C4NDXqQpI3wpKu1_5bGVmP9Xz0XGSyU/edit?gid=0#gid=0
16+
PORTFOLIO_TOKENS: {
17+
BROTHER: {
18+
address:
19+
"0x3b405a98c9e795d427fe82cdeeeed803f221b52471e3a757574a2b4180793ee",
20+
coingeckoId: "starknet-brother",
21+
decimals: 18,
22+
},
23+
CASH: {
24+
address:
25+
"0x0498edfaf50ca5855666a700c25dd629d577eb9afccdf3b5977aec79aee55ada",
26+
coingeckoId: "opus-cash",
27+
decimals: 18,
28+
},
29+
ETH: {
30+
address:
31+
"0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
32+
coingeckoId: "ethereum",
33+
decimals: 18,
34+
},
35+
LORDS: {
36+
address:
37+
"0x124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49",
38+
coingeckoId: "lords",
39+
decimals: 18,
40+
},
41+
STRK: {
42+
address:
43+
"0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d",
44+
coingeckoId: "starknet",
45+
decimals: 18,
46+
},
47+
USDC: {
48+
address:
49+
"0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8",
50+
coingeckoId: "usd-coin",
51+
decimals: 6,
52+
},
53+
USDT: {
54+
address:
55+
"0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8",
56+
coingeckoId: "tether",
57+
decimals: 6,
58+
},
59+
WBTC: {
60+
address:
61+
"0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac",
62+
coingeckoId: "bitcoin",
63+
decimals: 8,
64+
},
65+
},
66+
};
67+
68+
type CoingeckoPrices = {
69+
[cryptoName: string]: { usd: number };
70+
};
71+
72+
type TokenBalances = {
73+
[tokenAddress: string]: BigInt;
74+
};
75+
76+
export class WalletProvider {
77+
private runtime: IAgentRuntime;
78+
79+
constructor(runtime: IAgentRuntime) {
80+
this.runtime = runtime;
81+
}
82+
83+
async getWalletPortfolio(): Promise<TokenBalances> {
84+
const cacheKey = `walletPortfolio-${this.runtime.agentId}`;
85+
const cachedValues = await this.runtime.cacheManager.get<TokenBalances>(
86+
cacheKey,
87+
);
88+
if (cachedValues) {
89+
elizaLogger.debug("Using cached data for getWalletPortfolio()");
90+
return cachedValues;
91+
}
92+
93+
const starknetAccount = getStarknetAccount(this.runtime);
94+
const balances: TokenBalances = {};
95+
96+
// reading balances sequentially to prevent API issues
97+
for (const token of Object.values(CONFIG.PORTFOLIO_TOKENS)) {
98+
const erc20 = new ERC20Token(token.address, starknetAccount);
99+
const balance = await erc20.balanceOf(starknetAccount.address);
100+
balances[token.address] = balance;
101+
}
102+
103+
await this.runtime.cacheManager.set(cacheKey, balances, {
104+
expires: Date.now() + 180 * 60 * 1000, // 3 hours cache
105+
});
106+
107+
return balances;
108+
}
109+
110+
async getTokenUsdValues(): Promise<CoingeckoPrices> {
111+
const cacheKey = "tokenUsdValues";
112+
const cachedValues = await this.runtime.cacheManager.get<
113+
CoingeckoPrices
114+
>(cacheKey);
115+
if (cachedValues) {
116+
elizaLogger.debug("Using cached data for getTokenUsdValues()");
117+
return cachedValues;
118+
}
119+
120+
const coingeckoIds = Object.values(CONFIG.PORTFOLIO_TOKENS).map(
121+
(token) => token.coingeckoId,
122+
).join(",");
123+
124+
const coingeckoPrices = await fetchWithRetry<CoingeckoPrices>(
125+
`https://api.coingecko.com/api/v3/simple/price?ids=${coingeckoIds}&vs_currencies=usd`,
126+
);
127+
128+
await this.runtime.cacheManager.set(cacheKey, coingeckoPrices, {
129+
expires: Date.now() + 30 * 60 * 1000, // 30 minutes cache
130+
});
131+
132+
return coingeckoPrices;
133+
}
134+
}
135+
136+
const walletProvider: Provider = {
137+
get: async (
138+
runtime: IAgentRuntime,
139+
_message: Memory,
140+
_state?: State,
141+
): Promise<string> => {
142+
const provider = new WalletProvider(runtime);
143+
let walletPortfolio: TokenBalances = null;
144+
let tokenUsdValues: CoingeckoPrices = null;
145+
146+
try {
147+
walletPortfolio = await provider.getWalletPortfolio();
148+
tokenUsdValues = await provider.getTokenUsdValues();
149+
} catch (error) {
150+
elizaLogger.error("Error in walletProvider.get():", error);
151+
return "Unable to fetch wallet portfolio. Please try again later.";
152+
}
153+
154+
const rows = Object.entries(CONFIG.PORTFOLIO_TOKENS)
155+
.map(([symbol, token]) => {
156+
const rawBalance = walletPortfolio[token.address];
157+
if (rawBalance === undefined) return null;
158+
159+
const decimalBalance = Number(rawBalance) /
160+
Math.pow(10, token.decimals);
161+
const price = tokenUsdValues[token.coingeckoId]?.usd ?? 0;
162+
const usdValue = decimalBalance * price;
163+
164+
if (decimalBalance === 0 && usdValue === 0) return null;
165+
166+
return `${symbol.padEnd(9)}| ${
167+
decimalBalance.toFixed(18).replace(/\.?0+$/, "").padEnd(20)
168+
}| ${usdValue.toFixed(2)}`;
169+
})
170+
.filter((row): row is string => row !== null);
171+
172+
const header = "symbol | balance | USD value";
173+
const separator = "==================================================";
174+
return [header, separator, ...rows].join("\n");
175+
},
176+
};
177+
178+
export { walletProvider };

0 commit comments

Comments
 (0)