diff --git a/.changelog/unreleased/609-dx1-docs-resources.md b/.changelog/unreleased/609-dx1-docs-resources.md new file mode 100644 index 00000000..3df5ca68 --- /dev/null +++ b/.changelog/unreleased/609-dx1-docs-resources.md @@ -0,0 +1,8 @@ +--- +type: added +area: docs +pr: 615 +breaking: false +--- + +Added the first SO4 documentation resource set, a shared landing-page FAQ source, and an alphabetical perpetuals and Stellar glossary. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c350ca03..0fbd81c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: ${{ runner.os }}-bun- - name: Install dependencies - run: bun install + run: bun install - name: Lint run: bun lint @@ -45,6 +45,12 @@ jobs: - name: Design token check run: bun run check:tokens + - name: Documentation content check + run: bun run check:content + + - name: Documentation link and FAQ drift checks + run: bun run check:links && bun run --cwd apps/docs check:faq + - name: Test run: bun run test diff --git a/apps/docs/content/concepts/funding-and-fees.mdx b/apps/docs/content/concepts/funding-and-fees.mdx new file mode 100644 index 00000000..9ff0deec --- /dev/null +++ b/apps/docs/content/concepts/funding-and-fees.mdx @@ -0,0 +1,12 @@ +--- +title: Funding and fees +description: How funding, transaction charges, borrowing costs, and price impact affect a perpetual position. +updated: 2026-08-24 +status: stable +--- + +Trading cost is more than a displayed fee because time, market balance, execution, and the Stellar transaction can each affect the result. + +Funding transfers value between long and short positions. Price impact changes the execution estimate as a trade changes market exposure. Open interest describes positions that remain active. + +Always review the transaction simulation and wallet prompt before signing. See [risk](/concepts/risk) for failure modes. diff --git a/apps/docs/content/concepts/liquidation.mdx b/apps/docs/content/concepts/liquidation.mdx new file mode 100644 index 00000000..c8f2fb8f --- /dev/null +++ b/apps/docs/content/concepts/liquidation.mdx @@ -0,0 +1,12 @@ +--- +title: Liquidation +description: How maintenance margin and changing position value determine whether a leveraged position may be liquidated. +updated: 2026-08-24 +status: stable +--- + +Liquidation is the protocol process that closes an under-collateralised position before its losses exceed the collateral available to support it. + +The displayed liquidation price is an estimate. A position becomes eligible when it breaches maintenance margin; fees, funding, price impact, and fast markets can move the actual result. + +Read the broader [risk overview](/concepts/risk) before opening a leveraged position. diff --git a/apps/docs/content/concepts/risk.mdx b/apps/docs/content/concepts/risk.mdx new file mode 100644 index 00000000..58290ffb --- /dev/null +++ b/apps/docs/content/concepts/risk.mdx @@ -0,0 +1,12 @@ +--- +title: Risk +description: A concise overview of market, liquidation, oracle, contract, network, interface, and custody risks in SO4. +updated: 2026-08-24 +status: stable +--- + +Using SO4 can result in partial or total loss of collateral, including through mechanisms outside the interface's control. + +A leveraged position can cross its maintenance margin as prices move and be closed near its liquidation price. Other risks include volatile prices, adverse price impact, funding, unavailable liquidity, stale or incorrect oracle data, smart-contract defects, Stellar or RPC disruption, wallet compromise, and interface outages. + +The interface prepares transactions; your wallet authorises them and the protocol executes them. Review the [terms and plain-language disclosure](/resources/terms) before using the interface. diff --git a/apps/docs/content/meta.json b/apps/docs/content/meta.json new file mode 100644 index 00000000..835dd920 --- /dev/null +++ b/apps/docs/content/meta.json @@ -0,0 +1,22 @@ +{ + "sections": [ + { + "label": "Concepts", + "pages": [ + "concepts/risk", + "concepts/funding-and-fees", + "concepts/liquidation" + ] + }, + { "label": "Reference", "pages": ["reference/glossary"] }, + { + "label": "Resources", + "pages": [ + "resources/terms", + "resources/faq", + "resources/roadmap", + "resources/changelog" + ] + } + ] +} diff --git a/apps/docs/content/reference/glossary.mdx b/apps/docs/content/reference/glossary.mdx new file mode 100644 index 00000000..54fe47f5 --- /dev/null +++ b/apps/docs/content/reference/glossary.mdx @@ -0,0 +1,48 @@ +--- +title: Glossary +description: Alphabetical definitions linking perpetual-markets and Stellar or Soroban terms to their detailed documentation. +updated: 2026-08-24 +status: stable +--- + +Definitions here are short pointers. Follow each link for mechanics and context. + +## Authorisation entry {#authorisation-entry} + +A Soroban authorisation entry identifies an invocation a wallet or contract has permitted within a transaction. [Read about transaction risk](/concepts/risk). + +## Footprint {#footprint} + +A Soroban footprint declares the ledger entries a smart-contract transaction expects to read or write. [Read about transaction risk](/concepts/risk). + +## Funding {#funding} + +Funding is a recurring transfer between long and short positions intended to keep a perpetual market near its reference price. [Read about funding and fees](/concepts/funding-and-fees). + +## Ledger {#ledger} + +A ledger is a confirmed Stellar network state produced by consensus at a particular sequence number. [Read about protocol risk](/concepts/risk). + +## Liquidation price {#liquidation-price} + +Liquidation price is the estimated market price at which a position no longer satisfies its maintenance-margin requirement. [Read about liquidation](/concepts/liquidation). + +## Maintenance margin {#maintenance-margin} + +Maintenance margin is the minimum collateral value a leveraged position must retain to avoid liquidation. [Read about liquidation](/concepts/liquidation). + +## Open interest {#open-interest} + +Open interest is the total notional value of positions that remain open in a market. [Read about funding and fees](/concepts/funding-and-fees). + +## Price impact {#price-impact} + +Price impact is the execution-price change caused by a trade changing the market's balance of liquidity and exposure. [Read about funding and fees](/concepts/funding-and-fees). + +## SAC {#sac} + +A Stellar Asset Contract is the Soroban token interface through which a classic Stellar asset can be used by smart contracts. [Read about protocol risk](/concepts/risk). + +## Trustline {#trustline} + +A trustline is a Stellar ledger entry through which an account opts into holding a specific issued asset and records its balance and limits. [Read about protocol risk](/concepts/risk). diff --git a/apps/docs/content/resources/changelog.mdx b/apps/docs/content/resources/changelog.mdx new file mode 100644 index 00000000..319da84c --- /dev/null +++ b/apps/docs/content/resources/changelog.mdx @@ -0,0 +1,14 @@ +--- +title: Changelog +description: How documentation updates relate to the product release history published on the SO4 interface. +updated: 2026-08-24 +status: stable +--- + +The canonical SO4 product release history lives at [so4.market/changelog](https://so4.market/changelog). + +This documentation page explains releases and links readers to that history; it does not maintain a second copy. Product and protocol changes are announced on the interface changelog. Documentation-only corrections may be visible in repository history without becoming a product release. + +## Documentation site release + +**Shipped — 24 August 2026.** SO4 published its first documentation resource set, including the terms draft, shared FAQ, glossary, roadmap, and this changelog pointer. diff --git a/apps/docs/content/resources/faq.mdx b/apps/docs/content/resources/faq.mdx new file mode 100644 index 00000000..118496d2 --- /dev/null +++ b/apps/docs/content/resources/faq.mdx @@ -0,0 +1,37 @@ +--- +title: Frequently asked questions +description: Short answers to common SO4 trading, custody, fees, liquidation, wallet, and availability questions. +updated: 2026-08-24 +status: stable +landing: [what-is-so4, self-custody, liquidation, fees, availability] +--- + +These answers point to the detailed documentation that should guide a decision or troubleshooting step. + +## What is SO4? {#what-is-so4} + +SO4 is a perpetual-markets protocol on Stellar with a web interface for preparing and submitting transactions. [Read the protocol risk overview](/concepts/risk). + +## Do I keep custody of my assets? {#self-custody} + +The interface does not hold wallet keys; your wallet signs transactions and protocol contracts account for deposited collateral. [Read the interface and protocol distinction](/resources/terms#interface-and-protocol). + +## What can cause liquidation? {#liquidation} + +A position becomes liquidatable when its remaining collateral no longer meets the protocol's maintenance-margin requirement. [Read the liquidation mechanics](/concepts/liquidation). + +## What fees will I pay? {#fees} + +Costs can include opening, closing, funding, borrowing, network, and price-impact amounts, depending on the transaction. [Read about funding and fees](/concepts/funding-and-fees). + +## Is the interface always available? {#availability} + +No; deployments, incidents, RPC failures, congestion, and local connectivity can interrupt the website even while the protocol remains deployed. [Read the risk overview](/concepts/risk). + +## Is this financial advice? {#financial-advice} + +No; the interface and documentation provide operational information, not a recommendation to trade. [Read the terms](/resources/terms#no-financial-advice). + +## Why did my transaction fail? {#failed-transaction} + +A transaction can fail because of simulation errors, insufficient balance, stale state, authorization, slippage, or network conditions. [Review protocol risks](/concepts/risk). diff --git a/apps/docs/content/resources/roadmap.mdx b/apps/docs/content/resources/roadmap.mdx new file mode 100644 index 00000000..69f0a03d --- /dev/null +++ b/apps/docs/content/resources/roadmap.mdx @@ -0,0 +1,24 @@ +--- +title: Roadmap +description: The current status of shipped and planned SO4 interface, protocol, and documentation work. +updated: 2026-08-24 +status: stable +--- + +The roadmap separates work that has shipped from plans that remain subject to design, testing, and maintainer approval. + +## Shipped + +- **Shipped — 24 August 2026:** The first documentation site and its resource pages. Release details appear in the [changelog](/resources/changelog). +- **Shipped — 24 August 2026:** A shared FAQ source for documentation and the landing interface. Release details appear in the [changelog](/resources/changelog). + +## In progress + +- **In progress — undated:** Complete and review the remaining concept, guide, developer, and generated-reference pages defined by the DX1 content map. +- **In progress — undated:** Obtain legal review for the eligibility and jurisdiction language in the [terms draft](/resources/terms). + +## Planned + +- **Planned — undated:** Publish verified audit and responsible-disclosure information after maintainers confirm the reporting channel and current audit status. + +Dates are commitments only when an item explicitly says so. Undated work is intentionally not presented as a delivery promise. diff --git a/apps/docs/content/resources/terms.mdx b/apps/docs/content/resources/terms.mdx new file mode 100644 index 00000000..9b488886 --- /dev/null +++ b/apps/docs/content/resources/terms.mdx @@ -0,0 +1,44 @@ +--- +title: Terms of use +description: Draft terms governing access to the SO4 web interface and a plain-language disclosure of trading risks. +updated: 2026-08-24 +status: draft +--- + +> **Pending legal review.** This draft is provided for maintainer and counsel review. It is not final legal language and must not be represented as legally approved. + +These terms explain access to the SO4 web interface. They do not change how the independent SO4 protocol operates on Stellar. + +## Interface and protocol + +The **interface** is the website maintained by SO4 Labs that helps a user prepare and submit transactions. The **protocol** is a set of independently deployed smart contracts on Stellar. The interface does not take custody of assets, execute protocol transactions itself, or control transactions after they are submitted. The protocol may remain accessible through other software even if this interface is unavailable. + +## Risk disclosure + +Perpetual trading can cause rapid and total loss of posted collateral. Leverage, liquidation, oracle failure, smart-contract defects, network congestion, volatile markets, and unavailable interfaces can increase that risk. Transactions are generally irreversible, and displayed estimates may differ from execution results. Read [How risk works](/concepts/risk) before using the interface. + +## No financial advice + +Information shown by the interface and documentation is educational and operational only. It is not financial, investment, legal, tax, or accounting advice, and no content is a recommendation to enter a transaction. Users are responsible for deciding whether a transaction is appropriate for them and for obtaining professional advice where needed. + +## Eligibility and jurisdiction + +> **Pending legal sign-off.** Maintainers must confirm restricted jurisdictions, minimum age, sanctions screening, and any professional- or qualified-user requirements before this section is approved. + +You may use the interface only where doing so is lawful, if you have legal capacity to accept these terms, and if you are not subject to applicable sanctions or other restrictions. You are responsible for complying with the laws that apply to you. Access from a particular location does not mean that SO4 Labs offers or solicits a regulated service there. + +## Acceptable use + +Do not use the interface to violate law, evade sanctions, interfere with other users, introduce malicious code, or misrepresent your identity or authority. Access may be limited when reasonably necessary to protect the interface, comply with law, or respond to security incidents. Such limits do not pause or alter the protocol. + +## Availability and warranties + +The interface is provided on an “as available” basis. To the extent permitted by law, no promise is made that it will be uninterrupted, accurate, secure, or fit for a particular purpose. Nothing here excludes a right or liability that applicable law does not permit parties to exclude. + +## Changes + +**Effective date: 24 August 2026.** Material changes will be announced in the [SO4 changelog](/resources/changelog) before or when they take effect. The effective date above will be updated. Continued use after a change takes effect constitutes acceptance only to the extent permitted by applicable law. + +## Contact + +Questions about this draft should be raised through the project’s [GitHub repository](https://github.com/SO4-Markets/interface). Security reports should follow the project security policy rather than a public issue. diff --git a/apps/docs/package.json b/apps/docs/package.json new file mode 100644 index 00000000..8812c41d --- /dev/null +++ b/apps/docs/package.json @@ -0,0 +1,24 @@ +{ + "name": "docs", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "build": "bun run scripts/build.ts", + "dev": "bun run scripts/build.ts", + "lint": "bun run check:content", + "format": "prettier --write \"content/**/*.mdx\" \"scripts/**/*.ts\"", + "typecheck": "tsc --noEmit", + "test": "bun test", + "test:coverage": "bun test --coverage", + "check:content": "bun run scripts/check-content.ts", + "check:links": "bun run scripts/check-links.ts", + "generate:faq": "bun run scripts/generate-faq.ts", + "check:faq": "bun run scripts/generate-faq.ts --check" + }, + "devDependencies": { + "@types/bun": "^1.3.0", + "prettier": "^3.8.1", + "typescript": "^5.9.3" + } +} diff --git a/apps/docs/scripts/build.ts b/apps/docs/scripts/build.ts new file mode 100644 index 00000000..5a9293f5 --- /dev/null +++ b/apps/docs/scripts/build.ts @@ -0,0 +1,65 @@ +import { mkdir } from "node:fs/promises" +import { join } from "node:path" + +import { $ } from "bun" +import { appRoot, loadPages } from "./content" + +await $`bun run ${join(appRoot, "scripts/check-content.ts")}` +await $`bun run ${join(appRoot, "scripts/check-links.ts")}` +await $`bun run ${join(appRoot, "scripts/generate-faq.ts")} --check` + +const pages = await loadPages() +const outputRoot = join(appRoot, ".output/public") + +function escape(value: string) { + return value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") +} + +function renderInline(value: string) { + return value + .replace( + /([^<]+)<\/Term>/g, + '$2', + ) + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') + .replace(/\*\*([^*]+)\*\*/g, "$1") + .replace(/`([^`]+)`/g, "$1") +} + +function render(body: string) { + const blocks = body.split(/\n\n+/) + return blocks + .map((block) => { + const heading = block.match(/^## (.+?)(?: \{#([a-z0-9-]+)\})?$/) + if (heading) { + const id = + heading[2] ?? + heading[1] + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, "") + return `

${escape(heading[1])}

` + } + if (block.startsWith("> ")) + return `` + if (block.startsWith("- ")) + return `` + return `

${renderInline(block)}

` + }) + .join("\n") +} + +for (const page of pages) { + const directory = join(outputRoot, page.route.slice(1)) + await mkdir(directory, { recursive: true }) + const html = `${escape(page.frontmatter.title)} · SO4 docs
SO4 docsOpen interface

${escape(page.frontmatter.title)}

${render(page.body)}
` + await Bun.write(join(directory, "index.html"), html) +} + +console.log(`Built ${pages.length} static documentation routes.`) diff --git a/apps/docs/scripts/check-content.ts b/apps/docs/scripts/check-content.ts new file mode 100644 index 00000000..a0a61882 --- /dev/null +++ b/apps/docs/scripts/check-content.ts @@ -0,0 +1,65 @@ +import { readFile } from "node:fs/promises" +import { join } from "node:path" + +import { contentRoot, headingEntries, loadPages } from "./content" + +const pages = await loadPages() +const errors: Array = [] +const routes = new Set(pages.map((page) => page.route)) + +for (const page of pages) { + const { description, status, title, updated } = page.frontmatter + if (!title || title.length > 60) + errors.push(`${page.route}: title must be 1-60 characters`) + if (!description || description.length < 50 || description.length > 160) { + errors.push(`${page.route}: description must be 50-160 characters`) + } + if (!/^\d{4}-\d{2}-\d{2}$/.test(updated)) + errors.push(`${page.route}: updated must be ISO date`) + if (!["stable", "beta", "draft"].includes(status)) + errors.push(`${page.route}: invalid status`) +} + +const meta = JSON.parse( + await readFile(join(contentRoot, "meta.json"), "utf8"), +) as { + sections: Array<{ label: string; pages: Array }> +} +const navRoutes = meta.sections.flatMap((section) => + section.pages.map((page) => `/${page}`), +) +for (const route of navRoutes) + if (!routes.has(route)) errors.push(`sidebar references missing ${route}`) +for (const route of routes) + if (!navRoutes.includes(route)) errors.push(`orphan page ${route}`) + +const glossary = pages.find((page) => page.route === "/reference/glossary") +if (!glossary) { + errors.push("missing glossary") +} else { + const entries = headingEntries(glossary.body) + const titles = entries.map((entry) => entry.title) + const sorted = [...titles].sort((a, b) => + a.localeCompare(b, "en", { sensitivity: "base" }), + ) + if (titles.some((title, index) => title !== sorted[index])) + errors.push("glossary is not alphabetical") + for (const entry of entries) { + const link = entry.answer.match(/\]\((\/[a-z0-9/#-]+)\)/)?.[1] + if (!link) errors.push(`glossary#${entry.id}: missing onward link`) + else if (!routes.has(link.split("#")[0])) + errors.push(`glossary#${entry.id}: missing page ${link}`) + } +} + +const termPages = pages.filter((page) => page.body.includes(" must be demonstrated on at least three pages") + +if (errors.length) { + console.error(errors.join("\n")) + process.exit(1) +} +console.log( + `Content check passed: ${pages.length} pages, zero orphans, alphabetical glossary.`, +) diff --git a/apps/docs/scripts/check-links.ts b/apps/docs/scripts/check-links.ts new file mode 100644 index 00000000..ade25f9e --- /dev/null +++ b/apps/docs/scripts/check-links.ts @@ -0,0 +1,39 @@ +import { headingEntries, internalLinks, loadPages } from "./content" + +const pages = await loadPages() +const routeMap = new Map(pages.map((page) => [page.route, page])) +const errors: Array = [] + +for (const page of pages) { + for (const link of internalLinks(page.body)) { + const [route, anchor] = link.split("#") + const target = routeMap.get(route) + if (!target) { + errors.push(`${page.route}: broken link ${link}`) + continue + } + if (anchor) { + const explicit = new Set( + headingEntries(target.body).map((entry) => entry.id), + ) + const generated = new Set( + [...target.body.matchAll(/^## (.+)$/gm)].map((match) => + match[1] + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, ""), + ), + ) + if (!explicit.has(anchor) && !generated.has(anchor)) + errors.push(`${page.route}: broken anchor ${link}`) + } + } +} + +if (errors.length) { + console.error(errors.join("\n")) + process.exit(1) +} +console.log( + `Link check passed: ${pages.length} pages, zero broken internal links.`, +) diff --git a/apps/docs/scripts/content.test.ts b/apps/docs/scripts/content.test.ts new file mode 100644 index 00000000..92178cf9 --- /dev/null +++ b/apps/docs/scripts/content.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "bun:test" + +import { headingEntries, internalLinks, parsePage } from "./content" + +describe("documentation content helpers", () => { + test("parses frontmatter and stable heading anchors", () => { + const page = parsePage( + "/tmp/content/resources/example.mdx", + "---\ntitle: Example\ndescription: A sufficiently descriptive example page for the content parser test.\nupdated: 2026-08-24\nstatus: stable\nlanding: [one]\n---\n## One {#one}\n\nAnswer [link](/concepts/risk).", + ) + expect(page.frontmatter.landing).toEqual(["one"]) + expect(headingEntries(page.body)[0]?.id).toBe("one") + expect(internalLinks(page.body)).toEqual(["/concepts/risk"]) + }) +}) diff --git a/apps/docs/scripts/content.ts b/apps/docs/scripts/content.ts new file mode 100644 index 00000000..c7dd1b1b --- /dev/null +++ b/apps/docs/scripts/content.ts @@ -0,0 +1,93 @@ +import { readdir } from "node:fs/promises" +import { join, relative, resolve } from "node:path" + +export const appRoot = resolve(import.meta.dir, "..") +export const contentRoot = join(appRoot, "content") + +export type Frontmatter = { + title: string + description: string + updated: string + status: "stable" | "beta" | "draft" + landing?: Array +} + +export type Page = { + file: string + route: string + frontmatter: Frontmatter + body: string +} + +async function walk(directory: string): Promise> { + const entries = await readdir(directory, { withFileTypes: true }) + const files = await Promise.all( + entries.map((entry) => { + const path = join(directory, entry.name) + return entry.isDirectory() ? walk(path) : Promise.resolve([path]) + }), + ) + return files.flat() +} + +function parseValue(value: string): string | Array { + if (value.startsWith("[") && value.endsWith("]")) { + return value + .slice(1, -1) + .split(",") + .map((item) => item.trim()) + .filter(Boolean) + } + return value +} + +export function parsePage(file: string, source: string): Page { + const match = source.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/) + if (!match) throw new Error(`${file}: missing frontmatter`) + + const values: Record> = {} + for (const line of match[1].split("\n")) { + const separator = line.indexOf(":") + if (separator < 1) + throw new Error(`${file}: invalid frontmatter line: ${line}`) + values[line.slice(0, separator)] = parseValue( + line.slice(separator + 1).trim(), + ) + } + + return { + file, + route: `/${relative(contentRoot, file) + .replace(/\.mdx$/, "") + .replaceAll("\\", "/")}`, + frontmatter: values as Frontmatter, + body: match[2].trim(), + } +} + +export async function loadPages(): Promise> { + const files = (await walk(contentRoot)).filter((file) => + file.endsWith(".mdx"), + ) + return Promise.all( + files.map(async (file) => parsePage(file, await Bun.file(file).text())), + ) +} + +export function headingEntries(body: string) { + return [ + ...body.matchAll( + /^## (.+?) \{#([a-z0-9-]+)\}\n\n([\s\S]*?)(?=\n\n## |$)/gm, + ), + ].map(([, title, id, answer]) => ({ title, id, answer: answer.trim() })) +} + +export function internalLinks(body: string) { + const markdown = [...body.matchAll(/\]\((\/[a-z0-9/#-]+)\)/gi)].map( + (match) => match[1], + ) + const terms = [...body.matchAll(//g)].map( + (match) => `/reference/glossary#${match[1]}`, + ) + return [...markdown, ...terms] +} diff --git a/apps/docs/scripts/generate-faq.ts b/apps/docs/scripts/generate-faq.ts new file mode 100644 index 00000000..e4fe300b --- /dev/null +++ b/apps/docs/scripts/generate-faq.ts @@ -0,0 +1,46 @@ +import { resolve } from "node:path" + +import { headingEntries, loadPages } from "./content" + +const faq = (await loadPages()).find((page) => page.route === "/resources/faq") +if (!faq) throw new Error("FAQ source is missing") +const landing = new Set(faq.frontmatter.landing ?? []) +const entries = headingEntries(faq.body) + .filter((entry) => landing.has(entry.id)) + .map((entry) => { + const link = entry.answer.match(/\[([^\]]+)\]\(([^)]+)\)\.$/) + if (!link) + throw new Error(`FAQ ${entry.id} must end with one documentation link`) + return { + id: entry.id, + question: entry.title, + answer: entry.answer.slice(0, link.index).trim(), + linkLabel: link[1], + href: link[2], + } + }) + +if (entries.length !== landing.size) + throw new Error("landing frontmatter names an unknown FAQ entry") + +const output = `// Generated from apps/docs/content/resources/faq.mdx. Do not edit.\nexport const LANDING_FAQS = ${JSON.stringify(entries, null, 2)} as const\n` +const target = resolve( + import.meta.dir, + "../../web/src/ui/landing/faq.generated.ts", +) + +if (process.argv.includes("--check")) { + const current = await Bun.file(target) + .text() + .catch(() => "") + if (current !== output) { + console.error( + "Landing FAQ data is stale. Run: bun run --cwd apps/docs generate:faq", + ) + process.exit(1) + } + console.log("Landing FAQ data matches its MDX source.") +} else { + await Bun.write(target, output) + console.log(`Generated ${entries.length} landing FAQ entries.`) +} diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json new file mode 100644 index 00000000..67b15dca --- /dev/null +++ b/apps/docs/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["bun"], + "noEmit": true + }, + "include": ["scripts/**/*.ts"] +} diff --git a/apps/web/src/lib/glv-router-client.ts b/apps/web/src/lib/glv-router-client.ts index f028e0f5..c30987db 100644 --- a/apps/web/src/lib/glv-router-client.ts +++ b/apps/web/src/lib/glv-router-client.ts @@ -27,7 +27,7 @@ function getGlvClient(): GlvRouterClient { glvClient ??= new GlvRouterClient({ contractId: CONTRACTS.glvRouter, networkPassphrase: NETWORK.networkPassphrase, - rpcUrl: import.meta.env.VITE_RPC_URL, + rpcUrl: NETWORK.rpcUrl, }) return glvClient diff --git a/apps/web/src/ui/landing/faq-section.test.tsx b/apps/web/src/ui/landing/faq-section.test.tsx new file mode 100644 index 00000000..536bc1fd --- /dev/null +++ b/apps/web/src/ui/landing/faq-section.test.tsx @@ -0,0 +1,24 @@ +import { render, screen } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { describe, expect, it } from "vitest" + +import { FaqSection } from "./faq-section" +import { LANDING_FAQS } from "./faq.generated" + +describe("FaqSection", () => { + it("renders every generated entry and reveals its documentation link", async () => { + const user = userEvent.setup() + render() + for (const item of LANDING_FAQS) { + expect( + screen.getByRole("button", { name: item.question }), + ).toBeInTheDocument() + } + await user.click( + screen.getByRole("button", { name: LANDING_FAQS[0].question }), + ) + expect( + screen.getByRole("link", { name: LANDING_FAQS[0].linkLabel }), + ).toHaveAttribute("href", `https://docs.so4.market${LANDING_FAQS[0].href}`) + }) +}) diff --git a/apps/web/src/ui/landing/faq-section.tsx b/apps/web/src/ui/landing/faq-section.tsx index 3a57a359..0d756447 100644 --- a/apps/web/src/ui/landing/faq-section.tsx +++ b/apps/web/src/ui/landing/faq-section.tsx @@ -4,66 +4,10 @@ import { AccordionItem, AccordionTrigger, } from "@workspace/ui/components/accordion" -import type { ReactNode } from "react" -// Same shape as GMX (first answer bulleted, second numbered). Every claim -// here is checked against what the app actually does — see README.md and -// the linked routes — rather than adapted from GMX's own answers, since SO4 -// is a different protocol with a different feature set. -const FAQS: Array<{ id: string; question: string; answer: ReactNode }> = [ - { - id: "yield", - question: "What makes SO4 a good place to earn yield on my crypto?", - answer: ( -
    -
  • Provide liquidity to the GM pools and earn trading fees and funding, paid in real time.
  • -
  • Stake SO4 for GLV exposure — a diversified position across every pool, one token.
  • -
  • No lockups: withdraw whenever the pool has capacity, same block.
  • -
- ), - }, - { - id: "get-started", - question: "How do I get started on SO4?", - answer: ( -
    -
  1. Connect a Stellar wallet — no signup, no email.
  2. -
  3. Open Trade and pick BTC, ETH, or XLM to go long or short.
  4. -
  5. Or open Earn to deposit into a pool instead of trading directly.
  6. -
- ), - }, - { - id: "cost-efficiency", - question: "What makes SO4 cost-efficient compared to other perpetual platforms?", - answer: ( -

- Every position is filled against a single unified pool instead of a fragmented order book, - so fills don't depend on order book depth. Fees go to the liquidity that backs your - trade, not to a separate market maker spread. -

- ), - }, - { - id: "integrate", - question: "Can I build on top of SO4 or integrate it into my DeFi app?", - answer: ( -

- SO4 runs on public Soroban contracts — ExchangeRouter, DataStore, SyntheticsReader, and - OrderVault. There's no published SDK or integration docs yet; check the{" "} - - source - {" "} - in the meantime. -

- ), - }, -] +import { LANDING_FAQS } from "./faq.generated" + +const DOCS_ORIGIN = "https://docs.so4.market" export function FaqSection() { return ( @@ -76,20 +20,28 @@ export function FaqSection() { the aria-controls/labelledby wiring and focus-visible ring. Only the landing's typography and hairline rules are restyled here. */} - {FAQS.map(({ id, question, answer }) => ( + {LANDING_FAQS.map((item) => ( - {question} + {item.question} -
{answer}
+

+ {item.answer}{" "} + + {item.linkLabel} + +

))} diff --git a/apps/web/src/ui/landing/faq.generated.ts b/apps/web/src/ui/landing/faq.generated.ts new file mode 100644 index 00000000..069abae8 --- /dev/null +++ b/apps/web/src/ui/landing/faq.generated.ts @@ -0,0 +1,38 @@ +// Generated from apps/docs/content/resources/faq.mdx. Do not edit. +export const LANDING_FAQS = [ + { + "id": "what-is-so4", + "question": "What is SO4?", + "answer": "SO4 is a perpetual-markets protocol on Stellar with a web interface for preparing and submitting transactions.", + "linkLabel": "Read the protocol risk overview", + "href": "/concepts/risk" + }, + { + "id": "self-custody", + "question": "Do I keep custody of my assets?", + "answer": "The interface does not hold wallet keys; your wallet signs transactions and protocol contracts account for deposited collateral.", + "linkLabel": "Read the interface and protocol distinction", + "href": "/resources/terms#interface-and-protocol" + }, + { + "id": "liquidation", + "question": "What can cause liquidation?", + "answer": "A position becomes liquidatable when its remaining collateral no longer meets the protocol's maintenance-margin requirement.", + "linkLabel": "Read the liquidation mechanics", + "href": "/concepts/liquidation" + }, + { + "id": "fees", + "question": "What fees will I pay?", + "answer": "Costs can include opening, closing, funding, borrowing, network, and price-impact amounts, depending on the transaction.", + "linkLabel": "Read about funding and fees", + "href": "/concepts/funding-and-fees" + }, + { + "id": "availability", + "question": "Is the interface always available?", + "answer": "No; deployments, incidents, RPC failures, congestion, and local connectivity can interrupt the website even while the protocol remains deployed.", + "linkLabel": "Read the risk overview", + "href": "/concepts/risk" + } +] as const diff --git a/bun.lock b/bun.lock index fa9a914b..41c0aaf1 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,15 @@ "typescript": "5.9.3", }, }, + "apps/docs": { + "name": "docs", + "version": "0.0.1", + "devDependencies": { + "@types/bun": "^1.3.0", + "prettier": "^3.8.1", + "typescript": "^5.9.3", + }, + }, "apps/s03-indexer": { "name": "s03-indexer", "version": "0.0.1", @@ -1161,6 +1170,8 @@ "@types/bn.js": ["@types/bn.js@5.2.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q=="], + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], @@ -1547,6 +1558,8 @@ "bufferutil": ["bufferutil@4.1.0", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw=="], + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -1749,6 +1762,8 @@ "dns-over-http-resolver": ["dns-over-http-resolver@1.2.3", "", { "dependencies": { "debug": "^4.3.1", "native-fetch": "^3.0.0", "receptacle": "^1.3.2" } }, "sha512-miDiVSI6KSNbi4SVifzO/reD8rMnxgrlnkrlkugOLQpWQTe2qMdHsZp5DmfKjxNE+/T3VAAYLQUZMv9SMr6+AA=="], + "docs": ["docs@workspace:apps/docs"], + "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], "dot-prop": ["dot-prop@6.0.1", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA=="], diff --git a/docs/dx_1/001_docs_site.md b/docs/dx_1/001_docs_site.md index 0aa78df8..7200ced5 100644 --- a/docs/dx_1/001_docs_site.md +++ b/docs/dx_1/001_docs_site.md @@ -14,6 +14,15 @@ issue index in [`dx_issues.md`](./dx_issues.md). **Questions?** Reach out to the maintainer at [t.me/ibrahimijai](https://t.me/ibrahimijai). +> **Implementation note (2026-08-24):** DX-097–DX-100 introduced a dependency-light +> static documentation workspace while the wider DX1 foundation remains deferred. +> It keeps the specified `apps/docs/content` boundary, typed frontmatter checks, +> explicit navigation, link validation, printable HTML, and generated FAQ data. +> TanStack Start, runtime MDX compilation, Pagefind, syntax highlighting, Mermaid, +> generated reference pages, deployment configuration, and the complete docs chrome +> remain deferred because their prerequisite issues have not landed. This note records +> the built state rather than presenting the target architecture below as complete. + --- ## 1. Why a separate app diff --git a/docs/dx_1/002_changelog.md b/docs/dx_1/002_changelog.md index a976841c..9eb3877a 100644 --- a/docs/dx_1/002_changelog.md +++ b/docs/dx_1/002_changelog.md @@ -12,6 +12,12 @@ release, and how they are published in the app. Companion to **Questions?** Reach out to the maintainer at [t.me/ibrahimijai](https://t.me/ibrahimijai). +> **Implementation note (2026-08-24):** The DX1 resource batch adds an unreleased +> `docs` entry and `/resources/changelog`, which points to the canonical product +> history. The parser, release command, feeds, and `/changelog` application route +> described below remain deferred to DX-002 and its dependent issues; they were not +> recreated as part of documentation close-out work. + --- ## 1. Why the changelog lives in `apps/web` diff --git a/docs/dx_1/003_content_map.md b/docs/dx_1/003_content_map.md index 33c49f16..742d5750 100644 --- a/docs/dx_1/003_content_map.md +++ b/docs/dx_1/003_content_map.md @@ -6,6 +6,17 @@ content issue in [`dx_issues.md`](./dx_issues.md). Companion to **Questions?** Reach out to the maintainer at [t.me/ibrahimijai](https://t.me/ibrahimijai). +> **Implementation status (2026-08-24):** `/concepts/risk`, +> `/concepts/funding-and-fees`, `/concepts/liquidation`, `/reference/glossary`, +> `/resources/terms`, `/resources/faq`, `/resources/roadmap`, and +> `/resources/changelog` are implemented and reachable from the sidebar. All other +> pages in §2 are explicitly deferred because their prerequisite DX1 content or +> generated-reference issues have not landed. The three intended reader journeys +> therefore cannot yet be walked end to end: trader onboarding stops before +> quickstart/trading, liquidity-provider onboarding stops before pools, and the +> integrator journey stops before local setup and contract clients. This is the +> audited built state, not a claim that deferred paths exist. + --- ## 1. Who we are writing for diff --git a/e2e/landing-a11y-check.spec.ts b/e2e/landing-a11y-check.spec.ts index 34c20664..4c37cf05 100644 --- a/e2e/landing-a11y-check.spec.ts +++ b/e2e/landing-a11y-check.spec.ts @@ -5,7 +5,7 @@ test.use({ viewport: { width: 390, height: 844 } }) test("faq accordion opens via keyboard and is wired to its panel", async ({ page }) => { await page.goto("/") await page.waitForLoadState("networkidle") - const trigger = page.getByRole("button", { name: /good place to earn yield/i }) + const trigger = page.getByRole("button", { name: /what is so4/i }) await expect(trigger).toHaveAttribute("aria-expanded", "false") const controls = await trigger.getAttribute("aria-controls") expect(controls).toBeTruthy() diff --git a/package.json b/package.json index 1dfd0621..52bbbce0 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "indexer:dev": "bun run --cwd apps/s03-indexer dev", "indexer:start": "bun run --cwd apps/s03-indexer start", "check:integration": "bash scripts/check-integration.sh", - "check:tokens": "bun run scripts/check-design-tokens.ts" + "check:tokens": "bun run scripts/check-design-tokens.ts", + "check:content": "bun run --cwd apps/docs check:content", + "check:links": "bun run --cwd apps/docs check:links" }, "devDependencies": { "@playwright/test": "^1.61.1",