@hot-labs/kit 1.1.0-beta.2 → 1.1.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/build/OmniConnector.d.ts +1 -0
  2. package/build/OmniConnector.js.map +1 -1
  3. package/build/core/Intents.d.ts +18 -0
  4. package/build/core/Intents.js +19 -2
  5. package/build/core/Intents.js.map +1 -1
  6. package/build/core/api.d.ts +16 -1
  7. package/build/core/api.js +14 -4
  8. package/build/core/api.js.map +1 -1
  9. package/build/cosmos/connector.d.ts +2 -2
  10. package/build/cosmos/connector.js +15 -19
  11. package/build/cosmos/connector.js.map +1 -1
  12. package/build/cosmos/wallet.js.map +1 -1
  13. package/build/exchange.js +1 -5
  14. package/build/exchange.js.map +1 -1
  15. package/build/ui/Popup.d.ts +1 -1
  16. package/build/ui/Popup.js +1 -1
  17. package/build/ui/Popup.js.map +1 -1
  18. package/build/ui/payment/Bridge.js +13 -3
  19. package/build/ui/payment/Bridge.js.map +1 -1
  20. package/build/ui/payment/Payment.d.ts +7 -15
  21. package/build/ui/payment/Payment.js +44 -53
  22. package/build/ui/payment/Payment.js.map +1 -1
  23. package/build/ui/payment/Stepper.d.ts +13 -0
  24. package/build/ui/payment/Stepper.js +22 -0
  25. package/build/ui/payment/Stepper.js.map +1 -0
  26. package/build/ui/router.d.ts +6 -3
  27. package/build/ui/router.js +7 -7
  28. package/build/ui/router.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/OmniConnector.ts +1 -0
  31. package/src/core/Intents.ts +20 -2
  32. package/src/core/api.ts +26 -4
  33. package/src/cosmos/connector.ts +20 -24
  34. package/src/cosmos/wallet.ts +0 -1
  35. package/src/exchange.ts +2 -5
  36. package/src/ui/Popup.tsx +7 -4
  37. package/src/ui/payment/Bridge.tsx +15 -3
  38. package/src/ui/payment/Payment.tsx +73 -98
  39. package/src/ui/payment/Stepper.tsx +50 -0
  40. package/src/ui/router.tsx +15 -12
@@ -6,7 +6,7 @@ import { hex } from "@scure/base";
6
6
 
7
7
  import { api } from "../core/api";
8
8
  import { chains, WalletType } from "../core/chains";
9
- import { ConnectorType, OmniConnector, WC_ICON } from "../OmniConnector";
9
+ import { ConnectorType, OmniConnector, OmniConnectorOption, WC_ICON } from "../OmniConnector";
10
10
  import { HotConnector } from "../HotConnector";
11
11
  import { OmniWallet } from "../OmniWallet";
12
12
 
@@ -20,9 +20,9 @@ declare global {
20
20
  }
21
21
  }
22
22
 
23
- const wallets = {
23
+ const wallets: Record<string, OmniConnectorOption> = {
24
24
  keplr: {
25
- name: "Keplr",
25
+ name: "Keplr Wallet",
26
26
  icon: "https://cdn.prod.website-files.com/667dc891bc7b863b5397495b/68a4ca95f93a9ab64dc67ab4_keplr-symbol.svg",
27
27
  download: "https://www.keplr.app/get",
28
28
  deeplink: "keplrwallet://wcV2?",
@@ -30,13 +30,21 @@ const wallets = {
30
30
  id: "keplr",
31
31
  },
32
32
  leap: {
33
- name: "Leap",
33
+ name: "Leap Wallet",
34
34
  icon: "https://framerusercontent.com/images/AbGYvbwnLekBbsdf5g7PI5PpSg.png?scale-down-to=512",
35
35
  download: "https://www.leapwallet.io/download",
36
36
  deeplink: "leapcosmos://wcV2?",
37
37
  type: "extension",
38
38
  id: "leap",
39
39
  },
40
+ gonkaWallet: {
41
+ name: "Gonka Wallet",
42
+ icon: "https://gonka-wallet.startonus.com/images/logo.png",
43
+ download: "https://t.me/gonka_wallet",
44
+ deeplink: "https://gonka-wallet.startonus.com/wc?wc=",
45
+ type: "external",
46
+ id: "gonkaWallet",
47
+ },
40
48
  };
41
49
 
42
50
  export default class CosmosConnector extends OmniConnector<CosmosWallet> {
@@ -50,28 +58,12 @@ export default class CosmosConnector extends OmniConnector<CosmosWallet> {
50
58
  constructor(wibe3: HotConnector) {
51
59
  super(wibe3);
52
60
 
53
- this.options = [
54
- {
55
- name: "Keplr",
56
- download: "https://www.keplr.app/get",
57
- icon: "https://cdn.prod.website-files.com/667dc891bc7b863b5397495b/68a4ca95f93a9ab64dc67ab4_keplr-symbol.svg",
58
- type: "keplr" in window ? "extension" : "external",
59
- id: "keplr",
60
- },
61
- {
62
- name: "leap" in window ? "Leap" : "Leap Mobile",
63
- download: "https://www.leapwallet.io/download",
64
- icon: "https://framerusercontent.com/images/AbGYvbwnLekBbsdf5g7PI5PpSg.png?scale-down-to=512",
65
- type: "leap" in window ? "extension" : "external",
66
- id: "leap",
67
- },
68
- ];
69
-
61
+ this.options = Object.values(wallets);
70
62
  Keplr.getKeplr().then((keplr) => {
71
63
  const option = this.options.find((option) => option.id === "keplr")!;
72
64
  runInAction(() => {
73
65
  option.type = keplr ? "extension" : "external";
74
- option.name = keplr ? "Keplr" : "Keplr Mobile";
66
+ option.name = keplr ? "Keplr Wallet" : "Keplr Mobile";
75
67
  });
76
68
  });
77
69
 
@@ -113,7 +105,7 @@ export default class CosmosConnector extends OmniConnector<CosmosWallet> {
113
105
  return chains.getByType(WalletType.COSMOS).map((t) => t.key);
114
106
  }
115
107
 
116
- async setupWalletConnect(id?: "keplr" | "leap"): Promise<CosmosWallet> {
108
+ async setupWalletConnect(id?: "keplr" | "leap" | "gonkaWallet"): Promise<CosmosWallet> {
117
109
  const wc = await this.wc;
118
110
  if (!wc) throw new Error("WalletConnect not found");
119
111
 
@@ -183,7 +175,7 @@ export default class CosmosConnector extends OmniConnector<CosmosWallet> {
183
175
  );
184
176
  }
185
177
 
186
- async connectKeplr(type: "keplr" | "leap", extension?: Keplr): Promise<OmniWallet | { qrcode: string; deeplink?: string; task: Promise<OmniWallet> }> {
178
+ async connectKeplr(type: "keplr" | "leap" | "gonkaWallet", extension?: Keplr): Promise<OmniWallet | { qrcode: string; deeplink?: string; task: Promise<OmniWallet> }> {
187
179
  if (!extension) {
188
180
  return await this.connectWalletConnect({
189
181
  onConnect: () => this.setupWalletConnect(type),
@@ -232,6 +224,10 @@ export default class CosmosConnector extends OmniConnector<CosmosWallet> {
232
224
  });
233
225
  }
234
226
 
227
+ if (id === "gonkaWallet") {
228
+ return await this.connectKeplr("gonkaWallet");
229
+ }
230
+
235
231
  if (id === "keplr") {
236
232
  const keplr = await Keplr.getKeplr();
237
233
  return await this.connectKeplr("keplr", keplr);
@@ -2,7 +2,6 @@ import { StargateClient } from "@cosmjs/stargate";
2
2
  import { OmniWallet } from "../OmniWallet";
3
3
  import { chains, WalletType } from "../core/chains";
4
4
  import { ReviewFee } from "../core/bridge";
5
- import CosmosConnector from "./connector";
6
5
  import { Commitment } from "../core";
7
6
 
8
7
  interface ProtocolWallet {
package/src/exchange.ts CHANGED
@@ -334,14 +334,11 @@ export class Exchange {
334
334
 
335
335
  const depositAddress = review.qoute.depositAddress!;
336
336
  let hash = "";
337
+
337
338
  if (review.from.chain === Network.Hot) {
338
339
  hash = await this.wibe3
339
340
  .intentsBuilder(sender)
340
- .transfer({
341
- amount: review.amountIn,
342
- token: review.from.address as OmniToken,
343
- recipient: depositAddress,
344
- })
341
+ .transfer({ amount: review.amountIn, token: review.from.address as OmniToken, recipient: depositAddress })
345
342
  .execute();
346
343
  } else {
347
344
  hash = await sender.transfer({
package/src/ui/Popup.tsx CHANGED
@@ -22,7 +22,7 @@ interface PopupProps {
22
22
  widget?: boolean;
23
23
  children: React.ReactNode;
24
24
  header?: React.ReactNode;
25
- onClose: () => void;
25
+ onClose?: () => void;
26
26
  style?: React.CSSProperties;
27
27
  mobileFullscreen?: boolean;
28
28
  }
@@ -57,9 +57,12 @@ const Popup = ({ widget, children, header, onClose, style, mobileFullscreen }: P
57
57
  <ModalContent ref={contentRef} $mobileFullscreen={mobileFullscreen} style={{ opacity: 0, transform: "translateY(20px)", transition: "all 0.2s ease-in-out" }}>
58
58
  {header && (
59
59
  <ModalHeader>
60
- <button onClick={onClose} style={{ position: "absolute", right: 16, top: 16 }}>
61
- <CloseIcon />
62
- </button>
60
+ {onClose != null && (
61
+ <button onClick={onClose} style={{ position: "absolute", right: 16, top: 16 }}>
62
+ <CloseIcon />
63
+ </button>
64
+ )}
65
+
63
66
  {header}
64
67
  </ModalHeader>
65
68
  )}
@@ -21,6 +21,12 @@ import { openSelectRecipient, openSelectTokenPopup, openSelectSender } from "../
21
21
  import { TokenIcon } from "./TokenCard";
22
22
  import DepositQR from "./DepositQR";
23
23
 
24
+ const animations = {
25
+ success: "https://hex.exchange/success.json",
26
+ failed: "https://hex.exchange/error.json",
27
+ loading: "https://hex.exchange/loading.json",
28
+ };
29
+
24
30
  export interface BridgeProps {
25
31
  hot: HotConnector;
26
32
  widget?: boolean;
@@ -56,6 +62,12 @@ export const Bridge = observer(({ hot, widget, setup, onClose, onProcess, onSele
56
62
  const [isError, setIsError] = useState<string | null>(null);
57
63
  const [isReviewing, setIsReviewing] = useState(false);
58
64
 
65
+ useState(() => {
66
+ fetch(animations.loading);
67
+ fetch(animations.success);
68
+ fetch(animations.failed);
69
+ });
70
+
59
71
  const [processing, setProcessing] = useState<{
60
72
  status: "qr" | "processing" | "success" | "error";
61
73
  resolve?: (value: BridgeReview) => void;
@@ -227,7 +239,7 @@ export const Bridge = observer(({ hot, widget, setup, onClose, onProcess, onSele
227
239
  <Popup widget={widget} onClose={onClose} header={<p>{title}</p>} mobileFullscreen={setup?.mobileFullscreen}>
228
240
  <div style={{ width: "100%", height: 400, display: "flex", justifyContent: "center", alignItems: "center", flexDirection: "column" }}>
229
241
  {/* @ts-expect-error: dotlottie-wc is not typed */}
230
- <dotlottie-wc key="loading" src="/loading.json" speed="1" style={{ width: 300, height: 300 }} mode="forward" loop autoplay></dotlottie-wc>
242
+ <dotlottie-wc key="loading" src={animations.loading} speed="1" style={{ width: 300, height: 300 }} mode="forward" loop autoplay></dotlottie-wc>
231
243
  <p style={{ marginTop: -32, fontSize: 16 }}>{processing.message}</p>
232
244
  </div>
233
245
  </Popup>
@@ -239,7 +251,7 @@ export const Bridge = observer(({ hot, widget, setup, onClose, onProcess, onSele
239
251
  <Popup widget={widget} onClose={onClose} header={<p>{title}</p>} mobileFullscreen={setup?.mobileFullscreen}>
240
252
  <div style={{ width: "100%", height: 400, display: "flex", justifyContent: "center", alignItems: "center", flexDirection: "column" }}>
241
253
  {/* @ts-expect-error: dotlottie-wc is not typed */}
242
- <dotlottie-wc key="success" src="/success.json" speed="1" style={{ width: 300, height: 300 }} mode="forward" loop autoplay></dotlottie-wc>
254
+ <dotlottie-wc key="success" src={animations.success} speed="1" style={{ width: 300, height: 300 }} mode="forward" loop autoplay></dotlottie-wc>
243
255
  <p style={{ fontSize: 24, marginTop: -32, fontWeight: "bold" }}>Exchange successful</p>
244
256
  </div>
245
257
  <PopupButton style={{ marginTop: "auto" }} onClick={() => (cancelReview(), onClose())}>
@@ -254,7 +266,7 @@ export const Bridge = observer(({ hot, widget, setup, onClose, onProcess, onSele
254
266
  <Popup widget={widget} onClose={onClose} header={<p>{title}</p>} mobileFullscreen={setup?.mobileFullscreen}>
255
267
  <div style={{ width: "100%", height: 400, gap: 8, display: "flex", justifyContent: "center", alignItems: "center", flexDirection: "column" }}>
256
268
  {/* @ts-expect-error: dotlottie-wc is not typed */}
257
- <dotlottie-wc key="error" src="/error.json" speed="1" style={{ width: 300, height: 300 }} mode="forward" loop autoplay></dotlottie-wc>
269
+ <dotlottie-wc key="error" src={animations.failed} speed="1" style={{ width: 300, height: 300 }} mode="forward" loop autoplay></dotlottie-wc>
258
270
  <p style={{ fontSize: 24, marginTop: -32, fontWeight: "bold" }}>Exchange failed</p>
259
271
  <p style={{ fontSize: 14 }}>{processing.message}</p>
260
272
  </div>
@@ -1,6 +1,8 @@
1
1
  import { observer } from "mobx-react-lite";
2
2
  import { useEffect, useState } from "react";
3
3
 
4
+ import { WalletIcon } from "../icons/wallet";
5
+ import { PopupButton, PopupOption, PopupOptionInfo } from "../styles";
4
6
  import { Commitment, formatter, Intents } from "../../core";
5
7
  import { Recipient } from "../../core/recipient";
6
8
  import { Network } from "../../core/chains";
@@ -15,75 +17,26 @@ import { HotConnector } from "../../HotConnector";
15
17
  import Popup from "../Popup";
16
18
 
17
19
  import { TokenCard, TokenIcon } from "./TokenCard";
18
- import { PopupButton, PopupOption, PopupOptionInfo } from "../styles";
19
- import { WalletIcon } from "../icons/wallet";
20
+ import { HorizontalStepper } from "./Stepper";
20
21
  import { Loader } from "./Profile";
21
22
 
22
23
  interface PaymentProps {
23
24
  intents: Intents;
24
25
  connector: HotConnector;
25
- onClose: () => void;
26
- onConfirm: (task: Promise<string>) => void;
26
+ payload?: Record<string, any>;
27
+ onReject: (message: string) => void;
28
+ onSuccess: (task: { paymentId: string; tx: string }) => void;
27
29
  }
28
30
 
29
- import React from "react";
30
-
31
31
  const animations = {
32
32
  success: "https://hex.exchange/success.json",
33
33
  failed: "https://hex.exchange/error.json",
34
34
  loading: "https://hex.exchange/loading.json",
35
35
  };
36
36
 
37
- interface Step {
38
- label: string;
39
- completed?: boolean;
40
- active?: boolean;
41
- }
37
+ const PAY_SLIPPAGE = 0.002;
42
38
 
43
- interface StepperProps {
44
- steps: Step[];
45
- currentStep: number;
46
- style?: React.CSSProperties;
47
- }
48
-
49
- export const HorizontalStepper: React.FC<StepperProps> = ({ steps, currentStep, style }) => {
50
- return (
51
- <div style={{ padding: "0 32px 32px", display: "flex", alignItems: "center", width: "100%", margin: "16px 0", ...style }}>
52
- {steps.map((step, idx) => {
53
- const isCompleted = idx < currentStep;
54
- const isActive = idx === currentStep;
55
-
56
- return (
57
- <React.Fragment key={idx}>
58
- <div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
59
- <div
60
- style={{
61
- width: 16,
62
- height: 16,
63
- position: "relative",
64
- borderRadius: "50%",
65
- border: isActive || isCompleted ? "2px solid #ffffff" : "2px solid #a0a0a0",
66
- background: isCompleted ? "#ffffff" : "#333",
67
- display: "flex",
68
- alignItems: "center",
69
- justifyContent: "center",
70
- transition: "all 0.2s",
71
- zIndex: 1,
72
- }}
73
- >
74
- <p style={{ fontSize: 16, color: "#fff", opacity: isActive ? 1 : 0.5, position: "absolute", top: 24, width: 100 }}>{step.label}</p>
75
- </div>
76
- </div>
77
-
78
- {idx < steps.length - 1 && <div style={{ transition: "background 0.2s", flex: 1, height: 2, background: idx < currentStep ? "#ffffff" : "#333", margin: "0 6px", borderRadius: 24, minWidth: 24 }} />}
79
- </React.Fragment>
80
- );
81
- })}
82
- </div>
83
- );
84
- };
85
-
86
- export const Payment = observer(({ connector, intents, onClose, onConfirm }: PaymentProps) => {
39
+ export const Payment = observer(({ connector, intents, payload, onReject, onSuccess }: PaymentProps) => {
87
40
  useState(() => {
88
41
  fetch(animations.loading);
89
42
  fetch(animations.success);
@@ -99,8 +52,8 @@ export const Payment = observer(({ connector, intents, onClose, onConfirm }: Pay
99
52
  token?: Token;
100
53
  wallet?: OmniWallet;
101
54
  commitment?: Commitment;
102
- review?: BridgeReview;
103
-
55
+ review?: BridgeReview | "direct";
56
+ data?: { paymentId: string; tx: string };
104
57
  step?: "selectToken" | "sign" | "transfer" | "success" | "error" | "loading";
105
58
  success?: boolean;
106
59
  loading?: boolean;
@@ -114,25 +67,36 @@ export const Payment = observer(({ connector, intents, onClose, onConfirm }: Pay
114
67
  const selectToken = async (from: Token, wallet?: OmniWallet) => {
115
68
  if (!wallet) return;
116
69
 
117
- setFlow({ token: from, wallet, review: undefined, step: "sign" });
118
- const review = await connector.exchange.reviewSwap({
119
- recipient: Recipient.fromWallet(wallet)!,
120
- amount: needAmount,
121
- sender: wallet,
122
- refund: wallet,
123
- slippage: 0.005,
124
- type: "exactOut",
125
- to: need,
126
- from,
127
- });
128
-
129
- setFlow({ token: from, wallet, review, step: "sign" });
70
+ // Set signer as payer wallet if not set another
71
+ if (!intents.signer) intents.attachWallet(wallet);
72
+
73
+ if (from.id === need.id) {
74
+ return setFlow({ token: from, wallet, review: "direct", step: "sign" });
75
+ }
76
+
77
+ try {
78
+ setFlow({ token: from, wallet, review: undefined, step: "sign" });
79
+ const review = await connector.exchange.reviewSwap({
80
+ recipient: Recipient.fromWallet(intents.signer)!,
81
+ amount: needAmount + (needAmount * BigInt(Math.floor(PAY_SLIPPAGE * 1000))) / BigInt(1000),
82
+ slippage: PAY_SLIPPAGE,
83
+ sender: wallet,
84
+ refund: wallet,
85
+ type: "exactOut",
86
+ to: need,
87
+ from,
88
+ });
89
+
90
+ setFlow({ token: from, wallet, review, step: "sign" });
91
+ } catch {
92
+ setFlow({ token: from, wallet, error: true, step: "sign" });
93
+ }
130
94
  };
131
95
 
132
96
  const signStep = async () => {
133
97
  try {
134
98
  setFlow((t) => (t ? { ...t, step: "sign", loading: true } : null));
135
- const commitment = await intents.attachWallet(flow!.wallet!).sign();
99
+ const commitment = await intents.sign();
136
100
  setFlow((t) => (t ? { ...t, step: "transfer", commitment, loading: false } : null));
137
101
  } catch (error) {
138
102
  console.error(error);
@@ -146,17 +110,17 @@ export const Payment = observer(({ connector, intents, onClose, onConfirm }: Pay
146
110
  const commitment = flow?.commitment;
147
111
  if (!commitment) throw new Error("Commitment not found");
148
112
  if (!flow?.review) throw new Error("Review not found");
149
-
150
113
  setFlow((t) => (t ? { ...t, step: "loading" } : null));
151
- const result = await connector.exchange.makeSwap(flow.review, { log: () => {} });
152
114
 
153
- if (typeof result.review.qoute === "object") {
154
- await api.pendingPayment(commitment, result.review.qoute.depositAddress!);
155
- } else {
156
- await Intents.publish([commitment]);
115
+ // make swap if need
116
+ let depositAddress: string | undefined;
117
+ if (flow.review != "direct") {
118
+ const result = await connector.exchange.makeSwap(flow.review, { log: () => {} });
119
+ depositAddress = typeof result.review?.qoute === "object" ? result.review?.qoute?.depositAddress : undefined;
157
120
  }
158
121
 
159
- setFlow((t) => (t ? { ...t, step: "success", loading: false, success: true } : null));
122
+ const data = await api.yieldIntentCall({ depositAddress, commitment, payload });
123
+ setFlow((t) => (t ? { ...t, step: "success", loading: false, success: true, data } : null));
160
124
  } catch (error) {
161
125
  console.error(error);
162
126
  setFlow((t) => (t ? { ...t, step: "error", loading: false, error } : null));
@@ -166,13 +130,13 @@ export const Payment = observer(({ connector, intents, onClose, onConfirm }: Pay
166
130
 
167
131
  if (flow?.step === "success") {
168
132
  return (
169
- <Popup onClose={onClose} header={<p>{title}</p>}>
133
+ <Popup header={<p>{title}</p>}>
170
134
  <div style={{ width: "100%", height: 400, display: "flex", justifyContent: "center", alignItems: "center", flexDirection: "column" }}>
171
135
  {/* @ts-expect-error: dotlottie-wc is not typed */}
172
136
  <dotlottie-wc key="success" src={animations.success} speed="1" style={{ width: 300, height: 300 }} mode="forward" loop autoplay></dotlottie-wc>
173
137
  <p style={{ fontSize: 24, marginTop: -32, fontWeight: "bold" }}>Payment successful</p>
174
138
  </div>
175
- <PopupButton style={{ marginTop: "auto" }} onClick={() => onClose()}>
139
+ <PopupButton style={{ marginTop: "auto" }} onClick={() => onSuccess(flow.data!)}>
176
140
  Continue
177
141
  </PopupButton>
178
142
  </Popup>
@@ -181,7 +145,7 @@ export const Payment = observer(({ connector, intents, onClose, onConfirm }: Pay
181
145
 
182
146
  if (flow?.step === "loading") {
183
147
  return (
184
- <Popup onClose={onClose} header={<p>{title}</p>}>
148
+ <Popup header={<p>{title}</p>}>
185
149
  <div style={{ width: "100%", height: 400, display: "flex", justifyContent: "center", alignItems: "center", flexDirection: "column" }}>
186
150
  {/* @ts-expect-error: dotlottie-wc is not typed */}
187
151
  <dotlottie-wc key="loading" src={animations.loading} speed="1" style={{ marginTop: -64, width: 300, height: 300 }} mode="forward" loop autoplay></dotlottie-wc>
@@ -193,14 +157,14 @@ export const Payment = observer(({ connector, intents, onClose, onConfirm }: Pay
193
157
 
194
158
  if (flow?.step === "error") {
195
159
  return (
196
- <Popup onClose={onClose} header={<p>{title}</p>}>
160
+ <Popup header={<p>{title}</p>}>
197
161
  <div style={{ width: "100%", height: 400, gap: 8, display: "flex", justifyContent: "center", alignItems: "center", flexDirection: "column" }}>
198
162
  {/* @ts-expect-error: dotlottie-wc is not typed */}
199
163
  <dotlottie-wc key="error" src={animations.failed} speed="1" style={{ width: 300, height: 300 }} mode="forward" loop autoplay></dotlottie-wc>
200
164
  <p style={{ fontSize: 24, marginTop: -32, fontWeight: "bold" }}>Payment failed</p>
201
- <p style={{ fontSize: 14 }}>{flow.error?.toString?.() ?? "Unknown error"}</p>
165
+ <p style={{ fontSize: 14, width: "80%", textAlign: "center", overflowY: "auto", lineBreak: "anywhere" }}>{flow.error?.toString?.() ?? "Unknown error"}</p>
202
166
  </div>
203
- <PopupButton onClick={() => onClose()}>Close</PopupButton>
167
+ <PopupButton onClick={() => onReject(flow.error?.toString?.() ?? "Unknown error")}>Close</PopupButton>
204
168
  </Popup>
205
169
  );
206
170
  }
@@ -209,7 +173,7 @@ export const Payment = observer(({ connector, intents, onClose, onConfirm }: Pay
209
173
  if (!flow.token) return null;
210
174
  if (!flow.wallet) return null;
211
175
  return (
212
- <Popup onClose={onClose} header={<p>{title}</p>}>
176
+ <Popup onClose={() => onReject("closed")} header={<p>{title}</p>}>
213
177
  <HorizontalStepper steps={[{ label: "Select" }, { label: "Review" }, { label: "Confirm" }]} currentStep={2} />
214
178
 
215
179
  <PopupOption style={{ marginTop: 8 }}>
@@ -222,8 +186,8 @@ export const Payment = observer(({ connector, intents, onClose, onConfirm }: Pay
222
186
 
223
187
  {flow.review ? (
224
188
  <div style={{ paddingRight: 4, marginLeft: "auto", alignItems: "flex-end" }}>
225
- <p style={{ textAlign: "right", fontSize: 20 }}>{flow.token.readable(flow.review?.amountIn ?? 0)}</p>
226
- <p style={{ textAlign: "right", fontSize: 14, color: "#c6c6c6" }}>${flow.token.readable(flow.review?.amountIn ?? 0n, flow.token.usd)}</p>
189
+ <p style={{ textAlign: "right", fontSize: 20 }}>{flow.token.readable(flow.review === "direct" ? needAmount : flow.review?.amountIn ?? 0)}</p>
190
+ <p style={{ textAlign: "right", fontSize: 14, color: "#c6c6c6" }}>${flow.token.readable(flow.review === "direct" ? needAmount : flow.review?.amountIn ?? 0n, flow.token.usd)}</p>
227
191
  </div>
228
192
  ) : (
229
193
  <div style={{ paddingRight: 4, marginLeft: "auto", alignItems: "flex-end" }}>
@@ -243,7 +207,7 @@ export const Payment = observer(({ connector, intents, onClose, onConfirm }: Pay
243
207
  if (!flow.token) return null;
244
208
  if (!flow.wallet) return null;
245
209
  return (
246
- <Popup onClose={onClose} header={<p>{title}</p>}>
210
+ <Popup onClose={() => onReject("closed")} header={<p>{title}</p>}>
247
211
  <HorizontalStepper steps={[{ label: "Select" }, { label: "Review" }, { label: "Confirm" }]} currentStep={1} />
248
212
 
249
213
  <PopupOption style={{ marginTop: 8 }}>
@@ -256,40 +220,51 @@ export const Payment = observer(({ connector, intents, onClose, onConfirm }: Pay
256
220
 
257
221
  {flow.review ? (
258
222
  <div style={{ paddingRight: 4, marginLeft: "auto", alignItems: "flex-end" }}>
259
- <p style={{ textAlign: "right", fontSize: 20 }}>{flow.token.readable(flow.review?.amountIn ?? 0)}</p>
260
- <p style={{ textAlign: "right", fontSize: 14, color: "#c6c6c6" }}>${flow.token.readable(flow.review?.amountIn ?? 0n, flow.token.usd)}</p>
223
+ <p style={{ textAlign: "right", fontSize: 20 }}>{flow.token.readable(flow.review === "direct" ? needAmount : flow.review?.amountIn ?? 0)}</p>
224
+ <p style={{ textAlign: "right", fontSize: 14, color: "#c6c6c6" }}>${flow.token.readable(flow.review === "direct" ? needAmount : flow.review?.amountIn ?? 0n, flow.token.usd)}</p>
261
225
  </div>
262
226
  ) : (
263
227
  <div style={{ paddingRight: 4, marginLeft: "auto", alignItems: "flex-end" }}>
264
- <Loader />
228
+ {flow.error ? (
229
+ <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Failed" style={{ display: "block", margin: "0 auto" }}>
230
+ <circle cx="14" cy="14" r="13" stroke="#E74C3C" strokeWidth="2" />
231
+ <path d="M9 9l10 10M19 9l-10 10" stroke="#E74C3C" strokeWidth="2.5" strokeLinecap="round" />
232
+ </svg>
233
+ ) : (
234
+ <Loader />
235
+ )}
265
236
  </div>
266
237
  )}
267
238
  </PopupOption>
268
239
 
269
- <PopupButton style={{ marginTop: 24 }} disabled={!flow?.review} onClick={signStep}>
270
- {flow?.loading ? "Signing..." : flow?.review ? "Sign review" : "Quoting..."}
271
- </PopupButton>
240
+ {flow.error ? (
241
+ <PopupButton style={{ marginTop: 24 }} onClick={() => setFlow(null)}>
242
+ Select another token
243
+ </PopupButton>
244
+ ) : (
245
+ <PopupButton style={{ marginTop: 24 }} disabled={!flow?.review} onClick={signStep}>
246
+ {flow?.loading ? "Signing..." : flow?.review ? "Sign review" : "Quoting..."}
247
+ </PopupButton>
248
+ )}
272
249
  </Popup>
273
250
  );
274
251
  }
275
252
 
276
253
  return (
277
- <Popup onClose={onClose} header={<p>{title}</p>}>
254
+ <Popup onClose={() => onReject("closed")} header={<p>{title}</p>}>
278
255
  <HorizontalStepper steps={[{ label: "Select" }, { label: "Review" }, { label: "Confirm" }]} currentStep={0} />
279
256
 
280
257
  {connector.walletsTokens.map(({ token, wallet, balance }) => {
281
- if (token.id === need.id) return null;
282
258
  const availableBalance = token.float(balance) - token.reserve;
283
259
 
284
260
  if (need.originalChain === Network.Gonka || need.originalChain === Network.Juno) {
285
261
  if (token.id === need.id) return null;
286
262
  if (token.originalAddress !== need.originalAddress) return null;
287
-
288
263
  if (availableBalance < need.float(needAmount)) return null;
289
264
  return <TokenCard key={token.id} token={token} onSelect={selectToken} hot={connector} wallet={wallet} />;
290
265
  }
291
266
 
292
- if (availableBalance * token.usd <= need.usd * need.float(needAmount)) return null;
267
+ if (availableBalance * token.usd <= need.usd * need.float(needAmount) * (1 + PAY_SLIPPAGE)) return null;
293
268
  return <TokenCard key={token.id} token={token} onSelect={selectToken} hot={connector} wallet={wallet} />;
294
269
  })}
295
270
 
@@ -0,0 +1,50 @@
1
+ import React from "react";
2
+
3
+ interface Step {
4
+ label: string;
5
+ completed?: boolean;
6
+ active?: boolean;
7
+ }
8
+
9
+ interface StepperProps {
10
+ steps: Step[];
11
+ currentStep: number;
12
+ style?: React.CSSProperties;
13
+ }
14
+
15
+ export const HorizontalStepper: React.FC<StepperProps> = ({ steps, currentStep, style }) => {
16
+ return (
17
+ <div style={{ padding: "0 32px 32px", display: "flex", alignItems: "center", width: "100%", margin: "16px 0", ...style }}>
18
+ {steps.map((step, idx) => {
19
+ const isCompleted = idx < currentStep;
20
+ const isActive = idx === currentStep;
21
+
22
+ return (
23
+ <React.Fragment key={idx}>
24
+ <div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
25
+ <div
26
+ style={{
27
+ width: 16,
28
+ height: 16,
29
+ position: "relative",
30
+ borderRadius: "50%",
31
+ border: isActive || isCompleted ? "2px solid #ffffff" : "2px solid #a0a0a0",
32
+ background: isCompleted ? "#ffffff" : "#333",
33
+ display: "flex",
34
+ alignItems: "center",
35
+ justifyContent: "center",
36
+ transition: "all 0.2s",
37
+ zIndex: 1,
38
+ }}
39
+ >
40
+ <p style={{ fontSize: 16, color: "#fff", opacity: isActive ? 1 : 0.5, position: "absolute", top: 24, width: 100 }}>{step.label}</p>
41
+ </div>
42
+ </div>
43
+
44
+ {idx < steps.length - 1 && <div style={{ transition: "background 0.2s", flex: 1, height: 2, background: idx < currentStep ? "#ffffff" : "#333", margin: "0 6px", borderRadius: 24, minWidth: 24 }} />}
45
+ </React.Fragment>
46
+ );
47
+ })}
48
+ </div>
49
+ );
50
+ };
package/src/ui/router.tsx CHANGED
@@ -1,33 +1,36 @@
1
1
  import { HotConnector } from "../HotConnector";
2
2
  import { OmniConnector } from "../OmniConnector";
3
- import { BridgeReview } from "../exchange";
4
- import { Token } from "../core/token";
5
3
  import { OmniWallet } from "../OmniWallet";
4
+
5
+ import { BridgeReview } from "../exchange";
6
6
  import { WalletType } from "../core/chains";
7
7
  import { Recipient } from "../core/recipient";
8
8
  import { Intents } from "../core/Intents";
9
+ import { Token } from "../core/token";
9
10
 
10
11
  import { present } from "./Popup";
12
+ import { SelectTokenPopup } from "./payment/SelectToken";
13
+ import { SelectRecipient } from "./payment/SelectRecipient";
14
+ import { SelectSender } from "./payment/SelectSender";
15
+ import { BridgeProps } from "./payment/Bridge";
11
16
  import { Payment } from "./payment/Payment";
12
- import { LogoutPopup } from "./connect/LogoutPopup";
13
- import { Bridge } from "./payment/Bridge";
14
17
  import { Profile } from "./payment/Profile";
15
- import { SelectTokenPopup } from "./payment/SelectToken";
18
+ import { Bridge } from "./payment/Bridge";
19
+
20
+ import { LogoutPopup } from "./connect/LogoutPopup";
16
21
  import { WalletPicker } from "./connect/WalletPicker";
17
- import { BridgeProps } from "./payment/Bridge";
18
22
  import { Connector } from "./connect/ConnectWallet";
19
- import { SelectSender } from "./payment/SelectSender";
20
- import { SelectRecipient } from "./payment/SelectRecipient";
21
23
  import { WCRequest } from "./connect/WCRequest";
22
24
 
23
- export const openPayment = (connector: HotConnector, intents: Intents) => {
24
- return new Promise<Promise<string>>((resolve, reject) => {
25
+ export const openPayment = (connector: HotConnector, intents: Intents, payload?: Record<string, any>) => {
26
+ return new Promise<{ paymentId: string; tx: string }>((resolve, reject) => {
25
27
  present((close) => (
26
28
  <Payment //
27
- onClose={() => (close(), reject(new Error("User rejected")))}
28
- onConfirm={resolve}
29
+ onReject={() => (close(), reject(new Error("User rejected")))}
30
+ onSuccess={(task) => (close(), resolve(task))}
29
31
  connector={connector}
30
32
  intents={intents}
33
+ payload={payload}
31
34
  />
32
35
  ));
33
36
  });