Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a4b08f0
feat Time-Locked Dispute Resolution Defaulting
Vibeofkd Mar 30, 2026
2faaf53
feat(onchain): harden contract role rotation
Apr 27, 2026
5dff364
Closes #187: Mobile responsive audits and fixes
martoniel Apr 28, 2026
589aa69
feat Add dispute_deadline to the Escrow struct.
Vibeofkd Apr 28, 2026
9d21e0e
fix(contract): deadline/expiry spec parity + pause-mode behavior (#213)
legend-esc Apr 28, 2026
8eabc48
Merge pull request #243 from ExcelDsigN-tech/feat/rotate-contract-roles
Cedarich Apr 28, 2026
566acc1
Merge pull request #244 from martoniel/fix/mobile-responsive-layout
Cedarich Apr 28, 2026
b9c88e6
fix(onchain): apply rustfmt formatting to test.rs
legend-esc Apr 29, 2026
ade5da3
test(e2e): implement comprehensive Playwright test suite for auth, es…
mijinummi Apr 29, 2026
7272d6d
Changes made
Malcolm-Wander Apr 29, 2026
891e2b2
Merge pull request #251 from Malcolm-Wander/main
Cedarich Apr 29, 2026
1852be6
docs: add contributor onboarding guide for Stellar/Soroban local setup
JojoFlex1 Apr 29, 2026
114a870
feat(escrow): lifecycle event logging fix
mijinummi Apr 29, 2026
30be4b5
fix: add explicit '_ lifetime to resolve mismatched-lifetime-syntaxes…
legend-esc Apr 30, 2026
dd0a9f4
Merge pull request #252 from JojoFlex1/docs/contributor-onboarding-st…
Cedarich Apr 30, 2026
d10c536
style: apply rustfmt to setup_funded_escrow_for_refund return type
legend-esc Apr 30, 2026
a561169
Merge pull request #246 from legend-esc/fix/refund-expired-pause-and-…
Cedarich Apr 30, 2026
e8c1194
feat/escrow-lifecycle-fix
mijinummi May 1, 2026
32200ab
Merge pull request #253 from mijinummi/feat/escrow-lifecycle-fix
Cedarich May 1, 2026
683abb3
feat: implement backend test infrastructure
ohamamarachi474-del May 26, 2026
8e50f60
fix
ohamamarachi474-del May 26, 2026
6cdc6c5
Merge pull request #288 from ohamamarachi474-del/Tests
KuchiMercy May 26, 2026
d66281d
feat: implement escrow-stellar integration services, event tracking, …
Superray23 May 26, 2026
1ecdb35
Merge pull request #289 from Superray23/Multi-Asset-Support--Custom-S…
Cedarich May 26, 2026
810b29f
Component Directory Consolidation
MercyDuru May 27, 2026
a12effe
Merge pull request #290 from ObedChibunna/issue-#257
KuchiMercy May 27, 2026
ee726ae
Dispute Evidence Upload with IPFS Integration
ObedChibunna May 27, 2026
6c1943a
Merge pull request #291 from ObedChibunna/issue-#265
KuchiMercy May 27, 2026
b834343
API Key Management UI: Create, List, Revoke Keys
ObedChibunna May 27, 2026
27bb45e
Merge pull request #292 from ObedChibunna/issue-#267
KuchiMercy May 27, 2026
d612c83
Transaction History Page with CSV Export
ObedChibunna May 27, 2026
413888a
Merge pull request #293 from ObedChibunna/issue-#269
KuchiMercy May 27, 2026
dddc87a
feat Time-Locked Dispute Resolution Defaulting
Vibeofkd May 28, 2026
94a130f
feat Time-Locked Dispute Resolution Defaulting
Vibeofkd May 29, 2026
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
899 changes: 461 additions & 438 deletions README.md

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<<<<<<< HEAD
# Vaultix - Implementation Tracker

- [x] Add `disputeDeadline` field to backend `Escrow` entity (nullable datetime)
- [ ] Update dispute filing flow (`fileDispute` / `raise_dispute`) to set `escrow.disputeDeadline = now + X days`
- [x] Implement `trigger_default_resolution` in backend: apply fallback when deadline passes and dispute not resolved






- [ ] Add scheduler job (cron) to periodically call `trigger_default_resolution` for expired disputes
- [ ] Update/extend tests (if any exist) for dispute deadline behavior
- [ ] Run backend typecheck/lint/test

=======
# Dispute Deadline Implementation TODO

## Steps:
- [x] 1. Update Escrow entity (`escrow.entity.ts`): add `disputeDeadline` column, `Dispute` import, and `OneToOne` decorator imports
- [x] 2. Update EscrowEvent enum (`escrow-event.entity.ts`): add `DISPUTE_TIMEOUT = 'dispute_timeout'`
- [x] 3. Fix Escrow controller (`escrow.controller.ts`): add missing `AdminGuard` import
- [x] 4. Fix Escrow service (`escrow.service.ts`): replace string cast with `EscrowEventType.DISPUTE_TIMEOUT`
- [ ] 5. Update on-chain types (`lib.rs`): add `dispute_deadline: u64` to `Escrow` and `EscrowEntryV2` structs; update mapping functions
- [ ] 6. Update on-chain `raise_dispute` (`lib.rs`): set `dispute_deadline` when raising a dispute
- [ ] 7. Implement on-chain `trigger_default_resolution` (`lib.rs`): auto-resolve with 50/50 split after deadline
- [ ] 8. Add on-chain tests (`test.rs`): verify deadline setting and default resolution behavior
- [ ] 9. Verify backend builds (`npm run build`)
- [ ] 10. Verify on-chain tests pass (`cargo test`)

**Next:** Start editing files
>>>>>>> 589aa69adea7ff0b1b7706d1c0e19a7ffa6ba997

2 changes: 1 addition & 1 deletion apps/backend/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default tseslint.config(
},
},
{
files: ['**/*.spec.ts', '**/*.e2e-spec.ts'],
files: ['**/*.spec.ts', '**/*.e2e-spec.ts', 'test/setup/test-app.factory.ts'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'off',
Expand Down
2,189 changes: 910 additions & 1,279 deletions apps/backend/package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"socket.io": "^4.8.3",
"sqlite3": "^5.1.7",
"stellar-sdk": "^13.3.0",
"swagger-ui-express": "^5.0.1",
Expand All @@ -64,19 +65,19 @@
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.19.11",
"@types/nodemailer": "^7.0.11",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"jest": "^29.7.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-jest": "^29.4.11",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
Expand Down
197 changes: 189 additions & 8 deletions apps/backend/pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion apps/backend/src/api-key/api-key.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { ApiKeyController } from './api-key.controller';
import { ApiKey } from './entities/api-key.entity';
import { ApiRateLimitService } from './api-rate-limit.service';
import { ApiKeyGuard } from './guards/api-key.guard';
import { AuthModule } from '../modules/auth/auth.module';

@Module({
imports: [TypeOrmModule.forFeature([ApiKey])],
imports: [TypeOrmModule.forFeature([ApiKey]), AuthModule],
controllers: [ApiKeyController],
providers: [ApiKeysService, ApiRateLimitService, ApiKeyGuard],
exports: [ApiKeysService, ApiRateLimitService, ApiKeyGuard],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddDisputeDeadlineToEscrow1774364376000 implements MigrationInterface {
name = 'AddDisputeDeadlineToEscrow1774364376000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "escrows" ADD COLUMN "disputeDeadline" datetime`);
await queryRunner.query(`CREATE INDEX "idx_escrows_dispute_deadline" ON "escrows" ("disputeDeadline")`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "idx_escrows_dispute_deadline"`);
await queryRunner.query(`ALTER TABLE "escrows" DROP COLUMN "disputeDeadline"`);
}
}
2 changes: 1 addition & 1 deletion apps/backend/src/modules/admin/admin.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ import { Dispute } from '../escrow/entities/dispute.entity';
AdminAuditLogService,
AnalyticsService,
],
exports: [AdminService],
exports: [AdminService, ConsistencyCheckerService],
})
export class AdminModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export class ConsistencyCheckerService {
private mapContractStatus(contractStatus: string): string {
const statusMap: Record<string, string> = {
Created: 'pending',
Active: 'funded',
Active: 'active',
Completed: 'completed',
Cancelled: 'cancelled',
Disputed: 'disputed',
Expand Down
37 changes: 36 additions & 1 deletion apps/backend/src/modules/assets/assets.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,47 @@
import { Controller, Get } from '@nestjs/common';
import {
Controller,
Get,
Query,
UseGuards,
Request,
BadRequestException,
} from '@nestjs/common';
import { Request as ExpressRequest } from 'express';
import { AssetsService } from './assets.service';
import { AuthGuard } from '../auth/middleware/auth.guard';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';

interface AuthenticatedRequest extends ExpressRequest {
user: {
userId: string;
walletAddress: string;
email: string;
role: string;
};
}

@Controller('assets')
@ApiTags('assets')
export class AssetsController {
constructor(private readonly assetsService: AssetsService) {}

@Get()
async findAllActive() {
return this.assetsService.findAll(true);
}

@Get('balance')
@UseGuards(AuthGuard)
@ApiBearerAuth()
async getBalance(
@Query('assetCode') assetCode: string,
@Query('issuer') issuer: string | undefined,
@Request() req: AuthenticatedRequest,
) {
if (!assetCode) {
throw new BadRequestException('assetCode query parameter is required');
}
const walletAddress = req.user.walletAddress;
return this.assetsService.getBalance(walletAddress, assetCode, issuer);
}
}
42 changes: 42 additions & 0 deletions apps/backend/src/modules/assets/assets.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,46 @@ export class AssetsService {
const asset = await this.findOne(id);
await this.assetRepository.remove(asset);
}

async getBalance(
walletAddress: string,
assetCode: string,
issuer?: string,
): Promise<{ balance: number; assetCode: string; issuer?: string }> {
try {
const account = await this.stellarService.getAccount(walletAddress);
const balanceItem = account.balances.find((b) => {
if (assetCode === 'XLM' || assetCode === 'native') {
return b.asset_type === 'native';
} else {
return b.asset_code === assetCode && b.asset_issuer === issuer;
}
});

if (!balanceItem) {
if (assetCode === 'XLM') {
throw new BadRequestException(
'Account has no native XLM balance or is not funded',
);
} else {
throw new BadRequestException(
`Account does not trust the asset ${assetCode}. Please establish a trustline first.`,
);
}
}

return {
balance: parseFloat(balanceItem.balance),
assetCode,
issuer,
};
} catch (error) {
if (error instanceof BadRequestException) {
throw error;
}
throw new BadRequestException(
`Failed to fetch balance: account ${walletAddress} may not exist or cannot be reached.`,
);
}
}
}
15 changes: 15 additions & 0 deletions apps/backend/src/modules/escrow/controllers/escrow.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
ApiTags,
} from '@nestjs/swagger';
import { AuthGuard } from '../../auth/middleware/auth.guard';
import { AdminGuard } from '../../auth/middleware/admin.guard';
import { EscrowAccessGuard } from '../guards/escrow-access.guard';
import { EscrowExpireGuard } from '../guards/escrow-expire.guard';
import { EscrowService } from '../services/escrow.service';
Expand Down Expand Up @@ -305,6 +306,7 @@ export class EscrowController {
ipAddress,
);
}
<<<<<<< HEAD
@Post(':id/evidence')
@UseGuards(EscrowAccessGuard)
@UseInterceptors(FileInterceptor('file'))
Expand All @@ -326,4 +328,17 @@ export class EscrowController {
const userId = this.getAuthenticatedUserId(req);
return this.escrowService.uploadEvidence(id, userId, file);
}
=======

/**
* POST /escrows/:id/dispute/default-resolve
* Trigger default resolution for overdue disputes (scheduler/admin only).
*/
@Post(':id/dispute/default-resolve')
@UseGuards(AdminGuard) // Add AdminGuard import/use
async triggerDefaultResolution(@Param('id') id: string) {
return this.escrowService.triggerDefaultResolution(id);
}

>>>>>>> 589aa69adea7ff0b1b7706d1c0e19a7ffa6ba997
}
8 changes: 8 additions & 0 deletions apps/backend/src/modules/escrow/dto/list-escrows.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,12 @@ export class ListEscrowsDto {
@IsString()
@IsOptional()
search?: string;

@IsString()
@IsOptional()
assetCode?: string;

@IsString()
@IsOptional()
assetIssuer?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export enum EscrowEventType {
DISPUTED = 'disputed',
DISPUTE_FILED = 'dispute_filed',
DISPUTE_RESOLVED = 'dispute_resolved',
DISPUTE_TIMEOUT = 'dispute_timeout',
EXPIRED = 'expired',
EXPIRATION_WARNING_SENT = 'expiration_warning_sent',
}
Expand Down
31 changes: 31 additions & 0 deletions apps/backend/src/modules/escrow/entities/escrow.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import {
UpdateDateColumn,
ManyToOne,
OneToMany,
OneToOne,
JoinColumn,
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
import { Party } from './party.entity';
import { Condition } from './condition.entity';
import { EscrowEvent } from './escrow-event.entity';
import { Dispute } from './dispute.entity';

export enum EscrowStatus {
PENDING = 'pending',
Expand Down Expand Up @@ -40,6 +42,7 @@ export enum EscrowType {
'status',
'createdAt',
])
@Index('idx_escrows_dispute_deadline', ['disputeDeadline'])
export class Escrow {
@PrimaryGeneratedColumn('uuid')
id: string;
Expand Down Expand Up @@ -96,10 +99,14 @@ export class Escrow {
@Column({ type: 'datetime', nullable: true })
expirationNotifiedAt?: Date;

@Column({ type: 'datetime', nullable: true, name: 'dispute_deadline' })
disputeDeadline?: Date;

@Column({ default: true })
isActive: boolean;

@OneToMany(() => Party, (party) => party.escrow, { cascade: true })

parties: Party[];

@OneToMany(() => Condition, (condition) => condition.escrow, {
Expand All @@ -110,9 +117,33 @@ export class Escrow {
@OneToMany(() => EscrowEvent, (event) => event.escrow, { cascade: true })
events: EscrowEvent[];

@Column({ type: 'datetime', nullable: true })
disputeDeadline?: Date;

@OneToOne(() => Dispute, (dispute) => dispute.escrow)
dispute?: Dispute;

// @OneToMany(() => Milestone, (m) => m.escrow)
// milestones: Milestone[];
>>>>>>> 589aa69adea7ff0b1b7706d1c0e19a7ffa6ba997
=======
@Column({ nullable: true })
metadataHash?: string;

@OneToOne(() => Dispute, (dispute) => dispute.escrow)
dispute?: Dispute;

=======
@Column({ type: 'datetime', nullable: true })
disputeDeadline?: Date;

@OneToOne(() => Dispute, (dispute) => dispute.escrow)
dispute?: Dispute;

// @OneToMany(() => Milestone, (m) => m.escrow)
// milestones: Milestone[];
>>>>>>> 589aa69adea7ff0b1b7706d1c0e19a7ffa6ba997

@CreateDateColumn()
createdAt: Date;

Expand Down
43 changes: 43 additions & 0 deletions apps/backend/src/modules/escrow/escrow-dispute.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Injectable, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { Dispute, DisputeStatus } from './entities/dispute.entity';
import { Escrow, EscrowStatus } from './entities/escrow.entity';
import { validateTransition } from './escrow-state-machine';
import { DisputeOutcome } from './entities/dispute.entity';

@Injectable()
export class EscrowDisputeService {
constructor(
@InjectRepository(Dispute)
private disputeRepo: Repository<Dispute>,
@InjectRepository(Escrow)
private escrowRepo: Repository<Escrow>,
) {}

async fileDispute(escrow: Escrow, userId: string, reason: string) {
if (escrow.status !== EscrowStatus.ACTIVE) {
throw new ConflictException('Cannot dispute this escrow');
}

validateTransition(escrow.status, EscrowStatus.DISPUTED);

escrow.status = EscrowStatus.DISPUTED;
await this.escrowRepo.save(escrow);

return this.disputeRepo.save({
escrowId: escrow.id,
initiatorUserId: userId,
reason,
status: DisputeStatus.OPEN,
});
}

async resolve(dispute: Dispute, outcome: DisputeOutcome) {
dispute.status = DisputeStatus.RESOLVED;
dispute.outcome = outcome;

return this.disputeRepo.save(dispute);
}
}
Loading