Skip to content

Commit 8ed45f0

Browse files
ci(release): publish latest release
1 parent 0abac11 commit 8ed45f0

11 files changed

Lines changed: 226 additions & 31 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: `Qma941FXDbQqHNpD7hkUYG1PrK3s6vmuimRLL9M8p5tm3Z`
3-
- CIDv1: `bafybeifpkoosgylgtfnzobmelsuj6wjfwlpzwy5mg2jilplofd5tmxtfdq`
2+
- CIDv0: `QmWxnggRwg77vUtPTytonsniRaCU2EHrNmpSdLqFuoQMvp`
3+
- CIDv1: `bafybeiead5wcaopistyt53f5zrvgb7wkxfnzssqo2vuzu37pw4z62drqpe`
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://bafybeifpkoosgylgtfnzobmelsuj6wjfwlpzwy5mg2jilplofd5tmxtfdq.ipfs.dweb.link/
14-
- [ipfs://Qma941FXDbQqHNpD7hkUYG1PrK3s6vmuimRLL9M8p5tm3Z/](ipfs://Qma941FXDbQqHNpD7hkUYG1PrK3s6vmuimRLL9M8p5tm3Z/)
13+
- https://bafybeiead5wcaopistyt53f5zrvgb7wkxfnzssqo2vuzu37pw4z62drqpe.ipfs.dweb.link/
14+
- [ipfs://QmWxnggRwg77vUtPTytonsniRaCU2EHrNmpSdLqFuoQMvp/](ipfs://QmWxnggRwg77vUtPTytonsniRaCU2EHrNmpSdLqFuoQMvp/)

VERSION

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

apps/web/src/pages/Liquidity/CreateAuction/components/AuctionSupplySection.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AuctionSupplySelector } from '~/pages/Liquidity/CreateAuction/component
66
interface AuctionSupplySectionProps {
77
auctionSupplyAmount: CurrencyAmount<Currency>
88
tokenTotalSupply: CurrencyAmount<Currency>
9+
maxAuctionSupplyAmount: CurrencyAmount<Currency>
910
minAuctionSupplyAmount: CurrencyAmount<Currency>
1011
tokenSymbol: string
1112
onSelectAuctionSupplyPercent: (percent: number) => void
@@ -15,6 +16,7 @@ interface AuctionSupplySectionProps {
1516
export function AuctionSupplySection({
1617
auctionSupplyAmount,
1718
tokenTotalSupply,
19+
maxAuctionSupplyAmount,
1820
minAuctionSupplyAmount,
1921
tokenSymbol,
2022
onSelectAuctionSupplyPercent,
@@ -35,6 +37,7 @@ export function AuctionSupplySection({
3537
<AuctionSupplySelector
3638
auctionSupplyAmount={auctionSupplyAmount}
3739
tokenTotalSupply={tokenTotalSupply}
40+
maxAuctionSupplyAmount={maxAuctionSupplyAmount}
3841
minAuctionSupplyAmount={minAuctionSupplyAmount}
3942
tokenSymbol={tokenSymbol}
4043
onSelectPercent={onSelectAuctionSupplyPercent}

apps/web/src/pages/Liquidity/CreateAuction/components/AuctionSupplySelector.tsx

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ function parseSuffixedAmount(input: string, currency: Currency): CurrencyAmount<
4040
interface AuctionSupplySelectorProps {
4141
auctionSupplyAmount: CurrencyAmount<Currency>
4242
tokenTotalSupply: CurrencyAmount<Currency>
43+
/**
44+
* Ceiling the deposit is clamped to. For a new token this is the full minted supply; for an
45+
* existing token it is the connected wallet's held balance (what the auction can actually pull).
46+
* The "Max" preset and percent presets are computed against this, not the total supply.
47+
*/
48+
maxAuctionSupplyAmount: CurrencyAmount<Currency>
4349
/** Smallest deposit whose sold/LP split keeps both legs at >= 1 base unit; clamped to on blur. */
4450
minAuctionSupplyAmount: CurrencyAmount<Currency>
4551
tokenSymbol: string
@@ -50,6 +56,7 @@ interface AuctionSupplySelectorProps {
5056
export function AuctionSupplySelector({
5157
auctionSupplyAmount,
5258
tokenTotalSupply,
59+
maxAuctionSupplyAmount,
5360
minAuctionSupplyAmount,
5461
tokenSymbol,
5562
onSelectPercent,
@@ -93,11 +100,11 @@ export function AuctionSupplySelector({
93100
if (!parsed) {
94101
return
95102
}
96-
// Live-update with exact amount; cap to total supply so the store stays valid
97-
const capped = parsed.greaterThan(tokenTotalSupply) ? tokenTotalSupply : parsed
103+
// Live-update with exact amount; cap to the deposit ceiling so the store stays valid
104+
const capped = parsed.greaterThan(maxAuctionSupplyAmount) ? maxAuctionSupplyAmount : parsed
98105
onAmountChange(capped)
99106
},
100-
[currency, tokenTotalSupply, onAmountChange],
107+
[currency, maxAuctionSupplyAmount, onAmountChange],
101108
)
102109

103110
const {
@@ -115,7 +122,7 @@ export function AuctionSupplySelector({
115122
() => (isFocused ? parseSuffixedAmount(rawInput, currency) : null),
116123
[isFocused, rawInput, currency],
117124
)
118-
const exceedsTotalSupply = parsedAmount !== null && parsedAmount.greaterThan(tokenTotalSupply)
125+
const exceedsMax = parsedAmount !== null && parsedAmount.greaterThan(maxAuctionSupplyAmount)
119126

120127
const handleFocus = useCallback(() => {
121128
setIsFocused(true)
@@ -132,17 +139,17 @@ export function AuctionSupplySelector({
132139
return
133140
}
134141

135-
// Clamp into [min, totalSupply] on blur: the lower bound keeps the sold/LP split from
136-
// rounding a leg to zero base units (a degenerate auction that divides by zero in the
137-
// percent math); the upper bound keeps the deposit within the available supply. Raise to
138-
// the min first, then cap at total supply, so the supply ceiling always wins — in the
139-
// pathological case where min > totalSupply (a token with fewer base units than the
140-
// minimum two-leg deposit) the result never exceeds what exists, and the step's own
141-
// validation disables Continue since the deposit stays below the minimum.
142+
// Clamp into [min, max] on blur: the lower bound keeps the sold/LP split from rounding a leg to
143+
// zero base units (a degenerate auction that divides by zero in the percent math); the upper
144+
// bound is the deposit ceiling (full supply for a new token, wallet balance for an existing one).
145+
// Raise to the min first, then cap at the ceiling, so the ceiling always wins — in the
146+
// pathological case where min > max (a token with fewer base units than the minimum two-leg
147+
// deposit, or a wallet holding too little) the result never exceeds the ceiling, and the step's
148+
// own validation disables Continue since the deposit stays below the minimum.
142149
const raised = parsed.lessThan(minAuctionSupplyAmount) ? minAuctionSupplyAmount : parsed
143-
const clamped = raised.greaterThan(tokenTotalSupply) ? tokenTotalSupply : raised
150+
const clamped = raised.greaterThan(maxAuctionSupplyAmount) ? maxAuctionSupplyAmount : raised
144151
onAmountChange(clamped)
145-
}, [rawInput, currency, tokenTotalSupply, minAuctionSupplyAmount, onAmountChange])
152+
}, [rawInput, currency, maxAuctionSupplyAmount, minAuctionSupplyAmount, onAmountChange])
146153

147154
const trace = useTrace()
148155
const handleSelectPercent = useCallback(
@@ -195,7 +202,7 @@ export function AuctionSupplySelector({
195202
fontSize={fonts.heading3.fontSize}
196203
lineHeight={fonts.heading3.lineHeight}
197204
fontWeight={fonts.heading3.fontWeight}
198-
color={exceedsTotalSupply ? '$statusCritical' : '$neutral1'}
205+
color={exceedsMax ? '$statusCritical' : '$neutral1'}
199206
backgroundColor="$transparent"
200207
/>
201208
</Trace>
@@ -237,13 +244,13 @@ export function AuctionSupplySelector({
237244
<PercentButton
238245
key={pillPercent}
239246
label={`${pillPercent}%`}
240-
isActive={auctionSupplyAmount.equalTo(percentOfAmount(tokenTotalSupply, pillPercent))}
247+
isActive={auctionSupplyAmount.equalTo(percentOfAmount(maxAuctionSupplyAmount, pillPercent))}
241248
onPress={handleSelectPercent.bind(null, pillPercent)}
242249
/>
243250
))}
244251
<PercentButton
245252
label={t('common.max')}
246-
isActive={auctionSupplyAmount.equalTo(tokenTotalSupply)}
253+
isActive={auctionSupplyAmount.equalTo(maxAuctionSupplyAmount)}
247254
onPress={handleSelectPercent.bind(null, 100)}
248255
/>
249256
</Flex>

apps/web/src/pages/Liquidity/CreateAuction/components/LaunchAuctionErrorModal.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { Dialog } from 'uniswap/src/components/dialog/Dialog'
44
import { UniswapHelpUrls } from 'uniswap/src/constants/urls'
55
import { ModalName } from 'uniswap/src/features/telemetry/constants'
66
import { getCreateAuctionErrorMessage } from '~/pages/Liquidity/CreateAuction/getCreateAuctionErrorMessage'
7-
import { AuctionStartTimePassedError } from '~/pages/Liquidity/CreateAuction/hooks/useCreateAuctionSubmit'
7+
import {
8+
AuctionInsufficientBalanceError,
9+
AuctionStartTimePassedError,
10+
} from '~/pages/Liquidity/CreateAuction/hooks/useCreateAuctionSubmit'
811

912
interface LaunchAuctionErrorModalProps {
1013
isOpen: boolean
@@ -27,16 +30,16 @@ export function LaunchAuctionErrorModal({
2730
// Known pre-submission errors get specific, actionable copy. Backend input-validation failures
2831
// (e.g. an unsupported fee tier) carry a meaningful reason worth surfacing verbatim; everything
2932
// else falls back to a generic, localized reason since raw backend messages aren't user-friendly.
30-
// Verbatim backend reasons render on their own line; the generic reason stays inline.
3133
const validationReason = getCreateAuctionErrorMessage(error)
3234
const genericReason = t('toucan.createAuction.launchError.genericReason')
3335
const subtext =
3436
error instanceof AuctionStartTimePassedError
3537
? t('toucan.createAuction.launchError.startTimePassed', { tokenSymbol })
36-
: t('toucan.createAuction.launchError.description', {
37-
tokenSymbol,
38-
reason: validationReason ? `\n${validationReason}` : genericReason,
39-
})
38+
: error instanceof AuctionInsufficientBalanceError
39+
? t('toucan.createAuction.launchError.insufficientBalance', { tokenSymbol })
40+
: validationReason
41+
? t('toucan.createAuction.launchError.withReason', { tokenSymbol, reason: validationReason })
42+
: t('toucan.createAuction.launchError.description', { tokenSymbol, reason: genericReason })
4043

4144
return (
4245
<Dialog

apps/web/src/pages/Liquidity/CreateAuction/hooks/useCreateAuctionSubmit.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { act, renderHook } from '@testing-library/react'
2+
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
3+
import { UniverseChainId } from 'uniswap/src/features/chains/types'
4+
import type { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
25
import { AuctionEventName } from 'uniswap/src/features/telemetry/constants'
36
import { beforeEach, describe, expect, it, vi } from 'vitest'
47
import { zeroAddress } from '~/chains'
58
import {
9+
AuctionInsufficientBalanceError,
610
AuctionStartTimePassedError,
711
useCreateAuctionSubmit,
812
} from '~/pages/Liquidity/CreateAuction/hooks/useCreateAuctionSubmit'
913
import { createCreateAuctionStore } from '~/pages/Liquidity/CreateAuction/store/createCreateAuctionStore'
14+
import { TokenMode } from '~/pages/Liquidity/CreateAuction/types'
1015
import { MS_PER_DAY } from '~/pages/Liquidity/CreateAuction/utils/duration'
1116

1217
const WALLET = '0xF570F45f598fD48AF83FABD692629a2caFe899ec'
@@ -58,6 +63,37 @@ function buildableParams(overrides: Partial<Params> = {}): Params {
5863
}
5964
}
6065

66+
const UNI_ADDRESS = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'
67+
68+
/** Buildable params for an existing token (UNI) with a configured deposit of `depositRaw` base units. */
69+
function existingTokenParams(depositRaw: string, overrides: Partial<Params> = {}): Params {
70+
const store = createCreateAuctionStore()
71+
const { actions } = store.getState()
72+
const uni = new Token(UniverseChainId.Mainnet, UNI_ADDRESS, 18, 'UNI', 'Uniswap')
73+
actions.setTokenForm({
74+
mode: TokenMode.EXISTING,
75+
existingTokenCurrencyInfo: { currency: uni } as CurrencyInfo,
76+
description: '',
77+
xProfile: '',
78+
websiteLink: '',
79+
totalSupply: CurrencyAmount.fromRawAmount(uni, '1000000000000000000000000000'),
80+
})
81+
actions.commitTokenFormAndAdvance()
82+
actions.setStartTime(new Date(Date.now() + MS_PER_DAY))
83+
actions.setEndTime(new Date(Date.now() + 4 * MS_PER_DAY))
84+
actions.setFloorPrice('0.1')
85+
actions.setAuctionConfig({ auctionSupplyAmount: CurrencyAmount.fromRawAmount(uni, depositRaw) })
86+
const { tokenForm, configureAuction, customizePool } = store.getState()
87+
return {
88+
tokenForm,
89+
configureAuction,
90+
customizePool,
91+
walletAddress: WALLET,
92+
currencyAddress: zeroAddress,
93+
...overrides,
94+
}
95+
}
96+
6197
beforeEach(() => {
6298
mockMutateAsync.mockReset()
6399
mockValidate.mockReset()
@@ -130,6 +166,21 @@ describe('useCreateAuctionSubmit', () => {
130166
expect(result.current.error).toBeInstanceOf(AuctionStartTimePassedError)
131167
})
132168

169+
it('errors without calling the mutation when the deposit exceeds the wallet balance', async () => {
170+
// Configured to deposit 1000 base units but the wallet only holds 600.
171+
const params = existingTokenParams('1000', { existingTokenWalletBalanceRaw: 600n })
172+
const { result } = renderHook(() => useCreateAuctionSubmit(params))
173+
174+
let returned: Awaited<ReturnType<typeof result.current.onLaunch>>
175+
await act(async () => {
176+
returned = await result.current.onLaunch()
177+
})
178+
179+
expect(returned).toBeUndefined()
180+
expect(mockMutateAsync).not.toHaveBeenCalled()
181+
expect(result.current.error).toBeInstanceOf(AuctionInsufficientBalanceError)
182+
})
183+
133184
it('returns the validated result on success', async () => {
134185
const tx = { to: '0xto', from: WALLET, data: '0x', value: '0x0', chainId: 1 }
135186
mockMutateAsync.mockResolvedValue({

apps/web/src/pages/Liquidity/CreateAuction/hooks/useCreateAuctionSubmit.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ export class AuctionStartTimePassedError extends Error {
2626
}
2727
}
2828

29+
/**
30+
* Thrown at launch time when the wallet holds fewer tokens than the configured deposit — the auction
31+
* pulls the deposit from the wallet, so the backend would reject it. Mapped to actionable copy in
32+
* `LaunchAuctionErrorModal`.
33+
*/
34+
export class AuctionInsufficientBalanceError extends Error {
35+
constructor() {
36+
super('Wallet token balance is insufficient to fund the auction')
37+
this.name = 'AuctionInsufficientBalanceError'
38+
}
39+
}
40+
2941
export interface CreateAuctionSubmitResult {
3042
predictedTokenAddress: string
3143
predictedAuctionAddress: string
@@ -45,6 +57,12 @@ interface UseCreateAuctionSubmitParams {
4557
currencyAddress: string | undefined
4658
/** From X OAuth / VerifyXCallback when the creator linked their handle. */
4759
xVerificationToken?: string | null
60+
/**
61+
* Existing-token only: the connected wallet's on-chain balance in base units. Used for the
62+
* build-time insufficient-balance check so a deposit larger than the held balance can't hit the
63+
* backend's "insufficient balance" rejection.
64+
*/
65+
existingTokenWalletBalanceRaw?: bigint
4866
/**
4967
* Builds `Auction Create Failed` properties. Called by this hook at pre-submission failure points
5068
* (`build_request` for local validation, `create_auction_request` when the endpoint throws).
@@ -119,6 +137,7 @@ export function useCreateAuctionSubmit(params: UseCreateAuctionSubmitParams): Us
119137
walletAddress,
120138
currencyAddress,
121139
xVerificationToken,
140+
existingTokenWalletBalanceRaw,
122141
getCreateFailedProperties,
123142
} = params
124143
const createAuctionMutation = useCreateAuctionMutation()
@@ -153,6 +172,19 @@ export function useCreateAuctionSubmit(params: UseCreateAuctionSubmitParams): Us
153172
return undefined
154173
}
155174

175+
// Existing tokens pull the deposit from the wallet, so a deposit larger than the held balance
176+
// can't be funded — fail here instead of letting the backend reject it.
177+
if (
178+
existingTokenWalletBalanceRaw !== undefined &&
179+
configureAuction.committed &&
180+
BigInt(configureAuction.committed.auctionSupplyAmount.quotient.toString()) > existingTokenWalletBalanceRaw
181+
) {
182+
const err = new AuctionInsufficientBalanceError()
183+
setError(err)
184+
reportAuctionCreateFailed({ getCreateFailedProperties, failedStep: 'build_request', error: err, diagnostics })
185+
return undefined
186+
}
187+
156188
const request = buildCreateAuctionRequest({
157189
tokenForm,
158190
configureAuction,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { type Currency, CurrencyAmount, type Token } from '@uniswap/sdk-core'
2+
import { useMemo } from 'react'
3+
import { Platform } from 'uniswap/src/features/platforms/types/Platform'
4+
import { useReadContract } from 'wagmi'
5+
import { erc20Abi } from '~/chains'
6+
import { useActiveAddress } from '~/features/accounts/store/hooks'
7+
import { assume0xAddress } from '~/utils/wagmi'
8+
9+
interface UseExistingTokenWalletBalanceResult {
10+
balance: CurrencyAmount<Token> | undefined
11+
isLoading: boolean
12+
isError: boolean
13+
}
14+
15+
/**
16+
* Reads the connected EVM wallet's on-chain balance of the given token, on the token's own chain.
17+
*
18+
* Mirrors {@link useTotalSupply} (a direct wagmi `balanceOf` read keyed on the token's chainId)
19+
* rather than `useCurrencyBalance`, which gates on the wallet's currently-connected chain — an
20+
* existing auction token can live on a chain the wallet isn't actively connected to.
21+
*/
22+
export function useExistingTokenWalletBalance(token?: Currency): UseExistingTokenWalletBalanceResult {
23+
const account = useActiveAddress(Platform.EVM)
24+
const tokenAddress = token?.isToken ? assume0xAddress(token.address) : undefined
25+
26+
const { data, isLoading, isError } = useReadContract({
27+
address: tokenAddress,
28+
chainId: token?.chainId,
29+
abi: erc20Abi,
30+
functionName: 'balanceOf',
31+
args: account ? [assume0xAddress(account)] : undefined,
32+
query: { enabled: !!tokenAddress && !!account },
33+
})
34+
35+
const balance = useMemo(
36+
() => (token?.isToken && data !== undefined ? CurrencyAmount.fromRawAmount(token, data.toString()) : undefined),
37+
[token, data],
38+
)
39+
40+
return { balance, isLoading, isError }
41+
}

0 commit comments

Comments
 (0)