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
32 changes: 32 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,38 @@ <h3>Keyboard Shortcuts</h3>
<!-- Toast stack -->
<div id="toast-stack" class="toast-stack"></div>

<!-- Bulk actions menu bar -->
<div id="bulk-actions-bar" class="bulk-actions-bar hidden">
<div class="bulk-actions-info">
<span id="bulk-selected-count">0</span> positions selected
</div>
<div class="bulk-actions-buttons">
<button id="bulk-close-btn" class="btn btn-danger btn-sm">Close Selected</button>
<div class="bulk-adjust-group">
<span>Adjust Leverage by:</span>
<input type="number" id="bulk-adjust-input" class="input mono" placeholder="10" style="width: 70px; padding: 4px 8px; height: 28px; padding-right: 8px;" />
<span>%</span>
<button id="bulk-adjust-btn" class="btn btn-primary btn-sm">Adjust</button>
</div>
</div>
</div>

<!-- Bulk Action Confirmation Modal -->
<div id="bulk-confirm-overlay" class="alert-modal-overlay hidden" role="dialog" aria-modal="true">
<div class="alert-modal" style="max-width: 480px;">
<button id="bulk-confirm-close" class="alert-modal-close">&times;</button>
<h3 id="bulk-confirm-title">Confirm Bulk Action</h3>
<p class="alert-modal-desc">Review the positions affected by this action:</p>
<div id="bulk-confirm-list" class="bulk-confirm-list" style="max-height: 200px; overflow-y: auto; margin-bottom: 18px; border: 1px solid var(--border); border-radius: var(--r-xs); padding: 8px; background: var(--input-bg);">
<!-- List of positions with details will be injected here -->
</div>
<div style="display: flex; gap: 8px;">
<button id="bulk-confirm-submit" class="btn btn-primary" style="flex: 1;">Confirm &amp; Submit</button>
<button id="bulk-confirm-cancel" class="btn btn-secondary" style="flex: 1;">Cancel</button>
</div>
</div>
</div>

</main>
</div><!-- .main-wrap -->
</div>
Expand Down
98 changes: 98 additions & 0 deletions frontend/src/blend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,104 @@ export async function submitClassicXdr(signedXdr: string): Promise<string> {
return (result as any).hash;
}

export async function buildBulkCloseXdr(
poolPositions: { pool: PoolDef; positions: AssetPosition[] }[],
userAddress: string,
): Promise<string> {
const MAX_AMOUNT = 9_223_372_036_854_775_807n; // i64::MAX
const acc = await server.getAccount(userAddress);
const txBuilder = new TransactionBuilder(acc, {
fee: (BigInt(BASE_FEE) * 10n).toString(),
networkPassphrase: _cfg.passphrase,
});

const addrScVal = new Address(userAddress).toScVal();

for (const { pool, positions } of poolPositions) {
const reqItems: xdr.ScVal[] = [];
for (const pos of positions) {
if (pos.dTokens > 0n) {
reqItems.push(buildRequest(pos.asset.id, MAX_AMOUNT, REPAY));
}
reqItems.push(buildRequest(pos.asset.id, MAX_AMOUNT, WITHDRAW_COLLATERAL));
}
const requests = buildRequestsVec(reqItems);
const poolContract = new Contract(pool.id);
txBuilder.addOperation(poolContract.call("submit_with_allowance", addrScVal, addrScVal, addrScVal, requests));
}

const tx = txBuilder.setTimeout(60).build();
const sim = await server.simulateTransaction(tx);
if (!SorobanRpc.Api.isSimulationSuccess(sim))
throw new Error(`Bulk close simulation failed: ${(sim as SorobanRpc.Api.SimulateTransactionErrorResponse).error}`);
return SorobanRpc.assembleTransaction(tx, sim).build().toXDR();
}

export async function buildBulkAdjustXdr(
poolAdjustments: { pool: PoolDef; adjustments: { asset: AssetInfo; pos: AssetPosition; targetLev: number }[] }[],
userAddress: string,
): Promise<string> {
const acc = await server.getAccount(userAddress);
const txBuilder = new TransactionBuilder(acc, {
fee: (BigInt(BASE_FEE) * 10n).toString(),
networkPassphrase: _cfg.passphrase,
});

const addrScVal = new Address(userAddress).toScVal();

for (const { pool, adjustments } of poolAdjustments) {
const reqItems: xdr.ScVal[] = [];

for (const { asset, pos, targetLev } of adjustments) {
if (pos.equity <= 0) throw new Error(`No equity in ${asset.symbol} position`);

if (targetLev > pos.leverage) {
// Increase leverage
const targetCollateral = pos.equity * targetLev;
const additionalBorrow = targetCollateral - pos.collateral;
if (additionalBorrow <= 0) continue;

const additionalBorrowStroops = BigInt(Math.round(additionalBorrow * 1e7));
const cFactorBn = BigInt(Math.round(asset.cFactor * SCALAR_F));

let remaining = additionalBorrowStroops;
let balance = 0n;
while (remaining > 0n) {
const canBorrow = balance > 0n ? balance * cFactorBn / SCALAR : remaining;
const borrow = canBorrow < remaining ? canBorrow : remaining;
if (borrow <= 0n) break;
reqItems.push(buildRequest(asset.id, borrow, BORROW));
reqItems.push(buildRequest(asset.id, borrow, SUPPLY_COLLATERAL));
balance = borrow;
remaining -= borrow;
}
} else if (targetLev < pos.leverage) {
// Decrease leverage
const targetDebt = pos.equity * (targetLev - 1);
const debtReduction = pos.debt - targetDebt;
if (debtReduction <= 0) continue;

const debtReductionStroops = BigInt(Math.round(debtReduction * 1e7));
reqItems.push(buildRequest(asset.id, debtReductionStroops, WITHDRAW_COLLATERAL));
reqItems.push(buildRequest(asset.id, debtReductionStroops, REPAY));
}
}

if (reqItems.length === 0) continue;

const requests = buildRequestsVec(reqItems);
const poolContract = new Contract(pool.id);
txBuilder.addOperation(poolContract.call("submit_with_allowance", addrScVal, addrScVal, addrScVal, requests));
}

const tx = txBuilder.setTimeout(60).build();
const sim = await server.simulateTransaction(tx);
if (!SorobanRpc.Api.isSimulationSuccess(sim))
throw new Error(`Bulk adjust simulation failed: ${(sim as SorobanRpc.Api.SimulateTransactionErrorResponse).error}`);
return SorobanRpc.assembleTransaction(tx, sim).build().toXDR();
}


// ── Position event timeline (A25) ─────────────────────────────────────────────

export type PositionEventKind = "open" | "rebalance" | "harvest" | "close";
Expand Down
Loading