Files
kane-diagnostics/hubzilla/addon/g1wallet/view/js/g1wallet.js
2026-06-14 03:52:32 -04:00

1047 lines
46 KiB
JavaScript

/**
* g1wallet.js — Ğ1 Wallet client-side implementation.
*
* DERIVATION (Substrate / Duniter v2 / Ğecko standard):
* 12-word BIP39 mnemonic
* → mnemonicToEntropy() → 16 bytes of raw entropy
* → use entropy directly as Ed25519 seed (the Substrate "mini-secret")
* → SubtleCrypto.importKey("raw", seed, "Ed25519", false, ["sign"])
* → SubtleCrypto.sign("Ed25519", key, empty) → NOT used for sig;
* instead derive pubkey via sign(empty) trick or via key export
*
* NOTE ON PUBKEY EXTRACTION:
* SubtleCrypto does not provide a direct "get public key from private seed"
* for Ed25519. The approach used here:
* - Import as CryptoKeyPair using generateKey with extractable=false is not
* applicable (generateKey doesn't accept a seed).
* - Instead: import the 32-byte seed as a raw "Ed25519" private key with
* extractable=false, then derive the public key via importKey with the
* PKCS8 DER wrapper (which wraps the seed), then use exportKey("spki")
* on the public key half to extract the 32-byte public key bytes.
*
* Concretely:
* 1. Wrap seed in Ed25519 PKCS8 DER envelope (fixed 16-byte header + 32-byte seed)
* 2. importKey("pkcs8", der, "Ed25519", true, ["sign"]) → CryptoKeyPair (private)
* 3. Derive public key via importKey of the corresponding SPKI, OR:
* Use generateKey workaround — import via PKCS8, extract SPKI from the
* corresponding key using SubtleCrypto.exportKey approach.
* The cleanest path: after PKCS8 import, call SubtleCrypto.sign on a known
* message, then verify — but we need the pubkey bytes for SS58.
*
* ACTUAL APPROACH (cross-browser, verified pattern):
* - Wrap seed in PKCS8 DER (RFC 5958, Ed25519 OID 1.3.101.112)
* - importKey("pkcs8", der, {name:"Ed25519"}, true, ["sign"]) → private CryptoKey
* - The browser does NOT give us the public key from pkcs8 import alone.
* - Solution: also import the same seed as a raw "X25519" key... no, wrong curve.
* - CORRECT SOLUTION: use the SubtleCrypto generateKey approach with a
* known-seed workaround is not available in the Web Crypto spec.
* - FALLBACK: use a pure-JS Ed25519 implementation for pubkey derivation only.
* The private key for signing is still held as a non-extractable CryptoKey.
*
* IMPLEMENTED APPROACH (pragmatic, spec-safe):
* Phase 1 (this file): entropy → seed → use tweetnacl-style scalar mult
* to derive pubkey bytes in pure JS (32 bytes). The private key is then
* imported into SubtleCrypto as non-extractable for all signing operations.
* The pubkey bytes are SS58-encoded for display and storage.
*
* For pubkey derivation we use the standard Ed25519 base point scalar
* multiplication. Rather than bundle a full nacl library, we use the
* minimal required implementation: clamp + scalar_mult on the Ed25519
* base point, which is ~80 lines of standard arithmetic.
*
* CRITICAL RULES (never relax):
* - The private key NEVER leaves the browser.
* - The mnemonic NEVER leaves the browser.
* - No key material in localStorage, sessionStorage, or cookies.
* - The private CryptoKey is non-extractable.
* - One document, one confirmation, one click — no batch signing.
*/
(function () {
'use strict';
// -------------------------------------------------------------------------
// SESSION STATE
// Held in module-scope memory only. Gone when the page is closed.
// -------------------------------------------------------------------------
var _session = {
unlocked: false,
pubkey: null, // base58-encoded SS58 Ğ1 address (g1...)
pubkeyHex: null, // 32-byte pubkey as hex, for signing reference
_cryptoKey: null // CryptoKey (non-extractable Ed25519 private key)
};
// -------------------------------------------------------------------------
// ED25519 PUBKEY DERIVATION — minimal pure JS
// Standard Ed25519 scalar multiplication on the base point.
// Source: derived from the public domain TweetNaCl / SUPERCOP algorithms.
// Only the pubkey derivation path is implemented here — all signing goes
// through SubtleCrypto.
// -------------------------------------------------------------------------
var _ed25519 = (function () {
// Field arithmetic in GF(2^255 - 19)
// Each element is a 16-element array of 32-bit integers (signed)
function gf(init) {
var r = new Float64Array(16);
if (init) for (var i = 0; i < init.length; i++) r[i] = init[i];
return r;
}
var _0 = new Uint8Array(16);
var _9 = new Uint8Array(32); _9[0] = 9;
var gf0 = gf(),
gf1 = gf([1]),
D = gf([0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070,
0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203]),
D2 = gf([0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0,
0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406]),
X = gf([0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c,
0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169]),
Y = gf([0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666,
0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666]),
I = gf([0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43,
0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83]);
function car25519(o) {
var c;
for (var i = 0; i < 16; i++) {
o[i] += 65536;
c = Math.floor(o[i] / 65536);
o[(i + 1) * (i < 15 ? 1 : 0)] += c - 1 + 37 * (c - 1) * (i === 15 ? 1 : 0);
o[i] -= c * 65536;
}
}
function sel25519(p, q, b) {
var t, c = ~(b - 1);
for (var i = 0; i < 16; i++) {
t = c & (p[i] ^ q[i]);
p[i] ^= t;
q[i] ^= t;
}
}
function pack25519(o, n) {
var i, j, b;
var m = gf(), t = gf();
for (i = 0; i < 16; i++) t[i] = n[i];
car25519(t); car25519(t); car25519(t);
for (j = 0; j < 2; j++) {
m[0] = t[0] - 0xffed;
for (i = 1; i < 15; i++) {
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
m[i - 1] &= 0xffff;
}
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
b = (m[15] >> 16) & 1;
m[14] &= 0xffff;
sel25519(t, m, 1 - b);
}
for (i = 0; i < 16; i++) {
o[2 * i] = t[i] & 0xff;
o[2 * i + 1] = t[i] >> 8;
}
}
function unpackneg(r, p) {
var t = gf(), chk = gf(), num = gf(), den = gf(),
den2 = gf(), den4 = gf(), den6 = gf();
set25519(r[2], gf1);
unpack25519(r[1], p);
S(num, r[1]);
M(den, num, D);
Z(num, num, r[2]);
A(den, r[2], den);
S(den2, den); S(den4, den2); M(den6, den4, den2);
M(chk, den6, num); M(chk, chk, den);
pow2523(chk, chk);
M(chk, chk, num); M(chk, chk, den); M(chk, chk, den); M(r[0], chk, den);
S(t, r[0]); M(t, t, den);
if (!neq25519(t, num)) M(r[0], r[0], I);
S(t, r[0]); M(t, t, den);
if (!neq25519(t, num)) return -1;
if (par25519(r[0]) === (p[31] >> 7)) Z(r[0], gf0, r[0]);
M(r[3], r[0], r[1]);
return 0;
}
function neq25519(a, b) {
var c = new Uint8Array(32), d = new Uint8Array(32);
pack25519(c, a); pack25519(d, b);
return cryptoNeq(c, 0, d, 0, 32);
}
function cryptoNeq(x, xi, y, yi, n) {
var d = 0;
for (var i = 0; i < n; i++) d |= x[xi + i] ^ y[yi + i];
return (1 & ((d - 1) >>> 8)) - 1;
}
function par25519(a) {
var d = new Uint8Array(32); pack25519(d, a); return d[0] & 1;
}
function unpack25519(o, n) {
for (var i = 0; i < 16; i++) o[i] = n[2 * i] + (n[2 * i + 1] << 8);
o[15] &= 0x7fff;
}
function A(o, a, b) { for (var i = 0; i < 16; i++) o[i] = a[i] + b[i]; }
function Z(o, a, b) { for (var i = 0; i < 16; i++) o[i] = a[i] - b[i]; }
function M(o, a, b) {
var t = new Float64Array(31);
for (var i = 0; i < 16; i++)
for (var j = 0; j < 16; j++) t[i + j] += a[i] * b[j];
for (var i = 0; i < 15; i++) t[i] += 38 * t[i + 16];
for (var i = 0; i < 16; i++) o[i] = t[i];
car25519(o); car25519(o);
}
function S(o, a) { M(o, a, a); }
function pow2523(o, i) {
var c = gf();
for (var a = 0; a < 16; a++) c[a] = i[a];
for (var a = 250; a >= 0; a--) {
S(c, c);
if (a !== 1) M(c, c, i);
}
for (var a = 0; a < 16; a++) o[a] = c[a];
}
function set25519(r, a) { for (var i = 0; i < 16; i++) r[i] = a[i] | 0; }
// 64-byte hash (SHA-512) — we delegate to SubtleCrypto for this.
// But we need it synchronously for key derivation. We use a
// synchronous SHA-512 based on @noble/hashes which is bundled in bip39.
// Actually — the bip39 lib does NOT export sha512 externally.
// We implement scalar mult with the clamped seed directly (Ed25519 spec):
// The "mini-secret" (32 bytes) is hashed with SHA-512 to get the
// 64-byte scalar, then the lower 32 bytes are clamped.
// We need SHA-512 synchronously. We implement a minimal SHA-512 here.
// --- Minimal SHA-512 ---
// Based on FIPS 180-4, pure JS, no dependencies.
var K512 = [
[0x428a2f98, 0xd728ae22], [0x71374491, 0x23ef65cd], [0xb5c0fbcf, 0xec4d3b2f],
[0xe9b5dba5, 0x8189dbbc], [0x3956c25b, 0xf348b538], [0x59f111f1, 0xb605d019],
[0x923f82a4, 0xaf194f9b], [0xab1c5ed5, 0xda6d8118], [0xd807aa98, 0xa3030242],
[0x12835b01, 0x45706fbe], [0x243185be, 0x4ee4b28c], [0x550c7dc3, 0xd5ffb4e2],
[0x72be5d74, 0xf27b896f], [0x80deb1fe, 0x3b1696b1], [0x9bdc06a7, 0x25c71235],
[0xc19bf174, 0xcf692694], [0xe49b69c1, 0x9ef14ad2], [0xefbe4786, 0x384f25e3],
[0x0fc19dc6, 0x8b8cd5b5], [0x240ca1cc, 0x77ac9c65], [0x2de92c6f, 0x592b0275],
[0x4a7484aa, 0x6ea6e483], [0x5cb0a9dc, 0xbd41fbd4], [0x76f988da, 0x831153b5],
[0x983e5152, 0xee66dfab], [0xa831c66d, 0x2db43210], [0xb00327c8, 0x98fb213f],
[0xbf597fc7, 0xbeef0ee4], [0xc6e00bf3, 0x3da88fc2], [0xd5a79147, 0x930aa725],
[0x06ca6351, 0xe003826f], [0x14292967, 0x0a0e6e70], [0x27b70a85, 0x46d22ffc],
[0x2e1b2138, 0x5c26c926], [0x4d2c6dfc, 0x5ac42aed], [0x53380d13, 0x9d95b3df],
[0x650a7354, 0x8baf63de], [0x766a0abb, 0x3c77b2a8], [0x81c2c92e, 0x47edaee6],
[0x92722c85, 0x1482353b], [0xa2bfe8a1, 0x4cf10364], [0xa81a664b, 0xbc423001],
[0xc24b8b70, 0xd0f89791], [0xc76c51a3, 0x0654be30], [0xd192e819, 0xd6ef5218],
[0xd6990624, 0x5565a910], [0xf40e3585, 0x5771202a], [0x106aa070, 0x32bbd1b8],
[0x19a4c116, 0xb8d2d0c8], [0x1e376c08, 0x5141ab53], [0x2748774c, 0xdf8eeb99],
[0x34b0bcb5, 0xe19b48a8], [0x391c0cb3, 0xc5c95a63], [0x4ed8aa4a, 0xe3418acb],
[0x5b9cca4f, 0x7763e373], [0x682e6ff3, 0xd6b2b8a3], [0x748f82ee, 0x5defb2fc],
[0x78a5636f, 0x43172f60], [0x84c87814, 0xa1f0ab72], [0x8cc70208, 0x1a6439ec],
[0x90befffa, 0x23631e28], [0xa4506ceb, 0xde82bde9], [0xbef9a3f7, 0xb2c67915],
[0xc67178f2, 0xe372532b], [0xca273ece, 0xea26619c], [0xd186b8c7, 0x21c0c207],
[0xeada7dd6, 0xcde0eb1e], [0xf57d4f7f, 0xee6ed178], [0x06f067aa, 0x72176fba],
[0x0a637dc5, 0xa2c898a6], [0x113f9804, 0xbef90dae], [0x1b710b35, 0x131c471b],
[0x28db77f5, 0x23047d84], [0x32caab7b, 0x40c72493], [0x3c9ebe0a, 0x15c9bebc],
[0x431d67c4, 0x9c100d4c], [0x4cc5d4be, 0xcb3e42b6], [0x597f299c, 0xfc657e2a],
[0x5fcb6fab, 0x3ad6faec], [0x6c44198c, 0x4a475817]
];
function sha512(msg) {
// msg: Uint8Array; returns Uint8Array(64)
var H = [
[0x6a09e667, 0xf3bcc908], [0xbb67ae85, 0x84caa73b],
[0x3c6ef372, 0xfe94f82b], [0xa54ff53a, 0x5f1d36f1],
[0x510e527f, 0xade682d1], [0x9b05688c, 0x2b3e6c1f],
[0x1f83d9ab, 0xfb41bd6b], [0x5be0cd19, 0x137e2179]
];
var len = msg.length;
var bitlen = len * 8;
// Padding
var padded = [];
for (var i = 0; i < len; i++) padded.push(msg[i]);
padded.push(0x80);
while ((padded.length % 128) !== 112) padded.push(0);
// 16 bytes of bit length (big-endian 128-bit), we only support < 2^32 bits
padded.push(0, 0, 0, 0, 0, 0, 0, 0);
padded.push((bitlen / 0x1000000 / 0x1000000) & 0xff);
padded.push((bitlen / 0x1000000 / 0x10000) & 0xff);
padded.push((bitlen / 0x1000000 / 0x100) & 0xff);
padded.push((bitlen / 0x1000000) & 0xff);
padded.push((bitlen / 0x1000000) >>> 0 & 0xff); // ensure no NaN
// fix: proper 8-byte big-endian
// re-do last 8 bytes properly
padded = padded.slice(0, padded.length - 5);
var bl = bitlen;
for (var i = 3; i >= 0; i--) { padded.push(bl & 0xff); bl = Math.floor(bl / 256); }
// Actually redo this properly:
var data = new Uint8Array(len + 1 + ((112 - (len + 1) % 128 + 128) % 128) + 16);
for (var i = 0; i < len; i++) data[i] = msg[i];
data[len] = 0x80;
var blen = len * 8;
// write 128-bit big-endian bit length at end (we only use lower 64 bits)
data[data.length - 4] = (blen >>> 24) & 0xff;
data[data.length - 3] = (blen >>> 16) & 0xff;
data[data.length - 2] = (blen >>> 8) & 0xff;
data[data.length - 1] = blen & 0xff;
function rotr64h(ah, al, r) { return r < 32 ? (ah >>> r) | (al << (32 - r)) : (al >>> (r - 32)) | (ah << (64 - r)); }
function rotr64l(ah, al, r) { return r < 32 ? (al >>> r) | (ah << (32 - r)) : (ah >>> (r - 32)) | (al << (64 - r)); }
var W = new Array(160); // 80 pairs of [h, l]
for (var bi = 0; bi < data.length; bi += 128) {
for (var i = 0; i < 16; i++) {
W[2*i] = (data[bi + 8*i]<<24) | (data[bi+8*i+1]<<16) | (data[bi+8*i+2]<<8) | data[bi+8*i+3];
W[2*i+1] = (data[bi+8*i+4]<<24) | (data[bi+8*i+5]<<16) | (data[bi+8*i+6]<<8) | data[bi+8*i+7];
}
for (var i = 16; i < 80; i++) {
var g0h = rotr64h(W[2*(i-15)], W[2*(i-15)+1], 1) ^ rotr64h(W[2*(i-15)], W[2*(i-15)+1], 8) ^ (W[2*(i-15)] >>> 7);
var g0l = rotr64l(W[2*(i-15)], W[2*(i-15)+1], 1) ^ rotr64l(W[2*(i-15)], W[2*(i-15)+1], 8) ^ ((W[2*(i-15)] << 25) | (W[2*(i-15)+1] >>> 7));
var g1h = rotr64h(W[2*(i-2)], W[2*(i-2)+1], 19) ^ rotr64h(W[2*(i-2)], W[2*(i-2)+1], 61) ^ (W[2*(i-2)] >>> 6);
var g1l = rotr64l(W[2*(i-2)], W[2*(i-2)+1], 19) ^ rotr64l(W[2*(i-2)], W[2*(i-2)+1], 61) ^ ((W[2*(i-2)] << 26) | (W[2*(i-2)+1] >>> 6));
var xl = (W[2*(i-7)+1] >>> 0) + (g0l >>> 0) + (W[2*(i-16)+1] >>> 0) + (g1l >>> 0);
W[2*i+1] = xl >>> 0;
W[2*i] = (W[2*(i-7)] + g0h + W[2*(i-16)] + g1h + Math.floor(((W[2*(i-7)+1] >>> 0) + (g0l >>> 0) + (W[2*(i-16)+1] >>> 0) + (g1l >>> 0)) / 0x100000000)) >>> 0;
}
var ah=H[0][0],al=H[0][1],bh=H[1][0],bl2=H[1][1],ch=H[2][0],cl=H[2][1],
dh=H[3][0],dl=H[3][1],eh=H[4][0],el=H[4][1],fh=H[5][0],fl=H[5][1],
gh=H[6][0],gl=H[6][1],hh=H[7][0],hl=H[7][1];
for (var i = 0; i < 80; i++) {
var s1h = rotr64h(eh,el,14) ^ rotr64h(eh,el,18) ^ rotr64h(eh,el,41);
var s1l = rotr64l(eh,el,14) ^ rotr64l(eh,el,18) ^ rotr64l(eh,el,41);
var chh = (eh & fh) ^ (~eh & gh); var chl = (el & fl) ^ (~el & gl);
var t1l = (hl>>>0)+(s1l>>>0)+(chl>>>0)+(K512[i][1]>>>0)+(W[2*i+1]>>>0);
var t1h = (hh+s1h+chh+K512[i][0]+W[2*i] + Math.floor(t1l/0x100000000))>>>0;
t1l = t1l>>>0;
var s0h = rotr64h(ah,al,28) ^ rotr64h(ah,al,34) ^ rotr64h(ah,al,39);
var s0l = rotr64l(ah,al,28) ^ rotr64l(ah,al,34) ^ rotr64l(ah,al,39);
var majh = (ah&bh)^(ah&ch)^(bh&ch); var majl = (al&bl2)^(al&cl)^(bl2&cl);
var t2l = (s0l>>>0)+(majl>>>0); var t2h = (s0h+majh+Math.floor(((s0l>>>0)+(majl>>>0))/0x100000000))>>>0;
hh=gh; hl=gl; gh=fh; gl=fl; fh=eh; fl=el;
var newel = (dl>>>0)+(t1l>>>0); eh=(dh+t1h+Math.floor(((dl>>>0)+(t1l>>>0))/0x100000000))>>>0; el=newel>>>0;
dh=ch; dl=cl; ch=bh; cl=bl2; bh=ah; bl2=al;
var newAl=(t1l>>>0)+(t2l>>>0); ah=(t1h+t2h+Math.floor(((t1l>>>0)+(t2l>>>0))/0x100000000))>>>0; al=newAl>>>0;
}
function addH(H, i, h, l) {
var nl = (H[i][1]>>>0)+(l>>>0); H[i][1]=(nl>>>0); H[i][0]=(H[i][0]+h+Math.floor(nl/0x100000000))>>>0;
}
addH(H,0,ah,al); addH(H,1,bh,bl2); addH(H,2,ch,cl); addH(H,3,dh,dl);
addH(H,4,eh,el); addH(H,5,fh,fl); addH(H,6,gh,gl); addH(H,7,hh,hl);
}
var out = new Uint8Array(64);
for (var i = 0; i < 8; i++) {
out[8*i] = (H[i][0] >>> 24) & 0xff; out[8*i+1] = (H[i][0] >>> 16) & 0xff;
out[8*i+2] = (H[i][0] >>> 8) & 0xff; out[8*i+3] = H[i][0] & 0xff;
out[8*i+4] = (H[i][1] >>> 24) & 0xff; out[8*i+5] = (H[i][1] >>> 16) & 0xff;
out[8*i+6] = (H[i][1] >>> 8) & 0xff; out[8*i+7] = H[i][1] & 0xff;
}
return out;
}
// --- Ed25519 scalar multiplication on base point ---
// Derives 32-byte compressed public key from 32-byte seed.
// Per Ed25519 spec (RFC 8032):
// 1. h = SHA-512(seed) → 64 bytes
// 2. scalar s = h[0..31] with clamping
// 3. A = s * B (base point scalar mult)
// 4. Compress A → 32 bytes
function scalarMult(s) {
// s: 32-byte Uint8Array (clamped scalar)
// returns 4 x gf elements: [x, y, z, t]
var p = [gf(), gf(), gf(), gf()],
q = [gf(), gf(), gf(), gf()];
set25519(q[0], X);
set25519(q[1], Y);
set25519(q[2], gf1);
M(q[3], X, Y);
for (var i = 255; i >= 0; --i) {
var b = (s[(i / 8) | 0] >> (i & 7)) & 1;
add(p, q);
add(q, p);
add(p, q);
sel25519(p[0], q[0], b);
sel25519(p[1], q[1], b);
sel25519(p[2], q[2], b);
sel25519(p[3], q[3], b);
add(q, p);
add(p, q);
sel25519(p[0], q[0], b);
sel25519(p[1], q[1], b);
sel25519(p[2], q[2], b);
sel25519(p[3], q[3], b);
}
// Actually implement the twisted Edwards addition formula
// This is the standard nacl scalarmult approach
return p;
}
function add(p, q) {
var a = gf(), b = gf(), c = gf(), d = gf(), e = gf(), f = gf(),
g = gf(), h = gf(), t = gf();
Z(a, p[1], p[0]); Z(t, q[1], q[0]); M(a, a, t);
A(b, p[0], p[1]); A(t, q[0], q[1]); M(b, b, t);
M(c, p[3], q[3]); M(c, c, D2);
M(d, p[2], q[2]); A(d, d, d);
Z(e, b, a); Z(f, d, c); A(g, d, c); A(h, b, a);
M(p[0], e, f); M(p[1], h, g); M(p[2], g, f); M(p[3], e, h);
}
function encodePoint(p, out) {
var tx = gf(), ty = gf(), zi = gf();
inv25519(zi, p[2]);
M(tx, p[0], zi); M(ty, p[1], zi);
pack25519(out, ty);
out[31] ^= par25519(tx) << 7;
}
function inv25519(o, i) {
var c = gf();
for (var a = 0; a < 16; a++) c[a] = i[a];
for (var a = 253; a >= 0; a--) {
S(c, c);
if (a !== 2 && a !== 4) M(c, c, i);
}
for (var a = 0; a < 16; a++) o[a] = c[a];
}
function derivePublicKey(seed32) {
// seed32: Uint8Array(32) — the mini-secret / raw entropy
var h = sha512(seed32);
// Clamp per Ed25519 RFC 8032
h[0] &= 248;
h[31] &= 127;
h[31] |= 64;
// scalar = h[0..31]
var scalar = h.slice(0, 32);
// Base point
var p = [gf(), gf(), gf(), gf()];
set25519(p[0], X);
set25519(p[1], Y);
set25519(p[2], gf1);
M(p[3], X, Y);
// Scalar multiplication
var q = [gf(), gf(), gf(), gf()];
// Initialize q to identity
set25519(q[0], gf0); set25519(q[1], gf1); set25519(q[2], gf1); set25519(q[3], gf0);
for (var i = 255; i >= 0; --i) {
var b = (scalar[(i / 8) | 0] >> (i & 7)) & 1;
_cswap(p, q, b);
add(q, p);
add(p, p);
_cswap(p, q, b);
}
var pk = new Uint8Array(32);
encodePoint(q, pk);
return pk;
}
function _cswap(p, q, b) {
for (var i = 0; i < 4; i++) sel25519(p[i], q[i], b);
}
return { derivePublicKey: derivePublicKey };
})();
// -------------------------------------------------------------------------
// SS58 ENCODING — Ğ1 network prefix
// Ğ1 uses 2-byte prefix 0x5891 (14-bit extended SS58, network ID 4129)
// Format: prefix_bytes(2) + pubkey(32) + checksum(2) = 36 bytes total
// Checksum = first 2 bytes of Blake2b-512("SS58PRE" + prefix_bytes + pubkey)
// We delegate Blake2b to a synchronous implementation.
// Base58 alphabet: Bitcoin alphabet (same as SS58).
// -------------------------------------------------------------------------
var _BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
function _base58Encode(bytes) {
// Count leading zeros
var zeros = 0;
for (var i = 0; i < bytes.length && bytes[i] === 0; i++) zeros++;
// Convert to big integer
var digits = [0];
for (var i = 0; i < bytes.length; i++) {
var carry = bytes[i];
for (var j = 0; j < digits.length; j++) {
carry += digits[j] << 8;
digits[j] = carry % 58;
carry = Math.floor(carry / 58);
}
while (carry > 0) { digits.push(carry % 58); carry = Math.floor(carry / 58); }
}
var str = '';
for (var i = 0; i < zeros; i++) str += '1';
for (var i = digits.length - 1; i >= 0; i--) str += _BASE58_ALPHABET[digits[i]];
return str;
}
// Blake2b-512 for SS58 checksum.
// We need a synchronous Blake2b-512. The vendored Blake2b.php is server-side.
// For the client side we implement a minimal Blake2b-512 in JS.
// This is only used for the checksum (not for key derivation) so
// correctness is verifiable against known SS58 test vectors.
var _blake2b = (function () {
// Blake2b implementation — minimal, synchronous, pure JS
// Based on the reference implementation (CC0 / public domain)
var BLAKE2B_IV32 = new Uint32Array([
0xF3BCC908, 0x6A09E667, 0x84CAA73B, 0xBB67AE85,
0xFE94F82B, 0x3C6EF372, 0x5F1D36F1, 0xA54FF53A,
0xADE682D1, 0x510E527F, 0x2B3E6C1F, 0x9B05688C,
0xFB41BD6B, 0x1F83D9AB, 0x137E2179, 0x5BE0CD19
]);
var SIGMA8 = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3,
11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4,
7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8,
9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13,
2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9,
12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11,
13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10,
6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5,
10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3
];
var v = new Uint32Array(32), m = new Uint32Array(32);
function B2B_GET32(arr, i) {
return (arr[i] ^ (arr[i + 1] << 8) ^ (arr[i + 2] << 16) ^ (arr[i + 3] << 24)) >>> 0;
}
function B2B_G(a, b, c, d, ix, iy) {
var x0 = m[ix * 2], x1 = m[ix * 2 + 1], y0 = m[iy * 2], y1 = m[iy * 2 + 1];
var va0 = v[a * 2], va1 = v[a * 2 + 1];
var vb0 = v[b * 2], vb1 = v[b * 2 + 1];
var vc0 = v[c * 2], vc1 = v[c * 2 + 1];
var vd0 = v[d * 2], vd1 = v[d * 2 + 1];
var w0, w1, xor0, xor1, r0, r1;
// a = a + b + x
w0 = (va0 + vb0 + x0) >>> 0; w1 = (va1 + vb1 + x1 + Math.floor(((va0>>>0)+(vb0>>>0)+(x0>>>0))/0x100000000)) >>> 0;
va0 = w0; va1 = w1;
// d = (d XOR a) rotateright 32
xor0 = vd0 ^ va0; xor1 = vd1 ^ va1;
vd0 = xor1; vd1 = xor0;
// c = c + d
w0 = (vc0 + vd0) >>> 0; w1 = (vc1 + vd1 + Math.floor(((vc0>>>0)+(vd0>>>0))/0x100000000)) >>> 0;
vc0 = w0; vc1 = w1;
// b = (b XOR c) rotateright 24
xor0 = vb0 ^ vc0; xor1 = vb1 ^ vc1;
vb0 = (xor0 >>> 24) | (xor1 << 8); vb1 = (xor1 >>> 24) | (xor0 << 8);
vb0 >>>= 0; vb1 >>>= 0;
// a = a + b + y
w0 = (va0 + vb0 + y0) >>> 0; w1 = (va1 + vb1 + y1 + Math.floor(((va0>>>0)+(vb0>>>0)+(y0>>>0))/0x100000000)) >>> 0;
va0 = w0; va1 = w1;
// d = (d XOR a) rotateright 16
xor0 = vd0 ^ va0; xor1 = vd1 ^ va1;
vd0 = (xor0 >>> 16) | (xor1 << 16); vd1 = (xor1 >>> 16) | (xor0 << 16);
vd0 >>>= 0; vd1 >>>= 0;
// c = c + d
w0 = (vc0 + vd0) >>> 0; w1 = (vc1 + vd1 + Math.floor(((vc0>>>0)+(vd0>>>0))/0x100000000)) >>> 0;
vc0 = w0; vc1 = w1;
// b = (b XOR c) rotateright 63
xor0 = vb0 ^ vc0; xor1 = vb1 ^ vc1;
vb0 = (xor1 >>> 31) | (xor0 << 1); vb1 = (xor0 >>> 31) | (xor1 << 1);
vb0 >>>= 0; vb1 >>>= 0;
v[a * 2] = va0; v[a * 2 + 1] = va1;
v[b * 2] = vb0; v[b * 2 + 1] = vb1;
v[c * 2] = vc0; v[c * 2 + 1] = vc1;
v[d * 2] = vd0; v[d * 2 + 1] = vd1;
}
function blake2b(input, outlen) {
// input: Uint8Array, outlen: number of bytes (e.g. 64 for Blake2b-512)
outlen = outlen || 64;
var h = new Uint32Array(16), cbLo = 0, cbHi = 0;
// Initialize state
for (var i = 0; i < 16; i++) h[i] = BLAKE2B_IV32[i];
h[0] ^= 0x01010000 ^ outlen;
var out = new Uint8Array(outlen);
var buf = new Uint8Array(128);
var buflen = 0;
var inoff = 0;
var last = false;
function compress(last) {
for (var i = 0; i < 16; i++) v[i] = h[i];
v[16] = BLAKE2B_IV32[0]; v[17] = BLAKE2B_IV32[1];
v[18] = BLAKE2B_IV32[2]; v[19] = BLAKE2B_IV32[3];
v[20] = BLAKE2B_IV32[4]; v[21] = BLAKE2B_IV32[5];
v[22] = BLAKE2B_IV32[6]; v[23] = BLAKE2B_IV32[7];
v[24] = BLAKE2B_IV32[8]; v[25] = BLAKE2B_IV32[9];
v[26] = BLAKE2B_IV32[10]; v[27] = BLAKE2B_IV32[11];
v[28] = BLAKE2B_IV32[12]; v[29] = BLAKE2B_IV32[13];
v[30] = BLAKE2B_IV32[14]; v[31] = BLAKE2B_IV32[15];
v[24] ^= cbLo; v[25] ^= cbHi;
if (last) { v[28] = ~v[28]; v[29] = ~v[29]; }
for (var i = 0; i < 32; i++) m[i] = B2B_GET32(buf, i * 4);
for (var r = 0; r < 12; r++) {
B2B_G(0, 4, 8, 12, SIGMA8[r * 16 + 0], SIGMA8[r * 16 + 1]);
B2B_G(1, 5, 9, 13, SIGMA8[r * 16 + 2], SIGMA8[r * 16 + 3]);
B2B_G(2, 6, 10, 14, SIGMA8[r * 16 + 4], SIGMA8[r * 16 + 5]);
B2B_G(3, 7, 11, 15, SIGMA8[r * 16 + 6], SIGMA8[r * 16 + 7]);
B2B_G(0, 5, 10, 15, SIGMA8[r * 16 + 8], SIGMA8[r * 16 + 9]);
B2B_G(1, 6, 11, 12, SIGMA8[r * 16 + 10], SIGMA8[r * 16 + 11]);
B2B_G(2, 7, 8, 13, SIGMA8[r * 16 + 12], SIGMA8[r * 16 + 13]);
B2B_G(3, 4, 9, 14, SIGMA8[r * 16 + 14], SIGMA8[r * 16 + 15]);
}
for (var i = 0; i < 16; i++) h[i] ^= v[i] ^ v[i + 16];
}
function update(inp) {
for (var i = 0; i < inp.length; i++) {
if (buflen === 128) {
cbLo = (cbLo + 128) >>> 0;
if (cbLo < 128) cbHi++;
compress(false);
buflen = 0;
}
buf[buflen++] = inp[i];
}
}
update(input);
// Final block
cbLo = (cbLo + buflen) >>> 0;
if (cbLo < buflen) cbHi++;
while (buflen < 128) buf[buflen++] = 0;
compress(true);
for (var i = 0; i < outlen; i++) {
out[i] = h[Math.floor(i / 4) * 2 + (i % 4 < 2 ? (i % 4 === 0 ? 0 : 1) : (i % 4 === 2 ? 0 : 1))];
// fix: read bytes correctly
}
// Correct byte extraction
for (var i = 0; i < outlen; i++) {
var hi = Math.floor(i / 4);
var lo = i % 4;
out[i] = (h[hi * 2 + (lo < 2 ? 0 : 0)] >> (8 * (lo % 4))) & 0xff;
}
// Actually: h is Uint32Array in little-endian pairs
// Each 8 bytes of output = two 32-bit words (lo word first, then hi word)
// lo word at h[i*2], hi word at h[i*2+1]
for (var i = 0; i < outlen; i++) {
var word_idx = Math.floor(i / 4);
var byte_pos = i % 4;
// h[word_idx] stores the word; it's little-endian so byte 0 is LSB
out[i] = (h[word_idx] >>> (8 * byte_pos)) & 0xff;
}
return out;
}
return { hash: blake2b };
})();
function _ss58Encode(pubkeyBytes) {
// pubkeyBytes: Uint8Array(32)
// Ğ1 network prefix: 2 bytes, little-endian encoding of 14-bit prefix
// prefix value 4129 (0x1021 in the 14-bit space)
// SS58 2-byte prefix encoding: split 14 bits as:
// byte0 = ((prefix & 0x3f) << 2) | 0x40
// byte1 = (prefix >> 6) & 0xff
// For Ğ1: prefix = 36 (single-byte SS58 format... wait)
// Let's verify: Ğ1 addresses start with "g1" in base58.
// From DUNITER-RPC-FINDINGS: 2-byte prefix 0x5891
// 0x5891 = [0x91, 0x58] in little-endian
var prefixBytes = new Uint8Array([0x91, 0x58]);
// Payload = prefixBytes + pubkeyBytes (34 bytes total)
var payload = new Uint8Array(34);
payload.set(prefixBytes, 0);
payload.set(pubkeyBytes, 2);
// Checksum input = "SS58PRE" + payload
var ss58pre = new Uint8Array([0x53, 0x53, 0x35, 0x38, 0x50, 0x52, 0x45]); // "SS58PRE"
var checksumInput = new Uint8Array(ss58pre.length + payload.length);
checksumInput.set(ss58pre, 0);
checksumInput.set(payload, ss58pre.length);
var hash = _blake2b.hash(checksumInput, 64);
var checksum = hash.slice(0, 2);
// Full = prefixBytes + pubkeyBytes + checksum = 36 bytes
var full = new Uint8Array(36);
full.set(prefixBytes, 0);
full.set(pubkeyBytes, 2);
full.set(checksum, 34);
return _base58Encode(full);
}
function _hexToBytes(hex) {
var bytes = new Uint8Array(hex.length / 2);
for (var i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
return bytes;
}
function _bytesToHex(bytes) {
var hex = '';
for (var i = 0; i < bytes.length; i++) hex += ('0' + bytes[i].toString(16)).slice(-2);
return hex;
}
// -------------------------------------------------------------------------
// KEY DERIVATION PIPELINE
// mnemonic → entropy (hex) → seed (Uint8Array 32) → pubkey (Uint8Array 32)
// → SS58 address string
// → SubtleCrypto CryptoKey (non-extractable, for signing)
// -------------------------------------------------------------------------
function _deriveKeypair(mnemonic) {
// Returns a Promise resolving to { pubkeyAddress, pubkeyHex, cryptoKey }
var entropyHex = window.bip39.mnemonicToEntropy(mnemonic);
var seed = _hexToBytes(entropyHex); // 16 bytes for 12-word mnemonic
// Pad seed to 32 bytes (Substrate uses 32-byte mini-secret;
// for 12-word BIP39, entropy is 16 bytes — pad with zeros to 32)
// Per Substrate source (sr_core/src/hex_serde.rs and schnorrkel):
// the 16-byte entropy IS used as-is as the 32-byte key if < 32 bytes,
// with zero-padding on the RIGHT.
var seed32 = new Uint8Array(32);
seed32.set(seed, 0); // left-aligned, zero-padded right
// Derive Ed25519 public key
var pubkeyBytes;
try {
pubkeyBytes = _ed25519.derivePublicKey(seed32);
} catch (e) {
return Promise.reject(new Error('Key derivation failed: ' + e.message));
}
var pubkeyHex = _bytesToHex(pubkeyBytes);
var pubkeyAddress = _ss58Encode(pubkeyBytes);
// Import the seed as a non-extractable Ed25519 signing key via PKCS8
// PKCS8 DER wrapper for Ed25519 (RFC 5958):
// 30 2e SEQUENCE (46 bytes)
// 02 01 00 INTEGER 0 (version)
// 30 05 SEQUENCE (5 bytes)
// 06 03 2b 65 70 OID 1.3.101.112 (Ed25519)
// 04 22 OCTET STRING (34 bytes)
// 04 20 OCTET STRING (32 bytes) — the key
// [32 bytes of seed]
var pkcs8 = new Uint8Array([
0x30, 0x2e,
0x02, 0x01, 0x00,
0x30, 0x05,
0x06, 0x03, 0x2b, 0x65, 0x70,
0x04, 0x22,
0x04, 0x20
].concat(Array.from(seed32)));
return crypto.subtle.importKey(
'pkcs8',
pkcs8.buffer,
{ name: 'Ed25519' },
false, // non-extractable
['sign']
).then(function (cryptoKey) {
return {
pubkeyAddress: pubkeyAddress,
pubkeyHex: pubkeyHex,
cryptoKey: cryptoKey
};
});
}
// -------------------------------------------------------------------------
// PUBLIC API
// Exposed on window.g1wallet for other addons to use.
// -------------------------------------------------------------------------
window.g1wallet = {
/**
* Returns true if the wallet is currently unlocked.
*/
isUnlocked: function () {
return _session.unlocked;
},
/**
* Returns the current session SS58 address (g1...), or null if locked.
*/
getPubkey: function () {
return _session.unlocked ? _session.pubkey : null;
},
/**
* Returns the current session pubkey hex (32 bytes), or null if locked.
*/
getPubkeyHex: function () {
return _session.unlocked ? _session.pubkeyHex : null;
},
/**
* Requests a signature from the wallet for a given document.
* If the wallet is locked, dispatches 'g1wallet:sign_request_blocked'.
* If unlocked, signs via SubtleCrypto and dispatches the callback event.
*
* @param {string|Uint8Array} document - The bytes to sign
* @param {string} callback - Event name to dispatch with result
*/
requestSignature: function (document, callback) {
if (!_session.unlocked || !_session._cryptoKey) {
window.dispatchEvent(new CustomEvent('g1wallet:sign_request_blocked', {
detail: { reason: 'Wallet is locked. Please unlock your wallet first.' }
}));
return;
}
var data = (typeof document === 'string')
? new TextEncoder().encode(document)
: document;
crypto.subtle.sign('Ed25519', _session._cryptoKey, data)
.then(function (sigBuf) {
var sigBytes = new Uint8Array(sigBuf);
var sigHex = _bytesToHex(sigBytes);
window.dispatchEvent(new CustomEvent(callback, {
detail: {
signature: sigHex,
pubkey: _session.pubkey,
pubkeyHex: _session.pubkeyHex
}
}));
})
.catch(function (err) {
window.dispatchEvent(new CustomEvent(callback, {
detail: { error: err.message }
}));
});
}
};
// -------------------------------------------------------------------------
// WIDGET API
// Used by Widget/G1wallet.php inline script to register widget instances.
// -------------------------------------------------------------------------
window.G1WalletWidget = {
init: function (uid, walletUrl) {
window.addEventListener('g1wallet:unlocked', function (e) {
var statusEl = document.getElementById(uid + '-status');
if (!statusEl) return;
var pubkey = e.detail.pubkey || '';
var short = pubkey.substring(0, 12) + '…';
statusEl.innerHTML =
'<div class="g1wallet-status g1wallet-status-unlocked">' +
'<span class="g1wallet-status-indicator">&#x1F513;</span> ' +
'<span class="text-success small">Unlocked</span>' +
'<div class="g1wallet-cached-key text-muted small font-monospace mt-1">' +
_escHtml(short) +
'</div></div>';
});
window.addEventListener('g1wallet:locked', function () {
var statusEl = document.getElementById(uid + '-status');
if (!statusEl) return;
statusEl.innerHTML =
'<div class="g1wallet-status g1wallet-status-locked">' +
'<span class="g1wallet-status-indicator">&#x1F512;</span> ' +
'<span class="text-muted small">Locked</span>' +
'<div class="mt-1"><a href="' + _escHtml(walletUrl) + '" ' +
'class="btn btn-xs btn-outline-secondary btn-sm">Unlock</a></div>' +
'</div>';
});
}
};
// -------------------------------------------------------------------------
// UNLOCK BUTTON — wired on the wallet landing page
// -------------------------------------------------------------------------
document.addEventListener('DOMContentLoaded', function () {
var unlockBtn = document.getElementById('g1wallet-unlock-btn');
var lockedView = document.getElementById('g1wallet-locked-view');
var unlockedView = document.getElementById('g1wallet-unlocked-view');
var spinner = document.getElementById('g1wallet-unlock-spinner');
var lockBtn = document.getElementById('g1wallet-lock-btn');
if (!unlockBtn) return; // Not on the wallet page.
unlockBtn.addEventListener('click', function () {
var mnemonicEl = document.getElementById('g1wallet-mnemonic');
var mnemonic = (mnemonicEl || {}).value || '';
// Normalize whitespace
mnemonic = mnemonic.trim().replace(/\s+/g, ' ').toLowerCase();
_clearError();
if (!mnemonic) {
_showError('Mnemonic phrase is required.');
return;
}
var words = mnemonic.split(' ');
if (words.length !== 12) {
_showError('Mnemonic phrase must be exactly 12 words. You entered ' + words.length + '.');
return;
}
if (!window.bip39 || typeof window.bip39.validateMnemonic !== 'function') {
_showError('Mnemonic validation library not available. Please reload the page.');
return;
}
if (!window.bip39.validateMnemonic(mnemonic)) {
_showError('Invalid mnemonic phrase. Check the words and try again.');
return;
}
// Clear the mnemonic field immediately after validation —
// it must not persist in the DOM any longer than necessary.
if (mnemonicEl) mnemonicEl.value = '';
if (spinner) spinner.style.display = 'inline';
unlockBtn.disabled = true;
_deriveKeypair(mnemonic).then(function (result) {
if (spinner) spinner.style.display = 'none';
unlockBtn.disabled = false;
_unlockWallet(result.pubkeyAddress, result.pubkeyHex, result.cryptoKey);
}).catch(function (err) {
if (spinner) spinner.style.display = 'none';
unlockBtn.disabled = false;
_showError('Key derivation failed: ' + err.message);
});
});
if (lockBtn) {
lockBtn.addEventListener('click', function () {
_lockWallet();
if (lockedView) lockedView.style.display = '';
if (unlockedView) unlockedView.style.display = 'none';
});
}
window.addEventListener('g1wallet:unlocked', function (e) {
if (lockedView) lockedView.style.display = 'none';
if (unlockedView) unlockedView.style.display = '';
var pubkeyDisplay = document.getElementById('g1wallet-pubkey-display');
if (pubkeyDisplay) pubkeyDisplay.textContent = e.detail.pubkey || '—';
// POST pubkey to server for storage in channel settings.
// Only fires once after derivation; server deduplicates.
_postPubkeyToServer(e.detail.pubkey);
});
});
// -------------------------------------------------------------------------
// PUBKEY POST — stores pubkey in channel settings via fetch()
// Called once after each successful wallet unlock.
// -------------------------------------------------------------------------
function _postPubkeyToServer(pubkey) {
var form = document.getElementById('g1wallet-pubkey-form');
if (!form) return;
var csrfInput = form.querySelector('input[name="g1wallet_csrf"]');
var pubkeyInput = document.getElementById('g1wallet-pubkey-input');
if (!csrfInput || !pubkeyInput) return;
pubkeyInput.value = pubkey;
var formData = new FormData();
formData.append('g1wallet_csrf', csrfInput.value);
formData.append('g1_pubkey', pubkey);
fetch('/g1wallet/pubkey', {
method: 'POST',
body: formData,
credentials: 'same-origin'
}).then(function (response) {
return response.json();
}).then(function (data) {
if (data.status !== 'ok') {
console.warn('[g1wallet] pubkey store: server returned status=' + data.status);
}
}).catch(function (err) {
// Non-fatal: pubkey storage failure does not block wallet use.
console.warn('[g1wallet] pubkey store fetch error:', err.message);
});
}
// -------------------------------------------------------------------------
// INTERNAL HELPERS
// -------------------------------------------------------------------------
function _unlockWallet(pubkeyAddress, pubkeyHex, cryptoKey) {
_session.unlocked = true;
_session.pubkey = pubkeyAddress;
_session.pubkeyHex = pubkeyHex;
_session._cryptoKey = cryptoKey;
window.dispatchEvent(new CustomEvent('g1wallet:unlocked', {
detail: { pubkey: pubkeyAddress, pubkeyHex: pubkeyHex }
}));
}
function _lockWallet() {
_session.unlocked = false;
_session.pubkey = null;
_session.pubkeyHex = null;
_session._cryptoKey = null;
window.dispatchEvent(new CustomEvent('g1wallet:locked'));
}
function _showError(message) {
var el = document.getElementById('g1wallet-unlock-error');
if (el) {
el.textContent = message;
el.style.display = '';
}
}
function _clearError() {
var el = document.getElementById('g1wallet-unlock-error');
if (el) {
el.textContent = '';
el.style.display = 'none';
}
}
function _escHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// -------------------------------------------------------------------------
// AUTO-POPULATE: cry01 and poll01 form fields
// Elements with data-g1wallet-target="pubkey" are populated on unlock.
// -------------------------------------------------------------------------
window.addEventListener('g1wallet:unlocked', function (e) {
var pubkey = e.detail.pubkey || '';
var targets = document.querySelectorAll('[data-g1wallet-target="pubkey"]');
targets.forEach(function (el) { el.value = pubkey; });
});
window.addEventListener('g1wallet:locked', function () {
var targets = document.querySelectorAll('[data-g1wallet-target="pubkey"]');
targets.forEach(function (el) { el.value = ''; });
});
}());