Add injected-wallet signing and tx automation to launcher shell

This commit is contained in:
Joshua 2026-02-18 07:05:24 -08:00
parent 031ab2098c
commit 3b7e6cd5bc
3 changed files with 199 additions and 13 deletions

View File

@ -28,6 +28,14 @@ Launcher never contains private kernel internals. It verifies and installs signe
3. Member channel register/poll/ack/support
4. Governance install token/confirm/status
5. Lease heartbeat + offline renew
6. Injected wallet automation for intent signing and membership tx send
Wallet automation shortcuts in the shell:
1. `Connect wallet` fills address from `window.ethereum`.
2. `Sign intent (EIP-712)` signs the current intent payload and fills `walletSignature`.
3. `Sign payer proof` signs distinct-payer ownership proof and fills `payerProof`.
4. `Send membership tx` submits the quote transaction via `eth_sendTransaction` and fills `confirmTxHash`.
Run locally:

View File

@ -4,6 +4,8 @@ const $ = (id) => document.getElementById(id);
const state = {
eventMap: new Map(),
lastIntent: null,
lastQuote: null,
};
function nowISO() {
@ -22,6 +24,28 @@ function chainID() {
return Number($("chainId").value || 0);
}
function normalizedAddress(value) {
return String(value || "").trim().toLowerCase();
}
function injectedProvider() {
return globalThis.ethereum || null;
}
async function requireProvider() {
const provider = injectedProvider();
if (!provider) {
throw new Error("no injected wallet provider found (expected window.ethereum)");
}
return provider;
}
function utf8ToHex(value) {
return `0x${Array.from(new TextEncoder().encode(String(value)))
.map((b) => b.toString(16).padStart(2, "0"))
.join("")}`;
}
function logLine(label, payload) {
const log = $("log");
const line = `[${nowISO()}] ${label}\n${JSON.stringify(payload, null, 2)}\n\n`;
@ -121,6 +145,154 @@ function renderEvents(events) {
}
}
function buildIntentTypedData(intent, origin) {
return {
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
],
DesignationIntent: [
{ name: "designationCode", type: "string" },
{ name: "designationToken", type: "string" },
{ name: "nonce", type: "string" },
{ name: "issuedAt", type: "string" },
{ name: "origin", type: "string" },
],
},
primaryType: "DesignationIntent",
domain: {
name: intent.domain_name,
version: "1",
chainId: Number(intent.chain_id),
verifyingContract: intent.verifying_contract,
},
message: {
designationCode: intent.designation_code,
designationToken: intent.display_token,
nonce: intent.nonce,
issuedAt: intent.issued_at,
origin,
},
};
}
async function onConnectWallet() {
const provider = await requireProvider();
const accounts = await provider.request({ method: "eth_requestAccounts" });
if (!accounts || accounts.length === 0) {
throw new Error("wallet provider returned no accounts");
}
$("walletAddress").value = normalizedAddress(accounts[0]);
const chainHex = await provider.request({ method: "eth_chainId" });
const providerChainID = Number.parseInt(chainHex, 16);
if (Number.isFinite(providerChainID) && providerChainID !== chainID()) {
logLine("wallet chain mismatch", {
configured_chain_id: chainID(),
provider_chain_id: providerChainID,
provider_chain_hex: chainHex,
});
} else {
logLine("wallet connected", {
wallet: $("walletAddress").value,
provider_chain_id: providerChainID,
});
}
}
async function onSignIntent() {
if (!state.lastIntent) {
throw new Error("create intent before signing");
}
const provider = await requireProvider();
const owner = requireWallet();
const typedData = buildIntentTypedData(state.lastIntent, $("walletOrigin").value.trim() || "https://edut.ai");
const typedDataJSON = JSON.stringify(typedData);
let signature = "";
try {
signature = await provider.request({
method: "eth_signTypedData_v4",
params: [owner, typedDataJSON],
});
} catch (primaryErr) {
signature = await provider.request({
method: "eth_signTypedData",
params: [owner, typedData],
});
logLine("wallet sign intent fallback", { warning: String(primaryErr) });
}
$("walletSignature").value = signature;
logLine("wallet sign intent", {
wallet: owner,
designation_code: state.lastIntent.designation_code,
signature,
});
}
async function onSignPayerProof() {
const provider = await requireProvider();
const owner = requireWallet();
const payer = normalizedAddress($("payerWallet").value);
if (!payer) {
throw new Error("payer wallet is required to sign payer proof");
}
const designationCode = $("designationCode").value.trim();
if (!designationCode) {
throw new Error("designation code is required");
}
const message = `EDUT-PAYER-AUTH:${designationCode}:${normalizedAddress(owner)}:${payer}:${chainID()}`;
const messageHex = utf8ToHex(message);
let signature = "";
try {
signature = await provider.request({
method: "personal_sign",
params: [messageHex, owner],
});
} catch (primaryErr) {
signature = await provider.request({
method: "personal_sign",
params: [owner, messageHex],
});
logLine("wallet payer proof fallback", { warning: String(primaryErr) });
}
$("payerProof").value = signature;
logLine("wallet signed payer proof", {
wallet: owner,
payer_wallet: payer,
designation_code: designationCode,
signature,
});
}
async function onSendMembershipTx() {
if (!state.lastQuote || !state.lastQuote.tx) {
throw new Error("request quote before sending transaction");
}
const provider = await requireProvider();
const from = normalizedAddress(state.lastQuote.tx.from || requireWallet());
if (from !== normalizedAddress(requireWallet())) {
throw new Error(`active wallet ${requireWallet()} does not match quote payer ${from}`);
}
const txRequest = {
from,
to: state.lastQuote.tx.to,
data: state.lastQuote.tx.data,
value: state.lastQuote.tx.value || "0x0",
};
const txHash = await provider.request({
method: "eth_sendTransaction",
params: [txRequest],
});
$("confirmTxHash").value = txHash;
logLine("membership tx sent", {
quote_id: state.lastQuote.quote_id,
tx_hash: txHash,
payer_wallet: from,
});
}
async function onIntent() {
const payload = {
address: requireWallet(),
@ -129,6 +301,7 @@ async function onIntent() {
chain_id: chainID(),
};
const out = await request("POST", "/secret/wallet/intent", payload);
state.lastIntent = out;
$("intentId").value = out.intent_id || "";
$("designationCode").value = out.designation_code || "";
$("displayToken").value = out.display_token || "";
@ -178,6 +351,7 @@ async function onQuote() {
payload.sponsor_org_root_id = sponsorOrgRoot;
}
const out = await request("POST", "/secret/membership/quote", payload);
state.lastQuote = out;
$("quoteId").value = out.quote_id || "";
$("quoteValue").value = out.value || "";
$("quotePayer").value = out.payer_wallet || "";
@ -325,10 +499,14 @@ function bind(id, handler) {
});
}
bind("btnConnectWallet", onConnectWallet);
bind("btnIntent", onIntent);
bind("btnSignIntent", onSignIntent);
bind("btnVerify", onVerify);
bind("btnStatus", onStatus);
bind("btnQuote", onQuote);
bind("btnSignPayerProof", onSignPayerProof);
bind("btnSendMembershipTx", onSendMembershipTx);
bind("btnConfirmMembership", onConfirmMembership);
bind("btnRegisterChannel", onRegisterChannel);
bind("btnUnregisterChannel", onUnregisterChannel);

View File

@ -29,6 +29,13 @@
<section class="panel">
<h2>Wallet Intent</h2>
<div class="actions">
<button id="btnConnectWallet">Connect wallet</button>
<button id="btnIntent">Create intent</button>
<button id="btnSignIntent">Sign intent (EIP-712)</button>
<button id="btnVerify">Verify signature</button>
<button id="btnStatus">Membership status</button>
</div>
<div class="grid three">
<label>
Wallet
@ -43,10 +50,6 @@
<input id="walletLocale" value="en" />
</label>
</div>
<div class="actions">
<button id="btnIntent">Create intent</button>
<button id="btnStatus">Membership status</button>
</div>
<div class="grid three">
<label>
Intent ID
@ -65,13 +68,16 @@
Signature (EIP-712)
<textarea id="walletSignature" rows="2" placeholder="0x..."></textarea>
</label>
<div class="actions">
<button id="btnVerify">Verify signature</button>
</div>
</section>
<section class="panel">
<h2>Membership Quote + Confirm</h2>
<div class="actions">
<button id="btnQuote">Get quote</button>
<button id="btnSignPayerProof">Sign payer proof</button>
<button id="btnSendMembershipTx">Send membership tx</button>
<button id="btnConfirmMembership">Confirm membership tx</button>
</div>
<div class="grid three">
<label>
Payer wallet (optional)
@ -86,9 +92,6 @@
<input id="sponsorOrgRoot" placeholder="org_root_id" />
</label>
</div>
<div class="actions">
<button id="btnQuote">Get quote</button>
</div>
<div class="grid three">
<label>
Quote ID
@ -107,9 +110,6 @@
Confirm tx hash
<input id="confirmTxHash" placeholder="0x..." />
</label>
<div class="actions">
<button id="btnConfirmMembership">Confirm membership tx</button>
</div>
</section>
<section class="panel">