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
|
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:
|
||||||
|
|
||||||
|
|||||||
89
app/app.js
89
app/app.js
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user