send to lightning address

This commit is contained in:
keyan 2022-01-23 11:21:55 -06:00
parent 5776096eb1
commit e37475f927
7 changed files with 140 additions and 29 deletions

View File

@ -189,35 +189,30 @@ export default {
throw error
}
},
createWithdrawl: async (parent, { invoice, maxFee }, { me, models, lnd }) => {
// decode invoice to get amount
let decoded
try {
decoded = await decodePaymentRequest({ lnd, request: invoice })
} catch (error) {
throw new UserInputError('could not decode invoice')
createWithdrawl: createWithdrawal,
sendToLnAddr: async (parent, { addr, amount, maxFee }, { me, models, lnd }) => {
const [name, domain] = addr.split('@')
const res1 = await (await fetch(`https://${domain}/.well-known/lnurlp/${name}`)).json()
if (res1.status === 'ERROR') {
throw new Error(res1.reason)
}
if (!decoded.mtokens || Number(decoded.mtokens) <= 0) {
throw new UserInputError('you must specify amount')
const milliamount = amount * 1000
// check that amount is within min and max sendable
if (milliamount < res1.minSendable || milliamount > res1.maxSendable) {
throw new UserInputError(
`amount must be >= ${res1.minSendable / 1000} and <= ${res1.maxSendable / 1000}`,
{ argumentName: 'amount' })
}
const msatsFee = Number(maxFee) * 1000
// call callback with amount
const res2 = await (await fetch(`${res1.callback}?amount=${milliamount}`)).json()
if (res2.status === 'ERROR') {
throw new Error(res2.reason)
}
// create withdrawl transactionally (id, bolt11, amount, fee)
const [withdrawl] = await serialize(models,
models.$queryRaw`SELECT * FROM create_withdrawl(${decoded.id}, ${invoice},
${Number(decoded.mtokens)}, ${msatsFee}, ${me.name})`)
payViaPaymentRequest({
lnd,
request: invoice,
// can't use max_fee_mtokens https://github.com/alexbosworth/ln-service/issues/141
max_fee: Number(maxFee),
pathfinding_timeout: 30000
})
return withdrawl
// take pr and createWithdrawl
return await createWithdrawal(parent, { invoice: res2.pr, maxFee }, { me, models, lnd })
}
},
@ -242,3 +237,35 @@ export default {
}
}
}
async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd }) {
// decode invoice to get amount
let decoded
try {
decoded = await decodePaymentRequest({ lnd, request: invoice })
} catch (error) {
console.log(error)
throw new UserInputError('could not decode invoice')
}
if (!decoded.mtokens || Number(decoded.mtokens) <= 0) {
throw new UserInputError('you must specify amount')
}
const msatsFee = Number(maxFee) * 1000
// create withdrawl transactionally (id, bolt11, amount, fee)
const [withdrawl] = await serialize(models,
models.$queryRaw`SELECT * FROM create_withdrawl(${decoded.id}, ${invoice},
${Number(decoded.mtokens)}, ${msatsFee}, ${me.name})`)
payViaPaymentRequest({
lnd,
request: invoice,
// can't use max_fee_mtokens https://github.com/alexbosworth/ln-service/issues/141
max_fee: Number(maxFee),
pathfinding_timeout: 30000
})
return withdrawl
}

View File

@ -11,6 +11,13 @@ export default gql`
extend type Mutation {
createInvoice(amount: Int!): Invoice!
createWithdrawl(invoice: String!, maxFee: Int!): Withdrawl!
sendToLnAddr(addr: String!, amount: Int!, maxFee: Int!): Withdrawl!
}
type LnAddrResp {
callback: String!
maxSendable: String!
minSendable: String!
}
type Invoice {

View File

@ -56,11 +56,15 @@ export function CopyInput (props) {
)
}
export function InputSkeleton ({ label }) {
export function InputSkeleton ({ label, hint }) {
return (
<BootstrapForm.Group>
{label && <BootstrapForm.Label>{label}</BootstrapForm.Label>}
<div className='form-control clouds' />
{hint &&
<BootstrapForm.Text>
{hint}
</BootstrapForm.Text>}
</BootstrapForm.Group>
)
}

View File

@ -56,3 +56,10 @@ export const CREATE_WITHDRAWL = gql`
id
}
}`
export const SEND_TO_LNADDR = gql`
mutation sendToLnAddr($addr: String!, $amount: Int!, $maxFee: Int!) {
sendToLnAddr(addr: $addr, amount: $amount, maxFee: $maxFee) {
id
}
}`

View File

@ -8,7 +8,7 @@ export default async ({ query: { username } }, res) => {
}
return res.status(200).json({
callback: `https://stacker.news/api/lnurlp/${username}/pay`, // The URL from LN SERVICE which will accept the pay request parameters
callback: `${process.env.SELF_URL}/api/lnurlp/${username}/pay`, // The URL from LN SERVICE which will accept the pay request parameters
minSendable: 1000, // Min amount LN SERVICE is willing to receive, can not be less than 1 or more than `maxSendable`
maxSendable: Number.MAX_SAFE_INTEGER,
metadata: lnurlPayMetadataString(username), // Metadata json which must be presented as raw string here, this is required to pass signature verification at a later step

View File

@ -12,7 +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'
import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '../fragments/wallet'
export default function Wallet () {
return (
@ -64,8 +64,10 @@ export function WalletForm () {
return <FundForm />
} else if (router.query.type === 'withdraw') {
return <WithdrawlForm />
} else {
} else if (router.query.type === 'lnurl-withdraw') {
return <LnWithdrawal />
} else {
return <LnAddrWithdrawal />
}
}
@ -189,10 +191,13 @@ export function WithdrawlForm () {
/>
<SubmitButton variant='success' className='mt-2'>withdraw</SubmitButton>
</Form>
<span className='my-3 font-weight-bold text-muted'>or</span>
<span className='my-3 font-weight-bold text-muted'>or via</span>
<Link href='/wallet?type=lnurl-withdraw'>
<Button variant='grey'>QR code</Button>
</Link>
<Link href='/wallet?type=lnaddr-withdraw'>
<Button className='mt-2' variant='grey'>Lightning Address</Button>
</Link>
<WalletHistory />
</>
)
@ -236,3 +241,60 @@ export function LnWithdrawal () {
return <LnQRWith {...data.createWith} />
}
export const LnAddrSchema = Yup.object({
// addr: Yup.string().email('address is no good').required('required'),
amount: Yup.number().typeError('must be a number').required('required')
.positive('must be positive').integer('must be whole'),
maxFee: Yup.number().typeError('must be a number').required('required')
.min(0, 'must be positive').integer('must be whole')
})
export function LnAddrWithdrawal () {
const router = useRouter()
const [sendToLnAddr, { called, error }] = useMutation(SEND_TO_LNADDR)
if (called && !error) {
return <WithdrawlSkeleton status='sending' />
}
return (
<>
<YouHaveSats />
<Form
initial={{
addr: '',
amount: 1,
maxFee: 10
}}
schema={LnAddrSchema}
initialError={error ? error.toString() : undefined}
onSubmit={async ({ addr, amount, maxFee }) => {
const { data } = await sendToLnAddr({ variables: { addr, amount: Number(amount), maxFee } })
router.push(`/withdrawals/${data.sendToLnAddr.id}`)
}}
>
<Input
label='lightning address'
name='addr'
required
autoFocus
/>
<Input
label='amount'
name='amount'
required
autoFocus
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
<Input
label='max fee'
name='maxFee'
required
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
<SubmitButton variant='success' className='mt-2'>send</SubmitButton>
</Form>
</>
)
}

View File

@ -341,6 +341,10 @@ textarea.form-control {
fill: grey;
}
.fill-white {
fill: white;
}
.fill-success {
fill: #5c8001;
}