Skip to main content

server_rpc/
convert.rs

1
2use std::borrow::Borrow;
3use std::convert::TryFrom;
4use std::time::Duration;
5
6use bitcoin::hashes::sha256;
7use bitcoin::secp256k1::{schnorr, PublicKey};
8use bitcoin::{self, Amount, FeeRate, OutPoint, ScriptBuf, Transaction, Txid};
9
10use ark::{musig, ProtocolEncoding, SignedVtxoRequest, Vtxo, VtxoId, VtxoPolicy, VtxoRequest};
11use ark::vtxo::policy::check_block_delta;
12use ark::arkoor::{ArkoorCosignRequest, ArkoorCosignResponse, ArkoorDestination};
13use ark::arkoor::package::{ArkoorPackageCosignRequest, ArkoorPackageCosignResponse};
14use ark::attestations::{
15	ArkoorCosignAttestation, DelegatedRoundParticipationAttestation, LightningReceiveAttestation, OffboardRequestAttestation, RoundAttemptAttestation, VtxoStatusAttestation
16};
17use ark::board::BoardCosignResponse;
18use ark::fees::PpmFeeRate;
19use ark::forfeit::HashLockedForfeitBundle;
20use ark::lightning::{PaymentHash, Preimage};
21use ark::mailbox::BlindedMailboxIdentifier;
22use ark::offboard::OffboardRequest;
23use ark::rounds::{Challenge, RoundId};
24use ark::tree::signed::{LeafVtxoCosignRequest, LeafVtxoCosignResponse, VtxoTreeSpec};
25use ark::vtxo::{Bare, Full, VtxoRef};
26
27use crate::protos;
28
29
30#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
31#[error("rpc conversion error: {msg}")]
32pub struct ConvertError {
33	pub msg: &'static str,
34}
35
36impl From<&'static str> for ConvertError {
37	fn from(msg: &'static str) -> ConvertError {
38		ConvertError { msg }
39	}
40}
41
42impl From<ConvertError> for tonic::Status {
43	fn from(e: ConvertError) -> Self {
44		tonic::Status::invalid_argument(e.msg)
45	}
46}
47
48/// Trait to convert some types from byte slices.
49pub trait TryFromBytes: Sized {
50	fn from_bytes<T: AsRef<[u8]>>(b: T) -> Result<Self, ConvertError>;
51}
52
53macro_rules! impl_try_from_byte_array {
54	($ty:path, $exp:expr) => {
55		impl TryFromBytes for $ty {
56			fn from_bytes<T: AsRef<[u8]>>(b: T) -> Result<Self, ConvertError> {
57				#[allow(unused)]
58				use bitcoin::hashes::Hash;
59
60				let array = TryFrom::try_from(b.as_ref())
61					.map_err(|_| concat!("invalid ", $exp))?;
62				Ok(<$ty>::from_byte_array(array))
63			}
64		}
65	};
66}
67impl_try_from_byte_array!(PaymentHash, "lightning payment hash");
68impl_try_from_byte_array!(Preimage, "lightning payment preimage");
69impl_try_from_byte_array!(sha256::Hash, "SHA-256 hash");
70impl_try_from_byte_array!(Txid, "transaction id");
71
72macro_rules! impl_try_from_byte_array_result {
73	($ty:path, $exp:expr) => {
74		impl TryFromBytes for $ty {
75			fn from_bytes<T: AsRef<[u8]>>(b: T) -> Result<Self, ConvertError> {
76				Ok(TryFrom::try_from(b.as_ref()).ok()
77					.and_then(|b| <$ty>::from_byte_array(b).ok())
78					.ok_or(concat!("invalid ", $exp))?)
79			}
80		}
81	};
82}
83impl_try_from_byte_array_result!(musig::PublicNonce, "public musig nonce");
84impl_try_from_byte_array_result!(musig::PartialSignature, "partial musig signature");
85impl_try_from_byte_array_result!(musig::AggregatedNonce, "aggregated musig nonce");
86
87macro_rules! impl_try_from_byte_slice {
88	($ty:path, $exp:expr) => {
89		impl TryFromBytes for $ty {
90			fn from_bytes<T: AsRef<[u8]>>(b: T) -> Result<Self, ConvertError> {
91				#[allow(unused)] // used for the impls of hash types
92				use bitcoin::hashes::Hash;
93
94				Ok(<$ty>::from_slice(b.as_ref()).map_err(|_| concat!("invalid ", $exp))?)
95			}
96		}
97	};
98}
99impl_try_from_byte_slice!(PublicKey, "public key");
100impl_try_from_byte_slice!(schnorr::Signature, "Schnorr signature");
101impl_try_from_byte_slice!(VtxoId, "VTXO ID");
102impl_try_from_byte_slice!(RoundId, "VTXO ID");
103
104macro_rules! impl_try_from_bytes_protocol {
105	($ty:path, $exp:expr) => {
106		impl TryFromBytes for $ty {
107			fn from_bytes<T: AsRef<[u8]>>(b: T) -> Result<Self, ConvertError> {
108				Ok(ProtocolEncoding::deserialize(b.as_ref())
109					.map_err(|_| concat!("invalid ", $exp))?)
110			}
111		}
112	};
113}
114impl_try_from_bytes_protocol!(OutPoint, "outpoint");
115impl_try_from_bytes_protocol!(Vtxo<Bare>, "bare VTXO");
116impl_try_from_bytes_protocol!(Vtxo<Full>, "full VTXO (with genesis)");
117impl_try_from_bytes_protocol!(VtxoPolicy, "VTXO policy");
118impl_try_from_bytes_protocol!(BlindedMailboxIdentifier, "a blinded VTXO mailbox identifier");
119impl_try_from_bytes_protocol!(HashLockedForfeitBundle, "hArk forfeit bundle");
120impl_try_from_bytes_protocol!(RoundAttemptAttestation, "round attempt attestation");
121impl_try_from_bytes_protocol!(DelegatedRoundParticipationAttestation,
122	"delegated round participation attestation"
123);
124impl_try_from_bytes_protocol!(LightningReceiveAttestation, "lightning receive attestation");
125impl_try_from_bytes_protocol!(VtxoStatusAttestation, "VTXO status attestation");
126impl_try_from_bytes_protocol!(OffboardRequestAttestation, "offboard request attestation");
127
128macro_rules! impl_try_from_bytes_bitcoin {
129	($ty:path, $exp:expr) => {
130		impl TryFromBytes for $ty {
131			fn from_bytes<T: AsRef<[u8]>>(b: T) -> Result<Self, ConvertError> {
132				Ok(bitcoin::consensus::encode::deserialize(b.as_ref())
133					.map_err(|_| concat!("invalid ", $exp))?)
134			}
135		}
136	};
137}
138impl_try_from_bytes_bitcoin!(Transaction, "bitcoin transaction");
139
140
141impl From<ark::ArkInfo> for protos::ArkInfo {
142	#[allow(deprecated)] // offboard_feerate_sat_vkb kept for old clients
143	fn from(v: ark::ArkInfo) -> Self {
144		protos::ArkInfo {
145			network: v.network.to_string(),
146			server_pubkey: v.server_pubkey.serialize().to_vec(),
147			mailbox_pubkey: v.mailbox_pubkey.serialize().to_vec(),
148			round_interval_secs: v.round_interval.as_secs() as u32,
149			nb_round_nonces: v.nb_round_nonces as u32,
150			vtxo_exit_delta: v.vtxo_exit_delta as u32,
151			vtxo_expiry_delta: v.vtxo_expiry_delta as u32,
152			htlc_send_expiry_delta: v.htlc_send_expiry_delta as u32,
153			htlc_expiry_delta: v.htlc_expiry_delta as u32,
154			max_vtxo_amount: v.max_vtxo_amount.map(|v| v.to_sat()),
155			required_board_confirmations: v.required_board_confirmations as u32,
156			max_user_invoice_cltv_delta: v.max_user_invoice_cltv_delta as u32,
157			min_board_amount: v.min_board_amount.to_sat(),
158			offboard_feerate_sat_vkb: v.offboard_feerate.to_sat_per_kwu() * 4,
159			max_offboard_inputs: v.max_offboard_inputs as u32,
160			ln_receive_anti_dos_required: v.ln_receive_anti_dos_required,
161			fees: Some(v.fees.into()),
162			max_vtxo_exit_depth: v.max_vtxo_exit_depth as u32,
163		}
164	}
165}
166
167impl TryFrom<protos::ArkInfo> for ark::ArkInfo {
168	type Error = ConvertError;
169	#[allow(deprecated)] // offboard_feerate_sat_vkb kept for old clients
170	fn try_from(v: protos::ArkInfo) -> Result<Self, Self::Error> {
171		Ok(ark::ArkInfo {
172			network: v.network.parse().map_err(|_| "invalid network")?,
173			server_pubkey: PublicKey::from_slice(&v.server_pubkey)
174				.map_err(|_| "invalid server pubkey")?,
175			mailbox_pubkey: PublicKey::from_slice(&v.mailbox_pubkey)
176				.map_err(|_| "invalid mailbox pubkey")?,
177			round_interval: Duration::from_secs(v.round_interval_secs as u64),
178			nb_round_nonces: v.nb_round_nonces as usize,
179			vtxo_exit_delta: check_block_delta(v.vtxo_exit_delta)
180				.map_err(|_| "invalid vtxo_exit_delta")?,
181			vtxo_expiry_delta: check_block_delta(v.vtxo_expiry_delta)
182				.map_err(|_| "invalid vtxo_expiry_delta")?,
183			htlc_send_expiry_delta: check_block_delta(v.htlc_send_expiry_delta)
184				.map_err(|_| "invalid htlc_send_expiry_delta")?,
185			htlc_expiry_delta: check_block_delta(v.htlc_expiry_delta)
186				.map_err(|_| "invalid htlc_expiry_delta")?,
187			max_vtxo_amount: v.max_vtxo_amount.map(|v| Amount::from_sat(v)),
188			required_board_confirmations: check_block_delta(v.required_board_confirmations)
189				.map_err(|_| "invalid required_board_confirmations")? as usize,
190			max_user_invoice_cltv_delta: check_block_delta(v.max_user_invoice_cltv_delta)
191				.map_err(|_| "invalid max_user_invoice_cltv_delta")?,
192			min_board_amount: Amount::from_sat(v.min_board_amount),
193			offboard_feerate: FeeRate::from_sat_per_kwu(v.offboard_feerate_sat_vkb / 4),
194			max_offboard_inputs: v.max_offboard_inputs as usize,
195			ln_receive_anti_dos_required: v.ln_receive_anti_dos_required,
196			fees: v.fees.ok_or("missing fees")?.try_into()?,
197			max_vtxo_exit_depth: v.max_vtxo_exit_depth.try_into()
198				.map_err(|_| "invalid max_vtxo_exit_depth")?,
199		})
200	}
201}
202
203impl From<ark::fees::PpmExpiryFeeEntry> for protos::PpmExpiryFeeEntry {
204	fn from(v: ark::fees::PpmExpiryFeeEntry) -> Self {
205		protos::PpmExpiryFeeEntry {
206			expiry_blocks_threshold: v.expiry_blocks_threshold,
207			ppm: v.ppm.0,
208		}
209	}
210}
211
212impl From<protos::PpmExpiryFeeEntry> for ark::fees::PpmExpiryFeeEntry {
213	fn from(v: protos::PpmExpiryFeeEntry) -> Self {
214		ark::fees::PpmExpiryFeeEntry {
215			expiry_blocks_threshold: v.expiry_blocks_threshold,
216			ppm: PpmFeeRate(v.ppm),
217		}
218	}
219}
220
221impl From<ark::fees::BoardFees> for protos::BoardFees {
222	fn from(v: ark::fees::BoardFees) -> Self {
223		protos::BoardFees {
224			min_fee_sat: v.min_fee.to_sat(),
225			base_fee_sat: v.base_fee.to_sat(),
226			ppm: v.ppm.0,
227		}
228	}
229}
230
231impl From<protos::BoardFees> for ark::fees::BoardFees {
232	fn from(v: protos::BoardFees) -> Self {
233		ark::fees::BoardFees {
234			min_fee: Amount::from_sat(v.min_fee_sat),
235			base_fee: Amount::from_sat(v.base_fee_sat),
236			ppm: PpmFeeRate(v.ppm),
237		}
238	}
239}
240
241impl From<ark::fees::OffboardFees> for protos::OffboardFees {
242	fn from(v: ark::fees::OffboardFees) -> Self {
243		protos::OffboardFees {
244			base_fee_sat: v.base_fee.to_sat(),
245			fixed_additional_vb: v.fixed_additional_vb,
246			ppm_expiry_table: v.ppm_expiry_table.into_iter().map(Into::into).collect(),
247		}
248	}
249}
250
251impl From<protos::OffboardFees> for ark::fees::OffboardFees {
252	fn from(v: protos::OffboardFees) -> Self {
253		ark::fees::OffboardFees {
254			base_fee: Amount::from_sat(v.base_fee_sat),
255			fixed_additional_vb: v.fixed_additional_vb,
256			ppm_expiry_table: v.ppm_expiry_table.into_iter().map(Into::into).collect(),
257		}
258	}
259}
260
261impl From<ark::fees::RefreshFees> for protos::RefreshFees {
262	fn from(v: ark::fees::RefreshFees) -> Self {
263		protos::RefreshFees {
264			base_fee_sat: v.base_fee.to_sat(),
265			ppm_expiry_table: v.ppm_expiry_table.into_iter().map(Into::into).collect(),
266		}
267	}
268}
269
270impl From<protos::RefreshFees> for ark::fees::RefreshFees {
271	fn from(v: protos::RefreshFees) -> Self {
272		ark::fees::RefreshFees {
273			base_fee: Amount::from_sat(v.base_fee_sat),
274			ppm_expiry_table: v.ppm_expiry_table.into_iter().map(Into::into).collect(),
275		}
276	}
277}
278
279impl From<ark::fees::LightningReceiveFees> for protos::LightningReceiveFees {
280	fn from(v: ark::fees::LightningReceiveFees) -> Self {
281		protos::LightningReceiveFees {
282			base_fee_sat: v.base_fee.to_sat(),
283			ppm: v.ppm.0,
284		}
285	}
286}
287
288impl From<protos::LightningReceiveFees> for ark::fees::LightningReceiveFees {
289	fn from(v: protos::LightningReceiveFees) -> Self {
290		ark::fees::LightningReceiveFees {
291			base_fee: Amount::from_sat(v.base_fee_sat),
292			ppm: PpmFeeRate(v.ppm),
293		}
294	}
295}
296
297impl From<ark::fees::LightningSendFees> for protos::LightningSendFees {
298	fn from(v: ark::fees::LightningSendFees) -> Self {
299		protos::LightningSendFees {
300			min_fee_sat: v.min_fee.to_sat(),
301			base_fee_sat: v.base_fee.to_sat(),
302			ppm_expiry_table: v.ppm_expiry_table.into_iter().map(Into::into).collect(),
303		}
304	}
305}
306
307impl From<protos::LightningSendFees> for ark::fees::LightningSendFees {
308	fn from(v: protos::LightningSendFees) -> Self {
309		ark::fees::LightningSendFees {
310			min_fee: Amount::from_sat(v.min_fee_sat),
311			base_fee: Amount::from_sat(v.base_fee_sat),
312			ppm_expiry_table: v.ppm_expiry_table.into_iter().map(Into::into).collect(),
313		}
314	}
315}
316
317impl From<ark::fees::FeeSchedule> for protos::FeeSchedule {
318	fn from(v: ark::fees::FeeSchedule) -> Self {
319		protos::FeeSchedule {
320			board: Some(v.board.into()),
321			offboard: Some(v.offboard.into()),
322			refresh: Some(v.refresh.into()),
323			lightning_receive: Some(v.lightning_receive.into()),
324			lightning_send: Some(v.lightning_send.into()),
325		}
326	}
327}
328
329impl TryFrom<protos::FeeSchedule> for ark::fees::FeeSchedule {
330	type Error = ConvertError;
331	fn try_from(v: protos::FeeSchedule) -> Result<Self, Self::Error> {
332		Ok(ark::fees::FeeSchedule {
333			board: v.board.ok_or("missing board fees")?.into(),
334			offboard: v.offboard.ok_or("missing offboard fees")?.into(),
335			refresh: v.refresh.ok_or("missing refresh fees")?.into(),
336			lightning_receive: v.lightning_receive.ok_or("missing lightning receive fees")?.into(),
337			lightning_send: v.lightning_send.ok_or("missing lightning send fees")?.into(),
338		})
339	}
340}
341
342impl<'a> From<&'a ark::rounds::RoundEvent> for protos::RoundEvent {
343	fn from(e: &'a ark::rounds::RoundEvent) -> Self {
344		protos::RoundEvent {
345			event: Some(match e {
346				ark::rounds::RoundEvent::Attempt(ark::rounds::RoundAttempt {
347					round_seq, attempt_seq, challenge,
348				}) => {
349					protos::round_event::Event::Attempt(protos::RoundAttempt {
350						round_seq: (*round_seq).into(),
351						attempt_seq: *attempt_seq as u64,
352						round_attempt_challenge: challenge.inner().to_vec(),
353					})
354				},
355				ark::rounds::RoundEvent::VtxoProposal(ark::rounds::VtxoProposal {
356					round_seq, attempt_seq, vtxos_spec, unsigned_round_tx, cosign_agg_nonces,
357				}) => {
358					protos::round_event::Event::VtxoProposal(protos::VtxoProposal {
359						round_seq: (*round_seq).into(),
360						attempt_seq: *attempt_seq as u64,
361						vtxos_spec: vtxos_spec.serialize(),
362						unsigned_round_tx: bitcoin::consensus::serialize(&unsigned_round_tx),
363						vtxos_agg_nonces: cosign_agg_nonces.into_iter()
364							.map(|n| n.serialize().to_vec())
365							.collect(),
366					})
367				},
368				ark::rounds::RoundEvent::Finished(ark::rounds::RoundFinished {
369					round_seq, attempt_seq, cosign_sigs, signed_round_tx,
370				}) => {
371					protos::round_event::Event::Finished(protos::RoundFinished {
372						round_seq: (*round_seq).into(),
373						attempt_seq: *attempt_seq as u64,
374						vtxo_cosign_signatures: cosign_sigs.into_iter()
375							.map(|s| s.serialize().to_vec()).collect(),
376						signed_round_tx: bitcoin::consensus::serialize(&signed_round_tx),
377					})
378				},
379				ark::rounds::RoundEvent::Failed(ark::rounds::RoundFailed {
380					round_seq,
381				}) => {
382					protos::round_event::Event::Failed(protos::RoundFailed {
383						round_seq: (*round_seq).into(),
384					})
385				},
386			})
387		}
388	}
389}
390
391impl TryFrom<protos::RoundEvent> for ark::rounds::RoundEvent {
392	type Error = ConvertError;
393
394	fn try_from(m: protos::RoundEvent) -> Result<ark::rounds::RoundEvent, Self::Error> {
395		Ok(match m.event.ok_or("unknown round event")? {
396			protos::round_event::Event::Attempt(m) => {
397				ark::rounds::RoundEvent::Attempt(ark::rounds::RoundAttempt {
398					round_seq: m.round_seq.into(),
399					attempt_seq: m.attempt_seq as usize,
400					challenge: Challenge::new(
401						m.round_attempt_challenge.try_into().map_err(|_| "invalid challenge")?
402					),
403				})
404			},
405			protos::round_event::Event::VtxoProposal(m) => {
406				ark::rounds::RoundEvent::VtxoProposal(ark::rounds::VtxoProposal {
407					round_seq: m.round_seq.into(),
408					attempt_seq: m.attempt_seq as usize,
409					unsigned_round_tx: bitcoin::consensus::deserialize(&m.unsigned_round_tx)
410						.map_err(|_| "invalid unsigned_round_tx")?,
411					vtxos_spec: VtxoTreeSpec::deserialize(&m.vtxos_spec)
412						.map_err(|_| "invalid vtxos_spec")?,
413					cosign_agg_nonces: m.vtxos_agg_nonces.into_iter().map(|n| {
414						musig::AggregatedNonce::from_bytes(&n)
415					}).collect::<Result<_, _>>()?,
416				})
417			},
418			protos::round_event::Event::Finished(m) => {
419				ark::rounds::RoundEvent::Finished(ark::rounds::RoundFinished {
420					round_seq: m.round_seq.into(),
421					attempt_seq: m.attempt_seq as usize,
422					cosign_sigs: m.vtxo_cosign_signatures.into_iter().map(|s| {
423						schnorr::Signature::from_slice(&s)
424							.map_err(|_| "invalid vtxo_cosign_signatures")
425					}).collect::<Result<_, _>>()?,
426					signed_round_tx: bitcoin::consensus::deserialize(&m.signed_round_tx)
427						.map_err(|_| "invalid signed_round_tx")?,
428				})
429			},
430			protos::round_event::Event::Failed(m) => {
431				ark::rounds::RoundEvent::Failed(ark::rounds::RoundFailed {
432					round_seq: m.round_seq.into(),
433				})
434			},
435		})
436	}
437}
438
439impl From<crate::WalletStatus> for protos::WalletStatus {
440	fn from(s: crate::WalletStatus) -> Self {
441		protos::WalletStatus {
442			address: s.address.assume_checked().to_string(),
443			total_balance: s.total_balance.to_sat(),
444			trusted_balance: s.trusted_balance.to_sat(),
445			untrusted_balance: s.untrusted_balance.to_sat(),
446			confirmed_utxos: s.confirmed_utxos.iter().map(|u| u.to_string()).collect(),
447			unconfirmed_utxos: s.unconfirmed_utxos.iter().map(|u| u.to_string()).collect(),
448		}
449	}
450}
451
452impl TryFrom<protos::WalletStatus> for crate::WalletStatus {
453	type Error = ConvertError;
454	fn try_from(s: protos::WalletStatus) -> Result<Self, Self::Error> {
455		Ok(crate::WalletStatus {
456			address: s.address.parse().map_err(|_| "invalid address")?,
457			total_balance: Amount::from_sat(s.total_balance),
458			trusted_balance: Amount::from_sat(s.trusted_balance),
459			untrusted_balance: Amount::from_sat(s.untrusted_balance),
460			confirmed_utxos: s.confirmed_utxos.iter().map(|u| {
461				u.parse().map_err(|_| "invalid outpoint")
462			}).collect::<Result<_, _>>()?,
463			unconfirmed_utxos: s.unconfirmed_utxos.iter().map(|u| {
464				u.parse().map_err(|_| "invalid outpoint")
465			}).collect::<Result<_, _>>()?,
466		})
467	}
468}
469
470
471impl<'a> From<&'a VtxoRequest> for protos::VtxoRequest {
472	fn from(v: &'a VtxoRequest) -> Self {
473		protos::VtxoRequest {
474			amount: v.amount.to_sat(),
475			policy: v.policy.serialize(),
476		}
477	}
478}
479
480impl TryFrom<protos::VtxoRequest> for VtxoRequest {
481	type Error = ConvertError;
482	fn try_from(v: protos::VtxoRequest) -> Result<Self, Self::Error> {
483		Ok(Self {
484			amount: Amount::from_sat(v.amount),
485			policy: VtxoPolicy::deserialize(&v.policy).map_err(|_| "invalid policy")?,
486		})
487	}
488}
489
490impl TryFrom<protos::ArkoorDestination> for ArkoorDestination {
491	type Error = ConvertError;
492	fn try_from(v: protos::ArkoorDestination) -> Result<Self, Self::Error> {
493		Ok(Self {
494			total_amount: Amount::from_sat(v.total_amount),
495			policy: VtxoPolicy::deserialize(&v.policy).map_err(|_| "invalid policy")?,
496		})
497	}
498}
499
500impl From<ArkoorDestination> for protos::ArkoorDestination {
501	fn from(v: ArkoorDestination) -> Self {
502		Self {
503			total_amount: v.total_amount.to_sat(),
504			policy: v.policy.serialize(),
505		}
506	}
507}
508
509impl From<SignedVtxoRequest> for protos::SignedVtxoRequest {
510	fn from(v: SignedVtxoRequest) -> Self {
511		protos::SignedVtxoRequest {
512			vtxo: Some(protos::VtxoRequest {
513				amount: v.vtxo.amount.to_sat(),
514				policy: v.vtxo.policy.serialize(),
515			}),
516			cosign_pubkey: v.cosign_pubkey.serialize().to_vec(),
517			public_nonces: v.nonces.iter().map(|n| n.serialize().to_vec()).collect(),
518		}
519	}
520}
521
522impl TryFrom<protos::SignedVtxoRequest> for SignedVtxoRequest {
523	type Error = ConvertError;
524	fn try_from(v: protos::SignedVtxoRequest) -> Result<Self, Self::Error> {
525		let vtxo = v.vtxo.ok_or("vtxo field missing")?;
526		Ok(SignedVtxoRequest {
527			vtxo: VtxoRequest {
528				amount: Amount::from_sat(vtxo.amount),
529				policy: VtxoPolicy::from_bytes(&vtxo.policy)?,
530			},
531			cosign_pubkey: PublicKey::from_bytes(&v.cosign_pubkey)?,
532			nonces: v.public_nonces.into_iter()
533				.map(|n| musig::PublicNonce::from_bytes(n))
534				.collect::<Result<_, _>>()?,
535		})
536	}
537}
538
539impl From<BoardCosignResponse> for protos::BoardCosignResponse {
540	fn from(v: BoardCosignResponse) -> Self {
541		Self {
542			pub_nonce: v.pub_nonce.serialize().to_vec(),
543			partial_sig: v.partial_signature.serialize().to_vec(),
544		}
545	}
546}
547
548impl TryFrom<protos::BoardCosignResponse> for BoardCosignResponse {
549	type Error = ConvertError;
550	fn try_from(v: protos::BoardCosignResponse) -> Result<Self, Self::Error> {
551		Ok(Self {
552			pub_nonce: musig::PublicNonce::from_bytes(&v.pub_nonce)?,
553			partial_signature: musig::PartialSignature::from_bytes(&v.partial_sig)?,
554		})
555	}
556}
557
558impl From<ark::integration::TokenType> for protos::intman::TokenType {
559	fn from(value: ark::integration::TokenType) -> Self {
560		match value {
561			ark::integration::TokenType::SingleUseBoard => protos::intman::TokenType::SingleUseBoard,
562		}
563	}
564}
565
566impl From<protos::intman::TokenType> for ark::integration::TokenType {
567	fn from(value: protos::intman::TokenType) -> Self {
568		match value {
569			protos::intman::TokenType::SingleUseBoard => ark::integration::TokenType::SingleUseBoard,
570		}
571	}
572}
573
574impl From<protos::intman::TokenStatus> for ark::integration::TokenStatus {
575	fn from(value: protos::intman::TokenStatus) -> Self {
576		match value {
577			protos::intman::TokenStatus::Unused => ark::integration::TokenStatus::Unused,
578			protos::intman::TokenStatus::Used => ark::integration::TokenStatus::Used,
579			protos::intman::TokenStatus::Abused => ark::integration::TokenStatus::Abused,
580			protos::intman::TokenStatus::Disabled => ark::integration::TokenStatus::Disabled,
581			// Setting to `Unused` since this status is an alias for `Unused` + expired.
582			protos::intman::TokenStatus::Expired => ark::integration::TokenStatus::Unused,
583		}
584	}
585}
586
587impl From<ark::integration::TokenStatus> for protos::intman::TokenStatus {
588	fn from(value: ark::integration::TokenStatus) -> Self {
589		match value {
590			ark::integration::TokenStatus::Unused => protos::intman::TokenStatus::Unused,
591			ark::integration::TokenStatus::Used => protos::intman::TokenStatus::Used,
592			ark::integration::TokenStatus::Abused => protos::intman::TokenStatus::Abused,
593			ark::integration::TokenStatus::Disabled => protos::intman::TokenStatus::Disabled,
594		}
595	}
596}
597
598// Serialization of a CosignRequest
599impl<V: VtxoRef> From<ArkoorCosignRequest<V>> for protos::ArkoorCosignRequest {
600	fn from(v: ArkoorCosignRequest<V>) -> Self {
601		Self {
602			input_vtxo_id: v.input.vtxo_id().serialize(),
603			user_pub_nonces: v.user_pub_nonces.into_iter()
604				.map(|n| n.serialize().to_vec())
605				.collect::<Vec<_>>(),
606			outputs: v.outputs.into_iter().map(|output| output.into()).collect::<Vec<_>>(),
607			isolated_outputs: v.isolated_outputs.into_iter()
608				.map(|output| output.into())
609				.collect::<Vec<_>>(),
610			use_checkpoint: v.use_checkpoint,
611			attestation: v.attestation.serialize().to_vec(),
612		}
613	}
614}
615
616// Deserialization of CosignRequest
617impl TryFrom<protos::ArkoorCosignRequest> for ArkoorCosignRequest<VtxoId> {
618	type Error = ConvertError;
619	fn try_from(v: protos::ArkoorCosignRequest) -> Result<Self, Self::Error> {
620		let req = Self::new_with_attestation(
621			v.user_pub_nonces.into_iter()
622				.map(|n| musig::PublicNonce::from_bytes(&n))
623				.collect::<Result<Vec<_>, _>>()?,
624			VtxoId::from_bytes(&v.input_vtxo_id)?,
625			v.outputs.into_iter()
626				.map(|output| ArkoorDestination::try_from(output))
627				.collect::<Result<Vec<_>, _>>()?,
628			v.isolated_outputs.into_iter()
629				.map(|output| ArkoorDestination::try_from(output))
630				.collect::<Result<Vec<_>, _>>()?,
631			v.use_checkpoint,
632			ArkoorCosignAttestation::deserialize(&v.attestation)
633				.map_err(|_| "Failed to parse attestation")?,
634		);
635		Ok(req)
636	}
637}
638
639impl<V: VtxoRef> From<ArkoorPackageCosignRequest<V>> for protos::ArkoorPackageCosignRequest {
640	fn from(v: ArkoorPackageCosignRequest<V>) -> Self {
641		Self {
642			parts: v.requests.into_iter().map(|p| p.into()).collect(),
643		}
644	}
645}
646
647impl<'a> TryFrom<protos::ArkoorPackageCosignRequest> for ArkoorPackageCosignRequest<VtxoId> {
648	type Error = ConvertError;
649
650	fn try_from(v: protos::ArkoorPackageCosignRequest) -> Result<Self, Self::Error> {
651		Ok(Self {
652			requests: v.parts.into_iter().map(|p| p.try_into()).collect::<Result<Vec<_>, _>>()?,
653		})
654	}
655}
656
657impl<'a> TryFrom<protos::LightningPayHtlcCosignRequest> for ArkoorPackageCosignRequest<VtxoId> {
658	type Error = ConvertError;
659
660	fn try_from(v: protos::LightningPayHtlcCosignRequest) -> Result<Self, Self::Error> {
661		Ok(Self {
662			requests: v.parts.into_iter().map(|p| p.try_into()).collect::<Result<Vec<_>, _>>()?,
663		})
664	}
665}
666
667impl From<ArkoorCosignResponse> for protos::ArkoorCosignResponse {
668	fn from(v: ArkoorCosignResponse) -> Self {
669		Self {
670			server_pub_nonces: v.server_pub_nonces.into_iter().map(|p| p.serialize().to_vec()).collect::<Vec<_>>(),
671			server_partial_sigs: v.server_partial_sigs.into_iter().map(|p| p.serialize().to_vec()).collect::<Vec<_>>(),
672		}
673	}
674}
675
676impl TryFrom<protos::ArkoorCosignResponse> for ArkoorCosignResponse {
677	type Error = ConvertError;
678	fn try_from(v: protos::ArkoorCosignResponse) -> Result<Self, Self::Error> {
679		Ok(Self {
680			server_pub_nonces: v.server_pub_nonces.into_iter().map(|n| musig::PublicNonce::from_bytes(&n)).collect::<Result<Vec<_>, _>>()?,
681			server_partial_sigs: v.server_partial_sigs.into_iter().map(|n| musig::PartialSignature::from_bytes(&n)).collect::<Result<Vec<_>, _>>()?,
682		})
683	}
684}
685
686impl From<ArkoorPackageCosignResponse> for protos::ArkoorPackageCosignResponse {
687	fn from(v: ArkoorPackageCosignResponse) -> Self {
688		Self {
689			parts: v.responses.into_iter().map(|p| p.into()).collect::<Vec<_>>(),
690		}
691	}
692}
693
694impl TryFrom<protos::ArkoorPackageCosignResponse> for ArkoorPackageCosignResponse {
695	type Error = ConvertError;
696	fn try_from(v: protos::ArkoorPackageCosignResponse) -> Result<Self, Self::Error> {
697		Ok(Self {
698			responses: v.parts.into_iter().map(|p| p.try_into()).collect::<Result<Vec<_>, _>>()?,
699		})
700	}
701}
702
703impl From<LeafVtxoCosignRequest> for protos::LeafVtxoCosignRequest {
704	fn from(v: LeafVtxoCosignRequest) -> Self {
705		protos::LeafVtxoCosignRequest {
706			vtxo_id: v.vtxo_id.to_bytes().to_vec(),
707			public_nonce: v.pub_nonce.serialize().to_vec(),
708		}
709	}
710}
711
712impl From<LeafVtxoCosignResponse> for protos::LeafVtxoCosignResponse {
713	fn from(v: LeafVtxoCosignResponse) -> Self {
714		protos::LeafVtxoCosignResponse {
715			public_nonce: v.public_nonce.serialize().to_vec(),
716			partial_signature: v.partial_signature.serialize().to_vec(),
717		}
718	}
719}
720
721impl TryFrom<protos::LeafVtxoCosignResponse> for LeafVtxoCosignResponse {
722	type Error = ConvertError;
723
724	fn try_from(v: protos::LeafVtxoCosignResponse) -> Result<Self, Self::Error> {
725		Ok(Self {
726			public_nonce: TryFromBytes::from_bytes(v.public_nonce)?,
727			partial_signature: TryFromBytes::from_bytes(v.partial_signature)?,
728		})
729	}
730}
731
732impl<V: Borrow<OffboardRequest>> From<V> for protos::OffboardRequest {
733	fn from(v: V) -> Self {
734		let v = v.borrow();
735	    protos::OffboardRequest {
736			offboard_spk: v.script_pubkey.to_bytes(),
737			net_amount_sat: v.net_amount.to_sat(),
738			deduct_fees_from_gross_amount: v.deduct_fees_from_gross_amount,
739			fee_rate_kwu: v.fee_rate.to_sat_per_kwu(),
740		}
741	}
742}
743
744impl TryFrom<protos::OffboardRequest> for OffboardRequest {
745	type Error = ConvertError;
746
747	fn try_from(v: protos::OffboardRequest) -> Result<Self, Self::Error> {
748		Ok(Self {
749			script_pubkey: ScriptBuf::from_bytes(v.offboard_spk),
750			net_amount: Amount::from_sat(v.net_amount_sat),
751			deduct_fees_from_gross_amount: v.deduct_fees_from_gross_amount,
752			fee_rate: FeeRate::from_sat_per_kwu(v.fee_rate_kwu),
753		})
754	}
755}
756
757#[cfg(test)]
758mod test {
759	use std::str::FromStr;
760	use bitcoin::hex::FromHex;
761	use super::*;
762
763	#[test]
764	fn test_preimage_bytes() {
765		let h = "ef2cb05d04819ddb2b9d960c7e0e295ea48ffb429712dc8f30aa48dfcc20c97e";
766		let b = Vec::<u8>::from_hex(h).unwrap();
767
768		let preimage = Preimage::from_str(h).unwrap();
769		assert_eq!(preimage, Preimage::from_bytes(&b).unwrap());
770		assert_eq!(preimage, Preimage::from_slice(&b).unwrap());
771		assert_eq!(preimage, Preimage::from_slice(&preimage.to_vec()).unwrap());
772		assert_eq!(preimage, Preimage::from_bytes(&preimage.to_vec()).unwrap());
773	}
774
775	/// Baseline proto with valid network and pubkeys, so TryFrom proceeds far
776	/// enough to exercise the delta validators. Fields after the deltas may be
777	/// invalid; these tests only assert delta-rejection short-circuits.
778	fn baseline_ark_info_proto() -> protos::ArkInfo {
779		let pk = PublicKey::from_str(
780			"02dfa52f6690299d2d6a08323083e290597b56fee125063e5f4e2957731639c42c",
781		).unwrap();
782		protos::ArkInfo {
783			network: "regtest".into(),
784			server_pubkey: pk.serialize().to_vec(),
785			mailbox_pubkey: pk.serialize().to_vec(),
786			round_interval_secs: 60,
787			nb_round_nonces: 1,
788			vtxo_exit_delta: 12,
789			vtxo_expiry_delta: 100,
790			htlc_send_expiry_delta: 100,
791			htlc_expiry_delta: 6,
792			max_vtxo_amount: None,
793			required_board_confirmations: 1,
794			max_user_invoice_cltv_delta: 50,
795			min_board_amount: 0,
796			ln_receive_anti_dos_required: false,
797			#[allow(deprecated)]
798			offboard_feerate_sat_vkb: 1000,
799			fees: None,
800			max_vtxo_exit_depth: 5,
801			max_offboard_inputs: 10,
802		}
803	}
804
805	#[test]
806	fn ark_info_rejects_oversized_vtxo_exit_delta() {
807		let mut proto = baseline_ark_info_proto();
808		proto.vtxo_exit_delta = ark::vtxo::policy::MAX_BLOCK_DELTA as u32 + 1;
809		assert!(ark::ArkInfo::try_from(proto).is_err());
810	}
811
812	#[test]
813	fn ark_info_rejects_oversized_htlc_expiry_delta() {
814		let mut proto = baseline_ark_info_proto();
815		proto.htlc_expiry_delta = ark::vtxo::policy::MAX_BLOCK_DELTA as u32 + 1;
816		assert!(ark::ArkInfo::try_from(proto).is_err());
817	}
818
819	#[test]
820	fn ark_info_rejects_oversized_max_user_invoice_cltv_delta() {
821		let mut proto = baseline_ark_info_proto();
822		proto.max_user_invoice_cltv_delta = ark::vtxo::policy::MAX_BLOCK_DELTA as u32 + 1;
823		assert!(ark::ArkInfo::try_from(proto).is_err());
824	}
825
826	#[test]
827	fn ark_info_rejects_oversized_required_board_confirmations() {
828		// A malicious server could otherwise push this near u32::MAX and overflow
829		// the client's `current_height + required` board arithmetic.
830		let mut proto = baseline_ark_info_proto();
831		proto.required_board_confirmations = ark::vtxo::policy::MAX_BLOCK_DELTA as u32 + 1;
832		assert!(ark::ArkInfo::try_from(proto).is_err());
833	}
834
835	#[test]
836	fn ark_info_rejects_u32_max_delta() {
837		// Pre-MR a u32 value above u16::MAX would have failed try_into; verify
838		// the new validator still rejects values that exceed the policy bound.
839		let mut proto = baseline_ark_info_proto();
840		proto.htlc_expiry_delta = u32::MAX;
841		assert!(ark::ArkInfo::try_from(proto).is_err());
842	}
843}