@1001-digital/components 0.0.4 → 1.0.0

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 (32) hide show
  1. package/package.json +11 -2
  2. package/src/base/components/Button.vue +2 -2
  3. package/src/base/components/Calendar.vue +227 -0
  4. package/src/base/components/Combobox.vue +241 -0
  5. package/src/base/components/ConfirmDialog.vue +33 -0
  6. package/src/base/components/Dialog.vue +7 -1
  7. package/src/base/components/FormDateField.vue +111 -0
  8. package/src/base/components/FormDatePicker.vue +382 -0
  9. package/src/base/components/FormSlider.vue +142 -0
  10. package/src/base/components/FormSwitch.vue +103 -0
  11. package/src/base/components/Globals.vue +9 -0
  12. package/src/base/components/Opepicon.vue +45 -0
  13. package/src/base/components/PinInput.vue +105 -0
  14. package/src/base/components/Progress.vue +66 -0
  15. package/src/base/components/Toasts.vue +6 -1
  16. package/src/base/composables/confirm.ts +29 -0
  17. package/src/base/composables/toast.ts +1 -0
  18. package/src/base/icons.ts +3 -1
  19. package/src/evm/components/EvmAvatar.vue +62 -0
  20. package/src/evm/components/EvmConnect.vue +83 -32
  21. package/src/evm/components/EvmConnectorQR.vue +12 -42
  22. package/src/evm/components/EvmProfile.vue +183 -0
  23. package/src/evm/components/EvmSwitchNetwork.vue +130 -0
  24. package/src/evm/components/EvmTransactionFlow.vue +41 -11
  25. package/src/evm/components/EvmWalletConnectWallets.vue +199 -0
  26. package/src/evm/composables/chainId.ts +2 -2
  27. package/src/evm/composables/uri.ts +11 -0
  28. package/src/evm/composables/walletExplorer.ts +130 -0
  29. package/src/evm/config.ts +1 -0
  30. package/src/evm/index.ts +9 -0
  31. package/src/evm/utils/uri.ts +24 -0
  32. package/src/index.ts +18 -0
@@ -0,0 +1,183 @@
1
+ <template>
2
+ <Button
3
+ @click="dialogOpen = true"
4
+ :class="className"
5
+ >
6
+ <slot
7
+ :address="address"
8
+ :display="display"
9
+ :ens-name="ensName"
10
+ :ens-avatar="ensAvatar"
11
+ >
12
+ {{ display }}
13
+ </slot>
14
+ </Button>
15
+
16
+ <Dialog
17
+ v-model:open="dialogOpen"
18
+ class="evm-profile"
19
+ title="Account"
20
+ >
21
+ <div class="profile-header">
22
+ <div
23
+ class="banner"
24
+ :style="
25
+ ensHeader ? { backgroundImage: `url(${ensHeader})` } : undefined
26
+ "
27
+ />
28
+ <div class="avatar-wrapper">
29
+ <EvmAvatar :address="address" large />
30
+ </div>
31
+ </div>
32
+
33
+ <div class="profile-identity">
34
+ <strong v-if="ensName">{{ ensName }}</strong>
35
+ <Button
36
+ class="link muted small"
37
+ @click="copyAddress"
38
+ >
39
+ <span>{{ shortAddr }}</span>
40
+ <Icon :type="copied ? 'check' : 'copy'" />
41
+ </Button>
42
+ </div>
43
+
44
+ <div class="profile-actions">
45
+ <EvmSwitchNetwork>
46
+ <template #default="{ currentChain }">
47
+ <Icon type="wallet" />
48
+ <span>Switch Network ({{ currentChain?.name || 'Unknown' }})</span>
49
+ </template>
50
+ </EvmSwitchNetwork>
51
+
52
+ <Button
53
+ v-if="ensName"
54
+ :to="`https://app.ens.domains/${ensName}`"
55
+ target="_blank"
56
+ >
57
+ <Icon type="link" />
58
+ <span>Manage ENS</span>
59
+ </Button>
60
+
61
+ <Button
62
+ class="danger"
63
+ @click="disconnect"
64
+ >
65
+ <span>Disconnect</span>
66
+ </Button>
67
+ </div>
68
+ </Dialog>
69
+ </template>
70
+
71
+ <script setup lang="ts">
72
+ import { ref, computed, nextTick } from 'vue'
73
+ import { useConnection, useDisconnect } from '@wagmi/vue'
74
+ import { useClipboard } from '@vueuse/core'
75
+ import { useConfirm } from '../../base/composables/confirm'
76
+ import { useEnsProfile } from '../composables/ens'
77
+ import { useResolveUri } from '../composables/uri'
78
+ import { shortAddress } from '../utils/addresses'
79
+ import Button from '../../base/components/Button.vue'
80
+ import Dialog from '../../base/components/Dialog.vue'
81
+ import Icon from '../../base/components/Icon.vue'
82
+ import EvmAvatar from './EvmAvatar.vue'
83
+ import EvmSwitchNetwork from './EvmSwitchNetwork.vue'
84
+
85
+ defineProps<{
86
+ className?: string
87
+ }>()
88
+
89
+ const emit = defineEmits<{
90
+ disconnected: []
91
+ }>()
92
+
93
+ const { address } = useConnection()
94
+ const { mutate: disconnectWallet } = useDisconnect()
95
+ const { confirm } = useConfirm()
96
+ const { data: ensData } = useEnsProfile(address)
97
+
98
+ const { copy, copied } = useClipboard()
99
+ const resolve = useResolveUri()
100
+
101
+ const ensName = computed(() => ensData.value?.ens || null)
102
+ const ensAvatar = computed(() => resolve(ensData.value?.data?.avatar))
103
+ const ensHeader = computed(() => resolve(ensData.value?.data?.header))
104
+
105
+ const shortAddr = computed(() =>
106
+ address.value ? shortAddress(address.value) : '',
107
+ )
108
+
109
+ const display = computed(() => ensName.value || shortAddr.value)
110
+
111
+ const dialogOpen = ref(false)
112
+
113
+ const copyAddress = () => {
114
+ if (address.value) copy(address.value)
115
+ }
116
+
117
+ const disconnect = async () => {
118
+ const confirmed = await confirm({
119
+ title: 'Disconnect Wallet',
120
+ description: 'Are you sure you want to disconnect your wallet?',
121
+ okText: 'Disconnect',
122
+ })
123
+
124
+ if (!confirmed) return
125
+
126
+ dialogOpen.value = false
127
+ await nextTick()
128
+ disconnectWallet()
129
+ emit('disconnected')
130
+ }
131
+ </script>
132
+
133
+ <style scoped>
134
+ .profile-header {
135
+ position: relative;
136
+ margin: calc(var(--spacer) * -3) calc(var(--spacer) * -1) 0;
137
+ margin-bottom: 0;
138
+ z-index: -1;
139
+
140
+ .banner {
141
+ aspect-ratio: 3 / 1;
142
+ width: 100%;
143
+ height: auto;
144
+ background-color: var(--gray-z-1);
145
+ background-size: cover;
146
+ background-position: center;
147
+ border-bottom: var(--border);
148
+ }
149
+
150
+ .avatar-wrapper {
151
+ display: flex;
152
+ justify-content: center;
153
+ margin-top: -10%;
154
+ position: relative;
155
+ z-index: 1;
156
+ }
157
+
158
+ :deep(.evm-avatar) {
159
+ border: 3px solid var(--background);
160
+ }
161
+ }
162
+
163
+ .profile-identity {
164
+ display: grid;
165
+ justify-items: center;
166
+ gap: var(--spacer-sm);
167
+
168
+ > strong {
169
+ font-size: var(--font-lg);
170
+ }
171
+ }
172
+
173
+ .profile-actions {
174
+ display: grid;
175
+ gap: var(--spacer);
176
+ padding-block-start: var(--spacer);
177
+
178
+ & :deep(.button),
179
+ & :deep(button) {
180
+ width: 100%;
181
+ }
182
+ }
183
+ </style>
@@ -0,0 +1,130 @@
1
+ <template>
2
+ <Button
3
+ @click="dialogOpen = true"
4
+ :class="className"
5
+ >
6
+ <slot :current-chain="currentChain">
7
+ {{ currentChain?.name || 'Unknown Network' }}
8
+ </slot>
9
+ </Button>
10
+
11
+ <Dialog
12
+ title="Switch Network"
13
+ v-model:open="dialogOpen"
14
+ @closed="onClosed"
15
+ >
16
+ <Alert
17
+ v-if="errorMessage"
18
+ type="error"
19
+ >
20
+ {{ errorMessage }}
21
+ </Alert>
22
+
23
+ <Loading
24
+ v-if="switching"
25
+ spinner
26
+ stacked
27
+ :txt="`Switching to ${switchingTo}...`"
28
+ />
29
+
30
+ <div
31
+ v-if="!switching"
32
+ class="chain-list"
33
+ >
34
+ <Button
35
+ v-for="chain in chains"
36
+ :key="chain.id"
37
+ :disabled="chain.id === currentChainId || undefined"
38
+ :class="['chain-item', { active: chain.id === currentChainId }]"
39
+ @click="() => switchTo(chain)"
40
+ >
41
+ <span>{{ chain.name }}</span>
42
+ <Icon
43
+ v-if="chain.id === currentChainId"
44
+ type="check"
45
+ />
46
+ </Button>
47
+ </div>
48
+ </Dialog>
49
+ </template>
50
+
51
+ <script setup lang="ts">
52
+ import { ref, computed } from 'vue'
53
+ import type { Chain } from 'viem'
54
+ import { useConfig, useConnection, useSwitchChain } from '@wagmi/vue'
55
+ import Button from '../../base/components/Button.vue'
56
+ import Dialog from '../../base/components/Dialog.vue'
57
+ import Icon from '../../base/components/Icon.vue'
58
+ import Alert from '../../base/components/Alert.vue'
59
+ import Loading from '../../base/components/Loading.vue'
60
+
61
+ defineProps<{
62
+ className?: string
63
+ }>()
64
+
65
+ const emit = defineEmits<{
66
+ switched: [{ chainId: number; name: string }]
67
+ error: [{ message: string }]
68
+ }>()
69
+
70
+ const config = useConfig()
71
+ const { chainId: currentChainId } = useConnection()
72
+ const { mutateAsync: switchChainAsync } = useSwitchChain()
73
+
74
+ const chains = computed<readonly Chain[]>(() => config.chains)
75
+ const currentChain = computed(() =>
76
+ chains.value.find((c) => c.id === currentChainId.value),
77
+ )
78
+
79
+ const dialogOpen = ref(false)
80
+ const switching = ref(false)
81
+ const switchingTo = ref('')
82
+ const errorMessage = ref('')
83
+
84
+ const switchTo = async (chain: Chain) => {
85
+ if (chain.id === currentChainId.value) return
86
+
87
+ switching.value = true
88
+ switchingTo.value = chain.name
89
+ errorMessage.value = ''
90
+
91
+ try {
92
+ await switchChainAsync({ chainId: chain.id })
93
+ emit('switched', { chainId: chain.id, name: chain.name })
94
+ dialogOpen.value = false
95
+ } catch (e: unknown) {
96
+ const message = e instanceof Error ? e.message : 'Failed to switch network.'
97
+ errorMessage.value = message.includes('rejected') || message.includes('denied')
98
+ ? 'Network switch cancelled.'
99
+ : 'Failed to switch network. Please try again.'
100
+ emit('error', { message: errorMessage.value })
101
+ } finally {
102
+ switching.value = false
103
+ switchingTo.value = ''
104
+ }
105
+ }
106
+
107
+ const onClosed = () => {
108
+ switching.value = false
109
+ switchingTo.value = ''
110
+ errorMessage.value = ''
111
+ }
112
+ </script>
113
+
114
+ <style scoped>
115
+ .chain-list {
116
+ display: grid;
117
+ gap: var(--spacer-sm);
118
+ }
119
+
120
+ .chain-item {
121
+ width: 100%;
122
+ inline-size: auto;
123
+ justify-content: space-between;
124
+
125
+ &.active {
126
+ pointer-events: none;
127
+ opacity: 1;
128
+ }
129
+ }
130
+ </style>
@@ -12,6 +12,7 @@
12
12
  :click-outside="canDismiss"
13
13
  :title="text.title[step]"
14
14
  class="transaction-flow"
15
+ compat
15
16
  >
16
17
  <slot name="before" />
17
18
 
@@ -19,7 +20,9 @@
19
20
  v-if="step === 'requesting'"
20
21
  spinner
21
22
  stacked
22
- :txt="text.lead[step] || ''"
23
+ :txt="connector?.name
24
+ ? `Requesting signature from ${connector.name}...`
25
+ : text.lead[step] || ''"
23
26
  />
24
27
 
25
28
  <p v-if="step !== 'requesting' && step !== 'error' && text.lead[step]">
@@ -73,7 +76,7 @@
73
76
  <script setup lang="ts">
74
77
  import { ref, computed, watch, onBeforeUnmount } from 'vue'
75
78
  import { waitForTransactionReceipt, watchChainId } from '@wagmi/core'
76
- import { useConfig, type Config } from '@wagmi/vue'
79
+ import { useConfig, useConnection, type Config } from '@wagmi/vue'
77
80
  import type { TransactionReceipt, Hash } from 'viem'
78
81
  import Dialog from '../../base/components/Dialog.vue'
79
82
  import Loading from '../../base/components/Loading.vue'
@@ -120,14 +123,9 @@ const defaultText = {
120
123
  },
121
124
  } satisfies TextConfig
122
125
 
123
- const checkChain = useEnsureChainIdCheck()
124
-
125
- const wagmiConfig = useConfig()
126
- const blockExplorer = useBlockExplorer()
127
- const toast = useToast()
128
-
129
126
  const props = withDefaults(
130
127
  defineProps<{
128
+ chain?: string
131
129
  text?: TextConfig
132
130
  request?: () => Promise<Hash>
133
131
  delayAfter?: number
@@ -145,6 +143,24 @@ const props = withDefaults(
145
143
  },
146
144
  )
147
145
 
146
+ function isUserRejection(e: unknown): boolean {
147
+ const re = /reject|denied|cancel/i
148
+ let current = e as Record<string, unknown> | undefined
149
+ while (current) {
150
+ if ((current as { code?: number }).code === 4001) return true
151
+ if (re.test((current as { details?: string }).details || '')) return true
152
+ current = current.cause as Record<string, unknown> | undefined
153
+ }
154
+ return false
155
+ }
156
+
157
+ const checkChain = useEnsureChainIdCheck(props.chain)
158
+
159
+ const wagmiConfig = useConfig()
160
+ const { connector } = useConnection()
161
+ const blockExplorer = useBlockExplorer(props.chain)
162
+ const toast = useToast()
163
+
148
164
  const emit = defineEmits<{
149
165
  complete: [receipt: TransactionReceipt]
150
166
  cancel: []
@@ -192,8 +208,11 @@ const receipt = ref<TransactionReceipt | null>(null)
192
208
  const txLink = computed(() => `${blockExplorer}/tx/${tx.value}`)
193
209
 
194
210
  let mounted = true
211
+ let progressTimer: ReturnType<typeof setInterval> | undefined
212
+
195
213
  onBeforeUnmount(() => {
196
214
  mounted = false
215
+ clearInterval(progressTimer)
197
216
  })
198
217
 
199
218
  const canDismiss = computed(
@@ -217,10 +236,10 @@ const initializeRequest = async (request = cachedRequest.value) => {
217
236
  step.value = 'requesting'
218
237
  tx.value = await request!()
219
238
  } catch (e: unknown) {
220
- const err = e as { cause?: { code?: number }; shortMessage?: string }
221
- if (err?.cause?.code === 4001) {
239
+ if (isUserRejection(e)) {
222
240
  error.value = 'Transaction rejected by user.'
223
241
  } else {
242
+ const err = e as { shortMessage?: string }
224
243
  error.value = err.shortMessage || 'Error submitting transaction request.'
225
244
  }
226
245
  step.value = 'error'
@@ -238,17 +257,26 @@ const initializeRequest = async (request = cachedRequest.value) => {
238
257
  description: text.value.lead.waiting,
239
258
  duration: Infinity,
240
259
  loading: true,
260
+ progress: 0,
241
261
  action: {
242
262
  label: 'View on Block Explorer',
243
263
  onClick: () => window.open(link, '_blank'),
244
264
  },
245
265
  })
246
266
 
267
+ const start = Date.now()
268
+ progressTimer = setInterval(() => {
269
+ const elapsed = (Date.now() - start) / 1000
270
+ toast.update(toastId, { progress: Math.round(90 * (1 - Math.exp(-elapsed / 15))) })
271
+ }, 500)
272
+
247
273
  try {
248
274
  const receiptObject = await waitForTransactionReceipt(
249
275
  wagmiConfig as Config,
250
276
  { hash: tx.value },
251
277
  )
278
+ clearInterval(progressTimer)
279
+ toast.update(toastId, { progress: 100, loading: false })
252
280
  await delay(props.delayAfter)
253
281
  receipt.value = receiptObject
254
282
  emit('complete', receiptObject)
@@ -257,10 +285,11 @@ const initializeRequest = async (request = cachedRequest.value) => {
257
285
  variant: 'success',
258
286
  title: text.value.title.complete,
259
287
  description: text.value.lead.complete,
260
- loading: false,
288
+ progress: false,
261
289
  ...(props.autoCloseSuccess && { duration: props.delayAutoclose }),
262
290
  })
263
291
  } catch (e: unknown) {
292
+ clearInterval(progressTimer)
264
293
  const err = e as { shortMessage?: string }
265
294
  if (mounted) {
266
295
  toast.dismiss(toastId)
@@ -272,6 +301,7 @@ const initializeRequest = async (request = cachedRequest.value) => {
272
301
  title: text.value.title.error,
273
302
  description: err.shortMessage || 'Transaction failed.',
274
303
  loading: false,
304
+ progress: false,
275
305
  })
276
306
  }
277
307
  console.log(e)
@@ -0,0 +1,199 @@
1
+ <template>
2
+ <div class="wc-wallets">
3
+ <EvmConnectorQR :uri="uri">
4
+ <template #instruction> Scan with your wallet app </template>
5
+ </EvmConnectorQR>
6
+
7
+ <div class="separator">
8
+ <span>Or choose a wallet</span>
9
+ </div>
10
+
11
+ <FormItem>
12
+ <template #prefix>
13
+ <Icon type="lucide:search" />
14
+ </template>
15
+ <input
16
+ v-model="searchQuery"
17
+ type="text"
18
+ placeholder="Search wallets"
19
+ @input="onSearchInput"
20
+ />
21
+ </FormItem>
22
+
23
+ <div
24
+ v-if="displayedWallets.length > 0"
25
+ class="wallet-grid"
26
+ >
27
+ <Button
28
+ v-for="wallet in displayedWallets"
29
+ :key="wallet.id"
30
+ @click="openWallet(wallet)"
31
+ class="wallet-card tertiary"
32
+ >
33
+ <img
34
+ :src="explorer.imageUrl(wallet)"
35
+ :alt="wallet.name"
36
+ loading="lazy"
37
+ />
38
+ <span>{{ wallet.name }}</span>
39
+ </Button>
40
+ </div>
41
+
42
+ <p
43
+ v-if="
44
+ searchQuery &&
45
+ !explorer.searching.value &&
46
+ displayedWallets.length === 0
47
+ "
48
+ class="empty-state"
49
+ >
50
+ No wallets found
51
+ </p>
52
+
53
+ <Loading
54
+ v-if="explorer.loading.value || explorer.searching.value"
55
+ spinner
56
+ stacked
57
+ txt=""
58
+ />
59
+
60
+ <Button
61
+ to="https://ethereum.org/wallets/"
62
+ target="_blank"
63
+ class="link muted small help-link"
64
+ >
65
+ <Icon type="help" />
66
+ <span>New to wallets?</span>
67
+ </Button>
68
+ </div>
69
+ </template>
70
+
71
+ <script setup lang="ts">
72
+ import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
73
+ import Button from '../../base/components/Button.vue'
74
+ import FormItem from '../../base/components/FormItem.vue'
75
+ import Icon from '../../base/components/Icon.vue'
76
+ import Loading from '../../base/components/Loading.vue'
77
+ import EvmConnectorQR from './EvmConnectorQR.vue'
78
+ import {
79
+ useWalletExplorer,
80
+ type ExplorerWallet,
81
+ } from '../composables/walletExplorer'
82
+
83
+ const props = defineProps<{
84
+ uri: string
85
+ }>()
86
+
87
+ const explorer = useWalletExplorer()
88
+ const searchQuery = ref('')
89
+ let searchTimeout: ReturnType<typeof setTimeout>
90
+
91
+ const TOP_COUNT = 9
92
+
93
+ const displayedWallets = computed(() => {
94
+ if (searchQuery.value) return explorer.searchResults.value
95
+
96
+ const recents = explorer.recentWallets.value
97
+ const recentIds = new Set(recents.map((w) => w.id))
98
+ const rest = explorer.wallets.value
99
+ .filter((w) => !recentIds.has(w.id))
100
+ .slice(0, TOP_COUNT - recents.length)
101
+
102
+ return [...recents, ...rest]
103
+ })
104
+
105
+ function openWallet(wallet: ExplorerWallet) {
106
+ const href = explorer.walletHref(wallet, props.uri)
107
+ if (href) {
108
+ window.open(href, '_blank', 'noreferrer')
109
+ }
110
+ explorer.addRecent(wallet.id)
111
+ }
112
+
113
+ function onSearchInput() {
114
+ clearTimeout(searchTimeout)
115
+ searchTimeout = setTimeout(() => {
116
+ if (searchQuery.value) {
117
+ explorer.search(searchQuery.value)
118
+ }
119
+ }, 300)
120
+ }
121
+
122
+ onBeforeUnmount(() => {
123
+ clearTimeout(searchTimeout)
124
+ })
125
+
126
+ onMounted(() => {
127
+ explorer.fetchNextPage()
128
+ })
129
+ </script>
130
+
131
+ <style scoped>
132
+ .wc-wallets {
133
+ display: grid;
134
+ gap: var(--spacer);
135
+ }
136
+
137
+ /* Separator */
138
+ .separator {
139
+ display: flex;
140
+ align-items: center;
141
+ gap: var(--spacer-sm);
142
+ @mixin ui-font;
143
+ color: var(--muted);
144
+ font-size: var(--font-sm);
145
+
146
+ &::before,
147
+ &::after {
148
+ content: '';
149
+ flex: 1;
150
+ border-top: var(--border);
151
+ }
152
+ }
153
+
154
+ /* Wallet grid */
155
+ .wallet-grid {
156
+ display: grid;
157
+ grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr));
158
+ gap: var(--spacer-sm);
159
+ }
160
+
161
+ .wallet-card {
162
+ display: flex;
163
+ flex-direction: column;
164
+ align-items: center;
165
+ gap: var(--spacer-sm);
166
+ block-size: auto;
167
+ min-inline-size: 0;
168
+ inline-size: 100%;
169
+
170
+ img {
171
+ width: var(--size-6);
172
+ height: var(--size-6);
173
+ border-radius: var(--border-radius);
174
+ margin-top: var(--spacer-xs);
175
+ }
176
+
177
+ span {
178
+ font-size: var(--font-xs);
179
+ text-align: center;
180
+ overflow: hidden;
181
+ text-overflow: ellipsis;
182
+ white-space: nowrap;
183
+ max-width: 100%;
184
+ }
185
+ }
186
+
187
+ /* Help link */
188
+ .help-link {
189
+ justify-self: center;
190
+ font-size: var(--font-xs);
191
+ }
192
+
193
+ /* Empty state */
194
+ .empty-state {
195
+ text-align: center;
196
+ @mixin ui-font;
197
+ color: var(--muted);
198
+ }
199
+ </style>
@@ -22,8 +22,8 @@ export const useMainChainId = () => useChainConfig().id
22
22
  export const useBlockExplorer = (key?: string) =>
23
23
  useChainConfig(key).blockExplorer
24
24
 
25
- export const useEnsureChainIdCheck = () => {
26
- const chainId = useMainChainId()
25
+ export const useEnsureChainIdCheck = (key?: string) => {
26
+ const chainId = useChainConfig(key).id
27
27
  const { mutateAsync: switchChain } = useSwitchChain()
28
28
  const { chainId: currentChainId } = useConnection()
29
29
 
@@ -0,0 +1,11 @@
1
+ import { resolveUri } from '../utils/uri'
2
+
3
+ export const useResolveUri = () => {
4
+ const appConfig = useAppConfig()
5
+
6
+ return (uri?: string) =>
7
+ resolveUri(uri, {
8
+ ipfsGateway: appConfig.evm?.ipfsGateway,
9
+ arweaveGateway: appConfig.evm?.arweaveGateway,
10
+ })
11
+ }