From 0841917e097a7855c80d6af348532d47fe4d0b19 Mon Sep 17 00:00:00 2001 From: Emergent Reasons Date: Thu, 16 Oct 2025 06:23:59 +0000 Subject: [PATCH] blog infra --- packages/docs/src/.vitepress/config.ts | 417 +++++----- .../.vitepress/theme/components/BlogCards.vue | 204 +++++ packages/docs/src/.vitepress/theme/custom.css | 11 + packages/docs/src/.vitepress/theme/index.ts | 2 + .../theme/mermaid-lightbox.client.ts | 171 ++++ .../.vitepress/theme/mermaid-overrides.css | 227 ++++++ ...-10-15-Intro-to-XO-The-Why-and-the-What.md | 45 ++ ...0-16-P2PKH-in-XO-from-vision-to-reality.md | 704 ++++++++++++++++ packages/docs/src/blog/index.md | 13 + packages/docs/src/index.md | 3 + .../docs/src/public/images/blog/welcome.jpg | Bin 0 -> 32585 bytes packages/docs/src/roadmap-data.md | 318 ++++++++ packages/docs/src/roadmap.md | 757 ++++++++++++++++++ packages/stack/src/bcmr/bcmr-http.ts | 8 +- packages/stack/src/utils/misc.ts | 9 +- .../stack/tests/bcmr/bcmr-composite.test.ts | 4 +- 16 files changed, 2703 insertions(+), 190 deletions(-) create mode 100644 packages/docs/src/.vitepress/theme/components/BlogCards.vue create mode 100644 packages/docs/src/.vitepress/theme/mermaid-lightbox.client.ts create mode 100644 packages/docs/src/.vitepress/theme/mermaid-overrides.css create mode 100644 packages/docs/src/blog/2025-10-15-Intro-to-XO-The-Why-and-the-What.md create mode 100644 packages/docs/src/blog/2025-10-16-P2PKH-in-XO-from-vision-to-reality.md create mode 100644 packages/docs/src/blog/index.md create mode 100644 packages/docs/src/public/images/blog/welcome.jpg create mode 100644 packages/docs/src/roadmap-data.md create mode 100644 packages/docs/src/roadmap.md diff --git a/packages/docs/src/.vitepress/config.ts b/packages/docs/src/.vitepress/config.ts index 5cb5a60..2eb7fdf 100644 --- a/packages/docs/src/.vitepress/config.ts +++ b/packages/docs/src/.vitepress/config.ts @@ -2,6 +2,10 @@ import { defineConfig } from 'vitepress'; import { withMermaid } from 'vitepress-plugin-mermaid'; import typedocSidebar from '../api/typedoc-sidebar.json'; +// Node imports for auto blog sidebar (works at docs/src/.vitepress) +import fs from 'node:fs'; +import path from 'node:path'; + // Special cases map for acronyms and exceptions const SPECIAL_CASES = { cryptocompare: 'CryptoCompare', @@ -66,202 +70,247 @@ function convertNavToPascalCase(nav: Array) { }); } -// https://vitepress.dev/reference/site-config -export default withMermaid({ - outDir: '../dist', - title: 'XO Stack', - description: - 'XO Stack: Building smart applications on the Bitcoin Cash Network', - head: [ - // Favicon. - ['link', { rel: 'icon', href: '/favicon.png' }], - // Basic OpenGraph tags - ['meta', { property: 'og:type', content: 'website' }], - ['meta', { property: 'og:title', content: 'XO Stack' }], - [ - 'meta', - { - property: 'og:description', - content: - 'XO Stack: Building smart applications on the Bitcoin Cash Network', - }, - ], - [ - 'meta', - { - property: 'og:image', - content: 'https://stack.xo.cash/stack-opengraph.jpg', - }, - ], - ['meta', { property: 'og:url', content: 'https://stack.xo.cash/' }], +function blogSidebar() { + const blogDir = path.resolve(__dirname, '../blog'); - // Optional additional tags - ['meta', { property: 'og:site_name', content: 'XO Stack' }], + if (!fs.existsSync(blogDir)) { + return [{ text: 'XO Blog', items: [] }]; + } - // Twitter Card tags (optional but recommended) - ['meta', { name: 'twitter:card', content: 'summary_large_image' }], - ['meta', { name: 'twitter:site', content: '@yourtwitterhandle' }], - ], - themeConfig: { - // Set the logo - logo: { - light: '/logo-light.svg', - dark: '/logo-dark.svg', + const files = fs + .readdirSync(blogDir) + .filter((f) => f.endsWith('.md') && f !== 'index.md') + .sort() + .reverse(); + + return [ + { + text: 'XO Blog', + items: files.map((filePath) => { + const slug = path.parse(filePath).name; + + // Try to extract YYYY-MM-DD from the filename + const match = slug.match(/^(\d{4}-\d{2}-\d{2})-(.*)$/); + let date = ''; + let titleSlug = slug; + + if (match) { + date = match[1]; // "2025-09-19" + titleSlug = match[2]; // "first-post" + } + + const title = titleSlug + .replace(/-/g, ' ') + .replace(/^\w/, (c) => c.toUpperCase()); + + return { + // Use HTML for linebreak + text: date ? `${date}
${title}` : title, + link: `/blog/${slug}`, + }; + }), }, + ]; +} - // Enable search functionality. - // TODO: Re-enable once we release documentation. - /* +// https://vitepress.dev/reference/site-config +export default withMermaid( + defineConfig({ + outDir: '../dist', + title: 'XO Stack', + description: + 'XO Stack: Building smart applications on the Bitcoin Cash Network', + head: [ + // Favicon. + ['link', { rel: 'icon', href: '/favicon.png' }], + // Basic OpenGraph tags + ['meta', { property: 'og:type', content: 'website' }], + ['meta', { property: 'og:title', content: 'XO Stack' }], + [ + 'meta', + { + property: 'og:description', + content: + 'XO Stack: Building smart applications on the Bitcoin Cash Network', + }, + ], + [ + 'meta', + { + property: 'og:image', + content: 'https://stack.xo.cash/stack-opengraph.jpg', + }, + ], + ['meta', { property: 'og:url', content: 'https://stack.xo.cash/' }], + + // Optional additional tags + ['meta', { property: 'og:site_name', content: 'XO Stack' }], + + // Twitter Card tags (optional but recommended) + ['meta', { name: 'twitter:card', content: 'summary_large_image' }], + ['meta', { name: 'twitter:site', content: '@yourtwitterhandle' }], + ], + themeConfig: { + // Set the logo + logo: { + light: '/logo-light.svg', + dark: '/logo-dark.svg', + }, + + // Enable search functionality. + // TODO: Re-enable once we release documentation. + /* search: { provider: 'local', }, */ - // https://vitepress.dev/reference/default-theme-config - nav: [ - { text: 'Home', link: '/' }, - // TODO: Re-enable once we release documentation - // { text: 'Documentation', link: '/quick-start' }, - { text: 'Tools', link: '/tools/primitives' }, - { text: 'Playground', link: '/playground' }, - ], - - sidebar: { - // Default Side Menu. - '/': [ - { - text: 'Guide', - items: [ - { text: 'Quick Start', link: '/quick-start' }, - { text: 'Architecture', link: '/architecture' }, - { text: 'Best Practices', link: '/api-examples' }, - { text: 'Contributing', link: '/contributing' }, - ], - }, - { - text: 'Frameworks', - items: [ - { text: 'Vue', link: '/frameworks/vue' }, - { text: 'React', link: '/frameworks/react' }, - { text: 'Svelte', link: '/frameworks/svelte' }, - ], - }, - { - text: 'API', - items: convertNavToPascalCase(typedocSidebar), - }, + // https://vitepress.dev/reference/default-theme-config + nav: [ + { text: 'Home', link: '/' }, + // TODO: Re-enable once we release documentation + // { text: 'Documentation', link: '/quick-start' }, + { text: 'Blog', link: '/blog' }, + { text: 'Tools', link: '/tools/primitives' }, + { text: 'Playground', link: '/playground' }, ], - // Side-menu for all routes under /tools/ - '/tools/': [ - { - text: 'XO Tools', - items: [ - { - text: 'Primitives', - link: '/tools/primitives', - collapsed: false, - items: [ - { text: 'Address', link: '/tools/primitives#address' }, - { - text: 'Block Header', - link: '/tools/primitives#block-header', - }, - { text: 'Bytes', link: '/tools/primitives#bytes' }, - { - text: 'Encrypter', - link: '/tools/primitives#encrypter', - }, - { - text: 'Hasher', - link: '/tools/primitives#hasher', - }, - { - text: 'HD Private Node', - link: '/tools/primitives#hd-private-node', - }, - { - text: 'HD Public Node', - link: '/tools/primitives#hd-public-node', - }, - { - text: 'Mnemonic', - link: '/tools/primitives#mnemonic', - }, - { - text: 'Private Key', - link: '/tools/primitives#private-key', - }, - { - text: 'Public Key', - link: '/tools/primitives#public-key', - }, - { - text: 'Satoshis', - link: '/tools/primitives#satoshis', - }, - { - text: 'Script', - link: '/tools/primitives#script', - }, - { - text: 'Transaction', - link: '/tools/primitives#transaction', - }, - ], - }, - { - text: 'Stack', - collapsed: false, - items: [ - { - text: 'Blockchain', - link: '/tools/stack/blockchain', - items: [ - { - text: 'Electrum', - link: '/tools/stack/blockchain#electrum', - }, - ], - }, - ], - }, - ], - }, + + sidebar: { + // Default Side Menu. + '/': [ + { + text: 'Guide', + items: [ + { text: 'Quick Start', link: '/quick-start' }, + { text: 'Architecture', link: '/architecture' }, + { text: 'Best Practices', link: '/api-examples' }, + { text: 'Contributing', link: '/contributing' }, + ], + }, + { + text: 'Frameworks', + items: [ + { text: 'Vue', link: '/frameworks/vue' }, + { text: 'React', link: '/frameworks/react' }, + { text: 'Svelte', link: '/frameworks/svelte' }, + ], + }, + { + text: 'API', + items: convertNavToPascalCase(typedocSidebar), + }, + ], + // Side-menu for all routes under /tools/ + '/tools/': [ + { + text: 'XO Tools', + items: [ + { + text: 'Primitives', + link: '/tools/primitives', + collapsed: false, + items: [ + { text: 'Address', link: '/tools/primitives#address' }, + { + text: 'Block Header', + link: '/tools/primitives#block-header', + }, + { text: 'Bytes', link: '/tools/primitives#bytes' }, + { + text: 'Encrypter', + link: '/tools/primitives#encrypter', + }, + { + text: 'Hasher', + link: '/tools/primitives#hasher', + }, + { + text: 'HD Private Node', + link: '/tools/primitives#hd-private-node', + }, + { + text: 'HD Public Node', + link: '/tools/primitives#hd-public-node', + }, + { + text: 'Mnemonic', + link: '/tools/primitives#mnemonic', + }, + { + text: 'Private Key', + link: '/tools/primitives#private-key', + }, + { + text: 'Public Key', + link: '/tools/primitives#public-key', + }, + { + text: 'Satoshis', + link: '/tools/primitives#satoshis', + }, + { + text: 'Script', + link: '/tools/primitives#script', + }, + { + text: 'Transaction', + link: '/tools/primitives#transaction', + }, + ], + }, + { + text: 'Stack', + collapsed: false, + items: [ + { + text: 'Blockchain', + link: '/tools/stack/blockchain', + items: [ + { + text: 'Electrum', + link: '/tools/stack/blockchain#electrum', + }, + ], + }, + ], + }, + ], + }, + ], + // Blog sidebar (auto-generated) + '/blog/': blogSidebar(), + }, + + socialLinks: [ + // { icon: 'github', link: 'https://gitlab.com/GeneralProtocols/xo/stack' }, + { icon: 'x', link: 'https://x.com/xo_bitcoincash' }, ], - }, - /* - */ - - socialLinks: [ - // { icon: 'github', link: 'https://gitlab.com/GeneralProtocols/xo/stack' }, - { icon: 'x', link: 'https://x.com/xo_bitcoincash' }, - ], - - // This is the outline on the right-hand side of the page. - outline: { - level: [2, 4], - label: 'On this page', - }, - }, - - // Set the dev and build targets to "esnext" - // NOTE: We do this so that we can support top-level-await. - vite: { - esbuild: { - target: 'esnext', - }, - build: { - target: 'esnext', - }, - optimizeDeps: { - esbuildOptions: { - target: 'esnext', + // This is the outline on the right-hand side of the page. + outline: { + level: [2, 4], + label: 'On this page', }, }, - resolve: { - // Configure to use browser version of @xocash/stack - conditions: ['browser', 'import', 'module', 'default'], + + // Set the dev and build targets to "esnext" + // NOTE: We do this so that we can support top-level-await. + vite: { + esbuild: { + target: 'esnext', + }, + build: { + target: 'esnext', + }, + optimizeDeps: { + esbuildOptions: { + target: 'esnext', + }, + }, + resolve: { + // Configure to use browser version of @xocash/stack + conditions: ['browser', 'import', 'module', 'default'], + }, }, - }, -}); + }), +); diff --git a/packages/docs/src/.vitepress/theme/components/BlogCards.vue b/packages/docs/src/.vitepress/theme/components/BlogCards.vue new file mode 100644 index 0000000..b058540 --- /dev/null +++ b/packages/docs/src/.vitepress/theme/components/BlogCards.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/packages/docs/src/.vitepress/theme/custom.css b/packages/docs/src/.vitepress/theme/custom.css index 4c5e60d..8ee9067 100644 --- a/packages/docs/src/.vitepress/theme/custom.css +++ b/packages/docs/src/.vitepress/theme/custom.css @@ -222,3 +222,14 @@ details summary { display: flex; justify-content: center; } + +.tag-badge { + font-size: 12px; + font-weight: 600; + color: #fff; + padding: 4px 10px; + border-radius: 5px; + background: linear-gradient(120deg, var(--tag-grad-start), var(--tag-grad-end)); + line-height: 1; + display: inline-block; +} diff --git a/packages/docs/src/.vitepress/theme/index.ts b/packages/docs/src/.vitepress/theme/index.ts index 81f7fb1..4058e0e 100644 --- a/packages/docs/src/.vitepress/theme/index.ts +++ b/packages/docs/src/.vitepress/theme/index.ts @@ -5,6 +5,8 @@ import Layout from './Layout.vue'; import './style.css'; import './custom.css'; import './tools.css'; +import './mermaid-overrides.css'; +import './mermaid-lightbox.client'; export default { extends: DefaultTheme, diff --git a/packages/docs/src/.vitepress/theme/mermaid-lightbox.client.ts b/packages/docs/src/.vitepress/theme/mermaid-lightbox.client.ts new file mode 100644 index 0000000..deecb9a --- /dev/null +++ b/packages/docs/src/.vitepress/theme/mermaid-lightbox.client.ts @@ -0,0 +1,171 @@ +// mermaid-lightbox.client.ts +// Click any .mermaid diagram to open a zoomable modal (works beautifully on desktop + mobile) +// Includes a theme-aware "Click diagram to expand" note that hides instantly when lightbox opens + +if (typeof window !== 'undefined') { + const OVERLAY_ATTR = 'data-mermaid-lightbox'; + const NO_LIGHTBOX_ATTR = 'data-no-lightbox'; + const MEMO_CLASS = 'mermaid-expand-memo'; + + const hideAllMemos = () => + document + .querySelectorAll(`.${MEMO_CLASS}`) + .forEach((m) => ((m as HTMLElement).style.opacity = '0')); + + const showAllMemos = () => + document + .querySelectorAll(`.${MEMO_CLASS}`) + .forEach((m) => ((m as HTMLElement).style.opacity = '0.55')); + + // === Create overlay (only once) === + const ensureOverlay = () => { + let overlay = document.querySelector( + `div[${OVERLAY_ATTR}]`, + ); + if (overlay) return overlay; + + overlay = document.createElement('div'); + overlay.setAttribute(OVERLAY_ATTR, 'true'); + overlay.setAttribute('role', 'dialog'); + overlay.setAttribute('aria-modal', 'true'); + overlay.style.cssText = ` + position: fixed; inset: 0; z-index: 9999; + background: rgba(0,0,0,.6); + display: none; align-items: center; justify-content: center; + padding: 0; + touch-action: pan-x pan-y pinch-zoom; + `; + + const box = document.createElement('div'); + box.className = 'mermaid mermaid-lightbox-content'; + box.style.cssText = ` + background: var(--vp-c-bg); + border-radius: 8px; + width: 100%; + height: auto; + max-width: 100vw; + max-height: 100vh; + overflow: auto; + padding: 0.5rem; + position: relative; + box-shadow: 0 20px 60px rgba(0,0,0,.35); + `; + + const closeBtn = document.createElement('button'); + closeBtn.type = 'button'; + closeBtn.setAttribute('aria-label', 'Close'); + closeBtn.innerHTML = '×'; + closeBtn.style.cssText = ` + position: absolute; top: 4px; right: 10px; + font-size: 32px; line-height: 1; + border: 0; background: transparent; + cursor: pointer; color: var(--vp-c-text-1); + z-index: 2; + `; + + const hide = () => { + overlay!.style.display = 'none'; + box.replaceChildren(); + document.removeEventListener('keydown', onKey); + showAllMemos(); + }; + + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') hide(); + }; + + (overlay as any).showSvg = (svg: SVGSVGElement) => { + // Hide memos immediately + hideAllMemos(); + + // Clone ONLY the SVG element, not any parent containers + const clone = svg.cloneNode(true) as SVGSVGElement; + clone.removeAttribute('width'); + clone.removeAttribute('height'); + clone.style.display = 'block'; + clone.style.width = '100%'; + clone.style.height = 'auto'; + clone.style.maxWidth = '100%'; + clone.style.maxHeight = '90vh'; + clone.setAttribute('preserveAspectRatio', 'xMidYMid meet'); + + // Clear the box completely before adding new content + box.innerHTML = ''; + box.appendChild(closeBtn); + box.appendChild(clone); + + overlay!.style.display = 'flex'; + document.addEventListener('keydown', onKey); + }; + + closeBtn.addEventListener('click', (e) => { + e.stopPropagation(); + hide(); + }); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) hide(); + }); + + overlay.appendChild(box); + document.body.appendChild(overlay); + return overlay; + }; + + // === Add the small memo below each Mermaid === + const addMemo = (container: HTMLElement) => { + if (container.nextElementSibling?.classList.contains(MEMO_CLASS)) return; + const memo = document.createElement('div'); + memo.className = MEMO_CLASS; + memo.textContent = 'Click diagram to expand'; + memo.style.cssText = ` + text-align: center; + font-size: 0.78rem; + color: currentColor; + opacity: 0.55; + margin-top: 0.4rem; + font-family: system-ui, sans-serif; + user-select: none; + pointer-events: none; + transition: opacity 0.25s ease; + `; + container.insertAdjacentElement('afterend', memo); + }; + + // === Wire up click events === + const wire = (root: ParentNode = document) => { + const svgs = root.querySelectorAll( + '.mermaid svg[id^="mermaid-"]', + ); + svgs.forEach((svg) => { + const container = svg.closest('.mermaid') as HTMLElement | null; + if (!container || container.hasAttribute(NO_LIGHTBOX_ATTR)) return; + if ((svg as any)._mlb) return; + + container.style.cursor = 'zoom-in'; + container.setAttribute('role', 'button'); + container.setAttribute('tabindex', '0'); + addMemo(container); + + container.addEventListener('click', (e) => { + if ((e.target as HTMLElement)?.closest('a')) return; + const overlay = ensureOverlay(); + (overlay as any).showSvg(svg); + }); + + container.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + const overlay = ensureOverlay(); + (overlay as any).showSvg(svg); + } + }); + + (svg as any)._mlb = true; + }); + }; + + // Initial run + observer for re-renders + wire(); + const mo = new MutationObserver(() => wire()); + mo.observe(document.documentElement, { childList: true, subtree: true }); +} diff --git a/packages/docs/src/.vitepress/theme/mermaid-overrides.css b/packages/docs/src/.vitepress/theme/mermaid-overrides.css new file mode 100644 index 0000000..c95bcdc --- /dev/null +++ b/packages/docs/src/.vitepress/theme/mermaid-overrides.css @@ -0,0 +1,227 @@ +/* ────────────────────────────────────────────────────────────── + Mermaid — Site CSS + - Mindmap: your custom palette (section-*) + - Flowchart: blue theme with white text + - Works in light & dark (via CSS vars) + - Responsive width + centering + ────────────────────────────────────────────────────────────── */ + +/* ===== Brand palette (edit to taste) ===== */ +:root { + /* === Mindmap Blue–Teal Theme === */ + --mm-root-fill: #1e3a8a; + --mm-sec0-fill: #1e6091; + --mm-sec1-fill: #168aad; + --mm-sec2-fill: #34a0a4; + --mm-sec3-fill: #99d98c; + + + + /* uniform thicker grey stroke for all nodes */ + --mm-root-stroke: #7a7a7a; + --mm-sec0-stroke: #7a7a7a; + --mm-sec1-stroke: #7a7a7a; + --mm-sec2-stroke: #7a7a7a; + --mm-sec3-stroke: #7a7a7a; + + /* optional text + fallback colors */ + --mm-fallback-fill: #f8fafc; + --mm-fallback-stroke:#7a7a7a; + --mm-fallback-text: #0f172a; + + /* edge color (grey-green neutral to fit palette) */ + --mm-edge: #7a7a7a; +} + + + +/* ===== Shared text overrides (foreignObject + tspans) ===== */ +.mermaid svg[id^="mermaid-"] { + + font-size: 110% !important; /* small global bump */ +} + +.mermaid svg[id^="mermaid-"] text, +.mermaid svg[id^="mermaid-"] tspan { + fill: #ffffff !important; /* default white text */ + font-weight: 600; +} + +.mermaid svg[id^="mermaid-"] foreignObject, +.mermaid svg[id^="mermaid-"] foreignObject * { + color: #ffffff !important; +} + +/* ===== Mindmap palette (section-* classes) ===== */ +/* Root node */ +.mermaid svg[id^="mermaid-"] .section-root rect, +.mermaid svg[id^="mermaid-"] .section-root path, +.mermaid svg[id^="mermaid-"] .section-root circle, +.mermaid svg[id^="mermaid-"] .section-root polygon { + fill: var(--mm-root-fill) !important; + stroke: var(--mm-root-stroke) !important; + stroke-width: 4px !important; +} +.mermaid svg[id^="mermaid-"] .section-root text, +.mermaid svg[id^="mermaid-"] .section-root tspan { + fill: #ffffff !important; + font-weight: 700 !important; +} + +/* Depth sections */ +.mermaid svg[id^="mermaid-"] .section-0 rect, +.mermaid svg[id^="mermaid-"] .section-0 path, +.mermaid svg[id^="mermaid-"] .section-0 circle, +.mermaid svg[id^="mermaid-"] .section-0 polygon { + fill: var(--mm-sec0-fill) !important; + stroke: var(--mm-sec0-stroke) !important; +} +.mermaid svg[id^="mermaid-"] .section-0 text, +.mermaid svg[id^="mermaid-"] .section-0 tspan { fill: #ffffff !important; } + +.mermaid svg[id^="mermaid-"] .section-1 rect, +.mermaid svg[id^="mermaid-"] .section-1 path, +.mermaid svg[id^="mermaid-"] .section-1 circle, +.mermaid svg[id^="mermaid-"] .section-1 polygon { + fill: var(--mm-sec1-fill) !important; + stroke: var(--mm-sec1-stroke) !important; +} +.mermaid svg[id^="mermaid-"] .section-1 text, +.mermaid svg[id^="mermaid-"] .section-1 tspan { fill: #ffffff !important; } + +.mermaid svg[id^="mermaid-"] .section-2 rect, +.mermaid svg[id^="mermaid-"] .section-2 path, +.mermaid svg[id^="mermaid-"] .section-2 circle, +.mermaid svg[id^="mermaid-"] .section-2 polygon { + fill: var(--mm-sec2-fill) !important; + stroke: var(--mm-sec2-stroke) !important; +} +.mermaid svg[id^="mermaid-"] .section-2 text, +.mermaid svg[id^="mermaid-"] .section-2 tspan { fill: #ffffff !important; } + +.mermaid svg[id^="mermaid-"] .section-3 rect, +.mermaid svg[id^="mermaid-"] .section-3 path, +.mermaid svg[id^="mermaid-"] .section-3 circle, +.mermaid svg[id^="mermaid-"] .section-3 polygon { + fill: var(--mm-sec3-fill) !important; + stroke: var(--mm-sec3-stroke) !important; +} +.mermaid svg[id^="mermaid-"] .section-3 text, +.mermaid svg[id^="mermaid-"] .section-3 tspan { fill: #ffffff !important; } + +/* Fallback for other section-* */ +.mermaid svg[id^="mermaid-"] [class^="section-"] rect, +.mermaid svg[id^="mermaid-"] [class^="section-"] path, +.mermaid svg[id^="mermaid-"] [class^="section-"] circle, +.mermaid svg[id^="mermaid-"] [class^="section-"] polygon { + fill: var(--mm-fallback-fill) !important; + stroke: var(--mm-fallback-stroke) !important; +} +.mermaid svg[id^="mermaid-"] [class^="section-"] text, +.mermaid svg[id^="mermaid-"] [class^="section-"] tspan { + fill: var(--mm-fallback-text) !important; +} + +/* Mindmap branches/edges */ +.mermaid svg[id^="mermaid-"] .edge, +.mermaid svg[id^="mermaid-"] .mindmap-edges path, +.mermaid svg[id^="mermaid-"] .mindmap-edges line, +.mermaid svg[id^="mermaid-"] [class^="section-edge-"] { + stroke: var(--mm-edge) !important; + stroke-width: 2px !important; + fill: none !important; +} + +/* Optional: tiny underline lines Mermaid adds in some node styles */ +.mermaid svg[id^="mermaid-"] .node-line-0, +.mermaid svg[id^="mermaid-"] .node-line-1, +.mermaid svg[id^="mermaid-"] .node-line-2 { + stroke: rgba(0,0,0,0.25) !important; +} + + +:root { + --mm-node-blue: #1570ef; /* inner node fill (light) */ + --mm-surface: rgba(0,0,0,0.06); /* cluster/edge-label pill bg on light */ + --mm-edge: color-mix(in srgb, currentColor 70%, transparent); +} +html.dark { + --mm-node-blue: #1363d1; /* inner node fill (dark) */ + --mm-surface: rgba(255,255,255,0.08); /* cluster/edge-label pill bg on dark */ + --mm-edge: color-mix(in srgb, currentColor 60%, transparent); +} + +.mermaid svg[id^="mermaid-"] .label text, +.mermaid svg[id^="mermaid-"] text, +.mermaid svg[id^="mermaid-"] tspan, +.mermaid svg[id^="mermaid-"] foreignObject, +.mermaid svg[id^="mermaid-"] foreignObject * { + fill: currentColor !important; + color: currentColor !important; +} + +.mermaid svg[id^="mermaid-"] .node rect, +.mermaid svg[id^="mermaid-"] .node circle, +.mermaid svg[id^="mermaid-"] .node ellipse, +.mermaid svg[id^="mermaid-"] .node polygon, +.mermaid svg[id^="mermaid-"] .node path { + fill: var(--mm-node-blue) !important; + stroke: none !important; +} + +.mermaid svg[id^="mermaid-"] .node .label text, +.mermaid svg[id^="mermaid-"] .node .label tspan, +.mermaid svg[id^="mermaid-"] .node foreignObject, +.mermaid svg[id^="mermaid-"] .node foreignObject * { + fill: #ffffff !important; + color: #ffffff !important; + font-weight: 600; +} + +.mermaid svg[id^="mermaid-"] .cluster rect { + fill: var(--mm-surface) !important; + stroke: none !important; +} +.mermaid svg[id^="mermaid-"] .cluster text, +.mermaid svg[id^="mermaid-"] .cluster span { + fill: currentColor !important; + color: currentColor !important; + font-weight: 600; +} + +.mermaid svg[id^="mermaid-"] .edgePath .path, +.mermaid svg[id^="mermaid-"] .flowchart-link { + stroke: var(--mm-edge) !important; + stroke-width: 2px !important; + fill: none !important; +} +.mermaid svg[id^="mermaid-"] .marker, +.mermaid svg[id^="mermaid-"] .arrowheadPath { + fill: var(--mm-edge) !important; + stroke: var(--mm-edge) !important; +} + +.mermaid svg[id^="mermaid-"] .edgeLabel rect { + fill: var(--mm-surface) !important; + stroke: none !important; + opacity: 1 !important; +} +.mermaid svg[id^="mermaid-"] .edgeLabel span, +.mermaid svg[id^="mermaid-"] .edgeLabel text { + fill: currentColor !important; + color: currentColor !important; + font-weight: 500; +} + +.mermaid svg[id^="mermaid-"] rect.text { fill: none !important; } + +/* ===== Layout: responsive width + centering in docs ===== */ +.vp-doc .mermaid > svg { + width: 100% !important; + height: auto !important; + display: block; +} +.vp-doc .mermaid { + display: flex; + justify-content: center; +} diff --git a/packages/docs/src/blog/2025-10-15-Intro-to-XO-The-Why-and-the-What.md b/packages/docs/src/blog/2025-10-15-Intro-to-XO-The-Why-and-the-What.md new file mode 100644 index 0000000..36824fd --- /dev/null +++ b/packages/docs/src/blog/2025-10-15-Intro-to-XO-The-Why-and-the-What.md @@ -0,0 +1,45 @@ +--- +title: 'Intro to XO: The Why and the What' +date: 2025-10-15 +description: 'Introducing the XO Project: What is it and why is it important?' +author: John Moriarty +tags: [general audience] +--- + +# Intro to XO - The Why and the What + +You may have heard about XO, General Protocols’ project that promises to make it easy for developers to create superior Bitcoin Cash applications. But what is it, actually? And why do we need it? + +## The problems to be solved + +The Bitcoin Cash (BCH) blockchain has undergone several groundbreaking protocol upgrades in recent years. These upgrades made it possible to build complex and innovative decentralized financial applications (or “dapps”) using Bitcoin Cash smart contracts. + +But building and interacting with BCH applications is still relatively difficult for software developers. The process requires esoteric knowledge about Bitcoin Cash’s protocol, and it often involves writing an entire wallet application around a dapp. This is a huge burden for app developers, and it’s also a suboptimal experience for users, who are forced to keep track of more and more wallets. + +The Wallet Connect protocol lets a user interact with a decentralized application using their personal wallet, but that workflow has its own problems. In that case, the dapp provides the wallet with transactions to sign, but there is no guarantee to the user that the provided transaction does what the application says it does. The user is forced to trust a third party when using dapps because their wallet doesn’t understand how the decentralized application works, and so it can’t verify anything itself. This is a serious security risk. + +Additionally, when a wallet doesn’t understand a decentralized application, it can’t keep track of the data needed to continue interacting with that application. For example, to use a derivatives application like BCHBull, you need to hold on to the data used to create the contract: the price source, leverage amounts, and position amounts. This contextually relevant data is called “state” and, in many cases, losing access to state means losing access to your funds, since you won’t be able to construct the transactions that withdraw or claim the BCH that is tied up in your contracts. Once again, users are forced to trust a third party. + +## Enter the XO Stack + +What’s needed to address these problems are software tools and standards that allow wallets and applications to coordinate in a safe, private, and user friendly way. That’s what General Protocols is building with the XO stack. + +### Templates + +The first fundamental pillar of the XO Stack is the concept of templates. A template is a description of a Bitcoin Cash use case in a standardized format. It includes the use case’s different participants, what actions each participant can take, what state must be kept track of, as well as human readable descriptions of all data. + +A template-aware wallet can take a template and some contextual data as input and then construct the required transaction(s) independently. This on its own is a huge step forward, but it’s important to understand that this does not completely remove trust from the equation. Instead, it moves the trust from the app to the templates themselves. While a template-aware wallet can use a template to infer what it needs to do to complete an action, it can’t verify that the human readable descriptions are accurate, or determine whether a resulting action is something a user would actually want to do. + +So wallet developers and users must decide which template sources to trust. A wallet developer may pre-approve a select few well known and vetted templates, for instance, an Anyhedge template (Anyhedge is the protocol used by BCHBull), or a Pay to Public Key Hash template (for simple sends). Then, if their wallet allows adding custom trusted templates, a user might decide to add several decentralized exchange templates to their wallet’s trusted list, so that they can trade between Bitcoin Cash and fungible tokens, or add funds to liquidity pools. It’s critical, however, that a user treats new or unknown templates with extreme caution like they would any new financial app. + +Once a trusted set of templates is established, the benefits follow. Any application that makes use of those templates can pass the transaction construction responsibilities on to your wallet without any additional trust required. For example, you could use any number of different dapps that use the Anyhedge protocol to provide derivatives products, or you could use various decentralized exchanges that use the same swap and liquidity pool protocols. In each case, your wallet will be able to provide human readable descriptions of the actions you’re taking, allowing you to check that an application is doing exactly what it says it’s doing. This is still important because, while your wallet can verify the app is using a trusted template, it doesn’t necessarily know what values you entered in the application’s user interface! + +### The XO Engine + +This templating system has enormous benefits for users when it comes to safety and ease of use. But with all of Bitcoin Cash’s newly unlocked smart contract capabilities accessible via the template standard, developers will need sophisticated tools to manage that complexity. That’s where the XO Engine comes in. + +The XO engine is the workhorse and personal Bitcoin Cash expert that handles all the low-level details for developers. It’s responsible for interpreting templates, managing cryptographic keys, tracking state, generating invitations, constructing transactions, and more. When using the XO stack, developers only need to familiarize themselves with how to make high-level requests of the engine, and the rest is taken care of for them. This massively reduces development time, cost, and risk of safety-compromising programming errors. + +## Onward with XO! + +The Bitcoin Cash protocol is in an excellent position regarding its technical capabilities. We at General Protocols are excited to bring Bitcoin Cash infrastructure up to speed, making powerful BCH apps easy and intuitive to develop with the XO Stack. We also continue to design and develop our premier XO App, the XO Wallet. With the full power of Bitcoin Cash under the hood, the XO Wallet promises to be an essential part of a brand new ecosystem of apps, developed by General Protocols and others. Keep your eyes open for more XO-related content as we build the future of programmable money! diff --git a/packages/docs/src/blog/2025-10-16-P2PKH-in-XO-from-vision-to-reality.md b/packages/docs/src/blog/2025-10-16-P2PKH-in-XO-from-vision-to-reality.md new file mode 100644 index 0000000..2d495a9 --- /dev/null +++ b/packages/docs/src/blog/2025-10-16-P2PKH-in-XO-from-vision-to-reality.md @@ -0,0 +1,704 @@ +--- +title: P2PKH in XO - From vision to reality +date: 2025-10-16 +description: This is an XO article! +author: Jonathan Silverblood +tags: [templates, engine, technical audience] +--- + +# P2PKH in XO: From vision to reality + +```mermaid +mindmap + root )Responsibilities( + + [INVITATIONS] + (Connects) + 📱 Apps + 👛 Wallets + 👥 Users + (Coordinates) + ⚡ Actions + 🧩 Variables + 📥 Inputs + 📤 Outputs + + [TEMPLATES] + (Describes) + 🧩 Variables + 💸 Transactions + 📥 Inputs + 📤 Outputs + 🏷️ Lockscripts + (Defines) + ⚡ Actions + 💸 Transactions + 💾 Data + + [ENGINE] + (Generates) + 🔑 Keys + 🪪 Identities + ✉️ Invitations + 💸 Transactions + (Manages) + 🤫 Secrets + 🧩 Variables + 📤 Outputs + 🏷️ Lockscripts +``` + +In this article, we are going to go over how a templating engine might function in the context of a P2PKH template. + +We will go over both the expected flow for a user interaction, as well as explaining how the engine gets all data it requires to function. + +Finally, we will cover some of the rationale for why this system is valuable. + +## The Templating System + +### Engine + +At the center of the templating system is an engine that together with templates is responsible for the full lifecycle of `transactions`, `relevant state` and `private keys`. + +### Templates + +The templates are immutable and only contain detailed descriptions and instructions on how to create and manage blockchain and use-case related data structures, such as transactions and lockscripts. + +Templates are organized around actions which defines when and what data to create. + +### Invitations + +When a template action is requested, the engine follows a consistent and re-usable invitation process. + +First an invitation is created that defines what template and action has been requested. This invitation is then accepted by entities wanting to participate in that action, including which of the available roles they want take. + +Then, the invitation is extended with any missing information by the participants until it reaches a state where the action defined in the invitation can be executed. + +This process is the same regardless of the complexity of the template and allows for both single-user and multi-user participation. + +### Identity + +When we are working with templates and interactions using invitations, it is important that each entity that is providing data or is taking part in an action has a cryptographic identity. This helps provide a clear log and history, allows us to assign blame and to recover when something has gone wrong. + +The engine will deterministically create an identity based on the root entropy provided during initialization. If no entropy is provided it will generate a temporary root entropy and will not have a stable identity. + +More information on identities will be covered in a future article. + +### Templates + +### Dynamic content + +The templating system described here uses `CashASM` to provide dynamic descriptions, properties and other values. + +This article will not go into depth on how `CashASM` works, but here's what you need to know: + +- `$(expression)` evaluates the expression using the BCH VM opcodes, then return the top stack item. +- `` push something to the stack. + +Here's an example evaluating to `BitcoinCash`, for reference: + +> $(<'Bitcoin'> <'Cash'> OP_CAT) + +## Detailed explanation of a P2PKH interaction + +We will now go over a very detailed example of how the engine, a template and invitations all go together to make a request for `100.00` `MUSD` fungible tokens. + +### Starting the engine + +Before the engine can be used, it needs to be initialized. + +This process provides the engine an initial root entropy, storage facilities and ways to monitor the bitcoin cash blockchain: + +```ts +// Create seed. +// NOTE: this should be randomized the first time, then reused when the engine is setup. +const seed = ''; + +// Set up a way to monitor block chain status +const blockchainMonitor = + ''; + +// Set up ways to store state and history +const browserIDBState = ''; +const privateServerState = + ''; + +// Initialize the engine using the seed, blockchain and state providers. +const xoEngine = new Engine(seed, blockchainMonitor, [ + browserIDBState, + privateServerState, +]); +``` + +### Importing template + +Before we can use the P2PKH template, we need to import it into the engine: + +```ts +const p2pkhTemplateIdentifier = await xoEngine.importTemplate(TemplateP2PKH); +``` + +### Requesting Fungible Tokens + +In the template there is list of starting actions that are available at any time: + +```ts +start: +{ + owner: [ 'receive', 'requestSatoshis', 'requestFungibleTokens', 'requestNonfungibleTokens' ], +}, +``` + +Note that these actions are grouped by role, in this case the P2PKH owner can receive and request funds. Other templates might have different starting actions for different roles. + +Since we are looking to specifically request `100.00` `MUSD` fungible tokens, we will need to take a look at the `requestFungibleTokens` action: + +```ts +requestFungibleTokens: +{ + name: 'Request Fungible Tokens', + description: 'Requests a specific amount of a fungible tokens from one or more senders.', + icon: 'request', + + variables: [ 'requestedTokenCategory', 'requestedTokenAmount' ], + transactions: [ 'requestFungibleTokensTransaction' ], +}, +``` + +The name and description here describes the action itself and is intended to help user find the right action for what they want to do. + +The action will in the end produce a `requestFungibleTokensTransaction` transaction, but to do so it needs some variables like the `requestedTokenCategory` and the `requestedTokenAmount`. + +Here's the definition of these variables in the template: + +``` +requestedTokenCategory: +{ + name: 'Requested Token Category', + description: 'The token category requested', + type: 'bytes', + hint: 'token_category', +}, +requestedTokenAmount: +{ + name: 'Requested Token Amount', + description: 'The fungible token amount requested', + type: 'integer', + hint: 'token_amount', +}, +``` + +Note that there is a technical type, the `bytes`, and a type `hint`. + +The type hint is there so that apps and wallets can provide better user experiences, and lets the engine do some helpful validation. + +For example, rather than have the user provide the MUSD token category like a hex string: `b38a33f750f84c5c169a6f23cb873e6e79605021585d4f3408789689ed87f366`, the wallet might show a dropdown with known tokens the user can choose from with the option of manual input as needed. + +In our example we will simply hard-code `100.00` worth of `MUSD`: + +```ts +const moriaTokenCategory = + 'b38a33f750f84c5c169a6f23cb873e6e79605021585d4f3408789689ed87f366'; +const moriaDecimals = 2; + +const requestVariables = { + requestedTokenCategory: hexToBin(moriaTokenCategory), + requestedTokenAmount: BigInt(100.0 * 10 ** moriaDecimals), +}; + +const requestInvitation = await xoEngine.createStartingInvitation( + p2pkhTemplateIdentifier, + 'requestFungibleTokens', + { variables: requestVariables }, +); +``` + +This code will return an invitation that contains information such as the template identifier, what action is in progress and any variable values for that action. Additionally every value in the invitation is covered by a signature from the entity that provided it. + +In this example, we will use this invitation to coordinate the details of the request between the sender and the receiver. + +### Under the hood + +As part of creating the starting invitation, the engine also needs to provide all information that should come from the current user. Since the starting action here was grouped under the `owner` role, it also automatically accepts the invitation in order to take on that role. + +The next steps are going to drill down through the template in order to demonstrate how the engine finds the requirements of the action and how it generates the necessary data to satisfy those requirements without any further user interactions. + +Here's an overview of the P2PKH template structure: + +```mermaid +flowchart RL +subgraph Template + Transaction[requestFungibleTokensTransaction] + Output[requestFungibleTokensOutput] + Lock[receivingLockingScript] + + Hash --> Lock + Lock --> Output + Output --> Transaction +end +subgraph Engine + Hash[["$(<ownerKey.public_key> OP_HASH160)"]] + Public[ownerKey.publicKey] + Private[ownerKey] + Secret[Seed] + + Public --> Hash + Private --> Public + Secret --> Private +end +``` + +Here is the `requestFungibleTokensTransaction` that the action needs to produce: + +```ts +requestFungibleTokensTransaction: +{ + name: 'Completed request', + description: 'Completed request for $( OP_DIV).$( OP_MOD) $().', + icon: 'request', + + // Standard transaction without a locktime. + version: 2, + locktime: 0, + + // Inputs and outputs that must exist in the transaction. + inputs: [ ], + outputs [ 'requestFungibleTokensOutput' ], + + // ... + composable: true, +}, +``` + +As you can see, it's a simple transaction construction, with a requirement set for a single `requestFungibleTokensOutput` output. + +The transaction also has a description that might not look very human readable in here, but once you take the description provided and apply the CashASM evaluations in it, it transforms to a much more human friendly: + +> Completed request for 100.00 MUSD. + +The engine now needs to determine how to create the `requestFungibleTokensOutput`: + +```ts +requestFungibleTokensOutput: +{ + name: 'Requested', + description: '$( OP_DIV).$( OP_MOD) requested $() tokens.', + icon: 'request', + + // Defines how the requested funds should be locked. + lockscript: 'receivingLockingScript', + + // Require a flat 1000 satoshis to ensure the fungible tokens remains transferrable. + valueSatoshis: 1000n, + + // Require only the specified amount and type of fungible tokens. + // NOTE: This can be composed with a request for a non-fungible token, but will always result in two separate outputs. + token: + { + category: '$()', + amount: '$()', + nft: null, + }, +}, +``` + +Like the transaction, the output has a human readable name and description, but also requires a flat 1000 satoshis as well as the requested `amount` and `category`, and is managed by the `receivingLockingScript`. + +If we follow that lockscript we will get to a quite long section, but for now the part that we care about is determining how the engine will resolve this to get an actual lockscript it can use in this output. + +Here's a shortened version showing a deeper reference to the `lockP2PKH` script: + +```ts +receivingLockingScript: +{ + // Defines how spending future received funds should be authorized. + lockingType: 'standard', + lockingScript: 'lockP2PKH', +} +``` + +Following that reference shows the `lockP2PKH` script itself: + +```ts +lockP2PKH: 'OP_DUP OP_HASH160 <$( OP_HASH160)> OP_EQUALVERIFY OP_CHECKSIG', +``` + +As some of you might've noticed, this is where the actual P2PKH part comes in, and the lockscript as it will be used on-chain is constructed. + +However, we're not quite done yet as the `lockP2PKH` is defined with a dynamic CashASM expression: `<$( OP_HASH160)>`. + +This part takes the ownerKey, produces the corresponding public key and pushes it to the stack, then runs OP_HASH160 and return the hash. + +In order to resolve this it needs to know how to get to the `ownerKey`: + +```ts +// Describe the secret private key. +ownerKey: +{ + name: 'Owners Private Key', + description: 'The private key used to authorize spending of received funds.', + type: 'bytes', + hint: 'private_key', +}, +``` + +This key is not provided by the action, but since the engine knows what a private key is and no key was provided it will create a new key on-demand every time such a key is needed, avoiding unnecessary address re-use. + +The engine can now evaluate the CashASM part of the `lockP2PKH` script to create the `receivingLockingScript` that completes the `requestFungibleTokensOutput`, which is added to the invitation. + +The `ownerKey` is not added to the invitation though, and we will go into more details when discussing state and secrets later in the article. + +_At this point, we have gone through all the requirements for setting up a request to receive `100.00` `MUSD` and we have created an invitation that can be shared to complete the request._ + +### Handling the fungible tokens request + +Let's assume that we have received the request invitation above via an XO compatible transport, and that we have an XO compatible wallet with sufficient funds in it. + +In order to handle the request, there is a number of questions that will need to be answered: + +- what part do I take in this action? +- what information will I need to provide? +- which of my funds will be used for this? +- do I actually even want to do this? + +Ideally, we also want to interact with the wallet user as few times as possible, so the first thing we would need to do is figure out what is required of the user. + +#### Roles + +We start by checking if there is any open roles the user can take: + +```ts +const availableRoles = await xoEngine.listAvailableRoles(requestInvitation); +``` + +This step is needed because taking on a role defines which of the variables and resources needs to be provided later. + +In this case, the only role that for the action was the `owner`, and that is already taken, so the only thing for us to do here is to accept the request without taking on a specific role in it: + +```ts +const acceptedRequestInvitiation = await xoEngine.acceptInvitation( + requestInvitation, + [], +); +``` + +_In a more complex setup, this would require showing the user a list of available roles that they can choose from, before accepting the invitation_ + +#### Variables + +Next step is to check what variables are missing to complete the request: + +```ts +const { requiredVariables } = + await xoEngine.listRequirements(requestInvitation); +``` + +This step is needed because the required variables could be used to define the resources needed to be provided later. + +The action in this invitation only required two variables, the `requestedTokenAmount` and `requestedTokenCategory`. Both of these were already set by the `owner`, so there's nothing for us to do here. + +_In a more complex setup, this would require asking the user to input values for their required variables before moving on._ + +#### Inputs and outputs + +The last step is to determine what resources are required in terms of blockchain assets. + +For satoshis, we look at the the `requestFungibleTokensOutput` to see `1000`: + +> valueSatoshis: 1000n, + +For tokens we also look at the `requestFungibleTokensOutput` and here we can see that it has a fungible token amount that was evaluated to `10000n` and a token category of `b38a33f750f84c5c169a6f23cb873e6e79605021585d4f3408789689ed87f366`: + + token: + { + category: '$()', + amount: '$()', + nft: null, + }, + +Now that the engine knows what resources must exist in the output of the transaction, we can infer this as the required resources to provide if we want to complete this action. + +Now that the engine knows what resources must exist in the output of the transaction, it can infer the required inputs. In this case to cover the required output and mining fee. + +The next step would be to select some UTXOs to add as inputs, create any needed change outputs and then update the invitation with this information, which can be done automatically by the engine: + +```ts +const requirements = { + requiredSatoshis, + requiredFungibleTokens, + requiredNonFungibleToken, +}; + +const preparedRequestInvitation = await xoEngine.reserveResources( + acceptedRequestInvitiation, + requirements, +); +``` + +In a later example we will show how to do this manually, to achieve coin control, choice in coin selection strategy and using different lockscripts for different change outputs to maintain privacy. + +#### Authorization + +Now that we know all roles, variables and resources the user would provide, we can show this information to the user and ask for a confirmation. + +Assuming the user then agrees to take part in the action we go ahead and sign our inputs, marking our participation in this invitation complete: + +```ts +const signedRequestInvitation = await xoEngine.signInvitation( + preparedRequestInvitiation, +); +``` + +The invitation can then be sent back, or if signed by all relevant parties, we can execute the action in the template: + +```ts +const requestTransaction = await xoEngine.executeAction( + signedRequestInvitation, + BROADCAST_ON_EXECUTION, +); +``` + +The request for `100.00` `MUSD` fungible tokens is now complete and a transaction exist on-chain. + +### Managing state + +Now that the easy part is done, let's dig into the heart of the engine and the templating system. + +Now that a transaction has been broadcast on chain that has an output matching the locking script we used for our request, the engine will automatically detect this output and internally connect it with the template and action, keep track of the original variables. + +However, if you remember from earlier the `ownerKey` was generated on-demand as-needed, but in order to spend this new output we would need the same private key as was used to create the lockscript, so let's revisit the `receivingLockingScript`, this time without shortening it: + +```ts +receivingLockingScript: +{ + name: 'Received', + description: 'Funds received without wallet coordination.', + icon: 'address', + + // Defines how spending future received funds should be authorized. + lockingType: 'standard', + lockingScript: 'lockP2PKH', + + // Define a default unlocking script to be used when no action provided script is present. + unlockingScript: 'unlockP2PKH', + + roles: + { + owner: + { + // The only state that is required to be persisted when receiving funds is the owners private key. + state: [], + secrets: [ 'ownerKey' ], + + // List actions that can be taken with the ownerKey for each address/lockscript. + actions: [ 'sign', 'verify', 'sendSatoshis', 'sendFungibleTokens', 'sendNonfungibleTokens', 'burnFungibleTokens', 'burnNonfungibleTokens' ], + + // Indicates how much of received funds should be part of a wallets total balance. + balance: + { + satoshis: true, + fungibleTokens: true, + nonFungibleToken: true, + }, + + // Indicate when received funds should be considered in automatic coin selection to meet input requirements of other transactions. + selectable: true, + + // Indicate that received funds are not considered good privacy. + privacy: false, + }, + }, +} +} +``` + +The description here might seem a little odd at first, as it does not match the action we had earlier, but this is intentional. + +At any time when the lockscript is generated, it is the engines responsibility to start tracking that lockscript and all activity related to it. When such activity can be linked to an action, it will use the descriptions provided by that action, but sometimes there is activity that the engine is not aware of, such as when you receive an airdrop or someone dusts your address. + +The name and description of the lockscript are then used for such outputs that cannot be linked to an intentional action, allowing us to provide human meaningful context even for uncoordinated activity. + +You can also see that there is a default unlocking script, which is used when the engine needs to use an output on this lockscript and have not received any explicit instructions on how to unlock it. + +The important part for us right now though, is the `state`/`secrets`. + +```ts +// The only state that is required to be persisted when receiving funds is the owners private key. +state: [], +secrets: [ 'ownerKey' ], +``` + +When the engine is asked to create either an output of a lockscript, the template can define a set of variables that are to be automatically managed. The engine would then take this state and internally tie it to the output, or any output using the specific lockscript. + +When the output is later spent, the state attached to it will be made available to satisfy any constraints originating from the same template. In this P2PKH example, the `ownerKey` is marked as part of the `secrets` which is a subset of the full state that will not be shared when doing invitations or similar actions. + +In more advanced uses, the state might be defined at one transaction, but only be needed several transactions later. In these cases, it is up to the template authors to define outputs to carry the state from one transaction to the next until it is no longer needed. + +Note that a result of this behaviour is that state has a lifecycle and once an output is spent, any state attached to that output can be archived into history or even pruned entirely, depending on the needs of the wallet or application. + +Managing state is likely to most complex and valuable part of the templating system, and much more can be said on the matter. We will go into more detail in later examples. + +### What is ownership? + +There are a few more parts of the `receivingLockingScript` left to go over that relates to ownership and control of outputs: `actions`, `balance`, `selectable` and `privacy`. + +These properties provide information to an app or wallet that uses the engine. Let's go over each of them to get a better understanding of what they do: + +```ts +// List actions that can be taken with the ownerKey for each address/lockscript. +actions: [ 'sign', 'verify', 'sendSatoshis', 'sendFungibleTokens', 'sendNonfungibleTokens', 'burnFungibleTokens', 'burnNonfungibleTokens' ], +``` + +The `actions` list provides references to actions that you might be able to take with a given output or the state attached to an output. + +In the P2PKH template, these can be summarized as actions to sign and verify messages as well as sending and burning value. + +This allows an app or wallet to present possible actions when the user is looking at a given output or address. Some of the actions listed here may have additional constraints, for example: + +```ts +// Sending is only available for outputs that have fungible tokens on them. +condition: '$(OP_INPUTINDEX OP_UTXOTOKENAMOUNT <0> OP_GREATERTHAN)', +``` + +The `sendFungibleTokens` action has a condition that makes it unavailable if the input does not have any fungible tokens on it. + +Apart from potential `actions`, there is also a `balance` property which can be used to get a better understanding of how much of an output is estimated to owned by the user: + +```ts +balance: +{ + satoshis: true, + fungibleTokens: true, + nonFungibleToken: true, +}, +``` + +In the P2PKH example, these are set to `true` to indicate that the full output is estimated to be owned by the owner. However, these fields can also be set to `false`, or a number. + +In more advanced cases, these fields would be CashASM evaluations that would be able to determine values based on the variables in the state, and in some cases they would be impossible to define and would be unknown. + +It is up to the app or wallet to determine how they want to use, or not use, this information. + +Finally, there are two properties relating to coin selection: `selectable` and `privacy`: + +```ts +// Indicate when received funds should be considered in automatic coin selection. +selectable: true, + +// Indicate that received funds are not considered good privacy. +privacy: false, +``` + +Coin selection is a vast field with no best answer, so these are provided as additional information that a coin selection algorithm may use, and not as strict directives for when and how to do coin selection. + +The `selectable` property allows the template authors to indicate if it is sensible to use an output in automatic coin selection and can be either `true` (may be used), `false` (should not be used) or can be left `undefined`. + +The `privacy` property allows the template authors to indicate what level of privacy an output has, which can be helpful to avoid mixing of inputs of different levels of privacy. + +### Other actions + +Before we wrap up, let's take a deeper look at the `sign` and `verify`actions: + +```ts +sign: +{ + name: 'Sign Message', + description: 'Signs a provided message using the Bitcoin message signing protocol.', + icon: 'sign', + + variables: [ 'message' ], + data: [ 'messageSignature' ], +}, +verify: +{ + name: 'Verify Message Signature', + description: 'Verifies a provided message signature according to the Bitcoin message signing protocol.', + icon: 'verify', + + variables: [ 'messageSignature', 'message' ], + data: [ 'messageSignatureValidity' ], +}, +``` + +Contrary to previous actions we have looked at, these do not result in transactions, but instead provides arbitrary `data`. + +Let's look at the `messageSignature` definition, and the related `messagePrefix` constant: + +```ts +messageSignature: +{ + // Evaluate CashASM expression to get the signature needed. + value: '$( OP_CAT )', + type: 'bytes', + hint: 'signature', +}, +``` + +```ts +messagePrefix: +{ + name: 'Message Prefix', + description: 'Standard message prefix used in the bitcoin signed message protocol.', + type: 'bytes', + + // Value is enforce to the bitcoin signing magic string: + // "\x18Bitcoin Signed Message:\n" + value: '0x18426974636f696e205369676e6564204d6573736167653a0a', +}, +``` + +Together, these encode the standard bitcoin message signing procedure to safely sign an arbitrary message using the `ownerKey` from any given address or output, without exposing the keys to 3rd parties or allowing them to sign potentially dangerous messages. + +The validation of a signature is done in a similar way: + +```ts +messageSignatureValidity: +{ + // Evalute the validity of the message with the owners public key. + value: '$( OP_CAT OP_CHECKDATASIG)'; + type: 'integer', + hint: 'script_boolean', +}, +``` + +This means that any wallet that uses this P2PKH template would be have this feature without needing to add any special handling or code for it, simply find any address/lockscript or output, look at the associated actions and initiate an action to sign or verify a message. + +## Rationale + +After reading this article, you might rightfully feel that it's all a bit much, and wonder why someone would go through the effort and spend the resources required to build such a complicated machinery. + +The answer is that it allows us to build new applications both faster and safer, while providing a significantly better user experience. + +### Safety + +In the traditional wallet and app ecosystem today, the default is to run on HD wallets using seed words for backup and recovery. + +With every new feature added to the Bitcoin Cash blockchain that empowers smart contract developers, the number of use cases where the wallet seed is insufficient grows. + +Contracts by nature are stateful and loss of state is often equivalent to loss of funds. While this can be mitigated to some degree by carrying state in token commitments, it has the tradeoff of exposing that state to the world. Further, some state, like your prediction in a guru bet, is required to be kept secret, and cannot be safely stored in a token commitment. + +By addressing the need for state management in modern smart contracts we reduce the risk of state loss and therefor improve safety for users. + +Further, by creating an engine that is capable of both generating and storing keys, and keeping them isolated and restricting their use to template defined actions, we provide additional safety in terms of safe key management. This is also a missing piece in order to achieve fully non-custodial applications, where both control of all keys, as well as decisionmaking for what those keys are used for resides entirely with the users wallet. + +Finally, by creating an invitation and coordination mechanic that doesn't rely on single-key operation and that does template based validation of provided data, we improve safety for users when interacting with 3rd party distributed applications. + +### User experience + +With templates, every transaction, input, output, lockscript and address can be described with dynamically evaluated CashASM names and descriptions. Further, every variable or input required by the user can be described as well, and their type and data can be validated. + +With templates, the need for technical identifiers is massively reduced. + +Further, by having contextual actions, external links and other metadata it becomes much easier for users to be empowered and stay in control. + +Finally, with templates it becomes possible to estimate a users aggregate net worth across a wide variety of contracts, as well as show how much of that value is readily accessible for spending and how much is currently locked. + +### Higher productivity + +With templates, the correct use of a contract is directly encoded. Importing a template that someone else has made can be done to provide new features at almost no cost. + +Compared to some alternatives, templates also provide a consistent way to adopt new features where previously app and wallet makers would need to import specific libraries for each feature they want, or do full bespoke implementations. + +For reference, the AnyHedge library, excluding tests and examples, is `262.8 KiB` large. + +While it can do much more than a corresponding template, a lot of it is re-inventing the BitcoinCash domain knowledge, working with inputs and outputs etc, and my estimate is that less than a third of it would been needed if we had full template support. diff --git a/packages/docs/src/blog/index.md b/packages/docs/src/blog/index.md new file mode 100644 index 0000000..c737120 --- /dev/null +++ b/packages/docs/src/blog/index.md @@ -0,0 +1,13 @@ +--- +title: XO Blog +--- + + + +# XO Blog + +Welcome to the XO Blog where we talk about all the things + + diff --git a/packages/docs/src/index.md b/packages/docs/src/index.md index a41b478..3f6cfc5 100644 --- a/packages/docs/src/index.md +++ b/packages/docs/src/index.md @@ -7,6 +7,9 @@ hero: text: 'On Bitcoin Cash' tagline: Building smart applications on the BCH UTXO Network actions: + - theme: brand + text: XO Blog + link: /blog/ - theme: brand text: Tools link: /tools/primitives diff --git a/packages/docs/src/public/images/blog/welcome.jpg b/packages/docs/src/public/images/blog/welcome.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fe798765500a56cfc0495ed151b762c6c615932d GIT binary patch literal 32585 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 +

XO Roadmap

+

+ Drag the slider to pan the timeline. + Click bars, milestones, or releases for details. +

+ +
+
+ + + (loading…) +
+ + +
+ +
+
+
Loading roadmap…
+
+
+ + + + + + + + + + + diff --git a/packages/stack/src/bcmr/bcmr-http.ts b/packages/stack/src/bcmr/bcmr-http.ts index 451e7d3..2dc5783 100644 --- a/packages/stack/src/bcmr/bcmr-http.ts +++ b/packages/stack/src/bcmr/bcmr-http.ts @@ -110,12 +110,14 @@ export class BCMRHttp extends BaseBCMR { ); // Validate the payload body. - try{ + try { const metadata = MetadataRegistrySchema.parse(await response.json()); return metadata; - } catch(error: unknown) { - throw new Error(`Unable to parse the metadata registry format. Detailed error: ${error}`); + } catch (error: unknown) { + throw new Error( + `Unable to parse the metadata registry format. Detailed error: ${error}`, + ); } } diff --git a/packages/stack/src/utils/misc.ts b/packages/stack/src/utils/misc.ts index c4963f1..f3c302f 100644 --- a/packages/stack/src/utils/misc.ts +++ b/packages/stack/src/utils/misc.ts @@ -83,10 +83,15 @@ export function findAllKeyOcurrences( const keyValues: Array = []; - function traverse(currentObj: Record, depth: number = 0) { + function traverse( + currentObj: Record, + depth: number = 0, + ) { // Stop if depth exceeds the amount of 'allowedNestedLevels'. if (depth > allowedNestedLevels) - throw new Error(`Maximum nesting level of ${allowedNestedLevels} reached.`); + throw new Error( + `Maximum nesting level of ${allowedNestedLevels} reached.`, + ); // Stop when find a non object value. if (typeof currentObj !== 'object' || currentObj === null) return; diff --git a/packages/stack/tests/bcmr/bcmr-composite.test.ts b/packages/stack/tests/bcmr/bcmr-composite.test.ts index 03628c7..4333aa5 100644 --- a/packages/stack/tests/bcmr/bcmr-composite.test.ts +++ b/packages/stack/tests/bcmr/bcmr-composite.test.ts @@ -41,6 +41,8 @@ describe('BCMRComposite', () => { test('should fail if cantegoryId is not a registered entry', async () => { const invalidCategoryId = 'asd123'; - await expect(bcmrComposite.getTokenData(invalidCategoryId)).rejects.toThrow(); + await expect( + bcmrComposite.getTokenData(invalidCategoryId), + ).rejects.toThrow(); }); });