Add payer-wallet override flow with ownership proof in store
This commit is contained in:
parent
8f349a899a
commit
a215a7d0f0
@ -44,6 +44,7 @@ Implemented now:
|
||||
17. Download endpoints now validate wallet membership status before authorizing channel messaging.
|
||||
18. Governance install API contract, examples, backend handoff checklist, and conformance vectors.
|
||||
19. Repo boundary blueprint and free launcher specification aligned with first paid governance model.
|
||||
20. Store UI now supports distinct payer wallet overrides with ownership-proof signing before quote requests.
|
||||
|
||||
Remaining in this repo:
|
||||
|
||||
|
||||
@ -111,6 +111,16 @@
|
||||
font-size: 11px;
|
||||
color: #3d434a;
|
||||
}
|
||||
input {
|
||||
border: 1px solid #c2c8d0;
|
||||
background: #ffffff;
|
||||
padding: 6px 8px;
|
||||
font-family: 'IBM Plex Mono', 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
color: #3d434a;
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.status-log {
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid #d0d5db;
|
||||
@ -144,10 +154,14 @@
|
||||
<p class="line">Wallet: <span class="mono" id="wallet-label">not connected</span></p>
|
||||
<p class="line">Membership status: <span class="mono" id="membership-label">unknown</span></p>
|
||||
<p class="line">Gate decision: <span class="mono" id="gate-label">blocked</span></p>
|
||||
<p class="line">Payer wallet override (optional):</p>
|
||||
<input id="payer-wallet-input" type="text" placeholder="0x... (leave blank to use ownership wallet)">
|
||||
<p class="line">Ownership proof: <span class="mono" id="proof-label">not required</span></p>
|
||||
<span class="state block" id="gate-pill">membership required</span>
|
||||
<div class="actions">
|
||||
<button id="connect-btn" type="button">connect wallet</button>
|
||||
<button id="refresh-btn" type="button">refresh state</button>
|
||||
<button id="sign-proof-btn" type="button">sign ownership proof</button>
|
||||
<label class="line" for="mock-select">mock mode:</label>
|
||||
<select id="mock-select" aria-label="Mock state override">
|
||||
<option value="">live</option>
|
||||
@ -168,7 +182,7 @@
|
||||
<p class="line" id="offer-summary">Catalog data pending.</p>
|
||||
<p class="line" id="offer-price">Price: --</p>
|
||||
<p class="line" id="offer-policy">Policy: --</p>
|
||||
<p class="line">Action chain: membership check -> quote -> wallet confirm -> entitlement receipt</p>
|
||||
<p class="line">Action chain: membership check -> ownership proof (if needed) -> quote -> wallet confirm -> entitlement receipt</p>
|
||||
<div class="actions">
|
||||
<button id="checkout-btn" type="button" disabled>request checkout quote</button>
|
||||
</div>
|
||||
@ -193,6 +207,7 @@
|
||||
|
||||
const state = {
|
||||
wallet: null,
|
||||
ownershipProof: null,
|
||||
membership: 'unknown',
|
||||
gate: false,
|
||||
source: 'live',
|
||||
@ -208,8 +223,11 @@
|
||||
const checkoutLog = document.getElementById('checkout-log');
|
||||
const connectBtn = document.getElementById('connect-btn');
|
||||
const refreshBtn = document.getElementById('refresh-btn');
|
||||
const signProofBtn = document.getElementById('sign-proof-btn');
|
||||
const checkoutBtn = document.getElementById('checkout-btn');
|
||||
const mockSelect = document.getElementById('mock-select');
|
||||
const payerWalletInput = document.getElementById('payer-wallet-input');
|
||||
const proofLabel = document.getElementById('proof-label');
|
||||
const offerSelect = document.getElementById('offer-select');
|
||||
const offerTitle = document.getElementById('offer-title');
|
||||
const offerSummary = document.getElementById('offer-summary');
|
||||
@ -221,6 +239,33 @@
|
||||
return wallet.slice(0, 6) + '...' + wallet.slice(-4);
|
||||
}
|
||||
|
||||
function normalizeWallet(value) {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
function isValidWallet(wallet) {
|
||||
return /^0x[a-fA-F0-9]{40}$/.test(wallet);
|
||||
}
|
||||
|
||||
function getPayerWallet() {
|
||||
const override = normalizeWallet(payerWalletInput.value);
|
||||
return override || state.wallet;
|
||||
}
|
||||
|
||||
function updateProofLabel() {
|
||||
const payer = getPayerWallet();
|
||||
if (!state.wallet || !payer || payer.toLowerCase() === state.wallet.toLowerCase()) {
|
||||
proofLabel.textContent = 'not required';
|
||||
return;
|
||||
}
|
||||
if (state.ownershipProof && state.ownershipProof.payer_wallet &&
|
||||
state.ownershipProof.payer_wallet.toLowerCase() === payer.toLowerCase()) {
|
||||
proofLabel.textContent = 'signed';
|
||||
return;
|
||||
}
|
||||
proofLabel.textContent = 'required';
|
||||
}
|
||||
|
||||
function setLog(message) {
|
||||
statusLog.textContent = message;
|
||||
}
|
||||
@ -294,6 +339,7 @@
|
||||
}
|
||||
|
||||
checkoutBtn.disabled = !state.gate || !state.selectedOfferId;
|
||||
updateProofLabel();
|
||||
}
|
||||
|
||||
function getMockFromQuery() {
|
||||
@ -404,6 +450,7 @@
|
||||
throw new Error('Wallet connection not approved.');
|
||||
}
|
||||
state.wallet = accounts[0];
|
||||
state.ownershipProof = null;
|
||||
setLog('Wallet connected: ' + abbreviateWallet(state.wallet) + '.');
|
||||
await refreshMembershipState();
|
||||
} catch (err) {
|
||||
@ -411,6 +458,62 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function signOwnershipProof() {
|
||||
if (!state.wallet) {
|
||||
setLog('Connect ownership wallet first.');
|
||||
return;
|
||||
}
|
||||
if (!window.ethereum) {
|
||||
setLog('No wallet provider detected for signing.');
|
||||
return;
|
||||
}
|
||||
|
||||
const payer = getPayerWallet();
|
||||
if (!payer) {
|
||||
setLog('Payer wallet is empty.');
|
||||
return;
|
||||
}
|
||||
if (!isValidWallet(payer)) {
|
||||
setLog('Payer wallet format is invalid.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (payer.toLowerCase() === state.wallet.toLowerCase()) {
|
||||
state.ownershipProof = null;
|
||||
updateProofLabel();
|
||||
setLog('Ownership proof is not required when payer equals ownership wallet.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const selectedOffer = state.selectedOfferId || 'none';
|
||||
const issuedAt = new Date().toISOString();
|
||||
const message = [
|
||||
'EDUT OWNERSHIP PROOF',
|
||||
'Ownership wallet: ' + state.wallet,
|
||||
'Payer wallet: ' + payer,
|
||||
'Offer: ' + selectedOffer,
|
||||
'Issued at: ' + issuedAt,
|
||||
].join('\\n');
|
||||
|
||||
const signature = await window.ethereum.request({
|
||||
method: 'personal_sign',
|
||||
params: [message, state.wallet],
|
||||
});
|
||||
|
||||
state.ownershipProof = {
|
||||
payer_wallet: payer,
|
||||
signature: signature,
|
||||
message: message,
|
||||
issued_at: issuedAt,
|
||||
};
|
||||
updateProofLabel();
|
||||
setLog('Ownership proof signed for payer wallet override.');
|
||||
} catch (err) {
|
||||
setLog('Ownership proof signing failed: ' + err.message + '.');
|
||||
}
|
||||
}
|
||||
|
||||
async function requestCheckoutQuote() {
|
||||
if (!state.gate) {
|
||||
setCheckoutLog('Checkout blocked: membership is not active.');
|
||||
@ -422,26 +525,49 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const payerWallet = getPayerWallet();
|
||||
if (!payerWallet || !isValidWallet(payerWallet)) {
|
||||
setCheckoutLog('Checkout blocked: payer wallet is invalid.');
|
||||
return;
|
||||
}
|
||||
|
||||
const usingDistinctPayer = payerWallet.toLowerCase() !== state.wallet.toLowerCase();
|
||||
if (usingDistinctPayer) {
|
||||
if (!state.ownershipProof ||
|
||||
!state.ownershipProof.signature ||
|
||||
!state.ownershipProof.payer_wallet ||
|
||||
state.ownershipProof.payer_wallet.toLowerCase() !== payerWallet.toLowerCase()) {
|
||||
setCheckoutLog('Checkout blocked: sign ownership proof for the payer wallet override.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setCheckoutLog('Requesting quote...');
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
wallet: state.wallet,
|
||||
offer_id: state.selectedOfferId,
|
||||
};
|
||||
if (usingDistinctPayer) {
|
||||
payload.payer_wallet = payerWallet;
|
||||
payload.ownership_proof = state.ownershipProof.signature;
|
||||
}
|
||||
|
||||
const response = await fetch('/marketplace/checkout/quote', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
wallet: state.wallet,
|
||||
offer_id: state.selectedOfferId,
|
||||
}),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP ' + response.status);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
const quoteId = payload.quote_id || 'unknown';
|
||||
const amount = payload.amount || payload.amount_atomic || 'unknown';
|
||||
const currency = payload.currency || 'unknown';
|
||||
const quotePayload = await response.json();
|
||||
const quoteId = quotePayload.quote_id || 'unknown';
|
||||
const amount = quotePayload.amount || quotePayload.amount_atomic || 'unknown';
|
||||
const currency = quotePayload.currency || 'unknown';
|
||||
setCheckoutLog('Quote ready for ' + state.selectedOfferId + ': ' + quoteId + ' (' + amount + ' ' + currency + ').');
|
||||
} catch (err) {
|
||||
setCheckoutLog('Quote request failed: ' + err.message + '. API wiring pending.');
|
||||
@ -450,10 +576,16 @@
|
||||
|
||||
connectBtn.addEventListener('click', connectWallet);
|
||||
refreshBtn.addEventListener('click', refreshMembershipState);
|
||||
signProofBtn.addEventListener('click', signOwnershipProof);
|
||||
checkoutBtn.addEventListener('click', requestCheckoutQuote);
|
||||
mockSelect.addEventListener('change', refreshMembershipState);
|
||||
payerWalletInput.addEventListener('input', function () {
|
||||
state.ownershipProof = null;
|
||||
updateProofLabel();
|
||||
});
|
||||
offerSelect.addEventListener('change', function () {
|
||||
state.selectedOfferId = offerSelect.value;
|
||||
state.ownershipProof = null;
|
||||
renderSelectedOffer();
|
||||
applyGateState();
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user