Skip to content

Commit 839fc47

Browse files
ci(release): publish latest release
1 parent 85e9388 commit 839fc47

4 files changed

Lines changed: 186 additions & 12 deletions

File tree

RELEASE

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
IPFS hash of the deployment:
2-
- CIDv0: `QmNftmD2jYM2MD7TjHfd7wmu3tgBvXJCra1Z1z9xEYtrLA`
3-
- CIDv1: `bafybeiae5xwpty5fbnkusf3g5twy3dpy2bjgjemsw6jvd4wehdyn46xbam`
2+
- CIDv0: `QmPc7bbKd3n3rAv4VUE224YoCvYrRLxxWH9Uc8QswwMvpn`
3+
- CIDv1: `bafybeias2hgrfu22qvwpfn2pv56qeovr2nnnywreibcs2rzlpch2mipkq4`
44

55
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
66

@@ -10,5 +10,5 @@ You can also access the Uniswap Interface from an IPFS gateway.
1010
Your Uniswap settings are never remembered across different URLs.
1111

1212
IPFS gateways:
13-
- https://bafybeiae5xwpty5fbnkusf3g5twy3dpy2bjgjemsw6jvd4wehdyn46xbam.ipfs.dweb.link/
14-
- [ipfs://QmNftmD2jYM2MD7TjHfd7wmu3tgBvXJCra1Z1z9xEYtrLA/](ipfs://QmNftmD2jYM2MD7TjHfd7wmu3tgBvXJCra1Z1z9xEYtrLA/)
13+
- https://bafybeias2hgrfu22qvwpfn2pv56qeovr2nnnywreibcs2rzlpch2mipkq4.ipfs.dweb.link/
14+
- [ipfs://QmPc7bbKd3n3rAv4VUE224YoCvYrRLxxWH9Uc8QswwMvpn/](ipfs://QmPc7bbKd3n3rAv4VUE224YoCvYrRLxxWH9Uc8QswwMvpn/)

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
web/5.154.3
1+
web/5.154.4

apps/web/src/features/Toucan/Auction/hooks/useAuctionLiquidityLock.test.ts

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import {
1212
import { formatTimestampToDate } from '~/features/Toucan/Auction/utils/formatting'
1313

1414
const mockUseStatsBannerData = vi.fn()
15-
const mockStoreState = { auctionDetails: null as AuctionDetails | null }
15+
const mockStoreState = {
16+
auctionDetails: null as AuctionDetails | null,
17+
currentBlockNumber: undefined as number | undefined,
18+
}
1619

1720
vi.mock('~/features/Toucan/Auction/hooks/useStatsBannerData', () => ({
1821
useStatsBannerData: () => mockUseStatsBannerData(),
@@ -91,6 +94,7 @@ describe('useAuctionLiquidityLock', () => {
9194
vi.clearAllMocks()
9295
mockStatsBanner()
9396
mockStoreState.auctionDetails = null
97+
mockStoreState.currentBlockNumber = undefined
9498
})
9599

96100
it('returns not-locked defaults when the auction has no lock info', () => {
@@ -149,6 +153,124 @@ describe('useAuctionLiquidityLock', () => {
149153
expect(result.current.unlockDateFormatted).toBe(formatTimestampToDate(UNLOCK_TIMESTAMP))
150154
})
151155

156+
it('reads an expired timelock as never-locked', () => {
157+
mockStoreState.currentBlockNumber = 40000001
158+
mockStoreState.auctionDetails = buildAuctionDetails({
159+
poolOwner: POOL_OWNER,
160+
liquidityLock: {
161+
lockRecipient: LOCK_RECIPIENT,
162+
lockMode: 1,
163+
unlockBlock: '40000000',
164+
lpOperator: LP_OPERATOR,
165+
},
166+
})
167+
168+
const { result } = renderHook(() => useAuctionLiquidityLock())
169+
170+
// Lock-status indicators clear: no unlock date, LP owner falls back to the pool owner
171+
expect(result.current.isLocked).toBe(false)
172+
expect(result.current.isBuybackEnabled).toBe(false)
173+
expect(result.current.isFeesForwarder).toBe(false)
174+
expect(result.current.unlockTimestamp).toBeUndefined()
175+
expect(result.current.unlockDateFormatted).toBeUndefined()
176+
expect(result.current.lpOwner).toBe(POOL_OWNER)
177+
})
178+
179+
it('keeps the buyback-burn stat and pill after the lock expires', () => {
180+
mockStoreState.currentBlockNumber = 40000001
181+
mockStoreState.auctionDetails = buildAuctionDetails({
182+
poolOwner: POOL_OWNER,
183+
liquidityLock: {
184+
lockRecipient: LOCK_RECIPIENT,
185+
lockMode: 3,
186+
unlockBlock: '40000000',
187+
lpOperator: LP_OPERATOR,
188+
totalTokensBurned: BURNED_RAW,
189+
},
190+
})
191+
192+
const { result } = renderHook(() => useAuctionLiquidityLock())
193+
194+
// Lock-status indicators clear on expiry...
195+
expect(result.current.isLocked).toBe(false)
196+
expect(result.current.unlockDateFormatted).toBeUndefined()
197+
expect(result.current.lpOwner).toBe(POOL_OWNER)
198+
// ...but buyback & burn is a permanent characteristic and must persist
199+
expect(result.current.isBuybackEnabled).toBe(true)
200+
expect(result.current.hasBurnedTokens).toBe(true)
201+
expect(result.current.burnedAmountFormatted).toBe('1.25M TCAN')
202+
expect(result.current.burnedUsdFormatted).toBe('$187500000')
203+
})
204+
205+
it('shows no buyback stat for a never-locked auction', () => {
206+
mockStoreState.auctionDetails = buildAuctionDetails({ poolOwner: POOL_OWNER })
207+
208+
const { result } = renderHook(() => useAuctionLiquidityLock())
209+
210+
expect(result.current.isBuybackEnabled).toBe(false)
211+
expect(result.current.hasBurnedTokens).toBe(false)
212+
expect(result.current.burnedAmountFormatted).toBeUndefined()
213+
})
214+
215+
it('stays locked when the unlock block has not been reached yet', () => {
216+
mockStoreState.currentBlockNumber = 39999999
217+
mockStoreState.auctionDetails = buildAuctionDetails({
218+
poolOwner: POOL_OWNER,
219+
liquidityLock: {
220+
lockRecipient: LOCK_RECIPIENT,
221+
lockMode: 1,
222+
unlockBlock: '40000000',
223+
lpOperator: LP_OPERATOR,
224+
},
225+
})
226+
227+
const { result } = renderHook(() => useAuctionLiquidityLock())
228+
229+
expect(result.current.isLocked).toBe(true)
230+
expect(result.current.lockMode).toBe(AuctionLockMode.Timelock)
231+
expect(result.current.lpOwner).toBe(LP_OPERATOR)
232+
expect(result.current.unlockDateFormatted).toBe(formatTimestampToDate(UNLOCK_TIMESTAMP))
233+
})
234+
235+
it('keeps a permanent lock locked regardless of the current block', () => {
236+
mockStoreState.currentBlockNumber = 260000000001
237+
mockStoreState.auctionDetails = buildAuctionDetails({
238+
liquidityLock: {
239+
lockRecipient: LOCK_RECIPIENT,
240+
lockMode: 1,
241+
unlockBlock: '260000000000',
242+
lpOperator: LP_OPERATOR,
243+
lockedForever: true,
244+
},
245+
})
246+
247+
const { result } = renderHook(() => useAuctionLiquidityLock())
248+
249+
expect(result.current.isLocked).toBe(true)
250+
expect(result.current.isPermanentlyLocked).toBe(true)
251+
expect(result.current.lockMode).toBe(AuctionLockMode.Timelock)
252+
expect(result.current.lpOwner).toBe(LP_OPERATOR)
253+
})
254+
255+
it('stays locked while the current block is still loading (no flash-off)', () => {
256+
mockStoreState.currentBlockNumber = undefined
257+
mockStoreState.auctionDetails = buildAuctionDetails({
258+
poolOwner: POOL_OWNER,
259+
liquidityLock: {
260+
lockRecipient: LOCK_RECIPIENT,
261+
lockMode: 1,
262+
unlockBlock: '40000000',
263+
lpOperator: LP_OPERATOR,
264+
},
265+
})
266+
267+
const { result } = renderHook(() => useAuctionLiquidityLock())
268+
269+
expect(result.current.isLocked).toBe(true)
270+
expect(result.current.lockMode).toBe(AuctionLockMode.Timelock)
271+
expect(result.current.lpOwner).toBe(LP_OPERATOR)
272+
})
273+
152274
it('reads a burn lock as permanently locked with no unlock date or LP owner', () => {
153275
mockStoreState.auctionDetails = buildAuctionDetails({
154276
poolOwner: POOL_OWNER,

apps/web/src/features/Toucan/Auction/hooks/useAuctionLiquidityLock.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,15 @@ export interface AuctionLiquidityLockData {
1717
* timelocks (`locked_forever` on the wire). Render "Forever" instead of an unlock date.
1818
*/
1919
isPermanentlyLocked: boolean
20-
/** Lock mode — undefined when not locked or the mode is unrecognized */
20+
/**
21+
* Lock mode from the raw lock data — undefined only when there is no lock or the mode is
22+
* unrecognized. Independent of expiry (buyback-burn characteristics persist after a lock expires).
23+
*/
2124
lockMode: AuctionLockMode | undefined
22-
/** True when the lock is a buyback & burn recipient. All buyback UI must be hidden when false. */
25+
/**
26+
* True when the lock is a buyback & burn recipient. Independent of expiry — the burn is
27+
* permanent, so this stays true after a lock unlocks. All buyback UI must be hidden when false.
28+
*/
2329
isBuybackEnabled: boolean
2430
/** True when the lock is a fees-forwarder recipient */
2531
isFeesForwarder: boolean
@@ -76,6 +82,36 @@ function parseBigIntOrUndefined(value: string | number | bigint | undefined): bi
7682
}
7783
}
7884

85+
/**
86+
* A lock reads as active only while its unlock block is still in the future. Compare block
87+
* numbers against `currentBlockNumber` — never the estimated unlock date, which drifts.
88+
* `lockedForever` locks (burn / legacy max-int sentinel) never expire; any finite-unlockBlock
89+
* lock — including buyback-burn — expires once the block is reached. While `currentBlockNumber`
90+
* is undefined (still loading) the lock stays active to avoid a flash-off of the indicators.
91+
*/
92+
function resolveIsLocked({
93+
hasLockRecipient,
94+
isPermanentlyLocked,
95+
unlockBlock,
96+
currentBlockNumber,
97+
}: {
98+
hasLockRecipient: boolean
99+
isPermanentlyLocked: boolean
100+
unlockBlock: bigint | undefined
101+
currentBlockNumber: number | undefined
102+
}): boolean {
103+
if (isPermanentlyLocked) {
104+
return true
105+
}
106+
if (!hasLockRecipient) {
107+
return false
108+
}
109+
if (unlockBlock === undefined || currentBlockNumber === undefined) {
110+
return true
111+
}
112+
return BigInt(currentBlockNumber) < unlockBlock
113+
}
114+
79115
/**
80116
* Derives display state for an auction's liquidity lock (timelock / fees-forwarder /
81117
* buyback & burn) from `GetAuction` data.
@@ -93,27 +129,43 @@ export function useAuctionLiquidityLock(): AuctionLiquidityLockData {
93129
const { clearingPriceDecimal, bidTokenInfo } = useStatsBannerData()
94130

95131
const auctionDetails = useAuctionStore((state) => state.auctionDetails)
132+
const currentBlockNumber = useAuctionStore((state) => state.currentBlockNumber)
96133

97134
const lock = auctionDetails?.liquidityLock
98135
// Burn-mode locks and legacy max-int "Permanent" timelocks are served with `lockedForever`
99136
const isPermanentlyLocked = Boolean(lock?.lockedForever)
100-
const isLocked = Boolean(lock?.lockRecipient) || isPermanentlyLocked
101-
const lockMode = isLocked ? parseLockMode(lock?.lockMode) : undefined
137+
138+
// An expired lock reads as never-locked (see resolveIsLocked), so the lock-status indicators —
139+
// chip, lock icon, "LP locked until", lpOwner — clear on expiry. Buyback-burn UI is derived
140+
// separately below and intentionally survives expiry.
141+
const unlockBlockBigInt = parseBigIntOrUndefined(lock?.unlockBlock)
142+
const isLocked = resolveIsLocked({
143+
hasLockRecipient: Boolean(lock?.lockRecipient),
144+
isPermanentlyLocked,
145+
unlockBlock: unlockBlockBigInt,
146+
currentBlockNumber,
147+
})
148+
149+
// Buyback & burn is a permanent token characteristic, not a lock-active state, so its mode and
150+
// burned totals must survive lock expiry — derive them from the raw lock mode, never from the
151+
// expiry-aware isLocked. Fees-forwarding, by contrast, stops once the LP unlocks (see feeRecipient).
152+
const lockMode = parseLockMode(lock?.lockMode)
102153
const isBuybackEnabled = lockMode === AuctionLockMode.BuybackBurn
103154
const isFeesForwarder = lockMode === AuctionLockMode.FeesForwarder
104155

105156
// Backend serves the unlock block only — estimate the calendar date the same way the
106157
// auction countdown estimates future blocks. A permanent lock has no meaningful unlock
107158
// block (0 for burn), so it must never feed the estimator.
108-
const unlockBlock = isLocked && !isPermanentlyLocked ? parseBigIntOrUndefined(lock?.unlockBlock) : undefined
159+
const unlockBlock = isLocked && !isPermanentlyLocked ? unlockBlockBigInt : undefined
109160
const unlockTimestamp = useBlockTimestamp({
110161
chainId: auctionDetails?.chainId,
111162
blockNumber: unlockBlock === undefined ? undefined : Number(unlockBlock),
112163
})
113164
const unlockDateFormatted = unlockTimestamp === undefined ? undefined : formatTimestampToDate(unlockTimestamp)
114165

115166
const lpOwner = (isLocked ? lock?.lpOperator : auctionDetails?.poolOwner) || undefined
116-
const feeRecipient = (isFeesForwarder ? lock?.feeRecipient : undefined) || undefined
167+
// Fee forwarding is only meaningful while the LP is actually locked — hide it once expired
168+
const feeRecipient = (isLocked && isFeesForwarder ? lock?.feeRecipient : undefined) || undefined
117169

118170
const auctionTokenDecimals = getAuctionTokenDecimals(auctionDetails?.token)
119171
const auctionTokenSymbol = auctionDetails?.token?.currency.symbol ?? auctionDetails?.tokenSymbol

0 commit comments

Comments
 (0)