11'use client' ;
22
33import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
4- import type { Account , PublicClient , WalletClient } from 'viem' ;
4+ import type { Account , Hex , PublicClient , WalletClient } from 'viem' ;
55import { formatEther } from 'viem' ;
66
77import { trackValidityOrder } from '../../../analytics/events' ;
@@ -25,8 +25,9 @@ import {
2525 signCall ,
2626 tokenBalance ,
2727} from './lib/amm' ;
28+ import { clampNoncelessExpiry , signNoncelessCall } from './lib/aa' ;
2829import { startBots , allNeedGas , botNeedsGas , refuelValue } from './lib/bots' ;
29- import { MAX_EXPIRY_SECONDS } from './lib/constants' ;
30+ import { BLOCK_MS , MAX_EXPIRY_SECONDS , MAX_NONCELESS_SECONDS } from './lib/constants' ;
3031import { faucetErrorMessage , seedEthFromFaucet } from './lib/faucet' ;
3132import {
3233 maxBlockForExpiry ,
@@ -56,11 +57,11 @@ import {
5657 sendValidityTransaction ,
5758} from './lib/rpc' ;
5859import { accountsFrom , createState , dropDeployment , loadState , saveState , type StoredState } from './lib/store' ;
59- import type { ChainStatus , PlacedOrder , Rectangle , Reserves , Side } from './lib/types' ;
60+ import type { ChainStatus , PlacedOrder , Rectangle , Reserves , Side , SubmitMode } from './lib/types' ;
6061
61- const POLL_MS = 400 ;
62- /** L2 blocks are ~2s . viem's default 4s block cache made this skip 2–3 heads . */
63- const BLOCK_POLL_MS = 1_000 ;
62+ const POLL_MS = BLOCK_MS ;
63+ /** Denim heads are 200ms . viem's default 4s block cache would skip dozens . */
64+ const BLOCK_POLL_MS = BLOCK_MS ;
6465const DEFAULT_SIZE_FRACTION = 50n ; // 1/50 of inventory
6566
6667function wadToNumber ( wad : bigint ) : number {
@@ -85,7 +86,8 @@ export function ValidityDemo() {
8586 const [ hoverPrice , setHoverPrice ] = useState < bigint | null > ( null ) ;
8687 const [ side , setSide ] = useState < Side > ( 'buy' ) ;
8788 const [ offsetBps , setOffsetBps ] = useState ( 100 ) ;
88- const [ expirySeconds , setExpirySeconds ] = useState ( 60 ) ;
89+ const [ expirySeconds , setExpirySeconds ] = useState ( 15 ) ;
90+ const [ submitMode , setSubmitMode ] = useState < SubmitMode > ( 'concurrent' ) ;
8991 const [ orders , setOrders ] = useState < PlacedOrder [ ] > ( [ ] ) ;
9092 const [ hoveredOrderId , setHoveredOrderId ] = useState < string | null > ( null ) ;
9193 const [ samples , setSamples ] = useState < PriceSample [ ] > ( [ ] ) ;
@@ -429,7 +431,7 @@ export function ValidityDemo() {
429431
430432 const id = window . setInterval ( ( ) => {
431433 void tick ( ) ;
432- } , 700 ) ;
434+ } , 250 ) ;
433435 void tick ( ) ;
434436 return ( ) => {
435437 cancelled = true ;
@@ -613,12 +615,16 @@ export function ValidityDemo() {
613615 amount0Out,
614616 amount1Out,
615617 } ) ;
616- const confirmedNonce = await publicClient . getTransactionCount ( {
617- address : account . address ,
618- blockTag : 'latest' ,
619- } ) ;
620- const occupant = occupyingOrder ( ordersRef . current , confirmedNonce ) ;
621- const replaced = restingOrderToReplace ( ordersRef . current , confirmedNonce ) ;
618+ const seconds =
619+ submitMode === 'concurrent'
620+ ? clampNoncelessExpiry ( expirySeconds )
621+ : Math . min ( MAX_EXPIRY_SECONDS , expirySeconds ) ;
622+ const block = await publicClient . getBlockNumber ( { cacheTime : 0 } ) ;
623+ const maxBlock = maxBlockForExpiry ( block , seconds ) ;
624+ const validity = [ ...draft . predicates ] ;
625+ if ( status ?. blockNumberPredicate ) {
626+ validity . push ( blockExpiryPredicate ( maxBlock ) ) ;
627+ }
622628 const estimated = await publicClient . estimateFeesPerGas ( ) . catch ( ( ) => null ) ;
623629 const padded =
624630 estimated ?. maxFeePerGas !== undefined && estimated . maxPriorityFeePerGas !== undefined
@@ -627,55 +633,73 @@ export function ValidityDemo() {
627633 maxPriorityFeePerGas : estimated . maxPriorityFeePerGas ,
628634 } )
629635 : null ;
636+ trackValidityOrder ( side , 'submitted' ) ;
637+ let hash : Hex ;
638+ let nonce : number | undefined ;
630639 let fees = padded ;
631- if ( occupant ?. maxFeePerGas !== undefined && occupant . maxPriorityFeePerGas !== undefined ) {
632- fees = bumpReplacementFees (
633- {
634- maxFeePerGas : occupant . maxFeePerGas ,
635- maxPriorityFeePerGas : occupant . maxPriorityFeePerGas ,
636- } ,
637- padded ,
638- ) ;
639- }
640- const sign = ( nextFees : typeof fees ) =>
641- signCall ( {
642- wallet,
643- publicClient,
644- account,
640+ let replaced : ReturnType < typeof restingOrderToReplace > ;
641+ if ( submitMode === 'concurrent' ) {
642+ replaced = undefined ;
643+ const signed = await signNoncelessCall ( {
644+ privateKey : state . userKey ,
645+ chainId : status ?. chainId ?? state . chainId ,
645646 to : call . to ,
646647 data : call . data ,
647- nonce : confirmedNonce ,
648- fees : nextFees ,
648+ expiresIn : seconds ,
649+ fees : padded ,
650+ publicClient,
649651 } ) ;
650- let signedResult = await sign ( fees ) ;
651- const seconds = Math . min ( MAX_EXPIRY_SECONDS , expirySeconds ) ;
652- const block = await publicClient . getBlockNumber ( { cacheTime : 0 } ) ;
653- const maxBlock = maxBlockForExpiry ( block , seconds ) ;
654- const validity = [ ...draft . predicates ] ;
655- if ( status ?. blockNumberPredicate ) {
656- validity . push ( blockExpiryPredicate ( maxBlock ) ) ;
657- }
658- trackValidityOrder ( side , 'submitted' ) ;
659- let hash ;
660- try {
661- hash = await sendValidityTransaction ( publicClient , signedResult . signed , validity ) ;
662- } catch ( err ) {
663- if ( ! isReplacementUnderpriced ( err ) || ! signedResult . fees ) throw err ;
664- signedResult = await sign ( bumpReplacementFees ( signedResult . fees , padded ) ) ;
665- hash = await sendValidityTransaction ( publicClient , signedResult . signed , validity ) ;
652+ hash = await sendValidityTransaction ( publicClient , signed . signed , validity ) ;
653+ } else {
654+ const confirmedNonce = await publicClient . getTransactionCount ( {
655+ address : account . address ,
656+ blockTag : 'latest' ,
657+ } ) ;
658+ const occupant = occupyingOrder ( ordersRef . current , confirmedNonce ) ;
659+ replaced = restingOrderToReplace ( ordersRef . current , confirmedNonce ) ;
660+ if ( occupant ?. maxFeePerGas !== undefined && occupant . maxPriorityFeePerGas !== undefined ) {
661+ fees = bumpReplacementFees (
662+ {
663+ maxFeePerGas : occupant . maxFeePerGas ,
664+ maxPriorityFeePerGas : occupant . maxPriorityFeePerGas ,
665+ } ,
666+ padded ,
667+ ) ;
668+ }
669+ const sign = ( nextFees : typeof fees ) =>
670+ signCall ( {
671+ wallet,
672+ publicClient,
673+ account,
674+ to : call . to ,
675+ data : call . data ,
676+ nonce : confirmedNonce ,
677+ fees : nextFees ,
678+ } ) ;
679+ let signedResult = await sign ( fees ) ;
680+ try {
681+ hash = await sendValidityTransaction ( publicClient , signedResult . signed , validity ) ;
682+ } catch ( err ) {
683+ if ( ! isReplacementUnderpriced ( err ) || ! signedResult . fees ) throw err ;
684+ signedResult = await sign ( bumpReplacementFees ( signedResult . fees , padded ) ) ;
685+ hash = await sendValidityTransaction ( publicClient , signedResult . signed , validity ) ;
686+ }
687+ nonce = signedResult . nonce ;
688+ fees = signedResult . fees ;
666689 }
667690 const order : PlacedOrder = {
668691 id : newId ( ) ,
669692 side,
670693 targetPriceWad : draft . priceWad ,
671694 size : amountIn ,
672695 expirySeconds : seconds ,
696+ submitMode,
673697 maxBlock : status ?. blockNumberPredicate ? maxBlock : undefined ,
674698 submittedAt : Date . now ( ) ,
675699 txHash : hash ,
676- nonce : signedResult . nonce ,
677- maxFeePerGas : signedResult . fees ?. maxFeePerGas ,
678- maxPriorityFeePerGas : signedResult . fees ?. maxPriorityFeePerGas ,
700+ nonce,
701+ maxFeePerGas : fees ?. maxFeePerGas ,
702+ maxPriorityFeePerGas : fees ?. maxPriorityFeePerGas ,
679703 status : 'pending' ,
680704 rectangle : draft . rectangle ,
681705 validity,
@@ -747,6 +771,7 @@ export function ValidityDemo() {
747771 < div className = "flex flex-col gap-2" >
748772 < div className = "flex flex-wrap items-center gap-x-5 gap-y-2 font-mono text-[12px] text-bds-gray-60" >
749773 < span > { status ?. readHost ?? 'no rpc' } </ span >
774+ < span > 200ms blocks</ span >
750775 < span > validity { status ?. validitySupported ? 'on' : 'unavailable' } </ span >
751776 { status ?. blockNumberPredicate ? < span > block bounds on</ span > : < span > client-side expiry only</ span > }
752777 < span > simulation { botsOn ? ( makersDry ? 'out of ETH' : 'live' ) : 'paused' } </ span >
@@ -782,9 +807,10 @@ export function ValidityDemo() {
782807 < Card className = "flex flex-col gap-4 bg-background p-6 dark:bg-white/5" >
783808 < Text variant = "title3" > Simulated pool</ Text >
784809 < Text variant = "label.regular" tone = "muted" >
785- A local EOA (not the Vibenet 8130 account) signs the swaps. The faucet
786- funds it, then you deploy a VIBE/USDV pool. Simulated flow moves the mid
787- so you can see a price condition fire — or expire unused.
810+ A local key signs the swaps — type-2 replacements, or 8130 nonceless
811+ txs so several conditions can rest at once. The faucet funds it, then
812+ you deploy a VIBE/USDV pool. Simulated flow moves the mid so you can
813+ see a price condition fire — or expire unused.
788814 </ Text >
789815 { address ? (
790816 < div className = "flex items-center justify-between gap-3" >
@@ -836,11 +862,18 @@ export function ValidityDemo() {
836862 side = { side }
837863 offsetBps = { offsetBps }
838864 expirySeconds = { expirySeconds }
865+ submitMode = { submitMode }
839866 busy = { busy }
840867 validitySupported = { Boolean ( status ?. validitySupported ) }
841868 onSide = { setSide }
842869 onOffset = { setOffsetBps }
843870 onExpiry = { setExpirySeconds }
871+ onSubmitMode = { ( mode ) => {
872+ setSubmitMode ( mode ) ;
873+ if ( mode === 'concurrent' && expirySeconds > MAX_NONCELESS_SECONDS ) {
874+ setExpirySeconds ( 15 ) ;
875+ }
876+ } }
844877 onSubmit = { ( ) => void placeOrder ( ) }
845878 />
846879 ) : (
0 commit comments