Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core Lightning: CLNRest backend #2228

Merged
merged 17 commits into from
Jul 9, 2024
386 changes: 386 additions & 0 deletions backends/CLNRest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,386 @@
import stores from '../stores/Stores';
import TransactionRequest from '../models/TransactionRequest';
import OpenChannelRequest from '../models/OpenChannelRequest';
import VersionUtils from '../utils/VersionUtils';
import Base64Utils from '../utils/Base64Utils';
import { Hash as sha256Hash } from 'fast-sha256';
import BigNumber from 'bignumber.js';
import {
getBalance,
getOffchainBalance,
listPeers
} from './CoreLightningRequestHandler';
import { localeString } from '../utils/LocaleUtils';
import ReactNativeBlobUtil from 'react-native-blob-util';
import { doTorRequest, RequestMethod } from '../utils/TorUtils';

const calls = new Map<string, Promise<any>>();

export default class CLNRest {
getHeaders = (rune: string): any => {
return {
Rune: rune
};
};

supports = (
minVersion: string,
eosVersion?: string,
minApiVersion?: string
) => {
const { nodeInfo } = stores.nodeInfoStore;
const { version, api_version } = nodeInfo;
const { isSupportedVersion } = VersionUtils;
if (minApiVersion) {
return (
isSupportedVersion(version, minVersion, eosVersion) &&
isSupportedVersion(api_version, minApiVersion)
);
}
return isSupportedVersion(version, minVersion, eosVersion);
};

clearCachedCalls = () => calls.clear();

restReq = async (
headers: Headers | any,
url: string,
method: any,
data?: any,
certVerification?: boolean,
useTor?: boolean
) => {
// use body data as an identifier too, we don't want to cancel when we
// are making multiples calls to get all the node names, for example
const id = data ? `${url}${JSON.stringify(data)}` : url;
if (calls.has(id)) {
return calls.get(id);
}
// API is a bit of a mess but
// If tor enabled in setting, start up the daemon here
if (useTor === true) {
calls.set(
id,
doTorRequest(
url,
method as RequestMethod,
JSON.stringify(data),
headers
).then((response: any) => {
calls.delete(id);
return response;
})
);
} else {
calls.set(
id,
ReactNativeBlobUtil.config({
trusty: !certVerification
})
.fetch(
method,
url,
headers,
data ? JSON.stringify(data) : data
)
.then((response: any) => {
calls.delete(id);
if (response.info().status < 300) {
// handle ws responses
if (response.data.includes('\n')) {
const split = response.data.split('\n');
const length = split.length;
// last instance is empty
return JSON.parse(split[length - 2]);
}
return response.json();
} else {
try {
const errorInfo = response.json();
throw new Error(
(errorInfo.error &&
errorInfo.error.message) ||
errorInfo.message ||
errorInfo.error
);
} catch (e) {
if (
response.data &&
typeof response.data === 'string'
) {
throw new Error(response.data);
} else {
throw new Error(
localeString(
'backends.LND.restReq.connectionError'
)
);
}
}
}
})
);
}

return await calls.get(id);
};

request = (route: string, method: string, data?: any, params?: any) => {
const { host, port, rune, certVerification, enableTor } =
stores.settingsStore;

if (params) {
route = `${route}?${Object.keys(params)
.map((key: string) => key + '=' + params[key])
.join('&')}`;
}

const headers: any = this.getHeaders(rune);
headers['Content-Type'] = 'application/json';

const url = this.getURL(host, port, route);

return this.restReq(
headers,
url,
method,
data,
certVerification,
enableTor
);
};

getURL = (
host: string,
port: string | number,
route: string,
ws?: boolean
) => {
const hostPath = host.includes('://') ? host : `https://${host}`;
let baseUrl = `${hostPath}${port ? ':' + port : ''}`;

if (ws) {
baseUrl = baseUrl.replace('https', 'wss').replace('http', 'ws');
}

if (baseUrl[baseUrl.length - 1] === '/') {
baseUrl = baseUrl.slice(0, -1);
}

return `${baseUrl}${route}`;
};

postRequest = (route: string, data?: any) =>
this.request(route, 'post', data);

getNode = (data: any) =>
this.postRequest('/v1/listnodes', { id: data.id }).then((res) => {
return res;
});
getTransactions = () =>
this.postRequest('/v1/listfunds').then((res) => ({
transactions: res.outputs
}));
getChannels = async () => {
const channels = await this.postRequest('/v1/listpeerchannels');
return await listPeers(channels);
};
getBlockchainBalance = () =>
this.postRequest('/v1/listfunds').then((res) => {
return getBalance(res);
});
getLightningBalance = () =>
this.postRequest('/v1/listfunds').then((res) => {
return getOffchainBalance(res);
});
sendCoins = (data: TransactionRequest) => {
let request: any;
if (data.utxos) {
request = {
address: data.addr,
niteshbalusu11 marked this conversation as resolved.
Show resolved Hide resolved
feeRate: `${Number(data.sat_per_vbyte) * 1000}perkb`,
niteshbalusu11 marked this conversation as resolved.
Show resolved Hide resolved
satoshis: data.amount,
utxos: data.utxos
};
} else {
request = {
address: data.addr,
feeRate: `${Number(data.sat_per_vbyte) * 1000}perkb`,
satoshis: data.amount
};
}
return this.postRequest('/v1/withdraw', request);
};
getMyNodeInfo = () => this.postRequest('/v1/getinfo');
getInvoices = () => this.postRequest('/v1/listinvoices');
createInvoice = (data: any) =>
this.postRequest('/v1/invoice', {
description: data.memo,
label: 'zeus.' + Math.random() * 1000000,
amount_msat: Number(data.value) * 1000,
expiry: Number(data.expiry),
exposeprivatechannels: true
});

getPayments = () =>
this.postRequest('/v1/listpays').then((data: any) => ({
payments: data.pays
}));
getNewAddress = () => this.postRequest('/v1/newaddr');
openChannelSync = (data: OpenChannelRequest) => {
let request: any;
const feeRate = `${new BigNumber(data.sat_per_vbyte)
.times(1000)
.toString()}perkb`;
if (data.utxos && data.utxos.length > 0) {
request = {
id: data.id,
amount: data.satoshis,
feerate: feeRate,
announce: !data.privateChannel ? true : false,
minconf: data.min_confs,
utxos: data.utxos
};
} else {
request = {
id: data.id,
amount: data.satoshis,
feerate: feeRate,
announce: !data.privateChannel ? true : false,
minconf: data.min_confs
};
}

return this.postRequest('/v1/fundchannel', request);
};
connectPeer = (data: any) => {
const [host, port] = data.addr.host.split(':');

return this.postRequest('/v1/connect', {
id: data.addr.pubkey,
host,
port
});
};
decodePaymentRequest = (urlParams?: Array<string>) =>
this.postRequest('/v1/decode', {
string: urlParams && urlParams[0]
});

payLightningInvoice = (data: any) =>
this.postRequest('/v1/pay', {
bolt11: data.payment_request,
amount_msat: Number(data.amt && data.amt * 1000),
maxfeepercent: data.max_fee_percent
});
sendKeysend = (data: any) => {
return this.postRequest('/v1/keysend', {
destination: data.pubkey,
amount_msat: Number(data.amt && data.amt * 1000),
maxfeepercent: data.max_fee_percent
});
};
closeChannel = (urlParams?: Array<string>) => {
const request = {
id: urlParams && urlParams[0],
unilateraltimeout: urlParams && urlParams[1] ? 2 : 0
};
return this.postRequest('/v1/close', request);
};
getFees = () =>
this.postRequest('/v1/getinfo').then((res: any) => ({
total_fee_sum: res.fees_collected_msat / 1000
}));
setFees = (data: any) =>
this.postRequest('/v1/setchannel', {
id: data.global ? 'all' : data.channelId,
feebase: data.base_fee_msat,
feeppm: data.fee_rate
});
getUTXOs = () => this.postRequest('/v1/listfunds');
signMessage = (message: string) =>
this.postRequest('/v1/signmessage', {
message
});
verifyMessage = (data: any) =>
this.postRequest('/v1/checkmessage', {
message: data.msg,
zbase: data.signature
});
lnurlAuth = async (r_hash: string) => {
const signed = await this.signMessage(r_hash);
return {
signature: new sha256Hash()
.update(Base64Utils.stringToUint8Array(signed.signature))
.digest()
};
};

// BOLT 12 / Offers
listOffers = () =>
this.postRequest('/v1/listoffers', { active_only: true });
createOffer = ({
description,
label,
singleUse
}: {
description?: string;
label?: string;
singleUse?: boolean;
}) =>
this.postRequest('/v1/offer', {
amount: 'any',
description,
label,
single_use: singleUse || false
});
disableOffer = ({ offer_id }: { offer_id: string }) =>
this.postRequest('/v1/disableoffer', { offer_id });
fetchInvoiceFromOffer = async (bolt12: string, amountSatoshis: string) => {
return await this.postRequest('/v1/fetchinvoice', {
offer: bolt12,
amount_msat: Number(amountSatoshis) * 1000,
timeout: 60
});
};

supportsMessageSigning = () => true;
supportsLnurlAuth = () => true;
supportsOnchainSends = () => true;
supportsOnchainReceiving = () => true;
supportsLightningSends = () => true;
supportsKeysend = () => true;
supportsChannelManagement = () => true;
supportsPendingChannels = () => false;
supportsMPP = () => false;
supportsAMP = () => false;
supportsCoinControl = () => true;
supportsChannelCoinControl = () => true;
supportsHopPicking = () => false;
supportsAccounts = () => false;
supportsRouting = () => true;
supportsNodeInfo = () => true;
singleFeesEarnedTotal = () => true;
supportsAddressTypeSelection = () => false;
supportsTaproot = () => false;
supportsBumpFee = () => false;
supportsLSPs = () => false;
supportsNetworkInfo = () => false;
supportsSimpleTaprootChannels = () => false;
supportsCustomPreimages = () => false;
supportsSweep = () => true;
supportsOnchainBatching = () => false;
supportsChannelBatching = () => false;
supportsLSPS1customMessage = () => false;
supportsLSPS1rest = () => true;
supportsOffers = async () => {
const { configs } = await this.postRequest('/v1/listconfigs');

const supportsOffers: boolean = configs['experimental-offers']
? true
: false;

return supportsOffers;
};
isLNDBased = () => false;
}
Loading