@@ -43,15 +43,18 @@ mod test_utils;
43
43
pub use crate :: gossip:: { GOSSIP_SYNC_TIME_KEY , NETWORK_GRAPH_KEY , PROB_SCORER_KEY } ;
44
44
pub use crate :: keymanager:: generate_seed;
45
45
pub 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
- } ;
49
46
use crate :: { auth:: MutinyAuthClient , logging:: MutinyLogger } ;
50
47
use crate :: { error:: MutinyError , nostr:: ReservedProfile } ;
51
48
use crate :: {
52
49
event:: { HTLCStatus , MillisatAmount , PaymentInfo } ,
53
50
onchain:: FULL_SYNC_STOP_GAP ,
54
51
} ;
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
+ } ;
55
58
use crate :: {
56
59
federation:: { FederationClient , FederationIdentity , FederationIndex , FederationStorage } ,
57
60
labels:: { get_contact_key, Contact , LabelStorage } ,
@@ -1122,6 +1125,94 @@ impl<S: MutinyStorage> MutinyWallet<S> {
1122
1125
} )
1123
1126
}
1124
1127
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
+
1125
1216
async fn create_lightning_invoice (
1126
1217
& self ,
1127
1218
amount : u64 ,
@@ -2089,11 +2180,139 @@ pub(crate) async fn create_new_federation<S: MutinyStorage>(
2089
2180
} )
2090
2181
}
2091
2182
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
+
2092
2218
#[ 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" ) ]
2093
2312
mod tests {
2094
2313
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 ,
2097
2316
} ;
2098
2317
use bitcoin:: util:: bip32:: ExtendedPrivKey ;
2099
2318
use bitcoin:: Network ;
@@ -2401,4 +2620,9 @@ mod tests {
2401
2620
assert_eq ! ( next. len( ) , 2 ) ;
2402
2621
assert ! ( next. iter( ) . all( |m| !messages. contains( m) ) )
2403
2622
}
2623
+
2624
+ #[ test]
2625
+ fn test_max_routing_fee_amount ( ) {
2626
+ max_routing_fee_amount ( ) ;
2627
+ }
2404
2628
}
0 commit comments