refactor wallet validation
This commit is contained in:
parent
57603a936f
commit
e96982c353
|
@ -1,4 +1,4 @@
|
|||
import { inviteSchema, ssValidate } from '@/lib/validate'
|
||||
import { inviteSchema, validateSchema } from '@/lib/validate'
|
||||
import { msatsToSats } from '@/lib/format'
|
||||
import assertApiKeyNotPermitted from './apiKey'
|
||||
import { GqlAuthenticationError } from '@/lib/error'
|
||||
|
@ -35,7 +35,7 @@ export default {
|
|||
}
|
||||
assertApiKeyNotPermitted({ me })
|
||||
|
||||
await ssValidate(inviteSchema, { gift, limit })
|
||||
await validateSchema(inviteSchema, { gift, limit })
|
||||
|
||||
return await models.invite.create({
|
||||
data: { gift, limit, userId: me.id }
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
import { msatsToSats } from '@/lib/format'
|
||||
import { parse } from 'tldts'
|
||||
import uu from 'url-unshort'
|
||||
import { actSchema, advSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '@/lib/validate'
|
||||
import { actSchema, advSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, validateSchema } from '@/lib/validate'
|
||||
import { defaultCommentSort, isJob, deleteItemByAuthor } from '@/lib/item'
|
||||
import { datePivot, whenRange } from '@/lib/time'
|
||||
import { uploadIdsFromText } from './upload'
|
||||
|
@ -844,7 +844,7 @@ export default {
|
|||
return await deleteItemByAuthor({ models, id, item: old })
|
||||
},
|
||||
upsertLink: async (parent, { id, ...item }, { me, models, lnd }) => {
|
||||
await ssValidate(linkSchema, item, { models, me })
|
||||
await validateSchema(linkSchema, item, { models, me })
|
||||
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, ...item }, { me, models, lnd })
|
||||
|
@ -853,7 +853,7 @@ export default {
|
|||
}
|
||||
},
|
||||
upsertDiscussion: async (parent, { id, ...item }, { me, models, lnd }) => {
|
||||
await ssValidate(discussionSchema, item, { models, me })
|
||||
await validateSchema(discussionSchema, item, { models, me })
|
||||
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, ...item }, { me, models, lnd })
|
||||
|
@ -862,7 +862,7 @@ export default {
|
|||
}
|
||||
},
|
||||
upsertBounty: async (parent, { id, ...item }, { me, models, lnd }) => {
|
||||
await ssValidate(bountySchema, item, { models, me })
|
||||
await validateSchema(bountySchema, item, { models, me })
|
||||
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, ...item }, { me, models, lnd })
|
||||
|
@ -879,7 +879,7 @@ export default {
|
|||
})
|
||||
: 0
|
||||
|
||||
await ssValidate(pollSchema, item, { models, me, numExistingChoices })
|
||||
await validateSchema(pollSchema, item, { models, me, numExistingChoices })
|
||||
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, ...item }, { me, models, lnd })
|
||||
|
@ -894,7 +894,7 @@ export default {
|
|||
}
|
||||
|
||||
item.location = item.location?.toLowerCase() === 'remote' ? undefined : item.location
|
||||
await ssValidate(jobSchema, item, { models })
|
||||
await validateSchema(jobSchema, item, { models })
|
||||
if (item.logo !== undefined) {
|
||||
item.uploadId = item.logo
|
||||
delete item.logo
|
||||
|
@ -907,7 +907,7 @@ export default {
|
|||
}
|
||||
},
|
||||
upsertComment: async (parent, { id, ...item }, { me, models, lnd }) => {
|
||||
await ssValidate(commentSchema, item)
|
||||
await validateSchema(commentSchema, item)
|
||||
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, ...item }, { me, models, lnd })
|
||||
|
@ -937,7 +937,7 @@ export default {
|
|||
},
|
||||
act: async (parent, { id, sats, act = 'TIP' }, { me, models, lnd, headers }) => {
|
||||
assertApiKeyNotPermitted({ me })
|
||||
await ssValidate(actSchema, { sats, act })
|
||||
await validateSchema(actSchema, { sats, act })
|
||||
await assertGofacYourself({ models, headers })
|
||||
|
||||
const [item] = await models.$queryRawUnsafe(`
|
||||
|
@ -1369,7 +1369,7 @@ export const updateItem = async (parent, { sub: subName, forward, hash, hmac, ..
|
|||
}
|
||||
|
||||
// in case they lied about their existing boost
|
||||
await ssValidate(advSchema, { boost: item.boost }, { models, me, existingBoost: old.boost })
|
||||
await validateSchema(advSchema, { boost: item.boost }, { models, me, existingBoost: old.boost })
|
||||
|
||||
const user = await models.user.findUnique({ where: { id: meId } })
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { decodeCursor, LIMIT, nextNoteCursorEncoded } from '@/lib/cursor'
|
||||
import { getItem, filterClause, whereClause, muteClause, activeOrMine } from './item'
|
||||
import { getInvoice, getWithdrawl } from './wallet'
|
||||
import { pushSubscriptionSchema, ssValidate } from '@/lib/validate'
|
||||
import { pushSubscriptionSchema, validateSchema } from '@/lib/validate'
|
||||
import { replyToSubscription } from '@/lib/webPush'
|
||||
import { getSub } from './sub'
|
||||
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
|
||||
|
@ -375,7 +375,7 @@ export default {
|
|||
throw new GqlAuthenticationError()
|
||||
}
|
||||
|
||||
await ssValidate(pushSubscriptionSchema, { endpoint, p256dh, auth })
|
||||
await validateSchema(pushSubscriptionSchema, { endpoint, p256dh, auth })
|
||||
|
||||
let dbPushSubscription
|
||||
if (oldEndpoint) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { amountSchema, ssValidate } from '@/lib/validate'
|
||||
import { amountSchema, validateSchema } from '@/lib/validate'
|
||||
import { getAd, getItem } from './item'
|
||||
import { topUsers } from './user'
|
||||
import performPaidAction from '../paidAction'
|
||||
|
@ -171,7 +171,7 @@ export default {
|
|||
},
|
||||
Mutation: {
|
||||
donateToRewards: async (parent, { sats }, { me, models, lnd }) => {
|
||||
await ssValidate(amountSchema, { amount: sats })
|
||||
await validateSchema(amountSchema, { amount: sats })
|
||||
|
||||
return await performPaidAction('DONATE', { sats }, { me, models, lnd })
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { whenRange } from '@/lib/time'
|
||||
import { ssValidate, territorySchema } from '@/lib/validate'
|
||||
import { validateSchema, territorySchema } from '@/lib/validate'
|
||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
|
||||
import { viewGroup } from './growth'
|
||||
import { notifyTerritoryTransfer } from '@/lib/webPush'
|
||||
|
@ -157,7 +157,7 @@ export default {
|
|||
throw new GqlAuthenticationError()
|
||||
}
|
||||
|
||||
await ssValidate(territorySchema, data, { models, me, sub: { name: data.oldName } })
|
||||
await validateSchema(territorySchema, data, { models, me, sub: { name: data.oldName } })
|
||||
|
||||
if (data.oldName) {
|
||||
return await updateSub(parent, data, { me, models, lnd })
|
||||
|
@ -260,7 +260,7 @@ export default {
|
|||
|
||||
const { name } = data
|
||||
|
||||
await ssValidate(territorySchema, data, { models, me })
|
||||
await validateSchema(territorySchema, data, { models, me })
|
||||
|
||||
const oldSub = await models.sub.findUnique({ where: { name } })
|
||||
if (!oldSub) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { readFile } from 'fs/promises'
|
|||
import { join, resolve } from 'path'
|
||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
|
||||
import { msatsToSats } from '@/lib/format'
|
||||
import { bioSchema, emailSchema, settingsSchema, ssValidate, userSchema } from '@/lib/validate'
|
||||
import { bioSchema, emailSchema, settingsSchema, validateSchema, userSchema } from '@/lib/validate'
|
||||
import { getItem, updateItem, filterClause, createItem, whereClause, muteClause, activeOrMine } from './item'
|
||||
import { USER_ID, RESERVED_MAX_USER_ID, SN_NO_REWARDS_IDS, INVOICE_ACTION_NOTIFICATION_TYPES } from '@/lib/constants'
|
||||
import { viewGroup } from './growth'
|
||||
|
@ -632,7 +632,7 @@ export default {
|
|||
throw new GqlAuthenticationError()
|
||||
}
|
||||
|
||||
await ssValidate(userSchema, data, { models })
|
||||
await validateSchema(userSchema, data, { models })
|
||||
|
||||
try {
|
||||
await models.user.update({ where: { id: me.id }, data })
|
||||
|
@ -649,7 +649,7 @@ export default {
|
|||
throw new GqlAuthenticationError()
|
||||
}
|
||||
|
||||
await ssValidate(settingsSchema, { nostrRelays, ...data })
|
||||
await validateSchema(settingsSchema, { nostrRelays, ...data })
|
||||
|
||||
if (nostrRelays?.length) {
|
||||
const connectOrCreate = []
|
||||
|
@ -696,7 +696,7 @@ export default {
|
|||
throw new GqlAuthenticationError()
|
||||
}
|
||||
|
||||
await ssValidate(bioSchema, { text })
|
||||
await validateSchema(bioSchema, { text })
|
||||
|
||||
const user = await models.user.findUnique({ where: { id: me.id } })
|
||||
|
||||
|
@ -770,7 +770,7 @@ export default {
|
|||
}
|
||||
assertApiKeyNotPermitted({ me })
|
||||
|
||||
await ssValidate(emailSchema, { email })
|
||||
await validateSchema(emailSchema, { email })
|
||||
|
||||
try {
|
||||
await models.user.update({
|
||||
|
|
|
@ -12,7 +12,7 @@ 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, LND_PATHFINDING_TIMEOUT_MS
|
||||
} from '@/lib/constants'
|
||||
import { amountSchema, ssValidate, withdrawlSchema, lnAddrSchema, walletValidate } from '@/lib/validate'
|
||||
import { amountSchema, validateSchema, withdrawlSchema, lnAddrSchema } from '@/lib/validate'
|
||||
import { datePivot } from '@/lib/time'
|
||||
import assertGofacYourself from './ofac'
|
||||
import assertApiKeyNotPermitted from './apiKey'
|
||||
|
@ -23,6 +23,7 @@ import { generateResolverName, generateTypeDefName } from '@/wallets/graphql'
|
|||
import { lnAddrOptions } from '@/lib/lnurl'
|
||||
import { GqlAuthenticationError, GqlAuthorizationError, GqlInputError } from '@/lib/error'
|
||||
import { getNodeSockets, getOurPubkey } from '../lnd'
|
||||
import validateWallet from '@/wallets/validate'
|
||||
|
||||
function injectResolvers (resolvers) {
|
||||
console.group('injected GraphQL resolvers:')
|
||||
|
@ -32,7 +33,7 @@ function injectResolvers (resolvers) {
|
|||
resolvers.Mutation[resolverName] = async (parent, { settings, validateLightning, vaultEntries, ...data }, { me, models }) => {
|
||||
// allow transformation of the data on validation (this is optional ... won't do anything if not implemented)
|
||||
// TODO: our validation should be improved
|
||||
const validData = await walletValidate(walletDef, { ...data, ...settings, vaultEntries })
|
||||
const validData = await validateWallet(walletDef, { ...data, ...settings, vaultEntries })
|
||||
if (validData) {
|
||||
Object.keys(validData).filter(key => key in data).forEach(key => { data[key] = validData[key] })
|
||||
Object.keys(validData).filter(key => key in settings).forEach(key => { settings[key] = validData[key] })
|
||||
|
@ -437,7 +438,7 @@ const resolvers = {
|
|||
},
|
||||
Mutation: {
|
||||
createInvoice: async (parent, { amount, hodlInvoice = false, expireSecs = 3600 }, { me, models, lnd, headers }) => {
|
||||
await ssValidate(amountSchema, { amount })
|
||||
await validateSchema(amountSchema, { amount })
|
||||
await assertGofacYourself({ models, headers })
|
||||
|
||||
let expirePivot = { seconds: expireSecs }
|
||||
|
@ -783,7 +784,7 @@ async function upsertWallet (
|
|||
|
||||
export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, headers, walletId = null }) {
|
||||
assertApiKeyNotPermitted({ me })
|
||||
await ssValidate(withdrawlSchema, { invoice, maxFee })
|
||||
await validateSchema(withdrawlSchema, { invoice, maxFee })
|
||||
await assertGofacYourself({ models, headers })
|
||||
|
||||
// remove 'lightning:' prefix if present
|
||||
|
@ -867,7 +868,7 @@ export async function fetchLnAddrInvoice (
|
|||
me, models, lnd, autoWithdraw = false
|
||||
}) {
|
||||
const options = await lnAddrOptions(addr)
|
||||
await ssValidate(lnAddrSchema, { addr, amount, maxFee, comment, ...payer }, options)
|
||||
await validateSchema(lnAddrSchema, { addr, amount, maxFee, comment, ...payer }, options)
|
||||
|
||||
if (payer) {
|
||||
payer = {
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { InputGroup } from 'react-bootstrap'
|
||||
import { Checkbox, Input } from './form'
|
||||
import { Input } from './form'
|
||||
import { useMe } from './me'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { isNumber } from '@/lib/validate'
|
||||
import { useIsClient } from './use-client'
|
||||
import Link from 'next/link'
|
||||
import { isConfigured } from '@/wallets/common'
|
||||
|
||||
function autoWithdrawThreshold ({ me }) {
|
||||
return isNumber(me?.privates?.autoWithdrawThreshold) ? me?.privates?.autoWithdrawThreshold : 10000
|
||||
|
@ -19,7 +17,7 @@ export function autowithdrawInitial ({ me }) {
|
|||
}
|
||||
}
|
||||
|
||||
export function AutowithdrawSettings ({ wallet }) {
|
||||
export function AutowithdrawSettings () {
|
||||
const { me } = useMe()
|
||||
const threshold = autoWithdrawThreshold({ me })
|
||||
|
||||
|
@ -29,16 +27,8 @@ export function AutowithdrawSettings ({ wallet }) {
|
|||
setSendThreshold(Math.max(Math.floor(threshold / 10), 1))
|
||||
}, [autoWithdrawThreshold])
|
||||
|
||||
const isClient = useIsClient()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Checkbox
|
||||
disabled={isClient && !isConfigured(wallet)}
|
||||
label='enabled'
|
||||
id='enabled'
|
||||
name='enabled'
|
||||
/>
|
||||
<div className='my-4 border border-3 rounded'>
|
||||
<div className='p-3'>
|
||||
<h3 className='text-center text-muted'>desired balance</h3>
|
||||
|
|
417
lib/validate.js
417
lib/validate.js
|
@ -1,4 +1,4 @@
|
|||
import { string, ValidationError, number, object, array, addMethod, boolean, date } from 'yup'
|
||||
import { string, ValidationError, number, object, array, boolean, date } from './yup'
|
||||
import {
|
||||
BOOST_MIN, MAX_POLL_CHOICE_LENGTH, MAX_TITLE_LENGTH, MAX_POLL_NUM_CHOICES,
|
||||
MIN_POLL_NUM_CHOICES, MAX_FORWARDS, BOOST_MULT, MAX_TERRITORY_DESC_LENGTH, POST_TYPES,
|
||||
|
@ -6,19 +6,16 @@ import {
|
|||
} from './constants'
|
||||
import { SUPPORTED_CURRENCIES } from './currency'
|
||||
import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './nostr'
|
||||
import { msatsToSats, numWithUnits, abbrNum, ensureB64, B64_URL_REGEX, HEX_REGEX } from './format'
|
||||
import { msatsToSats, numWithUnits, abbrNum } from './format'
|
||||
import * as usersFragments from '@/fragments/users'
|
||||
import * as subsFragments from '@/fragments/subs'
|
||||
import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon'
|
||||
import { TOR_REGEXP, parseNwcUrl } from './url'
|
||||
import { datePivot } from './time'
|
||||
import { decodeRune } from '@/lib/cln'
|
||||
import bip39Words from './bip39-words'
|
||||
|
||||
const { SUB } = subsFragments
|
||||
const { NAME_QUERY } = usersFragments
|
||||
|
||||
export async function ssValidate (schema, data, args) {
|
||||
export async function validateSchema (schema, data, args) {
|
||||
try {
|
||||
if (typeof schema === 'function') {
|
||||
return await schema(args).validate(data)
|
||||
|
@ -33,159 +30,6 @@ export async function ssValidate (schema, data, args) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function formikValidate (validate, data) {
|
||||
const result = await validate(data)
|
||||
if (Object.keys(result).length > 0) {
|
||||
const [key, message] = Object.entries(result)[0]
|
||||
throw new Error(`${key}: ${message}`)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export async function walletValidate (walletDef, data) {
|
||||
if (typeof walletDef.fieldValidation === 'function') {
|
||||
return await formikValidate(walletDef.fieldValidation, data)
|
||||
} else {
|
||||
return await ssValidate(walletDef.fieldValidation, data)
|
||||
}
|
||||
}
|
||||
|
||||
addMethod(string, 'or', function (schemas, msg) {
|
||||
return this.test({
|
||||
name: 'or',
|
||||
message: msg,
|
||||
test: value => {
|
||||
if (Array.isArray(schemas) && schemas.length > 1) {
|
||||
const resee = schemas.map(schema => schema.isValidSync(value))
|
||||
return resee.some(res => res)
|
||||
} else {
|
||||
throw new TypeError('Schemas is not correct array schema')
|
||||
}
|
||||
},
|
||||
exclusive: false
|
||||
})
|
||||
})
|
||||
|
||||
addMethod(string, 'url', function (schemas, msg = 'invalid url') {
|
||||
return this.test({
|
||||
name: 'url',
|
||||
message: msg,
|
||||
test: value => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(value)
|
||||
return true
|
||||
} catch (e) {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(`http://${value}`)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
exclusive: false
|
||||
})
|
||||
})
|
||||
|
||||
addMethod(string, 'ws', function (schemas, msg = 'invalid websocket') {
|
||||
return this.test({
|
||||
name: 'ws',
|
||||
message: msg,
|
||||
test: value => {
|
||||
if (typeof value === 'undefined') return true
|
||||
try {
|
||||
const url = new URL(value)
|
||||
return url.protocol === 'ws:' || url.protocol === 'wss:'
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
exclusive: false
|
||||
})
|
||||
})
|
||||
|
||||
addMethod(string, 'socket', function (schemas, msg = 'invalid socket') {
|
||||
return this.test({
|
||||
name: 'socket',
|
||||
message: msg,
|
||||
test: value => {
|
||||
try {
|
||||
const url = new URL(`http://${value}`)
|
||||
return url.hostname && url.port && !url.username && !url.password &&
|
||||
(!url.pathname || url.pathname === '/') && !url.search && !url.hash
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
exclusive: false
|
||||
})
|
||||
})
|
||||
|
||||
addMethod(string, 'https', function () {
|
||||
return this.test({
|
||||
name: 'https',
|
||||
message: 'https required',
|
||||
test: (url) => {
|
||||
try {
|
||||
return new URL(url).protocol === 'https:'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
addMethod(string, 'wss', function (msg) {
|
||||
return this.test({
|
||||
name: 'wss',
|
||||
message: msg || 'wss required',
|
||||
test: (url) => {
|
||||
try {
|
||||
return new URL(url).protocol === 'wss:'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
addMethod(string, 'hex', function (msg) {
|
||||
return this.test({
|
||||
name: 'hex',
|
||||
message: msg || 'invalid hex encoding',
|
||||
test: (value) => !value || HEX_REGEX.test(value)
|
||||
})
|
||||
})
|
||||
|
||||
addMethod(string, 'nwcUrl', function () {
|
||||
return this.test({
|
||||
test: async (nwcUrl, context) => {
|
||||
if (!nwcUrl) return true
|
||||
|
||||
// run validation in sequence to control order of errors
|
||||
// inspired by https://github.com/jquense/yup/issues/851#issuecomment-1049705180
|
||||
try {
|
||||
await string().matches(/^nostr\+?walletconnect:\/\//, { message: 'must start with nostr+walletconnect://' }).validate(nwcUrl)
|
||||
let relayUrl, walletPubkey, secret
|
||||
try {
|
||||
({ relayUrl, walletPubkey, secret } = parseNwcUrl(nwcUrl))
|
||||
} catch {
|
||||
// invalid URL error. handle as if pubkey validation failed to not confuse user.
|
||||
throw new Error('pubkey must be 64 hex chars')
|
||||
}
|
||||
await string().required('pubkey required').trim().matches(NOSTR_PUBKEY_HEX, 'pubkey must be 64 hex chars').validate(walletPubkey)
|
||||
await string().required('relay url required').trim().wss('relay must use wss://').validate(relayUrl)
|
||||
await string().required('secret required').trim().matches(/^[0-9a-fA-F]{64}$/, 'secret must be 64 hex chars').validate(secret)
|
||||
} catch (err) {
|
||||
return context.createError({ message: err.message })
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const titleValidator = string().required('required').trim().max(
|
||||
MAX_TITLE_LENGTH,
|
||||
({ max, value }) => `-${Math.abs(max - value.length)} characters remaining`
|
||||
|
@ -203,32 +47,12 @@ const nameValidator = string()
|
|||
const intValidator = number().typeError('must be a number').integer('must be whole')
|
||||
const floatValidator = number().typeError('must be a number')
|
||||
|
||||
const lightningAddressValidator = process.env.NODE_ENV === 'development'
|
||||
export const lightningAddressValidator = process.env.NODE_ENV === 'development'
|
||||
? string().or(
|
||||
[string().matches(/^[\w_]+@localhost:\d+$/), string().matches(/^[\w_]+@app:\d+$/), string().email()],
|
||||
'address is no good')
|
||||
: string().email('address is no good')
|
||||
|
||||
const hexOrBase64Validator = string().test({
|
||||
name: 'hex-or-base64',
|
||||
message: 'invalid encoding',
|
||||
test: (val) => {
|
||||
if (typeof val === 'undefined') return true
|
||||
try {
|
||||
ensureB64(val)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}).transform(val => {
|
||||
try {
|
||||
return ensureB64(val)
|
||||
} catch {
|
||||
return val
|
||||
}
|
||||
})
|
||||
|
||||
async function usernameExists (name, { client, models }) {
|
||||
if (!client && !models) {
|
||||
throw new Error('cannot check for user')
|
||||
|
@ -363,56 +187,59 @@ export function advSchema (args) {
|
|||
})
|
||||
}
|
||||
|
||||
export const autowithdrawSchemaMembers = {
|
||||
enabled: boolean(),
|
||||
autoWithdrawThreshold: intValidator.required('required').min(0, 'must be at least 0').max(msatsToSats(BALANCE_LIMIT_MSATS), `must be at most ${abbrNum(msatsToSats(BALANCE_LIMIT_MSATS))}`),
|
||||
autoWithdrawMaxFeePercent: floatValidator.required('required').min(0, 'must be at least 0').max(50, 'must not exceed 50'),
|
||||
autoWithdrawMaxFeeTotal: intValidator.required('required').min(0, 'must be at least 0').max(1_000, 'must not exceed 1000')
|
||||
}
|
||||
|
||||
export const lnAddrAutowithdrawSchema = object({
|
||||
address: lightningAddressValidator.required('required').test({
|
||||
name: 'address',
|
||||
test: addr => !addr.endsWith('@stacker.news'),
|
||||
message: 'automated withdrawals must be external'
|
||||
}),
|
||||
...autowithdrawSchemaMembers
|
||||
export const autowithdrawSchemaMembers = object({
|
||||
autoWithdrawThreshold: intValidator.required('required').min(0, 'must be at least 0').max(msatsToSats(BALANCE_LIMIT_MSATS), `must be at most ${abbrNum(msatsToSats(BALANCE_LIMIT_MSATS))}`).transform(Number),
|
||||
autoWithdrawMaxFeePercent: floatValidator.required('required').min(0, 'must be at least 0').max(50, 'must not exceed 50').transform(Number),
|
||||
autoWithdrawMaxFeeTotal: intValidator.required('required').min(0, 'must be at least 0').max(1_000, 'must not exceed 1000').transform(Number)
|
||||
})
|
||||
|
||||
export const LNDAutowithdrawSchema = object({
|
||||
socket: string().socket().required('required'),
|
||||
macaroon: hexOrBase64Validator.required('required').test({
|
||||
name: 'macaroon',
|
||||
test: v => isInvoiceMacaroon(v) || isInvoicableMacaroon(v),
|
||||
message: 'not an invoice macaroon or an invoicable macaroon'
|
||||
}),
|
||||
cert: hexOrBase64Validator,
|
||||
...autowithdrawSchemaMembers
|
||||
})
|
||||
export const lnAddrSchema = ({ payerData, min, max, commentAllowed } = {}) =>
|
||||
object({
|
||||
addr: lightningAddressValidator.required('required'),
|
||||
amount: (() => {
|
||||
const schema = intValidator.required('required').positive('must be positive').min(
|
||||
min || 1, `must be at least ${min || 1}`)
|
||||
return max ? schema.max(max, `must be at most ${max}`) : schema
|
||||
})(),
|
||||
maxFee: intValidator.required('required').min(0, 'must be at least 0'),
|
||||
comment: commentAllowed
|
||||
? string().max(commentAllowed, `must be less than ${commentAllowed}`)
|
||||
: string()
|
||||
}).concat(object().shape(Object.keys(payerData || {}).reduce((accum, key) => {
|
||||
const entry = payerData[key]
|
||||
if (key === 'email') {
|
||||
accum[key] = string().email()
|
||||
} else if (key === 'identifier') {
|
||||
accum[key] = boolean()
|
||||
} else {
|
||||
accum[key] = string()
|
||||
}
|
||||
if (entry?.mandatory) {
|
||||
accum[key] = accum[key].required()
|
||||
}
|
||||
return accum
|
||||
}, {})))
|
||||
|
||||
export const CLNAutowithdrawSchema = object({
|
||||
socket: string().socket().required('required'),
|
||||
rune: string().matches(B64_URL_REGEX, { message: 'invalid rune' }).required('required')
|
||||
.test({
|
||||
name: 'rune',
|
||||
test: (v, context) => {
|
||||
const decoded = decodeRune(v)
|
||||
if (!decoded) return context.createError({ message: 'invalid rune' })
|
||||
if (decoded.restrictions.length === 0) {
|
||||
return context.createError({ message: 'rune must be restricted to method=invoice' })
|
||||
}
|
||||
if (decoded.restrictions.length !== 1 || decoded.restrictions[0].alternatives.length !== 1) {
|
||||
return context.createError({ message: 'rune must be restricted to method=invoice only' })
|
||||
}
|
||||
if (decoded.restrictions[0].alternatives[0] !== 'method=invoice') {
|
||||
return context.createError({ message: 'rune must be restricted to method=invoice only' })
|
||||
}
|
||||
return true
|
||||
}
|
||||
export const phoenixdSchema = object().shape({
|
||||
url: string().url().required('required').trim(),
|
||||
primaryPassword: string().length(64).hex()
|
||||
.when(['secondaryPassword'], ([secondary], schema) => {
|
||||
if (!secondary) return schema.required('required if secondary password not set')
|
||||
return schema.test({
|
||||
test: primary => secondary !== primary,
|
||||
message: 'primary password cannot be the same as secondary password'
|
||||
})
|
||||
}),
|
||||
secondaryPassword: string().length(64).hex()
|
||||
.when(['primaryPassword'], ([primary], schema) => {
|
||||
if (!primary) return schema.required('required if primary password not set')
|
||||
return schema.test({
|
||||
test: secondary => primary !== secondary,
|
||||
message: 'secondary password cannot be the same as primary password'
|
||||
})
|
||||
}),
|
||||
cert: hexOrBase64Validator,
|
||||
...autowithdrawSchemaMembers
|
||||
})
|
||||
}, ['primaryPassword', 'secondaryPassword'])
|
||||
|
||||
export function bountySchema (args) {
|
||||
return object({
|
||||
|
@ -663,146 +490,6 @@ export const withdrawlSchema = object({
|
|||
maxFee: intValidator.required('required').min(0, 'must be at least 0')
|
||||
})
|
||||
|
||||
export const lnAddrSchema = ({ payerData, min, max, commentAllowed } = {}) =>
|
||||
object({
|
||||
addr: lightningAddressValidator.required('required'),
|
||||
amount: (() => {
|
||||
const schema = intValidator.required('required').positive('must be positive').min(
|
||||
min || 1, `must be at least ${min || 1}`)
|
||||
return max ? schema.max(max, `must be at most ${max}`) : schema
|
||||
})(),
|
||||
maxFee: intValidator.required('required').min(0, 'must be at least 0'),
|
||||
comment: commentAllowed
|
||||
? string().max(commentAllowed, `must be less than ${commentAllowed}`)
|
||||
: string()
|
||||
}).concat(object().shape(Object.keys(payerData || {}).reduce((accum, key) => {
|
||||
const entry = payerData[key]
|
||||
if (key === 'email') {
|
||||
accum[key] = string().email()
|
||||
} else if (key === 'identifier') {
|
||||
accum[key] = boolean()
|
||||
} else {
|
||||
accum[key] = string()
|
||||
}
|
||||
if (entry?.mandatory) {
|
||||
accum[key] = accum[key].required()
|
||||
}
|
||||
return accum
|
||||
}, {})))
|
||||
|
||||
export const lnbitsSchema = object().shape({
|
||||
url: process.env.NODE_ENV === 'development'
|
||||
? string()
|
||||
.or([string().matches(/^(http:\/\/)?localhost:\d+$/), string().url()], 'invalid url')
|
||||
.required('required').trim()
|
||||
: string().url().required('required').trim()
|
||||
.test(async (url, context) => {
|
||||
if (TOR_REGEXP.test(url)) {
|
||||
// allow HTTP and HTTPS over Tor
|
||||
if (!/^https?:\/\//.test(url)) {
|
||||
return context.createError({ message: 'http or https required' })
|
||||
}
|
||||
return true
|
||||
}
|
||||
try {
|
||||
// force HTTPS over clearnet
|
||||
await string().https().validate(url)
|
||||
} catch (err) {
|
||||
return context.createError({ message: err.message })
|
||||
}
|
||||
return true
|
||||
}),
|
||||
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'
|
||||
})
|
||||
}),
|
||||
...autowithdrawSchemaMembers
|
||||
// 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().shape({
|
||||
nwcUrl: string().nwcUrl().when(['nwcUrlRecv'], ([nwcUrlRecv], schema) => {
|
||||
if (!nwcUrlRecv) return schema.required('required if connection for receiving not set')
|
||||
return schema.test({
|
||||
test: nwcUrl => nwcUrl !== nwcUrlRecv,
|
||||
message: 'connection for sending cannot be the same as for receiving'
|
||||
})
|
||||
}),
|
||||
nwcUrlRecv: string().nwcUrl().when(['nwcUrl'], ([nwcUrl], schema) => {
|
||||
if (!nwcUrl) return schema.required('required if connection for sending not set')
|
||||
return schema.test({
|
||||
test: nwcUrlRecv => nwcUrlRecv !== nwcUrl,
|
||||
message: 'connection for receiving cannot be the same as for sending'
|
||||
})
|
||||
}),
|
||||
...autowithdrawSchemaMembers
|
||||
}, ['nwcUrl', 'nwcUrlRecv'])
|
||||
|
||||
export const blinkSchema = object({
|
||||
apiKey: string()
|
||||
.required('required')
|
||||
.matches(/^blink_[A-Za-z0-9]+$/, { message: 'must match pattern blink_A-Za-z0-9' }),
|
||||
currency: string()
|
||||
.transform(value => value ? value.toUpperCase() : 'BTC')
|
||||
.oneOf(['USD', 'BTC'], 'must be BTC or USD')
|
||||
})
|
||||
|
||||
export const lncSchema = object({
|
||||
pairingPhrase: string()
|
||||
.test(async (value, context) => {
|
||||
const words = value ? value.trim().split(/[\s]+/) : []
|
||||
for (const w of words) {
|
||||
try {
|
||||
await string().oneOf(bip39Words).validate(w)
|
||||
} catch {
|
||||
return context.createError({ message: `'${w}' is not a valid pairing phrase word` })
|
||||
}
|
||||
}
|
||||
if (words.length < 2) {
|
||||
return context.createError({ message: 'needs at least two words' })
|
||||
}
|
||||
if (words.length > 10) {
|
||||
return context.createError({ message: 'max 10 words' })
|
||||
}
|
||||
return true
|
||||
})
|
||||
.required('required')
|
||||
})
|
||||
|
||||
export const phoenixdSchema = object().shape({
|
||||
url: string().url().required('required').trim(),
|
||||
primaryPassword: string().length(64).hex()
|
||||
.when(['secondaryPassword'], ([secondary], schema) => {
|
||||
if (!secondary) return schema.required('required if secondary password not set')
|
||||
return schema.test({
|
||||
test: primary => secondary !== primary,
|
||||
message: 'primary password cannot be the same as secondary password'
|
||||
})
|
||||
}),
|
||||
secondaryPassword: string().length(64).hex()
|
||||
.when(['primaryPassword'], ([primary], schema) => {
|
||||
if (!primary) return schema.required('required if primary password not set')
|
||||
return schema.test({
|
||||
test: secondary => primary !== secondary,
|
||||
message: 'secondary password cannot be the same as primary password'
|
||||
})
|
||||
}),
|
||||
...autowithdrawSchemaMembers
|
||||
}, ['primaryPassword', 'secondaryPassword'])
|
||||
|
||||
export const bioSchema = object({
|
||||
text: string().required('required').trim()
|
||||
})
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
import { addMethod, string, mixed } from 'yup'
|
||||
import { parseNwcUrl } from './url'
|
||||
import { NOSTR_PUBKEY_HEX } from './nostr'
|
||||
import { ensureB64, HEX_REGEX } from './format'
|
||||
|
||||
function orFunc (schemas, msg) {
|
||||
return this.test({
|
||||
name: 'or',
|
||||
message: msg,
|
||||
test: value => {
|
||||
if (Array.isArray(schemas) && schemas.length > 1) {
|
||||
const resee = schemas.map(schema => schema.isValidSync(value))
|
||||
return resee.some(res => res)
|
||||
} else {
|
||||
throw new TypeError('Schemas is not correct array schema')
|
||||
}
|
||||
},
|
||||
exclusive: false
|
||||
})
|
||||
}
|
||||
|
||||
addMethod(mixed, 'or', orFunc)
|
||||
addMethod(string, 'or', orFunc)
|
||||
|
||||
addMethod(string, 'hexOrBase64', function (schemas, msg = 'invalid hex or base64 encoding') {
|
||||
return this.test({
|
||||
name: 'hex-or-base64',
|
||||
message: 'invalid encoding',
|
||||
test: (val) => {
|
||||
if (typeof val === 'undefined') return true
|
||||
try {
|
||||
ensureB64(val)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}).transform(val => {
|
||||
try {
|
||||
return ensureB64(val)
|
||||
} catch {
|
||||
return val
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
addMethod(string, 'url', function (schemas, msg = 'invalid url') {
|
||||
return this.test({
|
||||
name: 'url',
|
||||
message: msg,
|
||||
test: value => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(value)
|
||||
return true
|
||||
} catch (e) {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(`http://${value}`)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
exclusive: false
|
||||
})
|
||||
})
|
||||
|
||||
addMethod(string, 'ws', function (schemas, msg = 'invalid websocket') {
|
||||
return this.test({
|
||||
name: 'ws',
|
||||
message: msg,
|
||||
test: value => {
|
||||
if (typeof value === 'undefined') return true
|
||||
try {
|
||||
const url = new URL(value)
|
||||
return url.protocol === 'ws:' || url.protocol === 'wss:'
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
exclusive: false
|
||||
})
|
||||
})
|
||||
|
||||
addMethod(string, 'socket', function (schemas, msg = 'invalid socket') {
|
||||
return this.test({
|
||||
name: 'socket',
|
||||
message: msg,
|
||||
test: value => {
|
||||
try {
|
||||
const url = new URL(`http://${value}`)
|
||||
return url.hostname && url.port && !url.username && !url.password &&
|
||||
(!url.pathname || url.pathname === '/') && !url.search && !url.hash
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
exclusive: false
|
||||
})
|
||||
})
|
||||
|
||||
addMethod(string, 'https', function () {
|
||||
return this.test({
|
||||
name: 'https',
|
||||
message: 'https required',
|
||||
test: (url) => {
|
||||
try {
|
||||
return new URL(url).protocol === 'https:'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
addMethod(string, 'wss', function (msg) {
|
||||
return this.test({
|
||||
name: 'wss',
|
||||
message: msg || 'wss required',
|
||||
test: (url) => {
|
||||
try {
|
||||
return new URL(url).protocol === 'wss:'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
addMethod(string, 'hex', function (msg) {
|
||||
return this.test({
|
||||
name: 'hex',
|
||||
message: msg || 'invalid hex encoding',
|
||||
test: (value) => !value || HEX_REGEX.test(value)
|
||||
})
|
||||
})
|
||||
|
||||
addMethod(string, 'nwcUrl', function () {
|
||||
return this.test({
|
||||
test: async (nwcUrl, context) => {
|
||||
if (!nwcUrl) return true
|
||||
|
||||
// run validation in sequence to control order of errors
|
||||
// inspired by https://github.com/jquense/yup/issues/851#issuecomment-1049705180
|
||||
try {
|
||||
await string().matches(/^nostr\+?walletconnect:\/\//, { message: 'must start with nostr+walletconnect://' }).validate(nwcUrl)
|
||||
let relayUrl, walletPubkey, secret
|
||||
try {
|
||||
({ relayUrl, walletPubkey, secret } = parseNwcUrl(nwcUrl))
|
||||
} catch {
|
||||
// invalid URL error. handle as if pubkey validation failed to not confuse user.
|
||||
throw new Error('pubkey must be 64 hex chars')
|
||||
}
|
||||
await string().required('pubkey required').trim().matches(NOSTR_PUBKEY_HEX, 'pubkey must be 64 hex chars').validate(walletPubkey)
|
||||
await string().required('relay url required').trim().wss('relay must use wss://').validate(relayUrl)
|
||||
await string().required('secret required').trim().matches(/^[0-9a-fA-F]{64}$/, 'secret must be 64 hex chars').validate(secret)
|
||||
} catch (err) {
|
||||
return context.createError({ message: err.message })
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export * from 'yup'
|
|
@ -7,7 +7,7 @@ import { schnorr } from '@noble/curves/secp256k1'
|
|||
import { createHash } from 'crypto'
|
||||
import { datePivot } from '@/lib/time'
|
||||
import { BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT, LNURLP_COMMENT_MAX_LENGTH, USER_IDS_BALANCE_NO_LIMIT } from '@/lib/constants'
|
||||
import { ssValidate, lud18PayerDataSchema } from '@/lib/validate'
|
||||
import { validateSchema, lud18PayerDataSchema } from '@/lib/validate'
|
||||
import assertGofacYourself from '@/api/resolvers/ofac'
|
||||
|
||||
export default async ({ query: { username, amount, nostr, comment, payerdata: payerData }, headers }, res) => {
|
||||
|
@ -59,7 +59,7 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa
|
|||
}
|
||||
|
||||
try {
|
||||
await ssValidate(lud18PayerDataSchema, parsedPayerData)
|
||||
await validateSchema(lud18PayerDataSchema, parsedPayerData)
|
||||
} catch (err) {
|
||||
console.error('error validating payer data', err)
|
||||
return res.status(400).json({ status: 'ERROR', reason: err.toString() })
|
||||
|
|
|
@ -9,12 +9,15 @@ import { useWallet } from '@/wallets/index'
|
|||
import Info from '@/components/info'
|
||||
import Text from '@/components/text'
|
||||
import { autowithdrawInitial, AutowithdrawSettings } from '@/components/autowithdraw-shared'
|
||||
import { canSend, isConfigured } from '@/wallets/common'
|
||||
import { canReceive, canSend, isConfigured } from '@/wallets/common'
|
||||
import { SSR } from '@/lib/constants'
|
||||
import WalletButtonBar from '@/components/wallet-buttonbar'
|
||||
import { useWalletConfigurator } from '@/wallets/config'
|
||||
import { useMemo } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useMe } from '@/components/me'
|
||||
import validateWallet from '@/wallets/validate'
|
||||
import { ValidationError } from 'yup'
|
||||
import { useFormikContext } from 'formik'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
||||
|
||||
|
@ -47,10 +50,19 @@ export default function WalletSettings () {
|
|||
}
|
||||
}, [wallet, me])
|
||||
|
||||
// check if wallet uses the form-level validation built into Formik or a Yup schema
|
||||
const validateProps = typeof wallet?.fieldValidation === 'function'
|
||||
? { validate: wallet?.fieldValidation }
|
||||
: { schema: wallet?.fieldValidation }
|
||||
const validate = useCallback(async (data) => {
|
||||
try {
|
||||
await validateWallet(wallet.def, data, { abortEarly: false, topLevel: false })
|
||||
} catch (error) {
|
||||
if (error instanceof ValidationError) {
|
||||
return error.inner.reduce((acc, error) => {
|
||||
acc[error.path] = error.message
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}, [wallet.def])
|
||||
|
||||
return (
|
||||
<CenterLayout>
|
||||
|
@ -60,7 +72,7 @@ export default function WalletSettings () {
|
|||
<Form
|
||||
initial={initial}
|
||||
enableReinitialize
|
||||
{...validateProps}
|
||||
validate={validate}
|
||||
onSubmit={async ({ amount, ...values }) => {
|
||||
try {
|
||||
const newConfig = !isConfigured(wallet)
|
||||
|
@ -81,18 +93,15 @@ export default function WalletSettings () {
|
|||
}}
|
||||
>
|
||||
{wallet && <WalletFields wallet={wallet} />}
|
||||
{wallet?.def.clientOnly
|
||||
? (
|
||||
<CheckboxGroup name='enabled'>
|
||||
<Checkbox
|
||||
disabled={!isConfigured(wallet)}
|
||||
label='enabled'
|
||||
name='enabled'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
</CheckboxGroup>
|
||||
)
|
||||
: <AutowithdrawSettings wallet={wallet} />}
|
||||
<CheckboxGroup name='enabled'>
|
||||
<Checkbox
|
||||
disabled={!isConfigured(wallet)}
|
||||
label='enabled'
|
||||
name='enabled'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
</CheckboxGroup>
|
||||
<ReceiveSettings walletDef={wallet.def} />
|
||||
<WalletButtonBar
|
||||
wallet={wallet} onDelete={async () => {
|
||||
try {
|
||||
|
@ -114,9 +123,14 @@ export default function WalletSettings () {
|
|||
)
|
||||
}
|
||||
|
||||
function ReceiveSettings ({ walletDef }) {
|
||||
const { values } = useFormikContext()
|
||||
return canReceive({ def: walletDef, config: values }) && <AutowithdrawSettings />
|
||||
}
|
||||
|
||||
function WalletFields ({ wallet }) {
|
||||
return wallet.def.fields
|
||||
.map(({ name, label = '', type, help, optional, editable, clientOnly, serverOnly, ...props }, i) => {
|
||||
.map(({ name, label = '', type, help, optional, editable, requiredWithout, validate, clientOnly, serverOnly, ...props }, i) => {
|
||||
const rawProps = {
|
||||
...props,
|
||||
name,
|
||||
|
|
|
@ -11,7 +11,7 @@ import { useToast } from '@/components/toast'
|
|||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
||||
|
||||
export default function Wallet ({ ssrData }) {
|
||||
const { wallets, setPriorities, reloadLocalWallets } = useWallets()
|
||||
const { wallets, setPriorities } = useWallets()
|
||||
const toast = useToast()
|
||||
const isClient = useIsClient()
|
||||
const [sourceIndex, setSourceIndex] = useState(null)
|
||||
|
@ -28,8 +28,7 @@ export default function Wallet ({ ssrData }) {
|
|||
.map((w, i) => ({ wallet: w, priority: i }))
|
||||
|
||||
await setPriorities(priorities)
|
||||
reloadLocalWallets()
|
||||
}, [setPriorities, reloadLocalWallets, wallets])
|
||||
}, [setPriorities, wallets])
|
||||
|
||||
const onDragStart = useCallback((i) => (e) => {
|
||||
// e.dataTransfer.dropEffect = 'move'
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { blinkSchema } from '@/lib/validate'
|
||||
import { string } from '@/lib/yup'
|
||||
|
||||
export const galoyBlinkUrl = 'https://api.blink.sv/graphql'
|
||||
export const galoyBlinkDashboardUrl = 'https://dashboard.blink.sv/'
|
||||
|
@ -7,7 +8,6 @@ export const name = 'blink'
|
|||
export const walletType = 'BLINK'
|
||||
export const walletField = 'walletBlink'
|
||||
export const fieldValidation = blinkSchema
|
||||
export const clientOnly = true
|
||||
|
||||
export const fields = [
|
||||
{
|
||||
|
@ -15,7 +15,10 @@ export const fields = [
|
|||
label: 'api key',
|
||||
type: 'password',
|
||||
help: `you can get an API key from [Blink Dashboard](${galoyBlinkDashboardUrl})`,
|
||||
placeholder: 'blink_...'
|
||||
placeholder: 'blink_...',
|
||||
clientOnly: true,
|
||||
validate: string()
|
||||
.matches(/^blink_[A-Za-z0-9]+$/, { message: 'must match pattern blink_A-Za-z0-9' })
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
|
@ -25,7 +28,11 @@ export const fields = [
|
|||
placeholder: 'BTC',
|
||||
optional: true,
|
||||
clear: true,
|
||||
autoComplete: 'off'
|
||||
autoComplete: 'off',
|
||||
clientOnly: true,
|
||||
validate: string()
|
||||
.transform(value => value ? value.toUpperCase() : 'BTC')
|
||||
.oneOf(['USD', 'BTC'], 'must be BTC or USD')
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { decodeRune } from '@/lib/cln'
|
||||
import { B64_URL_REGEX } from '@/lib/format'
|
||||
import { CLNAutowithdrawSchema } from '@/lib/validate'
|
||||
import { string } from '@/lib/yup'
|
||||
|
||||
export const name = 'cln'
|
||||
export const walletType = 'CLN'
|
||||
|
@ -13,7 +16,8 @@ export const fields = [
|
|||
placeholder: '55.5.555.55:3010',
|
||||
hint: 'tor or clearnet',
|
||||
clear: true,
|
||||
serverOnly: true
|
||||
serverOnly: true,
|
||||
validate: string().socket()
|
||||
},
|
||||
{
|
||||
name: 'rune',
|
||||
|
@ -25,7 +29,25 @@ export const fields = [
|
|||
placeholder: 'S34KtUW-6gqS_hD_9cc_PNhfF-NinZyBOCgr1aIrark9NCZtZXRob2Q9aW52b2ljZQ==',
|
||||
hint: 'must be restricted to method=invoice',
|
||||
clear: true,
|
||||
serverOnly: true
|
||||
serverOnly: true,
|
||||
validate: string().matches(B64_URL_REGEX, { message: 'invalid rune' })
|
||||
.test({
|
||||
name: 'rune',
|
||||
test: (v, context) => {
|
||||
const decoded = decodeRune(v)
|
||||
if (!decoded) return context.createError({ message: 'invalid rune' })
|
||||
if (decoded.restrictions.length === 0) {
|
||||
return context.createError({ message: 'rune must be restricted to method=invoice' })
|
||||
}
|
||||
if (decoded.restrictions.length !== 1 || decoded.restrictions[0].alternatives.length !== 1) {
|
||||
return context.createError({ message: 'rune must be restricted to method=invoice only' })
|
||||
}
|
||||
if (decoded.restrictions[0].alternatives[0] !== 'method=invoice') {
|
||||
return context.createError({ message: 'rune must be restricted to method=invoice only' })
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'cert',
|
||||
|
@ -35,7 +57,8 @@ export const fields = [
|
|||
optional: 'optional if from [CA](https://en.wikipedia.org/wiki/Certificate_authority) (e.g. voltage)',
|
||||
hint: 'hex or base64 encoded',
|
||||
clear: true,
|
||||
serverOnly: true
|
||||
serverOnly: true,
|
||||
validate: string().hexOrBase64()
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -62,7 +62,8 @@ export function isClientField (f) {
|
|||
function checkFields ({ fields, config }) {
|
||||
// a wallet is configured if all of its required fields are set
|
||||
let val = fields.every(f => {
|
||||
return f.optional ? true : !!config?.[f.name]
|
||||
if (f.optional && !f.requiredWithout) return true
|
||||
return !!config?.[f.name]
|
||||
})
|
||||
|
||||
// however, a wallet is not configured if all fields are optional and none are set
|
||||
|
@ -93,5 +94,5 @@ export function canSend ({ def, config }) {
|
|||
}
|
||||
|
||||
export function canReceive ({ def, config }) {
|
||||
return !def.clientOnly && isReceiveConfigured({ def, config })
|
||||
return def.fields.some(f => f.serverOnly) && isReceiveConfigured({ def, config })
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ import { canReceive, canSend, getStorageKey } from './common'
|
|||
import { useMutation } from '@apollo/client'
|
||||
import { generateMutation } from './graphql'
|
||||
import { REMOVE_WALLET } from '@/fragments/wallet'
|
||||
import { walletValidate } from '@/lib/validate'
|
||||
import { useWalletLogger } from '@/components/wallet-logger'
|
||||
import { useWallets } from '.'
|
||||
import validateWallet from './validate'
|
||||
|
||||
export function useWalletConfigurator (wallet) {
|
||||
const { me } = useMe()
|
||||
|
@ -20,11 +20,14 @@ export function useWalletConfigurator (wallet) {
|
|||
const _saveToServer = useCallback(async (serverConfig, clientConfig, validateLightning) => {
|
||||
const { serverWithShared, settings, clientOnly } = siftConfig(wallet.def.fields, { ...serverConfig, ...clientConfig })
|
||||
const vaultEntries = []
|
||||
if (clientOnly) {
|
||||
if (clientOnly && isActive) {
|
||||
for (const [key, value] of Object.entries(clientOnly)) {
|
||||
vaultEntries.push({ key, value: encrypt(value) })
|
||||
if (value) {
|
||||
vaultEntries.push({ key, value: encrypt(value) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await upsertWallet({ variables: { ...serverWithShared, settings, validateLightning, vaultEntries } })
|
||||
}, [encrypt, isActive, wallet.def.fields])
|
||||
|
||||
|
@ -40,7 +43,7 @@ export function useWalletConfigurator (wallet) {
|
|||
let serverConfig = serverWithShared
|
||||
|
||||
if (canSend({ def: wallet.def, config: clientConfig })) {
|
||||
let transformedConfig = await walletValidate(wallet.def, clientWithShared)
|
||||
let transformedConfig = await validateWallet(wallet.def, clientWithShared)
|
||||
if (transformedConfig) {
|
||||
clientConfig = Object.assign(clientConfig, transformedConfig)
|
||||
}
|
||||
|
@ -51,7 +54,7 @@ export function useWalletConfigurator (wallet) {
|
|||
}
|
||||
}
|
||||
} else if (canReceive({ def: wallet.def, config: serverConfig })) {
|
||||
const transformedConfig = await walletValidate(wallet.def, serverConfig)
|
||||
const transformedConfig = await validateWallet(wallet.def, serverConfig)
|
||||
if (transformedConfig) {
|
||||
serverConfig = Object.assign(serverConfig, transformedConfig)
|
||||
}
|
||||
|
@ -62,6 +65,15 @@ export function useWalletConfigurator (wallet) {
|
|||
return { clientConfig, serverConfig }
|
||||
}, [wallet])
|
||||
|
||||
const _detachFromServer = useCallback(async () => {
|
||||
await removeWallet({ variables: { id: wallet.config.id } })
|
||||
}, [wallet.config?.id])
|
||||
|
||||
const _detachFromLocal = useCallback(async () => {
|
||||
window.localStorage.removeItem(getStorageKey(wallet.def.name, me?.id))
|
||||
reloadLocalWallets()
|
||||
}, [me?.id, wallet.def.name, reloadLocalWallets])
|
||||
|
||||
const save = useCallback(async (newConfig, validateLightning = true) => {
|
||||
const { clientConfig, serverConfig } = await _validate(newConfig, validateLightning)
|
||||
|
||||
|
@ -71,20 +83,18 @@ export function useWalletConfigurator (wallet) {
|
|||
} else {
|
||||
if (canSend({ def: wallet.def, config: clientConfig })) {
|
||||
await _saveToLocal(clientConfig)
|
||||
} else {
|
||||
// if it previously had a client config, remove it
|
||||
await _detachFromLocal()
|
||||
}
|
||||
if (canReceive({ def: wallet.def, config: serverConfig })) {
|
||||
await _saveToServer(serverConfig, clientConfig, validateLightning)
|
||||
} else {
|
||||
// if it previously had a server config, remove it
|
||||
await _detachFromServer()
|
||||
}
|
||||
}
|
||||
}, [isActive, _saveToServer, _saveToLocal, _validate])
|
||||
|
||||
const _detachFromServer = useCallback(async () => {
|
||||
await removeWallet({ variables: { id: wallet.config.id } })
|
||||
}, [wallet.config?.id])
|
||||
|
||||
const _detachFromLocal = useCallback(async () => {
|
||||
window.localStorage.removeItem(getStorageKey(wallet.def.name, me?.id))
|
||||
}, [me?.id, wallet.def.name])
|
||||
}, [isActive, _saveToServer, _saveToLocal, _validate, _detachFromLocal, _detachFromServer])
|
||||
|
||||
const detach = useCallback(async () => {
|
||||
if (isActive) {
|
||||
|
|
|
@ -18,7 +18,7 @@ function useLocalWallets () {
|
|||
const [wallets, setWallets] = useState([])
|
||||
|
||||
const loadWallets = useCallback(() => {
|
||||
// form wallets into a list of { config, def }
|
||||
// form wallets from local storage into a list of { config, def }
|
||||
const wallets = walletDefs.map(w => {
|
||||
try {
|
||||
const storageKey = getStorageKey(w.name, me?.id)
|
||||
|
@ -66,15 +66,28 @@ export function WalletsProvider ({ children }) {
|
|||
}
|
||||
|
||||
// the specific wallet config on the server is stored in wallet.wallet
|
||||
// on the client, it's stored in unnested
|
||||
// on the client, it's stored unnested
|
||||
return { config: { ...config, ...w.wallet }, def }
|
||||
}) ?? []
|
||||
|
||||
// merge wallets on name
|
||||
// merge wallets on name like: { ...unconfigured, ...localConfig, ...serverConfig }
|
||||
const merged = {}
|
||||
for (const wallet of [...walletDefsOnly, ...localWallets, ...wallets]) {
|
||||
merged[wallet.def.name] = { ...merged[wallet.def.name], ...wallet }
|
||||
merged[wallet.def.name] = {
|
||||
def: wallet.def,
|
||||
config: {
|
||||
...merged[wallet.def.name]?.config,
|
||||
...Object.fromEntries(
|
||||
Object.entries(wallet.config ?? {}).map(([key, value]) => [
|
||||
key,
|
||||
value ?? merged[wallet.def.name]?.config?.[key]
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort by priority, then add status field
|
||||
return Object.values(merged)
|
||||
.sort(walletPrioritySort)
|
||||
.map(w => ({ ...w, status: w.config?.enabled ? Status.Enabled : Status.Disabled }))
|
||||
|
@ -87,6 +100,7 @@ export function WalletsProvider ({ children }) {
|
|||
}
|
||||
|
||||
if (wallet.config?.id) {
|
||||
// set priority on server if it has an id
|
||||
await setWalletPriority({ variables: { id: wallet.config.id, priority } })
|
||||
} else {
|
||||
const storageKey = getStorageKey(wallet.def.name, me?.id)
|
||||
|
@ -95,9 +109,14 @@ export function WalletsProvider ({ children }) {
|
|||
window.localStorage.setItem(storageKey, JSON.stringify(newConfig))
|
||||
}
|
||||
}
|
||||
}, [setWalletPriority, me?.id])
|
||||
// reload local wallets if any priorities were set
|
||||
if (priorities.length > 0) {
|
||||
reloadLocalWallets()
|
||||
}
|
||||
}, [setWalletPriority, me?.id, reloadLocalWallets])
|
||||
|
||||
// provides priority sorted wallets to children
|
||||
// provides priority sorted wallets to children, a function to reload local wallets,
|
||||
// and a function to set priorities
|
||||
return (
|
||||
<WalletsContext.Provider value={{ wallets, reloadLocalWallets, setPriorities }}>
|
||||
{children}
|
||||
|
@ -117,6 +136,7 @@ export function useWallet (name) {
|
|||
return wallets.find(w => w.def.name === name)
|
||||
}
|
||||
|
||||
// return the first enabled wallet that is available and can send
|
||||
return wallets
|
||||
.filter(w => !w.def.isAvailable || w.def.isAvailable())
|
||||
.filter(w => w.config?.enabled && canSend(w))[0]
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { lnAddrAutowithdrawSchema } from '@/lib/validate'
|
||||
import { lightningAddressValidator } from '@/lib/validate'
|
||||
|
||||
export const name = 'lightning-address'
|
||||
export const shortName = 'lnAddr'
|
||||
export const walletType = 'LIGHTNING_ADDRESS'
|
||||
export const walletField = 'walletLightningAddress'
|
||||
export const fieldValidation = lnAddrAutowithdrawSchema
|
||||
|
||||
export const fields = [
|
||||
{
|
||||
|
@ -12,7 +11,12 @@ export const fields = [
|
|||
label: 'lightning address',
|
||||
type: 'text',
|
||||
autoComplete: 'off',
|
||||
serverOnly: true
|
||||
serverOnly: true,
|
||||
validate: lightningAddressValidator.test({
|
||||
name: 'address',
|
||||
test: addr => !addr.toLowerCase().endsWith('@stacker.news'),
|
||||
message: 'automated withdrawals must be external'
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -1,15 +1,37 @@
|
|||
import { lnbitsSchema } from '@/lib/validate'
|
||||
import { TOR_REGEXP } from '@/lib/url'
|
||||
import { string } from '@/lib/yup'
|
||||
|
||||
export const name = 'lnbits'
|
||||
export const walletType = 'LNBITS'
|
||||
export const walletField = 'walletLNbits'
|
||||
export const fieldValidation = lnbitsSchema
|
||||
|
||||
export const fields = [
|
||||
{
|
||||
name: 'url',
|
||||
label: 'lnbits url',
|
||||
type: 'text'
|
||||
type: 'text',
|
||||
required: true,
|
||||
validate: process.env.NODE_ENV === 'development'
|
||||
? string()
|
||||
.or([string().matches(/^(http:\/\/)?localhost:\d+$/), string().url()], 'invalid url')
|
||||
.trim()
|
||||
: string().url().trim()
|
||||
.test(async (url, context) => {
|
||||
if (TOR_REGEXP.test(url)) {
|
||||
// allow HTTP and HTTPS over Tor
|
||||
if (!/^https?:\/\//.test(url)) {
|
||||
return context.createError({ message: 'http or https required' })
|
||||
}
|
||||
return true
|
||||
}
|
||||
try {
|
||||
// force HTTPS over clearnet
|
||||
await string().https().validate(url)
|
||||
} catch (err) {
|
||||
return context.createError({ message: err.message })
|
||||
}
|
||||
return true
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'invoiceKey',
|
||||
|
@ -17,7 +39,8 @@ export const fields = [
|
|||
type: 'password',
|
||||
optional: 'for receiving',
|
||||
serverOnly: true,
|
||||
editable: false
|
||||
requiredWithout: 'adminKey',
|
||||
validate: string().hex().length(32)
|
||||
},
|
||||
{
|
||||
name: 'adminKey',
|
||||
|
@ -25,7 +48,8 @@ export const fields = [
|
|||
type: 'password',
|
||||
optional: 'for sending',
|
||||
clientOnly: true,
|
||||
editable: false
|
||||
requiredWithout: 'invoiceKey',
|
||||
validate: string().hex().length(32)
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { lncSchema } from '@/lib/validate'
|
||||
import bip39Words from '@/lib/bip39-words'
|
||||
import { string } from '@/lib/yup'
|
||||
|
||||
export const name = 'lnc'
|
||||
export const walletType = 'LNC'
|
||||
export const walletField = 'walletLNC'
|
||||
export const clientOnly = true
|
||||
export const fieldValidation = lncSchema
|
||||
|
||||
export const fields = [
|
||||
{
|
||||
|
@ -13,7 +13,25 @@ export const fields = [
|
|||
type: 'password',
|
||||
help: 'We only need permissions for the uri `/lnrpc.Lightning/SendPaymentSync`\n\nCreate a budgeted account with narrow permissions:\n\n```$ litcli accounts create --balance <budget>```\n\n```$ litcli sessions add --type custom --label <your label> --account_id <account_id> --uri /lnrpc.Lightning/SendPaymentSync```\n\nGrab the `pairing_secret_mnemonic` from the output and paste it here.',
|
||||
editable: false,
|
||||
clientOnly: true
|
||||
clientOnly: true,
|
||||
validate: string()
|
||||
.test(async (value, context) => {
|
||||
const words = value ? value.trim().split(/[\s]+/) : []
|
||||
for (const w of words) {
|
||||
try {
|
||||
await string().oneOf(bip39Words).validate(w)
|
||||
} catch {
|
||||
return context.createError({ message: `'${w}' is not a valid pairing phrase word` })
|
||||
}
|
||||
}
|
||||
if (words.length < 2) {
|
||||
return context.createError({ message: 'needs at least two words' })
|
||||
}
|
||||
if (words.length > 10) {
|
||||
return context.createError({ message: 'max 10 words' })
|
||||
}
|
||||
return true
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'localKey',
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { LNDAutowithdrawSchema } from '@/lib/validate'
|
||||
import { isInvoicableMacaroon, isInvoiceMacaroon } from '@/lib/macaroon'
|
||||
import { string } from '@/lib/yup'
|
||||
|
||||
export const name = 'lnd'
|
||||
export const walletType = 'LND'
|
||||
export const walletField = 'walletLND'
|
||||
export const fieldValidation = LNDAutowithdrawSchema
|
||||
|
||||
export const fields = [
|
||||
{
|
||||
|
@ -13,7 +13,8 @@ export const fields = [
|
|||
placeholder: '55.5.555.55:10001',
|
||||
hint: 'tor or clearnet',
|
||||
clear: true,
|
||||
serverOnly: true
|
||||
serverOnly: true,
|
||||
validate: string().socket()
|
||||
},
|
||||
{
|
||||
name: 'macaroon',
|
||||
|
@ -26,7 +27,12 @@ export const fields = [
|
|||
placeholder: 'AgEDbG5kAlgDChCn7YgfWX7uTkQQgXZ2uahNEgEwGhYKB2FkZHJlc3MSBHJlYWQSBXdyaXRlGhcKCGludm9pY2VzEgRyZWFkEgV3cml0ZRoPCgdvbmNoYWluEgRyZWFkAAAGIJkMBrrDV0npU90JV0TGNJPrqUD8m2QYoTDjolaL6eBs',
|
||||
hint: 'hex or base64 encoded',
|
||||
clear: true,
|
||||
serverOnly: true
|
||||
serverOnly: true,
|
||||
validate: string().hexOrBase64().test({
|
||||
name: 'macaroon',
|
||||
test: v => isInvoiceMacaroon(v) || isInvoicableMacaroon(v),
|
||||
message: 'not an invoice macaroon or an invoicable macaroon'
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'cert',
|
||||
|
@ -36,7 +42,8 @@ export const fields = [
|
|||
optional: 'optional if from [CA](https://en.wikipedia.org/wiki/Certificate_authority) (e.g. voltage)',
|
||||
hint: 'hex or base64 encoded',
|
||||
clear: true,
|
||||
serverOnly: true
|
||||
serverOnly: true,
|
||||
validate: string().hexOrBase64()
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { Relay } from '@/lib/nostr'
|
||||
import { parseNwcUrl } from '@/lib/url'
|
||||
import { nwcSchema } from '@/lib/validate'
|
||||
import { string } from '@/lib/yup'
|
||||
import { finalizeEvent, nip04, verifyEvent } from 'nostr-tools'
|
||||
|
||||
export const name = 'nwc'
|
||||
export const walletType = 'NWC'
|
||||
export const walletField = 'walletNWC'
|
||||
export const fieldValidation = nwcSchema
|
||||
|
||||
export const fields = [
|
||||
{
|
||||
|
@ -15,7 +14,8 @@ export const fields = [
|
|||
type: 'password',
|
||||
optional: 'for sending',
|
||||
clientOnly: true,
|
||||
editable: false
|
||||
requiredWithout: 'nwcUrlRecv',
|
||||
validate: string().nwcUrl()
|
||||
},
|
||||
{
|
||||
name: 'nwcUrlRecv',
|
||||
|
@ -23,7 +23,8 @@ export const fields = [
|
|||
type: 'password',
|
||||
optional: 'for receiving',
|
||||
serverOnly: true,
|
||||
editable: false
|
||||
requiredWithout: 'nwcUrl',
|
||||
validate: string().nwcUrl()
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { phoenixdSchema } from '@/lib/validate'
|
||||
import { string } from '@/lib/yup'
|
||||
|
||||
export const name = 'phoenixd'
|
||||
export const walletType = 'PHOENIXD'
|
||||
export const walletField = 'walletPhoenixd'
|
||||
export const fieldValidation = phoenixdSchema
|
||||
|
||||
// configure wallet fields
|
||||
export const fields = [
|
||||
{
|
||||
name: 'url',
|
||||
label: 'url',
|
||||
type: 'text'
|
||||
type: 'text',
|
||||
validate: string().url().trim()
|
||||
},
|
||||
{
|
||||
name: 'primaryPassword',
|
||||
|
@ -19,7 +19,8 @@ export const fields = [
|
|||
optional: 'for sending',
|
||||
help: 'You can find the primary password as `http-password` in your phoenixd configuration file as mentioned [here](https://phoenix.acinq.co/server/api#security).',
|
||||
clientOnly: true,
|
||||
editable: false
|
||||
requiredWithout: 'secondaryPassword',
|
||||
validate: string().length(64).hex()
|
||||
},
|
||||
{
|
||||
name: 'secondaryPassword',
|
||||
|
@ -28,7 +29,8 @@ export const fields = [
|
|||
optional: 'for receiving',
|
||||
help: 'You can find the secondary password as `http-password-limited-access` in your phoenixd configuration file as mentioned [here](https://phoenix.acinq.co/server/api#security).',
|
||||
serverOnly: true,
|
||||
editable: false
|
||||
requiredWithout: 'primaryPassword',
|
||||
validate: string().length(64).hex()
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -38,6 +40,3 @@ export const card = {
|
|||
subtitle: 'use [phoenixd](https://phoenix.acinq.co/server) for payments',
|
||||
badges: ['send & receive']
|
||||
}
|
||||
|
||||
// phoenixd::TODO
|
||||
// set validation schema
|
||||
|
|
|
@ -42,11 +42,12 @@ export async function createInvoice (userId, { msats, description, descriptionHa
|
|||
for (const wallet of wallets) {
|
||||
const w = walletDefs.find(w => w.walletType === wallet.def.walletType)
|
||||
try {
|
||||
const { walletType, walletField, createInvoice } = w
|
||||
if (!canReceive({ def: w, config: wallet })) {
|
||||
continue
|
||||
}
|
||||
|
||||
const { walletType, walletField, createInvoice } = w
|
||||
|
||||
const walletFull = await models.wallet.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
we want to take all the validate members from the provided wallet
|
||||
and compose into a single yup schema for formik validation ...
|
||||
the validate member can be on of:
|
||||
- a yup schema
|
||||
- a function that throws on an invalid value
|
||||
- a regular expression that must match
|
||||
*/
|
||||
|
||||
import { autowithdrawSchemaMembers } from '@/lib/validate'
|
||||
import * as Yup from '@/lib/yup'
|
||||
import { canReceive } from './common'
|
||||
|
||||
export default async function validateWallet (walletDef, data, options = { abortEarly: true, topLevel: true }) {
|
||||
let schema = composeWalletSchema(walletDef)
|
||||
|
||||
if (canReceive({ def: walletDef, config: data })) {
|
||||
schema = schema.concat(autowithdrawSchemaMembers)
|
||||
}
|
||||
|
||||
await schema.validate(data, options)
|
||||
|
||||
const casted = schema.cast(data, { assert: false })
|
||||
if (options.topLevel && walletDef.validate) {
|
||||
await walletDef.validate(casted)
|
||||
}
|
||||
|
||||
return casted
|
||||
}
|
||||
|
||||
function createFieldSchema (name, validate) {
|
||||
if (!validate) {
|
||||
throw new Error(`No validation provided for field ${name}`)
|
||||
}
|
||||
|
||||
if (Yup.isSchema(validate)) {
|
||||
// If validate is already a Yup schema, return it directly
|
||||
return validate
|
||||
} else if (typeof validate === 'function') {
|
||||
// If validate is a function, create a custom Yup test
|
||||
return Yup.mixed().test({
|
||||
name,
|
||||
test: (value, context) => {
|
||||
try {
|
||||
validate(value)
|
||||
return true
|
||||
} catch (error) {
|
||||
return context.createError({ message: error.message })
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (validate instanceof RegExp) {
|
||||
// If validate is a regular expression, use Yup.matches
|
||||
return Yup.string().matches(validate, `${name} is invalid`)
|
||||
} else {
|
||||
throw new Error(`validate for ${name} must be a yup schema, function, or regular expression`)
|
||||
}
|
||||
}
|
||||
|
||||
function composeWalletSchema (walletDef) {
|
||||
const { fields } = walletDef
|
||||
|
||||
const schemaShape = fields.reduce((acc, field) => {
|
||||
const { name, validate, optional, requiredWithout } = field
|
||||
|
||||
acc[name] = createFieldSchema(name, validate)
|
||||
|
||||
if (!optional) {
|
||||
acc[name] = acc[name].required('Required')
|
||||
} else if (requiredWithout) {
|
||||
acc[name] = acc[name].when([requiredWithout], ([pairSetting], schema) => {
|
||||
if (!pairSetting) return schema.required(`required if ${requiredWithout} not set`)
|
||||
return Yup.mixed().or([schema.test({
|
||||
test: value => value !== pairSetting,
|
||||
message: `${name} cannot be the same as ${requiredWithout}`
|
||||
}), Yup.mixed().notRequired()])
|
||||
})
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// we use Object.keys(schemaShape).reverse() to avoid cyclic dependencies in Yup schema
|
||||
// see https://github.com/jquense/yup/issues/176#issuecomment-367352042
|
||||
const composedSchema = Yup.object().shape(schemaShape, Object.keys(schemaShape).reverse()).concat(Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
priority: Yup.number().min(0, 'must be at least 0').max(100, 'must be at most 100')
|
||||
}))
|
||||
|
||||
return composedSchema
|
||||
}
|
|
@ -1,22 +1,15 @@
|
|||
export const name = 'webln'
|
||||
export const walletType = 'WEBLN'
|
||||
export const walletField = 'walletWebLN'
|
||||
export const clientOnly = true
|
||||
|
||||
export const validate = ({ enabled }) => {
|
||||
if (enabled && typeof window?.webln === 'undefined') {
|
||||
throw new Error('no WebLN provider found')
|
||||
}
|
||||
}
|
||||
|
||||
export const fields = []
|
||||
|
||||
export const fieldValidation = ({ enabled }) => {
|
||||
if (typeof window?.webln === 'undefined') {
|
||||
// don't prevent disabling WebLN if no WebLN provider found
|
||||
if (enabled) {
|
||||
return {
|
||||
enabled: 'no WebLN provider found'
|
||||
}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
export const card = {
|
||||
title: 'WebLN',
|
||||
subtitle: 'use a [WebLN provider](https://www.webln.guide/ressources/webln-providers) for payments',
|
||||
|
|
Loading…
Reference in New Issue