Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E
if (supportedURI?.type === URIType.Address) {
onSelectRecipient(supportedURI.value)
onClose()
} else if (supportedURI?.type === URIType.ERC681) {
onSelectRecipient(supportedURI.value.recipient)
onClose()
} else {
Alert.alert(t('qrScanner.recipient.error.title'), t('qrScanner.recipient.error.message'), [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants
import Trace from 'uniswap/src/features/telemetry/Trace'
import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { UwULinkRequest } from 'uniswap/src/types/walletConnect'
import { TransactionState } from 'uniswap/src/features/transactions/types/transactionState'
import { logger } from 'utilities/src/logger/logger'
import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext'
import { getSendPrefilledState } from 'wallet/src/features/transactions/send/getSendPrefilledState'
import { useContractManager, useProviderManager } from 'wallet/src/features/wallet/context'
import { useActiveAccount } from 'wallet/src/features/wallet/hooks'

Expand All @@ -51,6 +54,7 @@ export function WalletConnectModal({
const [currentScreenState, setCurrentScreenState] = useState<ScannerModalState>(initialScreenState)
const [shouldFreezeCamera, setShouldFreezeCamera] = useState(false)
const { preload, navigate } = useEagerExternalProfileRootNavigation()
const { navigateToSend } = useWalletNavigation()
const dispatch = useDispatch()
const isUwULinkEnabled = useFeatureFlag(FeatureFlags.UwULink)
const isScantasticEnabled = useFeatureFlag(FeatureFlags.Scantastic)
Expand Down Expand Up @@ -105,6 +109,24 @@ export function WalletConnectModal({
return
}

if (supportedURI.type === URIType.ERC681) {
setShouldFreezeCamera(true)
const baseState = getSendPrefilledState({
chainId: supportedURI.value.chainId,
currencyAddress: supportedURI.value.tokenAddress,
})
const initialState: TransactionState = {
...baseState,
recipient: supportedURI.value.recipient,
showRecipientSelector: false,
exactAmountToken: supportedURI.value.formattedAmount ?? '',
isFiatInput: false,
}
navigateToSend({ initialState })
onClose()
return
}

if (supportedURI.type === URIType.WalletConnectURL) {
setShouldFreezeCamera(true)
Alert.alert(t('walletConnect.error.unsupportedV1.title'), t('walletConnect.error.unsupportedV1.message'), [
Expand Down Expand Up @@ -219,6 +241,7 @@ export function WalletConnectModal({
t,
preload,
navigate,
navigateToSend,
onClose,
dispatch,
uwuLinkContractAllowlist,
Expand Down
30 changes: 30 additions & 0 deletions apps/mobile/src/components/Requests/ScanSheet/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,36 @@ describe('getSupportedURI', () => {
expect(await getSupportedURI('ethereum:invalid_address')).toBeUndefined()
})

it('should extract correct ERC681 payload from payment request URI', async () => {
const validErc681Uri = 'ethereum:0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359@8453?value=1e16'
const result = await getSupportedURI(validErc681Uri)
expect(result).toEqual({
type: URIType.ERC681,
value: {
chainId: 8453,
recipient: '0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359',
tokenAddress: undefined,
rawAmount: '10000000000000000',
formattedAmount: '0.01',
},
})
})

it('should extract correct ERC681 payload from token transfer URI', async () => {
const tokenTransferUri = 'ethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913@8453/transfer?address=0x54235780057CC828C92aA40e3b02053881990153&uint256=1e6'
const result = await getSupportedURI(tokenTransferUri)
expect(result).toEqual({
type: URIType.ERC681,
value: {
chainId: 8453,
recipient: '0x54235780057CC828C92aA40e3b02053881990153',
tokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
rawAmount: '1000000',
formattedAmount: undefined,
},
})
})

describe('URL and HTML encoded URIs', () => {
it('should handle percent-encoded WalletConnect v2 URI', async () => {
// Simulate a URI that has been percent-encoded (& becomes %26)
Expand Down
22 changes: 17 additions & 5 deletions apps/mobile/src/components/Requests/ScanSheet/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
UNISWAP_WALLETCONNECT_URL,
} from 'src/features/deepLinking/constants'
import { Platform } from 'uniswap/src/features/platforms/types/Platform'
import { ERC681TransferRequest, parseERC681URI } from 'uniswap/src/features/transactions/send/erc681'
import { getValidAddress } from 'uniswap/src/utils/addresses'
import { logger } from 'utilities/src/logger/logger'
import { ScantasticParams, ScantasticParamsSchema } from 'wallet/src/features/scantastic/types'
Expand All @@ -23,12 +24,18 @@ export enum URIType {
EasterEgg = 'easter-egg',
Scantastic = 'scantastic',
UwULink = 'uwu-link',
ERC681 = 'erc681',
}

type URIFormat = {
type: URIType
value: string
}
export type URIFormat =
| {
type: URIType.ERC681
value: ERC681TransferRequest
}
| {
type: Exclude<URIType, URIType.ERC681>
value: string
}

interface EnabledFeatureFlags {
isUwULinkEnabled: boolean
Expand Down Expand Up @@ -60,10 +67,15 @@ export async function getSupportedURI(
}

const maybeMetamaskAddress = getMetamaskAddress(uri)
if (maybeMetamaskAddress) {
if (maybeMetamaskAddress && !uri.includes('?') && !uri.includes('@') && !uri.includes('/')) {
return { type: URIType.Address, value: maybeMetamaskAddress }
}

const maybeERC681Request = parseERC681URI(uri)
if (maybeERC681Request) {
return { type: URIType.ERC681, value: maybeERC681Request }
}

const maybeScantasticQueryParams = getScantasticQueryParams(uri)
if (enabledFeatureFlags?.isScantasticEnabled && maybeScantasticQueryParams) {
return { type: URIType.Scantastic, value: maybeScantasticQueryParams }
Expand Down
108 changes: 108 additions & 0 deletions packages/uniswap/src/features/transactions/send/erc681.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { formatERC681Amount, parseERC681URI, parseScientificOrIntString } from 'uniswap/src/features/transactions/send/erc681'

describe('parseScientificOrIntString', () => {
it('parses scientific notation accurately without precision loss', () => {
expect(parseScientificOrIntString('2.014e18')).toBe('2014000000000000000')
expect(parseScientificOrIntString('1e16')).toBe('10000000000000000')
expect(parseScientificOrIntString('1e6')).toBe('1000000')
expect(parseScientificOrIntString('1.5e6')).toBe('1500000')
})

it('parses standard integer strings and hex values', () => {
expect(parseScientificOrIntString('1000000')).toBe('1000000')
expect(parseScientificOrIntString('0xde0b6b3a7640000')).toBe('1000000000000000000')
})

it('returns undefined for invalid strings', () => {
expect(parseScientificOrIntString('')).toBeUndefined()
expect(parseScientificOrIntString('invalid')).toBeUndefined()
})
})

describe('formatERC681Amount', () => {
it('formats raw units cleanly without trailing zeros', () => {
expect(formatERC681Amount('2014000000000000000', 18)).toBe('2.014')
expect(formatERC681Amount('10000000000000000', 18)).toBe('0.01')
expect(formatERC681Amount('1000000', 6)).toBe('1')
expect(formatERC681Amount('1500000', 6)).toBe('1.5')
})
})

describe('parseERC681URI', () => {
const TEST_RECIPIENT = '0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359'
const TEST_RECIPIENT_2 = '0x54235780057CC828C92aA40e3b02053881990153'
const TEST_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
const TEST_DAI = '0x6B175474E89094C44Da98b954EedeAC495271d0F'

it('parses simple native ETH transfer on Mainnet (Test Vector 1)', () => {
const uri = `ethereum:${TEST_RECIPIENT}?value=2.014e18`
const result = parseERC681URI(uri)
expect(result).toEqual({
chainId: UniverseChainId.Mainnet,
recipient: TEST_RECIPIENT,
tokenAddress: undefined,
rawAmount: '2014000000000000000',
formattedAmount: '2.014',
})
})

it('parses native ETH transfer on Base (Test Vector 2)', () => {
const uri = `ethereum:${TEST_RECIPIENT}@8453?value=1e16`
const result = parseERC681URI(uri)
expect(result).toEqual({
chainId: UniverseChainId.Base,
recipient: TEST_RECIPIENT,
tokenAddress: undefined,
rawAmount: '10000000000000000',
formattedAmount: '0.01',
})
})

it('parses USDC transfer on Base (Test Vector 3)', () => {
const uri = `ethereum:${TEST_USDC}@8453/transfer?address=${TEST_RECIPIENT_2}&uint256=1e6`
const result = parseERC681URI(uri)
expect(result).toEqual({
chainId: UniverseChainId.Base,
recipient: TEST_RECIPIENT_2,
tokenAddress: TEST_USDC,
rawAmount: '1000000',
formattedAmount: undefined, // ERC-20 decimals require external resolution in UI
})
})

it('parses DAI transfer on Mainnet (Test Vector 4)', () => {
const uri = `ethereum:${TEST_DAI}/transfer?address=${TEST_RECIPIENT_2}&uint256=1e18`
const result = parseERC681URI(uri)
expect(result).toEqual({
chainId: UniverseChainId.Mainnet,
recipient: TEST_RECIPIENT_2,
tokenAddress: TEST_DAI,
rawAmount: '1000000000000000000',
formattedAmount: undefined,
})
})

it('parses plain ethereum:<address> URI without parameters', () => {
const uri = `ethereum:${TEST_RECIPIENT}`
const result = parseERC681URI(uri)
expect(result).toEqual({
chainId: UniverseChainId.Mainnet,
recipient: TEST_RECIPIENT,
tokenAddress: undefined,
rawAmount: undefined,
formattedAmount: undefined,
})
})

it('returns undefined for invalid URI schemes or addresses', () => {
expect(parseERC681URI('bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa')).toBeUndefined()
expect(parseERC681URI('ethereum:invalid_address')).toBeUndefined()
expect(parseERC681URI('ethereum:0x123/transfer?address=invalid_recipient')).toBeUndefined()
})

it('returns undefined for unsupported smart contract functions', () => {
const uri = `ethereum:${TEST_USDC}/approve?address=${TEST_RECIPIENT}&uint256=1000`
expect(parseERC681URI(uri)).toBeUndefined()
})
})
Loading