mirror of
https://github.com/apotdevin/thunderhub.git
synced 2024-11-19 09:50:03 +01:00
commit
05b41dbbbf
@ -38,6 +38,7 @@ module.exports = {
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/ban-types': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
|
@ -65,6 +65,7 @@ This repository consists of a **NextJS** server that handles both the backend **
|
||||
|
||||
### Management
|
||||
|
||||
- LNURL integration: ln-pay and ln-withdraw are available. Ln-auth soon.
|
||||
- Send and Receive Lightning payments.
|
||||
- Send and Receive Bitcoin payments.
|
||||
- Decode lightning payment requests.
|
||||
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -6906,6 +6906,12 @@
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"@types/crypto-js": {
|
||||
"version": "3.1.47",
|
||||
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-3.1.47.tgz",
|
||||
"integrity": "sha512-eI6gvpcGHLk3dAuHYnRCAjX+41gMv1nz/VP55wAe5HtmAKDOoPSfr3f6vkMc08ov1S0NsjvUBxDtHHxqQY1LGA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/express": {
|
||||
"version": "4.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz",
|
||||
|
@ -40,7 +40,9 @@
|
||||
"apollo-server-micro": "^2.17.0",
|
||||
"balanceofsatoshis": "^5.47.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bech32": "^1.1.4",
|
||||
"cookie": "^0.4.1",
|
||||
"crypto-js": "^4.0.0",
|
||||
"date-fns": "^2.16.1",
|
||||
"graphql": "^15.3.0",
|
||||
"graphql-iso-date": "^3.6.1",
|
||||
@ -96,6 +98,7 @@
|
||||
"@testing-library/react": "^11.0.4",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cookie": "^0.4.0",
|
||||
"@types/crypto-js": "^3.1.47",
|
||||
"@types/graphql-iso-date": "^3.4.0",
|
||||
"@types/js-cookie": "^2.2.6",
|
||||
"@types/js-yaml": "^3.12.5",
|
||||
|
@ -38,6 +38,8 @@ import { bosResolvers } from './bos/resolvers';
|
||||
import { bosTypes } from './bos/types';
|
||||
import { tbaseResolvers } from './tbase/resolvers';
|
||||
import { tbaseTypes } from './tbase/types';
|
||||
import { lnUrlResolvers } from './lnurl/resolvers';
|
||||
import { lnUrlTypes } from './lnurl/types';
|
||||
|
||||
const typeDefs = [
|
||||
generalTypes,
|
||||
@ -59,6 +61,7 @@ const typeDefs = [
|
||||
routeTypes,
|
||||
bosTypes,
|
||||
tbaseTypes,
|
||||
lnUrlTypes,
|
||||
];
|
||||
|
||||
const resolvers = merge(
|
||||
@ -82,7 +85,8 @@ const resolvers = merge(
|
||||
macaroonResolvers,
|
||||
networkResolvers,
|
||||
bosResolvers,
|
||||
tbaseResolvers
|
||||
tbaseResolvers,
|
||||
lnUrlResolvers
|
||||
);
|
||||
|
||||
export const schema = makeExecutableSchema({ typeDefs, resolvers });
|
||||
|
@ -13,7 +13,7 @@ import { logger } from 'server/helpers/logger';
|
||||
import { requestLimiter } from 'server/helpers/rateLimiter';
|
||||
import { getErrorMsg } from 'server/helpers/helpers';
|
||||
import { to } from 'server/helpers/async';
|
||||
import { DecodedType } from 'server/types/ln-service.types';
|
||||
import { CreateInvoiceType, DecodedType } from 'server/types/ln-service.types';
|
||||
|
||||
const KEYSEND_TYPE = '5482373484';
|
||||
|
||||
@ -90,7 +90,7 @@ export const invoiceResolvers = {
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
return await to(
|
||||
return await to<CreateInvoiceType>(
|
||||
createInvoiceRequest({
|
||||
lnd,
|
||||
...(description && { description }),
|
||||
|
243
server/schema/lnurl/resolvers.ts
Normal file
243
server/schema/lnurl/resolvers.ts
Normal file
@ -0,0 +1,243 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
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 {
|
||||
CreateInvoiceType,
|
||||
DecodedType,
|
||||
PayInvoiceType,
|
||||
} from 'server/types/ln-service.types';
|
||||
// import { GetPublicKeyType } from 'server/types/ln-service.types';
|
||||
// import hmacSHA256 from 'crypto-js/hmac-sha256';
|
||||
|
||||
type LnUrlPayResponseType = {
|
||||
pr?: string;
|
||||
successAction?: { tag: string };
|
||||
status?: string;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
type LnUrlParams = {
|
||||
type: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type FetchLnUrlParams = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
type LnUrlPayType = { callback: string; amount: number; comment: string };
|
||||
type LnUrlWithdrawType = {
|
||||
callback: string;
|
||||
k1: string;
|
||||
amount: number;
|
||||
description: string;
|
||||
};
|
||||
|
||||
type PayRequestType = {
|
||||
callback: string;
|
||||
maxSendable: string;
|
||||
minSendable: string;
|
||||
metadata: string;
|
||||
commentAllowed: number;
|
||||
tag: string;
|
||||
};
|
||||
|
||||
type WithdrawRequestType = {
|
||||
callback: string;
|
||||
k1: string;
|
||||
maxWithdrawable: string;
|
||||
defaultDescription: string;
|
||||
minWithdrawable: string;
|
||||
tag: string;
|
||||
};
|
||||
|
||||
type RequestType = PayRequestType | WithdrawRequestType;
|
||||
type RequestWithType = { isTypeOf: string } & RequestType;
|
||||
|
||||
export const lnUrlResolvers = {
|
||||
Mutation: {
|
||||
lnUrl: async (
|
||||
_: undefined,
|
||||
{ type, url }: LnUrlParams,
|
||||
context: ContextType
|
||||
): Promise<string> => {
|
||||
await requestLimiter(context.ip, 'lnUrl');
|
||||
|
||||
// const fullUrl = new URL(url);
|
||||
|
||||
// const { lnd } = context;
|
||||
|
||||
// if (type === 'login') {
|
||||
// logger.debug({ type, url });
|
||||
|
||||
// const info = await to<GetPublicKeyType>(
|
||||
// getPublicKey({ lnd, family: 138, index: 0 })
|
||||
// );
|
||||
|
||||
// const hashed = hmacSHA256(fullUrl.host, info.public_key);
|
||||
|
||||
// return info.public_key;
|
||||
// }
|
||||
|
||||
logger.debug({ type, url });
|
||||
|
||||
return 'confirmed';
|
||||
},
|
||||
fetchLnUrl: async (
|
||||
_: undefined,
|
||||
{ url }: FetchLnUrlParams,
|
||||
context: ContextType
|
||||
) => {
|
||||
await requestLimiter(context.ip, 'fetchLnUrl');
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const json = await response.json();
|
||||
|
||||
if (json.status === 'ERROR') {
|
||||
throw new Error(json.reason || 'LnServiceError');
|
||||
}
|
||||
|
||||
return json;
|
||||
} catch (error) {
|
||||
logger.error('Error fetching from LnUrl service: %o', error);
|
||||
throw new Error('ProblemFetchingFromLnUrlService');
|
||||
}
|
||||
},
|
||||
lnUrlPay: async (
|
||||
_: undefined,
|
||||
{ callback, amount, comment }: LnUrlPayType,
|
||||
context: ContextType
|
||||
) => {
|
||||
await requestLimiter(context.ip, 'lnUrlPay');
|
||||
const { lnd } = context;
|
||||
|
||||
logger.debug('LnUrlPay initiated with params %o', {
|
||||
callback,
|
||||
amount,
|
||||
comment,
|
||||
});
|
||||
|
||||
const random8byteNonce = randomBytes(8).toString('hex');
|
||||
|
||||
const finalUrl = `${callback}?amount=${
|
||||
amount * 1000
|
||||
}&nonce=${random8byteNonce}&comment=${comment}`;
|
||||
|
||||
let lnServiceResponse: LnUrlPayResponseType = {
|
||||
status: 'ERROR',
|
||||
reason: 'FailedToFetchLnService',
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(finalUrl);
|
||||
lnServiceResponse = await response.json();
|
||||
|
||||
if (lnServiceResponse.status === 'ERROR') {
|
||||
throw new Error(lnServiceResponse.reason || 'LnServiceError');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error paying to LnUrl service: %o', error);
|
||||
throw new Error('ProblemPayingLnUrlService');
|
||||
}
|
||||
|
||||
logger.debug('LnUrlPay response: %o', lnServiceResponse);
|
||||
|
||||
if (!lnServiceResponse.pr) {
|
||||
logger.error('No invoice in response from LnUrlService');
|
||||
throw new Error('ProblemPayingLnUrlService');
|
||||
}
|
||||
|
||||
if (lnServiceResponse.successAction) {
|
||||
const { tag } = lnServiceResponse.successAction;
|
||||
if (tag !== 'url' && tag !== 'message' && tag !== 'aes') {
|
||||
logger.error('LnUrlService provided an invalid tag: %o', tag);
|
||||
throw new Error('InvalidTagFromLnUrlService');
|
||||
}
|
||||
}
|
||||
|
||||
const decoded = await to<DecodedType>(
|
||||
decodePaymentRequest({
|
||||
lnd,
|
||||
request: lnServiceResponse.pr,
|
||||
})
|
||||
);
|
||||
|
||||
if (decoded.tokens > amount) {
|
||||
logger.error(
|
||||
`Invoice amount ${decoded.tokens} is higher than amount defined ${amount}`
|
||||
);
|
||||
throw new Error('LnServiceInvoiceAmountToHigh');
|
||||
}
|
||||
|
||||
const info = await to<PayInvoiceType>(
|
||||
pay({ lnd, request: lnServiceResponse.pr })
|
||||
);
|
||||
|
||||
if (!info.is_confirmed) {
|
||||
logger.error(`Failed to pay invoice: ${lnServiceResponse.pr}`);
|
||||
throw new Error('FailedToPayInvoiceToLnUrlService');
|
||||
}
|
||||
|
||||
return (
|
||||
lnServiceResponse.successAction || {
|
||||
tag: 'message',
|
||||
message: 'Succesfully Paid',
|
||||
}
|
||||
);
|
||||
},
|
||||
lnUrlWithdraw: async (
|
||||
_: undefined,
|
||||
{ callback, k1, amount, description }: LnUrlWithdrawType,
|
||||
context: ContextType
|
||||
) => {
|
||||
await requestLimiter(context.ip, 'lnUrlWithdraw');
|
||||
const { lnd } = context;
|
||||
|
||||
logger.debug('LnUrlWithdraw initiated with params: %o', {
|
||||
callback,
|
||||
amount,
|
||||
k1,
|
||||
description,
|
||||
});
|
||||
|
||||
// Create invoice to be paid by LnUrlService
|
||||
const info = await to<CreateInvoiceType>(
|
||||
createInvoice({ lnd, tokens: amount, description })
|
||||
);
|
||||
|
||||
const finalUrl = `${callback}?k1=${k1}&pr=${info.request}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(finalUrl);
|
||||
const json = await response.json();
|
||||
|
||||
logger.debug('LnUrlWithdraw response: %o', json);
|
||||
|
||||
if (json.status === 'ERROR') {
|
||||
throw new Error(json.reason || 'LnServiceError');
|
||||
}
|
||||
|
||||
// Return invoice id to check status
|
||||
return info.id;
|
||||
} catch (error) {
|
||||
logger.error('Error withdrawing from LnUrl service: %o', error);
|
||||
throw new Error('ProblemWithdrawingFromLnUrlService');
|
||||
}
|
||||
},
|
||||
},
|
||||
LnUrlRequest: {
|
||||
__resolveType(parent: RequestWithType) {
|
||||
if (parent.tag === 'payRequest') {
|
||||
return 'PayRequest';
|
||||
}
|
||||
if (parent.tag === 'withdrawRequest') {
|
||||
return 'WithdrawRequest';
|
||||
}
|
||||
return 'Unknown';
|
||||
},
|
||||
},
|
||||
};
|
32
server/schema/lnurl/types.ts
Normal file
32
server/schema/lnurl/types.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const lnUrlTypes = gql`
|
||||
type WithdrawRequest {
|
||||
callback: String
|
||||
k1: String
|
||||
maxWithdrawable: String
|
||||
defaultDescription: String
|
||||
minWithdrawable: String
|
||||
tag: String
|
||||
}
|
||||
|
||||
type PayRequest {
|
||||
callback: String
|
||||
maxSendable: String
|
||||
minSendable: String
|
||||
metadata: String
|
||||
commentAllowed: Int
|
||||
tag: String
|
||||
}
|
||||
|
||||
union LnUrlRequest = WithdrawRequest | PayRequest
|
||||
|
||||
type PaySuccess {
|
||||
tag: String
|
||||
description: String
|
||||
url: String
|
||||
message: String
|
||||
ciphertext: String
|
||||
iv: String
|
||||
}
|
||||
`;
|
@ -91,6 +91,15 @@ export const queryTypes = gql`
|
||||
|
||||
export const mutationTypes = gql`
|
||||
type Mutation {
|
||||
lnUrlPay(callback: String!, amount: Int!, comment: String): PaySuccess!
|
||||
lnUrlWithdraw(
|
||||
callback: String!
|
||||
amount: Int!
|
||||
k1: String!
|
||||
description: String
|
||||
): String!
|
||||
fetchLnUrl(url: String!): LnUrlRequest
|
||||
lnUrl(type: String!, url: String!): String!
|
||||
createBaseInvoice(amount: Int!): baseInvoiceType
|
||||
createThunderPoints(
|
||||
id: String!
|
||||
|
@ -99,3 +99,7 @@ export const verifyBackups = jest
|
||||
export const verifyMessage = jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve(res.verifyMessageResponse));
|
||||
|
||||
export const getPublicKey = jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve(res.getPublicKeyResponse));
|
||||
|
@ -759,3 +759,7 @@ export const verifyBackupsResponse = {
|
||||
export const verifyMessageResponse = {
|
||||
signed_by: 'abc',
|
||||
};
|
||||
|
||||
export const getPublicKeyResponse = {
|
||||
public_key: 'public_key',
|
||||
};
|
||||
|
@ -1,5 +1,27 @@
|
||||
export type LndObject = {};
|
||||
|
||||
export type PayInvoiceType = {
|
||||
fee: number;
|
||||
fee_mtokens: string;
|
||||
hops: [
|
||||
{
|
||||
channel: string;
|
||||
channel_capacity: number;
|
||||
fee_mtokens: string;
|
||||
forward_mtokens: string;
|
||||
timeout: number;
|
||||
}
|
||||
];
|
||||
id: string;
|
||||
is_confirmed: boolean;
|
||||
is_outgoing: boolean;
|
||||
mtokens: string;
|
||||
secret: string;
|
||||
safe_fee: number;
|
||||
safe_tokens: number;
|
||||
tokens: number;
|
||||
};
|
||||
|
||||
export type ChannelType = {
|
||||
id: string;
|
||||
tokens: number;
|
||||
@ -18,10 +40,25 @@ export type DecodedType = {
|
||||
tokens: number;
|
||||
};
|
||||
|
||||
export type GetPublicKeyType = {
|
||||
public_key: string;
|
||||
};
|
||||
|
||||
export type ClosedChannelsType = {
|
||||
channels: [];
|
||||
};
|
||||
|
||||
export type CreateInvoiceType = {
|
||||
chain_address?: string;
|
||||
created_at: string;
|
||||
description: string;
|
||||
id: string;
|
||||
mtokens?: string;
|
||||
request: string;
|
||||
secret: string;
|
||||
tokens?: number;
|
||||
};
|
||||
|
||||
export type CloseChannelType = {
|
||||
transaction_id: string;
|
||||
transaction_vout: number;
|
||||
|
@ -36,6 +36,7 @@ const InputLine = styled(SingleLine)`
|
||||
`;
|
||||
|
||||
type InputWithDecoProps = {
|
||||
inputMaxWidth?: string;
|
||||
title: string;
|
||||
value?: string | number | null;
|
||||
noInput?: boolean;
|
||||
@ -60,6 +61,7 @@ export const InputWithDeco: React.FC<InputWithDecoProps> = ({
|
||||
placeholder,
|
||||
color,
|
||||
noInput,
|
||||
inputMaxWidth,
|
||||
inputType = 'text',
|
||||
inputCallback,
|
||||
onKeyDown,
|
||||
@ -91,7 +93,7 @@ export const InputWithDeco: React.FC<InputWithDecoProps> = ({
|
||||
</InputTitleRow>
|
||||
{!noInput && (
|
||||
<Input
|
||||
maxWidth={'500px'}
|
||||
maxWidth={inputMaxWidth || '500px'}
|
||||
placeholder={placeholder}
|
||||
color={color}
|
||||
withMargin={'0 0 0 8px'}
|
||||
|
@ -12,6 +12,18 @@
|
||||
"name": "PaymentType"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "UNION",
|
||||
"name": "LnUrlRequest",
|
||||
"possibleTypes": [
|
||||
{
|
||||
"name": "WithdrawRequest"
|
||||
},
|
||||
{
|
||||
"name": "PayRequest"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
169
src/graphql/mutations/__generated__/lnUrl.generated.tsx
generated
Normal file
169
src/graphql/mutations/__generated__/lnUrl.generated.tsx
generated
Normal file
@ -0,0 +1,169 @@
|
||||
/* eslint-disable */
|
||||
import * as Types from '../../types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
export type FetchLnUrlMutationVariables = Types.Exact<{
|
||||
url: Types.Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type FetchLnUrlMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { fetchLnUrl?: Types.Maybe<(
|
||||
{ __typename?: 'WithdrawRequest' }
|
||||
& Pick<Types.WithdrawRequest, 'callback' | 'k1' | 'maxWithdrawable' | 'defaultDescription' | 'minWithdrawable' | 'tag'>
|
||||
) | (
|
||||
{ __typename?: 'PayRequest' }
|
||||
& Pick<Types.PayRequest, 'callback' | 'maxSendable' | 'minSendable' | 'metadata' | 'commentAllowed' | 'tag'>
|
||||
)> }
|
||||
);
|
||||
|
||||
export type PayLnUrlMutationVariables = Types.Exact<{
|
||||
callback: Types.Scalars['String'];
|
||||
amount: Types.Scalars['Int'];
|
||||
comment?: Types.Maybe<Types.Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type PayLnUrlMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { lnUrlPay: (
|
||||
{ __typename?: 'PaySuccess' }
|
||||
& Pick<Types.PaySuccess, 'tag' | 'description' | 'url' | 'message' | 'ciphertext' | 'iv'>
|
||||
) }
|
||||
);
|
||||
|
||||
export type WithdrawLnUrlMutationVariables = Types.Exact<{
|
||||
callback: Types.Scalars['String'];
|
||||
amount: Types.Scalars['Int'];
|
||||
k1: Types.Scalars['String'];
|
||||
description?: Types.Maybe<Types.Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type WithdrawLnUrlMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& Pick<Types.Mutation, 'lnUrlWithdraw'>
|
||||
);
|
||||
|
||||
|
||||
export const FetchLnUrlDocument = gql`
|
||||
mutation FetchLnUrl($url: String!) {
|
||||
fetchLnUrl(url: $url) {
|
||||
... on WithdrawRequest {
|
||||
callback
|
||||
k1
|
||||
maxWithdrawable
|
||||
defaultDescription
|
||||
minWithdrawable
|
||||
tag
|
||||
}
|
||||
... on PayRequest {
|
||||
callback
|
||||
maxSendable
|
||||
minSendable
|
||||
metadata
|
||||
commentAllowed
|
||||
tag
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type FetchLnUrlMutationFn = Apollo.MutationFunction<FetchLnUrlMutation, FetchLnUrlMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useFetchLnUrlMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useFetchLnUrlMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useFetchLnUrlMutation` 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 [fetchLnUrlMutation, { data, loading, error }] = useFetchLnUrlMutation({
|
||||
* variables: {
|
||||
* url: // value for 'url'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useFetchLnUrlMutation(baseOptions?: Apollo.MutationHookOptions<FetchLnUrlMutation, FetchLnUrlMutationVariables>) {
|
||||
return Apollo.useMutation<FetchLnUrlMutation, FetchLnUrlMutationVariables>(FetchLnUrlDocument, baseOptions);
|
||||
}
|
||||
export type FetchLnUrlMutationHookResult = ReturnType<typeof useFetchLnUrlMutation>;
|
||||
export type FetchLnUrlMutationResult = Apollo.MutationResult<FetchLnUrlMutation>;
|
||||
export type FetchLnUrlMutationOptions = Apollo.BaseMutationOptions<FetchLnUrlMutation, FetchLnUrlMutationVariables>;
|
||||
export const PayLnUrlDocument = gql`
|
||||
mutation PayLnUrl($callback: String!, $amount: Int!, $comment: String) {
|
||||
lnUrlPay(callback: $callback, amount: $amount, comment: $comment) {
|
||||
tag
|
||||
description
|
||||
url
|
||||
message
|
||||
ciphertext
|
||||
iv
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type PayLnUrlMutationFn = Apollo.MutationFunction<PayLnUrlMutation, PayLnUrlMutationVariables>;
|
||||
|
||||
/**
|
||||
* __usePayLnUrlMutation__
|
||||
*
|
||||
* To run a mutation, you first call `usePayLnUrlMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `usePayLnUrlMutation` 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 [payLnUrlMutation, { data, loading, error }] = usePayLnUrlMutation({
|
||||
* variables: {
|
||||
* callback: // value for 'callback'
|
||||
* amount: // value for 'amount'
|
||||
* comment: // value for 'comment'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function usePayLnUrlMutation(baseOptions?: Apollo.MutationHookOptions<PayLnUrlMutation, PayLnUrlMutationVariables>) {
|
||||
return Apollo.useMutation<PayLnUrlMutation, PayLnUrlMutationVariables>(PayLnUrlDocument, baseOptions);
|
||||
}
|
||||
export type PayLnUrlMutationHookResult = ReturnType<typeof usePayLnUrlMutation>;
|
||||
export type PayLnUrlMutationResult = Apollo.MutationResult<PayLnUrlMutation>;
|
||||
export type PayLnUrlMutationOptions = Apollo.BaseMutationOptions<PayLnUrlMutation, PayLnUrlMutationVariables>;
|
||||
export const WithdrawLnUrlDocument = gql`
|
||||
mutation WithdrawLnUrl($callback: String!, $amount: Int!, $k1: String!, $description: String) {
|
||||
lnUrlWithdraw(callback: $callback, amount: $amount, k1: $k1, description: $description)
|
||||
}
|
||||
`;
|
||||
export type WithdrawLnUrlMutationFn = Apollo.MutationFunction<WithdrawLnUrlMutation, WithdrawLnUrlMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useWithdrawLnUrlMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useWithdrawLnUrlMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useWithdrawLnUrlMutation` 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 [withdrawLnUrlMutation, { data, loading, error }] = useWithdrawLnUrlMutation({
|
||||
* variables: {
|
||||
* callback: // value for 'callback'
|
||||
* amount: // value for 'amount'
|
||||
* k1: // value for 'k1'
|
||||
* description: // value for 'description'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useWithdrawLnUrlMutation(baseOptions?: Apollo.MutationHookOptions<WithdrawLnUrlMutation, WithdrawLnUrlMutationVariables>) {
|
||||
return Apollo.useMutation<WithdrawLnUrlMutation, WithdrawLnUrlMutationVariables>(WithdrawLnUrlDocument, baseOptions);
|
||||
}
|
||||
export type WithdrawLnUrlMutationHookResult = ReturnType<typeof useWithdrawLnUrlMutation>;
|
||||
export type WithdrawLnUrlMutationResult = Apollo.MutationResult<WithdrawLnUrlMutation>;
|
||||
export type WithdrawLnUrlMutationOptions = Apollo.BaseMutationOptions<WithdrawLnUrlMutation, WithdrawLnUrlMutationVariables>;
|
53
src/graphql/mutations/lnUrl.ts
Normal file
53
src/graphql/mutations/lnUrl.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const FETCH_LN_URL = gql`
|
||||
mutation FetchLnUrl($url: String!) {
|
||||
fetchLnUrl(url: $url) {
|
||||
... on WithdrawRequest {
|
||||
callback
|
||||
k1
|
||||
maxWithdrawable
|
||||
defaultDescription
|
||||
minWithdrawable
|
||||
tag
|
||||
}
|
||||
... on PayRequest {
|
||||
callback
|
||||
maxSendable
|
||||
minSendable
|
||||
metadata
|
||||
commentAllowed
|
||||
tag
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const PAY_LN_URL = gql`
|
||||
mutation PayLnUrl($callback: String!, $amount: Int!, $comment: String) {
|
||||
lnUrlPay(callback: $callback, amount: $amount, comment: $comment) {
|
||||
tag
|
||||
description
|
||||
url
|
||||
message
|
||||
ciphertext
|
||||
iv
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const WITHDRAW_LN_URL = gql`
|
||||
mutation WithdrawLnUrl(
|
||||
$callback: String!
|
||||
$amount: Int!
|
||||
$k1: String!
|
||||
$description: String
|
||||
) {
|
||||
lnUrlWithdraw(
|
||||
callback: $callback
|
||||
amount: $amount
|
||||
k1: $k1
|
||||
description: $description
|
||||
)
|
||||
}
|
||||
`;
|
@ -208,6 +208,10 @@ export type QueryGetSessionTokenArgs = {
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
lnUrlPay: PaySuccess;
|
||||
lnUrlWithdraw: Scalars['String'];
|
||||
fetchLnUrl?: Maybe<LnUrlRequest>;
|
||||
lnUrl: Scalars['String'];
|
||||
createBaseInvoice?: Maybe<BaseInvoiceType>;
|
||||
createThunderPoints: Scalars['Boolean'];
|
||||
closeChannel?: Maybe<CloseChannelType>;
|
||||
@ -230,6 +234,32 @@ export type Mutation = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationLnUrlPayArgs = {
|
||||
callback: Scalars['String'];
|
||||
amount: Scalars['Int'];
|
||||
comment?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationLnUrlWithdrawArgs = {
|
||||
callback: Scalars['String'];
|
||||
amount: Scalars['Int'];
|
||||
k1: Scalars['String'];
|
||||
description?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationFetchLnUrlArgs = {
|
||||
url: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationLnUrlArgs = {
|
||||
type: Scalars['String'];
|
||||
url: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateBaseInvoiceArgs = {
|
||||
amount: Scalars['Int'];
|
||||
};
|
||||
@ -965,3 +995,35 @@ export type BaseInvoiceType = {
|
||||
id: Scalars['String'];
|
||||
request: Scalars['String'];
|
||||
};
|
||||
|
||||
export type WithdrawRequest = {
|
||||
__typename?: 'WithdrawRequest';
|
||||
callback?: Maybe<Scalars['String']>;
|
||||
k1?: Maybe<Scalars['String']>;
|
||||
maxWithdrawable?: Maybe<Scalars['String']>;
|
||||
defaultDescription?: Maybe<Scalars['String']>;
|
||||
minWithdrawable?: Maybe<Scalars['String']>;
|
||||
tag?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type PayRequest = {
|
||||
__typename?: 'PayRequest';
|
||||
callback?: Maybe<Scalars['String']>;
|
||||
maxSendable?: Maybe<Scalars['String']>;
|
||||
minSendable?: Maybe<Scalars['String']>;
|
||||
metadata?: Maybe<Scalars['String']>;
|
||||
commentAllowed?: Maybe<Scalars['Int']>;
|
||||
tag?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type LnUrlRequest = WithdrawRequest | PayRequest;
|
||||
|
||||
export type PaySuccess = {
|
||||
__typename?: 'PaySuccess';
|
||||
tag?: Maybe<Scalars['String']>;
|
||||
description?: Maybe<Scalars['String']>;
|
||||
url?: Maybe<Scalars['String']>;
|
||||
message?: Maybe<Scalars['String']>;
|
||||
ciphertext?: Maybe<Scalars['String']>;
|
||||
iv?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
@ -1,3 +1,5 @@
|
||||
import bech32 from 'bech32';
|
||||
|
||||
export const getUrlParam = (
|
||||
params: string | string[] | undefined
|
||||
): string | null => {
|
||||
@ -14,3 +16,10 @@ export const getUrlParam = (
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const decodeLnUrl = (url: string): string => {
|
||||
const cleanUrl = url.toLowerCase().replace('lightning:', '');
|
||||
const { words } = bech32.decode(cleanUrl, 500);
|
||||
const bytes = bech32.fromWords(words);
|
||||
return new String(Buffer.from(bytes)).toString();
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { X, Layers, GitBranch } from 'react-feather';
|
||||
import { X, Layers, GitBranch, Command } from 'react-feather';
|
||||
import {
|
||||
CardWithTitle,
|
||||
SubTitle,
|
||||
@ -17,6 +17,7 @@ import { DecodeCard } from './decode/Decode';
|
||||
import { SupportCard } from './donate/DonateCard';
|
||||
import { SupportBar } from './donate/DonateContent';
|
||||
import { OpenChannel } from './openChannel';
|
||||
import { LnUrlCard } from './lnurl';
|
||||
|
||||
const QuickCard = styled.div`
|
||||
background: ${cardColor};
|
||||
@ -59,6 +60,8 @@ export const QuickActions = () => {
|
||||
return 'Decode a Lightning Request';
|
||||
case 'open_channel':
|
||||
return 'Open a Channel';
|
||||
case 'ln_url':
|
||||
return 'Use lnurl';
|
||||
default:
|
||||
return 'Quick Actions';
|
||||
}
|
||||
@ -70,6 +73,8 @@ export const QuickActions = () => {
|
||||
return <SupportBar />;
|
||||
case 'decode':
|
||||
return <DecodeCard />;
|
||||
case 'ln_url':
|
||||
return <LnUrlCard />;
|
||||
case 'open_channel':
|
||||
return (
|
||||
<Card>
|
||||
@ -88,6 +93,10 @@ export const QuickActions = () => {
|
||||
<Layers size={24} />
|
||||
<QuickTitle>Decode</QuickTitle>
|
||||
</QuickCard>
|
||||
<QuickCard onClick={() => setOpenCard('ln_url')}>
|
||||
<Command size={24} />
|
||||
<QuickTitle>LNURL</QuickTitle>
|
||||
</QuickCard>
|
||||
</QuickRow>
|
||||
);
|
||||
}
|
||||
|
139
src/views/home/quickActions/lnurl/LnPay.tsx
Normal file
139
src/views/home/quickActions/lnurl/LnPay.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import { FC, useState } from 'react';
|
||||
import { PayRequest } from 'src/graphql/types';
|
||||
import styled from 'styled-components';
|
||||
import { Title } from 'src/components/typography/Styled';
|
||||
import { Separation } from 'src/components/generic/Styled';
|
||||
import { renderLine } from 'src/components/generic/helpers';
|
||||
import { InputWithDeco } from 'src/components/input/InputWithDeco';
|
||||
import { ColorButton } from 'src/components/buttons/colorButton/ColorButton';
|
||||
import { usePayLnUrlMutation } from 'src/graphql/mutations/__generated__/lnUrl.generated';
|
||||
import { Link } from 'src/components/link/Link';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getErrorContent } from 'src/utils/error';
|
||||
|
||||
const ModalText = styled.div`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const StyledLink = styled(ModalText)`
|
||||
margin: 16px 0 32px;
|
||||
font-size: 24px;
|
||||
`;
|
||||
|
||||
type LnPayProps = {
|
||||
request: PayRequest;
|
||||
};
|
||||
|
||||
export const LnPay: FC<LnPayProps> = ({ request }) => {
|
||||
const { minSendable, maxSendable, callback, commentAllowed } = request;
|
||||
|
||||
const min = Number(minSendable) / 1000 || 0;
|
||||
const max = Number(maxSendable) / 1000 || 0;
|
||||
|
||||
const isSame = min === max;
|
||||
|
||||
const [amount, setAmount] = useState<number>(min);
|
||||
const [comment, setComment] = useState<string>('');
|
||||
|
||||
const [payLnUrl, { data, loading }] = usePayLnUrlMutation({
|
||||
onError: error => toast.error(getErrorContent(error)),
|
||||
});
|
||||
|
||||
if (!callback) {
|
||||
return <ModalText>Missing information from LN Service</ModalText>;
|
||||
}
|
||||
|
||||
const callbackUrl = new URL(callback);
|
||||
|
||||
if (!loading && data?.lnUrlPay.tag) {
|
||||
const { tag, url, description, message, ciphertext, iv } = data.lnUrlPay;
|
||||
if (tag === 'url') {
|
||||
return (
|
||||
<>
|
||||
<Title>Success</Title>
|
||||
{(description || url) && <Separation />}
|
||||
{description && <ModalText>{description}</ModalText>}
|
||||
{url && (
|
||||
<StyledLink>
|
||||
<Link href={url}>{url}</Link>
|
||||
</StyledLink>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (tag === 'message') {
|
||||
return (
|
||||
<>
|
||||
<Title>Success</Title>
|
||||
{message && <Separation />}
|
||||
{message && <ModalText>{message}</ModalText>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (tag === 'aes') {
|
||||
return (
|
||||
<>
|
||||
<Title>Success</Title>
|
||||
{(description || ciphertext || iv) && <Separation />}
|
||||
{description && <ModalText>{description}</ModalText>}
|
||||
{renderLine('Ciphertext', ciphertext)}
|
||||
{renderLine('IV', iv)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <Title>Success</Title>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>Pay</Title>
|
||||
<Separation />
|
||||
<ModalText>{`Pay to ${callbackUrl.host}`}</ModalText>
|
||||
<Separation />
|
||||
{isSame && renderLine('Pay Amount (sats)', max)}
|
||||
{!isSame && renderLine('Max Pay Amount (sats)', max)}
|
||||
{!isSame && renderLine('Min Pay Amount (sats)', min)}
|
||||
<Separation />
|
||||
{!!commentAllowed && (
|
||||
<InputWithDeco
|
||||
inputMaxWidth={'300px'}
|
||||
title={`Comment (Max ${commentAllowed} characters)`}
|
||||
value={comment}
|
||||
inputCallback={value => {
|
||||
setComment(value.substring(0, commentAllowed));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isSame && (
|
||||
<InputWithDeco
|
||||
inputMaxWidth={'300px'}
|
||||
title={'Amount'}
|
||||
amount={amount}
|
||||
value={amount}
|
||||
inputType={'number'}
|
||||
inputCallback={value => {
|
||||
if (min && max) {
|
||||
setAmount(Math.min(max, Math.max(min, Number(value))));
|
||||
} else if (min && !max) {
|
||||
setAmount(Math.max(min, Number(value)));
|
||||
} else if (!min && max) {
|
||||
setAmount(Math.min(max, Number(value)));
|
||||
} else {
|
||||
setAmount(Number(value));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ColorButton
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
fullWidth={true}
|
||||
withMargin={'16px 0 0'}
|
||||
onClick={() => payLnUrl({ variables: { callback, amount, comment } })}
|
||||
>
|
||||
{`Pay (${amount} sats)`}
|
||||
</ColorButton>
|
||||
</>
|
||||
);
|
||||
};
|
180
src/views/home/quickActions/lnurl/LnWithdraw.tsx
Normal file
180
src/views/home/quickActions/lnurl/LnWithdraw.tsx
Normal file
@ -0,0 +1,180 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { WithdrawRequest } from 'src/graphql/types';
|
||||
import styled from 'styled-components';
|
||||
import { Title } from 'src/components/typography/Styled';
|
||||
import { DarkSubTitle, Separation } from 'src/components/generic/Styled';
|
||||
import { renderLine } from 'src/components/generic/helpers';
|
||||
import { InputWithDeco } from 'src/components/input/InputWithDeco';
|
||||
import { ColorButton } from 'src/components/buttons/colorButton/ColorButton';
|
||||
import { useWithdrawLnUrlMutation } from 'src/graphql/mutations/__generated__/lnUrl.generated';
|
||||
import { useGetInvoiceStatusChangeLazyQuery } from 'src/graphql/queries/__generated__/getInvoiceStatusChange.generated';
|
||||
import { chartColors } from 'src/styles/Themes';
|
||||
import { CheckCircle } from 'react-feather';
|
||||
import { Link } from 'src/components/link/Link';
|
||||
import { getErrorContent } from 'src/utils/error';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Timer } from '../../account/createInvoice/Timer';
|
||||
|
||||
const Center = styled.div`
|
||||
margin: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ModalText = styled.div`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
type LnWithdrawProps = {
|
||||
request: WithdrawRequest;
|
||||
};
|
||||
|
||||
export const LnWithdraw: FC<LnWithdrawProps> = ({ request }) => {
|
||||
const {
|
||||
minWithdrawable,
|
||||
maxWithdrawable,
|
||||
callback,
|
||||
defaultDescription,
|
||||
k1,
|
||||
} = request;
|
||||
|
||||
const min = Number(minWithdrawable) / 1000 || 0;
|
||||
const max = Number(maxWithdrawable) / 1000 || 0;
|
||||
|
||||
const isSame = min === max;
|
||||
|
||||
const [invoiceStatus, setInvoiceStatus] = useState<string>('none');
|
||||
const [amount, setAmount] = useState<number>(min);
|
||||
const [description, setDescription] = useState<string>(
|
||||
defaultDescription || ''
|
||||
);
|
||||
|
||||
const [withdraw, { data, loading }] = useWithdrawLnUrlMutation({
|
||||
onError: error => toast.error(getErrorContent(error)),
|
||||
});
|
||||
const [
|
||||
checkStatus,
|
||||
{ data: statusData, loading: statusLoading, error },
|
||||
] = useGetInvoiceStatusChangeLazyQuery({
|
||||
onError: error => toast.error(getErrorContent(error)),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && data?.lnUrlWithdraw) {
|
||||
checkStatus({ variables: { id: data.lnUrlWithdraw } });
|
||||
}
|
||||
}, [loading, data, checkStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (statusLoading || !statusData?.getInvoiceStatusChange) return;
|
||||
setInvoiceStatus(statusData.getInvoiceStatusChange);
|
||||
}, [statusLoading, statusData]);
|
||||
|
||||
if (!callback) {
|
||||
return <ModalText>Missing information from LN Service</ModalText>;
|
||||
}
|
||||
|
||||
const callbackUrl = new URL(callback);
|
||||
|
||||
const renderContent = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<Center>
|
||||
<DarkSubTitle>
|
||||
Failed to check status of the withdrawal. Please check the status in
|
||||
the
|
||||
<Link to={'/transactions'}> Transactions </Link>
|
||||
view
|
||||
</DarkSubTitle>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
if (invoiceStatus === 'paid') {
|
||||
return (
|
||||
<Center>
|
||||
<CheckCircle stroke={chartColors.green} size={32} />
|
||||
<Title>Paid</Title>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (invoiceStatus === 'not_paid' || invoiceStatus === 'timeout') {
|
||||
return (
|
||||
<Center>
|
||||
<Title>
|
||||
Check the status of this invoice in the
|
||||
<Link to={'/transactions'}> Transactions </Link>
|
||||
view
|
||||
</Title>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
if (statusLoading) {
|
||||
return (
|
||||
<>
|
||||
<Timer initialMinute={1} initialSeconds={30} />
|
||||
<div>hello</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{isSame && renderLine('Withdraw Amount', max)}
|
||||
{!isSame && renderLine('Max Withdraw Amount', max)}
|
||||
{!isSame && renderLine('Min Withdraw Amount', min)}
|
||||
<Separation />
|
||||
<InputWithDeco
|
||||
inputMaxWidth={'300px'}
|
||||
title={'Description'}
|
||||
value={description}
|
||||
inputCallback={value => setDescription(value)}
|
||||
/>
|
||||
{!isSame && (
|
||||
<InputWithDeco
|
||||
inputMaxWidth={'300px'}
|
||||
title={'Amount'}
|
||||
amount={amount}
|
||||
value={amount}
|
||||
inputType={'number'}
|
||||
inputCallback={value => {
|
||||
if (min && max) {
|
||||
setAmount(Math.min(max, Math.max(min, Number(value))));
|
||||
} else if (min && !max) {
|
||||
setAmount(Math.max(min, Number(value)));
|
||||
} else if (!min && max) {
|
||||
setAmount(Math.min(max, Number(value)));
|
||||
} else {
|
||||
setAmount(Number(value));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ColorButton
|
||||
loading={loading || statusLoading}
|
||||
disabled={loading || !k1 || statusLoading}
|
||||
fullWidth={true}
|
||||
withMargin={'16px 0 0'}
|
||||
onClick={() =>
|
||||
withdraw({
|
||||
variables: { callback, amount, k1: k1 || '', description },
|
||||
})
|
||||
}
|
||||
>
|
||||
{`Withdraw (${amount} sats)`}
|
||||
</ColorButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>Withdraw</Title>
|
||||
<Separation />
|
||||
<ModalText>{`Withdraw from ${callbackUrl.host}`}</ModalText>
|
||||
<Separation />
|
||||
{renderContent()}
|
||||
</>
|
||||
);
|
||||
};
|
66
src/views/home/quickActions/lnurl/index.tsx
Normal file
66
src/views/home/quickActions/lnurl/index.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { useState } 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 { decodeLnUrl } from 'src/utils/url';
|
||||
import { LnUrlModal } from './lnUrlModal';
|
||||
|
||||
export const LnUrlCard = () => {
|
||||
const [lnurl, setLnUrl] = useState<string>('');
|
||||
const [url, setUrl] = useState<string>('');
|
||||
const [type, setType] = useState<string>('');
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
|
||||
const handleDecode = () => {
|
||||
if (!lnurl) {
|
||||
toast.warning('Please input a LNURL');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const urlString = decodeLnUrl(lnurl);
|
||||
|
||||
const url = new URL(urlString);
|
||||
const tag = url.searchParams.get('tag') || '';
|
||||
|
||||
setUrl(urlString);
|
||||
setType(tag);
|
||||
|
||||
if (url && tag !== 'login') {
|
||||
setModalOpen(true);
|
||||
}
|
||||
if (tag === 'login') {
|
||||
toast.warning('LnAuth is not available yet');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Problem decoding LNURL');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<InputWithDeco
|
||||
value={lnurl}
|
||||
placeholder={'LnPay or LnWithdraw URL'}
|
||||
title={'LNURL'}
|
||||
inputCallback={value => setLnUrl(value)}
|
||||
onEnter={() => handleDecode()}
|
||||
/>
|
||||
<ColorButton
|
||||
arrow={true}
|
||||
fullWidth={true}
|
||||
disabled={!lnurl}
|
||||
withMargin={'16px 0 0'}
|
||||
onClick={() => handleDecode()}
|
||||
>
|
||||
Confirm
|
||||
</ColorButton>
|
||||
</Card>
|
||||
<Modal isOpen={modalOpen} closeCallback={() => setModalOpen(false)}>
|
||||
<LnUrlModal url={url} type={type} />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
62
src/views/home/quickActions/lnurl/lnUrlModal.tsx
Normal file
62
src/views/home/quickActions/lnurl/lnUrlModal.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { FC, useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { ColorButton } from 'src/components/buttons/colorButton/ColorButton';
|
||||
import { Separation } from 'src/components/generic/Styled';
|
||||
import { LoadingCard } from 'src/components/loading/LoadingCard';
|
||||
import { Title } from 'src/components/typography/Styled';
|
||||
import { useFetchLnUrlMutation } from 'src/graphql/mutations/__generated__/lnUrl.generated';
|
||||
import { getErrorContent } from 'src/utils/error';
|
||||
import styled from 'styled-components';
|
||||
import { LnPay } from './LnPay';
|
||||
import { LnWithdraw } from './LnWithdraw';
|
||||
|
||||
const ModalText = styled.div`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
type lnUrlProps = {
|
||||
url: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export const LnUrlModal: FC<lnUrlProps> = ({ url, type }) => {
|
||||
const fullUrl = new URL(url);
|
||||
|
||||
const [fetchLnUrl, { data, loading }] = useFetchLnUrlMutation({
|
||||
onError: error => toast.error(getErrorContent(error)),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!type) {
|
||||
fetchLnUrl({ variables: { url } });
|
||||
}
|
||||
}, [type, url, fetchLnUrl]);
|
||||
|
||||
if (!type && !data) {
|
||||
return <LoadingCard noCard={true} />;
|
||||
}
|
||||
|
||||
if (loading || !data) {
|
||||
return <LoadingCard noCard={true} />;
|
||||
}
|
||||
|
||||
if (data?.fetchLnUrl?.__typename === 'PayRequest') {
|
||||
return <LnPay request={data.fetchLnUrl} />;
|
||||
}
|
||||
|
||||
if (data?.fetchLnUrl?.__typename === 'WithdrawRequest') {
|
||||
return <LnWithdraw request={data.fetchLnUrl} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>Login</Title>
|
||||
<Separation />
|
||||
<ModalText>{`Login to ${fullUrl.host}`}</ModalText>;
|
||||
<ColorButton fullWidth={true} withMargin={'32px 0 0'}>
|
||||
Confirm
|
||||
</ColorButton>
|
||||
</>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user