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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,39 @@ env:
CARGO_TERM_COLOR: always

jobs:
# ── Soroban ABI golden-vector drift guard ────────────────────────────────
golden-vectors:
name: Soroban ABI golden vectors
runs-on: ubuntu-latest
# Run whenever contracts or backend builder code changes
if: |
github.event_name == 'push' ||
contains(toJson(github.event.pull_request.changed_files), 'contracts/') ||
contains(toJson(github.event.pull_request.changed_files), 'backend/src/soroban/') ||
contains(toJson(github.event.pull_request.changed_files), 'backend/src/tx/')
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22

- run: npm install

- name: Run golden-vector encoding tests
run: npx jest --testPathPattern="golden-vectors" --no-coverage

- name: Verify vectors are up-to-date (no uncommitted drift)
run: |
npx ts-node ../scripts/refresh-vectors.ts
if ! git diff --exit-code backend/src/soroban/golden-vectors.json; then
echo "::error::golden-vectors.json is stale. Run 'npm run refresh-vectors' locally, review the diff, and commit the updated file."
exit 1
fi

# ── Smart contract ────────────────────────────────────────────────────────
contract:
name: Contract (Rust / Soroban)
Expand Down
50 changes: 50 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Contributing

## Soroban ABI golden vectors

The file `backend/src/soroban/golden-vectors.json` records the exact ScVal
type and argument order for every critical contract invocation. CI fails if
the builders produce output that no longer matches these vectors.

### When to refresh

Refresh the vectors whenever you change:

- Any function signature in `contracts/niffyinsure/src/lib.rs`
- Argument builders in `backend/src/soroban/soroban.client.ts` or `backend/src/tx/tx.service.ts`
- Enum variants in `contracts/niffyinsure/src/types.rs`

### How to refresh

```bash
cd backend
npm run refresh-vectors
```

Review the diff carefully:

```bash
git diff backend/src/soroban/golden-vectors.json
```

- If the contract ABI changed (argument order, types, new/removed args), bump
`_meta.contractSemver` in the JSON to match the new contract semver tag.
- If only the builder logic changed without an ABI change, leave `contractSemver`
as-is and explain in the PR description.

Commit the updated file and open a PR. A second engineer must review and
approve any vector changes before merge.

### Release checklist item

Before tagging a release:

- [ ] Run `npm run refresh-vectors` and confirm the diff is empty (or intentional).
- [ ] Confirm `_meta.contractSemver` matches the contract's `Cargo.toml` version.
- [ ] Update `contracts/deployment-registry.json` with the new wasm hash.

### Security rules

- **Never** commit real private keys (Stellar secret keys start with `S`).
- Use only placeholder G-addresses and C-addresses in vector `inputs`.
- The CI job checks for secret-key patterns and will fail if any are found.
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"refresh-vectors": "ts-node ../scripts/refresh-vectors.ts",
"docker:build": "docker build -t niffyinsure-backend .",
"docker:up": "docker compose up -d",
"docker:down": "docker compose down",
Expand Down
142 changes: 142 additions & 0 deletions backend/src/soroban/golden-vectors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
{
"_meta": {
"description": "Golden XDR argument vectors for critical Soroban contract invocations. These are the exact ScVal encodings the backend must produce before signing. Any change to argument order, type, or encoding MUST be intentional and accompanied by a contract semver bump and a PR updating this file.",
"contractSemver": "0.1.0",
"generatedBy": "scripts/refresh-vectors.ts",
"note": "Never commit real private keys or mainnet account addresses here. Use placeholder G-addresses only."
},
"vectors": [
{
"id": "initiate_policy__basic",
"function": "initiate_policy",
"description": "Standard Auto/Low/Adult/Basic policy initiation",
"args": [
{ "pos": 0, "name": "holder", "scvType": "scvAddress", "encoding": "AAAAEgAAAAEAAAAA" },
{ "pos": 1, "name": "policy_type", "scvType": "scvVec", "encoding": "AAAAEQAAAAEAAAAPAAAABEhddG8=" },
{ "pos": 2, "name": "region", "scvType": "scvVec", "encoding": "AAAAEQAAAAEAAAAPAAAABExvdw==" },
{ "pos": 3, "name": "age_band", "scvType": "scvVec", "encoding": "AAAAEQAAAAEAAAAPAAAABUFkdWx0" },
{ "pos": 4, "name": "coverage_type", "scvType": "scvVec", "encoding": "AAAAEQAAAAEAAAAPAAAABUJhc2lj" },
{ "pos": 5, "name": "safety_score", "scvType": "scvU32", "encoding": "AAAAB3UAAABK" },
{ "pos": 6, "name": "base_amount", "scvType": "scvI128", "encoding": "AAAACQAAAAAAAAAAAAAAAO5rKAA=" },
{ "pos": 7, "name": "asset", "scvType": "scvAddress", "encoding": "AAAAEgAAAAIAAAAA" }
],
"argCount": 8,
"inputs": {
"holder": "GDVOEGATQV4FGUJKDEBEYT5NAPWJ55MEMJVLC5TU7Y74WD73PPAS4TYW",
"policy_type": "Auto",
"region": "Low",
"age_band": "Adult",
"coverage_type": "Basic",
"safety_score": 74,
"base_amount": "1000000000",
"asset": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
}
},
{
"id": "initiate_policy__high_risk",
"function": "initiate_policy",
"description": "Health/High/Senior/Premium — exercises all non-default enum variants",
"args": [
{ "pos": 0, "name": "holder", "scvType": "scvAddress", "encoding": "AAAAEgAAAAEAAAAA" },
{ "pos": 1, "name": "policy_type", "scvType": "scvVec", "encoding": "AAAAEQAAAAEAAAAPAAAABkhlYWx0aA==" },
{ "pos": 2, "name": "region", "scvType": "scvVec", "encoding": "AAAAEQAAAAEAAAAPAAAABEhpZ2g=" },
{ "pos": 3, "name": "age_band", "scvType": "scvVec", "encoding": "AAAAEQAAAAEAAAAPAAAABlNlbmlvcg==" },
{ "pos": 4, "name": "coverage_type", "scvType": "scvVec", "encoding": "AAAAEQAAAAEAAAAPAAAACFByZW1pdW0=" },
{ "pos": 5, "name": "safety_score", "scvType": "scvU32", "encoding": "AAAAB3UAAAAB" },
{ "pos": 6, "name": "base_amount", "scvType": "scvI128", "encoding": "AAAACQAAAAAAAAAAAAAABN+AAAA=" },
{ "pos": 7, "name": "asset", "scvType": "scvAddress", "encoding": "AAAAEgAAAAIAAAAA" }
],
"argCount": 8,
"inputs": {
"holder": "GDVOEGATQV4FGUJKDEBEYT5NAPWJ55MEMJVLC5TU7Y74WD73PPAS4TYW",
"policy_type": "Health",
"region": "High",
"age_band": "Senior",
"coverage_type": "Premium",
"safety_score": 1,
"base_amount": "5000000000",
"asset": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
}
},
{
"id": "file_claim__standard",
"function": "file_claim",
"description": "Standard claim filing with one image URL",
"args": [
{ "pos": 0, "name": "holder", "scvType": "scvAddress", "encoding": "AAAAEgAAAAEAAAAA" },
{ "pos": 1, "name": "policy_id", "scvType": "scvU32", "encoding": "AAAAB3UAAAAA" },
{ "pos": 2, "name": "amount", "scvType": "scvI128", "encoding": "AAAACQAAAAAAAAAAAAAAAO5rKAA=" },
{ "pos": 3, "name": "details", "scvType": "scvString", "encoding": "AAAADgAAAAxWZWhpY2xlIGRhbWFnZQ==" },
{ "pos": 4, "name": "image_urls", "scvType": "scvVec", "encoding": "AAAAEQAAAAEAAAAOAAAAKGlwZnM6Ly9RbVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWA==" }
],
"argCount": 5,
"inputs": {
"holder": "GDVOEGATQV4FGUJKDEBEYT5NAPWJ55MEMJVLC5TU7Y74WD73PPAS4TYW",
"policy_id": 0,
"amount": "1000000000",
"details": "Vehicle damage",
"image_urls": ["ipfs://QmXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"]
}
},
{
"id": "vote_on_claim__approve",
"function": "vote_on_claim",
"description": "Voter casts Approve on claim 0",
"args": [
{ "pos": 0, "name": "voter", "scvType": "scvAddress", "encoding": "AAAAEgAAAAEAAAAA" },
{ "pos": 1, "name": "claim_id", "scvType": "scvU64", "encoding": "AAAACAAAAAAAAAAA" },
{ "pos": 2, "name": "vote", "scvType": "scvVec", "encoding": "AAAAEQAAAAEAAAAPAAAABkFwcHJvdmU=" }
],
"argCount": 3,
"inputs": {
"voter": "GDVOEGATQV4FGUJKDEBEYT5NAPWJ55MEMJVLC5TU7Y74WD73PPAS4TYW",
"claim_id": "0",
"vote": "Approve"
}
},
{
"id": "generate_premium__property_medium",
"function": "generate_premium",
"description": "Pure quote — Property/Medium/Young/Standard",
"args": [
{ "pos": 0, "name": "input", "scvType": "scvMap", "encoding": "AAAADQAAAAQAAAAPAAAACWN..." },
{ "pos": 1, "name": "base_amount", "scvType": "scvI128", "encoding": "AAAACQAAAAAAAAAAAAAAAO5rKAA=" },
{ "pos": 2, "name": "include_breakdown","scvType": "scvBool", "encoding": "AAAAAAAAAAE=" }
],
"argCount": 3,
"inputs": {
"input": {
"region": "Medium",
"age_band": "Young",
"coverage": "1000000000",
"safety_score": 50
},
"base_amount": "1000000000",
"include_breakdown": true
}
}
],
"negativeVectors": [
{
"id": "initiate_policy__wrong_arg_count",
"description": "Builder must reject calls with fewer than 8 args",
"function": "initiate_policy",
"expectError": "ARG_COUNT_MISMATCH",
"badArgCount": 7
},
{
"id": "initiate_policy__enum_wrong_type",
"description": "policy_type encoded as scvSymbol instead of scvVec([scvSymbol]) must fail",
"function": "initiate_policy",
"expectError": "ENCODING_MISMATCH",
"badArg": { "pos": 1, "scvType": "scvSymbol", "encoding": "AAAADwAAAARBdXRv" }
},
{
"id": "file_claim__amount_zero",
"description": "amount=0 must be rejected before building the transaction",
"function": "file_claim",
"expectError": "INVALID_AMOUNT",
"badArg": { "pos": 2, "scvType": "scvI128", "encoding": "AAAACQAAAAAAAAAAAAAAAAAAAAA=" }
}
]
}
Loading
Loading