From afa309a1334be531fe681812299cd18e8041c514 Mon Sep 17 00:00:00 2001 From: Samuel Mmaduabuchi Date: Sat, 17 Oct 2026 01:32:01 +0100 Subject: [PATCH] feat: improve trade breakdown and Soroban transaction recovery --- app/buy-no-demo/page.tsx | 10 ++++++++++ lib/stellar/archival.ts | 14 ++++++++++++++ lib/stellar/authorization.ts | 19 +++++++++++++++++++ lib/stellar/transaction-recovery.ts | 20 ++++++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 lib/stellar/archival.ts create mode 100644 lib/stellar/authorization.ts create mode 100644 lib/stellar/transaction-recovery.ts diff --git a/app/buy-no-demo/page.tsx b/app/buy-no-demo/page.tsx index 5eaa1e3..a028e77 100644 --- a/app/buy-no-demo/page.tsx +++ b/app/buy-no-demo/page.tsx @@ -9,6 +9,8 @@ export default function BuyNoDemoPage() { const price = 0.68; const shares = (parseFloat(amount) || 0) / price; + const fee = (parseFloat(amount) || 0) * 0.01; + const priceImpact = Math.min(0.99, shares ? ((parseFloat(amount) || 0) / Math.max(shares, 1)) * 0.002 : 0); const potentialPayout = shares; // Since it's a binary outcome usually 1 USDC per share return ( @@ -115,6 +117,14 @@ export default function BuyNoDemoPage() { Estimated Shares {shares.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +
+ Trading fee + {fee.toFixed(2)} USDC +
+
+ Estimated price impact + {(priceImpact * 100).toFixed(2)}% +
Avg. Purchase Price 0.68 USDC diff --git a/lib/stellar/archival.ts b/lib/stellar/archival.ts new file mode 100644 index 0000000..e9c7566 --- /dev/null +++ b/lib/stellar/archival.ts @@ -0,0 +1,14 @@ +import type { AssembledTransaction } from '@stellar/stellar-sdk/contract'; + +export const isArchivedStateError = (error: unknown): boolean => /archiv|missing ledger entry|ttl|entry.?not.?found/i.test(String(error)); + +/** Re-simulate after restoring archived entries through the supplied backend/client. */ +export async function simulateWithRestore(build: () => Promise>, restore: () => Promise): Promise> { + const tx = await build(); + try { tx.simulation; return tx; } + catch (error) { + if (!isArchivedStateError(error)) throw error; + await restore(); + return build(); + } +} diff --git a/lib/stellar/authorization.ts b/lib/stellar/authorization.ts new file mode 100644 index 0000000..0f0f82f --- /dev/null +++ b/lib/stellar/authorization.ts @@ -0,0 +1,19 @@ +import { Address, xdr } from '@stellar/stellar-sdk'; + +export interface AuthorizationInspection { count: number; signers: string[]; functions: string[]; warnings: string[]; valid: boolean; } + +/** Inspect authorization entries before a wallet prompt; never sign unexpected addresses/functions. */ +export function inspectAuthorizationEntries(entries: readonly xdr.SorobanAuthorizationEntry[], expectedAddress?: string): AuthorizationInspection { + const signers: string[] = []; + const functions: string[] = []; + const warnings: string[] = []; + for (const entry of entries) { + try { + const address = entry.credentials().switch().name === 'sorobanCredentialsAddress' ? Address.fromScAddress(entry.credentials().address().address()).toString() : undefined; + if (address) signers.push(address); + functions.push(String(entry.rootInvocation().function())); + if (expectedAddress && address && address !== expectedAddress) warnings.push(`Unexpected signer ${address}`); + } catch { warnings.push('Unable to decode an authorization entry'); } + } + return { count: entries.length, signers, functions, warnings, valid: warnings.length === 0 }; +} diff --git a/lib/stellar/transaction-recovery.ts b/lib/stellar/transaction-recovery.ts new file mode 100644 index 0000000..e624535 --- /dev/null +++ b/lib/stellar/transaction-recovery.ts @@ -0,0 +1,20 @@ +import type { AssembledTransaction } from '@stellar/stellar-sdk/contract'; + +export const isFeeSurge = (error: unknown): boolean => /tx_bad_seq|insufficient fee|fee.?bump|resource limit|timeout/i.test(String(error)); +export const isExpiredTransaction = (error: unknown): boolean => /expired|tx_too_late|too late|time.?bound/i.test(String(error)); + +export interface RecoveryAdvice { kind: 'fee-bump' | 'rebuild' | 'retry'; message: string; } +export function transactionRecoveryAdvice(error: unknown): RecoveryAdvice { + if (isFeeSurge(error)) return { kind: 'fee-bump', message: 'Network fees increased. We can retry with a higher fee.' }; + if (isExpiredTransaction(error)) return { kind: 'rebuild', message: 'This transaction expired. Rebuild it to refresh its sequence and time bounds.' }; + return { kind: 'retry', message: 'The transaction did not complete. Please retry.' }; +} + +/** Retry a send once after a caller rebuilds the transaction with a fresh fee/sequence. */ +export async function sendWithRecovery(build: (feeBump: boolean) => Promise>, send: (tx: AssembledTransaction) => Promise): Promise { + try { return await send(await build(false)); } + catch (error) { + if (!isFeeSurge(error) && !isExpiredTransaction(error)) throw error; + return send(await build(isFeeSurge(error))); + } +}