Skip to content

Commit cd4e7ef

Browse files
authored
Merge pull request #1844 from SatoshiPortal/feat/swap-outspends
feat: swap outspend checks
2 parents 3b93c24 + 7338eb2 commit cd4e7ef

7 files changed

Lines changed: 380 additions & 86 deletions

File tree

lib/core/swaps/data/datasources/boltz_datasource.dart

Lines changed: 118 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import 'dart:async';
22

33
import 'package:bb_mobile/core/swaps/data/datasources/boltz_storage_datasource.dart';
44
import 'package:bb_mobile/core/swaps/data/models/swap_model.dart';
5+
import 'package:bb_mobile/core/swaps/data/models/swap_tx_outspend_model.dart';
56
import 'package:bb_mobile/core/swaps/domain/entity/swap.dart' as swap_entity;
7+
import 'package:bb_mobile/core/swaps/domain/entity/swap_tx_outspend.dart';
68
import 'package:bb_mobile/core/utils/constants.dart';
9+
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
710
import 'package:boltz/boltz.dart';
811

912
class BoltzDatasource {
@@ -652,16 +655,15 @@ class BoltzDatasource {
652655
}) async {
653656
try {
654657
final chainSwap = await _boltzStore.fetchChainSwap(swapId);
655-
final txId =
656-
await (broadcastViaBoltz
657-
? chainSwap.broadcastBoltz(
658-
signedHex: signedTxHex,
659-
kind: SwapTxKind.refund,
660-
)
661-
: chainSwap.broadcastLocal(
662-
signedHex: signedTxHex,
663-
kind: SwapTxKind.refund,
664-
));
658+
final txId = await (broadcastViaBoltz
659+
? chainSwap.broadcastBoltz(
660+
signedHex: signedTxHex,
661+
kind: SwapTxKind.refund,
662+
)
663+
: chainSwap.broadcastLocal(
664+
signedHex: signedTxHex,
665+
kind: SwapTxKind.refund,
666+
));
665667
return txId;
666668
} catch (e) {
667669
if (e is BoltzError) {
@@ -725,13 +727,13 @@ class BoltzDatasource {
725727
final chainSwap = await _boltzStore.fetchChainSwap(swapId);
726728
return await (broadcastViaBoltz
727729
? chainSwap.broadcastBoltz(
728-
signedHex: signedTxHex,
729-
kind: SwapTxKind.claim,
730-
)
730+
signedHex: signedTxHex,
731+
kind: SwapTxKind.claim,
732+
)
731733
: chainSwap.broadcastLocal(
732-
signedHex: signedTxHex,
733-
kind: SwapTxKind.claim,
734-
));
734+
signedHex: signedTxHex,
735+
kind: SwapTxKind.claim,
736+
));
735737
} catch (e) {
736738
if (e is BoltzError) {
737739
throw e.message;
@@ -1101,10 +1103,9 @@ class BoltzDatasource {
11011103
case SwapStatus.txnRefunded:
11021104
// Check if this swap needs to be refunded (no refundTxid)
11031105
if (swapModel is ChainSwapModel || swapModel is LnSendSwapModel) {
1104-
final refunded =
1105-
swapModel is ChainSwapModel
1106-
? swapModel.refundTxid != null
1107-
: (swapModel as LnSendSwapModel).refundTxid != null;
1106+
final refunded = swapModel is ChainSwapModel
1107+
? swapModel.refundTxid != null
1108+
: (swapModel as LnSendSwapModel).refundTxid != null;
11081109

11091110
if (!refunded) {
11101111
updatedSwapModel = swapModel.copyWith(
@@ -1128,15 +1129,13 @@ class BoltzDatasource {
11281129
case SwapStatus.txnFailed:
11291130
// Transaction failed - check if refundable
11301131
if (swapModel is ChainSwapModel || swapModel is LnSendSwapModel) {
1131-
final hasSentFunds =
1132-
swapModel is ChainSwapModel
1133-
? swapModel.sendTxid != null
1134-
: (swapModel as LnSendSwapModel).sendTxid != null;
1132+
final hasSentFunds = swapModel is ChainSwapModel
1133+
? swapModel.sendTxid != null
1134+
: (swapModel as LnSendSwapModel).sendTxid != null;
11351135

1136-
final hasRefunded =
1137-
swapModel is ChainSwapModel
1138-
? swapModel.refundTxid != null
1139-
: (swapModel as LnSendSwapModel).refundTxid != null;
1136+
final hasRefunded = swapModel is ChainSwapModel
1137+
? swapModel.refundTxid != null
1138+
: (swapModel as LnSendSwapModel).refundTxid != null;
11401139

11411140
if (hasSentFunds && !hasRefunded) {
11421141
updatedSwapModel = swapModel.copyWith(
@@ -1153,15 +1152,13 @@ class BoltzDatasource {
11531152
case SwapStatus.invoiceExpired:
11541153
// Check if funds were sent but not refunded
11551154
if (swapModel is ChainSwapModel || swapModel is LnSendSwapModel) {
1156-
final hasSentFunds =
1157-
swapModel is ChainSwapModel
1158-
? swapModel.sendTxid != null
1159-
: (swapModel as LnSendSwapModel).sendTxid != null;
1155+
final hasSentFunds = swapModel is ChainSwapModel
1156+
? swapModel.sendTxid != null
1157+
: (swapModel as LnSendSwapModel).sendTxid != null;
11601158

1161-
final hasRefunded =
1162-
swapModel is ChainSwapModel
1163-
? swapModel.refundTxid != null
1164-
: (swapModel as LnSendSwapModel).refundTxid != null;
1159+
final hasRefunded = swapModel is ChainSwapModel
1160+
? swapModel.refundTxid != null
1161+
: (swapModel as LnSendSwapModel).refundTxid != null;
11651162

11661163
if (hasSentFunds && !hasRefunded) {
11671164
updatedSwapModel = swapModel.copyWith(
@@ -1180,10 +1177,9 @@ class BoltzDatasource {
11801177

11811178
case SwapStatus.swapRefunded:
11821179
if (swapModel is ChainSwapModel || swapModel is LnSendSwapModel) {
1183-
final hasRefunded =
1184-
swapModel is ChainSwapModel
1185-
? swapModel.refundTxid != null
1186-
: (swapModel as LnSendSwapModel).refundTxid != null;
1180+
final hasRefunded = swapModel is ChainSwapModel
1181+
? swapModel.refundTxid != null
1182+
: (swapModel as LnSendSwapModel).refundTxid != null;
11871183

11881184
if (!hasRefunded) {
11891185
updatedSwapModel = swapModel.copyWith(
@@ -1200,15 +1196,13 @@ class BoltzDatasource {
12001196
case SwapStatus.swapError:
12011197
// Handle error states
12021198
if (swapModel is ChainSwapModel || swapModel is LnSendSwapModel) {
1203-
final hasSentFunds =
1204-
swapModel is ChainSwapModel
1205-
? swapModel.sendTxid != null
1206-
: (swapModel as LnSendSwapModel).sendTxid != null;
1199+
final hasSentFunds = swapModel is ChainSwapModel
1200+
? swapModel.sendTxid != null
1201+
: (swapModel as LnSendSwapModel).sendTxid != null;
12071202

1208-
final hasRefunded =
1209-
swapModel is ChainSwapModel
1210-
? swapModel.refundTxid != null
1211-
: (swapModel as LnSendSwapModel).refundTxid != null;
1203+
final hasRefunded = swapModel is ChainSwapModel
1204+
? swapModel.refundTxid != null
1205+
: (swapModel as LnSendSwapModel).refundTxid != null;
12121206

12131207
if (hasSentFunds && !hasRefunded) {
12141208
updatedSwapModel = swapModel.copyWith(
@@ -1271,8 +1265,9 @@ class BoltzDatasource {
12711265

12721266
void subscribeToSwaps(List<String> swapIds) {
12731267
final uniqueSwapIds = swapIds.toSet().toList();
1274-
final newSwapIds =
1275-
uniqueSwapIds.where((id) => !_subscribedSwapIds.contains(id)).toList();
1268+
final newSwapIds = uniqueSwapIds
1269+
.where((id) => !_subscribedSwapIds.contains(id))
1270+
.toList();
12761271

12771272
if (newSwapIds.isEmpty) {
12781273
return;
@@ -1283,8 +1278,9 @@ class BoltzDatasource {
12831278

12841279
void unsubscribeToSwaps(List<String> swapIds) {
12851280
final uniqueSwapIds = swapIds.toSet().toList();
1286-
final swapIdsToUnsubscribe =
1287-
uniqueSwapIds.where((id) => _subscribedSwapIds.contains(id)).toList();
1281+
final swapIdsToUnsubscribe = uniqueSwapIds
1282+
.where((id) => _subscribedSwapIds.contains(id))
1283+
.toList();
12881284
if (swapIdsToUnsubscribe.isEmpty) {
12891285
return;
12901286
}
@@ -1320,10 +1316,9 @@ class BoltzDatasource {
13201316
final fees = Fees(boltzUrl: _httpsUrl);
13211317
final reverseFees = await fees.reverse();
13221318
final submarineFees = await fees.submarine();
1323-
final swapType =
1324-
swap.kind == SwapType.reverse
1325-
? swap_entity.SwapType.lightningToBitcoin
1326-
: swap_entity.SwapType.bitcoinToLightning;
1319+
final swapType = swap.kind == SwapType.reverse
1320+
? swap_entity.SwapType.lightningToBitcoin
1321+
: swap_entity.SwapType.bitcoinToLightning;
13271322
if (swapType == swap_entity.SwapType.lightningToBitcoin) {
13281323
if (receiveWalletId == null) {
13291324
throw 'Receive wallet ID is required for lightning to bitcoin swaps';
@@ -1384,10 +1379,9 @@ class BoltzDatasource {
13841379
final fees = Fees(boltzUrl: _httpsUrl);
13851380
final reverseFees = await fees.reverse();
13861381
final submarineFees = await fees.submarine();
1387-
final swapType =
1388-
swap.kind == SwapType.reverse
1389-
? swap_entity.SwapType.lightningToLiquid
1390-
: swap_entity.SwapType.liquidToLightning;
1382+
final swapType = swap.kind == SwapType.reverse
1383+
? swap_entity.SwapType.lightningToLiquid
1384+
: swap_entity.SwapType.liquidToLightning;
13911385
if (swapType == swap_entity.SwapType.lightningToLiquid) {
13921386
if (receiveWalletId == null) {
13931387
throw 'Receive wallet ID is required for lightning to liquid swaps';
@@ -1447,10 +1441,9 @@ class BoltzDatasource {
14471441
) async {
14481442
final fees = Fees(boltzUrl: _httpsUrl);
14491443
final chainFees = await fees.chain();
1450-
final swapType =
1451-
swap.direction == ChainSwapDirection.lbtcToBtc
1452-
? swap_entity.SwapType.liquidToBitcoin
1453-
: swap_entity.SwapType.bitcoinToLiquid;
1444+
final swapType = swap.direction == ChainSwapDirection.lbtcToBtc
1445+
? swap_entity.SwapType.liquidToBitcoin
1446+
: swap_entity.SwapType.bitcoinToLiquid;
14541447
switch (swapType) {
14551448
case swap_entity.SwapType.liquidToBitcoin:
14561449
final swapModel = SwapModel.chain(
@@ -1463,10 +1456,12 @@ class BoltzDatasource {
14631456
paymentAddress: swap.scriptAddress,
14641457
paymentAmount: swap.outAmount.toInt(),
14651458
sendTxid: lockupTxid,
1466-
receiveWalletId:
1467-
isReceiveWalletExternal == false ? receiveWalletId : null,
1468-
receiveAddress:
1469-
isReceiveWalletExternal == true ? receiveWalletId : null,
1459+
receiveWalletId: isReceiveWalletExternal == false
1460+
? receiveWalletId
1461+
: null,
1462+
receiveAddress: isReceiveWalletExternal == true
1463+
? receiveWalletId
1464+
: null,
14701465
boltzFees:
14711466
(chainFees.btcFees.percentage * swap.outAmount.toInt() / 100)
14721467
.ceil()
@@ -1488,10 +1483,12 @@ class BoltzDatasource {
14881483
paymentAddress: swap.scriptAddress,
14891484
paymentAmount: swap.outAmount.toInt(),
14901485
sendTxid: lockupTxid,
1491-
receiveWalletId:
1492-
isReceiveWalletExternal == false ? receiveWalletId : null,
1493-
receiveAddress:
1494-
isReceiveWalletExternal == true ? receiveWalletId : null,
1486+
receiveWalletId: isReceiveWalletExternal == false
1487+
? receiveWalletId
1488+
: null,
1489+
receiveAddress: isReceiveWalletExternal == true
1490+
? receiveWalletId
1491+
: null,
14951492
boltzFees:
14961493
(chainFees.lbtcFees.percentage * swap.outAmount.toInt() / 100)
14971494
.ceil()
@@ -1506,4 +1503,53 @@ class BoltzDatasource {
15061503
throw Exception('Invalid swap type');
15071504
}
15081505
}
1506+
1507+
/// Checks the outspend status of a swap's lockup transaction
1508+
Future<SwapTxOutspendModel> checkSwapLockupOutspend({
1509+
required String swapId,
1510+
required swap_entity.SwapType swapType,
1511+
required Network network,
1512+
SwapDirection? swapDirection,
1513+
}) async {
1514+
final boltzSwapType = switch (swapType) {
1515+
swap_entity.SwapType.lightningToBitcoin ||
1516+
swap_entity.SwapType.lightningToLiquid => SwapType.reverse,
1517+
swap_entity.SwapType.bitcoinToLightning ||
1518+
swap_entity.SwapType.liquidToLightning => SwapType.submarine,
1519+
swap_entity.SwapType.bitcoinToLiquid ||
1520+
swap_entity.SwapType.liquidToBitcoin => SwapType.chain,
1521+
};
1522+
1523+
final boltzChain = switch (network) {
1524+
Network.bitcoinMainnet => Chain.bitcoin,
1525+
Network.bitcoinTestnet => Chain.bitcoinTestnet,
1526+
Network.liquidMainnet => Chain.liquid,
1527+
Network.liquidTestnet => Chain.liquidTestnet,
1528+
};
1529+
1530+
final chainSwapDirection = swapDirection != null
1531+
? switch (swapDirection) {
1532+
SwapDirection.bitcoinToLiquid => ChainSwapDirection.btcToLbtc,
1533+
SwapDirection.liquidToBitcoin => ChainSwapDirection.lbtcToBtc,
1534+
}
1535+
: null;
1536+
1537+
final outspendStatus = await checkVout0Outspend(
1538+
swapId: swapId,
1539+
swapType: boltzSwapType,
1540+
txKind: SwapTxKind.claim,
1541+
network: boltzChain,
1542+
boltzUrl: _httpsUrl,
1543+
chainSwapDirection: chainSwapDirection,
1544+
);
1545+
1546+
return SwapTxOutspendModel(
1547+
txid: outspendStatus.txid,
1548+
timestamp: outspendStatus.timestamp != null
1549+
? DateTime.fromMillisecondsSinceEpoch(
1550+
outspendStatus.timestamp!.toInt() * 1000,
1551+
)
1552+
: null,
1553+
);
1554+
}
15091555
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import 'package:bb_mobile/core/swaps/domain/entity/swap_tx_outspend.dart';
2+
3+
class SwapTxOutspendModel {
4+
final String? txid;
5+
final DateTime? timestamp;
6+
7+
const SwapTxOutspendModel({this.txid, this.timestamp});
8+
9+
SwapTxOutspend toEntity() {
10+
return SwapTxOutspend(txid: txid, timestamp: timestamp);
11+
}
12+
13+
factory SwapTxOutspendModel.fromEntity(SwapTxOutspend entity) {
14+
return SwapTxOutspendModel(txid: entity.txid, timestamp: entity.timestamp);
15+
}
16+
}

lib/core/swaps/data/repository/boltz_swap_repository.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import 'package:bb_mobile/core/swaps/data/models/auto_swap_model.dart';
66
import 'package:bb_mobile/core/swaps/data/models/swap_model.dart';
77
import 'package:bb_mobile/core/swaps/domain/entity/auto_swap.dart';
88
import 'package:bb_mobile/core/swaps/domain/entity/swap.dart';
9+
import 'package:bb_mobile/core/swaps/domain/entity/swap_tx_outspend.dart'
10+
hide SwapDirection;
11+
import 'package:bb_mobile/core/swaps/domain/entity/swap_tx_outspend.dart'
12+
as outspend;
13+
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
914

1015
class BoltzSwapRepository {
1116
final BoltzDatasource _boltz;
@@ -807,4 +812,20 @@ class BoltzSwapRepository {
807812
await _boltz.storage.storeAutoSwapSettings(model);
808813
}
809814
}
815+
816+
/// Checks the outspend status of a swap's lockup transaction
817+
Future<SwapTxOutspend> checkSwapLockupOutspend({
818+
required String swapId,
819+
required SwapType swapType,
820+
required Network network,
821+
outspend.SwapDirection? swapDirection,
822+
}) async {
823+
final model = await _boltz.checkSwapLockupOutspend(
824+
swapId: swapId,
825+
swapType: swapType,
826+
network: network,
827+
swapDirection: swapDirection,
828+
);
829+
return model.toEntity();
830+
}
810831
}

0 commit comments

Comments
 (0)