diff --git a/api/resolvers/invite.js b/api/resolvers/invite.js
index 579a6cc1..347d7152 100644
--- a/api/resolvers/invite.js
+++ b/api/resolvers/invite.js
@@ -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 }
diff --git a/api/resolvers/item.js b/api/resolvers/item.js
index 06ddec3c..ae7b6dc5 100644
--- a/api/resolvers/item.js
+++ b/api/resolvers/item.js
@@ -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 } })
diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js
index 20b6ceac..a934f64f 100644
--- a/api/resolvers/notifications.js
+++ b/api/resolvers/notifications.js
@@ -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) {
diff --git a/api/resolvers/rewards.js b/api/resolvers/rewards.js
index ce476f35..3dcfd2c2 100644
--- a/api/resolvers/rewards.js
+++ b/api/resolvers/rewards.js
@@ -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 })
}
diff --git a/api/resolvers/sub.js b/api/resolvers/sub.js
index 09279869..320670b6 100644
--- a/api/resolvers/sub.js
+++ b/api/resolvers/sub.js
@@ -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) {
diff --git a/api/resolvers/user.js b/api/resolvers/user.js
index 86e73e04..cd518e70 100644
--- a/api/resolvers/user.js
+++ b/api/resolvers/user.js
@@ -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({
diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js
index 05ac369e..6711b20b 100644
--- a/api/resolvers/wallet.js
+++ b/api/resolvers/wallet.js
@@ -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 = {
diff --git a/components/autowithdraw-shared.js b/components/autowithdraw-shared.js
index 47f01bb8..6bdc4045 100644
--- a/components/autowithdraw-shared.js
+++ b/components/autowithdraw-shared.js
@@ -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 (
<>
-
desired balance
diff --git a/lib/validate.js b/lib/validate.js
index 96753c94..5e9b30c9 100644
--- a/lib/validate.js
+++ b/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()
})
diff --git a/lib/yup.js b/lib/yup.js
new file mode 100644
index 00000000..6102eae4
--- /dev/null
+++ b/lib/yup.js
@@ -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'
diff --git a/pages/api/lnurlp/[username]/pay.js b/pages/api/lnurlp/[username]/pay.js
index b153a2ee..6cfc9b44 100644
--- a/pages/api/lnurlp/[username]/pay.js
+++ b/pages/api/lnurlp/[username]/pay.js
@@ -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() })
diff --git a/pages/settings/wallets/[wallet].js b/pages/settings/wallets/[wallet].js
index 95de9f61..f563b13c 100644
--- a/pages/settings/wallets/[wallet].js
+++ b/pages/settings/wallets/[wallet].js
@@ -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 (
@@ -60,7 +72,7 @@ export default function WalletSettings () {