1,000,000 braincells
+Braincells
Loading checkout…
-$5 in tez, plus the wallet’s network fee.
+ + diff --git a/system/tests/tezos-checkout.browser.mjs b/system/tests/tezos-checkout.browser.mjs index 12b652ddcb..125e4293de 100644 --- a/system/tests/tezos-checkout.browser.mjs +++ b/system/tests/tezos-checkout.browser.mjs @@ -17,18 +17,21 @@ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const origin = `http://127.0.0.1:${server.address().port}`; const browser = await chromium.launch({ headless:true, channel:process.env.PLAYWRIGHT_CHANNEL || 'chrome' }); try { + for (const [pack, credits, usd, amount] of [['braincells-600k-v1',600_000,3,'7500000'], ['braincells-1m-v1',1_000_000,5,'12500000']]) { + const tez = String(Number(amount) / 1e6); const page = await browser.newPage({ ...devices['iPhone 13'] }); const errors = []; page.on('pageerror', e => errors.push(e.message)); const secret = 'a'.repeat(64), sender = 'tz1inPpZMzFUv5mkmqDMEC8sYxmEq53vxRhw'; - let intent = { id:'test', handle:'tester', status:'created', credits:1_000_000, usd:5, + let intent = { id:'test', handle:'tester', status:'created', credits:1_000_000, usd:5, pack:'braincells-1m-v1', + offers:[{id:'braincells-600k-v1',credits:600_000,usd:3},{id:'braincells-1m-v1',credits:1_000_000,usd:5}], recipient:'tz1gkf8EexComFBJvjtT1zdsisdah791KwBE', payload:'05010000000474657374' }; let phase = 'confirming'; await page.route('**/api/easel-tezos', async route => { assert.equal(route.request().headers().authorization, `Bearer ${secret}`); const request = route.request().postDataJSON(); if (request.action === 'quote') { - assert.equal(request.address, sender); assert.equal(request.signature, 'fixture-signature'); - intent = { ...intent, status:'quoted', sender, amountMutez:'12500000', expiresAt:new Date(Date.now() + 900_000).toISOString() }; + assert.equal(request.address, sender); assert.equal(request.signature, 'fixture-signature'); assert.equal(request.pack, pack); + intent = { ...intent, status:'quoted', sender, pack, credits, usd, amountMutez:amount, expiresAt:new Date(Date.now() + 900_000).toISOString() }; } const value = request.action === 'confirm' ? { ...intent, status:phase } : intent; await route.fulfill({ json:value }); @@ -43,23 +46,29 @@ try { async requestOperation(request){ window.operations.push(request); return {transactionHash:'o'+'a'.repeat(50)}; } }};` })); await page.goto(`${origin}/braincells/#${secret}`); + await page.getByLabel('Amount', {exact:true}).selectOption(pack); + await page.getByRole('heading', {name:`${credits.toLocaleString('en-US')} braincells`,exact:true}).waitFor(); + await page.reload(); + assert.equal(await page.getByLabel('Amount', {exact:true}).inputValue(),pack,'selected pack survives reload'); await page.getByRole('button', { name:'Connect wallet', exact:true }).click(); - await page.getByRole('button', { name:'Pay 12.5 tez', exact:true }).waitFor(); + await page.getByRole('button', { name:`Pay ${tez} tez`, exact:true }).waitFor(); assert.equal(new URL(page.url()).hash, ''); assert.equal(await page.evaluate(() => window.operations.length), 0, 'connecting/signing never sends funds'); - await page.getByRole('button', { name:'Pay 12.5 tez', exact:true }).click(); + await page.getByRole('button', { name:`Pay ${tez} tez`, exact:true }).click(); await page.getByText('Payment seen. Waiting for three Tezos confirmations…', { exact:true }).waitFor(); - assert.equal(await page.getByRole('button', { name:'Pay 12.5 tez', exact:true }).isVisible(), false); + assert.equal(await page.getByRole('button', { name:`Pay ${tez} tez`, exact:true }).isVisible(), false); const operations = await page.evaluate(() => window.operations); assert.equal(operations.length, 1); - assert.deepEqual(operations[0].operationDetails, [{ kind:'transaction', destination:intent.recipient, amount:'12500000' }]); + assert.deepEqual(operations[0].operationDetails, [{ kind:'transaction', destination:intent.recipient, amount }]); await page.reload(); await page.getByText('Payment seen. Waiting for three Tezos confirmations…', { exact:true }).waitFor(); - assert.equal(await page.getByRole('button', { name:'Pay 12.5 tez', exact:true }).isVisible(), false, 'reload cannot offer a second payment'); + assert.equal(await page.getByRole('button', { name:`Pay ${tez} tez`, exact:true }).isVisible(), false, 'reload cannot offer a second payment'); phase = 'credited'; await page.getByRole('button', { name:'Check payment', exact:true }).click(); await page.getByText('Braincells added. They’re ready to use in AC.', { exact:true }).waitFor(); assert.equal(await page.getByRole('button', { name:'Check payment', exact:true }).isVisible(), false); assert.deepEqual(errors, []); - console.log('PASS: connect, exact quote, explicit payment, reload recovery, pending and credited states.'); + console.log(`PASS: $${usd} pack, connect, exact quote, explicit payment, reload recovery, pending and credited states.`); + await page.close(); + } } finally { await browser.close(); server.close(); } diff --git a/system/tests/tezos-credits.test.mjs b/system/tests/tezos-credits.test.mjs index aeef3a2d98..1eb1b39c14 100644 --- a/system/tests/tezos-credits.test.mjs +++ b/system/tests/tezos-credits.test.mjs @@ -35,7 +35,7 @@ function collection() { }, }; } -async function fixture() { +async function fixture(pack) { const intents = collection(), claims = collection(), wallets = collection(); let clock = time; const state = { head:{ ...head }, txs:[] }; @@ -43,7 +43,7 @@ async function fixture() { chain:async path => path === '/head' ? state.head : state.txs }); const created = await p.create('user', '@test'); const secret = new URL(created.checkoutURL).hash.slice(1); - const quoted = await p.quote(secret, { address:sender, signature:'valid' }); + const quoted = await p.quote(secret, { address:sender, signature:'valid', pack }); const intent = [...intents.docs.values()][0]; const tx = { id:1234, hash, status:'applied', sender:{ address:sender }, target:{ address:TEZOS_TREASURY }, amount:Number(quoted.amountMutez), timestamp:new Date(+time + 1000).toISOString(), level:100 }; @@ -52,11 +52,29 @@ async function fixture() { test('quotes use the existing $5 pack and reject stale or wrong-network prices', () => { assert.equal(quoteAmount(head, time), '12500000'); + assert.equal(quoteAmount(head, time, 300), '7500000'); + assert.throws(() => quoteAmount(head, time, 1), /Unknown/); for (const patch of [{ quoteUsd:0 }, { synced:false }, { chain:'ghostnet' }, { chainId:'other' }, { timestamp:'bad' }, { quoteLevel:0 }]) assert.throws(() => quoteAmount({ ...head, ...patch }, time), /price|quote/); assert.throws(() => quoteAmount(head, new Date(+time + 3600_000)), /fresh/); }); +test('$3 buys exactly 600,000 braincells and a signed quote cannot change packs', async () => { + const f = await fixture('braincells-600k-v1'); + assert.equal(f.quoted.usd, 3); + assert.equal(f.quoted.credits, 600_000); + assert.equal(f.quoted.amountMutez, '7500000'); + await assert.rejects(f.p.quote(f.secret, { address:sender, signature:'valid', pack:'braincells-1m-v1' }), /fixed pack/); + f.state.txs = [f.tx]; + assert.equal((await f.p.confirm(f.secret, hash)).credits, 600_000); + await f.p.confirm(f.secret, hash); + assert.equal(f.wallets.docs.get('user').balance, 600_000); + const created = await f.p.create('user', '@test'); + const secret = new URL(created.checkoutURL).hash.slice(1); + await assert.rejects(f.p.quote(secret, { address:sender, signature:'valid', pack:'invented', credits:99_000_000, usd:0.01 }), /Unknown/); + assert.equal((await f.p.status(secret)).status, 'created'); +}); + test('display conversions expose a dated, expiring rate without requiring a checkout', async () => { const price = () => displayPrice({ chain:async () => head, now:time }); const response = await createHandler({ price })({ httpMethod:'POST', body:JSON.stringify({ action:'price' }) }); diff --git a/tezos/README.md b/tezos/README.md index 9d4c315c8e..f9a73c257b 100644 --- a/tezos/README.md +++ b/tezos/README.md @@ -66,8 +66,9 @@ ### Braincell purchases Whistlegraph opens `/braincells/` in the browser to pair with Temple or another -Beacon wallet. `/api/easel-tezos` sells the existing $5 / 1,000,000 braincell pack -to `aesthetic.tez`, using a 15-minute mainnet quote. A signed account message binds +Beacon wallet. `/api/easel-tezos` offers $3 / 600,000 and $5 / 1,000,000 braincell packs +to `aesthetic.tez`, using a 15-minute mainnet quote. Choose the pack before +connecting the wallet; the first signed quote fixes its price and credit amount. A signed account message binds the payer; TzKT must report the exact transfer and three confirmations before AC adds credit. No wallet keys are stored by AC.