Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .agents/skills/nexus-sdk-bridge-flows/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: Implement bridge, bridgeAndTransfer, bridgeAndExecute, and execute
---

# Bridge and Execute Flows

## Call bridge(params, options?)
- Use to move tokens cross-chain (intent-based bridge).
- Signature:
Expand Down
68 changes: 57 additions & 11 deletions registry/nexus-elements/common/hooks/useTransactionExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ export function useTransactionExecution({
notifyHistoryRefresh?.();
};

const isRunStale = (currentRunId: number) =>
currentRunId !== runIdRef.current;

const handleTransaction = async () => {
if (commitLockRef.current) return;
commitLockRef.current = true;
Expand Down Expand Up @@ -221,7 +224,7 @@ export function useTransactionExecution({
}

const maxForCurrentSelection = await getMaxForCurrentSelection();
if (currentRunId !== runIdRef.current) return;
if (isRunStale(currentRunId)) return;
if (!maxForCurrentSelection) {
const message = `Unable to determine max ${operationName} amount for selected sources. Please try again.`;
setTxError(message);
Expand Down Expand Up @@ -250,7 +253,7 @@ export function useTransactionExecution({
setAppliedSourceSelectionKey(sourceSelectionKey);

const onEvent = (event: TransactionFlowEvent) => {
if (currentRunId !== runIdRef.current) return;
if (isRunStale(currentRunId)) return;
if (event.name === NEXUS_EVENTS.STEPS_LIST) {
const list = Array.isArray(event.args) ? event.args : [];
onStepsList(list as BridgeStepType[]);
Expand All @@ -269,6 +272,29 @@ export function useTransactionExecution({
}
};

if (nexusSDK) {
nexusSDK.setOnIntentHook((data) => {
if (isRunStale(currentRunId)) {
try { data.deny(); } catch {}
return;
}

if (!inputs.amount) {
try { data.deny(); } catch {}
return;
}

intent.current = data;
});

nexusSDK.setOnAllowanceHook((data) => {
if (isRunStale(currentRunId)) {
return;
}
allowance.current = data;
});
}

const transactionResult = await executeTransaction({
token: inputs.token,
amount: amountBigInt,
Expand Down Expand Up @@ -362,15 +388,35 @@ export function useTransactionExecution({
await handleTransaction();
};

const invalidatePendingExecution = useCallback(() => {
runIdRef.current += 1;
if (intent.current) {
intent.current.deny();
intent.current = null;
}
setRefreshing(false);
setAppliedSourceSelectionKey("ALL");
}, [intent, setAppliedSourceSelectionKey, setRefreshing]);
const invalidatePendingExecution = useCallback(
(options?: { forceResetUI?: boolean }) => {
runIdRef.current += 1;
commitLockRef.current = false;
if (intent.current) {
intent.current.deny();
intent.current = null;
}
allowance.current = null;
setAppliedSourceSelectionKey("ALL");

// Actively clear UI flags if inputs become cleanly invalid (zero, empty, etc.)
if (options?.forceResetUI) {
setStatus("idle");
setRefreshing(false);
resetSteps();
setLastExplorerUrl("");
}
},
[
allowance,
intent,
setAppliedSourceSelectionKey,
setLastExplorerUrl,
setRefreshing,
setStatus,
resetSteps,
],
);

return {
refreshIntent,
Expand Down
1 change: 1 addition & 0 deletions registry/nexus-elements/common/hooks/useTransactionFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,5 +573,6 @@ export function useTransactionFlow(props: UseTransactionFlowProps) {
requiredSafetyTotal: sourceSelection.requiredSafetyTotal,
maxAvailableAmount: selectedSourcesMaxAmount ?? undefined,
isInputsValid: areInputsValid,
invalidatePendingExecution,
};
}
42 changes: 39 additions & 3 deletions registry/nexus-elements/deposit/hooks/use-deposit-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ export function useDepositWidget(
const determiningSwapComplete = useRef(false);
const lastSimulationTime = useRef(0);
const suppressNextWidgetPreviewCancelError = useRef(false);
const depositRunIdRef = useRef(0);

const isRunStale = useCallback((runId: number) => {
return runId !== depositRunIdRef.current;
}, []);

const denyActiveSwapIntent = useCallback(
(options?: { suppressUiError?: boolean }) => {
Expand Down Expand Up @@ -187,9 +192,13 @@ export function useDepositWidget(
// Action callbacks
const setInputs = useCallback(
(next: Partial<DepositInputs>) => {
if (next.amount === "") {
dispatch({ type: "setStatus", payload: "idle" });
denyActiveSwapIntent({ suppressUiError: true });
}
dispatch({ type: "setInputs", payload: next });
},
[dispatch],
[dispatch, denyActiveSwapIntent],
);

const setTxError = useCallback(
Expand All @@ -203,10 +212,33 @@ export function useDepositWidget(
* Start the swap and execute flow with the SDK
*/
const start = useCallback(
(inputs: SwapAndExecuteParams, targetAmountUsd?: number) => {
(inputs: SwapAndExecuteParams, runId: number, targetAmountUsd?: number) => {
if (!nexusSDK || !inputs || isProcessing) return;

seed(SWAP_EXPECTED_STEPS);

if (nexusSDK) {
nexusSDK.setOnSwapIntentHook((data: OnSwapIntentHookData) => {
if (isRunStale(runId)) {
try { data.deny(); } catch {}
return;
}

if (!state.inputs.amount) {
try { data.deny(); } catch {}
return;
}

swapIntent.current = data;
});

nexusSDK.setOnAllowanceHook((data) => {
if (isRunStale(runId)) {
return;
}
});
}

const requiredAmountUsd =
targetAmountUsd ?? parseUsdAmount(state.inputs.amount);
const { sourcePoolIds, selectedSourceIds, fromSources } =
Expand Down Expand Up @@ -236,6 +268,7 @@ export function useDepositWidget(
nexusSDK
.swapAndExecute(inputsWithSources, {
onEvent: (event) => {
if (isRunStale(runId)) return;
if (event.name === NEXUS_EVENTS.SWAP_STEP_COMPLETE) {
const step = event.args as SwapStepType & {
completed?: boolean;
Expand Down Expand Up @@ -267,6 +300,7 @@ export function useDepositWidget(
},
})
.then((data: SwapAndExecuteResult) => {
if (isRunStale(runId)) return;
suppressNextWidgetPreviewCancelError.current = false;

// Extract source swaps from the result
Expand Down Expand Up @@ -359,6 +393,7 @@ export function useDepositWidget(
});
})
.catch((error) => {
if (isRunStale(runId)) return;
const { code, message } = handleNexusError(error);
const isUserRejectedError =
code === ERROR_CODES.USER_DENIED_INTENT ||
Expand Down Expand Up @@ -497,7 +532,8 @@ export function useDepositWidget(
});
dispatch({ type: "setStatus", payload: "simulation-loading" });
dispatch({ type: "setSimulationLoading", payload: true });
start(newInputs, totalAmountUsd);
const runId = ++depositRunIdRef.current;
start(newInputs, runId, totalAmountUsd);
return true;
},
[
Expand Down
73 changes: 70 additions & 3 deletions registry/nexus-elements/fast-bridge/fast-bridge.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import { type FC, useEffect, useState } from "react";
import { type FC, useEffect, useMemo, useRef, useState } from "react";
import { Card, CardContent } from "../ui/card";
import ChainSelect from "./components/chain-select";
import TokenSelect from "./components/token-select";
Expand Down Expand Up @@ -96,6 +96,7 @@ const FastBridge: FC<FastBridgeProps> = ({
requiredSafetyTotal,
maxAvailableAmount,
isInputsValid,
invalidatePendingExecution,
} = useBridge({
prefill,
network: network ?? "mainnet",
Expand All @@ -118,6 +119,67 @@ const FastBridge: FC<FastBridgeProps> = ({
}
}, [intent.current?.intent]);

const autoIntentTriggered = useRef(false);
const lastAutoIntentKeyRef = useRef("");
const autoIntentKey = useMemo(
() =>
[
inputs?.amount ?? "",
inputs?.chain ?? "",
inputs?.token ?? "",
inputs?.recipient ?? "",
].join("|"),
[inputs?.amount, inputs?.chain, inputs?.token, inputs?.recipient],
);

useEffect(() => {
if (lastAutoIntentKeyRef.current === autoIntentKey) {
return;
}
lastAutoIntentKeyRef.current = autoIntentKey;
autoIntentTriggered.current = false;
}, [autoIntentKey]);

useEffect(() => {
if (
!(inputs?.amount && inputs?.chain && inputs?.token && inputs?.recipient)
) {
return;
}
if (!isInputsValid) {
return;
}
if (!bridgableBalance) {
return;
}
if (availableSources.length === 0) {
return;
}
if (intent.current) {
return;
}
if (autoIntentTriggered.current) {
return;
}

const timer = setTimeout(() => {
autoIntentTriggered.current = true;
handleTransaction();
}, 500);

return () => clearTimeout(timer);
}, [
availableSources.length,
bridgableBalance,
inputs?.amount,
inputs?.chain,
inputs?.recipient,
inputs?.token,
isInputsValid,
intent,
handleTransaction,
]);

return (
<Card className="w-full max-w-xl">
<CardContent className="flex flex-col gap-y-4 w-full px-2 sm:px-6 relative">
Expand All @@ -141,7 +203,12 @@ const FastBridge: FC<FastBridgeProps> = ({
/>
<AmountInput
amount={inputs?.amount}
onChange={(amount) => setInputs({ ...inputs, amount })}
onChange={(amount) => {
setInputs({ ...inputs, amount });
if (!amount) {
invalidatePendingExecution({ forceResetUI: true });
}
} }
bridgableBalance={filteredBridgableBalance}
onCommit={() => void commitAmount()}
disabled={refreshing || !!prefill?.amount}
Expand All @@ -156,7 +223,7 @@ const FastBridge: FC<FastBridgeProps> = ({
}
disabled={!!prefill?.recipient}
/>
{intent?.current?.intent && (
{Boolean(inputs?.amount) && intent?.current?.intent && (
<>
<SourceBreakdown
intent={intent?.current?.intent}
Expand Down
1 change: 1 addition & 0 deletions registry/nexus-elements/fast-bridge/hooks/useBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const useBridge = ({
setInputs: flow.setInputs as (
next: FastBridgeState | Partial<FastBridgeState>,
) => void,
invalidatePendingExecution: flow.invalidatePendingExecution,
};
};

Expand Down
Loading