1use crate::config::Limits;
2use crate::config::RELAY_MAX_BUDGET_SATS;
3use crate::error::{Context, Error, bail, ensure};
4use crate::http;
5use crate::transaction::decode;
6use crate::{
7 journal::{self, Plan},
8 source::Source,
9};
10use bitcoin::{Network, Txid};
11use serde_json::{Value, json};
12use std::str::FromStr;
13
14#[derive(Debug, PartialEq, Eq)]
15enum Step {
16 Commit,
17 Reveals(Vec<usize>),
18 Wait,
19 Complete,
20}
21
22fn authorize(
23 network: Network,
24 planned_fee: u64,
25 budget: u64,
26 accept_provider_trust: bool,
27) -> Result<(), Error> {
28 ensure!(
29 matches!(network, Network::Testnet4 | Network::Regtest),
30 "relay supports only testnet4/regtest, never mainnet"
31 );
32 ensure!(
33 accept_provider_trust,
34 "relay requires explicit acceptance of provider chain-selection/unspentness trust"
35 );
36 ensure!(
37 (1..=RELAY_MAX_BUDGET_SATS).contains(&budget) && planned_fee <= budget,
38 "journal exceeds explicit relay fee budget"
39 );
40 Ok(())
41}
42
43fn missing_reveals(status: &Value) -> Result<Vec<usize>, Error> {
44 let reveals = status["reveal_statuses"]
45 .as_array()
46 .context("missing reveal statuses")?;
47 ensure!(
48 !reveals.is_empty() && reveals.len() <= Limits::RECORDS,
49 "invalid reveal status count"
50 );
51 let mut missing = Vec::new();
52 for (index, reveal) in reveals.iter().enumerate() {
53 match reveal["state"].as_str().context("missing reveal state")? {
54 "unknown" => missing.push(index),
55 "pending" | "confirmed" => (),
56 _ => bail!("invalid reveal state"),
57 }
58 }
59 Ok(missing)
60}
61
62fn next_step(status: &Value, allow_unconfirmed_commit: bool) -> Result<Step, Error> {
63 match status["status"].as_str().context("missing source status")? {
64 "prepared" => Ok(Step::Commit),
65 "commit_pending" if allow_unconfirmed_commit => {
66 ensure!(
67 status["commit_status"]["state"] == "pending",
68 "unconfirmed reveal requires a known pending commit"
69 );
70 let missing = missing_reveals(status)?;
71 Ok(if missing.is_empty() {
72 Step::Wait
73 } else {
74 Step::Reveals(missing)
75 })
76 }
77 "commit_pending" | "confirming" => Ok(Step::Wait),
78 "confirmed" => Ok(Step::Complete),
79 "awaiting_reveals" => {
80 ensure!(
81 status["commit_status"]["state"] == "confirmed",
82 "cannot reveal before commit confirmation"
83 );
84 Ok(Step::Reveals(missing_reveals(status)?))
85 }
86 _ => bail!("unsupported or inconsistent relay state"),
87 }
88}
89
90pub fn broadcast_plan(
91 url: &str,
92 network: Network,
93 plan: &Plan,
94 budget: u64,
95 accept_provider_trust: bool,
96 allow_unconfirmed_commit: bool,
97) -> Result<Value, Error> {
98 authorize(network, plan.fee_sats, budget, accept_provider_trust)?;
99 journal::validate_plan(plan)?;
100 ensure!(
101 plan.network == network.to_string(),
102 "relay network differs from journal"
103 );
104 let source = Source::connect(url, &network)?;
105 let before = source.status_plan(plan)?;
106 let step = next_step(&before, allow_unconfirmed_commit)?;
107 let mut submitted = Vec::new();
108 let action = match step {
109 Step::Commit => {
110 source.confirmed_funding(plan)?;
111 submitted.push(post_transaction(source.endpoint(), &plan.commit)?);
112 "commit_submitted"
113 }
114 Step::Reveals(indices) => {
115 let commit = decode(&plan.commit, plan.commit.len())?;
116 for index in indices {
117 ensure!(
118 index < plan.reveals.len(),
119 "source reveal index out of range"
120 );
121 source.require_unspent(commit.compute_txid(), u32::try_from(index)?)?;
122 submitted.push(post_transaction(source.endpoint(), &plan.reveals[index])?);
123 }
124 "reveals_submitted"
125 }
126 Step::Wait => "waiting_for_confirmation",
127 Step::Complete => "already_confirmed",
128 };
129 Ok(
130 json!({"action": action, "submitted_txids": submitted, "network": network.to_string(),
131 "fee_budget_sats": budget, "planned_fee_sats": plan.fee_sats, "before": before,
132 "allow_unconfirmed_commit": allow_unconfirmed_commit,
133 "note": "POST acceptance is not confirmation. Query status before advancing. Provider trusted for chain selection and unspentness."}),
134 )
135}
136
137fn post_transaction(endpoint: &str, raw: &str) -> Result<String, Error> {
138 ensure!(
139 raw.len() <= 8_000_000,
140 "transaction exceeds relay byte limit"
141 );
142 let tx = decode(raw, 8_000_000)?;
143 let expected = tx.compute_txid();
144 let response = minreq::post(format!("{endpoint}/tx"))
145 .with_timeout(20)
146 .with_max_redirects(0)
147 .with_max_headers_size(16_384)
148 .with_max_status_line_length(1_024)
149 .with_header("Content-Type", "text/plain")
150 .with_body(raw)
151 .send_lazy()
152 .context("relay POST failed; submission may be uncertain, check status before retry")?;
153 let status = response.status_code;
154 let body = http::body(response, 1024)?;
155 ensure!(
156 body.len() <= 1024,
157 "relay response exceeds byte limit; check transaction status"
158 );
159 ensure!(status == 200, "relay HTTP {status}: {}", hex::encode(&body));
160 let returned = Txid::from_str(std::str::from_utf8(&body)?.trim())
161 .context("invalid relay txid; check transaction status")?;
162 ensure!(
163 returned == expected,
164 "relay returned a different txid; check transaction status"
165 );
166 Ok(expected.to_string())
167}