Files
instantadpay/public/assets/wallet.js
T
martbost 85a6ac5bb0 Wallet: fix Trust Wallet chain-add + personal_sign ("invalid method parameters")
- wallet_addEthereumChain omits blockExplorerUrls when the config explorer is
  empty (rehearsal has none) — an empty-string URL made Trust Wallet reject the
  add with "invalid method parameters", blocking new users from adding the chain.
- personal_sign now hex-encodes the SIWE message (Trust Wallet requires hex;
  MetaMask took raw). Signed bytes are identical so server recovery is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-07 06:58:12 -05:00

83 lines
4.1 KiB
JavaScript

// Wallet plumbing: EIP-1193 connect, chain add/switch, SIWE sign-in, and raw
// calldata builders for the contract's tx functions (no library needed).
window.IAPWallet = (function () {
const SEL_BUY = '0xfd095e97'; // buy(uint32,uint32)
const SEL_ACTIVATE = '0x1a93ec95'; // activate(uint32)
const pad = v => BigInt(v).toString(16).padStart(64, '0');
function eth() {
if (!window.ethereum) throw new Error('No wallet found. Open this page in a browser with MetaMask (or a wallet browser).');
return window.ethereum;
}
async function ensureChain(c) {
const want = '0x' + Number(c.chainId).toString(16);
const cur = await eth().request({ method: 'eth_chainId' });
if (cur === want) return;
try {
await eth().request({ method: 'wallet_switchEthereumChain', params: [{ chainId: want }] });
} catch (e) {
if (e.code !== 4902) throw e;
const addParams = {
chainId: want, chainName: c.chainName, nativeCurrency: { name: 'POL', symbol: 'POL', decimals: 18 },
rpcUrls: [c.rpc] };
// only include a block explorer when it's a real URL — an empty string here
// makes wallets (Trust Wallet especially) reject with "invalid method parameters"
if (c.explorer && /^https?:\/\//i.test(c.explorer)) addParams.blockExplorerUrls = [c.explorer];
await eth().request({ method: 'wallet_addEthereumChain', params: [addParams] });
}
}
async function connect() {
const c = await IAP.getConfig();
const [addr] = await eth().request({ method: 'eth_requestAccounts' });
await ensureChain(c);
return addr;
}
// SIWE: challenge -> personal_sign -> verify (server sets the session cookie)
async function signIn() {
const addr = await connect();
const ch = await (await fetch('/api/auth/challenge', { method: 'POST',
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: addr }) })).json();
if (ch.error) throw new Error(ch.error);
// hex-encode the message: MetaMask accepts a raw string, but Trust Wallet and
// others require hex for personal_sign (else "invalid method parameters").
// The signed bytes are identical, so server-side recovery is unchanged.
const hexMsg = '0x' + Array.from(new TextEncoder().encode(ch.message)).map(b => b.toString(16).padStart(2, '0')).join('');
const sig = await eth().request({ method: 'personal_sign', params: [hexMsg, addr] });
const r = await (await fetch('/api/auth/verify', { method: 'POST',
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: addr, signature: sig }) })).json();
if (r.error) throw new Error(r.error);
return r; // {address, memberId, sponsorId}
}
async function sendTx(data, valueWei) {
const c = await IAP.getConfig();
const [addr] = await eth().request({ method: 'eth_requestAccounts' });
await ensureChain(c);
const tx = { from: addr, to: c.contract, data };
if (valueWei) tx.value = '0x' + BigInt(valueWei).toString(16);
return eth().request({ method: 'eth_sendTransaction', params: [tx] });
}
async function waitTx(hash) {
// poll our own server (CSP-friendly); it relays the receipt from the RPC
for (let i = 0; i < 60; i++) {
const r = await (await fetch('/api/tx/' + hash)).json();
if (r.found) return { status: r.status, blockNumber: r.blockNumber };
await new Promise(res => setTimeout(res, 2500));
}
throw new Error('Timed out waiting for the transaction. Check the explorer.');
}
// buy: quote is read live server-side; pad 2% for oracle drift, contract
// refunds every wei of excess in the same transaction.
async function buy(productId, sponsorId, costWei) {
const value = BigInt(costWei) + BigInt(costWei) / 50n;
const data = SEL_BUY + pad(productId) + pad(sponsorId || 0);
const hash = await sendTx(data, value);
return { hash, receipt: await waitTx(hash) };
}
async function activate(sponsorId) {
const data = SEL_ACTIVATE + pad(sponsorId || 0);
const hash = await sendTx(data, null);
return { hash, receipt: await waitTx(hash) };
}
return { connect, signIn, buy, activate, waitTx };
})();