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
13 changes: 13 additions & 0 deletions .github/workflows/contract-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,22 @@ jobs:
- name: Build WASM (soroban)
run: soroban contract build

- name: Generate contract ABI spec
run: bash ../scripts/generate-contract-spec.sh

- name: Verify committed ABI spec is current
working-directory: .
run: git diff --exit-code -- contracts/abi/tipz_contract.spec.json

- name: Upload WASM artifact
uses: actions/upload-artifact@v4
with:
name: soroban-wasm
path: |
contracts/target/wasm32-unknown-unknown/release/*.wasm

- name: Upload ABI spec artifact
uses: actions/upload-artifact@v4
with:
name: contract-abi-spec
path: contracts/abi/tipz_contract.spec.json
54 changes: 54 additions & 0 deletions .github/workflows/contract-fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Contract Fuzz

on:
pull_request:
branches: [main]
paths:
- "contracts/**"
- ".github/workflows/contract-fuzz.yml"
schedule:
- cron: "0 3 * * *"
workflow_dispatch:

permissions:
contents: read

jobs:
entrypoint-fuzz:
name: Entrypoint fuzz
runs-on: ubuntu-latest
defaults:
run:
working-directory: contracts

steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.88.0
with:
targets: wasm32-unknown-unknown

- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
contracts/target
key: ${{ runner.os }}-cargo-fuzz-${{ hashFiles('contracts/**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-fuzz-
${{ runner.os }}-cargo-

- name: Run bounded PR fuzzing
if: github.event_name == 'pull_request'
env:
PROPTEST_CASES: "64"
run: cargo test -p tipz-contract fuzz_ -- --nocapture

- name: Run nightly fuzzing
if: github.event_name != 'pull_request'
env:
PROPTEST_CASES: "1024"
run: cargo test -p tipz-contract fuzz_ -- --nocapture
14 changes: 13 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ jobs:
with:
node-version: '20'

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.88.0
with:
targets: wasm32-unknown-unknown

- name: Install soroban CLI
run: cargo install --locked soroban-cli

- name: Generate contract ABI artifact
run: bash ./scripts/generate-contract-spec.sh

- name: Generate changelog
run: npx conventional-changelog-cli@latest -p angular -i CHANGELOG.md -s -r 0

Expand All @@ -40,4 +51,5 @@ jobs:
NOTES=$(npx conventional-changelog-cli@latest -p angular -r 1 --outfile /dev/stdout 2>/dev/null || echo "See CHANGELOG.md for details.")
gh release create "${{ github.ref_name }}" \
--title "${{ github.ref_name }}" \
--notes "$NOTES"
--notes "$NOTES" \
contracts/abi/tipz_contract.spec.json
2 changes: 1 addition & 1 deletion backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions backend/src/common/utils/prisma-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ export function isPrismaError(err: unknown): err is Prisma.PrismaClientKnownRequ

/**
* Check if a Prisma error is a unique constraint violation (P2002).
*
*
* This error code indicates that a unique constraint was violated, typically
* due to a concurrent insert race or a duplicate value being inserted.
*
*
* @param err - The error to check
* @returns true if the error is a P2002 unique constraint violation
*/
Expand All @@ -25,15 +25,15 @@ export function isUniqueConstraintViolation(err: unknown): boolean {
* Handle P2002 unique constraint violations by throwing a ConflictError (409).
* Use this for operations where duplicate entries are not allowed and should
* result in a clean 409 response to the client.
*
*
* For idempotent operations (where duplicates should return the existing record),
* catch P2002 and query for the existing record instead of throwing.
*
*
* @param err - The error to check and potentially rethrow
* @param message - Optional custom conflict message (defaults to generic message)
* @throws ConflictError if the error is a P2002 unique constraint violation
* @throws The original error if it's not a P2002
*
*
* @example
* ```typescript
* try {
Expand All @@ -57,10 +57,10 @@ export function handleUniqueConstraintViolation(
/**
* Extract the target field(s) from a P2002 unique constraint violation error.
* This is useful for providing more specific error messages to users.
*
*
* @param err - The Prisma error
* @returns Array of field names that caused the constraint violation, or undefined
*
*
* @example
* ```typescript
* catch (err) {
Expand Down
92 changes: 91 additions & 1 deletion backend/src/modules/refunds/refunds.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { Request, Response, NextFunction } from 'express';
import { refundHistoryQuerySchema, requestRefundSchema } from './refunds.schema.js';
import {
refundHistoryQuerySchema,
refundIdParamSchema,
rejectRefundSchema,
requestRefundSchema,
submitRefundResolutionSchema,
} from './refunds.schema.js';
import * as refundsService from './refunds.service.js';

/** POST /refunds/request: request a refund for a tip sent by the authenticated user. */
Expand Down Expand Up @@ -31,3 +37,87 @@ export async function getMyRefunds(
next(err);
}
}

/** GET /refunds/received: refund requests for tips received by the authenticated creator. */
export async function getReceivedRefunds(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
try {
const { limit, offset } = refundHistoryQuerySchema.parse(req.query);
const result = await refundsService.getReceivedRefunds(req.user!.id, limit, offset);
res.status(200).json({ data: result });
} catch (err) {
next(err);
}
}

/** POST /refunds/:id/approve: prepare an unsigned approve_refund transaction. */
export async function approveRefund(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
try {
const { id } = refundIdParamSchema.parse(req.params);
const result = await refundsService.prepareApproveRefund(req.user!.id, id);
res.status(200).json({ data: result });
} catch (err) {
next(err);
}
}

/** POST /refunds/:id/reject: prepare an unsigned reject_refund transaction. */
export async function rejectRefund(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
try {
const { id } = refundIdParamSchema.parse(req.params);
rejectRefundSchema.parse(req.body);
const result = await refundsService.prepareRejectRefund(req.user!.id, id);
res.status(200).json({ data: result });
} catch (err) {
next(err);
}
}

/** POST /refunds/:id/approve/submit: submit a signed approve_refund transaction. */
export async function submitApproveRefund(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
try {
const { id } = refundIdParamSchema.parse(req.params);
const { signedTxXdr } = submitRefundResolutionSchema.parse(req.body);
const result = await refundsService.submitApproveRefund(req.user!.id, id, signedTxXdr);
res.status(200).json({ data: result });
} catch (err) {
next(err);
}
}

/** POST /refunds/:id/reject/submit: submit a signed reject_refund transaction. */
export async function submitRejectRefund(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
try {
const { id } = refundIdParamSchema.parse(req.params);
const { signedTxXdr, reason } = submitRefundResolutionSchema.parse(req.body);
const rejection = rejectRefundSchema.parse({ reason });
const result = await refundsService.submitRejectRefund(
req.user!.id,
id,
signedTxXdr,
rejection.reason,
);
res.status(200).json({ data: result });
} catch (err) {
next(err);
}
}
Loading