@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.
- package/README.md +49 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.js +45 -0
- package/dist/grinta.d.ts +99 -0
- package/dist/grinta.js +561 -0
- package/dist/identity.d.ts +24 -0
- package/dist/identity.js +74 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +284 -0
- package/dist/mcp.d.ts +8 -0
- package/dist/mcp.js +322 -0
- package/dist/price-feed.d.ts +23 -0
- package/dist/price-feed.js +151 -0
- package/dist/scan-safes.d.ts +5 -0
- package/dist/scan-safes.js +87 -0
- package/dist/swap.d.ts +25 -0
- package/dist/swap.js +271 -0
- package/dist/test-swap.d.ts +5 -0
- package/dist/test-swap.js +93 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +35 -0
- package/package.json +35 -0
|
@@ -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,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,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
|
+
});
|
package/dist/utils.d.ts
ADDED
|
@@ -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;
|