@otskit/mcp 0.6.4 → 0.7.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 +12 -12
- package/dist/chunk-KSFL3INC.js +79 -0
- package/dist/chunk-V4ONBNWS.js +120 -0
- package/dist/{chunk-4WQTV6CC.js → chunk-ZQUFY5MM.js} +140 -116
- package/dist/{cli-2KSRIDHM.js → cli-MR24FO2T.js} +4 -4
- package/dist/index.js +7 -7
- package/dist/{server-P3TIJGLB.js → server-6HE6K337.js} +14 -11
- package/dist/watch-JQ6XJCXF.js +10 -0
- package/package.json +1 -1
- package/dist/chunk-Y3F7WBP6.js +0 -133
- package/dist/watch-MF5SD2H6.js +0 -41
package/README.md
CHANGED
|
@@ -8,16 +8,16 @@
|
|
|
8
8
|
[](https://www.npmjs.com/package/@otskit/mcp)
|
|
9
9
|
[](https://www.npmjs.com/package/@otskit/mcp)
|
|
10
10
|
[](https://www.typescriptlang.org/)
|
|
11
|
-
[](https://nodejs.org)
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
[](https://glama.ai/mcp/servers/OTSkit/OTSkit-MCP)
|
|
14
14
|
[](https://glama.ai/mcp/servers/OTSkit/OTSkit-MCP)
|
|
15
15
|
|
|
16
|
-
OpenTimestamps MCP server
|
|
16
|
+
OpenTimestamps MCP server - stamp, upgrade, and verify Bitcoin timestamps via AI agents.
|
|
17
17
|
|
|
18
|
-
Exposes a set of tools to any MCP-compatible agent so it can timestamp documents, monitor confirmation status, and verify proofs against the Bitcoin blockchain
|
|
18
|
+
Exposes a set of tools to any MCP-compatible agent so it can timestamp documents, monitor confirmation status, and verify proofs against the Bitcoin blockchain - all from a conversation.
|
|
19
19
|
|
|
20
|
-
> **Note on confirmation times:** After stamping, a proof is `pending` until Bitcoin confirms it. This typically takes
|
|
20
|
+
> **Note on confirmation times:** After stamping, a proof is `pending` until Bitcoin confirms it. This typically takes **~60 minutes** but can take **several hours** during network congestion. Use `ots-mcp watch` or `upgrade_timestamp` to monitor. A pending status is not an error.
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
@@ -44,7 +44,7 @@ Each command writes the MCP entry into the agent's config file, makes a `.bak` b
|
|
|
44
44
|
| `ots-mcp upgrade <id>` | Check if a pending stamp has been confirmed |
|
|
45
45
|
| `ots-mcp verify <id>` | Verify a stamp against Bitcoin |
|
|
46
46
|
| `ots-mcp list [status]` | List stamps (`pending` / `confirmed` / `failed`) |
|
|
47
|
-
| `ots-mcp watch [minutes]` |
|
|
47
|
+
| `ots-mcp watch [minutes]` | Monitor pending stamps and attempt due upgrades (default: 30 min, minimum: 15 min) |
|
|
48
48
|
| `ots-mcp check-pending` | Run one upgrade pass over all pending stamps |
|
|
49
49
|
| `ots-mcp scheduler install\|remove\|status` | Manage OS-level scheduler for auto-upgrades |
|
|
50
50
|
| `ots-mcp backup [dest]` | Backup the SQLite database |
|
|
@@ -56,10 +56,10 @@ Each command writes the MCP entry into the agent's config file, makes a `.bak` b
|
|
|
56
56
|
|---|---|
|
|
57
57
|
| `create_timestamp` | Stamp a SHA-256 hash against 4 public OTS calendars |
|
|
58
58
|
| `upgrade_timestamp` | Check if a pending stamp has been confirmed in Bitcoin |
|
|
59
|
-
| `verify_timestamp` | Verify a stamp
|
|
59
|
+
| `verify_timestamp` | Verify a stamp - proves hash existed before a given Bitcoin block |
|
|
60
60
|
| `inspect_timestamp` | Inspect a stored proof file without network calls |
|
|
61
61
|
| `list_pending` | List stamps with status, retry count, and filters |
|
|
62
|
-
| `watch` | Open a terminal window monitoring pending stamps
|
|
62
|
+
| `watch` | Open a terminal window monitoring pending stamps and attempting due upgrades |
|
|
63
63
|
| `hash_file` | Compute the SHA-256 of a local file and return it as a 64-char hex string (no network calls) |
|
|
64
64
|
| `stamp_file` | Compute SHA-256 of a local file and stamp it on Bitcoin in one step |
|
|
65
65
|
|
|
@@ -67,7 +67,7 @@ Each command writes the MCP entry into the agent's config file, makes a `.bak` b
|
|
|
67
67
|
|
|
68
68
|
All data is stored in `~/.ots-mcp/`:
|
|
69
69
|
|
|
70
|
-
```
|
|
70
|
+
```text
|
|
71
71
|
~/.ots-mcp/
|
|
72
72
|
ots-mcp.db # SQLite database (stamps, proof files)
|
|
73
73
|
config.json # Optional config overrides
|
|
@@ -104,7 +104,7 @@ npm test # run tests
|
|
|
104
104
|
|
|
105
105
|
## Dependencies
|
|
106
106
|
|
|
107
|
-
- [`@otskit/core`](https://github.com/AlexAlves87/otskit-core)
|
|
108
|
-
- [`@otskit/client`](https://github.com/AlexAlves87/otskit-client)
|
|
109
|
-
- [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
110
|
-
- `node-sqlite3-wasm`
|
|
107
|
+
- [`@otskit/core`](https://github.com/AlexAlves87/otskit-core) - OpenTimestamps core logic
|
|
108
|
+
- [`@otskit/client`](https://github.com/AlexAlves87/otskit-client) - OTS calendar client
|
|
109
|
+
- [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) - MCP SDK
|
|
110
|
+
- `node-sqlite3-wasm` - local database (pure WASM, no native compilation)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDb,
|
|
3
|
+
loadConfig,
|
|
4
|
+
upgradeTimestamp
|
|
5
|
+
} from "./chunk-ZQUFY5MM.js";
|
|
6
|
+
|
|
7
|
+
// src/tools/watch.ts
|
|
8
|
+
var DEFAULT_WATCH_INTERVAL_MINUTES = 30;
|
|
9
|
+
var MIN_WATCH_INTERVAL_MINUTES = 15;
|
|
10
|
+
var MAX_UPGRADES_PER_TICK = 20;
|
|
11
|
+
function normalizeWatchInterval(intervalMinutes = DEFAULT_WATCH_INTERVAL_MINUTES) {
|
|
12
|
+
if (!Number.isFinite(intervalMinutes)) return DEFAULT_WATCH_INTERVAL_MINUTES;
|
|
13
|
+
return Math.max(MIN_WATCH_INTERVAL_MINUTES, Math.floor(intervalMinutes));
|
|
14
|
+
}
|
|
15
|
+
async function watchPending(intervalMinutes = DEFAULT_WATCH_INTERVAL_MINUTES) {
|
|
16
|
+
const config = loadConfig();
|
|
17
|
+
const db = getDb();
|
|
18
|
+
const minutes = normalizeWatchInterval(intervalMinutes);
|
|
19
|
+
process.stdout.write(`Watching pending stamps and upgrading due proofs every ${minutes} min. Ctrl+C to stop.
|
|
20
|
+
|
|
21
|
+
`);
|
|
22
|
+
async function tick() {
|
|
23
|
+
const dueRows = db.all(
|
|
24
|
+
`SELECT id FROM stamps
|
|
25
|
+
WHERE status = 'pending'
|
|
26
|
+
AND (next_retry_at IS NULL OR next_retry_at <= ?)
|
|
27
|
+
ORDER BY created_at ASC
|
|
28
|
+
LIMIT ?`,
|
|
29
|
+
[(/* @__PURE__ */ new Date()).toISOString(), MAX_UPGRADES_PER_TICK]
|
|
30
|
+
);
|
|
31
|
+
if (dueRows.length > 0) {
|
|
32
|
+
process.stdout.write(`${now()} - upgrading ${dueRows.length} due stamp(s)
|
|
33
|
+
`);
|
|
34
|
+
}
|
|
35
|
+
for (const row of dueRows) {
|
|
36
|
+
const result = await upgradeTimestamp({ id: row.id }, db, config);
|
|
37
|
+
const status = "status" in result ? result.status : `error:${result.error}`;
|
|
38
|
+
process.stdout.write(` upgrade ${row.id.slice(0, 8)} -> ${status}
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
const rows = db.all(
|
|
42
|
+
`SELECT id, hash, status, attempt_count, bitcoin_block, confirmed_at, next_retry_at
|
|
43
|
+
FROM stamps
|
|
44
|
+
WHERE status != 'confirmed'
|
|
45
|
+
ORDER BY created_at DESC`
|
|
46
|
+
);
|
|
47
|
+
const confirmed = db.get(`SELECT COUNT(*) as n FROM stamps WHERE status = 'confirmed'`);
|
|
48
|
+
process.stdout.write(`${now()} - ${rows.length} pending, ${confirmed.n} confirmed
|
|
49
|
+
`);
|
|
50
|
+
for (const row of rows) {
|
|
51
|
+
const next = row.next_retry_at ? ` next ${row.next_retry_at.replace("T", " ").slice(0, 19)}` : "";
|
|
52
|
+
process.stdout.write(` ${row.id.slice(0, 8)} ${row.status} (${row.attempt_count} attempts)${next}
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
if (rows.length === 0) {
|
|
56
|
+
process.stdout.write(` (no pending stamps)
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
process.stdout.write("\n");
|
|
60
|
+
}
|
|
61
|
+
function now() {
|
|
62
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
63
|
+
}
|
|
64
|
+
async function loop() {
|
|
65
|
+
try {
|
|
66
|
+
await tick();
|
|
67
|
+
} catch (e) {
|
|
68
|
+
process.stderr.write(`watch error: ${String(e)}
|
|
69
|
+
`);
|
|
70
|
+
}
|
|
71
|
+
setTimeout(loop, minutes * 60 * 1e3);
|
|
72
|
+
}
|
|
73
|
+
await loop();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export {
|
|
77
|
+
normalizeWatchInterval,
|
|
78
|
+
watchPending
|
|
79
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDataDir,
|
|
3
|
+
getStamp,
|
|
4
|
+
insertStamp,
|
|
5
|
+
listStamps,
|
|
6
|
+
logOperation
|
|
7
|
+
} from "./chunk-ZQUFY5MM.js";
|
|
8
|
+
import {
|
|
9
|
+
writeAtomic
|
|
10
|
+
} from "./chunk-YFSUDT24.js";
|
|
11
|
+
|
|
12
|
+
// src/tools/create-timestamp.ts
|
|
13
|
+
import { mkdirSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { randomUUID } from "crypto";
|
|
16
|
+
import { OpenTimestampsClient } from "@otskit/client";
|
|
17
|
+
var HEX64 = /^[0-9a-f]{64}$/i;
|
|
18
|
+
async function createTimestamp(input, db, config) {
|
|
19
|
+
if (!HEX64.test(input.hash)) {
|
|
20
|
+
return { error: "invalid_hash", details: "hash must be 64 hex characters (SHA-256)" };
|
|
21
|
+
}
|
|
22
|
+
const normalizedHash = input.hash.toLowerCase();
|
|
23
|
+
const client = new OpenTimestampsClient({
|
|
24
|
+
calendars: config.calendars,
|
|
25
|
+
resilience: { timeout: config.calendar_timeout_ms }
|
|
26
|
+
});
|
|
27
|
+
const t0 = Date.now();
|
|
28
|
+
let proofBuffer;
|
|
29
|
+
try {
|
|
30
|
+
proofBuffer = await client.stamp(normalizedHash);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return { error: "calendar_error", details: String(e) };
|
|
33
|
+
}
|
|
34
|
+
const responseTimeMs = Date.now() - t0;
|
|
35
|
+
const id = randomUUID();
|
|
36
|
+
const proofDir = join(getDataDir(), "proofs");
|
|
37
|
+
mkdirSync(proofDir, { recursive: true });
|
|
38
|
+
const proofPath = join(proofDir, `${id}.ots`);
|
|
39
|
+
try {
|
|
40
|
+
writeAtomic(proofPath, proofBuffer);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return { error: "storage_error", details: String(e) };
|
|
43
|
+
}
|
|
44
|
+
const record = insertStamp(db, { id, hash: normalizedHash, proof_path: proofPath });
|
|
45
|
+
logOperation(db, { stamp_id: id, action: "stamp", result: "success", response_time_ms: responseTimeMs });
|
|
46
|
+
return {
|
|
47
|
+
id: record.id,
|
|
48
|
+
hash: record.hash,
|
|
49
|
+
status: "pending",
|
|
50
|
+
calendars: config.calendars,
|
|
51
|
+
created_at: record.created_at,
|
|
52
|
+
proof_path: proofPath
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/tools/verify-timestamp.ts
|
|
57
|
+
import { readFileSync } from "fs";
|
|
58
|
+
import { OpenTimestampsClient as OpenTimestampsClient2 } from "@otskit/client";
|
|
59
|
+
async function verifyTimestamp(input, db, config) {
|
|
60
|
+
const record = getStamp(db, input.id);
|
|
61
|
+
if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
|
|
62
|
+
if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
|
|
63
|
+
let proofBytes;
|
|
64
|
+
try {
|
|
65
|
+
proofBytes = readFileSync(record.proof_path);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return { error: "storage_error", details: String(e) };
|
|
68
|
+
}
|
|
69
|
+
const client = new OpenTimestampsClient2({
|
|
70
|
+
calendars: config.calendars,
|
|
71
|
+
resilience: { timeout: config.calendar_timeout_ms }
|
|
72
|
+
});
|
|
73
|
+
let result;
|
|
74
|
+
try {
|
|
75
|
+
result = await client.verify(proofBytes, record.hash);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: String(e) });
|
|
78
|
+
return { status: "network_error", hash: record.hash, details: String(e) };
|
|
79
|
+
}
|
|
80
|
+
if (!result.valid) {
|
|
81
|
+
if (result.error?.includes("No Bitcoin attestation")) {
|
|
82
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "pending" });
|
|
83
|
+
return { status: "pending", hash: record.hash, calendars: config.calendars };
|
|
84
|
+
}
|
|
85
|
+
if (result.error?.toLowerCase().includes("invalid") || result.error?.toLowerCase().includes("corrupt")) {
|
|
86
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
|
|
87
|
+
return { status: "invalid", hash: record.hash, reason: result.error ?? "unknown" };
|
|
88
|
+
}
|
|
89
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
|
|
90
|
+
return { status: "unknown", hash: record.hash };
|
|
91
|
+
}
|
|
92
|
+
logOperation(db, { stamp_id: input.id, action: "verify", result: "success" });
|
|
93
|
+
return {
|
|
94
|
+
status: "confirmed",
|
|
95
|
+
hash: record.hash,
|
|
96
|
+
bitcoin_block: result.blockHeight,
|
|
97
|
+
bitcoin_time: new Date(result.timestamp * 1e3).toISOString()
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/tools/list-pending.ts
|
|
102
|
+
function toPublic({ attempt_count: _, last_attempt_at: __, next_retry_at: ___, ...rest }) {
|
|
103
|
+
return rest;
|
|
104
|
+
}
|
|
105
|
+
function listPending(input, db, _config) {
|
|
106
|
+
const result = listStamps(db, {
|
|
107
|
+
status: input.status ?? "pending",
|
|
108
|
+
limit: Math.min(input.limit ?? 50, 200),
|
|
109
|
+
offset: input.offset ?? 0,
|
|
110
|
+
older_than_hours: input.older_than_hours,
|
|
111
|
+
due_now: input.due_now
|
|
112
|
+
});
|
|
113
|
+
return { items: result.items.map(toPublic), total: result.total };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export {
|
|
117
|
+
createTimestamp,
|
|
118
|
+
verifyTimestamp,
|
|
119
|
+
listPending
|
|
120
|
+
};
|
|
@@ -1,15 +1,138 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getDataDir
|
|
3
|
-
} from "./chunk-Y3F7WBP6.js";
|
|
4
1
|
import {
|
|
5
2
|
writeAtomic
|
|
6
3
|
} from "./chunk-YFSUDT24.js";
|
|
7
4
|
|
|
8
|
-
// src/
|
|
9
|
-
import { mkdirSync } from "fs";
|
|
5
|
+
// src/config.ts
|
|
6
|
+
import { readFileSync, existsSync, mkdirSync } from "fs";
|
|
10
7
|
import { join } from "path";
|
|
11
|
-
import {
|
|
12
|
-
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
function getDataDir() {
|
|
10
|
+
return process.env.OTS_MCP_DATA_DIR ?? join(homedir(), ".ots-mcp");
|
|
11
|
+
}
|
|
12
|
+
var DEFAULTS = {
|
|
13
|
+
stamp_enabled: true,
|
|
14
|
+
preserve_enabled: true,
|
|
15
|
+
preserve_whitelist: [],
|
|
16
|
+
preserve_max_bytes: 104857600,
|
|
17
|
+
preserve_max_files: 1e4,
|
|
18
|
+
scheduler_interval_minutes: 30,
|
|
19
|
+
calendar_timeout_ms: 1e4,
|
|
20
|
+
calendar_max_response_bytes: 1048576,
|
|
21
|
+
retry_max_attempts: 20,
|
|
22
|
+
log_file: join(getDataDir(), "ots-mcp.log"),
|
|
23
|
+
calendars: [
|
|
24
|
+
"https://alice.btc.calendar.opentimestamps.org",
|
|
25
|
+
"https://bob.btc.calendar.opentimestamps.org",
|
|
26
|
+
"https://finney.calendar.eternitywall.com",
|
|
27
|
+
"https://btc.calendar.catallaxy.com"
|
|
28
|
+
],
|
|
29
|
+
esplora_url: "https://blockstream.info/api"
|
|
30
|
+
};
|
|
31
|
+
function loadConfig() {
|
|
32
|
+
const dir = getDataDir();
|
|
33
|
+
mkdirSync(dir, { recursive: true });
|
|
34
|
+
const configPath = join(dir, "config.json");
|
|
35
|
+
if (!existsSync(configPath)) return { ...DEFAULTS };
|
|
36
|
+
const raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
37
|
+
return { ...DEFAULTS, ...raw };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/db/index.ts
|
|
41
|
+
import { createRequire } from "module";
|
|
42
|
+
import { join as join2 } from "path";
|
|
43
|
+
import { mkdirSync as mkdirSync2, statSync } from "fs";
|
|
44
|
+
|
|
45
|
+
// src/db/schema.ts
|
|
46
|
+
function initDb(db) {
|
|
47
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
48
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
49
|
+
runMigrations(db);
|
|
50
|
+
}
|
|
51
|
+
function runMigrations(db) {
|
|
52
|
+
const row = db.get("PRAGMA user_version");
|
|
53
|
+
if (row.user_version < 1) migrateTo1(db);
|
|
54
|
+
}
|
|
55
|
+
function migrateTo1(db) {
|
|
56
|
+
db.exec("BEGIN");
|
|
57
|
+
try {
|
|
58
|
+
db.exec(`
|
|
59
|
+
CREATE TABLE IF NOT EXISTS stamps (
|
|
60
|
+
id TEXT PRIMARY KEY,
|
|
61
|
+
hash TEXT NOT NULL,
|
|
62
|
+
status TEXT NOT NULL,
|
|
63
|
+
created_at TEXT NOT NULL,
|
|
64
|
+
confirmed_at TEXT,
|
|
65
|
+
bitcoin_block INTEGER,
|
|
66
|
+
bitcoin_time TEXT,
|
|
67
|
+
proof_path TEXT,
|
|
68
|
+
archive_path TEXT,
|
|
69
|
+
last_attempt_at TEXT,
|
|
70
|
+
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
71
|
+
last_error TEXT,
|
|
72
|
+
next_retry_at TEXT,
|
|
73
|
+
metadata TEXT
|
|
74
|
+
);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_stamps_hash ON stamps(hash);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_stamps_status ON stamps(status);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE IF NOT EXISTS operations_log (
|
|
79
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
80
|
+
stamp_id TEXT NOT NULL REFERENCES stamps(id),
|
|
81
|
+
action TEXT NOT NULL,
|
|
82
|
+
result TEXT NOT NULL,
|
|
83
|
+
error_msg TEXT,
|
|
84
|
+
calendar_uri TEXT,
|
|
85
|
+
response_time_ms INTEGER,
|
|
86
|
+
created_at TEXT NOT NULL
|
|
87
|
+
);
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_oplog_stamp_id ON operations_log(stamp_id);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_oplog_created ON operations_log(created_at);
|
|
90
|
+
`);
|
|
91
|
+
db.exec("PRAGMA user_version = 1");
|
|
92
|
+
db.exec("COMMIT");
|
|
93
|
+
} catch (e) {
|
|
94
|
+
db.exec("ROLLBACK");
|
|
95
|
+
throw e;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/db/index.ts
|
|
100
|
+
var _db = null;
|
|
101
|
+
function getDb() {
|
|
102
|
+
if (_db) return _db;
|
|
103
|
+
const _require = createRequire(import.meta.url);
|
|
104
|
+
const { Database } = _require("node-sqlite3-wasm");
|
|
105
|
+
const dir = getDataDir();
|
|
106
|
+
mkdirSync2(dir, { recursive: true });
|
|
107
|
+
_db = new Database(join2(dir, "db.sqlite"));
|
|
108
|
+
initDb(_db);
|
|
109
|
+
reconcileOrphans(_db);
|
|
110
|
+
return _db;
|
|
111
|
+
}
|
|
112
|
+
function backupDb(destPath) {
|
|
113
|
+
const escaped = destPath.replace(/'/g, "''");
|
|
114
|
+
getDb().exec(`VACUUM INTO '${escaped}'`);
|
|
115
|
+
}
|
|
116
|
+
function reconcileOrphans(db) {
|
|
117
|
+
const pending = db.all(
|
|
118
|
+
`SELECT id, proof_path FROM stamps WHERE status = 'pending' AND proof_path IS NOT NULL`
|
|
119
|
+
);
|
|
120
|
+
for (const row of pending) {
|
|
121
|
+
try {
|
|
122
|
+
statSync(row.proof_path);
|
|
123
|
+
} catch {
|
|
124
|
+
db.run(
|
|
125
|
+
`UPDATE stamps SET status = 'failed', last_error = ? WHERE id = ?`,
|
|
126
|
+
["proof file missing on disk", row.id]
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/tools/upgrade-timestamp.ts
|
|
133
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
134
|
+
import { OpenTimestampsClient, UpgradeError } from "@otskit/client";
|
|
135
|
+
import { DetachedTimestampFile, StreamDeserializationContext } from "@otskit/core";
|
|
13
136
|
|
|
14
137
|
// src/db/stamps.ts
|
|
15
138
|
function insertStamp(db, params) {
|
|
@@ -86,50 +209,7 @@ function logOperation(db, params) {
|
|
|
86
209
|
);
|
|
87
210
|
}
|
|
88
211
|
|
|
89
|
-
// src/tools/create-timestamp.ts
|
|
90
|
-
var HEX64 = /^[0-9a-f]{64}$/i;
|
|
91
|
-
async function createTimestamp(input, db, config) {
|
|
92
|
-
if (!HEX64.test(input.hash)) {
|
|
93
|
-
return { error: "invalid_hash", details: "hash must be 64 hex characters (SHA-256)" };
|
|
94
|
-
}
|
|
95
|
-
const normalizedHash = input.hash.toLowerCase();
|
|
96
|
-
const client = new OpenTimestampsClient({
|
|
97
|
-
calendars: config.calendars,
|
|
98
|
-
resilience: { timeout: config.calendar_timeout_ms }
|
|
99
|
-
});
|
|
100
|
-
const t0 = Date.now();
|
|
101
|
-
let proofBuffer;
|
|
102
|
-
try {
|
|
103
|
-
proofBuffer = await client.stamp(normalizedHash);
|
|
104
|
-
} catch (e) {
|
|
105
|
-
return { error: "calendar_error", details: String(e) };
|
|
106
|
-
}
|
|
107
|
-
const responseTimeMs = Date.now() - t0;
|
|
108
|
-
const id = randomUUID();
|
|
109
|
-
const proofDir = join(getDataDir(), "proofs");
|
|
110
|
-
mkdirSync(proofDir, { recursive: true });
|
|
111
|
-
const proofPath = join(proofDir, `${id}.ots`);
|
|
112
|
-
try {
|
|
113
|
-
writeAtomic(proofPath, proofBuffer);
|
|
114
|
-
} catch (e) {
|
|
115
|
-
return { error: "storage_error", details: String(e) };
|
|
116
|
-
}
|
|
117
|
-
const record = insertStamp(db, { id, hash: normalizedHash, proof_path: proofPath });
|
|
118
|
-
logOperation(db, { stamp_id: id, action: "stamp", result: "success", response_time_ms: responseTimeMs });
|
|
119
|
-
return {
|
|
120
|
-
id: record.id,
|
|
121
|
-
hash: record.hash,
|
|
122
|
-
status: "pending",
|
|
123
|
-
calendars: config.calendars,
|
|
124
|
-
created_at: record.created_at,
|
|
125
|
-
proof_path: proofPath
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
212
|
// src/tools/upgrade-timestamp.ts
|
|
130
|
-
import { readFileSync } from "fs";
|
|
131
|
-
import { OpenTimestampsClient as OpenTimestampsClient2, UpgradeError } from "@otskit/client";
|
|
132
|
-
import { DetachedTimestampFile, StreamDeserializationContext } from "@otskit/core";
|
|
133
213
|
function collectAttestations(ts) {
|
|
134
214
|
const atts = [...ts.attestations];
|
|
135
215
|
for (const branch of ts.branches) {
|
|
@@ -159,8 +239,8 @@ async function upgradeTimestamp(input, db, config) {
|
|
|
159
239
|
const record = getStamp(db, input.id);
|
|
160
240
|
if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
|
|
161
241
|
if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
|
|
162
|
-
const proofBefore =
|
|
163
|
-
const client = new
|
|
242
|
+
const proofBefore = readFileSync2(record.proof_path);
|
|
243
|
+
const client = new OpenTimestampsClient({
|
|
164
244
|
calendars: config.calendars,
|
|
165
245
|
resilience: { timeout: config.calendar_timeout_ms }
|
|
166
246
|
});
|
|
@@ -200,70 +280,14 @@ async function upgradeTimestamp(input, db, config) {
|
|
|
200
280
|
return { id: input.id, status: "pending", attempt_count: newAttemptCount, last_attempt_at: now, next_retry_at: next };
|
|
201
281
|
}
|
|
202
282
|
|
|
203
|
-
// src/tools/verify-timestamp.ts
|
|
204
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
205
|
-
import { OpenTimestampsClient as OpenTimestampsClient3 } from "@otskit/client";
|
|
206
|
-
async function verifyTimestamp(input, db, config) {
|
|
207
|
-
const record = getStamp(db, input.id);
|
|
208
|
-
if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
|
|
209
|
-
if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
|
|
210
|
-
let proofBytes;
|
|
211
|
-
try {
|
|
212
|
-
proofBytes = readFileSync2(record.proof_path);
|
|
213
|
-
} catch (e) {
|
|
214
|
-
return { error: "storage_error", details: String(e) };
|
|
215
|
-
}
|
|
216
|
-
const client = new OpenTimestampsClient3({
|
|
217
|
-
calendars: config.calendars,
|
|
218
|
-
resilience: { timeout: config.calendar_timeout_ms }
|
|
219
|
-
});
|
|
220
|
-
let result;
|
|
221
|
-
try {
|
|
222
|
-
result = await client.verify(proofBytes, record.hash);
|
|
223
|
-
} catch (e) {
|
|
224
|
-
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: String(e) });
|
|
225
|
-
return { status: "network_error", hash: record.hash, details: String(e) };
|
|
226
|
-
}
|
|
227
|
-
if (!result.valid) {
|
|
228
|
-
if (result.error?.includes("No Bitcoin attestation")) {
|
|
229
|
-
logOperation(db, { stamp_id: input.id, action: "verify", result: "pending" });
|
|
230
|
-
return { status: "pending", hash: record.hash, calendars: config.calendars };
|
|
231
|
-
}
|
|
232
|
-
if (result.error?.toLowerCase().includes("invalid") || result.error?.toLowerCase().includes("corrupt")) {
|
|
233
|
-
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
|
|
234
|
-
return { status: "invalid", hash: record.hash, reason: result.error ?? "unknown" };
|
|
235
|
-
}
|
|
236
|
-
logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
|
|
237
|
-
return { status: "unknown", hash: record.hash };
|
|
238
|
-
}
|
|
239
|
-
logOperation(db, { stamp_id: input.id, action: "verify", result: "success" });
|
|
240
|
-
return {
|
|
241
|
-
status: "confirmed",
|
|
242
|
-
hash: record.hash,
|
|
243
|
-
bitcoin_block: result.blockHeight,
|
|
244
|
-
bitcoin_time: new Date(result.timestamp * 1e3).toISOString()
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// src/tools/list-pending.ts
|
|
249
|
-
function toPublic({ attempt_count: _, last_attempt_at: __, next_retry_at: ___, ...rest }) {
|
|
250
|
-
return rest;
|
|
251
|
-
}
|
|
252
|
-
function listPending(input, db, _config) {
|
|
253
|
-
const result = listStamps(db, {
|
|
254
|
-
status: input.status ?? "pending",
|
|
255
|
-
limit: Math.min(input.limit ?? 50, 200),
|
|
256
|
-
offset: input.offset ?? 0,
|
|
257
|
-
older_than_hours: input.older_than_hours,
|
|
258
|
-
due_now: input.due_now
|
|
259
|
-
});
|
|
260
|
-
return { items: result.items.map(toPublic), total: result.total };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
283
|
export {
|
|
284
|
+
getDataDir,
|
|
285
|
+
loadConfig,
|
|
286
|
+
getDb,
|
|
287
|
+
backupDb,
|
|
288
|
+
insertStamp,
|
|
264
289
|
getStamp,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
listPending
|
|
290
|
+
listStamps,
|
|
291
|
+
logOperation,
|
|
292
|
+
upgradeTimestamp
|
|
269
293
|
};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createTimestamp,
|
|
3
3
|
listPending,
|
|
4
|
-
upgradeTimestamp,
|
|
5
4
|
verifyTimestamp
|
|
6
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-V4ONBNWS.js";
|
|
7
6
|
import {
|
|
8
7
|
backupDb,
|
|
9
8
|
getDb,
|
|
10
|
-
loadConfig
|
|
11
|
-
|
|
9
|
+
loadConfig,
|
|
10
|
+
upgradeTimestamp
|
|
11
|
+
} from "./chunk-ZQUFY5MM.js";
|
|
12
12
|
import "./chunk-YFSUDT24.js";
|
|
13
13
|
|
|
14
14
|
// src/cli.ts
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ if (!command || command === "--help" || command === "help") {
|
|
|
7
7
|
Commands:
|
|
8
8
|
serve Start MCP server (stdio transport)
|
|
9
9
|
setup <target> Configure MCP for an agent (claude | claude-code | codex)
|
|
10
|
-
watch [interval] Watch pending stamps
|
|
10
|
+
watch [interval] Watch and upgrade due pending stamps (default: 30 min, minimum: 15 min)
|
|
11
11
|
stamp <hash> Stamp a SHA-256 hash
|
|
12
12
|
upgrade <id> Upgrade a pending stamp
|
|
13
13
|
verify <id> Verify a stamp
|
|
@@ -20,7 +20,7 @@ Commands:
|
|
|
20
20
|
}
|
|
21
21
|
switch (command) {
|
|
22
22
|
case "serve": {
|
|
23
|
-
const { runServer } = await import("./server-
|
|
23
|
+
const { runServer } = await import("./server-6HE6K337.js");
|
|
24
24
|
await runServer();
|
|
25
25
|
break;
|
|
26
26
|
}
|
|
@@ -49,11 +49,11 @@ switch (command) {
|
|
|
49
49
|
break;
|
|
50
50
|
}
|
|
51
51
|
case "watch": {
|
|
52
|
-
const { watchPending } = await import("./watch-
|
|
52
|
+
const { normalizeWatchInterval, watchPending } = await import("./watch-JQ6XJCXF.js");
|
|
53
53
|
const parsed = args[0] ? parseInt(args[0], 10) : NaN;
|
|
54
|
-
const interval = isNaN(parsed)
|
|
55
|
-
if (args[0] && (isNaN(parsed) || parsed <
|
|
56
|
-
process.stderr.write(`
|
|
54
|
+
const interval = normalizeWatchInterval(isNaN(parsed) ? void 0 : parsed);
|
|
55
|
+
if (args[0] && (isNaN(parsed) || parsed < 15))
|
|
56
|
+
process.stderr.write(`Invalid interval "${args[0]}", using ${interval} min
|
|
57
57
|
`);
|
|
58
58
|
await watchPending(interval);
|
|
59
59
|
break;
|
|
@@ -65,7 +65,7 @@ switch (command) {
|
|
|
65
65
|
case "check-pending":
|
|
66
66
|
case "backup":
|
|
67
67
|
case "scheduler": {
|
|
68
|
-
const { runCli } = await import("./cli-
|
|
68
|
+
const { runCli } = await import("./cli-MR24FO2T.js");
|
|
69
69
|
await runCli(command, args);
|
|
70
70
|
break;
|
|
71
71
|
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createTimestamp,
|
|
3
|
-
getStamp,
|
|
4
3
|
listPending,
|
|
5
|
-
upgradeTimestamp,
|
|
6
4
|
verifyTimestamp
|
|
7
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-V4ONBNWS.js";
|
|
6
|
+
import {
|
|
7
|
+
normalizeWatchInterval
|
|
8
|
+
} from "./chunk-KSFL3INC.js";
|
|
8
9
|
import {
|
|
9
10
|
getDb,
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
getStamp,
|
|
12
|
+
loadConfig,
|
|
13
|
+
upgradeTimestamp
|
|
14
|
+
} from "./chunk-ZQUFY5MM.js";
|
|
12
15
|
import "./chunk-YFSUDT24.js";
|
|
13
16
|
|
|
14
17
|
// src/server.ts
|
|
@@ -61,8 +64,8 @@ function inspectTimestamp(input, db, _config) {
|
|
|
61
64
|
|
|
62
65
|
// src/tools/watch-window.ts
|
|
63
66
|
import { exec } from "child_process";
|
|
64
|
-
function openWatchWindow(intervalMinutes
|
|
65
|
-
const minutes =
|
|
67
|
+
function openWatchWindow(intervalMinutes) {
|
|
68
|
+
const minutes = normalizeWatchInterval(intervalMinutes);
|
|
66
69
|
const cmd = `start powershell.exe -NoExit -Command "ots-mcp watch ${minutes}"`;
|
|
67
70
|
let errorMsg;
|
|
68
71
|
exec(cmd, { shell: "cmd" }, (err) => {
|
|
@@ -207,18 +210,18 @@ var TOOL_DEFINITIONS = [
|
|
|
207
210
|
},
|
|
208
211
|
{
|
|
209
212
|
name: "watch",
|
|
210
|
-
description: "Opens a new terminal window that monitors pending stamps
|
|
213
|
+
description: "Opens a new terminal window that monitors pending stamps and attempts due upgrades at a safe interval. The window stays open so the user can watch progress.",
|
|
211
214
|
inputSchema: {
|
|
212
215
|
type: "object",
|
|
213
216
|
properties: {
|
|
214
|
-
interval_minutes: { type: "number", description: "Polling interval in minutes (default:
|
|
217
|
+
interval_minutes: { type: "number", description: "Polling interval in minutes (default: 30, minimum: 15)" }
|
|
215
218
|
}
|
|
216
219
|
},
|
|
217
220
|
annotations: {
|
|
218
|
-
readOnlyHint:
|
|
221
|
+
readOnlyHint: false,
|
|
219
222
|
destructiveHint: false,
|
|
220
223
|
idempotentHint: false,
|
|
221
|
-
openWorldHint:
|
|
224
|
+
openWorldHint: true
|
|
222
225
|
}
|
|
223
226
|
}
|
|
224
227
|
];
|
package/package.json
CHANGED
package/dist/chunk-Y3F7WBP6.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
// src/config.ts
|
|
2
|
-
import { readFileSync, existsSync, mkdirSync } from "fs";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { homedir } from "os";
|
|
5
|
-
function getDataDir() {
|
|
6
|
-
return process.env.OTS_MCP_DATA_DIR ?? join(homedir(), ".ots-mcp");
|
|
7
|
-
}
|
|
8
|
-
var DEFAULTS = {
|
|
9
|
-
stamp_enabled: true,
|
|
10
|
-
preserve_enabled: true,
|
|
11
|
-
preserve_whitelist: [],
|
|
12
|
-
preserve_max_bytes: 104857600,
|
|
13
|
-
preserve_max_files: 1e4,
|
|
14
|
-
scheduler_interval_minutes: 30,
|
|
15
|
-
calendar_timeout_ms: 1e4,
|
|
16
|
-
calendar_max_response_bytes: 1048576,
|
|
17
|
-
retry_max_attempts: 20,
|
|
18
|
-
log_file: join(getDataDir(), "ots-mcp.log"),
|
|
19
|
-
calendars: [
|
|
20
|
-
"https://alice.btc.calendar.opentimestamps.org",
|
|
21
|
-
"https://bob.btc.calendar.opentimestamps.org",
|
|
22
|
-
"https://finney.calendar.eternitywall.com",
|
|
23
|
-
"https://btc.calendar.catallaxy.com"
|
|
24
|
-
],
|
|
25
|
-
esplora_url: "https://blockstream.info/api"
|
|
26
|
-
};
|
|
27
|
-
function loadConfig() {
|
|
28
|
-
const dir = getDataDir();
|
|
29
|
-
mkdirSync(dir, { recursive: true });
|
|
30
|
-
const configPath = join(dir, "config.json");
|
|
31
|
-
if (!existsSync(configPath)) return { ...DEFAULTS };
|
|
32
|
-
const raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
33
|
-
return { ...DEFAULTS, ...raw };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// src/db/index.ts
|
|
37
|
-
import { createRequire } from "module";
|
|
38
|
-
import { join as join2 } from "path";
|
|
39
|
-
import { mkdirSync as mkdirSync2, statSync } from "fs";
|
|
40
|
-
|
|
41
|
-
// src/db/schema.ts
|
|
42
|
-
function initDb(db) {
|
|
43
|
-
db.exec("PRAGMA busy_timeout = 5000");
|
|
44
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
45
|
-
runMigrations(db);
|
|
46
|
-
}
|
|
47
|
-
function runMigrations(db) {
|
|
48
|
-
const row = db.get("PRAGMA user_version");
|
|
49
|
-
if (row.user_version < 1) migrateTo1(db);
|
|
50
|
-
}
|
|
51
|
-
function migrateTo1(db) {
|
|
52
|
-
db.exec("BEGIN");
|
|
53
|
-
try {
|
|
54
|
-
db.exec(`
|
|
55
|
-
CREATE TABLE IF NOT EXISTS stamps (
|
|
56
|
-
id TEXT PRIMARY KEY,
|
|
57
|
-
hash TEXT NOT NULL,
|
|
58
|
-
status TEXT NOT NULL,
|
|
59
|
-
created_at TEXT NOT NULL,
|
|
60
|
-
confirmed_at TEXT,
|
|
61
|
-
bitcoin_block INTEGER,
|
|
62
|
-
bitcoin_time TEXT,
|
|
63
|
-
proof_path TEXT,
|
|
64
|
-
archive_path TEXT,
|
|
65
|
-
last_attempt_at TEXT,
|
|
66
|
-
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
67
|
-
last_error TEXT,
|
|
68
|
-
next_retry_at TEXT,
|
|
69
|
-
metadata TEXT
|
|
70
|
-
);
|
|
71
|
-
CREATE INDEX IF NOT EXISTS idx_stamps_hash ON stamps(hash);
|
|
72
|
-
CREATE INDEX IF NOT EXISTS idx_stamps_status ON stamps(status);
|
|
73
|
-
|
|
74
|
-
CREATE TABLE IF NOT EXISTS operations_log (
|
|
75
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
76
|
-
stamp_id TEXT NOT NULL REFERENCES stamps(id),
|
|
77
|
-
action TEXT NOT NULL,
|
|
78
|
-
result TEXT NOT NULL,
|
|
79
|
-
error_msg TEXT,
|
|
80
|
-
calendar_uri TEXT,
|
|
81
|
-
response_time_ms INTEGER,
|
|
82
|
-
created_at TEXT NOT NULL
|
|
83
|
-
);
|
|
84
|
-
CREATE INDEX IF NOT EXISTS idx_oplog_stamp_id ON operations_log(stamp_id);
|
|
85
|
-
CREATE INDEX IF NOT EXISTS idx_oplog_created ON operations_log(created_at);
|
|
86
|
-
`);
|
|
87
|
-
db.exec("PRAGMA user_version = 1");
|
|
88
|
-
db.exec("COMMIT");
|
|
89
|
-
} catch (e) {
|
|
90
|
-
db.exec("ROLLBACK");
|
|
91
|
-
throw e;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// src/db/index.ts
|
|
96
|
-
var _db = null;
|
|
97
|
-
function getDb() {
|
|
98
|
-
if (_db) return _db;
|
|
99
|
-
const _require = createRequire(import.meta.url);
|
|
100
|
-
const { Database } = _require("node-sqlite3-wasm");
|
|
101
|
-
const dir = getDataDir();
|
|
102
|
-
mkdirSync2(dir, { recursive: true });
|
|
103
|
-
_db = new Database(join2(dir, "db.sqlite"));
|
|
104
|
-
initDb(_db);
|
|
105
|
-
reconcileOrphans(_db);
|
|
106
|
-
return _db;
|
|
107
|
-
}
|
|
108
|
-
function backupDb(destPath) {
|
|
109
|
-
const escaped = destPath.replace(/'/g, "''");
|
|
110
|
-
getDb().exec(`VACUUM INTO '${escaped}'`);
|
|
111
|
-
}
|
|
112
|
-
function reconcileOrphans(db) {
|
|
113
|
-
const pending = db.all(
|
|
114
|
-
`SELECT id, proof_path FROM stamps WHERE status = 'pending' AND proof_path IS NOT NULL`
|
|
115
|
-
);
|
|
116
|
-
for (const row of pending) {
|
|
117
|
-
try {
|
|
118
|
-
statSync(row.proof_path);
|
|
119
|
-
} catch {
|
|
120
|
-
db.run(
|
|
121
|
-
`UPDATE stamps SET status = 'failed', last_error = ? WHERE id = ?`,
|
|
122
|
-
["proof file missing on disk", row.id]
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export {
|
|
129
|
-
getDataDir,
|
|
130
|
-
loadConfig,
|
|
131
|
-
getDb,
|
|
132
|
-
backupDb
|
|
133
|
-
};
|
package/dist/watch-MF5SD2H6.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getDb,
|
|
3
|
-
loadConfig
|
|
4
|
-
} from "./chunk-Y3F7WBP6.js";
|
|
5
|
-
|
|
6
|
-
// src/tools/watch.ts
|
|
7
|
-
async function watchPending(intervalMinutes = 5) {
|
|
8
|
-
const config = loadConfig();
|
|
9
|
-
const db = getDb();
|
|
10
|
-
process.stdout.write(`Watching pending stamps every ${intervalMinutes} min. Ctrl+C to stop.
|
|
11
|
-
|
|
12
|
-
`);
|
|
13
|
-
function tick() {
|
|
14
|
-
const rows = db.all(
|
|
15
|
-
`SELECT id, hash, status, attempt_count, bitcoin_block, confirmed_at FROM stamps WHERE status != 'confirmed' ORDER BY created_at DESC`
|
|
16
|
-
);
|
|
17
|
-
const confirmed = db.get(`SELECT COUNT(*) as n FROM stamps WHERE status = 'confirmed'`);
|
|
18
|
-
process.stdout.write(`${now()} \u2014 ${rows.length} pendientes, ${confirmed.n} confirmados
|
|
19
|
-
`);
|
|
20
|
-
for (const row of rows) {
|
|
21
|
-
process.stdout.write(` ${row.id.slice(0, 8)} ${row.status} (${row.attempt_count} intentos)
|
|
22
|
-
`);
|
|
23
|
-
}
|
|
24
|
-
if (rows.length === 0) {
|
|
25
|
-
process.stdout.write(` (ning\xFAn sello pendiente)
|
|
26
|
-
`);
|
|
27
|
-
}
|
|
28
|
-
process.stdout.write("\n");
|
|
29
|
-
}
|
|
30
|
-
function now() {
|
|
31
|
-
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
32
|
-
}
|
|
33
|
-
function loop() {
|
|
34
|
-
tick();
|
|
35
|
-
setTimeout(loop, Math.max(6e4, intervalMinutes * 60 * 1e3));
|
|
36
|
-
}
|
|
37
|
-
loop();
|
|
38
|
-
}
|
|
39
|
-
export {
|
|
40
|
-
watchPending
|
|
41
|
-
};
|