Implementing LUD-12 comments on payRequest in LNURLP Lightning Address flow (sending and receiving) (#498)
* Prototype implementing LUD-12 comments on payRequest in LNURLP Lightning Address flow * Support sending comment when withdrawing to ln addr (LUD-12) * Prevent `initialError` from being toasted informs multiple times * delete the old create_invoice function * improve lightning addr withdrawal styling * allow lnaddr comment to show up in notifications * enhance satistics --------- Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
This commit is contained in:
parent
0eb28ef020
commit
d60a589bc0
|
@ -91,6 +91,8 @@ export default {
|
||||||
WHEN "expiresAt" <= $2 THEN 'EXPIRED'
|
WHEN "expiresAt" <= $2 THEN 'EXPIRED'
|
||||||
WHEN cancelled THEN 'CANCELLED'
|
WHEN cancelled THEN 'CANCELLED'
|
||||||
ELSE 'PENDING' END as status,
|
ELSE 'PENDING' END as status,
|
||||||
|
"desc" as description,
|
||||||
|
comment as "invoiceComment",
|
||||||
'invoice' as type
|
'invoice' as type
|
||||||
FROM "Invoice"
|
FROM "Invoice"
|
||||||
WHERE "userId" = $1
|
WHERE "userId" = $1
|
||||||
|
@ -105,6 +107,8 @@ export default {
|
||||||
CASE WHEN status = 'CONFIRMED' THEN "msatsFeePaid"
|
CASE WHEN status = 'CONFIRMED' THEN "msatsFeePaid"
|
||||||
ELSE "msatsFeePaying" END as "msatsFee",
|
ELSE "msatsFeePaying" END as "msatsFee",
|
||||||
COALESCE(status::text, 'PENDING') as status,
|
COALESCE(status::text, 'PENDING') as status,
|
||||||
|
NULL as description,
|
||||||
|
NULL as "invoiceComment",
|
||||||
'withdrawal' as type
|
'withdrawal' as type
|
||||||
FROM "Withdrawl"
|
FROM "Withdrawl"
|
||||||
WHERE "userId" = $1
|
WHERE "userId" = $1
|
||||||
|
@ -129,6 +133,8 @@ export default {
|
||||||
) AS "msats",
|
) AS "msats",
|
||||||
0 AS "msatsFee",
|
0 AS "msatsFee",
|
||||||
NULL AS status,
|
NULL AS status,
|
||||||
|
NULL as description,
|
||||||
|
NULL as "invoiceComment",
|
||||||
'stacked' AS type
|
'stacked' AS type
|
||||||
FROM "ItemAct"
|
FROM "ItemAct"
|
||||||
JOIN "Item" ON "ItemAct"."itemId" = "Item".id
|
JOIN "Item" ON "ItemAct"."itemId" = "Item".id
|
||||||
|
@ -142,14 +148,14 @@ export default {
|
||||||
queries.push(
|
queries.push(
|
||||||
`(SELECT ('earn' || min("Earn".id)) as id, min("Earn".id) as "factId", NULL as bolt11,
|
`(SELECT ('earn' || min("Earn".id)) as id, min("Earn".id) as "factId", NULL as bolt11,
|
||||||
created_at as "createdAt", sum(msats),
|
created_at as "createdAt", sum(msats),
|
||||||
0 as "msatsFee", NULL as status, 'earn' as type
|
0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", 'earn' as type
|
||||||
FROM "Earn"
|
FROM "Earn"
|
||||||
WHERE "Earn"."userId" = $1 AND "Earn".created_at <= $2
|
WHERE "Earn"."userId" = $1 AND "Earn".created_at <= $2
|
||||||
GROUP BY "userId", created_at)`)
|
GROUP BY "userId", created_at)`)
|
||||||
queries.push(
|
queries.push(
|
||||||
`(SELECT ('referral' || "ReferralAct".id) as id, "ReferralAct".id as "factId", NULL as bolt11,
|
`(SELECT ('referral' || "ReferralAct".id) as id, "ReferralAct".id as "factId", NULL as bolt11,
|
||||||
created_at as "createdAt", msats,
|
created_at as "createdAt", msats,
|
||||||
0 as "msatsFee", NULL as status, 'referral' as type
|
0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", 'referral' as type
|
||||||
FROM "ReferralAct"
|
FROM "ReferralAct"
|
||||||
WHERE "ReferralAct"."referrerId" = $1 AND "ReferralAct".created_at <= $2)`)
|
WHERE "ReferralAct"."referrerId" = $1 AND "ReferralAct".created_at <= $2)`)
|
||||||
}
|
}
|
||||||
|
@ -158,7 +164,7 @@ export default {
|
||||||
queries.push(
|
queries.push(
|
||||||
`(SELECT ('spent' || "Item".id) as id, "Item".id as "factId", NULL as bolt11,
|
`(SELECT ('spent' || "Item".id) as id, "Item".id as "factId", NULL as bolt11,
|
||||||
MAX("ItemAct".created_at) as "createdAt", sum("ItemAct".msats) as msats,
|
MAX("ItemAct".created_at) as "createdAt", sum("ItemAct".msats) as msats,
|
||||||
0 as "msatsFee", NULL as status, 'spent' as type
|
0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", 'spent' as type
|
||||||
FROM "ItemAct"
|
FROM "ItemAct"
|
||||||
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
||||||
WHERE "ItemAct"."userId" = $1
|
WHERE "ItemAct"."userId" = $1
|
||||||
|
@ -167,7 +173,7 @@ export default {
|
||||||
queries.push(
|
queries.push(
|
||||||
`(SELECT ('donation' || "Donation".id) as id, "Donation".id as "factId", NULL as bolt11,
|
`(SELECT ('donation' || "Donation".id) as id, "Donation".id as "factId", NULL as bolt11,
|
||||||
created_at as "createdAt", sats * 1000 as msats,
|
created_at as "createdAt", sats * 1000 as msats,
|
||||||
0 as "msatsFee", NULL as status, 'donation' as type
|
0 as "msatsFee", NULL as status, NULL as description, NULL as "invoiceComment", 'donation' as type
|
||||||
FROM "Donation"
|
FROM "Donation"
|
||||||
WHERE "userId" = $1
|
WHERE "userId" = $1
|
||||||
AND created_at <= $2)`)
|
AND created_at <= $2)`)
|
||||||
|
@ -193,6 +199,7 @@ export default {
|
||||||
const { tags } = inv
|
const { tags } = inv
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
if (tag.tagName === 'description') {
|
if (tag.tagName === 'description') {
|
||||||
|
// prioritize description from bolt11 over description from our DB
|
||||||
f.description = tag.data
|
f.description = tag.data
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -252,7 +259,7 @@ export default {
|
||||||
|
|
||||||
const [inv] = await serialize(models,
|
const [inv] = await serialize(models,
|
||||||
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
|
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
|
||||||
${expiresAt}::timestamp, ${amount * 1000}, ${user.id}::INTEGER, ${description},
|
${expiresAt}::timestamp, ${amount * 1000}, ${user.id}::INTEGER, ${description}, NULL,
|
||||||
${invLimit}::INTEGER, ${balanceLimit})`)
|
${invLimit}::INTEGER, ${balanceLimit})`)
|
||||||
|
|
||||||
if (hodlInvoice) await models.invoice.update({ where: { hash: invoice.id }, data: { preimage: invoice.secret } })
|
if (hodlInvoice) await models.invoice.update({ where: { hash: invoice.id }, data: { preimage: invoice.secret } })
|
||||||
|
@ -269,8 +276,8 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createWithdrawl: createWithdrawal,
|
createWithdrawl: createWithdrawal,
|
||||||
sendToLnAddr: async (parent, { addr, amount, maxFee }, { me, models, lnd }) => {
|
sendToLnAddr: async (parent, { addr, amount, maxFee, comment }, { me, models, lnd }) => {
|
||||||
await ssValidate(lnAddrSchema, { addr, amount, maxFee })
|
await ssValidate(lnAddrSchema, { addr, amount, maxFee, comment })
|
||||||
|
|
||||||
const [name, domain] = addr.split('@')
|
const [name, domain] = addr.split('@')
|
||||||
let req
|
let req
|
||||||
|
@ -291,10 +298,26 @@ export default {
|
||||||
throw new GraphQLError(`amount must be >= ${res1.minSendable / 1000} and <= ${res1.maxSendable / 1000}`, { extensions: { code: 'BAD_INPUT' } })
|
throw new GraphQLError(`amount must be >= ${res1.minSendable / 1000} and <= ${res1.maxSendable / 1000}`, { extensions: { code: 'BAD_INPUT' } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if a comment is provided by the user
|
||||||
|
if (comment?.length) {
|
||||||
|
// if the receiving address doesn't accept comments, reject the request and tell the user why
|
||||||
|
if (res1.commentAllowed === undefined) {
|
||||||
|
throw new GraphQLError('comments are not accepted by this lightning address provider', { extensions: { code: 'BAD_INPUT' } })
|
||||||
|
}
|
||||||
|
// if the receiving address accepts comments, verify the max length isn't violated
|
||||||
|
if (res1.commentAllowed && Number(res1.commentAllowed) && comment.length > Number(res1.commentAllowed)) {
|
||||||
|
throw new GraphQLError(`comments sent to this lightning address provider must not exceed ${res1.commentAllowed} characters in length`, { extensions: { code: 'BAD_INPUT' } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const callback = new URL(res1.callback)
|
const callback = new URL(res1.callback)
|
||||||
callback.searchParams.append('amount', milliamount)
|
callback.searchParams.append('amount', milliamount)
|
||||||
|
|
||||||
// call callback with amount
|
if (comment?.length) {
|
||||||
|
callback.searchParams.append('comment', comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// call callback with amount and conditionally comment
|
||||||
const res2 = await (await fetch(callback.toString())).json()
|
const res2 = await (await fetch(callback.toString())).json()
|
||||||
if (res2.status === 'ERROR') {
|
if (res2.status === 'ERROR') {
|
||||||
throw new Error(res2.reason)
|
throw new Error(res2.reason)
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default gql`
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createInvoice(amount: Int!, expireSecs: Int, hodlInvoice: Boolean): Invoice!
|
createInvoice(amount: Int!, expireSecs: Int, hodlInvoice: Boolean): Invoice!
|
||||||
createWithdrawl(invoice: String!, maxFee: Int!): Withdrawl!
|
createWithdrawl(invoice: String!, maxFee: Int!): Withdrawl!
|
||||||
sendToLnAddr(addr: String!, amount: Int!, maxFee: Int!): Withdrawl!
|
sendToLnAddr(addr: String!, amount: Int!, maxFee: Int!, comment: String): Withdrawl!
|
||||||
cancelInvoice(hash: String!, hmac: String!): Invoice!
|
cancelInvoice(hash: String!, hmac: String!): Invoice!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ export default gql`
|
||||||
satsReceived: Int
|
satsReceived: Int
|
||||||
satsRequested: Int!
|
satsRequested: Int!
|
||||||
nostr: JSONObject
|
nostr: JSONObject
|
||||||
|
comment: String
|
||||||
hmac: String
|
hmac: String
|
||||||
isHeld: Boolean
|
isHeld: Boolean
|
||||||
}
|
}
|
||||||
|
@ -53,6 +54,7 @@ export default gql`
|
||||||
type: String!
|
type: String!
|
||||||
description: String
|
description: String
|
||||||
item: Item
|
item: Item
|
||||||
|
invoiceComment: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type History {
|
type History {
|
||||||
|
|
|
@ -486,9 +486,11 @@ export function Form ({
|
||||||
initial, schema, onSubmit, children, initialError, validateImmediately, storageKeyPrefix, validateOnChange = true, invoiceable, ...props
|
initial, schema, onSubmit, children, initialError, validateImmediately, storageKeyPrefix, validateOnChange = true, invoiceable, ...props
|
||||||
}) {
|
}) {
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
const initialErrorToasted = useRef(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialError) {
|
if (initialError && !initialErrorToasted) {
|
||||||
toaster.danger(initialError.message || initialError.toString?.())
|
toaster.danger(initialError.message || initialError.toString?.())
|
||||||
|
initialErrorToasted.current = true
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|
|
@ -241,6 +241,7 @@ function InvoicePaid ({ n }) {
|
||||||
<div className='fw-bold text-info ms-2 py-1'>
|
<div className='fw-bold text-info ms-2 py-1'>
|
||||||
<Check className='fill-info me-1' />{numWithUnits(n.earnedSats, { abbreviate: false })} were deposited in your account
|
<Check className='fill-info me-1' />{numWithUnits(n.earnedSats, { abbreviate: false })} were deposited in your account
|
||||||
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||||
|
{n.invoice.comment && <small className='d-block ms-4 ps-1 mt-1 mb-1 text-muted fw-normal'><Text>{n.invoice.comment}</Text></small>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,6 +99,7 @@ export const NOTIFICATIONS = gql`
|
||||||
invoice {
|
invoice {
|
||||||
id
|
id
|
||||||
nostr
|
nostr
|
||||||
|
comment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ export const WALLET_HISTORY = gql`
|
||||||
status
|
status
|
||||||
type
|
type
|
||||||
description
|
description
|
||||||
|
invoiceComment
|
||||||
item {
|
item {
|
||||||
...ItemFullFields
|
...ItemFullFields
|
||||||
}
|
}
|
||||||
|
@ -61,8 +62,8 @@ export const CREATE_WITHDRAWL = gql`
|
||||||
}`
|
}`
|
||||||
|
|
||||||
export const SEND_TO_LNADDR = gql`
|
export const SEND_TO_LNADDR = gql`
|
||||||
mutation sendToLnAddr($addr: String!, $amount: Int!, $maxFee: Int!) {
|
mutation sendToLnAddr($addr: String!, $amount: Int!, $maxFee: Int!, $comment: String) {
|
||||||
sendToLnAddr(addr: $addr, amount: $amount, maxFee: $maxFee) {
|
sendToLnAddr(addr: $addr, amount: $amount, maxFee: $maxFee, comment: $comment) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
|
@ -52,5 +52,6 @@ module.exports = {
|
||||||
ANON_POST_FEE: 1000,
|
ANON_POST_FEE: 1000,
|
||||||
ANON_COMMENT_FEE: 100,
|
ANON_COMMENT_FEE: 100,
|
||||||
SSR: typeof window === 'undefined',
|
SSR: typeof window === 'undefined',
|
||||||
MAX_FORWARDS: 5
|
MAX_FORWARDS: 5,
|
||||||
|
LNURLP_COMMENT_MAX_LENGTH: 1000
|
||||||
}
|
}
|
||||||
|
|
|
@ -242,7 +242,8 @@ export const withdrawlSchema = object({
|
||||||
export const lnAddrSchema = object({
|
export const lnAddrSchema = object({
|
||||||
addr: string().email('address is no good').required('required'),
|
addr: string().email('address is no good').required('required'),
|
||||||
amount: intValidator.required('required').positive('must be positive'),
|
amount: intValidator.required('required').positive('must be positive'),
|
||||||
maxFee: intValidator.required('required').min(0, 'must be at least 0')
|
maxFee: intValidator.required('required').min(0, 'must be at least 0'),
|
||||||
|
comment: string()
|
||||||
})
|
})
|
||||||
|
|
||||||
export const bioSchema = object({
|
export const bioSchema = object({
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { getPublicKey } from 'nostr'
|
import { getPublicKey } from 'nostr'
|
||||||
import models from '../../../../api/models'
|
import models from '../../../../api/models'
|
||||||
import { lnurlPayMetadataString } from '../../../../lib/lnurl'
|
import { lnurlPayMetadataString } from '../../../../lib/lnurl'
|
||||||
|
import { LNURLP_COMMENT_MAX_LENGTH } from '../../../../lib/constants'
|
||||||
|
|
||||||
export default async ({ query: { username } }, res) => {
|
export default async ({ query: { username } }, res) => {
|
||||||
const user = await models.user.findUnique({ where: { name: username } })
|
const user = await models.user.findUnique({ where: { name: username } })
|
||||||
|
@ -13,8 +14,9 @@ export default async ({ query: { username } }, res) => {
|
||||||
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: 1000000000,
|
maxSendable: 1000000000,
|
||||||
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
|
||||||
|
commentAllowed: LNURLP_COMMENT_MAX_LENGTH, // LUD-12 Comments for payRequests https://github.com/lnurl/luds/blob/luds/12.md
|
||||||
tag: 'payRequest', // Type of LNURL
|
tag: 'payRequest', // Type of LNURL
|
||||||
nostrPubkey: getPublicKey(process.env.NOSTR_PRIVATE_KEY),
|
nostrPubkey: process.env.NOSTR_PRIVATE_KEY ? getPublicKey(process.env.NOSTR_PRIVATE_KEY) : undefined,
|
||||||
allowsNostr: true
|
allowsNostr: !!process.env.NOSTR_PRIVATE_KEY
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,9 @@ import serialize from '../../../../api/resolvers/serial'
|
||||||
import { schnorr } from '@noble/curves/secp256k1'
|
import { schnorr } from '@noble/curves/secp256k1'
|
||||||
import { createHash } from 'crypto'
|
import { createHash } from 'crypto'
|
||||||
import { datePivot } from '../../../../lib/time'
|
import { datePivot } from '../../../../lib/time'
|
||||||
import { BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT } from '../../../../lib/constants'
|
import { BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT, LNURLP_COMMENT_MAX_LENGTH } from '../../../../lib/constants'
|
||||||
|
|
||||||
export default async ({ query: { username, amount, nostr } }, res) => {
|
export default async ({ query: { username, amount, nostr, comment } }, res) => {
|
||||||
const user = await models.user.findUnique({ where: { name: username } })
|
const user = await models.user.findUnique({ where: { name: username } })
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` })
|
return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` })
|
||||||
|
@ -37,6 +37,10 @@ export default async ({ query: { username, amount, nostr } }, res) => {
|
||||||
return res.status(400).json({ status: 'ERROR', reason: 'amount must be >=1000 msats' })
|
return res.status(400).json({ status: 'ERROR', reason: 'amount must be >=1000 msats' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (comment && comment.length > LNURLP_COMMENT_MAX_LENGTH) {
|
||||||
|
return res.status(400).json({ status: 'ERROR', reason: `comment cannot exceed ${LNURLP_COMMENT_MAX_LENGTH} characters in length` })
|
||||||
|
}
|
||||||
|
|
||||||
// generate invoice
|
// generate invoice
|
||||||
const expiresAt = datePivot(new Date(), { minutes: 1 })
|
const expiresAt = datePivot(new Date(), { minutes: 1 })
|
||||||
const invoice = await createInvoice({
|
const invoice = await createInvoice({
|
||||||
|
@ -50,7 +54,7 @@ export default async ({ query: { username, amount, nostr } }, res) => {
|
||||||
await serialize(models,
|
await serialize(models,
|
||||||
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
|
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
|
||||||
${expiresAt}::timestamp, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description},
|
${expiresAt}::timestamp, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description},
|
||||||
${INV_PENDING_LIMIT}::INTEGER, ${BALANCE_LIMIT_MSATS})`)
|
${comment || null}, ${INV_PENDING_LIMIT}::INTEGER, ${BALANCE_LIMIT_MSATS})`)
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
pr: invoice.request,
|
pr: invoice.request,
|
||||||
|
|
|
@ -111,7 +111,9 @@ function Detail ({ fact }) {
|
||||||
return (
|
return (
|
||||||
<div className='px-3'>
|
<div className='px-3'>
|
||||||
<Link className={satusClass(fact.status)} href={`/${fact.type}s/${fact.factId}`}>
|
<Link className={satusClass(fact.status)} href={`/${fact.type}s/${fact.factId}`}>
|
||||||
{fact.description || 'no invoice description'}
|
{fact.description && <span className='d-block'>{fact.description}</span>}
|
||||||
|
{fact.invoiceComment && <small className='text-muted'>sender says: {fact.invoiceComment}</small>}
|
||||||
|
{!fact.invoiceComment && !fact.description && <span className='d-block'>no description</span>}
|
||||||
<Satus status={fact.status} />
|
<Satus status={fact.status} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -304,12 +304,13 @@ export function LnAddrWithdrawal () {
|
||||||
initial={{
|
initial={{
|
||||||
addr: '',
|
addr: '',
|
||||||
amount: 1,
|
amount: 1,
|
||||||
maxFee: 10
|
maxFee: 10,
|
||||||
|
comment: ''
|
||||||
}}
|
}}
|
||||||
schema={lnAddrSchema}
|
schema={lnAddrSchema}
|
||||||
initialError={error ? error.toString() : undefined}
|
initialError={error ? error.toString() : undefined}
|
||||||
onSubmit={async ({ addr, amount, maxFee }) => {
|
onSubmit={async ({ addr, amount, maxFee, comment }) => {
|
||||||
const { data } = await sendToLnAddr({ variables: { addr, amount: Number(amount), maxFee: Number(maxFee) } })
|
const { data } = await sendToLnAddr({ variables: { addr, amount: Number(amount), maxFee: Number(maxFee), comment } })
|
||||||
router.push(`/withdrawals/${data.sendToLnAddr.id}`)
|
router.push(`/withdrawals/${data.sendToLnAddr.id}`)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -323,7 +324,6 @@ export function LnAddrWithdrawal () {
|
||||||
label='amount'
|
label='amount'
|
||||||
name='amount'
|
name='amount'
|
||||||
required
|
required
|
||||||
autoFocus
|
|
||||||
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
@ -332,6 +332,11 @@ export function LnAddrWithdrawal () {
|
||||||
required
|
required
|
||||||
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
label={<>comment <small className='text-muted ms-2'>optional</small></>}
|
||||||
|
name='comment'
|
||||||
|
hint='only certain lightning addresses can accept comments, and only of a certain length'
|
||||||
|
/>
|
||||||
<SubmitButton variant='success' className='mt-2'>send</SubmitButton>
|
<SubmitButton variant='success' className='mt-2'>send</SubmitButton>
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Invoice" ADD COLUMN "comment" TEXT;
|
||||||
|
|
||||||
|
-- Add comment parameter to invoice creation
|
||||||
|
CREATE OR REPLACE FUNCTION create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone,
|
||||||
|
msats_req BIGINT, user_id INTEGER, idesc TEXT, comment TEXT, inv_limit INTEGER, balance_limit_msats BIGINT)
|
||||||
|
RETURNS "Invoice"
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
invoice "Invoice";
|
||||||
|
inv_limit_reached BOOLEAN;
|
||||||
|
balance_limit_reached BOOLEAN;
|
||||||
|
inv_pending_msats BIGINT;
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
|
||||||
|
-- prevent too many pending invoices
|
||||||
|
SELECT inv_limit > 0 AND count(*) >= inv_limit, sum("msatsRequested") INTO inv_limit_reached, inv_pending_msats
|
||||||
|
FROM "Invoice"
|
||||||
|
WHERE "userId" = user_id AND "expiresAt" > now_utc() AND "confirmedAt" IS NULL AND cancelled = false;
|
||||||
|
|
||||||
|
IF inv_limit_reached THEN
|
||||||
|
RAISE EXCEPTION 'SN_INV_PENDING_LIMIT';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- prevent pending invoices + msats from exceeding the limit
|
||||||
|
SELECT balance_limit_msats > 0 AND inv_pending_msats+msats_req+msats > balance_limit_msats INTO balance_limit_reached
|
||||||
|
FROM users
|
||||||
|
WHERE id = user_id;
|
||||||
|
|
||||||
|
IF balance_limit_reached THEN
|
||||||
|
RAISE EXCEPTION 'SN_INV_EXCEED_BALANCE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- we good, proceed frens
|
||||||
|
INSERT INTO "Invoice" (hash, bolt11, "expiresAt", "msatsRequested", "userId", created_at, updated_at, "desc", comment)
|
||||||
|
VALUES (hash, bolt11, expires_at, msats_req, user_id, now_utc(), now_utc(), idesc, comment) RETURNING * INTO invoice;
|
||||||
|
|
||||||
|
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter)
|
||||||
|
VALUES ('checkInvoice', jsonb_build_object('hash', hash), 21, true, now() + interval '10 seconds');
|
||||||
|
|
||||||
|
RETURN invoice;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- make sure old function is gone
|
||||||
|
DROP FUNCTION IF EXISTS create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone,
|
||||||
|
msats_req BIGINT, user_id INTEGER, idesc TEXT, inv_limit INTEGER, balance_limit_msats BIGINT);
|
|
@ -434,6 +434,7 @@ model Invoice {
|
||||||
msatsRequested BigInt
|
msatsRequested BigInt
|
||||||
msatsReceived BigInt?
|
msatsReceived BigInt?
|
||||||
desc String?
|
desc String?
|
||||||
|
comment String?
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@index([createdAt], map: "Invoice.created_at_index")
|
@@index([createdAt], map: "Invoice.created_at_index")
|
||||||
|
|
Loading…
Reference in New Issue