11'use client'
22
3+ /* ──────────────────────────────────────────────────────────
4+ Imports
5+ ─────────────────────────────────────────────────────────── */
36import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'
47import AddShoppingCartIcon from '@mui/icons-material/AddShoppingCart'
58import GavelIcon from '@mui/icons-material/Gavel'
@@ -25,25 +28,28 @@ import type { NextPage } from 'next'
2528import Head from 'next/head'
2629import { useCallback , useEffect , useState } from 'react'
2730
28- /* ----------------------------------- Config ---------------------------------- */
29-
31+ /* ──────────────────────────────────────────────────────────
32+ Config
33+ ─────────────────────────────────────────────────────────── */
3034const NETWORK_ID = process . env . NEXT_PUBLIC_NETWORK_ID || 'mainnet'
3135const NODE_URL = process . env . NEXT_PUBLIC_NODE_URL || 'https://rpc.mainnet.near.org'
3236const WALLET_URL = process . env . NEXT_PUBLIC_WALLET_URL || 'https://app.mynearwallet.com'
3337const HELPER_URL = process . env . NEXT_PUBLIC_HELPER_URL || 'https://helper.mainnet.near.org'
3438const CONTRACT_NAME = process . env . NEXT_PUBLIC_CONTRACT_NAME || 'market.aigency.near'
3539const NFT_CONTRACT_ID = process . env . NEXT_PUBLIC_NFT_CONTRACT_NAME || 'my-new-nft-contract.near'
3640
37- // 150 Tgas
41+ // 150 Tgas
3842const GAS_BN = BigInt ( '150000000000000' )
39- // 0.00859 Ⓝ – pulled from contract but cached here for performers
43+ // 0.00859 Ⓝ
4044const STORAGE_FOR_SALE = BigInt ( '8590000000000000000000' )
4145
4246const yoctoToNear = ( y : string | number | bigint ) => utils . format . formatNearAmount ( y . toString ( ) , 2 )
4347const 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+ ─────────────────────────────────────────────────────────── */
4753interface 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
90110interface TokenWithListing {
91111 token : any
92112 listing : MarketDataJson | null
93113}
94114
95- /* ----------------------------------- Hook ------------------------------------ */
96-
115+ /* ──────────────────────────────────────────────────────────
116+ Near hook
117+ ─────────────────────────────────────────────────────────── */
97118const 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+ ─────────────────────────────────────────────────────────── */
148163const 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+ ─────────────────────────────────────────────────────────── */
213222const ConnectWalletButton = ( {
214223 accountId,
215224 onSignIn,
@@ -228,6 +237,9 @@ const ConnectWalletButton = ({
228237 </ Button >
229238)
230239
240+ /* ──────────────────────────────────────────────────────────
241+ Listing row
242+ ──────────────────────────────────────────────────────────*/
231243const 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+ ─────────────────────────────────────────────────────────── */
382455const Home : NextPage = ( ) => {
383456 const { accountId, wallet, contract, signIn, signOut } = useNear ( )
384457 const [ tokens , setTokens ] = useState < TokenWithListing [ ] > ( [ ] )
0 commit comments