Skip to content

Conversation

@dangeross
Copy link
Collaborator

@dangeross dangeross commented Dec 2, 2025

This PR adds a means to convert funds to/from Bitcoin and another supported token.

  • Adds Flashnet client
  • Adds interfaces to estimate amount/fee and to perform the conversion
  • Adds syncing of successful conversions with payment information
  • Handles failed conversion by manually checking and performing refunds if necessary

TODO:

  • Finalize interface
  • Mainnet testing
  • Docs

Closes #335

@dangeross dangeross changed the base branch from main to savage-payment-update-persistence December 2, 2025 14:12
@dangeross dangeross force-pushed the savage-token-transfers branch 3 times, most recently from a67d4d5 to eb80ff5 Compare December 2, 2025 16:18
@dangeross dangeross force-pushed the savage-token-transfers branch from eb80ff5 to 54047cb Compare December 3, 2025 09:00
@dangeross dangeross marked this pull request as ready for review December 3, 2025 12:44
Copy link
Member

@roeierez roeierez left a comment

Choose a reason for hiding this comment

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

Looks really good!

"Amount in {amount_in} is less than minimum required {min_in} for asset {asset_in_address}",
)));
}
return Ok(());
Copy link
Member

Choose a reason for hiding this comment

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

We skip here without checking the min_amount_out ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copied logic from the Flashnet SDK. You think it's worth checking?

Copy link
Member

Choose a reason for hiding this comment

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

To me it looks like the min_amount_out is not validated in this case. It looks like a bug unless it is not used when min_in is set.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I was assuming if there's a min in to check and it's valid, we don't need to check the min out also

let now = SystemTime::now().duration_since(UNIX_EPOCH).map_err(|_| {
FlashnetError::Generic("Failed to get current time".to_string())
})?;
expires.saturating_sub(now).as_millis()
Copy link
Member

Choose a reason for hiding this comment

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

Should we add take several seconds as margin and not count on the exact time?

@dangeross dangeross changed the title Token transfers Convert tokens Dec 4, 2025
Copy link
Collaborator

@danielgranhao danielgranhao left a comment

Choose a reason for hiding this comment

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

Awesome! A few comments

Comment on lines +241 to +244
/// Conversion information if this was a successful conversion
conversion_info: Option<ConversionInfo>,
/// Conversion refund information if this was a conversion that was refunded
conversion_refund_info: Option<ConversionRefundInfo>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Have you considered merging these 2 into an enum?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's split because of the two methods which they are synced. The conversion_info is synced from the Flashnet data and conversion_refund_info from rt-sync

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we could still merge into an enum even if in storage the variants are stored separately. Just an idea to avoid having to check 2 separate fields. I'm also ok with keeping like this.

/// Only include payments matching these payment details filters
#[cfg_attr(feature = "uniffi", uniffi(default=None))]
pub spark_htlc_status_filter: Option<Vec<SparkHtlcStatus>>,
pub payment_details_filter: Option<PaymentDetailsFilter>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we make it Option<Vec<PaymentDetailsFilter>> to allow both spark and token payments needing refund to be listed in a single call?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done 72f2758

Comment on lines 956 to 961
} else {
error!(
"Failed to find payment for swap {} with inbound transfer id {}",
swap.id, swap.inbound_transfer_id
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

IIUC, when we fail to find a payment for a swap, we ignore it (just log) and still mark it as synced. Isn't it a problem that we run sync_wallet and sync_conversion_info concurrently? Especially when recovering a wallet from scratch, it seems likely that we may try to look for a payment we haven't synced yet.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've removed sync_conversion_info from the parallel join, so it syncs after the wallet. I also now break out of the conversion info sync if the payment's not found and store the last updated offset. feaaf00

Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice. I realize this is an edge case, but if, for any reason, a payment is missing and stays missing, we'll never fully complete syncing conversion info 🤔

Wondering how we may address that. Could we still store the conversion info in storage even if the corresponding payment is not there?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We could put it in the storage cache. How likely do you think it is to happen?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants