#![cfg(feature = "network")]
use std::sync::Arc;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::auth::utils::nonce::create_nonce;
use crate::primitives::public_key::PublicKey;
use crate::remittance::error::RemittanceError;
use crate::remittance::remittance_module::{
AcceptSettlementResult, BuildSettlementResult, RemittanceModule,
};
use crate::remittance::types::{Invoice, ModuleContext, Settlement, Termination};
use crate::script::templates::p2pkh::P2PKH;
use crate::script::ScriptTemplateLock;
use crate::wallet::interfaces::{
BasketInsertion, CreateActionArgs, CreateActionOptions, CreateActionOutput, GetPublicKeyArgs,
InternalizeActionArgs, InternalizeOutput, Payment, WalletInterface,
};
use crate::wallet::types::{BooleanDefaultTrue, Counterparty, CounterpartyType, Protocol};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum InternalizeProtocol {
#[serde(rename = "wallet payment")]
WalletPayment,
#[serde(rename = "basket insertion")]
BasketInsertion,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Brc29OptionTerms {
pub amount_satoshis: u64,
pub payee: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_index: Option<u32>,
#[serde(rename = "protocolID", skip_serializing_if = "Option::is_none")]
pub protocol_id: Option<Protocol>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Brc29SettlementCustomInstructions {
pub derivation_prefix: String,
pub derivation_suffix: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Brc29SettlementArtifact {
pub custom_instructions: Brc29SettlementCustomInstructions,
pub transaction: Vec<u8>,
pub amount_satoshis: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_index: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Brc29ReceiptData {
#[serde(skip_serializing_if = "Option::is_none")]
pub internalize_result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rejected_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refund: Option<Brc29RefundData>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Brc29RefundData {
pub token: Brc29SettlementArtifact,
pub fee_satoshis: u64,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CustomInstructionsPayload<'a> {
derivation_prefix: &'a str,
derivation_suffix: &'a str,
payee: &'a str,
thread_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
note: Option<&'a str>,
}
#[async_trait]
pub trait NonceProvider: Send + Sync {
async fn create_nonce(
&self,
wallet: &Arc<dyn WalletInterface>,
originator: Option<&str>,
) -> Result<String, RemittanceError>;
}
pub struct DefaultNonceProvider;
#[async_trait]
impl NonceProvider for DefaultNonceProvider {
async fn create_nonce(
&self,
wallet: &Arc<dyn WalletInterface>,
_originator: Option<&str>,
) -> Result<String, RemittanceError> {
create_nonce(wallet.as_ref())
.await
.map_err(RemittanceError::from)
}
}
#[async_trait]
pub trait LockingScriptProvider: Send + Sync {
async fn get_locking_script(&self, public_key_hex: &str) -> Result<String, RemittanceError>;
}
pub struct DefaultLockingScriptProvider;
#[async_trait]
impl LockingScriptProvider for DefaultLockingScriptProvider {
async fn get_locking_script(&self, public_key_hex: &str) -> Result<String, RemittanceError> {
let pk = PublicKey::from_string(public_key_hex)
.map_err(|e| RemittanceError::Protocol(format!("invalid public key: {e}")))?;
let hash_vec = pk.to_hash(); let mut hash = [0u8; 20];
hash.copy_from_slice(&hash_vec);
let p2pkh = P2PKH::from_public_key_hash(hash);
let lock_script = p2pkh
.lock()
.map_err(|e| RemittanceError::Protocol(format!("P2PKH lock error: {e}")))?;
Ok(lock_script.to_hex())
}
}
pub struct Brc29RemittanceModuleConfig {
pub protocol_id: Protocol,
pub labels: Vec<String>,
pub description: String,
pub output_description: String,
pub nonce_provider: Arc<dyn NonceProvider>,
pub locking_script_provider: Arc<dyn LockingScriptProvider>,
pub refund_fee_satoshis: u64,
pub min_refund_satoshis: u64,
pub internalize_protocol: InternalizeProtocol,
}
impl Default for Brc29RemittanceModuleConfig {
fn default() -> Self {
Self {
protocol_id: Protocol {
security_level: 2,
protocol: "3241645161d8".to_string(),
},
labels: vec!["brc29".to_string()],
description: "BRC-29 payment".to_string(),
output_description: "Payment for remittance invoice".to_string(),
nonce_provider: Arc::new(DefaultNonceProvider),
locking_script_provider: Arc::new(DefaultLockingScriptProvider),
refund_fee_satoshis: 1000,
min_refund_satoshis: 1000,
internalize_protocol: InternalizeProtocol::WalletPayment,
}
}
}
pub struct Brc29RemittanceModule {
config: Brc29RemittanceModuleConfig,
}
impl Brc29RemittanceModule {
pub fn new(config: Brc29RemittanceModuleConfig) -> Self {
Self { config }
}
}
#[async_trait]
impl RemittanceModule for Brc29RemittanceModule {
type OptionTerms = Brc29OptionTerms;
type SettlementArtifact = Brc29SettlementArtifact;
type ReceiptData = Brc29ReceiptData;
fn id(&self) -> &str {
"brc29.p2pkh"
}
fn name(&self) -> &str {
"BSV (BRC-29 derived P2PKH)"
}
fn allow_unsolicited_settlements(&self) -> bool {
true
}
fn supports_create_option(&self) -> bool {
false
}
async fn create_option(
&self,
_thread_id: &str,
_invoice: &Invoice,
_ctx: &ModuleContext,
) -> Result<Brc29OptionTerms, RemittanceError> {
Err(RemittanceError::Protocol(
"BRC-29 module does not support create_option".into(),
))
}
async fn build_settlement(
&self,
thread_id: &str,
_invoice: Option<&Invoice>,
option: &Brc29OptionTerms,
note: Option<&str>,
ctx: &ModuleContext,
) -> Result<BuildSettlementResult<Brc29SettlementArtifact>, RemittanceError> {
match self
.build_settlement_inner(thread_id, option, note, ctx)
.await
{
Ok(result) => Ok(result),
Err(e) => Ok(BuildSettlementResult::Terminate {
termination: Termination {
code: "brc29.build_failed".to_string(),
message: e.to_string(),
details: None,
},
}),
}
}
async fn accept_settlement(
&self,
_thread_id: &str,
_invoice: Option<&Invoice>,
settlement: &Brc29SettlementArtifact,
sender: &str,
ctx: &ModuleContext,
) -> Result<AcceptSettlementResult<Brc29ReceiptData>, RemittanceError> {
if let Err(msg) = ensure_valid_settlement(settlement) {
return Ok(AcceptSettlementResult::Terminate {
termination: Termination {
code: "brc29.internalize_failed".to_string(),
message: msg,
details: None,
},
});
}
let sender_pk = match PublicKey::from_string(sender) {
Ok(pk) => pk,
Err(e) => {
return Ok(AcceptSettlementResult::Terminate {
termination: Termination {
code: "brc29.internalize_failed".to_string(),
message: format!("invalid sender key: {e}"),
details: None,
},
});
}
};
let output_index = settlement.output_index.unwrap_or(0);
let internalize_result = ctx
.wallet
.internalize_action(
InternalizeActionArgs {
tx: settlement.transaction.clone(),
description: "BRC-29 payment received".to_string(),
labels: self.config.labels.clone(),
seek_permission: BooleanDefaultTrue(Some(true)),
outputs: vec![match self.config.internalize_protocol {
InternalizeProtocol::WalletPayment => InternalizeOutput::WalletPayment {
output_index,
payment: Payment {
derivation_prefix: settlement
.custom_instructions
.derivation_prefix
.as_bytes()
.to_vec(),
derivation_suffix: settlement
.custom_instructions
.derivation_suffix
.as_bytes()
.to_vec(),
sender_identity_key: sender_pk,
},
},
InternalizeProtocol::BasketInsertion => {
InternalizeOutput::BasketInsertion {
output_index,
insertion: BasketInsertion {
basket: "brc29".to_string(),
custom_instructions: Some(format!(
"prefix={},suffix={}",
settlement.custom_instructions.derivation_prefix,
settlement.custom_instructions.derivation_suffix,
)),
tags: vec![],
},
}
}
}],
},
ctx.originator.as_deref(),
)
.await;
match internalize_result {
Ok(result) => Ok(AcceptSettlementResult::Accept {
receipt_data: Some(Brc29ReceiptData {
internalize_result: Some(
serde_json::to_value(&result).unwrap_or(serde_json::Value::Null),
),
rejected_reason: None,
refund: None,
}),
}),
Err(e) => Ok(AcceptSettlementResult::Terminate {
termination: Termination {
code: "brc29.internalize_failed".to_string(),
message: e.to_string(),
details: None,
},
}),
}
}
async fn process_receipt(
&self,
_thread_id: &str,
_invoice: Option<&Invoice>,
_receipt_data: &Brc29ReceiptData,
_sender: &str,
_ctx: &ModuleContext,
) -> Result<(), RemittanceError> {
Ok(())
}
async fn process_termination(
&self,
_thread_id: &str,
_invoice: Option<&Invoice>,
_settlement: Option<&Settlement>,
_termination: &Termination,
_sender: &str,
_ctx: &ModuleContext,
) -> Result<(), RemittanceError> {
Ok(())
}
}
impl Brc29RemittanceModule {
async fn build_settlement_inner(
&self,
thread_id: &str,
option: &Brc29OptionTerms,
note: Option<&str>,
ctx: &ModuleContext,
) -> Result<BuildSettlementResult<Brc29SettlementArtifact>, RemittanceError> {
if let Err(msg) = ensure_valid_option(option) {
return Ok(BuildSettlementResult::Terminate {
termination: Termination {
code: "brc29.invalid_option".to_string(),
message: msg,
details: None,
},
});
}
let derivation_prefix = self
.config
.nonce_provider
.create_nonce(&ctx.wallet, ctx.originator.as_deref())
.await?;
let derivation_suffix = self
.config
.nonce_provider
.create_nonce(&ctx.wallet, ctx.originator.as_deref())
.await?;
let protocol_id = option
.protocol_id
.clone()
.unwrap_or_else(|| self.config.protocol_id.clone());
let key_id = format!("{} {}", derivation_prefix, derivation_suffix);
let payee_pk = PublicKey::from_string(&option.payee)
.map_err(|e| RemittanceError::Protocol(format!("invalid payee key: {e}")))?;
let pk_result = ctx
.wallet
.get_public_key(
GetPublicKeyArgs {
identity_key: false,
protocol_id: Some(protocol_id),
key_id: Some(key_id),
counterparty: Some(Counterparty {
counterparty_type: CounterpartyType::Other,
public_key: Some(payee_pk),
}),
privileged: false,
privileged_reason: None,
for_self: None,
seek_permission: None,
},
ctx.originator.as_deref(),
)
.await?;
let script_hex = self
.config
.locking_script_provider
.get_locking_script(&pk_result.public_key.to_der_hex())
.await?;
let script_bytes = crate::primitives::utils::from_hex(&script_hex)
.map_err(|e| RemittanceError::Protocol(format!("invalid locking script hex: {e}")))?;
let custom_json = serde_json::to_string(&CustomInstructionsPayload {
derivation_prefix: &derivation_prefix,
derivation_suffix: &derivation_suffix,
payee: &option.payee,
thread_id,
note,
})
.map_err(|e| RemittanceError::Protocol(format!("custom instructions JSON error: {e}")))?;
let description = option
.description
.clone()
.unwrap_or_else(|| self.config.description.clone());
let labels = option
.labels
.clone()
.unwrap_or_else(|| self.config.labels.clone());
let action_result = ctx
.wallet
.create_action(
CreateActionArgs {
description,
labels,
outputs: vec![CreateActionOutput {
locking_script: Some(script_bytes),
satoshis: option.amount_satoshis,
output_description: self.config.output_description.clone(),
basket: None,
custom_instructions: Some(custom_json),
tags: vec![],
}],
options: Some(CreateActionOptions {
randomize_outputs: BooleanDefaultTrue(Some(false)),
..Default::default()
}),
input_beef: None,
inputs: vec![],
lock_time: None,
version: None,
reference: None,
},
ctx.originator.as_deref(),
)
.await?;
let tx = action_result
.tx
.or_else(|| action_result.signable_transaction.map(|st| st.tx));
let tx = match tx {
Some(tx) if is_atomic_beef(&tx) => tx,
_ => {
return Ok(BuildSettlementResult::Terminate {
termination: Termination {
code: "brc29.missing_tx".to_string(),
message: "wallet returned no transaction".to_string(),
details: None,
},
});
}
};
Ok(BuildSettlementResult::Settle {
artifact: Brc29SettlementArtifact {
custom_instructions: Brc29SettlementCustomInstructions {
derivation_prefix,
derivation_suffix,
},
transaction: tx,
amount_satoshis: option.amount_satoshis,
output_index: Some(option.output_index.unwrap_or(0)),
},
})
}
}
pub fn is_atomic_beef(tx: &[u8]) -> bool {
!tx.is_empty()
}
pub fn ensure_valid_option(option: &Brc29OptionTerms) -> Result<(), String> {
if option.amount_satoshis == 0 {
return Err("BRC-29 option amount must be a positive integer".into());
}
if option.payee.is_empty() || option.payee.trim().is_empty() {
return Err("BRC-29 option payee is required".into());
}
if let Some(pid) = &option.protocol_id {
if pid.protocol.trim().is_empty() {
return Err("BRC-29 option protocolID must have a non-empty protocol string".into());
}
}
if let Some(labels) = &option.labels {
if labels.iter().any(|l| l.trim().is_empty()) {
return Err("BRC-29 option labels must be a list of non-empty strings".into());
}
}
if let Some(desc) = &option.description {
if desc.trim().is_empty() {
return Err("BRC-29 option description must be a non-empty string".into());
}
}
Ok(())
}
pub fn ensure_valid_settlement(artifact: &Brc29SettlementArtifact) -> Result<(), String> {
if !is_atomic_beef(&artifact.transaction) {
return Err("BRC-29 settlement transaction must be a non-empty byte array".into());
}
if artifact.custom_instructions.derivation_prefix.is_empty()
|| artifact
.custom_instructions
.derivation_prefix
.trim()
.is_empty()
{
return Err("BRC-29 settlement derivation values are required".into());
}
if artifact.custom_instructions.derivation_suffix.is_empty()
|| artifact
.custom_instructions
.derivation_suffix
.trim()
.is_empty()
{
return Err("BRC-29 settlement derivation values are required".into());
}
if artifact.amount_satoshis == 0 {
return Err("BRC-29 settlement amount must be a positive integer".into());
}
Ok(())
}