From acd61f11bcb872dfe1c4c5ebd8a9e0ca65277f4e Mon Sep 17 00:00:00 2001 From: matcha Date: Mon, 5 Oct 2026 04:39:32 +0000 Subject: [PATCH] Add Super Testnet signing provider --- src/background.js | 127 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/background.js diff --git a/src/background.js b/src/background.js new file mode 100644 index 0000000..f27c640 --- /dev/null +++ b/src/background.js @@ -0,0 +1,127 @@ +import { Address, Signer, Tx } from "@cmdcode/tapscript"; +import { schnorr, secp256k1 } from "@noble/curves/secp256k1"; +import { bytesToHex } from "@noble/curves/abstract/utils"; + +const NETWORK = "super-testnet-local-v0"; +const KEY_STORAGE = "inscriboor:test-wallet:v1"; +const pending = new Map(); + +async function account() { + const saved = (await chrome.storage.local.get(KEY_STORAGE))[KEY_STORAGE]; + let secret = saved?.secret; + if (!secret || !/^[0-9a-f]{64}$/.test(secret) || !secp256k1.utils.isValidPrivateKey(secret)) { + secret = bytesToHex(secp256k1.utils.randomPrivateKey()); + await chrome.storage.local.set({ [KEY_STORAGE]: { kind:"super-testnet-test-key", version:1, network:NETWORK, secret } }); + } + const pubkey = bytesToHex(schnorr.getPublicKey(secret)); + return { secret, pubkey, address: Address.p2tr.fromPubKey(pubkey, "regtest"), network: NETWORK }; +} + +function hydrate(hex, prevouts) { + const tx = Tx.decode(hex); + if (!Array.isArray(prevouts) || prevouts.length !== tx.vin.length) throw new Error("Prevout set does not match transaction inputs."); + tx.vin.forEach((input, i) => { + const prevout = prevouts[i]; + if (!Number.isSafeInteger(prevout?.value) || prevout.value < 0 || !Array.isArray(prevout?.scriptPubKey)) throw new Error("Invalid signing prevout."); + input.prevout = { value: prevout.value, scriptPubKey: prevout.scriptPubKey }; + }); + return tx; +} + +function assertOwnKeyPath(me, tx, inputIndex) { + if (JSON.stringify(tx.vin[inputIndex]?.prevout?.scriptPubKey) !== JSON.stringify(["OP_1", me.pubkey])) { + throw new Error("Refusing key-path signature for an input not owned by Inscriboor."); + } +} + +async function signInscription(params) { + const me = await account(); + if (params?.network !== NETWORK) throw new Error("Inscriboor is currently limited to Super Testnet local mode."); + const commit = hydrate(params.commit.hex, params.commit.prevouts); + const reveal = hydrate(params.reveal.hex, params.reveal.prevouts); + assertOwnKeyPath(me, commit, 0); + const revealInput = Number(params.reveal.scriptInput); + if (!Number.isInteger(revealInput) || revealInput < 0 || revealInput >= reveal.vin.length) throw new Error("Invalid reveal script input."); + if (typeof params.reveal.leaf !== "string" || !/^[0-9a-f]+$/.test(params.reveal.leaf)) throw new Error("Missing reveal leaf."); + const result = { + commit: Signer.taproot.sign(me.secret, commit, 0).hex, + revealScript: Signer.taproot.sign(me.secret, reveal, revealInput, { extension: params.reveal.leaf }).hex, + revealKey: null + }; + if (revealInput === 1) { + assertOwnKeyPath(me, reveal, 0); + result.revealKey = Signer.taproot.sign(me.secret, reveal, 0).hex; + } + return result; +} + +function summary(params, origin) { + const intent = params?.intent ?? {}; + return { + origin, network: params?.network, contentType: intent.contentType ?? "unknown", size: intent.size ?? 0, + metadata: !!intent.metadata, metaprotocol: intent.metaprotocol || null, parent: intent.parent || null, + delegate: intent.delegate || null, pointer: intent.pointer ?? "automatic", reinscribe: intent.reinscribe || null, + postage: intent.postage ?? 1000, fees: intent.fees ?? 1000 + }; +} + +function finish(id, error, result) { + const request = pending.get(id); + if (!request) return; + pending.delete(id); + clearTimeout(request.timer); + try { request.port.postMessage({ id, ...(error ? { error } : { result }) }); } catch {} +} + +async function openApproval(message, port) { + const local = /^http:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/.test(message.origin); + if (message.origin !== "https://supertestnet.lol" && !local) throw new Error("This Inscriboor build only accepts Super Testnet origins."); + const id = message.id; + const timer = setTimeout(() => finish(id, "Signing request expired."), 120000); + pending.set(id, { port, params: message.params, origin: message.origin, timer }); + await chrome.windows.create({ url: chrome.runtime.getURL(`approval.html?id=${encodeURIComponent(id)}`), type:"popup", width:420, height:680, focused:true }); +} + +chrome.runtime.onConnect.addListener(port => { + if (port.name !== "inscriboor-provider") return; + port.onMessage.addListener(async message => { + try { + if (message.method === "bitcoin_getAccounts") { + const me = await account(); + port.postMessage({ id:message.id, result:[{ address:me.address, pubkey:me.pubkey, network:me.network }] }); + } else if (message.method === "bitcoin_getNetwork") { + port.postMessage({ id:message.id, result:NETWORK }); + } else if (message.method === "ord_signInscription") { + await openApproval(message, port); + } else { + port.postMessage({ id:message.id, error:`Unsupported Inscriboor method: ${message.method}` }); + } + } catch (error) { + port.postMessage({ id:message.id, error:error.message }); + } + }); + port.onDisconnect.addListener(() => { + for (const [id, request] of pending) if (request.port === port) finish(id, "Page disconnected before signing completed."); + }); +}); + +chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { + if (message?.type === "approval:get") { + const request = pending.get(message.id); + sendResponse(request ? { ok:true, summary:summary(request.params, request.origin) } : { ok:false, error:"Signing request no longer exists." }); + return; + } + if (message?.type === "approval:reject") { + finish(message.id, "User rejected signing request."); + sendResponse({ok:true}); + return; + } + if (message?.type === "approval:approve") { + const request = pending.get(message.id); + if (!request) { sendResponse({ok:false,error:"Signing request no longer exists."}); return; } + signInscription(request.params) + .then(result => { finish(message.id, null, result); sendResponse({ok:true}); }) + .catch(error => { finish(message.id, error.message); sendResponse({ok:false,error:error.message}); }); + return true; + } +}); \ No newline at end of file