From 504c3f55a6604e03511f0f7cff60d52c25a1e606 Mon Sep 17 00:00:00 2001 From: Gbola puku Date: Mon, 28 Sep 2026 15:53:39 +0000 Subject: [PATCH] feat: Soroban contract authorization tree visualization (#850) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add nested authorization entry visualization to the Soroban invocation UI so developers can verify required signers and their authorized invocation trees before signing. Changes: - stellar.ts: extend ContractSimulationResult with authEntries field; add serializeAuthEntry/serializeAuthInvocation helpers that convert XDR SorobanAuthorizationEntry values to plain JSON-safe objects - AuthorizationTree.tsx: new component rendering the auth entry tree — credentials badge (source account vs address), nonce/expiry ledger for address credentials, collapsible invocation nodes with sub-invocation counts, security notice, ARIA tree roles - ContractInteraction.tsx: import and render AuthorizationTree after simulation result; falls back silently when authEntries is empty - tests/unit/components/AuthorizationTree.test.tsx: 21 tests covering primary flow, boundary cases (empty, nested, multi-signer, unknown function types), and failure cases (null/undefined/non-array input, malformed entries) - docs/features/soroban-auth-tree.md: feature guide with usage, security notes, compatibility table, data model reference, and component API - README.md: add feature guide entry Closes #850 --- README.md | 1 + docs/features/soroban-auth-tree.md | 89 ++++ .../dashboard/AuthorizationTree.tsx | 434 ++++++++++++++++++ .../dashboard/ContractInteraction.tsx | 2 + src/lib/stellar.ts | 124 +++++ .../components/AuthorizationTree.test.tsx | 285 ++++++++++++ 6 files changed, 935 insertions(+) create mode 100644 docs/features/soroban-auth-tree.md create mode 100644 src/components/dashboard/AuthorizationTree.tsx create mode 100644 tests/unit/components/AuthorizationTree.test.tsx diff --git a/README.md b/README.md index 87268c82..f061e43d 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,7 @@ This keeps user-specific and operational endpoints behind explicit authenticatio - **Scheduled report delivery via webhooks (#869)** — authenticated HMAC/bearer delivery of analytics summaries with retries: [docs/features/report-webhook-delivery.md](docs/features/report-webhook-delivery.md). - **Transaction Builder i18n (#878)** — complete locale coverage of builder strings across all nine languages: [docs/features/builder-i18n.md](docs/features/builder-i18n.md). - **Mutation testing gate for fee math (#895)** — Stryker score gate on stroop conversion and fee estimation: [docs/features/mutation-testing-gate.md](docs/features/mutation-testing-gate.md). Run locally with `pnpm run test:mutation:feemath`. +- **Soroban contract authorization tree (#850)** — nested authorization entry visualization in the Soroban invocation UI showing required signers and their authorized invocation trees: [docs/features/soroban-auth-tree.md](docs/features/soroban-auth-tree.md). ## Canary Deployment Health Probes diff --git a/docs/features/soroban-auth-tree.md b/docs/features/soroban-auth-tree.md new file mode 100644 index 00000000..55d7b4d0 --- /dev/null +++ b/docs/features/soroban-auth-tree.md @@ -0,0 +1,89 @@ +# Soroban Contract Authorization Tree (#850) + +The Soroban invocation UI now renders an **Authorization Requirements** panel immediately after each simulation. This tree lets developers verify which accounts must sign and what invocations they are authorizing—including cross-contract calls—before committing a transaction. + +## Overview + +When you simulate a Soroban contract call, the Stellar RPC returns a list of `SorobanAuthorizationEntry` values embedded in the simulation result. Each entry describes: + +- **Who must sign** — the `SorobanCredentials` field, which is either the transaction's source account or an explicit Stellar address. +- **What they are authorizing** — the `rootInvocation` tree, which may recursively contain sub-invocations representing cross-contract calls. + +The `AuthorizationTree` component reads these entries from the simulation result, serializes them to plain JSON-safe values, and renders the tree visually in the Contract Interaction panel. + +## Using the feature + +1. Open the **Contract Interaction** panel and enter a valid contract ID, function name, and arguments. +2. Click **Simulate**. +3. If the contract function requires any authorization, the **Authorization Requirements** panel appears below the simulation output. +4. Each signer entry shows: + - A color-coded credentials badge (green for source-account signers, orange for explicit address signers). + - The nonce and signature-expiration ledger for address credentials. + - An expandable invocation tree with every function name and contract address the signer is authorizing, including nested cross-contract calls. + +## Security guidance + +- **Always inspect before signing.** The authorization tree shows the full scope of what a signer is committing to. An entry that lists only the expected top-level function is very different from one with unexpected sub-invocations (potential rug-pull patterns). +- **Address credentials with an expiry ledger close to the current ledger** should be treated with extra caution—the window for replay may still be open. +- **Source-account credentials** authorize the transaction signer directly. If you see a source-account entry for a function you did not expect to call, abort and investigate the contract. +- All displayed values originate from XDR data returned by the Soroban RPC server. React's default string escaping prevents XSS; no raw HTML is rendered. + +## Compatibility + +| Requirement | Notes | +|---|---| +| Stellar SDK | `@stellar/stellar-sdk` ≥ 17 (already required by this project) | +| Soroban Protocol | Protocol 20 and later (the `auth` field on `SimulateHostFunctionResult` was introduced in Protocol 20) | +| Browser | Any modern browser; no additional APIs required | +| Network | Works on Testnet, Mainnet, and custom networks | + +## Migration notes + +- No breaking changes. The `authEntries` field was **added** to `ContractSimulationResult`; existing callers that don't reference it are unaffected. +- The `AuthorizationTree` component renders `null` when `authEntries` is empty or absent, so rendering it unconditionally after a simulation result is safe. +- Simulations of non-invocation transactions (e.g. restoring a footprint) return no `result` object, and therefore no auth entries. The panel will not appear for those simulations. + +## Data model reference + +The serialized types used by `AuthorizationTree` are exported from `src/lib/stellar.ts`: + +```typescript +// Top-level entry +interface SerializedAuthEntry { + credentials: SerializedAuthCredentials; + rootInvocation: SerializedAuthInvocation; +} + +// Discriminated union for credentials +type SerializedAuthCredentials = + | { type: 'source_account' } + | { + type: 'address'; + address: string; // Stellar G... address + nonce: string; // i64 nonce as decimal string + signatureExpirationLedger: number; + }; + +// Recursive invocation node +interface SerializedAuthInvocation { + functionType: 'contract_fn' | 'create_contract' | 'create_contract_v2' | 'unknown'; + contractAddress: string; // empty for host-function variants + functionName: string; // empty for host-function variants + args: unknown[]; // serialized ScVal values + subInvocations: SerializedAuthInvocation[]; +} +``` + +## Component API + +```tsx +import AuthorizationTree from 'src/components/dashboard/AuthorizationTree'; + + +``` + +The component accepts `authEntries?: SerializedAuthEntry[]` and renders nothing (`null`) when the array is empty or the prop is absent/null/non-array. diff --git a/src/components/dashboard/AuthorizationTree.tsx b/src/components/dashboard/AuthorizationTree.tsx new file mode 100644 index 00000000..aca27cc2 --- /dev/null +++ b/src/components/dashboard/AuthorizationTree.tsx @@ -0,0 +1,434 @@ +/** + * AuthorizationTree — #850 + * + * Renders the nested authorization entries returned by a Soroban simulation. + * Each entry describes which signer is required and the full tree of + * contract invocations they must authorize, including cross-contract calls. + * + * Props: + * authEntries — Array of SerializedAuthEntry from simulateContractCall. + * Renders nothing when the array is empty. + * className — Optional CSS class forwarded to the outer wrapper. + * + * Security note: + * Values displayed here come from XDR data returned by the Soroban RPC. + * All values are treated as untrusted display text and rendered through + * React's normal string escaping — no dangerouslySetInnerHTML is used. + * Signers should independently verify authorization entries before signing. + */ + +import React, { useState } from 'react'; +import { Shield, ChevronRight, ChevronDown, Key, User, Lock, Code } from 'lucide-react'; +import type { SerializedAuthEntry, SerializedAuthInvocation } from '../../lib/stellar'; + +// ─── Internal helpers ───────────────────────────────────────────────────────── + +function truncateAddress(address: string, leadingChars = 8, trailingChars = 6): string { + if (!address || address.length <= leadingChars + trailingChars + 3) return address; + return `${address.slice(0, leadingChars)}…${address.slice(-trailingChars)}`; +} + +// ─── Sub-components ─────────────────────────────────────────────────────────── + +interface InvocationNodeProps { + invocation: SerializedAuthInvocation; + depth?: number; +} + +/** + * Renders a single invocation node and, recursively, all its sub-invocations. + * Nodes at depth > 0 are indented with a connecting tree-line gutter. + */ +function InvocationNode({ invocation, depth = 0 }: InvocationNodeProps) { + const [expanded, setExpanded] = useState(true); + const hasChildren = invocation.subInvocations.length > 0; + + const functionLabel: string = (() => { + if (invocation.functionType === 'contract_fn') { + return invocation.functionName || '(unknown function)'; + } + if (invocation.functionType === 'create_contract') return 'create_contract'; + if (invocation.functionType === 'create_contract_v2') return 'create_contract_v2'; + return '(unknown)'; + })(); + + const contractLabel = + invocation.functionType === 'contract_fn' && invocation.contractAddress + ? truncateAddress(invocation.contractAddress) + : null; + + return ( +
0 ? '20px' : '0', + borderLeft: depth > 0 ? '1px solid var(--border)' : 'none', + marginLeft: depth > 0 ? '10px' : '0', + }} + > + {/* Node header row */} +
setExpanded((v) => !v) : undefined} + > + {/* Expand/collapse toggle */} + + +