Skip to content
This repository has been archived by the owner on Dec 7, 2023. It is now read-only.

Commit

Permalink
feat: support nostr zap requests (#414)
Browse files Browse the repository at this point in the history
* feat: support nostr zap requests

* chore: make time expiry default Galoy settings of 1 day

---------

Co-authored-by: nicolasburtey <[email protected]>
  • Loading branch information
UncleSamtoshi and nicolasburtey authored Feb 17, 2023
1 parent 715269b commit 9d51641
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 50 deletions.
4 changes: 3 additions & 1 deletion lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ const GRAPHQL_URI_INTERNAL = `http://${GRAPHQL_HOSTNAME_INTERNAL}/graphql`
const GRAPHQL_URI = `https://${GRAPHQL_HOSTNAME}/graphql`
const GRAPHQL_SUBSCRIPTION_URI = `wss://${GRAPHQL_HOSTNAME}/graphql`

export { GRAPHQL_URI, GRAPHQL_SUBSCRIPTION_URI, GRAPHQL_URI_INTERNAL }
const NOSTR_PUBKEY = process.env.NOSTR_PUBKEY as string

export { GRAPHQL_URI, GRAPHQL_SUBSCRIPTION_URI, GRAPHQL_URI_INTERNAL, NOSTR_PUBKEY }
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"graphql": "^16.6.0",
"html2canvas": "^1.4.1",
"i18n-js": "^3.9.2",
"ioredis": "^5.3.1",
"lnurl-pay": "^1.0.1",
"lodash.debounce": "^4.0.8",
"next": "^12.3.0",
Expand Down
172 changes: 123 additions & 49 deletions pages/api/lnurlp/[username].ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import {
InMemoryCache,
} from "@apollo/client"
import type { NextApiRequest, NextApiResponse } from "next"
import Redis from "ioredis"

import { GRAPHQL_URI_INTERNAL } from "../../../lib/config"
import { GRAPHQL_URI_INTERNAL, NOSTR_PUBKEY } from "../../../lib/config"

const ipForwardingMiddleware = new ApolloLink((operation, forward) => {
operation.setContext(({ headers = {} }) => ({
Expand Down Expand Up @@ -62,101 +63,174 @@ const LNURL_INVOICE = gql`
}
invoice {
paymentRequest
paymentHash
}
}
}
`

type CreateInvoiceOutput = {
paymentRequest?: string
error?: Error
invoice?: {
paymentRequest: string
paymentHash: string
}
errors?: {
message: string
}[]
}

async function createInvoice(
walletId: string,
amount: number,
metadata: string,
): Promise<CreateInvoiceOutput> {
const descriptionHash = crypto.createHash("sha256").update(metadata).digest("hex")
type CreateInvoiceParams = {
walletId: string
amount: number
descriptionHash: string
}

async function createInvoice(params: CreateInvoiceParams): Promise<CreateInvoiceOutput> {
const {
data: {
mutationData: { errors, invoice },
},
} = await client.mutate({
mutation: LNURL_INVOICE,
variables: {
walletId,
amount,
descriptionHash,
},
variables: params,
})
if (errors && errors.length) {
throw new Error(`Failed to get invoice: ${errors[0].message}`)

return { errors, invoice }
}

const nostrEnabled = !!NOSTR_PUBKEY

let redis: Redis | null = null

if (nostrEnabled) {
const connectionObj = {
sentinelPassword: process.env.REDIS_PASSWORD,
sentinels: [
{
host: `${process.env.REDIS_0_DNS}`,
port: Number(process.env.REDIS_0_SENTINEL_PORT) || 26379,
},
{
host: `${process.env.REDIS_1_DNS}`,
port: Number(process.env.REDIS_1_SENTINEL_PORT) || 26379,
},
{
host: `${process.env.REDIS_2_DNS}`,
port: Number(process.env.REDIS_2_SENTINEL_PORT) || 26379,
},
],
name: process.env.REDIS_MASTER_NAME ?? "mymaster",
password: process.env.REDIS_PASSWORD,
}
return invoice

redis = new Redis(connectionObj)

redis.on("error", (err) => console.log({ err }, "Redis error"))
}

export default async function (req: NextApiRequest, res: NextApiResponse) {
const { username, amount } = req.query
console.log(NOSTR_PUBKEY)

const { username, amount, nostr } = req.query
const url = originalUrl(req)
const accountUsername = username ? username.toString() : ""

console.log({ headers: req.headers }, "request to NextApiRequest")
const walletId = await getUserWalletId(accountUsername, req)
if (!walletId) {
return res.json({
status: "ERROR",
reason: `Couldn't find user '${username}'.`,
})
}

const accountUsername = username ? username.toString() : ""
const metadata = JSON.stringify([
["text/plain", `Payment to ${username}`],
["text/identifier", `${username}@${url.hostname}`],
["text/plain", `Payment to ${accountUsername}`],
["text/identifier", `${accountUsername}@${url.hostname}`],
])
let walletId: string

try {
const { data } = await client.query({
query: ACCOUNT_DEFAULT_WALLET,
variables: { username: accountUsername, walletCurrency: "BTC" },
context: {
"x-real-ip": req.headers["x-real-ip"],
"x-forwarded-for": req.headers["x-forwarded-for"],
},
})
walletId = data?.accountDefaultWallet?.id ? data?.accountDefaultWallet?.id : ""
} catch (err) {
// lnurl options call
if (!amount) {
return res.json({
status: "ERROR",
reason: `Couldn't find user '${username}'.`,
callback: url.full,
minSendable: 1000,
maxSendable: 100000000000,
metadata: metadata,
tag: "payRequest",
...(nostrEnabled
? {
allowsNostr: true,
nostrPubkey: NOSTR_PUBKEY,
}
: {}),
})
}

if (amount) {
if (Array.isArray(amount)) {
// lnurl generate invoice
try {
if (Array.isArray(amount) || Array.isArray(nostr)) {
throw new Error("Invalid request")
}

const amountSats = Math.round(parseInt(amount, 10) / 1000)
if ((amountSats * 1000).toString() !== amount) {
return res.json({
status: "ERROR",
reason: "Millisatoshi amount is not supported, please send a value in full sats.",
})
}
const { paymentRequest, error } = await createInvoice(walletId, amountSats, metadata)
if (error instanceof Error) {

let descriptionHash: string

if (nostrEnabled && nostr) {
descriptionHash = crypto.createHash("sha256").update(nostr).digest("hex")
} else {
descriptionHash = crypto.createHash("sha256").update(metadata).digest("hex")
}

const { invoice, errors } = await createInvoice({
walletId,
amount: amountSats,
descriptionHash,
})

if ((errors && errors.length) || !invoice) {
console.log("error getting invoice", errors)
return res.json({
status: "ERROR",
reason: error instanceof Error ? error.message : "unexpected error",
reason: `Failed to get invoice: ${errors ? errors[0].message : "unknown error"}`,
})
}

if (nostrEnabled && nostr && redis) {
redis.set(`nostrInvoice:${invoice.paymentHash}`, nostr, "EX", 1440)
}

return res.json({
pr: paymentRequest,
pr: invoice.paymentRequest,
routes: [],
})
} else {
// first call
} catch (err: unknown) {
console.log("unexpected error getting invoice", err)
res.json({
callback: url.full,
minSendable: 1000,
maxSendable: 100000000000,
metadata: metadata,
tag: "payRequest",
status: "ERROR",
reason: err instanceof Error ? err.message : "unexpected error",
})
}
}

const getUserWalletId = async (accountUsername: string, req: NextApiRequest) => {
try {
const { data } = await client.query({
query: ACCOUNT_DEFAULT_WALLET,
variables: { username: accountUsername, walletCurrency: "BTC" },
context: {
"x-real-ip": req.headers["x-real-ip"],
"x-forwarded-for": req.headers["x-forwarded-for"],
},
})
return data?.accountDefaultWallet?.id
} catch (err) {
console.log("error getting user wallet id:", err)
return undefined
}
}
57 changes: 57 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==

"@ioredis/commands@^1.1.1":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11"
integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==

"@next/[email protected]":
version "12.3.0"
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.0.tgz#85f971fdc668cc312342761057c59cb8ab1abadf"
Expand Down Expand Up @@ -1170,6 +1175,11 @@ classnames@^2.3.1:
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==

cluster-key-slot@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==

color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
Expand Down Expand Up @@ -1349,6 +1359,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==

denque@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==

dequal@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
Expand Down Expand Up @@ -2216,6 +2231,21 @@ invariant@^2.2.4:
dependencies:
loose-envify "^1.0.0"

ioredis@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.1.tgz#55d394a51258cee3af9e96c21c863b1a97bf951f"
integrity sha512-C+IBcMysM6v52pTLItYMeV4Hz7uriGtoJdz7SSBDX6u+zwSYGirLdQh3L7t/OItWITcw3gTFMjJReYUwS4zihg==
dependencies:
"@ioredis/commands" "^1.1.1"
cluster-key-slot "^1.1.0"
debug "^4.3.4"
denque "^2.1.0"
lodash.defaults "^4.2.0"
lodash.isarguments "^3.1.0"
redis-errors "^1.2.0"
redis-parser "^3.0.0"
standard-as-callback "^2.1.0"

is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
Expand Down Expand Up @@ -2513,6 +2543,16 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==

lodash.defaults@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==

lodash.isarguments@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==

lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
Expand Down Expand Up @@ -3198,6 +3238,18 @@ recursive-readdir@^2.2.2:
dependencies:
minimatch "3.0.4"

redis-errors@^1.0.0, redis-errors@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==

redis-parser@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==
dependencies:
redis-errors "^1.0.0"

regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
Expand Down Expand Up @@ -3394,6 +3446,11 @@ source-map@^0.7.3:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==

standard-as-callback@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==

stream-browserify@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f"
Expand Down

0 comments on commit 9d51641

Please sign in to comment.