use crate::error::{Error, Result};
use crate::storage::{audit, meta, sql};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
const SELECT_APPROVAL_CONSUMED: &str =
"SELECT EXISTS(SELECT 1 FROM consumed_approvals WHERE key=?1)";
const SELECT_PENDING_SUBMISSION: &str =
"SELECT id FROM submissions WHERE account=?1 AND outcome IS NULL";
const SELECT_APPROVAL_EXISTS: &str = "SELECT EXISTS(SELECT 1 FROM approvals WHERE digest=?1)";
const SELECT_APPROVAL_LIVE: &str = "SELECT EXISTS(SELECT 1 FROM approvals \
WHERE digest=?1 AND principal=?2 AND expires_at>?3 AND consumed=0)";
const CONSUME_APPROVAL: &str = "UPDATE approvals SET consumed=1 \
WHERE digest=?1 AND principal=?2 AND expires_at>?3 AND consumed=0";
const INSERT_APPROVAL: &str = "INSERT INTO approvals(digest,principal,expires_at,operator) \
VALUES(?1,?2,?3,?4) ON CONFLICT(digest) DO UPDATE SET principal=excluded.principal,\
expires_at=excluded.expires_at,operator=excluded.operator \
WHERE approvals.consumed=0 AND approvals.expires_at<=?5 \
AND NOT EXISTS(SELECT 1 FROM consumed_approvals WHERE key=approvals.digest)";
const INSERT_CONSUMED_APPROVAL: &str = "INSERT INTO consumed_approvals VALUES(?1)";
const INSERT_CONSUMED_APPROVAL_IF_NEW: &str = "INSERT OR IGNORE INTO consumed_approvals VALUES(?1)";
const INSERT_USED_ORDER_TAG: &str = "INSERT OR IGNORE INTO used_order_tags VALUES(?1,?2)";
const RELEASE_ORDER_TAG: &str = "DELETE FROM used_order_tags WHERE EXISTS(\
SELECT 1 FROM submissions s WHERE s.id=?1 AND s.account=used_order_tags.account \
AND json_extract(s.request,'$.tag')=used_order_tags.tag) \
AND NOT EXISTS(SELECT 1 FROM orders o WHERE o.account=used_order_tags.account \
AND json_extract(o.payload,'$.tag')=used_order_tags.tag)";
const INSERT_SUBMISSION: &str = "INSERT INTO \
submissions(id,account,operation,created_at,request,outcome,dedup_key,armed) \
VALUES(?1,?2,?3,?4,?5,NULL,?6,0)";
const RESOLVE_SUBMISSION: &str =
"UPDATE submissions SET outcome=?1 WHERE id=?2 AND outcome IS NULL";
const ARM_SUBMISSION: &str = "UPDATE submissions SET armed=1 WHERE id=?1 AND outcome IS NULL";
const RELEASE_RESERVATION: &str =
"UPDATE reservations SET state='released' WHERE id=?1 AND state='held'";
const SELECT_SUBMISSION_OUTCOME: &str = "SELECT outcome FROM submissions WHERE id=?1";
const SELECT_SUBMISSION_DEDUP: &str =
"SELECT dedup_key,outcome IS NOT NULL FROM submissions WHERE id=?1";
const SELECT_APPROVAL_REVOKED: &str =
"SELECT EXISTS(SELECT 1 FROM audit WHERE kind='approval_revoked' AND subject=?1)";
const SELECT_SUBMISSIONS: &str = "SELECT id,account,operation,created_at,request,outcome,\
dedup_key,armed FROM submissions ORDER BY created_at,id";
const SELECT_UNRESOLVED_SUBMISSIONS: &str = "SELECT id,account,operation,created_at,request,\
outcome,dedup_key,armed FROM submissions WHERE account=?1 AND (outcome IS NULL \
OR (json_extract(outcome,'$.status')='accepted' \
AND json_extract(outcome,'$.order_id') IS NULL)) ORDER BY created_at,id";
const SELECT_SUBMISSION_BY_TAG: &str = "SELECT id,account,operation,created_at,request,outcome,\
dedup_key,armed FROM submissions WHERE account=?1 AND json_extract(request,'$.tag')=?2 \
ORDER BY created_at DESC,id LIMIT 1";
const SELECT_SUBMISSION: &str = "SELECT id,account,operation,created_at,request,outcome,\
dedup_key,armed FROM submissions WHERE id=?1";
const SELECT_SUBMISSIONS_SINCE: &str = "SELECT id,account,operation,created_at,request,outcome,\
dedup_key,armed FROM submissions WHERE account=?1 AND created_at>=?2 \
ORDER BY created_at,id";
const DELETE_ORDER_PLACEHOLDER: &str = "DELETE FROM orders WHERE account=?1 AND order_id=?2 \
AND json_extract(payload,'$.status')='UNKNOWN' AND json_extract(payload,'$.orderType') IS NULL";
const SELECT_PRUNABLE_ORDERS: &str = "SELECT account,order_id FROM orders WHERE observed_at<?1 \
AND json_extract(payload,'$.status') IN ('FILLED','CANCELED','REJECTED','EXPIRED','REPLACED') \
AND NOT EXISTS(SELECT 1 FROM reservations r JOIN submissions s ON s.id=r.id \
WHERE r.state='held' AND s.account=orders.account \
AND json_extract(s.outcome,'$.order_id')=orders.order_id)";
const DELETE_ORDER: &str = "DELETE FROM orders WHERE account=?1 AND order_id=?2";
const DELETE_ORDER_EXECUTIONS: &str = "DELETE FROM executions WHERE account=?1 \
AND execution_id>=?2 || ':' AND execution_id<?2 || ';'";
const PRUNE_RELEASED_RESERVATIONS: &str = "DELETE FROM reservations WHERE state='released' \
AND NOT EXISTS(SELECT 1 FROM submissions s WHERE s.id=reservations.id \
AND (s.outcome IS NULL OR s.created_at>=?1))";
const SELECT_AUDIT_PAGE: &str = "SELECT sequence,at,kind,subject,payload,prev_hash,hash FROM audit \
WHERE sequence>?1 ORDER BY sequence LIMIT ?2";
const PRUNE_RESOLVED_SUBMISSIONS: &str = "DELETE FROM submissions \
WHERE outcome IS NOT NULL AND created_at<?1 \
AND NOT EXISTS(SELECT 1 FROM audit WHERE subject=submissions.id AND sequence>?2) \
AND NOT EXISTS(SELECT 1 FROM reservations r WHERE r.id=submissions.id AND r.state='held') \
AND (json_extract(outcome,'$.status') IN ('rejected','reconciled') \
OR EXISTS(SELECT 1 FROM orders o \
WHERE o.account=submissions.account \
AND o.order_id=json_extract(submissions.outcome,'$.order_id') \
AND json_extract(o.payload,'$.status') \
IN ('FILLED','CANCELED','REJECTED','EXPIRED','REPLACED')))";
const SELECT_RECONCILIATION_TARGETS: &str = "SELECT order_id FROM orders \
WHERE account=?1 AND COALESCE(json_extract(payload,'$.status'),'UNKNOWN') \
NOT IN ('FILLED','CANCELED','REJECTED','EXPIRED','REPLACED') \
UNION \
SELECT json_extract(s.outcome,'$.order_id') FROM submissions s \
WHERE s.account=?1 AND json_extract(s.outcome,'$.order_id') IS NOT NULL \
AND NOT EXISTS(SELECT 1 FROM orders o \
WHERE o.account=s.account AND o.order_id=json_extract(s.outcome,'$.order_id') \
AND json_extract(o.payload,'$.status') \
IN ('FILLED','CANCELED','REJECTED','EXPIRED','REPLACED'))";
const OPERATOR_HALT_KEY: &str = "operator_halt";
pub(crate) fn ensure_not_halted(c: &rusqlite::Connection) -> Result<()> {
if meta::exists(c, OPERATOR_HALT_KEY)? {
return Err(Error::Halted("Operator trading halt is active".into()));
}
Ok(())
}
pub(crate) fn record_operator_halt(
tx: &rusqlite::Connection,
reason: Option<&str>,
operator: &str,
) -> Result<()> {
match reason {
Some(reason) => meta::put(tx, OPERATOR_HALT_KEY, reason)?,
None => meta::delete(tx, OPERATOR_HALT_KEY)?,
}
audit(
tx,
"operator_halt_changed",
operator,
&serde_json::json!({"reason":reason}),
)
}
pub(crate) fn trip_halt(tx: &rusqlite::Connection, reason: &str, control: &str) -> Result<()> {
if meta::exists(tx, OPERATOR_HALT_KEY)? {
return Ok(());
}
record_operator_halt(tx, Some(reason), control)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubmissionRecord {
pub attempt_id: String,
pub account: String,
pub operation: String,
pub request: serde_json::Value,
pub created_at: i64,
pub outcome: Option<SubmissionOutcome>,
#[serde(default)]
pub dedup_key: Option<String>,
#[serde(default = "armed_by_default")]
pub armed: bool,
}
fn armed_by_default() -> bool {
true
}
type SubmissionRow = (
String,
String,
String,
i64,
String,
Option<String>,
Option<String>,
bool,
);
fn submission_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<SubmissionRow> {
Ok((
r.get(0)?,
r.get(1)?,
r.get(2)?,
r.get(3)?,
r.get(4)?,
r.get(5)?,
r.get(6)?,
r.get(7)?,
))
}
fn submission_record(row: SubmissionRow) -> Result<SubmissionRecord> {
let (attempt_id, account, operation, created_at, request, outcome, dedup_key, armed) = row;
Ok(SubmissionRecord {
attempt_id,
account,
operation,
created_at,
request: serde_json::from_str(&request)?,
outcome: outcome.map(|s| serde_json::from_str(&s)).transpose()?,
dedup_key,
armed,
})
}
fn query_submissions(
c: &rusqlite::Connection,
sql_text: &str,
params: impl rusqlite::Params,
) -> Result<Vec<SubmissionRecord>> {
let mut query = c.prepare(sql_text).map_err(sql)?;
let rows = query.query_map(params, submission_row).map_err(sql)?;
rows.map(|row| submission_record(row.map_err(sql)?))
.collect()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum SubmissionOutcome {
Accepted {
order_id: Option<String>,
},
Rejected,
Reconciled {
evidence: String,
},
}
#[derive(Debug, Clone)]
pub struct OrderJournal {
database: crate::storage::StateDatabase,
}
impl OrderJournal {
pub fn new(path: PathBuf) -> Self {
Self {
database: crate::storage::StateDatabase::new(path),
}
}
pub fn database(&self) -> &crate::storage::StateDatabase {
&self.database
}
pub fn path(&self) -> &Path {
self.database.path()
}
pub async fn begin(
&self,
account: &str,
operation: &str,
request: serde_json::Value,
) -> Result<String> {
self.begin_once(account, operation, request, None).await
}
pub async fn begin_once(
&self,
account: &str,
operation: &str,
request: serde_json::Value,
dedup_key: Option<String>,
) -> Result<String> {
let account = account.to_owned();
let operation = operation.to_owned();
self.database
.run(move |conn| {
use rusqlite::{OptionalExtension, params};
let tx = conn
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.map_err(sql)?;
if let Some(key) = &dedup_key {
let used: bool = tx
.query_row(SELECT_APPROVAL_CONSUMED, [key], |r| r.get(0))
.map_err(sql)?;
if used {
return Err(Error::Duplicate("Execution intent already consumed".into()));
}
}
let pending: Option<String> = tx
.query_row(SELECT_PENDING_SUBMISSION, [&account], |r| r.get(0))
.optional()
.map_err(sql)?;
if let Some(attempt_id) = pending {
return Err(Error::ReconciliationRequired { attempt_id });
}
if let Some(key) = &dedup_key {
let configured: bool = tx
.query_row(SELECT_APPROVAL_EXISTS, [key], |r| r.get(0))
.map_err(sql)?;
if configured
&& tx
.execute(
CONSUME_APPROVAL,
params![key, account, chrono::Utc::now().timestamp()],
)
.map_err(sql)?
!= 1
{
return Err(Error::ApprovalInvalid(
"Approval expired, revoked or belongs to another principal".into(),
));
}
}
if let Some(tag) = request.get("tag").and_then(serde_json::Value::as_str) {
if tag.trim().is_empty() {
return Err(Error::InvalidParameter(
"Order tag must not be empty".into(),
));
}
if tx
.execute(INSERT_USED_ORDER_TAG, params![account, tag])
.map_err(sql)?
!= 1
{
return Err(Error::Duplicate(
"Order tag already belongs to an accepted or unresolved submission \
for this account"
.into(),
));
}
}
let id = uuid::Uuid::new_v4().to_string();
let record = SubmissionRecord {
attempt_id: id.clone(),
account,
operation,
request,
created_at: chrono::Utc::now().timestamp_millis(),
outcome: None,
dedup_key,
armed: false,
};
tx.execute(
INSERT_SUBMISSION,
params![
record.attempt_id,
record.account,
record.operation,
record.created_at,
serde_json::to_string(&record.request)?,
record.dedup_key
],
)
.map_err(sql)?;
if let Some(key) = &record.dedup_key {
tx.execute(INSERT_CONSUMED_APPROVAL, [key]).map_err(sql)?;
}
audit(&tx, "intent", &id, &serde_json::to_value(&record)?)?;
tx.commit().map_err(sql)?;
Ok(id)
})
.await
}
pub async fn finish(&self, id: &str, outcome: SubmissionOutcome) -> Result<()> {
if let SubmissionOutcome::Reconciled { evidence } = &outcome
&& evidence.trim().is_empty()
{
return Err(Error::InvalidParameter(
"Reconciliation evidence is required".into(),
));
}
let id = id.to_owned();
self.database
.run(move |conn| {
let tx = conn
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.map_err(sql)?;
let changed = tx
.execute(
RESOLVE_SUBMISSION,
rusqlite::params![serde_json::to_string(&outcome)?, id],
)
.map_err(sql)?;
if changed != 1 {
use rusqlite::OptionalExtension;
let recorded: Option<String> = tx
.query_row(SELECT_SUBMISSION_OUTCOME, [&id], |r| r.get(0))
.optional()
.map_err(sql)?
.flatten();
let Some(recorded) = recorded else {
return Err(Error::InvalidParameter("Submission is unknown".into()));
};
let recorded: SubmissionOutcome = serde_json::from_str(&recorded)?;
return match (&recorded, &outcome) {
(
SubmissionOutcome::Accepted { order_id: Some(a) },
SubmissionOutcome::Accepted { order_id: b },
) if b.as_ref().is_none_or(|b| b == a) => Ok(()),
(SubmissionOutcome::Rejected, SubmissionOutcome::Rejected) => Ok(()),
_ => Err(Error::Duplicate("Submission is already resolved".into())),
};
}
if matches!(outcome, SubmissionOutcome::Rejected) {
tx.execute(RELEASE_ORDER_TAG, [&id]).map_err(sql)?;
}
if !matches!(outcome, SubmissionOutcome::Accepted { .. })
&& tx.execute(RELEASE_RESERVATION, [&id]).map_err(sql)? == 1
{
audit(&tx, "risk_released", &id, &serde_json::json!({}))?;
}
audit(&tx, "outcome", &id, &serde_json::to_value(&outcome)?)?;
tx.commit().map_err(sql)?;
Ok(())
})
.await
}
pub async fn records(&self) -> Result<Vec<SubmissionRecord>> {
self.database
.run(|conn| query_submissions(conn, SELECT_SUBMISSIONS, []))
.await
}
pub async fn records_since(&self, account: &str, since: i64) -> Result<Vec<SubmissionRecord>> {
let account = account.to_owned();
self.database
.run(move |conn| {
query_submissions(
conn,
SELECT_SUBMISSIONS_SINCE,
rusqlite::params![account, since],
)
})
.await
}
pub async fn record_event(
&self,
kind: &str,
subject: &str,
payload: serde_json::Value,
) -> Result<()> {
let kind = kind.to_owned();
let subject = subject.to_owned();
self.database
.run(move |c| audit(c, &kind, &subject, &payload))
.await
}
pub async fn record(&self, id: &str) -> Result<Option<SubmissionRecord>> {
let id = id.to_owned();
self.database
.run(move |conn| Ok(query_submissions(conn, SELECT_SUBMISSION, [id])?.pop()))
.await
}
pub async fn arm(&self, id: &str) -> Result<()> {
let id = id.to_owned();
self.database
.run(move |c| {
let tx = c
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.map_err(sql)?;
if tx.execute(ARM_SUBMISSION, [&id]).map_err(sql)? != 1 {
return Err(Error::Duplicate(
"Submission was resolved before dispatch; it was not sent".into(),
));
}
audit(&tx, "armed", &id, &serde_json::json!({}))?;
tx.commit().map_err(sql)
})
.await
}
pub async fn prune_history(&self, before: i64) -> Result<usize> {
self.database
.run(move |c| {
let tx = c
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.map_err(sql)?;
let orders = {
let mut stmt = tx.prepare(SELECT_PRUNABLE_ORDERS).map_err(sql)?;
stmt.query_map([before], |r| {
Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?))
})
.map_err(sql)?
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(sql)?
};
for (account, order_id) in &orders {
tx.execute(DELETE_ORDER, [account, order_id]).map_err(sql)?;
tx.execute(DELETE_ORDER_EXECUTIONS, [account, order_id])
.map_err(sql)?;
}
let reservations = tx
.execute(PRUNE_RELEASED_RESERVATIONS, [before])
.map_err(sql)?;
audit(
&tx,
"history_pruned",
"state",
&serde_json::json!({
"before": before,
"orders": orders.len(),
"reservations": reservations,
}),
)?;
tx.commit().map_err(sql)?;
Ok(orders.len())
})
.await
}
pub async fn audit_page(&self, after: i64, limit: u32) -> Result<Vec<AuditEvent>> {
self.database
.run(move |conn| {
let mut stmt = conn.prepare(SELECT_AUDIT_PAGE).map_err(sql)?;
let rows = stmt
.query_map(rusqlite::params![after, limit.clamp(1, 10000)], |r| {
Ok((
r.get(0)?,
r.get(1)?,
r.get(2)?,
r.get(3)?,
r.get::<_, String>(4)?,
r.get(5)?,
r.get(6)?,
))
})
.map_err(sql)?;
rows.map(|row| {
let (sequence, at, kind, subject, payload, prev_hash, hash) =
row.map_err(sql)?;
Ok(AuditEvent {
sequence,
at,
kind,
subject,
payload: serde_json::from_str(&payload)?,
prev_hash,
hash,
})
})
.collect()
})
.await
}
pub async fn verify_audit(&self) -> Result<crate::audit_chain::AuditChainReport> {
self.database.run(crate::audit_chain::verify).await
}
pub async fn prune_resolved(&self, before: i64, exported_through: i64) -> Result<usize> {
self.database
.run(move |conn| {
let tx = conn
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.map_err(sql)?;
let pruned = tx
.execute(
PRUNE_RESOLVED_SUBMISSIONS,
rusqlite::params![before, exported_through],
)
.map_err(sql)?;
audit(
&tx,
"submissions_pruned",
"state",
&serde_json::json!({
"before": before,
"exported_through": exported_through,
"submissions": pruned,
}),
)?;
tx.commit().map_err(sql)?;
Ok(pruned)
})
.await
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct IntentRecord {
pub digest: String,
pub principal: String,
pub tool: String,
pub arguments: serde_json::Value,
pub first_seen: i64,
pub last_seen: i64,
pub seen: i64,
pub state: IntentState,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case")]
#[non_exhaustive]
pub enum IntentState {
Awaiting,
Approved { operator: String, expires_at: i64 },
Spent,
Revoked,
Lapsed,
}
const UPSERT_INTENT: &str = "INSERT INTO intents(digest,principal,tool,arguments,first_seen,last_seen) \
VALUES(?1,?2,?3,?4,?5,?5) ON CONFLICT(digest) DO UPDATE SET \
arguments=excluded.arguments,last_seen=excluded.last_seen,seen=intents.seen+1 RETURNING seen";
const SELECT_INTENTS: &str = "SELECT i.digest,i.principal,i.tool,i.arguments,i.first_seen,\
i.last_seen,i.seen,a.operator,a.expires_at,a.consumed,\
EXISTS(SELECT 1 FROM audit WHERE kind='approval_revoked' AND subject=i.digest) \
FROM intents i LEFT JOIN approvals a ON a.digest=i.digest \
WHERE (?1 IS NULL OR i.digest=?1) AND i.last_seen>=?2 ORDER BY i.last_seen DESC LIMIT ?3";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvent {
pub sequence: i64,
pub at: i64,
pub kind: String,
pub subject: String,
pub payload: serde_json::Value,
#[serde(default)]
pub prev_hash: String,
#[serde(default)]
pub hash: String,
}
impl OrderJournal {
pub async fn approve(
&self,
principal: &str,
digest: &str,
operator: &str,
expires_at: i64,
) -> Result<()> {
if principal == operator
|| operator.trim().is_empty()
|| digest.len() != 64
|| !digest.bytes().all(|b| b.is_ascii_hexdigit())
{
return Err(Error::InvalidParameter(
"Approval requires a distinct operator and a SHA-256 intent digest".into(),
));
}
let now = chrono::Utc::now().timestamp();
if expires_at <= now || expires_at > now + 3600 {
return Err(Error::InvalidParameter(
"Approval must expire within one hour".into(),
));
}
let principal = principal.to_owned();
let digest = digest.to_owned();
let operator = operator.to_owned();
self.database
.run(move |c| {
let tx = c
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.map_err(sql)?;
let now = chrono::Utc::now().timestamp();
if tx
.execute(
INSERT_APPROVAL,
rusqlite::params![digest, principal, expires_at, operator, now],
)
.map_err(sql)?
!= 1
{
return Err(Error::Duplicate(
"This intent is already approved, was spent on an order, or was \
revoked; only a lapsed approval can be granted again"
.into(),
));
}
audit(
&tx,
"approval_granted",
&digest,
&serde_json::json!({
"principal": principal,
"operator": operator,
"expires_at": expires_at
}),
)?;
tx.commit().map_err(sql)?;
Ok(())
})
.await
}
pub async fn record_intent(
&self,
principal: &str,
digest: &str,
tool: &str,
arguments: &serde_json::Value,
) -> Result<i64> {
let values = (
digest.to_owned(),
principal.to_owned(),
tool.to_owned(),
serde_json::to_string(arguments)?,
);
self.database
.run(move |c| {
c.query_row(
UPSERT_INTENT,
rusqlite::params![
values.0,
values.1,
values.2,
values.3,
chrono::Utc::now().timestamp_millis()
],
|r| r.get(0),
)
.map_err(sql)
})
.await
}
pub async fn intents(
&self,
digest: Option<&str>,
since: i64,
limit: u32,
) -> Result<Vec<IntentRecord>> {
let digest = digest.map(str::to_owned);
self.database
.run(move |c| {
let now = chrono::Utc::now().timestamp();
let mut stmt = c.prepare(SELECT_INTENTS).map_err(sql)?;
let rows = stmt
.query_map(rusqlite::params![digest, since, limit], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
r.get::<_, String>(3)?,
r.get::<_, i64>(4)?,
r.get::<_, i64>(5)?,
r.get::<_, i64>(6)?,
r.get::<_, Option<String>>(7)?,
r.get::<_, Option<i64>>(8)?,
r.get::<_, Option<bool>>(9)?,
r.get::<_, bool>(10)?,
))
})
.map_err(sql)?;
rows.map(|row| {
let (
digest,
principal,
tool,
arguments,
first_seen,
last_seen,
seen,
operator,
expires_at,
consumed,
revoked,
) = row.map_err(sql)?;
let state = match (operator, expires_at, consumed) {
_ if revoked => IntentState::Revoked,
(Some(_), Some(_), Some(true)) => IntentState::Spent,
(Some(operator), Some(expires_at), _) if expires_at > now => {
IntentState::Approved {
operator,
expires_at,
}
}
(Some(_), Some(_), _) => IntentState::Lapsed,
_ => IntentState::Awaiting,
};
Ok(IntentRecord {
digest,
principal,
tool,
arguments: serde_json::from_str(&arguments)?,
first_seen,
last_seen,
seen,
state,
})
})
.collect()
})
.await
}
pub async fn approval_operator(&self, digest: &str) -> Result<Option<String>> {
let digest = digest.to_owned();
self.database
.run(move |c| {
use rusqlite::OptionalExtension;
c.query_row(
"SELECT operator FROM approvals WHERE digest=?1",
[digest],
|r| r.get(0),
)
.optional()
.map_err(sql)
})
.await
}
pub async fn is_approved(&self, principal: &str, digest: &str) -> Result<bool> {
let principal = principal.to_owned();
let digest = digest.to_owned();
self.database
.run(move |c| {
c.query_row(
SELECT_APPROVAL_LIVE,
rusqlite::params![digest, principal, chrono::Utc::now().timestamp()],
|r| r.get(0),
)
.map_err(sql)
})
.await
}
pub async fn refund_approval(&self, id: &str) -> Result<()> {
let id = id.to_owned();
self.database
.run(move |c| {
use rusqlite::OptionalExtension;
let tx = c
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.map_err(sql)?;
let row: Option<(Option<String>, bool)> = tx
.query_row(SELECT_SUBMISSION_DEDUP, [&id], |r| {
Ok((r.get(0)?, r.get(1)?))
})
.optional()
.map_err(sql)?;
let (digest, resolved) =
row.ok_or_else(|| Error::InvalidParameter("Submission is unknown".into()))?;
let digest = digest.ok_or_else(|| {
Error::InvalidParameter("Submission consumed no approval".into())
})?;
if !resolved {
return Err(Error::InvalidParameter(
"Submission is unresolved; reconcile it first".into(),
));
}
let revoked: bool = tx
.query_row(SELECT_APPROVAL_REVOKED, [&digest], |r| r.get(0))
.map_err(sql)?;
if !revoked {
tx.execute("DELETE FROM consumed_approvals WHERE key=?1", [&digest])
.map_err(sql)?;
tx.execute("UPDATE approvals SET consumed=0 WHERE digest=?1", [&digest])
.map_err(sql)?;
}
tx.execute(RELEASE_ORDER_TAG, [&id]).map_err(sql)?;
tx.execute("UPDATE submissions SET dedup_key=NULL WHERE id=?1", [&id])
.map_err(sql)?;
audit(
&tx,
"approval_refunded",
&id,
&serde_json::json!({"digest": digest, "revoked": revoked}),
)?;
tx.commit().map_err(sql)
})
.await
}
pub async fn release_reservation(
&self,
id: &str,
operator: &str,
evidence: &str,
) -> Result<()> {
if operator.trim().is_empty() || evidence.trim().is_empty() {
return Err(Error::InvalidParameter(
"Operator and evidence are required".into(),
));
}
let (id, operator, evidence) = (id.to_owned(), operator.to_owned(), evidence.to_owned());
self.database
.run(move |c| {
use rusqlite::OptionalExtension;
let tx = c
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.map_err(sql)?;
let resolved: Option<bool> = tx
.query_row(
"SELECT outcome IS NOT NULL FROM submissions WHERE id=?1",
[&id],
|r| r.get(0),
)
.optional()
.map_err(sql)?;
match resolved {
None => return Err(Error::InvalidParameter("Submission is unknown".into())),
Some(false) => {
return Err(Error::InvalidParameter(
"Submission is unresolved; its order may be live. Reconcile it first"
.into(),
));
}
Some(true) => {}
}
if tx.execute(RELEASE_RESERVATION, [&id]).map_err(sql)? != 1 {
return Err(Error::InvalidParameter(
"Submission holds no reservation".into(),
));
}
audit(
&tx,
"risk_released_by_operator",
&id,
&serde_json::json!({"operator": operator, "evidence": evidence}),
)?;
tx.commit().map_err(sql)
})
.await
}
pub async fn revoke_approval(&self, digest: &str, operator: &str) -> Result<()> {
let digest = digest.to_owned();
let operator = operator.to_owned();
self.database
.run(move |c| {
let tx = c
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.map_err(sql)?;
tx.execute("UPDATE approvals SET consumed=1 WHERE digest=?1", [&digest])
.map_err(sql)?;
tx.execute(INSERT_CONSUMED_APPROVAL_IF_NEW, [&digest])
.map_err(sql)?;
audit(
&tx,
"approval_revoked",
&digest,
&serde_json::json!({"operator":operator}),
)?;
tx.commit().map_err(sql)?;
Ok(())
})
.await
}
}
#[async_trait::async_trait]
pub trait ExecutionStore: Send + Sync {
async fn begin_once(
&self,
account: &str,
operation: &str,
request: serde_json::Value,
dedup_key: Option<String>,
) -> Result<String>;
async fn finish(&self, id: &str, outcome: SubmissionOutcome) -> Result<()>;
async fn arm(&self, id: &str) -> Result<()>;
async fn records(&self) -> Result<Vec<SubmissionRecord>>;
async fn unresolved_records(&self, account: &str) -> Result<Vec<SubmissionRecord>> {
Ok(self
.records()
.await?
.into_iter()
.filter(|r| {
r.account == account
&& matches!(
r.outcome,
None | Some(SubmissionOutcome::Accepted { order_id: None })
)
})
.collect())
}
async fn submission_by_tag(
&self,
account: &str,
tag: &str,
) -> Result<Option<SubmissionRecord>> {
Ok(self
.records()
.await?
.into_iter()
.filter(|r| {
r.account == account
&& r.request.get("tag").and_then(serde_json::Value::as_str) == Some(tag)
})
.max_by_key(|r| r.created_at))
}
async fn forget_placeholder(&self, account: &str, order_id: &str) -> Result<bool>;
async fn tag_spent(&self, account: &str, tag: &str) -> Result<bool>;
async fn invalidate_account(&self, account: &str, generation: u64) -> Result<()>;
async fn account_state(
&self,
account: &str,
) -> Result<Option<crate::reconciliation::AccountState>>;
async fn reconcile_snapshot(
&self,
account: &str,
generation: u64,
started_at: i64,
snapshot: serde_json::Value,
orders: Vec<serde_json::Value>,
) -> Result<crate::reconciliation::ReconciliationReport>;
async fn audit_page(&self, after: i64, limit: u32) -> Result<Vec<AuditEvent>>;
async fn verify_audit(&self) -> Result<crate::audit_chain::AuditChainReport>;
async fn healthcheck(&self) -> Result<()>;
async fn pending_count(&self) -> Result<usize>;
async fn pending_count_for(&self, accounts: &[String]) -> Result<usize> {
let _ = accounts;
self.pending_count().await
}
async fn reconciliation_targets(&self, account: &str) -> Result<Vec<String>>;
async fn track_order(&self, account: &str, order_id: &str) -> Result<()>;
async fn record_order(&self, account: &str, order: serde_json::Value) -> Result<()>;
async fn operator_halt(&self) -> Result<Option<String>>;
async fn set_operator_halt(&self, reason: Option<String>, operator: &str) -> Result<()>;
}
#[async_trait::async_trait]
impl ExecutionStore for OrderJournal {
async fn reconciliation_targets(&self, account: &str) -> Result<Vec<String>> {
let account = account.to_owned();
self.database
.run(move |c| {
let mut stmt = c.prepare(SELECT_RECONCILIATION_TARGETS).map_err(sql)?;
stmt.query_map([account], |r| r.get(0))
.map_err(sql)?
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(sql)
})
.await
}
async fn track_order(&self, account: &str, order_id: &str) -> Result<()> {
if order_id.is_empty() || !order_id.bytes().all(|b| b.is_ascii_digit()) {
return Err(Error::InvalidParameter(
"Numeric broker order ID required".into(),
));
}
let account = account.to_owned();
let id = order_id.to_owned();
self.database
.run(move |c| {
c.execute(
"INSERT OR IGNORE INTO orders VALUES(?1,?2,0,?3)",
rusqlite::params![
account,
id,
serde_json::json!({"orderId":id,"status":"UNKNOWN"}).to_string()
],
)
.map_err(sql)?;
Ok(())
})
.await
}
async fn record_order(&self, account: &str, order: serde_json::Value) -> Result<()> {
if crate::reconciliation::broker_id(order.get("orderId")).is_none() {
return Err(Error::InvalidParameter(
"Broker order payload has no order ID".into(),
));
}
let account = account.to_owned();
self.database
.run(move |c| {
let tx = c
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.map_err(sql)?;
let mut activities = 0;
crate::reconciliation::persist_order(
&tx,
&account,
&order,
chrono::Utc::now().timestamp_millis(),
&mut activities,
)?;
tx.commit().map_err(sql)
})
.await
}
async fn operator_halt(&self) -> Result<Option<String>> {
self.database.run(|c| meta::get(c, OPERATOR_HALT_KEY)).await
}
async fn set_operator_halt(&self, reason: Option<String>, operator: &str) -> Result<()> {
if operator.trim().is_empty() || reason.as_ref().is_some_and(|r| r.trim().is_empty()) {
return Err(Error::InvalidParameter(
"Operator and halt reason must be nonempty".into(),
));
}
let operator = operator.to_owned();
self.database
.run(move |c| {
let tx = c
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.map_err(sql)?;
record_operator_halt(&tx, reason.as_deref(), &operator)?;
tx.commit().map_err(sql)
})
.await
}
async fn begin_once(
&self,
account: &str,
operation: &str,
request: serde_json::Value,
dedup_key: Option<String>,
) -> Result<String> {
OrderJournal::begin_once(self, account, operation, request, dedup_key).await
}
async fn finish(&self, id: &str, outcome: SubmissionOutcome) -> Result<()> {
OrderJournal::finish(self, id, outcome).await
}
async fn arm(&self, id: &str) -> Result<()> {
OrderJournal::arm(self, id).await
}
async fn records(&self) -> Result<Vec<SubmissionRecord>> {
OrderJournal::records(self).await
}
async fn unresolved_records(&self, account: &str) -> Result<Vec<SubmissionRecord>> {
let account = account.to_owned();
self.database
.run(move |c| query_submissions(c, SELECT_UNRESOLVED_SUBMISSIONS, [account]))
.await
}
async fn submission_by_tag(
&self,
account: &str,
tag: &str,
) -> Result<Option<SubmissionRecord>> {
let account = account.to_owned();
let tag = tag.to_owned();
self.database
.run(move |c| Ok(query_submissions(c, SELECT_SUBMISSION_BY_TAG, [account, tag])?.pop()))
.await
}
async fn tag_spent(&self, account: &str, tag: &str) -> Result<bool> {
let account = account.to_owned();
let tag = tag.to_owned();
self.database
.run(move |c| {
c.query_row(
"SELECT EXISTS(SELECT 1 FROM used_order_tags WHERE account=?1 AND tag=?2)",
[account, tag],
|r| r.get(0),
)
.map_err(sql)
})
.await
}
async fn forget_placeholder(&self, account: &str, order_id: &str) -> Result<bool> {
let account = account.to_owned();
let order_id = order_id.to_owned();
self.database
.run(move |c| {
let tx = c
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.map_err(sql)?;
let dropped = tx
.execute(DELETE_ORDER_PLACEHOLDER, [&account, &order_id])
.map_err(sql)?
== 1;
if dropped {
audit(
&tx,
"order_placeholder_dropped",
&order_id,
&serde_json::json!({"account": account}),
)?;
}
tx.commit().map_err(sql)?;
Ok(dropped)
})
.await
}
async fn invalidate_account(&self, account: &str, generation: u64) -> Result<()> {
self.database.invalidate_account(account, generation).await
}
async fn account_state(
&self,
account: &str,
) -> Result<Option<crate::reconciliation::AccountState>> {
self.database.account_state(account).await
}
async fn reconcile_snapshot(
&self,
account: &str,
generation: u64,
started_at: i64,
snapshot: serde_json::Value,
orders: Vec<serde_json::Value>,
) -> Result<crate::reconciliation::ReconciliationReport> {
self.database
.reconcile_snapshot(account, generation, started_at, snapshot, orders)
.await
}
async fn audit_page(&self, after: i64, limit: u32) -> Result<Vec<AuditEvent>> {
OrderJournal::audit_page(self, after, limit).await
}
async fn verify_audit(&self) -> Result<crate::audit_chain::AuditChainReport> {
OrderJournal::verify_audit(self).await
}
async fn pending_count(&self) -> Result<usize> {
self.database
.run(|c| {
c.query_row(
"SELECT COUNT(*) FROM submissions WHERE outcome IS NULL",
[],
|r| r.get::<_, i64>(0),
)
.map(|n| n as usize)
.map_err(sql)
})
.await
}
async fn pending_count_for(&self, accounts: &[String]) -> Result<usize> {
let accounts = serde_json::to_string(accounts)?;
self.database
.run(move |c| {
c.query_row(
"SELECT COUNT(*) FROM submissions WHERE outcome IS NULL \
AND account IN (SELECT value FROM json_each(?1))",
[accounts],
|r| r.get::<_, i64>(0),
)
.map(|n| n as usize)
.map_err(sql)
})
.await
}
async fn healthcheck(&self) -> Result<()> {
self.database
.run(|c| {
let tx = c
.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
.map_err(sql)?;
tx.commit().map_err(sql)
})
.await
}
}