Skip to content

Commit

Permalink
Merge pull request #143 from apotdevin/ln-url
Browse files Browse the repository at this point in the history
chore: 🔧 ln-url
  • Loading branch information
apotdevin authored Sep 23, 2020
2 parents 02cb697 + b9199a4 commit 05b41db
Show file tree
Hide file tree
Showing 23 changed files with 1,112 additions and 5 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion server/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -59,6 +61,7 @@ const typeDefs = [
routeTypes,
bosTypes,
tbaseTypes,
lnUrlTypes,
];

const resolvers = merge(
Expand All @@ -82,7 +85,8 @@ const resolvers = merge(
macaroonResolvers,
networkResolvers,
bosResolvers,
tbaseResolvers
tbaseResolvers,
lnUrlResolvers
);

export const schema = makeExecutableSchema({ typeDefs, resolvers });
4 changes: 2 additions & 2 deletions server/schema/invoice/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -90,7 +90,7 @@ export const invoiceResolvers = {
return date.toISOString();
};

return await to(
return await to<CreateInvoiceType>(
createInvoiceRequest({
lnd,
...(description && { description }),
Expand Down
243 changes: 243 additions & 0 deletions server/schema/lnurl/resolvers.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 32 additions & 0 deletions server/schema/lnurl/types.ts
Original file line number Diff line number Diff line change
@@ -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
}
`;
9 changes: 9 additions & 0 deletions server/schema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
4 changes: 4 additions & 0 deletions server/tests/__mocks__/ln-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Loading

0 comments on commit 05b41db

Please sign in to comment.