@aspan/sdk 0.4.0 → 0.4.2
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 +44 -4
- package/dist/index.d.mts +245 -8
- package/dist/index.d.ts +245 -8
- package/dist/index.js +278 -8
- package/dist/index.mjs +265 -7
- package/package.json +1 -1
- package/src/__tests__/fork.test.ts +2 -2
- package/src/abi/diamond.ts +74 -0
- package/src/abi/sApUSD.ts +100 -0
- package/src/bot/config.ts +51 -1
- package/src/bot/index.ts +69 -1
- package/src/bot/services/fee-manager.ts +303 -0
- package/src/bot/services/risk-keeper.ts +517 -0
- package/src/client.ts +215 -1
- package/src/index.ts +30 -6
- package/src/router.ts +69 -0
package/src/bot/index.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { createRobustClient } from "./services/rpc-client";
|
|
|
11
11
|
import { StatsMonitor } from "./monitors/stats-monitor";
|
|
12
12
|
import { CRMonitor } from "./monitors/cr-monitor";
|
|
13
13
|
import { MempoolMonitor } from "./monitors/mempool-monitor";
|
|
14
|
+
import { RiskKeeperService } from "./services/risk-keeper";
|
|
15
|
+
import { FeeManagerService } from "./services/fee-manager";
|
|
14
16
|
|
|
15
17
|
class AspanMonitoringBot {
|
|
16
18
|
private config = loadConfig();
|
|
@@ -18,6 +20,8 @@ class AspanMonitoringBot {
|
|
|
18
20
|
private statsMonitor: StatsMonitor;
|
|
19
21
|
private crMonitor: CRMonitor;
|
|
20
22
|
private mempoolMonitor: MempoolMonitor;
|
|
23
|
+
private riskKeeper?: RiskKeeperService;
|
|
24
|
+
private feeManager?: FeeManagerService;
|
|
21
25
|
private isRunning = false;
|
|
22
26
|
|
|
23
27
|
constructor() {
|
|
@@ -48,6 +52,47 @@ class AspanMonitoringBot {
|
|
|
48
52
|
this.config
|
|
49
53
|
);
|
|
50
54
|
|
|
55
|
+
// Initialize Risk Keeper if enabled
|
|
56
|
+
if (this.config.riskKeeper?.enabled) {
|
|
57
|
+
console.log("Initializing Risk Keeper...");
|
|
58
|
+
this.riskKeeper = new RiskKeeperService(
|
|
59
|
+
aspanClient,
|
|
60
|
+
this.slack,
|
|
61
|
+
this.config,
|
|
62
|
+
{
|
|
63
|
+
diamondAddress: this.config.diamondAddress,
|
|
64
|
+
sApUSDAddress: this.config.riskKeeper.sApUSDAddress,
|
|
65
|
+
routerAddress: this.config.riskKeeper.routerAddress,
|
|
66
|
+
wbnbAddress: this.config.riskKeeper.wbnbAddress,
|
|
67
|
+
privateKey: this.config.riskKeeper.privateKey,
|
|
68
|
+
rpcUrl: this.config.rpcUrl,
|
|
69
|
+
sm2CooldownMs: this.config.riskKeeper.sm2CooldownMs,
|
|
70
|
+
sm2MaxPoolPercent: this.config.riskKeeper.sm2MaxPoolPercent,
|
|
71
|
+
twapIntervalMs: this.config.riskKeeper.twapIntervalMs,
|
|
72
|
+
twapPercentage: this.config.riskKeeper.twapPercentage,
|
|
73
|
+
maxSlippageBps: this.config.riskKeeper.maxSlippageBps,
|
|
74
|
+
twapStartCR: this.config.riskKeeper.twapStartCR,
|
|
75
|
+
maxVolatilityPercent: this.config.riskKeeper.maxVolatilityPercent,
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Initialize Fee Manager if enabled
|
|
81
|
+
if (this.config.feeManager?.enabled) {
|
|
82
|
+
console.log("Initializing Fee Manager...");
|
|
83
|
+
this.feeManager = new FeeManagerService(
|
|
84
|
+
aspanClient,
|
|
85
|
+
this.slack,
|
|
86
|
+
this.config,
|
|
87
|
+
{
|
|
88
|
+
diamondAddress: this.config.diamondAddress,
|
|
89
|
+
privateKey: this.config.feeManager.privateKey,
|
|
90
|
+
rpcUrl: this.config.rpcUrl,
|
|
91
|
+
checkIntervalMs: this.config.feeManager.checkIntervalMs,
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
51
96
|
console.log(`Diamond Address: ${this.config.diamondAddress}`);
|
|
52
97
|
console.log(`RPC URL: ${this.config.rpcUrl}`);
|
|
53
98
|
}
|
|
@@ -61,8 +106,9 @@ class AspanMonitoringBot {
|
|
|
61
106
|
console.log("\nStarting Aspan Monitoring Bot...\n");
|
|
62
107
|
|
|
63
108
|
// Send startup notification
|
|
109
|
+
const riskKeeperStatus = this.riskKeeper ? " + Risk Keeper" : "";
|
|
64
110
|
await this.slack.send({
|
|
65
|
-
text:
|
|
111
|
+
text: `:robot_face: Aspan Monitoring Bot started${riskKeeperStatus}`,
|
|
66
112
|
});
|
|
67
113
|
|
|
68
114
|
// Start all monitors
|
|
@@ -70,6 +116,18 @@ class AspanMonitoringBot {
|
|
|
70
116
|
this.crMonitor.start();
|
|
71
117
|
await this.mempoolMonitor.start();
|
|
72
118
|
|
|
119
|
+
// Start Risk Keeper if enabled
|
|
120
|
+
if (this.riskKeeper) {
|
|
121
|
+
this.riskKeeper.start();
|
|
122
|
+
console.log("Risk Keeper started");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Start Fee Manager if enabled
|
|
126
|
+
if (this.feeManager) {
|
|
127
|
+
this.feeManager.start();
|
|
128
|
+
console.log("Fee Manager started");
|
|
129
|
+
}
|
|
130
|
+
|
|
73
131
|
this.isRunning = true;
|
|
74
132
|
console.log("\nAll monitors started successfully!\n");
|
|
75
133
|
}
|
|
@@ -86,6 +144,16 @@ class AspanMonitoringBot {
|
|
|
86
144
|
this.crMonitor.stop();
|
|
87
145
|
this.mempoolMonitor.stop();
|
|
88
146
|
|
|
147
|
+
// Stop Risk Keeper if running
|
|
148
|
+
if (this.riskKeeper) {
|
|
149
|
+
this.riskKeeper.stop();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Stop Fee Manager if running
|
|
153
|
+
if (this.feeManager) {
|
|
154
|
+
this.feeManager.stop();
|
|
155
|
+
}
|
|
156
|
+
|
|
89
157
|
// Send shutdown notification
|
|
90
158
|
await this.slack.send({
|
|
91
159
|
text: ":wave: Aspan Monitoring Bot stopped",
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fee Manager Service
|
|
3
|
+
* Automatically adjusts fee tiers based on CR zones
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createWalletClient,
|
|
8
|
+
http,
|
|
9
|
+
type Address,
|
|
10
|
+
type Chain,
|
|
11
|
+
type Account,
|
|
12
|
+
type Hash,
|
|
13
|
+
} from "viem";
|
|
14
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
15
|
+
import { bsc } from "viem/chains";
|
|
16
|
+
import type { AspanReadClient } from "../../client";
|
|
17
|
+
import { DiamondABI } from "../../abi/diamond";
|
|
18
|
+
import type { SlackService } from "./slack";
|
|
19
|
+
import { SlackColors } from "./slack";
|
|
20
|
+
import type { BotConfig } from "../config";
|
|
21
|
+
import { withRetry } from "./rpc-client";
|
|
22
|
+
|
|
23
|
+
// ============ Types ============
|
|
24
|
+
|
|
25
|
+
export interface FeeManagerConfig {
|
|
26
|
+
/** Diamond contract address */
|
|
27
|
+
diamondAddress: Address;
|
|
28
|
+
/** Fee manager private key */
|
|
29
|
+
privateKey: `0x${string}`;
|
|
30
|
+
/** RPC URL */
|
|
31
|
+
rpcUrl: string;
|
|
32
|
+
/** Chain */
|
|
33
|
+
chain?: Chain;
|
|
34
|
+
/** Check interval in ms (default 1 min) */
|
|
35
|
+
checkIntervalMs?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fee tier configuration per zone
|
|
40
|
+
* Based on Aspan Risk Management Framework
|
|
41
|
+
*/
|
|
42
|
+
interface FeeTier {
|
|
43
|
+
minCR: bigint; // Minimum CR for this tier (BPS)
|
|
44
|
+
apUSDMintFee: number; // BPS
|
|
45
|
+
apUSDRedeemFee: number; // BPS
|
|
46
|
+
xBNBMintFee: number; // BPS
|
|
47
|
+
xBNBRedeemFee: number; // BPS
|
|
48
|
+
apUSDMintDisabled: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Zone boundaries (in BPS)
|
|
52
|
+
const ZONE_A_MIN = 17000n; // > 170%
|
|
53
|
+
const ZONE_B_MIN = 13000n; // 130-170%
|
|
54
|
+
const ZONE_C_MIN = 11000n; // 110-130%
|
|
55
|
+
// Zone D: < 110%
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Fee tiers per zone as defined in risk framework
|
|
59
|
+
*
|
|
60
|
+
* Zone A (>170%): Healthy - promote growth
|
|
61
|
+
* Zone B (130-170%): Normal - balanced fees
|
|
62
|
+
* Zone C (110-130%): Warning - encourage rebalancing
|
|
63
|
+
* Zone D (<110%): Critical - emergency measures
|
|
64
|
+
*/
|
|
65
|
+
const FEE_TIERS: FeeTier[] = [
|
|
66
|
+
// Zone A: >170%
|
|
67
|
+
{
|
|
68
|
+
minCR: ZONE_A_MIN,
|
|
69
|
+
apUSDMintFee: 0, // 0% - promote minting
|
|
70
|
+
apUSDRedeemFee: 100, // 1%
|
|
71
|
+
xBNBMintFee: 200, // 2%
|
|
72
|
+
xBNBRedeemFee: 0, // 0% - promote redemption
|
|
73
|
+
apUSDMintDisabled: false,
|
|
74
|
+
},
|
|
75
|
+
// Zone B: 130-170%
|
|
76
|
+
{
|
|
77
|
+
minCR: ZONE_B_MIN,
|
|
78
|
+
apUSDMintFee: 10, // 0.1%
|
|
79
|
+
apUSDRedeemFee: 30, // 0.3%
|
|
80
|
+
xBNBMintFee: 100, // 1%
|
|
81
|
+
xBNBRedeemFee: 100, // 1%
|
|
82
|
+
apUSDMintDisabled: false,
|
|
83
|
+
},
|
|
84
|
+
// Zone C: 110-130%
|
|
85
|
+
{
|
|
86
|
+
minCR: ZONE_C_MIN,
|
|
87
|
+
apUSDMintFee: 50, // 0.5%
|
|
88
|
+
apUSDRedeemFee: 0, // 0% - encourage redemption
|
|
89
|
+
xBNBMintFee: 50, // 0.5% - promote minting
|
|
90
|
+
xBNBRedeemFee: 400, // 4% - discourage redemption
|
|
91
|
+
apUSDMintDisabled: false,
|
|
92
|
+
},
|
|
93
|
+
// Zone D: <110%
|
|
94
|
+
{
|
|
95
|
+
minCR: 0n,
|
|
96
|
+
apUSDMintFee: 0, // N/A - disabled
|
|
97
|
+
apUSDRedeemFee: 0, // 0% - encourage redemption
|
|
98
|
+
xBNBMintFee: 0, // 0% - encourage minting
|
|
99
|
+
xBNBRedeemFee: 800, // 8% - lock xBNB
|
|
100
|
+
apUSDMintDisabled: true, // Disable minting in emergency
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// ============ Fee Manager Service ============
|
|
105
|
+
|
|
106
|
+
export class FeeManagerService {
|
|
107
|
+
private readonly client: AspanReadClient;
|
|
108
|
+
private readonly walletClient: ReturnType<typeof createWalletClient>;
|
|
109
|
+
private readonly slack: SlackService;
|
|
110
|
+
private readonly config: BotConfig;
|
|
111
|
+
private readonly feeConfig: FeeManagerConfig;
|
|
112
|
+
private readonly account: Account;
|
|
113
|
+
|
|
114
|
+
private intervalId?: ReturnType<typeof setInterval>;
|
|
115
|
+
private currentZone: string = "";
|
|
116
|
+
private readonly CHECK_INTERVAL_MS: number;
|
|
117
|
+
|
|
118
|
+
constructor(
|
|
119
|
+
client: AspanReadClient,
|
|
120
|
+
slack: SlackService,
|
|
121
|
+
botConfig: BotConfig,
|
|
122
|
+
feeConfig: FeeManagerConfig
|
|
123
|
+
) {
|
|
124
|
+
this.client = client;
|
|
125
|
+
this.slack = slack;
|
|
126
|
+
this.config = botConfig;
|
|
127
|
+
this.feeConfig = feeConfig;
|
|
128
|
+
|
|
129
|
+
this.CHECK_INTERVAL_MS = feeConfig.checkIntervalMs ?? 60 * 1000; // 1 min
|
|
130
|
+
|
|
131
|
+
// Setup account and client
|
|
132
|
+
this.account = privateKeyToAccount(feeConfig.privateKey);
|
|
133
|
+
const chain = feeConfig.chain ?? bsc;
|
|
134
|
+
|
|
135
|
+
this.walletClient = createWalletClient({
|
|
136
|
+
chain,
|
|
137
|
+
transport: http(feeConfig.rpcUrl),
|
|
138
|
+
account: this.account,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
console.log(`[FeeManager] Initialized with address: ${this.account.address}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ============ Lifecycle ============
|
|
145
|
+
|
|
146
|
+
start(): void {
|
|
147
|
+
console.log("[FeeManager] Starting fee manager service...");
|
|
148
|
+
console.log(`[FeeManager] Check interval: ${this.CHECK_INTERVAL_MS / 1000}s`);
|
|
149
|
+
|
|
150
|
+
// Initial check
|
|
151
|
+
this.checkAndAdjustFees();
|
|
152
|
+
|
|
153
|
+
// Schedule regular checks
|
|
154
|
+
this.intervalId = setInterval(
|
|
155
|
+
() => this.checkAndAdjustFees(),
|
|
156
|
+
this.CHECK_INTERVAL_MS
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
console.log("[FeeManager] Service started");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
stop(): void {
|
|
163
|
+
if (this.intervalId) {
|
|
164
|
+
clearInterval(this.intervalId);
|
|
165
|
+
this.intervalId = undefined;
|
|
166
|
+
}
|
|
167
|
+
console.log("[FeeManager] Service stopped");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============ Core Logic ============
|
|
171
|
+
|
|
172
|
+
private async checkAndAdjustFees(): Promise<void> {
|
|
173
|
+
try {
|
|
174
|
+
// Get current CR
|
|
175
|
+
const sm = await withRetry(
|
|
176
|
+
() => this.client.getStabilityMode(),
|
|
177
|
+
this.config,
|
|
178
|
+
"getStabilityMode"
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const cr = sm.currentCR;
|
|
182
|
+
const newZone = this.getZone(cr);
|
|
183
|
+
|
|
184
|
+
// Check if zone changed
|
|
185
|
+
if (newZone === this.currentZone) {
|
|
186
|
+
return; // No change needed
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log(`[FeeManager] Zone change detected: ${this.currentZone || "initial"} -> ${newZone} (CR: ${cr})`);
|
|
190
|
+
|
|
191
|
+
// Get current on-chain fee tiers
|
|
192
|
+
const tierCount = await withRetry(
|
|
193
|
+
() => this.client.getFeeTierCount(),
|
|
194
|
+
this.config,
|
|
195
|
+
"getFeeTierCount"
|
|
196
|
+
);
|
|
197
|
+
const currentTiers = [];
|
|
198
|
+
for (let i = 0n; i < tierCount; i++) {
|
|
199
|
+
const tier = await withRetry(
|
|
200
|
+
() => this.client.getFeeTier(i),
|
|
201
|
+
this.config,
|
|
202
|
+
"getFeeTier"
|
|
203
|
+
);
|
|
204
|
+
currentTiers.push(tier);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Check if update needed
|
|
208
|
+
if (this.tiersMatch(currentTiers)) {
|
|
209
|
+
console.log("[FeeManager] Fee tiers already match target, skipping update");
|
|
210
|
+
this.currentZone = newZone;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Update fee tiers
|
|
215
|
+
const txHash = await this.updateFeeTiers();
|
|
216
|
+
|
|
217
|
+
// Notify
|
|
218
|
+
await this.notifyFeeUpdate(this.currentZone, newZone, cr, txHash);
|
|
219
|
+
|
|
220
|
+
this.currentZone = newZone;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error("[FeeManager] Fee check/adjust failed:", error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private getZone(cr: bigint): string {
|
|
227
|
+
if (cr >= ZONE_A_MIN) return "A";
|
|
228
|
+
if (cr >= ZONE_B_MIN) return "B";
|
|
229
|
+
if (cr >= ZONE_C_MIN) return "C";
|
|
230
|
+
return "D";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private tiersMatch(currentTiers: Array<{
|
|
234
|
+
minCR: bigint;
|
|
235
|
+
apUSDMintFee: number;
|
|
236
|
+
apUSDRedeemFee: number;
|
|
237
|
+
xBNBMintFee: number;
|
|
238
|
+
xBNBRedeemFee: number;
|
|
239
|
+
apUSDMintDisabled: boolean;
|
|
240
|
+
}>): boolean {
|
|
241
|
+
if (currentTiers.length !== FEE_TIERS.length) return false;
|
|
242
|
+
|
|
243
|
+
for (let i = 0; i < FEE_TIERS.length; i++) {
|
|
244
|
+
const target = FEE_TIERS[i];
|
|
245
|
+
const current = currentTiers[i];
|
|
246
|
+
|
|
247
|
+
if (
|
|
248
|
+
current.minCR !== target.minCR ||
|
|
249
|
+
current.apUSDMintFee !== target.apUSDMintFee ||
|
|
250
|
+
current.apUSDRedeemFee !== target.apUSDRedeemFee ||
|
|
251
|
+
current.xBNBMintFee !== target.xBNBMintFee ||
|
|
252
|
+
current.xBNBRedeemFee !== target.xBNBRedeemFee ||
|
|
253
|
+
current.apUSDMintDisabled !== target.apUSDMintDisabled
|
|
254
|
+
) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private async updateFeeTiers(): Promise<Hash> {
|
|
263
|
+
console.log("[FeeManager] Updating fee tiers...");
|
|
264
|
+
|
|
265
|
+
return this.walletClient.writeContract({
|
|
266
|
+
chain: bsc,
|
|
267
|
+
account: this.account, address: this.feeConfig.diamondAddress,
|
|
268
|
+
abi: DiamondABI,
|
|
269
|
+
functionName: "setFeeTiers",
|
|
270
|
+
args: [FEE_TIERS],
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ============ Notifications ============
|
|
275
|
+
|
|
276
|
+
private async notifyFeeUpdate(
|
|
277
|
+
fromZone: string,
|
|
278
|
+
toZone: string,
|
|
279
|
+
cr: bigint,
|
|
280
|
+
txHash: Hash
|
|
281
|
+
): Promise<void> {
|
|
282
|
+
const zoneColors: Record<string, string> = {
|
|
283
|
+
A: SlackColors.success,
|
|
284
|
+
B: SlackColors.info,
|
|
285
|
+
C: SlackColors.warning,
|
|
286
|
+
D: SlackColors.danger,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
await this.slack.send({
|
|
290
|
+
attachments: [{
|
|
291
|
+
color: zoneColors[toZone] || SlackColors.info,
|
|
292
|
+
title: `:gear: Fee Tiers Updated: Zone ${fromZone || "?"} -> ${toZone}`,
|
|
293
|
+
text: "Fee tiers automatically adjusted based on CR",
|
|
294
|
+
fields: [
|
|
295
|
+
{ title: "CR", value: `${Number(cr) / 100}%`, short: true },
|
|
296
|
+
{ title: "Zone", value: toZone, short: true },
|
|
297
|
+
{ title: "TX", value: `<https://bscscan.com/tx/${txHash}|View>`, short: true },
|
|
298
|
+
],
|
|
299
|
+
ts: Math.floor(Date.now() / 1000),
|
|
300
|
+
}],
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|