openttt 0.1.3 → 0.2.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 +48 -26
- package/dist/auto_mint.js +1 -1
- package/dist/grg_api_client.d.ts +41 -0
- package/dist/grg_api_client.js +116 -0
- package/dist/http_client.d.ts +98 -0
- package/dist/http_client.js +252 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/time_synthesis.js +0 -4
- package/dist/ttt_client.d.ts +18 -0
- package/dist/ttt_client.js +20 -0
- package/package.json +6 -2
- package/dist/golay.d.ts +0 -6
- package/dist/golay.js +0 -166
- package/dist/grg_forward.d.ts +0 -11
- package/dist/grg_forward.js +0 -74
- package/dist/grg_inverse.d.ts +0 -7
- package/dist/grg_inverse.js +0 -100
- package/dist/grg_pipeline.d.ts +0 -13
- package/dist/grg_pipeline.js +0 -64
- package/dist/reed_solomon.d.ts +0 -12
- package/dist/reed_solomon.js +0 -179
- package/vendor/helm-crypto/golay.d.ts +0 -6
- package/vendor/helm-crypto/golay.js +0 -167
- package/vendor/helm-crypto/golay.js.map +0 -1
- package/vendor/helm-crypto/grg_forward.d.ts +0 -22
- package/vendor/helm-crypto/grg_forward.js +0 -89
- package/vendor/helm-crypto/grg_forward.js.map +0 -1
- package/vendor/helm-crypto/grg_inverse.d.ts +0 -16
- package/vendor/helm-crypto/grg_inverse.js +0 -118
- package/vendor/helm-crypto/grg_inverse.js.map +0 -1
- package/vendor/helm-crypto/grg_pipeline.d.ts +0 -13
- package/vendor/helm-crypto/grg_pipeline.js +0 -66
- package/vendor/helm-crypto/grg_pipeline.js.map +0 -1
- package/vendor/helm-crypto/index.d.ts +0 -5
- package/vendor/helm-crypto/index.js +0 -17
- package/vendor/helm-crypto/index.js.map +0 -1
- package/vendor/helm-crypto/logger.d.ts +0 -6
- package/vendor/helm-crypto/logger.js +0 -11
- package/vendor/helm-crypto/logger.js.map +0 -1
- package/vendor/helm-crypto/reed_solomon.d.ts +0 -37
- package/vendor/helm-crypto/reed_solomon.js +0 -210
- package/vendor/helm-crypto/reed_solomon.js.map +0 -1
package/README.md
CHANGED
|
@@ -10,7 +10,9 @@ OpenTTT brings cryptographic time verification to blockchain transaction orderin
|
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
[](https://github.com/Helm-Protocol/OpenTTT/actions/workflows/ci.yml)
|
|
12
12
|
[](https://codecov.io/gh/Helm-Protocol/OpenTTT)
|
|
13
|
-
[]()
|
|
14
|
+
|
|
15
|
+
> If this project is useful to you, please [star it on GitHub](https://github.com/Helm-Protocol/OpenTTT) — it helps others find it.
|
|
14
16
|
|
|
15
17
|
```
|
|
16
18
|
npm install openttt
|
|
@@ -40,7 +42,22 @@ No governance vote. No slashing committee. Cheating is simply bad business.
|
|
|
40
42
|
|
|
41
43
|
## Quick Start
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
### Try it in 30 seconds — No ETH, No Wallet
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { HttpOnlyClient } from "openttt";
|
|
49
|
+
|
|
50
|
+
const client = new HttpOnlyClient();
|
|
51
|
+
const pot = await client.generatePoT();
|
|
52
|
+
console.log(pot.timestamp, pot.confidence, pot.sources);
|
|
53
|
+
|
|
54
|
+
const valid = client.verifyPoT(pot);
|
|
55
|
+
console.log("Valid:", valid); // true
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
No blockchain. No wallet. No gas fees. Just verified time from 4 independent HTTPS sources (NIST, Apple, Google, Cloudflare). Start here, upgrade to on-chain when ready.
|
|
59
|
+
|
|
60
|
+
### On-Chain Mode (Full Power)
|
|
44
61
|
|
|
45
62
|
```typescript
|
|
46
63
|
import { TTTClient } from "openttt";
|
|
@@ -49,9 +66,7 @@ const ttt = await TTTClient.forBase({ privateKey: process.env.OPERATOR_PK! });
|
|
|
49
66
|
ttt.startAutoMint();
|
|
50
67
|
```
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
> **Shorthand**: Pass `privateKey` directly as a string instead of the full `signer` config object. The verbose form `{ signer: { type: "privateKey", key: "0x..." } }` still works for when you need other signer types (Turnkey, KMS, Privy).
|
|
69
|
+
Connects to Base, synthesizes time from atomic clock sources, and mints Proof-of-Time tokens on-chain.
|
|
55
70
|
|
|
56
71
|
---
|
|
57
72
|
|
|
@@ -246,8 +261,8 @@ const ttt = await TTTClient.create({
|
|
|
246
261
|
|
|
247
262
|
| Method | Description |
|
|
248
263
|
|---|---|
|
|
249
|
-
| `GrgPipeline.processForward(data)` | Encode
|
|
250
|
-
| `GrgPipeline.processInverse(shards, length)` | Decode
|
|
264
|
+
| `GrgPipeline.processForward(data)` | Encode data through the multi-layer integrity pipeline, producing verifiable shards |
|
|
265
|
+
| `GrgPipeline.processInverse(shards, length)` | Decode shards back to original data with integrity verification |
|
|
251
266
|
|
|
252
267
|
### AdaptiveSwitch
|
|
253
268
|
|
|
@@ -269,32 +284,18 @@ TTTClient (entry point)
|
|
|
269
284
|
| |-- EVMConnector On-chain mint/burn/events (ethers v6)
|
|
270
285
|
| '-- ProtocolFee EIP-712 signed fee collection
|
|
271
286
|
|-- AdaptiveSwitch TURBO/FULL mode state machine
|
|
272
|
-
|-- GRG Pipeline
|
|
287
|
+
|-- GRG Pipeline Multi-layer data integrity (proprietary)
|
|
273
288
|
|-- PoolRegistry Multi-pool statistics tracking
|
|
274
289
|
'-- Signer Abstraction PrivateKey | Turnkey | Privy | KMS
|
|
275
290
|
```
|
|
276
291
|
|
|
277
|
-
###
|
|
292
|
+
### Data Integrity: GRG Pipeline
|
|
278
293
|
|
|
279
|
-
GRG
|
|
294
|
+
GRG is a multi-layer data integrity pipeline that protects PoT payloads — analogous to how the TLS record protocol protects HTTP payloads. It provides compression, erasure coding, and error correction in a single pass.
|
|
280
295
|
|
|
281
|
-
|
|
282
|
-
FORWARD (Encode)
|
|
283
|
-
Raw Data --> Golomb-Rice Compression
|
|
284
|
-
--> Reed-Solomon Erasure Coding
|
|
285
|
-
--> Golay(24,12) Verification Codes
|
|
286
|
-
--> Shards
|
|
287
|
-
|
|
288
|
-
INVERSE (Decode)
|
|
289
|
-
Shards --> Golay(24,12) Error Detection
|
|
290
|
-
--> Reed-Solomon Reconstruction
|
|
291
|
-
--> Golomb-Rice Decompression
|
|
292
|
-
--> Raw Data
|
|
293
|
-
```
|
|
296
|
+
The pipeline produces verifiable shards that can be independently validated and reconstructed, ensuring PoT integrity even under partial data loss.
|
|
294
297
|
|
|
295
|
-
|
|
296
|
-
**Reed-Solomon** adds erasure coding so data survives shard loss.
|
|
297
|
-
**Golay(24,12)** detects and corrects up to 3-bit errors per 24-bit codeword.
|
|
298
|
+
> Implementation details are proprietary. See the [IETF Draft](https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/) for the abstract specification.
|
|
298
299
|
|
|
299
300
|
### Adaptive Mode Switching
|
|
300
301
|
|
|
@@ -390,4 +391,25 @@ Copyright 2026 Helm Protocol.
|
|
|
390
391
|
|
|
391
392
|
---
|
|
392
393
|
|
|
394
|
+
## Learn More
|
|
395
|
+
|
|
396
|
+
- [IETF Draft: draft-helmprotocol-tttps-00](https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/) — TTTPS Protocol Specification
|
|
397
|
+
- [Yellow Paper](https://github.com/Helm-Protocol/OpenTTT/blob/main/YELLOW_PAPER.md) — Technical Deep Dive
|
|
398
|
+
- [MCP Server](https://github.com/Helm-Protocol/OpenTTT/tree/main/mcp) — AI Agent Integration (`@helm-protocol/ttt-mcp`)
|
|
399
|
+
- [Subgraph (The Graph)](https://api.studio.thegraph.com/query/1744392/openttt-base-sepolia/v0.2.0) — On-chain PoT Data
|
|
400
|
+
- [Base Sepolia Contracts](https://sepolia.basescan.org/address/0xde357135cA493e59680182CDE9E1c6A4dA400811) — TTT ERC-1155
|
|
401
|
+
- [Helm Protocol](https://github.com/Helm-Protocol) — GitHub Organization
|
|
402
|
+
|
|
393
403
|
[GitHub](https://github.com/Helm-Protocol/OpenTTT) | Built by [Helm Protocol](https://github.com/Helm-Protocol)
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Contributing
|
|
408
|
+
|
|
409
|
+
Contributions are welcome. If you find a bug, have a feature request, or want to improve the documentation, please open an issue or submit a pull request on [GitHub](https://github.com/Helm-Protocol/OpenTTT).
|
|
410
|
+
|
|
411
|
+
- **Bug reports**: Open an issue with a minimal reproduction case.
|
|
412
|
+
- **Feature requests**: Open an issue describing the use case and expected behavior.
|
|
413
|
+
- **Pull requests**: Fork the repo, make your changes, ensure all tests pass (`npm test`), and open a PR against `main`.
|
|
414
|
+
|
|
415
|
+
For significant changes, please open an issue first to discuss the approach.
|
package/dist/auto_mint.js
CHANGED
|
@@ -220,7 +220,7 @@ class AutoMintEngine {
|
|
|
220
220
|
const tokenId = ethers_1.ethers.keccak256(ethers_1.ethers.AbiCoder.defaultAbiCoder().encode(["uint256", "address", "uint64", "uint256"], [BigInt(this.config.chainId), this.config.poolAddress, synthesized.timestamp, nonceSuffix]));
|
|
221
221
|
// 3. Fee calculation
|
|
222
222
|
const feeCalculation = await this.feeEngine.calculateMintFee(this.config.tier);
|
|
223
|
-
// 4. EVM mint call — run
|
|
223
|
+
// 4. EVM mint call — run GRG integrity pipeline
|
|
224
224
|
const grgPayload = ethers_1.ethers.AbiCoder.defaultAbiCoder().encode(["bytes32", "bytes32", "uint64", "uint8"], [tokenId, potHash, synthesized.timestamp, pot.sources]);
|
|
225
225
|
const grgStart = Date.now();
|
|
226
226
|
const grgShards = helm_crypto_1.GrgForward.encode(ethers_1.ethers.getBytes(grgPayload), this.config.chainId, this.config.poolAddress);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GRG API Client — replaces local GRG computation with server-side API call.
|
|
3
|
+
* GRG source code stays in Helm private repo. Only API calls go through npm SDK.
|
|
4
|
+
*
|
|
5
|
+
* Drop-in replacement interface for local GrgForward.encode() and GrgInverse.verify().
|
|
6
|
+
*/
|
|
7
|
+
export interface GrgEncodeResult {
|
|
8
|
+
/** Hex-encoded serialized shards (joined as JSON array of hex strings) */
|
|
9
|
+
shards: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface GrgVerifyResult {
|
|
12
|
+
valid: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare class GrgApiClient {
|
|
15
|
+
private baseUrl;
|
|
16
|
+
private timeoutMs;
|
|
17
|
+
constructor(baseUrl?: string, options?: {
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Forward pass: encode data through GRG pipeline (server-side).
|
|
22
|
+
* Same interface contract as local GrgForward.encode().
|
|
23
|
+
*
|
|
24
|
+
* @returns Array of Uint8Array shards (same shape as GrgForward.encode())
|
|
25
|
+
*/
|
|
26
|
+
encode(data: Uint8Array, chainId: number, poolAddress: string): Promise<Uint8Array[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Verify: check GRG integrity (server-side).
|
|
29
|
+
* Same interface contract as local GrgInverse.verify().
|
|
30
|
+
*
|
|
31
|
+
* @returns boolean — true if data matches the original shards
|
|
32
|
+
*/
|
|
33
|
+
verify(data: Uint8Array, originalShards: Uint8Array[], chainId: number, poolAddress: string): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Health check — ping the GRG API server.
|
|
36
|
+
* Returns true if reachable within timeoutMs.
|
|
37
|
+
*/
|
|
38
|
+
isReachable(): Promise<boolean>;
|
|
39
|
+
}
|
|
40
|
+
export declare function getDefaultGrgApiClient(): GrgApiClient;
|
|
41
|
+
export declare function setDefaultGrgApiClient(client: GrgApiClient): void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* GRG API Client — replaces local GRG computation with server-side API call.
|
|
4
|
+
* GRG source code stays in Helm private repo. Only API calls go through npm SDK.
|
|
5
|
+
*
|
|
6
|
+
* Drop-in replacement interface for local GrgForward.encode() and GrgInverse.verify().
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.GrgApiClient = void 0;
|
|
10
|
+
exports.getDefaultGrgApiClient = getDefaultGrgApiClient;
|
|
11
|
+
exports.setDefaultGrgApiClient = setDefaultGrgApiClient;
|
|
12
|
+
class GrgApiClient {
|
|
13
|
+
baseUrl;
|
|
14
|
+
timeoutMs;
|
|
15
|
+
constructor(baseUrl = "https://grg.helmprotocol.com/api/v1", options) {
|
|
16
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
17
|
+
this.timeoutMs = options?.timeoutMs ?? 5000;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Forward pass: encode data through GRG pipeline (server-side).
|
|
21
|
+
* Same interface contract as local GrgForward.encode().
|
|
22
|
+
*
|
|
23
|
+
* @returns Array of Uint8Array shards (same shape as GrgForward.encode())
|
|
24
|
+
*/
|
|
25
|
+
async encode(data, chainId, poolAddress) {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
28
|
+
try {
|
|
29
|
+
const resp = await fetch(`${this.baseUrl}/encode`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
data: Buffer.from(data).toString("hex"),
|
|
34
|
+
chainId: chainId,
|
|
35
|
+
poolAddress,
|
|
36
|
+
}),
|
|
37
|
+
signal: controller.signal,
|
|
38
|
+
});
|
|
39
|
+
if (!resp.ok) {
|
|
40
|
+
throw new Error(`GRG API error: ${resp.status} ${resp.statusText}`);
|
|
41
|
+
}
|
|
42
|
+
const result = await resp.json();
|
|
43
|
+
return result.shards.map((hex) => Buffer.from(hex, "hex"));
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Verify: check GRG integrity (server-side).
|
|
51
|
+
* Same interface contract as local GrgInverse.verify().
|
|
52
|
+
*
|
|
53
|
+
* @returns boolean — true if data matches the original shards
|
|
54
|
+
*/
|
|
55
|
+
async verify(data, originalShards, chainId, poolAddress) {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
58
|
+
try {
|
|
59
|
+
const resp = await fetch(`${this.baseUrl}/verify`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "Content-Type": "application/json" },
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
data: Buffer.from(data).toString("hex"),
|
|
64
|
+
shards: originalShards.map((s) => Buffer.from(s).toString("hex")),
|
|
65
|
+
chainId: chainId,
|
|
66
|
+
poolAddress,
|
|
67
|
+
}),
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
});
|
|
70
|
+
if (!resp.ok) {
|
|
71
|
+
throw new Error(`GRG API error: ${resp.status} ${resp.statusText}`);
|
|
72
|
+
}
|
|
73
|
+
const result = await resp.json();
|
|
74
|
+
return result.valid;
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Health check — ping the GRG API server.
|
|
82
|
+
* Returns true if reachable within timeoutMs.
|
|
83
|
+
*/
|
|
84
|
+
async isReachable() {
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
87
|
+
try {
|
|
88
|
+
const resp = await fetch(`${this.baseUrl}/health`, {
|
|
89
|
+
method: "GET",
|
|
90
|
+
signal: controller.signal,
|
|
91
|
+
});
|
|
92
|
+
return resp.ok;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.GrgApiClient = GrgApiClient;
|
|
103
|
+
/**
|
|
104
|
+
* Singleton default client — uses production GRG API endpoint.
|
|
105
|
+
* Can be overridden via setDefaultGrgApiClient() for testing/staging.
|
|
106
|
+
*/
|
|
107
|
+
let _defaultClient = null;
|
|
108
|
+
function getDefaultGrgApiClient() {
|
|
109
|
+
if (!_defaultClient) {
|
|
110
|
+
_defaultClient = new GrgApiClient();
|
|
111
|
+
}
|
|
112
|
+
return _defaultClient;
|
|
113
|
+
}
|
|
114
|
+
function setDefaultGrgApiClient(client) {
|
|
115
|
+
_defaultClient = client;
|
|
116
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
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;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ __exportStar(require("./v4_hook"), exports);
|
|
|
26
26
|
__exportStar(require("./logger"), exports);
|
|
27
27
|
__exportStar(require("./types"), exports);
|
|
28
28
|
__exportStar(require("./ttt_client"), exports);
|
|
29
|
+
__exportStar(require("./http_client"), exports);
|
|
29
30
|
__exportStar(require("./auto_mint"), exports);
|
|
30
31
|
__exportStar(require("./time_synthesis"), exports);
|
|
31
32
|
__exportStar(require("./dynamic_fee"), exports);
|
package/dist/time_synthesis.js
CHANGED
|
@@ -222,10 +222,6 @@ class TimeSynthesis {
|
|
|
222
222
|
else if (s === 'apple') {
|
|
223
223
|
this.sources.push(new HTTPSTimeSource('apple', 'https://time.apple.com/'));
|
|
224
224
|
}
|
|
225
|
-
else if (s === 'kriss') {
|
|
226
|
-
// Legacy: KRISS NTP (plaintext UDP) — kept for backward compat, not in defaults
|
|
227
|
-
this.sources.push(new NTPSource('kriss', 'time.kriss.re.kr'));
|
|
228
|
-
}
|
|
229
225
|
}
|
|
230
226
|
}
|
|
231
227
|
async getFromSource(name) {
|