openttt 0.1.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 +391 -0
- package/dist/adaptive_switch.d.ts +44 -0
- package/dist/adaptive_switch.js +108 -0
- package/dist/auto_mint.d.ts +45 -0
- package/dist/auto_mint.js +244 -0
- package/dist/dynamic_fee.d.ts +64 -0
- package/dist/dynamic_fee.js +203 -0
- package/dist/errors.d.ts +45 -0
- package/dist/errors.js +74 -0
- package/dist/evm_connector.d.ts +88 -0
- package/dist/evm_connector.js +297 -0
- package/dist/golay.d.ts +6 -0
- package/dist/golay.js +166 -0
- package/dist/grg_forward.d.ts +6 -0
- package/dist/grg_forward.js +59 -0
- package/dist/grg_inverse.d.ts +7 -0
- package/dist/grg_inverse.js +97 -0
- package/dist/grg_pipeline.d.ts +13 -0
- package/dist/grg_pipeline.js +64 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +38 -0
- package/dist/logger.d.ts +18 -0
- package/dist/logger.js +51 -0
- package/dist/networks.d.ts +13 -0
- package/dist/networks.js +23 -0
- package/dist/pool_registry.d.ts +58 -0
- package/dist/pool_registry.js +129 -0
- package/dist/protocol_fee.d.ts +56 -0
- package/dist/protocol_fee.js +176 -0
- package/dist/reed_solomon.d.ts +12 -0
- package/dist/reed_solomon.js +179 -0
- package/dist/signer.d.ts +76 -0
- package/dist/signer.js +329 -0
- package/dist/time_synthesis.d.ts +49 -0
- package/dist/time_synthesis.js +372 -0
- package/dist/ttt_builder.d.ts +32 -0
- package/dist/ttt_builder.js +84 -0
- package/dist/ttt_client.d.ts +118 -0
- package/dist/ttt_client.js +352 -0
- package/dist/types.d.ts +141 -0
- package/dist/types.js +9 -0
- package/dist/v4_hook.d.ts +43 -0
- package/dist/v4_hook.js +115 -0
- package/dist/x402_enforcer.d.ts +29 -0
- package/dist/x402_enforcer.js +67 -0
- package/package.json +51 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AutoMintEngine = void 0;
|
|
4
|
+
const ethers_1 = require("ethers");
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
6
|
+
const time_synthesis_1 = require("./time_synthesis");
|
|
7
|
+
const dynamic_fee_1 = require("./dynamic_fee");
|
|
8
|
+
const evm_connector_1 = require("./evm_connector");
|
|
9
|
+
const protocol_fee_1 = require("./protocol_fee");
|
|
10
|
+
const types_1 = require("./types");
|
|
11
|
+
const logger_1 = require("./logger");
|
|
12
|
+
const errors_1 = require("./errors");
|
|
13
|
+
/**
|
|
14
|
+
* AutoMintEngine - TTT 자동 민팅 엔진
|
|
15
|
+
* 시간 합성, 동적 수수료 계산, EVM 민팅을 하나의 루프로 결합
|
|
16
|
+
*/
|
|
17
|
+
class AutoMintEngine {
|
|
18
|
+
config;
|
|
19
|
+
timeSynthesis;
|
|
20
|
+
feeEngine;
|
|
21
|
+
evmConnector;
|
|
22
|
+
feeCollector = null;
|
|
23
|
+
timer = null;
|
|
24
|
+
isRunning = false;
|
|
25
|
+
isProcessing = false;
|
|
26
|
+
onMintCallback;
|
|
27
|
+
onFailureCallback;
|
|
28
|
+
onLatencyCallback;
|
|
29
|
+
cachedSigner = null;
|
|
30
|
+
consecutiveFailures = 0;
|
|
31
|
+
maxConsecutiveFailures = 5;
|
|
32
|
+
constructor(config) {
|
|
33
|
+
this.config = config;
|
|
34
|
+
this.timeSynthesis = new time_synthesis_1.TimeSynthesis({ sources: config.timeSources });
|
|
35
|
+
this.feeEngine = new dynamic_fee_1.DynamicFeeEngine({
|
|
36
|
+
cacheDurationMs: 5000,
|
|
37
|
+
fallbackPriceUsd: config.fallbackPriceUsd || 10000n,
|
|
38
|
+
});
|
|
39
|
+
this.evmConnector = new evm_connector_1.EVMConnector();
|
|
40
|
+
if (config.signer) {
|
|
41
|
+
this.cachedSigner = config.signer;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
getEvmConnector() {
|
|
45
|
+
return this.evmConnector;
|
|
46
|
+
}
|
|
47
|
+
setOnMint(callback) {
|
|
48
|
+
this.onMintCallback = callback;
|
|
49
|
+
}
|
|
50
|
+
setOnFailure(callback) {
|
|
51
|
+
this.onFailureCallback = callback;
|
|
52
|
+
}
|
|
53
|
+
setOnLatency(callback) {
|
|
54
|
+
this.onLatencyCallback = callback;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 엔진 초기화 (RPC 연결 및 컨트랙트 설정)
|
|
58
|
+
*/
|
|
59
|
+
async initialize() {
|
|
60
|
+
try {
|
|
61
|
+
const signerOrKey = this.config.signer || this.config.privateKey;
|
|
62
|
+
if (!signerOrKey)
|
|
63
|
+
throw new errors_1.TTTConfigError("[AutoMint] Signer or Private Key is required", "Missing both 'signer' and 'privateKey' in config", "Provide a valid ethers.Signer or a private key string in your configuration.");
|
|
64
|
+
await this.evmConnector.connect(this.config.rpcUrl, signerOrKey);
|
|
65
|
+
await this.feeEngine.connect(this.config.rpcUrl);
|
|
66
|
+
this.cachedSigner = this.evmConnector.getSigner();
|
|
67
|
+
const tttAbi = [
|
|
68
|
+
"function mint(address to, uint256 amount, bytes32 grgHash) external returns (bool)",
|
|
69
|
+
"function burn(uint256 amount, bytes32 grgHash, uint256 tier) external",
|
|
70
|
+
"function balanceOf(address account, uint256 id) external view returns (uint256)",
|
|
71
|
+
"event TTTMinted(address indexed to, uint256 indexed tokenId, uint256 amount)",
|
|
72
|
+
"event TTTBurned(address indexed from, uint256 indexed tokenId, uint256 amount, uint256 tier)"
|
|
73
|
+
];
|
|
74
|
+
this.evmConnector.attachContract(this.config.contractAddress, tttAbi);
|
|
75
|
+
if (this.config.feeCollectorAddress) {
|
|
76
|
+
this.feeCollector = new protocol_fee_1.ProtocolFeeCollector(this.config.chainId, this.config.feeCollectorAddress, this.evmConnector, this.config.protocolFeeRecipient);
|
|
77
|
+
await this.feeCollector.validateChainId();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
// State rollback: Ensure connections are closed or reset
|
|
82
|
+
this.evmConnector = new evm_connector_1.EVMConnector();
|
|
83
|
+
this.cachedSigner = null;
|
|
84
|
+
this.feeCollector = null;
|
|
85
|
+
logger_1.logger.error(`[AutoMint] Initialization failed, state rolled back: ${error instanceof Error ? error.message : error}`);
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 자동 민팅 루프 시작
|
|
91
|
+
*/
|
|
92
|
+
start() {
|
|
93
|
+
if (this.isRunning)
|
|
94
|
+
return;
|
|
95
|
+
// Clear existing timer if any
|
|
96
|
+
if (this.timer) {
|
|
97
|
+
clearInterval(this.timer);
|
|
98
|
+
this.timer = null;
|
|
99
|
+
}
|
|
100
|
+
const interval = types_1.TierIntervals[this.config.tier];
|
|
101
|
+
this.isRunning = true;
|
|
102
|
+
this.timer = setInterval(async () => {
|
|
103
|
+
if (this.isProcessing)
|
|
104
|
+
return;
|
|
105
|
+
this.isProcessing = true;
|
|
106
|
+
const tickStart = Date.now();
|
|
107
|
+
try {
|
|
108
|
+
await this.mintTick();
|
|
109
|
+
this.consecutiveFailures = 0;
|
|
110
|
+
// H2: Report latency to TTTClient
|
|
111
|
+
if (this.onLatencyCallback) {
|
|
112
|
+
this.onLatencyCallback(Date.now() - tickStart);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
this.consecutiveFailures++;
|
|
117
|
+
// H2: Report failure to TTTClient
|
|
118
|
+
if (this.onFailureCallback) {
|
|
119
|
+
this.onFailureCallback(error instanceof Error ? error : new Error(String(error)));
|
|
120
|
+
}
|
|
121
|
+
logger_1.logger.error(`[AutoMint] Tick execution failed (${this.consecutiveFailures}/${this.maxConsecutiveFailures}): ${error instanceof Error ? error.message : error}`);
|
|
122
|
+
if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
|
|
123
|
+
logger_1.logger.error(`[AutoMint] Circuit breaker triggered: ${this.consecutiveFailures} consecutive failures. Stopping engine to prevent DoS.`);
|
|
124
|
+
this.stop();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
this.isProcessing = false;
|
|
129
|
+
}
|
|
130
|
+
}, interval);
|
|
131
|
+
logger_1.logger.info(`[AutoMint] Loop started for tier ${this.config.tier} (${interval}ms)`);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 자동 민팅 루프 정지
|
|
135
|
+
*/
|
|
136
|
+
stop() {
|
|
137
|
+
if (this.timer) {
|
|
138
|
+
clearInterval(this.timer);
|
|
139
|
+
this.timer = null;
|
|
140
|
+
}
|
|
141
|
+
this.isRunning = false;
|
|
142
|
+
logger_1.logger.info(`[AutoMint] Loop stopped`);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 단일 민트 틱 실행
|
|
146
|
+
* 시간합성 → tokenId 생성 → EVM mint 호출 → 수수료 계산/차감
|
|
147
|
+
*/
|
|
148
|
+
async mintTick() {
|
|
149
|
+
// 1. 시간 합성 (Time Synthesis)
|
|
150
|
+
const synthesized = await this.timeSynthesis.synthesize();
|
|
151
|
+
if (!synthesized) {
|
|
152
|
+
logger_1.logger.warn(`[AutoMint] Time synthesis returned null/undefined, skipping tick`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Fix-12: Integrity check
|
|
156
|
+
if (synthesized.confidence === 0 || synthesized.stratum >= 16) {
|
|
157
|
+
throw new errors_1.TTTTimeSynthesisError(`[AutoMint] Synthesis integrity check failed`, `confidence=${synthesized.confidence}, stratum=${synthesized.stratum}`, `Check NTP sources or network connectivity.`);
|
|
158
|
+
}
|
|
159
|
+
// 1-1. PoT Generation & Validation (W1-1)
|
|
160
|
+
const pot = await this.timeSynthesis.generateProofOfTime();
|
|
161
|
+
if (pot.confidence < 0.5) {
|
|
162
|
+
throw new errors_1.TTTTimeSynthesisError(`[PoT] Insufficient confidence`, `Calculated confidence ${pot.confidence} is below required 0.5`, `Ensure more NTP sources are reachable or decrease uncertainty.`);
|
|
163
|
+
}
|
|
164
|
+
const potHash = ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(JSON.stringify(pot, (key, value) => typeof value === 'bigint' ? value.toString() : value)));
|
|
165
|
+
// 2. tokenId 생성 (keccak256)
|
|
166
|
+
// chainId, poolAddress, timestamp 기반 유니크 ID
|
|
167
|
+
const tokenId = ethers_1.ethers.keccak256(ethers_1.ethers.AbiCoder.defaultAbiCoder().encode(["uint256", "address", "uint64"], [BigInt(this.config.chainId), this.config.poolAddress, synthesized.timestamp]));
|
|
168
|
+
// 3. 수수료 계산
|
|
169
|
+
const feeCalculation = await this.feeEngine.calculateMintFee(this.config.tier);
|
|
170
|
+
// 4. EVM mint 호출
|
|
171
|
+
// grgHash는 현재 tokenId를 기반으로 생성 (실제 구현에선 더 복잡한 GRG 페이로드 사용 가능)
|
|
172
|
+
const grgHash = tokenId;
|
|
173
|
+
// 수취인 주소 (기본적으로 signer 주소)
|
|
174
|
+
if (!this.cachedSigner) {
|
|
175
|
+
throw new errors_1.TTTSignerError("[AutoMint] Signer not initialized", "cachedSigner is null", "Initialize the engine before calling mintTick().");
|
|
176
|
+
}
|
|
177
|
+
const recipient = await this.cachedSigner.getAddress();
|
|
178
|
+
logger_1.logger.info(`[AutoMint] Executing mint: tokenId=${tokenId.substring(0, 10)}... amount=${feeCalculation.tttAmount}`);
|
|
179
|
+
const receipt = await this.evmConnector.mintTTT(recipient, feeCalculation.tttAmount, grgHash, potHash);
|
|
180
|
+
// 5. 수수료 차감/기록 (실제 컨트랙트에서 처리되거나 SDK 레벨에서 추적)
|
|
181
|
+
// W2-3: Actual ProtocolFeeCollector call
|
|
182
|
+
let actualFeePaid = feeCalculation.protocolFeeUsd;
|
|
183
|
+
if (this.feeCollector && this.config.feeCollectorAddress) {
|
|
184
|
+
try {
|
|
185
|
+
// NOTE: Single-threaded JS guarantees atomicity between nonce generation,
|
|
186
|
+
// signing, and collection below. If running multiple AutoMintEngine instances
|
|
187
|
+
// (e.g., worker_threads or cluster), a separate nonce manager with locking is required.
|
|
188
|
+
const nonce = BigInt("0x" + (0, crypto_1.randomBytes)(8).toString("hex")); // Cryptographic nonce
|
|
189
|
+
const deadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour validity
|
|
190
|
+
const signature = await this.signFeeMessage(feeCalculation, nonce, deadline);
|
|
191
|
+
const user = recipient;
|
|
192
|
+
await this.feeCollector.collectMintFee(feeCalculation, signature, user, nonce, deadline);
|
|
193
|
+
}
|
|
194
|
+
catch (feeError) {
|
|
195
|
+
logger_1.logger.error(`[AutoMint] Fee collection failed but mint was successful: ${feeError instanceof Error ? feeError.message : feeError}`);
|
|
196
|
+
// Reset to 0 so downstream (onMint callback, ledger) does not record a fee that was never collected
|
|
197
|
+
actualFeePaid = 0n;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
logger_1.logger.info(`[AutoMint] Mint success: tx=${receipt.hash}, feePaid=${actualFeePaid} (USDC eq)`);
|
|
201
|
+
if (this.onMintCallback) {
|
|
202
|
+
this.onMintCallback({
|
|
203
|
+
tokenId: tokenId,
|
|
204
|
+
grgHash: grgHash,
|
|
205
|
+
timestamp: synthesized.timestamp,
|
|
206
|
+
txHash: receipt.hash,
|
|
207
|
+
protocolFeePaid: actualFeePaid,
|
|
208
|
+
proofOfTime: pot
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async signFeeMessage(feeCalc, nonce, deadline) {
|
|
213
|
+
if (!this.cachedSigner) {
|
|
214
|
+
throw new errors_1.TTTSignerError("[AutoMint] Signer not initialized", "cachedSigner is null", "Ensure initialize() was called successfully.");
|
|
215
|
+
}
|
|
216
|
+
// Type casting to handle signTypedData if available on the signer (Wallet supports it)
|
|
217
|
+
const signer = this.cachedSigner;
|
|
218
|
+
if (typeof signer.signTypedData !== 'function') {
|
|
219
|
+
throw new errors_1.TTTSignerError("[AutoMint] Provided signer does not support signTypedData (EIP-712)", `Signer type ${signer.constructor.name} missing signTypedData`, "Use a Wallet or a signer that implements EIP-712 signTypedData.");
|
|
220
|
+
}
|
|
221
|
+
const domain = {
|
|
222
|
+
name: "Helm Protocol",
|
|
223
|
+
version: "1",
|
|
224
|
+
chainId: this.config.chainId,
|
|
225
|
+
verifyingContract: this.config.feeCollectorAddress
|
|
226
|
+
};
|
|
227
|
+
const types = {
|
|
228
|
+
CollectFee: [
|
|
229
|
+
{ name: "token", type: "address" },
|
|
230
|
+
{ name: "amount", type: "uint256" },
|
|
231
|
+
{ name: "nonce", type: "uint256" },
|
|
232
|
+
{ name: "deadline", type: "uint256" }
|
|
233
|
+
]
|
|
234
|
+
};
|
|
235
|
+
const value = {
|
|
236
|
+
token: ethers_1.ethers.getAddress(feeCalc.feeTokenAddress),
|
|
237
|
+
amount: feeCalc.protocolFeeUsd,
|
|
238
|
+
nonce: nonce,
|
|
239
|
+
deadline: deadline
|
|
240
|
+
};
|
|
241
|
+
return await signer.signTypedData(domain, types, value);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
exports.AutoMintEngine = AutoMintEngine;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export declare const TIER_USD_MICRO: Record<string, bigint>;
|
|
2
|
+
export declare const FEE_TIERS: {
|
|
3
|
+
BOOTSTRAP: {
|
|
4
|
+
mintFee: bigint;
|
|
5
|
+
burnFee: bigint;
|
|
6
|
+
threshold: bigint;
|
|
7
|
+
};
|
|
8
|
+
GROWTH: {
|
|
9
|
+
mintFee: bigint;
|
|
10
|
+
burnFee: bigint;
|
|
11
|
+
threshold: bigint;
|
|
12
|
+
};
|
|
13
|
+
MATURE: {
|
|
14
|
+
mintFee: bigint;
|
|
15
|
+
burnFee: bigint;
|
|
16
|
+
threshold: bigint;
|
|
17
|
+
};
|
|
18
|
+
PREMIUM: {
|
|
19
|
+
mintFee: bigint;
|
|
20
|
+
burnFee: bigint;
|
|
21
|
+
threshold: bigint;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export interface FeeCalculation {
|
|
25
|
+
tttAmount: bigint;
|
|
26
|
+
protocolFeeUsd: bigint;
|
|
27
|
+
feeToken: string;
|
|
28
|
+
feeTokenAddress: string;
|
|
29
|
+
clientNet: bigint;
|
|
30
|
+
tttPriceUsd: bigint;
|
|
31
|
+
usdCost: bigint;
|
|
32
|
+
feeRateMint: bigint;
|
|
33
|
+
feeRateBurn: bigint;
|
|
34
|
+
tier: string;
|
|
35
|
+
}
|
|
36
|
+
export interface PriceOracleConfig {
|
|
37
|
+
poolAddress?: string;
|
|
38
|
+
chainlinkFeed?: string;
|
|
39
|
+
cacheDurationMs: number;
|
|
40
|
+
fallbackPriceUsd: bigint;
|
|
41
|
+
}
|
|
42
|
+
export declare class DynamicFeeEngine {
|
|
43
|
+
private priceCache;
|
|
44
|
+
private provider;
|
|
45
|
+
private config;
|
|
46
|
+
private static readonly RECOMMENDED_MAX_CACHE_MS;
|
|
47
|
+
constructor(config: PriceOracleConfig);
|
|
48
|
+
connect(rpcUrl: string): Promise<void>;
|
|
49
|
+
getTTTPriceUsd(): Promise<bigint>;
|
|
50
|
+
/**
|
|
51
|
+
* 캐시 강제 무효화 — 외부에서 즉시 가격 갱신이 필요할 때 호출
|
|
52
|
+
*/
|
|
53
|
+
invalidateCache(): void;
|
|
54
|
+
private fetchUniswapPrice;
|
|
55
|
+
private fetchChainlinkPrice;
|
|
56
|
+
getFeeRate(tttPriceUsd: bigint): {
|
|
57
|
+
mintFee: bigint;
|
|
58
|
+
burnFee: bigint;
|
|
59
|
+
phase: string;
|
|
60
|
+
};
|
|
61
|
+
calculateMintFee(tier: string, tickCount?: number, feeToken?: string, feeTokenAddress?: string): Promise<FeeCalculation>;
|
|
62
|
+
calculateBurnFee(tier: string, tickCount?: number, feeToken?: string, feeTokenAddress?: string): Promise<FeeCalculation>;
|
|
63
|
+
calculateEmergencyMintFee(tier: string, tickCount?: number): Promise<FeeCalculation>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// sdk/src/dynamic_fee.ts — Dynamic Fee Engine
|
|
3
|
+
// TTT 시장가에 연동되어 자동으로 tick 비용 조정
|
|
4
|
+
// DEX 운영자는 tier만 설정 → 나머지 SDK가 자동 처리
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DynamicFeeEngine = exports.FEE_TIERS = exports.TIER_USD_MICRO = void 0;
|
|
7
|
+
const ethers_1 = require("ethers");
|
|
8
|
+
const logger_1 = require("./logger");
|
|
9
|
+
// Tier별 USD 목표 비용 (Scale: 1e6)
|
|
10
|
+
exports.TIER_USD_MICRO = {
|
|
11
|
+
T0_epoch: 1000n, // $0.001 * 1e6
|
|
12
|
+
T1_block: 10000n, // $0.01 * 1e6
|
|
13
|
+
T2_slot: 240000n, // $0.24 * 1e6
|
|
14
|
+
T3_micro: 12000000n, // $12 * 1e6
|
|
15
|
+
};
|
|
16
|
+
// Helm 프로토콜 수수료 구간 (Scale: 1e4, e.g., 500 = 5%)
|
|
17
|
+
exports.FEE_TIERS = {
|
|
18
|
+
BOOTSTRAP: { mintFee: 500n, burnFee: 200n, threshold: 5000n }, // threshold: $0.005 * 1e6
|
|
19
|
+
GROWTH: { mintFee: 1000n, burnFee: 300n, threshold: 50000n }, // threshold: $0.05 * 1e6
|
|
20
|
+
MATURE: { mintFee: 1000n, burnFee: 500n, threshold: 500000n }, // threshold: $0.50 * 1e6
|
|
21
|
+
PREMIUM: { mintFee: 800n, burnFee: 500n, threshold: -1n }, // Infinity replacement
|
|
22
|
+
};
|
|
23
|
+
class DynamicFeeEngine {
|
|
24
|
+
priceCache = null;
|
|
25
|
+
provider = null;
|
|
26
|
+
config;
|
|
27
|
+
// P2-3: Recommended max cache duration for DEX price freshness
|
|
28
|
+
static RECOMMENDED_MAX_CACHE_MS = 5000;
|
|
29
|
+
constructor(config) {
|
|
30
|
+
// R3-P0-1: Prevent division by zero in all fee calculations
|
|
31
|
+
if (config.fallbackPriceUsd <= 0n) {
|
|
32
|
+
throw new Error(`[DynamicFee] fallbackPriceUsd must be > 0, got: ${config.fallbackPriceUsd}`);
|
|
33
|
+
}
|
|
34
|
+
this.config = config;
|
|
35
|
+
// P2-3: Warn if cache duration is too long for DEX pricing
|
|
36
|
+
if (config.cacheDurationMs > DynamicFeeEngine.RECOMMENDED_MAX_CACHE_MS) {
|
|
37
|
+
logger_1.logger.warn(`[DynamicFee] cacheDurationMs=${config.cacheDurationMs}ms exceeds recommended ${DynamicFeeEngine.RECOMMENDED_MAX_CACHE_MS}ms for DEX pricing accuracy`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async connect(rpcUrl) {
|
|
41
|
+
if (!rpcUrl)
|
|
42
|
+
throw new Error("[DynamicFee] RPC URL is required");
|
|
43
|
+
this.provider = new ethers_1.JsonRpcProvider(rpcUrl);
|
|
44
|
+
}
|
|
45
|
+
async getTTTPriceUsd() {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
if (this.priceCache && (now - this.priceCache.timestamp) < this.config.cacheDurationMs) {
|
|
48
|
+
return this.priceCache.price;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
let price;
|
|
52
|
+
// R4-P1-2: Chainlink FIRST (resistant to flash loan), Uniswap spot as fallback only
|
|
53
|
+
if (this.config.chainlinkFeed && this.provider) {
|
|
54
|
+
price = await this.fetchChainlinkPrice();
|
|
55
|
+
}
|
|
56
|
+
else if (this.config.poolAddress && this.provider) {
|
|
57
|
+
logger_1.logger.warn("[DynamicFee] Using Uniswap spot price — vulnerable to flash loan manipulation. Configure chainlinkFeed for production.");
|
|
58
|
+
price = await this.fetchUniswapPrice();
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
price = this.config.fallbackPriceUsd;
|
|
62
|
+
}
|
|
63
|
+
if (price <= 0n) {
|
|
64
|
+
logger_1.logger.warn(`[DynamicFee] Invalid price ${price}, using fallback ${this.config.fallbackPriceUsd}`);
|
|
65
|
+
price = this.config.fallbackPriceUsd;
|
|
66
|
+
// R3-P0-1: Double-guard — fallback validated in constructor but belt-and-suspenders
|
|
67
|
+
if (price <= 0n)
|
|
68
|
+
throw new Error("[DynamicFee] FATAL: fallbackPriceUsd is zero — cannot calculate fees");
|
|
69
|
+
}
|
|
70
|
+
this.priceCache = { price, timestamp: now };
|
|
71
|
+
return price;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
logger_1.logger.warn(`[DynamicFee] Price fetch failed, using fallback`);
|
|
75
|
+
return this.config.fallbackPriceUsd;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 캐시 강제 무효화 — 외부에서 즉시 가격 갱신이 필요할 때 호출
|
|
80
|
+
*/
|
|
81
|
+
invalidateCache() {
|
|
82
|
+
this.priceCache = null;
|
|
83
|
+
logger_1.logger.info(`[DynamicFee] Price cache invalidated`);
|
|
84
|
+
}
|
|
85
|
+
async fetchUniswapPrice() {
|
|
86
|
+
if (!this.provider || !this.config.poolAddress)
|
|
87
|
+
return this.config.fallbackPriceUsd;
|
|
88
|
+
const stateViewAbi = ["function getSlot0(bytes32 poolId) external view returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee)"];
|
|
89
|
+
try {
|
|
90
|
+
const contract = new ethers_1.Contract(this.config.poolAddress, stateViewAbi, this.provider);
|
|
91
|
+
const poolId = ethers_1.ethers.keccak256(ethers_1.ethers.AbiCoder.defaultAbiCoder().encode(["address"], [this.config.poolAddress]));
|
|
92
|
+
const [sqrtPriceX96] = await contract.getSlot0(poolId);
|
|
93
|
+
// B1-7: Unified Uniswap price scaling
|
|
94
|
+
const priceScaled = (BigInt(sqrtPriceX96) * BigInt(sqrtPriceX96) * 1000000n) >> 192n;
|
|
95
|
+
return priceScaled;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
logger_1.logger.warn(`[DynamicFee] Uniswap price fetch failed, using fallback: ${error instanceof Error ? error.message : error}`);
|
|
99
|
+
return this.config.fallbackPriceUsd;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async fetchChainlinkPrice() {
|
|
103
|
+
if (!this.provider || !this.config.chainlinkFeed)
|
|
104
|
+
return this.config.fallbackPriceUsd;
|
|
105
|
+
const feedAbi = ["function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)"];
|
|
106
|
+
const MAX_PRICE = 10n ** 12n; // B1-8: Max price 10^12n
|
|
107
|
+
try {
|
|
108
|
+
const contract = new ethers_1.Contract(this.config.chainlinkFeed, feedAbi, this.provider);
|
|
109
|
+
const [, answer, , updatedAt] = await contract.latestRoundData();
|
|
110
|
+
// B1-8: Check updatedAt (within 1 hour) and price <= MAX_PRICE
|
|
111
|
+
const now = BigInt(Math.floor(Date.now() / 1000));
|
|
112
|
+
// P2-4: Tightened staleness check from 3600s to 1800s (30 min)
|
|
113
|
+
if (now - BigInt(updatedAt) > 1800n) {
|
|
114
|
+
throw new Error("Chainlink price stale (>30min)");
|
|
115
|
+
}
|
|
116
|
+
const price = BigInt(answer) / 100n; // 8 decimals -> 6 decimals
|
|
117
|
+
if (price > MAX_PRICE) {
|
|
118
|
+
throw new Error("Chainlink price exceeds MAX_PRICE");
|
|
119
|
+
}
|
|
120
|
+
return price;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
logger_1.logger.warn(`[DynamicFee] Chainlink price fetch failed, using fallback: ${error instanceof Error ? error.message : error}`);
|
|
124
|
+
return this.config.fallbackPriceUsd;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
getFeeRate(tttPriceUsd) {
|
|
128
|
+
if (tttPriceUsd < exports.FEE_TIERS.BOOTSTRAP.threshold) {
|
|
129
|
+
return { ...exports.FEE_TIERS.BOOTSTRAP, phase: "BOOTSTRAP" };
|
|
130
|
+
}
|
|
131
|
+
else if (tttPriceUsd < exports.FEE_TIERS.GROWTH.threshold) {
|
|
132
|
+
return { ...exports.FEE_TIERS.GROWTH, phase: "GROWTH" };
|
|
133
|
+
}
|
|
134
|
+
else if (tttPriceUsd < exports.FEE_TIERS.MATURE.threshold) {
|
|
135
|
+
return { ...exports.FEE_TIERS.MATURE, phase: "MATURE" };
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
return { ...exports.FEE_TIERS.PREMIUM, phase: "PREMIUM" };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async calculateMintFee(tier, tickCount = 1, feeToken = "USDC", feeTokenAddress = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913") {
|
|
142
|
+
// B1-4: Throw if tickCount <= 0
|
|
143
|
+
if (tickCount <= 0)
|
|
144
|
+
throw new Error("[DynamicFee] tickCount must be positive");
|
|
145
|
+
if (!exports.TIER_USD_MICRO[tier])
|
|
146
|
+
throw new Error(`[DynamicFee] Invalid tier: ${tier}`);
|
|
147
|
+
const tttPriceUsd = await this.getTTTPriceUsd(); // 6 decimals
|
|
148
|
+
const usdTarget = exports.TIER_USD_MICRO[tier]; // 6 decimals
|
|
149
|
+
const feeRate = this.getFeeRate(tttPriceUsd);
|
|
150
|
+
const totalUsdCost = usdTarget * BigInt(tickCount); // 6 decimals
|
|
151
|
+
// tttAmount = (totalUsdCost / tttPriceUsd) * 1e18
|
|
152
|
+
const tttAmount = (totalUsdCost * (10n ** 18n)) / tttPriceUsd;
|
|
153
|
+
// protocolFeeUsd = totalUsdCost * feeRate / 10000
|
|
154
|
+
const protocolFeeUsd = (totalUsdCost * feeRate.mintFee) / 10000n;
|
|
155
|
+
return {
|
|
156
|
+
tttAmount,
|
|
157
|
+
protocolFeeUsd,
|
|
158
|
+
feeToken,
|
|
159
|
+
feeTokenAddress,
|
|
160
|
+
clientNet: tttAmount,
|
|
161
|
+
tttPriceUsd,
|
|
162
|
+
usdCost: totalUsdCost + protocolFeeUsd,
|
|
163
|
+
feeRateMint: feeRate.mintFee,
|
|
164
|
+
feeRateBurn: feeRate.burnFee,
|
|
165
|
+
tier,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async calculateBurnFee(tier, tickCount = 1, feeToken = "USDC", feeTokenAddress = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913") {
|
|
169
|
+
// B1-4: Throw if tickCount <= 0
|
|
170
|
+
if (tickCount <= 0)
|
|
171
|
+
throw new Error("[DynamicFee] tickCount must be positive");
|
|
172
|
+
if (!exports.TIER_USD_MICRO[tier])
|
|
173
|
+
throw new Error(`[DynamicFee] Invalid tier: ${tier}`);
|
|
174
|
+
const tttPriceUsd = await this.getTTTPriceUsd();
|
|
175
|
+
const usdTarget = exports.TIER_USD_MICRO[tier];
|
|
176
|
+
const feeRate = this.getFeeRate(tttPriceUsd);
|
|
177
|
+
const totalUsdCost = usdTarget * BigInt(tickCount);
|
|
178
|
+
const tttAmount = (totalUsdCost * (10n ** 18n)) / tttPriceUsd;
|
|
179
|
+
const protocolFeeUsd = (totalUsdCost * feeRate.burnFee) / 10000n;
|
|
180
|
+
return {
|
|
181
|
+
tttAmount,
|
|
182
|
+
protocolFeeUsd,
|
|
183
|
+
feeToken,
|
|
184
|
+
feeTokenAddress,
|
|
185
|
+
clientNet: tttAmount,
|
|
186
|
+
tttPriceUsd,
|
|
187
|
+
usdCost: totalUsdCost + protocolFeeUsd,
|
|
188
|
+
feeRateMint: feeRate.mintFee,
|
|
189
|
+
feeRateBurn: feeRate.burnFee,
|
|
190
|
+
tier,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
async calculateEmergencyMintFee(tier, tickCount = 1) {
|
|
194
|
+
const base = await this.calculateMintFee(tier, tickCount);
|
|
195
|
+
const emergencyFeeUsd = (base.protocolFeeUsd * 150n) / 100n;
|
|
196
|
+
return {
|
|
197
|
+
...base,
|
|
198
|
+
protocolFeeUsd: emergencyFeeUsd,
|
|
199
|
+
usdCost: (base.usdCost * 150n) / 100n,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
exports.DynamicFeeEngine = DynamicFeeEngine;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for TTT SDK errors with storytelling capabilities.
|
|
3
|
+
*/
|
|
4
|
+
export declare class TTTBaseError extends Error {
|
|
5
|
+
readonly message: string;
|
|
6
|
+
readonly reason: string;
|
|
7
|
+
readonly fix: string;
|
|
8
|
+
constructor(message: string, reason: string, fix: string);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Errors related to SDK or Engine configuration.
|
|
12
|
+
*/
|
|
13
|
+
export declare class TTTConfigError extends TTTBaseError {
|
|
14
|
+
constructor(message: string, reason: string, fix: string);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Errors related to Signer (PrivateKey, Turnkey, Privy, KMS) acquisition or usage.
|
|
18
|
+
*/
|
|
19
|
+
export declare class TTTSignerError extends TTTBaseError {
|
|
20
|
+
constructor(message: string, reason: string, fix: string);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Errors related to Network (RPC, ChainID, Connectivity).
|
|
24
|
+
*/
|
|
25
|
+
export declare class TTTNetworkError extends TTTBaseError {
|
|
26
|
+
constructor(message: string, reason: string, fix: string);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Errors related to Smart Contract interaction (TTT.sol, ProtocolFee.sol).
|
|
30
|
+
*/
|
|
31
|
+
export declare class TTTContractError extends TTTBaseError {
|
|
32
|
+
constructor(message: string, reason: string, fix: string);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Errors related to NTP/KTSat Time Synthesis.
|
|
36
|
+
*/
|
|
37
|
+
export declare class TTTTimeSynthesisError extends TTTBaseError {
|
|
38
|
+
constructor(message: string, reason: string, fix: string);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Errors related to Dynamic Fee Engine or Protocol Fee collection.
|
|
42
|
+
*/
|
|
43
|
+
export declare class TTTFeeError extends TTTBaseError {
|
|
44
|
+
constructor(message: string, reason: string, fix: string);
|
|
45
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TTTFeeError = exports.TTTTimeSynthesisError = exports.TTTContractError = exports.TTTNetworkError = exports.TTTSignerError = exports.TTTConfigError = exports.TTTBaseError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Base error class for TTT SDK errors with storytelling capabilities.
|
|
6
|
+
*/
|
|
7
|
+
class TTTBaseError extends Error {
|
|
8
|
+
message;
|
|
9
|
+
reason;
|
|
10
|
+
fix;
|
|
11
|
+
constructor(message, reason, fix) {
|
|
12
|
+
super(`${message} (Reason: ${reason}. Fix: ${fix})`);
|
|
13
|
+
this.message = message;
|
|
14
|
+
this.reason = reason;
|
|
15
|
+
this.fix = fix;
|
|
16
|
+
this.name = this.constructor.name;
|
|
17
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.TTTBaseError = TTTBaseError;
|
|
21
|
+
/**
|
|
22
|
+
* Errors related to SDK or Engine configuration.
|
|
23
|
+
*/
|
|
24
|
+
class TTTConfigError extends TTTBaseError {
|
|
25
|
+
constructor(message, reason, fix) {
|
|
26
|
+
super(message, reason, fix);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.TTTConfigError = TTTConfigError;
|
|
30
|
+
/**
|
|
31
|
+
* Errors related to Signer (PrivateKey, Turnkey, Privy, KMS) acquisition or usage.
|
|
32
|
+
*/
|
|
33
|
+
class TTTSignerError extends TTTBaseError {
|
|
34
|
+
constructor(message, reason, fix) {
|
|
35
|
+
super(message, reason, fix);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.TTTSignerError = TTTSignerError;
|
|
39
|
+
/**
|
|
40
|
+
* Errors related to Network (RPC, ChainID, Connectivity).
|
|
41
|
+
*/
|
|
42
|
+
class TTTNetworkError extends TTTBaseError {
|
|
43
|
+
constructor(message, reason, fix) {
|
|
44
|
+
super(message, reason, fix);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.TTTNetworkError = TTTNetworkError;
|
|
48
|
+
/**
|
|
49
|
+
* Errors related to Smart Contract interaction (TTT.sol, ProtocolFee.sol).
|
|
50
|
+
*/
|
|
51
|
+
class TTTContractError extends TTTBaseError {
|
|
52
|
+
constructor(message, reason, fix) {
|
|
53
|
+
super(message, reason, fix);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.TTTContractError = TTTContractError;
|
|
57
|
+
/**
|
|
58
|
+
* Errors related to NTP/KTSat Time Synthesis.
|
|
59
|
+
*/
|
|
60
|
+
class TTTTimeSynthesisError extends TTTBaseError {
|
|
61
|
+
constructor(message, reason, fix) {
|
|
62
|
+
super(message, reason, fix);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.TTTTimeSynthesisError = TTTTimeSynthesisError;
|
|
66
|
+
/**
|
|
67
|
+
* Errors related to Dynamic Fee Engine or Protocol Fee collection.
|
|
68
|
+
*/
|
|
69
|
+
class TTTFeeError extends TTTBaseError {
|
|
70
|
+
constructor(message, reason, fix) {
|
|
71
|
+
super(message, reason, fix);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.TTTFeeError = TTTFeeError;
|