Skip to content

Solana WASM registry#41

Merged
niklabh merged 2 commits into
mainfrom
solana-wasm-registry
May 11, 2026
Merged

Solana WASM registry#41
niklabh merged 2 commits into
mainfrom
solana-wasm-registry

Conversation

@niklabh
Copy link
Copy Markdown
Owner

@niklabh niklabh commented May 11, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a markdown editor example with split-pane view and live preview functionality
    • Introduced multi-line text area widget for user input
    • Added Solana WASM Registry program for on-chain WASM module management with version control and publisher verification
  • Documentation

    • Comprehensive guide for Solana WASM Registry including setup, deployment, and usage workflows

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Warning

Rate limit exceeded

@niklabh has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 30 minutes and 53 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6b7d6428-806e-40ee-b956-60b5bdaee1f9

📥 Commits

Reviewing files that changed from the base of the PR and between 3d2d0d7 and f4714a6.

📒 Files selected for processing (7)
  • oxide-browser/Cargo.toml
  • oxide-browser/src/capabilities.rs
  • oxide-browser/src/lib.rs
  • oxide-browser/src/manifest.rs
  • oxide-browser/src/runtime.rs
  • oxide-browser/src/solana.rs
  • oxide-browser/src/ui.rs
📝 Walkthrough

Walkthrough

PR introduces a markdown editor WASM example integrated with oxide-sdk, extends the oxide browser/SDK stack with multi-line textarea widget support including caret tracking and keyboard editing, and adds a complete Solana on-chain WASM registry program with versioning and publisher authentication.

Changes

Markdown Editor Example

Layer / File(s) Summary
Workspace and Index Registration
Cargo.toml, examples/index/src/lib.rs
Markdown editor crate added to workspace members; example index card registered with TEAL color constant and display metadata.
Crate Configuration
examples/markdown-editor/Cargo.toml
New crate manifest configures cdylib target and declares oxide-sdk workspace dependency.
Entry Point and Frame Loop
examples/markdown-editor/src/lib.rs (lines 1–152)
start_app() initializes preview scroll state; on_frame() renders split-pane layout with header, markdown input, and scrollable preview clip region.
Markdown Parsing and Rendering
examples/markdown-editor/src/lib.rs (lines 162–335)
preview_walk() iterates markdown lines, dispatching styles and layout for headings, code blocks, rules, lists, and paragraphs with viewport clipping checks.
Text Layout and Measurement
examples/markdown-editor/src/lib.rs (lines 361–495)
wrap_plain() breaks lines by canvas width; emit_line() measures glyphs and draws only visible lines; helpers compute heading heights and detect rules.

Multi-line TextArea Widget Support

Layer / File(s) Summary
SDK Public API and Host FFI
oxide-sdk/src/lib.rs
ui_text_area() public function buffers calls to host FFI binding _api_ui_text_area, returning widget text as decoded UTF-8 string.
Host State and Widget Command
oxide-browser/src/capabilities.rs
HostState adds widget_text_caret map tracking per-widget byte caret positions; WidgetCommand::TextArea variant carries geometry and widget id for host rendering.
Guest Text Focus State Refactor
oxide-browser/src/ui.rs (lines 110–203)
GuestTextFocus enum replaces single-line text_input_focus, supporting both TextInput and TextArea modes; TabState caches per-textarea ScrollHandles.
TextArea Keyboard Input Handling
oxide-browser/src/ui.rs (lines 1617–1788)
on_key_down() branches on GuestTextFocus::TextArea, implementing caret movement (up/down by line, left/right by byte), character insertion with newlines, backspace/delete, and clipboard shortcuts.
TextArea Overlay Rendering and Hit-Testing
oxide-browser/src/ui.rs (lines 3927–4741)
TextArea overlay draws clipped scrollable text with positioned caret; click-to-set-caret uses byte index hit-testing; helper functions compute line splits, wrap widths, and caret geometry.
TextArea API Documentation
oxide-docs/src/lib.rs, oxide-sdk/src/lib.rs (lines 106–106)
API reference lists ui_text_area widget as multi-line text input returning current text.

Solana WASM Registry On-Chain Program

Layer / File(s) Summary
Workspace and Build Configuration
solana-wasm-registry/Cargo.toml, solana-wasm-registry/Anchor.toml, solana-wasm-registry/package.json
New workspace declares program member glob, release optimizations (LTO, overflow checks), and Anchor/TypeScript configuration for devnet/localnet with test script.
Program State and Constants
solana-wasm-registry/programs/wasm-registry/src/lib.rs (lines 1–9, 111–133)
WasmEntry account structure stores publisher, hash, name, version, timestamps, and bump; PDA derived from [b"wasm", publisher, name].
Instructions and Anchor Contexts
solana-wasm-registry/programs/wasm-registry/src/lib.rs (lines 10–109)
register() initializes entry; update() increments version with overflow protection; revoke() closes account. Contexts enforce PDA constraints and has_one publisher checks.
Events and Error Types
solana-wasm-registry/programs/wasm-registry/src/lib.rs (lines 134–164)
Event structs emitted for registration, update, and revocation; custom error codes validate name bounds and version overflow.
Test Suite
solana-wasm-registry/tests/wasm-registry.ts
Tests register (assert initial state), update (verify version bump), unauthorized update (permission denial), and revoke (account closure) via Anchor TypeScript client.
Documentation and Configuration
solana-wasm-registry/README.md, .gitignore, tsconfig.json
README documents registry purpose, account model, instructions, local/devnet/mainnet deployment, client usage, events, and risk notes; .gitignore and tsconfig.json configure VCS and TypeScript.

Sequence Diagram(s)

sequenceDiagram
  participant Guest as Guest WASM
  participant SDK as oxide_sdk
  participant Host as oxide-browser Host
  participant Canvas as oxide::canvas
  
  Guest->>SDK: ui_text_area(id, x, y, w, h, initial)
  SDK->>Host: _api_ui_text_area(..., out_buf, out_cap)
  Host->>Host: Load/initialize WidgetValue::Text
  Host->>Host: Enqueue WidgetCommand::TextArea
  Host->>Canvas: Render textarea overlay
  Host->>SDK: Write current text to out_buf, return len
  SDK->>SDK: Decode UTF-8 from buffer
  SDK->>Guest: Return String with current text
Loading
sequenceDiagram
  participant User as User Input
  participant UI as oxide-browser UI
  participant CaretMap as host_state.widget_text_caret
  
  User->>UI: Click at pixel position in textarea
  UI->>UI: Hit-test to compute byte index
  UI->>CaretMap: Store caret byte offset
  UI->>UI: Set guest_text_focus to TextArea(id)
  
  User->>UI: Press key (up/down/left/right/delete/backspace)
  UI->>CaretMap: Load current caret byte offset
  UI->>UI: Compute new caret position (byte-safe)
  UI->>UI: Insert/delete/navigate in text buffer
  UI->>CaretMap: Update caret byte offset
  UI->>UI: Redraw textarea with new caret position
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • niklabh/oxide#30: Both PRs extend HostState and WidgetCommand in oxide-browser/src/capabilities.rs and refactor text input handling in oxide-browser/src/ui.rs, introducing new widget command variants and guest focus state.
  • niklabh/oxide#6: Related through shared modification of oxide-browser/src/capabilities.rs, oxide-browser/src/ui.rs, and oxide-sdk—both PRs enhance the widget/input host API by adding primitives that extend text and UI widget support.

Poem

🐰 Split-pane markdown dreams in WASM so fine,
Textarea carets dancing, line by line.
Solana hashes registered, versioned with care,
Three changes bound: editor, widget, blockchain fair! ✨📝🔗

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Solana WASM registry' directly matches the primary focus of the changeset, which adds a complete Solana WASM registry program with supporting infrastructure across multiple directories and crates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch solana-wasm-registry

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (10)
solana-wasm-registry/programs/wasm-registry/src/lib.rs (4)

14-36: 💤 Low value

Re-registration after revoke resets version to 0 — document this in the README.

After revoke closes the PDA, the same (publisher, name) pair can call register again, getting a brand-new account with version = 0 and a fresh created_at. Downstream consumers (e.g., a browser pulling a name@version reference) cannot distinguish a re-created entry from the original by (name, version) alone. The summary mentions the README documents events/history; please ensure it explicitly calls out that revoke is destructive and not version-monotonic across the entry's lifetime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@solana-wasm-registry/programs/wasm-registry/src/lib.rs` around lines 14 - 36,
The README must explicitly state that calling revoke destroys the PDA and that
subsequent calls to register on the same (publisher, name) produce a fresh
account with version reset to 0 and new created_at, so consumers cannot rely on
(name, version) to identify the original artifact; update README to mention
revoke is destructive (not version-monotonic), reference the register and revoke
behaviors (register sets version = 0, created_at/updated_at from Clock::get(),
and emit EntryRegistered) and recommend using persistent identifiers or
event/history streams if consumers need non-ambiguous lineage.

5-5: 💤 Low value

Placeholder declare_id! must be replaced before mainnet/devnet deployment.

Already noted in the comment above, but flagging explicitly: this program ID is a placeholder. Anchor will mismatch program.programId from the IDL against this baked-in value if you forget to run anchor keys sync after the first build (cf. DeclaredProgramIdMismatch). If the workspace has CI or a deploy script, consider adding a guard there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@solana-wasm-registry/programs/wasm-registry/src/lib.rs` at line 5, The file
currently contains a placeholder program ID in the declare_id! macro
(declare_id!) which must be replaced before mainnet/devnet deployment; update
declare_id! to use the real deployed program ID (or wire it to an
environment/config value injected at build time) and ensure your deploy workflow
runs `anchor keys sync` (or otherwise regenerates the IDL) after the first build
so Anchor's IDL programId matches the baked-in value, and add a CI/deploy guard
that validates the declared program ID against the IDL/program.programId to fail
fast if the placeholder remains.

122-132: 💤 Low value

Consider #[derive(InitSpace)] with #[max_len(...)] instead of manual SPACE accounting.

Anchor's InitSpace derive auto-computes the on-chain layout and stays in sync if you ever add/remove a field. The manual layout works today, but every future field change requires hand-editing the const arithmetic and the comment block. Optional and safe to do in a follow-up.

♻️ Sketch
#[account]
#[derive(InitSpace)]
pub struct WasmEntry {
    pub publisher: Pubkey,
    pub hash: [u8; 32],
    #[max_len(MAX_NAME_LEN)]
    pub name: String,
    pub version: u32,
    pub created_at: i64,
    pub updated_at: i64,
    pub bump: u8,
}
// space = 8 + WasmEntry::INIT_SPACE
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@solana-wasm-registry/programs/wasm-registry/src/lib.rs` around lines 122 -
132, Replace the manual SPACE constant on WasmEntry with Anchor's automatic
InitSpace derivation: add #[derive(InitSpace)] to the WasmEntry struct, mark the
name field with #[max_len(MAX_NAME_LEN)], remove or stop maintaining the manual
pub const SPACE calculation, and use the generated WasmEntry::INIT_SPACE (plus
the 8-byte account discriminator where needed) for account sizing; update any
code that referenced WasmEntry::SPACE to use 8 + WasmEntry::INIT_SPACE instead.

56-62: 💤 Low value

revoke emits the event before the account is closed — confirm event field copying is intentional.

name is cloned out before close, so the event payload survives — that's correct. Just noting that any future refactor that moves the clone after Ok(()) (i.e., into a CPI/close step) would emit a stale or zero-byte event. Consider adding a brief inline comment to anchor the ordering invariant for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@solana-wasm-registry/programs/wasm-registry/src/lib.rs` around lines 56 - 62,
The revoke function currently emits EntryRevoked using
ctx.accounts.entry.name.clone() before the account is closed; add a brief inline
comment in revoke (near the emit! for EntryRevoked and the clone call) stating
that the event must be emitted before the account close/CPI so the cloned fields
survive, and warn maintainers not to move the clone/emit after the close or into
a CPI to avoid emitting empty/stale data; reference the revoke function and the
EntryRevoked event name in the comment.
oxide-browser/src/ui.rs (6)

3951-3956: 💤 Low value

textarea_scroll_handles is never pruned when textareas disappear.

A ScrollHandle is inserted per textarea id on first render, but the map is only cleared on drain_results (line 332) — i.e., on navigation. If a guest dynamically creates/destroys textareas at varying ids during a session (e.g., a long-running app that recreates widgets per frame with new ids), the map will grow unboundedly. Consider snapshotting current ids each frame and pruning entries not present in the latest WidgetCommand::TextArea set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@oxide-browser/src/ui.rs` around lines 3951 - 3956, textarea_scroll_handles
currently accumulates ScrollHandle entries for every observed textarea id and is
only cleared on drain_results; fix by snapshotting the set of active textarea
ids each render/frame (from the current WidgetCommand::TextArea instances) and
prune textarea_scroll_handles to remove keys not present in that set so stale
ids are dropped; perform this pruning in the same place you iterate/render
textareas (before inserting new entries) so each frame you keep only live ids
and avoid unbounded growth of the map.

1668-1671: 💤 Low value

Tab key is swallowed silently inside the textarea.

Pressing Tab inside the textarea simply triggers cx.notify(); return; without inserting a tab character or moving focus. Users typing into a code/snippet textarea will get nothing. If preventing focus-shift was the intent, consider inserting "\t" (or, at minimum, a couple of spaces) so the keystroke is at least meaningful.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@oxide-browser/src/ui.rs` around lines 1668 - 1671, The handler currently
swallows Tab by doing if event.keystroke.key == "tab" { cx.notify(); return; },
so change it to insert a tab (or spaces) into the textarea instead of silently
returning: when event.keystroke.key == "tab" prevent the default focus change,
compute the caret/selection in the textarea's value, insert "\t" (or desired
spaces) at the caret or replace the selection, update the textarea's value and
caret position, then call cx.notify() and return; (optionally handle Shift+Tab
for unindent). Use the existing event.keystroke.key check and cx.notify() call
sites to locate where to apply this change.

4647-4678: 💤 Low value

Newline-accounting in textarea_hit_index may skip the last line's \n.

The post-line advance only skips \n when iter.peek().is_some(). If text ends with a trailing newline, on the final iteration peek() is None, so byte_off is not incremented past the trailing \n — but then we fall through to text.len() at the end, which is still a valid char boundary, so this happens to be safe today. However, this coupling is fragile: a future change that does any byte-level work after the loop will see byte_off short by 1 on trailing-newline inputs. Consider always advancing past \n (and only relying on the loop body to early-return for the line under the cursor).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@oxide-browser/src/ui.rs` around lines 4647 - 4678, In textarea_hit_index, the
logic that advances byte_off past a newline is conditional on
iter.peek().is_some(), which leaves byte_off unchanged when the final line ends
with '\n'; change the post-line advance so it always checks the next byte (if
any) and increments byte_off when it's b'\n' regardless of peek, guarding
against going past text.len(); keep the early-return inside the loop unchanged
so cursor-hit still returns immediately for the matching line.

1701-1716: 💤 Low value

Caret column on up/down uses byte offset, not visual column.

col = caret - textarea_line_start(&text, caret) is a byte delta. On lines containing multi-byte UTF-8 (e.g., emoji or non-ASCII text), pressing up/down preserves byte count instead of column position, so the caret can land in a visually inconsistent location on lines of differing byte composition. floor_char_boundary only snaps to a valid char boundary, it does not correct the visual column. Consider tracking columns by char count (or shaped x-position) instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@oxide-browser/src/ui.rs` around lines 1701 - 1716, The caret column
calculation uses byte offsets (col = caret - textarea_line_start(&text, caret))
which breaks on multi-byte UTF-8; change it to compute column as a
character/grapheme count from the line start and then advance that many
characters on the target line: when handling "up"/"down" compute col = number of
chars (or grapheme clusters using unicode-segmentation) between
textarea_line_start(&text, caret) and caret, then when you have the target line
start (textarea_prev_line_start / textarea_next_line_start) advance by that many
characters but clamp to textarea_line_end(&text, st) and finally snap to a valid
byte boundary with text.floor_char_boundary(caret); update the code paths around
textarea_line_start, textarea_prev_line_start, textarea_next_line_start,
textarea_line_end and the caret assignment accordingly.

4565-4570: 💤 Low value

unwrap_or_else is reached with s[caret..].find('\n') == None; the closure value is correct but the path is more naturally unwrap_or.

s[caret..].find('\n') returns indices in 0..s.len()-caret. When no newline exists, you want caret + (s.len() - caret) = s.len(). The saturating_sub is unnecessary because caret <= s.len() is an invariant in every call site. A clearer form:

♻️ Suggested simplification
 fn textarea_line_end(s: &str, caret: usize) -> usize {
-    caret
-        + s[caret..]
-            .find('\n')
-            .unwrap_or_else(|| s.len().saturating_sub(caret))
+    match s[caret..].find('\n') {
+        Some(off) => caret + off,
+        None => s.len(),
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@oxide-browser/src/ui.rs` around lines 4565 - 4570, The function
textarea_line_end should simplify the fallback from unwrap_or_else to unwrap_or
and remove the unnecessary saturating_sub: replace the closure in
textarea_line_end that returns s.len().saturating_sub(caret) with a direct
unwrap_or(s.len() - caret) because caret <= s.len() is guaranteed; keep the
overall expression caret + s[caret..].find('\n').unwrap_or(s.len() - caret) to
return s.len() when no newline is found.

1635-1666: 💤 Low value

Multi-line Cmd/Ctrl-X clears the entire textarea regardless of selection.

"x" unconditionally copies the whole text to the clipboard and resets the textarea to empty (lines 1653-1660). With no selection model yet implemented for the textarea, this is effectively "select-all + cut". The same applies to "c" (always copies full text). This may surprise users expecting Cmd-X to be a no-op when nothing is selected. The single-line path has the same behavior, so it may be intentional — worth a TODO at minimum once selection support lands.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@oxide-browser/src/ui.rs` around lines 1635 - 1666, The multi-line
secondary-modifier handling for keys "c", "v", and "x" in the event.keystroke
branch currently acts on the whole `text` unconditionally (using
`arboard::Clipboard::new()`, `text`, `caret`, `caret_m`, and `states` /
`WidgetValue::Text`), which effectively performs select-all + cut/copy; update
this logic to only perform copy/cut when an actual selection exists (check your
selection model/range if present) and otherwise treat Cmd/Ctrl-C/X as a no-op
(or add a clear TODO comment if selection is not yet implemented), leaving paste
("v") behavior unchanged; ensure caret and `states.insert(id,
WidgetValue::Text(...))` are only updated when cutting a real selection, and
keep the `cx.notify()` / return semantics intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/markdown-editor/src/lib.rs`:
- Around line 171-333: The markdown walker currently allocates many temporary
Vec/String values every frame (e.g., building lines Vec, creating new code and
para Strings, and calling wrap_plain which uses heap allocations); fix by making
the parser allocation-minimal: iterate md.lines() directly instead of collecting
into lines Vec, reuse a single preallocated String buffer for code and para
(clear() between uses) instead of creating new Strings, refactor wrap_plain and
wrapped_body to accept &str slices and avoid formatting/collecting into Vecs
(return layout y without heap allocations), and ensure preview_walk calls the
non-allocating wrap_plain/wrapped_body variants; update simplify_paragraph to
work on &str or a reusable buffer to avoid per-frame allocations.

In `@solana-wasm-registry/programs/wasm-registry/src/lib.rs`:
- Line 7: The constant MAX_NAME_LEN (used in WasmEntry::SPACE) allows names up
to 64 bytes but the register PDA derivation uses name.as_bytes() directly (in
register), which can exceed Solana's per-seed 32-byte limit and cause
MaxSeedLengthExceeded at runtime; fix by either (A) clamping MAX_NAME_LEN to 32
and shrinking WasmEntry::SPACE accordingly so name fits in a single seed, or (B)
change the PDA seed in register (and any lookups) to a 32-byte hash of the name
using anchor_lang::solana_program::hash::hash(name.as_bytes()).to_bytes() while
keeping the full name in the account data and updating tests/helpers that derive
the PDA.

In `@solana-wasm-registry/README.md`:
- Around line 18-20: The fenced code block containing the PDA seed example
(seeds = [ "wasm", publisher_pubkey, name_bytes ]) needs a language tag to
satisfy markdownlint MD040; update the triple-backtick fence to include a
language (e.g., ```text or ```rust) so the block becomes ```text followed by the
seeds line and closing ``` to improve rendering and linting.
- Around line 126-138: The README example omits the publisher account used by
the tested instruction calls; update the client snippets that call
program.methods.register(name, hash) and program.methods.update(newHash) to
include the publisher account in the .accounts({ ... }) map (e.g., .accounts({
entry: entryPda, publisher: publisherPubkey })) and ensure the publisher keypair
is provided as a signer when sending the transaction so the instruction shapes
match the tests (refer to program.methods.register, program.methods.update,
entryPda, and the publisher account).

---

Nitpick comments:
In `@oxide-browser/src/ui.rs`:
- Around line 3951-3956: textarea_scroll_handles currently accumulates
ScrollHandle entries for every observed textarea id and is only cleared on
drain_results; fix by snapshotting the set of active textarea ids each
render/frame (from the current WidgetCommand::TextArea instances) and prune
textarea_scroll_handles to remove keys not present in that set so stale ids are
dropped; perform this pruning in the same place you iterate/render textareas
(before inserting new entries) so each frame you keep only live ids and avoid
unbounded growth of the map.
- Around line 1668-1671: The handler currently swallows Tab by doing if
event.keystroke.key == "tab" { cx.notify(); return; }, so change it to insert a
tab (or spaces) into the textarea instead of silently returning: when
event.keystroke.key == "tab" prevent the default focus change, compute the
caret/selection in the textarea's value, insert "\t" (or desired spaces) at the
caret or replace the selection, update the textarea's value and caret position,
then call cx.notify() and return; (optionally handle Shift+Tab for unindent).
Use the existing event.keystroke.key check and cx.notify() call sites to locate
where to apply this change.
- Around line 4647-4678: In textarea_hit_index, the logic that advances byte_off
past a newline is conditional on iter.peek().is_some(), which leaves byte_off
unchanged when the final line ends with '\n'; change the post-line advance so it
always checks the next byte (if any) and increments byte_off when it's b'\n'
regardless of peek, guarding against going past text.len(); keep the
early-return inside the loop unchanged so cursor-hit still returns immediately
for the matching line.
- Around line 1701-1716: The caret column calculation uses byte offsets (col =
caret - textarea_line_start(&text, caret)) which breaks on multi-byte UTF-8;
change it to compute column as a character/grapheme count from the line start
and then advance that many characters on the target line: when handling
"up"/"down" compute col = number of chars (or grapheme clusters using
unicode-segmentation) between textarea_line_start(&text, caret) and caret, then
when you have the target line start (textarea_prev_line_start /
textarea_next_line_start) advance by that many characters but clamp to
textarea_line_end(&text, st) and finally snap to a valid byte boundary with
text.floor_char_boundary(caret); update the code paths around
textarea_line_start, textarea_prev_line_start, textarea_next_line_start,
textarea_line_end and the caret assignment accordingly.
- Around line 4565-4570: The function textarea_line_end should simplify the
fallback from unwrap_or_else to unwrap_or and remove the unnecessary
saturating_sub: replace the closure in textarea_line_end that returns
s.len().saturating_sub(caret) with a direct unwrap_or(s.len() - caret) because
caret <= s.len() is guaranteed; keep the overall expression caret +
s[caret..].find('\n').unwrap_or(s.len() - caret) to return s.len() when no
newline is found.
- Around line 1635-1666: The multi-line secondary-modifier handling for keys
"c", "v", and "x" in the event.keystroke branch currently acts on the whole
`text` unconditionally (using `arboard::Clipboard::new()`, `text`, `caret`,
`caret_m`, and `states` / `WidgetValue::Text`), which effectively performs
select-all + cut/copy; update this logic to only perform copy/cut when an actual
selection exists (check your selection model/range if present) and otherwise
treat Cmd/Ctrl-C/X as a no-op (or add a clear TODO comment if selection is not
yet implemented), leaving paste ("v") behavior unchanged; ensure caret and
`states.insert(id, WidgetValue::Text(...))` are only updated when cutting a real
selection, and keep the `cx.notify()` / return semantics intact.

In `@solana-wasm-registry/programs/wasm-registry/src/lib.rs`:
- Around line 14-36: The README must explicitly state that calling revoke
destroys the PDA and that subsequent calls to register on the same (publisher,
name) produce a fresh account with version reset to 0 and new created_at, so
consumers cannot rely on (name, version) to identify the original artifact;
update README to mention revoke is destructive (not version-monotonic),
reference the register and revoke behaviors (register sets version = 0,
created_at/updated_at from Clock::get(), and emit EntryRegistered) and recommend
using persistent identifiers or event/history streams if consumers need
non-ambiguous lineage.
- Line 5: The file currently contains a placeholder program ID in the
declare_id! macro (declare_id!) which must be replaced before mainnet/devnet
deployment; update declare_id! to use the real deployed program ID (or wire it
to an environment/config value injected at build time) and ensure your deploy
workflow runs `anchor keys sync` (or otherwise regenerates the IDL) after the
first build so Anchor's IDL programId matches the baked-in value, and add a
CI/deploy guard that validates the declared program ID against the
IDL/program.programId to fail fast if the placeholder remains.
- Around line 122-132: Replace the manual SPACE constant on WasmEntry with
Anchor's automatic InitSpace derivation: add #[derive(InitSpace)] to the
WasmEntry struct, mark the name field with #[max_len(MAX_NAME_LEN)], remove or
stop maintaining the manual pub const SPACE calculation, and use the generated
WasmEntry::INIT_SPACE (plus the 8-byte account discriminator where needed) for
account sizing; update any code that referenced WasmEntry::SPACE to use 8 +
WasmEntry::INIT_SPACE instead.
- Around line 56-62: The revoke function currently emits EntryRevoked using
ctx.accounts.entry.name.clone() before the account is closed; add a brief inline
comment in revoke (near the emit! for EntryRevoked and the clone call) stating
that the event must be emitted before the account close/CPI so the cloned fields
survive, and warn maintainers not to move the clone/emit after the close or into
a CPI to avoid emitting empty/stale data; reference the revoke function and the
EntryRevoked event name in the comment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: caf5fd2c-3d20-4078-8c40-fdabb00eba32

📥 Commits

Reviewing files that changed from the base of the PR and between 0d53e5e and 3d2d0d7.

📒 Files selected for processing (17)
  • Cargo.toml
  • examples/index/src/lib.rs
  • examples/markdown-editor/Cargo.toml
  • examples/markdown-editor/src/lib.rs
  • oxide-browser/src/capabilities.rs
  • oxide-browser/src/ui.rs
  • oxide-docs/src/lib.rs
  • oxide-sdk/src/lib.rs
  • solana-wasm-registry/.gitignore
  • solana-wasm-registry/Anchor.toml
  • solana-wasm-registry/Cargo.toml
  • solana-wasm-registry/README.md
  • solana-wasm-registry/package.json
  • solana-wasm-registry/programs/wasm-registry/Cargo.toml
  • solana-wasm-registry/programs/wasm-registry/src/lib.rs
  • solana-wasm-registry/tests/wasm-registry.ts
  • solana-wasm-registry/tsconfig.json

Comment on lines +171 to +333
let lines: Vec<&str> = md.lines().collect();
let mut i = 0usize;
let text_r = 220u8;
let text_g = 220u8;
let text_b = 232u8;

while i < lines.len() {
let raw = lines[i];
let line = raw.trim_end();

if line.is_empty() {
st.y += 8.0;
i += 1;
continue;
}

if line.starts_with("```") {
i += 1;
let mut code = String::new();
while i < lines.len() && !lines[i].trim_start().starts_with("```") {
if !code.is_empty() {
code.push('\n');
}
code.push_str(lines[i]);
i += 1;
}
if i < lines.len() {
i += 1;
}

let box_top = st.y + 8.0;
let inner_x = x + 6.0;
let inner_w = max_w - 12.0;
let text_top = box_top + 10.0;
let zz = code.split('\n').fold(text_top, |yy, ln| {
if ln.is_empty() {
yy + 12.6 * 1.28
} else {
wrap_plain(
false, inner_x, yy, inner_w, ln, 12.6, 400, 200, 206, 222, clip_top,
clip_bot,
)
}
});
let h_box = (zz - box_top + 14.0).max(34.0);
if draw && overlaps(box_top - 8.0, box_top + h_box, clip_top, clip_bot) {
canvas_rect(x - 4.0, box_top - 4.0, max_w + 8.0, h_box, 42, 44, 58, 255);
let _ = code.split('\n').fold(text_top, |yy, ln| {
if ln.is_empty() {
yy + 12.6 * 1.28
} else {
wrap_plain(
true, inner_x, yy, inner_w, ln, 12.6, 400, 200, 206, 222, clip_top,
clip_bot,
)
}
});
}
st.y += h_box + 12.0;
continue;
}

if is_rule(line) {
let hy = st.y + 6.0;
if draw && hy >= clip_top && hy <= clip_bot {
canvas_line(x - 4.0, hy, x + max_w + 4.0, hy, 72, 72, 94, 255, 1.0);
}
st.y += 16.0;
i += 1;
continue;
}

if let Some(rest) = line.strip_prefix("### ") {
st.y = heading_height(
st.y, draw, x, rest, max_w, clip_top, clip_bot, 17.6, 600, 205, 210, 248,
);
i += 1;
continue;
}
if let Some(rest) = line.strip_prefix("## ") {
st.y = heading_height(
st.y, draw, x, rest, max_w, clip_top, clip_bot, 19.4, 650, 210, 220, 248,
);
i += 1;
continue;
}
if let Some(rest) = line.strip_prefix("# ") {
st.y = heading_height(
st.y, draw, x, rest, max_w, clip_top, clip_bot, 24.8, 700, 230, 228, 255,
);
i += 1;
continue;
}

if let Some(item) = line.strip_prefix("- ").or_else(|| line.strip_prefix("* ")) {
let pill = st.y + 18.0;
if draw && pill >= clip_top && pill <= clip_bot {
canvas_text_ex(
x + 4.0,
pill + 13.8,
14.8,
160,
178,
210,
255,
"",
700,
FONT_STYLE_NORMAL,
TEXT_ALIGN_LEFT,
"•",
);
}
st.y = wrapped_body(
st.y,
draw,
x + 24.0,
item,
max_w - 26.0,
clip_top,
clip_bot,
text_r,
text_g,
text_b,
);
st.y += 6.0;
i += 1;
continue;
}

let mut para = String::from(line);
i += 1;
while i < lines.len() {
let n = lines[i].trim_end();
if n.is_empty() {
break;
}
if n.starts_with('#')
|| n.starts_with("```")
|| n.starts_with("- ")
|| n.starts_with("* ")
|| is_rule(n)
{
break;
}
para.push(' ');
para.push_str(n.trim_start());
i += 1;
}
let simplified = simplify_paragraph(&para);
st.y = wrapped_body(
st.y,
draw,
x,
&simplified,
max_w,
clip_top,
clip_bot,
text_r,
text_g,
text_b,
);
st.y += 10.0;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Reduce per-frame heap churn in markdown parsing/wrapping hot paths.

preview_walk and wrap_plain currently allocate many temporary Vec/String values every frame (and preview is walked twice). This is expensive for wasm32-unknown-unknown guest examples.

Targeted refactor example (remove `Vec` + `format!` churn in `wrap_plain`)
 fn wrap_plain(
@@
 ) -> f32 {
-    let words: Vec<&str> = text.split_whitespace().collect();
-    if words.is_empty() {
+    let mut words = text.split_whitespace();
+    let Some(first) = words.next() else {
         return y + size * 0.6;
-    }
-    let mut line = String::new();
+    };
+    let mut line = String::from(first);
     let line_gap = size * 1.28;
 
     for w in words {
-        let trial = if line.is_empty() {
-            w.to_string()
-        } else {
-            format!("{line} {w}")
-        };
-        let m = canvas_measure_text(size, "", weight, FONT_STYLE_NORMAL, &trial);
-        if m.width > max_w && !line.is_empty() {
+        let prev_len = line.len();
+        line.push(' ');
+        line.push_str(w);
+        let m = canvas_measure_text(size, "", weight, FONT_STYLE_NORMAL, &line);
+        if m.width > max_w {
+            line.truncate(prev_len);
             emit_line(
                 draw,
                 x,
                 &line,
@@
                 y + size,
             );
             y += line_gap;
-            line = w.to_string();
-        } else {
-            line = trial;
+            line.clear();
+            line.push_str(w);
         }
     }

As per coding guidelines, Guest app code must remain allocation-minimal since examples run on wasm32-unknown-unknown with no std allocator by default unless alloc is explicitly linked.

Also applies to: 389-454

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/markdown-editor/src/lib.rs` around lines 171 - 333, The markdown
walker currently allocates many temporary Vec/String values every frame (e.g.,
building lines Vec, creating new code and para Strings, and calling wrap_plain
which uses heap allocations); fix by making the parser allocation-minimal:
iterate md.lines() directly instead of collecting into lines Vec, reuse a single
preallocated String buffer for code and para (clear() between uses) instead of
creating new Strings, refactor wrap_plain and wrapped_body to accept &str slices
and avoid formatting/collecting into Vecs (return layout y without heap
allocations), and ensure preview_walk calls the non-allocating
wrap_plain/wrapped_body variants; update simplify_paragraph to work on &str or a
reusable buffer to avoid per-frame allocations.

// replace this with the real program ID generated under target/deploy/.
declare_id!("8CZfcw3uB6wXjmzsQaVmDwxEGvKirwbBpemZE8eF8Sjb");

pub const MAX_NAME_LEN: usize = 64;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm test fixtures only use short names and never exercise the 33-64 byte range.
fd -t f 'wasm-registry.ts' --exec rg -nH -C3 'register|name' {} \;

Repository: niklabh/oxide

Length of output: 2230


🏁 Script executed:

cat -n solana-wasm-registry/programs/wasm-registry/src/lib.rs | head -120

Repository: niklabh/oxide

Length of output: 4045


🌐 Web query:

Solana MAX_SEED_LEN PDA seed length constraint 32 bytes

💡 Result:

Solana PDA (program derived address) seed length constraint is 32 bytes maximum per individual seed. Solana’s PDA derivation docs state that seeds have a maximum 32 bytes per seed limit [1]. This matches the SDK constant MAX_SEED_LEN: usize = 32 described in Solana’s Rust SDK [2] and the Solana core implementation which panics / errors if any individual seed’s length exceeds MAX_SEED_LEN [3]. So when you build PDA seeds for Pubkey::find_program_address / create_program_address, each seed element must be ≤ 32 bytes (and there’s also a separate limit on number of seeds, but your question was specifically about the 32-byte constraint) [1][4].

Citations:


MAX_NAME_LEN = 64 exceeds Solana's MAX_SEED_LEN (32 bytes per seed) → register will panic at runtime for names > 32 bytes.

The PDA seeds use name.as_bytes() directly (lines 75, 89, 103). Solana's runtime enforces a maximum of 32 bytes for each individual seed in PDA derivation, so any name between 33 and 64 bytes will pass the require! check at line 16 but fail at runtime with MaxSeedLengthExceeded. The on-chain validation and seed constraint are inconsistent.

Two clean fixes:

🛡️ Option A — clamp `MAX_NAME_LEN` to 32
-pub const MAX_NAME_LEN: usize = 64;
+pub const MAX_NAME_LEN: usize = 32;

…and update the error message accordingly:

-    #[msg("Name exceeds 64 bytes")]
+    #[msg("Name exceeds 32 bytes")]
     NameTooLong,

Plus shrink WasmEntry::SPACE (currently allocates 64 bytes for name).

🛡️ Option B — hash the name into a fixed 32-byte seed

Use anchor_lang::solana_program::hash::hash(name.as_bytes()).to_bytes() as the seed and keep name itself only inside the account data. This preserves longer human-readable names but changes the PDA scheme (and tests).

Current tests only exercise short names (e.g., "my-app"), so this bug does not surface in the test suite.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub const MAX_NAME_LEN: usize = 64;
pub const MAX_NAME_LEN: usize = 32;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@solana-wasm-registry/programs/wasm-registry/src/lib.rs` at line 7, The
constant MAX_NAME_LEN (used in WasmEntry::SPACE) allows names up to 64 bytes but
the register PDA derivation uses name.as_bytes() directly (in register), which
can exceed Solana's per-seed 32-byte limit and cause MaxSeedLengthExceeded at
runtime; fix by either (A) clamping MAX_NAME_LEN to 32 and shrinking
WasmEntry::SPACE accordingly so name fits in a single seed, or (B) change the
PDA seed in register (and any lookups) to a 32-byte hash of the name using
anchor_lang::solana_program::hash::hash(name.as_bytes()).to_bytes() while
keeping the full name in the account data and updating tests/helpers that derive
the PDA.

Comment on lines +18 to +20
```
seeds = [ "wasm", publisher_pubkey, name_bytes ]
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to the PDA seed code fence.

The fenced block should specify a language to satisfy markdownlint (MD040) and improve rendering consistency.

Suggested doc fix
-```
+```text
 seeds = [ "wasm", publisher_pubkey, name_bytes ]
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 18-18: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@solana-wasm-registry/README.md` around lines 18 - 20, The fenced code block
containing the PDA seed example (seeds = [ "wasm", publisher_pubkey, name_bytes
]) needs a language tag to satisfy markdownlint MD040; update the
triple-backtick fence to include a language (e.g., ```text or ```rust) so the
block becomes ```text followed by the seeds line and closing ``` to improve
rendering and linting.

Comment on lines +126 to +138
await program.methods
.register(name, hash)
.accounts({ entry: entryPda })
.rpc();

// Later, publish a new build under the same name
const newHash = Array.from(
createHash("sha256").update(fs.readFileSync("./my-app.v2.wasm")).digest()
);
await program.methods
.update(newHash)
.accounts({ entry: entryPda })
.rpc();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Client snippet omits the publisher account used by your tested instruction calls.

The README example currently diverges from the test invocation shape and can fail when copied as-is.

Suggested doc fix
 await program.methods
   .register(name, hash)
-  .accounts({ entry: entryPda })
+  .accounts({ entry: entryPda, publisher: provider.wallet.publicKey })
   .rpc();
@@
 await program.methods
   .update(newHash)
-  .accounts({ entry: entryPda })
+  .accounts({ entry: entryPda, publisher: provider.wallet.publicKey })
   .rpc();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await program.methods
.register(name, hash)
.accounts({ entry: entryPda })
.rpc();
// Later, publish a new build under the same name
const newHash = Array.from(
createHash("sha256").update(fs.readFileSync("./my-app.v2.wasm")).digest()
);
await program.methods
.update(newHash)
.accounts({ entry: entryPda })
.rpc();
await program.methods
.register(name, hash)
.accounts({ entry: entryPda, publisher: provider.wallet.publicKey })
.rpc();
// Later, publish a new build under the same name
const newHash = Array.from(
createHash("sha256").update(fs.readFileSync("./my-app.v2.wasm")).digest()
);
await program.methods
.update(newHash)
.accounts({ entry: entryPda, publisher: provider.wallet.publicKey })
.rpc();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@solana-wasm-registry/README.md` around lines 126 - 138, The README example
omits the publisher account used by the tested instruction calls; update the
client snippets that call program.methods.register(name, hash) and
program.methods.update(newHash) to include the publisher account in the
.accounts({ ... }) map (e.g., .accounts({ entry: entryPda, publisher:
publisherPubkey })) and ensure the publisher keypair is provided as a signer
when sending the transaction so the instruction shapes match the tests (refer to
program.methods.register, program.methods.update, entryPda, and the publisher
account).

@niklabh niklabh merged commit 1240907 into main May 11, 2026
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant