chore: lightning address (#345)

This commit is contained in:
Anthony Potdevin 2021-10-03 17:45:43 +02:00 committed by GitHub
parent 0a1f39e637
commit 1afae70edb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 375 additions and 1 deletions

View File

@ -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 (

View File

@ -29,4 +29,9 @@ export const ambossTypes = gql`
info: BosScoreInfo!
scores: [BosScore!]!
}
type LightningAddress {
pubkey: String!
lightning_address: String!
}
`;

View File

@ -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,

View File

@ -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!

View File

@ -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<string>, maxSendable?: Types.Maybe<string>, minSendable?: Types.Maybe<string>, metadata?: Types.Maybe<string>, commentAllowed?: Types.Maybe<number>, tag?: Types.Maybe<string> } };
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<GetLightningAddressInfoQuery, GetLightningAddressInfoQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetLightningAddressInfoQuery, GetLightningAddressInfoQueryVariables>(GetLightningAddressInfoDocument, options);
}
export function useGetLightningAddressInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetLightningAddressInfoQuery, GetLightningAddressInfoQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetLightningAddressInfoQuery, GetLightningAddressInfoQueryVariables>(GetLightningAddressInfoDocument, options);
}
export type GetLightningAddressInfoQueryHookResult = ReturnType<typeof useGetLightningAddressInfoQuery>;
export type GetLightningAddressInfoLazyQueryHookResult = ReturnType<typeof useGetLightningAddressInfoLazyQuery>;
export type GetLightningAddressInfoQueryResult = Apollo.QueryResult<GetLightningAddressInfoQuery, GetLightningAddressInfoQueryVariables>;

View File

@ -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<GetLightningAddressesQuery, GetLightningAddressesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetLightningAddressesQuery, GetLightningAddressesQueryVariables>(GetLightningAddressesDocument, options);
}
export function useGetLightningAddressesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetLightningAddressesQuery, GetLightningAddressesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetLightningAddressesQuery, GetLightningAddressesQueryVariables>(GetLightningAddressesDocument, options);
}
export type GetLightningAddressesQueryHookResult = ReturnType<typeof useGetLightningAddressesQuery>;
export type GetLightningAddressesLazyQueryHookResult = ReturnType<typeof useGetLightningAddressesLazyQuery>;
export type GetLightningAddressesQueryResult = Apollo.QueryResult<GetLightningAddressesQuery, GetLightningAddressesQueryVariables>;

View File

@ -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
}
}
`;

View File

@ -0,0 +1,10 @@
import { gql } from '@apollo/client';
export const GET_LIGHTNING_ADDRESSES = gql`
query GetLightningAddresses {
getLightningAddresses {
pubkey
lightning_address
}
}
`;

View File

@ -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<Maybe<Forward>>;
getInvoiceStatusChange?: Maybe<Scalars['String']>;
getLatestVersion?: Maybe<Scalars['String']>;
getLightningAddressInfo: PayRequest;
getLightningAddresses: Array<LightningAddress>;
getLnMarketsStatus: Scalars['String'];
getLnMarketsUrl: Scalars['String'];
getLnMarketsUserInfo?: Maybe<LnMarketsUserInfo>;
@ -651,6 +659,11 @@ export type QueryGetInvoiceStatusChangeArgs = {
};
export type QueryGetLightningAddressInfoArgs = {
address: Scalars['String'];
};
export type QueryGetMessagesArgs = {
initialize?: Maybe<Scalars['Boolean']>;
lastMessage?: Maybe<Scalars['String']>;

View File

@ -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 <DecodeCard />;
case 'ln_url':
return <LnUrlCard />;
case 'lightning_address':
return <LightningAddressCard />;
case 'open_channel':
return (
<Card>
@ -101,6 +106,10 @@ export const QuickActions = () => {
<QuickRow>
<SupportCard callback={() => setOpenCard('support')} />
<AmbossCard />
<QuickCard onClick={() => setOpenCard('lightning_address')}>
<Zap size={24} />
<QuickTitle>Address</QuickTitle>
</QuickCard>
<QuickCard onClick={() => setOpenCard('open_channel')}>
<GitBranch size={24} />
<QuickTitle>Open</QuickTitle>

View File

@ -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<AddressProps> = ({ handleClick }) => {
const [savedAddresses] = useLocalStorage<string[]>(
'saved_lightning_address',
[]
);
if (!savedAddresses.length) {
return null;
}
return (
<>
<Separation />
<Sub4Title>Previously Used Addresses:</Sub4Title>
<S.wrapper>
{savedAddresses.map((a, index) => (
<S.address onClick={() => handleClick(a)} key={`${index}${a}`}>
{a}
</S.address>
))}
</S.wrapper>
</>
);
};
export const AmbossAddresses: FC<AddressProps> = ({ 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 (
<>
<Separation />
<Sub4Title>Amboss Addresses:</Sub4Title>
<S.wrapper>
{mapped.map((a, index) => (
<S.address onClick={() => handleClick(a)} key={`${index}${a}`}>
{a}
</S.address>
))}
</S.wrapper>
</>
);
};

View File

@ -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<string>('');
const [savedAddresses, setSavedAddresses] = useLocalStorage<string[]>(
'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 => (
<div key={e.message}>{e.message}</div>
));
toast.error(<div>{messages}</div>);
},
});
const [data, reset] = useMutationResultWithReset(_data);
const handleClick = (address: string) => setAddress(address);
return (
<>
<Card>
<InputWithDeco
title={'Lightning Address'}
value={address}
inputCallback={v => setAddress(v)}
/>
<ColorButton
arrow={true}
fullWidth={true}
loading={loading}
disabled={!address || loading}
withMargin={'16px 0 0'}
onClick={() => getInfo({ variables: { address } })}
>
Pay
</ColorButton>
<PreviousAddresses handleClick={handleClick} />
<AmbossAddresses handleClick={handleClick} />
</Card>
<Modal
isOpen={!!data?.getLightningAddressInfo}
closeCallback={() => {
setAddress('');
reset();
}}
>
{data?.getLightningAddressInfo ? (
<LnPay request={data?.getLightningAddressInfo} />
) : null}
</Modal>
</>
);
};