feat(CLNRest): fallback to mempool.space for raw tx broadcast as it lacks publishTransaction
This commit is contained in:
@@ -35,11 +35,11 @@ import {
|
|||||||
posStore,
|
posStore,
|
||||||
settingsStore,
|
settingsStore,
|
||||||
swapStore,
|
swapStore,
|
||||||
sweepStore,
|
|
||||||
syncStore,
|
syncStore,
|
||||||
transactionsStore,
|
transactionsStore,
|
||||||
unitsStore,
|
unitsStore,
|
||||||
utxosStore
|
utxosStore,
|
||||||
|
sweepStore
|
||||||
} from './stores/Stores';
|
} from './stores/Stores';
|
||||||
import NavigationService from './NavigationService';
|
import NavigationService from './NavigationService';
|
||||||
import PushNotificationManager from './PushNotificationManager';
|
import PushNotificationManager from './PushNotificationManager';
|
||||||
@@ -259,6 +259,7 @@ import Watchtowers from './views/Tools/Watchtowers/WatchtowerList';
|
|||||||
import AddWatchtower from './views/Tools/Watchtowers/AddWatchtower';
|
import AddWatchtower from './views/Tools/Watchtowers/AddWatchtower';
|
||||||
import WatchtowerDetails from './views/Tools/Watchtowers/WatchtowerDetails';
|
import WatchtowerDetails from './views/Tools/Watchtowers/WatchtowerDetails';
|
||||||
import ShareIntentProcessing from './views/ShareIntentProcessing';
|
import ShareIntentProcessing from './views/ShareIntentProcessing';
|
||||||
|
import WIFSweeper from './views/Tools/WIFSweeper';
|
||||||
|
|
||||||
import { isLightTheme, themeColor } from './utils/ThemeUtils';
|
import { isLightTheme, themeColor } from './utils/ThemeUtils';
|
||||||
import LinkingUtils from './utils/LinkingUtils';
|
import LinkingUtils from './utils/LinkingUtils';
|
||||||
@@ -266,7 +267,6 @@ import CreateWithdrawalRequest from './views/Tools/CreateWithdrawalRequest';
|
|||||||
import WithdrawalRequestView from './views/WithdrawalRequest';
|
import WithdrawalRequestView from './views/WithdrawalRequest';
|
||||||
import WithdrawalRequestInfo from './views/WithdrawalRequestInfo';
|
import WithdrawalRequestInfo from './views/WithdrawalRequestInfo';
|
||||||
import RedeemWithdrawalRequest from './views/RedeemWithdrawalRequest';
|
import RedeemWithdrawalRequest from './views/RedeemWithdrawalRequest';
|
||||||
import WIFSweeper from './views/WIFSweeper';
|
|
||||||
|
|
||||||
export default class App extends React.PureComponent {
|
export default class App extends React.PureComponent {
|
||||||
private backPressListenerSubscription: NativeEventSubscription;
|
private backPressListenerSubscription: NativeEventSubscription;
|
||||||
|
|||||||
+13
-9
@@ -843,15 +843,16 @@
|
|||||||
"views.Wif.createTransaction": "Create Transaction",
|
"views.Wif.createTransaction": "Create Transaction",
|
||||||
"views.Wif.invalidBase58Chars": "Invalid base58 characters",
|
"views.Wif.invalidBase58Chars": "Invalid base58 characters",
|
||||||
"views.Wif.noUtxos": "The unspent outputs from this private key contain insufficient funds to spend(0 sats).",
|
"views.Wif.noUtxos": "The unspent outputs from this private key contain insufficient funds to spend(0 sats).",
|
||||||
"views.Sweep.failedToFetchTxHex": "Failed to fetch transaction hex for {{txid}}",
|
"views.Wif.failedToFetchTxHex": "Failed to fetch transaction hex for {{txid}}",
|
||||||
"views.Sweep.failedToFetchTxDetails": "Failed to fetch transaction details for {{txid}}",
|
"views.Wif.failedToFetchTxDetails": "Failed to fetch transaction details for {{txid}}",
|
||||||
"views.Sweep.outputNotFound": "Output {{vout}} not found in transaction {{txid}}",
|
"views.Wif.outputNotFound": "Output {{vout}} not found in transaction {{txid}}",
|
||||||
"views.Sweep.outputIndexNotFound": "Output index {{vout}} not found in transaction {{txid}}",
|
"views.Wif.outputIndexNotFound": "Output index {{vout}} not found in transaction {{txid}}",
|
||||||
"views.Sweep.failedToDerivePubkey": "Failed to derive public key",
|
"views.Wif.failedToDerivePubkey": "Failed to derive public key",
|
||||||
"views.Sweep.noUtxosFound": "No UTXOs found for the given private key",
|
"views.Wif.noUtxosFound": "No UTXOs found for the given private key",
|
||||||
"views.Sweep.addressTypeNotSupported": "This address type is not yet supported for sweeping",
|
"views.Wif.addressTypeNotSupported": "This address type is not yet supported for sweeping",
|
||||||
"views.Sweep.insufficientFundsAfterFees": "Insufficient funds after fees",
|
"views.Wif.insufficientFundsAfterFees": "Insufficient funds after fees",
|
||||||
"views.Sweep.noTaprootSupport": "Taproot (P2TR) sweep is not supported yet",
|
"views.Wif.failedToCreateTransaction": "Failed to create transaction. Please check your inputs and try again.",
|
||||||
|
"views.Wif.errorFetchingUtxos": "Error fetching UTXOs",
|
||||||
"views.Settings.title": "Settings",
|
"views.Settings.title": "Settings",
|
||||||
"views.Settings.enabled": "Enabled",
|
"views.Settings.enabled": "Enabled",
|
||||||
"views.Settings.disabled": "Disabled",
|
"views.Settings.disabled": "Disabled",
|
||||||
@@ -1539,6 +1540,9 @@
|
|||||||
"views.TxHex.channelWarning": "No channel loaded in the channel funding flow engine. DO NOT PUBLISH if this transaction is for a channel open.",
|
"views.TxHex.channelWarning": "No channel loaded in the channel funding flow engine. DO NOT PUBLISH if this transaction is for a channel open.",
|
||||||
"views.TxHex.broadcast": "Broadcast TX",
|
"views.TxHex.broadcast": "Broadcast TX",
|
||||||
"views.TxHex.finalizeFlowAndBroadcast": "Finalize funding flow + Broadcast",
|
"views.TxHex.finalizeFlowAndBroadcast": "Finalize funding flow + Broadcast",
|
||||||
|
"views.TxHex.broadcastingViaMempool": "Broadcasting via Mempool.space",
|
||||||
|
"views.TxHex.thirdPartyBroadcastWarning": "This implementation requires broadcasting via a third-party service (Mempool.space).",
|
||||||
|
"views.TxHex.transactionBroadcastCancelled": "Transaction broadcast cancelled",
|
||||||
"pos.customItem": "Custom item",
|
"pos.customItem": "Custom item",
|
||||||
"time.seconds": "Seconds",
|
"time.seconds": "Seconds",
|
||||||
"time.minutes": "Minutes",
|
"time.minutes": "Minutes",
|
||||||
|
|||||||
+1
-1
@@ -21,11 +21,11 @@ import PaymentsStore from './PaymentsStore';
|
|||||||
import PosStore from './PosStore';
|
import PosStore from './PosStore';
|
||||||
import SettingsStore from './SettingsStore';
|
import SettingsStore from './SettingsStore';
|
||||||
import SwapStore from './SwapStore';
|
import SwapStore from './SwapStore';
|
||||||
import SweepStore from './SweepStore';
|
|
||||||
import SyncStore from './SyncStore';
|
import SyncStore from './SyncStore';
|
||||||
import TransactionsStore from './TransactionsStore';
|
import TransactionsStore from './TransactionsStore';
|
||||||
import UnitsStore from './UnitsStore';
|
import UnitsStore from './UnitsStore';
|
||||||
import UTXOsStore from './UTXOsStore';
|
import UTXOsStore from './UTXOsStore';
|
||||||
|
import SweepStore from './SweepStore';
|
||||||
|
|
||||||
export const settingsStore = new SettingsStore();
|
export const settingsStore = new SettingsStore();
|
||||||
export const modalStore = new ModalStore();
|
export const modalStore = new ModalStore();
|
||||||
|
|||||||
+24
-30
@@ -12,10 +12,8 @@ import { localeString } from '../utils/LocaleUtils';
|
|||||||
import ecc from '../zeus_modules/noble_ecc';
|
import ecc from '../zeus_modules/noble_ecc';
|
||||||
|
|
||||||
export default class SweepStore {
|
export default class SweepStore {
|
||||||
@observable loading: boolean = false;
|
|
||||||
@observable sweepErrorMsg: string | null = null;
|
@observable sweepErrorMsg: string | null = null;
|
||||||
@observable sweepError: boolean = false;
|
@observable sweepError: boolean = false;
|
||||||
@observable sweepTxHex: string | null = null;
|
|
||||||
@observable onChainBalance: number;
|
@observable onChainBalance: number;
|
||||||
@observable destination: string;
|
@observable destination: string;
|
||||||
@observable txHex: string | null = null;
|
@observable txHex: string | null = null;
|
||||||
@@ -37,11 +35,6 @@ export default class SweepStore {
|
|||||||
this.nodeInfoStore = nodeInfoStore;
|
this.nodeInfoStore = nodeInfoStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
setLoading(loading: boolean) {
|
|
||||||
this.loading = loading;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
resetSweepError() {
|
resetSweepError() {
|
||||||
this.sweepError = false;
|
this.sweepError = false;
|
||||||
@@ -53,7 +46,7 @@ export default class SweepStore {
|
|||||||
`${wifUtils.baseUrl(network)}/address/${address}/utxo`
|
`${wifUtils.baseUrl(network)}/address/${address}/utxo`
|
||||||
);
|
);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(localeString(`Error fetching UTXOs`));
|
throw new Error(localeString('views.Wif.errorFetchingUtxos'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const utxos = await res.json();
|
const utxos = await res.json();
|
||||||
@@ -104,7 +97,7 @@ export default class SweepStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(localeString('views.Sweep.addressTypeNotSupported'));
|
throw new Error(localeString('views.Wif.noUtxosFound'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@@ -141,7 +134,7 @@ export default class SweepStore {
|
|||||||
if (this.addressType === 'p2tr') {
|
if (this.addressType === 'p2tr') {
|
||||||
this.sweepError = true;
|
this.sweepError = true;
|
||||||
this.sweepErrorMsg = localeString(
|
this.sweepErrorMsg = localeString(
|
||||||
'views.Sweep.addressTypeNotSupported'
|
'views.Wif.addressTypeNotSupported'
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -153,11 +146,12 @@ export default class SweepStore {
|
|||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${wifUtils.baseUrl(networkStr)}/tx/${txid}/hex`
|
`${wifUtils.baseUrl(networkStr)}/tx/${txid}/hex`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!res.ok)
|
if (!res.ok)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
localeString(
|
localeString('views.Wif.failedToFetchTxHex', {
|
||||||
`views.Sweep.failedToFetchTxHex:${txid}`
|
txid
|
||||||
)
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const rawTxHex = await res.text();
|
const rawTxHex = await res.text();
|
||||||
@@ -177,9 +171,9 @@ export default class SweepStore {
|
|||||||
);
|
);
|
||||||
if (!res.ok)
|
if (!res.ok)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
localeString(
|
localeString('views.Wif.failedToFetchTxDetails', {
|
||||||
`views.Sweep.failedToFetchTxDetails:${txid}`
|
txid
|
||||||
)
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const tx = await res.json();
|
const tx = await res.json();
|
||||||
@@ -187,9 +181,10 @@ export default class SweepStore {
|
|||||||
|
|
||||||
if (!output)
|
if (!output)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
localeString(
|
localeString('views.Sweep.outputIndexNotFound', {
|
||||||
`views.Sweep.outputIndexNotFound:${vout}:${txid}`
|
vout,
|
||||||
)
|
txid
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const value = Math.round(output.value);
|
const value = Math.round(output.value);
|
||||||
@@ -218,9 +213,9 @@ export default class SweepStore {
|
|||||||
);
|
);
|
||||||
if (!res.ok)
|
if (!res.ok)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
localeString(
|
localeString('views.Wif.failedToFetchTxDetails', {
|
||||||
`views.Sweep.failedToFetchTxDetails:${txid}`
|
txid
|
||||||
)
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const tx = await res.json();
|
const tx = await res.json();
|
||||||
@@ -228,9 +223,10 @@ export default class SweepStore {
|
|||||||
|
|
||||||
if (!output)
|
if (!output)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
localeString(
|
localeString('views.Wif.outputNotFound', {
|
||||||
`views.Sweep.outputNotFound:${vout}:${txid}`
|
vout,
|
||||||
)
|
txid
|
||||||
|
})
|
||||||
);
|
);
|
||||||
const scriptPubKeyHex = output.scriptpubkey;
|
const scriptPubKeyHex = output.scriptpubkey;
|
||||||
const script = Buffer.from(scriptPubKeyHex, 'hex');
|
const script = Buffer.from(scriptPubKeyHex, 'hex');
|
||||||
@@ -240,7 +236,7 @@ export default class SweepStore {
|
|||||||
|
|
||||||
if (!fullPub) {
|
if (!fullPub) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
localeString('views.Sweep.failedToDerivePubkey')
|
localeString('views.Wif.failedToDerivePubkey')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,9 +272,7 @@ export default class SweepStore {
|
|||||||
|
|
||||||
const fullPub = ecc.pointFromScalar(privateKey, true);
|
const fullPub = ecc.pointFromScalar(privateKey, true);
|
||||||
if (!fullPub)
|
if (!fullPub)
|
||||||
throw new Error(
|
throw new Error(localeString('views.Wif.failedToDerivePubkey'));
|
||||||
localeString('views.Sweep.failedToDerivePubkey')
|
|
||||||
);
|
|
||||||
|
|
||||||
const signer: bitcoin.Signer = {
|
const signer: bitcoin.Signer = {
|
||||||
publicKey: Buffer.from(fullPub),
|
publicKey: Buffer.from(fullPub),
|
||||||
@@ -310,7 +304,7 @@ export default class SweepStore {
|
|||||||
|
|
||||||
if (this.valueToSend <= 0)
|
if (this.valueToSend <= 0)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
localeString('views.Sweep.insufficientFundsAfterFees')
|
localeString('views.Wif.insufficientFundsAfterFees')
|
||||||
);
|
);
|
||||||
|
|
||||||
this.psbt.addOutput({
|
this.psbt.addOutput({
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ export default class TransactionsStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
public broadcast = (raw_final_tx: string) => {
|
public broadcast = async (raw_final_tx: string) => {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
const tx_hex = raw_final_tx.includes('=')
|
const tx_hex = raw_final_tx.includes('=')
|
||||||
@@ -144,6 +144,48 @@ export default class TransactionsStore {
|
|||||||
txid = tx.getId();
|
txid = tx.getId();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
|
// CLN REST does not support publishTransaction; broadcast via mempool.space
|
||||||
|
if (this.settingsStore.implementation === 'cln-rest') {
|
||||||
|
const headers = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Content-Type': 'text/plain'
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const url = `https://mempool.space/${
|
||||||
|
this.nodeInfoStore.nodeInfo.isTestNet ? 'testnet/' : ''
|
||||||
|
}api/tx`;
|
||||||
|
|
||||||
|
return ReactNativeBlobUtil.fetch('POST', url, headers, tx_hex)
|
||||||
|
.then((response: any) => {
|
||||||
|
const status = response.info().status;
|
||||||
|
const data = response.data;
|
||||||
|
if (status == 200) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.txid = data || txid;
|
||||||
|
this.publishSuccess = true;
|
||||||
|
this.loading = false;
|
||||||
|
this.channelsStore.resetOpenChannel();
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
runInAction(() => {
|
||||||
|
this.error_msg = errorToUserFriendly(data);
|
||||||
|
this.error = true;
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
runInAction(() => {
|
||||||
|
this.error_msg = errorToUserFriendly(
|
||||||
|
err?.error || err?.message || err?.toString()
|
||||||
|
);
|
||||||
|
this.error = true;
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return BackendUtils.publishTransaction({
|
return BackendUtils.publishTransaction({
|
||||||
tx_hex
|
tx_hex
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,34 +5,34 @@ import { Route } from '@react-navigation/native';
|
|||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { TouchableOpacity } from 'react-native-gesture-handler';
|
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||||
|
|
||||||
import InvoicesStore from '../stores/InvoicesStore';
|
import InvoicesStore from '../../stores/InvoicesStore';
|
||||||
import SweepStore from '../stores/SweepStore';
|
import SweepStore from '../../stores/SweepStore';
|
||||||
|
|
||||||
import Button from '../components/Button';
|
import Button from '../../components/Button';
|
||||||
import { ErrorMessage } from '../components/SuccessErrorMessage';
|
import { ErrorMessage } from '../../components/SuccessErrorMessage';
|
||||||
import Header from '../components/Header';
|
import Header from '../../components/Header';
|
||||||
import Screen from '../components/Screen';
|
import Screen from '../../components/Screen';
|
||||||
import TextInput from '../components/TextInput';
|
import TextInput from '../../components/TextInput';
|
||||||
import LoadingIndicator from '../components/LoadingIndicator';
|
import LoadingIndicator from '../../components/LoadingIndicator';
|
||||||
import OnchainFeeInput from '../components/OnchainFeeInput';
|
import OnchainFeeInput from '../../components/OnchainFeeInput';
|
||||||
import ShowHideToggle from '../components/ShowHideToggle';
|
import ShowHideToggle from '../../components/ShowHideToggle';
|
||||||
|
|
||||||
import { localeString } from '../utils/LocaleUtils';
|
import { localeString } from '../../utils/LocaleUtils';
|
||||||
import { themeColor } from '../utils/ThemeUtils';
|
import { themeColor } from '../../utils/ThemeUtils';
|
||||||
import wifUtils from '../utils/WIFUtils';
|
import wifUtils from '../../utils/WIFUtils';
|
||||||
import AddressUtils from '../utils/AddressUtils';
|
import AddressUtils from '../../utils/AddressUtils';
|
||||||
|
|
||||||
import Scan from '../assets/images/SVG/Scan.svg';
|
import Scan from '../../assets/images/SVG/Scan.svg';
|
||||||
|
|
||||||
interface SweepProps {
|
interface WIFSweepProps {
|
||||||
exitSetup: any;
|
exitSetup: any;
|
||||||
navigation: StackNavigationProp<any, any>;
|
navigation: StackNavigationProp<any, any>;
|
||||||
InvoicesStore: InvoicesStore;
|
InvoicesStore: InvoicesStore;
|
||||||
SweepStore: SweepStore;
|
SweepStore: SweepStore;
|
||||||
route: Route<'Sweep', { wif: string }>;
|
route: Route<'WIFSweeper', { wif: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SweepState {
|
interface WIFSweepState {
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
hidden: boolean;
|
hidden: boolean;
|
||||||
isValid: boolean;
|
isValid: boolean;
|
||||||
@@ -41,13 +41,14 @@ interface SweepState {
|
|||||||
onChainAddressloading: boolean;
|
onChainAddressloading: boolean;
|
||||||
feeRate: string;
|
feeRate: string;
|
||||||
feeLoadingError: boolean;
|
feeLoadingError: boolean;
|
||||||
|
isWifValid: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@inject('SweepStore', 'InvoicesStore')
|
@inject('SweepStore', 'InvoicesStore')
|
||||||
@observer
|
@observer
|
||||||
export default class WIFSweeper extends React.Component<
|
export default class WIFSweeper extends React.Component<
|
||||||
SweepProps,
|
WIFSweepProps,
|
||||||
SweepState
|
WIFSweepState
|
||||||
> {
|
> {
|
||||||
state = {
|
state = {
|
||||||
privateKey: '',
|
privateKey: '',
|
||||||
@@ -57,7 +58,8 @@ export default class WIFSweeper extends React.Component<
|
|||||||
loading: false,
|
loading: false,
|
||||||
onChainAddressloading: false,
|
onChainAddressloading: false,
|
||||||
feeRate: '2',
|
feeRate: '2',
|
||||||
feeLoadingError: false
|
feeLoadingError: false,
|
||||||
|
isWifValid: true
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -74,7 +76,7 @@ export default class WIFSweeper extends React.Component<
|
|||||||
this.initFromProps(this.props);
|
this.initFromProps(this.props);
|
||||||
}
|
}
|
||||||
|
|
||||||
initFromProps(props: SweepProps) {
|
initFromProps(props: WIFSweepProps) {
|
||||||
const { route } = props;
|
const { route } = props;
|
||||||
const scannedKey = route.params?.wif;
|
const scannedKey = route.params?.wif;
|
||||||
|
|
||||||
@@ -85,7 +87,7 @@ export default class WIFSweeper extends React.Component<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: SweepProps) {
|
componentDidUpdate(prevProps: WIFSweepProps) {
|
||||||
if (prevProps.route.params?.wif !== this.props.route.params?.wif) {
|
if (prevProps.route.params?.wif !== this.props.route.params?.wif) {
|
||||||
this.initFromProps(this.props);
|
this.initFromProps(this.props);
|
||||||
}
|
}
|
||||||
@@ -160,11 +162,25 @@ export default class WIFSweeper extends React.Component<
|
|||||||
'views.Wif.enterPrivateKeyPlaceholder'
|
'views.Wif.enterPrivateKeyPlaceholder'
|
||||||
)}
|
)}
|
||||||
value={privateKey}
|
value={privateKey}
|
||||||
onChangeText={(text: string) =>
|
onChangeText={(text: string) => {
|
||||||
this.setState({ privateKey: text })
|
const isWifValid =
|
||||||
}
|
text.length === 0 ||
|
||||||
|
wifUtils.validateWIF(text).isValid;
|
||||||
|
this.setState({
|
||||||
|
privateKey: text,
|
||||||
|
isWifValid
|
||||||
|
});
|
||||||
|
}}
|
||||||
secureTextEntry={hidden}
|
secureTextEntry={hidden}
|
||||||
style={{ flex: 1, marginRight: 15 }}
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
marginRight: 15
|
||||||
|
}}
|
||||||
|
textColor={
|
||||||
|
!this.state.isWifValid
|
||||||
|
? themeColor('delete')
|
||||||
|
: themeColor('text')
|
||||||
|
}
|
||||||
locked={loading}
|
locked={loading}
|
||||||
/>
|
/>
|
||||||
<ShowHideToggle
|
<ShowHideToggle
|
||||||
@@ -207,6 +223,13 @@ export default class WIFSweeper extends React.Component<
|
|||||||
marginHorizontal: 20
|
marginHorizontal: 20
|
||||||
}}
|
}}
|
||||||
value={SweepStore.destination}
|
value={SweepStore.destination}
|
||||||
|
textColor={
|
||||||
|
SweepStore.destination &&
|
||||||
|
!this.state.isValid
|
||||||
|
? themeColor('delete')
|
||||||
|
: themeColor('text')
|
||||||
|
}
|
||||||
|
locked={loading}
|
||||||
/>
|
/>
|
||||||
{onChainAddressloading && (
|
{onChainAddressloading && (
|
||||||
<View style={styles.loadingOverlay}>
|
<View style={styles.loadingOverlay}>
|
||||||
@@ -228,7 +251,7 @@ export default class WIFSweeper extends React.Component<
|
|||||||
onChainAddressloading: true
|
onChainAddressloading: true
|
||||||
});
|
});
|
||||||
this.props.SweepStore.resetSweepError();
|
this.props.SweepStore.resetSweepError();
|
||||||
const { isValid } =
|
const { isValid, error } =
|
||||||
wifUtils.validateWIF(privateKey);
|
wifUtils.validateWIF(privateKey);
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
await this.props.SweepStore.prepareSweepInputs(
|
await this.props.SweepStore.prepareSweepInputs(
|
||||||
@@ -236,16 +259,17 @@ export default class WIFSweeper extends React.Component<
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
localeString(
|
error ||
|
||||||
'views.Wif.invalidWif'
|
localeString(
|
||||||
)
|
'views.Wif.invalidWif'
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (SweepStore.sweepError) {
|
if (SweepStore.sweepError) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
SweepStore.sweepErrorMsg ||
|
SweepStore.sweepErrorMsg ||
|
||||||
localeString(
|
localeString(
|
||||||
'views.Sweep.unknownError'
|
'views.Wif.invalidWif'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -295,7 +319,9 @@ export default class WIFSweeper extends React.Component<
|
|||||||
)}
|
)}
|
||||||
secondary
|
secondary
|
||||||
disabled={
|
disabled={
|
||||||
!privateKey || !!SweepStore.destination
|
!privateKey ||
|
||||||
|
!this.state.isWifValid ||
|
||||||
|
!!SweepStore.destination
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -332,12 +358,30 @@ export default class WIFSweeper extends React.Component<
|
|||||||
this.state.privateKey
|
this.state.privateKey
|
||||||
);
|
);
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
await this.props.SweepStore.finalizeSweepTransaction(
|
try {
|
||||||
feeRate
|
await this.props.SweepStore.finalizeSweepTransaction(
|
||||||
);
|
feeRate
|
||||||
navigation.navigate('TxHex', {
|
);
|
||||||
txHex: SweepStore.txHex
|
if (SweepStore.txHex) {
|
||||||
});
|
navigation.navigate('TxHex', {
|
||||||
|
txHex: SweepStore.txHex
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
localeString(
|
||||||
|
'views.Wif.failedToCreateTransaction'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
this.props.SweepStore.sweepError = true;
|
||||||
|
this.props.SweepStore.sweepErrorMsg =
|
||||||
|
e.message ||
|
||||||
|
localeString(
|
||||||
|
'views.Wif.failedToCreateTransaction'
|
||||||
|
);
|
||||||
|
this.setState({ error: e.message });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.props.SweepStore.sweepError = true;
|
this.props.SweepStore.sweepError = true;
|
||||||
this.props.SweepStore.sweepErrorMsg =
|
this.props.SweepStore.sweepErrorMsg =
|
||||||
@@ -349,6 +393,7 @@ export default class WIFSweeper extends React.Component<
|
|||||||
!privateKey ||
|
!privateKey ||
|
||||||
loading ||
|
loading ||
|
||||||
!SweepStore.destination ||
|
!SweepStore.destination ||
|
||||||
|
!this.state.isValid ||
|
||||||
sweepError ||
|
sweepError ||
|
||||||
feeLoadingError ||
|
feeLoadingError ||
|
||||||
(!!error && error.length > 0)
|
(!!error && error.length > 0)
|
||||||
+68
-24
@@ -4,7 +4,8 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View
|
View,
|
||||||
|
Alert
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { inject, observer } from 'mobx-react';
|
import { inject, observer } from 'mobx-react';
|
||||||
import { ButtonGroup } from 'react-native-elements';
|
import { ButtonGroup } from 'react-native-elements';
|
||||||
@@ -12,6 +13,7 @@ import { UR, UREncoder } from '@ngraveio/bc-ur';
|
|||||||
import clone from 'lodash/clone';
|
import clone from 'lodash/clone';
|
||||||
import { Route } from '@react-navigation/native';
|
import { Route } from '@react-navigation/native';
|
||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
|
import { runInAction } from 'mobx';
|
||||||
|
|
||||||
const bitcoin = require('bitcoinjs-lib');
|
const bitcoin = require('bitcoinjs-lib');
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ import UrlUtils from '../utils/UrlUtils';
|
|||||||
import ChannelsStore from '../stores/ChannelsStore';
|
import ChannelsStore from '../stores/ChannelsStore';
|
||||||
import NodeInfoStore from '../stores/NodeInfoStore';
|
import NodeInfoStore from '../stores/NodeInfoStore';
|
||||||
import TransactionsStore from '../stores/TransactionsStore';
|
import TransactionsStore from '../stores/TransactionsStore';
|
||||||
|
import SettingsStore from '../stores/SettingsStore';
|
||||||
import {
|
import {
|
||||||
getQRAnimationInterval,
|
getQRAnimationInterval,
|
||||||
QRAnimationSpeed
|
QRAnimationSpeed
|
||||||
@@ -41,6 +44,7 @@ import {
|
|||||||
|
|
||||||
interface TxHexProps {
|
interface TxHexProps {
|
||||||
navigation: StackNavigationProp<any, any>;
|
navigation: StackNavigationProp<any, any>;
|
||||||
|
SettingsStore: SettingsStore;
|
||||||
ChannelsStore: ChannelsStore;
|
ChannelsStore: ChannelsStore;
|
||||||
NodeInfoStore: NodeInfoStore;
|
NodeInfoStore: NodeInfoStore;
|
||||||
TransactionsStore: TransactionsStore;
|
TransactionsStore: TransactionsStore;
|
||||||
@@ -59,7 +63,7 @@ interface TxHexState {
|
|||||||
qrAnimationSpeed: QRAnimationSpeed;
|
qrAnimationSpeed: QRAnimationSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@inject('ChannelsStore', 'NodeInfoStore', 'TransactionsStore')
|
@inject('ChannelsStore', 'NodeInfoStore', 'TransactionsStore', 'SettingsStore')
|
||||||
@observer
|
@observer
|
||||||
export default class TxHex extends React.Component<TxHexProps, TxHexState> {
|
export default class TxHex extends React.Component<TxHexProps, TxHexState> {
|
||||||
private qrAnimationInterval?: any;
|
private qrAnimationInterval?: any;
|
||||||
@@ -147,6 +151,67 @@ export default class TxHex extends React.Component<TxHexProps, TxHexState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleBroadcast = async () => {
|
||||||
|
const { txHex } = this.state;
|
||||||
|
const { navigation, SettingsStore, TransactionsStore, ChannelsStore } =
|
||||||
|
this.props;
|
||||||
|
const { pending_chan_ids } = ChannelsStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (pending_chan_ids.length > 0) {
|
||||||
|
await TransactionsStore.finalizeTxHexAndBroadcastChannel(
|
||||||
|
txHex,
|
||||||
|
pending_chan_ids
|
||||||
|
);
|
||||||
|
navigation.navigate('SendingOnChain');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SettingsStore.implementation === 'cln-rest') {
|
||||||
|
const userConfirmed = await new Promise<boolean>((resolve) => {
|
||||||
|
Alert.alert(
|
||||||
|
localeString('views.TxHex.broadcastingViaMempool'),
|
||||||
|
localeString('views.TxHex.thirdPartyBroadcastWarning'),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: localeString('general.cancel'),
|
||||||
|
style: 'cancel',
|
||||||
|
onPress: () => resolve(false)
|
||||||
|
},
|
||||||
|
{ text: 'OK', onPress: () => resolve(true) }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userConfirmed) {
|
||||||
|
runInAction(() => {
|
||||||
|
TransactionsStore.error = true;
|
||||||
|
TransactionsStore.error_msg = localeString(
|
||||||
|
'views.TxHex.transactionBroadcastCancelled'
|
||||||
|
);
|
||||||
|
TransactionsStore.loading = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await TransactionsStore.broadcast(txHex);
|
||||||
|
|
||||||
|
if (!TransactionsStore.error) {
|
||||||
|
navigation.navigate('SendingOnChain');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Broadcast failed:', error);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
TransactionsStore.error = true;
|
||||||
|
TransactionsStore.error_msg =
|
||||||
|
error?.message || 'Broadcast failed';
|
||||||
|
TransactionsStore.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { ChannelsStore, NodeInfoStore, TransactionsStore, navigation } =
|
const { ChannelsStore, NodeInfoStore, TransactionsStore, navigation } =
|
||||||
this.props;
|
this.props;
|
||||||
@@ -415,28 +480,7 @@ export default class TxHex extends React.Component<TxHexProps, TxHexState> {
|
|||||||
? 'views.TxHex.finalizeFlowAndBroadcast'
|
? 'views.TxHex.finalizeFlowAndBroadcast'
|
||||||
: 'views.TxHex.broadcast'
|
: 'views.TxHex.broadcast'
|
||||||
)}
|
)}
|
||||||
onPress={() => {
|
onPress={this.handleBroadcast}
|
||||||
if (
|
|
||||||
pending_chan_ids.length > 0
|
|
||||||
) {
|
|
||||||
TransactionsStore.finalizeTxHexAndBroadcastChannel(
|
|
||||||
txHex,
|
|
||||||
pending_chan_ids
|
|
||||||
).then(() => {
|
|
||||||
navigation.navigate(
|
|
||||||
'SendingOnChain'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
TransactionsStore.broadcast(
|
|
||||||
txHex
|
|
||||||
).then(() => {
|
|
||||||
navigation.navigate(
|
|
||||||
'SendingOnChain'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
containerStyle={{ width: '100%' }}
|
containerStyle={{ width: '100%' }}
|
||||||
buttonStyle={{ height: 40 }}
|
buttonStyle={{ height: 40 }}
|
||||||
tertiary
|
tertiary
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
pick 8f75f07d # fix: disable create transaction button on fee loading error
|
|
||||||
fixup b5f94d6d # fix: errors in the sweep view
|
|
||||||
|
|
||||||
# Rebase 5b024b2a..b5f94d6d onto 5b024b2a (2 commands)
|
|
||||||
#
|
|
||||||
# Commands:
|
|
||||||
# p, pick <commit> = use commit
|
|
||||||
# r, reword <commit> = use commit, but edit the commit message
|
|
||||||
# e, edit <commit> = use commit, but stop for amending
|
|
||||||
# s, squash <commit> = use commit, but meld into previous commit
|
|
||||||
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
|
|
||||||
# commit's log message, unless -C is used, in which case
|
|
||||||
# keep only this commit's message; -c is same as -C but
|
|
||||||
# opens the editor
|
|
||||||
# x, exec <command> = run command (the rest of the line) using shell
|
|
||||||
# b, break = stop here (continue rebase later with 'git rebase --continue')
|
|
||||||
# d, drop <commit> = remove commit
|
|
||||||
# l, label <label> = label current HEAD with a name
|
|
||||||
# t, reset <label> = reset HEAD to a label
|
|
||||||
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
|
|
||||||
# create a merge commit using the original merge commit's
|
|
||||||
# message (or the oneline, if no original merge commit was
|
|
||||||
# specified); use -c <commit> to reword the commit message
|
|
||||||
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
|
|
||||||
# to this position in the new commits. The <ref> is
|
|
||||||
# updated at the end of the rebase
|
|
||||||
#
|
|
||||||
# These lines can be re-ordered; they are executed from top to bottom.
|
|
||||||
#
|
|
||||||
# If you remove a line here THAT COMMIT WILL BE LOST.
|
|
||||||
#
|
|
||||||
# However, if you remove everything, the rebase will be aborted.
|
|
||||||
#
|
|
||||||
Reference in New Issue
Block a user