send to lightning address
This commit is contained in:
parent
5776096eb1
commit
e37475f927
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -341,6 +341,10 @@ textarea.form-control {
|
|||
fill: grey;
|
||||
}
|
||||
|
||||
.fill-white {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.fill-success {
|
||||
fill: #5c8001;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue