diff --git a/src/featureDetection.ts b/src/featureDetection.ts index 76396eb..af36fbc 100644 --- a/src/featureDetection.ts +++ b/src/featureDetection.ts @@ -157,3 +157,26 @@ export async function detectContractFeatures( return features; } + +// === Fee-bump support + +/** + * Fee-bump transactions (CAP-0015) were introduced in Stellar protocol + * version 13. Networks on an older base protocol cannot process them. + */ +export const FEE_BUMP_MIN_PROTOCOL_VERSION = 13; + +/** Minimal network info needed to decide fee-bump availability. */ +export interface FeeBumpNetworkInfo { + /** The network's current base protocol version. */ + protocolVersion: number; +} + +/** + * Determine whether a network supports fee-bump transactions, based on its + * base protocol version. No network calls are made; pass a network info object + * (e.g. from Horizon's root endpoint or an RPC `getNetwork` response). + */ +export function detectFeeBumpSupport(networkInfo: FeeBumpNetworkInfo): boolean { + return networkInfo.protocolVersion >= FEE_BUMP_MIN_PROTOCOL_VERSION; +} diff --git a/src/flowVisualizer.ts b/src/flowVisualizer.ts index 33a909f..9c9d2f4 100644 --- a/src/flowVisualizer.ts +++ b/src/flowVisualizer.ts @@ -3,6 +3,13 @@ import { InvoiceFlowFetcherNotRegisteredError } from "./errors.js"; export type InvoiceFlowFetcher = (invoiceId: string) => Promise; +export type FlowDiagramMode = "mermaid" | "ascii"; + +export interface FlowDiagramOptions { + /** Output format. Defaults to "mermaid" (unchanged legacy behavior). */ + mode?: FlowDiagramMode; +} + let invoiceFlowFetcher: InvoiceFlowFetcher | null = null; export function registerInvoiceFlowFetcher(fetcher: InvoiceFlowFetcher): void { @@ -37,7 +44,8 @@ function allocatePayments(recipients: Recipient[], funded: bigint): Map { const fetcher = getInvoice ?? invoiceFlowFetcher; if (!fetcher) { @@ -45,11 +53,20 @@ export async function generateFlowDiagram( } const invoice = await fetcher(invoiceId); - const creatorId = nodeId("creator", invoice.creator); - const invoiceNodeId = nodeId("invoice", invoice.id); const totalPaid = invoice.payments.reduce((sum, payment) => sum + payment.amount, 0n); const funded = totalPaid > invoice.funded ? totalPaid : invoice.funded; const allocations = allocatePayments(invoice.recipients, funded); + + if (options?.mode === "ascii") { + return renderAsciiDiagram(invoice, allocations); + } + + return renderMermaidDiagram(invoice, allocations); +} + +function renderMermaidDiagram(invoice: Invoice, allocations: Map): string { + const creatorId = nodeId("creator", invoice.creator); + const invoiceNodeId = nodeId("invoice", invoice.id); const lines = [ "flowchart LR", ` ${creatorId}["Creator: ${nodeLabel(invoice.creator)}"]`, @@ -86,3 +103,22 @@ export async function generateFlowDiagram( return lines.join("\n"); } + +// === ASCII rendering + +function renderAsciiDiagram(invoice: Invoice, allocations: Map): string { + const lines = [ + `[Creator: ${invoice.creator}]`, + ` --> [Invoice ${invoice.id}]`, + ]; + + for (const [index, recipient] of invoice.recipients.entries()) { + const paid = allocations.get(recipient.address) ?? 0n; + const status = paid >= recipient.amount ? "completed" : "pending"; + lines.push( + ` --> [Recipient ${index + 1}: ${recipient.address}] (${amountLabel(paid)} / ${amountLabel(recipient.amount)}) ${status}` + ); + } + + return lines.join("\n"); +} diff --git a/test/featureDetection.test.ts b/test/featureDetection.test.ts index cc0174a..762ea27 100644 --- a/test/featureDetection.test.ts +++ b/test/featureDetection.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { detectContractFeatures, clearFeatureCache, + detectFeeBumpSupport, } from "../src/featureDetection.js"; import type { ContractFeatures } from "../src/types.js"; @@ -173,4 +174,16 @@ describe("detectContractFeatures", () => { // archival (5th, odd = ok) -> true expect(features.archival).toBe(true); }); +}); + +describe("detectFeeBumpSupport", () => { + it("returns true for a network whose base protocol supports fee bumps", () => { + expect(detectFeeBumpSupport({ protocolVersion: 13 })).toBe(true); + expect(detectFeeBumpSupport({ protocolVersion: 21 })).toBe(true); + }); + + it("returns false for a network on a protocol older than fee-bump support", () => { + expect(detectFeeBumpSupport({ protocolVersion: 12 })).toBe(false); + expect(detectFeeBumpSupport({ protocolVersion: 10 })).toBe(false); + }); }); \ No newline at end of file diff --git a/test/flowVisualizer.test.ts b/test/flowVisualizer.test.ts index f1d06d8..2622f80 100644 --- a/test/flowVisualizer.test.ts +++ b/test/flowVisualizer.test.ts @@ -30,4 +30,47 @@ describe("generateFlowDiagram", () => { expect(diagram).toContain("class recipient_1_GRECIPIENTA completed"); expect(diagram).toContain("class recipient_2_GRECIPIENTB pending"); }); + + it("renders a multi-line ASCII diagram with [label] nodes and --> edges when mode is 'ascii'", async () => { + const invoice: Invoice = { + id: "inv-1", + creator: "GCREATOR", + recipients: [ + { address: "GRECIPIENTA", amount: 100n }, + { address: "GRECIPIENTB", amount: 50n }, + ], + token: "CUSDC", + deadline: 1_900_000_000, + funded: 120n, + status: "Pending", + payments: [{ payer: "GPAYER", amount: 120n }], + }; + + const diagram = await generateFlowDiagram("inv-1", async () => invoice, { + mode: "ascii", + }); + + expect(diagram.split("\n").length).toBeGreaterThan(1); + expect(diagram).toContain("[Creator: GCREATOR]"); + expect(diagram).toContain("--> [Invoice inv-1]"); + expect(diagram).toContain("--> [Recipient 1: GRECIPIENTA] (100 / 100) completed"); + expect(diagram).toContain("--> [Recipient 2: GRECIPIENTB] (20 / 50) pending"); + expect(diagram).not.toContain("flowchart LR"); + }); + + it("defaults to Mermaid output when no mode is given", async () => { + const invoice: Invoice = { + id: "inv-1", + creator: "GCREATOR", + recipients: [{ address: "GRECIPIENTA", amount: 100n }], + token: "CUSDC", + deadline: 1_900_000_000, + funded: 100n, + status: "Pending", + payments: [{ payer: "GPAYER", amount: 100n }], + }; + + const diagram = await generateFlowDiagram("inv-1", async () => invoice); + expect(diagram).toContain("flowchart LR"); + }); });