Skip to content

Commit c52dea4

Browse files
authored
chore: merge from develop to preview (#4426)
2 parents da9cc7f + 2503b9a commit c52dea4

File tree

5 files changed

+136
-55
lines changed

5 files changed

+136
-55
lines changed

mobile/packages/tx/transaction-builder/helpers.ts

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -247,20 +247,6 @@ export function selectUtxosForAmounts(
247247
const primaryTokenIdStr =
248248
typeof primaryTokenId === 'string' ? primaryTokenId : primaryTokenId
249249

250-
const requiredAda =
251-
(Object.keys(requiredAmounts) as Array<TokenId>).reduce((sum, tokenId) => {
252-
const tokenIdStr = typeof tokenId === 'string' ? tokenId : tokenId
253-
if (tokenIdStr === primaryTokenIdStr) {
254-
const quantity = requiredAmounts[tokenId]
255-
const qtyStr = typeof quantity === 'string' ? quantity : quantity
256-
return sum + BigInt(qtyStr || '0')
257-
}
258-
return sum
259-
}, BigInt(0)) +
260-
BigInt(feeStr) +
261-
minUtxoValue +
262-
feeBuffer
263-
264250
// Get all required token IDs (excluding primary token)
265251
const requiredTokenIds = new Set(
266252
Object.keys(requiredAmounts).filter((id) => {
@@ -299,7 +285,6 @@ export function selectUtxosForAmounts(
299285
}
300286

301287
// Check if we have enough of each token
302-
let needsMoreAda = selectedAda < requiredAda
303288
const needsMoreTokens: TokenId[] = []
304289

305290
for (const tokenId of requiredTokenIds) {
@@ -331,11 +316,105 @@ export function selectUtxosForAmounts(
331316
return selected
332317
}
333318

319+
// Calculate tokens that will remain in change (tokens in selected UTXOs minus tokens being sent)
320+
const tokensInChange: Record<string, bigint> = {}
321+
for (const [tokenId, inputAmount] of Object.entries(selectedAmounts)) {
322+
if (tokenId === primaryTokenIdStr) continue
323+
const outputAmount = BigInt(requiredAmounts[tokenId as TokenId] || '0')
324+
const remaining = inputAmount - outputAmount
325+
if (remaining > BigInt(0)) {
326+
tokensInChange[tokenId] = remaining
327+
}
328+
}
329+
330+
// Estimate minimum UTXO for change output based on token count
331+
// When tokens will be in change, the minimum UTXO requirement increases significantly
332+
// Use a conservative estimate: base minimum + additional ADA per token
333+
// This is a heuristic since exact calculation requires CSL
334+
const tokenCountInChange = Object.keys(tokensInChange).length
335+
let estimatedMinUtxoForChange = minUtxoValue
336+
if (tokenCountInChange > 0) {
337+
// Conservative estimate: base minimum + 0.1 ADA per token (scaled for safety)
338+
// Actual minimum can be higher depending on token sizes, but this provides a buffer
339+
const adaPerToken = BigInt('100000') // 0.1 ADA per token
340+
const tokenMultiplier = BigInt(Math.max(tokenCountInChange, 1))
341+
estimatedMinUtxoForChange =
342+
minUtxoValue + adaPerToken * tokenMultiplier * BigInt(2) // 2x multiplier for safety
343+
}
344+
345+
// Calculate base required ADA (outputs + fee + buffer) - min UTXO calculated separately
346+
const baseRequiredAda =
347+
(Object.keys(requiredAmounts) as Array<TokenId>).reduce((sum, tokenId) => {
348+
const tokenIdStr = typeof tokenId === 'string' ? tokenId : tokenId
349+
if (tokenIdStr === primaryTokenIdStr) {
350+
const quantity = requiredAmounts[tokenId]
351+
const qtyStr = typeof quantity === 'string' ? quantity : quantity
352+
return sum + BigInt(qtyStr || '0')
353+
}
354+
return sum
355+
}, BigInt(0)) +
356+
BigInt(feeStr) +
357+
feeBuffer
358+
359+
// Calculate required ADA including proper change output minimum
360+
let requiredAda = baseRequiredAda + estimatedMinUtxoForChange
361+
334362
// If we need more ADA, select from remaining UTXOs
363+
// Prefer pure ADA UTXOs to avoid adding unexpected tokens to change output
364+
// This prevents underestimating minimum UTXO when non-required tokens end up in change
365+
let needsMoreAda = selectedAda < requiredAda
335366
if (needsMoreAda) {
336-
const sortedRemaining = sortUtxosByAda(utxosWithoutTokens, primaryTokenId)
367+
const pureAdaUtxos = filterPureAdaUtxos(utxosWithoutTokens, primaryTokenId)
368+
const otherUtxos = utxosWithoutTokens.filter(
369+
(u) => !pureAdaUtxos.includes(u),
370+
)
371+
372+
// Always prefer pure ADA UTXOs first to avoid adding tokens to change
373+
// This ensures minimum UTXO estimate remains accurate
374+
const sortedRemaining = [
375+
...sortUtxosByAda(pureAdaUtxos, primaryTokenId),
376+
...sortUtxosByAda(otherUtxos, primaryTokenId),
377+
]
378+
379+
// Track tokens being added and update minimum UTXO estimate dynamically
380+
let currentTokensInChange = {...tokensInChange}
381+
let currentEstimatedMinUtxo = estimatedMinUtxoForChange
382+
const adaPerToken = BigInt('100000') // 0.1 ADA per token
337383

338384
for (const utxo of sortedRemaining) {
385+
// Check if this UTXO adds any tokens to change
386+
const utxoTokenIds = (Object.keys(utxo.balance) as Array<TokenId>).filter(
387+
(id) => {
388+
const idStr = typeof id === 'string' ? id : id
389+
return idStr !== primaryTokenIdStr
390+
},
391+
)
392+
393+
// Update tokens in change if UTXO contains non-required tokens
394+
if (utxoTokenIds.length > 0) {
395+
for (const tokenId of utxoTokenIds) {
396+
const tokenIdStr = typeof tokenId === 'string' ? tokenId : tokenId
397+
const tokenAmount = BigInt(utxo.balance[tokenId] || '0')
398+
if (tokenAmount > BigInt(0)) {
399+
// This token will be added to change (not required, so goes to change)
400+
if (!currentTokensInChange[tokenIdStr]) {
401+
currentTokensInChange[tokenIdStr] = BigInt(0)
402+
}
403+
currentTokensInChange[tokenIdStr] += tokenAmount
404+
}
405+
}
406+
407+
// Recalculate minimum UTXO estimate based on updated token count
408+
const newTokenCount = Object.keys(currentTokensInChange).length
409+
if (newTokenCount > 0) {
410+
const tokenMultiplier = BigInt(newTokenCount)
411+
currentEstimatedMinUtxo =
412+
minUtxoValue + adaPerToken * tokenMultiplier * BigInt(2)
413+
// Update required ADA with new minimum UTXO estimate
414+
requiredAda = baseRequiredAda + currentEstimatedMinUtxo
415+
}
416+
}
417+
339418
if (selectedAda >= requiredAda) break
340419
selected.push(utxo)
341420
selectedAda += BigInt(utxo.balance[primaryTokenIdStr] || '0')

mobile/src/features/Airdrop/ui/AirdropDetailsScreen.tsx

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,15 @@ import {
99
} from '@react-navigation/native'
1010
import {StackNavigationProp} from '@react-navigation/stack'
1111
import {BigNumber} from 'bignumber.js'
12+
import {LinearGradient} from 'expo-linear-gradient'
1213
import * as React from 'react'
13-
import {ScrollView, Text, TouchableOpacity, View} from 'react-native'
14+
import {
15+
ScrollView,
16+
StyleSheet,
17+
Text,
18+
TouchableOpacity,
19+
View,
20+
} from 'react-native'
1421
import {SafeAreaView} from 'react-native-safe-area-context'
1522

1623
import {Address} from '~/common/Address/Address'
@@ -333,15 +340,25 @@ export const AirdropDetailsScreen = () => {
333340
{/* Redeemable Now Card */}
334341
<TouchableOpacity
335342
activeOpacity={0.8}
336-
style={[
337-
a.p_lg,
338-
a.rounded_sm,
339-
{
340-
backgroundColor:
341-
currentlyRedeemableAmount > 0 ? p.bg_gradient_2[0] : p.gray_400,
342-
},
343-
]}
343+
style={[a.p_lg, a.rounded_sm, a.overflow_hidden]}
344344
>
345+
{currentlyRedeemableAmount > 0 ? (
346+
<LinearGradient
347+
colors={p.bg_gradient_1}
348+
start={{x: 1, y: 1}}
349+
end={{x: 0, y: 0}}
350+
style={[StyleSheet.absoluteFill]}
351+
/>
352+
) : (
353+
<View
354+
style={[
355+
StyleSheet.absoluteFill,
356+
a.border,
357+
a.rounded_sm,
358+
{borderColor: p.gray_100},
359+
]}
360+
/>
361+
)}
345362
<View style={[a.flex_row, a.justify_between, a.align_center]}>
346363
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
347364
<Text style={[a.body_1_lg_medium, ta.text_gray_max]}>
@@ -385,14 +402,7 @@ export const AirdropDetailsScreen = () => {
385402
<TouchableOpacity
386403
onPress={handleOpenThawSchedule}
387404
activeOpacity={0.8}
388-
style={[
389-
a.p_lg,
390-
a.rounded_sm,
391-
{
392-
borderWidth: 1,
393-
borderColor: p.gray_200,
394-
},
395-
]}
405+
style={[a.p_lg, a.rounded_sm, a.border, {borderColor: p.gray_200}]}
396406
>
397407
<View style={[a.flex_row, a.justify_between, a.align_center]}>
398408
<View style={[a.flex_row, a.align_center, a.gap_xs]}>

mobile/src/features/Airdrop/ui/AirdropSelectionScreen.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {useNavigation} from '@react-navigation/native'
44
import {StackNavigationProp} from '@react-navigation/stack'
55
import {useQueryClient} from '@tanstack/react-query'
66
import {BigNumber} from 'bignumber.js'
7+
import {LinearGradient} from 'expo-linear-gradient'
78
import * as React from 'react'
89
import {useIntl} from 'react-intl'
910
import {
@@ -12,6 +13,7 @@ import {
1213
Pressable,
1314
RefreshControl,
1415
ScrollView,
16+
StyleSheet,
1517
Text,
1618
TouchableOpacity,
1719
View,
@@ -390,15 +392,15 @@ const AddressCard = ({
390392
]}
391393
>
392394
{({pressed}) => (
393-
<View
394-
style={[
395-
a.flex_row,
396-
{
397-
backgroundColor:
398-
pressed || hasRedeemable ? p.bg_gradient_1[0] : 'transparent',
399-
},
400-
]}
401-
>
395+
<View style={[a.flex_row]}>
396+
{(pressed || hasRedeemable) && (
397+
<LinearGradient
398+
colors={pressed ? p.bg_gradient_2 : p.bg_gradient_1}
399+
start={{x: 1, y: 1}}
400+
end={{x: 0, y: 0}}
401+
style={[StyleSheet.absoluteFill]}
402+
/>
403+
)}
402404
<View style={[a.flex_1, a.p_lg]}>
403405
{/* Header row */}
404406
<View style={[a.flex_row, a.justify_between, a.align_center]}>

mobile/src/features/WalletManager/ui/screens/SelectWalletFromListScreen/WalletListItem.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import {
2323
ChevronRightDarkIllustration,
2424
ChevronRightGrayIllustration,
2525
} from '~/features/SetupWallet/illustrations/ChevronRight'
26-
import {features} from '~/kernel/features'
2726
import {Icon} from '~/ui/Icon'
2827
import {Loading} from '~/ui/Loading/Loading'
2928
import {Space} from '~/ui/Space/Space'
@@ -166,17 +165,11 @@ export const WalletListItem = ({
166165
</>
167166
)}
168167

169-
{features.walletListFeedback && (
170-
<>
171-
{(syncWalletInfo?.status === 'syncing' || isLoading) && (
172-
<Loading />
173-
)}
168+
{(syncWalletInfo?.status === 'syncing' || isLoading) && <Loading />}
174169

175-
<Space.Width.md />
170+
<Space.Width.md />
176171

177-
{isSelected && <Icon.Check size={20} color={p.primary_600} />}
178-
</>
179-
)}
172+
{isSelected && <Icon.Check size={20} color={p.primary_600} />}
180173

181174
<Space.Width.xl />
182175

mobile/src/kernel/features.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@ import {isDev} from './constants'
33
export const features = {
44
useTestnet: false,
55
prefillWalletInfo: false,
6-
showProdPoolsInDev: isDev,
76
moderatingNftsEnabled: false,
87
poolTransition: true,
98
portfolioPerformance: false,
109
portfolioNews: false,
1110
portfolioExport: false,
12-
walletListFeedback: isDev,
1311
walletListAggregatedBalance: false,
14-
walletListSwipeableActions: isDev,
1512
swapTokenLinks: true,
1613
utxoConsolidation: isDev,
1714
pushNotifications: true,

0 commit comments

Comments
 (0)