Skip to content

Commit 413127f

Browse files
feat(packages): broadcast flow (#552)
1 parent 32c63d9 commit 413127f

File tree

5 files changed

+353
-15
lines changed

5 files changed

+353
-15
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/**
2+
* BroadcastSignModal
3+
*
4+
* Modal for signing and broadcasting BTC transaction to Bitcoin network.
5+
* Opens when user clicks "Sign & Broadcast" button from deposits table.
6+
*
7+
* Shows provider steps during broadcast process.
8+
*/
9+
10+
import {
11+
Button,
12+
DialogBody,
13+
DialogFooter,
14+
DialogHeader,
15+
Loader,
16+
ResponsiveDialog,
17+
Text,
18+
} from "@babylonlabs-io/core-ui";
19+
import { useState } from "react";
20+
21+
import { useVaultActivityActions } from "../../../../hooks/useVaultActivityActions";
22+
import { usePeginStorage } from "../../../../storage/usePeginStorage";
23+
import type { VaultActivity } from "../../../../types/activity";
24+
25+
interface BroadcastSignModalProps {
26+
/** Modal open state */
27+
open: boolean;
28+
/** Close handler */
29+
onClose: () => void;
30+
/** The deposit/activity to broadcast for */
31+
activity: VaultActivity;
32+
/** Depositor's ETH address */
33+
depositorEthAddress: string;
34+
/** Success callback - refetch activities and show success modal */
35+
onSuccess: () => void;
36+
}
37+
38+
/**
39+
* Format address for display (first 6 and last 4 characters)
40+
*/
41+
function formatAddress(address: string): string {
42+
if (address.length < 10) return address;
43+
return `${address.slice(0, 6)}...${address.slice(-4)}`;
44+
}
45+
46+
/**
47+
* Provider step component
48+
*/
49+
function ProviderStep({
50+
step,
51+
providerName,
52+
providerAddress,
53+
isActive,
54+
}: {
55+
step: number;
56+
providerName?: string;
57+
providerAddress: string;
58+
isActive: boolean;
59+
}) {
60+
return (
61+
<div className="flex items-start gap-3">
62+
<div
63+
className={`flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full text-sm font-medium ${
64+
isActive
65+
? "bg-primary-main text-primary-contrast"
66+
: "bg-accent-secondary/20 text-accent-secondary"
67+
}`}
68+
>
69+
{isActive ? (
70+
<Loader size={16} className="text-primary-contrast" />
71+
) : (
72+
step
73+
)}
74+
</div>
75+
<div className="flex-1 pt-1">
76+
<Text
77+
variant="body2"
78+
className={`text-sm ${isActive ? "font-medium text-accent-primary" : "text-accent-secondary"}`}
79+
>
80+
{providerName || formatAddress(providerAddress)} payout
81+
</Text>
82+
</div>
83+
</div>
84+
);
85+
}
86+
87+
/**
88+
* Modal for broadcasting BTC transaction
89+
*
90+
* Shows provider steps and handles the broadcast process.
91+
*/
92+
export function BroadcastSignModal({
93+
open,
94+
onClose,
95+
activity,
96+
depositorEthAddress,
97+
onSuccess,
98+
}: BroadcastSignModalProps) {
99+
const [localBroadcasting, setLocalBroadcasting] = useState(false);
100+
101+
const { broadcasting, broadcastError, handleBroadcast } =
102+
useVaultActivityActions();
103+
104+
const { pendingPegins, updatePendingPeginStatus, addPendingPegin } =
105+
usePeginStorage({
106+
ethAddress: depositorEthAddress,
107+
confirmedPegins: [],
108+
});
109+
110+
const pendingPegin = pendingPegins.find((p) => p.id === activity.id);
111+
112+
const handleSign = async () => {
113+
setLocalBroadcasting(true);
114+
115+
try {
116+
await handleBroadcast({
117+
activityId: activity.id,
118+
activityAmount: activity.collateral.amount,
119+
activityProviders: activity.providers,
120+
connectedAddress: depositorEthAddress,
121+
pendingPegin,
122+
updatePendingPeginStatus,
123+
addPendingPegin,
124+
onRefetchActivities: () => {
125+
// Will be called after broadcast
126+
},
127+
onShowSuccessModal: () => {
128+
setLocalBroadcasting(false);
129+
onSuccess();
130+
},
131+
});
132+
} catch {
133+
setLocalBroadcasting(false);
134+
// Error is already set in the hook
135+
}
136+
};
137+
138+
const isBroadcasting = broadcasting || localBroadcasting;
139+
140+
return (
141+
<ResponsiveDialog
142+
open={open}
143+
onClose={isBroadcasting ? undefined : onClose}
144+
>
145+
<DialogHeader
146+
title="Sign BTC Transaction"
147+
onClose={isBroadcasting ? undefined : onClose}
148+
className="text-accent-primary"
149+
/>
150+
151+
<DialogBody className="flex flex-col gap-6 px-4 pb-8 pt-4 text-accent-primary sm:px-6">
152+
<Text
153+
variant="body2"
154+
className="text-sm text-accent-secondary sm:text-base"
155+
>
156+
Please sign the Bitcoin transaction to broadcast your deposit to the
157+
Bitcoin network.
158+
</Text>
159+
160+
{/* Provider Steps */}
161+
<div className="flex flex-col gap-4">
162+
{activity.providers.map((provider, index) => (
163+
<ProviderStep
164+
key={provider.id}
165+
step={index + 1}
166+
providerName={provider.name}
167+
providerAddress={provider.id}
168+
isActive={isBroadcasting && index === 0}
169+
/>
170+
))}
171+
</div>
172+
173+
{/* Error Display */}
174+
{broadcastError && (
175+
<div className="bg-error/10 rounded-lg p-4">
176+
<Text variant="body2" className="text-error text-sm">
177+
Error: {broadcastError}
178+
</Text>
179+
</div>
180+
)}
181+
</DialogBody>
182+
183+
<DialogFooter className="flex gap-4 px-4 pb-6 sm:px-6">
184+
{!isBroadcasting && (
185+
<Button
186+
variant="outlined"
187+
color="primary"
188+
onClick={onClose}
189+
className="flex-1 text-xs sm:text-base"
190+
>
191+
Cancel
192+
</Button>
193+
)}
194+
195+
<Button
196+
disabled={isBroadcasting}
197+
variant="contained"
198+
className="flex-1 text-xs sm:text-base"
199+
onClick={isBroadcasting ? undefined : handleSign}
200+
>
201+
{isBroadcasting ? (
202+
<Loader size={16} className="text-accent-contrast" />
203+
) : broadcastError ? (
204+
"Retry"
205+
) : (
206+
"Sign & Broadcast"
207+
)}
208+
</Button>
209+
</DialogFooter>
210+
</ResponsiveDialog>
211+
);
212+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* BroadcastSuccessModal
3+
*
4+
* Success confirmation modal shown after BTC transaction broadcast completes.
5+
* Displays success message and explains the confirmation process.
6+
*/
7+
8+
import {
9+
Button,
10+
DialogBody,
11+
DialogFooter,
12+
Heading,
13+
ResponsiveDialog,
14+
Text,
15+
} from "@babylonlabs-io/core-ui";
16+
17+
interface BroadcastSuccessModalProps {
18+
open: boolean;
19+
onClose: () => void;
20+
amount: string;
21+
}
22+
23+
/**
24+
* BroadcastSuccessModal - Success confirmation modal
25+
*
26+
* Displays:
27+
* - BTC icon
28+
* - "Broadcast Successful" heading
29+
* - Confirmation message about Bitcoin network confirmations
30+
* - "Done" button to close
31+
*/
32+
export function BroadcastSuccessModal({
33+
open,
34+
onClose,
35+
amount,
36+
}: BroadcastSuccessModalProps) {
37+
return (
38+
<ResponsiveDialog open={open} onClose={onClose}>
39+
<DialogBody className="px-4 py-16 text-center text-accent-primary sm:px-6">
40+
<img
41+
src="/images/btc.png"
42+
alt="Bitcoin"
43+
className="mx-auto h-auto w-full max-w-[160px]"
44+
/>
45+
46+
<Heading
47+
variant="h4"
48+
className="mb-4 mt-6 text-xl text-accent-primary sm:text-2xl"
49+
>
50+
Broadcast Successful
51+
</Heading>
52+
53+
<Text
54+
variant="body1"
55+
className="text-sm text-accent-secondary sm:text-base"
56+
>
57+
Your Bitcoin transaction has been broadcast to the network. Your
58+
deposit of {amount} BTC is now awaiting confirmation on the Bitcoin
59+
blockchain.
60+
</Text>
61+
62+
<Text
63+
variant="body2"
64+
className="mt-4 text-xs text-accent-secondary sm:text-sm"
65+
>
66+
This usually takes a few hours. You can continue using the platform
67+
while your deposit confirms.
68+
</Text>
69+
</DialogBody>
70+
71+
<DialogFooter className="flex gap-4 px-4 pb-8 sm:px-6">
72+
<Button
73+
variant="contained"
74+
color="primary"
75+
onClick={onClose}
76+
className="w-full"
77+
>
78+
Done
79+
</Button>
80+
</DialogFooter>
81+
</ResponsiveDialog>
82+
);
83+
}

services/vault/src/components/Overview/Deposits/DepositTableRow.tsx

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212

1313
import { Button } from "@babylonlabs-io/core-ui";
1414

15+
import {
16+
getPrimaryActionButton,
17+
PeginAction,
18+
} from "../../../models/peginStateMachine";
1519
import type { PendingPeginRequest } from "../../../storage/peginStorage";
1620
import type { VaultActivity } from "../../../types/activity";
1721
import type { Deposit } from "../../../types/vault";
@@ -29,6 +33,8 @@ interface DepositTableRowData {
2933
pendingPegin?: PendingPeginRequest;
3034
/** Callback when sign button clicked - passes transactions */
3135
onSignClick: (depositId: string, transactions: any[]) => void;
36+
/** Callback when broadcast button clicked */
37+
onBroadcastClick?: (depositId: string) => void;
3238
}
3339

3440
/**
@@ -43,27 +49,52 @@ export function DepositTableRowActions({
4349
btcPublicKey,
4450
pendingPegin,
4551
onSignClick,
52+
onBroadcastClick,
4653
}: DepositTableRowData) {
4754
// Poll for payout transactions at row level
48-
const { shouldShowSignButton, loading, transactions } = useDepositRowPolling({
55+
const { peginState, loading, transactions } = useDepositRowPolling({
4956
activity,
5057
btcPublicKey,
5158
pendingPegin,
5259
});
5360

54-
// Don't show button if no action available
55-
if (!shouldShowSignButton) {
61+
const actionButton = getPrimaryActionButton(peginState);
62+
63+
if (!actionButton) {
5664
return null;
5765
}
5866

59-
return (
60-
<Button
61-
size="small"
62-
variant="contained"
63-
onClick={() => onSignClick(deposit.id, transactions || [])}
64-
disabled={loading || !transactions}
65-
>
66-
{loading ? "Loading..." : "Sign"}
67-
</Button>
68-
);
67+
const { label, action } = actionButton;
68+
69+
switch (action) {
70+
case PeginAction.SIGN_PAYOUT_TRANSACTIONS:
71+
return (
72+
<Button
73+
size="small"
74+
variant="contained"
75+
onClick={() => onSignClick(deposit.id, transactions || [])}
76+
disabled={loading || !transactions}
77+
>
78+
{loading ? "Loading..." : label}
79+
</Button>
80+
);
81+
82+
case PeginAction.SIGN_AND_BROADCAST_TO_BITCOIN:
83+
if (!onBroadcastClick) return null;
84+
return (
85+
<Button
86+
size="small"
87+
variant="contained"
88+
onClick={() => onBroadcastClick(deposit.id)}
89+
>
90+
{label}
91+
</Button>
92+
);
93+
94+
case PeginAction.REDEEM:
95+
return null;
96+
97+
default:
98+
return null;
99+
}
69100
}

0 commit comments

Comments
 (0)