-
-
Notifications
You must be signed in to change notification settings - Fork 146
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2228 from niteshbalusu11/new-cln-rest-api
Core Lightning: CLNRest backend
- Loading branch information
Showing
18 changed files
with
837 additions
and
51 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.