@@ -43,15 +43,18 @@ mod test_utils;
4343pub use crate :: gossip:: { GOSSIP_SYNC_TIME_KEY , NETWORK_GRAPH_KEY , PROB_SCORER_KEY } ;
4444pub use crate :: keymanager:: generate_seed;
4545pub use crate :: ldkstorage:: { CHANNEL_MANAGER_KEY , MONITORS_PREFIX_KEY } ;
46- use crate :: storage:: {
47- list_payment_info, MutinyStorage , DEVICE_ID_KEY , EXPECTED_NETWORK_KEY , NEED_FULL_SYNC_KEY ,
48- } ;
4946use crate :: { auth:: MutinyAuthClient , logging:: MutinyLogger } ;
5047use crate :: { error:: MutinyError , nostr:: ReservedProfile } ;
5148use crate :: {
5249 event:: { HTLCStatus , MillisatAmount , PaymentInfo } ,
5350 onchain:: FULL_SYNC_STOP_GAP ,
5451} ;
52+ use crate :: {
53+ federation:: GatewayFees ,
54+ storage:: {
55+ list_payment_info, MutinyStorage , DEVICE_ID_KEY , EXPECTED_NETWORK_KEY , NEED_FULL_SYNC_KEY ,
56+ } ,
57+ } ;
5558use crate :: {
5659 federation:: { FederationClient , FederationIdentity , FederationIndex , FederationStorage } ,
5760 labels:: { get_contact_key, Contact , LabelStorage } ,
@@ -1122,6 +1125,94 @@ impl<S: MutinyStorage> MutinyWallet<S> {
11221125 } )
11231126 }
11241127
1128+ pub async fn sweep_federation_balance ( & self , amount : Option < u64 > ) -> Result < ( ) , MutinyError > {
1129+ // Attempt to create federation invoice if available and below max amount
1130+ let federation_ids = self . list_federation_ids ( ) . await ?;
1131+ if federation_ids. is_empty ( ) {
1132+ return Err ( MutinyError :: BadAmountError ) ;
1133+ }
1134+
1135+ // TODO support more than one federation
1136+ let federation_id = & federation_ids[ 0 ] ;
1137+ let federation_lock = self . federations . read ( ) . await ;
1138+ let fedimint_client = federation_lock
1139+ . get ( federation_id)
1140+ . ok_or ( MutinyError :: NotFound ) ?;
1141+
1142+ // if the user provided amount, this is easy
1143+ if let Some ( amt) = amount {
1144+ let inv = self . node_manager . create_invoice ( amt) . await ?;
1145+ let _ = fedimint_client
1146+ . pay_invoice ( inv. bolt11 . expect ( "create inv had one job" ) , vec ! [ ] )
1147+ . await ?;
1148+ return Ok ( ( ) ) ;
1149+ }
1150+
1151+ // If no amount, figure out the amount to send over
1152+ let current_balance = fedimint_client. get_balance ( ) . await ?;
1153+ log_info ! (
1154+ self . logger,
1155+ "current fedimint client balance: {}" ,
1156+ current_balance
1157+ ) ;
1158+
1159+ let fees = fedimint_client. gateway_fee ( ) . await ?;
1160+ let amt = max_spendable_amount ( current_balance, fees)
1161+ . map_or ( Err ( MutinyError :: InsufficientBalance ) , Ok ) ?;
1162+ log_info ! ( self . logger, "max spendable: {}" , amt) ;
1163+
1164+ // try to get an invoice for this exact amount
1165+ let inv = self . node_manager . create_invoice ( amt) . await ?;
1166+
1167+ let inv_amt = inv. amount_sats . ok_or ( MutinyError :: BadAmountError ) ?;
1168+ let inv_to_pay = if inv_amt > amt {
1169+ let new_amt = inv_amt - ( inv_amt - amt) ;
1170+ log_info ! ( self . logger, "adjusting amount to swap to: {}" , amt) ;
1171+ self . node_manager . create_invoice ( new_amt) . await ?
1172+ } else {
1173+ inv. clone ( )
1174+ } ;
1175+
1176+ log_info ! ( self . logger, "attempting payment from fedimint client" ) ;
1177+ let _ = fedimint_client
1178+ . pay_invoice ( inv_to_pay. bolt11 . expect ( "create inv had one job" ) , vec ! [ ] )
1179+ . await ?;
1180+
1181+ // pay_invoice returns invoice if Succeeded or Err if something else
1182+ // it's safe to assume that it went through and we can check remaining balance
1183+ let remaining_balance = fedimint_client. get_balance ( ) . await ?;
1184+ log_info ! (
1185+ self . logger,
1186+ "remaining fedimint balance: {}" ,
1187+ remaining_balance
1188+ ) ;
1189+ if remaining_balance != 0 {
1190+ // the fee for existing channel is voltage 1 sat + base fee + ppm
1191+ let remaining_balance_minus_fee = remaining_balance - 1 ; // Voltage is 1 sat fee
1192+
1193+ let inv = self
1194+ . node_manager
1195+ . create_invoice ( remaining_balance_minus_fee)
1196+ . await ?;
1197+
1198+ match fedimint_client
1199+ . pay_invoice ( inv. bolt11 . expect ( "create inv had one job" ) , vec ! [ ] )
1200+ . await
1201+ {
1202+ Ok ( _) => {
1203+ log_info ! ( self . logger, "paid remaining balance" )
1204+ }
1205+ Err ( e) => {
1206+ // Don't want to return this error since it's just "incomplete",
1207+ // and just not the full amount.
1208+ log_warn ! ( self . logger, "error paying remaining balance: {}" , e)
1209+ }
1210+ }
1211+ }
1212+
1213+ Ok ( ( ) )
1214+ }
1215+
11251216 async fn create_lightning_invoice (
11261217 & self ,
11271218 amount : u64 ,
@@ -2089,11 +2180,139 @@ pub(crate) async fn create_new_federation<S: MutinyStorage>(
20892180 } )
20902181}
20912182
2183+ // max amount that can be spent through a gateway
2184+ fn max_spendable_amount ( amount_sat : u64 , routing_fees : GatewayFees ) -> Option < u64 > {
2185+ let amount_msat = amount_sat as f64 * 1_000.0 ;
2186+
2187+ let prop_fee_msat = ( amount_msat * routing_fees. proportional_millionths as f64 ) / 1_000_000.0 ;
2188+
2189+ let initial_max = amount_msat - ( routing_fees. base_msat as f64 + prop_fee_msat) ;
2190+
2191+ if initial_max <= 0.0 {
2192+ return None ;
2193+ }
2194+
2195+ if amount_msat - initial_max < 1.0 {
2196+ return Some ( ( initial_max / 1_000.0 ) . floor ( ) as u64 ) ;
2197+ }
2198+
2199+ let mut new_max = initial_max;
2200+ while new_max < amount_msat {
2201+ let new_check = new_max + 1.0 ;
2202+
2203+ let prop_fee_sat = ( new_check * routing_fees. proportional_millionths as f64 ) / 1_000_000.0 ;
2204+
2205+ let new_amt = new_check + routing_fees. base_msat as f64 + prop_fee_sat;
2206+
2207+ if amount_msat - new_amt <= 0.0 {
2208+ // overshot it
2209+ return Some ( ( new_max / 1_000.0 ) . floor ( ) as u64 ) ;
2210+ }
2211+
2212+ new_max += 1.0 ;
2213+ }
2214+
2215+ Some ( ( new_max / 1_000.0 ) . floor ( ) as u64 )
2216+ }
2217+
20922218#[ cfg( test) ]
2219+ fn max_routing_fee_amount ( ) {
2220+ let initial_budget = 1 ;
2221+ let routing_fees = GatewayFees {
2222+ base_msat : 10_000 ,
2223+ proportional_millionths : 0 ,
2224+ } ;
2225+ assert_eq ! ( None , max_spendable_amount( initial_budget, routing_fees) ) ;
2226+
2227+ // only a percentage fee
2228+ let initial_budget = 100 ;
2229+ let routing_fees = GatewayFees {
2230+ base_msat : 0 ,
2231+ proportional_millionths : 0 ,
2232+ } ;
2233+ assert_eq ! (
2234+ Some ( 100 ) ,
2235+ max_spendable_amount( initial_budget, routing_fees)
2236+ ) ;
2237+
2238+ let initial_budget = 100 ;
2239+ let routing_fees = GatewayFees {
2240+ base_msat : 0 ,
2241+ proportional_millionths : 10_000 ,
2242+ } ;
2243+ assert_eq ! ( Some ( 99 ) , max_spendable_amount( initial_budget, routing_fees) ) ;
2244+
2245+ let initial_budget = 100 ;
2246+ let routing_fees = GatewayFees {
2247+ base_msat : 0 ,
2248+ proportional_millionths : 100_000 ,
2249+ } ;
2250+ assert_eq ! ( Some ( 90 ) , max_spendable_amount( initial_budget, routing_fees) ) ;
2251+
2252+ let initial_budget = 101_000 ;
2253+ let routing_fees = GatewayFees {
2254+ base_msat : 0 ,
2255+ proportional_millionths : 100_000 ,
2256+ } ;
2257+ assert_eq ! (
2258+ Some ( 91_818 ) ,
2259+ max_spendable_amount( initial_budget, routing_fees)
2260+ ) ;
2261+
2262+ let initial_budget = 101 ;
2263+ let routing_fees = GatewayFees {
2264+ base_msat : 0 ,
2265+ proportional_millionths : 100_000 ,
2266+ } ;
2267+ assert_eq ! ( Some ( 91 ) , max_spendable_amount( initial_budget, routing_fees) ) ;
2268+
2269+ // same tests but with a base fee
2270+ let initial_budget = 100 ;
2271+ let routing_fees = GatewayFees {
2272+ base_msat : 1_000 ,
2273+ proportional_millionths : 0 ,
2274+ } ;
2275+ assert_eq ! ( Some ( 99 ) , max_spendable_amount( initial_budget, routing_fees) ) ;
2276+
2277+ let initial_budget = 100 ;
2278+ let routing_fees = GatewayFees {
2279+ base_msat : 1_000 ,
2280+ proportional_millionths : 10_000 ,
2281+ } ;
2282+ assert_eq ! ( Some ( 98 ) , max_spendable_amount( initial_budget, routing_fees) ) ;
2283+
2284+ let initial_budget = 100 ;
2285+ let routing_fees = GatewayFees {
2286+ base_msat : 1_000 ,
2287+ proportional_millionths : 100_000 ,
2288+ } ;
2289+ assert_eq ! ( Some ( 89 ) , max_spendable_amount( initial_budget, routing_fees) ) ;
2290+
2291+ let initial_budget = 101 ;
2292+ let routing_fees = GatewayFees {
2293+ base_msat : 1_000 ,
2294+ proportional_millionths : 100_000 ,
2295+ } ;
2296+ assert_eq ! ( Some ( 90 ) , max_spendable_amount( initial_budget, routing_fees) ) ;
2297+ }
2298+
2299+ #[ cfg( test) ]
2300+ #[ cfg( not( target_arch = "wasm32" ) ) ]
2301+ mod tests {
2302+ use super :: * ;
2303+
2304+ #[ test]
2305+ fn test_max_routing_fee_amount ( ) {
2306+ max_routing_fee_amount ( ) ;
2307+ }
2308+ }
2309+
2310+ #[ cfg( test) ]
2311+ #[ cfg( target_arch = "wasm32" ) ]
20932312mod tests {
20942313 use crate :: {
2095- encrypt:: encryption_key_from_pass, generate_seed, nodemanager :: NodeManager , MutinyWallet ,
2096- MutinyWalletBuilder , MutinyWalletConfigBuilder ,
2314+ encrypt:: encryption_key_from_pass, generate_seed, max_routing_fee_amount ,
2315+ nodemanager :: NodeManager , MutinyWallet , MutinyWalletBuilder , MutinyWalletConfigBuilder ,
20972316 } ;
20982317 use bitcoin:: util:: bip32:: ExtendedPrivKey ;
20992318 use bitcoin:: Network ;
@@ -2401,4 +2620,9 @@ mod tests {
24012620 assert_eq ! ( next. len( ) , 2 ) ;
24022621 assert ! ( next. iter( ) . all( |m| !messages. contains( m) ) )
24032622 }
2623+
2624+ #[ test]
2625+ fn test_max_routing_fee_amount ( ) {
2626+ max_routing_fee_amount ( ) ;
2627+ }
24042628}
0 commit comments