Merge pull request #4195 from kaloudis/deduplicate-receive

refactor: Receive: deduplicate componentDidMount/render locals
This commit is contained in:
Evan Kaloudis
2026-06-26 11:11:15 -04:00
committed by GitHub
6 changed files with 216 additions and 49 deletions
+6 -1
View File
@@ -80,6 +80,8 @@ import {
verifyMessageWithAddr as verifyMsgWithAddr
} from '../lndmobile/wallet';
import { toLnrpcAddressTypeNum } from '../utils/LndUtils';
export default class EmbeddedLND extends LND {
openChannelListener: any;
@@ -123,7 +125,10 @@ export default class EmbeddedLND extends LND {
reversed?: boolean;
}) => await listPayments(params);
getNewAddress = async (data: any) =>
await newAddress(data.type, data.account);
await newAddress(
toLnrpcAddressTypeNum(data?.type) as any,
data?.account
);
getNewChangeAddress = async (data: any) =>
await newChangeAddress(data.type, data.account);
openChannelSync = async (data: OpenChannelRequest) =>
+8 -1
View File
@@ -9,6 +9,7 @@ import OpenChannelRequest from './../models/OpenChannelRequest';
import Base64Utils from './../utils/Base64Utils';
import VersionUtils from './../utils/VersionUtils';
import { localeString } from './../utils/LocaleUtils';
import { toLnrpcAddressType } from './../utils/LndUtils';
import { Hash as sha256Hash } from 'fast-sha256';
import BigNumber from 'bignumber.js';
@@ -371,7 +372,13 @@ export default class LND {
}`
);
getNewAddress = (data: any) => this.getRequest('/v1/newaddress', data);
getNewAddress = (data: any) => {
const params: any = { ...data };
const type = toLnrpcAddressType(params.type);
if (type !== undefined) params.type = type;
else delete params.type;
return this.getRequest('/v1/newaddress', params);
};
getNewChangeAddress = (data: any) =>
this.postRequest('/v2/wallet/address/next', data);
openChannelSync = (data: OpenChannelRequest) => {
+2 -10
View File
@@ -10,20 +10,12 @@ import OpenChannelRequest from '../models/OpenChannelRequest';
import Base64Utils from '../utils/Base64Utils';
import { snakeize } from '../utils/DataFormatUtils';
import { toLnrpcAddressType } from '../utils/LndUtils';
import VersionUtils from '../utils/VersionUtils';
import { Hash as sha256Hash } from 'fast-sha256';
import BigNumber from 'bignumber.js';
const ADDRESS_TYPES = [
'WITNESS_PUBKEY_HASH',
'NESTED_PUBKEY_HASH',
'UNUSED_WITNESS_PUBKEY_HASH',
'UNUSED_NESTED_PUBKEY_HASH',
'TAPROOT_PUBKEY',
'UNUSED_TAPROOT_PUBKEY'
];
const NEXT_ADDR_MAP: any = {
WITNESS_PUBKEY_HASH: 0,
NESTED_PUBKEY_HASH: 1,
@@ -220,7 +212,7 @@ export default class LightningNodeConnect {
getNewAddress = async (data: any) =>
await this.lnc.lnd.lightning
.newAddress({
type: ADDRESS_TYPES[data.type] || data.type,
type: toLnrpcAddressType(data.type),
account: data.account || 'default'
})
.then((data: walletrpc.AddrRequest) => snakeize(data));
+103
View File
@@ -0,0 +1,103 @@
import { toLnrpcAddressType, toLnrpcAddressTypeNum } from './LndUtils';
describe('LndUtils', () => {
describe('toLnrpcAddressType', () => {
it('returns the lnrpc enum name for numeric-string input from the picker / settings', () => {
expect(toLnrpcAddressType('0')).toEqual('WITNESS_PUBKEY_HASH');
expect(toLnrpcAddressType('1')).toEqual('NESTED_PUBKEY_HASH');
expect(toLnrpcAddressType('2')).toEqual(
'UNUSED_WITNESS_PUBKEY_HASH'
);
expect(toLnrpcAddressType('3')).toEqual(
'UNUSED_NESTED_PUBKEY_HASH'
);
expect(toLnrpcAddressType('4')).toEqual('TAPROOT_PUBKEY');
expect(toLnrpcAddressType('5')).toEqual('UNUSED_TAPROOT_PUBKEY');
});
it('accepts numeric input as well as numeric strings', () => {
expect(toLnrpcAddressType(0)).toEqual('WITNESS_PUBKEY_HASH');
expect(toLnrpcAddressType(1)).toEqual('NESTED_PUBKEY_HASH');
expect(toLnrpcAddressType(4)).toEqual('TAPROOT_PUBKEY');
});
it('maps walletrpc enum names returned by ListAccounts to the lnrpc equivalent', () => {
// lnrpc has no hybrid variant; nested-segwit p2sh-p2wkh is the
// closest equivalent the receiver sees.
expect(toLnrpcAddressType('NESTED_WITNESS_PUBKEY_HASH')).toEqual(
'NESTED_PUBKEY_HASH'
);
expect(
toLnrpcAddressType('HYBRID_NESTED_WITNESS_PUBKEY_HASH')
).toEqual('NESTED_PUBKEY_HASH');
});
it('passes lnrpc enum names through unchanged', () => {
expect(toLnrpcAddressType('WITNESS_PUBKEY_HASH')).toEqual(
'WITNESS_PUBKEY_HASH'
);
expect(toLnrpcAddressType('NESTED_PUBKEY_HASH')).toEqual(
'NESTED_PUBKEY_HASH'
);
expect(toLnrpcAddressType('TAPROOT_PUBKEY')).toEqual(
'TAPROOT_PUBKEY'
);
});
it('returns undefined for null / undefined input so the backend default kicks in', () => {
expect(toLnrpcAddressType(undefined)).toBeUndefined();
expect(toLnrpcAddressType(null)).toBeUndefined();
});
it('passes unrecognised values through verbatim rather than swallowing them', () => {
// Lets callers decide how to handle (LND surfaces a clear
// "not a valid value" error rather than silently picking '0').
expect(toLnrpcAddressType('SOMETHING_NEW')).toEqual(
'SOMETHING_NEW'
);
});
});
describe('toLnrpcAddressTypeNum', () => {
it('returns the lnrpc numeric AddressType for numeric-string input', () => {
expect(toLnrpcAddressTypeNum('0')).toEqual(0);
expect(toLnrpcAddressTypeNum('1')).toEqual(1);
expect(toLnrpcAddressTypeNum('2')).toEqual(2);
expect(toLnrpcAddressTypeNum('3')).toEqual(3);
expect(toLnrpcAddressTypeNum('4')).toEqual(4);
expect(toLnrpcAddressTypeNum('5')).toEqual(5);
});
it('returns the lnrpc numeric AddressType for lnrpc enum names', () => {
expect(toLnrpcAddressTypeNum('WITNESS_PUBKEY_HASH')).toEqual(0);
expect(toLnrpcAddressTypeNum('NESTED_PUBKEY_HASH')).toEqual(1);
expect(toLnrpcAddressTypeNum('TAPROOT_PUBKEY')).toEqual(4);
});
it('returns the lnrpc numeric AddressType for walletrpc enum names', () => {
expect(toLnrpcAddressTypeNum('NESTED_WITNESS_PUBKEY_HASH')).toEqual(
1
);
expect(
toLnrpcAddressTypeNum('HYBRID_NESTED_WITNESS_PUBKEY_HASH')
).toEqual(1);
});
it('returns undefined for null / undefined input', () => {
expect(toLnrpcAddressTypeNum(undefined)).toBeUndefined();
expect(toLnrpcAddressTypeNum(null)).toBeUndefined();
});
it('returns undefined for unrecognised non-numeric strings', () => {
// protobufjs would silently encode garbage as 0 — better to
// fall through to the backend default.
expect(toLnrpcAddressTypeNum('SOMETHING_NEW')).toBeUndefined();
});
it('accepts numeric input directly', () => {
expect(toLnrpcAddressTypeNum(0)).toEqual(0);
expect(toLnrpcAddressTypeNum(1)).toEqual(1);
expect(toLnrpcAddressTypeNum(4)).toEqual(4);
});
});
});
+55
View File
@@ -0,0 +1,55 @@
// LND's lnrpc.NewAddress endpoint expects the AddressType enum by name.
// Address types reach the backend in three forms:
//
// 1. The numeric strings used by the address-type picker / settings ('0',
// '1', '4') — LND REST's grpc-gateway silently treats these as the
// default (WITNESS_PUBKEY_HASH = native segwit) unless converted.
// 2. The lnrpc enum names (already correct).
// 3. The walletrpc enum names returned by ListAccounts and forwarded by
// OnChainAddresses → Receive: NESTED_WITNESS_PUBKEY_HASH and
// HYBRID_NESTED_WITNESS_PUBKEY_HASH. lnrpc.NewAddress has no hybrid
// variant; the resulting address is still a nested-segwit p2sh-p2wkh,
// which is the visible part the receiver cares about.
const LNRPC_NEW_ADDRESS_TYPE_NAMES: { [key: string]: string } = {
'0': 'WITNESS_PUBKEY_HASH',
'1': 'NESTED_PUBKEY_HASH',
'2': 'UNUSED_WITNESS_PUBKEY_HASH',
'3': 'UNUSED_NESTED_PUBKEY_HASH',
'4': 'TAPROOT_PUBKEY',
'5': 'UNUSED_TAPROOT_PUBKEY',
NESTED_WITNESS_PUBKEY_HASH: 'NESTED_PUBKEY_HASH',
HYBRID_NESTED_WITNESS_PUBKEY_HASH: 'NESTED_PUBKEY_HASH'
};
export const toLnrpcAddressType = (
value: string | number | undefined | null
): string | undefined => {
if (value == null) return undefined;
const key = String(value);
return LNRPC_NEW_ADDRESS_TYPE_NAMES[key] ?? key;
};
// Numeric form of the lnrpc enum, for the embedded LND backend.
// protobufjs's NewAddressRequest encoder writes the `type` field via
// `writer.int32(...)`, which does numeric coercion — enum-name strings
// like 'NESTED_PUBKEY_HASH' become NaN and get serialized as 0
// (= WITNESS_PUBKEY_HASH). Always send a number.
const LNRPC_ADDRESS_TYPE_NUMS: { [key: string]: number } = {
WITNESS_PUBKEY_HASH: 0,
NESTED_PUBKEY_HASH: 1,
UNUSED_WITNESS_PUBKEY_HASH: 2,
UNUSED_NESTED_PUBKEY_HASH: 3,
TAPROOT_PUBKEY: 4,
UNUSED_TAPROOT_PUBKEY: 5
};
export const toLnrpcAddressTypeNum = (
value: string | number | undefined | null
): number | undefined => {
const name = toLnrpcAddressType(value);
if (name == null) return undefined;
if (LNRPC_ADDRESS_TYPE_NUMS[name] !== undefined)
return LNRPC_ADDRESS_TYPE_NUMS[name];
const num = Number(name);
return Number.isInteger(num) ? num : undefined;
};
+42 -37
View File
@@ -254,6 +254,38 @@ export default class Receive extends React.Component<
return defaultInvoiceType === DefaultInvoiceType.Lightning ? 1 : 0;
};
private getReceiveModeFlags = (): {
lnOnly: boolean;
onChainOnly: boolean;
} => {
const { route, SettingsStore } = this.props;
const { posStatus, settings } = SettingsStore;
const lnOnly =
(settings &&
posStatus === 'active' &&
settings.pos?.confirmationPreference === 'lnOnly') ||
!!route.params?.forceLn ||
!BackendUtils.supportsOnchainReceiving();
const onChainOnly = !!route.params?.forceOnChain;
return { lnOnly, onChainOnly };
};
private getSkipOnchain = (): boolean => {
const { settings } = this.props.SettingsStore;
return (
settings?.invoices?.defaultInvoiceType !==
DefaultInvoiceType.Unified
);
};
private getAddressType = (): string => {
const { route, SettingsStore } = this.props;
const { settings } = SettingsStore;
return (
route.params?.addressType || settings?.invoices?.addressType || '0'
);
};
async componentDidMount() {
const {
InvoicesStore,
@@ -263,7 +295,7 @@ export default class Receive extends React.Component<
route
} = this.props;
const { reset } = InvoicesStore;
const { getSettings, posStatus } = SettingsStore;
const { getSettings } = SettingsStore;
const { status, lightningAddressHandle } = LightningAddressStore;
const settings = await getSettings();
@@ -307,7 +339,7 @@ export default class Receive extends React.Component<
false;
this.setState({
addressType: settings?.invoices?.addressType || '0',
addressType: this.getAddressType(),
expirationIndex,
memo: settings?.invoices?.memo || '',
receiverName: settings?.invoices?.receiverName || '',
@@ -322,16 +354,7 @@ export default class Receive extends React.Component<
flowLspNotConfigured
});
const lnOnly =
(settings &&
posStatus &&
posStatus === 'active' &&
settings.pos &&
settings.pos.confirmationPreference &&
settings.pos.confirmationPreference === 'lnOnly') ||
route.params?.forceLn ||
!BackendUtils.supportsOnchainReceiving();
const onChainOnly = route.params?.forceOnChain;
const { lnOnly, onChainOnly } = this.getReceiveModeFlags();
reset();
@@ -360,8 +383,7 @@ export default class Receive extends React.Component<
this.setState({ selectedIndex: this.getDefaultIndex() });
}
const addressType =
route.params?.addressType || settings?.invoices?.addressType || '0';
const addressType = this.getAddressType();
// POS
const memo = route.params?.memo ?? this.state.memo;
@@ -550,15 +572,12 @@ export default class Receive extends React.Component<
addressType?: string,
lspIsActive?: boolean
) => {
const { InvoicesStore, PosStore, SettingsStore } = this.props;
const { InvoicesStore, PosStore } = this.props;
const { receiverName, orderId, orderTip, exchangeRate } = this.state;
// Use passed lspIsActive parameter, fall back to state if not provided
const effectiveLspIsActive = lspIsActive ?? this.state.lspIsActive;
const { createUnifiedInvoice } = InvoicesStore;
const { settings } = SettingsStore;
const skipOnchain =
settings?.invoices?.defaultInvoiceType !==
DefaultInvoiceType.Unified;
const skipOnchain = this.getSkipOnchain();
// POS invoice reuse logic
const checkExistingInvoice = async () => {
@@ -679,14 +698,11 @@ export default class Receive extends React.Component<
};
validateAddress = (text: string) => {
const { navigation, InvoicesStore, SettingsStore, route } = this.props;
const { navigation, InvoicesStore, route } = this.props;
const { lspIsActive, receiverName } = this.state;
const { createUnifiedInvoice } = InvoicesStore;
const { settings } = SettingsStore;
const satAmount = getSatAmount(route.params?.amount);
const skipOnchain =
settings?.invoices?.defaultInvoiceType !==
DefaultInvoiceType.Unified;
const skipOnchain = this.getSkipOnchain();
handleAnything(text, satAmount.toString())
.then((response) => {
@@ -1289,20 +1305,9 @@ export default class Receive extends React.Component<
const showCustomPreimageField =
settings?.invoices?.showCustomPreimageField;
const skipOnchain =
settings?.invoices?.defaultInvoiceType !==
DefaultInvoiceType.Unified;
const skipOnchain = this.getSkipOnchain();
const lnOnly =
(settings &&
posStatus &&
posStatus === 'active' &&
settings.pos &&
settings.pos.confirmationPreference &&
settings.pos.confirmationPreference === 'lnOnly') ||
route.params?.forceLn ||
!BackendUtils.supportsOnchainReceiving();
const onChainOnly = route.params?.forceOnChain;
const { lnOnly, onChainOnly } = this.getReceiveModeFlags();
const lnurl = route.params?.lnurlParams;