diff --git a/contract/,tx.json b/contract/,tx.json new file mode 100644 index 0000000..8b94221 --- /dev/null +++ b/contract/,tx.json @@ -0,0 +1 @@ +{"height":"1085","txhash":"97AD126A4FBF7A4522275A12FD21EEF14FE2A502D0A2E489785CEAA56CA1F9A5","codespace":"","code":0,"data":"12260A242F636F736D6F732E62616E6B2E763162657461312E4D736753656E64526573706F6E7365","raw_log":"[{\"msg_index\":0,\"events\":[{\"type\":\"coin_received\",\"attributes\":[{\"key\":\"receiver\",\"value\":\"agoric1rwwley550k9mmk6uq6mm6z4udrg8kyuyvfszjk\"},{\"key\":\"amount\",\"value\":\"321000000ubld\"}]},{\"type\":\"coin_spent\",\"attributes\":[{\"key\":\"spender\",\"value\":\"agoric1estsewt6jqsx77pwcxkn5ah0jqgu8rhgflwfdl\"},{\"key\":\"amount\",\"value\":\"321000000ubld\"}]},{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"/cosmos.bank.v1beta1.MsgSend\"},{\"key\":\"sender\",\"value\":\"agoric1estsewt6jqsx77pwcxkn5ah0jqgu8rhgflwfdl\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"agoric1rwwley550k9mmk6uq6mm6z4udrg8kyuyvfszjk\"},{\"key\":\"sender\",\"value\":\"agoric1estsewt6jqsx77pwcxkn5ah0jqgu8rhgflwfdl\"},{\"key\":\"amount\",\"value\":\"321000000ubld\"}]}]}]","logs":[{"msg_index":0,"log":"","events":[{"type":"coin_received","attributes":[{"key":"receiver","value":"agoric1rwwley550k9mmk6uq6mm6z4udrg8kyuyvfszjk"},{"key":"amount","value":"321000000ubld"}]},{"type":"coin_spent","attributes":[{"key":"spender","value":"agoric1estsewt6jqsx77pwcxkn5ah0jqgu8rhgflwfdl"},{"key":"amount","value":"321000000ubld"}]},{"type":"message","attributes":[{"key":"action","value":"/cosmos.bank.v1beta1.MsgSend"},{"key":"sender","value":"agoric1estsewt6jqsx77pwcxkn5ah0jqgu8rhgflwfdl"},{"key":"module","value":"bank"}]},{"type":"transfer","attributes":[{"key":"recipient","value":"agoric1rwwley550k9mmk6uq6mm6z4udrg8kyuyvfszjk"},{"key":"sender","value":"agoric1estsewt6jqsx77pwcxkn5ah0jqgu8rhgflwfdl"},{"key":"amount","value":"321000000ubld"}]}]}],"info":"","gas_wanted":"82400","gas_used":"65647","tx":null,"timestamp":"","events":[{"type":"tx","attributes":[{"key":"ZmVl","value":"","index":true},{"key":"ZmVlX3BheWVy","value":"YWdvcmljMWVzdHNld3Q2anFzeDc3cHdjeGtuNWFoMGpxZ3U4cmhnZmx3ZmRs","index":true}]},{"type":"tx","attributes":[{"key":"YWNjX3NlcQ==","value":"YWdvcmljMWVzdHNld3Q2anFzeDc3cHdjeGtuNWFoMGpxZ3U4cmhnZmx3ZmRsLzYw","index":true}]},{"type":"tx","attributes":[{"key":"c2lnbmF0dXJl","value":"MW1MQS9GRTlsM09EMW9yUkN3LzdBbFdWTGNKRnorMjRWcldGenpkTUEvOUhrcEhYU1N6Q3JpQmtwazlDakFCd0pXVElGajBMTEtJUTFncUl1cU5kRVE9PQ==","index":true}]},{"type":"message","attributes":[{"key":"YWN0aW9u","value":"L2Nvc21vcy5iYW5rLnYxYmV0YTEuTXNnU2VuZA==","index":true}]},{"type":"coin_spent","attributes":[{"key":"c3BlbmRlcg==","value":"YWdvcmljMWVzdHNld3Q2anFzeDc3cHdjeGtuNWFoMGpxZ3U4cmhnZmx3ZmRs","index":true},{"key":"YW1vdW50","value":"MzIxMDAwMDAwdWJsZA==","index":true}]},{"type":"coin_received","attributes":[{"key":"cmVjZWl2ZXI=","value":"YWdvcmljMXJ3d2xleTU1MGs5bW1rNnVxNm1tNno0dWRyZzhreXV5dmZzempr","index":true},{"key":"YW1vdW50","value":"MzIxMDAwMDAwdWJsZA==","index":true}]},{"type":"transfer","attributes":[{"key":"cmVjaXBpZW50","value":"YWdvcmljMXJ3d2xleTU1MGs5bW1rNnVxNm1tNno0dWRyZzhreXV5dmZzempr","index":true},{"key":"c2VuZGVy","value":"YWdvcmljMWVzdHNld3Q2anFzeDc3cHdjeGtuNWFoMGpxZ3U4cmhnZmx3ZmRs","index":true},{"key":"YW1vdW50","value":"MzIxMDAwMDAwdWJsZA==","index":true}]},{"type":"message","attributes":[{"key":"c2VuZGVy","value":"YWdvcmljMWVzdHNld3Q2anFzeDc3cHdjeGtuNWFoMGpxZ3U4cmhnZmx3ZmRs","index":true}]},{"type":"message","attributes":[{"key":"bW9kdWxl","value":"YmFuaw==","index":true}]}]} diff --git a/contract/src/offer-up-proposal.js b/contract/src/offer-up-proposal.js index d35e1e3..ca21080 100644 --- a/contract/src/offer-up-proposal.js +++ b/contract/src/offer-up-proposal.js @@ -67,7 +67,7 @@ export const startOfferUpContract = async permittedPowers => { const istIssuer = await istIssuerP; const istBrand = await istBrandP; - const terms = { tradePrice: AmountMath.make(istBrand, 25n * CENT) }; + const terms = { minBidPrice: AmountMath.make(istBrand, 25n * CENT), maxBids: 3n }; // agoricNames gets updated each time; the promise space only once XXXXXXX const installation = await offerUpInstallationP; diff --git a/contract/src/offer-up.contract.js b/contract/src/offer-up.contract.js index e24f7d7..dfd635e 100644 --- a/contract/src/offer-up.contract.js +++ b/contract/src/offer-up.contract.js @@ -11,7 +11,7 @@ * to make an invitation. * 3. client makes an offer using the invitation, along with * a proposal (with give and want) and payments. Zoe escrows the payments, and then - * 4. Zoe invokes the offer handler specified in step 2 -- here {@link tradeHandler}. + * 4. Zoe invokes the offer handler specified in step 2 -- here {@link bidHandler}. * * @see {@link https://docs.agoric.com/guides/zoe/|Zoe Overview} for a walk-thru of this contract * @see {@link https://docs.agoric.com/guides/js-programming/hardened-js.html|Hardened JavaScript} @@ -21,44 +21,35 @@ import { Far } from '@endo/far'; import { M, getCopyBagEntries } from '@endo/patterns'; -import { AssetKind } from '@agoric/ertp/src/amountMath.js'; +import { AssetKind, AmountMath } from '@agoric/ertp/src/amountMath.js'; import { AmountShape } from '@agoric/ertp/src/typeGuards.js'; import { atomicRearrange } from '@agoric/zoe/src/contractSupport/atomicTransfer.js'; import '@agoric/zoe/exported.js'; -const { Fail, quote: q } = assert; +// Added for debugging/console.log +const bigintReplacer = (_, value) => + typeof value === 'bigint' ? value.toString() : value; -// #region bag utilities -/** @type { (xs: bigint[]) => bigint } */ -const sum = xs => xs.reduce((acc, x) => acc + x, 0n); - -/** - * @param {import('@endo/patterns').CopyBag} bag - * @returns {bigint[]} - */ -const bagCounts = bag => { - const entries = getCopyBagEntries(bag); - return entries.map(([_k, ct]) => ct); -}; // #endregion /** * In addition to the standard `issuers` and `brands` terms, - * this contract is parameterized by terms for price and, - * optionally, a maximum number of items sold for that price (default: 3). + * this contract is parameterized by terms for bid value and, + * optionally, a maximum number of bids before closing an auction (default: 3). * * @typedef {{ - * tradePrice: Amount; - * maxItems?: bigint; - * }} OfferUpTerms + * minBidPrice: Amount; + * maxBids?: bigint; + * }} AuctionTerms */ export const meta = { customTermsShape: M.splitRecord( - { tradePrice: AmountShape }, + { minBidPrice: AmountShape }, { maxItems: M.bigint() }, ), }; + // compatibility with an earlier contract metadata API export const customTermsShape = meta.customTermsShape; @@ -67,70 +58,93 @@ export const customTermsShape = meta.customTermsShape; * - creates a new non-fungible asset type for Items, and * - handles offers to buy up to `maxItems` items at a time. * - * @param {ZCF} zcf + * @param {ZCF} zcf */ export const start = async zcf => { - const { tradePrice, maxItems = 3n } = zcf.getTerms(); - - /** - * a new ERTP mint for items, accessed thru the Zoe Contract Facet. - * Note: `makeZCFMint` makes the associated brand and issuer available - * in the contract's terms. - * - * AssetKind.COPY_BAG can express non-fungible (or rather: semi-fungible) - * amounts such as: 3 potions and 1 map. - */ + const { minBidPrice, maxBids = 3n } = zcf.getTerms(); + const itemMint = await zcf.makeZCFMint('Item', AssetKind.COPY_BAG); const { brand: itemBrand } = itemMint.getIssuerRecord(); - /** - * a pattern to constrain proposals given to {@link tradeHandler} - * - * The `Price` amount must be >= `tradePrice` term. - * The `Items` amount must use the `Item` brand and a bag value. - */ const proposalShape = harden({ - give: { Price: M.gte(tradePrice) }, + give: { Price: M.gte(minBidPrice) }, want: { Items: { brand: itemBrand, value: M.bag() } }, exit: M.any(), }); - /** a seat for allocating proceeds of sales */ + /** a seat for allocating proceeds of auction sales */ const proceeds = zcf.makeEmptySeatKit().zcfSeat; + // A register to keep all the bids of all items + let bidsRegister = new Map(); /** @type {OfferHandler} */ - const tradeHandler = buyerSeat => { + const bidHandler = bidderSeat => { // give and want are guaranteed by Zoe to match proposalShape - const { want } = buyerSeat.getProposal(); - - sum(bagCounts(want.Items.value)) <= maxItems || - Fail`max ${q(maxItems)} items allowed: ${q(want.Items)}`; - - const newItems = itemMint.mintGains(want); - atomicRearrange( - zcf, - harden([ - // price from buyer to proceeds - [buyerSeat, proceeds, { Price: tradePrice }], - // new items to buyer - [newItems, buyerSeat, want], - ]), - ); - - buyerSeat.exit(true); - newItems.exit(); - return 'trade complete'; + const { want } = bidderSeat.getProposal(); + const bidItem = JSON.stringify(want.Items.value, bigintReplacer); + console.log(' bidItem is : ', bidItem); + + /** + From the bidsRegister Map object, get the array of bids for the given item if it exists otherwise create one. + and then place the current bid in that array. + then count the number of bids in that array and check whether it is equal to the maxBids. + If the number of bids are equal to max bid, we want to find out value of the max bid in the array. + */ + + // Check if the item already has a bid array in the Map; if not, create one + if (!bidsRegister.has(bidItem)) { + bidsRegister.set(bidItem, []); + } + + // Get the existing bid array for the item + let bids = bidsRegister.get(bidItem); + + // Add the current bid to the bid array + bids.push(bidderSeat); + + // Check if the number of bids is equal to maxBids + if (bids.length == maxBids) { + console.log('Maximum bids reached for this item.'); + + // Find the maximum bid in the array + // Initialize the maximum bid as the first bid in the array + let maxBid = bids[0]; + // Loop through the bids array to find the maximum bid + for (let i = 1; i < bids.length; i++) { + if ( + AmountMath.isGTE( + bids[i].getCurrentAllocation().Price, + maxBid.getCurrentAllocation().Price, + ) + ) { + maxBid = bids[i]; + } + } + + const newItems = itemMint.mintGains(want); + atomicRearrange( + zcf, + harden([ + [newItems, maxBid, want], + [maxBid, proceeds, { Price: maxBid.getCurrentAllocation().Price }], + ]), + ); + newItems.exit(); + maxBid.exit(true); + + bids.forEach(async bidderSeatItem => { + console.log(JSON.stringify(bidderSeatItem)); + if (!bidderSeatItem.hasExited()) { + bidderSeatItem.exit(true); + } + }); + bidsRegister.set(bidItem, []); + } + return 'bid placed.'; }; - /** - * Make an invitation to trade for items. - * - * Proposal Keywords used in offers using these invitations: - * - give: `Price` - * - want: `Items` - */ const makeTradeInvitation = () => - zcf.makeInvitation(tradeHandler, 'buy items', undefined, proposalShape); + zcf.makeInvitation(bidHandler, 'bid on items', undefined, proposalShape); // Mark the publicFacet Far, i.e. reachable from outside the contract const publicFacet = Far('Items Public Facet', { diff --git a/contract/test/test-contract.js b/contract/test/test-contract.js index 6dc1f6a..11c5685 100644 --- a/contract/test/test-contract.js +++ b/contract/test/test-contract.js @@ -17,6 +17,7 @@ import { AmountMath, makeIssuerKit } from '@agoric/ertp'; import { makeStableFaucet } from './mintStable.js'; import { startOfferUpContract } from '../src/offer-up-proposal.js'; + /** @typedef {typeof import('../src/offer-up.contract.js').start} AssetContractFn */ const myRequire = createRequire(import.meta.url); @@ -47,8 +48,6 @@ const makeTestContext = async _t => { test.before(async t => (t.context = await makeTestContext(t))); -// IDEA: use test.serial and pass work products -// between tests using t.context. test('Install the contract', async t => { const { zoe, bundle } = t.context; @@ -63,7 +62,7 @@ test('Start the contract', async t => { const money = makeIssuerKit('PlayMoney'); const issuers = { Price: money.issuer }; - const terms = { tradePrice: AmountMath.make(money.brand, 5n) }; + const terms = { minBidPrice: AmountMath.make(money.brand, 5n) }; t.log('terms:', terms); /** @type {ERef>} */ @@ -82,77 +81,50 @@ test('Start the contract', async t => { * @param {Purse} purse * @param {string[]} choices */ -const alice = async (t, zoe, instance, purse, choices = ['map', 'scroll']) => { +const alice = async (t, zoe, instance, purse, choices = ['map'], bidValue = 25n * CENT, isMaxbid = false) => { const publicFacet = E(zoe).getPublicFacet(instance); // @ts-expect-error Promise seems to work const terms = await E(zoe).getTerms(instance); - const { issuers, brands, tradePrice } = terms; - + const { brands, minBidPrice } = terms; + const bidPrice = AmountMath.make(minBidPrice.brand, bidValue); + const choiceBag = makeCopyBag(choices.map(name => [name, 1n])); const proposal = { - give: { Price: tradePrice }, + give: { Price: bidPrice }, want: { Items: AmountMath.make(brands.Item, choiceBag) }, }; - const pmt = await E(purse).withdraw(tradePrice); + const pmt = await E(purse).withdraw(bidPrice); t.log('Alice gives', proposal.give); // #endregion makeProposal const toTrade = E(publicFacet).makeTradeInvitation(); - const seat = E(zoe).offer(toTrade, proposal, { Price: pmt }); - const items = await E(seat).getPayout('Items'); + const seat = await E(zoe).offer(toTrade, proposal, { Price: pmt } ); + return {seat, proposal, pmt}; - const actual = await E(issuers.Item).getAmountOf(items); - t.log('Alice payout brand', actual.brand); - t.log('Alice payout value', actual.value); - t.deepEqual(actual, proposal.want.Items); }; +const verifyBidSeat = async (t, seat, proposal, zoe, instance, purse, isMaxbid = false) => { -test('Alice trades: give some play money, want items', async t => { - const { zoe, bundle } = t.context; - - const money = makeIssuerKit('PlayMoney'); - const issuers = { Price: money.issuer }; - const terms = { tradePrice: AmountMath.make(money.brand, 5n) }; - - /** @type {ERef>} */ - const installation = E(zoe).install(bundle); - const { instance } = await E(zoe).startInstance(installation, issuers, terms); - t.log(instance); - t.is(typeof instance, 'object'); - - const alicePurse = money.issuer.makeEmptyPurse(); - const amountOfMoney = AmountMath.make(money.brand, 10n); - const moneyPayment = money.mint.mintPayment(amountOfMoney); - alicePurse.deposit(moneyPayment); - await alice(t, zoe, instance, alicePurse); -}); + const terms = await E(zoe).getTerms(instance); + const { issuers, minBidPrice } = terms; + const zeroPrice = AmountMath.make(minBidPrice.brand, 0n); + + + if (isMaxbid){ + const items = await E(seat).getPayout('Items') ; + const actual = await E(issuers.Item).getAmountOf(items); + t.deepEqual(actual, proposal.want.Items); + const pmt = await E(seat).getPayout('Price') ; + const pmtAmount = await E(purse).deposit(pmt); + t.deepEqual(zeroPrice, pmtAmount); + } else { + const pmt = await E(seat).getPayout('Price') ; + const pmtAmount = await E(purse).deposit(pmt); + t.notDeepEqual(zeroPrice, pmtAmount); + } -test('Trade in IST rather than play money', async t => { - /** - * Start the contract, providing it with - * the IST issuer. - * - * @param {{ zoe: ZoeService, bundle: {} }} powers - */ - const startContract = async ({ zoe, bundle }) => { - /** @type {ERef>} */ - const installation = E(zoe).install(bundle); - const feeIssuer = await E(zoe).getFeeIssuer(); - const feeBrand = await E(feeIssuer).getBrand(); - const tradePrice = AmountMath.make(feeBrand, 25n * CENT); - return E(zoe).startInstance( - installation, - { Price: feeIssuer }, - { tradePrice }, - ); - }; +}; - const { zoe, bundle, bundleCache, feeMintAccess } = t.context; - const { instance } = await startContract({ zoe, bundle }); - const { faucet } = makeStableFaucet({ bundleCache, feeMintAccess, zoe }); - await alice(t, zoe, instance, await faucet(5n * UNIT6)); -}); test('use the code that will go on chain to start the contract', async t => { const noop = harden(() => {}); @@ -226,4 +198,73 @@ test('use the code that will go on chain to start the contract', async t => { const { feeMintAccess, bundleCache } = t.context; const { faucet } = makeStableFaucet({ bundleCache, feeMintAccess, zoe }); await alice(t, zoe, instance, await faucet(5n * UNIT6)); + t.log(instance); + t.is(typeof instance, 'object'); +}); + + +test('bidSeats saved in Maps', async t => { + + const startContract = async ({ zoe, bundle }) => { + /** @type {ERef>} */ + const installation = E(zoe).install(bundle); + const feeIssuer = await E(zoe).getFeeIssuer(); + const feeBrand = await E(feeIssuer).getBrand(); + const minBidPrice = AmountMath.make(feeBrand, 25n * CENT); + return E(zoe).startInstance( + installation, + { Price: feeIssuer }, + { minBidPrice }, + ); + }; + const { zoe, bundle, bundleCache, feeMintAccess } = t.context; + const { instance } = await startContract({ zoe, bundle }); + const { faucet } = makeStableFaucet({ bundleCache, feeMintAccess, zoe }); + const purse1 = await faucet(500n * UNIT6); + const purse2 = await faucet(500n * UNIT6); + const purse3 = await faucet(500n * UNIT6); + const {seat: seat1, proposal: proposal1} = await alice(t, zoe, instance, purse1 , ['map'], 8n* UNIT6); + const {seat: seat2, proposal:proposal2} = await alice(t, zoe, instance, purse2, ['map'], 7n* UNIT6, false); + const {seat: seat3, proposal:proposal3} = await alice(t, zoe, instance, purse3, ['map'], 6n* UNIT6); + await verifyBidSeat(t, seat1, proposal1, zoe, instance, purse1,true); + await verifyBidSeat(t, seat2, proposal2, zoe, instance, purse2,false); + await verifyBidSeat(t, seat3, proposal3, zoe, instance, purse3,false); +}); + + + +test('bid on multiple Items', async t => { + + const startContract = async ({ zoe, bundle }) => { + /** @type {ERef>} */ + const installation = E(zoe).install(bundle); + const feeIssuer = await E(zoe).getFeeIssuer(); + const feeBrand = await E(feeIssuer).getBrand(); + const minBidPrice = AmountMath.make(feeBrand, 25n * CENT); + return E(zoe).startInstance( + installation, + { Price: feeIssuer }, + { minBidPrice }, + ); + }; + const { zoe, bundle, bundleCache, feeMintAccess } = t.context; + const { instance } = await startContract({ zoe, bundle }); + const { faucet } = makeStableFaucet({ bundleCache, feeMintAccess, zoe }); + const purse1 = await faucet(500n * UNIT6); + const purse2 = await faucet(500n * UNIT6); + const purse3 = await faucet(500n * UNIT6); + const {seat: seat1, proposal: proposal1} = await alice(t, zoe, instance, purse1 , ['map'], 6n* UNIT6); + const {seat: seat4, proposal: proposal4} = await alice(t, zoe, instance, purse1 , ['scroll'], 6n* UNIT6); + const {seat: seat5, proposal: proposal5} = await alice(t, zoe, instance, purse2 , ['scroll'], 9n* UNIT6); + const {seat: seat2, proposal:proposal2} = await alice(t, zoe, instance, purse2, ['map'], 7n* UNIT6, false); + const {seat: seat3, proposal:proposal3} = await alice(t, zoe, instance, purse3, ['map'], 5n* UNIT6); + const {seat: seat6, proposal: proposal6} = await alice(t, zoe, instance, purse3 , ['scroll'], 6n* UNIT6); + + await verifyBidSeat(t, seat1, proposal1, zoe, instance, purse1,false); + await verifyBidSeat(t, seat2, proposal2, zoe, instance, purse2,true); + await verifyBidSeat(t, seat3, proposal3, zoe, instance, purse3,false); + + await verifyBidSeat(t, seat4, proposal4, zoe, instance, purse1,false); + await verifyBidSeat(t, seat5, proposal5, zoe, instance, purse2,true); + await verifyBidSeat(t, seat6, proposal6, zoe, instance, purse3,false); }); diff --git a/ui/src/App.tsx b/ui/src/App.tsx index d62c47a..fdaf5af 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -70,6 +70,7 @@ const connectWallet = async () => { }; const makeOffer = (giveValue: bigint, wantChoices: Record) => { +//const makeOffer = (giveValue: bigint) => { const { wallet, offerUpInstance, brands } = useAppStore.getState(); if (!offerUpInstance) throw Error('no contract instance'); if (!(brands && brands.IST && brands.Item)) @@ -78,18 +79,20 @@ const makeOffer = (giveValue: bigint, wantChoices: Record) => { const value = makeCopyBag(entries(wantChoices)); const want = { Items: { brand: brands.Item, value } }; const give = { Price: { brand: brands.IST, value: giveValue } }; - + + wallet?.makeOffer( { source: 'contract', instance: offerUpInstance, publicInvitationMaker: 'makeTradeInvitation', }, - { give, want }, + { give, want}, undefined, + // { want }, (update: { status: string; data?: unknown }) => { if (update.status === 'error') { - alert(`Offer error: ${update.data}`); + alert(`Offer error: ${ JSON.stringify(update.data) }`); } if (update.status === 'accepted') { alert('Offer accepted'); @@ -128,7 +131,7 @@ function App() { return ( <> -

Items Listed on Offer Up

+

Items Listed for Auction

; inputClassName: string; inputStep?: string; -}) => ( -
- - {icon && } - {coinIcon && } - -
-); + option: boolean; +}) => { + let inputField; + if (option) { + inputField = ( + + ); + } else { + inputField = ( + + ); + } + return ( +
+ + {icon && } + {coinIcon && } + {inputField} +
+ ); +}; type TradeProps = { makeOffer: (giveValue: bigint, wantChoices: Record) => void; @@ -71,23 +92,21 @@ type TradeProps = { // TODO: IST displayInfo is available in vbankAsset or boardAux const Trade = ({ makeOffer, istPurse, walletConnected }: TradeProps) => { const [giveValue, setGiveValue] = useState(terms.price); - const [choices, setChoices] = useState({ map: 1n, scroll: 2n }); + const [choices, setChoices] = useState({ map: 1n }); const changeChoice = (ev: FormEvent) => { if (!ev.target) return; const elt = ev.target as HTMLInputElement; const title = elt.title as ItemName; if (!title) return; - const qty = BigInt(elt.value); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { [title]: _old, ...rest }: ItemChoices = choices; - const newChoices = qty > 0 ? { ...rest, [title]: qty } : rest; + const qty = BigInt(1); + const newChoices = { [title]: qty }; setChoices(newChoices); }; return ( <>
-

Want: Choose up to 3 items

+

Want: Choose 1 item to bid on

{entries(nameToIcon).map(([title, icon]) => ( { inputClassName={ sum(values(choices)) <= terms.maxItems ? 'ok' : 'error' } + option={true} /> ))}
-

Give: Offer at least 0.25 IST

+

Give: Bid at least 0.25 IST

{ } inputClassName={giveValue >= terms.price ? 'ok' : 'error'} inputStep="0.01" + option={false} />
{walletConnected && ( )}