Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 0 additions & 75 deletions .github/workflows/ci.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/hygiene.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@v4
- name: Search for conflict markers
run: |
if grep -rE "<<<<<<<|=======|>>>>>>>" . --exclude-dir=.git --exclude=hygiene.yml; then
if grep -rnE "^<<<<<<<|^=======|^>>>>>>>" . --exclude-dir=.git --exclude=hygiene.yml; then
echo "Merge conflict markers found in the codebase. Please resolve them."
exit 1
else
Expand Down
32 changes: 18 additions & 14 deletions contracts/prompt-hash/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::events::Events;
use super::storage::Storage;
use super::types::{DataKey, Error, Prompt, PromptHashTrait};
use soroban_sdk::{contract, contractimpl, Address, Bytes, BytesN, Env, String, Vec};
use super::types::{DataKey, Error, PricingConfig, Prompt, PromptHashTrait};
use soroban_sdk::{contract, contractimpl, token, Address, Bytes, BytesN, Env, String, Vec};
use stellar_access::ownable::{self as ownable, Ownable};
use stellar_macros::{default_impl, only_owner};

Expand Down Expand Up @@ -52,7 +52,7 @@ impl PromptHashTrait for PromptHashContract {
encryption_iv: String,
wrapped_key: String,
content_hash: BytesN<32>,
price_stroops: i128,
pricing: PricingConfig,
) -> Result<u128, Error> {
creator.require_auth();
ensure(!Storage::is_paused(&env), Error::ContractIsPaused)?;
Expand All @@ -64,9 +64,12 @@ impl PromptHashTrait for PromptHashContract {
&encrypted_prompt,
&encryption_iv,
&wrapped_key,
price_stroops,
pricing.price,
)?;

// Validate that the asset address implements the token interface
token::Client::new(&env, &pricing.asset).decimals();

Comment on lines +70 to +72

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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:


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.

Suggested change
// 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.

let prompt_id = Storage::get_prompt_counter(&env);
let prompt = Prompt {
id: prompt_id,
Expand All @@ -79,15 +82,16 @@ impl PromptHashTrait for PromptHashContract {
encryption_iv,
wrapped_key,
content_hash,
price_stroops,
price_stroops: pricing.price,
asset: pricing.asset.clone(),
active: true,
sales_count: 0,
max_supply: 0, // default unlimited; use set_prompt_max_supply to restrict
max_supply: 0,
};

Storage::save_prompt(&env, &prompt)?;
Storage::add_prompt_to_creator(&env, &creator, prompt_id);
Events::emit_prompt_created(&env, prompt_id, creator, price_stroops);
Events::emit_prompt_created(&env, prompt_id, creator, pricing.price, pricing.asset);
Ok(prompt_id)
}

Expand Down Expand Up @@ -231,17 +235,17 @@ impl PromptHashTrait for PromptHashContract {
.checked_sub(deductions)
.ok_or(Error::ArithmeticOverflow)?;

let xlm = Storage::get_stellar_asset_contract(&env)?;
let asset_client = token::StellarAssetClient::new(&env, &prompt.asset);

xlm.transfer_from(&this_contract, &buyer, &prompt.creator, &creator_amount);
asset_client.transfer_from(&this_contract, &buyer, &prompt.creator, &creator_amount);

if fee_amount > 0 {
xlm.transfer_from(&this_contract, &buyer, &fee_wallet, &fee_amount);
asset_client.transfer_from(&this_contract, &buyer, &fee_wallet, &fee_amount);
}

if let Some(ref r) = referrer {
if referral_amount > 0 {
xlm.transfer_from(&this_contract, &buyer, r, &referral_amount);
asset_client.transfer_from(&this_contract, &buyer, r, &referral_amount);
}
}

Expand Down Expand Up @@ -315,10 +319,10 @@ impl PromptHashTrait for PromptHashContract {
.checked_sub(fee_amount)
.ok_or(Error::ArithmeticOverflow)?;

let xlm = Storage::get_stellar_asset_contract(&env)?;
xlm.transfer_from(&this_contract, &buyer, &prompt.creator, &seller_amount);
let asset_client = token::StellarAssetClient::new(&env, &prompt.asset);
asset_client.transfer_from(&this_contract, &buyer, &prompt.creator, &seller_amount);
if fee_amount > 0 {
xlm.transfer_from(&this_contract, &buyer, &fee_wallet, &fee_amount);
asset_client.transfer_from(&this_contract, &buyer, &fee_wallet, &fee_amount);
}

prompt.sales_count = prompt
Expand Down
10 changes: 9 additions & 1 deletion contracts/prompt-hash/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct PromptCreated {
pub prompt_id: u128,
pub creator: Address,
pub price_stroops: i128,
pub asset: Address,
}
Comment on lines +9 to 10

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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.ts

Repository: 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 50

Repository: 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.rs

Repository: 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 -S

Repository: 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 -n

Repository: 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 || true

Repository: 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 -S

Repository: 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 -S

Repository: 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.


#[contractevent]
Expand Down Expand Up @@ -75,11 +76,18 @@ struct FeeWalletUpdated {
pub struct Events;

impl Events {
pub fn emit_prompt_created(env: &Env, prompt_id: u128, creator: Address, price_stroops: i128) {
pub fn emit_prompt_created(
env: &Env,
prompt_id: u128,
creator: Address,
price_stroops: i128,
asset: Address,
) {
PromptCreated {
prompt_id,
creator,
price_stroops,
asset,
}
.publish(env);
}
Expand Down
Loading
Loading