@zerodev/smart-routing-address-react-ui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +117 -0
  2. package/dist/_types/components/AddressDisplay/index.d.ts +43 -0
  3. package/dist/_types/components/AddressDisplay/index.d.ts.map +1 -0
  4. package/dist/_types/components/ErrorRetryCard/index.d.ts +21 -0
  5. package/dist/_types/components/ErrorRetryCard/index.d.ts.map +1 -0
  6. package/dist/_types/components/FeeBreakdown/feeInfo.d.ts +24 -0
  7. package/dist/_types/components/FeeBreakdown/feeInfo.d.ts.map +1 -0
  8. package/dist/_types/components/FeeBreakdown/index.d.ts +28 -0
  9. package/dist/_types/components/FeeBreakdown/index.d.ts.map +1 -0
  10. package/dist/_types/components/LoadingCard/index.d.ts +6 -0
  11. package/dist/_types/components/LoadingCard/index.d.ts.map +1 -0
  12. package/dist/_types/components/PendingDeposits/index.d.ts +14 -0
  13. package/dist/_types/components/PendingDeposits/index.d.ts.map +1 -0
  14. package/dist/_types/components/QrSheet/QrCode.d.ts +28 -0
  15. package/dist/_types/components/QrSheet/QrCode.d.ts.map +1 -0
  16. package/dist/_types/components/QrSheet/index.d.ts +16 -0
  17. package/dist/_types/components/QrSheet/index.d.ts.map +1 -0
  18. package/dist/_types/components/TxnItem/ExplorerLink.d.ts +16 -0
  19. package/dist/_types/components/TxnItem/ExplorerLink.d.ts.map +1 -0
  20. package/dist/_types/components/TxnItem/index.d.ts +19 -0
  21. package/dist/_types/components/TxnItem/index.d.ts.map +1 -0
  22. package/dist/_types/constants.d.ts +20 -0
  23. package/dist/_types/constants.d.ts.map +1 -0
  24. package/dist/_types/context/SmartRoutingAddressContext.d.ts +32 -0
  25. package/dist/_types/context/SmartRoutingAddressContext.d.ts.map +1 -0
  26. package/dist/_types/context/SmartRoutingAddressProvider.d.ts +13 -0
  27. package/dist/_types/context/SmartRoutingAddressProvider.d.ts.map +1 -0
  28. package/dist/_types/hooks/useCreateSmartRoutingAddress.d.ts +23 -0
  29. package/dist/_types/hooks/useCreateSmartRoutingAddress.d.ts.map +1 -0
  30. package/dist/_types/hooks/useDepositStatus.d.ts +25 -0
  31. package/dist/_types/hooks/useDepositStatus.d.ts.map +1 -0
  32. package/dist/_types/hooks/useNewDeposits.d.ts +8 -0
  33. package/dist/_types/hooks/useNewDeposits.d.ts.map +1 -0
  34. package/dist/_types/hooks/useProviderFees.d.ts +22 -0
  35. package/dist/_types/hooks/useProviderFees.d.ts.map +1 -0
  36. package/dist/_types/hooks/useSmartRoutingAddress.d.ts +20 -0
  37. package/dist/_types/hooks/useSmartRoutingAddress.d.ts.map +1 -0
  38. package/dist/_types/iconAssets.d.ts +7 -0
  39. package/dist/_types/iconAssets.d.ts.map +1 -0
  40. package/dist/_types/index.d.ts +14 -0
  41. package/dist/_types/index.d.ts.map +1 -0
  42. package/dist/_types/pages/Deposit.d.ts +12 -0
  43. package/dist/_types/pages/Deposit.d.ts.map +1 -0
  44. package/dist/_types/pages/PastDeposits.d.ts +14 -0
  45. package/dist/_types/pages/PastDeposits.d.ts.map +1 -0
  46. package/dist/_types/pages/TransactionDetails.d.ts +11 -0
  47. package/dist/_types/pages/TransactionDetails.d.ts.map +1 -0
  48. package/dist/_types/pages/index.d.ts +25 -0
  49. package/dist/_types/pages/index.d.ts.map +1 -0
  50. package/dist/_types/types.d.ts +75 -0
  51. package/dist/_types/types.d.ts.map +1 -0
  52. package/dist/_types/utils/chains.d.ts +8 -0
  53. package/dist/_types/utils/chains.d.ts.map +1 -0
  54. package/dist/_types/utils/config.d.ts +35 -0
  55. package/dist/_types/utils/config.d.ts.map +1 -0
  56. package/dist/_types/utils/deposits.d.ts +29 -0
  57. package/dist/_types/utils/deposits.d.ts.map +1 -0
  58. package/dist/_types/utils/fees.d.ts +14 -0
  59. package/dist/_types/utils/fees.d.ts.map +1 -0
  60. package/dist/_types/utils/format.d.ts +27 -0
  61. package/dist/_types/utils/format.d.ts.map +1 -0
  62. package/dist/_types/utils/providerFees.d.ts +103 -0
  63. package/dist/_types/utils/providerFees.d.ts.map +1 -0
  64. package/dist/index.cjs +2 -0
  65. package/dist/index.cjs.map +1 -0
  66. package/dist/index.mjs +2180 -0
  67. package/dist/index.mjs.map +1 -0
  68. package/dist/styles.css +2 -0
  69. package/package.json +91 -0
  70. package/src/components/AddressDisplay/index.tsx +108 -0
  71. package/src/components/ErrorRetryCard/index.tsx +68 -0
  72. package/src/components/FeeBreakdown/feeInfo.ts +25 -0
  73. package/src/components/FeeBreakdown/index.tsx +218 -0
  74. package/src/components/LoadingCard/index.tsx +27 -0
  75. package/src/components/PendingDeposits/index.tsx +152 -0
  76. package/src/components/QrSheet/QrCode.tsx +163 -0
  77. package/src/components/QrSheet/index.tsx +80 -0
  78. package/src/components/TxnItem/ExplorerLink.tsx +37 -0
  79. package/src/components/TxnItem/index.tsx +178 -0
  80. package/src/constants.ts +102 -0
  81. package/src/context/SmartRoutingAddressContext.ts +48 -0
  82. package/src/context/SmartRoutingAddressProvider.tsx +145 -0
  83. package/src/hooks/useCreateSmartRoutingAddress.ts +28 -0
  84. package/src/hooks/useDepositStatus.ts +110 -0
  85. package/src/hooks/useNewDeposits.ts +25 -0
  86. package/src/hooks/useProviderFees.ts +235 -0
  87. package/src/hooks/useSmartRoutingAddress.ts +25 -0
  88. package/src/iconAssets.ts +130 -0
  89. package/src/index.ts +27 -0
  90. package/src/pages/Deposit.tsx +605 -0
  91. package/src/pages/PastDeposits.tsx +232 -0
  92. package/src/pages/TransactionDetails.tsx +466 -0
  93. package/src/pages/index.tsx +165 -0
  94. package/src/types.ts +84 -0
  95. package/src/utils/chains.ts +26 -0
  96. package/src/utils/config.ts +142 -0
  97. package/src/utils/deposits.ts +106 -0
  98. package/src/utils/fees.ts +51 -0
  99. package/src/utils/format.ts +108 -0
  100. package/src/utils/providerFees.ts +301 -0
@@ -0,0 +1,165 @@
1
+ import { PoweredBy, Screen, TopNav } from '@zerodev/react-ui'
2
+ import type { DepositedToken } from '@zerodev/smart-routing-address'
3
+ import { type ReactNode, useEffect, useState } from 'react'
4
+ import type { Address } from 'viem'
5
+ import { QrSheet } from '../components/QrSheet'
6
+ import { useSmartRoutingAddressContext } from '../context/SmartRoutingAddressContext'
7
+ import { Deposit } from './Deposit'
8
+ import { PastDeposits } from './PastDeposits'
9
+ import { TransactionDetails } from './TransactionDetails'
10
+
11
+ /**
12
+ * Steps rendered by `SmartRoutingAddress`. Sub-views advance/rewind local
13
+ * step state; the widget owns navigation and the `TopNav` chrome accordingly.
14
+ */
15
+ export type SmartRoutingAddressStep = 'deposit' | 'past' | 'transaction'
16
+
17
+ const TITLE_BY_STEP: Record<SmartRoutingAddressStep, string> = {
18
+ deposit: 'Deposit funds',
19
+ past: 'Past deposits',
20
+ transaction: 'Transaction details',
21
+ }
22
+
23
+ function renderStep(
24
+ step: SmartRoutingAddressStep,
25
+ {
26
+ onQrClick,
27
+ onViewPastDeposits,
28
+ onSelectDeposit,
29
+ selectedDeposit,
30
+ }: {
31
+ onQrClick?: (() => void) | undefined
32
+ onViewPastDeposits?: (() => void) | undefined
33
+ onSelectDeposit?: ((deposit: DepositedToken) => void) | undefined
34
+ selectedDeposit?: DepositedToken | undefined
35
+ },
36
+ ): ReactNode {
37
+ switch (step) {
38
+ case 'deposit':
39
+ return (
40
+ <Deposit
41
+ {...(onQrClick && { onQrClick })}
42
+ {...(onViewPastDeposits && { onViewPastDeposits })}
43
+ {...(onSelectDeposit && { onSelectDeposit })}
44
+ />
45
+ )
46
+ case 'past':
47
+ return <PastDeposits {...(onSelectDeposit && { onSelectDeposit })} />
48
+ case 'transaction':
49
+ // `selectedDeposit` is set by the past-deposits row-click handler
50
+ // before advancing the step, so this branch always has a deposit.
51
+ return selectedDeposit ? (
52
+ <TransactionDetails deposit={selectedDeposit} />
53
+ ) : null
54
+ default:
55
+ return null
56
+ }
57
+ }
58
+
59
+ /**
60
+ * The Figma "Deposit funds" flow (`17634:104268` / `18721:96092`).
61
+ *
62
+ * Owns the shared `Screen` + `TopNav` chrome and delegates the body to the
63
+ * current page in this directory. Mirrors the layout of `wallet-react-ui`'s
64
+ * `auth/index.tsx` and `signing/index.tsx`.
65
+ *
66
+ * Wrap the subtree with `<SmartRoutingAddressProvider config={…}>` first.
67
+ */
68
+ export interface SmartRoutingAddressProps {
69
+ /** Recipient address the smart routing address is created for. */
70
+ recipient: Address
71
+ /** Called when the top-right × close button is clicked. */
72
+ onClose: () => void
73
+ className?: string
74
+ size?: 'sm' | 'md' | 'lg'
75
+ }
76
+
77
+ export function SmartRoutingAddress({
78
+ recipient,
79
+ onClose,
80
+ className,
81
+ size,
82
+ }: SmartRoutingAddressProps) {
83
+ const { addressState, getOrCreateAddress } = useSmartRoutingAddressContext()
84
+ const [qrOpen, setQrOpen] = useState(false)
85
+ const [step, setStep] = useState<SmartRoutingAddressStep>('deposit')
86
+ const [selectedDeposit, setSelectedDeposit] = useState<
87
+ DepositedToken | undefined
88
+ >(undefined)
89
+
90
+ useEffect(() => {
91
+ // Errors surface via `addressState.status === 'error'`; swallow rejection
92
+ // here so React doesn't warn about an unhandled promise.
93
+ getOrCreateAddress(recipient).catch(() => {})
94
+ }, [recipient, getOrCreateAddress])
95
+
96
+ const title = TITLE_BY_STEP[step]
97
+
98
+ const address =
99
+ addressState.status === 'success' ? addressState.address : undefined
100
+
101
+ // The QR button in AddressDisplay only exists once the address has resolved,
102
+ // but guard here too so we don't render an empty QrSheet.
103
+ const handleQrClick = address ? () => setQrOpen(true) : undefined
104
+
105
+ const handleCopy = async () => {
106
+ if (!address) return
107
+ try {
108
+ await navigator.clipboard.writeText(address)
109
+ } catch {
110
+ // Clipboard can reject on insecure contexts / permissions; the visual
111
+ // "copied" state isn't in the spec yet, so just close and swallow.
112
+ }
113
+ setQrOpen(false)
114
+ }
115
+
116
+ // Sub-view chrome — sub-views swap the left slot for a back chevron that
117
+ // returns to the parent step. Root (deposit) step has no left button
118
+ // (TopNav renders a spacer so the title stays centred).
119
+ const goBack =
120
+ step === 'transaction'
121
+ ? () => setStep('past')
122
+ : step === 'past'
123
+ ? () => setStep('deposit')
124
+ : undefined
125
+
126
+ return (
127
+ <Screen
128
+ className={className}
129
+ {...(size && { size })}
130
+ topNav={
131
+ <TopNav
132
+ title={title}
133
+ {...(goBack && {
134
+ onLeftButtonClick: goBack,
135
+ leftButtonIcon: 'chevronLeft' as const,
136
+ })}
137
+ onRightButtonClick={onClose}
138
+ />
139
+ }
140
+ footer={<PoweredBy />}
141
+ >
142
+ {renderStep(step, {
143
+ onQrClick: handleQrClick,
144
+ onViewPastDeposits: () => setStep('past'),
145
+ onSelectDeposit: (deposit) => {
146
+ setSelectedDeposit(deposit)
147
+ setStep('transaction')
148
+ },
149
+ selectedDeposit,
150
+ })}
151
+ {/* QrSheet renders itself via `useScreenOverlayContainer()` + portal, so
152
+ it stays inside the card frame while composing at this level rather
153
+ than being hoisted into `Screen`'s API. Mounted whenever an address
154
+ exists so Radix Dialog owns the open/close animation lifecycle. */}
155
+ {address && (
156
+ <QrSheet
157
+ open={qrOpen}
158
+ onOpenChange={setQrOpen}
159
+ address={address}
160
+ onCopy={handleCopy}
161
+ />
162
+ )}
163
+ </Screen>
164
+ )
165
+ }
package/src/types.ts ADDED
@@ -0,0 +1,84 @@
1
+ import type {
2
+ CreateSmartRoutingAddressParams,
3
+ DepositedToken,
4
+ GetSmartRoutingAddressFeeEstimatesReturns,
5
+ SmartRoutingAddressVersion,
6
+ TOKEN_TYPE,
7
+ } from '@zerodev/smart-routing-address'
8
+ import type { Address, Chain } from 'viem'
9
+
10
+ /** `DepositedToken` with the optional `createdAt` some SRA servers ship
11
+ * alongside the response. The SDK's public type doesn't include it yet, so
12
+ * we model it as an optional augmentation here — one canonical shape shared
13
+ * by the pending/past/detail views. */
14
+ export type DepositWithTimestamp = DepositedToken & { createdAt?: string }
15
+
16
+ export type EstimatedFee =
17
+ GetSmartRoutingAddressFeeEstimatesReturns['estimatedFees'][number]
18
+
19
+ export type EstimatedFeeData = EstimatedFee['data'][number]
20
+
21
+ /** Internal chain-object form of a source token */
22
+ export type SourceToken = {
23
+ tokenType: TOKEN_TYPE
24
+ chain: Chain
25
+ minAmount?: bigint
26
+ }
27
+
28
+ export type SmartRoutingAddressConfig = {
29
+ /**
30
+ * ZeroDev project id; when non-empty it is appended to the server URL
31
+ * for every request
32
+ */
33
+ projectId?: string
34
+ /** Chain id where funds settle */
35
+ targetChainId: number
36
+ /** Smart routing address version, defaults to the latest stable */
37
+ version?: SmartRoutingAddressVersion
38
+ /**
39
+ * Destination actions per token type. When omitted, funds are simply
40
+ * transferred to the owner.
41
+ */
42
+ actions?: CreateSmartRoutingAddressParams['actions']
43
+ /** Max slippage in basis points (50 = 0.5%) */
44
+ slippage?: number
45
+ /**
46
+ * Override the smart routing address server root URL; the projectId is
47
+ * appended to it
48
+ */
49
+ baseUrl?: string
50
+ }
51
+
52
+ export type AddressState =
53
+ | { status: 'idle' }
54
+ | { status: 'loading' }
55
+ | {
56
+ status: 'success'
57
+ address: Address
58
+ estimatedFees: EstimatedFee[]
59
+ }
60
+ | { status: 'error'; error: Error }
61
+
62
+ export type DepositStage = 'pending' | 'bridging' | 'completed' | 'failed'
63
+
64
+ /**
65
+ * The route the deposit UI is currently showing (selected source token +
66
+ * chain + estimated fee), surfaced through context so hosts can mirror it —
67
+ * e.g. a demo "send" panel that matches the chosen token, or analytics.
68
+ */
69
+ export type ActiveRoute = {
70
+ /** Source chain id the deposit would come from */
71
+ sourceChainId: number
72
+ /** Source chain display name (e.g. "Base") */
73
+ sourceChainName: string
74
+ /** On-chain address of the selected source token */
75
+ token: Address
76
+ /** Display symbol of the selected source token (e.g. "USDC", "ETH") */
77
+ symbol: string
78
+ /** Decimals of the selected source token */
79
+ decimals: number
80
+ /** Estimated all-in fee amount in source-token atomic units, as a
81
+ * base-10 string (e.g. `"250000"` for 0.25 USDC). Consumers can pass it
82
+ * straight to `BigInt()` or `parseInt(x, 10)`. */
83
+ feeAmount: string
84
+ }
@@ -0,0 +1,26 @@
1
+ import type { Chain } from 'viem'
2
+ import { CHAINS_BY_ID, SUPPORTED_CHAINS } from '../constants'
3
+
4
+ /** Resolve a chain id to its viem chain object, throwing for unknown ids */
5
+ export function getChainById(chainId: number): Chain {
6
+ const chain = CHAINS_BY_ID.get(chainId)
7
+ if (!chain) {
8
+ const supported = SUPPORTED_CHAINS.map((c) => `${c.name} (${c.id})`)
9
+ throw new Error(
10
+ `Unsupported chain id ${chainId}. Supported chains: ${supported.join(', ')}`,
11
+ )
12
+ }
13
+ return chain
14
+ }
15
+
16
+ /** Block explorer URL for a transaction, or `undefined` when no explorer is
17
+ * known. Returns `undefined` (not `null`) so the value can be passed straight
18
+ * to optional link `href` props under `exactOptionalPropertyTypes`. */
19
+ export function getTxUrl(
20
+ chainId: number,
21
+ transactionHash: string,
22
+ ): string | undefined {
23
+ const explorer = CHAINS_BY_ID.get(chainId)?.blockExplorers?.default
24
+ if (!explorer) return undefined
25
+ return `${explorer.url}/tx/${transactionHash}`
26
+ }
@@ -0,0 +1,142 @@
1
+ import type {
2
+ CreateSmartRoutingAddressParams,
3
+ SmartRoutingAddressVersion,
4
+ TOKEN_TYPE,
5
+ } from '@zerodev/smart-routing-address'
6
+ import {
7
+ createCall,
8
+ FLEX,
9
+ SMART_ROUTING_ADDRESS_SERVER_URL,
10
+ SMART_ROUTING_ADDRESS_V0_2_1,
11
+ SUPPORTED_TOKENS,
12
+ } from '@zerodev/smart-routing-address'
13
+ import { type Address, type Chain, erc20Abi } from 'viem'
14
+ import { DEFAULT_DASHBOARD_URL, DEFAULT_SOURCE_TOKENS } from '../constants'
15
+ import type {
16
+ EstimatedFee,
17
+ SmartRoutingAddressConfig,
18
+ SourceToken,
19
+ } from '../types'
20
+ import { getChainById } from './chains'
21
+ import { tokenAddressMatches } from './fees'
22
+
23
+ export function resolveVersion(
24
+ config: SmartRoutingAddressConfig,
25
+ ): SmartRoutingAddressVersion {
26
+ return config.version ?? SMART_ROUTING_ADDRESS_V0_2_1
27
+ }
28
+
29
+ /**
30
+ * Server URL for all smart routing address requests: the server root
31
+ * (default or `config.baseUrl`) with the ZeroDev project id appended.
32
+ * Without a project id the URL is only overridden when `config.baseUrl`
33
+ * is set; undefined leaves the SDK default in place.
34
+ */
35
+ export function resolveBaseUrl(
36
+ config: SmartRoutingAddressConfig,
37
+ ): string | undefined {
38
+ if (!config.projectId) return config.baseUrl
39
+ const root = config.baseUrl ?? SMART_ROUTING_ADDRESS_SERVER_URL
40
+ return `${root.replace(/\/+$/, '')}/${config.projectId}`
41
+ }
42
+
43
+ export function resolveDashboardUrl(address?: string): string {
44
+ if (!address) return DEFAULT_DASHBOARD_URL
45
+ return `${DEFAULT_DASHBOARD_URL.replace(/\/+$/, '')}/address/${address}`
46
+ }
47
+
48
+ /** Chain where the routed funds settle */
49
+ export function resolveDestChain(config: SmartRoutingAddressConfig): Chain {
50
+ return getChainById(config.targetChainId)
51
+ }
52
+
53
+ /**
54
+ * Whether the SDK can resolve the token type to an asset on the chain:
55
+ * generic ERC20 deposits are always accepted, every other type (including
56
+ * NATIVE) needs a SUPPORTED_TOKENS entry for the chain.
57
+ */
58
+ function isTokenOnChain(tokenType: TOKEN_TYPE, chainId: number): boolean {
59
+ if (tokenType === 'ERC20') return true
60
+ return SUPPORTED_TOKENS[chainId]?.[tokenType] !== undefined
61
+ }
62
+
63
+ /**
64
+ * The DEFAULT_SOURCE_TOKENS, excluding token types that do not exist on the
65
+ * destination chain (the default actions deliver the deposited token type
66
+ * there, so a missing destination token would make the deposit unfulfillable).
67
+ */
68
+ export function resolveSourceTokens(
69
+ config: SmartRoutingAddressConfig,
70
+ ): SourceToken[] {
71
+ const destChainId = resolveDestChain(config).id
72
+ return DEFAULT_SOURCE_TOKENS.filter((source) =>
73
+ isTokenOnChain(source.tokenType, destChainId),
74
+ )
75
+ }
76
+
77
+ /**
78
+ * Source tokens the server actually returned routes for, taken straight from
79
+ * the fee estimates. Each returned (chain, token address) pair is matched back
80
+ * to a DEFAULT_SOURCE_TOKENS entry to recover its TOKEN_TYPE.
81
+ */
82
+ export function sourceTokensFromFees(
83
+ estimatedFees: EstimatedFee[],
84
+ ): SourceToken[] {
85
+ return estimatedFees.flatMap((fee) =>
86
+ fee.data.flatMap((data) => {
87
+ const token = DEFAULT_SOURCE_TOKENS.find(
88
+ (source) =>
89
+ source.chain.id === fee.chainId &&
90
+ tokenAddressMatches(source.tokenType, fee.chainId, data.token),
91
+ )
92
+ return token ? [token] : []
93
+ }),
94
+ )
95
+ }
96
+
97
+ function uniqueTokenTypes(sources: SourceToken[]): TOKEN_TYPE[] {
98
+ return [...new Set(sources.map((source) => source.tokenType))]
99
+ }
100
+
101
+ /**
102
+ * Destination actions for every resolved source token type: ERC-20 deposits
103
+ * are transferred to the recipient and native deposits are forwarded as
104
+ * value. FLEX placeholders are resolved by the server per deposit.
105
+ */
106
+ export function resolveActions(
107
+ config: SmartRoutingAddressConfig,
108
+ recipient: Address,
109
+ ): CreateSmartRoutingAddressParams['actions'] {
110
+ const erc20Call = createCall({
111
+ target: FLEX.TOKEN_ADDRESS,
112
+ value: 0n,
113
+ abi: erc20Abi,
114
+ functionName: 'transfer',
115
+ args: [recipient, FLEX.AMOUNT],
116
+ })
117
+ const nativeCall = createCall({
118
+ target: recipient,
119
+ value: FLEX.NATIVE_AMOUNT,
120
+ })
121
+
122
+ return Object.fromEntries(
123
+ uniqueTokenTypes(resolveSourceTokens(config)).map((tokenType) => [
124
+ tokenType,
125
+ {
126
+ action: tokenType === 'NATIVE' ? [nativeCall] : [erc20Call],
127
+ fallBack: tokenType === 'NATIVE' ? [nativeCall] : [erc20Call],
128
+ },
129
+ ]),
130
+ )
131
+ }
132
+
133
+ /** Display symbol for a source token (native tokens use the chain symbol) */
134
+ export function getSourceTokenSymbol(source: SourceToken): string {
135
+ if (source.tokenType === 'NATIVE') {
136
+ return source.chain.nativeCurrency.symbol
137
+ }
138
+ if (source.tokenType === 'WRAPPED_NATIVE') {
139
+ return `W${source.chain.nativeCurrency.symbol}`
140
+ }
141
+ return source.tokenType
142
+ }
@@ -0,0 +1,106 @@
1
+ import type { DepositedToken } from '@zerodev/smart-routing-address'
2
+ import type {
3
+ DepositStage,
4
+ EstimatedFee,
5
+ SmartRoutingAddressConfig,
6
+ } from '../types'
7
+ import { resolveDestChain, resolveSourceTokens } from './config'
8
+ import { findFeeDataByToken } from './fees'
9
+ import { formatTokenAmount, truncateAddress } from './format'
10
+
11
+ export const DEPOSIT_STAGE_LABELS: Record<DepositStage, string> = {
12
+ pending: 'Deposit detected',
13
+ bridging: 'Bridging',
14
+ completed: 'Completed',
15
+ failed: 'Failed',
16
+ }
17
+
18
+ /**
19
+ * Newer SRA servers split a deposit into legs that each carry their own
20
+ * bridge/execution status; the published SDK types don't include the
21
+ * field yet, so it is modeled here as an optional extension.
22
+ */
23
+ export type DepositSplitLeg = Pick<DepositedToken, 'bridge' | 'execution'> & {
24
+ splitIndex: number
25
+ amount: string
26
+ }
27
+
28
+ export type DepositWithSplits = DepositedToken & {
29
+ splits?: DepositSplitLeg[]
30
+ }
31
+
32
+ export function getDepositKey(deposit: DepositedToken): string {
33
+ return deposit.deposit.transactionHash
34
+ }
35
+
36
+ export function getDepositStage(deposit: DepositWithSplits): DepositStage {
37
+ if (deposit.error) return 'failed'
38
+ // Split deposits carry bridge/execution on each leg; the top-level
39
+ // fields stay null.
40
+ if (deposit.splits && deposit.splits.length > 0) {
41
+ if (deposit.splits.every((leg) => leg.execution !== null)) {
42
+ return 'completed'
43
+ }
44
+ if (
45
+ deposit.splits.some(
46
+ (leg) => leg.bridge !== null || leg.execution !== null,
47
+ )
48
+ ) {
49
+ return 'bridging'
50
+ }
51
+ return 'pending'
52
+ }
53
+ if (deposit.execution) return 'completed'
54
+ if (deposit.bridge) return 'bridging'
55
+ return 'pending'
56
+ }
57
+
58
+ export function getNewDeposits(
59
+ deposits: DepositedToken[],
60
+ baseline: ReadonlySet<string>,
61
+ ): DepositedToken[] {
62
+ return deposits.filter((deposit) => !baseline.has(getDepositKey(deposit)))
63
+ }
64
+
65
+ export type DepositSummary = {
66
+ key: string
67
+ stage: DepositStage
68
+ stageLabel: string
69
+ chainName: string
70
+ symbol: string
71
+ amount: string
72
+ transactionHash: string
73
+ }
74
+
75
+ function getChainName(
76
+ chainId: number,
77
+ config: SmartRoutingAddressConfig,
78
+ ): string {
79
+ const destChain = resolveDestChain(config)
80
+ if (destChain.id === chainId) return destChain.name
81
+ const source = resolveSourceTokens(config).find(
82
+ (token) => token.chain.id === chainId,
83
+ )
84
+ return source?.chain.name ?? `Chain ${chainId}`
85
+ }
86
+
87
+ export function summarizeDeposit(
88
+ deposit: DepositedToken,
89
+ estimatedFees: EstimatedFee[],
90
+ config: SmartRoutingAddressConfig,
91
+ ): DepositSummary {
92
+ const { chainId, token, amount, transactionHash } = deposit.deposit
93
+ const feeData = findFeeDataByToken(estimatedFees, chainId, token)
94
+ const stage = getDepositStage(deposit)
95
+ return {
96
+ key: getDepositKey(deposit),
97
+ stage,
98
+ stageLabel: DEPOSIT_STAGE_LABELS[stage],
99
+ chainName: getChainName(chainId, config),
100
+ symbol: feeData?.name ?? truncateAddress(token),
101
+ amount: feeData
102
+ ? formatTokenAmount(amount, feeData.decimal)
103
+ : String(amount),
104
+ transactionHash,
105
+ }
106
+ }
@@ -0,0 +1,51 @@
1
+ import type { TOKEN_TYPE } from '@zerodev/smart-routing-address'
2
+ import { SUPPORTED_TOKENS } from '@zerodev/smart-routing-address'
3
+ import type { Address } from 'viem'
4
+ import { isAddressEqual, zeroAddress } from 'viem'
5
+ import type { EstimatedFee, EstimatedFeeData } from '../types'
6
+
7
+ /**
8
+ * Resolve the on-chain token address used by the routing server for a given
9
+ * token type on a chain. Returns null when the type cannot be resolved
10
+ * (generic ERC20 or unknown token on that chain).
11
+ */
12
+ export function resolveTokenAddress(
13
+ tokenType: TOKEN_TYPE,
14
+ chainId: number,
15
+ ): Address | null {
16
+ if (tokenType === 'NATIVE') return zeroAddress
17
+ if (tokenType === 'ERC20') return null
18
+ return SUPPORTED_TOKENS[chainId]?.[tokenType] ?? null
19
+ }
20
+
21
+ /** Whether the token type resolves to the given on-chain address on a chain. */
22
+ export function tokenAddressMatches(
23
+ tokenType: TOKEN_TYPE,
24
+ chainId: number,
25
+ token: Address,
26
+ ): boolean {
27
+ const address = resolveTokenAddress(tokenType, chainId)
28
+ return address !== null && isAddressEqual(address, token)
29
+ }
30
+
31
+ export function findFeeDataByToken(
32
+ estimatedFees: EstimatedFee[],
33
+ chainId: number,
34
+ token: Address,
35
+ ): EstimatedFeeData | null {
36
+ const chainFees = estimatedFees.find((fee) => fee.chainId === chainId)
37
+ if (!chainFees) return null
38
+ return (
39
+ chainFees.data.find((data) => isAddressEqual(data.token, token)) ?? null
40
+ )
41
+ }
42
+
43
+ export function findFeeData(
44
+ estimatedFees: EstimatedFee[],
45
+ chainId: number,
46
+ tokenType: TOKEN_TYPE,
47
+ ): EstimatedFeeData | null {
48
+ const token = resolveTokenAddress(tokenType, chainId)
49
+ if (!token) return null
50
+ return findFeeDataByToken(estimatedFees, chainId, token)
51
+ }
@@ -0,0 +1,108 @@
1
+ import { formatUnits } from 'viem'
2
+
3
+ /**
4
+ * Format a raw token amount (decimal string, hex string or bigint) into a
5
+ * human readable unit string. Falls back to the raw value if parsing fails.
6
+ */
7
+ export function formatTokenAmount(
8
+ value: string | bigint,
9
+ decimals: number,
10
+ ): string {
11
+ try {
12
+ const amount = typeof value === 'bigint' ? value : BigInt(value)
13
+ return formatUnits(amount, decimals)
14
+ } catch {
15
+ return String(value)
16
+ }
17
+ }
18
+
19
+ /** Rounding direction for display amounts. `'up'` / `'down'` are ceil / floor
20
+ * — use them for values where over- or under-stating is unsafe (fees, min
21
+ * deposit). `'nearest'` is `Math.round` — use for values the user reads as a
22
+ * final quantity ("you received X") so 249.9996… reads as 250.00. */
23
+ export type RoundDirection = 'up' | 'down' | 'nearest'
24
+
25
+ function displayFractionDigits(value: number): number {
26
+ if (value >= 1000) return 0
27
+ if (value >= 1) return 2
28
+ // Two significant digits for sub-unit amounts
29
+ return Math.min(8, 1 - Math.floor(Math.log10(value)))
30
+ }
31
+
32
+ /**
33
+ * Format a raw token amount for display: thousands grouping and a
34
+ * magnitude-aware precision (whole numbers above 1000, two decimals above 1,
35
+ * two significant digits below). Rounds in the given direction so limits and
36
+ * fees are never overstated/understated. Falls back to the raw value if
37
+ * parsing fails.
38
+ */
39
+ export function formatDisplayAmount(
40
+ value: string | bigint,
41
+ decimals: number,
42
+ round: RoundDirection,
43
+ ): string {
44
+ let amount: number
45
+ try {
46
+ amount = Number(
47
+ formatUnits(typeof value === 'bigint' ? value : BigInt(value), decimals),
48
+ )
49
+ } catch {
50
+ return String(value)
51
+ }
52
+ if (amount === 0) return '0'
53
+
54
+ const digits = displayFractionDigits(amount)
55
+ const factor = 10 ** digits
56
+ const roundFn =
57
+ round === 'up' ? Math.ceil : round === 'down' ? Math.floor : Math.round
58
+ const rounded = roundFn(amount * factor) / factor
59
+ return rounded.toLocaleString('en-US', {
60
+ maximumFractionDigits: digits,
61
+ })
62
+ }
63
+
64
+ export function truncateAddress(address: string): string {
65
+ if (address.length <= 12) return address
66
+ return `${address.slice(0, 6)}…${address.slice(-4)}`
67
+ }
68
+
69
+ export function formatSlippage(basisPoints: number): string {
70
+ return `${(basisPoints / 100).toFixed(2).replace(/\.?0+$/, '')}%`
71
+ }
72
+
73
+ export function formatDuration(seconds: number): string {
74
+ if (seconds < 60) return `~${Math.max(1, Math.round(seconds))} sec`
75
+ return `~${Math.round(seconds / 60)} min`
76
+ }
77
+
78
+ /** Symbols treated as ~$1, so a USD equivalent can be shown for their amounts */
79
+ export const STABLE_SYMBOLS = new Set([
80
+ 'USDC',
81
+ 'USDT',
82
+ 'DAI',
83
+ 'USDBC',
84
+ 'USDC.E',
85
+ ])
86
+
87
+ /** ISO timestamp → compact "N unit ago" ("just now", "2 m ago", "3 h ago",
88
+ * "5 d ago", "2 mo ago", "1 y ago"). Returns `null` on invalid input. */
89
+ export function formatRelativeTime(
90
+ iso: string,
91
+ now: number = Date.now(),
92
+ ): string | null {
93
+ const then = Date.parse(iso)
94
+ if (Number.isNaN(then)) return null
95
+ const seconds = Math.max(0, Math.round((now - then) / 1000))
96
+ if (seconds < 30) return 'just now'
97
+ if (seconds < 60) return `${seconds} s ago`
98
+ const minutes = Math.round(seconds / 60)
99
+ if (minutes < 60) return `${minutes} m ago`
100
+ const hours = Math.round(minutes / 60)
101
+ if (hours < 24) return `${hours} h ago`
102
+ const days = Math.round(hours / 24)
103
+ if (days < 30) return `${days} d ago`
104
+ const months = Math.round(days / 30)
105
+ if (months < 12) return `${months} mo ago`
106
+ const years = Math.round(months / 12)
107
+ return `${years} y ago`
108
+ }