Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contracts/satoshi-bridge/src/api/token_receiver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ impl FungibleTokenReceiver for Contract {
&vutxos,
amount,
withdraw_fee,
max_gas_fee
max_gas_fee,
);

let need_signature_num = psbt.unsigned_tx.input.len();
Expand Down
6 changes: 5 additions & 1 deletion contracts/satoshi-bridge/src/btc_light_client/deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ impl Contract {
.deposit_bridge_fee
.get_protocol_and_relayer_fee(deposit_fee);

let post_actions = self.check_deposit_msg(deposit_msg, mint_amount);
let post_actions = self.check_deposit_msg(
deposit_msg,
mint_amount,
pending_utxo_info.utxo_storage_key.clone(),
);
promise.then(
Self::ext(env::current_account_id())
.with_static_gas(GAS_FOR_VERIFY_DEPOSIT_CALL_BACK)
Expand Down
47 changes: 34 additions & 13 deletions contracts/satoshi-bridge/src/deposit_msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ impl Contract {
&self,
deposit_msg: DepositMsg,
actual_mintable_amount: u128,
utxo_storage_key: String,
) -> Option<Vec<PostAction>> {
let post_actions = deposit_msg.post_actions?;
let mut post_actions = deposit_msg.post_actions?;
if post_actions.is_empty() {
Event::InvalidPostAction {
index: None,
Expand All @@ -65,7 +66,7 @@ impl Contract {
}
let mut total_gas = 0;
let mut total_amount = 0;
for (index, post_action) in post_actions.iter().enumerate() {
for (index, post_action) in post_actions.iter_mut().enumerate() {
Copy link
Collaborator

@karim-en karim-en Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (index, post_action) in post_actions.iter_mut().enumerate() {
for (index, post_action) in post_actions.iter_mut().enumerate() {
post_action.msg = post_action.msg.replace("{{UTXO_TX_ID}}", utxo_storage_key);

I think the changes can be done with less code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, we can do it this way, it’s really the simplest option. In that case, the replacement will always happen regardless of what is specified in the templates.

I also thought about adding a special template like 'add UTXO', and if such a template exists, then perform the replacement.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think about something like that:
3145bfa

I’d like to isolate the contracts to which the changes are applied.

In this option, the code is simpler. However, the logic becomes stranger. Templates stop being templates and turn into modifiers.

The first option sounds more logical in the context of templates.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also thought that it is important to add additional restrictions, but after thinking more on it, I think we can make things simple and just do replace.
There is a low chance to get collision with this naming.

total_amount += post_action.amount.0;
// The receiver_id must be on the whitelist.
if !self
Expand All @@ -88,21 +89,41 @@ impl Contract {
.post_action_msg_templates
.get(&post_action.receiver_id)
{
let is_match =
let updated_post_action: Option<String> =
match serde_json::from_str::<Value>(&post_action.msg) {
Ok(msg_value) => msg_templates.iter().any(|template| {
match serde_json::from_str::<Value>(template) {
Ok(template_value) => {
is_structure_equal(&template_value, &msg_value)
Ok(msg_value) => {
let mut res = None;
for template in msg_templates {
if let Ok(template_value) = serde_json::from_str::<Value>(template)
{
res = check_template_and_update_msg(
&template_value,
&msg_value,
&utxo_storage_key,
);
if res.is_some() {
break;
}
}
Err(_) => false,
}
}),
Err(_) => msg_templates
.iter()
.any(|template| template == &post_action.msg),

res.map(|x| x.to_string())
}
Err(_) => {
if msg_templates
.iter()
.any(|template| template == &post_action.msg)
{
Some(post_action.msg.clone())
} else {
None
}
}
};
if !is_match {

if let Some(updated_post_action) = updated_post_action {
post_action.msg = updated_post_action;
} else {
Event::InvalidPostAction {
index: Some(index),
err_msg: "Unsupported post_action.msg.".to_string(),
Expand Down
52 changes: 38 additions & 14 deletions contracts/satoshi-bridge/src/json_utils.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::*;

const INSERT_UTXO_TX_ID_TEMPLATE: &str = "{{UTXO_TX_ID}}";

/// Recursively checks whether the structure of `input` matches the structure of `template`.
/// Values can differ, but keys and value types must conform to the `template`.
///
Expand All @@ -11,14 +13,22 @@ use crate::*;
/// 4. If the template array has multiple elements, it's treated as an enum array:
/// all elements in `input` must match one of the enum variants.
/// 5. If a template value is `null`, then any corresponding input value is accepted (i.e., unconstrained).
pub fn is_structure_equal(template: &Value, input: &Value) -> bool {
pub fn check_template_and_update_msg(
template: &Value,
input: &Value,
utxo_storage_key: &str,
) -> Option<Value> {
let mut res = input.clone();
match (template, input) {
(Value::Object(t_obj), Value::Object(i_obj)) => {
for (key, t_val) in t_obj {
match i_obj.get(key) {
Some(i_val) => {
if !is_structure_equal(t_val, i_val) {
return false;
match check_template_and_update_msg(t_val, i_val, utxo_storage_key) {
Some(val) => {
res.as_object_mut().unwrap().insert(key.clone(), val);
}
None => return None,
}
}
None => {
Expand All @@ -30,33 +40,47 @@ pub fn is_structure_equal(template: &Value, input: &Value) -> bool {
// The input must not contain fields that are not defined in the template.
for key in i_obj.keys() {
if !t_obj.contains_key(key) {
return false;
return None;
}
}
true
Some(res)
}
(Value::Array(t_arr), Value::Array(i_arr)) => {
if t_arr.is_empty() {
return i_arr.is_empty();
if i_arr.is_empty() {
return Some(res);
}
return None;
}
res = Value::Array(vec![]);

for i_item in i_arr {
let mut matched = false;
for t_item in t_arr {
if is_structure_equal(t_item, i_item) {
if let Some(sub_res) =
check_template_and_update_msg(t_item, i_item, utxo_storage_key)
{
res.as_array_mut().unwrap().push(sub_res);

matched = true;
break;
}
}
if !matched {
return false;
return None;
}
}
true
Some(res)
}
(Value::String(temp_str), Value::String(real_str)) => {
if temp_str == INSERT_UTXO_TX_ID_TEMPLATE && real_str == INSERT_UTXO_TX_ID_TEMPLATE {
res = Value::String(utxo_storage_key.to_string());
}
Some(res)
}
(Value::String(_), Value::String(_)) => true,
(Value::Number(_), Value::Number(_)) => true,
(Value::Bool(_), Value::Bool(_)) => true,
(Value::Null, _) => true, // When a key’s value is not restricted, set its value to null.
_ => false,
(Value::Number(_), Value::Number(_)) => Some(res),
(Value::Bool(_), Value::Bool(_)) => Some(res),
(Value::Null, _) => Some(res), // When a key’s value is not restricted, set its value to null.
_ => None,
}
}
10 changes: 8 additions & 2 deletions contracts/satoshi-bridge/src/psbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ impl Contract {
amount,
withdraw_fee,
);

if let Some(max_gas_fee) = max_gas_fee {
require!(gas_fee <= max_gas_fee.0, format!("Gas fee does not match the provided max fee (gas fee = {}; max gas fee = {})", gas_fee, max_gas_fee.0));
require!(
gas_fee <= max_gas_fee.0,
format!(
"Gas fee does not match the provided max fee (gas fee = {}; max gas fee = {})",
gas_fee, max_gas_fee.0
)
);
}

require!(
Expand Down
Loading