openttt 0.2.10 → 0.2.12
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 +99 -284
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -2
- package/package.json +9 -12
- package/dist/dynamic_fee.d.ts +0 -75
- package/dist/dynamic_fee.js +0 -254
- package/dist/http_client.d.ts +0 -98
- package/dist/http_client.js +0 -252
- package/dist/integrity_client.d.ts +0 -41
- package/dist/integrity_client.js +0 -122
- package/dist/osnma_source.d.ts +0 -82
- package/dist/osnma_source.js +0 -169
- package/dist/protocol_fee.d.ts +0 -72
- package/dist/protocol_fee.js +0 -199
- package/dist/revenue_tiers.d.ts +0 -36
- package/dist/revenue_tiers.js +0 -83
- package/dist/x402_enforcer.d.ts +0 -44
- package/dist/x402_enforcer.js +0 -92
package/dist/dynamic_fee.d.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
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 rpcUrls;
|
|
46
|
-
private config;
|
|
47
|
-
private warnedSpotPrice;
|
|
48
|
-
private static readonly RECOMMENDED_MAX_CACHE_MS;
|
|
49
|
-
constructor(config: PriceOracleConfig);
|
|
50
|
-
/**
|
|
51
|
-
* Connect to an RPC provider. Accepts a single URL or an array of URLs
|
|
52
|
-
* for multi-RPC fallback. On connection failure, the next URL is tried.
|
|
53
|
-
*/
|
|
54
|
-
connect(rpcUrl: string | string[]): Promise<void>;
|
|
55
|
-
/**
|
|
56
|
-
* Iterate through stored RPC URLs and connect to the first one that succeeds.
|
|
57
|
-
* Throws if all URLs fail.
|
|
58
|
-
*/
|
|
59
|
-
private connectToNext;
|
|
60
|
-
getTTTPriceUsd(): Promise<bigint>;
|
|
61
|
-
/**
|
|
62
|
-
* Force-invalidate price cache -- call when immediate price refresh is needed.
|
|
63
|
-
*/
|
|
64
|
-
invalidateCache(): void;
|
|
65
|
-
private fetchUniswapPrice;
|
|
66
|
-
private fetchChainlinkPrice;
|
|
67
|
-
getFeeRate(tttPriceUsd: bigint): {
|
|
68
|
-
mintFee: bigint;
|
|
69
|
-
burnFee: bigint;
|
|
70
|
-
phase: string;
|
|
71
|
-
};
|
|
72
|
-
calculateMintFee(tier: string, tickCount?: number, feeToken?: string, feeTokenAddress?: string): Promise<FeeCalculation>;
|
|
73
|
-
calculateBurnFee(tier: string, tickCount?: number, feeToken?: string, feeTokenAddress?: string): Promise<FeeCalculation>;
|
|
74
|
-
calculateEmergencyMintFee(tier: string, tickCount?: number): Promise<FeeCalculation>;
|
|
75
|
-
}
|
package/dist/dynamic_fee.js
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// sdk/src/dynamic_fee.ts — Dynamic Fee Engine
|
|
3
|
-
// Automatically adjusts tick cost based on TTT market price
|
|
4
|
-
// DEX operators only set tier; the SDK handles the rest automatically
|
|
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
|
-
// Target USD cost per tier (Scale: 1e6)
|
|
10
|
-
//
|
|
11
|
-
// PRICING (2026-03-14 감사 수정):
|
|
12
|
-
// T2/T3 가격 하향 — 감사 결과 "트랜잭션당 1틱 소비" 구조에서
|
|
13
|
-
// 매 틱 판매 가정은 비현실적. 볼륨 기반 수익 구조로 전환.
|
|
14
|
-
// T2: $0.24 → $0.05 (4.8x 하향)
|
|
15
|
-
// T3: $12.00 → $0.10 (120x 하향)
|
|
16
|
-
//
|
|
17
|
-
// YP discrepancies (code is authoritative in all cases):
|
|
18
|
-
// YP5: TURBO entry threshold — YP says 90%, code uses 95% (more conservative).
|
|
19
|
-
// YP6: BOOTSTRAP mintFee — YP says 3%, code uses 5% (500 basis points).
|
|
20
|
-
// YP7: PoT min confidence — YP says 0.7, code uses 0.5 (auto_mint.ts).
|
|
21
|
-
exports.TIER_USD_MICRO = {
|
|
22
|
-
T0_epoch: 1000n, // $0.001 * 1e6
|
|
23
|
-
T1_block: 10000n, // $0.01 * 1e6
|
|
24
|
-
T2_slot: 50000n, // $0.05 * 1e6 — 감사 수정: 볼륨 기반 수익 구조
|
|
25
|
-
T3_micro: 100000n, // $0.10 * 1e6 — 감사 수정: 트랜잭션당 1틱 소비 기준
|
|
26
|
-
};
|
|
27
|
-
// Helm protocol fee tiers (Scale: 1e4, e.g., 500 = 5%)
|
|
28
|
-
exports.FEE_TIERS = {
|
|
29
|
-
BOOTSTRAP: { mintFee: 500n, burnFee: 200n, threshold: 5000n }, // threshold: $0.005 * 1e6
|
|
30
|
-
GROWTH: { mintFee: 1000n, burnFee: 300n, threshold: 50000n }, // threshold: $0.05 * 1e6
|
|
31
|
-
MATURE: { mintFee: 1000n, burnFee: 500n, threshold: 500000n }, // threshold: $0.50 * 1e6
|
|
32
|
-
PREMIUM: { mintFee: 800n, burnFee: 500n, threshold: -1n }, // Infinity replacement
|
|
33
|
-
};
|
|
34
|
-
class DynamicFeeEngine {
|
|
35
|
-
priceCache = null;
|
|
36
|
-
provider = null;
|
|
37
|
-
rpcUrls = [];
|
|
38
|
-
config;
|
|
39
|
-
warnedSpotPrice = false;
|
|
40
|
-
// P2-3: Recommended max cache duration for DEX price freshness
|
|
41
|
-
static RECOMMENDED_MAX_CACHE_MS = 5000;
|
|
42
|
-
constructor(config) {
|
|
43
|
-
// R3-P0-1: Prevent division by zero in all fee calculations
|
|
44
|
-
if (config.fallbackPriceUsd <= 0n) {
|
|
45
|
-
throw new Error(`[DynamicFee] fallbackPriceUsd must be > 0, got: ${config.fallbackPriceUsd}`);
|
|
46
|
-
}
|
|
47
|
-
this.config = config;
|
|
48
|
-
// P2-3: Warn if cache duration is too long for DEX pricing
|
|
49
|
-
if (config.cacheDurationMs > DynamicFeeEngine.RECOMMENDED_MAX_CACHE_MS) {
|
|
50
|
-
logger_1.logger.warn(`[DynamicFee] cacheDurationMs=${config.cacheDurationMs}ms exceeds recommended ${DynamicFeeEngine.RECOMMENDED_MAX_CACHE_MS}ms for DEX pricing accuracy`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Connect to an RPC provider. Accepts a single URL or an array of URLs
|
|
55
|
-
* for multi-RPC fallback. On connection failure, the next URL is tried.
|
|
56
|
-
*/
|
|
57
|
-
async connect(rpcUrl) {
|
|
58
|
-
const urls = Array.isArray(rpcUrl) ? rpcUrl : [rpcUrl];
|
|
59
|
-
if (urls.length === 0 || urls.every(u => !u)) {
|
|
60
|
-
throw new Error("[DynamicFee] At least one valid RPC URL is required");
|
|
61
|
-
}
|
|
62
|
-
this.rpcUrls = urls.filter(u => !!u);
|
|
63
|
-
await this.connectToNext();
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Iterate through stored RPC URLs and connect to the first one that succeeds.
|
|
67
|
-
* Throws if all URLs fail.
|
|
68
|
-
*/
|
|
69
|
-
async connectToNext() {
|
|
70
|
-
let lastError = null;
|
|
71
|
-
for (const url of this.rpcUrls) {
|
|
72
|
-
try {
|
|
73
|
-
const provider = new ethers_1.JsonRpcProvider(url);
|
|
74
|
-
// Verify connectivity by requesting the network
|
|
75
|
-
await provider.getNetwork();
|
|
76
|
-
this.provider = provider;
|
|
77
|
-
logger_1.logger.info(`[DynamicFee] Connected to RPC: ${url}`);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
catch (err) {
|
|
81
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
82
|
-
logger_1.logger.warn(`[DynamicFee] RPC connection failed for ${url}: ${lastError.message}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
// If all URLs failed, fall back to the first URL without connectivity check
|
|
86
|
-
// so that subsequent calls can still attempt requests
|
|
87
|
-
this.provider = new ethers_1.JsonRpcProvider(this.rpcUrls[0]);
|
|
88
|
-
logger_1.logger.warn(`[DynamicFee] All RPC URLs failed connectivity check, using first URL as fallback`);
|
|
89
|
-
}
|
|
90
|
-
async getTTTPriceUsd() {
|
|
91
|
-
const now = Date.now();
|
|
92
|
-
if (this.priceCache && (now - this.priceCache.timestamp) < this.config.cacheDurationMs) {
|
|
93
|
-
return this.priceCache.price;
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
let price;
|
|
97
|
-
// R4-P1-2: Chainlink FIRST (resistant to flash loan), Uniswap spot as fallback only
|
|
98
|
-
if (this.config.chainlinkFeed && this.provider) {
|
|
99
|
-
price = await this.fetchChainlinkPrice();
|
|
100
|
-
}
|
|
101
|
-
else if (this.config.poolAddress && this.provider) {
|
|
102
|
-
// To suppress this warning, set `chainlinkFeed` in PriceOracleConfig to a
|
|
103
|
-
// Chainlink AggregatorV3 address (e.g. the TTT/USD feed on your target chain).
|
|
104
|
-
// Chainlink TWAP prices are resistant to single-block flash loan manipulation.
|
|
105
|
-
if (!this.warnedSpotPrice) {
|
|
106
|
-
logger_1.logger.warn("[DynamicFee] Using Uniswap spot price — vulnerable to flash loan manipulation. Configure chainlinkFeed for production.");
|
|
107
|
-
this.warnedSpotPrice = true;
|
|
108
|
-
}
|
|
109
|
-
price = await this.fetchUniswapPrice();
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
price = this.config.fallbackPriceUsd;
|
|
113
|
-
}
|
|
114
|
-
if (price <= 0n) {
|
|
115
|
-
logger_1.logger.warn(`[DynamicFee] Invalid price ${price}, using fallback ${this.config.fallbackPriceUsd}`);
|
|
116
|
-
price = this.config.fallbackPriceUsd;
|
|
117
|
-
// R3-P0-1: Double-guard — fallback validated in constructor but belt-and-suspenders
|
|
118
|
-
if (price <= 0n)
|
|
119
|
-
throw new Error("[DynamicFee] FATAL: fallbackPriceUsd is zero — cannot calculate fees");
|
|
120
|
-
}
|
|
121
|
-
this.priceCache = { price, timestamp: now };
|
|
122
|
-
return price;
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
logger_1.logger.warn(`[DynamicFee] Price fetch failed, using fallback`);
|
|
126
|
-
return this.config.fallbackPriceUsd;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Force-invalidate price cache -- call when immediate price refresh is needed.
|
|
131
|
-
*/
|
|
132
|
-
invalidateCache() {
|
|
133
|
-
this.priceCache = null;
|
|
134
|
-
logger_1.logger.info(`[DynamicFee] Price cache invalidated`);
|
|
135
|
-
}
|
|
136
|
-
async fetchUniswapPrice() {
|
|
137
|
-
if (!this.provider || !this.config.poolAddress)
|
|
138
|
-
return this.config.fallbackPriceUsd;
|
|
139
|
-
const stateViewAbi = ["function getSlot0(bytes32 poolId) external view returns (uint160 sqrtPriceX96, int24 tick, uint24 protocolFee, uint24 lpFee)"];
|
|
140
|
-
try {
|
|
141
|
-
const contract = new ethers_1.Contract(this.config.poolAddress, stateViewAbi, this.provider);
|
|
142
|
-
const poolId = ethers_1.ethers.keccak256(ethers_1.ethers.AbiCoder.defaultAbiCoder().encode(["address"], [this.config.poolAddress]));
|
|
143
|
-
const [sqrtPriceX96] = await contract.getSlot0(poolId);
|
|
144
|
-
// B1-7: Unified Uniswap price scaling
|
|
145
|
-
const priceScaled = (BigInt(sqrtPriceX96) * BigInt(sqrtPriceX96) * 1000000n) >> 192n;
|
|
146
|
-
return priceScaled;
|
|
147
|
-
}
|
|
148
|
-
catch (error) {
|
|
149
|
-
logger_1.logger.warn(`[DynamicFee] Uniswap price fetch failed, using fallback: ${error instanceof Error ? error.message : error}`);
|
|
150
|
-
return this.config.fallbackPriceUsd;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
async fetchChainlinkPrice() {
|
|
154
|
-
if (!this.provider || !this.config.chainlinkFeed)
|
|
155
|
-
return this.config.fallbackPriceUsd;
|
|
156
|
-
const feedAbi = ["function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)"];
|
|
157
|
-
const MAX_PRICE = 10n ** 12n; // B1-8: Max price 10^12n
|
|
158
|
-
try {
|
|
159
|
-
const contract = new ethers_1.Contract(this.config.chainlinkFeed, feedAbi, this.provider);
|
|
160
|
-
const [, answer, , updatedAt] = await contract.latestRoundData();
|
|
161
|
-
// B1-8: Check updatedAt (within 1 hour) and price <= MAX_PRICE
|
|
162
|
-
const now = BigInt(Math.floor(Date.now() / 1000));
|
|
163
|
-
// P2-4: Tightened staleness check from 3600s to 1800s (30 min)
|
|
164
|
-
if (now - BigInt(updatedAt) > 1800n) {
|
|
165
|
-
throw new Error("Chainlink price stale (>30min)");
|
|
166
|
-
}
|
|
167
|
-
const price = BigInt(answer) / 100n; // 8 decimals -> 6 decimals
|
|
168
|
-
if (price > MAX_PRICE) {
|
|
169
|
-
throw new Error("Chainlink price exceeds MAX_PRICE");
|
|
170
|
-
}
|
|
171
|
-
return price;
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
logger_1.logger.warn(`[DynamicFee] Chainlink price fetch failed, using fallback: ${error instanceof Error ? error.message : error}`);
|
|
175
|
-
return this.config.fallbackPriceUsd;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
getFeeRate(tttPriceUsd) {
|
|
179
|
-
if (tttPriceUsd < exports.FEE_TIERS.BOOTSTRAP.threshold) {
|
|
180
|
-
return { ...exports.FEE_TIERS.BOOTSTRAP, phase: "BOOTSTRAP" };
|
|
181
|
-
}
|
|
182
|
-
else if (tttPriceUsd < exports.FEE_TIERS.GROWTH.threshold) {
|
|
183
|
-
return { ...exports.FEE_TIERS.GROWTH, phase: "GROWTH" };
|
|
184
|
-
}
|
|
185
|
-
else if (tttPriceUsd < exports.FEE_TIERS.MATURE.threshold) {
|
|
186
|
-
return { ...exports.FEE_TIERS.MATURE, phase: "MATURE" };
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
return { ...exports.FEE_TIERS.PREMIUM, phase: "PREMIUM" };
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
async calculateMintFee(tier, tickCount = 1, feeToken = "USDC", feeTokenAddress = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913") {
|
|
193
|
-
// B1-4: Throw if tickCount <= 0
|
|
194
|
-
if (tickCount <= 0)
|
|
195
|
-
throw new Error("[DynamicFee] tickCount must be positive");
|
|
196
|
-
if (!exports.TIER_USD_MICRO[tier])
|
|
197
|
-
throw new Error(`[DynamicFee] Invalid tier: ${tier}`);
|
|
198
|
-
const tttPriceUsd = await this.getTTTPriceUsd(); // 6 decimals
|
|
199
|
-
const usdTarget = exports.TIER_USD_MICRO[tier]; // 6 decimals
|
|
200
|
-
const feeRate = this.getFeeRate(tttPriceUsd);
|
|
201
|
-
const totalUsdCost = usdTarget * BigInt(tickCount); // 6 decimals
|
|
202
|
-
// tttAmount = (totalUsdCost / tttPriceUsd) * 1e18
|
|
203
|
-
const tttAmount = (totalUsdCost * (10n ** 18n)) / tttPriceUsd;
|
|
204
|
-
// protocolFeeUsd = totalUsdCost * feeRate / 10000
|
|
205
|
-
const protocolFeeUsd = (totalUsdCost * feeRate.mintFee) / 10000n;
|
|
206
|
-
return {
|
|
207
|
-
tttAmount,
|
|
208
|
-
protocolFeeUsd,
|
|
209
|
-
feeToken,
|
|
210
|
-
feeTokenAddress,
|
|
211
|
-
clientNet: tttAmount,
|
|
212
|
-
tttPriceUsd,
|
|
213
|
-
usdCost: totalUsdCost + protocolFeeUsd,
|
|
214
|
-
feeRateMint: feeRate.mintFee,
|
|
215
|
-
feeRateBurn: feeRate.burnFee,
|
|
216
|
-
tier,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
async calculateBurnFee(tier, tickCount = 1, feeToken = "USDC", feeTokenAddress = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913") {
|
|
220
|
-
// B1-4: Throw if tickCount <= 0
|
|
221
|
-
if (tickCount <= 0)
|
|
222
|
-
throw new Error("[DynamicFee] tickCount must be positive");
|
|
223
|
-
if (!exports.TIER_USD_MICRO[tier])
|
|
224
|
-
throw new Error(`[DynamicFee] Invalid tier: ${tier}`);
|
|
225
|
-
const tttPriceUsd = await this.getTTTPriceUsd();
|
|
226
|
-
const usdTarget = exports.TIER_USD_MICRO[tier];
|
|
227
|
-
const feeRate = this.getFeeRate(tttPriceUsd);
|
|
228
|
-
const totalUsdCost = usdTarget * BigInt(tickCount);
|
|
229
|
-
const tttAmount = (totalUsdCost * (10n ** 18n)) / tttPriceUsd;
|
|
230
|
-
const protocolFeeUsd = (totalUsdCost * feeRate.burnFee) / 10000n;
|
|
231
|
-
return {
|
|
232
|
-
tttAmount,
|
|
233
|
-
protocolFeeUsd,
|
|
234
|
-
feeToken,
|
|
235
|
-
feeTokenAddress,
|
|
236
|
-
clientNet: tttAmount,
|
|
237
|
-
tttPriceUsd,
|
|
238
|
-
usdCost: totalUsdCost + protocolFeeUsd,
|
|
239
|
-
feeRateMint: feeRate.mintFee,
|
|
240
|
-
feeRateBurn: feeRate.burnFee,
|
|
241
|
-
tier,
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
async calculateEmergencyMintFee(tier, tickCount = 1) {
|
|
245
|
-
const base = await this.calculateMintFee(tier, tickCount);
|
|
246
|
-
const emergencyFeeUsd = (base.protocolFeeUsd * 150n) / 100n;
|
|
247
|
-
return {
|
|
248
|
-
...base,
|
|
249
|
-
protocolFeeUsd: emergencyFeeUsd,
|
|
250
|
-
usdCost: (base.usdCost * 150n) / 100n,
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
exports.DynamicFeeEngine = DynamicFeeEngine;
|
package/dist/http_client.d.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TTTClient.httpOnly() — Zero-friction Proof of Time
|
|
3
|
-
* No ETH, no signer, no on-chain. Just verified time.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* const client = TTTClient.httpOnly();
|
|
7
|
-
* const pot = await client.generatePoT();
|
|
8
|
-
* console.log(pot.timestamp, pot.confidence);
|
|
9
|
-
*
|
|
10
|
-
* Time sources: NIST, Apple, Google, Cloudflare HTTPS Date headers.
|
|
11
|
-
* HMAC-SHA256 signature — no ethers.js dependency required.
|
|
12
|
-
*/
|
|
13
|
-
import { ProofOfTime } from "./types";
|
|
14
|
-
export interface HttpPoT {
|
|
15
|
-
/** Synthesized timestamp in Unix nanoseconds (bigint). */
|
|
16
|
-
timestamp: bigint;
|
|
17
|
-
/** Fraction of configured sources that responded (0.0–1.0). */
|
|
18
|
-
confidence: number;
|
|
19
|
-
/** Lowest NTP stratum observed across sources (2 for HTTPS Date headers). */
|
|
20
|
-
stratum: number;
|
|
21
|
-
/** Number of sources that successfully responded. */
|
|
22
|
-
sources: number;
|
|
23
|
-
/** Per-source readings used to compute the median. */
|
|
24
|
-
sourceReadings: {
|
|
25
|
-
source: string;
|
|
26
|
-
timestamp: bigint;
|
|
27
|
-
uncertainty: number;
|
|
28
|
-
stratum?: number;
|
|
29
|
-
}[];
|
|
30
|
-
/** Replay-protection nonce (hex). */
|
|
31
|
-
nonce: string;
|
|
32
|
-
/** Expiry timestamp in Unix milliseconds (bigint). */
|
|
33
|
-
expiresAt: bigint;
|
|
34
|
-
/** HMAC-SHA256 over canonical fields, hex-encoded. */
|
|
35
|
-
hmac: string;
|
|
36
|
-
}
|
|
37
|
-
export interface HttpPoTVerifyResult {
|
|
38
|
-
valid: boolean;
|
|
39
|
-
reason?: string;
|
|
40
|
-
}
|
|
41
|
-
export interface HttpOnlyClientOptions {
|
|
42
|
-
/**
|
|
43
|
-
* Override HMAC secret for non-sandbox usage.
|
|
44
|
-
* Defaults to a fixed sandbox key — sufficient for local verification only.
|
|
45
|
-
*/
|
|
46
|
-
hmacSecret?: string;
|
|
47
|
-
/**
|
|
48
|
-
* Per-source request timeout in ms. Default: 3000.
|
|
49
|
-
*/
|
|
50
|
-
timeoutMs?: number;
|
|
51
|
-
/**
|
|
52
|
-
* PoT validity window in seconds. Default: 60.
|
|
53
|
-
*/
|
|
54
|
-
expirySeconds?: number;
|
|
55
|
-
/**
|
|
56
|
-
* Maximum divergence allowed between sources in nanoseconds.
|
|
57
|
-
* Default: 2_000_000_000n (2 seconds — lenient for HTTPS Date 1s resolution).
|
|
58
|
-
*/
|
|
59
|
-
toleranceNs?: bigint;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* HttpOnlyClient — zero-dependency Proof of Time over HTTPS.
|
|
63
|
-
*
|
|
64
|
-
* Fetches time from NIST, Apple, Google, and Cloudflare HTTPS endpoints,
|
|
65
|
-
* computes the median, and returns a PoT with HMAC integrity protection.
|
|
66
|
-
*
|
|
67
|
-
* No ETH, no signer, no on-chain interaction required.
|
|
68
|
-
*/
|
|
69
|
-
export declare class HttpOnlyClient {
|
|
70
|
-
private readonly hmacSecret;
|
|
71
|
-
private readonly timeoutMs;
|
|
72
|
-
private readonly expirySeconds;
|
|
73
|
-
private readonly toleranceNs;
|
|
74
|
-
private readonly usedNonces;
|
|
75
|
-
private readonly NONCE_TTL_MS;
|
|
76
|
-
private readonly MAX_NONCE_CACHE;
|
|
77
|
-
constructor(options?: HttpOnlyClientOptions);
|
|
78
|
-
/**
|
|
79
|
-
* Fetch time from all four HTTPS sources, compute median, return PoT.
|
|
80
|
-
* Requires at least 1 source to succeed.
|
|
81
|
-
*/
|
|
82
|
-
generatePoT(): Promise<HttpPoT>;
|
|
83
|
-
/**
|
|
84
|
-
* Verify an HttpPoT:
|
|
85
|
-
* - HMAC integrity check
|
|
86
|
-
* - Expiry check
|
|
87
|
-
* - Nonce replay protection
|
|
88
|
-
* - Source divergence tolerance check
|
|
89
|
-
*
|
|
90
|
-
* No on-chain interaction. Pure local verification.
|
|
91
|
-
*/
|
|
92
|
-
verifyPoT(pot: HttpPoT): HttpPoTVerifyResult;
|
|
93
|
-
/**
|
|
94
|
-
* Convert HttpPoT to the standard ProofOfTime shape used by the rest of the SDK.
|
|
95
|
-
* The issuerSignature field is omitted (no on-chain signer in httpOnly mode).
|
|
96
|
-
*/
|
|
97
|
-
static toProofOfTime(pot: HttpPoT): ProofOfTime;
|
|
98
|
-
}
|
package/dist/http_client.js
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* TTTClient.httpOnly() — Zero-friction Proof of Time
|
|
4
|
-
* No ETH, no signer, no on-chain. Just verified time.
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* const client = TTTClient.httpOnly();
|
|
8
|
-
* const pot = await client.generatePoT();
|
|
9
|
-
* console.log(pot.timestamp, pot.confidence);
|
|
10
|
-
*
|
|
11
|
-
* Time sources: NIST, Apple, Google, Cloudflare HTTPS Date headers.
|
|
12
|
-
* HMAC-SHA256 signature — no ethers.js dependency required.
|
|
13
|
-
*/
|
|
14
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
-
if (k2 === undefined) k2 = k;
|
|
16
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
-
}
|
|
20
|
-
Object.defineProperty(o, k2, desc);
|
|
21
|
-
}) : (function(o, m, k, k2) {
|
|
22
|
-
if (k2 === undefined) k2 = k;
|
|
23
|
-
o[k2] = m[k];
|
|
24
|
-
}));
|
|
25
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
-
}) : function(o, v) {
|
|
28
|
-
o["default"] = v;
|
|
29
|
-
});
|
|
30
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
-
var ownKeys = function(o) {
|
|
32
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
-
var ar = [];
|
|
34
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
-
return ar;
|
|
36
|
-
};
|
|
37
|
-
return ownKeys(o);
|
|
38
|
-
};
|
|
39
|
-
return function (mod) {
|
|
40
|
-
if (mod && mod.__esModule) return mod;
|
|
41
|
-
var result = {};
|
|
42
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
-
__setModuleDefault(result, mod);
|
|
44
|
-
return result;
|
|
45
|
-
};
|
|
46
|
-
})();
|
|
47
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
-
exports.HttpOnlyClient = void 0;
|
|
49
|
-
const crypto = __importStar(require("crypto"));
|
|
50
|
-
const https = __importStar(require("https"));
|
|
51
|
-
const SOURCES = [
|
|
52
|
-
{ name: "nist", url: "https://time.nist.gov/" },
|
|
53
|
-
{ name: "apple", url: "https://time.apple.com/" },
|
|
54
|
-
{ name: "google", url: "https://time.google.com/" },
|
|
55
|
-
{ name: "cloudflare", url: "https://time.cloudflare.com/" },
|
|
56
|
-
];
|
|
57
|
-
function fetchHttpsDate(name, url, timeoutMs = 3000) {
|
|
58
|
-
return new Promise((resolve, reject) => {
|
|
59
|
-
const t1 = BigInt(Date.now()) * 1000000n;
|
|
60
|
-
const timer = setTimeout(() => {
|
|
61
|
-
req.destroy();
|
|
62
|
-
reject(new Error(`[httpOnly] Timeout: ${name} (${timeoutMs}ms)`));
|
|
63
|
-
}, timeoutMs);
|
|
64
|
-
const req = https.request(url, { method: "HEAD" }, (res) => {
|
|
65
|
-
clearTimeout(timer);
|
|
66
|
-
res.resume(); // drain so socket is freed
|
|
67
|
-
const t4 = BigInt(Date.now()) * 1000000n;
|
|
68
|
-
const dateHeader = res.headers["date"];
|
|
69
|
-
if (!dateHeader) {
|
|
70
|
-
reject(new Error(`[httpOnly] No Date header from ${name}`));
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const parsed = new Date(dateHeader).getTime();
|
|
74
|
-
if (isNaN(parsed)) {
|
|
75
|
-
reject(new Error(`[httpOnly] Invalid Date header from ${name}: ${dateHeader}`));
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
const serverNs = BigInt(parsed) * 1000000n;
|
|
79
|
-
const rttNs = t4 - t1;
|
|
80
|
-
// Offset-corrected: server time + half RTT
|
|
81
|
-
const corrected = serverNs + rttNs / 2n;
|
|
82
|
-
const rttMs = Number(rttNs) / 1_000_000;
|
|
83
|
-
resolve({
|
|
84
|
-
source: name,
|
|
85
|
-
timestamp: corrected,
|
|
86
|
-
uncertainty: rttMs / 2 + 500, // 500ms base — HTTP Date has 1s resolution
|
|
87
|
-
stratum: 2,
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
req.on("error", (err) => {
|
|
91
|
-
clearTimeout(timer);
|
|
92
|
-
reject(new Error(`[httpOnly] Request error for ${name}: ${err.message}`));
|
|
93
|
-
});
|
|
94
|
-
req.end();
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
// HMAC helpers — default sandbox key derived from fixed chain+address strings
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
const SANDBOX_HMAC_SECRET = "openttt-sandbox:chainId=0:address=0x0000000000000000000000000000000000000000";
|
|
101
|
-
function computeHmac(timestamp, nonce, expiresAt, sources, secret = SANDBOX_HMAC_SECRET) {
|
|
102
|
-
// Canonical message: deterministic field concatenation
|
|
103
|
-
const msg = `${timestamp.toString()}:${nonce}:${expiresAt.toString()}:${sources}`;
|
|
104
|
-
return crypto.createHmac("sha256", secret).update(msg).digest("hex");
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* HttpOnlyClient — zero-dependency Proof of Time over HTTPS.
|
|
108
|
-
*
|
|
109
|
-
* Fetches time from NIST, Apple, Google, and Cloudflare HTTPS endpoints,
|
|
110
|
-
* computes the median, and returns a PoT with HMAC integrity protection.
|
|
111
|
-
*
|
|
112
|
-
* No ETH, no signer, no on-chain interaction required.
|
|
113
|
-
*/
|
|
114
|
-
class HttpOnlyClient {
|
|
115
|
-
hmacSecret;
|
|
116
|
-
timeoutMs;
|
|
117
|
-
expirySeconds;
|
|
118
|
-
toleranceNs;
|
|
119
|
-
usedNonces = new Map();
|
|
120
|
-
NONCE_TTL_MS = 300_000; // 5 min
|
|
121
|
-
MAX_NONCE_CACHE = 10_000;
|
|
122
|
-
constructor(options = {}) {
|
|
123
|
-
this.hmacSecret = options.hmacSecret ?? SANDBOX_HMAC_SECRET;
|
|
124
|
-
this.timeoutMs = options.timeoutMs ?? 3000;
|
|
125
|
-
this.expirySeconds = options.expirySeconds ?? 60;
|
|
126
|
-
this.toleranceNs = options.toleranceNs ?? 2000000000n; // 2 s
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Fetch time from all four HTTPS sources, compute median, return PoT.
|
|
130
|
-
* Requires at least 1 source to succeed.
|
|
131
|
-
*/
|
|
132
|
-
async generatePoT() {
|
|
133
|
-
const results = await Promise.allSettled(SOURCES.map((s) => fetchHttpsDate(s.name, s.url, this.timeoutMs)));
|
|
134
|
-
const readings = [];
|
|
135
|
-
for (const r of results) {
|
|
136
|
-
if (r.status === "fulfilled")
|
|
137
|
-
readings.push(r.value);
|
|
138
|
-
}
|
|
139
|
-
if (readings.length === 0) {
|
|
140
|
-
throw new Error("[httpOnly] All HTTPS time sources failed. Check network connectivity.");
|
|
141
|
-
}
|
|
142
|
-
// Sort by timestamp for median selection
|
|
143
|
-
readings.sort((a, b) => (a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0));
|
|
144
|
-
let finalTs;
|
|
145
|
-
let finalUnc;
|
|
146
|
-
let finalStratum;
|
|
147
|
-
if (readings.length === 1) {
|
|
148
|
-
finalTs = readings[0].timestamp;
|
|
149
|
-
finalUnc = readings[0].uncertainty;
|
|
150
|
-
finalStratum = readings[0].stratum;
|
|
151
|
-
}
|
|
152
|
-
else if (readings.length === 2) {
|
|
153
|
-
finalTs = (readings[0].timestamp + readings[1].timestamp) / 2n;
|
|
154
|
-
finalUnc = (readings[0].uncertainty + readings[1].uncertainty) / 2;
|
|
155
|
-
finalStratum = Math.min(readings[0].stratum, readings[1].stratum);
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
const mid = Math.floor(readings.length / 2);
|
|
159
|
-
finalTs = readings[mid].timestamp;
|
|
160
|
-
finalUnc = readings[mid].uncertainty;
|
|
161
|
-
finalStratum = readings[mid].stratum;
|
|
162
|
-
}
|
|
163
|
-
const nonce = crypto.randomBytes(16).toString("hex");
|
|
164
|
-
const expiresAt = BigInt(Date.now()) + BigInt(this.expirySeconds * 1000);
|
|
165
|
-
const sourceReadings = readings.map((r) => ({
|
|
166
|
-
source: r.source,
|
|
167
|
-
timestamp: r.timestamp,
|
|
168
|
-
uncertainty: r.uncertainty,
|
|
169
|
-
stratum: r.stratum,
|
|
170
|
-
}));
|
|
171
|
-
const hmac = computeHmac(finalTs, nonce, expiresAt, readings.length, this.hmacSecret);
|
|
172
|
-
return {
|
|
173
|
-
timestamp: finalTs,
|
|
174
|
-
confidence: readings.length / SOURCES.length,
|
|
175
|
-
stratum: finalStratum,
|
|
176
|
-
sources: readings.length,
|
|
177
|
-
sourceReadings,
|
|
178
|
-
nonce,
|
|
179
|
-
expiresAt,
|
|
180
|
-
hmac,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Verify an HttpPoT:
|
|
185
|
-
* - HMAC integrity check
|
|
186
|
-
* - Expiry check
|
|
187
|
-
* - Nonce replay protection
|
|
188
|
-
* - Source divergence tolerance check
|
|
189
|
-
*
|
|
190
|
-
* No on-chain interaction. Pure local verification.
|
|
191
|
-
*/
|
|
192
|
-
verifyPoT(pot) {
|
|
193
|
-
// 1. Expiry
|
|
194
|
-
if (BigInt(Date.now()) > pot.expiresAt) {
|
|
195
|
-
return { valid: false, reason: "PoT expired" };
|
|
196
|
-
}
|
|
197
|
-
// 2. HMAC integrity
|
|
198
|
-
const expected = computeHmac(pot.timestamp, pot.nonce, pot.expiresAt, pot.sources, this.hmacSecret);
|
|
199
|
-
if (!crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(pot.hmac, "hex"))) {
|
|
200
|
-
return { valid: false, reason: "HMAC mismatch — PoT may have been tampered" };
|
|
201
|
-
}
|
|
202
|
-
// 3. Nonce replay (bounded cache + TTL)
|
|
203
|
-
const now = Date.now();
|
|
204
|
-
if (this.usedNonces.size > this.MAX_NONCE_CACHE / 2) {
|
|
205
|
-
for (const [k, ts] of this.usedNonces) {
|
|
206
|
-
if (now - ts > this.NONCE_TTL_MS)
|
|
207
|
-
this.usedNonces.delete(k);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
if (this.usedNonces.has(pot.nonce)) {
|
|
211
|
-
return { valid: false, reason: "Duplicate nonce — replay detected" };
|
|
212
|
-
}
|
|
213
|
-
if (this.usedNonces.size >= this.MAX_NONCE_CACHE) {
|
|
214
|
-
const oldest = this.usedNonces.keys().next().value;
|
|
215
|
-
if (oldest !== undefined)
|
|
216
|
-
this.usedNonces.delete(oldest);
|
|
217
|
-
}
|
|
218
|
-
this.usedNonces.set(pot.nonce, now);
|
|
219
|
-
// 4. Source divergence
|
|
220
|
-
for (const reading of pot.sourceReadings) {
|
|
221
|
-
const diff = reading.timestamp > pot.timestamp
|
|
222
|
-
? reading.timestamp - pot.timestamp
|
|
223
|
-
: pot.timestamp - reading.timestamp;
|
|
224
|
-
if (diff > this.toleranceNs) {
|
|
225
|
-
return {
|
|
226
|
-
valid: false,
|
|
227
|
-
reason: `Source ${reading.source} diverges by ${diff}ns (tolerance: ${this.toleranceNs}ns)`,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return { valid: true };
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Convert HttpPoT to the standard ProofOfTime shape used by the rest of the SDK.
|
|
235
|
-
* The issuerSignature field is omitted (no on-chain signer in httpOnly mode).
|
|
236
|
-
*/
|
|
237
|
-
static toProofOfTime(pot) {
|
|
238
|
-
return {
|
|
239
|
-
timestamp: pot.timestamp,
|
|
240
|
-
uncertainty: pot.sourceReadings.length > 0
|
|
241
|
-
? pot.sourceReadings.reduce((sum, r) => sum + r.uncertainty, 0) / pot.sourceReadings.length
|
|
242
|
-
: 500,
|
|
243
|
-
sources: pot.sources,
|
|
244
|
-
stratum: pot.stratum,
|
|
245
|
-
confidence: pot.confidence,
|
|
246
|
-
sourceReadings: pot.sourceReadings,
|
|
247
|
-
nonce: pot.nonce,
|
|
248
|
-
expiresAt: pot.expiresAt,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
exports.HttpOnlyClient = HttpOnlyClient;
|