lnurl-withdrawal support
This commit is contained in:
		
							parent
							
								
									16fe74edf1
								
							
						
					
					
						commit
						b8080137a8
					
				| @ -1,26 +1,52 @@ | ||||
| import { randomBytes } from 'crypto' | ||||
| import { bech32 } from 'bech32' | ||||
| import { AuthenticationError } from 'apollo-server-micro' | ||||
| 
 | ||||
| function encodedUrl (iurl, tag, k1) { | ||||
|   const url = new URL(iurl) | ||||
|   url.searchParams.set('tag', tag) | ||||
|   url.searchParams.set('k1', k1) | ||||
|   // bech32 encode url
 | ||||
|   const words = bech32.toWords(Buffer.from(url.toString(), 'utf8')) | ||||
|   return bech32.encode('lnurl', words, 1023) | ||||
| } | ||||
| 
 | ||||
| function k1 () { | ||||
|   return randomBytes(32).toString('hex') | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
|   Query: { | ||||
|     lnAuth: async (parent, { k1 }, { models }) => { | ||||
|       return await models.lnAuth.findUnique({ where: { k1 } }) | ||||
|     }, | ||||
|     lnWith: async (parent, { k1 }, { models }) => { | ||||
|       return await models.lnWith.findUnique({ where: { k1 } }) | ||||
|     } | ||||
|   }, | ||||
|   Mutation: { | ||||
|     createAuth: async (parent, args, { models }) => { | ||||
|       const k1 = randomBytes(32).toString('hex') | ||||
|       return await models.lnAuth.create({ data: { k1 } }) | ||||
|       return await models.lnAuth.create({ data: { k1: k1() } }) | ||||
|     }, | ||||
|     createWith: async (parent, args, { me, models }) => { | ||||
|       if (!me) { | ||||
|         throw new AuthenticationError('you must be logged in') | ||||
|       } | ||||
| 
 | ||||
|       return await models.lnWith.create({ data: { k1: k1(), userId: me.id } }) | ||||
|     } | ||||
|   }, | ||||
|   LnAuth: { | ||||
|     encodedUrl: async (lnAuth, args, { models }) => { | ||||
|       const url = new URL(process.env.LNAUTH_URL) | ||||
|       url.searchParams.set('tag', 'login') | ||||
|       url.searchParams.set('k1', lnAuth.k1) | ||||
|       // bech32 encode url
 | ||||
|       const words = bech32.toWords(Buffer.from(url.toString(), 'utf8')) | ||||
|       return bech32.encode('lnurl', words, 1023) | ||||
|       return encodedUrl(process.env.LNAUTH_URL, 'login', lnAuth.k1) | ||||
|     } | ||||
|   }, | ||||
|   LnWith: { | ||||
|     encodedUrl: async (lnWith, args, { models }) => { | ||||
|       return encodedUrl(process.env.LNWITH_URL, 'withdrawRequest', lnWith.k1) | ||||
|     }, | ||||
|     user: async (lnWith, args, { models }) => { | ||||
|       return await models.user.findUnique({ where: { id: lnWith.userId } }) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -89,10 +89,6 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|     createWithdrawl: async (parent, { invoice, maxFee }, { me, models, lnd }) => { | ||||
|       if (!me) { | ||||
|         throw new AuthenticationError('you must be logged in') | ||||
|       } | ||||
| 
 | ||||
|       // decode invoice to get amount
 | ||||
|       let decoded | ||||
|       try { | ||||
|  | ||||
| @ -6,8 +6,9 @@ import resolvers from './resolvers' | ||||
| import typeDefs from './typeDefs' | ||||
| import models from './models' | ||||
| import { print } from 'graphql' | ||||
| import lnd from './lnd' | ||||
| 
 | ||||
| export default async function getSSRApolloClient (req) { | ||||
| export default async function getSSRApolloClient (req, me = null) { | ||||
|   const session = req && await getSession({ req }) | ||||
|   return new ApolloClient({ | ||||
|     ssrMode: true, | ||||
| @ -18,7 +19,8 @@ export default async function getSSRApolloClient (req) { | ||||
|       }), | ||||
|       context: { | ||||
|         models, | ||||
|         me: session ? session.user : null | ||||
|         me: session ? session.user : me, | ||||
|         lnd | ||||
|       } | ||||
|     }), | ||||
|     cache: new InMemoryCache() | ||||
|  | ||||
| @ -3,10 +3,12 @@ import { gql } from 'apollo-server-micro' | ||||
| export default gql` | ||||
|   extend type Query { | ||||
|     lnAuth(k1: String!): LnAuth! | ||||
|     lnWith(k1: String!): LnWith! | ||||
|   } | ||||
| 
 | ||||
|   extend type Mutation { | ||||
|     createAuth: LnAuth! | ||||
|     createWith: LnWith! | ||||
|   } | ||||
| 
 | ||||
|   type LnAuth { | ||||
| @ -16,4 +18,13 @@ export default gql` | ||||
|     pubkey: String | ||||
|     encodedUrl: String! | ||||
|   } | ||||
| 
 | ||||
|   type LnWith { | ||||
|     id: ID! | ||||
|     createdAt: String! | ||||
|     k1: String! | ||||
|     user: User! | ||||
|     withdrawalId: Int | ||||
|     encodedUrl: String! | ||||
|   } | ||||
| ` | ||||
|  | ||||
| @ -23,3 +23,10 @@ export const WITHDRAWL = gql` | ||||
|       status | ||||
|     } | ||||
|   }` | ||||
| 
 | ||||
| export const CREATE_WITHDRAWL = gql` | ||||
|   mutation createWithdrawl($invoice: String!, $maxFee: Int!) { | ||||
|     createWithdrawl(invoice: $invoice, maxFee: $maxFee) { | ||||
|       id | ||||
|     } | ||||
| }` | ||||
|  | ||||
							
								
								
									
										71
									
								
								pages/api/lnwith.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								pages/api/lnwith.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | ||||
| // verify k1 exists
 | ||||
| // send back
 | ||||
| import models from '../../api/models' | ||||
| import getSSRApolloClient from '../../api/ssrApollo' | ||||
| import { CREATE_WITHDRAWL } from '../../fragments/wallet' | ||||
| 
 | ||||
| export default async ({ query }, res) => { | ||||
|   if (query.pr) { | ||||
|     return doWithdrawal(query, res) | ||||
|   } | ||||
| 
 | ||||
|   let reason | ||||
|   try { | ||||
|     // TODO: make sure lnwith was recently generated ... or better use a stateless
 | ||||
|     // bearer token to auth user
 | ||||
|     const lnwith = await models.lnWith.findUnique({ where: { k1: query.k1 } }) | ||||
|     if (lnwith) { | ||||
|       const user = await models.user.findUnique({ where: { id: lnwith.userId } }) | ||||
|       if (user) { | ||||
|         return res.status(200).json({ | ||||
|           tag: 'withdrawRequest', // type of LNURL
 | ||||
|           callback: process.env.LNWITH_URL, // The URL which LN SERVICE would accept a withdrawal Lightning invoice as query parameter
 | ||||
|           k1: query.k1, // Random or non-random string to identify the user's LN WALLET when using the callback URL
 | ||||
|           defaultDescription: `Withdrawal for @${user.name} on SN`, // A default withdrawal invoice description
 | ||||
|           minWithdrawable: 1000, // Min amount (in millisatoshis) the user can withdraw from LN SERVICE, or 0
 | ||||
|           maxWithdrawable: user.msats - 10000 // Max amount (in millisatoshis) the user can withdraw from LN SERVICE, or equal to minWithdrawable if the user has no choice over the amounts
 | ||||
|         }) | ||||
|       } else { | ||||
|         reason = 'user not found' | ||||
|       } | ||||
|     } else { | ||||
|       reason = 'withdrawal not found' | ||||
|     } | ||||
|   } catch (error) { | ||||
|     console.log(error) | ||||
|     reason = 'internal server error' | ||||
|   } | ||||
| 
 | ||||
|   return res.status(400).json({ status: 'ERROR', reason }) | ||||
| } | ||||
| 
 | ||||
| async function doWithdrawal (query, res) { | ||||
|   if (!query.k1) { | ||||
|     return res.status(400).json({ status: 'ERROR', reason: 'k1 not provided' }) | ||||
|   } | ||||
| 
 | ||||
|   const lnwith = await models.lnWith.findUnique({ where: { k1: query.k1 } }) | ||||
|   if (!lnwith) { | ||||
|     return res.status(400).json({ status: 'ERROR', reason: 'invalid k1' }) | ||||
|   } | ||||
|   const me = await models.user.findUnique({ where: { id: lnwith.userId } }) | ||||
|   if (!me) { | ||||
|     return res.status(400).json({ status: 'ERROR', reason: 'user not found' }) | ||||
|   } | ||||
| 
 | ||||
|   // create withdrawal in gql
 | ||||
|   const client = await getSSRApolloClient(null, me) | ||||
|   const { error, data } = await client.mutate({ | ||||
|     mutation: CREATE_WITHDRAWL, | ||||
|     variables: { invoice: query.pr, maxFee: 10 } | ||||
|   }) | ||||
| 
 | ||||
|   if (error || !data?.createWithdrawl) { | ||||
|     return res.status(400).json({ status: 'ERROR', reason: error?.toString() || 'could not generate withdrawl' }) | ||||
|   } | ||||
| 
 | ||||
|   // store withdrawal id lnWith so client can show it
 | ||||
|   await models.lnWith.update({ where: { k1: query.k1 }, data: { withdrawalId: Number(data.createWithdrawl.id) } }) | ||||
| 
 | ||||
|   return res.status(200).json({ status: 'OK' }) | ||||
| } | ||||
| @ -3,8 +3,8 @@ import { Form, Input, SubmitButton } from '../components/form' | ||||
| import Link from 'next/link' | ||||
| import Button from 'react-bootstrap/Button' | ||||
| import * as Yup from 'yup' | ||||
| import { gql, useMutation } from '@apollo/client' | ||||
| import { LnQRSkeleton } from '../components/lnqr' | ||||
| import { gql, useMutation, useQuery } from '@apollo/client' | ||||
| import LnQR, { LnQRSkeleton } from '../components/lnqr' | ||||
| import LayoutCenter from '../components/layout-center' | ||||
| import InputGroup from 'react-bootstrap/InputGroup' | ||||
| import { WithdrawlSkeleton } from './withdrawals/[id]' | ||||
| @ -12,6 +12,7 @@ import { useMe } from '../components/me' | ||||
| import { useEffect, useState } from 'react' | ||||
| import { requestProvider } from 'webln' | ||||
| import { Alert } from 'react-bootstrap' | ||||
| import { CREATE_WITHDRAWL } from '../fragments/wallet' | ||||
| 
 | ||||
| export default function Wallet () { | ||||
|   return ( | ||||
| @ -49,8 +50,10 @@ export function WalletForm () { | ||||
| 
 | ||||
|   if (router.query.type === 'fund') { | ||||
|     return <FundForm /> | ||||
|   } else { | ||||
|   } else if (router.query.type === 'withdraw') { | ||||
|     return <WithdrawlForm /> | ||||
|   } else { | ||||
|     return <LnWithdrawal /> | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @ -125,12 +128,7 @@ export function WithdrawlForm () { | ||||
|   const router = useRouter() | ||||
|   const me = useMe() | ||||
| 
 | ||||
|   const [createWithdrawl, { called, error }] = useMutation(gql` | ||||
|     mutation createWithdrawl($invoice: String!, $maxFee: Int!) { | ||||
|       createWithdrawl(invoice: $invoice, maxFee: $maxFee) { | ||||
|         id | ||||
|       } | ||||
|   }`)
 | ||||
|   const [createWithdrawl, { called, error }] = useMutation(CREATE_WITHDRAWL) | ||||
| 
 | ||||
|   useEffect(async () => { | ||||
|     try { | ||||
| @ -153,7 +151,6 @@ export function WithdrawlForm () { | ||||
|     <> | ||||
|       <YouHaveSats /> | ||||
|       <Form | ||||
|         className='pt-3' | ||||
|         initial={{ | ||||
|           invoice: '', | ||||
|           maxFee: MAX_FEE_DEFAULT | ||||
| @ -179,6 +176,49 @@ export function WithdrawlForm () { | ||||
|         /> | ||||
|         <SubmitButton variant='success' className='mt-2'>withdraw</SubmitButton> | ||||
|       </Form> | ||||
|       <span className='my-3 font-weight-bold text-muted'>or</span> | ||||
|       <Link href='/wallet?type=lnurl-withdraw'> | ||||
|         <Button variant='grey'>QR code</Button> | ||||
|       </Link> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| function LnQRWith ({ k1, encodedUrl }) { | ||||
|   const router = useRouter() | ||||
|   const query = gql` | ||||
|   { | ||||
|     lnWith(k1: "${k1}") { | ||||
|       withdrawalId | ||||
|       k1 | ||||
|     } | ||||
|   }` | ||||
|   const { data } = useQuery(query, { pollInterval: 1000, fetchPolicy: 'cache-first' }) | ||||
| 
 | ||||
|   if (data?.lnWith?.withdrawalId) { | ||||
|     router.push(`/withdrawals/${data.lnWith.withdrawalId}`) | ||||
|   } | ||||
| 
 | ||||
|   return <LnQR value={encodedUrl} status='waiting for you' /> | ||||
| } | ||||
| 
 | ||||
| export function LnWithdrawal () { | ||||
|   // query for challenge
 | ||||
|   const [createAuth, { data, error }] = useMutation(gql` | ||||
|     mutation createAuth { | ||||
|       createWith { | ||||
|         k1 | ||||
|         encodedUrl | ||||
|       } | ||||
|     }`)
 | ||||
| 
 | ||||
|   useEffect(createAuth, []) | ||||
| 
 | ||||
|   if (error) return <div>error</div> | ||||
| 
 | ||||
|   if (!data) { | ||||
|     return <LnQRSkeleton status='generating' /> | ||||
|   } | ||||
| 
 | ||||
|   return <LnQRWith {...data.createWith} /> | ||||
| } | ||||
|  | ||||
							
								
								
									
										14
									
								
								prisma/migrations/20211027230800_lnwith/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								prisma/migrations/20211027230800_lnwith/migration.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| -- CreateTable | ||||
| CREATE TABLE "LnWith" ( | ||||
|     "id" SERIAL NOT NULL, | ||||
|     "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "k1" TEXT NOT NULL, | ||||
|     "userId" INTEGER NOT NULL, | ||||
|     "withdrawalId" INTEGER NOT NULL, | ||||
| 
 | ||||
|     PRIMARY KEY ("id") | ||||
| ); | ||||
| 
 | ||||
| -- CreateIndex | ||||
| CREATE UNIQUE INDEX "LnWith.k1_unique" ON "LnWith"("k1"); | ||||
| @ -0,0 +1,2 @@ | ||||
| -- AlterTable | ||||
| ALTER TABLE "LnWith" ALTER COLUMN "withdrawalId" DROP NOT NULL; | ||||
| @ -48,6 +48,15 @@ model LnAuth { | ||||
|   pubkey    String? | ||||
| } | ||||
| 
 | ||||
| model LnWith { | ||||
|   id           Int      @id @default(autoincrement()) | ||||
|   createdAt    DateTime @default(now()) @map(name: "created_at") | ||||
|   updatedAt    DateTime @default(now()) @updatedAt @map(name: "updated_at") | ||||
|   k1           String   @unique | ||||
|   userId       Int | ||||
|   withdrawalId Int? | ||||
| } | ||||
| 
 | ||||
| model Invite { | ||||
|   id        String   @id @default(cuid()) | ||||
|   createdAt DateTime @default(now()) @map(name: "created_at") | ||||
|  | ||||
| @ -5,7 +5,8 @@ $theme-colors: ( | ||||
|   "info" : #007cbe, | ||||
|   "success" : #5c8001, | ||||
|   "twitter" : #1da1f2, | ||||
|   "boost" : #8c25f4 | ||||
|   "boost" : #8c25f4, | ||||
|   "grey" : #e9ecef | ||||
| ); | ||||
| 
 | ||||
| $body-bg: #f5f5f5; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user