Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
61b8155
init stub direct tx executor service
piatoss3612 Nov 25, 2025
9fc5ee1
define types for direct tx execution
piatoss3612 Nov 26, 2025
3baaa52
define messages for direct tx execution
piatoss3612 Nov 26, 2025
0bdbf56
rename to background tx executor service
piatoss3612 Nov 26, 2025
9bf3f1b
rename directory
piatoss3612 Nov 26, 2025
99638f0
add options to execute presigned tx on background tx executor
piatoss3612 Nov 26, 2025
22c5feb
stub flow definition for background tx executor
piatoss3612 Nov 27, 2025
5722794
implement tx broadcasting and tracing logic on bg tx executor
piatoss3612 Nov 27, 2025
50f25af
implement evm tx direct signing logic
piatoss3612 Nov 27, 2025
e0ff478
return tx batch result status for subsequent operations
piatoss3612 Nov 27, 2025
bcbc32f
minor refactor
piatoss3612 Nov 27, 2025
55fda85
rename background tx executor types
piatoss3612 Nov 28, 2025
a6dc2df
handle history record in background tx executor
piatoss3612 Nov 28, 2025
a056ff7
minor refactor
piatoss3612 Nov 28, 2025
104f4d1
implement stub cosmos tx signing logic on tx executor
piatoss3612 Nov 28, 2025
6d00f06
refactor `sign**PreAuthorized` method should only sign the message
piatoss3612 Nov 28, 2025
9feef38
calculate cosmos tx fee on background tx executor
piatoss3612 Nov 28, 2025
e0b4c1d
record fee type
piatoss3612 Nov 28, 2025
1a956a5
add simple message queue
piatoss3612 Nov 28, 2025
f91b303
handle sophisticated state update in tx executor
piatoss3612 Nov 30, 2025
b92d98e
publish executable chain ids
piatoss3612 Nov 30, 2025
0b8165a
use fee type
piatoss3612 Nov 30, 2025
22231e2
add hold button
piatoss3612 Dec 1, 2025
801414b
add console log for debugging
piatoss3612 Dec 2, 2025
902a6c2
apply one click swap for mnemonic and private key accounts
piatoss3612 Dec 2, 2025
e0601f1
minor fix
piatoss3612 Dec 2, 2025
d2045c0
add `preventAutoSign` flag
piatoss3612 Dec 2, 2025
308ae5e
hardware wallet should generate signed tx on ui and submit it to back…
piatoss3612 Dec 2, 2025
6511d18
handle fee type and fee currency for each background tx
piatoss3612 Dec 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions apps/extension/src/components/hold-button/circular-progress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { CSSProperties, FunctionComponent } from "react";

export interface CircularProgressProps {
progress: number; // 0 ~ 1
size?: string;
color?: string;
style?: CSSProperties;
}

export const CircularProgress: FunctionComponent<CircularProgressProps> = ({
progress,
size = "1.25rem",
color = "white",
style,
}) => {
const radius = 8.5;
const strokeWidth = 3;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset = circumference * (1 - progress);

return (
<svg
width={size}
height={size}
viewBox="0 0 20 20"
fill="none"
style={{
transform: "rotate(-90deg) scaleY(-1)", // start from 12 o'clock, counter-clockwise
...style,
}}
>
{/* Background donut - 30% opacity */}
<circle
cx="10"
cy="10"
r={radius}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
opacity={0.3}
/>
{/* Progress circle */}
<circle
cx="10"
cy="10"
r={radius}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
style={{
transition: progress === 0 ? "none" : "stroke-dashoffset 16ms linear",
}}
/>
</svg>
);
};
165 changes: 165 additions & 0 deletions apps/extension/src/components/hold-button/hold-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import React, {
FunctionComponent,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { HoldButtonProps } from "./types";
import { Styles } from "../button/styles";
import { LoadingIcon } from "../icon";
import { Box } from "../box";
import { useTheme } from "styled-components";
import { CircularProgress } from "./circular-progress";

const UPDATE_INTERVAL_MS = 16;
const MIN_HOLD_DURATION_MS = 100;

export const HoldButton: FunctionComponent<HoldButtonProps> = ({
holdDurationMs,
onConfirm,
onHoldStart,
onHoldEnd,
onProgressChange,
type = "button",
progressSize = "1.25rem",
style,
className,
text,
holdingText,
left,
right,
isLoading,
disabled,
...otherProps
}) => {
const theme = useTheme();

const [isHolding, setIsHolding] = useState(false);
const [progress, setProgress] = useState(0);

const buttonRef = useRef<HTMLButtonElement>(null);
const holdStartTimeRef = useRef<number | null>(null);
const intervalRef = useRef<number | null>(null);
const confirmedRef = useRef(false);

const clearHoldInterval = useCallback(() => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, []);

const startHold = useCallback(() => {
if (disabled || isLoading) return;

confirmedRef.current = false;
setIsHolding(true);
setProgress(0);
holdStartTimeRef.current = Date.now();
onHoldStart?.();

intervalRef.current = window.setInterval(() => {
if (holdStartTimeRef.current === null) return;

const elapsed = Date.now() - holdStartTimeRef.current;
const newProgress = Math.min(
elapsed / Math.max(holdDurationMs, MIN_HOLD_DURATION_MS),
1
);

setProgress(newProgress);
onProgressChange?.(newProgress);

if (newProgress >= 1 && !confirmedRef.current) {
confirmedRef.current = true;
clearHoldInterval();
setIsHolding(false);
setProgress(0);
holdStartTimeRef.current = null;

onConfirm?.();

if (type === "submit" && buttonRef.current?.form) {
buttonRef.current.form.requestSubmit();
}
}
}, UPDATE_INTERVAL_MS);
}, [
disabled,
isLoading,
holdDurationMs,
onHoldStart,
onProgressChange,
onConfirm,
type,
clearHoldInterval,
]);

const endHold = useCallback(() => {
if (!isHolding) return;

clearHoldInterval();
setIsHolding(false);
setProgress(0);
holdStartTimeRef.current = null;
onHoldEnd?.();
}, [isHolding, clearHoldInterval, onHoldEnd]);

useEffect(() => {
return () => {
clearHoldInterval();
};
}, [clearHoldInterval]);

const displayText = isHolding && holdingText ? holdingText : text;

// CircularProgress is default left content
const leftContent =
left !== undefined ? (
left
) : (
<CircularProgress
progress={progress}
size={progressSize}
style={{ opacity: isLoading ? 0 : 1 }}
/>
);

return (
<Styles.Container
style={style}
className={className}
mode={otherProps.mode}
>
<Styles.Button
ref={buttonRef}
isLoading={isLoading}
type="button" // prevent default form submit behavior
disabled={disabled}
{...otherProps}
style={{ gap: "0.25rem" }}
onMouseDown={startHold}
onMouseUp={endHold}
onMouseLeave={endHold}
onTouchStart={startHold}
onTouchEnd={endHold}
onTouchCancel={endHold}
data-holding={isHolding}
data-progress={progress}
>
{leftContent ? <Styles.Left>{leftContent}</Styles.Left> : null}

{isLoading ? (
<Styles.Loading buttonColor={otherProps.color} theme={theme}>
<LoadingIcon width="1rem" height="1rem" />
</Styles.Loading>
) : null}

<Box style={{ opacity: isLoading ? 0 : 1 }}>{displayText}</Box>

{right ? <Styles.Right>{right}</Styles.Right> : null}
</Styles.Button>
</Styles.Container>
);
};
3 changes: 3 additions & 0 deletions apps/extension/src/components/hold-button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./hold-button";
export * from "./circular-progress";
export * from "./types";
13 changes: 13 additions & 0 deletions apps/extension/src/components/hold-button/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ReactNode } from "react";
import { ButtonProps } from "../button/types";

export interface HoldButtonProps extends ButtonProps {
holdDurationMs: number;
onConfirm?: () => void;
onHoldStart?: () => void;
onHoldEnd?: () => void;
onProgressChange?: (progress: number) => void;

progressSize?: string;
holdingText?: string | ReactNode;
}
1 change: 0 additions & 1 deletion apps/extension/src/keplr-wallet-private
Submodule keplr-wallet-private deleted from cfc88b
2 changes: 2 additions & 0 deletions apps/extension/src/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,8 @@

"page.ibc-swap.title.swap": "Swap",
"page.ibc-swap.button.next": "Swap",
"page.ibc-swap.button.hold-to-approve": "Hold to Approve & Swap",
"page.ibc-swap.button.keep-holding": "Keep holding...",
"page.ibc-swap.error.no-route-found": "No routes found for this swap",
"page.ibc-swap.warning.unable-to-populate-price": "Unable to retrieve {assets} price from Coingecko. Check the fiat values of input and output from other sources before clicking \"Next\".",
"page.ibc-swap.warning.high-price-impact-title": "{inPrice} on {srcChain} → {outPrice} on {dstChain}",
Expand Down
2 changes: 2 additions & 0 deletions apps/extension/src/languages/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,8 @@

"page.ibc-swap.title.swap": "토큰 교환",
"page.ibc-swap.button.next": "스왑",
"page.ibc-swap.button.hold-to-approve": "꾹 눌러서 승인 & 스왑",
"page.ibc-swap.button.keep-holding": "계속 누르세요...",
"page.ibc-swap.error.no-route-found": "이 교환에 맞는 경로가 검색되지 않습니다.",
"page.ibc-swap.warning.unable-to-populate-price": "{assets}의 가격을 알 수 없습니다. \"다음\"을 누르기 전에 각각 통화 가치를 확인하십시오.",
"page.ibc-swap.warning.high-price-impact-title": "{inPrice} ({srcChain}) → {outPrice} ({dstChain})",
Expand Down
2 changes: 2 additions & 0 deletions apps/extension/src/languages/zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,8 @@

"page.ibc-swap.title.swap": "兑换",
"page.ibc-swap.button.next": "交换",
"page.ibc-swap.button.hold-to-approve": "按住以批准并交换",
"page.ibc-swap.button.keep-holding": "请继续按住...",
"page.ibc-swap.error.no-route-found": "找不到此兑换的路线",
"page.ibc-swap.warning.unable-to-populate-price": "无法从Coingecko检索到{assets}的价格。在点击“下一步”之前从其他来源检查输入和输出的法定价值。",
"page.ibc-swap.warning.high-price-impact-title": "{inPrice} ({srcChain}) → {outPrice} ({dstChain})",
Expand Down
Loading
Loading