forked from Vero-protocol/vero-core-engine
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgas-oracle.ts
More file actions
123 lines (102 loc) · 3.44 KB
/
Copy pathgas-oracle.ts
File metadata and controls
123 lines (102 loc) · 3.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import { RpcClient } from "./rpc-client";
/**
* gas-oracle.ts — Dynamic fee/base-fee resolution for tx submission.
*
* Notes:
* - This bridge currently focuses on resolving base fee + computing a
* deterministic max-fee for callers.
* - “Fail closed” is used: if base fee cannot be fetched, fee resolution
* throws (no silent fallback).
*/
export interface BaseFeeStats {
/** Stellar base fee (stroops per operation) */
baseFee: number;
}
export interface FeeResolutionOptions {
/**
* Multiplier applied to baseFee.
* Example: 2.0 means maxFee = ceil(baseFee * multiplier).
*/
multiplier: number;
/**
* Additional safety stroops added after multiplier.
* Example: +100 stroops.
*/
safetyStroops?: number;
/**
* Cache TTL for baseFee lookups.
* Short TTL improves “estimates accurate” during submission bursts.
*/
cacheTtlMs?: number;
}
export interface ResolvedFee {
baseFee: number;
multiplier: number;
safetyStroops: number;
/** final computed fee in stroops */
maxFee: number;
}
type Cached<T> = { value: T; expiresAt: number };
const DEFAULTS: Required<Pick<FeeResolutionOptions, "multiplier" | "safetyStroops" | "cacheTtlMs">> = {
multiplier: 1.2,
safetyStroops: 0,
cacheTtlMs: 5_000,
};
export class GasOracle {
private baseFeeCache?: Cached<BaseFeeStats>;
/**
* Fetch current base fee from the network.
*
* Fail-closed: throws if the value cannot be fetched/parsed.
*/
async fetchBaseFee(rpc: RpcClient): Promise<BaseFeeStats> {
const now = Date.now();
if (this.baseFeeCache && this.baseFeeCache.expiresAt > now) {
return this.baseFeeCache.value;
}
const ttl = DEFAULTS.cacheTtlMs;
const stats = await rpc.call(async (server: any) => {
// Horizon/Soroban endpoint exposed via stellar-sdk.
// We intentionally use the most common call: getFeeStats.
// If stellar-sdk changes, this will fail loudly.
const res = await server.getFeeStats();
// res.base_fee in stroops per operation
const baseFee = Number(res.base_fee ?? res.baseFee);
if (!Number.isFinite(baseFee) || baseFee <= 0) {
throw new Error(`GasOracle: invalid base fee received: ${JSON.stringify(res)}`);
}
return { baseFee };
});
this.baseFeeCache = { value: stats, expiresAt: now + ttl };
return stats;
}
/**
* Deterministically compute max-fee based on resolved base fee.
*/
estimateFee(stats: BaseFeeStats, opts: FeeResolutionOptions): ResolvedFee {
const multiplier = opts.multiplier ?? DEFAULTS.multiplier;
const safetyStroops = opts.safetyStroops ?? DEFAULTS.safetyStroops;
// Deterministic + integer output (stroops must be integer)
const maxFee = Math.ceil(stats.baseFee * multiplier + safetyStroops);
return {
baseFee: stats.baseFee,
multiplier,
safetyStroops,
maxFee,
};
}
/**
* Convenience: fetch base fee (with caching) and compute maxFee.
*/
async resolveFee(rpc: RpcClient, opts: FeeResolutionOptions): Promise<ResolvedFee> {
const cacheTtlMs = opts.cacheTtlMs ?? DEFAULTS.cacheTtlMs;
// For now, caching is implemented with a GasOracle-level TTL.
// If caller provides a different TTL, we bypass the cache to ensure
// the estimate uses fresh base fee.
if (cacheTtlMs !== DEFAULTS.cacheTtlMs) {
this.baseFeeCache = undefined;
}
const stats = await this.fetchBaseFee(rpc);
return this.estimateFee(stats, opts);
}
}