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
|
3. Member channel register/poll/ack/support
|
||||||
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
|
||||||
|
|
||||||
|
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:
|
Run locally:
|
||||||
|
|
||||||
|
|||||||
178
app/app.js
178
app/app.js
@ -4,6 +4,8 @@ const $ = (id) => document.getElementById(id);
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
eventMap: new Map(),
|
eventMap: new Map(),
|
||||||
|
lastIntent: null,
|
||||||
|
lastQuote: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function nowISO() {
|
function nowISO() {
|
||||||
@ -22,6 +24,28 @@ function chainID() {
|
|||||||
return Number($("chainId").value || 0);
|
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) {
|
function logLine(label, payload) {
|
||||||
const log = $("log");
|
const log = $("log");
|
||||||
const line = `[${nowISO()}] ${label}\n${JSON.stringify(payload, null, 2)}\n\n`;
|
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() {
|
async function onIntent() {
|
||||||
const payload = {
|
const payload = {
|
||||||
address: requireWallet(),
|
address: requireWallet(),
|
||||||
@ -129,6 +301,7 @@ async function onIntent() {
|
|||||||
chain_id: chainID(),
|
chain_id: chainID(),
|
||||||
};
|
};
|
||||||
const out = await request("POST", "/secret/wallet/intent", payload);
|
const out = await request("POST", "/secret/wallet/intent", payload);
|
||||||
|
state.lastIntent = out;
|
||||||
$("intentId").value = out.intent_id || "";
|
$("intentId").value = out.intent_id || "";
|
||||||
$("designationCode").value = out.designation_code || "";
|
$("designationCode").value = out.designation_code || "";
|
||||||
$("displayToken").value = out.display_token || "";
|
$("displayToken").value = out.display_token || "";
|
||||||
@ -178,6 +351,7 @@ async function onQuote() {
|
|||||||
payload.sponsor_org_root_id = sponsorOrgRoot;
|
payload.sponsor_org_root_id = sponsorOrgRoot;
|
||||||
}
|
}
|
||||||
const out = await request("POST", "/secret/membership/quote", payload);
|
const out = await request("POST", "/secret/membership/quote", payload);
|
||||||
|
state.lastQuote = out;
|
||||||
$("quoteId").value = out.quote_id || "";
|
$("quoteId").value = out.quote_id || "";
|
||||||
$("quoteValue").value = out.value || "";
|
$("quoteValue").value = out.value || "";
|
||||||
$("quotePayer").value = out.payer_wallet || "";
|
$("quotePayer").value = out.payer_wallet || "";
|
||||||
@ -325,10 +499,14 @@ function bind(id, handler) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bind("btnConnectWallet", onConnectWallet);
|
||||||
bind("btnIntent", onIntent);
|
bind("btnIntent", onIntent);
|
||||||
|
bind("btnSignIntent", onSignIntent);
|
||||||
bind("btnVerify", onVerify);
|
bind("btnVerify", onVerify);
|
||||||
bind("btnStatus", onStatus);
|
bind("btnStatus", onStatus);
|
||||||
bind("btnQuote", onQuote);
|
bind("btnQuote", onQuote);
|
||||||
|
bind("btnSignPayerProof", onSignPayerProof);
|
||||||
|
bind("btnSendMembershipTx", onSendMembershipTx);
|
||||||
bind("btnConfirmMembership", onConfirmMembership);
|
bind("btnConfirmMembership", onConfirmMembership);
|
||||||
bind("btnRegisterChannel", onRegisterChannel);
|
bind("btnRegisterChannel", onRegisterChannel);
|
||||||
bind("btnUnregisterChannel", onUnregisterChannel);
|
bind("btnUnregisterChannel", onUnregisterChannel);
|
||||||
|
|||||||
@ -29,6 +29,13 @@
|
|||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Wallet Intent</h2>
|
<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">
|
<div class="grid three">
|
||||||
<label>
|
<label>
|
||||||
Wallet
|
Wallet
|
||||||
@ -43,10 +50,6 @@
|
|||||||
<input id="walletLocale" value="en" />
|
<input id="walletLocale" value="en" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
|
||||||
<button id="btnIntent">Create intent</button>
|
|
||||||
<button id="btnStatus">Membership status</button>
|
|
||||||
</div>
|
|
||||||
<div class="grid three">
|
<div class="grid three">
|
||||||
<label>
|
<label>
|
||||||
Intent ID
|
Intent ID
|
||||||
@ -65,13 +68,16 @@
|
|||||||
Signature (EIP-712)
|
Signature (EIP-712)
|
||||||
<textarea id="walletSignature" rows="2" placeholder="0x..."></textarea>
|
<textarea id="walletSignature" rows="2" placeholder="0x..."></textarea>
|
||||||
</label>
|
</label>
|
||||||
<div class="actions">
|
|
||||||
<button id="btnVerify">Verify signature</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Membership Quote + Confirm</h2>
|
<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">
|
<div class="grid three">
|
||||||
<label>
|
<label>
|
||||||
Payer wallet (optional)
|
Payer wallet (optional)
|
||||||
@ -86,9 +92,6 @@
|
|||||||
<input id="sponsorOrgRoot" placeholder="org_root_id" />
|
<input id="sponsorOrgRoot" placeholder="org_root_id" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
|
||||||
<button id="btnQuote">Get quote</button>
|
|
||||||
</div>
|
|
||||||
<div class="grid three">
|
<div class="grid three">
|
||||||
<label>
|
<label>
|
||||||
Quote ID
|
Quote ID
|
||||||
@ -107,9 +110,6 @@
|
|||||||
Confirm tx hash
|
Confirm tx hash
|
||||||
<input id="confirmTxHash" placeholder="0x..." />
|
<input id="confirmTxHash" placeholder="0x..." />
|
||||||
</label>
|
</label>
|
||||||
<div class="actions">
|
|
||||||
<button id="btnConfirmMembership">Confirm membership tx</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user