@hot-labs/kit 1.1.0-beta.3 → 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.
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
  )}
@@ -1,6 +1,8 @@
1
1
  import { observer } from "mobx-react-lite";
2
- import React, { useEffect, useState } from "react";
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,15 +17,15 @@ 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
31
  const animations = {
@@ -32,56 +34,9 @@ const animations = {
32
34
  loading: "https://hex.exchange/loading.json",
33
35
  };
34
36
 
35
- interface Step {
36
- label: string;
37
- completed?: boolean;
38
- active?: boolean;
39
- }
40
-
41
- interface StepperProps {
42
- steps: Step[];
43
- currentStep: number;
44
- style?: React.CSSProperties;
45
- }
46
-
47
- export const HorizontalStepper: React.FC<StepperProps> = ({ steps, currentStep, style }) => {
48
- return (
49
- <div style={{ padding: "0 32px 32px", display: "flex", alignItems: "center", width: "100%", margin: "16px 0", ...style }}>
50
- {steps.map((step, idx) => {
51
- const isCompleted = idx < currentStep;
52
- const isActive = idx === currentStep;
53
-
54
- return (
55
- <React.Fragment key={idx}>
56
- <div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
57
- <div
58
- style={{
59
- width: 16,
60
- height: 16,
61
- position: "relative",
62
- borderRadius: "50%",
63
- border: isActive || isCompleted ? "2px solid #ffffff" : "2px solid #a0a0a0",
64
- background: isCompleted ? "#ffffff" : "#333",
65
- display: "flex",
66
- alignItems: "center",
67
- justifyContent: "center",
68
- transition: "all 0.2s",
69
- zIndex: 1,
70
- }}
71
- >
72
- <p style={{ fontSize: 16, color: "#fff", opacity: isActive ? 1 : 0.5, position: "absolute", top: 24, width: 100 }}>{step.label}</p>
73
- </div>
74
- </div>
75
-
76
- {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 }} />}
77
- </React.Fragment>
78
- );
79
- })}
80
- </div>
81
- );
82
- };
37
+ const PAY_SLIPPAGE = 0.002;
83
38
 
84
- export const Payment = observer(({ connector, intents, onClose, onConfirm }: PaymentProps) => {
39
+ export const Payment = observer(({ connector, intents, payload, onReject, onSuccess }: PaymentProps) => {
85
40
  useState(() => {
86
41
  fetch(animations.loading);
87
42
  fetch(animations.success);
@@ -97,8 +52,8 @@ export const Payment = observer(({ connector, intents, onClose, onConfirm }: Pay
97
52
  token?: Token;
98
53
  wallet?: OmniWallet;
99
54
  commitment?: Commitment;
100
- review?: BridgeReview;
101
-
55
+ review?: BridgeReview | "direct";
56
+ data?: { paymentId: string; tx: string };
102
57
  step?: "selectToken" | "sign" | "transfer" | "success" | "error" | "loading";
103
58
  success?: boolean;
104
59
  loading?: boolean;
@@ -112,25 +67,36 @@ export const Payment = observer(({ connector, intents, onClose, onConfirm }: Pay
112
67
  const selectToken = async (from: Token, wallet?: OmniWallet) => {
113
68
  if (!wallet) return;
114
69
 
115
- setFlow({ token: from, wallet, review: undefined, step: "sign" });
116
- const review = await connector.exchange.reviewSwap({
117
- recipient: Recipient.fromWallet(wallet)!,
118
- amount: needAmount,
119
- sender: wallet,
120
- refund: wallet,
121
- slippage: 0.005,
122
- type: "exactOut",
123
- to: need,
124
- from,
125
- });
126
-
127
- 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
+ }
128
94
  };
129
95
 
130
96
  const signStep = async () => {
131
97
  try {
132
98
  setFlow((t) => (t ? { ...t, step: "sign", loading: true } : null));
133
- const commitment = await intents.attachWallet(flow!.wallet!).sign();
99
+ const commitment = await intents.sign();
134
100
  setFlow((t) => (t ? { ...t, step: "transfer", commitment, loading: false } : null));
135
101
  } catch (error) {
136
102
  console.error(error);
@@ -144,19 +110,17 @@ export const Payment = observer(({ connector, intents, onClose, onConfirm }: Pay
144
110
  const commitment = flow?.commitment;
145
111
  if (!commitment) throw new Error("Commitment not found");
146
112
  if (!flow?.review) throw new Error("Review not found");
147
-
148
113
  setFlow((t) => (t ? { ...t, step: "loading" } : null));
149
- const result = await connector.exchange.makeSwap(flow.review, { log: () => {} });
150
-
151
- if (typeof result.review.qoute === "object") {
152
- const data = await api.pendingPayment(commitment, result.review.qoute.depositAddress!);
153
- setFlow((t) => (t ? { ...t, step: "success", loading: false, success: true } : null));
154
- return data;
155
- } else {
156
- const hash = await Intents.publish([commitment]);
157
- setFlow((t) => (t ? { ...t, step: "success", loading: false, success: true } : null));
158
- return { hash };
114
+
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;
159
120
  }
121
+
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" }}>
@@ -232,7 +196,7 @@ export const Payment = observer(({ connector, intents, onClose, onConfirm }: Pay
232
196
  )}
233
197
  </PopupOption>
234
198
 
235
- <PopupButton style={{ marginTop: 24 }} disabled={!flow?.review} onClick={() => onConfirm(confirmPaymentStep())}>
199
+ <PopupButton style={{ marginTop: 24 }} disabled={!flow?.review} onClick={confirmPaymentStep}>
236
200
  {flow?.loading ? "Confirming..." : "Confirm payment"}
237
201
  </PopupButton>
238
202
  </Popup>
@@ -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
  });