four issues resolved: multi-currency contracts (37 tests passing), de… - #153
Conversation
…leted stale CI workflow, added React.lazy code splitting, created case study doc
|
@harystyleseze is attempting to deploy a commit to the obiajulugif's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@harystyleseze Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
|
Warning Review limit reached
More reviews will be available in 44 minutes and 12 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, 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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR implements multi-currency support for the PromptHash marketplace, optimizes frontend bundle size, documents Soroban capabilities via case study, and removes a stale CI workflow. The contract now accepts configurable payment assets, validates them, and routes purchase/lease transfers accordingly. Frontend routes are lazy-loaded with vendor chunks optimized for caching. ChangesMulti-Currency Prompt Pricing
Frontend Bundle Optimization
Documentation and CI Updates
Sequence Diagram(s)sequenceDiagram
participant Buyer
participant PromptHashContract
participant TokenClient as StellarAssetClient<br/>(prompt.asset)
participant Storage
Buyer->>PromptHashContract: buy_prompt(prompt_id, amount)
PromptHashContract->>Storage: fetch Prompt (includes asset)
PromptHashContract->>TokenClient: transfer_from buyer → creator
TokenClient-->>PromptHashContract: creator payment sent
PromptHashContract->>TokenClient: transfer_from buyer → platform_fee
TokenClient-->>PromptHashContract: fee sent (if configured)
PromptHashContract->>Storage: record access, emit event
PromptHashContract-->>Buyer: success
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsStopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a 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: 8
🤖 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 `@contracts/prompt-hash/src/contract.rs`:
- Around line 70-72: Replace the infallible token::Client::new(&env,
&pricing.asset).decimals() call with the fallible client used for transfers
(token::StellarAssetClient); call StellarAssetClient::new(&env,
&pricing.asset).try_decimals() and map any Err or None result into returning
Err(Error::InvalidAsset) instead of allowing a trap—ensure you use the same
client family (StellarAssetClient), call try_decimals(), and propagate/map
failures to Error::InvalidAsset when validating pricing.asset.
In `@contracts/prompt-hash/src/events.rs`:
- Around line 9-10: The indexer is normalizing prices with a fixed divisor and
ignoring the new asset field, so update the indexing and model code to persist
the token and use its decimals for normalization: in
server/src/services/indexer.ts, read the asset field from the PromptCreated
event (and PromptPriceUpdated when present) and compute display price by
dividing price_stroops by 10^decimals instead of 10_000_000; in
server/src/models/Prompt.js persist the asset (or store decimals) on creation so
subsequent PromptPriceUpdated handlers can look up decimals and normalize
correctly; ensure you use the contract fields asset and price_stroops and
validate/convert using the token's decimals() value when available.
In `@contracts/prompt-hash/src/test.rs`:
- Around line 1140-1256: Add a negative unit test that calls
PromptHashContractClient::try_create_prompt with a non-token contract address
(e.g., an Address::generate() value) to exercise the invalid-asset branch and
assert it returns Err(Ok(Error::InvalidAsset)); specifically, create a test
function (similar to existing tests) that builds env/context, generates a fake
asset address, calls client.try_create_prompt(..., &fake_asset_address, ...),
and asserts the result equals Err(Ok(Error::InvalidAsset)) to prevent trap-based
regressions.
In `@docs/case-study-stellar.md`:
- Around line 27-42: The fenced block containing the numbered steps lacks a
language tag; update the block around the steps (the triple-backtick section
listing steps 1–14) to either add a language identifier such as "text" (e.g.,
```text) or replace the fenced code block with a normal Markdown numbered list
so the content renders semantically and satisfies the MD040 lint rule.
- Around line 91-122: The fenced ASCII diagram block that begins with the line
"┌─────────────────┐ ┌──────────────────────────┐" lacks a language
identifier; update the opening ``` of that diagram to include a language token
such as "text" (e.g., ```text) so the block is properly rendered and satisfies
MD040. Ensure only the opening fence is changed and no diagram characters are
modified.
- Around line 71-78: The fenced code block showing the reentrancy guard sequence
lacks a language identifier; update that markdown block (the one containing
lines like "set_reentrancy_guard()", "transfer_from (creator)",
"clear_reentrancy_guard()") to include a language token (e.g., add ```text or
```plain immediately after the opening backticks) so the block renders correctly
and satisfies MD040.
- Line 63: The docs incorrectly state that `#[only_owner]` is "admin-only via
OpenZeppelin's Ownable"; update the wording to reference Soroban ownership
utilities (`stellar_access::ownable::Ownable` and the
`stellar_macros::only_owner` macro) and note that the owner is set in the
constructor with `ownable::set_owner(&env, &admin)` to avoid Solidity/EVM
confusion, and also modify the fenced reentrancy code block (around the security
model paragraph) to include a language identifier (e.g., ```rust or ```text) for
proper rendering.
In `@vite.config.ts`:
- Around line 35-45: manualChunks currently lists "`@stellar/stellar-sdk`" as a
literal entry so imports of subpaths like "`@stellar/stellar-sdk/rpc`" or
"`@stellar/stellar-sdk/contract`" won't be matched; change the "vendor-stellar"
chunking to detect module IDs that start with the package path (e.g., use
manualChunks as a function or a regex test against moduleId and return
"vendor-stellar" when moduleId startsWith or matches
/^`@stellar`\/stellar-sdk(\/|$)/) so all subpath imports (seen in
src/lib/stellar/tx.ts and src/pages/Debugger.tsx) are grouped into the
vendor-stellar chunk.
🪄 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: beeb519d-bb76-4bdb-84d0-696f7be1664a
📒 Files selected for processing (8)
.github/workflows/ci.ymlcontracts/prompt-hash/src/contract.rscontracts/prompt-hash/src/events.rscontracts/prompt-hash/src/test.rscontracts/prompt-hash/src/types.rsdocs/case-study-stellar.mdsrc/App.tsxvite.config.ts
💤 Files with no reviewable changes (1)
- .github/workflows/ci.yml
| // Validate that the asset address implements the token interface | ||
| token::Client::new(&env, &pricing.asset).decimals(); | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For soroban_sdk token clients, what is the recommended way to validate a token/SAC contract address without trapping, and does token::StellarAssetClientexposetry_decimals() (or equivalent fallible APIs)?
💡 Result:
Recommended validation approach (no trapping): don’t call the infallible metadata methods (e.g., decimals()), because they are specified to panic if the SAC/token contract “has not yet been initialized” [1]. Instead, for Soroban token/SAC clients, prefer the fallible API that returns a Result (or otherwise gate your call behind a pre-check), so you can handle “not initialized / wrong contract” as an error rather than allowing a trap. For token::StellarAssetClient: yes—StellarAssetClient exposes a fallible equivalent of decimals() named try_decimals(&self) -> Result<..., Error> [2]. The docs also explicitly say decimals() panics when the contract hasn’t been initialized [2]. Caveat/contract-shape expectation: decimals(env) -> u32 is part of the StellarAssetInterface (CAP-46-6 functions) and is defined as panicking in that interface if the contract is not initialized [1][3]. So try_decimals() is the API you want when you need “validate that this address is a suitable SAC/token without trapping.”
Citations:
- 1: https://docs.rs/soroban-sdk/latest/src/soroban_sdk/token.rs.html
- 2: https://docs.rs/soroban-sdk/latest/soroban_sdk/token/struct.StellarAssetClient.html
- 3: https://docs.rs/soroban-sdk/latest/soroban_sdk/token/trait.StellarAssetInterface.html
Fix asset validation to return Error::InvalidAsset via fallible try_decimals (no trapping)
token::Client::new(&env, &pricing.asset).decimals() can panic/trap when the SAC/token contract isn’t initialized, so Error::InvalidAsset isn’t reliably returned. Validate with the same client family used later for transfers (token::StellarAssetClient) and map try_decimals() failure to Error::InvalidAsset.
Suggested fix
- // Validate that the asset address implements the token interface
- token::Client::new(&env, &pricing.asset).decimals();
+ // Validate that the asset address is a compatible SAC and return typed error on failure
+ let sac = token::StellarAssetClient::new(&env, &pricing.asset);
+ if sac.try_decimals().is_err() {
+ return Err(Error::InvalidAsset);
+ }📝 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.
| // Validate that the asset address implements the token interface | |
| token::Client::new(&env, &pricing.asset).decimals(); | |
| // Validate that the asset address is a compatible SAC and return typed error on failure | |
| let sac = token::StellarAssetClient::new(&env, &pricing.asset); | |
| if sac.try_decimals().is_err() { | |
| return Err(Error::InvalidAsset); | |
| } |
🤖 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 `@contracts/prompt-hash/src/contract.rs` around lines 70 - 72, Replace the
infallible token::Client::new(&env, &pricing.asset).decimals() call with the
fallible client used for transfers (token::StellarAssetClient); call
StellarAssetClient::new(&env, &pricing.asset).try_decimals() and map any Err or
None result into returning Err(Error::InvalidAsset) instead of allowing a
trap—ensure you use the same client family (StellarAssetClient), call
try_decimals(), and propagate/map failures to Error::InvalidAsset when
validating pricing.asset.
| pub asset: Address, | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify downstream PromptCreated handling is multi-asset aware
fd -i 'indexer.ts'
rg -n -C3 'PromptCreated|price_stroops|asset|10_000_000|10_000_0000' server/src/services/indexer.tsRepository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 954
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect the PromptCreated event definition (asset field presence/types)
rg -n "PromptCreated" contracts/prompt-hash/src/events.rs
rg -n "asset" contracts/prompt-hash/src/events.rs
# Print the relevant sections around the matches (keep output small)
rg -n "PromptCreated" -n -C3 contracts/prompt-hash/src/events.rs
rg -n "asset" -n -C3 contracts/prompt-hash/src/events.rs
# 2) Inspect PromptCreated handling/normalization in the indexer
sed -n '1,160p' server/src/services/indexer.ts
# 3) Check whether asset/decimals is used anywhere near PromptCreated or price_stroops normalization
rg -n "price_stroops" server/src/services/indexer.ts
rg -n "asset" server/src/services/indexer.ts
rg -n "10_000_000" server/src/services/indexer.ts
# 4) Broader search: any other consumer of PromptCreated / price_stroops / asset decimals
rg -n "PromptCreated" server/src | head -n 50
rg -n "price_stroops" server/src | head -n 50
rg -n "\"PromptPriceUpdated\"" server/src | head -n 50
rg -n "10_000_000" server/src | head -n 50Repository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 4648
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "struct PromptPriceUpdated|PromptPriceUpdated" contracts/prompt-hash/src/events.rs
rg -n "PromptPriceUpdated" -n -C3 contracts/prompt-hash/src/events.rsRepository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 488
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate PromptCreated emission
rg -n "emit_prompt_created|PromptCreated \{" contracts/prompt-hash/src -S
rg -n "price_stroops|asset" contracts/prompt-hash/src -S
# Show surrounding code for the emission (limit output)
rg -n "emit_prompt_created" -n -C5 contracts/prompt-hash/src
# If emit functions exist, inspect their definitions and any helpers that compute price
rg -n "fn emit_.*prompt.*" contracts/prompt-hash/src -SRepository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 10010
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect contract.rs around pricing/emit_prompt_created and decimals validation
sed -n '1,220p' contracts/prompt-hash/src/contract.rs
# Inspect server Prompt model to see if it stores asset/decimals
fd -i "*Prompt*" server/src -a
fd -i "prompt" server/src/models -a 2>/dev/null || true
ls -R server/src/models 2>/dev/null || true
rg -n "class Prompt|interface Prompt|mongoose.*Prompt|price|asset|decimals" server/src/models -S || true
# Inspect any DB update for asset during PromptCreated indexing
rg -n "asset" server/src/services/indexer.ts
rg -n "Prompt.findOneAndUpdate\\(" server/src/services/indexer.ts -nRepository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 7772
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "10_000_000" server/src
rg -n "asset" server/src/models server/src/services/indexer.ts
fd -i "Prompt*" server/src/models -t f || trueRepository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 265
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "10_000_000" server/src
fd -i "Prompt*" server/src/models -t f || true
rg -n "class Prompt|interface Prompt|mongoose.*Prompt|asset|decimals" server/src/models server/src -S
rg -n "price_stroops" server/src -SRepository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 1251
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' server/src/models/Prompt.js
sed -n '1,120p' server/src/models/PromptVersion.ts
rg -n "asset|decimals|price_stroops|10_000_000" server/src/models/Prompt.js server/src/models/PromptVersion.ts server/src -SRepository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 2375
Fix indexer price normalization for PromptCreated/PromptPriceUpdated.
PromptCreated now emits asset, and the contract stores price_stroops in the selected token’s smallest units (it validates token decimals() on the contract). However server/src/services/indexer.ts ignores asset and still normalizes with a hard-coded / 10_000_000 for both PromptCreated and PromptPriceUpdated. Since server/src/models/Prompt.js also does not persist asset/decimals, later PromptPriceUpdated events can’t be normalized correctly for non-XLM assets. Store asset (or at least decimals) from PromptCreated and normalize display price using that token’s decimals instead of a fixed divisor.
🤖 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 `@contracts/prompt-hash/src/events.rs` around lines 9 - 10, The indexer is
normalizing prices with a fixed divisor and ignoring the new asset field, so
update the indexing and model code to persist the token and use its decimals for
normalization: in server/src/services/indexer.ts, read the asset field from the
PromptCreated event (and PromptPriceUpdated when present) and compute display
price by dividing price_stroops by 10^decimals instead of 10_000_000; in
server/src/models/Prompt.js persist the asset (or store decimals) on creation so
subsequent PromptPriceUpdated handlers can look up decimals and normalize
correctly; ensure you use the contract fields asset and price_stroops and
validate/convert using the token's decimals() value when available.
| #[test] | ||
| fn test_buy_prompt_with_non_xlm_asset() { | ||
| let env: Env = Default::default(); | ||
| let context = setup(&env); | ||
| let client = PromptHashContractClient::new(&env, &context.contract); | ||
|
|
||
| // Register a second token (e.g., USDC) | ||
| let usdc = env.register(FungibleTokenContract, (context.admin.clone(),)); | ||
| let usdc_client = token::StellarAssetClient::new(&env, &usdc); | ||
|
|
||
| let creator = Address::generate(&env); | ||
| let buyer = Address::generate(&env); | ||
| let price: i128 = 5_000_000; // 5 USDC (6 decimals) | ||
| let prompt_id = create_prompt(&env, &client, &creator, "USDC Prompt", price, &usdc); | ||
|
|
||
| // Fund buyer with USDC | ||
| usdc_client.mint(&buyer, &price); | ||
| usdc_client.approve(&buyer, &context.contract, &price, &1_000); | ||
|
|
||
| let creator_start = usdc_client.balance(&creator); | ||
| let fee_start = usdc_client.balance(&context.fee_wallet); | ||
|
|
||
| client.buy_prompt(&buyer, &prompt_id, &None::<Address>, &price, &None::<Bytes>); | ||
|
|
||
| let expected_fee = price * 500 / 10_000; | ||
| let expected_creator = price - expected_fee; | ||
|
|
||
| assert_eq!(usdc_client.balance(&creator), creator_start + expected_creator); | ||
| assert_eq!(usdc_client.balance(&context.fee_wallet), fee_start + expected_fee); | ||
| assert!(client.has_access(&buyer, &prompt_id)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_create_and_buy_different_assets() { | ||
| let env: Env = Default::default(); | ||
| let context = setup(&env); | ||
| let client = PromptHashContractClient::new(&env, &context.contract); | ||
| let xlm_client = token::StellarAssetClient::new(&env, &context.xlm); | ||
|
|
||
| // Register a second token | ||
| let usdc = env.register(FungibleTokenContract, (context.admin.clone(),)); | ||
| let usdc_client = token::StellarAssetClient::new(&env, &usdc); | ||
|
|
||
| let creator = Address::generate(&env); | ||
| let buyer = Address::generate(&env); | ||
|
|
||
| // Create one prompt priced in XLM, another in USDC | ||
| let xlm_price: i128 = 10_000; | ||
| let usdc_price: i128 = 2_000_000; | ||
| let prompt_xlm = create_prompt(&env, &client, &creator, "XLM Prompt", xlm_price, &context.xlm); | ||
| let prompt_usdc = create_prompt(&env, &client, &creator, "USDC Prompt", usdc_price, &usdc); | ||
|
|
||
| // Fund buyer with both tokens | ||
| fund_buyer(&xlm_client, &buyer, &context.contract, xlm_price); | ||
| usdc_client.mint(&buyer, &usdc_price); | ||
| usdc_client.approve(&buyer, &context.contract, &usdc_price, &1_000); | ||
|
|
||
| // Buy the XLM prompt - XLM balances should change, USDC should not | ||
| let creator_xlm_before = xlm_client.balance(&creator); | ||
| let creator_usdc_before = usdc_client.balance(&creator); | ||
|
|
||
| client.buy_prompt(&buyer, &prompt_xlm, &None::<Address>, &xlm_price, &None::<Bytes>); | ||
|
|
||
| let xlm_fee = xlm_price * 500 / 10_000; | ||
| assert_eq!(xlm_client.balance(&creator), creator_xlm_before + xlm_price - xlm_fee); | ||
| assert_eq!(usdc_client.balance(&creator), creator_usdc_before); | ||
|
|
||
| // Buy the USDC prompt - USDC balances should change | ||
| let creator_usdc_before = usdc_client.balance(&creator); | ||
| client.buy_prompt(&buyer, &prompt_usdc, &None::<Address>, &usdc_price, &None::<Bytes>); | ||
|
|
||
| let usdc_fee = usdc_price * 500 / 10_000; | ||
| assert_eq!(usdc_client.balance(&creator), creator_usdc_before + usdc_price - usdc_fee); | ||
|
|
||
| assert!(client.has_access(&buyer, &prompt_xlm)); | ||
| assert!(client.has_access(&buyer, &prompt_usdc)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_lease_prompt_with_non_xlm_asset() { | ||
| let env: Env = Default::default(); | ||
| let context = setup(&env); | ||
| let client = PromptHashContractClient::new(&env, &context.contract); | ||
|
|
||
| env.ledger().with_mut(|ledger| { | ||
| ledger.timestamp = 1_000; | ||
| }); | ||
|
|
||
| // Register a second token | ||
| let usdc = env.register(FungibleTokenContract, (context.admin.clone(),)); | ||
| let usdc_client = token::StellarAssetClient::new(&env, &usdc); | ||
|
|
||
| let creator = Address::generate(&env); | ||
| let buyer = Address::generate(&env); | ||
| let price: i128 = 10_000_000; | ||
| let prompt_id = create_prompt(&env, &client, &creator, "USDC Lease Prompt", price, &usdc); | ||
|
|
||
| // Lease price = 40% of base price | ||
| let lease_price = price * 4_000 / 10_000; | ||
| usdc_client.mint(&buyer, &lease_price); | ||
| usdc_client.approve(&buyer, &context.contract, &lease_price, &1_000); | ||
|
|
||
| let creator_start = usdc_client.balance(&creator); | ||
|
|
||
| client.lease_prompt(&buyer, &prompt_id, &600); | ||
|
|
||
| let expected_fee = lease_price * 500 / 10_000; | ||
| let expected_seller = lease_price - expected_fee; | ||
| assert_eq!(usdc_client.balance(&creator), creator_start + expected_seller); | ||
| assert!(client.has_access(&buyer, &prompt_id)); | ||
|
|
||
| // Verify lease expires | ||
| env.ledger().with_mut(|ledger| { | ||
| ledger.timestamp = 1_700; | ||
| }); | ||
| assert!(!client.has_access(&buyer, &prompt_id)); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Add a negative test for invalid asset to lock Error::InvalidAsset behavior.
Multi-asset happy paths are covered well; please add one try_create_prompt case with a non-token contract address and assert Err(Ok(Error::InvalidAsset)). This prevents regressions to trap-based failures.
🤖 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 `@contracts/prompt-hash/src/test.rs` around lines 1140 - 1256, Add a negative
unit test that calls PromptHashContractClient::try_create_prompt with a
non-token contract address (e.g., an Address::generate() value) to exercise the
invalid-asset branch and assert it returns Err(Ok(Error::InvalidAsset));
specifically, create a test function (similar to existing tests) that builds
env/context, generates a fake asset address, calls client.try_create_prompt(...,
&fake_asset_address, ...), and asserts the result equals
Err(Ok(Error::InvalidAsset)) to prevent trap-based regressions.
| ``` | ||
| 1. Validate listing (active, not duplicate, supply check) | ||
| 2. Apply voucher discount (if provided) | ||
| 3. Validate payment amount >= required price | ||
| 4. Validate referrer constraints | ||
| 5. Set reentrancy guard | ||
| 6. Calculate fee, referral, and creator splits | ||
| 7. Execute transfer_from: buyer → creator | ||
| 8. Execute transfer_from: buyer → fee_wallet | ||
| 9. Execute transfer_from: buyer → referrer (if present) | ||
| 10. Increment sales count | ||
| 11. Grant purchase access record | ||
| 12. Clear reentrancy guard | ||
| 13. Emit PromptPurchased event | ||
| 14. Emit PromptTipped event (if overpayment) | ||
| ``` |
There was a problem hiding this comment.
Add language identifier to code block.
The fenced code block should specify a language for proper rendering and linting compliance.
📝 Proposed fix
Since this is a numbered list rather than code, consider either:
Option 1 - Add a language identifier (e.g., text):
-```
+```text
1. Validate listing (active, not duplicate, supply check)Option 2 - Use markdown list formatting instead of a code block for better semantic markup.
As per coding guidelines (static analysis): Fenced code blocks should have a language specified (MD040).
📝 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.
| ``` | |
| 1. Validate listing (active, not duplicate, supply check) | |
| 2. Apply voucher discount (if provided) | |
| 3. Validate payment amount >= required price | |
| 4. Validate referrer constraints | |
| 5. Set reentrancy guard | |
| 6. Calculate fee, referral, and creator splits | |
| 7. Execute transfer_from: buyer → creator | |
| 8. Execute transfer_from: buyer → fee_wallet | |
| 9. Execute transfer_from: buyer → referrer (if present) | |
| 10. Increment sales count | |
| 11. Grant purchase access record | |
| 12. Clear reentrancy guard | |
| 13. Emit PromptPurchased event | |
| 14. Emit PromptTipped event (if overpayment) | |
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 27-27: 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 `@docs/case-study-stellar.md` around lines 27 - 42, The fenced block containing
the numbered steps lacks a language tag; update the block around the steps (the
triple-backtick section listing steps 1–14) to either add a language identifier
such as "text" (e.g., ```text) or replace the fenced code block with a normal
Markdown numbered list so the content renders semantically and satisfies the
MD040 lint rule.
|
|
||
| - `create_prompt`: `creator.require_auth()` | ||
| - `buy_prompt`: `buyer.require_auth()` | ||
| - `set_fee_percentage`: `#[only_owner]` macro (admin-only via OpenZeppelin's Ownable) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check for ownership implementation in the contract
# Search for ownership-related macros or implementations
rg -n "only_owner|Ownable|OpenZeppelin" contracts/prompt-hash/
# Search for admin/owner authorization patterns
rg -nC3 "set_fee_percentage|set_pause_status" contracts/prompt-hash/src/contract.rsRepository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 2013
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the relevant section of the docs with line numbers
sed -n '50,90p' docs/case-study-stellar.md | nl -ba -w2 -s': 'Repository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 119
🏁 Script executed:
sed -n '50,90p' docs/case-study-stellar.md | nl -ba -w2 -s': 'Repository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 119
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '55,88p' docs/case-study-stellar.md | nl -ba -w2 -s': 'Repository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 119
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="docs/case-study-stellar.md"
echo "== Matching docs references =="
rg -n "set_fee_percentage|only_owner|OpenZeppelin|Ownable" "$file" || true
echo
echo "== Docs lines 55-88 with line numbers (no nl) =="
sed -n '55,88p' "$file" | awk '{printf "%4d: %s\n", NR+54, $0}'Repository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 2065
Clarify ownership source and add a language to the reentrancy code fence in the security model
docs/case-study-stellar.mdsays#[only_owner]is “admin-only via OpenZeppelin's Ownable” at line 63, but the contract uses Soroban ownership utilities (stellar_access::ownable::Ownable+stellar_macros::only_owner), with the owner set in the constructor viaownable::set_owner(&env, &admin)—adjust wording to avoid confusion with Solidity/EVMOwnable.- The fenced code block around line 71 starts with ``` without a language identifier; add one (e.g.,
text/`rust`) for consistency and better rendering.
🤖 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 `@docs/case-study-stellar.md` at line 63, The docs incorrectly state that
`#[only_owner]` is "admin-only via OpenZeppelin's Ownable"; update the wording
to reference Soroban ownership utilities (`stellar_access::ownable::Ownable` and
the `stellar_macros::only_owner` macro) and note that the owner is set in the
constructor with `ownable::set_owner(&env, &admin)` to avoid Solidity/EVM
confusion, and also modify the fenced reentrancy code block (around the security
model paragraph) to include a language identifier (e.g., ```rust or ```text) for
proper rendering.
| ``` | ||
| set_reentrancy_guard() ← blocks re-entry | ||
| ├─ transfer_from (creator) | ||
| ├─ transfer_from (fee_wallet) | ||
| ├─ transfer_from (referrer) | ||
| ├─ update prompt state | ||
| └─ grant purchase record | ||
| clear_reentrancy_guard() ← re-enables entry |
There was a problem hiding this comment.
Add language identifier to code block.
The reentrancy guard illustration should specify a language for proper rendering.
📝 Proposed fix
-```
+```text
set_reentrancy_guard() ← blocks re-entryAs per coding guidelines (static analysis): Fenced code blocks should have a language specified (MD040).
📝 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.
| ``` | |
| set_reentrancy_guard() ← blocks re-entry | |
| ├─ transfer_from (creator) | |
| ├─ transfer_from (fee_wallet) | |
| ├─ transfer_from (referrer) | |
| ├─ update prompt state | |
| └─ grant purchase record | |
| clear_reentrancy_guard() ← re-enables entry |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 71-71: 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 `@docs/case-study-stellar.md` around lines 71 - 78, The fenced code block
showing the reentrancy guard sequence lacks a language identifier; update that
markdown block (the one containing lines like "set_reentrancy_guard()",
"transfer_from (creator)", "clear_reentrancy_guard()") to include a language
token (e.g., add ```text or ```plain immediately after the opening backticks) so
the block renders correctly and satisfies MD040.
| ``` | ||
| ┌─────────────────┐ ┌──────────────────────────┐ | ||
| │ Creator Browser │ │ Buyer Browser │ | ||
| │ │ │ │ | ||
| │ encrypt prompt │ │ select prompt + pay │ | ||
| │ submit listing │ │ unlock after purchase │ | ||
| └────────┬─────────┘ └────────────┬─────────────┘ | ||
| │ │ | ||
| │ create_prompt(pricing) │ buy_prompt(payment) | ||
| │ │ | ||
| ▼ ▼ | ||
| ┌──────────────────────────────────────────────────────┐ | ||
| │ Soroban Contract │ | ||
| │ │ | ||
| │ ┌──────────┐ ┌──────────┐ ┌────────────────────┐ │ | ||
| │ │ Listings │ │ Purchases│ │ Fee/Referral Config │ │ | ||
| │ │ (Prompt) │ │ (Access) │ │ │ │ | ||
| │ └──────────┘ └──────────┘ └────────────────────┘ │ | ||
| │ │ | ||
| │ transfer_from ──► SAC (XLM, USDC, ARS, etc.) │ | ||
| └──────────────────────────────────────────────────────┘ | ||
| │ | ||
| │ has_access query | ||
| ▼ | ||
| ┌──────────────────┐ | ||
| │ Unlock Service │ | ||
| │ │ | ||
| │ verify access │ | ||
| │ decrypt prompt │ | ||
| │ return content │ | ||
| └──────────────────┘ | ||
| ``` |
There was a problem hiding this comment.
Add language identifier to code block.
The ASCII architecture diagram should specify a language for proper rendering.
📝 Proposed fix
-```
+```text
┌─────────────────┐ ┌──────────────────────────┐As per coding guidelines (static analysis): Fenced code blocks should have a language specified (MD040).
📝 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.
| ``` | |
| ┌─────────────────┐ ┌──────────────────────────┐ | |
| │ Creator Browser │ │ Buyer Browser │ | |
| │ │ │ │ | |
| │ encrypt prompt │ │ select prompt + pay │ | |
| │ submit listing │ │ unlock after purchase │ | |
| └────────┬─────────┘ └────────────┬─────────────┘ | |
| │ │ | |
| │ create_prompt(pricing) │ buy_prompt(payment) | |
| │ │ | |
| ▼ ▼ | |
| ┌──────────────────────────────────────────────────────┐ | |
| │ Soroban Contract │ | |
| │ │ | |
| │ ┌──────────┐ ┌──────────┐ ┌────────────────────┐ │ | |
| │ │ Listings │ │ Purchases│ │ Fee/Referral Config │ │ | |
| │ │ (Prompt) │ │ (Access) │ │ │ │ | |
| │ └──────────┘ └──────────┘ └────────────────────┘ │ | |
| │ │ | |
| │ transfer_from ──► SAC (XLM, USDC, ARS, etc.) │ | |
| └──────────────────────────────────────────────────────┘ | |
| │ | |
| │ has_access query | |
| ▼ | |
| ┌──────────────────┐ | |
| │ Unlock Service │ | |
| │ │ | |
| │ verify access │ | |
| │ decrypt prompt │ | |
| │ return content │ | |
| └──────────────────┘ | |
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 91-91: 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 `@docs/case-study-stellar.md` around lines 91 - 122, The fenced ASCII diagram
block that begins with the line "┌─────────────────┐
┌──────────────────────────┐" lacks a language identifier; update the opening
``` of that diagram to include a language token such as "text" (e.g., ```text)
so the block is properly rendered and satisfies MD040. Ensure only the opening
fence is changed and no diagram characters are modified.
| manualChunks: { | ||
| "vendor-stellar": [ | ||
| "@stellar/stellar-sdk", | ||
| "@stellar/stellar-base", | ||
| "@stellar/design-system", | ||
| "@creit.tech/stellar-wallets-kit", | ||
| ], | ||
| "vendor-charts": ["chart.js", "react-chartjs-2"], | ||
| "vendor-motion": ["framer-motion"], | ||
| "vendor-crypto": ["libsodium-wrappers"], | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether manual-chunked packages are imported via subpaths.
# Expected: If subpath imports exist, prefer function-based manualChunks.
rg -nP --type=ts --type=tsx --type=js --type=jsx -C2 \
'from\s+["'\''](`@stellar/stellar-sdk/`[^"'\'']+|`@stellar/stellar-base/`[^"'\'']+|chart\.js/[^"'\'']+|framer-motion/[^"'\'']+|libsodium-wrappers/[^"'\'']+)["'\'']'Repository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 104
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show relevant part of vite.config.ts
if [ -f "vite.config.ts" ]; then
echo "=== vite.config.ts (manualChunks area) ==="
nl -ba vite.config.ts | sed -n '1,120p'
echo
fi
# Find subpath imports/usages for the packages in question
echo "=== occurrences of `@stellar/stellar-sdk/` (subpaths) ==="
rg -n --hidden --no-ignore -S "`@stellar/stellar-sdk/`" . || true
echo
echo "=== occurrences of `@stellar/stellar-base/` (subpaths) ==="
rg -n --hidden --no-ignore -S "`@stellar/stellar-base/`" . || true
echo
echo "=== occurrences of chart.js/ (subpaths) ==="
rg -n --hidden --no-ignore -S "chart.js/" . || true
echo
echo "=== occurrences of framer-motion/ (subpaths) ==="
rg -n --hidden --no-ignore -S "framer-motion/" . || true
echo
echo "=== occurrences of libsodium-wrappers/ (subpaths) ==="
rg -n --hidden --no-ignore -S "libsodium-wrappers/" . || trueRepository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 180
🌐 Web query:
Rollup output.manualChunks object array of ids matching subpath module ids (e.g., manualChunks: {vendor: ['@stellar/stellar-sdk']} match '@stellar/stellar-sdk/rpc')
💡 Result:
In Rollup, the output.manualChunks option accepts either an object or a function to define custom shared chunks [1]. When you need to match specific subpath imports—such as @stellar/stellar-sdk/rpc—the function form is the required approach, as the object form only accepts exact module names or package identifiers to group those modules and their dependencies [1]. To target subpaths, you should implement a function that inspects the module ID passed to it. Example implementation: // rollup.config.js export default { //... output: { manualChunks(id) { // Check if the module ID contains the subpath if (id.includes('@stellar/stellar-sdk/rpc')) { return 'stellar-rpc'; // Name of the resulting chunk } // Fallback: handle other dependencies if (id.includes('node_modules')) { return 'vendor'; } } } }; Key considerations: 1. Object Form Limitation: The object form, such as manualChunks: { vendor: ['@stellar/stellar-sdk'] }, maps specific listed modules to a chunk [1]. It does not inherently provide pattern matching for subpath modules (e.g., it will not automatically include @stellar/stellar-sdk/rpc just because the parent package is listed) [1]. 2. Function Form Flexibility: The function form receives the resolved module ID as the first argument [1]. This allows you to use string methods like .includes(), .endsWith(), or regular expressions to match specific subpaths dynamically [1][2]. 3. Dependency Handling: When you return a string from the function, Rollup assigns that label to the module and all its dependencies, provided those dependencies have not already been assigned to a different manual chunk [1][3]. 4. Advanced Control: The function receives a second argument containing utility functions like getModuleInfo, which can be used for more complex logic if you need to inspect the dependency graph to determine if a module should be grouped [1][2].
Citations:
- 1: https://rollupjs.org/configuration-options/
- 2: Extend manualChunks API rollup/rollup#3542
- 3: Manual chunks function rollup/rollup#2831
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show relevant part of vite.config.ts
if [ -f "vite.config.ts" ]; then
echo "=== vite.config.ts (manualChunks area) ==="
nl -ba vite.config.ts | sed -n '1,120p'
echo
fi
# Find subpath imports/usages for the packages in question
echo "=== occurrences of `@stellar/stellar-sdk/` (subpaths) ==="
rg -n --hidden --no-ignore -S "`@stellar/stellar-sdk/`" . || true
echo
echo "=== occurrences of `@stellar/stellar-base/` (subpaths) ==="
rg -n --hidden --no-ignore -S "`@stellar/stellar-base/`" . || true
echo
echo "=== occurrences of chart.js/ (subpaths) ==="
rg -n --hidden --no-ignore -S "chart.js/" . || true
echo
echo "=== occurrences of framer-motion/ (subpaths) ==="
rg -n --hidden --no-ignore -S "framer-motion/" . || true
echo
echo "=== occurrences of libsodium-wrappers/ (subpaths) ==="
rg -n --hidden --no-ignore -S "libsodium-wrappers/" . || trueRepository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 180
🌐 Web query:
Rollup output.manualChunks object array of ids matching subpath module ids (e.g., manualChunks: {vendor: ['@stellar/stellar-sdk']} match '@stellar/stellar-sdk/rpc')
💡 Result:
To match subpath module IDs in Rollup's manualChunks, you must use the function form of the manualChunks configuration option [1][2]. The object form only allows static, exact mapping of modules to chunks, and does not support pattern matching or subpath logic [1]. When you use the function form, Rollup passes the module ID of every resolved file to your function [1]. You can then perform string matching or regex operations on that ID to return the desired chunk name [1][2]. Implementation approach: In your rollup.config.js (or vite.config.js): export default { //... output: { manualChunks(id) { // Check if the module belongs to the specific package or path if (id.includes('@stellar/stellar-sdk')) { return 'stellar-vendor'; // All files matching this path go into this chunk } // Default behavior for other modules if (id.includes('node_modules')) { return 'vendor'; } } } }; Key considerations: 1. Object Form Limitation: The object form, e.g., manualChunks: { vendor: ['@stellar/stellar-sdk'] }, only bundles the specific modules listed [1]. It does not automatically capture subpaths (e.g., @stellar/stellar-sdk/rpc) unless they are explicitly included in the array or handled by a plugin that resolves them [1]. 2. Function Form Flexibility: The function form is the authoritative way to handle complex grouping [1]. Because it receives the resolved ID, you can easily target specific subpaths by checking if the string starts with or includes your target path [2]. 3. Dependency Inclusion: When you return a chunk name for a module ID, Rollup automatically includes that module and all its dependencies in the specified chunk, unless those dependencies are already assigned to a different manual chunk [3][1][4]. 4. Evaluation Order: The function is evaluated for each module [1][4]. You can implement logic to prioritize certain chunks over others if a module belongs to multiple potential groups [3][5][4].
Citations:
- 1: https://rollupjs.org/configuration-options/
- 2: https://soledadpenades.com/posts/2025/use-manual-chunks-with-vite-to-facilitate-dependency-caching/
- 3: Manual chunks support rollup/rollup#2084
- 4: Manual chunks function rollup/rollup#2831
- 5: Allow more programmatic control over manual chunks rollup/rollup#2688
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect vite.config.ts manualChunks
echo "=== vite.config.ts: manualChunks definition ==="
if [ -f "vite.config.ts" ]; then
rg -n "manualChunks" vite.config.ts || true
echo
echo "=== vite.config.ts: lines 1-160 (manualChunks context) ==="
# Avoid nl (not available). Use cat -n.
cat -n vite.config.ts | sed -n '1,160p'
else
echo "vite.config.ts not found"
fi
# 2) Search for subpath imports in the repo
patterns=(
'`@stellar/stellar-sdk/`'
'`@stellar/stellar-sdk/rpc`'
'`@stellar/stellar-base/`'
'chart.js/'
'framer-motion/'
'libsodium-wrappers/'
)
for p in "${patterns[@]}"; do
echo
echo "=== occurrences of pattern: $p ==="
rg -n --hidden --no-ignore -S "$p" . || true
doneRepository: Obiajulu-gif/Prompt-Hash-Stellar
Length of output: 4547
Cover @stellar/stellar-sdk subpath imports in vendor-stellar chunking
vite.config.ts currently uses the object form manualChunks: { "vendor-stellar": ["@stellar/stellar-sdk", ...] }, but the app imports subpaths like @stellar/stellar-sdk/rpc and @stellar/stellar-sdk/contract (e.g., src/lib/stellar/tx.ts, src/pages/Debugger.tsx). Those subpath module IDs won’t reliably be grouped into vendor-stellar, fragmenting cache behavior.
Suggested change
- rollupOptions: {
- output: {
- manualChunks: {
- "vendor-stellar": [
- "`@stellar/stellar-sdk`",
- "`@stellar/stellar-base`",
- "`@stellar/design-system`",
- "`@creit.tech/stellar-wallets-kit`",
- ],
- "vendor-charts": ["chart.js", "react-chartjs-2"],
- "vendor-motion": ["framer-motion"],
- "vendor-crypto": ["libsodium-wrappers"],
- },
- },
- },
+ rollupOptions: {
+ output: {
+ manualChunks(id) {
+ if (
+ id.includes("`@stellar/stellar-sdk`") ||
+ id.includes("`@stellar/stellar-base`") ||
+ id.includes("`@stellar/design-system`") ||
+ id.includes("`@creit.tech/stellar-wallets-kit`")
+ ) return "vendor-stellar";
+ if (id.includes("chart.js") || id.includes("react-chartjs-2")) return "vendor-charts";
+ if (id.includes("framer-motion")) return "vendor-motion";
+ if (id.includes("libsodium-wrappers")) return "vendor-crypto";
+ },
+ },
+ },📝 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.
| manualChunks: { | |
| "vendor-stellar": [ | |
| "@stellar/stellar-sdk", | |
| "@stellar/stellar-base", | |
| "@stellar/design-system", | |
| "@creit.tech/stellar-wallets-kit", | |
| ], | |
| "vendor-charts": ["chart.js", "react-chartjs-2"], | |
| "vendor-motion": ["framer-motion"], | |
| "vendor-crypto": ["libsodium-wrappers"], | |
| }, | |
| rollupOptions: { | |
| output: { | |
| manualChunks(id) { | |
| if ( | |
| id.includes("`@stellar/stellar-sdk`") || | |
| id.includes("`@stellar/stellar-base`") || | |
| id.includes("`@stellar/design-system`") || | |
| id.includes("`@creit.tech/stellar-wallets-kit`") | |
| ) return "vendor-stellar"; | |
| if (id.includes("chart.js") || id.includes("react-chartjs-2")) return "vendor-charts"; | |
| if (id.includes("framer-motion")) return "vendor-motion"; | |
| if (id.includes("libsodium-wrappers")) return "vendor-crypto"; | |
| }, | |
| }, | |
| }, |
🤖 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 `@vite.config.ts` around lines 35 - 45, manualChunks currently lists
"`@stellar/stellar-sdk`" as a literal entry so imports of subpaths like
"`@stellar/stellar-sdk/rpc`" or "`@stellar/stellar-sdk/contract`" won't be matched;
change the "vendor-stellar" chunking to detect module IDs that start with the
package path (e.g., use manualChunks as a function or a regex test against
moduleId and return "vendor-stellar" when moduleId startsWith or matches
/^`@stellar`\/stellar-sdk(\/|$)/) so all subpath imports (seen in
src/lib/stellar/tx.ts and src/pages/Debugger.tsx) are grouped into the
vendor-stellar chunk.
Summary
via the SAC standard, not just XLM
ci.ymlworkflow that duplicated the split frontend/backend/contracts workflowssize
Closes #47
Closes #143
Closes #144
Closes #137
Changes
Contracts (Issue #47)
asset: Addressfield toPromptstruct for per-listing currencyPricingConfigstruct (price + asset) to stay within Soroban's 10-parameter limitcreate_promptto accept aPricingConfigand validate the asset contract viatoken::Clientbuy_promptandlease_promptto use the prompt's asset instead of the global XLM addressInvalidAsseterror variantPromptCreatedevent to include asset addressCI (Issue #143)
.github/workflows/ci.yml(legacy Node 18 + npm workflow, fully replaced by frontend.yml, backend.yml,contracts.yml)
Frontend (Issue #144)
Docs (Issue #137)
multi-currency design notes
Test Plan
cargo test -p prompt-hash— all contract tests pass including new multi-asset testsyarn build— produces split chunks, no single file > 500 kB warningSummary by CodeRabbit
New Features
Documentation
Performance