Skip to content

Commit 63bf87d

Browse files
committed
feat(nft-marketplace): make a bid on an auction
1 parent 7327f3f commit 63bf87d

File tree

1 file changed

+136
-63
lines changed

1 file changed

+136
-63
lines changed

apps/nft-marketplace/src/app/page.tsx

Lines changed: 136 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
'use client'
22

3+
/* ──────────────────────────────────────────────────────────
4+
Imports
5+
─────────────────────────────────────────────────────────── */
36
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'
47
import AddShoppingCartIcon from '@mui/icons-material/AddShoppingCart'
58
import GavelIcon from '@mui/icons-material/Gavel'
@@ -25,25 +28,28 @@ import type { NextPage } from 'next'
2528
import Head from 'next/head'
2629
import { useCallback, useEffect, useState } from 'react'
2730

28-
/* ----------------------------------- Config ---------------------------------- */
29-
31+
/* ──────────────────────────────────────────────────────────
32+
Config
33+
─────────────────────────────────────────────────────────── */
3034
const NETWORK_ID = process.env.NEXT_PUBLIC_NETWORK_ID || 'mainnet'
3135
const NODE_URL = process.env.NEXT_PUBLIC_NODE_URL || 'https://rpc.mainnet.near.org'
3236
const WALLET_URL = process.env.NEXT_PUBLIC_WALLET_URL || 'https://app.mynearwallet.com'
3337
const HELPER_URL = process.env.NEXT_PUBLIC_HELPER_URL || 'https://helper.mainnet.near.org'
3438
const CONTRACT_NAME = process.env.NEXT_PUBLIC_CONTRACT_NAME || 'market.aigency.near'
3539
const NFT_CONTRACT_ID = process.env.NEXT_PUBLIC_NFT_CONTRACT_NAME || 'my-new-nft-contract.near'
3640

37-
// 150Tgas
41+
// 150 Tgas
3842
const GAS_BN = BigInt('150000000000000')
39-
// 0.00859 Ⓝ – pulled from contract but cached here for performers
43+
// 0.00859 Ⓝ
4044
const STORAGE_FOR_SALE = BigInt('8590000000000000000000')
4145

4246
const yoctoToNear = (y: string | number | bigint) => utils.format.formatNearAmount(y.toString(), 2)
4347
const nearToYocto = (n: string) => utils.format.parseNearAmount(n) || '0'
48+
const nowNs = () => BigInt(Date.now()) * 1000000n // ms ⇒ ns
4449

45-
/* ------------------------------------ Types ----------------------------------- */
46-
50+
/* ──────────────────────────────────────────────────────────
51+
Types
52+
─────────────────────────────────────────────────────────── */
4753
interface Bid {
4854
bidder_id: string
4955
price: string
@@ -70,6 +76,7 @@ interface MarketContract extends Contract {
7076
gas?: BigInt,
7177
amount?: BigInt
7278
) => Promise<MarketDataJson>
79+
7380
buy: (
7481
p: {
7582
nft_contract_id: string
@@ -80,20 +87,34 @@ interface MarketContract extends Contract {
8087
gas?: BigInt,
8188
amount?: BigInt
8289
) => Promise<void>
90+
8391
add_bid: (
8492
p: { nft_contract_id: string; ft_token_id: string; token_id: string; amount: string },
8593
gas?: BigInt,
8694
amount?: BigInt
8795
) => Promise<void>
96+
97+
accept_bid: (
98+
p: { nft_contract_id: string; token_id: string },
99+
gas?: BigInt,
100+
amount?: BigInt
101+
) => Promise<void>
102+
103+
end_auction: (
104+
p: { nft_contract_id: string; token_id: string },
105+
gas?: BigInt,
106+
amount?: BigInt
107+
) => Promise<void>
88108
}
89109

90110
interface TokenWithListing {
91111
token: any
92112
listing: MarketDataJson | null
93113
}
94114

95-
/* ----------------------------------- Hook ------------------------------------ */
96-
115+
/* ──────────────────────────────────────────────────────────
116+
Near hook
117+
─────────────────────────────────────────────────────────── */
97118
const useNear = () => {
98119
const [wallet, setWallet] = useState<WalletConnection | null>(null)
99120
const [account, setAccount] = useState<string | null>(null)
@@ -117,7 +138,7 @@ const useNear = () => {
117138

118139
const ctr = new Contract(walletConn.account(), CONTRACT_NAME, {
119140
viewMethods: ['get_market_data', 'storage_balance_of', 'get_supply_by_owner_id'],
120-
changeMethods: ['buy', 'add_bid', 'storage_deposit'],
141+
changeMethods: ['buy', 'add_bid', 'accept_bid', 'end_auction', 'storage_deposit'],
121142
useLocalViewExecution: true,
122143
}) as unknown as MarketContract
123144
setContract(ctr)
@@ -136,49 +157,36 @@ const useNear = () => {
136157
return { accountId: account, wallet, contract, signIn, signOut }
137158
}
138159

139-
/* ------------------------------ Storage helper ------------------------------- */
140-
141-
/**
142-
* Builds a batch (array) of NEAR transactions that will:
143-
* 1. deposit missing storage on the marketplace (if any)
144-
* 2. call `nft_approve` on the NFT contract to create / update the listing
145-
*
146-
* The wallet shows *one* confirmation for the whole array.
147-
*/
160+
/* ──────────────────────────────────────────────────────────
161+
Tx builders
162+
─────────────────────────────────────────────────────────── */
148163
const buildListingTransactions = async (
149164
wallet: WalletConnection,
150165
tokenId: string,
151-
priceYocto: string
166+
priceYocto: string,
167+
msgExtra: Record<string, unknown> = {}
152168
) => {
153-
// 1. How much storage the user already owns on the marketplace
169+
// 1. storage already paid?
154170
const storagePaid: string = (await wallet.account().viewFunction({
155171
contractId: CONTRACT_NAME,
156172
methodName: 'storage_balance_of',
157173
args: { account_id: wallet.getAccountId() },
158174
})) as string
159175

160-
// 2. Active listings count – each requires STORAGE_FOR_SALE
176+
// 2. current listings
161177
const currentListings: string = (await wallet.account().viewFunction({
162178
contractId: CONTRACT_NAME,
163179
methodName: 'get_supply_by_owner_id',
164180
args: { account_id: wallet.getAccountId() },
165181
})) as string
166182

167183
const required = (BigInt(currentListings) + 1n) * STORAGE_FOR_SALE
168-
const paid = BigInt(storagePaid)
184+
const paid = BigInt(storagePaid || 0)
169185
const shortfall = required > paid ? required - paid : 0n
170186

171-
/* -------------------- build actions ------------------------------------ */
172-
187+
/* ─ actions ─ */
173188
const actionsMarketplace: transactions.Action[] = shortfall
174-
? [
175-
transactions.functionCall(
176-
'storage_deposit',
177-
{},
178-
GAS_BN, // Gas
179-
shortfall // Deposit Ⓝ
180-
),
181-
]
189+
? [transactions.functionCall('storage_deposit', {}, GAS_BN, shortfall)]
182190
: []
183191

184192
const actionsApprove: transactions.Action[] = [
@@ -191,10 +199,10 @@ const buildListingTransactions = async (
191199
market_type: 'sale',
192200
price: priceYocto,
193201
ft_token_id: 'near',
202+
...msgExtra,
194203
}),
195204
},
196205
GAS_BN,
197-
// Approval itself just needs NEARs
198206
BigInt('310000000000000000000')
199207
),
200208
]
@@ -208,8 +216,9 @@ const buildListingTransactions = async (
208216
return txs
209217
}
210218

211-
/* ------------------------------ UI Components ------------------------------- */
212-
219+
/* ──────────────────────────────────────────────────────────
220+
UI Components
221+
─────────────────────────────────────────────────────────── */
213222
const ConnectWalletButton = ({
214223
accountId,
215224
onSignIn,
@@ -228,6 +237,9 @@ const ConnectWalletButton = ({
228237
</Button>
229238
)
230239

240+
/* ──────────────────────────────────────────────────────────
241+
Listing row
242+
──────────────────────────────────────────────────────────*/
231243
const ListingRow = ({
232244
token,
233245
listing,
@@ -246,27 +258,30 @@ const ListingRow = ({
246258
const meta = token.metadata || {}
247259
const imgSrc = meta.media ?? meta.reference ?? 'https://placehold.co/80x80?text=No+Image'
248260

249-
/* ---------------------------- Not yet listed --------------------------- */
261+
/* ──────── Not (yet) listed ──────── */
250262
if (!listing) {
251263
const isOwner = accountId === token.owner_id
252264

253-
const handleList = async () => {
265+
const handleListSale = async (isAuction: boolean) => {
254266
if (!wallet) return
255267

256-
const priceNear = prompt('Sale price (NEAR):')
268+
const priceNear = prompt(isAuction ? 'Starting price (NEAR):' : 'Sale price (NEAR):')
257269
if (!priceNear) return
258-
259270
const priceYocto = nearToYocto(priceNear)
260271

261-
try {
262-
// Build transactions and send in a single wallet prompt
263-
const txs = await buildListingTransactions(wallet, token.token_id, priceYocto)
264-
265-
// ToDo: workaround!
266-
for (const tx of txs) {
267-
await wallet.account().signAndSendTransaction(tx)
272+
let msgExtra: Record<string, unknown> = {}
273+
if (isAuction) {
274+
const hoursStr = prompt('Auction duration in hours (default 24):') || '24'
275+
const hours = Math.max(1, parseInt(hoursStr, 10))
276+
msgExtra = {
277+
is_auction: true,
278+
ended_at: (nowNs() + BigInt(hours) * 3600n * 1000000000n).toString(),
268279
}
280+
}
269281

282+
try {
283+
const txs = await buildListingTransactions(wallet, token.token_id, priceYocto, msgExtra)
284+
for (const tx of txs) await wallet.account().signAndSendTransaction(tx)
270285
alert('Listing submitted – complete the wallet approval.')
271286
refresh()
272287
} catch (err) {
@@ -290,25 +305,30 @@ const ListingRow = ({
290305
<TableCell align="right"></TableCell>
291306
<TableCell align="right">
292307
{isOwner && (
293-
<Button
294-
size="small"
295-
variant="contained"
296-
startIcon={<AddShoppingCartIcon />}
297-
onClick={handleList}
298-
>
299-
List for sale
300-
</Button>
308+
<>
309+
<Button
310+
size="small"
311+
variant="contained"
312+
startIcon={<AddShoppingCartIcon />}
313+
onClick={() => handleListSale(false)}
314+
>
315+
List for sale
316+
</Button>{' '}
317+
<Button size="small" startIcon={<GavelIcon />} onClick={() => handleListSale(true)}>
318+
List auction
319+
</Button>
320+
</>
301321
)}
302322
</TableCell>
303323
</TableRow>
304324
)
305325
}
306326

307-
/* ------------------------------ Already listed ------------------------------ */
308-
327+
/* ──────── Already listed ──────── */
309328
const isOwner = accountId === listing.owner_id
310329
const isAuction = !!listing.is_auction
311330
const latestBid = listing.bids && listing.bids[listing.bids.length - 1]
331+
const ended = isAuction && listing.ended_at && BigInt(listing.ended_at) < nowNs()
312332

313333
const handleBuy = async () => {
314334
if (!contract) return
@@ -338,13 +358,33 @@ const ListingRow = ({
338358
refresh()
339359
}
340360

361+
const handleAcceptBid = async () => {
362+
if (!contract) return
363+
await contract.accept_bid(
364+
{ nft_contract_id: listing.nft_contract_id, token_id: listing.token_id },
365+
GAS_BN,
366+
1n
367+
)
368+
refresh()
369+
}
370+
371+
const handleEndAuction = async () => {
372+
if (!contract) return
373+
await contract.end_auction(
374+
{ nft_contract_id: listing.nft_contract_id, token_id: listing.token_id },
375+
GAS_BN,
376+
1n
377+
)
378+
refresh()
379+
}
380+
341381
const priceDisplay = isAuction
342382
? `${yoctoToNear(listing.price)} NEAR` +
343383
(latestBid ? ` / ${yoctoToNear(latestBid.price)} NEAR (highest bid)` : '')
344384
: `${yoctoToNear(listing.price)} NEAR`
345385

346386
return (
347-
<TableRow hover selected={isOwner}>
387+
<TableRow hover selected={isOwner} style={ended ? { opacity: 0.5 } : {}}>
348388
<TableCell>
349389
<CardMedia
350390
component="img"
@@ -353,14 +393,46 @@ const ListingRow = ({
353393
sx={{ width: 40, height: 40, borderRadius: 1 }}
354394
/>
355395
</TableCell>
356-
<TableCell>{meta.title ?? listing.token_id}</TableCell>
396+
<TableCell>
397+
{meta.title ?? listing.token_id}
398+
{isAuction && listing.started_at && (
399+
<>
400+
<br />
401+
<small>
402+
{new Date(Number(listing.started_at) / 1e6).toLocaleString()}{' '}
403+
{new Date(Number(listing.ended_at) / 1e6).toLocaleString()}
404+
</small>
405+
</>
406+
)}
407+
</TableCell>
357408
<TableCell>{listing.owner_id}</TableCell>
358409
<TableCell align="right">{priceDisplay}</TableCell>
359410
<TableCell align="right">
360411
{isAuction ? (
361-
<Button size="small" startIcon={<GavelIcon />} disabled={!accountId} onClick={handleBid}>
362-
Bid
363-
</Button>
412+
isOwner ? (
413+
<>
414+
<Button
415+
size="small"
416+
startIcon={<GavelIcon />}
417+
onClick={handleAcceptBid}
418+
disabled={!latestBid}
419+
>
420+
Accept bid
421+
</Button>{' '}
422+
<Button size="small" onClick={handleEndAuction}>
423+
End auction
424+
</Button>
425+
</>
426+
) : (
427+
<Button
428+
size="small"
429+
startIcon={<GavelIcon />}
430+
disabled={!accountId || !!ended}
431+
onClick={handleBid}
432+
>
433+
Bid
434+
</Button>
435+
)
364436
) : (
365437
<Button
366438
size="small"
@@ -377,8 +449,9 @@ const ListingRow = ({
377449
)
378450
}
379451

380-
/* ---------------------------------- Page ---------------------------------- */
381-
452+
/* ──────────────────────────────────────────────────────────
453+
Page component
454+
─────────────────────────────────────────────────────────── */
382455
const Home: NextPage = () => {
383456
const { accountId, wallet, contract, signIn, signOut } = useNear()
384457
const [tokens, setTokens] = useState<TokenWithListing[]>([])

0 commit comments

Comments
 (0)