feat: add Verified Dev Contact badge + /verify route for signed messages
- Add 'Verified Dev Contact' badge on the address page when the address
matches the official dev contact (TRsiRzkMWm87ZuWFwPB8YXFGYr5AQZo7fb).
Badge includes the signed message + signature, collapsible by default.
- Add /verify page with a form that takes address + signature + message
and calls the daemon's verifymessage RPC to re-verify.
- Add /api/verify server endpoint (POST) that calls the daemon's
verifymessage via JSON-RPC and returns { valid: boolean, error? }.
- Add 'Verify' link to the main nav.
Dev contact proof (verified on DNS2 v5.9.23 mainnet 2026-06-23):
- Address: TRsiRzkMWm87ZuWFwPB8YXFGYr5AQZo7fb
- Message: 'ROGER THAT, TRIANGLES DEV ADDRESS IS A GO.\n\n5.9.23 IS LIVE.'
- Signature: H/gT/bFSL+WFT4F4WYPvDIVAnt0/M2WQy/ypUvhMtdXgAop+Euycakif9QERNcUfPLNF29vDxuXZf1BJjd8Snro=
This commit is contained in:
@@ -1 +1,6 @@
|
|||||||
TRIANGLES_API_URL=http://127.0.0.1:19112
|
TRIANGLES_API_URL=http://127.0.0.1:19112
|
||||||
|
|
||||||
|
# JSON-RPC credentials for the /api/verify endpoint (daemon's verifymessage)
|
||||||
|
TRIANGLES_RPC_URL=http://127.0.0.1:19112
|
||||||
|
TRIANGLES_RPC_USER=trianglesrpc
|
||||||
|
TRIANGLES_RPC_PASSWORD=
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
{ href: '/nodes', label: 'Nodes' },
|
{ href: '/nodes', label: 'Nodes' },
|
||||||
{ href: '/richlist', label: 'Rich List' },
|
{ href: '/richlist', label: 'Rich List' },
|
||||||
{ href: '/mempool', label: 'Mempool' },
|
{ href: '/mempool', label: 'Mempool' },
|
||||||
|
{ href: '/verify', label: 'Verify' },
|
||||||
{ href: '/api', label: 'API' }
|
{ href: '/api', label: 'API' }
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,7 +3,19 @@
|
|||||||
import QRCode from '$lib/components/QRCode.svelte';
|
import QRCode from '$lib/components/QRCode.svelte';
|
||||||
import { formatAmount, truncateHash, formatNumber } from '$lib/utils';
|
import { formatAmount, truncateHash, formatNumber } from '$lib/utils';
|
||||||
|
|
||||||
|
// Verified dev contact — populated on 2026-06-23. The signature is
|
||||||
|
// hardcoded and matches the proof published on the project website.
|
||||||
|
// To rotate, generate a new address in TrianglesQt, sign the same
|
||||||
|
// message text, and replace the three fields below.
|
||||||
|
const DEV_CONTACT = {
|
||||||
|
address: 'TRsiRzkMWm87ZuWFwPB8YXFGYr5AQZo7fb',
|
||||||
|
message: 'ROGER THAT, TRIANGLES DEV ADDRESS IS A GO.\n\n5.9.23 IS LIVE.',
|
||||||
|
signature:
|
||||||
|
'H/gT/bFSL+WFT4F4WYPvDIVAnt0/M2WQy/ypUvhMtdXgAop+Euycakif9QERNcUfPLNF29vDxuXZf1BJjd8Snro='
|
||||||
|
};
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
const isDevContact = data.address === DEV_CONTACT.address;
|
||||||
|
|
||||||
// Copy address to clipboard
|
// Copy address to clipboard
|
||||||
let copied = $state(false);
|
let copied = $state(false);
|
||||||
@@ -20,6 +32,40 @@
|
|||||||
<meta name="description" content="Triangles address {data.address} - balance, transactions, and QR code" />
|
<meta name="description" content="Triangles address {data.address} - balance, transactions, and QR code" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
{#if isDevContact}
|
||||||
|
<!-- Verified Dev Contact badge -->
|
||||||
|
<div class="mb-6 bg-tri-green/10 border-2 border-tri-green/40 rounded-lg p-5">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<span class="text-tri-green text-2xl">✓</span>
|
||||||
|
<h2 class="text-white font-semibold text-lg">Verified Dev Contact</h2>
|
||||||
|
</div>
|
||||||
|
<p class="text-tri-muted text-sm mb-3">
|
||||||
|
This address is cryptographically verified as the official Triangles developer contact address. The
|
||||||
|
signature below was produced by the private key for this address and can be re-verified by anyone
|
||||||
|
using <code class="bg-tri-surface px-1 rounded text-tri-text">trianglesd verifymessage</code> or
|
||||||
|
the <a href="/verify" class="text-tri-accent hover:text-tri-accent-light">/verify</a> tool.
|
||||||
|
</p>
|
||||||
|
<details class="text-sm">
|
||||||
|
<summary class="cursor-pointer text-tri-accent hover:text-tri-accent-light select-none">
|
||||||
|
Show signed message proof
|
||||||
|
</summary>
|
||||||
|
<div class="mt-3 space-y-3">
|
||||||
|
<div>
|
||||||
|
<div class="text-tri-muted text-xs uppercase tracking-wider mb-1">Message</div>
|
||||||
|
<pre
|
||||||
|
class="font-mono text-xs text-tri-text bg-tri-bg/50 border border-tri-border rounded p-2 whitespace-pre-wrap break-all">{DEV_CONTACT.message}</pre>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-tri-muted text-xs uppercase tracking-wider mb-1">Signature</div>
|
||||||
|
<code
|
||||||
|
class="font-mono text-xs text-tri-text bg-tri-bg/50 border border-tri-border rounded p-2 break-all block"
|
||||||
|
>{DEV_CONTACT.signature}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if data.indexError}
|
{#if data.indexError}
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h1 class="text-3xl font-bold text-white mb-3">Address Details</h1>
|
<h1 class="text-3xl font-bold text-white mb-3">Address Details</h1>
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import { env } from '$env/dynamic/private';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/verify
|
||||||
|
* Body: { address: string, signature: string, message: string }
|
||||||
|
* Returns: { valid: boolean, error?: string }
|
||||||
|
*
|
||||||
|
* Calls the Triangles daemon's verifymessage via JSON-RPC.
|
||||||
|
*/
|
||||||
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
let body: { address?: string; signature?: string; message?: string };
|
||||||
|
try {
|
||||||
|
body = await request.json();
|
||||||
|
} catch {
|
||||||
|
error(400, 'Invalid JSON body');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { address, signature, message } = body;
|
||||||
|
if (!address || !signature || !message) {
|
||||||
|
error(400, 'Missing required fields: address, signature, message');
|
||||||
|
}
|
||||||
|
|
||||||
|
const rpcUrl = env.TRIANGLES_RPC_URL || 'http://127.0.0.1:19112';
|
||||||
|
const rpcUser = env.TRIANGLES_RPC_USER || 'trianglesrpc';
|
||||||
|
const rpcPassword = env.TRIANGLES_RPC_PASSWORD || '';
|
||||||
|
|
||||||
|
if (!rpcPassword) {
|
||||||
|
console.error('[api/verify] TRIANGLES_RPC_PASSWORD not set');
|
||||||
|
error(500, 'RPC credentials not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth = Buffer.from(`${rpcUser}:${rpcPassword}`).toString('base64');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rpcRes = await fetch(rpcUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Basic ${auth}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: '1.0',
|
||||||
|
id: 'verify',
|
||||||
|
method: 'verifymessage',
|
||||||
|
params: [address, signature, message]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read body regardless of status — Triangles daemon returns JSON-RPC errors
|
||||||
|
// as HTTP 500 with a valid JSON body.
|
||||||
|
const rpcJson = await rpcRes.json().catch(() => null);
|
||||||
|
if (!rpcJson) {
|
||||||
|
console.error(`[api/verify] RPC HTTP ${rpcRes.status} with non-JSON body`);
|
||||||
|
error(502, `Upstream RPC error: ${rpcRes.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rpcJson.error) {
|
||||||
|
return json({ valid: false, error: rpcJson.error.message || String(rpcJson.error) });
|
||||||
|
}
|
||||||
|
return json({ valid: rpcJson.result === true });
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
console.error(`[api/verify] ${msg}`);
|
||||||
|
error(500, msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let address = $state('');
|
||||||
|
let signature = $state('');
|
||||||
|
let message = $state('');
|
||||||
|
let result = $state<{ valid: boolean; error?: string } | null>(null);
|
||||||
|
let loading = $state(false);
|
||||||
|
|
||||||
|
async function verify(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
loading = true;
|
||||||
|
result = null;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/verify', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ address, signature, message })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
result = data;
|
||||||
|
} catch (err) {
|
||||||
|
result = { valid: false, error: err instanceof Error ? err.message : String(err) };
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillExample() {
|
||||||
|
address = 'TRsiRzkMWm87ZuWFwPB8YXFGYr5AQZo7fb';
|
||||||
|
signature = 'H/gT/bFSL+WFT4F4WYPvDIVAnt0/M2WQy/ypUvhMtdXgAop+Euycakif9QERNcUfPLNF29vDxuXZf1BJjd8Snro=';
|
||||||
|
message = 'ROGER THAT, TRIANGLES DEV ADDRESS IS A GO.\n\n5.9.23 IS LIVE.';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Verify Signed Message - Triangles Explorer</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Verify the signature of any Triangles (TRI) signed message. Re-check cryptographic proof of address ownership."
|
||||||
|
/>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-3xl font-bold text-white mb-2">Verify Signed Message</h1>
|
||||||
|
<p class="text-tri-muted text-sm">
|
||||||
|
Paste a Triangles address, a signed message, and the signature. The explorer will call the daemon's
|
||||||
|
<code class="bg-tri-surface px-1 rounded text-tri-text">verifymessage</code> RPC and report whether the
|
||||||
|
signature is valid for that address and message.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onsubmit={verify} class="bg-tri-surface border border-tri-border rounded-lg p-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="addr" class="block text-tri-muted text-xs uppercase tracking-wider mb-2">Address</label>
|
||||||
|
<input
|
||||||
|
id="addr"
|
||||||
|
type="text"
|
||||||
|
bind:value={address}
|
||||||
|
placeholder="TRsiRzkMWm87ZuWFwPB8YXFGYr5AQZo7fb"
|
||||||
|
class="w-full bg-tri-bg border border-tri-border rounded px-3 py-2 text-tri-text font-mono text-sm focus:outline-none focus:border-tri-accent"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="msg" class="block text-tri-muted text-xs uppercase tracking-wider mb-2">Message</label>
|
||||||
|
<textarea
|
||||||
|
id="msg"
|
||||||
|
bind:value={message}
|
||||||
|
rows="5"
|
||||||
|
placeholder="The exact message that was signed (preserve newlines)"
|
||||||
|
class="w-full bg-tri-bg border border-tri-border rounded px-3 py-2 text-tri-text font-mono text-sm focus:outline-none focus:border-tri-accent whitespace-pre-wrap"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
<p class="text-tri-muted text-xs mt-1">
|
||||||
|
If your source has literal <code class="bg-tri-bg px-1 rounded">\n</code> characters instead of real
|
||||||
|
newlines, they will be converted automatically.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="sig" class="block text-tri-muted text-xs uppercase tracking-wider mb-2">Signature</label>
|
||||||
|
<textarea
|
||||||
|
id="sig"
|
||||||
|
bind:value={signature}
|
||||||
|
rows="3"
|
||||||
|
placeholder="Base64-encoded compact ECDSA signature"
|
||||||
|
class="w-full bg-tri-bg border border-tri-border rounded px-3 py-2 text-tri-text font-mono text-xs focus:outline-none focus:border-tri-accent break-all"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
class="px-5 py-2 bg-tri-accent hover:bg-tri-accent-light disabled:bg-tri-muted text-white rounded transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
{loading ? 'Verifying…' : 'Verify Signature'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={fillExample}
|
||||||
|
class="px-4 py-2 bg-tri-border/50 hover:bg-tri-border text-tri-text rounded transition-colors text-sm"
|
||||||
|
>
|
||||||
|
Fill with verified dev contact proof
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#if result}
|
||||||
|
<div
|
||||||
|
class="mt-6 rounded-lg p-5 border-2 {result.valid
|
||||||
|
? 'bg-tri-green/10 border-tri-green/40'
|
||||||
|
: 'bg-tri-yellow/10 border-tri-yellow/40'}"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<span class="text-2xl {result.valid ? 'text-tri-green' : 'text-tri-yellow'}">
|
||||||
|
{result.valid ? '✓' : '✗'}
|
||||||
|
</span>
|
||||||
|
<h2 class="text-white font-semibold text-lg">
|
||||||
|
{result.valid ? 'Signature valid' : 'Signature invalid'}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p class="text-tri-muted text-sm">
|
||||||
|
{#if result.valid}
|
||||||
|
The signature is cryptographically valid for the provided address and message. The holder
|
||||||
|
of the private key for this address produced the signature.
|
||||||
|
{:else}
|
||||||
|
The signature does <strong>not</strong> match the address + message combination.
|
||||||
|
{#if result.error}
|
||||||
|
<br /><span class="text-tri-yellow">{result.error}</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mt-8 bg-tri-surface border border-tri-border rounded-lg p-5 text-sm">
|
||||||
|
<h3 class="text-white font-semibold mb-2">How this works</h3>
|
||||||
|
<ol class="list-decimal list-inside text-tri-muted space-y-1">
|
||||||
|
<li>The browser sends your input to <code class="bg-tri-bg px-1 rounded">/api/verify</code> (server-side).</li>
|
||||||
|
<li>
|
||||||
|
The server calls the Triangles daemon's <code class="bg-tri-bg px-1 rounded"
|
||||||
|
>verifymessage</code
|
||||||
|
>JSON-RPC with the trio.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
The daemon recovers the public key from the signature, hashes it, and compares to the address's
|
||||||
|
hash. If they match, the signature is valid.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<p class="text-tri-muted text-xs mt-3">
|
||||||
|
You can also run the same check yourself from the command line:<br />
|
||||||
|
<code class="block mt-1 bg-tri-bg p-2 rounded text-tri-text font-mono text-xs break-all">
|
||||||
|
trianglesd verifymessage "<address>" "<signature>" "<message>"
|
||||||
|
</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user