From afc9a27dbf4625feaa0ba1be20957932bf74cb53 Mon Sep 17 00:00:00 2001 From: villadel Date: Sun, 18 Oct 2026 11:18:39 +0000 Subject: [PATCH] fix: resolve issues #883 #884 #885 #886 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #886 — return tx_hash in error response for failed invoice creation - Attach tx_hash to thrown errors in createInvoice() for timeout, FAILED status and ERROR send result - Surface tx_hash in POST /invoices error JSON body so callers can identify orphaned contract records #885 — use UTC timestamps instead of local time - Add formatUTC() helper in TransactionHistory.tsx - Replace toLocaleString() with ISO UTC string in both TransactionHistory and InvoicePayment expiry display #884 — fix merchant invoice search ignoring status filter - Add normaliseStatus() helper in InvoiceSearchFilter.tsx - Use normalised string comparison to handle TypeScript string-enum vs string-union mismatch between comebackhere-frontend and frontend #883 — add settlement transaction hash to receipt view - Add proposed_tx_hash and executed_tx_hash to Settlement type - Include both fields in GET /pending-settlements API response - Display them in SettlementDetail.tsx with monospace hash styling - Add .settlement-detail__value--hash CSS class chore: expand .gitignore - Add test snapshots (__snapshots__/, *.snap) - Add Playwright reports, build outputs, TS/Vite cache - Add Cargo target/, OS noise, editor files, logs --- .gitignore | 62 ++++++++++++++++++- comebackhere-backend/src/routes/invoices.ts | 11 ++-- comebackhere-backend/src/routes/treasury.ts | 2 + .../src/tests/invoices.test.ts | 18 ++++++ .../src/components/InvoicePayment.tsx | 2 +- .../src/components/TransactionHistory.tsx | 7 ++- .../src/components/InvoiceSearchFilter.tsx | 12 +++- .../SettlementDetail/SettlementDetail.css | 7 +++ .../SettlementDetail/SettlementDetail.tsx | 22 +++++++ frontend/src/types/index.ts | 2 + 10 files changed, 137 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 09c69f3..a12b4d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,66 @@ +# ── Secrets & environment ──────────────────────────────────────────────────── .env .env.mainnet .env.testnet +.env.local +.env.*.local + +# ── Deployed contract addresses (environment-specific) ─────────────────────── artifacts/addresses.json -coverage/ + +# ── Dependencies ───────────────────────────────────────────────────────────── node_modules/ +**/node_modules/ + +# ── Build outputs ───────────────────────────────────────────────────────────── +dist/ +build/ +out/ +**/dist/ + +# ── Test artifacts ──────────────────────────────────────────────────────────── +coverage/ +**/coverage/ + +# Vitest / Jest snapshots +**/__snapshots__/ +**/*.snap + +# Playwright +**/playwright-report/ +**/test-results/ +**/blob-report/ +**/playwright/.cache/ + +# ── TypeScript cache ────────────────────────────────────────────────────────── +*.tsbuildinfo +**/tsconfig.tsbuildinfo + +# ── Vite cache ──────────────────────────────────────────────────────────────── +**/.vite/ + +# ── Rust / Cargo build artifacts ───────────────────────────────────────────── +**/target/ + +# ── OS / editor noise ──────────────────────────────────────────────────────── +.DS_Store +Thumbs.db +*.swp +*.swo +*~ + +# ── IDE-specific ────────────────────────────────────────────────────────────── +.vscode/settings.json +.idea/ +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# ── Logs ────────────────────────────────────────────────────────────────────── +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* diff --git a/comebackhere-backend/src/routes/invoices.ts b/comebackhere-backend/src/routes/invoices.ts index f57ebd1..75cae01 100644 --- a/comebackhere-backend/src/routes/invoices.ts +++ b/comebackhere-backend/src/routes/invoices.ts @@ -97,7 +97,7 @@ export async function createInvoice( if (sendResult.status === "ERROR") { throw Object.assign( new Error(`Soroban submission failed: ${(sendResult as any).errorResult?.toXDR("base64")}`), - { status: 422 } + { status: 422, tx_hash: (sendResult as any).hash as string | undefined } ) } @@ -110,11 +110,11 @@ export async function createInvoice( } if (!getResult || getResult.status === SorobanRpc.Api.GetTransactionStatus.NOT_FOUND) { - throw Object.assign(new Error("Transaction confirmation timeout"), { status: 504 }) + throw Object.assign(new Error("Transaction confirmation timeout"), { status: 504, tx_hash: hash }) } if (getResult.status === SorobanRpc.Api.GetTransactionStatus.FAILED) { - throw Object.assign(new Error("Soroban transaction failed"), { status: 422 }) + throw Object.assign(new Error("Soroban transaction failed"), { status: 422, tx_hash: hash }) } const returnVal = (getResult as SorobanRpc.Api.GetSuccessfulTransactionResponse).returnValue @@ -213,7 +213,10 @@ router.post("/", async (req: Request, res: Response) => { } catch (err: unknown) { const status = (err as any)?.status ?? 500 const message = err instanceof Error ? err.message : String(err) - res.status(status).json({ error: message }) + const tx_hash = (err as any)?.tx_hash as string | undefined + const body: { error: string; tx_hash?: string } = { error: message } + if (tx_hash) body.tx_hash = tx_hash + res.status(status).json(body) } }) diff --git a/comebackhere-backend/src/routes/treasury.ts b/comebackhere-backend/src/routes/treasury.ts index ec3fe5e..2cca3be 100644 --- a/comebackhere-backend/src/routes/treasury.ts +++ b/comebackhere-backend/src/routes/treasury.ts @@ -57,6 +57,8 @@ router.get("/pending-settlements", async (_req: Request, res: Response) => { approval_weight: s.approval_weight, status: s.status, hold_reason: s.hold_reason, + proposed_tx_hash: s.proposed_tx_hash ?? null, + executed_tx_hash: s.executed_tx_hash ?? null, })), ) } catch (err: unknown) { diff --git a/comebackhere-backend/src/tests/invoices.test.ts b/comebackhere-backend/src/tests/invoices.test.ts index 90952cf..84c43da 100644 --- a/comebackhere-backend/src/tests/invoices.test.ts +++ b/comebackhere-backend/src/tests/invoices.test.ts @@ -184,6 +184,24 @@ describe("createInvoice — Soroban interaction", () => { .rejects.toMatchObject({ message: expect.stringMatching(/submission failed/), status: 422 }) }) + it("throws 422 with tx_hash when getTransaction returns FAILED", async () => { + const client = makeMockClient({ + getAccount: vi.fn().mockResolvedValue(fakeAccount), + simulateTransaction: vi.fn().mockResolvedValue(PARSED_SIM_SUCCESS), + sendTransaction: vi.fn().mockResolvedValue({ status: "PENDING", hash: "txhash-failed" }), + getTransaction: vi.fn().mockResolvedValue({ + status: SorobanRpc.Api.GetTransactionStatus.FAILED, + latestLedger: 1, + latestLedgerCloseTime: 0, + oldestLedger: 1, + oldestLedgerCloseTime: 0, + }), + }) + + await expect(createInvoice(VALID_BODY, client, CONTRACT_ID, SIGNER_SECRET, NETWORK)) + .rejects.toMatchObject({ message: expect.stringMatching(/transaction failed/), status: 422, tx_hash: "txhash-failed" }) + }) + it("throws 504 when confirmation times out", async () => { const client = makeMockClient({ getAccount: vi.fn().mockResolvedValue(fakeAccount), diff --git a/comebackhere-frontend/src/components/InvoicePayment.tsx b/comebackhere-frontend/src/components/InvoicePayment.tsx index b995723..62d5eb9 100644 --- a/comebackhere-frontend/src/components/InvoicePayment.tsx +++ b/comebackhere-frontend/src/components/InvoicePayment.tsx @@ -203,7 +203,7 @@ export function InvoicePayment() {
Expiry - {new Date(invoice.expires_at * 1000).toLocaleString()} + {new Date(invoice.expires_at * 1000).toISOString().replace("T", " ").slice(0, 19) + " UTC"}
diff --git a/comebackhere-frontend/src/components/TransactionHistory.tsx b/comebackhere-frontend/src/components/TransactionHistory.tsx index 437e0c0..d7c47c3 100644 --- a/comebackhere-frontend/src/components/TransactionHistory.tsx +++ b/comebackhere-frontend/src/components/TransactionHistory.tsx @@ -14,6 +14,11 @@ const EVENT_LABELS: Record = { const PAGE_SIZE = 5 +/** Format a millisecond timestamp as a UTC date-time string. */ +function formatUTC(ms: number): string { + return new Date(ms).toISOString().replace("T", " ").slice(0, 19) + " UTC" +} + function buildEvents(invoice: Invoice): TransactionEvent[] { const now = Date.now() const expiresAt = invoice.expires_at * 1000 @@ -186,7 +191,7 @@ export function TransactionHistory({ invoice }: { invoice: Invoice }) {
{EVENT_LABELS[entry.type]} - {new Date(entry.timestamp).toLocaleString()} + {formatUTC(entry.timestamp)}

{entry.description}

diff --git a/frontend/src/components/InvoiceSearchFilter.tsx b/frontend/src/components/InvoiceSearchFilter.tsx index 589ad76..7593725 100644 --- a/frontend/src/components/InvoiceSearchFilter.tsx +++ b/frontend/src/components/InvoiceSearchFilter.tsx @@ -22,6 +22,15 @@ function formatDate(ts: number | null): string { return new Date(ts * 1000).toLocaleDateString(); } +/** Normalise an invoice status value to a plain lowercase string for comparison. + * Handles TypeScript string-enums (whose .valueOf() is the string) and plain + * string unions alike, so the filter works regardless of which type the caller + * passes in. */ +function normaliseStatus(s: unknown): string { + if (s == null) return ""; + return String(s).trim(); +} + export default function InvoiceSearchFilter({ invoices }: Props) { const [query, setQuery] = useState(""); const [statusFilter, setStatusFilter] = useState(""); @@ -34,12 +43,13 @@ export default function InvoiceSearchFilter({ invoices }: Props) { const q = query.trim().toLowerCase(); const fromTs = dateFrom ? new Date(dateFrom).getTime() / 1000 : null; const toTs = dateTo ? new Date(dateTo).getTime() / 1000 + 86400 : null; + const normalisedFilter = normaliseStatus(statusFilter); return invoices.filter((inv) => { if (q && !inv.id.toLowerCase().includes(q) && !inv.merchant.toLowerCase().includes(q)) { return false; } - if (statusFilter && inv.status !== statusFilter) return false; + if (normalisedFilter && normaliseStatus(inv.status) !== normalisedFilter) return false; if (fromTs && inv.created_at !== null && inv.created_at < fromTs) return false; if (toTs && inv.created_at !== null && inv.created_at > toTs) return false; return true; diff --git a/frontend/src/components/SettlementDetail/SettlementDetail.css b/frontend/src/components/SettlementDetail/SettlementDetail.css index 0e21fb3..b7d7aa7 100644 --- a/frontend/src/components/SettlementDetail/SettlementDetail.css +++ b/frontend/src/components/SettlementDetail/SettlementDetail.css @@ -82,6 +82,13 @@ color: #111827; } +.settlement-detail__value--hash { + font-family: monospace; + font-size: 0.8125rem; + color: #1d4ed8; + word-break: break-all; +} + .settlement-detail__progress-section { margin-bottom: 1.5rem; } diff --git a/frontend/src/components/SettlementDetail/SettlementDetail.tsx b/frontend/src/components/SettlementDetail/SettlementDetail.tsx index b84af48..4be418b 100644 --- a/frontend/src/components/SettlementDetail/SettlementDetail.tsx +++ b/frontend/src/components/SettlementDetail/SettlementDetail.tsx @@ -157,6 +157,28 @@ export default function SettlementDetail({ settlement, threshold, signers }: Set Total Signer Weight {totalWeight}
+ {settlement.proposed_tx_hash && ( +
+ Proposal Tx Hash + + {shorten(settlement.proposed_tx_hash)} + +
+ )} + {settlement.executed_tx_hash && ( +
+ Execution Tx Hash + + {shorten(settlement.executed_tx_hash)} + +
+ )}
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 86ab85f..c2d33a3 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -6,6 +6,8 @@ export interface Settlement { approval_weight: number status: 'Pending' | 'Executed' | 'PartiallyExecuted' | 'OnHold' | 'Cancelled' hold_reason: string | null + proposed_tx_hash?: string + executed_tx_hash?: string } export interface SignerInfo {