Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/xrp-vm-sdk-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@relayprotocol/relay-sdk': patch
---

Add XRP dead address and include the Tron, Zero and XRP dead addresses in isDeadAddress
5 changes: 5 additions & 0 deletions .changeset/xrp-vm-ui-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@relayprotocol/relay-kit-ui': patch
---

Add XRP vm support for destination transactions
36 changes: 35 additions & 1 deletion packages/sdk/src/actions/execute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { mainnet } from 'viem/chains'
import { MAINNET_RELAY_API } from '../constants'
import { executeBridge } from '../../tests/data/executeBridge'
import type { AdaptedWallet, Execute } from '../types'
import { evmDeadAddress } from '../constants/address'
import {
evmDeadAddress,
tronDeadAddress,
xrpDeadAddress
} from '../constants/address'

let client: RelayClient | undefined
let wallet: AdaptedWallet = {
Expand Down Expand Up @@ -174,4 +178,34 @@ describe('Should test the execute action.', () => {
})
).toThrow('Recipient should never be burn address')
})

it('Should throw an error when recipient is the XRP dead address', () => {
client = createClient({
baseApiUrl: MAINNET_RELAY_API
})
if (quote.details?.sender) {
quote.details.recipient = xrpDeadAddress
}
expect(() =>
client?.actions?.execute({
wallet,
quote
})
).toThrow('Recipient should never be burn address')
})

it('Should throw an error when recipient is the Tron dead address', () => {
client = createClient({
baseApiUrl: MAINNET_RELAY_API
})
if (quote.details?.sender) {
quote.details.recipient = tronDeadAddress
}
expect(() =>
client?.actions?.execute({
wallet,
quote
})
).toThrow('Recipient should never be burn address')
})
})
48 changes: 47 additions & 1 deletion packages/sdk/src/actions/getQuote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,25 @@ import {
arbitrumNova
} from 'viem/chains'
import { MAINNET_RELAY_API } from '../constants'
import { xrpDeadAddress } from '../constants/address'
import { axios } from '../utils'
import type { RelayChain } from '../types'

const viemChains = [mainnet, base, zora, optimism, arbitrum, arbitrumNova]
const relayChains = viemChains.map(convertViemChainToRelayChain)
const xrpChain: RelayChain = {
id: 537724,
name: 'xrp',
displayName: 'XRP Ledger',
vmType: 'xrpvm',
currency: {
id: 'xrp',
symbol: 'XRP',
name: 'XRP',
address: 'xrp',
decimals: 6
}
}
const relayChains = [...viemChains.map(convertViemChainToRelayChain), xrpChain]

let client: RelayClient | undefined
let wallet = {
Expand Down Expand Up @@ -142,4 +157,35 @@ describe('Should test the getQuote action.', () => {
})
)
})

it('Should use the XRP dead address as the default recipient for XRP destinations', async () => {
client = createClient({
baseApiUrl: MAINNET_RELAY_API,
chains: relayChains
})

await client?.actions?.getQuote(
{
toChainId: 537724,
chainId: 8453,
currency: '0x0000000000000000000000000000000000000000',
toCurrency: 'xrp',
tradeType: 'EXACT_INPUT',
amount: '1000000000000000' // 0.001 ETH
},
true
)

expect(axiosRequestSpy).toHaveBeenCalledWith(
expect.objectContaining({
url: expect.stringContaining('quote'),
data: expect.objectContaining({
// Origin is still EVM, so the user stays the EVM dead address
user: '0x000000000000000000000000000000000000dead',
destinationChainId: 537724,
recipient: xrpDeadAddress
})
})
)
})
})
8 changes: 7 additions & 1 deletion packages/sdk/src/constants/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const tronDeadAddress = 'THa7BwoPfacfiELa63pbmm3g5PGKYmtJyt'
export const zeroDeadAddress = '0x00000000000000000000000000000000000dead0'
export const tonDeadAddress =
'EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADerZ0z' as const
export const xrpDeadAddress = 'rrrrrrrrrrrrrrrrrrrrBZbvji' as const

const eclipseId = 9286185
const zeroChainId = 543210
Expand All @@ -26,6 +27,8 @@ export const getDeadAddress = (vmType?: ChainVM, chainId?: number) => {
return tronDeadAddress
} else if (vmType === 'tonvm') {
return tonDeadAddress
} else if (vmType === 'xrpvm') {
return xrpDeadAddress
} else {
return evmDeadAddress
}
Expand All @@ -41,7 +44,10 @@ export const isDeadAddress = (address?: string) => {
address === solDeadAddress ||
address === bitcoinDeadAddress ||
address === evmDeadAddress ||
address === tonDeadAddress
address === tonDeadAddress ||
address === tronDeadAddress ||
address === zeroDeadAddress ||
address === xrpDeadAddress
) {
return true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export type PaymentMethodProps = {
isValidAddress?: boolean
multiWalletSupportEnabled?: boolean
fromChainWalletVMSupported?: boolean
supportedWalletVMs?: Omit<ChainVM, 'hypevm' | 'lvm'>[]
supportedWalletVMs?: Omit<ChainVM, 'hypevm' | 'lvm' | 'xrpvm'>[]
popularChainIds?: number[]
linkedWallets?: any[]
setToken: (token: Token) => void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export type TokenSelectorProps = {
isValidAddress?: boolean
multiWalletSupportEnabled?: boolean
fromChainWalletVMSupported?: boolean
supportedWalletVMs?: Omit<ChainVM, 'hypevm' | 'lvm'>[]
supportedWalletVMs?: Omit<ChainVM, 'hypevm' | 'lvm' | 'xrpvm'>[]
popularChainIds?: number[]
setToken: (token: Token) => void
onAnalyticEvent?: (eventName: string, data?: any) => void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export type ChildrenProps = {

type OnrampWidgetRendererProps = {
defaultWalletAddress?: string
supportedWalletVMs: Omit<ChainVM, 'hypevm' | 'lvm'>[]
supportedWalletVMs: Omit<ChainVM, 'hypevm' | 'lvm' | 'xrpvm'>[]
linkedWallets?: LinkedWallet[]
multiWalletSupportEnabled?: boolean
moonPayApiKey: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {

type BaseOnrampWidgetProps = {
defaultWalletAddress?: string
supportedWalletVMs: Omit<ChainVM, 'hypevm' | 'lvm'>[]
supportedWalletVMs: Omit<ChainVM, 'hypevm' | 'lvm' | 'xrpvm'>[]
moonPayApiKey: string
moonPayThemeId?: string
moonPayThemeMode?: 'dark' | 'light'
Expand Down
6 changes: 2 additions & 4 deletions packages/ui/src/components/widgets/SwapWidget/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
findSupportedWallet,
isChainVmTypeSupported
} from '../../../utils/address.js'
import { isDeadAddress, tronDeadAddress } from '@relayprotocol/relay-sdk'
import { isDeadAddress } from '@relayprotocol/relay-sdk'
import {
ProviderOptionsContext,
useHapticEvent
Expand Down Expand Up @@ -70,7 +70,7 @@ type BaseSwapWidgetProps = {
lockChainId?: number
singleChainMode?: boolean
wallet?: AdaptedWallet
supportedWalletVMs: Omit<ChainVM, 'hypevm' | 'lvm'>[]
supportedWalletVMs: Omit<ChainVM, 'hypevm' | 'lvm' | 'xrpvm'>[]
disableInputAutoFocus?: boolean
popularChainIds?: number[]
disablePasteWalletAddressOption?: boolean
Expand Down Expand Up @@ -977,7 +977,6 @@ const SwapWidget: FC<SwapWidgetProps> = ({
displaySymbol={false}
isConnected={
!isDeadAddress(address) &&
address !== tronDeadAddress &&
address !== undefined
}
pending={fromBalancePending}
Expand Down Expand Up @@ -1389,7 +1388,6 @@ const SwapWidget: FC<SwapWidgetProps> = ({
displaySymbol={false}
isConnected={
!isDeadAddress(address) &&
address !== tronDeadAddress &&
address !== undefined
}
pending={toBalancePending}
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/components/widgets/SwapWidgetRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ type SwapWidgetRendererProps = {
wallet?: AdaptedWallet
linkedWallets?: LinkedWallet[]
multiWalletSupportEnabled?: boolean
supportedWalletVMs: Omit<ChainVM, 'hypevm' | 'lvm'>[]
supportedWalletVMs: Omit<ChainVM, 'hypevm' | 'lvm' | 'xrpvm'>[]
onConnectWallet?: () => void
onAnalyticEvent?: (eventName: string, data?: any) => void
onSwapError?: (error: string, data?: Execute) => void
Expand Down Expand Up @@ -133,7 +133,7 @@ export type ChildrenProps = {
isBvmSwap: boolean
isValidFromAddress: boolean
isValidToAddress: boolean
supportedWalletVMs: Omit<ChainVM, 'hypevm' | 'lvm'>[]
supportedWalletVMs: Omit<ChainVM, 'hypevm' | 'lvm' | 'xrpvm'>[]
fromChainWalletVMSupported: boolean
toChainWalletVMSupported: boolean
isRecipientLinked?: boolean
Expand Down
3 changes: 1 addition & 2 deletions packages/ui/src/components/widgets/WidgetErrorWell.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Execute } from '@relayprotocol/relay-sdk'
import { isDeadAddress, tronDeadAddress } from '@relayprotocol/relay-sdk'
import { isDeadAddress } from '@relayprotocol/relay-sdk'
import { type FC } from 'react'
import { Box, Flex, Text } from '../primitives/index.js'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
Expand Down Expand Up @@ -98,7 +98,6 @@ export const WidgetErrorWell: FC<Props> = ({
!recipientWalletSupportsChain &&
recipient &&
!isDeadAddress(recipient) &&
recipient !== tronDeadAddress &&
toChainWalletVMSupported &&
(!recipientLinkedWallet || recipientLinkedWallet.vmType === toChainVmType)
) {
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/src/constants/depositAddresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export const UnsupportedDepositAddressChainIds = [
1996, //sanko
1514, //story
660279, //xai
224235520 //ton
224235520, //ton
537724 //xrp
]
7 changes: 5 additions & 2 deletions packages/ui/src/utils/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { RelayKitProviderProps } from '../providers/RelayKitProvider.js'
import { isTronAddress } from './tron.js'
import { isTonAddress } from './ton.js'
import { isLighterAddress } from './lighter.js'
import { isXrpAddress } from './xrp.js'

export const isWalletVmTypeCompatible = (
walletVmType?: ChainVM,
Expand All @@ -35,7 +36,7 @@ export const isWalletVmTypeCompatible = (

export const isChainVmTypeSupported = (
chainVmType: ChainVM | undefined,
supportedWalletVMs: Omit<ChainVM, 'hypevm' | 'lvm'>[]
supportedWalletVMs: Omit<ChainVM, 'hypevm' | 'lvm' | 'xrpvm'>[]
) => {
if (!chainVmType) {
return true
Expand All @@ -46,7 +47,7 @@ export const isChainVmTypeSupported = (
}

return supportedWalletVMs.includes(
chainVmType as Omit<ChainVM, 'hypevm' | 'lvm'>
chainVmType as Omit<ChainVM, 'hypevm' | 'lvm' | 'xrpvm'>
)
}

Expand Down Expand Up @@ -92,6 +93,8 @@ export const isValidAddress = (
return isTonAddress(address)
} else if (vmType === 'lvm') {
return isLighterAddress(address)
} else if (vmType === 'xrpvm') {
return isXrpAddress(address)
}
}
return false
Expand Down
69 changes: 69 additions & 0 deletions packages/ui/src/utils/xrp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { sha256 } from '@noble/hashes/sha2.js'

export const xrp = {
id: 537724
}

// XRPL's own base58 ordering. Same 58 characters as Bitcoin's alphabet, so a
// Bitcoin ordering looks right but indexes to the wrong bytes.
const XRP_BASE58_ALPHABET =
'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz'

function decodeXrpBase58(address: string): Uint8Array | undefined {
const bytes: number[] = []

for (const char of address) {
let carry = XRP_BASE58_ALPHABET.indexOf(char)
if (carry === -1) {
return undefined
}

for (let i = 0; i < bytes.length; i++) {
carry += bytes[i] * 58
bytes[i] = carry & 0xff
carry >>= 8
}
while (carry > 0) {
bytes.push(carry & 0xff)
carry >>= 8
}
}

// Each leading alphabet-zero character represents one leading zero byte
for (const char of address) {
if (char !== XRP_BASE58_ALPHABET[0]) {
break
}
bytes.push(0)
}

return new Uint8Array(bytes.reverse())
}

/**
* Classic `r...` addresses only. X-addresses have a non-zero version byte and
* are rejected, since the destination tag they encode can't be forwarded.
*
* Does not trim: callers store the string they validated as the recipient, so
* accepting padded input would forward the padding to the quote.
*/
export function isXrpAddress(address: string): boolean {
// Bounds the decode; every 25 byte base58check payload lands in this range
if (address.length < 25 || address.length > 35) {
return false
}

const decoded = decodeXrpBase58(address)
// 1 version byte + 20 byte account id + 4 byte checksum. Version 0x00 can
// only come from a leading `r`, so this also pins the prefix.
if (!decoded || decoded.length !== 25 || decoded[0] !== 0x00) {
return false
}

const expectedChecksum = sha256(sha256(decoded.subarray(0, 21))).subarray(
0,
4
)

return decoded.subarray(21).every((byte, i) => byte === expectedChecksum[i])
}