Add marketplace quote-send-confirm transaction flow controls

This commit is contained in:
Joshua 2026-02-18 13:29:31 -08:00
parent b1fb4706a8
commit 89fcd2be14
3 changed files with 73 additions and 4 deletions

View File

@ -37,7 +37,7 @@ Advanced integration controls (collapsible):
1. API/chain connection settings 1. API/chain connection settings
2. Wallet intent + verify primitives 2. Wallet intent + verify primitives
3. Membership quote + confirm primitives 3. Membership quote + confirm primitives
4. Marketplace offer list + checkout quote/confirm primitives 4. Marketplace offer list + checkout quote/send/confirm primitives
5. Member channel register/poll primitives 5. Member channel register/poll primitives
6. Governance install + lease primitives 6. Governance install + lease primitives
7. Raw response log for deterministic troubleshooting 7. Raw response log for deterministic troubleshooting

View File

@ -421,11 +421,11 @@ async function waitForTxMined(txHash, timeoutMs = 120000, intervalMs = 3000) {
if (statusHex === "0x1" || statusHex === "1") { if (statusHex === "0x1" || statusHex === "1") {
return receipt; return receipt;
} }
throw new Error(`membership transaction reverted: status=${receipt.status}`); throw new Error(`transaction reverted: status=${receipt.status}`);
} }
await sleep(intervalMs); await sleep(intervalMs);
} }
throw new Error(`membership transaction not mined within ${timeoutMs}ms`); throw new Error(`transaction not mined within ${timeoutMs}ms`);
} }
async function confirmMembershipWithRetry(maxAttempts = 8, intervalMs = 2500) { async function confirmMembershipWithRetry(maxAttempts = 8, intervalMs = 2500) {
@ -598,7 +598,7 @@ async function onCheckoutQuote() {
org_root_id: orgRootID(), org_root_id: orgRootID(),
principal_id: principalID(), principal_id: principalID(),
principal_role: principalRole(), principal_role: principalRole(),
include_membership_if_missing: true, include_membership_if_missing: false,
}; };
const payerWallet = $("payerWallet").value.trim(); const payerWallet = $("payerWallet").value.trim();
const payerProof = $("payerProof").value.trim(); const payerProof = $("payerProof").value.trim();
@ -616,6 +616,33 @@ async function onCheckoutQuote() {
return out; return out;
} }
async function onSendCheckoutTx() {
if (!state.lastCheckoutQuote || !state.lastCheckoutQuote.tx) {
throw new Error("request checkout quote before sending transaction");
}
const provider = await requireProvider();
const from = normalizedAddress(state.lastCheckoutQuote.tx.from || requireWallet());
if (from !== normalizedAddress(requireWallet())) {
throw new Error(`active wallet ${requireWallet()} does not match checkout payer ${from}`);
}
const txRequest = {
from,
to: state.lastCheckoutQuote.tx.to,
data: state.lastCheckoutQuote.tx.data,
value: state.lastCheckoutQuote.tx.value || "0x0",
};
const txHash = await provider.request({
method: "eth_sendTransaction",
params: [txRequest],
});
$("checkoutTxHash").value = txHash;
logLine("marketplace tx sent", {
quote_id: state.lastCheckoutQuote.quote_id,
tx_hash: txHash,
payer_wallet: from,
});
}
async function onCheckoutConfirm() { async function onCheckoutConfirm() {
const quoteID = $("checkoutQuoteId").value.trim(); const quoteID = $("checkoutQuoteId").value.trim();
if (!quoteID) { if (!quoteID) {
@ -641,6 +668,44 @@ async function onCheckoutConfirm() {
return out; return out;
} }
async function confirmCheckoutWithRetry(maxAttempts = 8, intervalMs = 2500) {
let lastErr = null;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
try {
const out = await onCheckoutConfirm();
return out;
} catch (err) {
lastErr = err;
const message = String(err || "");
if (!message.includes("tx verification pending/failed") && !message.includes("membership verification pending/failed")) {
throw err;
}
setFlowStatus(`checkout confirm pending (${attempt}/${maxAttempts})`);
await sleep(intervalMs);
}
}
throw lastErr || new Error("checkout confirm failed");
}
async function onRunCheckoutFlow() {
setFlowStatus("quoting checkout");
await onCheckoutQuote();
setFlowStatus("sending checkout transaction");
await onSendCheckoutTx();
const txHash = $("checkoutTxHash").value.trim();
if (!txHash) {
throw new Error("missing checkout tx hash after send");
}
setFlowStatus("waiting for checkout chain confirmation");
await waitForTxMined(txHash);
setFlowStatus("confirming checkout with API");
await confirmCheckoutWithRetry();
setFlowStatus("refreshing entitlements");
await onListEntitlements();
setFlowStatus("checkout flow complete");
}
async function onListEntitlements() { async function onListEntitlements() {
const out = await request("GET", `/marketplace/entitlements?wallet=${encodeURIComponent(requireWallet())}`); const out = await request("GET", `/marketplace/entitlements?wallet=${encodeURIComponent(requireWallet())}`);
logLine("marketplace entitlements", out); logLine("marketplace entitlements", out);
@ -755,7 +820,9 @@ bind("btnLeaseHeartbeat", onLeaseHeartbeat);
bind("btnOfflineRenew", onOfflineRenew); bind("btnOfflineRenew", onOfflineRenew);
bind("btnListOffers", onListOffers); bind("btnListOffers", onListOffers);
bind("btnCheckoutQuote", onCheckoutQuote); bind("btnCheckoutQuote", onCheckoutQuote);
bind("btnSendCheckoutTx", onSendCheckoutTx);
bind("btnCheckoutConfirm", onCheckoutConfirm); bind("btnCheckoutConfirm", onCheckoutConfirm);
bind("btnRunCheckoutFlow", onRunCheckoutFlow);
bind("btnListEntitlements", onListEntitlements); bind("btnListEntitlements", onListEntitlements);
logLine("launcher shell ready", { logLine("launcher shell ready", {

View File

@ -171,7 +171,9 @@
<h2>Marketplace Checkout</h2> <h2>Marketplace Checkout</h2>
<div class="actions"> <div class="actions">
<button id="btnListOffers">List offers</button> <button id="btnListOffers">List offers</button>
<button id="btnRunCheckoutFlow">Run checkout flow</button>
<button id="btnCheckoutQuote">Checkout quote</button> <button id="btnCheckoutQuote">Checkout quote</button>
<button id="btnSendCheckoutTx">Send checkout tx</button>
<button id="btnCheckoutConfirm">Checkout confirm</button> <button id="btnCheckoutConfirm">Checkout confirm</button>
<button id="btnListEntitlements">List entitlements</button> <button id="btnListEntitlements">List entitlements</button>
</div> </div>