@hot-labs/kit 1.4.0 → 1.4.2

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 (49) hide show
  1. package/build/HotConnector.js +3 -2
  2. package/build/HotConnector.js.map +1 -1
  3. package/build/core/address.d.ts +25 -0
  4. package/build/core/address.js +162 -0
  5. package/build/core/address.js.map +1 -0
  6. package/build/core/chains.d.ts +1 -1
  7. package/build/core/chains.js +1 -1
  8. package/build/core/chains.js.map +1 -1
  9. package/build/core/recipient.d.ts +1 -0
  10. package/build/core/recipient.js +6 -0
  11. package/build/core/recipient.js.map +1 -1
  12. package/build/core/token.d.ts +1 -1
  13. package/build/core/token.js +3 -1
  14. package/build/core/token.js.map +1 -1
  15. package/build/cosmos/connector.d.ts +7 -2
  16. package/build/cosmos/connector.js +42 -11
  17. package/build/cosmos/connector.js.map +1 -1
  18. package/build/near/connector.js +14 -6
  19. package/build/near/connector.js.map +1 -1
  20. package/build/ton/connector.js +1 -1
  21. package/build/ton/connector.js.map +1 -1
  22. package/build/ui/bridge/Bridge.js +8 -9
  23. package/build/ui/bridge/Bridge.js.map +1 -1
  24. package/build/ui/bridge/SelectRecipient.js +4 -3
  25. package/build/ui/bridge/SelectRecipient.js.map +1 -1
  26. package/build/ui/bridge/SelectToken.js +2 -2
  27. package/build/ui/bridge/SelectToken.js.map +1 -1
  28. package/build/ui/connect/WalletPicker.js +29 -26
  29. package/build/ui/connect/WalletPicker.js.map +1 -1
  30. package/build/ui/profile/Payment.js +3 -3
  31. package/build/ui/profile/Payment.js.map +1 -1
  32. package/build/ui/uikit/button.d.ts +3 -1
  33. package/build/ui/uikit/button.js +20 -1
  34. package/build/ui/uikit/button.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/HotConnector.ts +3 -2
  37. package/src/core/address.ts +155 -0
  38. package/src/core/chains.ts +1 -1
  39. package/src/core/recipient.ts +7 -0
  40. package/src/core/token.ts +2 -1
  41. package/src/cosmos/connector.ts +44 -11
  42. package/src/near/connector.ts +12 -5
  43. package/src/ton/connector.ts +1 -1
  44. package/src/ui/bridge/Bridge.tsx +11 -13
  45. package/src/ui/bridge/SelectRecipient.tsx +16 -9
  46. package/src/ui/bridge/SelectToken.tsx +2 -2
  47. package/src/ui/connect/WalletPicker.tsx +45 -35
  48. package/src/ui/profile/Payment.tsx +3 -3
  49. package/src/ui/uikit/button.tsx +22 -2
@@ -1 +1 @@
1
- {"version":3,"file":"button.js","sourceRoot":"","sources":["../../../src/ui/uikit/button.tsx"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,mBAAmB,CAAC;AAEvC,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCxC,CAAC;AAEF,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;CAYlC,CAAC"}
1
+ {"version":3,"file":"button.js","sourceRoot":"","sources":["../../../src/ui/uikit/button.tsx"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAsC5D,CAAC,CAAC,EAAE,EAAE,CACN,CAAC,CAAC,OAAO;IACT,GAAG,CAAA;;;;;;;;;;;;;;;KAeF;CACJ,CAAC;AAEF,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;;;;;;;;CAYlC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-labs/kit",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "HOT Labs Kit is chain agnostic connector with omni payments",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -109,7 +109,6 @@ export class HotConnector {
109
109
  });
110
110
 
111
111
  this.onConnect((payload) => {
112
- console.log("onConnect", payload.wallet.type);
113
112
  payload.wallet.fetchBalances(Network.Omni);
114
113
  this.fetchTokens(payload.wallet);
115
114
  });
@@ -231,7 +230,9 @@ export class HotConnector {
231
230
  omniBalance(token: OmniToken) {
232
231
  const omni = tokens.get(token);
233
232
  const omniToken = this.balance(this.priorityWallet, omni);
234
- let onchainToken = Math.max(0, ...this.walletsTokens.filter((t) => t.token.originalId === omni.originalId).map((t) => Math.max(0, t.token.float(t.balance) - t.token.reserve)));
233
+ const onchainTokens = this.walletsTokens.filter((t) => t.token.type !== WalletType.OMNI && t.token.originalId === omni.originalId);
234
+ const onchainBalances = onchainTokens.map((t) => Math.max(0, t.token.float(t.balance) - t.token.reserve));
235
+ let onchainToken = Math.max(0, ...onchainBalances);
235
236
  onchainToken *= 0.99; // Slippage protection
236
237
 
237
238
  return {
@@ -0,0 +1,155 @@
1
+ import { Address } from "@ton/core";
2
+ import { Address as StellarAddress } from "@stellar/stellar-sdk";
3
+ import { base32, base58, hex } from "@scure/base";
4
+ import { sha256 } from "@noble/hashes/sha2.js";
5
+ import * as ethers from "ethers";
6
+ import { chains, Network, WalletType } from "./chains";
7
+
8
+ export const MinAccountIdLen = 2;
9
+ export const MaxAccountIdLen = 64;
10
+ export const ValidAccountRe = /^(([a-z\d]+[-_])*[a-z\d]+\.)*([a-z\d]+[-_])*[a-z\d]+$/;
11
+ export const NEAR_DOMAINS = [".near", ".sweat", ".usn", ".tg"];
12
+ export const NEAR_ADDRESS_HEX_LENGTH = 64;
13
+ export const EVM_DOMAINS = [".eth", ".cb.id"];
14
+
15
+ export const validateAddress = (address: string) => {
16
+ if (isValidNearAddress(address)) return { chainId: Network.Near, isEvm: false };
17
+ if (isValidSolanaAddress(address)) return { chainId: Network.Solana, isEvm: false };
18
+ if (isValidTronAddress(address)) return { chainId: Network.Tron, isEvm: false };
19
+ if (isValidTonAddress(address)) return { chainId: Network.Ton, isEvm: false };
20
+ if (isValidEvmAddress(address)) return { chainId: 1, isEvm: true };
21
+ if (isValidBtcAddress(address)) return { chainId: Network.Btc, isEvm: false };
22
+ if (isValidStellarAddress(address)) return { chainId: Network.Stellar, isEvm: false };
23
+ return { chainId: -1, isEvm: false };
24
+ };
25
+
26
+ export const isBase58 = (address: string) => {
27
+ try {
28
+ base58.decode(address);
29
+ return true;
30
+ } catch (e) {
31
+ return false;
32
+ }
33
+ };
34
+
35
+ export const isBase32 = (address: string) => {
36
+ try {
37
+ base32.decode(address);
38
+ return true;
39
+ } catch (e) {
40
+ return false;
41
+ }
42
+ };
43
+
44
+ export const isHex = (address: string) => {
45
+ try {
46
+ hex.decode(address);
47
+ return true;
48
+ } catch (e) {
49
+ return false;
50
+ }
51
+ };
52
+
53
+ export const isValidBtcAddress = (address: string) => {
54
+ // Basic validation for Bitcoin addresses (P2PKH, P2SH, Bech32)
55
+ const p2pkhRegex = /^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/;
56
+ const p2shRegex = /^[2mn][a-km-zA-HJ-NP-Z1-9]{25,39}$/;
57
+ const bech32Regex = /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,90}$/;
58
+
59
+ return p2pkhRegex.test(address) || p2shRegex.test(address) || bech32Regex.test(address);
60
+ };
61
+
62
+ export function isValidNearAccountId(accountId: string) {
63
+ return !!accountId && accountId.length >= MinAccountIdLen && accountId.length <= MaxAccountIdLen && accountId.match(ValidAccountRe) != null;
64
+ }
65
+
66
+ export const isValidNearAddress = (address: string, { allowWithoutDomain, allowSubdomain } = { allowWithoutDomain: false, allowSubdomain: true }) => {
67
+ if (!isValidNearAccountId(address)) return false;
68
+
69
+ if (address.length === NEAR_ADDRESS_HEX_LENGTH) return true;
70
+ if (allowWithoutDomain && !address.includes(".")) return true;
71
+
72
+ const endsWithValidDomain = NEAR_DOMAINS.some((t) => address.endsWith(t));
73
+ if (!endsWithValidDomain) return false;
74
+
75
+ const parts = address.split(".");
76
+
77
+ const lastPart = `.${parts[parts.length - 1]}`;
78
+ if (!NEAR_DOMAINS.includes(lastPart)) return false;
79
+
80
+ const otherParts = parts.slice(0, -1);
81
+ const hasOtherDomains = otherParts.some((part) => NEAR_DOMAINS.includes(`.${part}`));
82
+ if (hasOtherDomains) return false;
83
+
84
+ if (allowSubdomain) return true;
85
+ return parts.length <= 2;
86
+ };
87
+
88
+ export const isValidTronAddress = (base58Sting: string) => {
89
+ if (base58Sting.length <= 4) return false;
90
+ let address: Uint8Array;
91
+
92
+ try {
93
+ address = base58.decode(base58Sting);
94
+ } catch (e) {
95
+ return false;
96
+ }
97
+
98
+ if (base58Sting.length <= 4) return false;
99
+
100
+ const len = address.length;
101
+ const offset = len - 4;
102
+ const checkSum = address.slice(offset);
103
+ address = address.slice(0, offset);
104
+ const hash0 = sha256(address);
105
+ const hash1 = sha256(new Uint8Array(hash0));
106
+ const checkSum1 = hash1.slice(0, 4);
107
+
108
+ if (checkSum[0] === checkSum1[0] && checkSum[1] === checkSum1[1] && checkSum[2] === checkSum1[2] && checkSum[3] === checkSum1[3]) {
109
+ return true;
110
+ }
111
+
112
+ return false;
113
+ };
114
+
115
+ export const isValidSolanaAddress = (address: string) => {
116
+ if (address.startsWith("0x")) return false;
117
+ if (ethers.isAddress(address) as boolean) return false;
118
+ if (isBase58(address) && [32, 44].includes(address.length)) return true;
119
+ return !!isValidNearAccountId(address) && address.endsWith(".sol");
120
+ };
121
+
122
+ export const isValidEvmAddress = (address: string) => {
123
+ return EVM_DOMAINS.some((t) => address.endsWith(t)) || ethers.isAddress(address);
124
+ };
125
+
126
+ export const isValidStellarAddress = (address: string) => {
127
+ try {
128
+ new StellarAddress(address);
129
+ return true;
130
+ } catch (e) {
131
+ return false;
132
+ }
133
+ };
134
+
135
+ export const isValidTonAddress = (address: string) => {
136
+ try {
137
+ Address.parse(address);
138
+ return true;
139
+ } catch (e) {
140
+ return false;
141
+ }
142
+ };
143
+
144
+ export const isValidAddress = (chain: number, address: string) => {
145
+ if (chains.get(chain)?.type === WalletType.EVM) return isValidEvmAddress(address);
146
+ if (chains.get(chain)?.type === WalletType.TON) return isValidTonAddress(address);
147
+
148
+ if (chain === Network.Omni) return isValidNearAddress(address, { allowWithoutDomain: true, allowSubdomain: false });
149
+ if (chain === Network.Near) return isValidNearAddress(address);
150
+ if (chain === Network.Solana) return isValidSolanaAddress(address);
151
+ if (chain === Network.Tron) return isValidTronAddress(address);
152
+ if (chain === Network.Btc) return isValidBtcAddress(address);
153
+ if (chain === Network.Stellar) return isValidStellarAddress(address);
154
+ return false;
155
+ };
@@ -77,7 +77,7 @@ export enum Network {
77
77
  Manta = 169,
78
78
  Kava = 2222,
79
79
  ZkSync = 324,
80
- Monad = 10_143,
80
+ Monad = 143,
81
81
  Metis = 1088,
82
82
  Gnosis = 100,
83
83
  Fantom = 250,
@@ -4,6 +4,7 @@ import { hex, base32, base58 } from "@scure/base";
4
4
  import { type OmniWallet } from "./OmniWallet";
5
5
  import { tonApi } from "../ton/utils";
6
6
  import { WalletType } from "./chains";
7
+ import { isValidAddress } from "./address";
7
8
 
8
9
  export class Recipient {
9
10
  constructor(readonly type: WalletType, readonly address: string, readonly omniAddress: string) {}
@@ -13,7 +14,13 @@ export class Recipient {
13
14
  return new Recipient(wallet.type, wallet.address, wallet.omniAddress);
14
15
  }
15
16
 
17
+ static isValidAddress(type: WalletType, address: string) {
18
+ return isValidAddress(type, address);
19
+ }
20
+
16
21
  static async fromAddress(type: WalletType, address: string) {
22
+ if (!isValidAddress(type, address)) throw new Error("Invalid address");
23
+
17
24
  if (type === WalletType.TON) {
18
25
  const data = await tonApi.accounts.getAccountPublicKey(Address.parse(address));
19
26
  return new Recipient(WalletType.TON, address, data.publicKey.toLowerCase());
package/src/core/token.ts CHANGED
@@ -114,8 +114,9 @@ export class Token {
114
114
  return BigInt(formatter.parseAmount(t.toString(), this.decimals));
115
115
  }
116
116
 
117
- readable(t: number | bigint | string, rate = 1) {
117
+ readable(t: number | bigint | string, rate = 1, min = 0) {
118
118
  const n = typeof t === "number" ? t : formatter.formatAmount(t ?? 0, this.decimals);
119
+ if (n * rate < min) return "0";
119
120
  return formatter.amount(n * rate);
120
121
  }
121
122
  }
@@ -4,12 +4,12 @@ import { StargateClient } from "@cosmjs/stargate";
4
4
  import { base64, hex } from "@scure/base";
5
5
  import { runInAction } from "mobx";
6
6
 
7
- import { api } from "../core/api";
8
- import { chains, WalletType } from "../core/chains";
9
7
  import { ConnectorType, OmniConnector, OmniConnectorOption, WC_ICON } from "../core/OmniConnector";
10
- import { HotConnector } from "../HotConnector";
8
+ import { chains, WalletType } from "../core/chains";
11
9
  import { OmniWallet } from "../core/OmniWallet";
10
+ import { api } from "../core/api";
12
11
 
12
+ import type { HotConnector } from "../HotConnector";
13
13
  import { signAndSendTx } from "./helpers";
14
14
  import CosmosWallet from "./wallet";
15
15
 
@@ -118,13 +118,28 @@ export default class CosmosConnector extends OmniConnector<CosmosWallet> {
118
118
  publicKey = Buffer.from(account.pubKey, "base64").toString("hex");
119
119
  address = account.bech32Address || "";
120
120
  } else {
121
- const data = await wc.request({ method: "cosmos_getAccounts", params: { chainId: "gonka-mainnet" } });
122
- if (!Array.isArray(data) || data.length === 0) throw new Error("Account not found");
123
- publicKey = hex.encode(base64.decode(data[0].pubkey));
124
- address = data[0].address;
121
+ const savedAccount = await this.getStorage().catch(() => null);
122
+ if (savedAccount?.address && savedAccount?.publicKey) {
123
+ publicKey = savedAccount.publicKey;
124
+ address = savedAccount.address;
125
+ } else {
126
+ const data = await this.requestWalletConnect({
127
+ deeplink: id ? wallets[id].deeplink : undefined,
128
+ icon: id ? wallets[id].icon : undefined,
129
+ name: id ? wallets[id].name : undefined,
130
+ request: {
131
+ method: "cosmos_getAccounts",
132
+ params: { chainId: "gonka-mainnet" },
133
+ },
134
+ });
135
+
136
+ if (!Array.isArray(data) || data.length === 0) throw new Error("Account not found");
137
+ publicKey = hex.encode(base64.decode(data[0].pubkey));
138
+ address = data[0].address;
139
+ }
125
140
  }
126
141
 
127
- this.setStorage({ type: "walletconnect", id });
142
+ this.setStorage({ type: "walletconnect", id, address, publicKey });
128
143
  const wallet = new CosmosWallet({
129
144
  address: address,
130
145
  publicKeyHex: publicKey,
@@ -150,8 +165,8 @@ export default class CosmosConnector extends OmniConnector<CosmosWallet> {
150
165
  });
151
166
 
152
167
  const protobufTx = TxRaw.encode({
153
- bodyBytes: signDoc.bodyBytes,
154
- authInfoBytes: signDoc.authInfoBytes,
168
+ bodyBytes: Object.keys(signed.bodyBytes).length > 0 ? signed.bodyBytes : signDoc.bodyBytes,
169
+ authInfoBytes: Object.keys(signed.authInfoBytes).length > 0 ? signed.authInfoBytes : signDoc.authInfoBytes,
155
170
  signatures: [Buffer.from(signature.signature, "base64")],
156
171
  }).finish();
157
172
 
@@ -183,6 +198,24 @@ export default class CosmosConnector extends OmniConnector<CosmosWallet> {
183
198
  );
184
199
  }
185
200
 
201
+ async connectGonkaWallet(): Promise<OmniWallet | { qrcode: string; deeplink?: string; task: Promise<OmniWallet> }> {
202
+ const result = await this.connectWalletConnect({
203
+ onConnect: () => this.setupWalletConnect("gonkaWallet"),
204
+ deeplink: wallets["gonkaWallet"].deeplink,
205
+ namespaces: {
206
+ cosmos: {
207
+ methods: ["cosmos_getAccounts", "cosmos_signDirect"],
208
+ events: ["chainChanged", "accountsChanged"],
209
+ chains: this.chains.map((chain) => `cosmos:${chain}`),
210
+ rpcMap: {},
211
+ },
212
+ },
213
+ });
214
+
215
+ window.parent.postMessage({ type: "wc_connect", payload: { wc: result.qrcode } }, "*");
216
+ return result;
217
+ }
218
+
186
219
  async connectKeplr(type: "keplr" | "leap" | "gonkaWallet", extension?: Keplr): Promise<OmniWallet | { qrcode: string; deeplink?: string; task: Promise<OmniWallet> }> {
187
220
  if (!extension) {
188
221
  return await this.connectWalletConnect({
@@ -233,7 +266,7 @@ export default class CosmosConnector extends OmniConnector<CosmosWallet> {
233
266
  }
234
267
 
235
268
  if (id === "gonkaWallet") {
236
- return await this.connectKeplr("gonkaWallet");
269
+ return await this.connectGonkaWallet();
237
270
  }
238
271
 
239
272
  if (id === "keplr") {
@@ -29,12 +29,15 @@ class Connector extends OmniConnector<NearWallet> {
29
29
  this.connector.on("wallet:signIn", async ({ wallet, accounts }) => {
30
30
  if (accounts.length === 0) return;
31
31
  const { accountId, publicKey } = accounts[0];
32
+ if (this.wallets.find((t) => t.address === accountId)) return;
32
33
  this.setWallet(new NearWallet(accountId, publicKey, wallet));
33
34
  });
34
35
 
35
36
  this.connector.getConnectedWallet().then(async ({ wallet }) => {
36
37
  const [account] = await wallet.getAccounts();
37
- if (account) this.setWallet(new NearWallet(account.accountId, account.publicKey, wallet));
38
+ if (!account) return;
39
+ if (this.wallets.find((t) => t.address === account.accountId)) return;
40
+ this.setWallet(new NearWallet(account.accountId, account.publicKey, wallet));
38
41
  });
39
42
 
40
43
  this.connector.whenManifestLoaded.then(() => {
@@ -51,11 +54,15 @@ class Connector extends OmniConnector<NearWallet> {
51
54
  }
52
55
 
53
56
  async connect(id: string) {
54
- const wallet = await this.connector.connect(id);
55
- if (!wallet) throw new Error("Wallet not found");
56
- const [account] = await wallet.getAccounts();
57
+ const instance = await this.connector.connect(id);
58
+ if (!instance) throw new Error("Wallet not found");
59
+
60
+ const [account] = await instance.getAccounts();
57
61
  if (!account) throw new Error("No account found");
58
- return this.setWallet(new NearWallet(account.accountId, account.publicKey, wallet));
62
+
63
+ const wallet = this.wallets.find((t) => t.address === account.accountId);
64
+ if (!wallet) throw new Error("Wallet not found");
65
+ return wallet;
59
66
  }
60
67
 
61
68
  async disconnect() {
@@ -19,7 +19,7 @@ const hotWallet = {
19
19
  app_name: "hot",
20
20
  image: "https://raw.githubusercontent.com/hot-dao/media/main/logo.png",
21
21
  about_url: "https://hot-labs.org/",
22
- universal_url: "https://t.me/herewalletbot?attach=wallet",
22
+ universal_url: "https://app.hot-labs.org/link",
23
23
  bridge: [
24
24
  { type: "sse", url: "https://sse-bridge.hot-labs.org" },
25
25
  { type: "js", key: "hotWallet" },
@@ -1,16 +1,16 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
- import styled from "styled-components";
3
2
  import { observer } from "mobx-react-lite";
3
+ import styled from "styled-components";
4
4
  import uuid4 from "uuid4";
5
5
 
6
- import RefreshIcon from "../icons/refresh";
7
6
  import { ArrowRightIcon } from "../icons/arrow-right";
7
+ import ExchangeIcon from "../icons/exchange";
8
+ import RefreshIcon from "../icons/refresh";
8
9
 
9
10
  import { HotConnector } from "../../HotConnector";
11
+ import { chains, WalletType } from "../../core/chains";
10
12
  import { BridgeReview } from "../../core/exchange";
11
13
  import { OmniWallet } from "../../core/OmniWallet";
12
-
13
- import { chains, WalletType } from "../../core/chains";
14
14
  import { Recipient } from "../../core/recipient";
15
15
  import { formatter } from "../../core/utils";
16
16
  import { tokens } from "../../core/tokens";
@@ -20,10 +20,8 @@ import { ActionButton, Button } from "../uikit/button";
20
20
  import { PLarge, PSmall, PTiny } from "../uikit/text";
21
21
  import { Skeleton } from "../uikit/loader";
22
22
  import { ImageView } from "../uikit/image";
23
- import ExchangeIcon from "../icons/exchange";
24
23
 
25
24
  import Popup from "../Popup";
26
- import { PopupButton } from "../styles";
27
25
  import { openSelectRecipient, openSelectSender, openSelectTokenPopup, openWalletPicker } from "../router";
28
26
  import DepositQR from "../profile/DepositQR";
29
27
  import { TokenIcon } from "./TokenCard";
@@ -261,11 +259,11 @@ export const Bridge = observer(({ hot, widget, setup, onClose, onProcess, onSele
261
259
  <div style={{ width: "100%", height: 400, display: "flex", justifyContent: "center", alignItems: "center", flexDirection: "column" }}>
262
260
  {/* @ts-expect-error: dotlottie-wc is not typed */}
263
261
  <dotlottie-wc key="success" src={animations.success} speed="1" style={{ width: 300, height: 300 }} mode="forward" loop autoplay></dotlottie-wc>
264
- <p style={{ fontSize: 24, marginTop: -32, fontWeight: "bold" }}>Exchange successful</p>
262
+ <p style={{ fontSize: 24, marginTop: -32, fontWeight: "bold" }}>{title} successful</p>
265
263
  </div>
266
- <PopupButton style={{ marginTop: "auto" }} onClick={() => (cancelReview(), onClose())}>
264
+ <ActionButton style={{ marginTop: "auto" }} onClick={() => (cancelReview(), onClose())}>
267
265
  Continue
268
- </PopupButton>
266
+ </ActionButton>
269
267
  </Popup>
270
268
  );
271
269
  }
@@ -276,7 +274,7 @@ export const Bridge = observer(({ hot, widget, setup, onClose, onProcess, onSele
276
274
  <div style={{ width: "100%", height: 400, gap: 8, display: "flex", justifyContent: "center", alignItems: "center", flexDirection: "column" }}>
277
275
  {/* @ts-expect-error: dotlottie-wc is not typed */}
278
276
  <dotlottie-wc key="error" src={animations.failed} speed="1" style={{ width: 300, height: 300 }} mode="forward" loop autoplay></dotlottie-wc>
279
- <p style={{ fontSize: 24, marginTop: -32, fontWeight: "bold" }}>Exchange failed</p>
277
+ <p style={{ fontSize: 24, marginTop: -32, fontWeight: "bold" }}>{title} failed</p>
280
278
  <p style={{ fontSize: 14 }}>{processing.message}</p>
281
279
  </div>
282
280
  <ActionButton onClick={() => (cancelReview(), onClose())}>Continue</ActionButton>
@@ -285,9 +283,9 @@ export const Bridge = observer(({ hot, widget, setup, onClose, onProcess, onSele
285
283
  }
286
284
 
287
285
  const button = () => {
288
- if (sender == null) return <PopupButton disabled>Set sender</PopupButton>;
289
- if (recipient == null) return <PopupButton disabled>Set recipient</PopupButton>;
290
- if (sender !== "qr" && +from.float(hot.balance(sender, from)).toFixed(FIXED) < +amountFrom.toFixed(FIXED)) return <PopupButton disabled>Insufficient balance</PopupButton>;
286
+ if (sender == null) return <ActionButton disabled>Set sender</ActionButton>;
287
+ if (recipient == null) return <ActionButton disabled>Set recipient</ActionButton>;
288
+ if (sender !== "qr" && +from.float(hot.balance(sender, from)).toFixed(FIXED) < +amountFrom.toFixed(FIXED)) return <ActionButton disabled>Insufficient balance</ActionButton>;
291
289
  return (
292
290
  <ActionButton style={{ width: "100%", marginTop: 40 }} disabled={isReviewing || isError != null} onClick={handleConfirm}>
293
291
  {isReviewing ? "Quoting..." : isError != null ? isError : "Confirm"}
@@ -5,7 +5,7 @@ import { useState } from "react";
5
5
  import { ArrowRightIcon } from "../icons/arrow-right";
6
6
 
7
7
  import { Recipient } from "../../core/recipient";
8
- import { WalletType } from "../../core/chains";
8
+ import { chains, WalletType } from "../../core/chains";
9
9
  import { formatter } from "../../core/utils";
10
10
 
11
11
  import { PSmall } from "../uikit/text";
@@ -28,6 +28,8 @@ export const SelectRecipient = observer(({ recipient, hot, type, onSelect, onClo
28
28
  const connectors = hot.connectors.filter((t) => t.walletTypes.includes(type) && t.type !== ConnectorType.SOCIAL);
29
29
  const [customAddress, setCustomAddress] = useState<string>(recipient?.address || "");
30
30
 
31
+ const isError = !Recipient.isValidAddress(type, customAddress) && customAddress.length > 0;
32
+
31
33
  const selectCustom = async () => {
32
34
  const recipient = await Recipient.fromAddress(type, customAddress);
33
35
  onSelect(recipient);
@@ -55,14 +57,17 @@ export const SelectRecipient = observer(({ recipient, hot, type, onSelect, onClo
55
57
 
56
58
  {type !== WalletType.OMNI && (
57
59
  <>
58
- <div style={{ display: "flex", alignItems: "center", gap: 8, width: "100%", margin: "12px 0" }}>
59
- <div style={{ width: "100%", height: 1, background: "rgba(255,255,255,0.1)" }}></div>
60
- <PSmall>OR</PSmall>
61
- <div style={{ width: "100%", height: 1, background: "rgba(255,255,255,0.1)" }}></div>
62
- </div>
60
+ {connectors.length > 0 && (
61
+ <div style={{ display: "flex", alignItems: "center", gap: 8, width: "100%", margin: "12px 0" }}>
62
+ <div style={{ width: "100%", height: 1, background: "rgba(255,255,255,0.1)" }}></div>
63
+ <PSmall>OR</PSmall>
64
+ <div style={{ width: "100%", height: 1, background: "rgba(255,255,255,0.1)" }}></div>
65
+ </div>
66
+ )}
67
+
63
68
  <div style={{ width: "100%" }}>
64
69
  <PSmall style={{ textAlign: "left" }}>Enter recipient address, avoid CEX</PSmall>
65
- <CustomRecipient>
70
+ <CustomRecipient $error={isError}>
66
71
  <input //
67
72
  type="text"
68
73
  placeholder="Enter wallet address"
@@ -73,6 +78,8 @@ export const SelectRecipient = observer(({ recipient, hot, type, onSelect, onClo
73
78
  Select
74
79
  </button>
75
80
  </CustomRecipient>
81
+
82
+ {isError && <PSmall style={{ marginTop: 4, textAlign: "left", color: "#F34747" }}>Invalid {type === WalletType.EVM ? "EVM" : chains.get(type)?.name} address</PSmall>}
76
83
  </div>
77
84
  </>
78
85
  )}
@@ -80,10 +87,10 @@ export const SelectRecipient = observer(({ recipient, hot, type, onSelect, onClo
80
87
  );
81
88
  });
82
89
 
83
- const CustomRecipient = styled.div`
90
+ const CustomRecipient = styled.div<{ $error: boolean }>`
84
91
  display: flex;
85
92
  align-items: center;
86
- border: 1px solid #2d2d2d;
93
+ border: 1px solid ${({ $error }) => ($error ? "#F34747" : "#2d2d2d")};
87
94
  border-radius: 12px;
88
95
  overflow: hidden;
89
96
  margin-top: 8px;
@@ -73,7 +73,7 @@ export const SelectTokenPopup = observer(({ hot, initialChain, onClose, onSelect
73
73
  <SearchInput type="text" placeholder="Search token" onChange={(e) => setSearch(e.target.value)} />
74
74
 
75
75
  {tokens.list
76
- .filter((token) => token.chain === chain)
76
+ .filter((token) => token.chain === chain && token.symbol.toLowerCase().includes(search.toLowerCase()))
77
77
  .sort((a, b) => {
78
78
  const wallet = hot.wallets.find((w) => w.type === a.type)!;
79
79
  const aBalance = a.float(hot.balance(wallet, a)) * a.usd;
@@ -111,7 +111,7 @@ export const SelectTokenPopup = observer(({ hot, initialChain, onClose, onSelect
111
111
  ))}
112
112
 
113
113
  {Object.values(OmniToken)
114
- .filter((token) => !used.has(token))
114
+ .filter((token) => !used.has(token) && hot.omni(token).symbol.toLowerCase().includes(search.toLowerCase()))
115
115
  .map((token) => (
116
116
  <TokenCard key={token} token={hot.omni(token)} onSelect={onSelect} hot={hot} wallet={hot.priorityWallet} />
117
117
  ))}
@@ -2,9 +2,13 @@ import { useState } from "react";
2
2
  import { observer } from "mobx-react-lite";
3
3
 
4
4
  import { ImageView } from "../uikit/image";
5
+ import { ActionButton } from "../uikit/button";
6
+ import { H4, PMedium } from "../uikit/text";
7
+
5
8
  import { OmniWallet } from "../../core/OmniWallet";
6
9
  import { OmniConnector, OmniConnectorOption } from "../../core/OmniConnector";
7
- import { PopupButton, PopupOption, PopupOptionInfo } from "../styles";
10
+ import { PopupOption, PopupOptionInfo } from "../styles";
11
+
8
12
  import { WCPopup } from "./WCPopup";
9
13
  import Popup from "../Popup";
10
14
 
@@ -21,6 +25,31 @@ export const WalletPicker = observer(({ initialConnector, onSelect, onClose }: W
21
25
  const [error, setError] = useState<string | null>(null);
22
26
  const [loading, setLoading] = useState<boolean>(false);
23
27
 
28
+ const connectWallet = async (connector: OmniConnector, wallet: OmniConnectorOption) => {
29
+ try {
30
+ setLoading(true);
31
+ setError(null);
32
+ setWallet(wallet);
33
+
34
+ const instance = await connector.connect(wallet.id);
35
+ if (typeof instance === "object" && "qrcode" in instance) {
36
+ setQrcode({ uri: instance.qrcode, deeplink: instance.deeplink, icon: connector.icon });
37
+ const wallet = await instance.task;
38
+ onSelect?.(wallet);
39
+ onClose();
40
+ return;
41
+ }
42
+
43
+ onSelect?.(instance);
44
+ onClose();
45
+ } catch (e) {
46
+ console.error(e);
47
+ setError(e instanceof Error ? e.message : "Unknown error");
48
+ } finally {
49
+ setLoading(false);
50
+ }
51
+ };
52
+
24
53
  if (qrcode) {
25
54
  return (
26
55
  <WCPopup //
@@ -36,14 +65,21 @@ export const WalletPicker = observer(({ initialConnector, onSelect, onClose }: W
36
65
  if (wallet != null) {
37
66
  return (
38
67
  <Popup onClose={onClose}>
39
- <div style={{ width: "100%", height: 300, display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", gap: 0 }}>
40
- <ImageView style={{ marginTop: "auto" }} src={wallet.icon} alt={wallet.name} size={100} />
68
+ <div style={{ width: "100%", display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", gap: 0 }}>
69
+ <ImageView style={{ marginTop: 32 }} src={wallet.icon} alt={wallet.name} size={100} />
41
70
 
42
- <h3 style={{ fontSize: 32, margin: "12px 0 0", fontWeight: "bold", textAlign: "center" }}>{wallet.name}</h3>
43
- <p style={{ textAlign: "center" }}>{error}</p>
44
- <PopupButton disabled={loading} style={{ marginTop: "auto" }} onClick={() => window.open(wallet.download, "_blank")}>
71
+ <H4 style={{ marginTop: 12, textAlign: "center" }}>{wallet.name}</H4>
72
+ <PMedium style={{ textAlign: "center" }}>{error}</PMedium>
73
+
74
+ <ActionButton disabled={loading} style={{ marginTop: 32 }} onClick={() => window.open(wallet.download, "_blank")}>
45
75
  {loading ? "Connecting..." : "Get wallet"}
46
- </PopupButton>
76
+ </ActionButton>
77
+
78
+ {!!error && !loading && (
79
+ <ActionButton $stroke style={{ marginTop: 8 }} onClick={() => connectWallet(connector!, wallet)}>
80
+ Try again
81
+ </ActionButton>
82
+ )}
47
83
  </div>
48
84
  </Popup>
49
85
  );
@@ -51,35 +87,9 @@ export const WalletPicker = observer(({ initialConnector, onSelect, onClose }: W
51
87
 
52
88
  if (connector != null) {
53
89
  return (
54
- <Popup header={<p>Select wallet</p>} onClose={onClose}>
90
+ <Popup header={<p>Select {connector.name}</p>} onClose={onClose}>
55
91
  {connector.options.map((wallet) => (
56
- <PopupOption
57
- key={wallet.id}
58
- onClick={async () => {
59
- try {
60
- setLoading(true);
61
- setError(null);
62
- setWallet(wallet);
63
-
64
- const instance = await connector.connect(wallet.id);
65
- if (typeof instance === "object" && "qrcode" in instance) {
66
- setQrcode({ uri: instance.qrcode, deeplink: instance.deeplink, icon: connector.icon });
67
- const wallet = await instance.task;
68
- onSelect?.(wallet);
69
- onClose();
70
- return;
71
- }
72
-
73
- onSelect?.(instance);
74
- onClose();
75
- } catch (e) {
76
- console.error(e);
77
- setError(e instanceof Error ? e.message : "Unknown error");
78
- } finally {
79
- setLoading(false);
80
- }
81
- }}
82
- >
92
+ <PopupOption key={wallet.id} onClick={() => connectWallet(connector, wallet)}>
83
93
  <ImageView src={wallet.icon} alt={wallet.name} size={44} />
84
94
  <PopupOptionInfo className="connect-item-info">
85
95
  <p style={{ fontSize: 20, fontWeight: "bold" }}>{wallet.name}</p>