send to lightning address
This commit is contained in:
parent
5776096eb1
commit
e37475f927
@ -189,35 +189,30 @@ export default {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createWithdrawl: async (parent, { invoice, maxFee }, { me, models, lnd }) => {
|
createWithdrawl: createWithdrawal,
|
||||||
// decode invoice to get amount
|
sendToLnAddr: async (parent, { addr, amount, maxFee }, { me, models, lnd }) => {
|
||||||
let decoded
|
const [name, domain] = addr.split('@')
|
||||||
try {
|
const res1 = await (await fetch(`https://${domain}/.well-known/lnurlp/${name}`)).json()
|
||||||
decoded = await decodePaymentRequest({ lnd, request: invoice })
|
if (res1.status === 'ERROR') {
|
||||||
} catch (error) {
|
throw new Error(res1.reason)
|
||||||
throw new UserInputError('could not decode invoice')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!decoded.mtokens || Number(decoded.mtokens) <= 0) {
|
const milliamount = amount * 1000
|
||||||
throw new UserInputError('you must specify amount')
|
// 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)
|
// take pr and createWithdrawl
|
||||||
const [withdrawl] = await serialize(models,
|
return await createWithdrawal(parent, { invoice: res2.pr, maxFee }, { me, models, lnd })
|
||||||
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
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -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 {
|
extend type Mutation {
|
||||||
createInvoice(amount: Int!): Invoice!
|
createInvoice(amount: Int!): Invoice!
|
||||||
createWithdrawl(invoice: String!, maxFee: Int!): Withdrawl!
|
createWithdrawl(invoice: String!, maxFee: Int!): Withdrawl!
|
||||||
|
sendToLnAddr(addr: String!, amount: Int!, maxFee: Int!): Withdrawl!
|
||||||
|
}
|
||||||
|
|
||||||
|
type LnAddrResp {
|
||||||
|
callback: String!
|
||||||
|
maxSendable: String!
|
||||||
|
minSendable: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Invoice {
|
type Invoice {
|
||||||
|
@ -56,11 +56,15 @@ export function CopyInput (props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InputSkeleton ({ label }) {
|
export function InputSkeleton ({ label, hint }) {
|
||||||
return (
|
return (
|
||||||
<BootstrapForm.Group>
|
<BootstrapForm.Group>
|
||||||
{label && <BootstrapForm.Label>{label}</BootstrapForm.Label>}
|
{label && <BootstrapForm.Label>{label}</BootstrapForm.Label>}
|
||||||
<div className='form-control clouds' />
|
<div className='form-control clouds' />
|
||||||
|
{hint &&
|
||||||
|
<BootstrapForm.Text>
|
||||||
|
{hint}
|
||||||
|
</BootstrapForm.Text>}
|
||||||
</BootstrapForm.Group>
|
</BootstrapForm.Group>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -56,3 +56,10 @@ export const CREATE_WITHDRAWL = gql`
|
|||||||
id
|
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({
|
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`
|
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,
|
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
|
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 { useEffect, useState } from 'react'
|
||||||
import { requestProvider } from 'webln'
|
import { requestProvider } from 'webln'
|
||||||
import { Alert } from 'react-bootstrap'
|
import { Alert } from 'react-bootstrap'
|
||||||
import { CREATE_WITHDRAWL } from '../fragments/wallet'
|
import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '../fragments/wallet'
|
||||||
|
|
||||||
export default function Wallet () {
|
export default function Wallet () {
|
||||||
return (
|
return (
|
||||||
@ -64,8 +64,10 @@ export function WalletForm () {
|
|||||||
return <FundForm />
|
return <FundForm />
|
||||||
} else if (router.query.type === 'withdraw') {
|
} else if (router.query.type === 'withdraw') {
|
||||||
return <WithdrawlForm />
|
return <WithdrawlForm />
|
||||||
} else {
|
} else if (router.query.type === 'lnurl-withdraw') {
|
||||||
return <LnWithdrawal />
|
return <LnWithdrawal />
|
||||||
|
} else {
|
||||||
|
return <LnAddrWithdrawal />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,10 +191,13 @@ export function WithdrawlForm () {
|
|||||||
/>
|
/>
|
||||||
<SubmitButton variant='success' className='mt-2'>withdraw</SubmitButton>
|
<SubmitButton variant='success' className='mt-2'>withdraw</SubmitButton>
|
||||||
</Form>
|
</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'>
|
<Link href='/wallet?type=lnurl-withdraw'>
|
||||||
<Button variant='grey'>QR code</Button>
|
<Button variant='grey'>QR code</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href='/wallet?type=lnaddr-withdraw'>
|
||||||
|
<Button className='mt-2' variant='grey'>Lightning Address</Button>
|
||||||
|
</Link>
|
||||||
<WalletHistory />
|
<WalletHistory />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@ -236,3 +241,60 @@ export function LnWithdrawal () {
|
|||||||
|
|
||||||
return <LnQRWith {...data.createWith} />
|
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: grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fill-white {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
.fill-success {
|
.fill-success {
|
||||||
fill: #5c8001;
|
fill: #5c8001;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user