diff --git a/docs.json b/docs.json index a582267..94d6179 100644 --- a/docs.json +++ b/docs.json @@ -78,6 +78,7 @@ "pages": [ "learn/general-governance", "learn/proposals", + "learn/sips", "learn/general-staking" ] }, diff --git a/learn/sips.mdx b/learn/sips.mdx new file mode 100644 index 0000000..ec8480d --- /dev/null +++ b/learn/sips.mdx @@ -0,0 +1,121 @@ +--- +title: 'Sei Improvement Proposals' +sidebarTitle: 'SIPs' +description: 'Browse every Sei Improvement Proposal (SIP) with status, type, authorship, and section outlines pulled live from the sei-protocol/sips repository.' +keywords: ['SIP', 'Sei Improvement Proposal', 'sei governance', 'protocol proposals', 'SIP process', 'SIP status', 'SIP-1', 'SIP-3', 'sei-protocol/sips'] +--- + +import { SipIndex } from '/snippets/sip-index.jsx'; + +A Sei Improvement Proposal (SIP) is a design document for a change to Sei. It might cover a new protocol feature, a change to how the project runs, or a convention for the wider ecosystem. Writing one is how you put an idea in front of the community, and how the reasoning behind a decision gets recorded. + +Every SIP lives in the [sei-protocol/sips](https://github.com/sei-protocol/sips) repository, and the list below reads from it directly. Titles, statuses, authors, and section outlines come from the source files rather than a copy that can fall behind. + + + +A SIP is not the same as an on-chain governance proposal. The SIP holds the design and the reasoning; the on-chain proposal is the vote that enacts it. One SIP can take several governance proposals to carry out, as SIP-3 did. For the on-chain process, see [Governance](/learn/general-governance) and [Proposals](/learn/proposals). + + + +## All proposals + + + +## Proposal types + +Every SIP declares a type, and the type decides how much community consensus it needs. [SIP-1](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#sip-type) defines all three. + +| Type | Scope | Needs consensus | +| --- | --- | --- | +| [Standard](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#standard) | Changes that affect most implementations of Sei, such as consensus, networking, RPC, EVM and CosmWasm behavior, and contract standards. Must also declare a category. | Yes | +| [Process](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#process) | Changes around Sei rather than inside it, such as developer tooling, node and validator procedures, and the SIP process itself. | Yes | +| [Informational](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#informational) | Guidelines or information for the community, with no change to the protocol or development environment. | No | + +### Categories + +A Standard SIP must also declare one of five [categories](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#sip-category). + +| Category | Covers | +| --- | --- | +| Core | Consensus, execution, storage, and account signatures | +| Networking | Mempool and network protocols | +| Interface | RPC specifications and lower-level naming conventions | +| Framework | EVM and CosmWasm contracts and primitives included in the Sei codebase | +| Application | New EVM or CosmWasm standards and primitives that sit outside the Sei codebase | + +## Status lifecycle + +A successful SIP usually moves through Draft, Review, Last Call, Final, Implemented, and Activated. Each card above shows the status recorded in that proposal's own source file. + +| Status | What it means | +| --- | --- | +| [Idea](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#idea) | Written, but not yet admitted to the process and not yet numbered. | +| [Draft](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#draft) | An editor has accepted the formatting and assigned a number. The author now gathers community feedback. | +| [Review](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#review) | The author has worked through early feedback. Assigned reviewers now give a formal review, and it takes at least three of them. | +| [Fast Track](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#fast-track) | For proposals that already reached consensus outside the SIP process. Runs the same way as Review. | +| [Last Call](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#last-call) | A final window for objections, usually at least one week. | +| [Final](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#final) | Accepted, and the specification is now frozen. | +| [Implemented](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#implemented) | Development is complete, but the feature is not yet live on mainnet. | +| [Activated](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#activated) | Live on Sei mainnet. | +| [Stagnant](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#stagnant) | Inactive for four weeks while in Draft, Review, or Last Call. | +| [Withdrawn](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#withdrawn) | The author has withdrawn the proposal. | +| [Living](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#living) | A continuously updated proposal that never reaches Final, such as SIP-1 itself. | + + + +Once a SIP reaches Final, nobody can withdraw or edit it. To change an accepted specification, submit a new SIP that extends or replaces the original. + + + +## Guides for specific proposals + +SIP-3 and SIP-4 both affect how you use and build on Sei. These pages cover what they mean in practice. + + + + Move assets off Cosmos-native addresses and IBC tokens before Sei becomes EVM-only. + + + Integration changes for exchanges and custodians supporting Sei deposits and withdrawals. + + + How Sei's EIP-1559 implementation works, including the parameters SIP-4 revises. + + + Legacy Cosmos SDK and CosmWasm documentation, deprecated under SIP-3. + + + +## Submit a proposal + +Anyone can author a SIP. Talk the idea over with the community before you write it, so you can gauge whether the proposal is needed at all. + + + + +[SIP-1](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md) defines the process, the header fields your file needs, and what authors and editors are each responsible for. + + + +Open a thread in [GitHub Discussions](https://github.com/sei-protocol/sips/discussions) to collect early feedback, especially from the people who would implement the change. + + + +Fork [sei-protocol/sips](https://github.com/sei-protocol/sips) and create your proposal at `sips/sip-temporary_title.md`. Put any images in `assets/sip-temporary_title/`. + + + +Start the file with the two-column header table and follow the section structure described in [SIP format](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#sip-format). Keep the title under 64 characters and don't end it with a period. + + + +Submit the pull request from your fork. A [SIP editor](https://github.com/sei-protocol/sips/blob/main/sips/sip-1.md#sip-editor) reviews the formatting, assigns a number, moves the status to Draft, and opens the official comments thread. + + + + + + +From Draft onward, keeping the proposal moving is your job. An editor will help you find the right people to talk to, but won't chase the process for you. + + diff --git a/snippets/sip-index.jsx b/snippets/sip-index.jsx new file mode 100644 index 0000000..62c4242 --- /dev/null +++ b/snippets/sip-index.jsx @@ -0,0 +1,515 @@ +export const SipIndex = () => { + const REPO = 'sei-protocol/sips'; + const BRANCH = 'main'; + const LIST_URL = `https://api.github.com/repos/${REPO}/contents/sips?ref=${BRANCH}`; + // Suffix is bumped whenever parseSip's output shape changes, so a cached + // entry from an older build is discarded instead of rendered. + const CACHE_KEY = 'sei-sip-index-v1'; + const CACHE_TTL = 3600000; + // Highest sip-N.md the fallback path will look for. + const MAX_PROBE = 40; + + const rawUrl = (file) => `https://raw.githubusercontent.com/${REPO}/${BRANCH}/sips/${file}`; + const blobUrl = (file) => `https://github.com/${REPO}/blob/${BRANCH}/sips/${file}`; + + // --- Header-field parsing ------------------------------------------------- + // Each SIP opens with a two-column markdown table. Field names drifted + // between SIPs ("SIP-Number" vs "SIP Number", "Comments" vs "Comments-URI", + // "Author" vs "Authors"), so keys are normalized to bare alphanumerics. + const normKey = (key) => key.toLowerCase().replace(/[^a-z0-9]/g, ''); + + const pick = (meta, ...keys) => { + for (const key of keys) { + const value = meta[normKey(key)]; + if (value) return value; + } + return ''; + }; + + // Turn a table cell into plain text: `
` becomes a separator, markdown + // links collapse to their label, and contact emails are dropped so the + // Author row reads as names rather than mailto noise. + const toPlainText = (value) => + value + .replace(//gi, ', ') + .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') + .replace(/\\([[\]])/g, '$1') + .replace(/\[[^\]]*@[^\]]*\]/g, '') + .replace(/`/g, '') + .replace(/\s+/g, ' ') + .replace(/\s+,/g, ',') + .replace(/[,\s]+$/, '') + .trim(); + + const firstUrl = (value) => { + const match = value.match(/https?:\/\/[^\s)|]+/); + return match ? match[0] : ''; + }; + + // GitHub builds heading anchors by lowercasing the rendered text, dropping + // punctuation and mapping whitespace to hyphens. Mirroring that lets the + // outline deep-link straight into the source file. Letters and digits are + // matched by Unicode property so accented headings keep their characters, + // and underscores survive because GitHub keeps them in snake_case names. + const slugify = (text) => + text + .toLowerCase() + .replace(/[^\p{L}\p{N}_\s-]/gu, '') + .trim() + .replace(/\s/g, '-'); + + // Reduce a heading to the text GitHub would render, since the anchor is + // derived from that rather than from the markdown source. + const headingText = (markdown) => + markdown + .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') + .replace(/\*\*/g, '') + .replace(/\*/g, '') + .replace(/`/g, '') + .trim(); + + const parseSip = (file, raw) => { + const number = Number((file.match(/sip-(\d+)\.md$/i) || [])[1]); + // Null-prototype maps: field names and heading slugs come from the SIP + // text, so a heading like "Constructor" must not collide with + // Object.prototype members. + const meta = Object.create(null); + + // Several SIPs contain further tables in their body (editor registers, + // parameter lists). Only the block above the first section heading is + // the proposal header, so metadata parsing stops there. + const header = raw.split(/^##\s/m)[0]; + + for (const line of header.split('\n')) { + const trimmed = line.trim(); + if (!trimmed.startsWith('|')) continue; + // The closing pipe is optional in GitHub-flavored markdown, so drop + // it only when present rather than requiring it. + const cells = trimmed.slice(1).split('|'); + if (cells.length && cells[cells.length - 1].trim() === '') cells.pop(); + if (cells.length < 2) continue; + const key = cells[0].trim(); + const value = cells.slice(1).join('|').trim(); + // Skip the table's alignment row (| ----- | :---- |). + if (/^:?-{2,}:?$/.test(key)) continue; + if (!key || meta[normKey(key)]) continue; + meta[normKey(key)] = value; + } + + const headline = (header.match(/^\s*\*\*SIP[-\s]?\d+:\s*(.+?)\*\*/m) || [])[1]; + const title = toPlainText(headline || pick(meta, 'Title') || file); + + const seen = Object.create(null); + const sections = []; + // Fenced blocks can contain lines that look like headings (SIP-1's format + // section quotes a template), and those produce anchors GitHub never + // renders, so strip the fences before scanning. + const prose = raw.replace(/^```[\s\S]*?^```/gm, ''); + const headingPattern = /^(#{2,4})\s+(.+?)\s*$/gm; + let match; + while ((match = headingPattern.exec(prose)) !== null) { + const text = headingText(match[2]); + if (!text) continue; + let anchor = slugify(text); + // GitHub disambiguates repeated headings with a numeric suffix. + if (seen[anchor] !== undefined) { + seen[anchor] += 1; + anchor = `${anchor}-${seen[anchor]}`; + } else { + seen[anchor] = 0; + } + sections.push({ level: match[1].length, text, anchor }); + } + + const type = toPlainText(pick(meta, 'Type')); + const category = toPlainText(pick(meta, 'Category')); + + return { + number, + file, + title, + description: toPlainText(pick(meta, 'Description')), + // SIP-3/4/5 fold the category into Type as "Standard (Core)"; SIP-2 + // keeps it in its own row. Present both the same way. + type: type && category && !type.includes(category) ? `${type} (${category})` : type || category, + status: toPlainText(pick(meta, 'Status')) || 'Unknown', + author: toPlainText(pick(meta, 'Author', 'Authors')), + reviewer: toPlainText(pick(meta, 'Reviewer', 'Editor')), + created: toPlainText(pick(meta, 'Created')), + discussion: firstUrl(pick(meta, 'Comments', 'Comments-URI', 'CommentsURI')), + sections + }; + }; + + // --- Data loading --------------------------------------------------------- + const [sips, setSips] = useState([]); + const [state, setState] = useState('loading'); + + useEffect(() => { + let cancelled = false; + + const readCache = () => { + try { + const cached = JSON.parse(localStorage.getItem(CACHE_KEY) || 'null'); + if (cached && Date.now() - cached.time < CACHE_TTL && Array.isArray(cached.sips)) { + return cached.sips; + } + } catch { + // Corrupt or unavailable storage just means we fetch again. + } + return null; + }; + + // Fetch and parse one file, returning null instead of throwing so a + // single failure never takes down the whole listing. + const readSip = async (file) => { + try { + const res = await fetch(rawUrl(file)); + if (!res.ok) return null; + return parseSip(file, await res.text()); + } catch { + return null; + } + }; + + // Primary path: one directory listing, then the files it names. + const loadFromApi = async () => { + const res = await fetch(LIST_URL, { headers: { Accept: 'application/vnd.github+json' } }); + if (!res.ok) throw new Error(`GitHub API ${res.status}`); + const files = (await res.json()).filter((entry) => entry.type === 'file' && /^sip-\d+\.md$/i.test(entry.name)).map((entry) => entry.name); + if (!files.length) throw new Error('No SIP files listed'); + // One unreadable file should cost that single card, not send the whole + // listing down the fallback path and refetch everything. + const fetched = await Promise.all(files.map(readSip)); + const parsed = fetched.filter(Boolean); + if (!parsed.length) throw new Error('No SIP files could be read'); + return parsed; + }; + + // Fallback for when the unauthenticated API quota is exhausted: probe + // sip-1.md through sip-40.md on the raw CDN. Every number is requested + // so a withdrawn or reserved number cannot truncate the index the way + // stopping after a run of misses would. + const loadByProbing = async () => { + const numbers = Array.from({ length: MAX_PROBE }, (_, i) => i + 1); + const fetched = await Promise.all(numbers.map((n) => readSip(`sip-${n}.md`))); + const found = fetched.filter(Boolean); + if (!found.length) throw new Error('No SIPs reachable'); + return found; + }; + + const cached = readCache(); + if (cached) { + setSips(cached); + setState('ready'); + return; + } + + loadFromApi() + .catch(loadByProbing) + .then((parsed) => { + if (cancelled) return; + const ordered = parsed.sort((a, b) => a.number - b.number); + setSips(ordered); + setState('ready'); + try { + localStorage.setItem(CACHE_KEY, JSON.stringify({ time: Date.now(), sips: ordered })); + } catch { + // Cache is an optimization; ignore quota or privacy-mode errors. + } + }) + .catch(() => { + if (cancelled) return; + setState('error'); + }); + + return () => { + cancelled = true; + }; + }, []); + + // --- Theming -------------------------------------------------------------- + // Mintlify serves a prebuilt stylesheet, so Tailwind utilities that appear + // only inside a snippet aren't guaranteed to exist in it. Variants carrying + // an opacity modifier (`dark:bg-neutral-900/40`) drop out that way, which is + // what left these cards white on a black page. The other data snippets here + // watch the `dark` class on and style inline from the style.css + // tokens instead, so this follows the same approach. + const [isDark, setIsDark] = useState(false); + + useEffect(() => { + const root = document.documentElement; + const sync = () => setIsDark(root.classList.contains('dark')); + sync(); + const observer = new MutationObserver(sync); + observer.observe(root, { attributes: true, attributeFilter: ['class'] }); + return () => observer.disconnect(); + }, []); + + const byMode = (light, dark) => (isDark ? dark : light); + + const theme = { + cardBg: byMode('var(--sei-card-bg-light)', 'var(--sei-card-bg-dark)'), + border: byMode('var(--sei-card-border-light)', 'var(--sei-card-border-dark)'), + strong: byMode('var(--sei-grey-600)', 'var(--sei-white)'), + body: byMode('var(--sei-grey-300)', 'var(--sei-grey-50)'), + muted: byMode('var(--sei-grey-200)', 'var(--sei-grey-75)'), + label: byMode('var(--sei-gold-100)', 'var(--sei-gold-25)'), + accent: byMode('var(--sei-maroon-100)', 'var(--sei-maroon-25)'), + hoverBg: byMode('rgba(96, 0, 20, 0.02)', 'rgba(96, 0, 20, 0.08)'), + hoverBorder: byMode('rgba(96, 0, 20, 0.35)', 'rgba(185, 155, 161, 0.25)') + }; + + // Tones map the SIP-1 lifecycle onto the Sei palette: gold while a proposal + // is still moving, green once accepted or shipped, grey for dormant states, + // red for withdrawn. These mirror the style.css tokens as literal hex + // because each badge tints its own fill, and the alpha channels can't be + // derived from a var() reference. `--sei-live` and `--sei-error` are pitched + // for dark surfaces and drop under 2:1 as text on white, so light mode takes + // a darkened counterpart. + const TONES = { + live: byMode('#1a7a00', '#38df00'), + error: byMode('#c20a00', '#fa0c00'), + gold: byMode('#966f22', '#d6c9ac'), + maroon: byMode('#600014', '#b99ba1'), + grey: byMode('#666666', '#999999') + }; + + const STATUS_TONES = { + idea: 'grey', + draft: 'gold', + review: 'gold', + 'fast track': 'gold', + 'last call': 'gold', + final: 'live', + implemented: 'live', + activated: 'live', + living: 'maroon', + stagnant: 'grey', + withdrawn: 'error' + }; + + const channels = (hex) => { + const n = parseInt(hex.slice(1), 16); + return `${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}`; + }; + + const eyebrow = { + fontFamily: 'var(--sei-font-mono)', + fontSize: '10px', + letterSpacing: '0.04em', + textTransform: 'uppercase' + }; + + const statusStyle = (status) => { + const hex = TONES[STATUS_TONES[status.toLowerCase()] || 'grey']; + const rgb = channels(hex); + return { + ...eyebrow, + color: hex, + backgroundColor: `rgba(${rgb}, ${byMode(0.09, 0.14)})`, + border: `1px solid rgba(${rgb}, ${byMode(0.28, 0.32)})`, + borderRadius: 'var(--sei-radius-sm)', + padding: '3px 6px', + whiteSpace: 'nowrap' + }; + }; + + const cardStyle = { + background: theme.cardBg, + border: `1px solid ${theme.border}`, + borderRadius: 'var(--sei-radius-sm)', + padding: '20px' + }; + + const buttonStyle = { + display: 'inline-flex', + alignItems: 'center', + gap: '6px', + padding: '6px 12px', + fontFamily: 'var(--sei-font-mono)', + fontSize: '12px', + letterSpacing: '0.02em', + color: theme.strong, + background: 'transparent', + border: `1px solid ${theme.border}`, + borderRadius: 'var(--sei-radius-sm)', + textDecoration: 'none', + transition: 'background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease' + }; + + // Inline styles can't express :hover, so the maroon card hover from + // style.css is reproduced with handlers. + const hoverSwap = (hovering) => (event) => { + event.currentTarget.style.backgroundColor = hovering ? theme.hoverBg : 'transparent'; + event.currentTarget.style.borderColor = hovering ? theme.hoverBorder : theme.border; + event.currentTarget.style.color = hovering ? theme.accent : theme.strong; + }; + + const tintSwap = (hovering) => (event) => { + event.currentTarget.style.color = hovering ? theme.accent : theme.body; + }; + + const ExternalLinkIcon = () => ( + + ); + + const Field = ({ label, value }) => + value ? ( +
+
{label}
+
{value}
+
+ ) : null; + + const statusMessage = { ...cardStyle, fontSize: '14px', color: theme.body }; + + if (state === 'loading') { + return ( +
+ Loading proposals from {REPO}… +
+ ); + } + + if (state === 'error') { + return ( +
+ Could not reach the SIPs repository. Browse the proposals directly at{' '} + + github.com/{REPO} + + . +
+ ); + } + + return ( +
+ {sips.map((sip) => ( +
+
+ + SIP-{sip.number} + + {sip.status} + {sip.type ? {sip.type} : null} +
+ +

+ {sip.title} +

+ + {sip.description ? ( +

{sip.description}

+ ) : null} + +
+ + + +
+ + {/* style.css frames every
as the shared accordion with + !important, so the outline is inset by the summary's own 16px + padding rather than given a competing border here. */} + {sip.sections.length ? ( +
+ + Contents ({sip.sections.length} sections) + + +
+ ) : null} + +
+ + Read SIP-{sip.number} + + {sip.discussion ? ( + + Discussion + + ) : null} +
+
+ ))} + +

+ Pulled live from{' '} + + {REPO} + + . Statuses and metadata reflect the {BRANCH} branch. +

+
+ ); +};