diff --git a/.env b/.env
index 579463ead85..012099c765a 100644
--- a/.env
+++ b/.env
@@ -189,7 +189,6 @@ VITE_NEAR_NODE_URL=https://rpc.mainnet.near.org
VITE_NEAR_NODE_URL_FALLBACK_1=https://near.lava.build
VITE_NEAR_NODE_URL_FALLBACK_2=https://rpc.fastnear.com
VITE_FASTNEAR_API_URL=https://api.fastnear.com
-VITE_ALCHEMY_POLYGON_URL=https://polygon-mainnet.g.alchemy.com/v2/anoTMcIc2hbPUxri37h4DeuUwg2p5_xZ
# midgard
VITE_THORCHAIN_MIDGARD_URL=https://api.thorchain.shapeshift.com/midgard/v2
@@ -205,15 +204,9 @@ VITE_COINCAP_API_KEY=dab646843b251ce2d28864982989c335e1f0d32fa14e4ecc6b40cd057ec
VITE_EXCHANGERATEHOST_BASE_URL=https://api.exchangerate.host
VITE_EXCHANGERATEHOST_API_KEY=8f7515ffddef9d3e449b45f93108ca4d
-# Alchemy API key - to be used either with Alchemy SDK or directly with the REST endpoints
-VITE_ALCHEMY_API_KEY=anoTMcIc2hbPUxri37h4DeuUwg2p5_xZ
-
-# Moralis API key - to be used either with Alchemy SDK or directly with the REST endpoints
+# Moralis API key
VITE_MORALIS_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJub25jZSI6ImY1ZmVjMTg4LTc0YzUtNDk0ZC05ZmFjLTZmYzQ5MTAyZTVhOCIsIm9yZ0lkIjoiNDYzNTU5IiwidXNlcklkIjoiNDc2OTA5IiwidHlwZUlkIjoiODE0NWUwYjEtYjEwNi00NzQyLTg2NDAtNzk1NmU4ZGQ5ZGFkIiwidHlwZSI6IlBST0pFQ1QiLCJpYXQiOjE3NTQ0NzEyNTksImV4cCI6NDkxMDIzMTI1OX0.Aa-ELM_vZR0i7Z1nQfh0rsx6ES21DHNEbNsjL28AMMw
-# Alchemy Solana endpoint for custom token import
-VITE_ALCHEMY_SOLANA_BASE_URL=https://solana-mainnet.g.alchemy.com/v2
-
# boardroom
VITE_BOARDROOM_API_BASE_URL=https://api.boardroom.info/v1/protocols/shapeshift/
VITE_BOARDROOM_APP_BASE_URL=https://boardroom.io/shapeshift/
@@ -259,6 +252,9 @@ VITE_WALLET_CONNECT_RELAY_URL=wss://relay.walletconnect.com
# Portals
VITE_PORTALS_BASE_URL=https://api.proxy.shapeshift.com/api/v1/portals
+# Proxy API
+VITE_PROXY_API_BASE_URL=https://api.proxy.shapeshift.com
+
VITE_SNAP_ID=npm:@shapeshiftoss/metamask-snaps
VITE_SNAP_VERSION=1.0.9
# VITE_SNAP_ID=local:http://localhost:9000
diff --git a/.env.development b/.env.development
index 5666ec50e22..0da3372f524 100644
--- a/.env.development
+++ b/.env.development
@@ -116,6 +116,9 @@ VITE_NOTIFICATIONS_SERVER_URL=https://dev-api.notifications-service.shapeshift.c
# VITE_USER_SERVER_URL=/user-api
# VITE_NOTIFICATIONS_SERVER_URL=/notifications-api
+# Proxy API
+VITE_PROXY_API_BASE_URL=https://dev-api.proxy.shapeshift.com
+
VITE_FEATURE_WC_DIRECT_CONNECTION=true
VITE_FEATURE_CETUS_SWAP=true
VITE_FEATURE_MANTLE=true
diff --git a/headers/csps/alchemy.ts b/headers/csps/alchemy.ts
deleted file mode 100644
index 1c4c8397fa4..00000000000
--- a/headers/csps/alchemy.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import type { Csp } from '../types'
-
-export const csp: Csp = {
- 'connect-src': ['https://*.g.alchemy.com/v2/'],
-}
diff --git a/headers/csps/chains/ethereum.ts b/headers/csps/chains/ethereum.ts
index f627bdf50bf..8e960034175 100644
--- a/headers/csps/chains/ethereum.ts
+++ b/headers/csps/chains/ethereum.ts
@@ -11,7 +11,6 @@ export const csp: Csp = {
env.VITE_ETHEREUM_NODE_URL,
env.VITE_UNCHAINED_ETHEREUM_HTTP_URL,
env.VITE_UNCHAINED_ETHEREUM_WS_URL,
- env.VITE_ALCHEMY_POLYGON_URL,
...FALLBACK_RPC_URLS.ethereum,
],
}
diff --git a/headers/csps/index.ts b/headers/csps/index.ts
index 93be4936c67..a1a1aeeb97b 100644
--- a/headers/csps/index.ts
+++ b/headers/csps/index.ts
@@ -1,6 +1,5 @@
import { csp as across } from './across'
import { csp as agenticChat } from './agenticChat'
-import { csp as alchemy } from './alchemy'
import { csp as trustwallet } from './assetService/trustwallet'
import { csp as base } from './base'
import { csp as chainflip } from './chainflip'
@@ -108,7 +107,6 @@ export const csps = [
base,
agenticChat,
hypelab,
- alchemy,
moralis,
chainflip,
chatwoot,
diff --git a/package.json b/package.json
index c928567fd82..673b7c62a60 100644
--- a/package.json
+++ b/package.json
@@ -156,7 +156,6 @@
"@walletconnect/utils": "^2.20.2",
"@xstate/react": "5.0.5",
"ai": "^6.0.39",
- "alchemy-sdk": "^3.4.1",
"axios": "^1.13.5",
"axios-cache-interceptor": "^1.11.1",
"bech32": "^2.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f7776c0e47c..082a07b85af 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -303,9 +303,6 @@ importers:
ai:
specifier: ^6.0.39
version: 6.0.105(zod@3.25.76)
- alchemy-sdk:
- specifier: ^3.4.1
- version: 3.6.5(bufferutil@4.1.0)(utf-8-validate@6.0.6)
axios:
specifier: ^1.13.5
version: 1.13.6(debug@4.4.3)
@@ -4227,9 +4224,6 @@ packages:
'@ethersproject/units@5.7.0':
resolution: {integrity: sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==}
- '@ethersproject/units@5.8.0':
- resolution: {integrity: sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==}
-
'@ethersproject/wallet@5.7.0':
resolution: {integrity: sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==}
@@ -9253,9 +9247,6 @@ packages:
ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
- alchemy-sdk@3.6.5:
- resolution: {integrity: sha512-vikvJvExqPoifnOtnIPoANwS2C46Nv44XsEWJz8kd5hrnZrS320GmhKWGyKSgupd8cvudAWv1+76iSr0pjy8DA==}
-
algo-msgpack-with-bigint@2.1.1:
resolution: {integrity: sha512-F1tGh056XczEaEAqu7s+hlZUDWwOBT70Eq0lfMpBP2YguSQVyxRbprLq5rELXKQOyOaixTWYhMeMQMzP0U5FoQ==}
engines: {node: '>= 10'}
@@ -10590,10 +10581,6 @@ packages:
d3-voronoi@1.1.4:
resolution: {integrity: sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==}
- d@1.0.2:
- resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
- engines: {node: '>=0.12'}
-
damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@@ -11126,26 +11113,15 @@ packages:
es-toolkit@1.44.0:
resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==}
- es5-ext@0.10.64:
- resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==}
- engines: {node: '>=0.10'}
-
es6-error@4.1.1:
resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==}
- es6-iterator@2.0.3:
- resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==}
-
es6-promise@4.2.8:
resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
es6-promisify@5.0.0:
resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==}
- es6-symbol@3.1.4:
- resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==}
- engines: {node: '>=0.12'}
-
esbuild@0.21.5:
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
engines: {node: '>=12'}
@@ -11371,10 +11347,6 @@ packages:
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
- esniff@2.0.1:
- resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==}
- engines: {node: '>=0.10'}
-
espree@10.4.0:
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -11556,9 +11528,6 @@ packages:
ev-emitter@2.1.2:
resolution: {integrity: sha512-jQ5Ql18hdCQ4qS+RCrbLfz1n+Pags27q5TwMKvZyhp5hh2UULUYZUy1keqj6k6SYsdqIYjnmz7xyyEY0V67B8Q==}
- event-emitter@0.3.5:
- resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==}
-
eventemitter2@5.0.1:
resolution: {integrity: sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==}
@@ -11624,9 +11593,6 @@ packages:
exsolve@1.0.8:
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
- ext@1.7.0:
- resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
-
extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
@@ -13684,9 +13650,6 @@ packages:
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
engines: {node: '>= 0.4.0'}
- next-tick@1.1.0:
- resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
-
nice-try@1.0.5:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
@@ -15656,9 +15619,6 @@ packages:
resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
engines: {node: '>=18'}
- sturdy-websocket@0.2.1:
- resolution: {integrity: sha512-NnzSOEKyv4I83qbuKw9ROtJrrT6Z/Xt7I0HiP/e6H6GnpeTDvzwGIGeJ8slai+VwODSHQDooW2CAilJwT9SpRg==}
-
style-to-js@1.1.21:
resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
@@ -16103,9 +16063,6 @@ packages:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
- type@2.7.3:
- resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==}
-
typecast@0.0.1:
resolution: {integrity: sha512-L2f5OCLKsJdCjSyN0d5O6CkNxhiC8EQ2XlXnHpWZVNfF+mj2OTaXhAVnP0/7SY/sxO1DHZpOFMpIuGlFUZEGNA==}
@@ -17041,10 +16998,6 @@ packages:
websocket-stream@5.5.2:
resolution: {integrity: sha512-8z49MKIHbGk3C4HtuHWDtYX8mYej1wWabjthC/RupM9ngeukU4IWoM46dgth1UOS/T4/IqgEdCDJuMe2039OQQ==}
- websocket@1.0.35:
- resolution: {integrity: sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==}
- engines: {node: '>=4.0.0'}
-
whatwg-fetch@2.0.4:
resolution: {integrity: sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==}
@@ -17289,11 +17242,6 @@ packages:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
- yaeti@0.0.6:
- resolution: {integrity: sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==}
- engines: {node: '>=0.10.32'}
- deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
-
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@@ -20376,12 +20324,6 @@ snapshots:
'@ethersproject/constants': 5.7.0
'@ethersproject/logger': 5.7.0
- '@ethersproject/units@5.8.0':
- dependencies:
- '@ethersproject/bignumber': 5.8.0
- '@ethersproject/constants': 5.8.0
- '@ethersproject/logger': 5.8.0
-
'@ethersproject/wallet@5.7.0':
dependencies:
'@ethersproject/abstract-provider': 5.7.0
@@ -34515,30 +34457,6 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
- alchemy-sdk@3.6.5(bufferutil@4.1.0)(utf-8-validate@6.0.6):
- dependencies:
- '@ethersproject/abi': 5.8.0
- '@ethersproject/abstract-provider': 5.8.0
- '@ethersproject/bignumber': 5.8.0
- '@ethersproject/bytes': 5.8.0
- '@ethersproject/contracts': 5.8.0
- '@ethersproject/hash': 5.8.0
- '@ethersproject/networks': 5.8.0
- '@ethersproject/providers': 5.8.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
- '@ethersproject/units': 5.8.0
- '@ethersproject/wallet': 5.8.0
- '@ethersproject/web': 5.8.0
- '@solana/web3.js': 1.98.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
- axios: 1.13.6(debug@4.4.3)
- sturdy-websocket: 0.2.1
- websocket: 1.0.35
- transitivePeerDependencies:
- - bufferutil
- - debug
- - encoding
- - supports-color
- - utf-8-validate
-
algo-msgpack-with-bigint@2.1.1: {}
algosdk@1.24.1:
@@ -36143,11 +36061,6 @@ snapshots:
d3-voronoi@1.1.4: {}
- d@1.0.2:
- dependencies:
- es5-ext: 0.10.64
- type: 2.7.3
-
damerau-levenshtein@1.0.8: {}
dargs@7.0.0: {}
@@ -36768,33 +36681,15 @@ snapshots:
es-toolkit@1.44.0: {}
- es5-ext@0.10.64:
- dependencies:
- es6-iterator: 2.0.3
- es6-symbol: 3.1.4
- esniff: 2.0.1
- next-tick: 1.1.0
-
es6-error@4.1.1:
optional: true
- es6-iterator@2.0.3:
- dependencies:
- d: 1.0.2
- es5-ext: 0.10.64
- es6-symbol: 3.1.4
-
es6-promise@4.2.8: {}
es6-promisify@5.0.0:
dependencies:
es6-promise: 4.2.8
- es6-symbol@3.1.4:
- dependencies:
- d: 1.0.2
- ext: 1.7.0
-
esbuild@0.21.5:
optionalDependencies:
'@esbuild/aix-ppc64': 0.21.5
@@ -37183,13 +37078,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- esniff@2.0.1:
- dependencies:
- d: 1.0.2
- es5-ext: 0.10.64
- event-emitter: 0.3.5
- type: 2.7.3
-
espree@10.4.0:
dependencies:
acorn: 8.16.0
@@ -37610,11 +37498,6 @@ snapshots:
ev-emitter@2.1.2: {}
- event-emitter@0.3.5:
- dependencies:
- d: 1.0.2
- es5-ext: 0.10.64
-
eventemitter2@5.0.1: {}
eventemitter2@6.4.9: {}
@@ -37702,10 +37585,6 @@ snapshots:
exsolve@1.0.8: {}
- ext@1.7.0:
- dependencies:
- type: 2.7.3
-
extend@3.0.2: {}
extension-port-stream@2.1.1:
@@ -40349,8 +40228,6 @@ snapshots:
netmask@2.0.2: {}
- next-tick@1.1.0: {}
-
nice-try@1.0.5: {}
no-case@3.0.4:
@@ -42877,8 +42754,6 @@ snapshots:
dependencies:
'@tokenizer/token': 0.3.0
- sturdy-websocket@0.2.1: {}
-
style-to-js@1.1.21:
dependencies:
style-to-object: 1.0.14
@@ -43359,8 +43234,6 @@ snapshots:
media-typer: 0.3.0
mime-types: 2.1.35
- type@2.7.3: {}
-
typecast@0.0.1: {}
typed-array-buffer@1.0.3:
@@ -44733,17 +44606,6 @@ snapshots:
- bufferutil
- utf-8-validate
- websocket@1.0.35:
- dependencies:
- bufferutil: 4.1.0
- debug: 2.6.9
- es5-ext: 0.10.64
- typedarray-to-buffer: 3.1.5
- utf-8-validate: 5.0.10
- yaeti: 0.0.6
- transitivePeerDependencies:
- - supports-color
-
whatwg-fetch@2.0.4: {}
whatwg-fetch@3.6.20: {}
@@ -45027,8 +44889,6 @@ snapshots:
y18n@5.0.8: {}
- yaeti@0.0.6: {}
-
yallist@3.1.1: {}
yallist@4.0.0: {}
diff --git a/src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx b/src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx
index e9c273f494e..605f35e69e7 100644
--- a/src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx
+++ b/src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx
@@ -1,4 +1,4 @@
-import { List } from '@chakra-ui/react'
+import { Flex, List, Spinner } from '@chakra-ui/react'
import type { Asset } from '@shapeshiftoss/types'
import { memo, useMemo } from 'react'
@@ -9,17 +9,25 @@ import { SearchEmpty } from '@/components/StakingVaults/SearchEmpty'
export type AssetSearchResultsProps = {
results: Asset[]
searchQuery: string
- isSearching: boolean
+ isLoading: boolean
onClickResult: (item: Asset) => void
}
export const AssetSearchResults = memo(
- ({ results, searchQuery, isSearching, onClickResult }: AssetSearchResultsProps) => {
+ ({ results, searchQuery, isLoading, onClickResult }: AssetSearchResultsProps) => {
const noResults = useMemo(() => {
return !results.length
}, [results.length])
- if (isSearching && noResults) {
+ if (isLoading && noResults) {
+ return (
+
+
+
+ )
+ }
+
+ if (!isLoading && noResults && searchQuery) {
return
}
diff --git a/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx b/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx
index 148eeed7369..a7279f4b03e 100644
--- a/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx
+++ b/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx
@@ -9,9 +9,8 @@ import {
useUpdateEffect,
} from '@chakra-ui/react'
import { captureException, setContext } from '@sentry/react'
-import { solanaChainId, toAssetId } from '@shapeshiftoss/caip'
-import type { Asset, KnownChainIds } from '@shapeshiftoss/types'
-import { getAssetNamespaceFromChainId, makeAsset } from '@shapeshiftoss/utils'
+import type { Asset } from '@shapeshiftoss/types'
+import { makeAsset } from '@shapeshiftoss/utils'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import MultiRef from 'react-multi-ref'
import { useNavigate } from 'react-router-dom'
@@ -22,8 +21,7 @@ import { AssetSearchResults } from './AssetSearchResults'
import { GlobalFilter } from '@/components/StakingVaults/GlobalFilter'
import { useGetCustomTokensQuery } from '@/components/TradeAssetSearch/hooks/useGetCustomTokensQuery'
import { useModalRegistration } from '@/context/ModalStackProvider'
-import { ALCHEMY_SDK_SUPPORTED_CHAIN_IDS } from '@/lib/alchemySdkInstance'
-import { isSome } from '@/lib/utils'
+import { CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS } from '@/lib/customTokenImportSupportedChainIds'
import { assets as assetsSlice } from '@/state/slices/assetsSlice/assetsSlice'
import { selectAssets, selectAssetsBySearchQuery } from '@/state/slices/selectors'
import { tradeInput } from '@/state/slices/tradeInputSlice/tradeInputSlice'
@@ -47,9 +45,7 @@ export const GlobalSearchModal = memo(
const navigate = useNavigate()
const dispatch = useAppDispatch()
const searchFilter = useMemo(() => ({ searchQuery, limit: 10 }), [searchQuery])
- const results = useAppSelector(state => selectAssetsBySearchQuery(state, searchFilter))
- const assetResults = results
- const resultsCount = results.length
+ const searchAssets = useAppSelector(state => selectAssetsBySearchQuery(state, searchFilter))
const isMac = useMemo(() => /Mac/.test(navigator.userAgent), [])
const handleClose = useCallback(() => {
setSearchQuery('')
@@ -61,61 +57,35 @@ export const GlobalSearchModal = memo(
onClose: handleClose,
})
- const customTokenSupportedChainIds = useMemo(() => {
- // Solana _is_ supported by Alchemy, but not by the SDK
- return [...ALCHEMY_SDK_SUPPORTED_CHAIN_IDS, solanaChainId]
- }, [])
-
const { data: customTokens, isLoading: isLoadingCustomTokens } = useGetCustomTokensQuery({
contractAddress: searchQuery,
- chainIds: customTokenSupportedChainIds,
+ chainIds: CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS,
})
const customAssets = useMemo(() => {
if (!customTokens?.length) return []
- // Do not move me to a regular useSelector(), as this is reactive on the *whole* assets set and would make this component extremely reactive for no reason
const assetsById = selectAssets(store.getState())
- const assets = customTokens
- .map(metaData => {
- if (!metaData) return null
- const { name, symbol, decimals, logo, chainId, contractAddress } = metaData
-
- if (!name || !symbol || !decimals) return null
-
- const assetId = toAssetId({
- chainId,
- assetNamespace: getAssetNamespaceFromChainId(chainId as KnownChainIds),
- assetReference: contractAddress,
- })
-
- const minimalAsset = {
- assetId,
- name,
- symbol,
- precision: decimals,
- icon: logo ?? undefined,
- }
-
- return makeAsset(assetsById, minimalAsset)
- })
- .filter(isSome)
-
- return assets
+ return customTokens
+ .filter(token => !assetsById[token.assetId])
+ .map(token => makeAsset(assetsById, token))
}, [customTokens])
useEffect(() => {
customAssets.forEach(asset => {
- // Do not move me to a regular useSelector(), as this is reactive on the *whole* assets set and would make this component extremely reactive for no reason
- const assetsById = selectAssets(store.getState())
-
- if (!assetsById[asset.assetId]) {
- dispatch(assetsSlice.actions.upsertAsset(asset))
- }
+ dispatch(assetsSlice.actions.upsertAsset(asset))
})
}, [customAssets, dispatch])
+ const results = useMemo(() => {
+ if (!customAssets.length) return searchAssets
+ const existingIds = new Set(searchAssets.map(a => a.assetId))
+ return searchAssets.concat(customAssets.filter(a => !existingIds.has(a.assetId)))
+ }, [searchAssets, customAssets])
+
+ const resultsCount = results.length
+
useEffect(() => {
if (!searchQuery) setActiveIndex(0)
}, [searchQuery])
@@ -203,11 +173,6 @@ export const GlobalSearchModal = memo(
setActiveIndex(0)
}, [searchQuery])
- const isSearching = useMemo(
- () => searchQuery.length > 0 || isLoadingCustomTokens,
- [searchQuery.length, isLoadingCustomTokens],
- )
-
return (
@@ -230,9 +195,9 @@ export const GlobalSearchModal = memo(
diff --git a/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx b/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx
index 86543a8928e..47ffadd1efe 100644
--- a/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx
+++ b/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx
@@ -1,9 +1,8 @@
import { Box, useMediaQuery } from '@chakra-ui/react'
import type { AssetId, ChainId } from '@shapeshiftoss/caip'
-import { fromAssetId, isNft, solanaChainId, toAssetId } from '@shapeshiftoss/caip'
-import type { Asset, KnownChainIds } from '@shapeshiftoss/types'
-import type { MinimalAsset } from '@shapeshiftoss/utils'
-import { bnOrZero, getAssetNamespaceFromChainId, makeAsset } from '@shapeshiftoss/utils'
+import { fromAssetId, isNft } from '@shapeshiftoss/caip'
+import type { Asset } from '@shapeshiftoss/types'
+import { bnOrZero, makeAsset } from '@shapeshiftoss/utils'
import { orderBy } from 'lodash'
import { useMemo } from 'react'
@@ -12,8 +11,8 @@ import { useGetCustomTokensQuery } from '../hooks/useGetCustomTokensQuery'
import { AssetList } from '@/components/AssetSearch/components/AssetList'
import { Text } from '@/components/Text'
-import { ALCHEMY_SDK_SUPPORTED_CHAIN_IDS } from '@/lib/alchemySdkInstance'
import { searchAssets } from '@/lib/assetSearch'
+import { CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS } from '@/lib/customTokenImportSupportedChainIds'
import { isSome } from '@/lib/utils'
import { isContractAddress } from '@/lib/utils/isContractAddress'
import {
@@ -22,8 +21,8 @@ import {
selectRelatedAssetIdsByAssetIdInclusive,
selectWalletConnectedChainIds,
} from '@/state/slices/common-selectors'
-import { selectPrimaryAssets } from '@/state/slices/selectors'
-import { useAppSelector } from '@/state/store'
+import { selectAssets } from '@/state/slices/selectors'
+import { store, useAppSelector } from '@/state/store'
import { breakpoints } from '@/theme/theme'
export type SearchTermAssetListProps = {
@@ -53,23 +52,17 @@ export const SearchTermAssetList = ({
)
const relatedAssetIdsById = useAppSelector(selectRelatedAssetIdsByAssetIdInclusive)
const portfolioUserCurrencyBalances = useAppSelector(selectPortfolioUserCurrencyBalances)
- const assetsById = useAppSelector(selectPrimaryAssets)
const walletConnectedChainIds = useAppSelector(selectWalletConnectedChainIds)
- const customTokenSupportedChainIds = useMemo(() => {
- // Solana _is_ supported by Alchemy, but not by the SDK
- return [...ALCHEMY_SDK_SUPPORTED_CHAIN_IDS, solanaChainId]
- }, [])
-
const chainIds = useMemo(() => {
if (activeChainId === 'All') {
- return customTokenSupportedChainIds
- } else if (customTokenSupportedChainIds.includes(activeChainId)) {
+ return CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS
+ } else if (CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS.includes(activeChainId)) {
return [activeChainId]
} else {
return []
}
- }, [activeChainId, customTokenSupportedChainIds])
+ }, [activeChainId])
const { data: customTokens, isLoading: isLoadingCustomTokens } = useGetCustomTokensQuery({
contractAddress: searchString,
@@ -114,32 +107,14 @@ export const SearchTermAssetList = ({
}, [assetsForChain])
const customAssets: Asset[] = useMemo(() => {
- return (customTokens ?? [])
- .map(metaData => {
- if (!metaData) return null
- const { name, symbol, decimals, logo } = metaData
- // If we can't get all the information we need to create an Asset, don't allow the custom token
- if (!name || !symbol || !decimals) return null
- const assetId = toAssetId({
- chainId: metaData.chainId,
- assetNamespace: getAssetNamespaceFromChainId(metaData.chainId as KnownChainIds),
- assetReference: metaData.contractAddress,
- })
-
- // Skip if we already have this asset
- if (assetIdMap[assetId] !== undefined) return null
-
- const minimalAsset: MinimalAsset = {
- assetId,
- name,
- symbol,
- precision: decimals,
- icon: logo ?? undefined,
- }
- return makeAsset(assetsById, minimalAsset)
- })
- .filter(isSome)
- }, [assetIdMap, assetsById, customTokens])
+ if (!customTokens?.length) return []
+
+ const assetsById = selectAssets(store.getState())
+
+ return customTokens
+ .filter(token => !assetsById[token.assetId])
+ .map(token => makeAsset(assetsById, token))
+ }, [customTokens])
const searchTermAssets = useMemo(() => {
const filteredAssets: Asset[] = (() => {
diff --git a/src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx b/src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx
index 49e305afff9..dbefed5352b 100644
--- a/src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx
+++ b/src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx
@@ -1,24 +1,24 @@
-import { Metaplex } from '@metaplex-foundation/js'
import type { ChainId } from '@shapeshiftoss/caip'
-import { solanaChainId } from '@shapeshiftoss/caip'
-import { isEvmChainId } from '@shapeshiftoss/chain-adapters'
-import { Connection, PublicKey } from '@solana/web3.js'
+import { toAssetId } from '@shapeshiftoss/caip'
+import type { KnownChainIds } from '@shapeshiftoss/types'
+import type { MinimalAsset } from '@shapeshiftoss/utils'
+import { getAssetNamespaceFromChainId } from '@shapeshiftoss/utils'
import type { UseQueryResult } from '@tanstack/react-query'
import { skipToken, useQueries } from '@tanstack/react-query'
-import type { TokenMetadataResponse } from 'alchemy-sdk'
+import axios, { isAxiosError } from 'axios'
import { useCallback, useMemo } from 'react'
import { isAddress } from 'viem'
import { getConfig } from '@/config'
import { useFeatureFlag } from '@/hooks/useFeatureFlag/useFeatureFlag'
-import { getAlchemyInstanceByChainId } from '@/lib/alchemySdkInstance'
import { isSolanaAddress } from '@/lib/utils/isSolanaAddress'
import { mergeQueryOutputs } from '@/react-queries/helpers'
-type TokenMetadata = TokenMetadataResponse & {
- chainId: ChainId
- contractAddress: string
- price: string
+type TokenMetadataResponse = {
+ name: string
+ symbol: string
+ decimals: number | null
+ logo: string | null
}
type UseGetCustomTokensQueryProps = {
@@ -37,38 +37,43 @@ const getCustomTokenQueryKey = (contractAddress: string, chainId: ChainId): Cust
export const useGetCustomTokensQuery = ({
contractAddress,
chainIds,
-}: UseGetCustomTokensQueryProps): UseQueryResult<(TokenMetadata | undefined)[], Error[]> => {
+}: UseGetCustomTokensQueryProps): UseQueryResult => {
const customTokenImportEnabled = useFeatureFlag('CustomTokenImport')
- const getEvmTokenMetadata = useCallback(
- async (chainId: ChainId) => {
- const alchemy = getAlchemyInstanceByChainId(chainId)
- const tokenMetadataResponse = await alchemy.core.getTokenMetadata(contractAddress)
- return { ...tokenMetadataResponse, chainId, contractAddress, price: '0' }
- },
- [contractAddress],
- )
+ const getCustomToken = useCallback(
+ async (chainId: ChainId): Promise => {
+ try {
+ const { data } = await axios.get(
+ `${getConfig().VITE_PROXY_API_BASE_URL}/api/v1/tokens/metadata`,
+ {
+ params: {
+ chainId,
+ tokenAddress: contractAddress,
+ },
+ },
+ )
- const getSolanaTokenMetadata = useCallback(
- async (mintAddress: string): Promise => {
- const solanaRpcUrl = `${getConfig().VITE_ALCHEMY_SOLANA_BASE_URL}/${
- getConfig().VITE_ALCHEMY_API_KEY
- }`
- const connection = new Connection(solanaRpcUrl)
- const metaplex = Metaplex.make(connection)
- const metadata = await metaplex.nfts().findByMint({ mintAddress: new PublicKey(mintAddress) })
-
- return {
- name: metadata.name,
- symbol: metadata.symbol,
- decimals: metadata.mint.currency.decimals,
- chainId: solanaChainId,
- contractAddress: mintAddress,
- price: '0',
- logo: metadata.json?.image ?? '',
+ if (data.decimals === null) return undefined
+
+ const assetId = toAssetId({
+ chainId,
+ assetNamespace: getAssetNamespaceFromChainId(chainId as KnownChainIds),
+ assetReference: contractAddress,
+ })
+
+ return {
+ assetId,
+ name: data.name,
+ symbol: data.symbol,
+ precision: data.decimals,
+ icon: data.logo ?? undefined,
+ }
+ } catch (err) {
+ if (isAxiosError(err) && (err.response?.status ?? 0) >= 500) throw err
+ return undefined
}
},
- [],
+ [contractAddress],
)
const isValidEvmAddress = useMemo(
@@ -80,26 +85,15 @@ export const useGetCustomTokensQuery = ({
const getQueryFn = useCallback(
(chainId: ChainId) => () => {
- if (isValidSolanaAddress && chainId === solanaChainId) {
- return getSolanaTokenMetadata(contractAddress)
- } else if (isValidEvmAddress && isEvmChainId(chainId)) {
- return getEvmTokenMetadata(chainId)
- } else {
- return skipToken
- }
+ if (!isValidEvmAddress && !isValidSolanaAddress) return skipToken
+ return getCustomToken(chainId)
},
- [
- contractAddress,
- getEvmTokenMetadata,
- getSolanaTokenMetadata,
- isValidEvmAddress,
- isValidSolanaAddress,
- ],
+ [getCustomToken, isValidEvmAddress, isValidSolanaAddress],
)
- const isTokenMetadata = (
- result: TokenMetadata | typeof skipToken | undefined,
- ): result is TokenMetadata => result !== undefined && result !== skipToken
+ const isMinimalAsset = (
+ result: MinimalAsset | typeof skipToken | undefined,
+ ): result is MinimalAsset => result !== undefined && result !== skipToken
const customTokenQueries = useQueries({
queries: chainIds.map(chainId => ({
@@ -108,7 +102,7 @@ export const useGetCustomTokensQuery = ({
enabled: customTokenImportEnabled,
staleTime: Infinity,
})),
- combine: queries => mergeQueryOutputs(queries, results => results.filter(isTokenMetadata)),
+ combine: queries => mergeQueryOutputs(queries, results => results.filter(isMinimalAsset)),
})
return customTokenQueries
diff --git a/src/config.ts b/src/config.ts
index 225db766027..4b991116a7f 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -95,7 +95,6 @@ const validators = {
VITE_NEAR_NODE_URL_FALLBACK_1: url({ default: '' }),
VITE_NEAR_NODE_URL_FALLBACK_2: url({ default: '' }),
VITE_FASTNEAR_API_URL: url(),
- VITE_ALCHEMY_POLYGON_URL: url(),
VITE_KEEPKEY_VERSIONS_URL: url(),
VITE_KEEPKEY_LATEST_RELEASE_URL: url(),
VITE_WALLET_MIGRATION_URL: url(),
@@ -212,9 +211,7 @@ const validators = {
VITE_FEATURE_PORTALS_SWAPPER: bool({ default: false }),
VITE_FEATURE_ONE_INCH: bool({ default: false }),
VITE_SENTRY_DSN_URL: url(),
- VITE_ALCHEMY_API_KEY: str(),
VITE_MORALIS_API_KEY: str(),
- VITE_ALCHEMY_SOLANA_BASE_URL: url(),
VITE_CHATWOOT_TOKEN: str(),
VITE_CHATWOOT_URL: str(),
VITE_FEATURE_CHATWOOT: bool({ default: false }),
@@ -241,6 +238,7 @@ const validators = {
VITE_FEATURE_RUNEPOOL_WITHDRAW: bool({ default: false }),
VITE_FEATURE_MARKETS: bool({ default: false }),
VITE_PORTALS_BASE_URL: url(),
+ VITE_PROXY_API_BASE_URL: url({ default: 'https://api.proxy.shapeshift.com' }),
VITE_ZERION_BASE_URL: url(),
VITE_FEATURE_PHANTOM_WALLET: bool({ default: false }),
VITE_FEATURE_FOX_PAGE_FOX_SECTION: bool({ default: true }),
diff --git a/src/index.tsx b/src/index.tsx
index 566c59eadc7..983145e6e70 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -112,13 +112,7 @@ if (shouldEnableSentry) {
[500, 599], // Only server errors, not client errors
],
- denyUrls: [
- 'alchemy.com',
- 'snapshot.org',
- 'coingecko.com',
- 'coincap.io',
- 'coinmarketcap.com',
- ],
+ denyUrls: ['snapshot.org', 'coingecko.com', 'coincap.io', 'coinmarketcap.com'],
}),
browserApiErrorsIntegration(),
breadcrumbsIntegration(),
diff --git a/src/lib/alchemySdkInstance.ts b/src/lib/alchemySdkInstance.ts
deleted file mode 100644
index 726b94c718f..00000000000
--- a/src/lib/alchemySdkInstance.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import type { ChainId } from '@shapeshiftoss/caip'
-import {
- arbitrumChainId,
- baseChainId,
- ethChainId,
- optimismChainId,
- polygonChainId,
-} from '@shapeshiftoss/caip'
-import { Alchemy, Network } from 'alchemy-sdk'
-
-import { getConfig } from '@/config'
-
-const alchemyInstanceMap: Map = new Map()
-
-export const ALCHEMY_SDK_SUPPORTED_CHAIN_IDS = [
- ethChainId,
- polygonChainId,
- optimismChainId,
- arbitrumChainId,
- baseChainId,
-] as const
-
-export const getAlchemyInstanceByChainId = (chainId: ChainId): Alchemy => {
- // Note, make sure to not unify this guy and `instance` below.
- // This is a set, not an array, calling .set() will not automagically update `maybeInstance` to the new reference
- // This should probably be an Array for dev QoL but cba to change it as part of this eslint PR
- const maybeInstance = alchemyInstanceMap.get(chainId)
- if (maybeInstance) return maybeInstance
-
- const apiKey = (() => {
- switch (chainId) {
- case polygonChainId:
- case ethChainId:
- case optimismChainId:
- case arbitrumChainId:
- case baseChainId:
- return getConfig().VITE_ALCHEMY_API_KEY
- default:
- return undefined
- }
- })()
-
- const network = (() => {
- switch (chainId) {
- case polygonChainId:
- return Network.MATIC_MAINNET
- case ethChainId:
- return Network.ETH_MAINNET
- case optimismChainId:
- return Network.OPT_MAINNET
- case arbitrumChainId:
- return Network.ARB_MAINNET
- case baseChainId:
- return Network.BASE_MAINNET
- default:
- return undefined
- }
- })()
-
- if (!apiKey || !network) throw new Error(`Cannot get Alchemy Instance for chainId: ${chainId}`)
-
- const config = {
- apiKey,
- network,
- }
-
- alchemyInstanceMap.set(chainId, new Alchemy(config))
-
- const instance = alchemyInstanceMap.get(chainId)
-
- if (!instance) throw new Error(`Cannot get Alchemy Instance for chainId: ${chainId}`)
-
- return instance
-}
diff --git a/src/lib/assetSearch/deduplicateAssets.test.ts b/src/lib/assetSearch/deduplicateAssets.test.ts
index 5e39e5e9b83..1e41863d15c 100644
--- a/src/lib/assetSearch/deduplicateAssets.test.ts
+++ b/src/lib/assetSearch/deduplicateAssets.test.ts
@@ -87,10 +87,10 @@ describe('deduplicateAssets', () => {
it('prefers primary AXLUSDC over non-primary when both have exact match', () => {
// Create AXLUSDC assets with their own family and a primary
- const axlusdcFamily = 'eip155:1/erc20:axlusdc-family'
+ const axlusdcFamily = 'eip155:1/erc20:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
const axlusdcPrimary = {
...AXLUSDC_OPTIMISM,
- assetId: 'eip155:1/erc20:axlusdc-primary' as const,
+ assetId: 'eip155:1/erc20:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as const,
relatedAssetKey: axlusdcFamily,
isPrimary: true,
}
@@ -109,10 +109,10 @@ describe('deduplicateAssets', () => {
it('returns primary even when non-primary exact match comes first in array', () => {
// Create AXLUSDC assets with primary coming second in array
- const axlusdcFamily = 'eip155:1/erc20:axlusdc-family'
+ const axlusdcFamily = 'eip155:1/erc20:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
const axlusdcPrimary = {
...AXLUSDC_OPTIMISM,
- assetId: 'eip155:1/erc20:axlusdc-primary' as const,
+ assetId: 'eip155:1/erc20:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as const,
relatedAssetKey: axlusdcFamily,
isPrimary: true,
}
@@ -130,13 +130,13 @@ describe('deduplicateAssets', () => {
it('shows both AXLUSDC and AXLUSDT groups when searching "axlusd"', () => {
// Create separate families for AXLUSDC and AXLUSDT
- const axlusdcFamily = 'eip155:1/erc20:axlusdc-family'
- const axlusdtFamily = 'eip155:1/erc20:axlusdt-family'
+ const axlusdcFamily = 'eip155:1/erc20:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+ const axlusdtFamily = 'eip155:1/erc20:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
const assets = [
{ ...AXLUSDC_OPTIMISM, relatedAssetKey: axlusdcFamily, isPrimary: false },
{
...AXLUSDC_OPTIMISM,
- assetId: 'eip155:1/erc20:axlusdc-primary' as const,
+ assetId: 'eip155:1/erc20:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as const,
relatedAssetKey: axlusdcFamily,
isPrimary: true,
},
@@ -149,7 +149,7 @@ describe('deduplicateAssets', () => {
},
{
...AXLUSDC_ARBITRUM,
- assetId: 'eip155:1/erc20:axlusdt-primary' as const,
+ assetId: 'eip155:1/erc20:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' as const,
symbol: 'AXLUSDT',
name: 'Axelar USDT',
relatedAssetKey: axlusdtFamily,
@@ -207,6 +207,16 @@ describe('deduplicateAssets', () => {
expect(result[0].isPrimary).toBe(true)
})
+ it('shows each chain variant independently when searching by contract address', () => {
+ // AXLUSDC_OPTIMISM and AXLUSDC_ARBITRUM share address 0xeb466342c4d449bc9f53a865d5cb90586f405215
+ const assets = [AXLUSDC_OPTIMISM, AXLUSDC_ARBITRUM]
+
+ const result = deduplicateAssets(assets, '0xeb466342c4d449bc9f53a865d5cb90586f405215')
+
+ expect(result).toHaveLength(2)
+ expect(result.map(a => a.chainId).sort()).toEqual(['eip155:10', 'eip155:42161'].sort())
+ })
+
describe('name search grouping', () => {
it('groups "Axelar Bridged" name search results by family', () => {
// AXLUSDC and AXLUSDT are in different families (USDC and USDT families)
diff --git a/src/lib/assetSearch/deduplicateAssets.ts b/src/lib/assetSearch/deduplicateAssets.ts
index 07c7e9a9efe..57fdf3347fb 100644
--- a/src/lib/assetSearch/deduplicateAssets.ts
+++ b/src/lib/assetSearch/deduplicateAssets.ts
@@ -1,3 +1,6 @@
+import { fromAssetId } from '@shapeshiftoss/caip'
+
+import { isContractAddress } from '../utils/isContractAddress'
import type { SearchableAsset } from './types'
import { isExactMatch } from './utils'
@@ -26,7 +29,13 @@ export const deduplicateAssets = (
: false
for (const asset of assets) {
- const familyKey = asset.relatedAssetKey ?? asset.assetId
+ // When the search is a valid contract address matching this asset's assetReference,
+ // use assetId as the family key so each chain variant is shown independently rather than
+ // collapsed into its family via relatedAssetKey.
+ const matchedByAddress =
+ isContractAddress(searchLower) &&
+ fromAssetId(asset.assetId).assetReference.toLowerCase() === searchLower
+ const familyKey = matchedByAddress ? asset.assetId : asset.relatedAssetKey ?? asset.assetId
const existing = familyToAsset.get(familyKey)
const isExact = hasExactSymbolMatch && isExactMatch(searchLower, asset.symbol)
diff --git a/src/lib/customTokenImportSupportedChainIds.ts b/src/lib/customTokenImportSupportedChainIds.ts
new file mode 100644
index 00000000000..3664c292743
--- /dev/null
+++ b/src/lib/customTokenImportSupportedChainIds.ts
@@ -0,0 +1,18 @@
+import type { ChainId } from '@shapeshiftoss/caip'
+import {
+ arbitrumChainId,
+ baseChainId,
+ ethChainId,
+ optimismChainId,
+ polygonChainId,
+ solanaChainId,
+} from '@shapeshiftoss/caip'
+
+export const CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS: ChainId[] = [
+ ethChainId,
+ polygonChainId,
+ optimismChainId,
+ arbitrumChainId,
+ baseChainId,
+ solanaChainId,
+]
diff --git a/src/state/slices/common-selectors.ts b/src/state/slices/common-selectors.ts
index aecf987d99f..66fca10c4fe 100644
--- a/src/state/slices/common-selectors.ts
+++ b/src/state/slices/common-selectors.ts
@@ -595,11 +595,13 @@ export const selectAssetsBySearchQuery = createCachedSelector(
const sortedAssets = useAllAssets ? allAssets : primaryAssets
- // Filter out spam tokens (low market cap) but keep assets with no market data
- const filteredAssets = sortedAssets.filter(asset => {
- const marketCap = bnOrZero(marketDataUsd[asset.assetId]?.marketCap)
- return marketCap.isZero() || marketCap.gte(MINIMUM_MARKET_CAP_THRESHOLD)
- })
+ // Filter out spam tokens (low market cap) but keep assets with no market data, unless user is searching by contract address
+ const filteredAssets = isContractAddressSearch
+ ? sortedAssets
+ : sortedAssets.filter(asset => {
+ const marketCap = bnOrZero(marketDataUsd[asset.assetId]?.marketCap)
+ return marketCap.isZero() || marketCap.gte(MINIMUM_MARKET_CAP_THRESHOLD)
+ })
const matchedAssets = searchAssets(searchQuery, filteredAssets)
const deduplicated = deduplicateAssets(matchedAssets, searchQuery)
diff --git a/src/utils/sentry/httpclient.ts b/src/utils/sentry/httpclient.ts
index 4dde3b8378a..394a3f42fbf 100644
--- a/src/utils/sentry/httpclient.ts
+++ b/src/utils/sentry/httpclient.ts
@@ -45,7 +45,7 @@ interface HttpClientOptions {
* This array can contain strings, not regular expressions.
* If omitted, no filtering by requests blacklist will be applied.
*
- * Example: ['snapshot.org', 'alchemy']
+ * Example: ['snapshot.org']
*/
denyUrls?: string[]
}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index df73f5b8306..c82708f9c12 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -83,14 +83,11 @@ interface ImportMetaEnv {
readonly VITE_KEEPKEY_VERSIONS_URL: string
readonly VITE_KEEPKEY_LATEST_RELEASE_URL: string
readonly VITE_COWSWAP_BASE_URL: string
- readonly VITE_ALCHEMY_POLYGON_URL: string
readonly VITE_TOKEMAK_STATS_URL: string
readonly VITE_COINCAP_API_KEY: string
readonly VITE_EXCHANGERATEHOST_BASE_URL: string
readonly VITE_EXCHANGERATEHOST_API_KEY: string
- readonly VITE_ALCHEMY_API_KEY: string
readonly VITE_MORALIS_API_KEY: string
- readonly VITE_ALCHEMY_SOLANA_BASE_URL: string
readonly VITE_BOARDROOM_API_BASE_URL: string
readonly VITE_BOARDROOM_APP_BASE_URL: string
readonly VITE_SNAPSHOT_BASE_URL: string
@@ -114,6 +111,7 @@ interface ImportMetaEnv {
readonly VITE_WALLET_CONNECT_WALLET_PROJECT_ID: string
readonly VITE_WALLET_CONNECT_RELAY_URL: string
readonly VITE_PORTALS_BASE_URL: string
+ readonly VITE_PROXY_API_BASE_URL: string
readonly VITE_SNAP_ID: string
readonly VITE_SNAP_VERSION: string
readonly VITE_EXPERIMENTAL_CUSTOM_SEND_NONCE: string