sorokeep 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/CONTRIBUTING.md +296 -0
  2. package/LICENSE +21 -0
  3. package/README.md +677 -0
  4. package/dist/alerts/alerts.d.ts +21 -0
  5. package/dist/alerts/alerts.d.ts.map +1 -0
  6. package/dist/alerts/alerts.js +18 -0
  7. package/dist/alerts/alerts.js.map +1 -0
  8. package/dist/alerts/discord.d.ts +3 -0
  9. package/dist/alerts/discord.d.ts.map +1 -0
  10. package/dist/alerts/discord.js +199 -0
  11. package/dist/alerts/discord.js.map +1 -0
  12. package/dist/alerts/dispatcher.d.ts +13 -0
  13. package/dist/alerts/dispatcher.d.ts.map +1 -0
  14. package/dist/alerts/dispatcher.js +91 -0
  15. package/dist/alerts/dispatcher.js.map +1 -0
  16. package/dist/alerts/keys.d.ts +24 -0
  17. package/dist/alerts/keys.d.ts.map +1 -0
  18. package/dist/alerts/keys.js +44 -0
  19. package/dist/alerts/keys.js.map +1 -0
  20. package/dist/alerts/pagerduty.d.ts +8 -0
  21. package/dist/alerts/pagerduty.d.ts.map +1 -0
  22. package/dist/alerts/pagerduty.js +164 -0
  23. package/dist/alerts/pagerduty.js.map +1 -0
  24. package/dist/alerts/resource.d.ts +23 -0
  25. package/dist/alerts/resource.d.ts.map +1 -0
  26. package/dist/alerts/resource.js +167 -0
  27. package/dist/alerts/resource.js.map +1 -0
  28. package/dist/alerts/slack.d.ts +7 -0
  29. package/dist/alerts/slack.d.ts.map +1 -0
  30. package/dist/alerts/slack.js +184 -0
  31. package/dist/alerts/slack.js.map +1 -0
  32. package/dist/alerts/telegram.d.ts +9 -0
  33. package/dist/alerts/telegram.d.ts.map +1 -0
  34. package/dist/alerts/telegram.js +133 -0
  35. package/dist/alerts/telegram.js.map +1 -0
  36. package/dist/alerts/templates.d.ts +109 -0
  37. package/dist/alerts/templates.d.ts.map +1 -0
  38. package/dist/alerts/templates.js +139 -0
  39. package/dist/alerts/templates.js.map +1 -0
  40. package/dist/alerts/types.d.ts +113 -0
  41. package/dist/alerts/types.d.ts.map +1 -0
  42. package/dist/alerts/types.js +87 -0
  43. package/dist/alerts/types.js.map +1 -0
  44. package/dist/alerts/webhook.d.ts +13 -0
  45. package/dist/alerts/webhook.d.ts.map +1 -0
  46. package/dist/alerts/webhook.js +56 -0
  47. package/dist/alerts/webhook.js.map +1 -0
  48. package/dist/cli/schemaFormatter.d.ts +11 -0
  49. package/dist/cli/schemaFormatter.d.ts.map +1 -0
  50. package/dist/cli/schemaFormatter.js +74 -0
  51. package/dist/cli/schemaFormatter.js.map +1 -0
  52. package/dist/commands/alerts.d.ts +3 -0
  53. package/dist/commands/alerts.d.ts.map +1 -0
  54. package/dist/commands/alerts.js +256 -0
  55. package/dist/commands/alerts.js.map +1 -0
  56. package/dist/commands/budget.d.ts +3 -0
  57. package/dist/commands/budget.d.ts.map +1 -0
  58. package/dist/commands/budget.js +47 -0
  59. package/dist/commands/budget.js.map +1 -0
  60. package/dist/commands/channels.d.ts +3 -0
  61. package/dist/commands/channels.d.ts.map +1 -0
  62. package/dist/commands/channels.js +84 -0
  63. package/dist/commands/channels.js.map +1 -0
  64. package/dist/commands/check.d.ts +3 -0
  65. package/dist/commands/check.d.ts.map +1 -0
  66. package/dist/commands/check.js +58 -0
  67. package/dist/commands/check.js.map +1 -0
  68. package/dist/commands/completion.d.ts +3 -0
  69. package/dist/commands/completion.d.ts.map +1 -0
  70. package/dist/commands/completion.js +36 -0
  71. package/dist/commands/completion.js.map +1 -0
  72. package/dist/commands/costs.d.ts +15 -0
  73. package/dist/commands/costs.d.ts.map +1 -0
  74. package/dist/commands/costs.js +280 -0
  75. package/dist/commands/costs.js.map +1 -0
  76. package/dist/commands/daemon.d.ts +3 -0
  77. package/dist/commands/daemon.d.ts.map +1 -0
  78. package/dist/commands/daemon.js +93 -0
  79. package/dist/commands/daemon.js.map +1 -0
  80. package/dist/commands/db.d.ts +3 -0
  81. package/dist/commands/db.d.ts.map +1 -0
  82. package/dist/commands/db.js +140 -0
  83. package/dist/commands/db.js.map +1 -0
  84. package/dist/commands/guard.d.ts +3 -0
  85. package/dist/commands/guard.d.ts.map +1 -0
  86. package/dist/commands/guard.js +181 -0
  87. package/dist/commands/guard.js.map +1 -0
  88. package/dist/commands/history.d.ts +3 -0
  89. package/dist/commands/history.d.ts.map +1 -0
  90. package/dist/commands/history.js +57 -0
  91. package/dist/commands/history.js.map +1 -0
  92. package/dist/commands/inspect.d.ts +3 -0
  93. package/dist/commands/inspect.d.ts.map +1 -0
  94. package/dist/commands/inspect.js +82 -0
  95. package/dist/commands/inspect.js.map +1 -0
  96. package/dist/commands/mcp.d.ts +3 -0
  97. package/dist/commands/mcp.d.ts.map +1 -0
  98. package/dist/commands/mcp.js +20 -0
  99. package/dist/commands/mcp.js.map +1 -0
  100. package/dist/commands/pause.d.ts +3 -0
  101. package/dist/commands/pause.d.ts.map +1 -0
  102. package/dist/commands/pause.js +32 -0
  103. package/dist/commands/pause.js.map +1 -0
  104. package/dist/commands/resources.d.ts +3 -0
  105. package/dist/commands/resources.d.ts.map +1 -0
  106. package/dist/commands/resources.js +95 -0
  107. package/dist/commands/resources.js.map +1 -0
  108. package/dist/commands/restore.d.ts +3 -0
  109. package/dist/commands/restore.d.ts.map +1 -0
  110. package/dist/commands/restore.js +92 -0
  111. package/dist/commands/restore.js.map +1 -0
  112. package/dist/commands/resume.d.ts +3 -0
  113. package/dist/commands/resume.d.ts.map +1 -0
  114. package/dist/commands/resume.js +31 -0
  115. package/dist/commands/resume.js.map +1 -0
  116. package/dist/commands/status.d.ts +3 -0
  117. package/dist/commands/status.d.ts.map +1 -0
  118. package/dist/commands/status.js +63 -0
  119. package/dist/commands/status.js.map +1 -0
  120. package/dist/commands/watch.d.ts +3 -0
  121. package/dist/commands/watch.d.ts.map +1 -0
  122. package/dist/commands/watch.js +163 -0
  123. package/dist/commands/watch.js.map +1 -0
  124. package/dist/core/aws_secrets.d.ts +13 -0
  125. package/dist/core/aws_secrets.d.ts.map +1 -0
  126. package/dist/core/aws_secrets.js +34 -0
  127. package/dist/core/aws_secrets.js.map +1 -0
  128. package/dist/core/budget.d.ts +10 -0
  129. package/dist/core/budget.d.ts.map +1 -0
  130. package/dist/core/budget.js +31 -0
  131. package/dist/core/budget.js.map +1 -0
  132. package/dist/core/channels.d.ts +67 -0
  133. package/dist/core/channels.d.ts.map +1 -0
  134. package/dist/core/channels.js +136 -0
  135. package/dist/core/channels.js.map +1 -0
  136. package/dist/core/check.d.ts +25 -0
  137. package/dist/core/check.d.ts.map +1 -0
  138. package/dist/core/check.js +85 -0
  139. package/dist/core/check.js.map +1 -0
  140. package/dist/core/completion.d.ts +9 -0
  141. package/dist/core/completion.d.ts.map +1 -0
  142. package/dist/core/completion.js +68 -0
  143. package/dist/core/completion.js.map +1 -0
  144. package/dist/core/costs.d.ts +90 -0
  145. package/dist/core/costs.d.ts.map +1 -0
  146. package/dist/core/costs.js +147 -0
  147. package/dist/core/costs.js.map +1 -0
  148. package/dist/core/decoder.d.ts +7 -0
  149. package/dist/core/decoder.d.ts.map +1 -0
  150. package/dist/core/decoder.js +38 -0
  151. package/dist/core/decoder.js.map +1 -0
  152. package/dist/core/discovery.d.ts +38 -0
  153. package/dist/core/discovery.d.ts.map +1 -0
  154. package/dist/core/discovery.js +208 -0
  155. package/dist/core/discovery.js.map +1 -0
  156. package/dist/core/extension.d.ts +83 -0
  157. package/dist/core/extension.d.ts.map +1 -0
  158. package/dist/core/extension.js +429 -0
  159. package/dist/core/extension.js.map +1 -0
  160. package/dist/core/inspect.d.ts +62 -0
  161. package/dist/core/inspect.d.ts.map +1 -0
  162. package/dist/core/inspect.js +303 -0
  163. package/dist/core/inspect.js.map +1 -0
  164. package/dist/core/introspection.d.ts +8 -0
  165. package/dist/core/introspection.d.ts.map +1 -0
  166. package/dist/core/introspection.js +8 -0
  167. package/dist/core/introspection.js.map +1 -0
  168. package/dist/core/monitor.d.ts +42 -0
  169. package/dist/core/monitor.d.ts.map +1 -0
  170. package/dist/core/monitor.js +188 -0
  171. package/dist/core/monitor.js.map +1 -0
  172. package/dist/core/rent_projection.d.ts +153 -0
  173. package/dist/core/rent_projection.d.ts.map +1 -0
  174. package/dist/core/rent_projection.js +146 -0
  175. package/dist/core/rent_projection.js.map +1 -0
  176. package/dist/core/scvalTranslator.d.ts +11 -0
  177. package/dist/core/scvalTranslator.d.ts.map +1 -0
  178. package/dist/core/scvalTranslator.js +36 -0
  179. package/dist/core/scvalTranslator.js.map +1 -0
  180. package/dist/core/state_diff.d.ts +45 -0
  181. package/dist/core/state_diff.d.ts.map +1 -0
  182. package/dist/core/state_diff.js +112 -0
  183. package/dist/core/state_diff.js.map +1 -0
  184. package/dist/core/status.d.ts +24 -0
  185. package/dist/core/status.d.ts.map +1 -0
  186. package/dist/core/status.js +57 -0
  187. package/dist/core/status.js.map +1 -0
  188. package/dist/core/vault.d.ts +37 -0
  189. package/dist/core/vault.d.ts.map +1 -0
  190. package/dist/core/vault.js +179 -0
  191. package/dist/core/vault.js.map +1 -0
  192. package/dist/core/watch.d.ts +36 -0
  193. package/dist/core/watch.d.ts.map +1 -0
  194. package/dist/core/watch.js +176 -0
  195. package/dist/core/watch.js.map +1 -0
  196. package/dist/daemon/loop.d.ts +38 -0
  197. package/dist/daemon/loop.d.ts.map +1 -0
  198. package/dist/daemon/loop.js +169 -0
  199. package/dist/daemon/loop.js.map +1 -0
  200. package/dist/db/backup.d.ts +12 -0
  201. package/dist/db/backup.d.ts.map +1 -0
  202. package/dist/db/backup.js +132 -0
  203. package/dist/db/backup.js.map +1 -0
  204. package/dist/db/budget.d.ts +17 -0
  205. package/dist/db/budget.d.ts.map +1 -0
  206. package/dist/db/budget.js +31 -0
  207. package/dist/db/budget.js.map +1 -0
  208. package/dist/db/database.d.ts +6 -0
  209. package/dist/db/database.d.ts.map +1 -0
  210. package/dist/db/database.js +134 -0
  211. package/dist/db/database.js.map +1 -0
  212. package/dist/db/migrations/.gitkeep +1 -0
  213. package/dist/db/migrations/001_resource_usage_logs.sql +28 -0
  214. package/dist/db/migrator.d.ts +28 -0
  215. package/dist/db/migrator.d.ts.map +1 -0
  216. package/dist/db/migrator.js +75 -0
  217. package/dist/db/migrator.js.map +1 -0
  218. package/dist/db/repositories.d.ts +474 -0
  219. package/dist/db/repositories.d.ts.map +1 -0
  220. package/dist/db/repositories.js +809 -0
  221. package/dist/db/repositories.js.map +1 -0
  222. package/dist/db/schema.sql +207 -0
  223. package/dist/index.d.ts +3 -0
  224. package/dist/index.d.ts.map +1 -0
  225. package/dist/index.js +47 -0
  226. package/dist/index.js.map +1 -0
  227. package/dist/lib.d.ts +20 -0
  228. package/dist/lib.d.ts.map +1 -0
  229. package/dist/lib.js +16 -0
  230. package/dist/lib.js.map +1 -0
  231. package/dist/logging/index.d.ts +22 -0
  232. package/dist/logging/index.d.ts.map +1 -0
  233. package/dist/logging/index.js +34 -0
  234. package/dist/logging/index.js.map +1 -0
  235. package/dist/logging/logger.d.ts +14 -0
  236. package/dist/logging/logger.d.ts.map +1 -0
  237. package/dist/logging/logger.js +135 -0
  238. package/dist/logging/logger.js.map +1 -0
  239. package/dist/logging/types.d.ts +10 -0
  240. package/dist/logging/types.d.ts.map +1 -0
  241. package/dist/logging/types.js +2 -0
  242. package/dist/logging/types.js.map +1 -0
  243. package/dist/mcp/index.d.ts +3 -0
  244. package/dist/mcp/index.d.ts.map +1 -0
  245. package/dist/mcp/index.js +14 -0
  246. package/dist/mcp/index.js.map +1 -0
  247. package/dist/mcp/server.d.ts +10 -0
  248. package/dist/mcp/server.d.ts.map +1 -0
  249. package/dist/mcp/server.js +51 -0
  250. package/dist/mcp/server.js.map +1 -0
  251. package/dist/mcp/tools/get-extension-costs.d.ts +4 -0
  252. package/dist/mcp/tools/get-extension-costs.d.ts.map +1 -0
  253. package/dist/mcp/tools/get-extension-costs.js +33 -0
  254. package/dist/mcp/tools/get-extension-costs.js.map +1 -0
  255. package/dist/mcp/tools/get_contract_status.d.ts +8 -0
  256. package/dist/mcp/tools/get_contract_status.d.ts.map +1 -0
  257. package/dist/mcp/tools/get_contract_status.js +33 -0
  258. package/dist/mcp/tools/get_contract_status.js.map +1 -0
  259. package/dist/rpc/client.d.ts +154 -0
  260. package/dist/rpc/client.d.ts.map +1 -0
  261. package/dist/rpc/client.js +766 -0
  262. package/dist/rpc/client.js.map +1 -0
  263. package/dist/utils/config.d.ts +51 -0
  264. package/dist/utils/config.d.ts.map +1 -0
  265. package/dist/utils/config.js +86 -0
  266. package/dist/utils/config.js.map +1 -0
  267. package/dist/utils/formatting.d.ts +10 -0
  268. package/dist/utils/formatting.d.ts.map +1 -0
  269. package/dist/utils/formatting.js +66 -0
  270. package/dist/utils/formatting.js.map +1 -0
  271. package/dist/utils/watch-config.d.ts +13 -0
  272. package/dist/utils/watch-config.d.ts.map +1 -0
  273. package/dist/utils/watch-config.js +32 -0
  274. package/dist/utils/watch-config.js.map +1 -0
  275. package/package.json +73 -0
@@ -0,0 +1,809 @@
1
+ export { upsertBudget, getBudget, addBudgetSpent } from "./budget.js";
2
+ // ---------------------------- Database Access Functions For Schema: Contract ----------------------------
3
+ export function insertContract(db, contract) {
4
+ db.prepare(`
5
+ INSERT INTO contracts (id, name, network, wasm_hash, tags, poll_interval_seconds, active)
6
+ VALUES (@id, @name, @network, @wasm_hash, @tags, @poll_interval_seconds, @active)
7
+ ON CONFLICT(id) DO UPDATE SET
8
+ name = excluded.name,
9
+ network = excluded.network,
10
+ wasm_hash = excluded.wasm_hash,
11
+ tags = excluded.tags,
12
+ poll_interval_seconds = COALESCE(excluded.poll_interval_seconds, contracts.poll_interval_seconds),
13
+ active = COALESCE(excluded.active, contracts.active)
14
+ `).run({
15
+ id: contract.id,
16
+ name: contract.name ?? null,
17
+ network: contract.network,
18
+ wasm_hash: contract.wasm_hash ?? null,
19
+ tags: contract.tags ?? null,
20
+ poll_interval_seconds: contract.poll_interval_seconds ?? null,
21
+ active: contract.active ?? 1,
22
+ });
23
+ }
24
+ export function getContract(db, id) {
25
+ return db.prepare("SELECT * FROM contracts WHERE id = ?").get(id);
26
+ }
27
+ export function getAllContracts(db) {
28
+ return db.prepare("SELECT * FROM contracts").all();
29
+ }
30
+ export function updateLastCheckedLedger(db, contractId, ledger) {
31
+ db.prepare("UPDATE contracts SET last_checked_ledger = ? WHERE id = ?").run(ledger, contractId);
32
+ }
33
+ export function deleteContract(db, id) {
34
+ db.prepare("DELETE FROM contracts WHERE id = ?").run(id);
35
+ }
36
+ export function updateContractPollInterval(db, contractId, pollIntervalSeconds) {
37
+ db.prepare("UPDATE contracts SET poll_interval_seconds = ? WHERE id = ?").run(pollIntervalSeconds, contractId);
38
+ }
39
+ export function getContractPollInterval(db, contractId) {
40
+ const row = db.prepare("SELECT poll_interval_seconds FROM contracts WHERE id = ?").get(contractId);
41
+ return row?.poll_interval_seconds ?? null;
42
+ }
43
+ export function setContractActiveStatus(db, contractId, active) {
44
+ db.prepare("UPDATE contracts SET active = ? WHERE id = ?").run(active ? 1 : 0, contractId);
45
+ }
46
+ /**
47
+ * Record that a successful contract introspection (instance/WASM key discovery)
48
+ * was performed at the given timestamp. Accepts an ISO-8601 string so callers
49
+ * can control the clock in tests.
50
+ */
51
+ export function updateLastIntrospectedAt(db, contractId, isoTimestamp) {
52
+ db.prepare("UPDATE contracts SET last_introspected_at = ? WHERE id = ?").run(isoTimestamp, contractId);
53
+ }
54
+ /**
55
+ * Return true when the introspection cache for the given contract is still
56
+ * valid — i.e. `last_introspected_at` is not NULL and the timestamp is
57
+ * strictly less than `maxAgeMs` milliseconds ago.
58
+ *
59
+ * The default max-age is 24 hours (86 400 000 ms).
60
+ * The boundary is *exclusive on the valid side*: exactly 24 h ago is expired.
61
+ */
62
+ export function isIntrospectionCacheValid(db, contractId, maxAgeMs = 24 * 60 * 60 * 1_000) {
63
+ const row = db
64
+ .prepare("SELECT last_introspected_at FROM contracts WHERE id = ?")
65
+ .get(contractId);
66
+ if (!row || row.last_introspected_at === null)
67
+ return false;
68
+ const introspectedAt = new Date(row.last_introspected_at).getTime();
69
+ const ageMs = Date.now() - introspectedAt;
70
+ // strictly less than → at exactly 24 h the cache is expired
71
+ return ageMs < maxAgeMs;
72
+ }
73
+ // ---------------------------- Database Access Functions For Schema: ContractEntry ----------------------------
74
+ export function upsertEntry(db, entry) {
75
+ db.prepare(`
76
+ INSERT INTO contract_entries (contract_id, entry_key_xdr, entry_type, label, live_until_ledger, last_modified_ledger, discovery_source, last_checked_at)
77
+ VALUES (@contract_id, @entry_key_xdr, @entry_type, @label, @live_until_ledger, @last_modified_ledger, @discovery_source, datetime('now'))
78
+ ON CONFLICT(contract_id, entry_key_xdr) DO UPDATE SET
79
+ live_until_ledger = @live_until_ledger,
80
+ last_modified_ledger = @last_modified_ledger,
81
+ last_checked_at = datetime('now')
82
+ `).run({
83
+ contract_id: entry.contract_id,
84
+ entry_key_xdr: entry.entry_key_xdr,
85
+ entry_type: entry.entry_type,
86
+ label: entry.label ?? null,
87
+ live_until_ledger: entry.live_until_ledger ?? null,
88
+ last_modified_ledger: entry.last_modified_ledger ?? null,
89
+ discovery_source: entry.discovery_source ?? "deterministic",
90
+ });
91
+ }
92
+ export function getEntriesForContract(db, contractId) {
93
+ return db.prepare("SELECT * FROM contract_entries WHERE contract_id = ?").all(contractId);
94
+ }
95
+ // ---------------------------- Database Access Functions For Other Schema: ExtensionPolicy----------------------------
96
+ export function upsertExtensionPolicy(db, policy) {
97
+ db.prepare(`
98
+ INSERT INTO extension_policies (contract_id, enabled, target_ttl_ledgers, extend_when_below_ledgers, keypair_public, keypair_source)
99
+ VALUES (@contract_id, @enabled, @target_ttl_ledgers, @extend_when_below_ledgers, @keypair_public, @keypair_source)
100
+ ON CONFLICT(contract_id) DO UPDATE SET
101
+ enabled = @enabled,
102
+ target_ttl_ledgers = @target_ttl_ledgers,
103
+ extend_when_below_ledgers = @extend_when_below_ledgers,
104
+ keypair_public = @keypair_public,
105
+ keypair_source = @keypair_source
106
+ `).run({
107
+ contract_id: policy.contract_id,
108
+ enabled: policy.enabled !== false ? 1 : 0,
109
+ target_ttl_ledgers: policy.target_ttl_ledgers,
110
+ extend_when_below_ledgers: policy.extend_when_below_ledgers,
111
+ keypair_public: policy.keypair_public ?? null,
112
+ keypair_source: policy.keypair_source ?? null,
113
+ });
114
+ }
115
+ export function getExtensionPolicy(db, contractId) {
116
+ return db.prepare("SELECT * FROM extension_policies WHERE contract_id = ?").get(contractId);
117
+ }
118
+ // ---------------------------- Database Access Functions For Other Schema: AlertConfig----------------------------
119
+ export function insertAlertConfig(db, config) {
120
+ db.prepare(`
121
+ INSERT INTO alert_configs (contract_id, channel_type, channel_target, threshold_ledgers, webhook_secret)
122
+ VALUES (@contract_id, @channel_type, @channel_target, @threshold_ledgers, @webhook_secret)
123
+ `).run({
124
+ ...config,
125
+ webhook_secret: config.webhook_secret ?? null,
126
+ });
127
+ }
128
+ export function getAlertConfigById(db, id) {
129
+ return db.prepare("SELECT * FROM alert_configs WHERE id = ?").get(id);
130
+ }
131
+ export function getAlertConfigsForContract(db, contractId) {
132
+ return db.prepare("SELECT * FROM alert_configs WHERE contract_id = ?").all(contractId);
133
+ }
134
+ export function deleteAlertConfig(db, id) {
135
+ db.prepare("DELETE FROM alert_configs WHERE id = ?").run(id);
136
+ }
137
+ // ---------------------------- Database Access Functions For Other Schema: AlertFired----------------------------
138
+ export function recordAlertFired(db, alert) {
139
+ db.prepare(`
140
+ INSERT INTO alerts_fired (alert_config_id, contract_entry_id, fired_at_ledger, ttl_at_fire)
141
+ VALUES (@alert_config_id, @contract_entry_id, @fired_at_ledger, @ttl_at_fire)
142
+ `).run(alert);
143
+ }
144
+ export function hasUnresolvedAlert(db, alertConfigId, entryId) {
145
+ const row = db.prepare(`
146
+ SELECT 1 FROM alerts_fired
147
+ WHERE alert_config_id = ? AND contract_entry_id = ? AND resolved = 0
148
+ LIMIT 1
149
+ `).get(alertConfigId, entryId);
150
+ return row !== undefined;
151
+ }
152
+ export function resolveAlerts(db, entryId, alertConfigId) {
153
+ const rows = db.prepare(`
154
+ SELECT alert_config_id FROM alerts_fired
155
+ WHERE contract_entry_id = ? AND alert_config_id = ? AND resolved = 0
156
+ `).all(entryId, alertConfigId);
157
+ if (rows.length > 0) {
158
+ db.prepare(`
159
+ UPDATE alerts_fired SET resolved = 1, resolved_at = datetime('now')
160
+ WHERE contract_entry_id = ? AND alert_config_id = ? AND resolved = 0
161
+ `).run(entryId, alertConfigId);
162
+ }
163
+ return rows.map(r => r.alert_config_id);
164
+ }
165
+ // ---------------------------- Database Access Functions For Other Schema: ExtensionRecord----------------------------
166
+ export function recordExtension(db, record) {
167
+ db.prepare(`
168
+ INSERT INTO extension_history (contract_id, contract_entry_id, old_ttl_ledgers, new_ttl_ledgers, tx_hash, cost_xlm, cpu_insns, mem_bytes, is_anomaly, executed_at_ledger)
169
+ VALUES (@contract_id, @contract_entry_id, @old_ttl_ledgers, @new_ttl_ledgers, @tx_hash, @cost_xlm, @cpu_insns, @mem_bytes, @is_anomaly, @executed_at_ledger)
170
+ `).run({
171
+ ...record,
172
+ cost_xlm: record.cost_xlm ?? null,
173
+ cpu_insns: record.cpu_insns ?? null,
174
+ mem_bytes: record.mem_bytes ?? null,
175
+ is_anomaly: record.is_anomaly ? 1 : 0,
176
+ });
177
+ }
178
+ export function getExtensionHistory(db, contractId, days) {
179
+ if (days) {
180
+ return db.prepare(`
181
+ SELECT * FROM extension_history
182
+ WHERE contract_id = ? AND executed_at >= datetime('now', ?)
183
+ ORDER BY executed_at DESC
184
+ `).all(contractId, `-${days} days`);
185
+ }
186
+ return db.prepare(`
187
+ SELECT * FROM extension_history WHERE contract_id = ? ORDER BY executed_at DESC
188
+ `).all(contractId);
189
+ }
190
+ export function aggregateDailyCostSnapshots(db) {
191
+ const rows = db.prepare(`
192
+ SELECT
193
+ eh.contract_id AS contract_id,
194
+ date(eh.executed_at) AS snapshot_date,
195
+ COUNT(*) AS total_extensions,
196
+ SUM(COALESCE(eh.cost_xlm, 0.0)) AS total_cost_xlm,
197
+ SUM(CASE WHEN ce.entry_type = 'instance' THEN 1 ELSE 0 END) AS instance_extensions,
198
+ SUM(CASE WHEN ce.entry_type = 'instance' THEN COALESCE(eh.cost_xlm, 0.0) ELSE 0 END) AS instance_cost_xlm,
199
+ SUM(CASE WHEN ce.entry_type = 'wasm' THEN 1 ELSE 0 END) AS wasm_extensions,
200
+ SUM(CASE WHEN ce.entry_type = 'wasm' THEN COALESCE(eh.cost_xlm, 0.0) ELSE 0 END) AS wasm_cost_xlm,
201
+ SUM(CASE WHEN ce.entry_type = 'persistent' THEN 1 ELSE 0 END) AS persistent_extensions,
202
+ SUM(CASE WHEN ce.entry_type = 'persistent' THEN COALESCE(eh.cost_xlm, 0.0) ELSE 0 END) AS persistent_cost_xlm,
203
+ SUM(CASE WHEN ce.entry_type = 'temporary' THEN 1 ELSE 0 END) AS temporary_extensions,
204
+ SUM(CASE WHEN ce.entry_type = 'temporary' THEN COALESCE(eh.cost_xlm, 0.0) ELSE 0 END) AS temporary_cost_xlm
205
+ FROM extension_history eh
206
+ JOIN contract_entries ce ON ce.id = eh.contract_entry_id
207
+ WHERE date(eh.executed_at) < date('now')
208
+ AND date(eh.executed_at) >= date('now', '-7 days')
209
+ GROUP BY eh.contract_id, date(eh.executed_at)
210
+ `).all();
211
+ const upsert = db.prepare(`
212
+ INSERT INTO cost_daily_snapshots (
213
+ contract_id, snapshot_date,
214
+ total_extensions, total_cost_xlm,
215
+ instance_extensions, instance_cost_xlm,
216
+ wasm_extensions, wasm_cost_xlm,
217
+ persistent_extensions, persistent_cost_xlm,
218
+ temporary_extensions, temporary_cost_xlm
219
+ ) VALUES (
220
+ @contract_id, @snapshot_date,
221
+ @total_extensions, @total_cost_xlm,
222
+ @instance_extensions, @instance_cost_xlm,
223
+ @wasm_extensions, @wasm_cost_xlm,
224
+ @persistent_extensions, @persistent_cost_xlm,
225
+ @temporary_extensions, @temporary_cost_xlm
226
+ )
227
+ ON CONFLICT(contract_id, snapshot_date) DO UPDATE SET
228
+ total_extensions = excluded.total_extensions,
229
+ total_cost_xlm = excluded.total_cost_xlm,
230
+ instance_extensions = excluded.instance_extensions,
231
+ instance_cost_xlm = excluded.instance_cost_xlm,
232
+ wasm_extensions = excluded.wasm_extensions,
233
+ wasm_cost_xlm = excluded.wasm_cost_xlm,
234
+ persistent_extensions = excluded.persistent_extensions,
235
+ persistent_cost_xlm = excluded.persistent_cost_xlm,
236
+ temporary_extensions = excluded.temporary_extensions,
237
+ temporary_cost_xlm = excluded.temporary_cost_xlm
238
+ `);
239
+ const transaction = db.transaction((snapshotRows) => {
240
+ for (const row of snapshotRows) {
241
+ upsert.run(row);
242
+ }
243
+ });
244
+ transaction(rows);
245
+ }
246
+ export function getCostDailySnapshots(db, contractId, days) {
247
+ if (days) {
248
+ return db.prepare(`
249
+ SELECT * FROM cost_daily_snapshots
250
+ WHERE contract_id = ? AND snapshot_date >= date('now', ?)
251
+ ORDER BY snapshot_date DESC
252
+ `).all(contractId, `-${Math.max(days - 1, 0)} days`);
253
+ }
254
+ return db.prepare(`
255
+ SELECT * FROM cost_daily_snapshots
256
+ WHERE contract_id = ?
257
+ ORDER BY snapshot_date DESC
258
+ `).all(contractId);
259
+ }
260
+ export function getContractCostSummary(db, contractId, days) {
261
+ const snapshotParams = days ? [`-${Math.max(days - 1, 0)} days`] : [];
262
+ const snapshotRow = days
263
+ ? db.prepare(`
264
+ SELECT
265
+ COALESCE(SUM(total_extensions), 0) AS total_extensions,
266
+ COALESCE(SUM(total_cost_xlm), 0.0) AS total_cost_xlm,
267
+ COALESCE(SUM(instance_extensions), 0) AS instance_extensions,
268
+ COALESCE(SUM(instance_cost_xlm), 0.0) AS instance_cost_xlm,
269
+ COALESCE(SUM(wasm_extensions), 0) AS wasm_extensions,
270
+ COALESCE(SUM(wasm_cost_xlm), 0.0) AS wasm_cost_xlm,
271
+ COALESCE(SUM(persistent_extensions), 0) AS persistent_extensions,
272
+ COALESCE(SUM(persistent_cost_xlm), 0.0) AS persistent_cost_xlm,
273
+ COALESCE(SUM(temporary_extensions), 0) AS temporary_extensions,
274
+ COALESCE(SUM(temporary_cost_xlm), 0.0) AS temporary_cost_xlm
275
+ FROM cost_daily_snapshots
276
+ WHERE contract_id = ? AND snapshot_date >= date('now', ?)
277
+ `).get(contractId, ...snapshotParams)
278
+ : db.prepare(`
279
+ SELECT
280
+ COALESCE(SUM(total_extensions), 0) AS total_extensions,
281
+ COALESCE(SUM(total_cost_xlm), 0.0) AS total_cost_xlm,
282
+ COALESCE(SUM(instance_extensions), 0) AS instance_extensions,
283
+ COALESCE(SUM(instance_cost_xlm), 0.0) AS instance_cost_xlm,
284
+ COALESCE(SUM(wasm_extensions), 0) AS wasm_extensions,
285
+ COALESCE(SUM(wasm_cost_xlm), 0.0) AS wasm_cost_xlm,
286
+ COALESCE(SUM(persistent_extensions), 0) AS persistent_extensions,
287
+ COALESCE(SUM(persistent_cost_xlm), 0.0) AS persistent_cost_xlm,
288
+ COALESCE(SUM(temporary_extensions), 0) AS temporary_extensions,
289
+ COALESCE(SUM(temporary_cost_xlm), 0.0) AS temporary_cost_xlm
290
+ FROM cost_daily_snapshots
291
+ WHERE contract_id = ?
292
+ `).get(contractId);
293
+ const currentDayRow = db.prepare(`
294
+ SELECT
295
+ COUNT(*) AS total_extensions,
296
+ COALESCE(SUM(COALESCE(eh.cost_xlm, 0.0)), 0.0) AS total_cost_xlm,
297
+ COALESCE(SUM(CASE WHEN ce.entry_type = 'instance' THEN 1 ELSE 0 END), 0) AS instance_extensions,
298
+ COALESCE(SUM(CASE WHEN ce.entry_type = 'instance' THEN COALESCE(eh.cost_xlm, 0.0) ELSE 0 END), 0.0) AS instance_cost_xlm,
299
+ COALESCE(SUM(CASE WHEN ce.entry_type = 'wasm' THEN 1 ELSE 0 END), 0) AS wasm_extensions,
300
+ COALESCE(SUM(CASE WHEN ce.entry_type = 'wasm' THEN COALESCE(eh.cost_xlm, 0.0) ELSE 0 END), 0.0) AS wasm_cost_xlm,
301
+ COALESCE(SUM(CASE WHEN ce.entry_type = 'persistent' THEN 1 ELSE 0 END), 0) AS persistent_extensions,
302
+ COALESCE(SUM(CASE WHEN ce.entry_type = 'persistent' THEN COALESCE(eh.cost_xlm, 0.0) ELSE 0 END), 0.0) AS persistent_cost_xlm,
303
+ COALESCE(SUM(CASE WHEN ce.entry_type = 'temporary' THEN 1 ELSE 0 END), 0) AS temporary_extensions,
304
+ COALESCE(SUM(CASE WHEN ce.entry_type = 'temporary' THEN COALESCE(eh.cost_xlm, 0.0) ELSE 0 END), 0.0) AS temporary_cost_xlm
305
+ FROM extension_history eh
306
+ JOIN contract_entries ce ON ce.id = eh.contract_entry_id
307
+ WHERE eh.contract_id = ?
308
+ AND date(eh.executed_at) = date('now')
309
+ `).get(contractId);
310
+ return {
311
+ contract_id: contractId,
312
+ total_extensions: Number((snapshotRow.total_extensions ?? 0) + (currentDayRow.total_extensions ?? 0)),
313
+ total_cost_xlm: Number((snapshotRow.total_cost_xlm ?? 0) + (currentDayRow.total_cost_xlm ?? 0)),
314
+ byType: {
315
+ instance: {
316
+ count: Number((snapshotRow.instance_extensions ?? 0) + (currentDayRow.instance_extensions ?? 0)),
317
+ cost_xlm: Number((snapshotRow.instance_cost_xlm ?? 0) + (currentDayRow.instance_cost_xlm ?? 0)),
318
+ },
319
+ wasm: {
320
+ count: Number((snapshotRow.wasm_extensions ?? 0) + (currentDayRow.wasm_extensions ?? 0)),
321
+ cost_xlm: Number((snapshotRow.wasm_cost_xlm ?? 0) + (currentDayRow.wasm_cost_xlm ?? 0)),
322
+ },
323
+ persistent: {
324
+ count: Number((snapshotRow.persistent_extensions ?? 0) + (currentDayRow.persistent_extensions ?? 0)),
325
+ cost_xlm: Number((snapshotRow.persistent_cost_xlm ?? 0) + (currentDayRow.persistent_cost_xlm ?? 0)),
326
+ },
327
+ temporary: {
328
+ count: Number((snapshotRow.temporary_extensions ?? 0) + (currentDayRow.temporary_extensions ?? 0)),
329
+ cost_xlm: Number((snapshotRow.temporary_cost_xlm ?? 0) + (currentDayRow.temporary_cost_xlm ?? 0)),
330
+ },
331
+ },
332
+ };
333
+ }
334
+ /**
335
+ * Count the number of auto-extension transactions that were executed for the
336
+ * given contract within the last hour.
337
+ *
338
+ * Used by the rate limiter to enforce a maximum of N extensions per hour
339
+ * per contract (issue #142).
340
+ *
341
+ * @param db - The SQLite database connection.
342
+ * @param contractId - The contract to check.
343
+ * @returns The number of extensions recorded in the last 60 minutes.
344
+ */
345
+ export function countExtensionsInLastHour(db, contractId) {
346
+ const row = db.prepare(`
347
+ SELECT COUNT(*) AS cnt
348
+ FROM extension_history
349
+ WHERE contract_id = ?
350
+ AND datetime(executed_at) >= datetime('now', '-1 hour')
351
+ `).get(contractId);
352
+ return row?.cnt ?? 0;
353
+ }
354
+ export function getAverageResourceUsage(db, contractId, limit) {
355
+ const params = [contractId];
356
+ let query = `
357
+ SELECT cpu_insns, mem_bytes
358
+ FROM extension_history
359
+ WHERE contract_id = ? AND cpu_insns IS NOT NULL AND mem_bytes IS NOT NULL
360
+ ORDER BY executed_at DESC, id DESC
361
+ `;
362
+ if (limit !== undefined) {
363
+ query += ` LIMIT ?`;
364
+ params.push(limit);
365
+ }
366
+ const rows = db.prepare(query).all(...params);
367
+ if (rows.length === 0)
368
+ return null;
369
+ const sumCpu = rows.reduce((acc, row) => acc + row.cpu_insns, 0);
370
+ const sumMem = rows.reduce((acc, row) => acc + row.mem_bytes, 0);
371
+ return {
372
+ avg_cpu_insns: sumCpu / rows.length,
373
+ avg_mem_bytes: sumMem / rows.length,
374
+ count: rows.length
375
+ };
376
+ }
377
+ /** Maximum number of delivery attempts before giving up on an alert. */
378
+ export const MAX_RETRY_COUNT = 5;
379
+ /**
380
+ * Return all undelivered (delivered = 0) alerts for the given network,
381
+ * joining alerts_fired → alert_configs → contract_entries → contracts.
382
+ * Alerts that have exceeded MAX_RETRY_COUNT are excluded.
383
+ */
384
+ export function getUndeliveredAlerts(db, network) {
385
+ const rows = db.prepare(`
386
+ SELECT
387
+ af.id AS alertFiredId,
388
+ ac.id AS alertConfigId,
389
+ c.id AS contractId,
390
+ c.name AS contractName,
391
+ c.network AS network,
392
+ ce.id AS entryId,
393
+ ce.entry_key_xdr AS entryKeyXdr,
394
+ ce.entry_type AS entryType,
395
+ ce.label AS entryLabel,
396
+ ac.channel_type AS channelType,
397
+ ac.channel_target AS channelTarget,
398
+ ac.threshold_ledgers AS thresholdLedgers,
399
+ ac.webhook_secret AS webhookSecret,
400
+ af.ttl_at_fire AS remainingTTL,
401
+ af.fired_at_ledger AS firedAtLedger,
402
+ af.fired_at AS firedAt,
403
+ af.retry_count AS retryCount
404
+ FROM alerts_fired af
405
+ JOIN alert_configs ac ON ac.id = af.alert_config_id
406
+ JOIN contract_entries ce ON ce.id = af.contract_entry_id
407
+ JOIN contracts c ON c.id = ce.contract_id
408
+ WHERE af.delivered = 0
409
+ AND af.retry_count < ?
410
+ AND c.network = ?
411
+ ORDER BY af.fired_at ASC
412
+ `).all(MAX_RETRY_COUNT, network);
413
+ return rows;
414
+ }
415
+ /**
416
+ * Mark a single alerts_fired record as delivered.
417
+ * Idempotent — safe to call more than once.
418
+ */
419
+ export function markAlertDelivered(db, alertFiredId) {
420
+ db.prepare(`
421
+ UPDATE alerts_fired
422
+ SET delivered = 1, delivered_at = datetime('now')
423
+ WHERE id = ?
424
+ `).run(alertFiredId);
425
+ }
426
+ /**
427
+ * Count the number of undelivered alerts for the given network.
428
+ * Uses the same filtering logic as getUndeliveredAlerts:
429
+ * - delivered = 0
430
+ * - retry_count < MAX_RETRY_COUNT
431
+ * - matches the specified network
432
+ */
433
+ export function countUndeliveredAlerts(db, network) {
434
+ const row = db.prepare(`
435
+ SELECT COUNT(*) as count
436
+ FROM alerts_fired af
437
+ JOIN alert_configs ac ON ac.id = af.alert_config_id
438
+ JOIN contract_entries ce ON ce.id = af.contract_entry_id
439
+ JOIN contracts c ON c.id = ce.contract_id
440
+ WHERE af.delivered = 0
441
+ AND af.retry_count < ?
442
+ AND c.network = ?
443
+ `).get(MAX_RETRY_COUNT, network);
444
+ return row.count;
445
+ }
446
+ /**
447
+ * Increment the retry count for a failed alert delivery.
448
+ */
449
+ export function incrementRetryCount(db, alertFiredId) {
450
+ db.prepare(`
451
+ UPDATE alerts_fired
452
+ SET retry_count = retry_count + 1
453
+ WHERE id = ?
454
+ `).run(alertFiredId);
455
+ }
456
+ export function getAlertHistory(db, contractId, limit) {
457
+ const sql = `
458
+ SELECT
459
+ af.id AS alertFiredId,
460
+ ac.channel_type AS channelType,
461
+ ac.channel_target AS channelTarget,
462
+ ce.entry_key_xdr AS entryKeyXdr,
463
+ ce.entry_type AS entryType,
464
+ ce.label AS entryLabel,
465
+ ac.threshold_ledgers AS thresholdLedgers,
466
+ af.ttl_at_fire AS ttlAtFire,
467
+ af.fired_at_ledger AS firedAtLedger,
468
+ af.fired_at AS firedAt,
469
+ af.resolved AS resolved,
470
+ af.resolved_at AS resolvedAt,
471
+ af.delivered AS delivered,
472
+ af.delivered_at AS deliveredAt,
473
+ af.retry_count AS retryCount
474
+ FROM alerts_fired af
475
+ JOIN alert_configs ac ON ac.id = af.alert_config_id
476
+ JOIN contract_entries ce ON ce.id = af.contract_entry_id
477
+ WHERE ac.contract_id = ?
478
+ ORDER BY af.fired_at DESC
479
+ ${limit ? "LIMIT ?" : ""}
480
+ `;
481
+ return (limit
482
+ ? db.prepare(sql).all(contractId, limit)
483
+ : db.prepare(sql).all(contractId));
484
+ }
485
+ export function insertChannelAccount(db, account) {
486
+ db.prepare(`
487
+ INSERT INTO channel_accounts (public_key, label, network)
488
+ VALUES (@public_key, @label, @network)
489
+ `).run({
490
+ public_key: account.public_key,
491
+ label: account.label ?? null,
492
+ network: account.network,
493
+ });
494
+ }
495
+ export function upsertChannelAccount(db, account) {
496
+ db.prepare(`
497
+ INSERT INTO channel_accounts (public_key, keypair_source, network)
498
+ VALUES (@public_key, @keypair_source, @network)
499
+ ON CONFLICT(public_key) DO UPDATE SET
500
+ keypair_source = @keypair_source,
501
+ network = @network
502
+ `).run(account);
503
+ }
504
+ export function getChannelAccounts(db, network) {
505
+ return db.prepare("SELECT * FROM channel_accounts WHERE network = ? ORDER BY id ASC")
506
+ .all(network);
507
+ }
508
+ export function updateChannelBalance(db, publicKey, balanceXlm) {
509
+ db.prepare(`
510
+ UPDATE channel_accounts
511
+ SET balance_xlm = ?, balance_checked_at = datetime('now')
512
+ WHERE public_key = ?
513
+ `).run(balanceXlm, publicKey);
514
+ }
515
+ export function deleteChannelAccount(db, publicKey) {
516
+ db.prepare("DELETE FROM channel_accounts WHERE public_key = ?").run(publicKey);
517
+ }
518
+ export function markChannelFunded(db, publicKey) {
519
+ db.prepare("UPDATE channel_accounts SET funded = 1 WHERE public_key = ?").run(publicKey);
520
+ }
521
+ // ---------------------------- Database Access Functions For Schema: StateSnapshot ----------------------------
522
+ export function insertStateSnapshot(db, snapshot) {
523
+ const result = db.prepare(`
524
+ INSERT INTO state_snapshots (contract_entry_id, snapshot_ledger, value_hash, value_xdr)
525
+ VALUES (@contract_entry_id, @snapshot_ledger, @value_hash, @value_xdr)
526
+ `).run(snapshot);
527
+ return result.lastInsertRowid;
528
+ }
529
+ export function getLatestSnapshot(db, contractEntryId) {
530
+ return db.prepare(`
531
+ SELECT * FROM state_snapshots
532
+ WHERE contract_entry_id = ?
533
+ ORDER BY snapshot_ledger DESC, id DESC
534
+ LIMIT 1
535
+ `).get(contractEntryId);
536
+ }
537
+ // ---------------------------- Database Access Functions For Schema: StateChange ----------------------------
538
+ export function insertStateChange(db, change) {
539
+ const result = db.prepare(`
540
+ INSERT INTO state_changes (contract_entry_id, old_snapshot_id, new_snapshot_id, diff_type, diff_json, detected_at_ledger)
541
+ VALUES (@contract_entry_id, @old_snapshot_id, @new_snapshot_id, @diff_type, @diff_json, @detected_at_ledger)
542
+ `).run({
543
+ ...change,
544
+ old_snapshot_id: change.old_snapshot_id ?? null,
545
+ new_snapshot_id: change.new_snapshot_id ?? null,
546
+ });
547
+ return result.lastInsertRowid;
548
+ }
549
+ export function getStateChanges(db, contractEntryId, limit) {
550
+ let sql = "SELECT * FROM state_changes WHERE contract_entry_id = ? ORDER BY detected_at_ledger DESC, id DESC";
551
+ if (limit !== undefined) {
552
+ if (limit < 0) {
553
+ throw new Error("limit must be non-negative");
554
+ }
555
+ sql += " LIMIT ?";
556
+ return db.prepare(sql).all(contractEntryId, limit);
557
+ }
558
+ return db.prepare(sql).all(contractEntryId);
559
+ }
560
+ /**
561
+ * Return a rich history view of state changes for a contract,
562
+ * joining state_changes → contract_entries → state_snapshots.
563
+ */
564
+ export function getStateChangeHistory(db, contractId, opts) {
565
+ const limit = opts?.limit;
566
+ const entryKeyXdr = opts?.entryKeyXdr;
567
+ let sql = `
568
+ SELECT
569
+ sc.id AS changeId,
570
+ sc.contract_entry_id AS contractEntryId,
571
+ ce.entry_key_xdr AS entryKeyXdr,
572
+ ce.entry_type AS entryType,
573
+ ce.label AS entryLabel,
574
+ sc.diff_type AS diffType,
575
+ sc.diff_json AS diffJson,
576
+ sc.detected_at_ledger AS detectedAtLedger,
577
+ sc.created_at AS createdAt,
578
+ old_snap.value_xdr AS oldValueXdr,
579
+ new_snap.value_xdr AS newValueXdr
580
+ FROM state_changes sc
581
+ JOIN contract_entries ce ON ce.id = sc.contract_entry_id
582
+ LEFT JOIN state_snapshots old_snap ON old_snap.id = sc.old_snapshot_id
583
+ LEFT JOIN state_snapshots new_snap ON new_snap.id = sc.new_snapshot_id
584
+ WHERE ce.contract_id = ?
585
+ `;
586
+ const params = [contractId];
587
+ if (entryKeyXdr) {
588
+ sql += " AND ce.entry_key_xdr = ?";
589
+ params.push(entryKeyXdr);
590
+ }
591
+ sql += " ORDER BY sc.detected_at_ledger DESC, sc.id DESC";
592
+ if (limit !== undefined && limit >= 0) {
593
+ sql += " LIMIT ?";
594
+ params.push(limit);
595
+ }
596
+ return db.prepare(sql).all(...params);
597
+ }
598
+ export function insertResourceAlertConfig(db, config) {
599
+ db.prepare(`
600
+ INSERT INTO resource_alert_configs (contract_id, channel_type, channel_target, cpu_limit, mem_limit, webhook_secret)
601
+ VALUES (@contract_id, @channel_type, @channel_target, @cpu_limit, @mem_limit, @webhook_secret)
602
+ `).run({
603
+ contract_id: config.contract_id,
604
+ channel_type: config.channel_type,
605
+ channel_target: config.channel_target,
606
+ cpu_limit: config.cpu_limit,
607
+ mem_limit: config.mem_limit,
608
+ webhook_secret: config.webhook_secret ?? null,
609
+ });
610
+ }
611
+ export function getResourceAlertConfigsForContract(db, contractId) {
612
+ return db.prepare("SELECT * FROM resource_alert_configs WHERE contract_id = ?").all(contractId);
613
+ }
614
+ export function getResourceAlertConfigById(db, id) {
615
+ return db.prepare("SELECT * FROM resource_alert_configs WHERE id = ?").get(id);
616
+ }
617
+ export function deleteResourceAlertConfig(db, id) {
618
+ db.prepare("DELETE FROM resource_alert_configs WHERE id = ?").run(id);
619
+ }
620
+ export function recordResourceAlertFired(db, alert) {
621
+ const result = db.prepare(`
622
+ INSERT INTO resource_alerts_fired (resource_alert_config_id, resource_type, usage, "limit", usage_percent, fired_at_ledger)
623
+ VALUES (@resource_alert_config_id, @resource_type, @usage, @limit, @usage_percent, @fired_at_ledger)
624
+ `).run({
625
+ resource_alert_config_id: alert.resource_alert_config_id,
626
+ resource_type: alert.resource_type,
627
+ usage: alert.usage,
628
+ limit: alert.limit,
629
+ usage_percent: alert.usage_percent,
630
+ fired_at_ledger: alert.fired_at_ledger ?? null,
631
+ });
632
+ return result.lastInsertRowid;
633
+ }
634
+ export function getResourceUsageHistory(db, contractId, days) {
635
+ if (days) {
636
+ return db.prepare(`
637
+ SELECT raf.resource_type AS resourceType,
638
+ raf.usage,
639
+ raf.usage_percent AS usagePercent,
640
+ raf.fired_at AS firedAt
641
+ FROM resource_alerts_fired raf
642
+ JOIN resource_alert_configs rac ON rac.id = raf.resource_alert_config_id
643
+ WHERE rac.contract_id = ?
644
+ AND raf.fired_at >= datetime('now', ?)
645
+ ORDER BY raf.fired_at DESC
646
+ `).all(contractId, `-${days} days`);
647
+ }
648
+ return db.prepare(`
649
+ SELECT raf.resource_type AS resourceType,
650
+ raf.usage,
651
+ raf.usage_percent AS usagePercent,
652
+ raf.fired_at AS firedAt
653
+ FROM resource_alerts_fired raf
654
+ JOIN resource_alert_configs rac ON rac.id = raf.resource_alert_config_id
655
+ WHERE rac.contract_id = ?
656
+ ORDER BY raf.fired_at DESC
657
+ `).all(contractId);
658
+ }
659
+ export function getUndeliveredResourceAlerts(db, network) {
660
+ return db.prepare(`
661
+ SELECT
662
+ raf.id AS alertFiredId,
663
+ raf.resource_alert_config_id AS resourceAlertConfigId,
664
+ c.id AS contractId,
665
+ c.name AS contractName,
666
+ c.network,
667
+ raf.resource_type AS resourceType,
668
+ raf.usage,
669
+ raf."limit",
670
+ raf.usage_percent AS usagePercent,
671
+ rac.channel_type AS channelType,
672
+ rac.channel_target AS channelTarget,
673
+ rac.webhook_secret AS webhookSecret,
674
+ raf.retry_count AS retryCount,
675
+ raf.fired_at_ledger AS firedAtLedger
676
+ FROM resource_alerts_fired raf
677
+ JOIN resource_alert_configs rac ON rac.id = raf.resource_alert_config_id
678
+ JOIN contracts c ON c.id = rac.contract_id
679
+ WHERE c.network = ? AND raf.delivered = 0 AND raf.retry_count < ?
680
+ ORDER BY raf.fired_at DESC
681
+ `).all(network, MAX_RETRY_COUNT);
682
+ }
683
+ export function markResourceAlertDelivered(db, alertFiredId) {
684
+ db.prepare(`
685
+ UPDATE resource_alerts_fired SET delivered = 1, delivered_at = datetime('now') WHERE id = ?
686
+ `).run(alertFiredId);
687
+ }
688
+ export function incrementResourceAlertRetryCount(db, alertFiredId) {
689
+ db.prepare("UPDATE resource_alerts_fired SET retry_count = retry_count + 1 WHERE id = ?").run(alertFiredId);
690
+ }
691
+ export function hasUnresolvedResourceAlert(db, configId, resourceType, currentUsagePercent) {
692
+ if (typeof currentUsagePercent === "undefined") {
693
+ const result = db.prepare(`
694
+ SELECT 1 FROM resource_alerts_fired
695
+ WHERE resource_alert_config_id = ? AND resource_type = ? AND resolved = 0
696
+ LIMIT 1
697
+ `).get(configId, resourceType);
698
+ return !!result;
699
+ }
700
+ // If we have a current usage percent, only consider an existing unresolved
701
+ // alert to be blocking if its recorded usage_percent is greater than or
702
+ // equal to the current usage (i.e., no increase). If the current usage
703
+ // is higher, allow firing a new alert.
704
+ const row = db.prepare(`
705
+ SELECT usage_percent FROM resource_alerts_fired
706
+ WHERE resource_alert_config_id = ? AND resource_type = ? AND resolved = 0
707
+ ORDER BY fired_at DESC
708
+ LIMIT 1
709
+ `).get(configId, resourceType);
710
+ if (!row || typeof row.usage_percent === "undefined")
711
+ return false;
712
+ return row.usage_percent >= currentUsagePercent;
713
+ }
714
+ /**
715
+ * Insert a new resource-usage log entry.
716
+ *
717
+ * @returns The auto-assigned row id of the inserted entry.
718
+ */
719
+ export function insertResourceUsageLog(db, log) {
720
+ const result = db.prepare(`
721
+ INSERT INTO resource_usage_logs (
722
+ contract_id,
723
+ cpu_insns,
724
+ mem_bytes,
725
+ fee_instructions,
726
+ fee_read_ledger_entries,
727
+ fee_write_ledger_entries,
728
+ fee_read_bytes,
729
+ fee_write_bytes,
730
+ fee_transaction_size,
731
+ fee_historical_ledger,
732
+ fee_rent_ledger,
733
+ fee_refundable,
734
+ recorded_at
735
+ ) VALUES (
736
+ @contract_id,
737
+ @cpu_insns,
738
+ @mem_bytes,
739
+ @fee_instructions,
740
+ @fee_read_ledger_entries,
741
+ @fee_write_ledger_entries,
742
+ @fee_read_bytes,
743
+ @fee_write_bytes,
744
+ @fee_transaction_size,
745
+ @fee_historical_ledger,
746
+ @fee_rent_ledger,
747
+ @fee_refundable,
748
+ COALESCE(@recorded_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
749
+ )
750
+ `).run({
751
+ contract_id: log.contract_id,
752
+ cpu_insns: log.cpu_insns,
753
+ mem_bytes: log.mem_bytes,
754
+ fee_instructions: log.fee_instructions ?? null,
755
+ fee_read_ledger_entries: log.fee_read_ledger_entries ?? null,
756
+ fee_write_ledger_entries: log.fee_write_ledger_entries ?? null,
757
+ fee_read_bytes: log.fee_read_bytes ?? null,
758
+ fee_write_bytes: log.fee_write_bytes ?? null,
759
+ fee_transaction_size: log.fee_transaction_size ?? null,
760
+ fee_historical_ledger: log.fee_historical_ledger ?? null,
761
+ fee_rent_ledger: log.fee_rent_ledger ?? null,
762
+ fee_refundable: log.fee_refundable ?? null,
763
+ recorded_at: log.recorded_at ?? null,
764
+ });
765
+ return result.lastInsertRowid;
766
+ }
767
+ /**
768
+ * Return resource-usage logs for a contract, newest first.
769
+ *
770
+ * @param options.limit Maximum number of rows to return (must be ≥ 0).
771
+ * @param options.since ISO-8601 lower-bound for recorded_at (inclusive).
772
+ */
773
+ export function getResourceUsageLogs(db, contractId, options = {}) {
774
+ const { limit, since } = options;
775
+ if (limit !== undefined && limit < 0) {
776
+ throw new Error("limit must be non-negative");
777
+ }
778
+ const conditions = ["contract_id = @contractId"];
779
+ if (since !== undefined) {
780
+ conditions.push("recorded_at >= @since");
781
+ }
782
+ const where = conditions.join(" AND ");
783
+ const params = { contractId, since: since ?? null };
784
+ let query = `
785
+ SELECT *
786
+ FROM resource_usage_logs
787
+ WHERE ${where}
788
+ ORDER BY recorded_at DESC, id DESC
789
+ `;
790
+ if (limit !== undefined) {
791
+ query += ` LIMIT @limit`;
792
+ params.limit = limit;
793
+ }
794
+ return db.prepare(query).all(params);
795
+ }
796
+ /**
797
+ * Return the single most-recent resource-usage log for a contract,
798
+ * or `undefined` if none exist.
799
+ */
800
+ export function getLatestResourceUsageLog(db, contractId) {
801
+ return db.prepare(`
802
+ SELECT *
803
+ FROM resource_usage_logs
804
+ WHERE contract_id = ?
805
+ ORDER BY recorded_at DESC, id DESC
806
+ LIMIT 1
807
+ `).get(contractId);
808
+ }
809
+ //# sourceMappingURL=repositories.js.map