@aspan/sdk 0.1.4
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 +451 -0
- package/dist/index.d.mts +1324 -0
- package/dist/index.d.ts +1324 -0
- package/dist/index.js +1451 -0
- package/dist/index.mjs +1413 -0
- package/package.json +63 -0
- package/src/abi/diamond.ts +531 -0
- package/src/bot/config.ts +84 -0
- package/src/bot/index.ts +133 -0
- package/src/bot/monitors/cr-monitor.ts +182 -0
- package/src/bot/monitors/mempool-monitor.ts +167 -0
- package/src/bot/monitors/stats-monitor.ts +287 -0
- package/src/bot/services/rpc-client.ts +61 -0
- package/src/bot/services/slack.ts +95 -0
- package/src/client.ts +1030 -0
- package/src/index.ts +144 -0
- package/src/types.ts +237 -0
package/src/bot/index.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aspan Protocol Monitoring Bot
|
|
3
|
+
* Main entry point
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import "dotenv/config";
|
|
7
|
+
import { createAspanReadClient } from "../index";
|
|
8
|
+
import { loadConfig } from "./config";
|
|
9
|
+
import { SlackService } from "./services/slack";
|
|
10
|
+
import { createRobustClient } from "./services/rpc-client";
|
|
11
|
+
import { StatsMonitor } from "./monitors/stats-monitor";
|
|
12
|
+
import { CRMonitor } from "./monitors/cr-monitor";
|
|
13
|
+
import { MempoolMonitor } from "./monitors/mempool-monitor";
|
|
14
|
+
|
|
15
|
+
class AspanMonitoringBot {
|
|
16
|
+
private config = loadConfig();
|
|
17
|
+
private slack: SlackService;
|
|
18
|
+
private statsMonitor: StatsMonitor;
|
|
19
|
+
private crMonitor: CRMonitor;
|
|
20
|
+
private mempoolMonitor: MempoolMonitor;
|
|
21
|
+
private isRunning = false;
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
console.log("Initializing Aspan Monitoring Bot...");
|
|
25
|
+
|
|
26
|
+
// Initialize Slack service
|
|
27
|
+
this.slack = new SlackService(
|
|
28
|
+
this.config.slackWebhookUrl,
|
|
29
|
+
this.config.slackRateLimitMs
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Create SDK client
|
|
33
|
+
const aspanClient = createAspanReadClient(
|
|
34
|
+
this.config.diamondAddress,
|
|
35
|
+
this.config.rpcUrl
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Create robust public client for event watching
|
|
39
|
+
const publicClient = createRobustClient(this.config);
|
|
40
|
+
|
|
41
|
+
// Initialize monitors
|
|
42
|
+
this.statsMonitor = new StatsMonitor(aspanClient, this.slack, this.config);
|
|
43
|
+
this.crMonitor = new CRMonitor(aspanClient, this.slack, this.config);
|
|
44
|
+
this.mempoolMonitor = new MempoolMonitor(
|
|
45
|
+
aspanClient,
|
|
46
|
+
publicClient,
|
|
47
|
+
this.slack,
|
|
48
|
+
this.config
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
console.log(`Diamond Address: ${this.config.diamondAddress}`);
|
|
52
|
+
console.log(`RPC URL: ${this.config.rpcUrl}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async start(): Promise<void> {
|
|
56
|
+
if (this.isRunning) {
|
|
57
|
+
console.warn("Bot is already running");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log("\nStarting Aspan Monitoring Bot...\n");
|
|
62
|
+
|
|
63
|
+
// Send startup notification
|
|
64
|
+
await this.slack.send({
|
|
65
|
+
text: ":robot_face: Aspan Monitoring Bot started",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Start all monitors
|
|
69
|
+
this.statsMonitor.start();
|
|
70
|
+
this.crMonitor.start();
|
|
71
|
+
await this.mempoolMonitor.start();
|
|
72
|
+
|
|
73
|
+
this.isRunning = true;
|
|
74
|
+
console.log("\nAll monitors started successfully!\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async stop(): Promise<void> {
|
|
78
|
+
if (!this.isRunning) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log("\nStopping Aspan Monitoring Bot...");
|
|
83
|
+
|
|
84
|
+
// Stop all monitors
|
|
85
|
+
this.statsMonitor.stop();
|
|
86
|
+
this.crMonitor.stop();
|
|
87
|
+
this.mempoolMonitor.stop();
|
|
88
|
+
|
|
89
|
+
// Send shutdown notification
|
|
90
|
+
await this.slack.send({
|
|
91
|
+
text: ":wave: Aspan Monitoring Bot stopped",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this.isRunning = false;
|
|
95
|
+
console.log("Bot stopped\n");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Main execution
|
|
100
|
+
async function main() {
|
|
101
|
+
const bot = new AspanMonitoringBot();
|
|
102
|
+
|
|
103
|
+
// Graceful shutdown handlers
|
|
104
|
+
const shutdown = async (signal: string) => {
|
|
105
|
+
console.log(`\nReceived ${signal}, shutting down gracefully...`);
|
|
106
|
+
await bot.stop();
|
|
107
|
+
process.exit(0);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
111
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
112
|
+
|
|
113
|
+
process.on("uncaughtException", (error) => {
|
|
114
|
+
console.error("Uncaught exception:", error);
|
|
115
|
+
shutdown("uncaughtException");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
process.on("unhandledRejection", (reason) => {
|
|
119
|
+
console.error("Unhandled rejection:", reason);
|
|
120
|
+
shutdown("unhandledRejection");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Start the bot
|
|
124
|
+
await bot.start();
|
|
125
|
+
|
|
126
|
+
// Keep process alive
|
|
127
|
+
console.log("Bot is running. Press Ctrl+C to stop.\n");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
main().catch((error) => {
|
|
131
|
+
console.error("Fatal error:", error);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CR (Collateral Ratio) Monitor
|
|
3
|
+
* Monitors CR and alerts when thresholds are breached
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AspanReadClient } from "../../client";
|
|
7
|
+
import { formatCR } from "../../index";
|
|
8
|
+
import type { SlackService } from "../services/slack";
|
|
9
|
+
import { SlackColors } from "../services/slack";
|
|
10
|
+
import type { BotConfig } from "../config";
|
|
11
|
+
import { withRetry } from "../services/rpc-client";
|
|
12
|
+
|
|
13
|
+
interface AlertState {
|
|
14
|
+
mode1Triggered: boolean;
|
|
15
|
+
mode2Triggered: boolean;
|
|
16
|
+
lastAlertTime: number;
|
|
17
|
+
lastCR: bigint;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class CRMonitor {
|
|
21
|
+
private client: AspanReadClient;
|
|
22
|
+
private slack: SlackService;
|
|
23
|
+
private config: BotConfig;
|
|
24
|
+
private intervalId?: ReturnType<typeof setInterval>;
|
|
25
|
+
private state: AlertState = {
|
|
26
|
+
mode1Triggered: false,
|
|
27
|
+
mode2Triggered: false,
|
|
28
|
+
lastAlertTime: 0,
|
|
29
|
+
lastCR: 0n,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Cooldown between repeat alerts (5 minutes)
|
|
33
|
+
private readonly ALERT_COOLDOWN_MS = 5 * 60 * 1000;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
client: AspanReadClient,
|
|
37
|
+
slack: SlackService,
|
|
38
|
+
config: BotConfig
|
|
39
|
+
) {
|
|
40
|
+
this.client = client;
|
|
41
|
+
this.slack = slack;
|
|
42
|
+
this.config = config;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
start(): void {
|
|
46
|
+
console.log(
|
|
47
|
+
`[CRMonitor] Starting with ${this.config.crCheckInterval / 1000}s interval`
|
|
48
|
+
);
|
|
49
|
+
console.log(
|
|
50
|
+
`[CRMonitor] Thresholds - Mode1: ${formatCR(this.config.crThresholdMode1)}, Mode2: ${formatCR(this.config.crThresholdMode2)}`
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Initial check
|
|
54
|
+
this.checkCR();
|
|
55
|
+
|
|
56
|
+
// Schedule regular checks
|
|
57
|
+
this.intervalId = setInterval(
|
|
58
|
+
() => this.checkCR(),
|
|
59
|
+
this.config.crCheckInterval
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
stop(): void {
|
|
64
|
+
if (this.intervalId) {
|
|
65
|
+
clearInterval(this.intervalId);
|
|
66
|
+
this.intervalId = undefined;
|
|
67
|
+
console.log("[CRMonitor] Stopped");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private async checkCR(): Promise<void> {
|
|
72
|
+
try {
|
|
73
|
+
const stabilityMode = await withRetry(
|
|
74
|
+
() => this.client.getStabilityMode(),
|
|
75
|
+
this.config,
|
|
76
|
+
"getStabilityMode"
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const currentCR = stabilityMode.currentCR;
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
|
|
82
|
+
// Check Mode 2 threshold (< 130%)
|
|
83
|
+
if (currentCR < this.config.crThresholdMode2) {
|
|
84
|
+
if (!this.state.mode2Triggered || this.shouldRepeatAlert(now)) {
|
|
85
|
+
await this.sendCriticalAlert(currentCR);
|
|
86
|
+
this.state.mode2Triggered = true;
|
|
87
|
+
this.state.mode1Triggered = true; // Also in mode1 range
|
|
88
|
+
this.state.lastAlertTime = now;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Check Mode 1 threshold (< 150% but >= 130%)
|
|
92
|
+
else if (currentCR < this.config.crThresholdMode1) {
|
|
93
|
+
// If recovering from Mode 2
|
|
94
|
+
if (this.state.mode2Triggered) {
|
|
95
|
+
await this.sendRecoveryAlert(currentCR, "Mode 2", "Mode 1");
|
|
96
|
+
this.state.mode2Triggered = false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!this.state.mode1Triggered || this.shouldRepeatAlert(now)) {
|
|
100
|
+
await this.sendWarningAlert(currentCR);
|
|
101
|
+
this.state.mode1Triggered = true;
|
|
102
|
+
this.state.lastAlertTime = now;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// CR is healthy (>= 150%)
|
|
106
|
+
else {
|
|
107
|
+
if (this.state.mode1Triggered || this.state.mode2Triggered) {
|
|
108
|
+
const fromMode = this.state.mode2Triggered ? "Mode 2" : "Mode 1";
|
|
109
|
+
await this.sendRecoveryAlert(currentCR, fromMode, "Normal");
|
|
110
|
+
this.state.mode1Triggered = false;
|
|
111
|
+
this.state.mode2Triggered = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.state.lastCR = currentCR;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error("[CRMonitor] CR check failed:", error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private shouldRepeatAlert(now: number): boolean {
|
|
122
|
+
return now - this.state.lastAlertTime > this.ALERT_COOLDOWN_MS;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async sendWarningAlert(cr: bigint): Promise<void> {
|
|
126
|
+
console.log(`[CRMonitor] Sending Mode 1 warning alert - CR: ${formatCR(cr)}`);
|
|
127
|
+
|
|
128
|
+
await this.slack.send({
|
|
129
|
+
attachments: [
|
|
130
|
+
{
|
|
131
|
+
color: SlackColors.warning,
|
|
132
|
+
title: ":warning: CR Warning - Mode 1 Threshold Breached",
|
|
133
|
+
text: "Collateral Ratio has dropped below 150%. Protocol entering stability mode.",
|
|
134
|
+
fields: [
|
|
135
|
+
{ title: "Current CR", value: formatCR(cr), short: true },
|
|
136
|
+
{ title: "Threshold", value: "150%", short: true },
|
|
137
|
+
],
|
|
138
|
+
ts: Math.floor(Date.now() / 1000),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async sendCriticalAlert(cr: bigint): Promise<void> {
|
|
145
|
+
console.log(`[CRMonitor] Sending Mode 2 CRITICAL alert - CR: ${formatCR(cr)}`);
|
|
146
|
+
|
|
147
|
+
await this.slack.send({
|
|
148
|
+
attachments: [
|
|
149
|
+
{
|
|
150
|
+
color: SlackColors.danger,
|
|
151
|
+
title: ":rotating_light: CRITICAL - Mode 2 Threshold Breached",
|
|
152
|
+
text: "Collateral Ratio has dropped below 130%! Stability Mode 2 may trigger forced conversion.",
|
|
153
|
+
fields: [
|
|
154
|
+
{ title: "Current CR", value: formatCR(cr), short: true },
|
|
155
|
+
{ title: "Threshold", value: "130%", short: true },
|
|
156
|
+
],
|
|
157
|
+
ts: Math.floor(Date.now() / 1000),
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async sendRecoveryAlert(
|
|
164
|
+
cr: bigint,
|
|
165
|
+
from: string,
|
|
166
|
+
to: string
|
|
167
|
+
): Promise<void> {
|
|
168
|
+
console.log(`[CRMonitor] CR recovered from ${from} to ${to} - CR: ${formatCR(cr)}`);
|
|
169
|
+
|
|
170
|
+
await this.slack.send({
|
|
171
|
+
attachments: [
|
|
172
|
+
{
|
|
173
|
+
color: SlackColors.success,
|
|
174
|
+
title: `:white_check_mark: CR Recovered - ${from} -> ${to}`,
|
|
175
|
+
text: "Collateral Ratio has recovered above threshold.",
|
|
176
|
+
fields: [{ title: "Current CR", value: formatCR(cr), short: true }],
|
|
177
|
+
ts: Math.floor(Date.now() / 1000),
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mempool/TVL Monitor
|
|
3
|
+
* Monitors contract events for large TVL changes
|
|
4
|
+
*
|
|
5
|
+
* Note: BSC doesn't expose public mempool, so we use event-based monitoring
|
|
6
|
+
* which is reactive (post-confirmation) rather than proactive
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PublicClient, Log } from "viem";
|
|
10
|
+
import type { AspanReadClient } from "../../client";
|
|
11
|
+
import { DiamondABI, formatAmount } from "../../index";
|
|
12
|
+
import type { SlackService } from "../services/slack";
|
|
13
|
+
import { SlackColors } from "../services/slack";
|
|
14
|
+
import type { BotConfig } from "../config";
|
|
15
|
+
import { withRetry } from "../services/rpc-client";
|
|
16
|
+
|
|
17
|
+
export class MempoolMonitor {
|
|
18
|
+
private client: AspanReadClient;
|
|
19
|
+
private publicClient: PublicClient;
|
|
20
|
+
private slack: SlackService;
|
|
21
|
+
private config: BotConfig;
|
|
22
|
+
private unwatch?: () => void;
|
|
23
|
+
private lastKnownTVL: bigint = 0n;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
client: AspanReadClient,
|
|
27
|
+
publicClient: PublicClient,
|
|
28
|
+
slack: SlackService,
|
|
29
|
+
config: BotConfig
|
|
30
|
+
) {
|
|
31
|
+
this.client = client;
|
|
32
|
+
this.publicClient = publicClient;
|
|
33
|
+
this.slack = slack;
|
|
34
|
+
this.config = config;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async start(): Promise<void> {
|
|
38
|
+
console.log(
|
|
39
|
+
`[MempoolMonitor] Starting with ${this.config.tvlImpactThresholdPercent}% TVL impact threshold`
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Get initial TVL
|
|
43
|
+
this.lastKnownTVL = await withRetry(
|
|
44
|
+
() => this.client.getTVLInUSD(),
|
|
45
|
+
this.config,
|
|
46
|
+
"getTVLInUSD"
|
|
47
|
+
);
|
|
48
|
+
console.log(
|
|
49
|
+
`[MempoolMonitor] Initial TVL: $${formatAmount(this.lastKnownTVL, 2)}`
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Watch for contract events
|
|
53
|
+
this.unwatch = this.publicClient.watchContractEvent({
|
|
54
|
+
address: this.config.diamondAddress,
|
|
55
|
+
abi: DiamondABI,
|
|
56
|
+
onLogs: (logs) => this.handleLogs(logs),
|
|
57
|
+
onError: (error) => console.error("[MempoolMonitor] Event watch error:", error),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
console.log("[MempoolMonitor] Event watcher started");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
stop(): void {
|
|
64
|
+
if (this.unwatch) {
|
|
65
|
+
this.unwatch();
|
|
66
|
+
this.unwatch = undefined;
|
|
67
|
+
console.log("[MempoolMonitor] Stopped");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private async handleLogs(logs: Log[]): Promise<void> {
|
|
72
|
+
for (const log of logs) {
|
|
73
|
+
// Check if this is a TVL-impacting event by looking at event signature
|
|
74
|
+
await this.checkTVLImpact(log);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async checkTVLImpact(log: Log): Promise<void> {
|
|
79
|
+
try {
|
|
80
|
+
// Get current TVL
|
|
81
|
+
const currentTVL = await withRetry(
|
|
82
|
+
() => this.client.getTVLInUSD(),
|
|
83
|
+
this.config,
|
|
84
|
+
"getTVLInUSD"
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (this.lastKnownTVL === 0n) {
|
|
88
|
+
this.lastKnownTVL = currentTVL;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const tvlChange = currentTVL - this.lastKnownTVL;
|
|
93
|
+
const impactPercent =
|
|
94
|
+
this.lastKnownTVL > 0n
|
|
95
|
+
? Number((tvlChange * 10000n) / this.lastKnownTVL) / 100
|
|
96
|
+
: 0;
|
|
97
|
+
|
|
98
|
+
const previousTVL = this.lastKnownTVL;
|
|
99
|
+
this.lastKnownTVL = currentTVL;
|
|
100
|
+
|
|
101
|
+
// Check if impact exceeds threshold
|
|
102
|
+
if (Math.abs(impactPercent) >= this.config.tvlImpactThresholdPercent) {
|
|
103
|
+
const isDecrease = tvlChange < 0n;
|
|
104
|
+
await this.sendImpactAlert(
|
|
105
|
+
log.transactionHash ?? "unknown",
|
|
106
|
+
previousTVL,
|
|
107
|
+
currentTVL,
|
|
108
|
+
impactPercent,
|
|
109
|
+
isDecrease
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error("[MempoolMonitor] Error checking TVL impact:", error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async sendImpactAlert(
|
|
118
|
+
txHash: string,
|
|
119
|
+
previousTVL: bigint,
|
|
120
|
+
currentTVL: bigint,
|
|
121
|
+
impactPercent: number,
|
|
122
|
+
isDecrease: boolean
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
const direction = isDecrease ? "DECREASE" : "INCREASE";
|
|
125
|
+
const emoji = isDecrease
|
|
126
|
+
? ":chart_with_downwards_trend:"
|
|
127
|
+
: ":chart_with_upwards_trend:";
|
|
128
|
+
const color = isDecrease ? SlackColors.danger : SlackColors.success;
|
|
129
|
+
|
|
130
|
+
console.log(
|
|
131
|
+
`[MempoolMonitor] Large TVL ${direction}: ${impactPercent.toFixed(2)}% - TX: ${txHash.slice(0, 18)}...`
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
await this.slack.send({
|
|
135
|
+
attachments: [
|
|
136
|
+
{
|
|
137
|
+
color,
|
|
138
|
+
title: `${emoji} Large TVL ${direction} Detected`,
|
|
139
|
+
text: `A transaction caused a ${Math.abs(impactPercent).toFixed(2)}% ${direction.toLowerCase()} in TVL`,
|
|
140
|
+
fields: [
|
|
141
|
+
{
|
|
142
|
+
title: "Impact",
|
|
143
|
+
value: `${impactPercent >= 0 ? "+" : ""}${impactPercent.toFixed(2)}%`,
|
|
144
|
+
short: true,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
title: "Previous TVL",
|
|
148
|
+
value: `$${formatAmount(previousTVL, 2)}`,
|
|
149
|
+
short: true,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
title: "Current TVL",
|
|
153
|
+
value: `$${formatAmount(currentTVL, 2)}`,
|
|
154
|
+
short: true,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
title: "Transaction",
|
|
158
|
+
value: `\`${txHash.slice(0, 18)}...\``,
|
|
159
|
+
short: true,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
ts: Math.floor(Date.now() / 1000),
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|