Compare commits
9 Commits
3863edb871
...
454ad26bd7
Author | SHA1 | Date | |
---|---|---|---|
|
454ad26bd7 | ||
|
ce7d2b888d | ||
|
ae73b0c19f | ||
|
68758b3443 | ||
|
c5f043c625 | ||
|
e897a2d1dc | ||
|
ed6ef2f82f | ||
|
bcae5e6d2e | ||
|
ef229b378e |
@ -160,3 +160,8 @@ TOR_PROXY=http://127.0.0.1:7050/
|
|||||||
|
|
||||||
# lnbits
|
# lnbits
|
||||||
LNBITS_WEB_PORT=5001
|
LNBITS_WEB_PORT=5001
|
||||||
|
|
||||||
|
# CPU shares for each category
|
||||||
|
CPU_SHARES_IMPORTANT=1024
|
||||||
|
CPU_SHARES_MODERATE=512
|
||||||
|
CPU_SHARES_LOW=256
|
@ -1,7 +1,7 @@
|
|||||||
import { ANON_ITEM_SPAM_INTERVAL, ITEM_SPAM_INTERVAL, USER_ID } from '@/lib/constants'
|
import { ANON_ITEM_SPAM_INTERVAL, ITEM_SPAM_INTERVAL, USER_ID } from '@/lib/constants'
|
||||||
import { notifyItemMention, notifyItemParents, notifyMention, notifyTerritorySubscribers, notifyUserSubscribers } from '@/lib/webPush'
|
import { notifyItemMention, notifyItemParents, notifyMention, notifyTerritorySubscribers, notifyUserSubscribers } from '@/lib/webPush'
|
||||||
import { getItemMentions, getMentions, performBotBehavior } from './lib/item'
|
import { getItemMentions, getMentions, performBotBehavior } from './lib/item'
|
||||||
import { satsToMsats } from '@/lib/format'
|
import { msatsToSats, satsToMsats } from '@/lib/format'
|
||||||
|
|
||||||
export const anonable = true
|
export const anonable = true
|
||||||
export const supportsPessimism = true
|
export const supportsPessimism = true
|
||||||
@ -51,8 +51,7 @@ export async function perform (args, context) {
|
|||||||
itemActs.push({
|
itemActs.push({
|
||||||
msats: cost - boostMsats, act: 'FEE', userId: data.userId, ...invoiceData
|
msats: cost - boostMsats, act: 'FEE', userId: data.userId, ...invoiceData
|
||||||
})
|
})
|
||||||
} else {
|
data.cost = msatsToSats(cost - boostMsats)
|
||||||
data.freebie = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mentions = await getMentions(args, context)
|
const mentions = await getMentions(args, context)
|
||||||
|
@ -30,12 +30,12 @@ function commentsOrderByClause (me, models, sort) {
|
|||||||
return `ORDER BY COALESCE(
|
return `ORDER BY COALESCE(
|
||||||
personal_hot_score,
|
personal_hot_score,
|
||||||
${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3)) DESC NULLS LAST,
|
${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3)) DESC NULLS LAST,
|
||||||
"Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC`
|
"Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
||||||
} else {
|
} else {
|
||||||
if (sort === 'top') {
|
if (sort === 'top') {
|
||||||
return `ORDER BY ${orderByNumerator(models, 0)} DESC NULLS LAST, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC`
|
return `ORDER BY ${orderByNumerator(models, 0)} DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
||||||
} else {
|
} else {
|
||||||
return `ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC`
|
return `ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -225,22 +225,16 @@ export async function filterClause (me, models, type) {
|
|||||||
|
|
||||||
// handle freebies
|
// handle freebies
|
||||||
// by default don't include freebies unless they have upvotes
|
// by default don't include freebies unless they have upvotes
|
||||||
let freebieClauses = ['NOT "Item".freebie', '"Item"."weightedVotes" - "Item"."weightedDownVotes" > 0']
|
let investmentClause = '("Item".cost + "Item".boost + ("Item".msats / 1000)) >= 10'
|
||||||
if (me) {
|
if (me) {
|
||||||
const user = await models.user.findUnique({ where: { id: me.id } })
|
const user = await models.user.findUnique({ where: { id: me.id } })
|
||||||
// wild west mode has everything
|
|
||||||
if (user.wildWestMode) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
// greeter mode includes freebies if feebies haven't been flagged
|
|
||||||
if (user.greeterMode) {
|
|
||||||
freebieClauses = ['NOT "Item".freebie', '"Item"."weightedVotes" - "Item"."weightedDownVotes" >= 0']
|
|
||||||
}
|
|
||||||
|
|
||||||
// always include if it's mine
|
investmentClause = `(("Item".cost + "Item".boost + ("Item".msats / 1000)) >= ${user.satsFilter} OR "Item"."userId" = ${me.id})`
|
||||||
freebieClauses.push(`"Item"."userId" = ${me.id}`)
|
|
||||||
|
if (user.wildWestMode) {
|
||||||
|
return investmentClause
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const freebieClause = '(' + freebieClauses.join(' OR ') + ')'
|
|
||||||
|
|
||||||
// handle outlawed
|
// handle outlawed
|
||||||
// if the item is above the threshold or is mine
|
// if the item is above the threshold or is mine
|
||||||
@ -250,7 +244,7 @@ export async function filterClause (me, models, type) {
|
|||||||
}
|
}
|
||||||
const outlawClause = '(' + outlawClauses.join(' OR ') + ')'
|
const outlawClause = '(' + outlawClauses.join(' OR ') + ')'
|
||||||
|
|
||||||
return [freebieClause, outlawClause]
|
return [investmentClause, outlawClause]
|
||||||
}
|
}
|
||||||
|
|
||||||
function typeClause (type) {
|
function typeClause (type) {
|
||||||
@ -268,7 +262,7 @@ function typeClause (type) {
|
|||||||
case 'comments':
|
case 'comments':
|
||||||
return '"Item"."parentId" IS NOT NULL'
|
return '"Item"."parentId" IS NOT NULL'
|
||||||
case 'freebies':
|
case 'freebies':
|
||||||
return '"Item".freebie'
|
return '"Item".cost = 0'
|
||||||
case 'outlawed':
|
case 'outlawed':
|
||||||
return `"Item"."weightedVotes" - "Item"."weightedDownVotes" <= -${ITEM_FILTER_THRESHOLD} OR "Item".outlawed`
|
return `"Item"."weightedVotes" - "Item"."weightedDownVotes" <= -${ITEM_FILTER_THRESHOLD} OR "Item".outlawed`
|
||||||
case 'borderland':
|
case 'borderland':
|
||||||
@ -470,10 +464,10 @@ export default {
|
|||||||
'"Item".bio = false',
|
'"Item".bio = false',
|
||||||
activeOrMine(me),
|
activeOrMine(me),
|
||||||
await filterClause(me, models, type))}
|
await filterClause(me, models, type))}
|
||||||
ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC
|
ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC
|
||||||
OFFSET $1
|
OFFSET $1
|
||||||
LIMIT $2`,
|
LIMIT $2`,
|
||||||
orderBy: `ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC`
|
orderBy: `ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
||||||
}, decodedCursor.offset, limit, ...subArr)
|
}, decodedCursor.offset, limit, ...subArr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1054,6 +1048,9 @@ export default {
|
|||||||
freedFreebie: async (item) => {
|
freedFreebie: async (item) => {
|
||||||
return item.weightedVotes - item.weightedDownVotes > 0
|
return item.weightedVotes - item.weightedDownVotes > 0
|
||||||
},
|
},
|
||||||
|
freebie: async (item) => {
|
||||||
|
return item.cost === 0
|
||||||
|
},
|
||||||
meSats: async (item, args, { me, models }) => {
|
meSats: async (item, args, { me, models }) => {
|
||||||
if (!me) return 0
|
if (!me) return 0
|
||||||
if (typeof item.meMsats !== 'undefined') {
|
if (typeof item.meMsats !== 'undefined') {
|
||||||
@ -1255,7 +1252,7 @@ export const updateItem = async (parent, { sub: subName, forward, ...item }, { m
|
|||||||
const differentSub = subName && old.subName !== subName
|
const differentSub = subName && old.subName !== subName
|
||||||
if (differentSub) {
|
if (differentSub) {
|
||||||
const sub = await models.sub.findUnique({ where: { name: subName } })
|
const sub = await models.sub.findUnique({ where: { name: subName } })
|
||||||
if (old.freebie) {
|
if (old.cost === 0) {
|
||||||
if (!sub.allowFreebies) {
|
if (!sub.allowFreebies) {
|
||||||
throw new GraphQLError(`~${subName} does not allow freebies`, { extensions: { code: 'BAD_INPUT' } })
|
throw new GraphQLError(`~${subName} does not allow freebies`, { extensions: { code: 'BAD_INPUT' } })
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import serialize from './serial'
|
|||||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
|
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
|
||||||
import { SELECT, itemQueryWithMeta } from './item'
|
import { SELECT, itemQueryWithMeta } from './item'
|
||||||
import { msatsToSats, msatsToSatsDecimal } from '@/lib/format'
|
import { msatsToSats, msatsToSatsDecimal } from '@/lib/format'
|
||||||
import { amountSchema, ssValidate, withdrawlSchema, lnAddrSchema, formikValidate } from '@/lib/validate'
|
import { amountSchema, ssValidate, withdrawlSchema, lnAddrSchema, walletValidate } from '@/lib/validate'
|
||||||
import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, USER_ID, BALANCE_LIMIT_MSATS, INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT } from '@/lib/constants'
|
import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, USER_ID, BALANCE_LIMIT_MSATS, INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT } from '@/lib/constants'
|
||||||
import { datePivot } from '@/lib/time'
|
import { datePivot } from '@/lib/time'
|
||||||
import assertGofacYourself from './ofac'
|
import assertGofacYourself from './ofac'
|
||||||
@ -19,20 +19,14 @@ import { lnAddrOptions } from '@/lib/lnurl'
|
|||||||
function injectResolvers (resolvers) {
|
function injectResolvers (resolvers) {
|
||||||
console.group('injected GraphQL resolvers:')
|
console.group('injected GraphQL resolvers:')
|
||||||
for (const w of walletDefs) {
|
for (const w of walletDefs) {
|
||||||
const { fieldValidation, walletType, walletField, testConnectServer } = w
|
const resolverName = generateResolverName(w.walletField)
|
||||||
const resolverName = generateResolverName(walletField)
|
|
||||||
console.log(resolverName)
|
console.log(resolverName)
|
||||||
|
|
||||||
// check if wallet uses the form-level validation built into Formik or a Yup schema
|
|
||||||
const validateArgs = typeof fieldValidation === 'function'
|
|
||||||
? { formikValidate: fieldValidation }
|
|
||||||
: { schema: fieldValidation }
|
|
||||||
|
|
||||||
resolvers.Mutation[resolverName] = async (parent, { settings, ...data }, { me, models }) => {
|
resolvers.Mutation[resolverName] = async (parent, { settings, ...data }, { me, models }) => {
|
||||||
|
await walletValidate(w, { ...data, ...settings })
|
||||||
return await upsertWallet({
|
return await upsertWallet({
|
||||||
...validateArgs,
|
wallet: { field: w.walletField, type: w.walletType },
|
||||||
wallet: { field: walletField, type: walletType },
|
testConnectServer: (data) => w.testConnectServer(data, { me, models })
|
||||||
testConnectServer: (data) => testConnectServer(data, { me, models })
|
|
||||||
}, { settings, data }, { me, models })
|
}, { settings, data }, { me, models })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -353,7 +347,7 @@ const resolvers = {
|
|||||||
},
|
},
|
||||||
WalletDetails: {
|
WalletDetails: {
|
||||||
__resolveType (wallet) {
|
__resolveType (wallet) {
|
||||||
return wallet.address ? 'WalletLNAddr' : wallet.macaroon ? 'WalletLND' : 'WalletCLN'
|
return wallet.address ? 'WalletLNAddr' : wallet.macaroon ? 'WalletLND' : wallet.rune ? 'WalletCLN' : 'WalletLNbits'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
@ -462,7 +456,8 @@ const resolvers = {
|
|||||||
|
|
||||||
await models.$transaction([
|
await models.$transaction([
|
||||||
models.wallet.delete({ where: { userId: me.id, id: Number(id) } }),
|
models.wallet.delete({ where: { userId: me.id, id: Number(id) } }),
|
||||||
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet detached' } })
|
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'INFO', message: 'receives disabled' } }),
|
||||||
|
models.walletLog.create({ data: { userId: me.id, wallet: wallet.type, level: 'SUCCESS', message: 'wallet detached for receives' } })
|
||||||
])
|
])
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@ -557,26 +552,20 @@ export const addWalletLog = async ({ wallet, level, message }, { me, models }) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function upsertWallet (
|
async function upsertWallet (
|
||||||
{ schema, formikValidate: validate, wallet, testConnectServer }, { settings, data }, { me, models }) {
|
{ wallet, testConnectServer }, { settings, data }, { me, models }) {
|
||||||
if (!me) {
|
if (!me) {
|
||||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
|
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
|
||||||
}
|
}
|
||||||
assertApiKeyNotPermitted({ me })
|
assertApiKeyNotPermitted({ me })
|
||||||
|
|
||||||
if (schema) {
|
|
||||||
await ssValidate(schema, { ...data, ...settings }, { me, models })
|
|
||||||
}
|
|
||||||
if (validate) {
|
|
||||||
await formikValidate(validate, { ...data, ...settings })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (testConnectServer) {
|
if (testConnectServer) {
|
||||||
try {
|
try {
|
||||||
await testConnectServer(data)
|
await testConnectServer(data)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
const message = err.message || err.toString?.()
|
const message = 'failed to create test invoice: ' + (err.message || err.toString?.())
|
||||||
await addWalletLog({ wallet, level: 'ERROR', message: 'failed to attach: ' + message }, { me, models })
|
await addWalletLog({ wallet, level: 'ERROR', message }, { me, models })
|
||||||
|
await addWalletLog({ wallet, level: 'INFO', message: 'receives disabled' }, { me, models })
|
||||||
throw new GraphQLError(message, { extensions: { code: 'BAD_INPUT' } })
|
throw new GraphQLError(message, { extensions: { code: 'BAD_INPUT' } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -632,7 +621,7 @@ async function upsertWallet (
|
|||||||
userId: me.id,
|
userId: me.id,
|
||||||
wallet: wallet.type,
|
wallet: wallet.type,
|
||||||
level: 'SUCCESS',
|
level: 'SUCCESS',
|
||||||
message: id ? 'wallet updated' : 'wallet attached'
|
message: id ? 'receive details updated' : 'wallet attached for receives'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
models.walletLog.create({
|
models.walletLog.create({
|
||||||
@ -640,7 +629,7 @@ async function upsertWallet (
|
|||||||
userId: me.id,
|
userId: me.id,
|
||||||
wallet: wallet.type,
|
wallet: wallet.type,
|
||||||
level: enabled ? 'SUCCESS' : 'INFO',
|
level: enabled ? 'SUCCESS' : 'INFO',
|
||||||
message: enabled ? 'wallet enabled' : 'wallet disabled'
|
message: enabled ? 'receives enabled' : 'receives disabled'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -142,7 +142,11 @@ export function getGetServerSideProps (
|
|||||||
const { data: { me } } = await client.query({ query: ME })
|
const { data: { me } } = await client.query({ query: ME })
|
||||||
|
|
||||||
if (authRequired && !me) {
|
if (authRequired && !me) {
|
||||||
const callback = process.env.NEXT_PUBLIC_URL + req.url
|
let callback = process.env.NEXT_PUBLIC_URL + req.url
|
||||||
|
// On client-side routing, the callback is a NextJS URL
|
||||||
|
// so we need to remove the NextJS stuff.
|
||||||
|
// Example: /_next/data/development/territory.json
|
||||||
|
callback = callback.replace(/\/_next\/data\/\w+\//, '/').replace(/\.json$/, '')
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: `/signup?callbackUrl=${encodeURIComponent(callback)}`
|
destination: `/signup?callbackUrl=${encodeURIComponent(callback)}`
|
||||||
|
@ -71,6 +71,7 @@ export default gql`
|
|||||||
diagnostics: Boolean!
|
diagnostics: Boolean!
|
||||||
noReferralLinks: Boolean!
|
noReferralLinks: Boolean!
|
||||||
fiatCurrency: String!
|
fiatCurrency: String!
|
||||||
|
satsFilter: Int!
|
||||||
greeterMode: Boolean!
|
greeterMode: Boolean!
|
||||||
hideBookmarks: Boolean!
|
hideBookmarks: Boolean!
|
||||||
hideCowboyHat: Boolean!
|
hideCowboyHat: Boolean!
|
||||||
@ -140,6 +141,7 @@ export default gql`
|
|||||||
diagnostics: Boolean!
|
diagnostics: Boolean!
|
||||||
noReferralLinks: Boolean!
|
noReferralLinks: Boolean!
|
||||||
fiatCurrency: String!
|
fiatCurrency: String!
|
||||||
|
satsFilter: Int!
|
||||||
greeterMode: Boolean!
|
greeterMode: Boolean!
|
||||||
hideBookmarks: Boolean!
|
hideBookmarks: Boolean!
|
||||||
hideCowboyHat: Boolean!
|
hideCowboyHat: Boolean!
|
||||||
|
@ -2,19 +2,22 @@ import { gql } from 'graphql-tag'
|
|||||||
import { generateResolverName } from '@/lib/wallet'
|
import { generateResolverName } from '@/lib/wallet'
|
||||||
|
|
||||||
import walletDefs from 'wallets/server'
|
import walletDefs from 'wallets/server'
|
||||||
|
import { isServerField } from 'wallets'
|
||||||
|
|
||||||
function injectTypeDefs (typeDefs) {
|
function injectTypeDefs (typeDefs) {
|
||||||
console.group('injected GraphQL type defs:')
|
console.group('injected GraphQL type defs:')
|
||||||
const injected = walletDefs.map(
|
const injected = walletDefs.map(
|
||||||
(w) => {
|
(w) => {
|
||||||
let args = 'id: ID, '
|
let args = 'id: ID, '
|
||||||
args += w.fields.map(f => {
|
args += w.fields
|
||||||
let arg = `${f.name}: String`
|
.filter(isServerField)
|
||||||
if (!f.optional) {
|
.map(f => {
|
||||||
arg += '!'
|
let arg = `${f.name}: String`
|
||||||
}
|
if (!f.optional) {
|
||||||
return arg
|
arg += '!'
|
||||||
}).join(', ')
|
}
|
||||||
|
return arg
|
||||||
|
}).join(', ')
|
||||||
args += ', settings: AutowithdrawSettings!'
|
args += ', settings: AutowithdrawSettings!'
|
||||||
const resolverName = generateResolverName(w.walletField)
|
const resolverName = generateResolverName(w.walletField)
|
||||||
const typeDef = `${resolverName}(${args}): Boolean`
|
const typeDef = `${resolverName}(${args}): Boolean`
|
||||||
@ -74,7 +77,12 @@ const typeDefs = `
|
|||||||
cert: String
|
cert: String
|
||||||
}
|
}
|
||||||
|
|
||||||
union WalletDetails = WalletLNAddr | WalletLND | WalletCLN
|
type WalletLNbits {
|
||||||
|
url: String!
|
||||||
|
invoiceKey: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
union WalletDetails = WalletLNAddr | WalletLND | WalletCLN | WalletLNbits
|
||||||
|
|
||||||
input AutowithdrawSettings {
|
input AutowithdrawSettings {
|
||||||
autoWithdrawThreshold: Int!
|
autoWithdrawThreshold: Int!
|
||||||
|
@ -118,4 +118,5 @@ takitakitanana,issue,,#1257,good-first-issue,,,,2k,takitakitanana@stacker.news,2
|
|||||||
SatsAllDay,pr,#1263,#1112,medium,,,1,225k,weareallsatoshi@getalby.com,2024-07-31
|
SatsAllDay,pr,#1263,#1112,medium,,,1,225k,weareallsatoshi@getalby.com,2024-07-31
|
||||||
OneOneSeven117,issue,#1272,#1268,easy,,,,10k,OneOneSeven@stacker.news,2024-07-31
|
OneOneSeven117,issue,#1272,#1268,easy,,,,10k,OneOneSeven@stacker.news,2024-07-31
|
||||||
aniskhalfallah,pr,#1264,#1226,good-first-issue,,,,20k,aniskhalfallah@stacker.news,2024-07-31
|
aniskhalfallah,pr,#1264,#1226,good-first-issue,,,,20k,aniskhalfallah@stacker.news,2024-07-31
|
||||||
Gudnessuche,issue,#1264,#1226,good-first-issue,,,,2k,???,???
|
Gudnessuche,issue,#1264,#1226,good-first-issue,,,,2k,everythingsatoshi@getalby.com,2024-08-10
|
||||||
|
aniskhalfallah,pr,#1289,,easy,,,,100k,aniskhalfallah@blink.sv,2024-08-12
|
||||||
|
|
@ -25,10 +25,15 @@ export function AutowithdrawSettings ({ wallet }) {
|
|||||||
setSendThreshold(Math.max(Math.floor(threshold / 10), 1))
|
setSendThreshold(Math.max(Math.floor(threshold / 10), 1))
|
||||||
}, [autoWithdrawThreshold])
|
}, [autoWithdrawThreshold])
|
||||||
|
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
disabled={!wallet.isConfigured}
|
disabled={mounted && !wallet.isConfigured}
|
||||||
label='enabled'
|
label='enabled'
|
||||||
id='enabled'
|
id='enabled'
|
||||||
name='enabled'
|
name='enabled'
|
||||||
|
@ -97,7 +97,7 @@ export default function Comment ({
|
|||||||
}) {
|
}) {
|
||||||
const [edit, setEdit] = useState()
|
const [edit, setEdit] = useState()
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
const isHiddenFreebie = !me?.privates?.wildWestMode && !me?.privates?.greeterMode && !item.mine && item.freebie && !item.freedFreebie
|
const isHiddenFreebie = me?.privates?.satsFilter !== 0 && !item.mine && item.freebie && !item.freedFreebie
|
||||||
const [collapse, setCollapse] = useState(
|
const [collapse, setCollapse] = useState(
|
||||||
(isHiddenFreebie || item?.user?.meMute || (item?.outlawed && !me?.privates?.wildWestMode)) && !includeParent
|
(isHiddenFreebie || item?.user?.meMute || (item?.outlawed && !me?.privates?.wildWestMode)) && !includeParent
|
||||||
? 'yep'
|
? 'yep'
|
||||||
|
@ -243,9 +243,6 @@ export function useWalletLogger (wallet) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't store logs for receiving wallets on client since logs are stored on server
|
|
||||||
if (wallet.walletType) return
|
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// also send this to us if diagnostics was enabled,
|
// also send this to us if diagnostics was enabled,
|
||||||
// very similar to how the service worker logger works.
|
// very similar to how the service worker logger works.
|
||||||
|
@ -35,6 +35,7 @@ services:
|
|||||||
- db:/var/lib/postgresql/data
|
- db:/var/lib/postgresql/data
|
||||||
labels:
|
labels:
|
||||||
CONNECT: "localhost:5431"
|
CONNECT: "localhost:5431"
|
||||||
|
cpu_shares: "${CPU_SHARES_IMPORTANT}"
|
||||||
app:
|
app:
|
||||||
container_name: app
|
container_name: app
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
@ -58,6 +59,7 @@ services:
|
|||||||
- ./:/app
|
- ./:/app
|
||||||
labels:
|
labels:
|
||||||
CONNECT: "localhost:3000"
|
CONNECT: "localhost:3000"
|
||||||
|
cpu_shares: "${CPU_SHARES_IMPORTANT}"
|
||||||
capture:
|
capture:
|
||||||
container_name: capture
|
container_name: capture
|
||||||
build:
|
build:
|
||||||
@ -79,6 +81,7 @@ services:
|
|||||||
- "5678:5678"
|
- "5678:5678"
|
||||||
labels:
|
labels:
|
||||||
CONNECT: "localhost:5678"
|
CONNECT: "localhost:5678"
|
||||||
|
cpu_shares: "${CPU_SHARES_LOW}"
|
||||||
worker:
|
worker:
|
||||||
container_name: worker
|
container_name: worker
|
||||||
build:
|
build:
|
||||||
@ -97,6 +100,7 @@ services:
|
|||||||
entrypoint: ["/bin/sh", "-c"]
|
entrypoint: ["/bin/sh", "-c"]
|
||||||
command:
|
command:
|
||||||
- npm run worker:dev
|
- npm run worker:dev
|
||||||
|
cpu_shares: "${CPU_SHARES_IMPORTANT}"
|
||||||
imgproxy:
|
imgproxy:
|
||||||
container_name: imgproxy
|
container_name: imgproxy
|
||||||
image: darthsim/imgproxy:v3.23.0
|
image: darthsim/imgproxy:v3.23.0
|
||||||
@ -113,6 +117,7 @@ services:
|
|||||||
- "8080"
|
- "8080"
|
||||||
labels:
|
labels:
|
||||||
- "CONNECT=localhost:3001"
|
- "CONNECT=localhost:3001"
|
||||||
|
cpu_shares: "${CPU_SHARES_LOW}"
|
||||||
s3:
|
s3:
|
||||||
container_name: s3
|
container_name: s3
|
||||||
image: localstack/localstack:s3-latest
|
image: localstack/localstack:s3-latest
|
||||||
@ -138,6 +143,7 @@ services:
|
|||||||
- './docker/s3/cors.json:/etc/localstack/init/ready.d/cors.json'
|
- './docker/s3/cors.json:/etc/localstack/init/ready.d/cors.json'
|
||||||
labels:
|
labels:
|
||||||
- "CONNECT=localhost:4566"
|
- "CONNECT=localhost:4566"
|
||||||
|
cpu_shares: "${CPU_SHARES_LOW}"
|
||||||
opensearch:
|
opensearch:
|
||||||
image: opensearchproject/opensearch:2.12.0
|
image: opensearchproject/opensearch:2.12.0
|
||||||
container_name: opensearch
|
container_name: opensearch
|
||||||
@ -177,6 +183,7 @@ services:
|
|||||||
echo "OpenSearch index created."
|
echo "OpenSearch index created."
|
||||||
fg
|
fg
|
||||||
'
|
'
|
||||||
|
cpu_shares: "${CPU_SHARES_LOW}"
|
||||||
os-dashboard:
|
os-dashboard:
|
||||||
image: opensearchproject/opensearch-dashboards:2.12.0
|
image: opensearchproject/opensearch-dashboards:2.12.0
|
||||||
container_name: os-dashboard
|
container_name: os-dashboard
|
||||||
@ -198,6 +205,7 @@ services:
|
|||||||
- opensearch
|
- opensearch
|
||||||
labels:
|
labels:
|
||||||
CONNECT: "localhost:5601"
|
CONNECT: "localhost:5601"
|
||||||
|
cpu_shares: "${CPU_SHARES_LOW}"
|
||||||
bitcoin:
|
bitcoin:
|
||||||
image: polarlightning/bitcoind:26.0
|
image: polarlightning/bitcoind:26.0
|
||||||
container_name: bitcoin
|
container_name: bitcoin
|
||||||
@ -254,6 +262,7 @@ services:
|
|||||||
bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} generatetoaddress 1 ${STACKER_CLN_ADDR}
|
bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} generatetoaddress 1 ${STACKER_CLN_ADDR}
|
||||||
fi
|
fi
|
||||||
'
|
'
|
||||||
|
cpu_shares: "${CPU_SHARES_MODERATE}"
|
||||||
sn_lnd:
|
sn_lnd:
|
||||||
build:
|
build:
|
||||||
context: ./docker/lnd
|
context: ./docker/lnd
|
||||||
@ -311,6 +320,7 @@ services:
|
|||||||
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
|
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
|
||||||
fi
|
fi
|
||||||
"
|
"
|
||||||
|
cpu_shares: "${CPU_SHARES_MODERATE}"
|
||||||
stacker_lnd:
|
stacker_lnd:
|
||||||
build:
|
build:
|
||||||
context: ./docker/lnd
|
context: ./docker/lnd
|
||||||
@ -370,6 +380,7 @@ services:
|
|||||||
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
|
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
|
||||||
fi
|
fi
|
||||||
"
|
"
|
||||||
|
cpu_shares: "${CPU_SHARES_MODERATE}"
|
||||||
litd:
|
litd:
|
||||||
container_name: litd
|
container_name: litd
|
||||||
build:
|
build:
|
||||||
@ -404,6 +415,7 @@ services:
|
|||||||
- '--loop.server.host=test.swap.lightning.today:11010'
|
- '--loop.server.host=test.swap.lightning.today:11010'
|
||||||
labels:
|
labels:
|
||||||
CONNECT: "localhost:8443"
|
CONNECT: "localhost:8443"
|
||||||
|
cpu_shares: "${CPU_SHARES_MODERATE}"
|
||||||
stacker_cln:
|
stacker_cln:
|
||||||
build:
|
build:
|
||||||
context: ./docker/cln
|
context: ./docker/cln
|
||||||
@ -446,6 +458,7 @@ services:
|
|||||||
amount=1000000000 push_msat=500000000000 minconf=0
|
amount=1000000000 push_msat=500000000000 minconf=0
|
||||||
fi
|
fi
|
||||||
"
|
"
|
||||||
|
cpu_shares: "${CPU_SHARES_MODERATE}"
|
||||||
channdler:
|
channdler:
|
||||||
image: mcuadros/ofelia:latest
|
image: mcuadros/ofelia:latest
|
||||||
container_name: channdler
|
container_name: channdler
|
||||||
@ -460,6 +473,7 @@ services:
|
|||||||
command: daemon --docker -f label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}
|
command: daemon --docker -f label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
cpu_shares: "${CPU_SHARES_LOW}"
|
||||||
mailhog:
|
mailhog:
|
||||||
image: mailhog/mailhog:latest
|
image: mailhog/mailhog:latest
|
||||||
container_name: mailhog
|
container_name: mailhog
|
||||||
@ -476,6 +490,7 @@ services:
|
|||||||
- app
|
- app
|
||||||
labels:
|
labels:
|
||||||
CONNECT: "localhost:8025"
|
CONNECT: "localhost:8025"
|
||||||
|
cpu_shares: "${CPU_SHARES_LOW}"
|
||||||
nwc:
|
nwc:
|
||||||
build:
|
build:
|
||||||
context: ./docker/nwc
|
context: ./docker/nwc
|
||||||
@ -507,6 +522,7 @@ services:
|
|||||||
- '0'
|
- '0'
|
||||||
- '--daily-limit'
|
- '--daily-limit'
|
||||||
- '0'
|
- '0'
|
||||||
|
cpu_shares: "${CPU_SHARES_LOW}"
|
||||||
lnbits:
|
lnbits:
|
||||||
image: lnbits/lnbits:0.12.5
|
image: lnbits/lnbits:0.12.5
|
||||||
container_name: lnbits
|
container_name: lnbits
|
||||||
@ -525,6 +541,9 @@ services:
|
|||||||
- LND_GRPC_MACAROON=/app/.lnd/regtest/admin.macaroon
|
- LND_GRPC_MACAROON=/app/.lnd/regtest/admin.macaroon
|
||||||
volumes:
|
volumes:
|
||||||
- ./docker/lnd/stacker:/app/.lnd
|
- ./docker/lnd/stacker:/app/.lnd
|
||||||
|
labels:
|
||||||
|
CONNECT: "localhost:${LNBITS_WEB_PORT}"
|
||||||
|
cpu_shares: "${CPU_SHARES_LOW}"
|
||||||
volumes:
|
volumes:
|
||||||
db:
|
db:
|
||||||
os:
|
os:
|
||||||
|
@ -1,81 +0,0 @@
|
|||||||
# attach lnbits
|
|
||||||
|
|
||||||
To test sending from an attached wallet, it's easiest to use [lnbits](https://lnbits.com/) hooked up to a [local lnd node](./local-lnd.md) in your regtest network.
|
|
||||||
|
|
||||||
This will attempt to walk you through setting up lnbits with docker and connecting it to your local lnd node.
|
|
||||||
|
|
||||||
🚨 this a dev guide. do not use this guide for real funds 🚨
|
|
||||||
|
|
||||||
From [this guide](https://docs.lnbits.org/guide/installation.html#option-3-docker):
|
|
||||||
|
|
||||||
## 1. pre-configuration
|
|
||||||
|
|
||||||
Create a directory for lnbits, get the sample environment file, and create a shared data directory for lnbits to use:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir lnbits
|
|
||||||
cd lnbits
|
|
||||||
wget https://raw.githubusercontent.com/lnbits/lnbits/main/.env.example -O .env
|
|
||||||
mkdir data
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. configure
|
|
||||||
|
|
||||||
To configure lnbits to use a [local lnd node](./local-lnd.md) in your regtest network, go to [polar](https://lightningpolar.com/) and click on the LND node you want to use as a funding source. Then click on `Connect`.
|
|
||||||
|
|
||||||
In the `Connect` tab, click the `File paths` tab and copy the `TLS cert` and `Admin macaroon` files to the `data` directory you created earlier.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp /path/to/tls.cert /path/to/admin.macaroon data/
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, open the `.env` file you created and override the following values:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
LNBITS_ADMIN_UI=true
|
|
||||||
LNBITS_BACKEND_WALLET_CLASS=LndWallet
|
|
||||||
LND_GRPC_ENDPOINT=host.docker.internal
|
|
||||||
LND_GRPC_PORT=${Port from the polar connect page}
|
|
||||||
LND_GRPC_CERT=data/tls.cert
|
|
||||||
LND_GRPC_MACAROON=data/admin.macaroon
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Install and run lnbits
|
|
||||||
|
|
||||||
Pull the latest image:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull lnbitsdocker/lnbits-legend
|
|
||||||
docker run --detach --publish 5001:5000 --name lnbits --volume ${PWD}/.env:/app/.env --volume ${PWD}/data/:/app/data lnbitsdocker/lnbits-legend
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: we make lnbits available on the host's port 5001 here (on Mac, 5000 is used by AirPlay), but you can change that to whatever you want.
|
|
||||||
|
|
||||||
## 3. Accessing the admin wallet
|
|
||||||
|
|
||||||
By enabling the [Admin UI](https://docs.lnbits.org/guide/admin_ui.html), lnbits creates a so called super_user. Get this super_user id by running:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cat data/.super_user
|
|
||||||
```
|
|
||||||
|
|
||||||
Open your browser and go to `http://localhost:5001/wallet?usr=${super_user id from above}`. LNBits will redirect you to a default wallet we will use called `LNBits wallet`.
|
|
||||||
|
|
||||||
## 4. Fund the wallet
|
|
||||||
|
|
||||||
To fund `LNBits wallet`, click the `+` next the wallet balance. Enter the number of sats you want to credit the wallet and hit enter.
|
|
||||||
|
|
||||||
## 5. Attach the wallet to stackernews
|
|
||||||
|
|
||||||
Open up your local stackernews, go to `http://localhost:3000/settings/wallets` and click on `attach` in the `lnbits` card.
|
|
||||||
|
|
||||||
In the form, fill in `lnbits url` with `http://localhost:5001`.
|
|
||||||
|
|
||||||
Back in lnbits click on `API Docs` in the right pane. Copy the Admin key and paste it into the `admin key` field in the form.
|
|
||||||
|
|
||||||
Click `attach` and you should be good to go.
|
|
||||||
|
|
||||||
## Debugging
|
|
||||||
|
|
||||||
- you can view lnbits logs with `docker logs lnbits` or in `data/logs/` in the `data` directory you created earlier
|
|
||||||
- with the [Admin UI](https://docs.lnbits.org/guide/admin_ui.html), you can modify LNBits in the GUI by clicking `Server` in left pane
|
|
@ -15,7 +15,7 @@ export const ME = gql`
|
|||||||
diagnostics
|
diagnostics
|
||||||
noReferralLinks
|
noReferralLinks
|
||||||
fiatCurrency
|
fiatCurrency
|
||||||
greeterMode
|
satsFilter
|
||||||
hideCowboyHat
|
hideCowboyHat
|
||||||
hideFromTopUsers
|
hideFromTopUsers
|
||||||
hideGithub
|
hideGithub
|
||||||
@ -104,7 +104,7 @@ export const SETTINGS_FIELDS = gql`
|
|||||||
nostrCrossposting
|
nostrCrossposting
|
||||||
nostrRelays
|
nostrRelays
|
||||||
wildWestMode
|
wildWestMode
|
||||||
greeterMode
|
satsFilter
|
||||||
nsfwMode
|
nsfwMode
|
||||||
authMethods {
|
authMethods {
|
||||||
lightning
|
lightning
|
||||||
|
@ -129,6 +129,10 @@ export const WALLET = gql`
|
|||||||
rune
|
rune
|
||||||
cert
|
cert
|
||||||
}
|
}
|
||||||
|
... on WalletLNbits {
|
||||||
|
url
|
||||||
|
invoiceKey
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,6 +161,10 @@ export const WALLET_BY_TYPE = gql`
|
|||||||
rune
|
rune
|
||||||
cert
|
cert
|
||||||
}
|
}
|
||||||
|
... on WalletLNbits {
|
||||||
|
url
|
||||||
|
invoiceKey
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
} from './constants'
|
} from './constants'
|
||||||
import { SUPPORTED_CURRENCIES } from './currency'
|
import { SUPPORTED_CURRENCIES } from './currency'
|
||||||
import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './nostr'
|
import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './nostr'
|
||||||
import { msatsToSats, numWithUnits, abbrNum, ensureB64, B64_URL_REGEX } from './format'
|
import { msatsToSats, numWithUnits, abbrNum, ensureB64, B64_URL_REGEX, HEX_REGEX } from './format'
|
||||||
import * as usersFragments from '@/fragments/users'
|
import * as usersFragments from '@/fragments/users'
|
||||||
import * as subsFragments from '@/fragments/subs'
|
import * as subsFragments from '@/fragments/subs'
|
||||||
import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon'
|
import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon'
|
||||||
@ -41,6 +41,14 @@ export async function formikValidate (validate, data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function walletValidate (wallet, data) {
|
||||||
|
if (typeof wallet.fieldValidation === 'function') {
|
||||||
|
return await formikValidate(wallet.fieldValidation, data)
|
||||||
|
} else {
|
||||||
|
await ssValidate(wallet.fieldValidation, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addMethod(string, 'or', function (schemas, msg) {
|
addMethod(string, 'or', function (schemas, msg) {
|
||||||
return this.test({
|
return this.test({
|
||||||
name: 'or',
|
name: 'or',
|
||||||
@ -142,6 +150,14 @@ addMethod(string, 'wss', function (msg) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
addMethod(string, 'hex', function (msg) {
|
||||||
|
return this.test({
|
||||||
|
name: 'hex',
|
||||||
|
message: msg || 'invalid hex encoding',
|
||||||
|
test: (value) => !value || HEX_REGEX.test(value)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const titleValidator = string().required('required').trim().max(
|
const titleValidator = string().required('required').trim().max(
|
||||||
MAX_TITLE_LENGTH,
|
MAX_TITLE_LENGTH,
|
||||||
({ max, value }) => `-${Math.abs(max - value.length)} characters remaining`
|
({ max, value }) => `-${Math.abs(max - value.length)} characters remaining`
|
||||||
@ -579,6 +595,7 @@ export const settingsSchema = object().shape({
|
|||||||
diagnostics: boolean(),
|
diagnostics: boolean(),
|
||||||
noReferralLinks: boolean(),
|
noReferralLinks: boolean(),
|
||||||
hideIsContributor: boolean(),
|
hideIsContributor: boolean(),
|
||||||
|
satsFilter: intValidator.required('required').min(0, 'must be at least 0').max(1000, 'must be at most 1000'),
|
||||||
zapUndos: intValidator.nullable().min(0, 'must be greater or equal to 0')
|
zapUndos: intValidator.nullable().min(0, 'must be greater or equal to 0')
|
||||||
// exclude from cyclic analysis. see https://github.com/jquense/yup/issues/720
|
// exclude from cyclic analysis. see https://github.com/jquense/yup/issues/720
|
||||||
}, [['tipRandomMax', 'tipRandomMin']])
|
}, [['tipRandomMax', 'tipRandomMin']])
|
||||||
@ -620,7 +637,7 @@ export const lnAddrSchema = ({ payerData, min, max, commentAllowed } = {}) =>
|
|||||||
return accum
|
return accum
|
||||||
}, {})))
|
}, {})))
|
||||||
|
|
||||||
export const lnbitsSchema = object({
|
export const lnbitsSchema = object().shape({
|
||||||
url: process.env.NODE_ENV === 'development'
|
url: process.env.NODE_ENV === 'development'
|
||||||
? string()
|
? string()
|
||||||
.or([string().matches(/^(http:\/\/)?localhost:\d+$/), string().url()], 'invalid url')
|
.or([string().matches(/^(http:\/\/)?localhost:\d+$/), string().url()], 'invalid url')
|
||||||
@ -642,8 +659,25 @@ export const lnbitsSchema = object({
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}),
|
}),
|
||||||
adminKey: string().length(32).required('required')
|
adminKey: string().length(32).hex()
|
||||||
})
|
.when(['invoiceKey'], ([invoiceKey], schema) => {
|
||||||
|
if (!invoiceKey) return schema.required('required if invoice key not set')
|
||||||
|
return schema.test({
|
||||||
|
test: adminKey => adminKey !== invoiceKey,
|
||||||
|
message: 'admin key cannot be the same as invoice key'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
invoiceKey: string().length(32).hex()
|
||||||
|
.when(['adminKey'], ([adminKey], schema) => {
|
||||||
|
if (!adminKey) return schema.required('required if admin key not set')
|
||||||
|
return schema.test({
|
||||||
|
test: invoiceKey => adminKey !== invoiceKey,
|
||||||
|
message: 'invoice key cannot be the same as admin key'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// need to set order to avoid cyclic dependencies in Yup schema
|
||||||
|
// see https://github.com/jquense/yup/issues/176#issuecomment-367352042
|
||||||
|
}, ['adminKey', 'invoiceKey'])
|
||||||
|
|
||||||
export const nwcSchema = object({
|
export const nwcSchema = object({
|
||||||
nwcUrl: string()
|
nwcUrl: string()
|
||||||
|
@ -140,7 +140,7 @@ export default function Settings ({ ssrData }) {
|
|||||||
hideTwitter: settings?.hideTwitter,
|
hideTwitter: settings?.hideTwitter,
|
||||||
imgproxyOnly: settings?.imgproxyOnly,
|
imgproxyOnly: settings?.imgproxyOnly,
|
||||||
wildWestMode: settings?.wildWestMode,
|
wildWestMode: settings?.wildWestMode,
|
||||||
greeterMode: settings?.greeterMode,
|
satsFilter: settings?.satsFilter,
|
||||||
nsfwMode: settings?.nsfwMode,
|
nsfwMode: settings?.nsfwMode,
|
||||||
nostrPubkey: settings?.nostrPubkey ? bech32encode(settings.nostrPubkey) : '',
|
nostrPubkey: settings?.nostrPubkey ? bech32encode(settings.nostrPubkey) : '',
|
||||||
nostrCrossposting: settings?.nostrCrossposting,
|
nostrCrossposting: settings?.nostrCrossposting,
|
||||||
@ -152,7 +152,11 @@ export default function Settings ({ ssrData }) {
|
|||||||
noReferralLinks: settings?.noReferralLinks
|
noReferralLinks: settings?.noReferralLinks
|
||||||
}}
|
}}
|
||||||
schema={settingsSchema}
|
schema={settingsSchema}
|
||||||
onSubmit={async ({ tipDefault, tipRandom, tipRandomMin, tipRandomMax, withdrawMaxFeeDefault, zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, ...values }) => {
|
onSubmit={async ({
|
||||||
|
tipDefault, tipRandom, tipRandomMin, tipRandomMax, withdrawMaxFeeDefault,
|
||||||
|
zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, satsFilter,
|
||||||
|
...values
|
||||||
|
}) => {
|
||||||
if (nostrPubkey.length === 0) {
|
if (nostrPubkey.length === 0) {
|
||||||
nostrPubkey = null
|
nostrPubkey = null
|
||||||
} else {
|
} else {
|
||||||
@ -172,6 +176,7 @@ export default function Settings ({ ssrData }) {
|
|||||||
tipRandomMin: tipRandom ? Number(tipRandomMin) : null,
|
tipRandomMin: tipRandom ? Number(tipRandomMin) : null,
|
||||||
tipRandomMax: tipRandom ? Number(tipRandomMax) : null,
|
tipRandomMax: tipRandom ? Number(tipRandomMax) : null,
|
||||||
withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
|
withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
|
||||||
|
satsFilter: Number(satsFilter),
|
||||||
zapUndos: zapUndosEnabled ? Number(zapUndos) : null,
|
zapUndos: zapUndosEnabled ? Number(zapUndos) : null,
|
||||||
nostrPubkey,
|
nostrPubkey,
|
||||||
nostrRelays: nostrRelaysFiltered,
|
nostrRelays: nostrRelaysFiltered,
|
||||||
@ -467,7 +472,27 @@ export default function Settings ({ ssrData }) {
|
|||||||
label={<>don't create referral links on copy</>}
|
label={<>don't create referral links on copy</>}
|
||||||
name='noReferralLinks'
|
name='noReferralLinks'
|
||||||
/>
|
/>
|
||||||
<div className='form-label'>content</div>
|
<h4>content</h4>
|
||||||
|
<Input
|
||||||
|
label={
|
||||||
|
<div className='d-flex align-items-center'>filter by sats
|
||||||
|
<Info>
|
||||||
|
<ul className='fw-bold'>
|
||||||
|
<li>hide the post if the sum of these is less than your setting:</li>
|
||||||
|
<ul>
|
||||||
|
<li>posting cost</li>
|
||||||
|
<li>total sats from zaps</li>
|
||||||
|
<li>boost</li>
|
||||||
|
</ul>
|
||||||
|
<li>set to zero to be a greeter, with the tradeoff of seeing more spam</li>
|
||||||
|
</ul>
|
||||||
|
</Info>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
name='satsFilter'
|
||||||
|
required
|
||||||
|
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
||||||
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={
|
label={
|
||||||
<div className='d-flex align-items-center'>wild west mode
|
<div className='d-flex align-items-center'>wild west mode
|
||||||
@ -482,21 +507,6 @@ export default function Settings ({ ssrData }) {
|
|||||||
name='wildWestMode'
|
name='wildWestMode'
|
||||||
groupClassName='mb-0'
|
groupClassName='mb-0'
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
|
||||||
label={
|
|
||||||
<div className='d-flex align-items-center'>greeter mode
|
|
||||||
<Info>
|
|
||||||
<ul className='fw-bold'>
|
|
||||||
<li>see and screen free posts and comments</li>
|
|
||||||
<li>help onboard new stackers to SN and Lightning</li>
|
|
||||||
<li>you might be subject to more spam</li>
|
|
||||||
</ul>
|
|
||||||
</Info>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
name='greeterMode'
|
|
||||||
groupClassName='mb-0'
|
|
||||||
/>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={
|
label={
|
||||||
<div className='d-flex align-items-center'>nsfw mode
|
<div className='d-flex align-items-center'>nsfw mode
|
||||||
|
@ -57,15 +57,11 @@ export default function WalletSettings () {
|
|||||||
|
|
||||||
await wallet.save(values)
|
await wallet.save(values)
|
||||||
|
|
||||||
if (values.enabled) wallet.enable()
|
|
||||||
else wallet.disable()
|
|
||||||
|
|
||||||
toaster.success('saved settings')
|
toaster.success('saved settings')
|
||||||
router.push('/settings/wallets')
|
router.push('/settings/wallets')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
const message = 'failed to attach: ' + err.message || err.toString?.()
|
toaster.danger(err.message || err.toString?.())
|
||||||
toaster.danger(message)
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -106,12 +102,12 @@ export default function WalletSettings () {
|
|||||||
|
|
||||||
function WalletFields ({ wallet: { config, fields, isConfigured } }) {
|
function WalletFields ({ wallet: { config, fields, isConfigured } }) {
|
||||||
return fields
|
return fields
|
||||||
.map(({ name, label, type, help, optional, editable, ...props }, i) => {
|
.map(({ name, label, type, help, optional, editable, clientOnly, serverOnly, ...props }, i) => {
|
||||||
const rawProps = {
|
const rawProps = {
|
||||||
...props,
|
...props,
|
||||||
name,
|
name,
|
||||||
initialValue: config?.[name],
|
initialValue: config?.[name],
|
||||||
readOnly: isConfigured && editable === false,
|
readOnly: isConfigured && editable === false && !!config?.[name],
|
||||||
groupClassName: props.hidden ? 'd-none' : undefined,
|
groupClassName: props.hidden ? 'd-none' : undefined,
|
||||||
label: label
|
label: label
|
||||||
? (
|
? (
|
||||||
|
24
prisma/migrations/20240729195320_lnbits_recv/migration.sql
Normal file
24
prisma/migrations/20240729195320_lnbits_recv/migration.sql
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "WalletType" ADD VALUE 'LNBITS';
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "WalletLNbits" (
|
||||||
|
"int" SERIAL NOT NULL,
|
||||||
|
"walletId" INTEGER NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"url" TEXT NOT NULL,
|
||||||
|
"invoiceKey" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "WalletLNbits_pkey" PRIMARY KEY ("int")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "WalletLNbits_walletId_key" ON "WalletLNbits"("walletId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "WalletLNbits" ADD CONSTRAINT "WalletLNbits_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
CREATE TRIGGER wallet_lnbits_as_jsonb
|
||||||
|
AFTER INSERT OR UPDATE ON "WalletLNbits"
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb();
|
14
prisma/migrations/20240808234214_sats_filter/migration.sql
Normal file
14
prisma/migrations/20240808234214_sats_filter/migration.sql
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Item" ADD COLUMN "cost" INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- use existing "ItemAct".act = FEE AND "Item"."userId" = "ItemAct"."userId" to calculate the cost for existing "Item"s
|
||||||
|
UPDATE "Item" SET "cost" = "ItemAct"."msats" / 1000
|
||||||
|
FROM "ItemAct"
|
||||||
|
WHERE "Item"."id" = "ItemAct"."itemId" AND "ItemAct"."act" = 'FEE' AND "Item"."userId" = "ItemAct"."userId";
|
||||||
|
|
||||||
|
ALTER TABLE "users" ADD COLUMN "satsFilter" INTEGER NOT NULL DEFAULT 10;
|
||||||
|
|
||||||
|
UPDATE "users" SET "satsFilter" = 0 WHERE "greeterMode";
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Item_cost_idx" ON "Item"("cost");
|
@ -0,0 +1,4 @@
|
|||||||
|
-- fix missing 'bio' marker for bios
|
||||||
|
UPDATE "Item" SET bio = 't' WHERE id IN (
|
||||||
|
SELECT "bioId" FROM users WHERE "bioId" IS NOT NULL
|
||||||
|
);
|
@ -54,7 +54,7 @@ model User {
|
|||||||
upvoteTrust Float @default(0)
|
upvoteTrust Float @default(0)
|
||||||
hideInvoiceDesc Boolean @default(false)
|
hideInvoiceDesc Boolean @default(false)
|
||||||
wildWestMode Boolean @default(false)
|
wildWestMode Boolean @default(false)
|
||||||
greeterMode Boolean @default(false)
|
satsFilter Int @default(10)
|
||||||
nsfwMode Boolean @default(false)
|
nsfwMode Boolean @default(false)
|
||||||
fiatCurrency String @default("USD")
|
fiatCurrency String @default("USD")
|
||||||
withdrawMaxFeeDefault Int @default(10)
|
withdrawMaxFeeDefault Int @default(10)
|
||||||
@ -66,6 +66,7 @@ model User {
|
|||||||
hideWalletBalance Boolean @default(false)
|
hideWalletBalance Boolean @default(false)
|
||||||
referrerId Int?
|
referrerId Int?
|
||||||
nostrPubkey String?
|
nostrPubkey String?
|
||||||
|
greeterMode Boolean @default(false)
|
||||||
nostrAuthPubkey String? @unique(map: "users.nostrAuthPubkey_unique")
|
nostrAuthPubkey String? @unique(map: "users.nostrAuthPubkey_unique")
|
||||||
nostrCrossposting Boolean @default(false)
|
nostrCrossposting Boolean @default(false)
|
||||||
slashtagId String? @unique(map: "users.slashtagId_unique")
|
slashtagId String? @unique(map: "users.slashtagId_unique")
|
||||||
@ -167,6 +168,7 @@ enum WalletType {
|
|||||||
LIGHTNING_ADDRESS
|
LIGHTNING_ADDRESS
|
||||||
LND
|
LND
|
||||||
CLN
|
CLN
|
||||||
|
LNBITS
|
||||||
}
|
}
|
||||||
|
|
||||||
model Wallet {
|
model Wallet {
|
||||||
@ -190,6 +192,7 @@ model Wallet {
|
|||||||
walletLightningAddress WalletLightningAddress?
|
walletLightningAddress WalletLightningAddress?
|
||||||
walletLND WalletLND?
|
walletLND WalletLND?
|
||||||
walletCLN WalletCLN?
|
walletCLN WalletCLN?
|
||||||
|
walletLNbits WalletLNbits?
|
||||||
withdrawals Withdrawl[]
|
withdrawals Withdrawl[]
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@ -238,6 +241,16 @@ model WalletCLN {
|
|||||||
cert String?
|
cert String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model WalletLNbits {
|
||||||
|
int Int @id @default(autoincrement())
|
||||||
|
walletId Int @unique
|
||||||
|
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||||
|
url String
|
||||||
|
invoiceKey String
|
||||||
|
}
|
||||||
|
|
||||||
model Mute {
|
model Mute {
|
||||||
muterId Int
|
muterId Int
|
||||||
mutedId Int
|
mutedId Int
|
||||||
@ -425,6 +438,7 @@ model Item {
|
|||||||
lastZapAt DateTime?
|
lastZapAt DateTime?
|
||||||
ncomments Int @default(0)
|
ncomments Int @default(0)
|
||||||
msats BigInt @default(0)
|
msats BigInt @default(0)
|
||||||
|
cost Int @default(0)
|
||||||
weightedDownVotes Float @default(0)
|
weightedDownVotes Float @default(0)
|
||||||
bio Boolean @default(false)
|
bio Boolean @default(false)
|
||||||
freebie Boolean @default(false)
|
freebie Boolean @default(false)
|
||||||
@ -489,6 +503,7 @@ model Item {
|
|||||||
@@index([weightedVotes], map: "Item.weightedVotes_index")
|
@@index([weightedVotes], map: "Item.weightedVotes_index")
|
||||||
@@index([invoiceId])
|
@@index([invoiceId])
|
||||||
@@index([invoiceActionState])
|
@@index([invoiceActionState])
|
||||||
|
@@index([cost])
|
||||||
}
|
}
|
||||||
|
|
||||||
// we use this to denormalize a user's aggregated interactions (zaps) with an item
|
// we use this to denormalize a user's aggregated interactions (zaps) with an item
|
||||||
|
193
wallets/index.js
193
wallets/index.js
@ -1,6 +1,6 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useMe } from '@/components/me'
|
import { useMe } from '@/components/me'
|
||||||
import useLocalConfig from '@/components/use-local-state'
|
import useClientConfig from '@/components/use-local-state'
|
||||||
import { useWalletLogger } from '@/components/wallet-logger'
|
import { useWalletLogger } from '@/components/wallet-logger'
|
||||||
import { SSR } from '@/lib/constants'
|
import { SSR } from '@/lib/constants'
|
||||||
import { bolt11Tags } from '@/lib/bolt11'
|
import { bolt11Tags } from '@/lib/bolt11'
|
||||||
@ -12,6 +12,7 @@ import { autowithdrawInitial } from '@/components/autowithdraw-shared'
|
|||||||
import { useShowModal } from '@/components/modal'
|
import { useShowModal } from '@/components/modal'
|
||||||
import { useToast } from '../components/toast'
|
import { useToast } from '../components/toast'
|
||||||
import { generateResolverName } from '@/lib/wallet'
|
import { generateResolverName } from '@/lib/wallet'
|
||||||
|
import { walletValidate } from '@/lib/validate'
|
||||||
|
|
||||||
export const Status = {
|
export const Status = {
|
||||||
Initialized: 'Initialized',
|
Initialized: 'Initialized',
|
||||||
@ -32,6 +33,22 @@ export function useWallet (name) {
|
|||||||
const hasConfig = wallet?.fields.length > 0
|
const hasConfig = wallet?.fields.length > 0
|
||||||
const _isConfigured = isConfigured({ ...wallet, config })
|
const _isConfigured = isConfigured({ ...wallet, config })
|
||||||
|
|
||||||
|
const enablePayments = useCallback(() => {
|
||||||
|
enableWallet(name, me)
|
||||||
|
logger.ok('payments enabled')
|
||||||
|
}, [name, me, logger])
|
||||||
|
|
||||||
|
const disablePayments = useCallback(() => {
|
||||||
|
disableWallet(name, me)
|
||||||
|
logger.info('payments disabled')
|
||||||
|
}, [name, me, logger])
|
||||||
|
|
||||||
|
if (wallet) {
|
||||||
|
wallet.isConfigured = _isConfigured
|
||||||
|
wallet.enablePayments = enablePayments
|
||||||
|
wallet.disablePayments = disablePayments
|
||||||
|
}
|
||||||
|
|
||||||
const status = config?.enabled ? Status.Enabled : Status.Initialized
|
const status = config?.enabled ? Status.Enabled : Status.Initialized
|
||||||
const enabled = status === Status.Enabled
|
const enabled = status === Status.Enabled
|
||||||
const priority = config?.priority
|
const priority = config?.priority
|
||||||
@ -49,53 +66,40 @@ export function useWallet (name) {
|
|||||||
}
|
}
|
||||||
}, [me, wallet, config, logger, status])
|
}, [me, wallet, config, logger, status])
|
||||||
|
|
||||||
const enable = useCallback(() => {
|
|
||||||
enableWallet(name, me)
|
|
||||||
logger.ok('wallet enabled')
|
|
||||||
}, [name, me, logger])
|
|
||||||
|
|
||||||
const disable = useCallback(() => {
|
|
||||||
disableWallet(name, me)
|
|
||||||
logger.info('wallet disabled')
|
|
||||||
}, [name, me, logger])
|
|
||||||
|
|
||||||
const setPriority = useCallback(async (priority) => {
|
const setPriority = useCallback(async (priority) => {
|
||||||
if (_isConfigured && priority !== config.priority) {
|
if (_isConfigured && priority !== config.priority) {
|
||||||
try {
|
try {
|
||||||
await saveConfig({ ...config, priority })
|
await saveConfig({ ...config, priority }, { logger })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toaster.danger(`failed to change priority of ${wallet.name} wallet: ${err.message}`)
|
toaster.danger(`failed to change priority of ${wallet.name} wallet: ${err.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [wallet, config, logger, toaster])
|
}, [wallet, config, toaster])
|
||||||
|
|
||||||
const save = useCallback(async (newConfig) => {
|
const save = useCallback(async (newConfig) => {
|
||||||
|
// testConnectClient should log custom INFO and OK message
|
||||||
|
// testConnectClient is optional since validation might happen during save on server
|
||||||
|
// TODO: add timeout
|
||||||
|
let validConfig
|
||||||
try {
|
try {
|
||||||
// testConnectClient should log custom INFO and OK message
|
validConfig = await wallet.testConnectClient?.(newConfig, { me, logger })
|
||||||
// testConnectClient is optional since validation might happen during save on server
|
|
||||||
// TODO: add timeout
|
|
||||||
const validConfig = await wallet.testConnectClient?.(newConfig, { me, logger })
|
|
||||||
await saveConfig(validConfig ?? newConfig)
|
|
||||||
logger.ok(_isConfigured ? 'wallet updated' : 'wallet attached')
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err.message || err.toString?.()
|
logger.error(err.message)
|
||||||
logger.error('failed to attach: ' + message)
|
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}, [_isConfigured, saveConfig, me, logger])
|
await saveConfig(validConfig ?? newConfig, { logger })
|
||||||
|
}, [saveConfig, me, logger])
|
||||||
|
|
||||||
// delete is a reserved keyword
|
// delete is a reserved keyword
|
||||||
const delete_ = useCallback(async () => {
|
const delete_ = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await clearConfig()
|
await clearConfig({ logger })
|
||||||
logger.ok('wallet detached')
|
|
||||||
disable()
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err.message || err.toString?.()
|
const message = err.message || err.toString?.()
|
||||||
logger.error(message)
|
logger.error(message)
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}, [clearConfig, logger, disable])
|
}, [clearConfig, logger, disablePayments])
|
||||||
|
|
||||||
if (!wallet) return null
|
if (!wallet) return null
|
||||||
|
|
||||||
@ -106,11 +110,8 @@ export function useWallet (name) {
|
|||||||
save,
|
save,
|
||||||
delete: delete_,
|
delete: delete_,
|
||||||
deleteLogs,
|
deleteLogs,
|
||||||
enable,
|
|
||||||
disable,
|
|
||||||
setPriority,
|
setPriority,
|
||||||
hasConfig,
|
hasConfig,
|
||||||
isConfigured: _isConfigured,
|
|
||||||
status,
|
status,
|
||||||
enabled,
|
enabled,
|
||||||
priority,
|
priority,
|
||||||
@ -118,34 +119,111 @@ export function useWallet (name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractConfig (fields, config, client) {
|
||||||
|
return Object.entries(config).reduce((acc, [key, value]) => {
|
||||||
|
const field = fields.find(({ name }) => name === key)
|
||||||
|
|
||||||
|
// filter server config which isn't specified as wallet fields
|
||||||
|
if (client && (key.startsWith('autoWithdraw') || key === 'id')) return acc
|
||||||
|
|
||||||
|
// field might not exist because config.enabled doesn't map to a wallet field
|
||||||
|
if (!field || (client ? isClientField(field) : isServerField(field))) {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[key]: value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isServerField (f) {
|
||||||
|
return f.serverOnly || !f.clientOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isClientField (f) {
|
||||||
|
return f.clientOnly || !f.serverOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractClientConfig (fields, config) {
|
||||||
|
return extractConfig(fields, config, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractServerConfig (fields, config) {
|
||||||
|
return extractConfig(fields, config, false)
|
||||||
|
}
|
||||||
|
|
||||||
function useConfig (wallet) {
|
function useConfig (wallet) {
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
|
|
||||||
const storageKey = getStorageKey(wallet?.name, me)
|
const storageKey = getStorageKey(wallet?.name, me)
|
||||||
const [localConfig, setLocalConfig, clearLocalConfig] = useLocalConfig(storageKey)
|
const [clientConfig, setClientConfig, clearClientConfig] = useClientConfig(storageKey)
|
||||||
|
|
||||||
const [serverConfig, setServerConfig, clearServerConfig] = useServerConfig(wallet)
|
const [serverConfig, setServerConfig, clearServerConfig] = useServerConfig(wallet)
|
||||||
|
|
||||||
const hasLocalConfig = !!wallet?.sendPayment
|
const hasClientConfig = !!wallet?.sendPayment
|
||||||
const hasServerConfig = !!wallet?.walletType
|
const hasServerConfig = !!wallet?.walletType
|
||||||
|
|
||||||
const config = {
|
let config = {}
|
||||||
// only include config if it makes sense for this wallet
|
if (hasClientConfig) config = clientConfig
|
||||||
// since server config always returns default values for autowithdraw settings
|
if (hasServerConfig) {
|
||||||
// which might be confusing to have for wallets that don't support autowithdraw
|
const { enabled } = config || {}
|
||||||
...(hasLocalConfig ? localConfig : {}),
|
config = {
|
||||||
...(hasServerConfig ? serverConfig : {})
|
...config,
|
||||||
|
...serverConfig
|
||||||
|
}
|
||||||
|
// wallet is enabled if enabled is set in client or server config
|
||||||
|
config.enabled ||= enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveConfig = useCallback(async (config) => {
|
const saveConfig = useCallback(async (newConfig, { logger }) => {
|
||||||
if (hasLocalConfig) setLocalConfig(config)
|
// NOTE:
|
||||||
if (hasServerConfig) await setServerConfig(config)
|
// verifying the client/server configuration before saving it
|
||||||
}, [wallet])
|
// prevents unsetting just one configuration if both are set.
|
||||||
|
// This means there is no way of unsetting just one configuration
|
||||||
|
// since 'detach' detaches both.
|
||||||
|
// Not optimal UX but the trade-off is saving invalid configurations
|
||||||
|
// and maybe it's not that big of an issue.
|
||||||
|
if (hasClientConfig) {
|
||||||
|
const newClientConfig = extractClientConfig(wallet.fields, newConfig)
|
||||||
|
|
||||||
const clearConfig = useCallback(async () => {
|
let valid = true
|
||||||
if (hasLocalConfig) clearLocalConfig()
|
try {
|
||||||
|
await walletValidate(wallet, newClientConfig)
|
||||||
|
} catch {
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
setClientConfig(newClientConfig)
|
||||||
|
logger.ok(wallet.isConfigured ? 'payment details updated' : 'wallet attached for payments')
|
||||||
|
if (newConfig.enabled) wallet.enablePayments()
|
||||||
|
else wallet.disablePayments()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasServerConfig) {
|
||||||
|
const newServerConfig = extractServerConfig(wallet.fields, newConfig)
|
||||||
|
|
||||||
|
let valid = true
|
||||||
|
try {
|
||||||
|
await walletValidate(wallet, newServerConfig)
|
||||||
|
} catch {
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valid) await setServerConfig(newServerConfig)
|
||||||
|
}
|
||||||
|
}, [hasClientConfig, hasServerConfig, setClientConfig, setServerConfig, wallet])
|
||||||
|
|
||||||
|
const clearConfig = useCallback(async ({ logger }) => {
|
||||||
|
if (hasClientConfig) {
|
||||||
|
clearClientConfig()
|
||||||
|
wallet.disablePayments()
|
||||||
|
logger.ok('wallet detached for payments')
|
||||||
|
}
|
||||||
if (hasServerConfig) await clearServerConfig()
|
if (hasServerConfig) await clearServerConfig()
|
||||||
}, [wallet])
|
}, [hasClientConfig, hasServerConfig, clearClientConfig, clearServerConfig, wallet])
|
||||||
|
|
||||||
return [config, saveConfig, clearConfig]
|
return [config, saveConfig, clearConfig]
|
||||||
}
|
}
|
||||||
@ -174,6 +252,8 @@ function useServerConfig (wallet) {
|
|||||||
enabled: data?.walletByType?.enabled,
|
enabled: data?.walletByType?.enabled,
|
||||||
...data?.walletByType?.wallet
|
...data?.walletByType?.wallet
|
||||||
}
|
}
|
||||||
|
delete serverConfig.__typename
|
||||||
|
|
||||||
const autowithdrawSettings = autowithdrawInitial({ me })
|
const autowithdrawSettings = autowithdrawInitial({ me })
|
||||||
const config = { ...serverConfig, ...autowithdrawSettings }
|
const config = { ...serverConfig, ...autowithdrawSettings }
|
||||||
|
|
||||||
@ -189,8 +269,8 @@ function useServerConfig (wallet) {
|
|||||||
return await client.mutate({
|
return await client.mutate({
|
||||||
mutation,
|
mutation,
|
||||||
variables: {
|
variables: {
|
||||||
id: walletId,
|
|
||||||
...config,
|
...config,
|
||||||
|
id: walletId,
|
||||||
settings: {
|
settings: {
|
||||||
autoWithdrawThreshold: Number(autoWithdrawThreshold),
|
autoWithdrawThreshold: Number(autoWithdrawThreshold),
|
||||||
autoWithdrawMaxFeePercent: Number(autoWithdrawMaxFeePercent),
|
autoWithdrawMaxFeePercent: Number(autoWithdrawMaxFeePercent),
|
||||||
@ -206,6 +286,9 @@ function useServerConfig (wallet) {
|
|||||||
}, [client, walletId])
|
}, [client, walletId])
|
||||||
|
|
||||||
const clearConfig = useCallback(async () => {
|
const clearConfig = useCallback(async () => {
|
||||||
|
// only remove wallet if there is a wallet to remove
|
||||||
|
if (!walletId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.mutate({
|
await client.mutate({
|
||||||
mutation: REMOVE_WALLET,
|
mutation: REMOVE_WALLET,
|
||||||
@ -224,17 +307,21 @@ function generateMutation (wallet) {
|
|||||||
const resolverName = generateResolverName(wallet.walletField)
|
const resolverName = generateResolverName(wallet.walletField)
|
||||||
|
|
||||||
let headerArgs = '$id: ID, '
|
let headerArgs = '$id: ID, '
|
||||||
headerArgs += wallet.fields.map(f => {
|
headerArgs += wallet.fields
|
||||||
let arg = `$${f.name}: String`
|
.filter(isServerField)
|
||||||
if (!f.optional) {
|
.map(f => {
|
||||||
arg += '!'
|
let arg = `$${f.name}: String`
|
||||||
}
|
if (!f.optional) {
|
||||||
return arg
|
arg += '!'
|
||||||
}).join(', ')
|
}
|
||||||
|
return arg
|
||||||
|
}).join(', ')
|
||||||
headerArgs += ', $settings: AutowithdrawSettings!'
|
headerArgs += ', $settings: AutowithdrawSettings!'
|
||||||
|
|
||||||
let inputArgs = 'id: $id, '
|
let inputArgs = 'id: $id, '
|
||||||
inputArgs += wallet.fields.map(f => `${f.name}: $${f.name}`).join(', ')
|
inputArgs += wallet.fields
|
||||||
|
.filter(isServerField)
|
||||||
|
.map(f => `${f.name}: $${f.name}`).join(', ')
|
||||||
inputArgs += ', settings: $settings'
|
inputArgs += ', settings: $settings'
|
||||||
|
|
||||||
return gql`mutation ${resolverName}(${headerArgs}) {
|
return gql`mutation ${resolverName}(${headerArgs}) {
|
||||||
|
27
wallets/lnbits/ATTACH.md
Normal file
27
wallets/lnbits/ATTACH.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
For testing LNbits, you need to create a LNbits account first via the web interface.
|
||||||
|
|
||||||
|
By default, you can access it at `localhost:5001` (see `LNBITS_WEB_PORT` in .env.development).
|
||||||
|
|
||||||
|
After you created a wallet, you should find the invoice and admin key under `Node URL, API keys and API docs`.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
>
|
||||||
|
> Since your browser is running on your host machine but the server is running inside a docker container, the server will not be able to reach LNbits with `localhost:5001` to create invoices. This makes it hard to test send+receive at the same time.
|
||||||
|
>
|
||||||
|
> For now, you need to patch the `_createInvoice` function in wallets/lnbits/server.js to always use `lnbits:5000` as the URL:
|
||||||
|
>
|
||||||
|
> ```diff
|
||||||
|
> diff --git a/wallets/lnbits/server.js b/wallets/lnbits/server.js
|
||||||
|
> index 39949775..e3605c45 100644
|
||||||
|
> --- a/wallets/lnbits/server.js
|
||||||
|
> +++ b/wallets/lnbits/server.js
|
||||||
|
> @@ -11,6 +11,7 @@ async function _createInvoice ({ url, invoiceKey, amount, expiry }, { me }) {
|
||||||
|
> const memo = me.hideInvoiceDesc ? undefined : 'autowithdraw to LNbits from SN'
|
||||||
|
> const body = JSON.stringify({ amount, unit: 'sat', expiry, memo, out: false })
|
||||||
|
>
|
||||||
|
> + url = 'http://lnbits:5000'
|
||||||
|
> const res = await fetch(url + path, { method: 'POST', headers, body })
|
||||||
|
> if (!res.ok) {
|
||||||
|
> const errBody = await res.json()
|
||||||
|
> ```
|
||||||
|
>
|
@ -1,10 +1,10 @@
|
|||||||
export * from 'wallets/lnbits'
|
export * from 'wallets/lnbits'
|
||||||
|
|
||||||
export async function testConnectClient ({ url, adminKey }, { logger }) {
|
export async function testConnectClient ({ url, adminKey, invoiceKey }, { logger }) {
|
||||||
logger.info('trying to fetch wallet')
|
logger.info('trying to fetch wallet')
|
||||||
|
|
||||||
url = url.replace(/\/+$/, '')
|
url = url.replace(/\/+$/, '')
|
||||||
await getWallet({ url, adminKey })
|
await getWallet({ url, adminKey, invoiceKey })
|
||||||
|
|
||||||
logger.ok('wallet found')
|
logger.ok('wallet found')
|
||||||
}
|
}
|
||||||
@ -23,13 +23,13 @@ export async function sendPayment (bolt11, { url, adminKey }) {
|
|||||||
return { preimage }
|
return { preimage }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWallet ({ url, adminKey }) {
|
async function getWallet ({ url, adminKey, invoiceKey }) {
|
||||||
const path = '/api/v1/wallet'
|
const path = '/api/v1/wallet'
|
||||||
|
|
||||||
const headers = new Headers()
|
const headers = new Headers()
|
||||||
headers.append('Accept', 'application/json')
|
headers.append('Accept', 'application/json')
|
||||||
headers.append('Content-Type', 'application/json')
|
headers.append('Content-Type', 'application/json')
|
||||||
headers.append('X-Api-Key', adminKey)
|
headers.append('X-Api-Key', adminKey || invoiceKey)
|
||||||
|
|
||||||
const res = await fetch(url + path, { method: 'GET', headers })
|
const res = await fetch(url + path, { method: 'GET', headers })
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
@ -8,17 +8,32 @@ export const fields = [
|
|||||||
label: 'lnbits url',
|
label: 'lnbits url',
|
||||||
type: 'text'
|
type: 'text'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'invoiceKey',
|
||||||
|
label: 'invoice key',
|
||||||
|
type: 'password',
|
||||||
|
optional: 'for receiving',
|
||||||
|
serverOnly: true,
|
||||||
|
editable: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'adminKey',
|
name: 'adminKey',
|
||||||
label: 'admin key',
|
label: 'admin key',
|
||||||
type: 'password'
|
type: 'password',
|
||||||
|
optional: 'for sending',
|
||||||
|
clientOnly: true,
|
||||||
|
editable: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export const card = {
|
export const card = {
|
||||||
title: 'LNbits',
|
title: 'LNbits',
|
||||||
subtitle: 'use [LNbits](https://lnbits.com/) for payments',
|
subtitle: 'use [LNbits](https://lnbits.com/) for payments',
|
||||||
badges: ['send only']
|
badges: ['send & receive']
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fieldValidation = lnbitsSchema
|
export const fieldValidation = lnbitsSchema
|
||||||
|
|
||||||
|
export const walletType = 'LNBITS'
|
||||||
|
|
||||||
|
export const walletField = 'walletLNbits'
|
||||||
|
30
wallets/lnbits/server.js
Normal file
30
wallets/lnbits/server.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export * from 'wallets/lnbits'
|
||||||
|
|
||||||
|
async function _createInvoice ({ url, invoiceKey, amount, expiry }, { me }) {
|
||||||
|
const path = '/api/v1/payments'
|
||||||
|
|
||||||
|
const headers = new Headers()
|
||||||
|
headers.append('Accept', 'application/json')
|
||||||
|
headers.append('Content-Type', 'application/json')
|
||||||
|
headers.append('X-Api-Key', invoiceKey)
|
||||||
|
|
||||||
|
const memo = me.hideInvoiceDesc ? undefined : 'autowithdraw to LNbits from SN'
|
||||||
|
const body = JSON.stringify({ amount, unit: 'sat', expiry, memo, out: false })
|
||||||
|
|
||||||
|
const res = await fetch(url + path, { method: 'POST', headers, body })
|
||||||
|
if (!res.ok) {
|
||||||
|
const errBody = await res.json()
|
||||||
|
throw new Error(errBody.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
const payment = await res.json()
|
||||||
|
return payment.payment_request
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testConnectServer ({ url, invoiceKey }, { me }) {
|
||||||
|
return await _createInvoice({ url, invoiceKey, amount: 1, expiry: 1 }, { me })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createInvoice ({ amount, maxFee }, { url, invoiceKey }, { me }) {
|
||||||
|
return await _createInvoice({ url, invoiceKey, amount, expiry: 360 }, { me })
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import * as lnd from 'wallets/lnd/server'
|
import * as lnd from 'wallets/lnd/server'
|
||||||
import * as cln from 'wallets/cln/server'
|
import * as cln from 'wallets/cln/server'
|
||||||
import * as lnAddr from 'wallets/lightning-address/server'
|
import * as lnAddr from 'wallets/lightning-address/server'
|
||||||
|
import * as lnbits from 'wallets/lnbits/server'
|
||||||
|
|
||||||
export default [lnd, cln, lnAddr]
|
export default [lnd, cln, lnAddr, lnbits]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user