mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-01-05 18:39:25 +00:00
fix: scan when switching, fix multiple unspents in same tx
This commit is contained in:
parent
2967809a0e
commit
a44bd6b8f9
4 changed files with 105 additions and 79 deletions
|
@ -19,7 +19,7 @@ class ElectrumTransactionBundle {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ElectrumTransactionInfo extends TransactionInfo {
|
class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
BitcoinUnspent? unspent;
|
List<BitcoinUnspent>? unspents;
|
||||||
|
|
||||||
ElectrumTransactionInfo(this.type,
|
ElectrumTransactionInfo(this.type,
|
||||||
{required String id,
|
{required String id,
|
||||||
|
@ -31,7 +31,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
required DateTime date,
|
required DateTime date,
|
||||||
required int confirmations,
|
required int confirmations,
|
||||||
String? to,
|
String? to,
|
||||||
this.unspent}) {
|
this.unspents}) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.amount = amount;
|
this.amount = amount;
|
||||||
|
@ -160,10 +160,12 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
isPending: data['isPending'] as bool,
|
isPending: data['isPending'] as bool,
|
||||||
confirmations: data['confirmations'] as int,
|
confirmations: data['confirmations'] as int,
|
||||||
to: data['to'] as String?,
|
to: data['to'] as String?,
|
||||||
unspent: data['unspent'] != null
|
unspents: data['unspent'] != null
|
||||||
? BitcoinUnspent.fromJSON(
|
? (data['unspent'] as List<dynamic>)
|
||||||
BitcoinAddressRecord.fromJSON(data['unspent']['address_record'] as String),
|
.map((unspent) => BitcoinUnspent.fromJSON(
|
||||||
data['unspent'] as Map<String, dynamic>)
|
BitcoinAddressRecord.fromJSON(unspent['address_record'] as String),
|
||||||
|
data['unspent'] as Map<String, dynamic>))
|
||||||
|
.toList()
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -210,11 +212,11 @@ class ElectrumTransactionInfo extends TransactionInfo {
|
||||||
m['confirmations'] = confirmations;
|
m['confirmations'] = confirmations;
|
||||||
m['fee'] = fee;
|
m['fee'] = fee;
|
||||||
m['to'] = to;
|
m['to'] = to;
|
||||||
m['unspent'] = unspent?.toJson() ?? <String, dynamic>{};
|
m['unspent'] = unspents?.map((e) => e.toJson()) ?? [];
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspent)';
|
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,6 +142,9 @@ abstract class ElectrumWalletBase
|
||||||
@observable
|
@observable
|
||||||
bool nodeSupportsSilentPayments = true;
|
bool nodeSupportsSilentPayments = true;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
int? currentChainTip;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
BitcoinWalletKeys get keys =>
|
BitcoinWalletKeys get keys =>
|
||||||
BitcoinWalletKeys(wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!);
|
BitcoinWalletKeys(wif: hd.wif!, privateKey: hd.privKey!, publicKey: hd.pubKey!);
|
||||||
|
@ -197,30 +200,37 @@ abstract class ElectrumWalletBase
|
||||||
syncStatus = UnsupportedSyncStatus();
|
syncStatus = UnsupportedSyncStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message is BitcoinUnspent) {
|
|
||||||
if (!unspentCoins.any((utx) =>
|
|
||||||
utx.hash.contains(message.hash) &&
|
|
||||||
utx.vout == message.vout &&
|
|
||||||
utx.address.contains(message.address))) {
|
|
||||||
unspentCoins.add(message);
|
|
||||||
|
|
||||||
if (unspentCoinsInfo.values.any((element) =>
|
|
||||||
element.walletId.contains(id) &&
|
|
||||||
element.hash.contains(message.hash) &&
|
|
||||||
element.address.contains(message.address))) {
|
|
||||||
_addCoinInfo(message);
|
|
||||||
|
|
||||||
await walletInfo.save();
|
|
||||||
await save();
|
|
||||||
}
|
|
||||||
|
|
||||||
balance[currency] = await _fetchBalances();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message is Map<String, ElectrumTransactionInfo>) {
|
if (message is Map<String, ElectrumTransactionInfo>) {
|
||||||
transactionHistory.addMany(message);
|
for (final map in message.entries) {
|
||||||
await transactionHistory.save();
|
final txid = map.key;
|
||||||
|
final tx = map.value;
|
||||||
|
|
||||||
|
if (tx.unspents != null) {
|
||||||
|
tx.unspents!.forEach((unspent) => walletAddresses.addSilentAddresses(
|
||||||
|
[unspent.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord]));
|
||||||
|
|
||||||
|
final existingTxInfo = transactionHistory.transactions[txid];
|
||||||
|
if (existingTxInfo != null) {
|
||||||
|
final newUnspents = tx.unspents!
|
||||||
|
.where((unspent) => !existingTxInfo.unspents!.any((element) =>
|
||||||
|
element.hash.contains(unspent.hash) && element.vout == unspent.vout))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (newUnspents.isNotEmpty) {
|
||||||
|
existingTxInfo.unspents ??= [];
|
||||||
|
existingTxInfo.unspents!.addAll(newUnspents);
|
||||||
|
existingTxInfo.amount += newUnspents.length > 1
|
||||||
|
? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent)
|
||||||
|
: newUnspents[0].value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
transactionHistory.addMany(message);
|
||||||
|
transactionHistory.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUnspent();
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if is a SyncStatus type since "is SyncStatus" doesn't work here
|
// check if is a SyncStatus type since "is SyncStatus" doesn't work here
|
||||||
|
@ -236,17 +246,16 @@ abstract class ElectrumWalletBase
|
||||||
@override
|
@override
|
||||||
Future<void> startSync() async {
|
Future<void> startSync() async {
|
||||||
try {
|
try {
|
||||||
|
syncStatus = AttemptingSyncStatus();
|
||||||
|
|
||||||
if (hasSilentPaymentsScanning) {
|
if (hasSilentPaymentsScanning) {
|
||||||
try {
|
try {
|
||||||
await _setInitialHeight();
|
await _setInitialHeight();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
final currentChainTip = await electrumClient.getCurrentBlockChainTip();
|
|
||||||
if ((currentChainTip ?? 0) > walletInfo.restoreHeight) {
|
if ((currentChainTip ?? 0) > walletInfo.restoreHeight) {
|
||||||
_setListeners(walletInfo.restoreHeight, chainTip: currentChainTip);
|
_setListeners(walletInfo.restoreHeight, chainTip: currentChainTip);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
syncStatus = AttemptingSyncStatus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateTransactions();
|
await updateTransactions();
|
||||||
|
@ -258,9 +267,9 @@ abstract class ElectrumWalletBase
|
||||||
Timer.periodic(
|
Timer.periodic(
|
||||||
const Duration(minutes: 1), (timer) async => _feeRates = await electrumClient.feeRates());
|
const Duration(minutes: 1), (timer) async => _feeRates = await electrumClient.feeRates());
|
||||||
|
|
||||||
// if (!hasSilentPaymentsScanning) {
|
if (!hasSilentPaymentsScanning || walletInfo.restoreHeight == currentChainTip) {
|
||||||
syncStatus = SyncedSyncStatus();
|
syncStatus = SyncedSyncStatus();
|
||||||
// }
|
}
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
print(stacktrace);
|
print(stacktrace);
|
||||||
print(e.toString());
|
print(e.toString());
|
||||||
|
@ -312,10 +321,22 @@ abstract class ElectrumWalletBase
|
||||||
leftAmount = leftAmount - utx.value;
|
leftAmount = leftAmount - utx.value;
|
||||||
|
|
||||||
final address = _addressTypeFromStr(utx.address, network);
|
final address = _addressTypeFromStr(utx.address, network);
|
||||||
final privkey = generateECPrivate(
|
|
||||||
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
|
ECPrivate? privkey;
|
||||||
index: utx.bitcoinAddressRecord.index,
|
if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
|
||||||
network: network);
|
privkey = walletAddresses.primarySilentAddress!.b_spend.clone().tweakAdd(
|
||||||
|
BigintUtils.fromBytes(BytesUtils.fromHexString(
|
||||||
|
(utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord)
|
||||||
|
.silentPaymentTweak)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
privkey = generateECPrivate(
|
||||||
|
hd: utx.bitcoinAddressRecord.isHidden
|
||||||
|
? walletAddresses.sideHd
|
||||||
|
: walletAddresses.mainHd,
|
||||||
|
index: utx.bitcoinAddressRecord.index,
|
||||||
|
network: network);
|
||||||
|
}
|
||||||
|
|
||||||
privateKeys.add(privkey);
|
privateKeys.add(privkey);
|
||||||
inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr));
|
inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr));
|
||||||
|
@ -661,18 +682,19 @@ abstract class ElectrumWalletBase
|
||||||
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
|
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
|
||||||
|
|
||||||
Future<void> updateUnspent() async {
|
Future<void> updateUnspent() async {
|
||||||
|
List<BitcoinUnspent> updatedUnspentCoins = [];
|
||||||
|
|
||||||
// Update unspents stored from scanned silent payment transactions
|
// Update unspents stored from scanned silent payment transactions
|
||||||
transactionHistory.transactions.values.forEach((tx) {
|
transactionHistory.transactions.values.forEach((tx) {
|
||||||
if (tx.unspent != null) {
|
if (tx.unspents != null) {
|
||||||
if (!unspentCoins
|
if (!unspentCoins.any((utx) =>
|
||||||
.any((utx) => utx.hash.contains(tx.unspent!.hash) && utx.vout == tx.unspent!.vout)) {
|
tx.unspents!.any((element) => utx.hash.contains(element.hash)) &&
|
||||||
unspentCoins.add(tx.unspent!);
|
tx.unspents!.any((element) => utx.vout == element.vout))) {
|
||||||
|
updatedUnspentCoins.addAll(tx.unspents!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
List<BitcoinUnspent> updatedUnspentCoins = [];
|
|
||||||
|
|
||||||
final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet();
|
final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet();
|
||||||
|
|
||||||
await Future.wait(walletAddresses.allAddresses.map((address) => electrumClient
|
await Future.wait(walletAddresses.allAddresses.map((address) => electrumClient
|
||||||
|
@ -866,7 +888,7 @@ abstract class ElectrumWalletBase
|
||||||
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
|
final Map<String, ElectrumTransactionInfo> historiesWithDetails = {};
|
||||||
|
|
||||||
final history = await electrumClient
|
final history = await electrumClient
|
||||||
.getHistory(addressRecord.scriptHash ?? addressRecord.updateScriptHash(network)!);
|
.getHistory(addressRecord.scriptHash ?? addressRecord.updateScriptHash(network));
|
||||||
|
|
||||||
if (history.isNotEmpty) {
|
if (history.isNotEmpty) {
|
||||||
addressRecord.setAsUsed();
|
addressRecord.setAsUsed();
|
||||||
|
@ -1048,8 +1070,8 @@ abstract class ElectrumWalletBase
|
||||||
}
|
}
|
||||||
|
|
||||||
if (walletInfo.restoreHeight == 0) {
|
if (walletInfo.restoreHeight == 0) {
|
||||||
final currentHeight = await electrumClient.getCurrentBlockChainTip();
|
currentChainTip = await electrumClient.getCurrentBlockChainTip();
|
||||||
if (currentHeight != null) walletInfo.restoreHeight = currentHeight;
|
if (currentChainTip != null) walletInfo.restoreHeight = currentChainTip!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1102,13 +1124,18 @@ class SyncResponse {
|
||||||
Future<void> startRefresh(ScanData scanData) async {
|
Future<void> startRefresh(ScanData scanData) async {
|
||||||
var cachedBlockchainHeight = scanData.chainTip;
|
var cachedBlockchainHeight = scanData.chainTip;
|
||||||
|
|
||||||
|
Future<ElectrumClient> connect() async {
|
||||||
|
final electrumClient = scanData.electrumClient;
|
||||||
|
if (!electrumClient.isConnected) {
|
||||||
|
final node = scanData.node;
|
||||||
|
await electrumClient.connectToUri(Uri.parse(node));
|
||||||
|
}
|
||||||
|
return electrumClient;
|
||||||
|
}
|
||||||
|
|
||||||
Future<int> getNodeHeightOrUpdate(int baseHeight) async {
|
Future<int> getNodeHeightOrUpdate(int baseHeight) async {
|
||||||
if (cachedBlockchainHeight < baseHeight || cachedBlockchainHeight == 0) {
|
if (cachedBlockchainHeight < baseHeight || cachedBlockchainHeight == 0) {
|
||||||
final electrumClient = scanData.electrumClient;
|
final electrumClient = await connect();
|
||||||
if (!electrumClient.isConnected) {
|
|
||||||
final node = scanData.node;
|
|
||||||
await electrumClient.connectToUri(Uri.parse(node));
|
|
||||||
}
|
|
||||||
|
|
||||||
cachedBlockchainHeight =
|
cachedBlockchainHeight =
|
||||||
await electrumClient.getCurrentBlockChainTip() ?? cachedBlockchainHeight;
|
await electrumClient.getCurrentBlockChainTip() ?? cachedBlockchainHeight;
|
||||||
|
@ -1151,16 +1178,8 @@ Future<void> startRefresh(ScanData scanData) async {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
print(["Scanning from height:", syncHeight]);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all the tweaks from the block
|
final electrumClient = await connect();
|
||||||
final electrumClient = scanData.electrumClient;
|
|
||||||
if (!electrumClient.isConnected) {
|
|
||||||
final node = scanData.node;
|
|
||||||
print(node);
|
|
||||||
await electrumClient.connectToUri(Uri.parse(node));
|
|
||||||
}
|
|
||||||
|
|
||||||
List<dynamic>? tweaks;
|
List<dynamic>? tweaks;
|
||||||
try {
|
try {
|
||||||
|
@ -1177,7 +1196,6 @@ Future<void> startRefresh(ScanData scanData) async {
|
||||||
|
|
||||||
for (var i = 0; i < tweaks.length; i++) {
|
for (var i = 0; i < tweaks.length; i++) {
|
||||||
try {
|
try {
|
||||||
// final txid = tweaks.keys.toList()[i];
|
|
||||||
final details = tweaks[i] as Map<String, dynamic>;
|
final details = tweaks[i] as Map<String, dynamic>;
|
||||||
final output_pubkeys = (details["output_pubkeys"] as List<dynamic>);
|
final output_pubkeys = (details["output_pubkeys"] as List<dynamic>);
|
||||||
final tweak = details["tweak"].toString();
|
final tweak = details["tweak"].toString();
|
||||||
|
@ -1193,9 +1211,7 @@ Future<void> startRefresh(ScanData scanData) async {
|
||||||
final result = spb.scanOutputs(
|
final result = spb.scanOutputs(
|
||||||
scanData.primarySilentAddress.b_scan,
|
scanData.primarySilentAddress.b_scan,
|
||||||
scanData.primarySilentAddress.B_spend,
|
scanData.primarySilentAddress.B_spend,
|
||||||
output_pubkeys
|
output_pubkeys.map((output) => output.toString()).toList(),
|
||||||
.map((p) => ECPublic.fromBytes(BytesUtils.fromHexString(p.toString()).sublist(2)))
|
|
||||||
.toList(),
|
|
||||||
precomputedLabels: scanData.labels,
|
precomputedLabels: scanData.labels,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1206,8 +1222,7 @@ Future<void> startRefresh(ScanData scanData) async {
|
||||||
|
|
||||||
result.forEach((key, value) async {
|
result.forEach((key, value) async {
|
||||||
final t_k = value[0];
|
final t_k = value[0];
|
||||||
final address =
|
final address = ECPublic.fromHex(key).toTaprootAddress().toAddress(scanData.network);
|
||||||
ECPublic.fromHex(key).toTaprootAddress(tweak: false).toAddress(scanData.network);
|
|
||||||
|
|
||||||
final listUnspent =
|
final listUnspent =
|
||||||
await electrumClient.getListUnspentWithAddress(address, scanData.network);
|
await electrumClient.getListUnspentWithAddress(address, scanData.network);
|
||||||
|
@ -1228,6 +1243,10 @@ Future<void> startRefresh(ScanData scanData) async {
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (info == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// final tweak = value[0];
|
// final tweak = value[0];
|
||||||
// String? label;
|
// String? label;
|
||||||
// if (value.length > 1) label = value[1];
|
// if (value.length > 1) label = value[1];
|
||||||
|
@ -1237,20 +1256,16 @@ Future<void> startRefresh(ScanData scanData) async {
|
||||||
WalletType.bitcoin,
|
WalletType.bitcoin,
|
||||||
id: tx.hash,
|
id: tx.hash,
|
||||||
height: syncHeight,
|
height: syncHeight,
|
||||||
amount: tx.value,
|
amount: 0, // will be added later via unspent
|
||||||
fee: 0,
|
fee: 0,
|
||||||
direction: TransactionDirection.incoming,
|
direction: TransactionDirection.incoming,
|
||||||
isPending: false,
|
isPending: false,
|
||||||
date: DateTime.now(),
|
date: DateTime.now(),
|
||||||
confirmations: currentChainTip - syncHeight,
|
confirmations: currentChainTip - syncHeight - 1,
|
||||||
to: scanData.primarySilentAddress.toString(),
|
to: scanData.primarySilentAddress.toString(),
|
||||||
unspent: tx,
|
unspents: [tx],
|
||||||
);
|
);
|
||||||
|
|
||||||
// final status = json.decode((await http
|
|
||||||
// .get(Uri.parse("https://blockstream.info/testnet/api/tx/$txid/outspends")))
|
|
||||||
// .body) as List<dynamic>;
|
|
||||||
|
|
||||||
// bool spent = false;
|
// bool spent = false;
|
||||||
// for (final s in status) {
|
// for (final s in status) {
|
||||||
// if ((s["spent"] as bool) == true) {
|
// if ((s["spent"] as bool) == true) {
|
||||||
|
|
|
@ -35,7 +35,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||||
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
|
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
|
||||||
primarySilentAddress = silentAddress,
|
primarySilentAddress = silentAddress,
|
||||||
addressesByReceiveType =
|
addressesByReceiveType =
|
||||||
ObservableList<BitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
|
ObservableList<BaseBitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
|
||||||
receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
|
receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
|
||||||
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
|
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
|
||||||
.toSet()),
|
.toSet()),
|
||||||
|
@ -408,6 +408,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
|
||||||
updateAddressesByMatch();
|
updateAddressesByMatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
void addSilentAddresses(Iterable<BitcoinSilentPaymentAddressRecord> addresses) {
|
||||||
|
final addressesSet = this.silentAddresses.toSet();
|
||||||
|
addressesSet.addAll(addresses);
|
||||||
|
this.silentAddresses.clear();
|
||||||
|
this.silentAddresses.addAll(addressesSet);
|
||||||
|
updateAddressesByMatch();
|
||||||
|
}
|
||||||
|
|
||||||
void _validateSideHdAddresses(List<BitcoinAddressRecord> addrWithTransactions) {
|
void _validateSideHdAddresses(List<BitcoinAddressRecord> addrWithTransactions) {
|
||||||
addrWithTransactions.forEach((element) {
|
addrWithTransactions.forEach((element) {
|
||||||
if (element.address !=
|
if (element.address !=
|
||||||
|
|
|
@ -80,7 +80,7 @@ packages:
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: cake-update-v2
|
ref: cake-update-v2
|
||||||
resolved-ref: be980da9ed063da13db3907ca76d534298bb1d40
|
resolved-ref: "7634511b15e5a48bd18e3c19f81971628090c04f"
|
||||||
url: "https://github.com/cake-tech/bitcoin_base.git"
|
url: "https://github.com/cake-tech/bitcoin_base.git"
|
||||||
source: git
|
source: git
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
|
|
Loading…
Reference in a new issue