From 1afae70edb6b6eeec77a88863c4ee70ba0534a0c Mon Sep 17 00:00:00 2001 From: Anthony Potdevin <31413433+apotdevin@users.noreply.github.com> Date: Sun, 3 Oct 2021 17:45:43 +0200 Subject: [PATCH] chore: lightning address (#345) --- server/schema/amboss/resolvers.ts | 30 +++++++ server/schema/amboss/types.ts | 5 ++ server/schema/lnurl/resolvers.ts | 35 ++++++++ server/schema/types.ts | 2 + .../getLightningAddressInfo.generated.tsx | 54 +++++++++++++ .../getLightningAddresses.generated.tsx | 47 +++++++++++ .../queries/getLightningAddressInfo.ts | 14 ++++ src/graphql/queries/getLightningAddresses.ts | 10 +++ src/graphql/types.ts | 13 +++ src/views/home/quickActions/QuickActions.tsx | 11 ++- .../lightningAddress/Addresses.tsx | 81 +++++++++++++++++++ .../lightningAddress/LightningAddress.tsx | 74 +++++++++++++++++ 12 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 src/graphql/queries/__generated__/getLightningAddressInfo.generated.tsx create mode 100644 src/graphql/queries/__generated__/getLightningAddresses.generated.tsx create mode 100644 src/graphql/queries/getLightningAddressInfo.ts create mode 100644 src/graphql/queries/getLightningAddresses.ts create mode 100644 src/views/home/quickActions/lightningAddress/Addresses.tsx create mode 100644 src/views/home/quickActions/lightningAddress/LightningAddress.tsx diff --git a/server/schema/amboss/resolvers.ts b/server/schema/amboss/resolvers.ts index b709a43c..c2954c32 100644 --- a/server/schema/amboss/resolvers.ts +++ b/server/schema/amboss/resolvers.ts @@ -111,6 +111,15 @@ const getBosScoresQuery = gql` } `; +const getLightningAddresses = gql` + query GetLightningAddresses { + getLightningAddresses { + pubkey + lightning_address + } + } +`; + export const ambossResolvers = { Query: { getAmbossUser: async ( @@ -199,6 +208,27 @@ export const ambossResolvers = { return data.getBosScores; }, + getLightningAddresses: async ( + _: undefined, + __: undefined, + { ip }: ContextType + ) => { + await requestLimiter(ip, 'getLightningAddresses'); + + const { data, error } = await graphqlFetchWithProxy( + appUrls.amboss, + print(getLightningAddresses) + ); + + if (!data?.getLightningAddresses || error) { + if (error) { + logger.error(error); + } + throw new Error('Error getting Lightning Addresses from Amboss'); + } + + return data.getLightningAddresses; + }, }, Mutation: { loginAmboss: async ( diff --git a/server/schema/amboss/types.ts b/server/schema/amboss/types.ts index 6488b1c1..55edd9c9 100644 --- a/server/schema/amboss/types.ts +++ b/server/schema/amboss/types.ts @@ -29,4 +29,9 @@ export const ambossTypes = gql` info: BosScoreInfo! scores: [BosScore!]! } + + type LightningAddress { + pubkey: String! + lightning_address: String! + } `; diff --git a/server/schema/lnurl/resolvers.ts b/server/schema/lnurl/resolvers.ts index 8bc38213..6e33ba6c 100644 --- a/server/schema/lnurl/resolvers.ts +++ b/server/schema/lnurl/resolvers.ts @@ -66,6 +66,41 @@ type RequestType = PayRequestType | WithdrawRequestType; type RequestWithType = { isTypeOf: string } & RequestType; export const lnUrlResolvers = { + Query: { + getLightningAddressInfo: async ( + _: undefined, + { address }: { address: string }, + { ip }: ContextType + ) => { + await requestLimiter(ip, 'getLightningAddressInfo'); + + const split = address.split('@'); + + if (split.length !== 2) { + throw new Error('Invalid lightning address'); + } + + try { + const response = await fetchWithProxy( + `https://${split[1]}/.well-known/lnurlp/${split[0]}` + ); + const result = await response.json(); + + let valid = true; + if (!result.callback) valid = false; + if (!result.maxSendable) valid = false; + if (!result.minSendable) valid = false; + + if (!valid) { + throw new Error('Invalid lightning address'); + } + + return result; + } catch (error) { + throw new Error('Invalid lightning address'); + } + }, + }, Mutation: { lnUrlAuth: async ( _: undefined, diff --git a/server/schema/types.ts b/server/schema/types.ts index d56a8b6b..343af1fe 100644 --- a/server/schema/types.ts +++ b/server/schema/types.ts @@ -28,6 +28,8 @@ export const generalTypes = gql` export const queryTypes = gql` type Query { + getLightningAddressInfo(address: String!): PayRequest! + getLightningAddresses: [LightningAddress!]! getAmbossLoginToken: String! getAmbossUser: AmbossUserType getNodeBalances: BalancesType! diff --git a/src/graphql/queries/__generated__/getLightningAddressInfo.generated.tsx b/src/graphql/queries/__generated__/getLightningAddressInfo.generated.tsx new file mode 100644 index 00000000..601a1c5b --- /dev/null +++ b/src/graphql/queries/__generated__/getLightningAddressInfo.generated.tsx @@ -0,0 +1,54 @@ +/* eslint-disable */ +import * as Types from '../../types'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} +export type GetLightningAddressInfoQueryVariables = Types.Exact<{ + address: Types.Scalars['String']; +}>; + + +export type GetLightningAddressInfoQuery = { __typename?: 'Query', getLightningAddressInfo: { __typename?: 'PayRequest', callback?: Types.Maybe, maxSendable?: Types.Maybe, minSendable?: Types.Maybe, metadata?: Types.Maybe, commentAllowed?: Types.Maybe, tag?: Types.Maybe } }; + + +export const GetLightningAddressInfoDocument = gql` + query GetLightningAddressInfo($address: String!) { + getLightningAddressInfo(address: $address) { + callback + maxSendable + minSendable + metadata + commentAllowed + tag + } +} + `; + +/** + * __useGetLightningAddressInfoQuery__ + * + * To run a query within a React component, call `useGetLightningAddressInfoQuery` and pass it any options that fit your needs. + * When your component renders, `useGetLightningAddressInfoQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetLightningAddressInfoQuery({ + * variables: { + * address: // value for 'address' + * }, + * }); + */ +export function useGetLightningAddressInfoQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetLightningAddressInfoDocument, options); + } +export function useGetLightningAddressInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetLightningAddressInfoDocument, options); + } +export type GetLightningAddressInfoQueryHookResult = ReturnType; +export type GetLightningAddressInfoLazyQueryHookResult = ReturnType; +export type GetLightningAddressInfoQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/src/graphql/queries/__generated__/getLightningAddresses.generated.tsx b/src/graphql/queries/__generated__/getLightningAddresses.generated.tsx new file mode 100644 index 00000000..e861786e --- /dev/null +++ b/src/graphql/queries/__generated__/getLightningAddresses.generated.tsx @@ -0,0 +1,47 @@ +/* eslint-disable */ +import * as Types from '../../types'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} +export type GetLightningAddressesQueryVariables = Types.Exact<{ [key: string]: never; }>; + + +export type GetLightningAddressesQuery = { __typename?: 'Query', getLightningAddresses: Array<{ __typename?: 'LightningAddress', pubkey: string, lightning_address: string }> }; + + +export const GetLightningAddressesDocument = gql` + query GetLightningAddresses { + getLightningAddresses { + pubkey + lightning_address + } +} + `; + +/** + * __useGetLightningAddressesQuery__ + * + * To run a query within a React component, call `useGetLightningAddressesQuery` and pass it any options that fit your needs. + * When your component renders, `useGetLightningAddressesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetLightningAddressesQuery({ + * variables: { + * }, + * }); + */ +export function useGetLightningAddressesQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetLightningAddressesDocument, options); + } +export function useGetLightningAddressesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetLightningAddressesDocument, options); + } +export type GetLightningAddressesQueryHookResult = ReturnType; +export type GetLightningAddressesLazyQueryHookResult = ReturnType; +export type GetLightningAddressesQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/src/graphql/queries/getLightningAddressInfo.ts b/src/graphql/queries/getLightningAddressInfo.ts new file mode 100644 index 00000000..90c9ac46 --- /dev/null +++ b/src/graphql/queries/getLightningAddressInfo.ts @@ -0,0 +1,14 @@ +import { gql } from '@apollo/client'; + +export const GET_LIGHTNING_ADDRESS_INFO = gql` + query GetLightningAddressInfo($address: String!) { + getLightningAddressInfo(address: $address) { + callback + maxSendable + minSendable + metadata + commentAllowed + tag + } + } +`; diff --git a/src/graphql/queries/getLightningAddresses.ts b/src/graphql/queries/getLightningAddresses.ts new file mode 100644 index 00000000..8fb21f34 --- /dev/null +++ b/src/graphql/queries/getLightningAddresses.ts @@ -0,0 +1,10 @@ +import { gql } from '@apollo/client'; + +export const GET_LIGHTNING_ADDRESSES = gql` + query GetLightningAddresses { + getLightningAddresses { + pubkey + lightning_address + } + } +`; diff --git a/src/graphql/types.ts b/src/graphql/types.ts index 9c169415..7ed96cbe 100644 --- a/src/graphql/types.ts +++ b/src/graphql/types.ts @@ -188,6 +188,12 @@ export type InvoiceType = { type: Scalars['String']; }; +export type LightningAddress = { + __typename?: 'LightningAddress'; + lightning_address: Scalars['String']; + pubkey: Scalars['String']; +}; + export type LightningBalanceType = { __typename?: 'LightningBalanceType'; active: Scalars['String']; @@ -563,6 +569,8 @@ export type Query = { getForwards: Array>; getInvoiceStatusChange?: Maybe; getLatestVersion?: Maybe; + getLightningAddressInfo: PayRequest; + getLightningAddresses: Array; getLnMarketsStatus: Scalars['String']; getLnMarketsUrl: Scalars['String']; getLnMarketsUserInfo?: Maybe; @@ -651,6 +659,11 @@ export type QueryGetInvoiceStatusChangeArgs = { }; +export type QueryGetLightningAddressInfoArgs = { + address: Scalars['String']; +}; + + export type QueryGetMessagesArgs = { initialize?: Maybe; lastMessage?: Maybe; diff --git a/src/views/home/quickActions/QuickActions.tsx b/src/views/home/quickActions/QuickActions.tsx index b916db9f..d36b872e 100644 --- a/src/views/home/quickActions/QuickActions.tsx +++ b/src/views/home/quickActions/QuickActions.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import styled from 'styled-components'; -import { X, Layers, GitBranch, Command } from 'react-feather'; +import { X, Layers, GitBranch, Command, Zap } from 'react-feather'; import { CardWithTitle, SubTitle, @@ -21,6 +21,7 @@ import { OpenChannel } from './openChannel'; import { LnUrlCard } from './lnurl'; import { LnMarketsCard } from './lnmarkets'; import { AmbossCard } from './amboss/AmbossCard'; +import { LightningAddressCard } from './lightningAddress/LightningAddress'; export const QuickCard = styled.div` background: ${cardColor}; @@ -73,6 +74,8 @@ export const QuickActions = () => { return 'Open a Channel'; case 'ln_url': return 'Use lnurl'; + case 'lightning_address': + return 'Pay to a Lightning Address'; default: return 'Quick Actions'; } @@ -90,6 +93,8 @@ export const QuickActions = () => { return ; case 'ln_url': return ; + case 'lightning_address': + return ; case 'open_channel': return ( @@ -101,6 +106,10 @@ export const QuickActions = () => { setOpenCard('support')} /> + setOpenCard('lightning_address')}> + + Address + setOpenCard('open_channel')}> Open diff --git a/src/views/home/quickActions/lightningAddress/Addresses.tsx b/src/views/home/quickActions/lightningAddress/Addresses.tsx new file mode 100644 index 00000000..16ff4439 --- /dev/null +++ b/src/views/home/quickActions/lightningAddress/Addresses.tsx @@ -0,0 +1,81 @@ +import { useLocalStorage } from 'src/hooks/UseLocalStorage'; +import { Separation, Sub4Title } from 'src/components/generic/Styled'; +import styled from 'styled-components'; +import { cardBorderColor, subCardColor } from 'src/styles/Themes'; +import { FC } from 'react'; +import { useGetLightningAddressesQuery } from 'src/graphql/queries/__generated__/getLightningAddresses.generated'; + +const S = { + wrapper: styled.div` + display: flex; + flex-wrap: wrap; + `, + address: styled.button` + font-size: 14px; + padding: 4px 8px; + margin: 2px; + border: 1px solid ${cardBorderColor}; + background-color: ${subCardColor}; + border-radius: 4px; + cursor: pointer; + color: inherit; + + :hover { + background-color: ${cardBorderColor}; + } + `, +}; + +type AddressProps = { + handleClick: (address: string) => void; +}; + +export const PreviousAddresses: FC = ({ handleClick }) => { + const [savedAddresses] = useLocalStorage( + 'saved_lightning_address', + [] + ); + + if (!savedAddresses.length) { + return null; + } + + return ( + <> + + Previously Used Addresses: + + {savedAddresses.map((a, index) => ( + handleClick(a)} key={`${index}${a}`}> + {a} + + ))} + + + ); +}; + +export const AmbossAddresses: FC = ({ handleClick }) => { + const { data, loading, error } = useGetLightningAddressesQuery(); + + if (loading || error || !data?.getLightningAddresses.length) { + return null; + } + + const addresses = data?.getLightningAddresses || []; + const mapped = addresses.map(a => a.lightning_address); + + return ( + <> + + Amboss Addresses: + + {mapped.map((a, index) => ( + handleClick(a)} key={`${index}${a}`}> + {a} + + ))} + + + ); +}; diff --git a/src/views/home/quickActions/lightningAddress/LightningAddress.tsx b/src/views/home/quickActions/lightningAddress/LightningAddress.tsx new file mode 100644 index 00000000..79c956b9 --- /dev/null +++ b/src/views/home/quickActions/lightningAddress/LightningAddress.tsx @@ -0,0 +1,74 @@ +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 { useGetLightningAddressInfoLazyQuery } from 'src/graphql/queries/__generated__/getLightningAddressInfo.generated'; +import { useLocalStorage } from 'src/hooks/UseLocalStorage'; +import { useMutationResultWithReset } from 'src/hooks/UseMutationWithReset'; +import { LnPay } from '../lnurl/LnPay'; +import { PreviousAddresses, AmbossAddresses } from './Addresses'; + +export const LightningAddressCard = () => { + const [address, setAddress] = useState(''); + const [savedAddresses, setSavedAddresses] = useLocalStorage( + 'saved_lightning_address', + [] + ); + + const [getInfo, { data: _data, loading }] = + useGetLightningAddressInfoLazyQuery({ + fetchPolicy: 'network-only', + onCompleted: () => { + const filtered = savedAddresses.filter(a => a !== address); + const final = [address, ...filtered]; + setSavedAddresses(final); + }, + onError: ({ graphQLErrors }) => { + const messages = graphQLErrors.map(e => ( +
{e.message}
+ )); + toast.error(
{messages}
); + }, + }); + + const [data, reset] = useMutationResultWithReset(_data); + + const handleClick = (address: string) => setAddress(address); + + return ( + <> + + setAddress(v)} + /> + getInfo({ variables: { address } })} + > + Pay + + + + + { + setAddress(''); + reset(); + }} + > + {data?.getLightningAddressInfo ? ( + + ) : null} + + + ); +};