Skip to content

Commit 55e6ba1

Browse files
authored
Merge pull request #267 from Jambox11/feature/claim-evidence-content-hash
feat: claim evidence URL + SHA-256 commitment (file_claim, events, st…
2 parents a980b5b + e153a2f commit 55e6ba1

47 files changed

Lines changed: 615 additions & 227 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/loadtests/claim-submit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export default function () {
7070
policyId: 1,
7171
amount: '100',
7272
details: 'k6 load test claim — staging only',
73-
imageUrls: [],
73+
evidence: [],
7474
},
7575
JWT,
7676
'build-tx',

backend/src/claims/claims.controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export class ClaimsController {
115115
policyId: dto.policyId,
116116
amount: BigInt(dto.amount),
117117
details: dto.details,
118-
imageUrls: dto.imageUrls,
118+
evidence: dto.evidence,
119119
});
120120
}
121121

@@ -190,3 +190,4 @@ export class ClaimsController {
190190
unsubscribe();
191191
});
192192
}
193+
}

backend/src/claims/claims.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ export class ClaimsService {
315315
policyId: number;
316316
amount: bigint;
317317
details: string;
318-
imageUrls: string[];
318+
evidence: { url: string; contentSha256Hex: string }[];
319319
}) {
320320
return this.soroban.buildFileClaimTransaction(args);
321321
}

backend/src/claims/dto/build-claim-transaction.dto.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ApiProperty } from '@nestjs/swagger';
2+
import { Type } from 'class-transformer';
23
import {
34
IsArray,
45
IsInt,
@@ -7,6 +8,7 @@ import {
78
Matches,
89
MaxLength,
910
Validate,
11+
ValidateNested,
1012
ValidatorConstraint,
1113
ValidatorConstraintInterface,
1214
} from 'class-validator';
@@ -21,6 +23,26 @@ class PositiveIntStringConstraint implements ValidatorConstraintInterface {
2123
}
2224
}
2325

26+
export class ClaimEvidenceItemDto {
27+
@ApiProperty({
28+
description: 'Evidence location (e.g. ipfs:// or gateway URL).',
29+
example: 'ipfs://QmXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
30+
})
31+
@IsString()
32+
url!: string;
33+
34+
@ApiProperty({
35+
description: 'Lowercase hex SHA-256 of file bytes (64 chars). Prefer value from IPFS upload/proxy.',
36+
example:
37+
'0100000000000000000000000000000000000000000000000000000000000000',
38+
})
39+
@IsString()
40+
@Matches(/^[0-9a-fA-F]{64}$/, {
41+
message: 'contentSha256Hex must be 64 hex characters (32-byte SHA-256)',
42+
})
43+
contentSha256Hex!: string;
44+
}
45+
2446
export class BuildClaimTransactionDto {
2547
@ApiProperty({
2648
description: 'Stellar public key of the claimant.',
@@ -57,10 +79,12 @@ export class BuildClaimTransactionDto {
5779
details!: string;
5880

5981
@ApiProperty({
60-
description: 'List of IPFS URLs (or CIDs) for evidence images.',
61-
example: ['https://ipfs.io/ipfs/Qm...'],
82+
description:
83+
'Evidence attachments: URL plus SHA-256 content hash (from proxy when pinning).',
84+
type: [ClaimEvidenceItemDto],
6285
})
6386
@IsArray()
64-
@IsString({ each: true })
65-
imageUrls!: string[];
87+
@ValidateNested({ each: true })
88+
@Type(() => ClaimEvidenceItemDto)
89+
evidence!: ClaimEvidenceItemDto[];
6690
}

backend/src/events/events.schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ export interface ClaimFiledEvent {
3939
policy_id: number;
4040
/** Requested payout in stroops (i128 as string). */
4141
amount: string;
42-
/** FNV-1a u64 hash of concatenated IPFS CIDs (number). Full CIDs stored off-chain. */
43-
image_hash: number;
42+
/** SHA-256 digests (32 bytes each), same order as claim evidence; commitment only on-chain. */
43+
evidence_hashes: string[];
4444
/** Ledger sequence at filing time. */
4545
filed_at: number;
4646
}

backend/src/events/events.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ describe('clm_filed', () => {
4141
version: SCHEMA_VERSION,
4242
policy_id: 3,
4343
amount: '5000000',
44-
image_hash: 2864434397,
44+
evidence_hashes: [
45+
'0100000000000000000000000000000000000000000000000000000000000000',
46+
],
4547
filed_at: LEDGER,
4648
};
4749

@@ -62,7 +64,9 @@ describe('clm_filed', () => {
6264
expect(p.version).toBe(SCHEMA_VERSION);
6365
expect(p.policy_id).toBe(3);
6466
expect(p.amount).toBe('5000000');
65-
expect(p.image_hash).toBe(2864434397);
67+
expect(p.evidence_hashes).toEqual([
68+
'0100000000000000000000000000000000000000000000000000000000000000',
69+
]);
6670
expect(p.filed_at).toBe(LEDGER);
6771
});
6872
});
@@ -300,7 +304,13 @@ describe('parseEvent', () => {
300304
});
301305

302306
it('returns null for unsupported schema version', () => {
303-
const payload = { version: 999, policy_id: 1, amount: '0', image_hash: 0, filed_at: 0 };
307+
const payload = {
308+
version: 999,
309+
policy_id: 1,
310+
amount: '0',
311+
evidence_hashes: [],
312+
filed_at: 0,
313+
};
304314
expect(parseEvent(['niffyins', 'clm_filed', 1n, HOLDER], payload, LEDGER, TX)).toBeNull();
305315
});
306316

backend/src/indexer/indexer.service.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config';
33
import { Prisma } from '@prisma/client';
44
import { PrismaService } from '../prisma/prisma.service';
55
import { SorobanService } from '../rpc/soroban.service';
6+
import { parseEvent } from '../events/events.schema';
67
import { rpc as SorobanRpc, scValToNative } from '@stellar/stellar-sdk';
78

89
type IndexerTx = Prisma.TransactionClient;
@@ -327,6 +328,10 @@ export class IndexerService {
327328
});
328329
}
329330

331+
/**
332+
* On-chain `ClaimFiled` carries claim_id + holder in topics and `evidence_hashes` in the value.
333+
* Full claim rows need policy_id / amount / URLs from `get_claim` — backfill TBD.
334+
*/
330335
private async handleClaimFiled(tx: IndexerTx, data: EventPayload, event: SorobanEvent) {
331336
const claimId = getNumberValue(data.claim_id);
332337
const policyDbId = `${getStringValue(data.claimant)}:${getNumberValue(data.policy_id)}`;

backend/src/ipfs/ipfs.controller.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ export class IpfsController {
131131
'https://cloudflare-ipfs.com/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco',
132132
],
133133
},
134+
contentSha256Hex: {
135+
type: 'string',
136+
example:
137+
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
138+
description: 'SHA-256 of uploaded bytes (64 hex chars); use in file_claim evidence.',
139+
},
134140
filename: { type: 'string', example: 'document.pdf' },
135141
size: { type: 'number', example: 123456 },
136142
mimeType: { type: 'string', example: 'application/pdf' },

backend/src/ipfs/services/idempotency.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface IdempotencyRecord {
2121
cid: string;
2222
gatewayUrls: string[];
2323
uploadedAt: string;
24+
contentSha256Hex?: string;
2425
};
2526
/** Timestamp when the record was created */
2627
createdAt: string;
@@ -120,7 +121,7 @@ export class IdempotencyService {
120121
async storeResult(
121122
key: string,
122123
contentHash: string,
123-
response: { cid: string; gatewayUrls: string[] },
124+
response: { cid: string; gatewayUrls: string[]; contentSha256Hex: string },
124125
): Promise<void> {
125126
const record: IdempotencyRecord = {
126127
key,
@@ -129,6 +130,7 @@ export class IdempotencyService {
129130
cid: response.cid,
130131
gatewayUrls: response.gatewayUrls,
131132
uploadedAt: new Date().toISOString(),
133+
contentSha256Hex: response.contentSha256Hex,
132134
},
133135
createdAt: new Date().toISOString(),
134136
hitCount: 0,

backend/src/ipfs/services/ipfs.service.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export interface IpfsUploadResponse {
1818
cid: string;
1919
/** Gateway URLs where the content can be accessed */
2020
gatewayUrls: string[];
21+
/** SHA-256 hex (64 chars) of uploaded bytes (after EXIF strip); for `file_claim` evidence. */
22+
contentSha256Hex: string;
2123
/** Original filename (sanitized) */
2224
filename?: string;
2325
/** File size in bytes */
@@ -140,12 +142,17 @@ export class IpfsService {
140142

141143
if (!idempotencyCheck.shouldUpload && idempotencyCheck.existingRecord) {
142144
this.logger.log(`Returning cached response for idempotent request: ${idempotencyCheck.key}`);
145+
const cached = idempotencyCheck.existingRecord.response;
143146
return {
144-
...idempotencyCheck.existingRecord.response,
147+
cid: cached.cid,
148+
gatewayUrls: cached.gatewayUrls,
149+
contentSha256Hex:
150+
cached.contentSha256Hex ?? idempotencyCheck.contentHash,
145151
filename: sanitizedFilename,
146152
size: processedBuffer.length,
147153
mimeType,
148154
duplicated: true,
155+
uploadedAt: cached.uploadedAt,
149156
};
150157
}
151158

@@ -160,9 +167,12 @@ export class IpfsService {
160167
throw new ServiceUnavailableException('IPFS provider is temporarily unavailable');
161168
}
162169

170+
const contentSha256Hex =
171+
this.fileValidationService.calculateContentHash(processedBuffer);
172+
163173
// Upload to IPFS
164174
this.logger.debug(`Uploading ${sanitizedFilename} (${processedBuffer.length} bytes) to IPFS`);
165-
175+
166176
let uploadResult: IpfsUploadResult;
167177
try {
168178
uploadResult = await this.provider.upload(
@@ -183,7 +193,7 @@ export class IpfsService {
183193
await this.idempotencyService.storeResult(
184194
idempotencyCheck.key,
185195
contentHash,
186-
{ cid: uploadResult.cid, gatewayUrls },
196+
{ cid: uploadResult.cid, gatewayUrls, contentSha256Hex },
187197
);
188198

189199
const duration = Date.now() - startTime;
@@ -194,6 +204,7 @@ export class IpfsService {
194204
return {
195205
cid: uploadResult.cid,
196206
gatewayUrls,
207+
contentSha256Hex,
197208
filename: sanitizedFilename,
198209
size: uploadResult.size,
199210
mimeType: uploadResult.mimeType,

0 commit comments

Comments
 (0)