|
|
|
@@ -11,6 +11,14 @@
|
|
|
|
|
<link rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;," />
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
#divHeaderText {
|
|
|
|
|
font-size: calc(max(var(--header-height), var(--header-min-height)) * 0.5) !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#divHeaderFlexRight {
|
|
|
|
|
padding-right: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#divBody {
|
|
|
|
|
flex-direction: column !important;
|
|
|
|
|
flex-wrap: nowrap !important;
|
|
|
|
@@ -128,8 +136,9 @@
|
|
|
|
|
.pq-warning strong { color: var(--accent-color); }
|
|
|
|
|
|
|
|
|
|
.pq-seed-display {
|
|
|
|
|
background: var(--primary-color);
|
|
|
|
|
color: var(--secondary-color);
|
|
|
|
|
background: var(--secondary-color);
|
|
|
|
|
color: var(--primary-color);
|
|
|
|
|
border: var(--border);
|
|
|
|
|
border-radius: var(--border-radius);
|
|
|
|
|
padding: 20px;
|
|
|
|
|
margin: 15px 0;
|
|
|
|
@@ -210,7 +219,8 @@
|
|
|
|
|
.pq-status-error { background: var(--accent-color); color: var(--secondary-color); }
|
|
|
|
|
|
|
|
|
|
.pq-event-preview {
|
|
|
|
|
background: var(--primary-color); color: var(--secondary-color); border-radius: var(--border-radius);
|
|
|
|
|
background: var(--secondary-color); color: var(--primary-color); border: var(--border);
|
|
|
|
|
border-radius: var(--border-radius);
|
|
|
|
|
padding: 15px; font-size: 11px; white-space: pre-wrap; word-break: break-all;
|
|
|
|
|
max-height: 400px; overflow-y: auto; text-align: left;
|
|
|
|
|
}
|
|
|
|
@@ -325,10 +335,6 @@
|
|
|
|
|
<li>Publish the event to relays</li>
|
|
|
|
|
</ol>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-warning">
|
|
|
|
|
<strong>Important:</strong> Your seed phrase is generated and handled entirely in your browser.
|
|
|
|
|
It is never sent to any server. Write it down on paper — it's your quantum-safe backup.
|
|
|
|
|
</div>
|
|
|
|
|
<button class="pq-button" id="pqStartBtn">Start Migration</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
@@ -341,27 +347,24 @@
|
|
|
|
|
The code is open source and auditable.
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Toggle: Generate vs Bring your own -->
|
|
|
|
|
<!-- Mode toggle + copy row (always visible) -->
|
|
|
|
|
<div class="pq-button-row" style="margin-bottom: 15px;">
|
|
|
|
|
<button class="pq-button pq-button-secondary pq-seed-toggle pq-seed-toggle-active" id="pqSeedToggleGenerate">Generate New Seed</button>
|
|
|
|
|
<button class="pq-button pq-button-secondary pq-seed-toggle" id="pqSeedToggleOwn">I Have a Seed Phrase</button>
|
|
|
|
|
<button class="pq-button pq-button-secondary" id="pqCopySeedBtn">Copy</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Generate mode -->
|
|
|
|
|
<div id="pqSeedGeneratePanel">
|
|
|
|
|
<div class="pq-seed-display" id="pqSeedDisplay"></div>
|
|
|
|
|
<div class="pq-button-row">
|
|
|
|
|
<button class="pq-button pq-button-secondary" id="pqCopySeedBtn">Copy</button>
|
|
|
|
|
<button class="pq-button pq-button-secondary" id="pqRegenerateBtn">Generate New Seed</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-info-text" style="margin-top: 15px; font-size: 13px;">
|
|
|
|
|
<strong>Optional:</strong> Add extra entropy by moving your mouse and typing in the box below.
|
|
|
|
|
This mixes your input with the browser's random number generator, making the seed unpredictable
|
|
|
|
|
even if the browser's RNG is compromised. You can skip this — a seed has already been generated for you above.
|
|
|
|
|
</div>
|
|
|
|
|
<div id="pqEntropyBox" style="background: var(--primary-color); color: var(--secondary-color); border-radius: var(--border-radius); padding: 15px; font-size: 13px; min-height: 60px; margin: 10px 0; cursor: text; text-align: left;" tabindex="0">
|
|
|
|
|
<div id="pqEntropyBox" style="background: var(--secondary-color); color: var(--primary-color); border: var(--border); border-radius: var(--border-radius); padding: 15px; font-size: 13px; min-height: 60px; margin: 10px 0; cursor: text; text-align: left;" tabindex="0">
|
|
|
|
|
<span id="pqEntropyHint">Optional: click here and type random characters, move your mouse to add entropy...</span>
|
|
|
|
|
<div style="margin-top: 8px; height: 6px; background: var(--secondary-color); border-radius: 3px; overflow: hidden;">
|
|
|
|
|
<div style="margin-top: 8px; height: 6px; background: var(--muted-color); border-radius: 3px; overflow: hidden;">
|
|
|
|
|
<div id="pqEntropyBar" style="height: 100%; width: 0%; background: var(--accent-color); transition: width 0.2s;"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
@@ -403,47 +406,37 @@
|
|
|
|
|
<div class="pq-progress-bar">
|
|
|
|
|
<div class="pq-progress-fill" id="pqDeriveProgress"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-key-list" id="pqKeyList">
|
|
|
|
|
<div class="pq-key-item" id="pqKeyMlDsa44">
|
|
|
|
|
<div class="pq-key-icon"></div>
|
|
|
|
|
<div class="pq-key-details">
|
|
|
|
|
<div class="pq-key-name">ML-DSA-44 (Dilithium)</div>
|
|
|
|
|
<div class="pq-key-meta">FIPS 204 · Cat 2 · 1312-byte pubkey · m/44'/1237'/0'/0/1</div>
|
|
|
|
|
<div class="pq-key-pubkey" id="pqKeyMlDsa44Pub"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-checklist" id="pqKeyList" style="margin-bottom: 15px;">
|
|
|
|
|
<div class="pq-checklist-item" id="pqKeyMlDsa44">
|
|
|
|
|
<span class="pq-checklist-box"></span>
|
|
|
|
|
<span>ML-DSA-44 (Dilithium) — FIPS 204 · Cat 2 · 1312-byte pubkey · m/44'/1237'/0'/0/1
|
|
|
|
|
<div class="pq-key-pubkey" id="pqKeyMlDsa44Pub" style="margin-top: 2px;"></div>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-key-item" id="pqKeyMlDsa65">
|
|
|
|
|
<div class="pq-key-icon"></div>
|
|
|
|
|
<div class="pq-key-details">
|
|
|
|
|
<div class="pq-key-name">ML-DSA-65 (Dilithium)</div>
|
|
|
|
|
<div class="pq-key-meta">FIPS 204 · Cat 3 · 1952-byte pubkey · m/44'/1237'/0'/0/2</div>
|
|
|
|
|
<div class="pq-key-pubkey" id="pqKeyMlDsa65Pub"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-checklist-item" id="pqKeyMlDsa65">
|
|
|
|
|
<span class="pq-checklist-box"></span>
|
|
|
|
|
<span>ML-DSA-65 (Dilithium) — FIPS 204 · Cat 3 · 1952-byte pubkey · m/44'/1237'/0'/0/2
|
|
|
|
|
<div class="pq-key-pubkey" id="pqKeyMlDsa65Pub" style="margin-top: 2px;"></div>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-key-item" id="pqKeySlhDsa">
|
|
|
|
|
<div class="pq-key-icon"></div>
|
|
|
|
|
<div class="pq-key-details">
|
|
|
|
|
<div class="pq-key-name">SLH-DSA-128s (SPHINCS+)</div>
|
|
|
|
|
<div class="pq-key-meta">FIPS 205 · Cat 1 · 32-byte pubkey · m/44'/1237'/0'/0/3+4</div>
|
|
|
|
|
<div class="pq-key-pubkey" id="pqKeySlhDsaPub"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-checklist-item" id="pqKeySlhDsa">
|
|
|
|
|
<span class="pq-checklist-box"></span>
|
|
|
|
|
<span>SLH-DSA-128s (SPHINCS+) — FIPS 205 · Cat 1 · 32-byte pubkey · m/44'/1237'/0'/0/3+4
|
|
|
|
|
<div class="pq-key-pubkey" id="pqKeySlhDsaPub" style="margin-top: 2px;"></div>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-key-item" id="pqKeyFalcon">
|
|
|
|
|
<div class="pq-key-icon"></div>
|
|
|
|
|
<div class="pq-key-details">
|
|
|
|
|
<div class="pq-key-name">Falcon-512</div>
|
|
|
|
|
<div class="pq-key-meta">FIPS 206 (draft) · Cat 1 · 897-byte pubkey · m/44'/1237'/0'/0/5+6</div>
|
|
|
|
|
<div class="pq-key-pubkey" id="pqKeyFalconPub"></div>
|
|
|
|
|
<div class="pq-checklist-item" id="pqKeyFalcon">
|
|
|
|
|
<span class="pq-checklist-box"></span>
|
|
|
|
|
<span>Falcon-512 — FIPS 206 (draft) · Cat 1 · 897-byte pubkey · m/44'/1237'/0'/0/5+6
|
|
|
|
|
<div class="pq-key-pubkey" id="pqKeyFalconPub" style="margin-top: 2px;"></div>
|
|
|
|
|
<div style="font-size: 11px; color: var(--accent-color); margin-top: 4px;">⚠ Draft standard — may need re-issuing if FIPS 206 changes</div>
|
|
|
|
|
</div>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-key-item" id="pqKeyMlKem">
|
|
|
|
|
<div class="pq-key-icon"></div>
|
|
|
|
|
<div class="pq-key-details">
|
|
|
|
|
<div class="pq-key-name">ML-KEM-768 (Kyber)</div>
|
|
|
|
|
<div class="pq-key-meta">FIPS 203 · Cat 3 · 1184-byte pubkey · m/44'/1237'/0'/0/7+8</div>
|
|
|
|
|
<div class="pq-key-pubkey" id="pqKeyMlKemPub"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-checklist-item" id="pqKeyMlKem">
|
|
|
|
|
<span class="pq-checklist-box"></span>
|
|
|
|
|
<span>ML-KEM-768 (Kyber) — FIPS 203 · Cat 3 · 1184-byte pubkey · m/44'/1237'/0'/0/7+8
|
|
|
|
|
<div class="pq-key-pubkey" id="pqKeyMlKemPub" style="margin-top: 2px;"></div>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="pqDeriveStatus"></div>
|
|
|
|
@@ -460,50 +453,6 @@
|
|
|
|
|
full kind 1 event (including its secp256k1 signature) is submitted to OpenTimestamps.
|
|
|
|
|
</div>
|
|
|
|
|
<div id="pqSignStatus"></div>
|
|
|
|
|
<div class="pq-key-list" id="pqSignChecklist">
|
|
|
|
|
<div class="pq-key-item" id="pqSignPqMlDsa44">
|
|
|
|
|
<div class="pq-key-icon"></div>
|
|
|
|
|
<div class="pq-key-details">
|
|
|
|
|
<div class="pq-key-name">PQ signature: ML-DSA-44</div>
|
|
|
|
|
<div class="pq-key-meta">Done in browser (pure JS)</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-key-item" id="pqSignPqMlDsa65">
|
|
|
|
|
<div class="pq-key-icon"></div>
|
|
|
|
|
<div class="pq-key-details">
|
|
|
|
|
<div class="pq-key-name">PQ signature: ML-DSA-65</div>
|
|
|
|
|
<div class="pq-key-meta">Done in browser (pure JS)</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-key-item" id="pqSignPqSlhDsa">
|
|
|
|
|
<div class="pq-key-icon"></div>
|
|
|
|
|
<div class="pq-key-details">
|
|
|
|
|
<div class="pq-key-name">PQ signature: SLH-DSA-128s</div>
|
|
|
|
|
<div class="pq-key-meta">Done in browser (pure JS)</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-key-item" id="pqSignPqFalcon">
|
|
|
|
|
<div class="pq-key-icon"></div>
|
|
|
|
|
<div class="pq-key-details">
|
|
|
|
|
<div class="pq-key-name">PQ signature: Falcon-512</div>
|
|
|
|
|
<div class="pq-key-meta">Done in browser (pure JS)</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-key-item" id="pqSignSecp">
|
|
|
|
|
<div class="pq-key-icon"></div>
|
|
|
|
|
<div class="pq-key-details">
|
|
|
|
|
<div class="pq-key-name">secp256k1 signature: kind 1 announcement</div>
|
|
|
|
|
<div class="pq-key-meta">Requires approval from your signer</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-key-item" id="pqSignSecpWrapper">
|
|
|
|
|
<div class="pq-key-icon"></div>
|
|
|
|
|
<div class="pq-key-details">
|
|
|
|
|
<div class="pq-key-name">secp256k1 signature: kind 11112 wrapper</div>
|
|
|
|
|
<div class="pq-key-meta">Requires approval from your signer</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="pq-button" id="pqSignBtn">Sign Event</button>
|
|
|
|
|
<div id="pqEventPreviewWrap" class="pq-hidden" style="margin-top: 15px;">
|
|
|
|
|
<div class="pq-info-text" style="margin-bottom: 5px;">Event preview:</div>
|
|
|
|
@@ -527,12 +476,17 @@
|
|
|
|
|
<div class="pq-info-text" style="margin-top: 15px; margin-bottom: 5px;"><strong>Kind 11112 wrapper event:</strong></div>
|
|
|
|
|
<div id="pqEventIdDisplay" style="margin: 5px 0; font-size: 11px; color: var(--primary-color); word-break: break-all;"></div>
|
|
|
|
|
<div class="pq-event-preview" id="pqSuccessEventPreview"></div>
|
|
|
|
|
<div class="pq-button-row" style="margin-top: 10px;">
|
|
|
|
|
<button class="pq-button pq-button-secondary" id="pqCopyKind11112Btn">Copy Kind 11112 Event</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style="margin-top: 15px;">
|
|
|
|
|
<div class="pq-info-text" style="margin-bottom: 5px;"><strong>Relays to publish to:</strong></div>
|
|
|
|
|
<input type="text" class="pq-relay-input" id="pqRelaysInput"
|
|
|
|
|
value="wss://laantungir.net/relay,wss://relay.damus.io,wss://nos.lol,wss://relay.primal.net"
|
|
|
|
|
placeholder="wss://relay1.com,wss://relay2.com" />
|
|
|
|
|
<div id="pqRelayList" style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px;"></div>
|
|
|
|
|
<div class="pq-button-row">
|
|
|
|
|
<input type="text" class="pq-relay-input" id="pqRelayAddInput" style="flex: 1; margin: 0;" placeholder="wss://relay.example.com" />
|
|
|
|
|
<button class="pq-button pq-button-secondary" id="pqRelayAddBtn" style="flex: 0 0 auto; width: auto; padding: 12px 20px;">Add Relay</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="pqPublishStatus"></div>
|
|
|
|
@@ -540,6 +494,9 @@
|
|
|
|
|
<div class="pq-button-row" style="margin-top: 15px;">
|
|
|
|
|
<button class="pq-button" id="pqPublishBtn">Publish Both Events to Relays</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-button-row">
|
|
|
|
|
<button class="pq-button pq-button-secondary pq-hidden" id="pqOtsContinueBtn">Continue to OpenTimestamps</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-button-row">
|
|
|
|
|
<button class="pq-button pq-button-secondary" id="pqDoneBtn">Done</button>
|
|
|
|
|
</div>
|
|
|
|
@@ -549,47 +506,41 @@
|
|
|
|
|
<div id="pqOtsStep" class="pq-card pq-hidden">
|
|
|
|
|
<div class="pq-card-title">OpenTimestamps Attestation</div>
|
|
|
|
|
<div class="pq-info-text">
|
|
|
|
|
The NIP-QR event is being timestamped on the Bitcoin blockchain via OpenTimestamps.
|
|
|
|
|
This proves the event existed at this point in time, preventing a future quantum attacker
|
|
|
|
|
from backdating a fraudulent event. Bitcoin confirmation typically takes 10-30 minutes.
|
|
|
|
|
Your event hash has already been submitted to OpenTimestamps in the previous step, and a
|
|
|
|
|
<strong>pending</strong> timestamp proof is embedded in the kind 11112 event you just published.
|
|
|
|
|
OpenTimestamps anchors that hash into the Bitcoin blockchain, proving your event existed at this
|
|
|
|
|
point in time — preventing a future quantum attacker from backdating a fraudulent event.
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-button-row" id="pqOtsSubmitButtonRow">
|
|
|
|
|
<button class="pq-button" id="pqOtsStartBtn">Submit to OpenTimestamps</button>
|
|
|
|
|
<div class="pq-info-text">
|
|
|
|
|
Bitcoin confirmation typically takes <strong>10-30 minutes</strong>. You can leave this page open
|
|
|
|
|
to watch the progress below — the polling log updates every minute. Once the Bitcoin attestation is
|
|
|
|
|
confirmed, this page will automatically re-sign and republish the kind 11112 event with the upgraded
|
|
|
|
|
proof. You're free to close the page; the workflow resumes on your next visit.
|
|
|
|
|
</div>
|
|
|
|
|
<div id="pqOtsStatus"></div>
|
|
|
|
|
|
|
|
|
|
<!-- Polling log window -->
|
|
|
|
|
<div class="pq-info-text" style="margin-top: 10px; margin-bottom: 5px;">Polling log:</div>
|
|
|
|
|
<div id="pqOtsLog" style="background: var(--primary-color); color: var(--secondary-color); border-radius: var(--border-radius); padding: 10px; font-size: 11px; font-family: monospace; height: 120px; overflow-y: auto; text-align: left; margin-bottom: 15px;"></div>
|
|
|
|
|
<div id="pqOtsLog" style="background: var(--secondary-color); color: var(--primary-color); border: var(--border); border-radius: var(--border-radius); padding: 10px; font-size: 11px; font-family: monospace; height: 120px; overflow-y: auto; text-align: left; margin-bottom: 15px;"></div>
|
|
|
|
|
|
|
|
|
|
<div class="pq-key-list">
|
|
|
|
|
<div class="pq-key-item" id="pqOtsSubmit">
|
|
|
|
|
<div class="pq-key-icon"></div>
|
|
|
|
|
<div class="pq-key-details">
|
|
|
|
|
<div class="pq-key-name">Submit hash to OpenTimestamps</div>
|
|
|
|
|
<div class="pq-key-meta">Creates a complete pending detached .ots proof</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-checklist" style="margin-bottom: 15px;">
|
|
|
|
|
<div class="pq-checklist-item pq-done" id="pqOtsSubmit">
|
|
|
|
|
<span class="pq-checklist-box">✓</span>
|
|
|
|
|
<span>Submit hash to OpenTimestamps — pending .ots proof created</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-key-item" id="pqOtsPendingPublish">
|
|
|
|
|
<div class="pq-key-icon"></div>
|
|
|
|
|
<div class="pq-key-details">
|
|
|
|
|
<div class="pq-key-name">Pending proof embedded in kind 11112 event</div>
|
|
|
|
|
<div class="pq-key-meta">The pending .ots proof is embedded as a tag in the initial event</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-checklist-item pq-done" id="pqOtsPendingPublish">
|
|
|
|
|
<span class="pq-checklist-box">✓</span>
|
|
|
|
|
<span>Pending proof embedded in kind 11112 event</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-key-item" id="pqOtsConfirm">
|
|
|
|
|
<div class="pq-key-icon"></div>
|
|
|
|
|
<div class="pq-key-details">
|
|
|
|
|
<div class="pq-key-name">Wait for Bitcoin confirmation</div>
|
|
|
|
|
<div class="pq-key-meta" id="pqOtsConfirmDetail">Polling every 60 seconds...</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-checklist-item pq-active" id="pqOtsConfirm">
|
|
|
|
|
<span class="pq-checklist-box"></span>
|
|
|
|
|
<span>Wait for Bitcoin confirmation
|
|
|
|
|
<div class="pq-key-meta" id="pqOtsConfirmDetail" style="margin-top: 2px;">Polling every 60 seconds...</div>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-key-item" id="pqOtsConfirmedPublish">
|
|
|
|
|
<div class="pq-key-icon"></div>
|
|
|
|
|
<div class="pq-key-details">
|
|
|
|
|
<div class="pq-key-name">Republish kind 11112 with confirmed proof</div>
|
|
|
|
|
<div class="pq-key-meta">Republishes the event with the upgraded Bitcoin-attested proof</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pq-checklist-item" id="pqOtsConfirmedPublish">
|
|
|
|
|
<span class="pq-checklist-box"></span>
|
|
|
|
|
<span>Republish kind 11112 with confirmed proof — automatic on confirmation</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="pqOtsProofWrap" class="pq-hidden" style="margin-top: 15px;">
|
|
|
|
@@ -766,7 +717,7 @@
|
|
|
|
|
console.log('[ots] Found saved OTS workflow from previous session, event:', pendingOts.eventId);
|
|
|
|
|
pendingOtsBytes = pendingOts.ots;
|
|
|
|
|
pqEvent = { id: pendingOts.eventId, kind: pendingOts.targetKind || NIP_QR_KIND };
|
|
|
|
|
if (pendingOts.relayUrls) document.getElementById('pqRelaysInput').value = pendingOts.relayUrls;
|
|
|
|
|
if (pendingOts.relayUrls) setRelayUrls(pendingOts.relayUrls);
|
|
|
|
|
|
|
|
|
|
// F-H3: Build the OTS resume notice with safe DOM construction
|
|
|
|
|
const otsNotice = document.createElement('div');
|
|
|
|
@@ -802,8 +753,16 @@
|
|
|
|
|
|
|
|
|
|
function setKeyIcon(elementId, icon) {
|
|
|
|
|
const el = document.getElementById(elementId);
|
|
|
|
|
const iconEl = el.querySelector('.pq-key-icon');
|
|
|
|
|
iconEl.textContent = icon;
|
|
|
|
|
if (!el) return;
|
|
|
|
|
// Supports both the old .pq-key-icon elements and the new checklist
|
|
|
|
|
// .pq-checklist-box elements (used in Step 3 and Step 6).
|
|
|
|
|
const iconEl = el.querySelector('.pq-key-icon') || el.querySelector('.pq-checklist-box');
|
|
|
|
|
if (iconEl) iconEl.textContent = icon;
|
|
|
|
|
if (icon && el.classList.contains('pq-checklist-item')) {
|
|
|
|
|
el.classList.add('pq-done');
|
|
|
|
|
} else if (!icon && el.classList.contains('pq-checklist-item')) {
|
|
|
|
|
el.classList.remove('pq-done');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ================================================================
|
|
|
|
@@ -1077,10 +1036,6 @@
|
|
|
|
|
setStatus(pqSignStatus, 'info', ' Building kind 1 announcement event...');
|
|
|
|
|
const kind1Template = buildKind1Announcement(currentPubkey, blockHeight, pqKeys);
|
|
|
|
|
|
|
|
|
|
setKeyIcon('pqSignPqMlDsa44', 'Done');
|
|
|
|
|
setKeyIcon('pqSignPqMlDsa65', 'Done');
|
|
|
|
|
setKeyIcon('pqSignPqSlhDsa', 'Done');
|
|
|
|
|
setKeyIcon('pqSignPqFalcon', 'Done');
|
|
|
|
|
setStatus(pqSignStatus, 'info', 'PQ signatures complete. Requesting secp256k1 signature for kind 1 announcement...');
|
|
|
|
|
|
|
|
|
|
const kind1Signed = await window.nostr.signEvent(kind1Template);
|
|
|
|
@@ -1124,8 +1079,6 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const kind11112Signed = await window.nostr.signEvent(kind11112Template);
|
|
|
|
|
setKeyIcon('pqSignSecp', 'Done');
|
|
|
|
|
setKeyIcon('pqSignSecpWrapper', 'Done');
|
|
|
|
|
|
|
|
|
|
pqEvent = {
|
|
|
|
|
id: kind11112Signed.id,
|
|
|
|
@@ -1156,7 +1109,7 @@
|
|
|
|
|
document.getElementById('pqKind1Preview').textContent = JSON.stringify(kind1Event, null, 2);
|
|
|
|
|
pqEventIdDisplay.textContent = `Event ID: ${pqEvent.id}`;
|
|
|
|
|
document.getElementById('pqSuccessEventPreview').textContent = eventJsonStr;
|
|
|
|
|
setTimeout(() => showView('pqPublishStep'), 1000);
|
|
|
|
|
setTimeout(() => { renderRelayList(); showView('pqPublishStep'); }, 1000);
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[post-quantum] Sign failed:', error);
|
|
|
|
@@ -1196,6 +1149,112 @@
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ================================================================
|
|
|
|
|
RELAY LIST MANAGEMENT
|
|
|
|
|
================================================================ */
|
|
|
|
|
const DEFAULT_RELAYS = [
|
|
|
|
|
'wss://laantungir.net/relay',
|
|
|
|
|
'wss://relay.damus.io',
|
|
|
|
|
'wss://nos.lol',
|
|
|
|
|
'wss://relay.primal.net'
|
|
|
|
|
];
|
|
|
|
|
let pqRelays = DEFAULT_RELAYS.slice();
|
|
|
|
|
|
|
|
|
|
// Per-relay publish status: { url: { kind1: bool, kind11112: bool } }
|
|
|
|
|
const pqRelayStatus = {};
|
|
|
|
|
|
|
|
|
|
function makeStatusCheckbox(label) {
|
|
|
|
|
const wrap = document.createElement('label');
|
|
|
|
|
wrap.style.cssText = 'display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--muted-color); cursor: default; flex: 0 0 auto;';
|
|
|
|
|
const box = document.createElement('span');
|
|
|
|
|
box.className = 'pq-checklist-box';
|
|
|
|
|
box.textContent = '';
|
|
|
|
|
box.style.cssText = 'width: 16px; height: 16px; border: 2px solid var(--muted-color); border-radius: 4px; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; font-weight: bold;';
|
|
|
|
|
const lbl = document.createElement('span');
|
|
|
|
|
lbl.textContent = label;
|
|
|
|
|
wrap.appendChild(box);
|
|
|
|
|
wrap.appendChild(lbl);
|
|
|
|
|
return { wrap, box };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setRelayStatus(url, kind, ok) {
|
|
|
|
|
if (!pqRelayStatus[url]) pqRelayStatus[url] = { kind1: false, kind11112: false };
|
|
|
|
|
pqRelayStatus[url][kind] = ok;
|
|
|
|
|
const row = document.querySelector(`.pq-relay-row[data-relay="${CSS.escape(url)}"]`);
|
|
|
|
|
if (!row) return;
|
|
|
|
|
const box = row.querySelector(`.pq-relay-status-${kind}`);
|
|
|
|
|
if (box) {
|
|
|
|
|
box.textContent = ok ? '✓' : '';
|
|
|
|
|
box.style.background = ok ? 'var(--accent-color)' : 'transparent';
|
|
|
|
|
box.style.color = ok ? 'var(--secondary-color)' : 'var(--primary-color)';
|
|
|
|
|
box.style.borderColor = ok ? 'var(--accent-color)' : 'var(--muted-color)';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderRelayList() {
|
|
|
|
|
const listEl = document.getElementById('pqRelayList');
|
|
|
|
|
if (!listEl) return;
|
|
|
|
|
listEl.innerHTML = '';
|
|
|
|
|
pqRelays.forEach((url, idx) => {
|
|
|
|
|
const row = document.createElement('div');
|
|
|
|
|
row.className = 'pq-relay-row';
|
|
|
|
|
row.dataset.relay = url;
|
|
|
|
|
row.style.cssText = 'display: flex; align-items: center; gap: 10px; background: var(--secondary-color); border: var(--border); border-radius: var(--border-radius); padding: 10px 14px;';
|
|
|
|
|
|
|
|
|
|
const urlSpan = document.createElement('span');
|
|
|
|
|
urlSpan.textContent = url;
|
|
|
|
|
urlSpan.style.cssText = 'flex: 1; font-size: 14px; color: var(--primary-color); word-break: break-all;';
|
|
|
|
|
row.appendChild(urlSpan);
|
|
|
|
|
|
|
|
|
|
const k1 = makeStatusCheckbox('k1');
|
|
|
|
|
k1.box.classList.add('pq-relay-status-kind1');
|
|
|
|
|
row.appendChild(k1.wrap);
|
|
|
|
|
|
|
|
|
|
const k11112 = makeStatusCheckbox('k11112');
|
|
|
|
|
k11112.box.classList.add('pq-relay-status-kind11112');
|
|
|
|
|
row.appendChild(k11112.wrap);
|
|
|
|
|
|
|
|
|
|
const removeBtn = document.createElement('button');
|
|
|
|
|
removeBtn.className = 'pq-button pq-button-secondary';
|
|
|
|
|
removeBtn.textContent = 'Remove';
|
|
|
|
|
removeBtn.style.cssText = 'flex: 0 0 auto; width: auto; padding: 8px 16px; font-size: 13px;';
|
|
|
|
|
removeBtn.addEventListener('click', () => {
|
|
|
|
|
pqRelays.splice(idx, 1);
|
|
|
|
|
delete pqRelayStatus[url];
|
|
|
|
|
renderRelayList();
|
|
|
|
|
});
|
|
|
|
|
row.appendChild(removeBtn);
|
|
|
|
|
|
|
|
|
|
listEl.appendChild(row);
|
|
|
|
|
|
|
|
|
|
// Restore any existing status
|
|
|
|
|
const st = pqRelayStatus[url];
|
|
|
|
|
if (st) {
|
|
|
|
|
if (st.kind1) setRelayStatus(url, 'kind1', true);
|
|
|
|
|
if (st.kind11112) setRelayStatus(url, 'kind11112', true);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getRelayUrls() {
|
|
|
|
|
return pqRelays.slice();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setRelayUrls(urls) {
|
|
|
|
|
// Accept either a comma-separated string or an array
|
|
|
|
|
if (typeof urls === 'string') {
|
|
|
|
|
pqRelays = urls.split(',').map(s => s.trim()).filter(Boolean);
|
|
|
|
|
} else if (Array.isArray(urls)) {
|
|
|
|
|
pqRelays = urls.slice();
|
|
|
|
|
}
|
|
|
|
|
if (pqRelays.length === 0) pqRelays = DEFAULT_RELAYS.slice();
|
|
|
|
|
renderRelayList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getRelayUrlsString() {
|
|
|
|
|
return pqRelays.join(',');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ================================================================
|
|
|
|
|
EVENT LISTENERS
|
|
|
|
|
================================================================ */
|
|
|
|
@@ -1255,19 +1314,6 @@
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// "Generate New Seed" — generates a fresh seed using pure CSPRNG (no user entropy)
|
|
|
|
|
document.getElementById('pqRegenerateBtn').addEventListener('click', () => {
|
|
|
|
|
userEntropyChunks = [];
|
|
|
|
|
generateAndShowSeed();
|
|
|
|
|
// Reset entropy UI
|
|
|
|
|
const entropyBar = document.getElementById('pqEntropyBar');
|
|
|
|
|
const entropyHint = document.getElementById('pqEntropyHint');
|
|
|
|
|
const regenWithEntropyBtn = document.getElementById('pqRegenerateWithEntropyBtn');
|
|
|
|
|
if (entropyBar) entropyBar.style.width = '0%';
|
|
|
|
|
if (entropyHint) entropyHint.textContent = 'Optional: click here and type random characters, move your mouse to add entropy...';
|
|
|
|
|
if (regenWithEntropyBtn) { regenWithEntropyBtn.classList.add('pq-hidden'); regenWithEntropyBtn.disabled = true; }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// "Generate with Extra Entropy" — generates a fresh seed mixing CSPRNG + user entropy
|
|
|
|
|
document.getElementById('pqRegenerateWithEntropyBtn').addEventListener('click', () => { generateAndShowSeed(); });
|
|
|
|
|
|
|
|
|
@@ -1283,10 +1329,11 @@
|
|
|
|
|
setStepActive(5);
|
|
|
|
|
publishStatus.innerHTML = '<div class="pq-status pq-status-info">Publishing kind 1 announcement to relays...</div>';
|
|
|
|
|
try {
|
|
|
|
|
const relayUrls = document.getElementById('pqRelaysInput').value.split(',').map(s => s.trim()).filter(s => s);
|
|
|
|
|
const relayUrls = getRelayUrls();
|
|
|
|
|
|
|
|
|
|
// Publish kind 1 announcement first
|
|
|
|
|
const results1 = await publishToRelays(kind1Event, relayUrls);
|
|
|
|
|
results1.forEach(r => setRelayStatus(r.relay, 'kind1', r.success));
|
|
|
|
|
const success1 = results1.filter(r => r.success).length;
|
|
|
|
|
const fail1 = results1.filter(r => !r.success).length;
|
|
|
|
|
otsLog(`Kind 1 announcement published: ${success1} ok, ${fail1} failed.`);
|
|
|
|
@@ -1294,15 +1341,18 @@
|
|
|
|
|
// Publish kind 11112 wrapper
|
|
|
|
|
publishStatus.innerHTML = '<div class="pq-status pq-status-info">Publishing kind 11112 wrapper to relays...</div>';
|
|
|
|
|
const results2 = await publishToRelays(pqEvent, relayUrls);
|
|
|
|
|
results2.forEach(r => setRelayStatus(r.relay, 'kind11112', r.success));
|
|
|
|
|
const success2 = results2.filter(r => r.success).length;
|
|
|
|
|
const fail2 = results2.filter(r => !r.success).length;
|
|
|
|
|
otsLog(`Kind 11112 wrapper published: ${success2} ok, ${fail2} failed.`);
|
|
|
|
|
|
|
|
|
|
setStatus(publishStatus, 'success', `Published! Kind 1: ${success1}/${relayUrls.length} relays. Kind 11112: ${success2}/${relayUrls.length} relays.`);
|
|
|
|
|
publishBtn.textContent = 'Published';
|
|
|
|
|
publishBtn.disabled = true;
|
|
|
|
|
setStepDone(5);
|
|
|
|
|
// Present the OpenTimestamps approval step; submission starts on user approval.
|
|
|
|
|
prepareOtsStep();
|
|
|
|
|
// Show a Continue button so the user can review the relay results before advancing.
|
|
|
|
|
const continueBtn = document.getElementById('pqOtsContinueBtn');
|
|
|
|
|
if (continueBtn) continueBtn.classList.remove('pq-hidden');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
setStatus(publishStatus, 'error', `Publish failed: ${error.message}`);
|
|
|
|
|
publishBtn.disabled = false;
|
|
|
|
@@ -1311,6 +1361,43 @@
|
|
|
|
|
|
|
|
|
|
document.getElementById('pqDoneBtn').addEventListener('click', () => { showAuthedView(); });
|
|
|
|
|
|
|
|
|
|
// Continue to OpenTimestamps step (shown after successful publish)
|
|
|
|
|
document.getElementById('pqOtsContinueBtn').addEventListener('click', () => { prepareOtsStep(); });
|
|
|
|
|
|
|
|
|
|
// Add Relay button
|
|
|
|
|
document.getElementById('pqRelayAddBtn').addEventListener('click', () => {
|
|
|
|
|
const input = document.getElementById('pqRelayAddInput');
|
|
|
|
|
const url = input.value.trim();
|
|
|
|
|
if (!url) return;
|
|
|
|
|
if (!/^wss?:\/\//.test(url)) {
|
|
|
|
|
setStatus(document.getElementById('pqPublishStatus'), 'error', 'Relay URL must start with wss:// or ws://');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (pqRelays.includes(url)) {
|
|
|
|
|
setStatus(document.getElementById('pqPublishStatus'), 'error', 'That relay is already in the list.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
pqRelays.push(url);
|
|
|
|
|
input.value = '';
|
|
|
|
|
renderRelayList();
|
|
|
|
|
document.getElementById('pqPublishStatus').innerHTML = '';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Allow pressing Enter in the add-relay input to add the relay
|
|
|
|
|
document.getElementById('pqRelayAddInput').addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.key === 'Enter') { e.preventDefault(); document.getElementById('pqRelayAddBtn').click(); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Copy Kind 11112 Event button
|
|
|
|
|
document.getElementById('pqCopyKind11112Btn').addEventListener('click', () => {
|
|
|
|
|
if (!pqEvent) return;
|
|
|
|
|
navigator.clipboard.writeText(JSON.stringify(pqEvent, null, 2)).then(() => {
|
|
|
|
|
const btn = document.getElementById('pqCopyKind11112Btn');
|
|
|
|
|
btn.textContent = 'Copied!';
|
|
|
|
|
setTimeout(() => { btn.textContent = 'Copy Kind 11112 Event'; }, 2000);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Sign out button
|
|
|
|
|
const pqSignOutBtn = document.getElementById('pqSignOutBtn');
|
|
|
|
|
pqSignOutBtn.addEventListener('click', async () => {
|
|
|
|
@@ -1334,6 +1421,7 @@
|
|
|
|
|
userEntropyChunks = [];
|
|
|
|
|
pendingOtsBytes = null;
|
|
|
|
|
currentBlockHeight = 0;
|
|
|
|
|
pqRelays = DEFAULT_RELAYS.slice();
|
|
|
|
|
// Clear auth/session storage, then restore only the OTS workflow record.
|
|
|
|
|
try {
|
|
|
|
|
localStorage.clear();
|
|
|
|
@@ -1370,32 +1458,16 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function prepareOtsStep() {
|
|
|
|
|
showView('pqOtsStep');
|
|
|
|
|
setStepActive(6);
|
|
|
|
|
const otsStatus = document.getElementById('pqOtsStatus');
|
|
|
|
|
const otsLogEl = document.getElementById('pqOtsLog');
|
|
|
|
|
const startBtn = document.getElementById('pqOtsStartBtn');
|
|
|
|
|
if (otsLogEl) otsLogEl.innerHTML = '';
|
|
|
|
|
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Review this step, then submit the NIP-QR event hash to OpenTimestamps.</div>';
|
|
|
|
|
startBtn.disabled = false;
|
|
|
|
|
startBtn.textContent = 'Submit to OpenTimestamps';
|
|
|
|
|
document.getElementById('pqOtsSubmitButtonRow').classList.remove('pq-hidden');
|
|
|
|
|
setKeyIcon('pqOtsSubmit', '');
|
|
|
|
|
setKeyIcon('pqOtsPendingPublish', '');
|
|
|
|
|
setKeyIcon('pqOtsConfirm', '');
|
|
|
|
|
setKeyIcon('pqOtsConfirmedPublish', '');
|
|
|
|
|
otsLog('Waiting for user approval to submit the event hash.');
|
|
|
|
|
// The OTS submission already happened in step 4. Auto-start the polling flow.
|
|
|
|
|
startOtsFlow();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function startOtsFlow() {
|
|
|
|
|
showView('pqOtsStep');
|
|
|
|
|
setStepActive(6);
|
|
|
|
|
const otsStatus = document.getElementById('pqOtsStatus');
|
|
|
|
|
const startBtn = document.getElementById('pqOtsStartBtn');
|
|
|
|
|
startBtn.disabled = true;
|
|
|
|
|
startBtn.textContent = 'Processing...';
|
|
|
|
|
setKeyIcon('pqOtsSubmit', '');
|
|
|
|
|
setKeyIcon('pqOtsPendingPublish', '');
|
|
|
|
|
const otsLogEl = document.getElementById('pqOtsLog');
|
|
|
|
|
if (otsLogEl) otsLogEl.innerHTML = '';
|
|
|
|
|
setKeyIcon('pqOtsConfirm', '');
|
|
|
|
|
setKeyIcon('pqOtsConfirmedPublish', '');
|
|
|
|
|
|
|
|
|
@@ -1409,7 +1481,6 @@
|
|
|
|
|
if (existing && existing.eventId === pqEvent.id) {
|
|
|
|
|
pendingOtsBytes = existing.ots;
|
|
|
|
|
otsLog(`Loaded saved OTS workflow for event ${pqEvent.id.substring(0, 16)}...`);
|
|
|
|
|
setKeyIcon('pqOtsSubmit', 'Done');
|
|
|
|
|
if (existing.pendingPublished) setKeyIcon('pqOtsPendingPublish', 'Done');
|
|
|
|
|
if (existing.confirmedPublished) {
|
|
|
|
|
setKeyIcon('pqOtsConfirm', 'Done');
|
|
|
|
@@ -1418,13 +1489,11 @@
|
|
|
|
|
setStepActive(7);
|
|
|
|
|
otsStatus.innerHTML = '<div class="pq-status pq-status-success">Confirmed timestamp proof was already published.</div>';
|
|
|
|
|
showOtsProof(pendingOtsBytes);
|
|
|
|
|
startBtn.textContent = 'Completed';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
showOtsProof(pendingOtsBytes);
|
|
|
|
|
setKeyIcon('pqOtsPendingPublish', 'Done');
|
|
|
|
|
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Pending proof is embedded in the kind 11112 event. Waiting for Bitcoin confirmation...</div>';
|
|
|
|
|
startBtn.textContent = 'Submitted';
|
|
|
|
|
startOtsPolling();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@@ -1437,18 +1506,16 @@
|
|
|
|
|
pendingOtsBytes = base64ToBytes(otsTag[1]);
|
|
|
|
|
if (isDetachedOtsFile(pendingOtsBytes)) {
|
|
|
|
|
otsLog(`Found pending OTS proof embedded in event (${pendingOtsBytes.length} bytes).`);
|
|
|
|
|
setKeyIcon('pqOtsSubmit', 'Done');
|
|
|
|
|
showOtsProof(pendingOtsBytes);
|
|
|
|
|
savePendingOts(pqEvent.id, pendingOtsBytes, {
|
|
|
|
|
ownerPubkey: currentPubkey,
|
|
|
|
|
targetKind: pqEvent.kind,
|
|
|
|
|
relayUrls: document.getElementById('pqRelaysInput').value,
|
|
|
|
|
relayUrls: getRelayUrlsString(),
|
|
|
|
|
pendingPublished: true,
|
|
|
|
|
confirmedPublished: false
|
|
|
|
|
});
|
|
|
|
|
setKeyIcon('pqOtsPendingPublish', 'Done');
|
|
|
|
|
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Pending proof is embedded in the kind 11112 event. Waiting for Bitcoin confirmation (polling every 60 seconds)...</div>';
|
|
|
|
|
startBtn.textContent = 'Submitted';
|
|
|
|
|
startOtsPolling();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
@@ -1463,8 +1530,6 @@
|
|
|
|
|
if (!sha256Tag || !sha256Tag[1]) {
|
|
|
|
|
otsLog('ERROR: No sha256 tag found in event. Cannot timestamp.');
|
|
|
|
|
otsStatus.innerHTML = '<div class="pq-status pq-status-error">Event is missing the sha256 tag.</div>';
|
|
|
|
|
startBtn.disabled = false;
|
|
|
|
|
startBtn.textContent = 'Retry';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const fullHash = sha256Tag[1];
|
|
|
|
@@ -1475,7 +1540,6 @@
|
|
|
|
|
const otsBytes = await timestampEvent(fullHash);
|
|
|
|
|
pendingOtsBytes = otsBytes;
|
|
|
|
|
otsLog(`Created pending detached .ots file (${otsBytes.length} bytes).`);
|
|
|
|
|
setKeyIcon('pqOtsSubmit', 'Done');
|
|
|
|
|
showOtsProof(otsBytes);
|
|
|
|
|
|
|
|
|
|
// Republish the event with the OTS proof embedded
|
|
|
|
@@ -1485,19 +1549,16 @@
|
|
|
|
|
savePendingOts(pqEvent.id, otsBytes, {
|
|
|
|
|
ownerPubkey: currentPubkey,
|
|
|
|
|
targetKind: pqEvent.kind,
|
|
|
|
|
relayUrls: document.getElementById('pqRelaysInput').value,
|
|
|
|
|
relayUrls: getRelayUrlsString(),
|
|
|
|
|
pendingPublished: true,
|
|
|
|
|
confirmedPublished: false
|
|
|
|
|
});
|
|
|
|
|
otsStatus.innerHTML = '<div class="pq-status pq-status-info">Pending proof embedded. Waiting for Bitcoin confirmation (polling every 60 seconds)...</div>';
|
|
|
|
|
startBtn.textContent = 'Submitted';
|
|
|
|
|
startOtsPolling();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[ots] Submission failed:', error);
|
|
|
|
|
otsLog(`ERROR: ${error.message}`);
|
|
|
|
|
setStatus(otsStatus, 'error', `OpenTimestamps workflow failed: ${error.message}`);
|
|
|
|
|
startBtn.disabled = false;
|
|
|
|
|
startBtn.textContent = 'Retry OpenTimestamps Submission';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1549,11 +1610,19 @@
|
|
|
|
|
// Structural check found a Bitcoin attestation tag but full
|
|
|
|
|
// verification failed — log the errors for diagnosis.
|
|
|
|
|
const errs = verifyResult.errors.length > 0 ? ` Errors: ${verifyResult.errors.join('; ')}` : '';
|
|
|
|
|
otsLog(`Poll ${pollCount}: Bitcoin attestation tag found but verification failed.${errs} Will retry.`);
|
|
|
|
|
const detail = result.detail ? ` (${result.detail.replace(/\s+/g, ' ').slice(0, 180)})` : '';
|
|
|
|
|
confirmDetail.textContent = `Attestation found but unverified (attempt ${pollCount}). Retrying in 60s...`;
|
|
|
|
|
otsLog(`Poll ${pollCount}: Bitcoin attestation tag found but verification incomplete.${errs} Will retry.`);
|
|
|
|
|
confirmDetail.textContent = `Attestation found, verifying (attempt ${pollCount}). Retrying in 60s...`;
|
|
|
|
|
} else {
|
|
|
|
|
const detail = result.detail ? ` (${result.detail.replace(/\s+/g, ' ').slice(0, 180)})` : '';
|
|
|
|
|
// Still pending — the upgrade helper's stderr often contains
|
|
|
|
|
// "Failed! Timestamp not complete" which is normal pending output
|
|
|
|
|
// from ots-cli, not an actual error. Sanitize it for display.
|
|
|
|
|
const rawDetail = (result.detail || '').replace(/\s+/g, ' ').trim();
|
|
|
|
|
const friendlyDetail = rawDetail
|
|
|
|
|
.replace(/Failed!\s*Timestamp not complete\.?\s*/gi, '')
|
|
|
|
|
.replace(/Failed!/gi, 'Pending')
|
|
|
|
|
.replace(/Pending confirmation in Bitcoin blockchain/gi, 'Waiting for Bitcoin confirmation')
|
|
|
|
|
.trim();
|
|
|
|
|
const detail = friendlyDetail ? ` (${friendlyDetail.slice(0, 180)})` : '';
|
|
|
|
|
otsLog(`Poll ${pollCount}: Still pending${detail}. Next poll in 60 seconds.`);
|
|
|
|
|
confirmDetail.textContent = `Still pending (attempt ${pollCount}). Next poll in 60s...`;
|
|
|
|
|
}
|
|
|
|
@@ -1572,7 +1641,7 @@
|
|
|
|
|
try {
|
|
|
|
|
otsLog('Building upgraded kind 11112 event with confirmed OTS proof...');
|
|
|
|
|
const savedWorkflow = loadPendingOts();
|
|
|
|
|
const configuredRelays = document.getElementById('pqRelaysInput').value || (savedWorkflow && savedWorkflow.relayUrls) || 'wss://laantungir.net/relay';
|
|
|
|
|
const configuredRelays = getRelayUrlsString() || (savedWorkflow && savedWorkflow.relayUrls) || 'wss://laantungir.net/relay';
|
|
|
|
|
|
|
|
|
|
// Build the upgraded event: copy all tags from the original, replace the ots tag
|
|
|
|
|
const upgradedTemplate = buildUpgradedEvent(pqEvent, upgradedOtsBytes);
|
|
|
|
@@ -1610,8 +1679,6 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OTS event listeners
|
|
|
|
|
document.getElementById('pqOtsStartBtn').addEventListener('click', () => { startOtsFlow(); });
|
|
|
|
|
|
|
|
|
|
document.getElementById('pqOtsDownloadBtn').addEventListener('click', () => {
|
|
|
|
|
if (pendingOtsBytes) {
|
|
|
|
|
const blob = new Blob([pendingOtsBytes], { type: 'application/octet-stream' });
|
|
|
|
|