Skip to content

Commit f0d1436

Browse files
Merge branch 'main' into feat/idempotency-key-donation-recording-148
2 parents c147913 + 25cf31d commit f0d1436

25 files changed

Lines changed: 4196 additions & 278 deletions

File tree

.github/workflows/contracts.yml

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ on:
77
pull_request:
88
paths: [contracts/**]
99
workflow_dispatch:
10-
schedule:
11-
# Nightly deep fuzz run -- 1 000 000 iterations every day at 03:00 UTC
12-
- cron: "0 3 * * *"
1310

1411
jobs:
1512
test:
@@ -67,38 +64,10 @@ jobs:
6764
with:
6865
workspaces: contracts
6966

70-
- name: Run fuzz tests (10k iterations)
67+
- name: Run fuzz tests (1.5k iterations)
7168
run: cargo test --features testutils -- fuzz
7269
env:
73-
FUZZ_ITERATIONS: 10000
74-
75-
deep-fuzz:
76-
name: Deep Fuzz (1M iterations -- nightly)
77-
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
78-
runs-on: ubuntu-latest
79-
timeout-minutes: 120
80-
defaults:
81-
run:
82-
working-directory: contracts
83-
steps:
84-
- uses: actions/checkout@v4
85-
86-
- name: Install Rust toolchain
87-
uses: dtolnay/rust-toolchain@stable
88-
with:
89-
toolchain: 1.91.0
90-
targets: wasm32v1-none
91-
components: rustfmt, clippy
92-
93-
- name: Cache Rust dependencies
94-
uses: Swatinem/rust-cache@v2
95-
with:
96-
workspaces: contracts
97-
98-
- name: Run deep fuzz tests (1M iterations)
99-
run: cargo test --features testutils -- fuzz
100-
env:
101-
FUZZ_ITERATIONS: 1000000
70+
FUZZ_ITERATIONS: 1500
10271

10372
coverage:
10473
name: Coverage (tarpaulin)

ROADMAP.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@
8787

8888
> **Ideas for the next release — contributions welcome!**
8989
90-
- [ ] Cross-chain donation attestations
90+
- [x] Cross-chain donation attestations — landed in #125. New
91+
`attestation-contract` Soroban contract, off-chain backend API at
92+
`/api/attestations`, frontend `/verify` page + bridge page upgrade.
9193
- [ ] Deeper Stellar DEX integration for auto conversion
9294
- [ ] Push notification overhaul
9395
- [ ] In-app wallet (non-custodial key management)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"use strict";
2+
3+
/**
4+
* Migration: 005_attestations
5+
*
6+
* Adds the `attestations` table that the backend keeps in lockstep with the
7+
* Soroban `attestation-contract` (cross-chain donation attestation bridge,
8+
* issue #125). The schema mirrors the on-chain fields exactly so that the
9+
* backend can fall back to DB reads when the RPC is unreachable, and the
10+
* indexer can write transactions with full exchange coverage.
11+
*
12+
* Replay protection:
13+
* UNIQUE (source_chain, source_tx_hash) — a duplicate source tx can never
14+
* be recorded twice in the DB even before the on-chain guard fires. This
15+
* gives the indexer a fast path to dedupe without round-tripping every op
16+
* to Soroban.
17+
*
18+
* Foreign key:
19+
* project_id references projects(id) ON DELETE SET NULL so that
20+
* removing a project (e.g. after admin takedown) doesn't cascade-delete
21+
* the historical donation trail.
22+
*/
23+
module.exports = {
24+
name: "005_attestations",
25+
26+
async up(client) {
27+
await client.query(`
28+
CREATE TABLE IF NOT EXISTS attestations (
29+
id UUID PRIMARY KEY,
30+
-- Mirrors the on-chain monotonic counter set by the relayer.
31+
-- Separate from `id` (UUID) so multiple backend replicas can
32+
-- converge on the same on-chain id without conflicting inserts.
33+
on_chain_id BIGINT NOT NULL UNIQUE,
34+
source_chain TEXT NOT NULL,
35+
source_tx_hash TEXT NOT NULL,
36+
donor_address TEXT NOT NULL,
37+
project_id UUID REFERENCES projects(id) ON DELETE SET NULL,
38+
amount_usd NUMERIC(20, 6) NOT NULL CHECK (amount_usd > 0),
39+
amount_xlm NUMERIC(20, 7) NOT NULL CHECK (amount_xlm > 0),
40+
message_hash BIGINT NOT NULL DEFAULT 0,
41+
-- Mirrors AttestationStatus on-chain: pending | verified | revoked.
42+
status TEXT NOT NULL DEFAULT 'pending',
43+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
44+
verified_at TIMESTAMPTZ,
45+
-- The relayer Stellar address that submitted `record_attestation`.
46+
recorded_by TEXT,
47+
CONSTRAINT attestations_status_check
48+
CHECK (status IN ('pending','verified','revoked')),
49+
CONSTRAINT attestations_source_unique
50+
UNIQUE (source_chain, source_tx_hash)
51+
)
52+
`);
53+
54+
await client.query(
55+
`CREATE INDEX IF NOT EXISTS idx_attestations_donor
56+
ON attestations (donor_address, created_at DESC)`,
57+
);
58+
await client.query(
59+
`CREATE INDEX IF NOT EXISTS idx_attestations_project
60+
ON attestations (project_id, created_at DESC)`,
61+
);
62+
await client.query(
63+
`CREATE INDEX IF NOT EXISTS idx_attestations_status
64+
ON attestations (status, created_at DESC)`,
65+
);
66+
67+
// Surface verify-projection: history view that powers the public
68+
// /api/attestations/by-donor/:addr endpoint without exposing internal
69+
// columns like `recorded_by` outside the admin scope.
70+
await client.query(`
71+
CREATE OR REPLACE VIEW attestations_public AS
72+
SELECT
73+
id,
74+
on_chain_id,
75+
source_chain,
76+
source_tx_hash,
77+
donor_address,
78+
project_id,
79+
amount_usd,
80+
amount_xlm,
81+
status,
82+
message_hash,
83+
created_at,
84+
verified_at
85+
FROM attestations
86+
`);
87+
},
88+
89+
async down(client) {
90+
await client.query(`DROP VIEW IF EXISTS attestations_public`);
91+
await client.query(`DROP TABLE IF EXISTS attestations`);
92+
},
93+
};

0 commit comments

Comments
 (0)