Skip to content

Commit

Permalink
Merge pull request #2228 from niteshbalusu11/new-cln-rest-api
Browse files Browse the repository at this point in the history
Core Lightning: CLNRest backend
  • Loading branch information
kaloudis authored Jul 9, 2024
2 parents aba79c9 + 34ce487 commit c1e948a
Show file tree
Hide file tree
Showing 18 changed files with 837 additions and 51 deletions.
384 changes: 384 additions & 0 deletions backends/CLNRest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,384 @@
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,
getChainTransactions,
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 = async () => await getChainTransactions();
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 = {
destination: data.addr,
feerate: `${Number(data.sat_per_vbyte) * 1000}perkb`,
satoshi: data.amount,
utxos: data.utxos
};
} else {
request = {
destination: data.addr,
feerate: `${Number(data.sat_per_vbyte) * 1000}perkb`,
satoshi: 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

0 comments on commit c1e948a

Please sign in to comment.