@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.
- package/README.md +117 -0
- package/dist/_types/components/AddressDisplay/index.d.ts +43 -0
- package/dist/_types/components/AddressDisplay/index.d.ts.map +1 -0
- package/dist/_types/components/ErrorRetryCard/index.d.ts +21 -0
- package/dist/_types/components/ErrorRetryCard/index.d.ts.map +1 -0
- package/dist/_types/components/FeeBreakdown/feeInfo.d.ts +24 -0
- package/dist/_types/components/FeeBreakdown/feeInfo.d.ts.map +1 -0
- package/dist/_types/components/FeeBreakdown/index.d.ts +28 -0
- package/dist/_types/components/FeeBreakdown/index.d.ts.map +1 -0
- package/dist/_types/components/LoadingCard/index.d.ts +6 -0
- package/dist/_types/components/LoadingCard/index.d.ts.map +1 -0
- package/dist/_types/components/PendingDeposits/index.d.ts +14 -0
- package/dist/_types/components/PendingDeposits/index.d.ts.map +1 -0
- package/dist/_types/components/QrSheet/QrCode.d.ts +28 -0
- package/dist/_types/components/QrSheet/QrCode.d.ts.map +1 -0
- package/dist/_types/components/QrSheet/index.d.ts +16 -0
- package/dist/_types/components/QrSheet/index.d.ts.map +1 -0
- package/dist/_types/components/TxnItem/ExplorerLink.d.ts +16 -0
- package/dist/_types/components/TxnItem/ExplorerLink.d.ts.map +1 -0
- package/dist/_types/components/TxnItem/index.d.ts +19 -0
- package/dist/_types/components/TxnItem/index.d.ts.map +1 -0
- package/dist/_types/constants.d.ts +20 -0
- package/dist/_types/constants.d.ts.map +1 -0
- package/dist/_types/context/SmartRoutingAddressContext.d.ts +32 -0
- package/dist/_types/context/SmartRoutingAddressContext.d.ts.map +1 -0
- package/dist/_types/context/SmartRoutingAddressProvider.d.ts +13 -0
- package/dist/_types/context/SmartRoutingAddressProvider.d.ts.map +1 -0
- package/dist/_types/hooks/useCreateSmartRoutingAddress.d.ts +23 -0
- package/dist/_types/hooks/useCreateSmartRoutingAddress.d.ts.map +1 -0
- package/dist/_types/hooks/useDepositStatus.d.ts +25 -0
- package/dist/_types/hooks/useDepositStatus.d.ts.map +1 -0
- package/dist/_types/hooks/useNewDeposits.d.ts +8 -0
- package/dist/_types/hooks/useNewDeposits.d.ts.map +1 -0
- package/dist/_types/hooks/useProviderFees.d.ts +22 -0
- package/dist/_types/hooks/useProviderFees.d.ts.map +1 -0
- package/dist/_types/hooks/useSmartRoutingAddress.d.ts +20 -0
- package/dist/_types/hooks/useSmartRoutingAddress.d.ts.map +1 -0
- package/dist/_types/iconAssets.d.ts +7 -0
- package/dist/_types/iconAssets.d.ts.map +1 -0
- package/dist/_types/index.d.ts +14 -0
- package/dist/_types/index.d.ts.map +1 -0
- package/dist/_types/pages/Deposit.d.ts +12 -0
- package/dist/_types/pages/Deposit.d.ts.map +1 -0
- package/dist/_types/pages/PastDeposits.d.ts +14 -0
- package/dist/_types/pages/PastDeposits.d.ts.map +1 -0
- package/dist/_types/pages/TransactionDetails.d.ts +11 -0
- package/dist/_types/pages/TransactionDetails.d.ts.map +1 -0
- package/dist/_types/pages/index.d.ts +25 -0
- package/dist/_types/pages/index.d.ts.map +1 -0
- package/dist/_types/types.d.ts +75 -0
- package/dist/_types/types.d.ts.map +1 -0
- package/dist/_types/utils/chains.d.ts +8 -0
- package/dist/_types/utils/chains.d.ts.map +1 -0
- package/dist/_types/utils/config.d.ts +35 -0
- package/dist/_types/utils/config.d.ts.map +1 -0
- package/dist/_types/utils/deposits.d.ts +29 -0
- package/dist/_types/utils/deposits.d.ts.map +1 -0
- package/dist/_types/utils/fees.d.ts +14 -0
- package/dist/_types/utils/fees.d.ts.map +1 -0
- package/dist/_types/utils/format.d.ts +27 -0
- package/dist/_types/utils/format.d.ts.map +1 -0
- package/dist/_types/utils/providerFees.d.ts +103 -0
- package/dist/_types/utils/providerFees.d.ts.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +2180 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles.css +2 -0
- package/package.json +91 -0
- package/src/components/AddressDisplay/index.tsx +108 -0
- package/src/components/ErrorRetryCard/index.tsx +68 -0
- package/src/components/FeeBreakdown/feeInfo.ts +25 -0
- package/src/components/FeeBreakdown/index.tsx +218 -0
- package/src/components/LoadingCard/index.tsx +27 -0
- package/src/components/PendingDeposits/index.tsx +152 -0
- package/src/components/QrSheet/QrCode.tsx +163 -0
- package/src/components/QrSheet/index.tsx +80 -0
- package/src/components/TxnItem/ExplorerLink.tsx +37 -0
- package/src/components/TxnItem/index.tsx +178 -0
- package/src/constants.ts +102 -0
- package/src/context/SmartRoutingAddressContext.ts +48 -0
- package/src/context/SmartRoutingAddressProvider.tsx +145 -0
- package/src/hooks/useCreateSmartRoutingAddress.ts +28 -0
- package/src/hooks/useDepositStatus.ts +110 -0
- package/src/hooks/useNewDeposits.ts +25 -0
- package/src/hooks/useProviderFees.ts +235 -0
- package/src/hooks/useSmartRoutingAddress.ts +25 -0
- package/src/iconAssets.ts +130 -0
- package/src/index.ts +27 -0
- package/src/pages/Deposit.tsx +605 -0
- package/src/pages/PastDeposits.tsx +232 -0
- package/src/pages/TransactionDetails.tsx +466 -0
- package/src/pages/index.tsx +165 -0
- package/src/types.ts +84 -0
- package/src/utils/chains.ts +26 -0
- package/src/utils/config.ts +142 -0
- package/src/utils/deposits.ts +106 -0
- package/src/utils/fees.ts +51 -0
- package/src/utils/format.ts +108 -0
- 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
|
+
}
|