feat(launcher): add one-click membership flow automation

This commit is contained in:
Joshua 2026-02-18 09:08:08 -08:00
parent 3b7e6cd5bc
commit 06e48d7aa4
4 changed files with 99 additions and 1 deletions

View File

@ -29,6 +29,7 @@ Launcher never contains private kernel internals. It verifies and installs signe
4. Governance install token/confirm/status 4. Governance install token/confirm/status
5. Lease heartbeat + offline renew 5. Lease heartbeat + offline renew
6. Injected wallet automation for intent signing and membership tx send 6. Injected wallet automation for intent signing and membership tx send
7. One-click `Run membership flow` path (intent -> verify -> quote -> tx -> confirm)
Wallet automation shortcuts in the shell: Wallet automation shortcuts in the shell:

View File

@ -52,6 +52,17 @@ function logLine(label, payload) {
log.textContent = line + log.textContent; log.textContent = line + log.textContent;
} }
function setFlowStatus(message) {
const el = $("flowStatus");
if (el) {
el.textContent = message;
}
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function request(method, path, body) { async function request(method, path, body) {
const opts = { const opts = {
method, method,
@ -330,6 +341,7 @@ async function onStatus() {
$("designationCode").value = out.designation_code; $("designationCode").value = out.designation_code;
} }
logLine("membership status", out); logLine("membership status", out);
return out;
} }
async function onQuote() { async function onQuote() {
@ -356,6 +368,7 @@ async function onQuote() {
$("quoteValue").value = out.value || ""; $("quoteValue").value = out.value || "";
$("quotePayer").value = out.payer_wallet || ""; $("quotePayer").value = out.payer_wallet || "";
logLine("membership quote", out); logLine("membership quote", out);
return out;
} }
async function onConfirmMembership() { async function onConfirmMembership() {
@ -367,6 +380,81 @@ async function onConfirmMembership() {
chain_id: chainID(), chain_id: chainID(),
}); });
logLine("membership confirm", out); logLine("membership confirm", out);
return out;
}
async function waitForTxMined(txHash, timeoutMs = 120000, intervalMs = 3000) {
const provider = await requireProvider();
const started = Date.now();
while (Date.now() - started < timeoutMs) {
const receipt = await provider.request({
method: "eth_getTransactionReceipt",
params: [txHash],
});
if (receipt) {
const statusHex = String(receipt.status || "").toLowerCase();
if (statusHex === "0x1" || statusHex === "1") {
return receipt;
}
throw new Error(`membership transaction reverted: status=${receipt.status}`);
}
await sleep(intervalMs);
}
throw new Error(`membership transaction not mined within ${timeoutMs}ms`);
}
async function confirmMembershipWithRetry(maxAttempts = 8, intervalMs = 2500) {
let lastErr = null;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
try {
const out = await onConfirmMembership();
return out;
} catch (err) {
lastErr = err;
const message = String(err || "");
if (!message.includes("tx verification pending/failed")) {
throw err;
}
setFlowStatus(`confirm pending (${attempt}/${maxAttempts})`);
await sleep(intervalMs);
}
}
throw lastErr || new Error("membership confirm failed");
}
async function onRunMembershipFlow() {
setFlowStatus("connecting wallet");
await onConnectWallet();
setFlowStatus("checking membership");
const status = await onStatus();
if (String(status.status || "").toLowerCase() === "active") {
setFlowStatus("membership already active");
return;
}
setFlowStatus("creating intent");
await onIntent();
setFlowStatus("signing intent");
await onSignIntent();
setFlowStatus("verifying intent");
await onVerify();
setFlowStatus("quoting membership");
await onQuote();
setFlowStatus("sending membership transaction");
await onSendMembershipTx();
const txHash = $("confirmTxHash").value.trim();
if (!txHash) {
throw new Error("missing tx hash after send");
}
setFlowStatus("waiting for chain confirmation");
await waitForTxMined(txHash);
setFlowStatus("confirming membership with API");
await confirmMembershipWithRetry();
setFlowStatus("refreshing status");
await onStatus();
setFlowStatus("membership flow complete");
} }
async function onRegisterChannel() { async function onRegisterChannel() {
@ -500,6 +588,7 @@ function bind(id, handler) {
} }
bind("btnConnectWallet", onConnectWallet); bind("btnConnectWallet", onConnectWallet);
bind("btnRunMembershipFlow", onRunMembershipFlow);
bind("btnIntent", onIntent); bind("btnIntent", onIntent);
bind("btnSignIntent", onSignIntent); bind("btnSignIntent", onSignIntent);
bind("btnVerify", onVerify); bind("btnVerify", onVerify);

View File

@ -18,7 +18,7 @@
<div class="grid two"> <div class="grid two">
<label> <label>
API base URL API base URL
<input id="apiBase" value="http://127.0.0.1:8080" /> <input id="apiBase" value="https://api.edut.ai" />
</label> </label>
<label> <label>
Chain ID Chain ID
@ -30,12 +30,14 @@
<section class="panel"> <section class="panel">
<h2>Wallet Intent</h2> <h2>Wallet Intent</h2>
<div class="actions"> <div class="actions">
<button id="btnRunMembershipFlow">Run membership flow</button>
<button id="btnConnectWallet">Connect wallet</button> <button id="btnConnectWallet">Connect wallet</button>
<button id="btnIntent">Create intent</button> <button id="btnIntent">Create intent</button>
<button id="btnSignIntent">Sign intent (EIP-712)</button> <button id="btnSignIntent">Sign intent (EIP-712)</button>
<button id="btnVerify">Verify signature</button> <button id="btnVerify">Verify signature</button>
<button id="btnStatus">Membership status</button> <button id="btnStatus">Membership status</button>
</div> </div>
<p id="flowStatus" class="flow-status">flow idle</p>
<div class="grid three"> <div class="grid three">
<label> <label>
Wallet Wallet

View File

@ -101,6 +101,12 @@ textarea {
margin: 10px 0; margin: 10px 0;
} }
.flow-status {
margin: 0 0 6px;
font-size: 12px;
color: var(--warn);
}
button { button {
border: 1px solid #2d7f4a; border: 1px solid #2d7f4a;
background: #173325; background: #173325;