feat(launcher): add one-click membership flow automation
This commit is contained in:
parent
3b7e6cd5bc
commit
06e48d7aa4
@ -29,6 +29,7 @@ Launcher never contains private kernel internals. It verifies and installs signe
|
||||
4. Governance install token/confirm/status
|
||||
5. Lease heartbeat + offline renew
|
||||
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:
|
||||
|
||||
|
||||
89
app/app.js
89
app/app.js
@ -52,6 +52,17 @@ function logLine(label, payload) {
|
||||
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) {
|
||||
const opts = {
|
||||
method,
|
||||
@ -330,6 +341,7 @@ async function onStatus() {
|
||||
$("designationCode").value = out.designation_code;
|
||||
}
|
||||
logLine("membership status", out);
|
||||
return out;
|
||||
}
|
||||
|
||||
async function onQuote() {
|
||||
@ -356,6 +368,7 @@ async function onQuote() {
|
||||
$("quoteValue").value = out.value || "";
|
||||
$("quotePayer").value = out.payer_wallet || "";
|
||||
logLine("membership quote", out);
|
||||
return out;
|
||||
}
|
||||
|
||||
async function onConfirmMembership() {
|
||||
@ -367,6 +380,81 @@ async function onConfirmMembership() {
|
||||
chain_id: chainID(),
|
||||
});
|
||||
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() {
|
||||
@ -500,6 +588,7 @@ function bind(id, handler) {
|
||||
}
|
||||
|
||||
bind("btnConnectWallet", onConnectWallet);
|
||||
bind("btnRunMembershipFlow", onRunMembershipFlow);
|
||||
bind("btnIntent", onIntent);
|
||||
bind("btnSignIntent", onSignIntent);
|
||||
bind("btnVerify", onVerify);
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
<div class="grid two">
|
||||
<label>
|
||||
API base URL
|
||||
<input id="apiBase" value="http://127.0.0.1:8080" />
|
||||
<input id="apiBase" value="https://api.edut.ai" />
|
||||
</label>
|
||||
<label>
|
||||
Chain ID
|
||||
@ -30,12 +30,14 @@
|
||||
<section class="panel">
|
||||
<h2>Wallet Intent</h2>
|
||||
<div class="actions">
|
||||
<button id="btnRunMembershipFlow">Run membership flow</button>
|
||||
<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>
|
||||
<p id="flowStatus" class="flow-status">flow idle</p>
|
||||
<div class="grid three">
|
||||
<label>
|
||||
Wallet
|
||||
|
||||
@ -101,6 +101,12 @@ textarea {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.flow-status {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12px;
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
button {
|
||||
border: 1px solid #2d7f4a;
|
||||
background: #173325;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user