@grinta-mcp/server 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * BTC/USD Price Feed for Grinta OracleRelayer
3
+ *
4
+ * Fetches BTC/USD from CoinGecko and pushes to OracleRelayer on Starknet Sepolia.
5
+ * TypeScript equivalent of update_btc_price.sh
6
+ */
7
+ import { type Account, type RpcProvider } from "starknet";
8
+ /**
9
+ * Push BTC/USD price to OracleRelayer and return the tx hash.
10
+ */
11
+ export declare function updateBtcPrice(account: Account): Promise<{
12
+ txHash: string;
13
+ priceUsd: number;
14
+ priceWad: bigint;
15
+ }>;
16
+ /**
17
+ * Read the current stored BTC/USD price from OracleRelayer.
18
+ */
19
+ export declare function readBtcPrice(provider: RpcProvider): Promise<{
20
+ priceWad: bigint;
21
+ priceUsd: number;
22
+ lastUpdateTime: number;
23
+ }>;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * BTC/USD Price Feed for Grinta OracleRelayer
3
+ *
4
+ * Fetches BTC/USD from CoinGecko and pushes to OracleRelayer on Starknet Sepolia.
5
+ * TypeScript equivalent of update_btc_price.sh
6
+ */
7
+ import { Contract } from "starknet";
8
+ import { GRINTA, TOKENS } from "./config.js";
9
+ const WAD = BigInt(10) ** 18n;
10
+ const ORACLE_RELAYER_ABI = [
11
+ {
12
+ type: "interface",
13
+ name: "IOracleRelayer",
14
+ items: [
15
+ {
16
+ type: "function",
17
+ name: "update_price",
18
+ inputs: [
19
+ { name: "base_token", type: "core::starknet::contract_address::ContractAddress" },
20
+ { name: "quote_token", type: "core::starknet::contract_address::ContractAddress" },
21
+ { name: "price_usd_wad", type: "core::integer::u256" },
22
+ ],
23
+ outputs: [],
24
+ state_mutability: "external",
25
+ },
26
+ {
27
+ type: "function",
28
+ name: "get_price_wad",
29
+ inputs: [
30
+ { name: "base_token", type: "core::starknet::contract_address::ContractAddress" },
31
+ { name: "quote_token", type: "core::starknet::contract_address::ContractAddress" },
32
+ ],
33
+ outputs: [{ type: "core::integer::u256" }],
34
+ state_mutability: "view",
35
+ },
36
+ {
37
+ type: "function",
38
+ name: "get_last_update_time",
39
+ inputs: [],
40
+ outputs: [{ type: "core::integer::u64" }],
41
+ state_mutability: "view",
42
+ },
43
+ ],
44
+ },
45
+ ];
46
+ /**
47
+ * Fetch BTC/USD price from CoinGecko (free, no API key)
48
+ */
49
+ async function fetchBtcPrice() {
50
+ const res = await fetch("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd");
51
+ if (!res.ok)
52
+ throw new Error(`CoinGecko HTTP ${res.status}`);
53
+ const data = (await res.json());
54
+ const price = data?.bitcoin?.usd;
55
+ if (!price)
56
+ throw new Error(`Invalid CoinGecko response: ${JSON.stringify(data)}`);
57
+ return price;
58
+ }
59
+ /**
60
+ * Convert a USD price to WAD (18 decimals)
61
+ */
62
+ function priceToWad(price) {
63
+ // Use string manipulation to avoid floating point precision loss
64
+ const [whole, frac = ""] = price.toString().split(".");
65
+ const paddedFrac = frac.padEnd(18, "0").slice(0, 18);
66
+ return BigInt(whole + paddedFrac);
67
+ }
68
+ /**
69
+ * Push BTC/USD price to OracleRelayer and return the tx hash.
70
+ */
71
+ export async function updateBtcPrice(account) {
72
+ // 1. Fetch price
73
+ const priceUsd = await fetchBtcPrice();
74
+ const priceWad = priceToWad(priceUsd);
75
+ console.log(` BTC/USD: $${priceUsd}`);
76
+ console.log(` Price WAD: ${priceWad}`);
77
+ // 2. Push to OracleRelayer
78
+ const oracleRelayer = new Contract({
79
+ abi: ORACLE_RELAYER_ABI,
80
+ address: GRINTA.ORACLE_RELAYER,
81
+ providerOrAccount: account,
82
+ });
83
+ console.log(" Calling OracleRelayer.update_price()...");
84
+ const result = await account.execute([
85
+ oracleRelayer.populate("update_price", [TOKENS.WBTC, TOKENS.USDC, priceWad]),
86
+ ]);
87
+ console.log(` tx: ${result.transaction_hash}`);
88
+ await account.waitForTransaction(result.transaction_hash);
89
+ return { txHash: result.transaction_hash, priceUsd, priceWad };
90
+ }
91
+ /**
92
+ * Read the current stored BTC/USD price from OracleRelayer.
93
+ */
94
+ export async function readBtcPrice(provider) {
95
+ const oracleRelayer = new Contract({
96
+ abi: ORACLE_RELAYER_ABI,
97
+ address: GRINTA.ORACLE_RELAYER,
98
+ providerOrAccount: provider,
99
+ });
100
+ const [priceWad, lastUpdateTime] = await Promise.all([
101
+ oracleRelayer.get_price_wad(TOKENS.WBTC, TOKENS.USDC).then((r) => BigInt(r)),
102
+ oracleRelayer.get_last_update_time().then((r) => Number(r)),
103
+ ]);
104
+ const priceUsd = Number(priceWad * 100n / WAD) / 100;
105
+ return { priceWad, priceUsd, lastUpdateTime };
106
+ }
107
+ /**
108
+ * Standalone runner — can be executed directly with: npx tsx src/price-feed.ts
109
+ */
110
+ async function main() {
111
+ // Load env
112
+ const { default: dotenv } = await import("dotenv");
113
+ const { fileURLToPath } = await import("url");
114
+ const { dirname, join } = await import("path");
115
+ const __dirname = dirname(fileURLToPath(import.meta.url));
116
+ dotenv.config({ path: join(__dirname, "..", ".env") });
117
+ const { CONFIG } = await import("./config.js");
118
+ const { Account, RpcProvider } = await import("starknet");
119
+ if (!CONFIG.ACCOUNT_ADDRESS || !CONFIG.PRIVATE_KEY) {
120
+ console.error("Missing STARKNET_ACCOUNT_ADDRESS or STARKNET_PRIVATE_KEY in .env");
121
+ process.exit(1);
122
+ }
123
+ const provider = new RpcProvider({ nodeUrl: CONFIG.RPC_URL });
124
+ const account = new Account({
125
+ provider,
126
+ address: CONFIG.ACCOUNT_ADDRESS,
127
+ signer: CONFIG.PRIVATE_KEY,
128
+ });
129
+ console.log("--- Update BTC/USD Price ---");
130
+ console.log(`Account: ${CONFIG.ACCOUNT_ADDRESS}`);
131
+ // Read current price
132
+ console.log("\nCurrent on-chain price:");
133
+ const before = await readBtcPrice(provider);
134
+ console.log(` Stored: $${before.priceUsd} (last update: ${new Date(before.lastUpdateTime * 1000).toISOString()})`);
135
+ // Push new price
136
+ console.log("\nPushing new price...");
137
+ const { priceUsd } = await updateBtcPrice(account);
138
+ // Verify
139
+ console.log("\nVerifying...");
140
+ const after = await readBtcPrice(provider);
141
+ console.log(` Stored: $${after.priceUsd} (last update: ${new Date(after.lastUpdateTime * 1000).toISOString()})`);
142
+ console.log("\nDone!");
143
+ }
144
+ // Run if executed directly
145
+ const isMain = process.argv[1]?.includes("price-feed");
146
+ if (isMain) {
147
+ main().catch((err) => {
148
+ console.error("Fatal:", err);
149
+ process.exit(1);
150
+ });
151
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Scan for SAFEs owned by this account and display balances.
3
+ * Run: npx tsx src/scan-safes.ts
4
+ */
5
+ export {};
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Scan for SAFEs owned by this account and display balances.
3
+ * Run: npx tsx src/scan-safes.ts
4
+ */
5
+ import dotenv from "dotenv";
6
+ import { fileURLToPath } from "url";
7
+ import { dirname, join } from "path";
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ dotenv.config({ path: join(__dirname, "..", ".env") });
10
+ import { RpcProvider, Account, Contract } from "starknet";
11
+ import { CONFIG, GRINTA } from "./config.js";
12
+ import { GrintaClient } from "./grinta.js";
13
+ import { formatAmount } from "./utils.js";
14
+ async function main() {
15
+ const provider = new RpcProvider({ nodeUrl: CONFIG.RPC_URL });
16
+ const account = new Account({
17
+ provider,
18
+ address: CONFIG.ACCOUNT_ADDRESS,
19
+ signer: CONFIG.PRIVATE_KEY,
20
+ });
21
+ const grinta = new GrintaClient(account, provider);
22
+ // Check balances
23
+ console.log("--- Wallet Balances ---");
24
+ const [wbtcBal, gritBal] = await Promise.all([
25
+ grinta.getWbtcBalance(CONFIG.ACCOUNT_ADDRESS),
26
+ grinta.getGritBalance(CONFIG.ACCOUNT_ADDRESS),
27
+ ]);
28
+ console.log(` WBTC: ${formatAmount(wbtcBal, 8)} WBTC`);
29
+ console.log(` GRIT: ${formatAmount(gritBal, 18)} GRIT`);
30
+ // Get total safe count
31
+ const safeEngineAbi = [
32
+ {
33
+ type: "interface",
34
+ name: "I",
35
+ items: [
36
+ {
37
+ type: "function",
38
+ name: "get_safe_count",
39
+ inputs: [],
40
+ outputs: [{ type: "core::integer::u64" }],
41
+ state_mutability: "view",
42
+ },
43
+ ],
44
+ },
45
+ ];
46
+ const safeEngine = new Contract({
47
+ abi: safeEngineAbi,
48
+ address: GRINTA.SAFE_ENGINE,
49
+ providerOrAccount: provider,
50
+ });
51
+ const safeCount = Number(await safeEngine.get_safe_count());
52
+ console.log(`\nTotal SAFEs in system: ${safeCount}`);
53
+ const myHex = BigInt(CONFIG.ACCOUNT_ADDRESS).toString(16);
54
+ console.log(`Scanning for SAFEs owned by ${CONFIG.ACCOUNT_ADDRESS}...\n`);
55
+ let found = 0;
56
+ for (let i = 1; i <= safeCount; i++) {
57
+ try {
58
+ const ownerRaw = await grinta.getSafeOwner(i);
59
+ const ownerHex = BigInt(ownerRaw).toString(16);
60
+ if (ownerHex === myHex) {
61
+ found++;
62
+ const safe = await grinta.getSafe(i);
63
+ console.log(`SAFE #${i} (YOURS)`);
64
+ console.log(` Collateral: ${formatAmount(safe.collateral, 18)} WBTC (WAD)`);
65
+ console.log(` Debt: ${formatAmount(safe.debt, 18)} GRIT`);
66
+ if (safe.debt > 0n) {
67
+ const health = await grinta.getPositionHealth(i);
68
+ console.log(grinta.formatHealth(health));
69
+ }
70
+ console.log("");
71
+ }
72
+ }
73
+ catch {
74
+ // skip invalid safe IDs
75
+ }
76
+ }
77
+ if (found === 0) {
78
+ console.log("No SAFEs found for this account.");
79
+ }
80
+ else {
81
+ console.log(`Found ${found} SAFE(s).`);
82
+ }
83
+ }
84
+ main().catch((err) => {
85
+ console.error("Fatal:", err);
86
+ process.exit(1);
87
+ });
package/dist/swap.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Ekubo DEX Swap — direct on-chain swaps via Ekubo Router V3
3
+ *
4
+ * Replaces AVNU for GRIT/USDC swaps on Starknet Sepolia.
5
+ *
6
+ * Run: npx tsx src/swap.ts <sell_token> <buy_token> <amount>
7
+ * Examples:
8
+ * npx tsx src/swap.ts USDC GRIT 100
9
+ * npx tsx src/swap.ts GRIT USDC 50
10
+ */
11
+ import { Account, RpcProvider } from "starknet";
12
+ export declare const EKUBO: {
13
+ ROUTER: string;
14
+ CORE: string;
15
+ };
16
+ export declare const TOKEN_INFO: Record<string, {
17
+ address: string;
18
+ decimals: number;
19
+ }>;
20
+ export declare function ekuboSwap(account: Account, provider: RpcProvider, sellTokenId: string, buyTokenId: string, humanAmount: string): Promise<{
21
+ txHash: string;
22
+ sellName: string;
23
+ buyName: string;
24
+ sellAmount: bigint;
25
+ }>;
package/dist/swap.js ADDED
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Ekubo DEX Swap — direct on-chain swaps via Ekubo Router V3
3
+ *
4
+ * Replaces AVNU for GRIT/USDC swaps on Starknet Sepolia.
5
+ *
6
+ * Run: npx tsx src/swap.ts <sell_token> <buy_token> <amount>
7
+ * Examples:
8
+ * npx tsx src/swap.ts USDC GRIT 100
9
+ * npx tsx src/swap.ts GRIT USDC 50
10
+ */
11
+ import dotenv from "dotenv";
12
+ import { fileURLToPath } from "url";
13
+ import { dirname, join } from "path";
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ dotenv.config({ path: join(__dirname, "..", ".env") });
16
+ import { Account, RpcProvider, Contract } from "starknet";
17
+ import { CONFIG, TOKENS, GRINTA } from "./config.js";
18
+ import { formatAmount, parseAmount } from "./utils.js";
19
+ // --- Ekubo Contracts (Sepolia) ---
20
+ export const EKUBO = {
21
+ ROUTER: "0x0045f933adf0607292468ad1c1dedaa74d5ad166392590e72676a34d01d7b763",
22
+ CORE: "0x0444a09d96389aa7148f1aada508e30b71299ffe650d9c97fdaae38cb9a23384",
23
+ };
24
+ // --- Token Registry ---
25
+ export const TOKEN_INFO = {
26
+ ETH: { address: TOKENS.ETH, decimals: 18 },
27
+ STRK: { address: TOKENS.STRK, decimals: 18 },
28
+ WBTC: { address: TOKENS.WBTC, decimals: 8 },
29
+ USDC: { address: TOKENS.USDC, decimals: 6 },
30
+ GRIT: { address: GRINTA.GRIT, decimals: 18 },
31
+ };
32
+ // token0 must have the smaller address
33
+ const POOLS = [
34
+ {
35
+ token0: TOKENS.USDC, // 0x0728...
36
+ token1: GRINTA.GRIT, // 0x0788...
37
+ fee: 0n,
38
+ tickSpacing: 1000n,
39
+ extension: GRINTA.GRINTA_HOOK,
40
+ },
41
+ ];
42
+ // Ekubo sqrt_ratio bounds (max slippage — fine for testnet)
43
+ const MIN_SQRT_RATIO = 18446748437148339061n;
44
+ const MAX_SQRT_RATIO = 6277100250585753475930931601400621808602321654880405518632n;
45
+ // --- ABIs ---
46
+ const ERC20_ABI = [
47
+ {
48
+ type: "interface",
49
+ name: "IERC20",
50
+ items: [
51
+ {
52
+ type: "function",
53
+ name: "transfer",
54
+ inputs: [
55
+ { name: "recipient", type: "core::starknet::contract_address::ContractAddress" },
56
+ { name: "amount", type: "core::integer::u256" },
57
+ ],
58
+ outputs: [{ type: "core::bool" }],
59
+ state_mutability: "external",
60
+ },
61
+ {
62
+ type: "function",
63
+ name: "balance_of",
64
+ inputs: [
65
+ { name: "account", type: "core::starknet::contract_address::ContractAddress" },
66
+ ],
67
+ outputs: [{ type: "core::integer::u256" }],
68
+ state_mutability: "view",
69
+ },
70
+ ],
71
+ },
72
+ ];
73
+ const ROUTER_ABI = [
74
+ {
75
+ type: "struct",
76
+ name: "ekubo::types::i129",
77
+ members: [
78
+ { name: "mag", type: "core::integer::u128" },
79
+ { name: "sign", type: "core::bool" },
80
+ ],
81
+ },
82
+ {
83
+ type: "struct",
84
+ name: "ekubo::types::keys::PoolKey",
85
+ members: [
86
+ { name: "token0", type: "core::starknet::contract_address::ContractAddress" },
87
+ { name: "token1", type: "core::starknet::contract_address::ContractAddress" },
88
+ { name: "fee", type: "core::integer::u128" },
89
+ { name: "tick_spacing", type: "core::integer::u128" },
90
+ { name: "extension", type: "core::starknet::contract_address::ContractAddress" },
91
+ ],
92
+ },
93
+ {
94
+ type: "struct",
95
+ name: "ekubo::router::RouteNode",
96
+ members: [
97
+ { name: "pool_key", type: "ekubo::types::keys::PoolKey" },
98
+ { name: "sqrt_ratio_limit", type: "core::integer::u256" },
99
+ { name: "skip_ahead", type: "core::integer::u128" },
100
+ ],
101
+ },
102
+ {
103
+ type: "struct",
104
+ name: "ekubo::router::TokenAmount",
105
+ members: [
106
+ { name: "token", type: "core::starknet::contract_address::ContractAddress" },
107
+ { name: "amount", type: "ekubo::types::i129" },
108
+ ],
109
+ },
110
+ {
111
+ type: "struct",
112
+ name: "ekubo::types::Delta",
113
+ members: [
114
+ { name: "amount0", type: "ekubo::types::i129" },
115
+ { name: "amount1", type: "ekubo::types::i129" },
116
+ ],
117
+ },
118
+ {
119
+ type: "interface",
120
+ name: "ekubo::interfaces::router::IRouter",
121
+ items: [
122
+ {
123
+ type: "function",
124
+ name: "swap",
125
+ inputs: [
126
+ { name: "node", type: "ekubo::router::RouteNode" },
127
+ { name: "token_amount", type: "ekubo::router::TokenAmount" },
128
+ ],
129
+ outputs: [{ type: "ekubo::types::Delta" }],
130
+ state_mutability: "external",
131
+ },
132
+ {
133
+ type: "function",
134
+ name: "clear",
135
+ inputs: [
136
+ { name: "token", type: "core::starknet::contract_address::ContractAddress" },
137
+ ],
138
+ outputs: [{ type: "core::integer::u256" }],
139
+ state_mutability: "external",
140
+ },
141
+ ],
142
+ },
143
+ ];
144
+ // --- Helpers ---
145
+ function resolveToken(nameOrAddress) {
146
+ const upper = nameOrAddress.toUpperCase();
147
+ const info = TOKEN_INFO[upper];
148
+ if (info)
149
+ return { ...info, name: upper };
150
+ throw new Error(`Unknown token: ${nameOrAddress}. Supported: ${Object.keys(TOKEN_INFO).join(", ")}`);
151
+ }
152
+ function findPool(tokenA, tokenB) {
153
+ const pool = POOLS.find((p) => (p.token0 === tokenA && p.token1 === tokenB) ||
154
+ (p.token0 === tokenB && p.token1 === tokenA));
155
+ if (!pool)
156
+ throw new Error(`No Ekubo pool for ${tokenA} / ${tokenB}`);
157
+ return pool;
158
+ }
159
+ async function getBalance(provider, tokenAddress, ownerAddress) {
160
+ const token = new Contract({ abi: ERC20_ABI, address: tokenAddress, providerOrAccount: provider });
161
+ const result = await token.balance_of(ownerAddress);
162
+ return BigInt(result);
163
+ }
164
+ // --- Core Swap ---
165
+ export async function ekuboSwap(account, provider, sellTokenId, buyTokenId, humanAmount) {
166
+ const sell = resolveToken(sellTokenId);
167
+ const buy = resolveToken(buyTokenId);
168
+ const sellAmount = parseAmount(humanAmount, sell.decimals);
169
+ const pool = findPool(sell.address, buy.address);
170
+ // Direction: selling token0 pushes price down → MIN, selling token1 pushes price up → MAX
171
+ const sellingToken0 = sell.address.toLowerCase() === pool.token0.toLowerCase();
172
+ const sqrtRatioLimit = sellingToken0 ? MIN_SQRT_RATIO : MAX_SQRT_RATIO;
173
+ const router = new Contract({
174
+ abi: ROUTER_ABI,
175
+ address: EKUBO.ROUTER,
176
+ providerOrAccount: account,
177
+ });
178
+ const sellTokenContract = new Contract({
179
+ abi: ERC20_ABI,
180
+ address: sell.address,
181
+ providerOrAccount: account,
182
+ });
183
+ console.log(` Swapping ${humanAmount} ${sell.name} → ${buy.name}`);
184
+ console.log(` Direction: selling ${sellingToken0 ? "token0" : "token1"}`);
185
+ // Ekubo Router flow: transfer input → swap → clear output
186
+ const calls = [
187
+ // 1. Send sell tokens to the Router
188
+ sellTokenContract.populate("transfer", [EKUBO.ROUTER, sellAmount]),
189
+ // 2. Execute swap through Ekubo Core
190
+ router.populate("swap", [
191
+ {
192
+ pool_key: {
193
+ token0: pool.token0,
194
+ token1: pool.token1,
195
+ fee: pool.fee,
196
+ tick_spacing: pool.tickSpacing,
197
+ extension: pool.extension,
198
+ },
199
+ sqrt_ratio_limit: sqrtRatioLimit,
200
+ skip_ahead: 0n,
201
+ },
202
+ {
203
+ token: sell.address,
204
+ amount: { mag: sellAmount, sign: false },
205
+ },
206
+ ]),
207
+ // 3. Clear output tokens from Router back to caller
208
+ router.populate("clear", [buy.address]),
209
+ ];
210
+ const result = await account.execute(calls);
211
+ console.log(` tx: ${result.transaction_hash}`);
212
+ await account.waitForTransaction(result.transaction_hash);
213
+ console.log(` confirmed`);
214
+ return {
215
+ txHash: result.transaction_hash,
216
+ sellName: sell.name,
217
+ buyName: buy.name,
218
+ sellAmount,
219
+ };
220
+ }
221
+ // --- CLI (only runs when executed directly) ---
222
+ const isDirectRun = process.argv[1]?.replace(/\\/g, "/").endsWith("src/swap.ts");
223
+ if (isDirectRun) {
224
+ const [, , sellName, buyName, amount] = process.argv;
225
+ if (!sellName || !buyName || !amount) {
226
+ console.log("Usage: npx tsx src/swap.ts <sell_token> <buy_token> <amount>");
227
+ console.log("Example: npx tsx src/swap.ts USDC GRIT 100");
228
+ console.log(`Supported tokens: ${Object.keys(TOKEN_INFO).join(", ")}`);
229
+ process.exit(1);
230
+ }
231
+ if (!CONFIG.ACCOUNT_ADDRESS || !CONFIG.PRIVATE_KEY) {
232
+ console.error("Missing STARKNET_ACCOUNT_ADDRESS or STARKNET_PRIVATE_KEY in .env");
233
+ process.exit(1);
234
+ }
235
+ const provider = new RpcProvider({ nodeUrl: CONFIG.RPC_URL });
236
+ const account = new Account({
237
+ provider,
238
+ address: CONFIG.ACCOUNT_ADDRESS,
239
+ signer: CONFIG.PRIVATE_KEY,
240
+ });
241
+ const sell = resolveToken(sellName);
242
+ const buy = resolveToken(buyName);
243
+ console.log("--- Ekubo Swap ---");
244
+ console.log(`Account: ${CONFIG.ACCOUNT_ADDRESS}`);
245
+ // Balances before
246
+ const [sellBefore, buyBefore] = await Promise.all([
247
+ getBalance(provider, sell.address, CONFIG.ACCOUNT_ADDRESS),
248
+ getBalance(provider, buy.address, CONFIG.ACCOUNT_ADDRESS),
249
+ ]);
250
+ console.log(`\nBefore:`);
251
+ console.log(` ${sell.name}: ${formatAmount(sellBefore, sell.decimals)}`);
252
+ console.log(` ${buy.name}: ${formatAmount(buyBefore, buy.decimals)}`);
253
+ // Execute swap
254
+ console.log("");
255
+ const { txHash } = await ekuboSwap(account, provider, sellName, buyName, amount);
256
+ // Balances after
257
+ const [sellAfter, buyAfter] = await Promise.all([
258
+ getBalance(provider, sell.address, CONFIG.ACCOUNT_ADDRESS),
259
+ getBalance(provider, buy.address, CONFIG.ACCOUNT_ADDRESS),
260
+ ]);
261
+ console.log(`\nAfter:`);
262
+ console.log(` ${sell.name}: ${formatAmount(sellAfter, sell.decimals)}`);
263
+ console.log(` ${buy.name}: ${formatAmount(buyAfter, buy.decimals)}`);
264
+ // Delta
265
+ const sellDelta = sellBefore - sellAfter;
266
+ const buyDelta = buyAfter - buyBefore;
267
+ console.log(`\nResult:`);
268
+ console.log(` Sold: ${formatAmount(sellDelta, sell.decimals)} ${sell.name}`);
269
+ console.log(` Received: ${formatAmount(buyDelta, buy.decimals)} ${buy.name}`);
270
+ console.log(` tx: ${txHash}`);
271
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Test AVNU swap on Sepolia
3
+ * Run: npx tsx src/test-swap.ts
4
+ */
5
+ export {};
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Test AVNU swap on Sepolia
3
+ * Run: npx tsx src/test-swap.ts
4
+ */
5
+ import dotenv from "dotenv";
6
+ import { fileURLToPath } from "url";
7
+ import { dirname, join } from "path";
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ dotenv.config({ path: join(__dirname, "..", ".env") });
10
+ import { Account, RpcProvider } from "starknet";
11
+ import { getQuotes } from "@avnu/avnu-sdk";
12
+ import { CONFIG, TOKENS } from "./config.js";
13
+ async function main() {
14
+ const provider = new RpcProvider({ nodeUrl: CONFIG.RPC_URL });
15
+ const account = new Account({
16
+ provider,
17
+ address: CONFIG.ACCOUNT_ADDRESS,
18
+ signer: CONFIG.PRIVATE_KEY,
19
+ });
20
+ console.log("--- AVNU Swap Test (Sepolia) ---");
21
+ console.log(`Account: ${CONFIG.ACCOUNT_ADDRESS}`);
22
+ console.log(`AVNU URL: ${CONFIG.AVNU_BASE_URL}\n`);
23
+ // Try ETH -> STRK quote first (most liquid pair)
24
+ console.log("1) Fetching ETH -> STRK quote (0.001 ETH)...");
25
+ try {
26
+ const ethQuote = {
27
+ sellTokenAddress: TOKENS.ETH,
28
+ buyTokenAddress: TOKENS.STRK,
29
+ sellAmount: BigInt(10 ** 15), // 0.001 ETH
30
+ takerAddress: CONFIG.ACCOUNT_ADDRESS,
31
+ };
32
+ const quotes = await getQuotes(ethQuote, { baseUrl: CONFIG.AVNU_BASE_URL });
33
+ if (quotes.length > 0) {
34
+ const buyAmount = BigInt(quotes[0].buyAmount);
35
+ console.log(` Got ${quotes.length} quote(s). Best: 0.001 ETH -> ${Number(buyAmount) / 1e18} STRK`);
36
+ }
37
+ else {
38
+ console.log(" No quotes available.");
39
+ }
40
+ }
41
+ catch (err) {
42
+ console.log(" Error:", err.message);
43
+ }
44
+ // Try WBTC -> GRIT quote (Grinta-specific, likely via Ekubo pool)
45
+ console.log("\n2) Fetching WBTC -> GRIT quote (0.01 WBTC)...");
46
+ try {
47
+ const wbtcGritQuote = {
48
+ sellTokenAddress: TOKENS.WBTC,
49
+ buyTokenAddress: CONFIG.ACCOUNT_ADDRESS, // wrong, let me use GRIT
50
+ sellAmount: BigInt(10 ** 6), // 0.01 WBTC (8 decimals)
51
+ takerAddress: CONFIG.ACCOUNT_ADDRESS,
52
+ };
53
+ // Fix: use GRIT (SAFEEngine) address
54
+ wbtcGritQuote.buyTokenAddress = "0x078802abe86444d116c73821c7b6aff8175bd558bf335b28247b825d49490ef2";
55
+ const quotes = await getQuotes(wbtcGritQuote, { baseUrl: CONFIG.AVNU_BASE_URL });
56
+ if (quotes.length > 0) {
57
+ const buyAmount = BigInt(quotes[0].buyAmount);
58
+ console.log(` Got ${quotes.length} quote(s). Best: 0.01 WBTC -> ${Number(buyAmount) / 1e18} GRIT`);
59
+ }
60
+ else {
61
+ console.log(" No quotes available.");
62
+ }
63
+ }
64
+ catch (err) {
65
+ console.log(" Error:", err.message);
66
+ }
67
+ // Try GRIT -> USDC quote (the Ekubo pool pair)
68
+ console.log("\n3) Fetching GRIT -> USDC quote (100 GRIT)...");
69
+ try {
70
+ const gritUsdcQuote = {
71
+ sellTokenAddress: "0x078802abe86444d116c73821c7b6aff8175bd558bf335b28247b825d49490ef2",
72
+ buyTokenAddress: TOKENS.USDC,
73
+ sellAmount: BigInt(100) * BigInt(10 ** 18), // 100 GRIT
74
+ takerAddress: CONFIG.ACCOUNT_ADDRESS,
75
+ };
76
+ const quotes = await getQuotes(gritUsdcQuote, { baseUrl: CONFIG.AVNU_BASE_URL });
77
+ if (quotes.length > 0) {
78
+ const buyAmount = BigInt(quotes[0].buyAmount);
79
+ console.log(` Got ${quotes.length} quote(s). Best: 100 GRIT -> ${Number(buyAmount) / 1e6} USDC`);
80
+ }
81
+ else {
82
+ console.log(" No quotes available.");
83
+ }
84
+ }
85
+ catch (err) {
86
+ console.log(" Error:", err.message);
87
+ }
88
+ console.log("\nDone.");
89
+ }
90
+ main().catch((err) => {
91
+ console.error("Fatal:", err);
92
+ process.exit(1);
93
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared utilities for Starknet Agent
3
+ */
4
+ /**
5
+ * Format a token amount from raw units to human-readable
6
+ */
7
+ export declare function formatAmount(raw: bigint, decimals: number): string;
8
+ /**
9
+ * Parse a human-readable amount to raw units
10
+ */
11
+ export declare function parseAmount(amount: string, decimals: number): bigint;
12
+ /**
13
+ * Sleep for the specified milliseconds
14
+ */
15
+ export declare function sleep(ms: number): Promise<void>;
16
+ /**
17
+ * Validate a Starknet address format
18
+ */
19
+ export declare function isValidAddress(address: string): boolean;