Commit 226cd45
authored
feat(backend): connect the inactivity watchdog to on-chain Soroban payout execution (#1045)
* fix(backend): repair the migration chain so it applies from scratch
`sqlx::migrate!()` aborted on the first failing migration, so on an empty
database nothing after the core tables was ever created — including the
inactivity watchdog's own `status` / `inactivity_deadline_at` columns, which
the watchdog then queried on every sweep.
Three separate breakages:
- `20260624000000_add_inactivity_watchdog_fields` derived
`inactivity_deadline_at` by adding an INTERVAL to `last_ping`, but
`last_ping` is a BIGINT of Unix seconds (core tables migration, and the API
binds it as an i64), so the statement failed with "operator does not exist:
bigint + interval". Derive the deadline with `to_timestamp()` instead, and
drop the no-op attempt to re-add `last_ping` as a timestamp.
- `20260628000000` was used by two migrations (`add_kyc_records` and
`create_admins`); sqlx keys migrations by version and rejected the
duplicate with VersionMismatch. Renumber `create_admins` to
`20260628000001`, preserving the original order.
- `20260729000000_create_plans_beneficiaries_payouts` re-created `plans` and
`beneficiaries`, which the core tables migration already creates, failing
with "relation \"plans\" already exists". Only `payout_logs` was new, and
nothing in the codebase references the duplicate definitions' columns, so
keep just that table and point it at the existing `beneficiaries`.
Verified by applying the full chain to an empty PostgreSQL database through
`DbManager::run_migrations`.
* feat(backend): connect the inactivity watchdog to on-chain Soroban payout execution
Closes #1039
The inactivity watchdog only ever flipped an expired plan's status to
TRIGGERED in PostgreSQL and enqueued a `plan.triggered` webhook; the funds
were never unlocked on-chain. It now calls `trigger_inheritance` on the
Soroban inheritance contract and only records the plan as triggered once the
contract confirms it.
stellar_submit.rs — StellarSubmitClient learns to invoke Soroban contracts:
- `SorobanConfig::from_env()` reads SOROBAN_RPC_URL, INHERITANCE_CONTRACT_ID,
STELLAR_SIGNER_SECRET (plus optional passphrase and poll settings), and
`with_soroban()` validates the strkeys up front so a misconfigured signer
fails at startup rather than on the first expired plan.
- `invoke_contract()` builds an InvokeHostFunction transaction, simulates it
over JSON-RPC to pick up the footprint, resource fee and auth entries,
signs it for the configured network, submits it and polls `getTransaction`
until it lands or the timeout expires.
- `contract_events()` / `find_event()` / `event_u64_field()` decode the
emitted events out of TransactionMeta V3 and V4.
inactivity_watchdog.rs — the sweep is now two-phase:
- Expired plans are claimed into a new TRIGGERING status under the existing
advisory lock, then triggered on-chain outside the transaction. A plan only
becomes TRIGGERED after the contract's INHERIT/TRIGGER event is found and
its plan_id matches the one we asked for; anything else lands in
TRIGGER_FAILED with the reason recorded.
- Failed submissions are retried with exponential backoff
(INACTIVITY_WATCHDOG_ONCHAIN_MAX_ATTEMPTS / _BACKOFF_MS / _MAX_BACKOFF_MS).
Only transport and inclusion failures are retried — a contract-level
rejection will be rejected identically next time.
- Alerting: failures log at error level, increment the new
`inheritx_watchdog_onchain_triggers_total{outcome="failure"}` counter, and
enqueue a `plan.trigger_failed` webhook. `plan.triggered` now carries the
transaction hash.
- A plan left in TRIGGERING by a crashed worker is re-claimed once it goes
stale (INACTIVITY_WATCHDOG_TRIGGER_STALE_AFTER_SECS, default 15 minutes).
- With no Soroban configuration the watchdog keeps its previous behaviour and
says so loudly at startup.
Supporting changes:
- New migration adds `onchain_plan_id` (the contract's u64 plan id, unique
where present), plus `trigger_tx_hash`, `trigger_attempts`,
`trigger_started_at` and `last_trigger_error`.
- `POST /api/plans` accepts an optional `onchain_plan_id` and it is returned
on plan responses, so a plan can actually be linked to its contract plan.
- main.rs builds one Stellar client and shares it between the API and the
watchdog.
Tests: unit coverage for the signing payload, event decoding and matching,
config validation, retry classification and backoff; plus database-backed
sweep tests. CI's PostgreSQL service now has a health check and the tests get
WATCHDOG_TEST_DATABASE_URL so those run (deliberately not DATABASE_URL, which
other tests expect to be unreachable).
* fix(backend): only count a watchdog trigger as an on-chain success when one happened
`inheritx_watchdog_onchain_triggers_total{outcome="success"}` was also
incremented on the fallback path taken when no Soroban signer is configured,
where the watchdog never contacts the chain. Gate it on an actual transaction
hash so the series only reflects real submissions.1 parent 1b41650 commit 226cd45
17 files changed
Lines changed: 1935 additions & 91 deletions
File tree
- .github/workflows
- backend
- migrations
- src
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
31 | 36 | | |
32 | 37 | | |
33 | 38 | | |
| |||
52 | 57 | | |
53 | 58 | | |
54 | 59 | | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
55 | 66 | | |
56 | 67 | | |
57 | 68 | | |
Lines changed: 8 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
2 | 9 | | |
3 | 10 | | |
4 | | - | |
5 | | - | |
6 | 11 | | |
7 | 12 | | |
8 | 13 | | |
| |||
15 | 20 | | |
16 | 21 | | |
17 | 22 | | |
18 | | - | |
19 | | - | |
20 | | - | |
| 23 | + | |
21 | 24 | | |
22 | 25 | | |
23 | 26 | | |
| |||
File renamed without changes.
Lines changed: 2 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
2 | | - | |
3 | | - | |
| 1 | + | |
| 2 | + | |
Lines changed: 9 additions & 23 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
2 | | - | |
3 | | - | |
4 | | - | |
5 | | - | |
6 | | - | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
20 | 7 | | |
21 | | - | |
| 8 | + | |
22 | 9 | | |
23 | | - | |
| 10 | + | |
24 | 11 | | |
25 | 12 | | |
26 | 13 | | |
27 | 14 | | |
28 | 15 | | |
29 | | - | |
30 | | - | |
| 16 | + | |
Lines changed: 12 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
Lines changed: 29 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
56 | 60 | | |
57 | 61 | | |
58 | 62 | | |
| |||
312 | 316 | | |
313 | 317 | | |
314 | 318 | | |
| 319 | + | |
315 | 320 | | |
316 | 321 | | |
317 | 322 | | |
| |||
345 | 350 | | |
346 | 351 | | |
347 | 352 | | |
| 353 | + | |
348 | 354 | | |
349 | 355 | | |
350 | 356 | | |
| |||
432 | 438 | | |
433 | 439 | | |
434 | 440 | | |
| 441 | + | |
435 | 442 | | |
436 | 443 | | |
437 | 444 | | |
| |||
622 | 629 | | |
623 | 630 | | |
624 | 631 | | |
625 | | - | |
626 | | - | |
627 | | - | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
628 | 636 | | |
629 | 637 | | |
630 | 638 | | |
| |||
638 | 646 | | |
639 | 647 | | |
640 | 648 | | |
| 649 | + | |
641 | 650 | | |
642 | 651 | | |
643 | 652 | | |
| |||
715 | 724 | | |
716 | 725 | | |
717 | 726 | | |
| 727 | + | |
718 | 728 | | |
719 | 729 | | |
720 | 730 | | |
| |||
787 | 797 | | |
788 | 798 | | |
789 | 799 | | |
790 | | - | |
| 800 | + | |
791 | 801 | | |
792 | 802 | | |
793 | 803 | | |
| |||
903 | 913 | | |
904 | 914 | | |
905 | 915 | | |
906 | | - | |
| 916 | + | |
907 | 917 | | |
908 | 918 | | |
909 | 919 | | |
| |||
960 | 970 | | |
961 | 971 | | |
962 | 972 | | |
| 973 | + | |
963 | 974 | | |
964 | 975 | | |
965 | 976 | | |
| |||
1012 | 1023 | | |
1013 | 1024 | | |
1014 | 1025 | | |
1015 | | - | |
| 1026 | + | |
1016 | 1027 | | |
1017 | 1028 | | |
1018 | 1029 | | |
| |||
1040 | 1051 | | |
1041 | 1052 | | |
1042 | 1053 | | |
1043 | | - | |
| 1054 | + | |
1044 | 1055 | | |
1045 | 1056 | | |
1046 | 1057 | | |
| |||
1070 | 1081 | | |
1071 | 1082 | | |
1072 | 1083 | | |
1073 | | - | |
| 1084 | + | |
1074 | 1085 | | |
1075 | 1086 | | |
1076 | 1087 | | |
| |||
1100 | 1111 | | |
1101 | 1112 | | |
1102 | 1113 | | |
1103 | | - | |
| 1114 | + | |
1104 | 1115 | | |
1105 | 1116 | | |
1106 | 1117 | | |
| |||
1287 | 1298 | | |
1288 | 1299 | | |
1289 | 1300 | | |
1290 | | - | |
| 1301 | + | |
1291 | 1302 | | |
1292 | 1303 | | |
1293 | 1304 | | |
| |||
2020 | 2031 | | |
2021 | 2032 | | |
2022 | 2033 | | |
2023 | | - | |
| 2034 | + | |
2024 | 2035 | | |
2025 | 2036 | | |
2026 | 2037 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
319 | 319 | | |
320 | 320 | | |
321 | 321 | | |
| 322 | + | |
322 | 323 | | |
323 | 324 | | |
324 | 325 | | |
| |||
0 commit comments