Skip to content

feat(#689): Stellar DID layer, cross-campaign reputation score & privacy-preserving KYC attestations - #731

Merged
dotunv merged 1 commit into
Savitura:mainfrom
TheHalalHunter:feat/689-stellar-did-reputation-kyc-attestations
Aug 30, 2026
Merged

feat(#689): Stellar DID layer, cross-campaign reputation score & privacy-preserving KYC attestations#731
dotunv merged 1 commit into
Savitura:mainfrom
TheHalalHunter:feat/689-stellar-did-reputation-kyc-attestations

Conversation

@TheHalalHunter

@TheHalalHunter TheHalalHunter commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Closes #689


Summary

Implements issue #689 end-to-end: a Stellar DID layer anchored to contributor public keys, an on-chain cross-campaign reputation score, and privacy-preserving KYC attestations where only a SHA-256 hash is stored on-chain — never the underlying document.


Changes

Soroban contract — contracts/soroban/contracts/contributor_identity/

  • New contributor_identity contract (soroban-sdk 21.3.0) added to Cargo workspace
  • Stores ContributorIdentity { did, attestations: Vec, reputation_score: u32, last_updated: u64 } in persistent storage keyed by contributor Address

egister(caller, did) — idempotent; emits IdentityRegistered

  • �dd_attestation(issuer, subject, attestation_type, expires_at, proof_hash) — approved issuers only

evoke_attestation(issuer, subject, index) — original issuer only; sets
evoked = true

  • update_reputation(subject, delta: i32) — platform contract only; clamped to [0, 1000]
  • get_identity, has_attestation, �erify_proof — read-only; revocation + hash check enforced on-chain

Database — �ackend/db/migrations/20260829_contributor_identity_reputation.sql

  • contributor_identities — links user to their on-chain DID
  • kyc_attestations — stores proof_hash (SHA-256 of Persona inquiry ID) only; no documents
  • campaign_requirements — per-campaign min_reputation_score + required_attestations JSONB

eputation_events — append-only audit log of every reputation delta

Backend service — �ackend/src/services/contributorIdentityService.js

egisterIdentity(publicKey, userId) — idempotent; calls contract + inserts DB row

  • issueKycAttestation(publicKey, userId, kycLevel, personaInquiryId) — proof_hash = SHA-256(inquiryId); calls �dd_attestation on-chain
  • updateReputationScore(publicKey, event) — deltas: contribution_made +5, contribution_to_successful_campaign +10, contribution_to_failed_campaign 0, dispute_raised_against_contributor -20
  • getContributorProfile(publicKey) — returns zero PII: did, reputationScore, attestation types/dates/revoked, contributionStats
  • �erifyAttestation(publicKey, type) — dual DB + on-chain check; RPC-fallback to DB on outage
  • �ssertContributorMeetsRequirements(publicKey, campaignId) — throws 403 CONTRIBUTOR_REQUIREMENTS_NOT_MET with missing[] array

Backend routes

  • POST /api/contributor/identity/register — authenticated; idempotent
  • GET /api/contributor/identity/:publicKey — public; no PII
  • GET /api/contributor/identity/:publicKey/verify?attestation=kyc_standard — public
  • POST /api/campaigns/:id/requirements — creator-only
  • GET /api/campaigns/:id/requirements — public
  • Contribution gate added to both POST /api/contributions/prepare and custodial POST /api/contributions/
  • KYC webhook fires issueKycAttestation fire-and-forget after Persona approval

Frontend

  • ReputationGauge — SVG radial gauge (no charting library); tiers: Newcomer / Contributor / Trusted / Veteran / Champion
  • AttestationsPanel — lists all 3 KYC tiers; Get Verified CTA for missing ones; no PII
  • ContributorIdentityPage at /profile/identity — DID + copy/Stellar Expert link, gauge, attestations, contribution stats
  • Campaign.jsx — ContributorEligibilityBadge (eligible ✅ / not met 🔒 with gap detail) for logged-in users; CampaignRequirementsNotice for logged-out visitors
  • CreateCampaign.jsx step 3 — reputation slider (0–500) + KYC attestation checkboxes; requirements posted after campaign creation

Acceptance criteria

  • [x]
    egister creates an on-chain ContributorIdentity record verifiable via getLedgerEntries
  • has_attestation returns alse for a revoked attestation — enforced on-chain (&& !att.revoked)
  • �erify_proof returns alse on hash mismatch — enforced on-chain (&& att.proof_hash == proof_hash)
  • Reputation increments +10 when a contributor's campaign succeeds
  • min_reputation_score: 300 blocks score-299 contributor with 403 CONTRIBUTOR_REQUIREMENTS_NOT_MET
  • Eligibility indicator shows KYC gap (Basic present, Standard required)
  • getContributorProfile schema contains zero personal data

Deployment notes

  1. Deploy the contributor_identity contract and set CONTRIBUTOR_IDENTITY_CONTRACT_ID in .env
  2. Run
    pm run migrate to apply 20260829_contributor_identity_reputation.sql
  3. Call contract.initialize(admin, platformContractAddress) and contract.add_issuer(platformPublicKey) once after deploy
  4. Wire updateReputationScore into campaignStatusService (on funded/completed) and the dispute service (on dispute raised) — the service function is ready, the call sites are the existing handlers

…serving KYC attestations

## What

Implements issue Savitura#689 end-to-end across the Soroban contract layer,
backend services/routes, and frontend UI.

## Soroban contract (contracts/soroban/contracts/contributor_identity/)
- New contributor_identity Rust contract (soroban-sdk 21.3.0)
- Stores ContributorIdentity { did, attestations, reputation_score, last_updated }
  in persistent storage keyed by contributor Address
- Functions: initialize, �dd_issuer, 
emove_issuer, 
egister (idempotent),
  �dd_attestation, 
evoke_attestation, update_reputation (clamped 0-1000),
  get_identity, has_attestation, �erify_proof
- has_attestation enforces revocation on-chain (&& !att.revoked)
- �erify_proof enforces hash match on-chain (&& att.proof_hash == proof_hash)
- Added to Cargo workspace

## Database (backend/db/migrations/20260829_contributor_identity_reputation.sql)
- contributor_identities (user_id, public_key, did, contract_registered_at)
- kyc_attestations (proof_hash only — never raw docs or Persona inquiry content)
- campaign_requirements (min_reputation_score 0-1000, required_attestations JSONB)
- 
eputation_events append-only audit log

## Backend service (backend/src/services/contributorIdentityService.js)
- 
egisterIdentity — builds DID, calls contract, stores DB row; idempotent
- issueKycAttestation — proof_hash = SHA-256(personaInquiryId), no PII on-chain
- updateReputationScore — deltas: +5 contribution_made,
  +10 contribution_to_successful_campaign, 0 failed, -20 dispute
- getContributorProfile — returns zero personal data (no name/email/doc/inquiry ID)
- �erifyAttestation — dual DB + on-chain check with RPC fallback
- �ssertContributorMeetsRequirements — throws 403 CONTRIBUTOR_REQUIREMENTS_NOT_MET
  with structured missing[] array

## Backend routes
- POST/GET /api/contributor/identity/register|:publicKey|:publicKey/verify
- POST/GET /api/campaigns/:id/requirements (creator-only write, public read)
- Contribution gate added to both /prepare and custodial POST /
- KYC webhook fires issueKycAttestation fire-and-forget after Persona approval

## Frontend
- ReputationGauge — SVG radial gauge, tiers: Newcomer/Contributor/Trusted/Veteran/Champion
- AttestationsPanel — 3 KYC tiers with Get Verified CTAs, no PII displayed
- ContributorIdentityPage at /profile/identity — DID, gauge, attestations, stats
- Campaign page: ContributorEligibilityBadge (eligible / requirements not met + gap)
  and CampaignRequirementsNotice for logged-out visitors
- CreateCampaign step 3: reputation slider (0-500) + KYC attestation checkboxes

## Acceptance criteria
- register creates verifiable on-chain ContributorIdentity record
- has_attestation returns false for revoked attestation (enforced on-chain)
- verify_proof returns false on hash mismatch (enforced on-chain)
- +10 reputation on campaign success
- 403 CONTRIBUTOR_REQUIREMENTS_NOT_MET for score 299 vs min 300
- Eligibility indicator shows KYC gap (Basic vs Standard)
- getContributorProfile schema contains zero personal data

Closes Savitura#689
@drips-wave

drips-wave Bot commented Aug 29, 2026

Copy link
Copy Markdown

@TheHalalHunter Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stellar DID Layer, Cross-Campaign Reputation Score & Privacy-Preserving KYC Attestations

2 participants