From 0dc036a3938772e642eb3d32a0e74bfa8e59bb83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=5B200=7Eegwujiohaifesinachiperpetual-max=E2=80=AF?= Date: Wed, 27 May 2026 02:13:44 +0100 Subject: [PATCH 1/3] feat: propagate logging context through contracts service --- src/app/api/attestations/route.ts | 2 +- src/app/api/commitments/[id]/history/route.ts | 2 +- src/app/api/commitments/[id]/route.ts | 2 +- src/app/api/commitments/[id]/settle/route.ts | 6 +-- src/app/api/commitments/[id]/status/route.ts | 2 +- src/app/api/commitments/route.ts | 4 +- src/lib/backend/services/contracts.ts | 47 +++++++++++++++---- 7 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/app/api/attestations/route.ts b/src/app/api/attestations/route.ts index 4fa6a484..db7c6963 100644 --- a/src/app/api/attestations/route.ts +++ b/src/app/api/attestations/route.ts @@ -127,7 +127,7 @@ export const POST = withApiHandler(async (req: NextRequest, _context, correlatio } try { - await getCommitmentFromChain(body.commitmentId); + await getCommitmentFromChain(body.commitmentId, { requestId: correlationId }); } catch (err) { const normalized = normalizeBackendError(err, { code: 'BLOCKCHAIN_CALL_FAILED', diff --git a/src/app/api/commitments/[id]/history/route.ts b/src/app/api/commitments/[id]/history/route.ts index 715654d6..8e475a85 100644 --- a/src/app/api/commitments/[id]/history/route.ts +++ b/src/app/api/commitments/[id]/history/route.ts @@ -90,7 +90,7 @@ export const GET = withApiHandler(async ( // Resolve commitment — throws NotFoundError (→ 404) if absent let commitment; try { - commitment = await getCommitmentFromChain(commitmentId); + commitment = await getCommitmentFromChain(commitmentId, { requestId: correlationId }); } catch { throw new NotFoundError('Commitment', { commitmentId }); } diff --git a/src/app/api/commitments/[id]/route.ts b/src/app/api/commitments/[id]/route.ts index 477eb612..5833d50c 100644 --- a/src/app/api/commitments/[id]/route.ts +++ b/src/app/api/commitments/[id]/route.ts @@ -29,7 +29,7 @@ export const GET = withApiHandler(async (_req: NextRequest, context, correlation let commitment: any; try { - commitment = await getCommitmentFromChain(commitmentId); + commitment = await getCommitmentFromChain(commitmentId, { requestId: correlationId }); } catch (err) { if (err instanceof BackendError && err.code === 'NOT_FOUND') { throw new NotFoundError('Commitment', { commitmentId }); diff --git a/src/app/api/commitments/[id]/settle/route.ts b/src/app/api/commitments/[id]/settle/route.ts index 83bd0939..36e6f83d 100644 --- a/src/app/api/commitments/[id]/settle/route.ts +++ b/src/app/api/commitments/[id]/settle/route.ts @@ -46,7 +46,7 @@ export const POST = withApiHandler(async (req: NextRequest, { params }, correlat } const callerAddress = validation.data.callerAddress; - const commitment: any = await getCommitmentFromChain(id); + const commitment: any = await getCommitmentFromChain(id, { requestId: correlationId }); if (!commitment) { throw new NotFoundError('Commitment', { commitmentId: id }); @@ -64,7 +64,7 @@ export const POST = withApiHandler(async (req: NextRequest, { params }, correlat const settlementResult = await settleCommitmentOnChain({ commitmentId: id, callerAddress, - }); + }, { requestId: correlationId }); logCommitmentSettled({ ip, @@ -83,7 +83,7 @@ export const POST = withApiHandler(async (req: NextRequest, { params }, correlat txHash: settlementResult.txHash, reference: settlementResult.reference, settledAt: new Date().toISOString(), - }, + }, { requestId: correlationId }, undefined, 200, correlationId, diff --git a/src/app/api/commitments/[id]/status/route.ts b/src/app/api/commitments/[id]/status/route.ts index 3791acbd..8ed97237 100644 --- a/src/app/api/commitments/[id]/status/route.ts +++ b/src/app/api/commitments/[id]/status/route.ts @@ -61,7 +61,7 @@ export const GET = withApiHandler(async ( let commitment; try { - commitment = await getCommitmentFromChain(commitmentId); + commitment = await getCommitmentFromChain(commitmentId, { requestId: correlationId }); } catch { throw new NotFoundError('Commitment', { commitmentId }); } diff --git a/src/app/api/commitments/route.ts b/src/app/api/commitments/route.ts index 978109d1..da830bac 100644 --- a/src/app/api/commitments/route.ts +++ b/src/app/api/commitments/route.ts @@ -55,7 +55,7 @@ export const GET = withApiHandler(async (req: NextRequest, _context, correlation throw new TooManyRequestsError(); } - const commitments = await getUserCommitmentsFromChain(ownerAddress); + const commitments = await getUserCommitmentsFromChain(ownerAddress, { requestId: correlationId }); let mapped = commitments.map((c: any) => ({ commitmentId: String(c.id ?? c.commitmentId), ownerAddress: c.ownerAddress, @@ -128,7 +128,7 @@ export const POST = withApiHandler(async (req: NextRequest, _context, correlatio durationDays, maxLossBps, metadata, - }); + }, { requestId: correlationId }); return ok(result, undefined, 201, correlationId); }, { cors: COMMITMENTS_CORS_POLICY }); diff --git a/src/lib/backend/services/contracts.ts b/src/lib/backend/services/contracts.ts index e85d6af1..75f0cc3a 100644 --- a/src/lib/backend/services/contracts.ts +++ b/src/lib/backend/services/contracts.ts @@ -36,6 +36,11 @@ export interface CreateCommitmentOnChainParams { metadata?: Record; } +export interface LoggingContext { + requestId?: string; + commitmentId?: string; +} + export interface ChainCommitment { id: string; ownerAddress: string; @@ -88,6 +93,19 @@ export interface SettleCommitmentOnChainResult { finalStatus: string; } +export interface EarlyExitCommitmentOnChainParams { + commitmentId: string; + callerAddress?: string; +} + +export interface EarlyExitCommitmentOnChainResult { + exitAmount: string; + penaltyAmount: string; + finalStatus: string; + txHash?: string; + reference?: string; +} + type ContractCallMode = "read" | "write"; interface ContractInvocationResult { value: unknown; @@ -487,6 +505,7 @@ function validateOwnerAddress(ownerAddress: string): void { export async function createCommitmentOnChain( params: CreateCommitmentOnChainParams, + loggingContext?: LoggingContext, ): Promise { try { validateOwnerAddress(params.ownerAddress); @@ -527,6 +546,7 @@ export async function createCommitmentOnChain( export async function getCommitmentFromChain( commitmentId: string, + loggingContext?: LoggingContext, ): Promise { try { if (!commitmentId) { @@ -540,10 +560,10 @@ export async function getCommitmentFromChain( const cacheKey = CacheKey.commitment(commitmentId); const cached = await cache.get(cacheKey); if (cached !== null) { - logInfo(undefined, "[cache] hit commitment", { commitmentId }); + logInfo(loggingContext?.requestId, "[cache] hit commitment", { commitmentId }); return cached; } - logInfo(undefined, "[cache] miss commitment", { commitmentId }); + logInfo(loggingContext?.requestId, "[cache] miss commitment", { commitmentId }); const invocation = await invokeContractMethod( getContractId("commitmentCore"), @@ -575,6 +595,7 @@ export async function getCommitmentFromChain( export async function getUserCommitmentsFromChain( ownerAddress: string, + loggingContext?: LoggingContext, ): Promise { try { validateOwnerAddress(ownerAddress); @@ -582,10 +603,10 @@ export async function getUserCommitmentsFromChain( const cacheKey = CacheKey.userCommitments(ownerAddress); const cached = await cache.get(cacheKey); if (cached !== null) { - logInfo(undefined, "[cache] hit user-commitments", { ownerAddress }); + logInfo(loggingContext?.requestId, "[cache] hit user-commitments", { ownerAddress }); return cached; } - logInfo(undefined, "[cache] miss user-commitments", { ownerAddress }); + logInfo(loggingContext?.requestId, "[cache] miss user-commitments", { ownerAddress }); const contractId = getContractId("commitmentCore"); @@ -620,7 +641,7 @@ export async function getUserCommitmentsFromChain( ? idsResult.value.map((id) => asString(id)).filter(Boolean) : []; const commitments = await Promise.all( - commitmentIds.map((commitmentId) => getCommitmentFromChain(commitmentId)), + commitmentIds.map((commitmentId) => getCommitmentFromChain(commitmentId, loggingContext)), ); await cache.set(cacheKey, commitments, CacheTTL.USER_COMMITMENTS); @@ -637,13 +658,14 @@ export async function getUserCommitmentsFromChain( code: "BLOCKCHAIN_CALL_FAILED", message: "Unable to fetch user commitments from chain.", status: 502, - details: { method: "get_user_commitments", ownerAddress }, + details: { method: "get_user_commitments", ownerAddress, requestId: loggingContext?.requestId }, }); } } export async function recordAttestationOnChain( params: RecordAttestationOnChainParams, + loggingContext?: LoggingContext, ): Promise { try { if (!params.commitmentId) { @@ -686,6 +708,10 @@ export async function recordAttestationOnChain( ); } + // Add logging context to payload if needed + const eventPayload = { ...params, requestId: loggingContext?.requestId }; + // (Potentially emit an event here) + return parseAttestationResult(invocation.value, invocation.txHash); } catch (error) { // Increment chain failures counter on blockchain operation failures @@ -706,6 +732,7 @@ export async function recordAttestationOnChain( export async function settleCommitmentOnChain( params: SettleCommitmentOnChainParams, + loggingContext?: LoggingContext, ): Promise { try { if (!params.commitmentId) { @@ -717,7 +744,7 @@ export async function settleCommitmentOnChain( } // First, get the commitment to check if it's matured - const commitment = await getCommitmentFromChain(params.commitmentId); + const commitment = await getCommitmentFromChain(params.commitmentId, loggingContext); // Check if commitment is matured (expired or can be settled) if (commitment.status === "SETTLED") { @@ -790,13 +817,15 @@ export async function settleCommitmentOnChain( details: { method: "settle_commitment", commitmentId: params.commitmentId, + requestId: loggingContext?.requestId, }, }); } } export async function earlyExitCommitmentOnChain( - params: EarlyExitCommitmentOnChainParams + params: EarlyExitCommitmentOnChainParams, + loggingContext?: LoggingContext, ): Promise { try { if (!params.commitmentId) { @@ -807,7 +836,7 @@ export async function earlyExitCommitmentOnChain( }); } - const commitment = await getCommitmentFromChain(params.commitmentId); + const commitment = await getCommitmentFromChain(params.commitmentId, loggingContext); if (commitment.status === 'SETTLED') { throw new BackendError({ From bce8e8c8f601347aa394a1290ea92fbbfb51e2d4 Mon Sep 17 00:00:00 2001 From: egwujiohaifesinachiperpetual-max Date: Wed, 27 May 2026 18:06:37 +0100 Subject: [PATCH 2/3] feat: add input validation for amount and duration --- contracts/escrow/src/lib.rs | 20 +++++++++++++++++--- contracts/escrow/src/test.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index 677f1757..070cbf20 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -18,6 +18,15 @@ use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, Address, Env, String, Symbol, Vec, }; +// Configuration constants for escrow contract +const SECONDS_PER_DAY: u64 = 86_400; +// Maximum allowed commitment amount (example limit) +const MAX_AMOUNT: i128 = 1_000_000_000_000; +// Maximum allowed duration in days +const MAX_DURATION_DAYS: u32 = 365; +// Maximum penalty basis points (100% = 10_000 bps) +const MAX_PENALTY_BPS: u32 = 10_000; + /// Storage keys for persistent contract state. #[contracttype] #[derive(Clone)] @@ -98,8 +107,7 @@ pub enum Error { PenaltyTooHigh = 9, } -const MAX_PENALTY_BPS: u32 = 10_000; -const SECONDS_PER_DAY: u64 = 86_400; + #[contract] pub struct EscrowContract; @@ -147,16 +155,22 @@ impl EscrowContract { if amount <= 0 { return Err(Error::InvalidAmount); } + if amount > MAX_AMOUNT { + return Err(Error::InvalidAmount); + } if duration_days == 0 { return Err(Error::InvalidDuration); } + if duration_days > MAX_DURATION_DAYS { + return Err(Error::InvalidDuration); + } if penalty_bps > MAX_PENALTY_BPS { return Err(Error::PenaltyTooHigh); } let id = Self::next_id(&env); let now = env.ledger().timestamp(); - let maturity = now + (duration_days as u64) * SECONDS_PER_DAY; + let maturity = now.checked_add((duration_days as u64).checked_mul(SECONDS_PER_DAY).ok_or(Error::InvalidDuration)?).ok_or(Error::InvalidDuration)?; let commitment = Commitment { id, diff --git a/contracts/escrow/src/test.rs b/contracts/escrow/src/test.rs index 11d7b7f2..f778aba2 100644 --- a/contracts/escrow/src/test.rs +++ b/contracts/escrow/src/test.rs @@ -206,4 +206,34 @@ fn owner_index_tracks_commitments() { assert_eq!(ids.len(), 2); assert_eq!(ids.get(0).unwrap(), a); assert_eq!(ids.get(1).unwrap(), b); + + #[test] + fn create_rejects_excessive_amount() { + let f = setup(); + let owner = Address::generate(&f.env); + let res = f.client.try_create_commitment( + &owner, + &f.asset, + &(MAX_AMOUNT + 1), + &RiskProfile::Safe, + &(MAX_DURATION_DAYS + 1), + &2000, + ); + assert_eq!(res, Err(Ok(Error::InvalidAmount))); + } + + #[test] + fn create_rejects_excessive_duration() { + let f = setup(); + let owner = Address::generate(&f.env); + let res = f.client.try_create_commitment( + &owner, + &f.asset, + &1_000, + &RiskProfile::Safe, + &(MAX_DURATION_DAYS + 1), + &2000, + ); + assert_eq!(res, Err(Ok(Error::InvalidDuration))); + } } From bae412b22776472df4b58f760134f5aec1f3c62a Mon Sep 17 00:00:00 2001 From: 1nonlypiece <1nonlypiece@users.noreply.github.com> Date: Thu, 28 May 2026 19:50:09 +0530 Subject: [PATCH 3/3] chore: drop workflow-file changes from merge (avoid workflow-scope push reject) --- .github/workflows/contracts.yml | 45 --------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 .github/workflows/contracts.yml diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml deleted file mode 100644 index ed8ec70d..00000000 --- a/.github/workflows/contracts.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Contracts CI - -on: - push: - branches: [ master, main ] - paths: - - 'contracts/**' - - '.github/workflows/contracts.yml' - pull_request: - branches: [ master, main ] - paths: - - 'contracts/**' - - '.github/workflows/contracts.yml' - -jobs: - test-build: - name: Rust & Soroban CI - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - targets: wasm32-unknown-unknown, wasm32v1-none - - - name: Cache Cargo dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: | - contracts - - - name: Install Stellar CLI - uses: stellar/stellar-cli@v23.0.0 - - - name: Run Cargo Tests - working-directory: contracts - run: cargo test - - - name: Build Soroban Smart Contracts - working-directory: contracts - run: stellar contract build