0xtrails 0.1.3 → 0.1.4

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 (111) hide show
  1. package/dist/address.d.ts.map +1 -1
  2. package/dist/analytics.d.ts +18 -0
  3. package/dist/analytics.d.ts.map +1 -1
  4. package/dist/apiClient.d.ts +1 -1
  5. package/dist/apiClient.d.ts.map +1 -1
  6. package/dist/{ccip-CWd4g9uZ.js → ccip-dLSEJjCf.js} +55 -55
  7. package/dist/cctpqueue.d.ts +1 -1
  8. package/dist/cctpqueue.d.ts.map +1 -1
  9. package/dist/constants.d.ts +1 -0
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/decoders.d.ts +58 -0
  12. package/dist/decoders.d.ts.map +1 -0
  13. package/dist/ens.d.ts +6 -0
  14. package/dist/ens.d.ts.map +1 -1
  15. package/dist/error.d.ts +7 -0
  16. package/dist/error.d.ts.map +1 -1
  17. package/dist/{index-BTUBzx4R.js → index-BXbaLmtt.js} +25694 -24507
  18. package/dist/index.js +2 -2
  19. package/dist/intents.d.ts +4 -4
  20. package/dist/intents.d.ts.map +1 -1
  21. package/dist/metaTxns.d.ts +1 -1
  22. package/dist/metaTxns.d.ts.map +1 -1
  23. package/dist/mode.d.ts +1 -1
  24. package/dist/mode.d.ts.map +1 -1
  25. package/dist/preconditions.d.ts +1 -1
  26. package/dist/preconditions.d.ts.map +1 -1
  27. package/dist/prepareSend.d.ts +30 -24
  28. package/dist/prepareSend.d.ts.map +1 -1
  29. package/dist/prices.d.ts +1 -1
  30. package/dist/prices.d.ts.map +1 -1
  31. package/dist/proxyCaller.d.ts +0 -1
  32. package/dist/proxyCaller.d.ts.map +1 -1
  33. package/dist/tokenBalances.d.ts +1 -1
  34. package/dist/tokenBalances.d.ts.map +1 -1
  35. package/dist/tokens.d.ts.map +1 -1
  36. package/dist/trails.d.ts +1 -1
  37. package/dist/trails.d.ts.map +1 -1
  38. package/dist/transactions.d.ts +4 -0
  39. package/dist/transactions.d.ts.map +1 -1
  40. package/dist/utils.d.ts +6 -0
  41. package/dist/utils.d.ts.map +1 -1
  42. package/dist/widget/components/DebugScreensDropdown.d.ts.map +1 -1
  43. package/dist/widget/components/ErrorDisplay.d.ts +9 -0
  44. package/dist/widget/components/ErrorDisplay.d.ts.map +1 -0
  45. package/dist/widget/components/FundSendForm.d.ts +1 -1
  46. package/dist/widget/components/FundSendForm.d.ts.map +1 -1
  47. package/dist/widget/components/OriginTransferInformation.d.ts +10 -0
  48. package/dist/widget/components/OriginTransferInformation.d.ts.map +1 -0
  49. package/dist/widget/components/PaySendForm.d.ts +1 -1
  50. package/dist/widget/components/PaySendForm.d.ts.map +1 -1
  51. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  52. package/dist/widget/components/Receipt.d.ts.map +1 -1
  53. package/dist/widget/components/Receive.d.ts +12 -0
  54. package/dist/widget/components/Receive.d.ts.map +1 -0
  55. package/dist/widget/components/Swap.d.ts +5 -1
  56. package/dist/widget/components/Swap.d.ts.map +1 -1
  57. package/dist/widget/components/SwapDisplay.d.ts +9 -0
  58. package/dist/widget/components/SwapDisplay.d.ts.map +1 -0
  59. package/dist/widget/components/TransferPendingVertical.d.ts +2 -0
  60. package/dist/widget/components/TransferPendingVertical.d.ts.map +1 -1
  61. package/dist/widget/components/YellowWarningAnimation.d.ts +2 -0
  62. package/dist/widget/components/YellowWarningAnimation.d.ts.map +1 -0
  63. package/dist/widget/hooks/useDebugScreens.d.ts +22 -0
  64. package/dist/widget/hooks/useDebugScreens.d.ts.map +1 -0
  65. package/dist/widget/hooks/useSendForm.d.ts +7 -3
  66. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  67. package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
  68. package/dist/widget/index.js +1 -1
  69. package/dist/widget/widget.d.ts.map +1 -1
  70. package/package.json +16 -15
  71. package/src/address.ts +3 -0
  72. package/src/analytics.ts +105 -4
  73. package/src/apiClient.ts +1 -1
  74. package/src/cctpqueue.ts +1 -1
  75. package/src/constants.ts +3 -0
  76. package/src/decoders.ts +310 -0
  77. package/src/ens.ts +16 -1
  78. package/src/error.ts +86 -1
  79. package/src/intents.ts +10 -2
  80. package/src/metaTxns.ts +1 -1
  81. package/src/mode.ts +1 -1
  82. package/src/preconditions.ts +1 -1
  83. package/src/prepareSend.ts +432 -114
  84. package/src/prices.ts +5 -1
  85. package/src/proxyCaller.ts +2 -14
  86. package/src/tokenBalances.ts +21 -17
  87. package/src/tokens.ts +62 -3
  88. package/src/trails.ts +2 -2
  89. package/src/transactions.ts +35 -18
  90. package/src/utils.ts +28 -0
  91. package/src/widget/compiled.css +2 -2
  92. package/src/widget/components/DebugScreensDropdown.tsx +3 -0
  93. package/src/widget/components/ErrorDisplay.tsx +150 -0
  94. package/src/widget/components/FundSendForm.tsx +62 -9
  95. package/src/widget/components/OriginTransferInformation.tsx +59 -0
  96. package/src/widget/components/PaySendForm.tsx +65 -12
  97. package/src/widget/components/QRCodeDeposit.tsx +5 -5
  98. package/src/widget/components/QrCode.tsx +1 -1
  99. package/src/widget/components/QuoteDetails.tsx +93 -25
  100. package/src/widget/components/Receipt.tsx +296 -103
  101. package/src/widget/components/Receive.tsx +146 -0
  102. package/src/widget/components/RecentTokens.tsx +1 -1
  103. package/src/widget/components/Swap.tsx +206 -85
  104. package/src/widget/components/SwapDisplay.tsx +68 -0
  105. package/src/widget/components/TokenSelector.tsx +1 -1
  106. package/src/widget/components/TransferPendingVertical.tsx +162 -112
  107. package/src/widget/components/YellowWarningAnimation.tsx +146 -0
  108. package/src/widget/hooks/useDebugScreens.ts +583 -0
  109. package/src/widget/hooks/useSendForm.ts +46 -4
  110. package/src/widget/hooks/useTokenList.ts +8 -27
  111. package/src/widget/widget.tsx +201 -484
package/src/prices.ts CHANGED
@@ -1,4 +1,8 @@
1
- import type { SequenceAPIClient, Token, TokenPrice } from "@0xsequence/api"
1
+ import type {
2
+ SequenceAPIClient,
3
+ Token,
4
+ TokenPrice,
5
+ } from "@0xsequence/trails-api"
2
6
  import { QueryClient, useQuery } from "@tanstack/react-query"
3
7
  import { zeroAddress } from "viem"
4
8
 
@@ -1,18 +1,9 @@
1
1
  import { encodeFunctionData } from "viem"
2
- import * as chains from "viem/chains"
2
+ import { TRAILS_BALANCE_INJECTOR_ADDRESS } from "./constants.js"
3
3
 
4
4
  export const TRAILS_CONTRACT_PLACEHOLDER_AMOUNT =
5
5
  0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefn
6
6
 
7
- const proxyCallers: Record<string, string> = {
8
- [chains.base.id]: "0x4F54Dc2C6bCa2D01e066fa36f4CFdA96B544DD20",
9
- [chains.arbitrum.id]: "0x8c070e800D0e0c33A0A4ceef7A91677410ed9444",
10
- }
11
-
12
- export function getProxyCallerAddress(chainId: number) {
13
- return proxyCallers[chainId]
14
- }
15
-
16
7
  function getAmountOffset(calldata: `0x${string}`, placeholder: bigint): number {
17
8
  const hex = placeholder.toString(16).padStart(64, "0")
18
9
  const search = hex.toLowerCase()
@@ -99,10 +90,7 @@ export function wrapCalldataWithProxyCallerIfNeeded({
99
90
  }
100
91
  }
101
92
 
102
- const proxyCallerAddress = getProxyCallerAddress(destinationChainId)
103
- if (!proxyCallerAddress) {
104
- return null
105
- }
93
+ const proxyCallerAddress = TRAILS_BALANCE_INJECTOR_ADDRESS
106
94
 
107
95
  const amountOffset = getAmountOffset(
108
96
  calldata,
@@ -7,7 +7,7 @@ import type {
7
7
  TokenBalance,
8
8
  } from "@0xsequence/indexer"
9
9
  import { ContractVerificationStatus } from "@0xsequence/indexer"
10
- import type { Page, Price, SequenceAPIClient } from "@0xsequence/api"
10
+ import type { Page, Price, SequenceAPIClient } from "@0xsequence/trails-api"
11
11
  import { QueryClient, useQuery } from "@tanstack/react-query"
12
12
  import type { Address } from "ox"
13
13
  import { useEffect, useState } from "react"
@@ -18,12 +18,14 @@ import { getTokenPrices, useTokenPrices } from "./prices.js"
18
18
 
19
19
  export type { NativeTokenBalance, TokenBalance }
20
20
 
21
+ const REFRESH_INTERVAL = 10000 // 10 seconds
22
+
21
23
  // Initialize query client for token balances
22
24
  const tokenBalancesQueryClient = new QueryClient({
23
25
  defaultOptions: {
24
26
  queries: {
25
- staleTime: 60000, // 1 minute
26
- gcTime: 300000, // 5 minutes
27
+ staleTime: REFRESH_INTERVAL, // 10 seconds - faster updates for balance changes
28
+ gcTime: REFRESH_INTERVAL, // 10 seconds
27
29
  retry: 2,
28
30
  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
29
31
  refetchOnWindowFocus: false,
@@ -167,8 +169,8 @@ export function useTokenBalances(
167
169
  }
168
170
  },
169
171
  enabled: !!address && !!indexerClient,
170
- staleTime: 60000, // 1 minute
171
- gcTime: 300000, // 5 minutes cache time
172
+ staleTime: 10000, // 10 seconds - faster updates for balance changes
173
+ gcTime: 10000, // 10 seconds cache time
172
174
  retry: (failureCount, error) => {
173
175
  // Don't retry 404s or network errors after 3 attempts
174
176
  if (error && "status" in error && error.status === 404) return false
@@ -176,10 +178,11 @@ export function useTokenBalances(
176
178
  return false
177
179
  },
178
180
  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff
179
- refetchOnWindowFocus: false, // Prevent refetch on window focus
181
+ refetchOnWindowFocus: true, // Prevent refetch on window focus
180
182
  refetchOnReconnect: true, // Refetch on reconnect
181
- refetchInterval: 300000, // Background refetch every 5 minutes
183
+ refetchInterval: REFRESH_INTERVAL, // Background refetch every 10 seconds
182
184
  refetchIntervalInBackground: true,
185
+ refetchOnMount: true,
183
186
  })
184
187
 
185
188
  const { tokenPrices, isLoadingTokenPrices } = useTokenPrices(
@@ -262,9 +265,9 @@ export function useTokenBalances(
262
265
  !isLoadingTokenPrices &&
263
266
  !!tokenBalancesData &&
264
267
  !!tokenPrices,
265
- staleTime: 30000, // 30 seconds for sorted tokens
266
- gcTime: 120000, // 2 minutes cache time
267
- refetchOnWindowFocus: false,
268
+ staleTime: REFRESH_INTERVAL, // 10 seconds for sorted tokens
269
+ gcTime: REFRESH_INTERVAL, // 10 seconds cache time
270
+ refetchOnWindowFocus: true,
268
271
  })
269
272
 
270
273
  return {
@@ -432,8 +435,8 @@ export async function getTokenBalances({
432
435
  queryKey: ["tokenBalances", "summary", account],
433
436
  queryFn: () =>
434
437
  fetchGetTokenBalancesSummary({ account, indexerGatewayClient }),
435
- staleTime: 60000, // 1 minute
436
- gcTime: 300000, // 5 minutes
438
+ staleTime: REFRESH_INTERVAL, // 10 seconds - faster updates for balance changes
439
+ gcTime: REFRESH_INTERVAL, // 10 seconds
437
440
  })
438
441
  }
439
442
 
@@ -727,8 +730,8 @@ export function useHasSufficientBalanceUsd(
727
730
  })
728
731
  : false,
729
732
  enabled: !!account && !!targetAmountUsd,
730
- staleTime: 45000, // 45 seconds
731
- gcTime: 180000, // 3 minutes cache time
733
+ staleTime: REFRESH_INTERVAL, // 10 seconds
734
+ gcTime: REFRESH_INTERVAL, // 10 seconds cache time
732
735
  retry: (failureCount, error) => {
733
736
  if (error && "status" in error && error.status === 404) return false
734
737
  if (failureCount < 2) return true
@@ -786,8 +789,8 @@ export function useAccountTotalBalanceUsd(account: string): {
786
789
  })
787
790
  : null,
788
791
  enabled: !!account,
789
- staleTime: 60000, // 1 minute
790
- gcTime: 300000, // 5 minutes cache time
792
+ staleTime: REFRESH_INTERVAL, // 10 seconds - faster updates for balance changes
793
+ gcTime: REFRESH_INTERVAL, // 10 seconds cache time
791
794
  retry: (failureCount, error) => {
792
795
  if (error && "status" in error && error.status === 404) return false
793
796
  if (failureCount < 2) return true
@@ -796,8 +799,9 @@ export function useAccountTotalBalanceUsd(account: string): {
796
799
  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
797
800
  refetchOnWindowFocus: false,
798
801
  refetchOnReconnect: true,
799
- refetchInterval: 300000, // Background refetch every 5 minutes
802
+ refetchInterval: REFRESH_INTERVAL, // Background refetch every 10 seconds
800
803
  refetchIntervalInBackground: true,
804
+ refetchOnMount: true,
801
805
  })
802
806
 
803
807
  return {
package/src/tokens.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { QueryClient, useQuery } from "@tanstack/react-query"
2
- import { zeroAddress, erc20Abi } from "viem"
2
+ import { zeroAddress, erc20Abi, createPublicClient, http } from "viem"
3
3
  import { getChainInfo, getSupportedChains } from "./chains.js"
4
4
  import * as chains from "viem/chains"
5
5
  import { getRelaySupportedTokens } from "./relaySdk.js"
@@ -535,6 +535,9 @@ export async function getTokenInfo(
535
535
  t.contractAddress.toLowerCase() === normalizedAddress,
536
536
  )
537
537
 
538
+ console.log("tokens", tokens)
539
+ console.log("token", token)
540
+
538
541
  if (token) {
539
542
  // Cache the token info
540
543
  setTokenInfoCache(`${chainId}:${normalizedAddress}`, token)
@@ -542,8 +545,64 @@ export async function getTokenInfo(
542
545
  return token
543
546
  }
544
547
 
545
- // If not found in supported tokens, return null
546
- return null
548
+ // If not found in supported tokens, try to fetch on-chain data
549
+ try {
550
+ const chainInfo = getChainInfo(chainId)
551
+ if (!chainInfo) {
552
+ console.warn(`[trails-sdk] Chain info not found for chainId: ${chainId}`)
553
+ return null
554
+ }
555
+
556
+ // Create a public client for the specific chain
557
+ const publicClient = createPublicClient({
558
+ chain: chainInfo,
559
+ transport: http(),
560
+ })
561
+
562
+ // Read token data on-chain
563
+ const [name, symbol, decimals] = await Promise.all([
564
+ publicClient.readContract({
565
+ address: normalizedAddress as `0x${string}`,
566
+ abi: erc20Abi,
567
+ functionName: "name",
568
+ }),
569
+ publicClient.readContract({
570
+ address: normalizedAddress as `0x${string}`,
571
+ abi: erc20Abi,
572
+ functionName: "symbol",
573
+ }),
574
+ publicClient.readContract({
575
+ address: normalizedAddress as `0x${string}`,
576
+ abi: erc20Abi,
577
+ functionName: "decimals",
578
+ }),
579
+ ])
580
+
581
+ // Create token info from on-chain data
582
+ const onChainTokenInfo: SupportedToken = {
583
+ id: `${symbol}-${chainInfo.name}`,
584
+ name: name,
585
+ symbol: symbol,
586
+ decimals: decimals,
587
+ chainId: chainId,
588
+ contractAddress: normalizedAddress,
589
+ chainName: chainInfo.name,
590
+ imageUrl: getTokenImageUrl({
591
+ chainId,
592
+ contractAddress: normalizedAddress,
593
+ symbol: symbol,
594
+ }),
595
+ }
596
+
597
+ // Cache the on-chain token info
598
+ setTokenInfoCache(`${chainId}:${normalizedAddress}`, onChainTokenInfo)
599
+
600
+ console.log("[trails-sdk] Fetched token info on-chain:", onChainTokenInfo)
601
+ return onChainTokenInfo
602
+ } catch (error) {
603
+ console.error("[trails-sdk] Error fetching on-chain token info:", error)
604
+ return null
605
+ }
547
606
  }
548
607
 
549
608
  export async function getTokenAddress(chainId: number, tokenSymbol: string) {
package/src/trails.ts CHANGED
@@ -4,8 +4,8 @@ import type {
4
4
  GetIntentConfigReturn,
5
5
  IntentCallsPayload,
6
6
  IntentPrecondition,
7
- } from "@0xsequence/api"
8
- import type { SequenceAPIClient } from "@0xsequence/api"
7
+ } from "@0xsequence/trails-api"
8
+ import type { SequenceAPIClient } from "@0xsequence/trails-api"
9
9
  import type { Relayer } from "@0xsequence/wallet-core"
10
10
  import { useMutation, useQuery } from "@tanstack/react-query"
11
11
  import { Address } from "ox"
@@ -1,6 +1,8 @@
1
1
  import { createPublicClient, http } from "viem"
2
2
  import { getChainInfo } from "./chains.js"
3
- import { getRpcSequenceProjectAccessKey } from "./config.js"
3
+ import { getRpcSequenceProjectAccessKey, getSequenceEnv } from "./config.js"
4
+ import type { GuestModuleEvent, TrailsTokenSweeperEvent } from "./decoders.js"
5
+ import * as chains from "viem/chains"
4
6
 
5
7
  export type TransactionStateStatus = "pending" | "failed" | "confirmed"
6
8
 
@@ -11,6 +13,8 @@ export type TransactionState = {
11
13
  chainId: number
12
14
  state: TransactionStateStatus
13
15
  label: string
16
+ decodedTrailsTokenSweeperEvents?: TrailsTokenSweeperEvent[]
17
+ decodedGuestModuleEvents?: GuestModuleEvent[]
14
18
  }
15
19
 
16
20
  export type TransferType = "SEND" | "RECEIVE"
@@ -163,24 +167,37 @@ export async function getTxTimeDiff(
163
167
  }
164
168
  }
165
169
 
170
+ export function getIndexerUrlChainSlug(chainSlug: string) {
171
+ const env = getSequenceEnv()
172
+ let envPrefix = ""
173
+
174
+ // Only use dev prefix for katana in dev environment, since it's not deployed to prod yet
175
+ if (env === "dev" && chainSlug === "katana") {
176
+ envPrefix = "dev-"
177
+ }
178
+
179
+ return `https://${envPrefix}${chainSlug}-indexer.sequence.app`
180
+ }
181
+
166
182
  const chainIdToIndexerUrl = {
167
- 42161: "https://arbitrum-indexer.sequence.app",
168
- 8453: "https://base-indexer.sequence.app",
169
- 84532: "https://base-sepolia-indexer.sequence.app",
170
- 10: "https://optimism-indexer.sequence.app",
171
- 137: "https://polygon-indexer.sequence.app",
172
- 1: "https://mainnet-indexer.sequence.app",
173
- 33139: "https://apechain-indexer.sequence.app",
174
- 42170: "https://arbitrum-nova-indexer.sequence.app",
175
- 43114: "https://avalanche-indexer.sequence.app",
176
- 8333: "https://b3-indexer.sequence.app",
177
- 81457: "https://blast-indexer.sequence.app",
178
- 100: "https://gnosis-indexer.sequence.app",
179
- 1868: "https://soneium-indexer.sequence.app",
180
- 660279: "https://xai-indexer.sequence.app",
181
- 56: "https://bsc-indexer.sequence.app",
182
- 421613: "https://arbitrum-nova-sepolia-indexer.sequence.app",
183
- 42793: "https://etherlink-indexer.sequence.app",
183
+ [chains.arbitrum.id]: getIndexerUrlChainSlug("arbitrum"),
184
+ [chains.base.id]: getIndexerUrlChainSlug("base"),
185
+ [chains.baseSepolia.id]: getIndexerUrlChainSlug("base-sepolia"),
186
+ [chains.optimism.id]: getIndexerUrlChainSlug("optimism"),
187
+ [chains.polygon.id]: getIndexerUrlChainSlug("polygon"),
188
+ [chains.mainnet.id]: getIndexerUrlChainSlug("mainnet"),
189
+ [chains.apeChain.id]: getIndexerUrlChainSlug("apechain"),
190
+ [chains.arbitrumNova.id]: getIndexerUrlChainSlug("arbitrum-nova"),
191
+ [chains.avalanche.id]: getIndexerUrlChainSlug("avalanche"),
192
+ [chains.b3.id]: getIndexerUrlChainSlug("b3"),
193
+ [chains.blast.id]: getIndexerUrlChainSlug("blast"),
194
+ [chains.gnosis.id]: getIndexerUrlChainSlug("gnosis"),
195
+ [chains.soneium.id]: getIndexerUrlChainSlug("soneium"),
196
+ [chains.xai.id]: getIndexerUrlChainSlug("xai"),
197
+ [chains.bsc.id]: getIndexerUrlChainSlug("bsc"),
198
+ 421613: getIndexerUrlChainSlug("arbitrum-nova-sepolia"),
199
+ [chains.etherlink.id]: getIndexerUrlChainSlug("etherlink"),
200
+ [chains.katana.id]: getIndexerUrlChainSlug("katana"),
184
201
  }
185
202
 
186
203
  export async function getAccountTransactionHistory({
package/src/utils.ts CHANGED
@@ -40,3 +40,31 @@ export function formatElapsed(seconds: number): string {
40
40
  }
41
41
  }
42
42
  }
43
+
44
+ /**
45
+ * Calculates and formats time ago from a timestamp
46
+ * @param timestamp - Timestamp in milliseconds
47
+ * @returns Formatted time ago string (e.g., "Sent just now", "Sent 5 minutes ago")
48
+ */
49
+ export function getTimeAgo(timestamp?: number): string {
50
+ if (!timestamp) return "Sent just now"
51
+
52
+ const now = Date.now()
53
+ const diffInSeconds = Math.floor((now - timestamp) / 1000)
54
+
55
+ if (diffInSeconds < 1) return "Sent just now"
56
+ if (diffInSeconds === 1) return "Sent 1 second ago"
57
+ if (diffInSeconds < 60) return `Sent ${diffInSeconds} seconds ago`
58
+
59
+ const diffInMinutes = Math.floor(diffInSeconds / 60)
60
+ if (diffInMinutes === 1) return "Sent 1 minute ago"
61
+ if (diffInMinutes < 60) return `Sent ${diffInMinutes} minutes ago`
62
+
63
+ const diffInHours = Math.floor(diffInMinutes / 60)
64
+ if (diffInHours === 1) return "Sent 1 hour ago"
65
+ if (diffInHours < 24) return `Sent ${diffInHours} hours ago`
66
+
67
+ const diffInDays = Math.floor(diffInHours / 24)
68
+ if (diffInDays === 1) return "Sent 1 day ago"
69
+ return `Sent ${diffInDays} days ago`
70
+ }