Add injected-wallet signing and tx automation to launcher shell
This commit is contained in:
parent
031ab2098c
commit
3b7e6cd5bc
@ -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:
|
||||
|
||||
|
||||
178
app/app.js
178
app/app.js
@ -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);
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user