Add Super Testnet signing provider
This commit is contained in:
1 parent
05d6cd934d
commit
acd61f11bc
1 file changed
+127
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user