mirror of
https://github.com/apotdevin/thunderhub.git
synced 2024-11-19 09:50:03 +01:00
chore: 🔧 add ln-auth (#148)
This commit is contained in:
parent
78efc34e68
commit
56b690753b
27
package-lock.json
generated
27
package-lock.json
generated
@ -7309,6 +7309,15 @@
|
||||
"@types/redis": "*"
|
||||
}
|
||||
},
|
||||
"@types/secp256k1": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.1.tgz",
|
||||
"integrity": "sha512-+ZjSA8ELlOp8SlKi0YLB2tz9d5iPNEmOBd+8Rz21wTMdaXQIa9b6TEnD6l5qKOCypE7FSyPyck12qZJxSDNoog==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/serve-static": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.4.tgz",
|
||||
@ -9612,6 +9621,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"bip39": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.2.tgz",
|
||||
"integrity": "sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ==",
|
||||
"requires": {
|
||||
"@types/node": "11.11.6",
|
||||
"create-hash": "^1.1.0",
|
||||
"pbkdf2": "^3.0.9",
|
||||
"randombytes": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "11.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz",
|
||||
"integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"bip65": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bip65/-/bip65-1.0.3.tgz",
|
||||
|
@ -41,6 +41,8 @@
|
||||
"balanceofsatoshis": "^5.47.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bech32": "^1.1.4",
|
||||
"bip32": "^2.0.5",
|
||||
"bip39": "^3.0.2",
|
||||
"cookie": "^0.4.1",
|
||||
"crypto-js": "^4.0.0",
|
||||
"date-fns": "^2.16.1",
|
||||
@ -72,6 +74,7 @@
|
||||
"react-table": "^7.5.1",
|
||||
"react-toastify": "^6.0.8",
|
||||
"react-tooltip": "^4.2.10",
|
||||
"secp256k1": "^4.0.2",
|
||||
"styled-components": "^5.2.0",
|
||||
"styled-react-modal": "^2.0.1",
|
||||
"styled-theming": "^2.2.0",
|
||||
@ -112,6 +115,7 @@
|
||||
"@types/qrcode.react": "^1.0.1",
|
||||
"@types/react-copy-to-clipboard": "^4.3.0",
|
||||
"@types/react-table": "^7.0.23",
|
||||
"@types/secp256k1": "^4.0.1",
|
||||
"@types/styled-components": "^5.1.3",
|
||||
"@types/styled-react-modal": "^1.2.0",
|
||||
"@types/styled-theming": "^2.2.5",
|
||||
|
@ -3,14 +3,27 @@ import { to } from 'server/helpers/async';
|
||||
import { logger } from 'server/helpers/logger';
|
||||
import { requestLimiter } from 'server/helpers/rateLimiter';
|
||||
import { ContextType } from 'server/types/apiTypes';
|
||||
import { createInvoice, decodePaymentRequest, pay } from 'ln-service';
|
||||
import {
|
||||
createInvoice,
|
||||
decodePaymentRequest,
|
||||
pay,
|
||||
getWalletInfo,
|
||||
diffieHellmanComputeSecret,
|
||||
} from 'ln-service';
|
||||
import {
|
||||
CreateInvoiceType,
|
||||
DecodedType,
|
||||
DiffieHellmanComputeSecretType,
|
||||
GetWalletInfoType,
|
||||
PayInvoiceType,
|
||||
} from 'server/types/ln-service.types';
|
||||
// import { GetPublicKeyType } from 'server/types/ln-service.types';
|
||||
// import hmacSHA256 from 'crypto-js/hmac-sha256';
|
||||
|
||||
import hmacSHA256 from 'crypto-js/hmac-sha256';
|
||||
import { enc } from 'crypto-js';
|
||||
import * as bip39 from 'bip39';
|
||||
import * as bip32 from 'bip32';
|
||||
import * as secp256k1 from 'secp256k1';
|
||||
import { BIP32Interface } from 'bip32';
|
||||
|
||||
type LnUrlPayResponseType = {
|
||||
pr?: string;
|
||||
@ -57,34 +70,99 @@ type WithdrawRequestType = {
|
||||
type RequestType = PayRequestType | WithdrawRequestType;
|
||||
type RequestWithType = { isTypeOf: string } & RequestType;
|
||||
|
||||
const fromHexString = (hexString: string) =>
|
||||
new Uint8Array(
|
||||
hexString.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []
|
||||
);
|
||||
|
||||
const toHexString = (bytes: Uint8Array) =>
|
||||
bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
|
||||
|
||||
export const lnUrlResolvers = {
|
||||
Mutation: {
|
||||
lnUrl: async (
|
||||
lnUrlAuth: async (
|
||||
_: undefined,
|
||||
{ type, url }: LnUrlParams,
|
||||
{ url }: LnUrlParams,
|
||||
context: ContextType
|
||||
): Promise<string> => {
|
||||
): Promise<{ status: string; message: string }> => {
|
||||
await requestLimiter(context.ip, 'lnUrl');
|
||||
const { lnd } = context;
|
||||
|
||||
// const fullUrl = new URL(url);
|
||||
const domainUrl = new URL(url);
|
||||
const host = domainUrl.host;
|
||||
|
||||
// const { lnd } = context;
|
||||
const k1 = domainUrl.searchParams.get('k1');
|
||||
|
||||
// if (type === 'login') {
|
||||
// logger.debug({ type, url });
|
||||
if (!host || !k1) {
|
||||
logger.error('Missing host or k1 in url: %o', url);
|
||||
throw new Error('WrongUrlFormat');
|
||||
}
|
||||
|
||||
// const info = await to<GetPublicKeyType>(
|
||||
// getPublicKey({ lnd, family: 138, index: 0 })
|
||||
// );
|
||||
const wallet = await to<GetWalletInfoType>(getWalletInfo({ lnd }));
|
||||
|
||||
// const hashed = hmacSHA256(fullUrl.host, info.public_key);
|
||||
// Generate entropy
|
||||
const secret = await to<DiffieHellmanComputeSecretType>(
|
||||
diffieHellmanComputeSecret({
|
||||
lnd,
|
||||
key_family: 138,
|
||||
key_index: 0,
|
||||
partner_public_key: wallet?.public_key,
|
||||
})
|
||||
);
|
||||
|
||||
// return info.public_key;
|
||||
// }
|
||||
// Generate hash from host and entropy
|
||||
const hashed = hmacSHA256(host, secret.secret).toString(enc.Hex);
|
||||
|
||||
logger.debug({ type, url });
|
||||
const indexes =
|
||||
hashed.match(/.{1,4}/g)?.map(index => parseInt(index, 16)) || [];
|
||||
|
||||
return 'confirmed';
|
||||
// Generate private seed from entropy
|
||||
const secretKey = bip39.entropyToMnemonic(hashed);
|
||||
const base58 = bip39.mnemonicToSeedSync(secretKey);
|
||||
|
||||
// Derive private seed from previous private seed and path
|
||||
const node: BIP32Interface = bip32.fromSeed(base58);
|
||||
const derived = node.derivePath(
|
||||
`m/138/${indexes[0]}/${indexes[1]}/${indexes[2]}/${indexes[3]}`
|
||||
);
|
||||
|
||||
// Get private and public key from derived private seed
|
||||
const privateKey = derived.privateKey?.toString('hex');
|
||||
const linkingKey = derived.publicKey.toString('hex');
|
||||
|
||||
if (!privateKey || !linkingKey) {
|
||||
logger.error('Error deriving private or public key: %o', url);
|
||||
throw new Error('ErrorDerivingPrivateKey');
|
||||
}
|
||||
|
||||
// Sign k1 with derived private seed
|
||||
const sigObj = secp256k1.ecdsaSign(
|
||||
fromHexString(k1),
|
||||
fromHexString(privateKey)
|
||||
);
|
||||
|
||||
// Get signature
|
||||
const signature = secp256k1.signatureExport(sigObj.signature);
|
||||
const encodedSignature = toHexString(signature);
|
||||
|
||||
// Build final url with signature and public key
|
||||
const finalUrl = `${url}&sig=${encodedSignature}&key=${linkingKey}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(finalUrl);
|
||||
const json = await response.json();
|
||||
|
||||
logger.debug('LnUrlAuth response: %o', json);
|
||||
|
||||
if (json.status === 'ERROR') {
|
||||
return { ...json, message: json.reason || 'LnServiceError' };
|
||||
}
|
||||
|
||||
return { ...json, message: json.event || 'LnServiceSuccess' };
|
||||
} catch (error) {
|
||||
logger.error('Error authenticating with LnUrl service: %o', error);
|
||||
throw new Error('ProblemAuthenticatingWithLnUrlService');
|
||||
}
|
||||
},
|
||||
fetchLnUrl: async (
|
||||
_: undefined,
|
||||
|
@ -21,6 +21,11 @@ export const lnUrlTypes = gql`
|
||||
|
||||
union LnUrlRequest = WithdrawRequest | PayRequest
|
||||
|
||||
type AuthResponse {
|
||||
status: String!
|
||||
message: String!
|
||||
}
|
||||
|
||||
type PaySuccess {
|
||||
tag: String
|
||||
description: String
|
||||
|
@ -91,6 +91,7 @@ export const queryTypes = gql`
|
||||
|
||||
export const mutationTypes = gql`
|
||||
type Mutation {
|
||||
lnUrlAuth(url: String!): AuthResponse!
|
||||
lnUrlPay(callback: String!, amount: Int!, comment: String): PaySuccess!
|
||||
lnUrlWithdraw(
|
||||
callback: String!
|
||||
@ -99,7 +100,6 @@ export const mutationTypes = gql`
|
||||
description: String
|
||||
): String!
|
||||
fetchLnUrl(url: String!): LnUrlRequest
|
||||
lnUrl(type: String!, url: String!): String!
|
||||
createBaseInvoice(amount: Int!): baseInvoiceType
|
||||
createThunderPoints(
|
||||
id: String!
|
||||
|
@ -103,6 +103,10 @@ export type GetWalletInfoType = {
|
||||
public_key: string;
|
||||
};
|
||||
|
||||
export type DiffieHellmanComputeSecretType = {
|
||||
secret: string;
|
||||
};
|
||||
|
||||
export type GetNodeType = { alias: string; color: string };
|
||||
|
||||
export type UtxoType = {};
|
||||
|
@ -19,6 +19,19 @@ export type FetchLnUrlMutation = (
|
||||
)> }
|
||||
);
|
||||
|
||||
export type AuthLnUrlMutationVariables = Types.Exact<{
|
||||
url: Types.Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type AuthLnUrlMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { lnUrlAuth: (
|
||||
{ __typename?: 'AuthResponse' }
|
||||
& Pick<Types.AuthResponse, 'status' | 'message'>
|
||||
) }
|
||||
);
|
||||
|
||||
export type PayLnUrlMutationVariables = Types.Exact<{
|
||||
callback: Types.Scalars['String'];
|
||||
amount: Types.Scalars['Int'];
|
||||
@ -95,6 +108,39 @@ export function useFetchLnUrlMutation(baseOptions?: Apollo.MutationHookOptions<F
|
||||
export type FetchLnUrlMutationHookResult = ReturnType<typeof useFetchLnUrlMutation>;
|
||||
export type FetchLnUrlMutationResult = Apollo.MutationResult<FetchLnUrlMutation>;
|
||||
export type FetchLnUrlMutationOptions = Apollo.BaseMutationOptions<FetchLnUrlMutation, FetchLnUrlMutationVariables>;
|
||||
export const AuthLnUrlDocument = gql`
|
||||
mutation AuthLnUrl($url: String!) {
|
||||
lnUrlAuth(url: $url) {
|
||||
status
|
||||
message
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type AuthLnUrlMutationFn = Apollo.MutationFunction<AuthLnUrlMutation, AuthLnUrlMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useAuthLnUrlMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useAuthLnUrlMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useAuthLnUrlMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [authLnUrlMutation, { data, loading, error }] = useAuthLnUrlMutation({
|
||||
* variables: {
|
||||
* url: // value for 'url'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useAuthLnUrlMutation(baseOptions?: Apollo.MutationHookOptions<AuthLnUrlMutation, AuthLnUrlMutationVariables>) {
|
||||
return Apollo.useMutation<AuthLnUrlMutation, AuthLnUrlMutationVariables>(AuthLnUrlDocument, baseOptions);
|
||||
}
|
||||
export type AuthLnUrlMutationHookResult = ReturnType<typeof useAuthLnUrlMutation>;
|
||||
export type AuthLnUrlMutationResult = Apollo.MutationResult<AuthLnUrlMutation>;
|
||||
export type AuthLnUrlMutationOptions = Apollo.BaseMutationOptions<AuthLnUrlMutation, AuthLnUrlMutationVariables>;
|
||||
export const PayLnUrlDocument = gql`
|
||||
mutation PayLnUrl($callback: String!, $amount: Int!, $comment: String) {
|
||||
lnUrlPay(callback: $callback, amount: $amount, comment: $comment) {
|
||||
|
@ -23,6 +23,15 @@ export const FETCH_LN_URL = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const AUTH_LN_URL = gql`
|
||||
mutation AuthLnUrl($url: String!) {
|
||||
lnUrlAuth(url: $url) {
|
||||
status
|
||||
message
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const PAY_LN_URL = gql`
|
||||
mutation PayLnUrl($callback: String!, $amount: Int!, $comment: String) {
|
||||
lnUrlPay(callback: $callback, amount: $amount, comment: $comment) {
|
||||
|
@ -208,10 +208,10 @@ export type QueryGetSessionTokenArgs = {
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
lnUrlAuth: AuthResponse;
|
||||
lnUrlPay: PaySuccess;
|
||||
lnUrlWithdraw: Scalars['String'];
|
||||
fetchLnUrl?: Maybe<LnUrlRequest>;
|
||||
lnUrl: Scalars['String'];
|
||||
createBaseInvoice?: Maybe<BaseInvoiceType>;
|
||||
createThunderPoints: Scalars['Boolean'];
|
||||
closeChannel?: Maybe<CloseChannelType>;
|
||||
@ -234,6 +234,11 @@ export type Mutation = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationLnUrlAuthArgs = {
|
||||
url: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationLnUrlPayArgs = {
|
||||
callback: Scalars['String'];
|
||||
amount: Scalars['Int'];
|
||||
@ -254,12 +259,6 @@ export type MutationFetchLnUrlArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationLnUrlArgs = {
|
||||
type: Scalars['String'];
|
||||
url: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateBaseInvoiceArgs = {
|
||||
amount: Scalars['Int'];
|
||||
};
|
||||
@ -1041,6 +1040,12 @@ export type PayRequest = {
|
||||
|
||||
export type LnUrlRequest = WithdrawRequest | PayRequest;
|
||||
|
||||
export type AuthResponse = {
|
||||
__typename?: 'AuthResponse';
|
||||
status: Scalars['String'];
|
||||
message: Scalars['String'];
|
||||
};
|
||||
|
||||
export type PaySuccess = {
|
||||
__typename?: 'PaySuccess';
|
||||
tag?: Maybe<Scalars['String']>;
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { ColorButton } from 'src/components/buttons/colorButton/ColorButton';
|
||||
import { Card } from 'src/components/generic/Styled';
|
||||
import { InputWithDeco } from 'src/components/input/InputWithDeco';
|
||||
import Modal from 'src/components/modal/ReactModal';
|
||||
import { useAuthLnUrlMutation } from 'src/graphql/mutations/__generated__/lnUrl.generated';
|
||||
import { getErrorContent } from 'src/utils/error';
|
||||
import { decodeLnUrl } from 'src/utils/url';
|
||||
import { LnUrlModal } from './lnUrlModal';
|
||||
|
||||
@ -13,6 +15,24 @@ export const LnUrlCard = () => {
|
||||
const [type, setType] = useState<string>('');
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
|
||||
const [auth, { data, loading }] = useAuthLnUrlMutation({
|
||||
onError: error => toast.error(getErrorContent(error)),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (loading || !data?.lnUrlAuth) return;
|
||||
|
||||
const { status, message } = data.lnUrlAuth;
|
||||
if (status === 'ERROR') {
|
||||
toast.error(message);
|
||||
} else {
|
||||
toast.success(message);
|
||||
setLnUrl('');
|
||||
setUrl('');
|
||||
setType('');
|
||||
}
|
||||
}, [data, loading]);
|
||||
|
||||
const handleDecode = () => {
|
||||
if (!lnurl) {
|
||||
toast.warning('Please input a LNURL');
|
||||
@ -31,7 +51,7 @@ export const LnUrlCard = () => {
|
||||
setModalOpen(true);
|
||||
}
|
||||
if (tag === 'login') {
|
||||
toast.warning('LnAuth is not available yet');
|
||||
auth({ variables: { url: urlString } });
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Problem decoding LNURL');
|
||||
@ -43,7 +63,7 @@ export const LnUrlCard = () => {
|
||||
<Card>
|
||||
<InputWithDeco
|
||||
value={lnurl}
|
||||
placeholder={'LnPay or LnWithdraw URL'}
|
||||
placeholder={'LnPay / LnWithdraw / LnAuth'}
|
||||
title={'LNURL'}
|
||||
inputCallback={value => setLnUrl(value)}
|
||||
onEnter={() => handleDecode()}
|
||||
@ -51,7 +71,7 @@ export const LnUrlCard = () => {
|
||||
<ColorButton
|
||||
arrow={true}
|
||||
fullWidth={true}
|
||||
disabled={!lnurl}
|
||||
disabled={!lnurl || loading}
|
||||
withMargin={'16px 0 0'}
|
||||
onClick={() => handleDecode()}
|
||||
>
|
||||
|
Loading…
Reference in New Issue
Block a user