Files
Evan Kaloudis 89447a25c2 feat: Developer Tools: add updateChannelPolicy with create_missing_edge
Adds an updateChannelPolicy command to the LND Developer Tools,
mirroring lncli updatechanpolicy: base_fee_msat, fee_rate_ppm,
time_lock_delta, and min/max_htlc_msat inputs, a create_missing_edge
toggle, and a global or per-channel target picker.

Backed by a raw PolicyUpdateRequest passthrough on lnd (REST),
embedded-lnd (new lndmobile binding + iOS dispatch table entry), and
lightning-node-connect, kept separate from the production setFees
flow so units and inbound-fee handling there are untouched.
2026-07-13 11:39:14 -04:00

482 lines
17 KiB
TypeScript

import LND from './LND';
import OpenChannelRequest from '../models/OpenChannelRequest';
import Base64Utils from './../utils/Base64Utils';
import lndMobile from '../lndmobile/LndMobileInjection';
import {
checkLndStreamErrorResponse,
LndMobileEventEmitter
} from '../utils/LndMobileUtils';
const {
addInvoice,
getInfo,
connectPeer,
listInvoices,
listUnspent,
getNodeInfo,
decodePayReq,
sendPaymentV2Sync,
sendKeysendPaymentV2,
listPayments,
getNetworkInfo,
getRecoveryInfo,
queryRoutes,
lookupInvoice,
fundingStateStep,
sendCustomMessage,
subscribeCustomMessages,
listPeers,
disconnectPeer
} = lndMobile.index;
const {
channelBalance,
getChanInfo,
listChannels,
pendingChannels,
closedChannels,
closeChannel,
abandonChannel,
openChannel,
openChannelSync,
decodeOpenStatusUpdate,
updateChannelPolicy
} = lndMobile.channel;
const {
signMessageNodePubkey,
verifyMessageNodePubkey,
bumpFee,
bumpForceCloseFee,
fundPsbt,
signPsbt,
finalizePsbt,
publishTransaction,
listAccounts,
listAddresses,
importAccount,
rescan
} = lndMobile.wallet;
const {
walletBalance,
newAddress,
newChangeAddress,
getTransactions,
sendCoins
} = lndMobile.onchain;
const {
WatchtowerClientAddTower,
WatchtowerClientRemoveTower,
WatchtowerClientListTowers,
WatchtowerClientGetTowerInfo,
WatchtowerClientGetStats,
WatchtowerClientGetPolicy,
WatchtowerClientDeactivateTower,
WatchtowerClientTerminateSession
} = lndMobile.wtclient;
import {
signMessageWithAddr as signMsgWithAddr,
verifyMessageWithAddr as verifyMsgWithAddr
} from '../lndmobile/wallet';
import { toLnrpcAddressTypeNum } from '../utils/LndUtils';
export default class EmbeddedLND extends LND {
openChannelListener: any;
getTransactions = async (data?: any) => await getTransactions(data);
getChannels = async () => await listChannels();
getPendingChannels = async () => await pendingChannels();
getClosedChannels = async () => await closedChannels();
getChannelInfo = async (chanId: string) => await getChanInfo(chanId);
getBlockchainBalance = async (data: any) => await walletBalance(data);
getLightningBalance = async () => await channelBalance();
sendCoins = async (data: any) =>
await sendCoins(
data.addr,
data.amount,
data.sat_per_vbyte,
data.spend_unconfirmed,
data.send_all,
data.outpoints
);
sendCustomMessage = async (data: any) =>
await sendCustomMessage(data.peer, data.type, data.data);
subscribeCustomMessages = async () => await subscribeCustomMessages();
getMyNodeInfo = async () => await getInfo();
getNetworkInfo = async () => await getNetworkInfo();
getRecoveryInfo = async () => await getRecoveryInfo();
getInvoices = async () => await listInvoices();
createInvoice = async (data: any) =>
await addInvoice({
amount: data.value ? Number(data.value) : undefined,
amount_msat: data.value_msat ? Number(data.value_msat) : undefined,
memo: data.memo,
expiry: data.expiry_seconds,
is_amp: data.is_amp,
is_blinded: data.is_blinded,
is_private: data.private,
preimage: data.preimage,
route_hints: data.route_hints
});
getPayments = async (params?: {
maxPayments?: number;
reversed?: boolean;
}) => await listPayments(params);
getNewAddress = async (data: any) =>
await newAddress(
toLnrpcAddressTypeNum(data?.type) as any,
data?.account
);
getNewChangeAddress = async (data: any) =>
await newChangeAddress(data.type, data.account);
openChannelSync = async (data: OpenChannelRequest) =>
await openChannelSync(
data.node_pubkey_string,
Number(data.local_funding_amount),
data.privateChannel || false,
data.sat_per_vbyte ? Number(data.sat_per_vbyte) : undefined,
data.scidAlias,
data.min_confs,
data.spend_unconfirmed,
data.simpleTaprootChannel,
data.fundMax,
data.utxos,
data.close_address
);
openChannelStream = async (data: OpenChannelRequest) => {
return await new Promise((resolve, reject) => {
LndMobileEventEmitter.addListener('OpenChannel', (e: any) => {
try {
const error = checkLndStreamErrorResponse('OpenChannel', e);
if (error === 'EOF') {
return;
} else if (error) {
console.error('Got error from OpenChannel', [error]);
reject(error);
return;
}
const result = decodeOpenStatusUpdate(e.data);
if (result?.psbt_fund) {
resolve({ result });
}
} catch (error) {
console.error(error);
}
});
openChannel(
data.node_pubkey_string,
Number(data.local_funding_amount),
data.privateChannel || false,
data.sat_per_vbyte && !data.funding_shim
? Number(data.sat_per_vbyte)
: undefined,
data.scidAlias,
data.min_confs,
data.spend_unconfirmed,
data.simpleTaprootChannel,
data.fundMax,
data.utxos,
data.funding_shim
);
});
};
listPeers = async (): Promise<any[]> => {
try {
const response = await listPeers();
const formattedPeers = (response?.peers || []).map((peer: any) => {
const formattedPeer = {
pub_key: peer.pub_key || '',
address: peer.address || '',
bytes_sent: peer.bytes_sent || '0',
bytes_recv: peer.bytes_recv || '0',
ping_time: peer.ping_time || '0',
sync_type: peer.sync_type || '',
sat_sent: peer.sats_sent || '0',
sat_recv: peer.sats_recv || '0',
inbound: peer.inbound || false,
flap_count: peer.flap_count || '0'
};
return formattedPeer;
});
return formattedPeers;
} catch (error) {
console.error('Error fetching peers:', error);
throw error;
}
};
disconnectPeer = async (pubkey: string): Promise<boolean | null> => {
try {
await disconnectPeer(pubkey);
return true;
} catch (error) {
console.error(`Error while disconnecting peer ${pubkey}:`, error);
return null;
}
};
connectPeer = async (data: any) =>
await connectPeer(data.addr.pubkey, data.addr.host, data.perm);
decodePaymentRequest = async (urlParams?: string[]) =>
await decodePayReq((urlParams && urlParams[0]) || '');
payLightningInvoice = async (data: any) => {
const sendPaymentReq = {
payment_request: data.payment_request,
payment_hash: data.payment_hash,
amt: data?.amt,
max_parts: data?.max_parts,
max_shard_amt: data?.max_shard_amt,
fee_limit_sat: data?.fee_limit_sat || 0,
outgoing_chan_ids: data?.outgoing_chan_ids,
last_hop_pubkey: data?.last_hop_pubkey,
message: data?.message
? Base64Utils.hexToBase64(Base64Utils.utf8ToHex(data?.message))
: undefined,
amp: data?.amp,
timeout_seconds: data?.timeout_seconds || 60,
allow_self_payment: true,
multi_path: data?.multi_path,
max_shard_size_msat: data?.max_shard_size_msat,
dest: data.dest
};
return await sendPaymentV2Sync(sendPaymentReq);
};
sendKeysend = async (data: any) =>
await sendKeysendPaymentV2({
dest: data.pubkey,
amt: data.amt,
dest_custom_records: data.dest_custom_records,
payment_hash: data.payment_hash,
fee_limit_sat: data.fee_limit_sat,
max_shard_size_msat: data.max_shard_size_msat,
max_parts: data.max_parts,
cltv_limit: data.cltv_limit,
amp: data.amp
});
closeChannel = async (urlParams?: Array<string>) => {
const fundingTxId = (urlParams && urlParams[0]) || '';
const outputIndex =
urlParams && urlParams[1] ? Number(urlParams[1]) : 0;
const force = urlParams && urlParams[2] ? true : false;
const sat_per_vbyte =
urlParams && urlParams[3] && !urlParams[2]
? Number(urlParams[3])
: undefined;
const delivery_address =
urlParams && urlParams[4] ? urlParams[4] : undefined;
return await closeChannel(
fundingTxId,
outputIndex,
force,
sat_per_vbyte,
delivery_address
);
};
abandonChannel = async (
urlParams?: Array<string | boolean | undefined>
) => {
const fundingTxId =
urlParams && typeof urlParams[0] === 'string' ? urlParams[0] : '';
const outputIndex =
urlParams && typeof urlParams[1] === 'string'
? Number(urlParams[1])
: 0;
const pendingFundingShimOnly =
urlParams && urlParams[2] ? Boolean(urlParams[2]) : undefined;
const iKnowWhatIAmDoing =
urlParams && urlParams[3] ? Boolean(urlParams[3]) : undefined;
return await abandonChannel(
fundingTxId,
outputIndex,
pendingFundingShimOnly,
iKnowWhatIAmDoing
);
};
getNodeInfo = async (urlParams?: Array<string>) =>
await getNodeInfo((urlParams && urlParams[0]) || '');
signMessage = async (msg: any) => {
return await signMessageNodePubkey(Base64Utils.stringToUint8Array(msg));
};
verifyMessage = async (data: any) => {
const { signature, msg } = data;
return await verifyMessageNodePubkey(
signature,
Base64Utils.stringToUint8Array(msg)
);
};
// getFees = () => N/A;
// setFees = () => N/A;
updateChannelPolicy = async (data: any) => await updateChannelPolicy(data);
signMessageWithAddr = async (message: string, address: string) => {
return await signMsgWithAddr(
Base64Utils.stringToUint8Array(message),
address
);
};
verifyMessageWithAddr = async (
message: string,
signature: string,
address: string
) => {
return await verifyMsgWithAddr(
Base64Utils.stringToUint8Array(message),
signature,
address
);
};
getRoutes = async (urlParams?: Array<any>) =>
urlParams && (await queryRoutes(urlParams[0], urlParams[1]));
// getForwardingHistory = () => N/A
// // Coin Control
fundPsbt = async (data: any) => await fundPsbt(data);
signPsbt = async (data: any) => await signPsbt(data);
finalizePsbt = async (data: any) => await finalizePsbt(data);
publishTransaction = async (data: any) => {
if (data.tx_hex) data.tx_hex = Base64Utils.hexToBase64(data.tx_hex);
return await publishTransaction(data);
};
fundingStateStep = async (data: any) => {
// Verify
if (
data.psbt_finalize?.funded_psbt &&
Base64Utils.isHex(data.psbt_finalize?.funded_psbt)
)
data.psbt_finalize.funded_psbt = Base64Utils.hexToBase64(
data.psbt_finalize.funded_psbt
);
// Finalize
if (
data.psbt_finalize?.final_raw_tx &&
Base64Utils.isHex(data.psbt_finalize?.final_raw_tx)
)
data.psbt_finalize.final_raw_tx = Base64Utils.hexToBase64(
data.psbt_finalize.final_raw_tx
);
if (
data.psbt_finalize?.signed_psbt &&
Base64Utils.isHex(data.psbt_finalize?.signed_psbt)
)
data.psbt_finalize.signed_psbt = Base64Utils.hexToBase64(
data.psbt_finalize.signed_psbt
);
return await fundingStateStep(data);
};
getUTXOs = async (data: any) => await listUnspent(data);
bumpFee = async (data: any) => await bumpFee(data);
bumpForceCloseFee = async (data: any) => await bumpForceCloseFee(data);
lookupInvoice = async (data: any) => await lookupInvoice(data.r_hash);
listAccounts = async () => await listAccounts();
listAddresses = async () => await listAddresses();
importAccount = async (data: any) => await importAccount(data);
rescan = async (data: any) => await rescan(data);
// Watchtower
addWatchtower = async (data: { pubkey: string; address: string }) =>
await WatchtowerClientAddTower(data.pubkey, data.address);
removeWatchtower = async (pubkey: string) =>
await WatchtowerClientRemoveTower(pubkey);
listWatchtowers = async (params?: {
include_sessions?: boolean;
exclude_exhausted_sessions?: boolean;
}) => {
return await WatchtowerClientListTowers(params?.include_sessions);
};
getWatchtowerInfo = async (
pubkey: string,
params?: {
include_sessions?: boolean;
exclude_exhausted_sessions?: boolean;
}
) => await WatchtowerClientGetTowerInfo(pubkey, params?.include_sessions);
getWatchtowerStats = async () => await WatchtowerClientGetStats();
getWatchtowerPolicy = async (policy_type?: number) =>
await WatchtowerClientGetPolicy(policy_type);
deactivateWatchtower = async (pubkey: string) =>
await WatchtowerClientDeactivateTower(pubkey);
terminateWatchtowerSession = async (sessionId: string) =>
await WatchtowerClientTerminateSession(sessionId);
// TODO rewrite subscription logic, starting on Receive view
// subscribeInvoice = (r_hash: string) =>
// this.getRequest(`/v2/invoices/subscribe/${r_hash}`);
// subscribeTransactions = () => this.getRequest('/v1/transactions/subscribe');
// initChannelAcceptor = async (callback: any) =>
// await channelAcceptor(callback);
supportsWatchtowerClient = () => true;
supportsPeers = () => true;
supportsMessageSigning = () => true;
supportsMessageVerification = () => true;
requiresVerifyPubkey = () => false;
supportsAddressMessageSigning = () => true;
supportsLnurlAuth = () => true;
supportsOnchainBalance = () => true;
supportsOnchainSends = () => true;
supportsOnchainReceiving = () => true;
supportsLightningSends = () => true;
supportsKeysend = () => true;
supportsChannelManagement = () => true;
supportsCircularRebalancing = () => true;
supportsForceClose = () => true;
supportsPendingChannels = () => true;
supportsClosedChannels = () => true;
supportsMPP = () => this.supports('v0.10.0');
supportsAMP = () => this.supports('v0.13.0');
supportsCoinControl = () => this.supports('v0.12.0');
supportsChannelCoinControl = () => this.supports('v0.17.0');
supportsHopPicking = () => this.supports('v0.11.0');
supportsAccounts = () => true;
supportsRouting = () => false;
supportsNodeInfo = () => true;
supportsWithdrawalRequests = () => false;
singleFeesEarnedTotal = () => false;
supportsAddressTypeSelection = () => true;
supportsNestedSegWit = () => true;
supportsTaproot = () => this.supports('v0.15.0');
supportsBumpFee = () => true;
supportsFlowLSP = () => true;
supportsNetworkInfo = () => true;
supportsSimpleTaprootChannels = () => this.supports('v0.17.0');
supportsCustomPreimages = () => true;
supportsSweep = () => true;
supportsOnchainSendMax = () => this.supports('v0.18.3');
supportsOnchainBatching = () => true;
supportsChannelBatching = () => true;
supportsChannelFundMax = () => true;
supportsLSPScustomMessage = () => true;
supportsLSPS1rest = () => false;
supportsOffers = () => false;
supportsListingOffers = () => false;
supportsBolt12Address = () => false;
supportsBolt11BlindedRoutes = () => this.supports('v0.18.3');
supportsAddressesWithDerivationPaths = () => this.supports('v0.18.0');
supportsCustomFeeLimit = () => true;
isLNDBased = () => true;
supportsForwardingHistory = () => false;
supportInboundFees = () => this.supports('v0.18.0');
supportsCashuWallet = () => true;
supportsSettingInvoiceExpiration = () => true;
supportsNostrWalletConnectService = () => true;
}