verify-proof 0.3.1__tar.gz → 0.3.2__tar.gz
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.
- {verify_proof-0.3.1/verify_proof.egg-info → verify_proof-0.3.2}/PKG-INFO +9 -4
- {verify_proof-0.3.1 → verify_proof-0.3.2}/README.md +7 -2
- {verify_proof-0.3.1 → verify_proof-0.3.2}/proofledger_api.py +129 -2
- {verify_proof-0.3.1 → verify_proof-0.3.2}/pyproject.toml +2 -2
- {verify_proof-0.3.1 → verify_proof-0.3.2/verify_proof.egg-info}/PKG-INFO +9 -4
- {verify_proof-0.3.1 → verify_proof-0.3.2}/verify_proof.py +2 -2
- {verify_proof-0.3.1 → verify_proof-0.3.2}/verify_proof_mcp.py +119 -6
- {verify_proof-0.3.1 → verify_proof-0.3.2}/LICENSE +0 -0
- {verify_proof-0.3.1 → verify_proof-0.3.2}/setup.cfg +0 -0
- {verify_proof-0.3.1 → verify_proof-0.3.2}/verify_proof.egg-info/SOURCES.txt +0 -0
- {verify_proof-0.3.1 → verify_proof-0.3.2}/verify_proof.egg-info/dependency_links.txt +0 -0
- {verify_proof-0.3.1 → verify_proof-0.3.2}/verify_proof.egg-info/entry_points.txt +0 -0
- {verify_proof-0.3.1 → verify_proof-0.3.2}/verify_proof.egg-info/requires.txt +0 -0
- {verify_proof-0.3.1 → verify_proof-0.3.2}/verify_proof.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: verify-proof
|
|
3
|
-
Version: 0.3.
|
|
4
|
-
Summary: Verify blockchain-anchored timestamp proofs
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Verify blockchain-anchored timestamp proofs offline, and create them through the ProofLedger API — CLI + MCP server
|
|
5
5
|
Author-email: Craig Solomon <craig@fulcrumenterprises.tech>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/Fulcrum-Enterprises/verify-proof
|
|
@@ -128,7 +128,9 @@ verify-proof create document.pdf --no-filename # send only the hash
|
|
|
128
128
|
Only the 64-character SHA-256 digest leaves your machine, plus the filename
|
|
129
129
|
unless you pass `--no-filename`. The file itself is never uploaded, on any
|
|
130
130
|
command. Free accounts include 25 API proofs a month and unlimited Polygon
|
|
131
|
-
anchoring through the web app:
|
|
131
|
+
anchoring through the web app:
|
|
132
|
+
|
|
133
|
+
[proofledger.io](https://proofledger.io/login.html?utm_source=verify-proof&utm_medium=readme&utm_campaign=create)
|
|
132
134
|
|
|
133
135
|
### Proof file format
|
|
134
136
|
|
|
@@ -169,7 +171,10 @@ The base install stays dependency-free; the `[mcp]` extra adds the MCP SDK and i
|
|
|
169
171
|
| `verify_file` | Verify a local file against a proof JSON file |
|
|
170
172
|
| `verify_hash` | Verify a known hash against inline or file-based proof data |
|
|
171
173
|
| `explain_proof` | Describe, in plain language, what a proof asserts and how to check it on a block explorer |
|
|
172
|
-
| `create_proof` | Anchor a file's hash on ProofLedger to create a new proof.
|
|
174
|
+
| `create_proof` | Anchor a file's hash on ProofLedger to create a new proof. Uses the network and needs an API key |
|
|
175
|
+
| `create_proofs_batch` | Anchor up to 100 files in one API call; file names double as external ids, so re-running returns the existing proofs. Network, API key |
|
|
176
|
+
| `get_proof_status` | Current anchoring status of one of your proofs. Network, API key |
|
|
177
|
+
| `save_proof_bundle` | Download a proof's bundle (this package's proof format) and save it next to the file, so verification needs nothing from ProofLedger later. Network, API key |
|
|
173
178
|
|
|
174
179
|
### Connect it to Claude Desktop
|
|
175
180
|
|
|
@@ -93,7 +93,9 @@ verify-proof create document.pdf --no-filename # send only the hash
|
|
|
93
93
|
Only the 64-character SHA-256 digest leaves your machine, plus the filename
|
|
94
94
|
unless you pass `--no-filename`. The file itself is never uploaded, on any
|
|
95
95
|
command. Free accounts include 25 API proofs a month and unlimited Polygon
|
|
96
|
-
anchoring through the web app:
|
|
96
|
+
anchoring through the web app:
|
|
97
|
+
|
|
98
|
+
[proofledger.io](https://proofledger.io/login.html?utm_source=verify-proof&utm_medium=readme&utm_campaign=create)
|
|
97
99
|
|
|
98
100
|
### Proof file format
|
|
99
101
|
|
|
@@ -134,7 +136,10 @@ The base install stays dependency-free; the `[mcp]` extra adds the MCP SDK and i
|
|
|
134
136
|
| `verify_file` | Verify a local file against a proof JSON file |
|
|
135
137
|
| `verify_hash` | Verify a known hash against inline or file-based proof data |
|
|
136
138
|
| `explain_proof` | Describe, in plain language, what a proof asserts and how to check it on a block explorer |
|
|
137
|
-
| `create_proof` | Anchor a file's hash on ProofLedger to create a new proof.
|
|
139
|
+
| `create_proof` | Anchor a file's hash on ProofLedger to create a new proof. Uses the network and needs an API key |
|
|
140
|
+
| `create_proofs_batch` | Anchor up to 100 files in one API call; file names double as external ids, so re-running returns the existing proofs. Network, API key |
|
|
141
|
+
| `get_proof_status` | Current anchoring status of one of your proofs. Network, API key |
|
|
142
|
+
| `save_proof_bundle` | Download a proof's bundle (this package's proof format) and save it next to the file, so verification needs nothing from ProofLedger later. Network, API key |
|
|
138
143
|
|
|
139
144
|
### Connect it to Claude Desktop
|
|
140
145
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""proofledger_api — minimal client for
|
|
2
|
+
"""proofledger_api — minimal client for the ProofLedger API.
|
|
3
3
|
|
|
4
4
|
This is the one part of verify-proof that talks to a network. Everything else
|
|
5
5
|
in this package (hashing, verification, explanation) runs entirely offline and
|
|
@@ -17,9 +17,14 @@ Standard library only, so installing verify-proof still pulls in nothing.
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
+
import hashlib
|
|
21
|
+
import hmac
|
|
20
22
|
import json
|
|
23
|
+
import re
|
|
21
24
|
import os
|
|
25
|
+
import time
|
|
22
26
|
import urllib.error
|
|
27
|
+
import urllib.parse
|
|
23
28
|
import urllib.request
|
|
24
29
|
|
|
25
30
|
DEFAULT_BASE_URL = "https://proofledger.io"
|
|
@@ -116,6 +121,121 @@ def submit_proof(
|
|
|
116
121
|
) from exc
|
|
117
122
|
|
|
118
123
|
|
|
124
|
+
def _request(
|
|
125
|
+
method: str,
|
|
126
|
+
path: str,
|
|
127
|
+
payload: dict | None = None,
|
|
128
|
+
key: str | None = None,
|
|
129
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
130
|
+
timeout: int = 30,
|
|
131
|
+
) -> dict:
|
|
132
|
+
"""One authenticated call to the v1 API. Errors become ProofLedgerError."""
|
|
133
|
+
request = urllib.request.Request(
|
|
134
|
+
f"{base_url.rstrip('/')}{path}",
|
|
135
|
+
data=None if payload is None else json.dumps(payload).encode("utf-8"),
|
|
136
|
+
headers={
|
|
137
|
+
"Authorization": f"Bearer {key or api_key()}",
|
|
138
|
+
"Content-Type": "application/json",
|
|
139
|
+
"User-Agent": "verify-proof (https://pypi.org/project/verify-proof/)",
|
|
140
|
+
},
|
|
141
|
+
method=method,
|
|
142
|
+
)
|
|
143
|
+
try:
|
|
144
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
145
|
+
body = response.read().decode("utf-8")
|
|
146
|
+
return json.loads(body) if body else {}
|
|
147
|
+
except urllib.error.HTTPError as exc:
|
|
148
|
+
raise ProofLedgerError(_http_message(exc)) from exc
|
|
149
|
+
except urllib.error.URLError as exc:
|
|
150
|
+
raise ProofLedgerError(
|
|
151
|
+
f"Could not reach {base_url}: {exc.reason}\n"
|
|
152
|
+
"Hashing and verification still work offline."
|
|
153
|
+
) from exc
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _digest(sha256: str) -> str:
|
|
157
|
+
digest = sha256.strip().lower().removeprefix("0x")
|
|
158
|
+
if len(digest) != 64 or any(c not in "0123456789abcdef" for c in digest):
|
|
159
|
+
raise ProofLedgerError(f"Not a SHA-256 hex digest: {sha256!r}")
|
|
160
|
+
return digest
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def submit_batch(
|
|
164
|
+
items: list[dict],
|
|
165
|
+
key: str | None = None,
|
|
166
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
167
|
+
timeout: int = 60,
|
|
168
|
+
) -> dict:
|
|
169
|
+
"""Anchor up to 100 hashes in one call (POST /api/v1/proofs/batch).
|
|
170
|
+
|
|
171
|
+
Each item: {"sha256": ..., "external_id": optional, "filename": optional}.
|
|
172
|
+
Items are checked first and written together; Polygon only. The response
|
|
173
|
+
has a `summary` and one entry per item with `result` created / exists /
|
|
174
|
+
invalid and, for the first two, the `proof`.
|
|
175
|
+
"""
|
|
176
|
+
if not items or len(items) > 100:
|
|
177
|
+
raise ProofLedgerError("submit_batch takes 1 to 100 items.")
|
|
178
|
+
payload = {"items": []}
|
|
179
|
+
for it in items:
|
|
180
|
+
out = {"sha256": _digest(it["sha256"])}
|
|
181
|
+
if it.get("external_id") is not None:
|
|
182
|
+
out["external_id"] = str(it["external_id"])
|
|
183
|
+
if it.get("filename"):
|
|
184
|
+
out["filename"] = str(it["filename"])[:500]
|
|
185
|
+
payload["items"].append(out)
|
|
186
|
+
return _request("POST", "/api/v1/proofs/batch", payload, key, base_url, timeout)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def get_proof(proof_id: int | str, key: str | None = None, base_url: str = DEFAULT_BASE_URL) -> dict:
|
|
190
|
+
"""One of your proofs by ProofLedger id (GET /api/v1/proof/:id)."""
|
|
191
|
+
return _request("GET", f"/api/v1/proof/{urllib.parse.quote(str(proof_id))}", None, key, base_url)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_proof_by_external_id(external_id: str, key: str | None = None, base_url: str = DEFAULT_BASE_URL) -> dict | None:
|
|
195
|
+
"""One of your proofs by your own record id, or None when there is none."""
|
|
196
|
+
try:
|
|
197
|
+
r = _request("GET", "/api/v1/proofs?" + urllib.parse.urlencode({"external_id": external_id}), None, key, base_url)
|
|
198
|
+
except ProofLedgerError as exc:
|
|
199
|
+
if "404" in str(exc):
|
|
200
|
+
return None
|
|
201
|
+
raise
|
|
202
|
+
return r.get("proof")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def get_bundle(proof_id: int | str, chain: str = "polygon", key: str | None = None, base_url: str = DEFAULT_BASE_URL) -> dict:
|
|
206
|
+
"""The bundle for a proof (GET /api/v1/proof/:id/bundle): everything needed
|
|
207
|
+
to verify it without ProofLedger, in this package's proof-file format.
|
|
208
|
+
Save it next to the file and `verify-proof verify <file> --proof bundle.json`
|
|
209
|
+
works offline, for as long as the chain exists."""
|
|
210
|
+
if chain not in ("polygon", "bitcoin"):
|
|
211
|
+
raise ProofLedgerError("chain must be 'polygon' or 'bitcoin'")
|
|
212
|
+
return _request("GET", f"/api/v1/proof/{urllib.parse.quote(str(proof_id))}/bundle?chain={chain}", None, key, base_url)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def verify_webhook_signature(
|
|
216
|
+
secret: str,
|
|
217
|
+
header: str,
|
|
218
|
+
raw_body: bytes | str,
|
|
219
|
+
tolerance_seconds: int = 300,
|
|
220
|
+
now: int | None = None,
|
|
221
|
+
) -> bool:
|
|
222
|
+
"""Check a ProofLedger webhook delivery before trusting it.
|
|
223
|
+
|
|
224
|
+
ProofLedger sends `ProofLedger-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, t + "." + body)>`.
|
|
225
|
+
Pass the RAW request body exactly as received; re-serialising it breaks the MAC.
|
|
226
|
+
Pure computation, no network.
|
|
227
|
+
"""
|
|
228
|
+
m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", str(header or ""))
|
|
229
|
+
if not m:
|
|
230
|
+
return False
|
|
231
|
+
t = int(m.group(1))
|
|
232
|
+
if abs((now if now is not None else int(time.time())) - t) > tolerance_seconds:
|
|
233
|
+
return False
|
|
234
|
+
body = raw_body if isinstance(raw_body, bytes) else str(raw_body).encode("utf-8")
|
|
235
|
+
expected = hmac.new(secret.encode("utf-8"), f"{t}.".encode("utf-8") + body, hashlib.sha256).hexdigest()
|
|
236
|
+
return hmac.compare_digest(expected, m.group(2))
|
|
237
|
+
|
|
238
|
+
|
|
119
239
|
def _http_message(exc: urllib.error.HTTPError) -> str:
|
|
120
240
|
"""Turn an API error into something the reader can act on."""
|
|
121
241
|
detail = ""
|
|
@@ -132,6 +252,12 @@ def _http_message(exc: urllib.error.HTTPError) -> str:
|
|
|
132
252
|
+ (f"\n({detail})" if detail else "")
|
|
133
253
|
)
|
|
134
254
|
if exc.code == 429:
|
|
255
|
+
retry_after = exc.headers.get("Retry-After") if exc.headers else None
|
|
256
|
+
if retry_after:
|
|
257
|
+
return (
|
|
258
|
+
f"Rate limit: 120 requests per minute per key (429). Retry after {retry_after} s.\n"
|
|
259
|
+
"One batch call carries up to 100 hashes." + (f"\n({detail})" if detail else "")
|
|
260
|
+
)
|
|
135
261
|
return (
|
|
136
262
|
"Monthly API proof limit reached (429).\n"
|
|
137
263
|
"Free and Standard include 25 proofs a month, Professional 500, "
|
|
@@ -183,7 +309,8 @@ def summarize(proof: dict, base_url: str = DEFAULT_BASE_URL) -> str:
|
|
|
183
309
|
lines += [
|
|
184
310
|
"",
|
|
185
311
|
"Anchoring is not instant. Polygon usually settles within minutes; "
|
|
186
|
-
"Bitcoin is
|
|
312
|
+
"Bitcoin, when requested, is anchored in hand-signed batches with no fixed "
|
|
313
|
+
"timeline. Re-check the proof, or verify the "
|
|
187
314
|
"downloaded proof file offline with: verify-proof verify <file> "
|
|
188
315
|
"--proof <proof.json>",
|
|
189
316
|
]
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "verify-proof"
|
|
7
|
-
version = "0.3.
|
|
8
|
-
description = "Verify blockchain-anchored timestamp proofs
|
|
7
|
+
version = "0.3.2"
|
|
8
|
+
description = "Verify blockchain-anchored timestamp proofs offline, and create them through the ProofLedger API — CLI + MCP server"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: verify-proof
|
|
3
|
-
Version: 0.3.
|
|
4
|
-
Summary: Verify blockchain-anchored timestamp proofs
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Verify blockchain-anchored timestamp proofs offline, and create them through the ProofLedger API — CLI + MCP server
|
|
5
5
|
Author-email: Craig Solomon <craig@fulcrumenterprises.tech>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/Fulcrum-Enterprises/verify-proof
|
|
@@ -128,7 +128,9 @@ verify-proof create document.pdf --no-filename # send only the hash
|
|
|
128
128
|
Only the 64-character SHA-256 digest leaves your machine, plus the filename
|
|
129
129
|
unless you pass `--no-filename`. The file itself is never uploaded, on any
|
|
130
130
|
command. Free accounts include 25 API proofs a month and unlimited Polygon
|
|
131
|
-
anchoring through the web app:
|
|
131
|
+
anchoring through the web app:
|
|
132
|
+
|
|
133
|
+
[proofledger.io](https://proofledger.io/login.html?utm_source=verify-proof&utm_medium=readme&utm_campaign=create)
|
|
132
134
|
|
|
133
135
|
### Proof file format
|
|
134
136
|
|
|
@@ -169,7 +171,10 @@ The base install stays dependency-free; the `[mcp]` extra adds the MCP SDK and i
|
|
|
169
171
|
| `verify_file` | Verify a local file against a proof JSON file |
|
|
170
172
|
| `verify_hash` | Verify a known hash against inline or file-based proof data |
|
|
171
173
|
| `explain_proof` | Describe, in plain language, what a proof asserts and how to check it on a block explorer |
|
|
172
|
-
| `create_proof` | Anchor a file's hash on ProofLedger to create a new proof.
|
|
174
|
+
| `create_proof` | Anchor a file's hash on ProofLedger to create a new proof. Uses the network and needs an API key |
|
|
175
|
+
| `create_proofs_batch` | Anchor up to 100 files in one API call; file names double as external ids, so re-running returns the existing proofs. Network, API key |
|
|
176
|
+
| `get_proof_status` | Current anchoring status of one of your proofs. Network, API key |
|
|
177
|
+
| `save_proof_bundle` | Download a proof's bundle (this package's proof format) and save it next to the file, so verification needs nothing from ProofLedger later. Network, API key |
|
|
173
178
|
|
|
174
179
|
### Connect it to Claude Desktop
|
|
175
180
|
|
|
@@ -8,7 +8,7 @@ Supports verification of:
|
|
|
8
8
|
- SHA-256 file hashes against Bitcoin and Polygon blockchain anchors
|
|
9
9
|
- Proof of creation timestamps for digital content
|
|
10
10
|
- Pre-loss evidence timestamps for legal/insurance records (ProofLedger)
|
|
11
|
-
-
|
|
11
|
+
- JSON proof files carrying hash, blockchain, tx_id and merkle_path (ProofLedger bundles; binary OpenTimestamps .ots files are a different format and are not parsed here, use the ots tool)
|
|
12
12
|
|
|
13
13
|
Usage:
|
|
14
14
|
python verify_proof.py <file> [--proof <proof.json>] [--algorithm sha256]
|
|
@@ -158,7 +158,7 @@ def main():
|
|
|
158
158
|
prog="verify-proof",
|
|
159
159
|
description=(
|
|
160
160
|
"Verify blockchain-anchored timestamp proofs. "
|
|
161
|
-
"
|
|
161
|
+
"Reads JSON proofs (ProofLedger bundles and compatible); .ots files are not parsed."
|
|
162
162
|
),
|
|
163
163
|
epilog=(
|
|
164
164
|
"Examples:\n"
|
|
@@ -3,19 +3,22 @@
|
|
|
3
3
|
|
|
4
4
|
A Model Context Protocol (MCP) server that lets MCP-compatible clients
|
|
5
5
|
(Claude Desktop, Cursor, and any other MCP host) verify blockchain-anchored
|
|
6
|
-
timestamp proofs directly. It exposes the `verify-proof` library as
|
|
6
|
+
timestamp proofs directly. It exposes the `verify-proof` library as tools
|
|
7
7
|
so an assistant can, on the user's own machine:
|
|
8
8
|
|
|
9
9
|
- compute the SHA-256 hash of a local file (the file is never uploaded),
|
|
10
10
|
- verify a local file against a blockchain-anchored proof JSON,
|
|
11
11
|
- verify a known hash against inline proof data,
|
|
12
12
|
- explain in plain language what a proof file asserts,
|
|
13
|
-
- create a new proof by anchoring a file's hash on ProofLedger
|
|
13
|
+
- create a new proof by anchoring a file's hash on ProofLedger,
|
|
14
|
+
- create proofs for up to 100 files in one call, check a proof's status,
|
|
15
|
+
and save a proof's bundle so it verifies later without ProofLedger.
|
|
14
16
|
|
|
15
17
|
The first four run entirely offline and always will: verification that depends
|
|
16
|
-
on the issuing service is not verification.
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
on the issuing service is not verification. The ProofLedger tools (create,
|
|
19
|
+
batch, status, bundle) are the exception and say so in their own descriptions:
|
|
20
|
+
anchoring requires a service, and they send hashes (never files) to
|
|
21
|
+
ProofLedger, with a free API key.
|
|
19
22
|
|
|
20
23
|
Blockchain timestamping proves a file existed at a point in time by anchoring
|
|
21
24
|
its SHA-256 hash to a public blockchain (Polygon or Bitcoin). ProofLedger
|
|
@@ -47,7 +50,15 @@ except ImportError as exc: # pragma: no cover - depends on installed SDK
|
|
|
47
50
|
f"(underlying import error: {exc})"
|
|
48
51
|
)
|
|
49
52
|
|
|
50
|
-
from proofledger_api import
|
|
53
|
+
from proofledger_api import (
|
|
54
|
+
DEFAULT_BASE_URL,
|
|
55
|
+
ProofLedgerError,
|
|
56
|
+
get_bundle,
|
|
57
|
+
get_proof,
|
|
58
|
+
submit_batch,
|
|
59
|
+
submit_proof,
|
|
60
|
+
summarize,
|
|
61
|
+
)
|
|
51
62
|
from verify_proof import hash_file, load_proof
|
|
52
63
|
from verify_proof import verify_proof as _verify_proof
|
|
53
64
|
|
|
@@ -269,6 +280,108 @@ def create_proof(file_path: str, bitcoin: bool = False, send_filename: bool = Tr
|
|
|
269
280
|
return summarize(proof)
|
|
270
281
|
|
|
271
282
|
|
|
283
|
+
@mcp.tool()
|
|
284
|
+
def create_proofs_batch(file_paths: list[str], send_filenames: bool = True) -> str:
|
|
285
|
+
"""Create proofs for several local files in ONE ProofLedger API call (up to 100).
|
|
286
|
+
|
|
287
|
+
Uses the network and needs PROOFLEDGER_API_KEY, like create_proof. Each
|
|
288
|
+
file is hashed locally; only the hashes (and filenames, if allowed) are
|
|
289
|
+
sent. The file name is also used as the external_id, so running this twice
|
|
290
|
+
on the same files returns the existing proofs instead of creating more.
|
|
291
|
+
Polygon anchoring only.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
file_paths: Paths of the files to timestamp, 1 to 100.
|
|
295
|
+
send_filenames: Send each file's name as its label. Set false to send
|
|
296
|
+
only the hashes (external ids are still the names).
|
|
297
|
+
"""
|
|
298
|
+
paths = [Path(p) for p in file_paths]
|
|
299
|
+
missing = [str(p) for p in paths if not p.is_file()]
|
|
300
|
+
if missing:
|
|
301
|
+
return "File not found: " + ", ".join(missing)
|
|
302
|
+
if not paths or len(paths) > 100:
|
|
303
|
+
return "Give between 1 and 100 files."
|
|
304
|
+
items = [
|
|
305
|
+
{"sha256": hash_file(str(p)), "external_id": p.name, "filename": p.name if send_filenames else None}
|
|
306
|
+
for p in paths
|
|
307
|
+
]
|
|
308
|
+
try:
|
|
309
|
+
r = submit_batch(items)
|
|
310
|
+
except ProofLedgerError as exc:
|
|
311
|
+
return str(exc)
|
|
312
|
+
summary = r.get("summary", {})
|
|
313
|
+
lines = [
|
|
314
|
+
f"Batch submitted: {summary.get('created', 0)} created, "
|
|
315
|
+
f"{summary.get('exists', 0)} already existed, {summary.get('invalid', 0)} invalid.",
|
|
316
|
+
"",
|
|
317
|
+
]
|
|
318
|
+
for it, p in zip(r.get("items", []), paths):
|
|
319
|
+
proof = it.get("proof") or {}
|
|
320
|
+
if it.get("result") in ("created", "exists"):
|
|
321
|
+
lines.append(
|
|
322
|
+
f" {p.name}: proof {proof.get('id')} ({it['result']}) "
|
|
323
|
+
f"{DEFAULT_BASE_URL}{proof.get('certificate_url', '')}"
|
|
324
|
+
)
|
|
325
|
+
else:
|
|
326
|
+
lines.append(f" {p.name}: {it.get('result')} - {it.get('error', '')}")
|
|
327
|
+
return "\n".join(lines)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@mcp.tool()
|
|
331
|
+
def get_proof_status(proof_id: int) -> str:
|
|
332
|
+
"""Fetch the current status of one of YOUR ProofLedger proofs by id.
|
|
333
|
+
|
|
334
|
+
Uses the network and needs PROOFLEDGER_API_KEY. Shows whether the Polygon
|
|
335
|
+
anchor has landed (usually within a minute of creation) and the Bitcoin
|
|
336
|
+
status when it was requested. For checking somebody else's proof, use
|
|
337
|
+
verify_file with their bundle instead; that needs no key and no network.
|
|
338
|
+
"""
|
|
339
|
+
try:
|
|
340
|
+
p = get_proof(proof_id)
|
|
341
|
+
except ProofLedgerError as exc:
|
|
342
|
+
return str(exc)
|
|
343
|
+
lines = [
|
|
344
|
+
f"Proof {p.get('id')} ({p.get('filename') or 'no filename'})",
|
|
345
|
+
f" SHA-256 : {p.get('sha256')}",
|
|
346
|
+
f" Polygon : {p.get('polygon_status')}" + (f" tx {p.get('chain_tx')}" if p.get("chain_tx") else ""),
|
|
347
|
+
f" Bitcoin : {p.get('bitcoin_status')}" + (f" tx {p.get('bitcoin_txid')}" if p.get("bitcoin_txid") else ""),
|
|
348
|
+
f" Created : {p.get('created_at')}",
|
|
349
|
+
f" Certificate : {DEFAULT_BASE_URL}{p.get('certificate_url', '')}",
|
|
350
|
+
]
|
|
351
|
+
if p.get("issued_via"):
|
|
352
|
+
lines.append(f" Recorded through {p['issued_via']} \u00b7 Anchored by ProofLedger")
|
|
353
|
+
return "\n".join(lines)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@mcp.tool()
|
|
357
|
+
def save_proof_bundle(proof_id: int, output_path: str, chain: str = "polygon") -> str:
|
|
358
|
+
"""Download the bundle for one of YOUR proofs and save it as a JSON file.
|
|
359
|
+
|
|
360
|
+
Uses the network and needs PROOFLEDGER_API_KEY. The bundle is everything
|
|
361
|
+
needed to verify the proof WITHOUT ProofLedger: hash, chain, transaction
|
|
362
|
+
id, anchor time and, for Bitcoin, the Merkle path. Save it next to the
|
|
363
|
+
file; `verify_file` (or `verify-proof verify`) then works offline, for as
|
|
364
|
+
long as the blockchain exists. Before the anchor lands the bundle says
|
|
365
|
+
anchored: false; fetch it again afterwards.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
proof_id: The ProofLedger proof id.
|
|
369
|
+
output_path: Where to write the JSON (e.g. song.mp3.proof.json).
|
|
370
|
+
chain: "polygon" (default) or "bitcoin" for the top-level fields.
|
|
371
|
+
"""
|
|
372
|
+
try:
|
|
373
|
+
bundle = get_bundle(proof_id, chain=chain)
|
|
374
|
+
except ProofLedgerError as exc:
|
|
375
|
+
return str(exc)
|
|
376
|
+
out = Path(output_path)
|
|
377
|
+
out.write_text(json.dumps(bundle, indent=2), encoding="utf-8")
|
|
378
|
+
state = "anchored" if bundle.get("anchored") else "NOT YET ANCHORED (fetch again later)"
|
|
379
|
+
return (
|
|
380
|
+
f"Saved bundle for proof {proof_id} to {out} ({bundle.get('blockchain')}, {state}). "
|
|
381
|
+
f"Verify offline with: verify-proof verify <file> --proof {out}"
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
|
|
272
385
|
def main() -> None:
|
|
273
386
|
"""Entry point — run the server over stdio (the transport MCP hosts use)."""
|
|
274
387
|
mcp.run()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|