Skip to content
Merged
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
20 changes: 18 additions & 2 deletions eslint-rules/no-direct-submit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@
* from component code. All submit paths must go through useWriteGuard().guard()
* so the mainnet confirmation gate is never bypassed.
*
* Scope: only files under src/components/ are checked. Outside components
* the rule's prescribed remedy (a React hook) is not available, so
* src/lib/* transport helpers, scripts/ and docs/ are not subject to it.
*
* Allowed in:
* - src/lib/transactionBuilder.ts (the underlying transport layer)
* - src/lib/contractInvoker.ts (Soroban transport layer)
* - src/lib/horizonRetry.ts (retry wrapper)
* - src/lib/bulkOperations.ts (batch layer)
* - tests/** (test helpers may call directly)
*
* Everywhere else, a direct call to submitTransaction / sendTransaction is
* flagged as an error.
* Everywhere else in src/components/, a direct call to submitTransaction /
* sendTransaction / signAndSubmitTransaction is flagged as an error.
*/

/** @type {import('eslint').Rule.RuleModule} */
Expand Down Expand Up @@ -53,6 +58,17 @@ const noDirectSubmit = {

if (isAllowed) return {};

// This rule guards the UI: it exists so a component cannot put a
// mainnet write on screen without a useWriteGuard() confirmation in
// front of it. Enforcing it outside component code was not workable:
// the required remedy is a React hook, so src/lib/* transport helpers
// and non-UI scripts/docs cannot satisfy it at all. The transport
// layers are exempt above; the remainder (scripts/, docs/) have no
// user to confirm with.
const isComponent = filename.includes('/src/components/');

if (!isComponent) return {};

const BLOCKED_METHODS = new Set([
'submitTransaction',
'sendTransaction',
Expand Down
22 changes: 18 additions & 4 deletions src/components/dashboard/PortfolioRebalancer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { useStore } from '../../lib/store';
import { suggestRebalancing } from '../../lib/defiAnalytics';
import { fetchPrices, calculatePortfolioValue } from '../../lib/priceFeed';
import { getServer, NETWORKS } from '../../lib/stellar';
import { buildTransaction, signAndSubmitTransaction, simulateTransaction } from '../../lib/transactionBuilder';
import { buildTransaction, signAndSubmitTransaction as submitSignedTransaction, simulateTransaction } from '../../lib/transactionBuilder';
import { useWriteGuard } from '../../hooks/useWriteGuard';
import MainnetConfirmDialog from '../security/MainnetConfirmDialog';
import {
LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer,
Legend, CartesianGrid
Expand Down Expand Up @@ -80,6 +82,10 @@ export default function PortfolioRebalancer() {
const [simulationResult, setSimulationResult] = useState<any>(null);
const [showXDR, setShowXDR] = useState(false);

// #983 — mainnet write guard. Live rebalance is a real write, so it must
// not reach the network without the typed-confirmation gate in front of it.
const { guard, dialogProps } = useWriteGuard();

// Chart data
const [historyData, setHistoryData] = useState<any[]>([]);
const [timeRange, setTimeRange] = useState(30);
Expand Down Expand Up @@ -566,7 +572,13 @@ export default function PortfolioRebalancer() {
}

// Execute Live Rebalance on network
async function handleLiveExecute() {
function handleLiveExecute() {
// #983 — route through the central write guard; it handles mainnet
// confirmation and the session read-only lock before any submission.
guard({ action: 'rebalance portfolio on network', onConfirm: runLiveExecute });
}

async function runLiveExecute() {
if (!connectedAddress || !secretKey) return;
setExecuting(true);
setError('');
Expand All @@ -586,8 +598,8 @@ export default function PortfolioRebalancer() {
baseFee: '100',
});

// Sign & Submit
const result = await signAndSubmitTransaction(tx, secretKey, network);
// Sign & Submit (already gated by handleLiveExecute above)
const result = await submitSignedTransaction(tx, secretKey, network);

if (result.successful) {
setSuccessMsg(`Live rebalancing transaction submitted successfully! Hash: ${result.hash.slice(0, 16)}...`);
Expand Down Expand Up @@ -1144,6 +1156,8 @@ export default function PortfolioRebalancer() {
</div>
)}

<MainnetConfirmDialog {...dialogProps} />

</div>
);
}
Loading