Solana WASM registry#41
Conversation
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughPR 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. ChangesMarkdown Editor Example
Multi-line TextArea Widget Support
Solana WASM Registry On-Chain Program
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (10)
solana-wasm-registry/programs/wasm-registry/src/lib.rs (4)
14-36: 💤 Low valueRe-registration after revoke resets
versionto 0 — document this in the README.After
revokecloses the PDA, the same(publisher, name)pair can callregisteragain, getting a brand-new account withversion = 0and a freshcreated_at. Downstream consumers (e.g., a browser pulling aname@versionreference) 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 valuePlaceholder
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.programIdfrom the IDL against this baked-in value if you forget to runanchor keys syncafter 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 valueConsider
#[derive(InitSpace)]with#[max_len(...)]instead of manual SPACE accounting.Anchor's
InitSpacederive 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
revokeemits the event before the account is closed — confirm event field copying is intentional.
nameis cloned out before close, so the event payload survives — that's correct. Just noting that any future refactor that moves the clone afterOk(())(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_handlesis never pruned when textareas disappear.A
ScrollHandleis inserted per textarea id on first render, but the map is only cleared ondrain_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 latestWidgetCommand::TextAreaset.🤖 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 valueTab 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 valueNewline-accounting in
textarea_hit_indexmay skip the last line's\n.The post-line advance only skips
\nwheniter.peek().is_some(). Iftextends with a trailing newline, on the final iterationpeek()isNone, sobyte_offis not incremented past the trailing\n— but then we fall through totext.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 seebyte_offshort 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 valueCaret 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_boundaryonly snaps to a valid char boundary, it does not correct the visual column. Consider tracking columns bycharcount (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_elseis reached withs[caret..].find('\n') == None; the closure value is correct but the path is more naturallyunwrap_or.
s[caret..].find('\n')returns indices in0..s.len()-caret. When no newline exists, you wantcaret + (s.len() - caret) = s.len(). Thesaturating_subis unnecessary becausecaret <= 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 valueMulti-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
📒 Files selected for processing (17)
Cargo.tomlexamples/index/src/lib.rsexamples/markdown-editor/Cargo.tomlexamples/markdown-editor/src/lib.rsoxide-browser/src/capabilities.rsoxide-browser/src/ui.rsoxide-docs/src/lib.rsoxide-sdk/src/lib.rssolana-wasm-registry/.gitignoresolana-wasm-registry/Anchor.tomlsolana-wasm-registry/Cargo.tomlsolana-wasm-registry/README.mdsolana-wasm-registry/package.jsonsolana-wasm-registry/programs/wasm-registry/Cargo.tomlsolana-wasm-registry/programs/wasm-registry/src/lib.rssolana-wasm-registry/tests/wasm-registry.tssolana-wasm-registry/tsconfig.json
| 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(¶); | ||
| 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; | ||
| } |
There was a problem hiding this comment.
🛠️ 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; |
There was a problem hiding this comment.
🧩 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 -120Repository: 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:
- 1: https://solana.com/docs/core/pda/pda-derivation
- 2: https://docs.rs/solana-sdk/latest/solana_sdk/pubkey/constant.MAX_SEED_LEN.html
- 3: https://github.com/solana-labs/solana/blob/27eff8408b7223bb3c4ab70523f8a8dca3ca6645/sdk/program/src/pubkey.rs
- 4: https://solana.com/docs/core/pda
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.
| 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.
| ``` | ||
| seeds = [ "wasm", publisher_pubkey, name_bytes ] | ||
| ``` |
There was a problem hiding this comment.
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.
| 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(); |
There was a problem hiding this comment.
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.
| 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).
Summary by CodeRabbit
Release Notes
New Features
Documentation