Generate validation schema for LND
This commit is contained in:
parent
3933a4f460
commit
587bfa34be
|
@ -13,18 +13,18 @@ import assertApiKeyNotPermitted from './apiKey'
|
|||
import { bolt11Tags } from '@/lib/bolt11'
|
||||
import { checkInvoice } from 'worker/wallet'
|
||||
import walletDefs from 'wallets/server'
|
||||
import { generateResolverName } from '@/lib/wallet'
|
||||
import { generateResolverName, generateSchema } from '@/lib/wallet'
|
||||
import { lnAddrOptions } from '@/lib/lnurl'
|
||||
|
||||
function injectResolvers (resolvers) {
|
||||
console.group('injected GraphQL resolvers:')
|
||||
for (const w of walletDefs) {
|
||||
const { schema, walletType, walletField, testConnect } = w
|
||||
const { walletType, walletField, testConnect } = w
|
||||
const resolverName = generateResolverName(walletField)
|
||||
console.log(resolverName)
|
||||
resolvers.Mutation[resolverName] = async (parent, { settings, ...data }, { me, models }) => {
|
||||
return await upsertWallet({
|
||||
schema,
|
||||
schema: generateSchema(w),
|
||||
wallet: { field: walletField, type: walletType },
|
||||
testConnect: (data) =>
|
||||
testConnect(data, { me, models })
|
||||
|
|
|
@ -9,7 +9,6 @@ import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './no
|
|||
import { msatsToSats, numWithUnits, abbrNum, ensureB64, B64_URL_REGEX } from './format'
|
||||
import * as usersFragments from '@/fragments/users'
|
||||
import * as subsFragments from '@/fragments/subs'
|
||||
import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon'
|
||||
import { datePivot } from './time'
|
||||
import { decodeRune } from '@/lib/cln'
|
||||
import bip39Words from './bip39-words'
|
||||
|
@ -156,7 +155,7 @@ const lightningAddressValidator = process.env.NODE_ENV === 'development'
|
|||
'address is no good')
|
||||
: string().email('address is no good')
|
||||
|
||||
const hexOrBase64Validator = string().test({
|
||||
export const hexOrBase64Validator = string().test({
|
||||
name: 'hex-or-base64',
|
||||
message: 'invalid encoding',
|
||||
test: (val) => {
|
||||
|
@ -311,20 +310,7 @@ export function lnAddrAutowithdrawSchema ({ me } = {}) {
|
|||
test: addr => !addr.endsWith('@stacker.news'),
|
||||
message: 'automated withdrawals must be external'
|
||||
}),
|
||||
...autowithdrawSchemaMembers({ me })
|
||||
})
|
||||
}
|
||||
|
||||
export function LNDAutowithdrawSchema ({ me } = {}) {
|
||||
return 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({ me })
|
||||
...autowithdrawSchemaMembers
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -350,16 +336,14 @@ export function CLNAutowithdrawSchema ({ me } = {}) {
|
|||
}
|
||||
}),
|
||||
cert: hexOrBase64Validator,
|
||||
...autowithdrawSchemaMembers({ me })
|
||||
...autowithdrawSchemaMembers
|
||||
})
|
||||
}
|
||||
|
||||
export function autowithdrawSchemaMembers ({ me } = {}) {
|
||||
return {
|
||||
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')
|
||||
}
|
||||
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')
|
||||
}
|
||||
|
||||
export function bountySchema (args) {
|
||||
|
|
105
lib/wallet.js
105
lib/wallet.js
|
@ -1,4 +1,109 @@
|
|||
import { array, object, string } from 'yup'
|
||||
import { autowithdrawSchemaMembers, hexOrBase64Validator } from '@/lib/validate'
|
||||
import { TOR_REGEXP } from '@/lib/url'
|
||||
|
||||
export function generateResolverName (walletField) {
|
||||
const capitalized = walletField[0].toUpperCase() + walletField.slice(1)
|
||||
return `upsertWallet${capitalized}`
|
||||
}
|
||||
|
||||
export function generateSchema (wallet) {
|
||||
if (wallet.schema) return wallet.schema
|
||||
|
||||
const fieldValidator = (field) => {
|
||||
if (!field.validate) {
|
||||
// default validation
|
||||
let validator = string()
|
||||
if (!field.optional) validator = validator.required('required')
|
||||
return validator
|
||||
}
|
||||
|
||||
// complex validation
|
||||
if (field.validate.schema) return field.validate.schema
|
||||
|
||||
const { type: validationType, words, min, max } = field.validate
|
||||
|
||||
let validator
|
||||
|
||||
const stringTypes = ['url', 'string']
|
||||
|
||||
if (stringTypes.includes(validationType)) {
|
||||
validator = string()
|
||||
|
||||
if (field.validate.length) validator = validator.length(field.validate.length)
|
||||
}
|
||||
|
||||
if (validationType === 'url') {
|
||||
validator = process.env.NODE_ENV === 'development'
|
||||
? validator
|
||||
.or([string().matches(/^(http:\/\/)?localhost:\d+$/), string().url()], 'invalid url')
|
||||
: validator
|
||||
.url()
|
||||
.test(async (url, context) => {
|
||||
if (field.validate.torAllowed && 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
|
||||
})
|
||||
}
|
||||
|
||||
if (words) {
|
||||
validator = array()
|
||||
.transform(function (value, originalValue) {
|
||||
if (this.isType(value) && value !== null) {
|
||||
return value
|
||||
}
|
||||
return originalValue ? originalValue.trim().split(/[\s]+/) : []
|
||||
})
|
||||
.test(async (values, context) => {
|
||||
for (const v of values) {
|
||||
try {
|
||||
await string().oneOf(words).validate(v)
|
||||
} catch {
|
||||
return context.createError({ message: `'${v}' is not a valid ${field.label} word` })
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
if (validationType === 'socket') validator = string().socket()
|
||||
|
||||
if (validationType === 'hexOrBase64') validator = hexOrBase64Validator
|
||||
|
||||
if (min !== undefined) validator = validator.min(min)
|
||||
if (max !== undefined) validator = validator.max(max)
|
||||
|
||||
if (!field.optional) validator = validator.required('required')
|
||||
|
||||
if (field.validate.test) {
|
||||
validator = validator.test({
|
||||
name: field.name,
|
||||
test: field.validate.test,
|
||||
message: field.validate.message
|
||||
})
|
||||
}
|
||||
|
||||
return validator
|
||||
}
|
||||
|
||||
return object({
|
||||
...wallet.fields.reduce((acc, field) => {
|
||||
return {
|
||||
...acc,
|
||||
[field.name]: fieldValidator(field)
|
||||
}
|
||||
}, {}),
|
||||
...(wallet.walletType ? autowithdrawSchemaMembers : {})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,10 +10,7 @@ import Info from '@/components/info'
|
|||
import Text from '@/components/text'
|
||||
import { AutowithdrawSettings } from '@/components/autowithdraw-shared'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { array, object, string } from 'yup'
|
||||
import { autowithdrawSchemaMembers } from '@/lib/validate'
|
||||
import { useMe } from '@/components/me'
|
||||
import { TOR_REGEXP } from '@/lib/url'
|
||||
import { generateSchema } from '@/lib/wallet'
|
||||
|
||||
const WalletButtonBar = dynamic(() => import('@/components/wallet-buttonbar.js'), { ssr: false })
|
||||
|
||||
|
@ -24,7 +21,6 @@ export default function WalletSettings () {
|
|||
const router = useRouter()
|
||||
const { wallet: name } = router.query
|
||||
const wallet = useWallet(name)
|
||||
const me = useMe()
|
||||
|
||||
const initial = wallet.fields.reduce((acc, field) => {
|
||||
// We still need to run over all wallet fields via reduce
|
||||
|
@ -38,7 +34,7 @@ export default function WalletSettings () {
|
|||
}
|
||||
}, wallet.config)
|
||||
|
||||
const schema = generateSchema(wallet, { me })
|
||||
const schema = generateSchema(wallet)
|
||||
|
||||
return (
|
||||
<CenterLayout>
|
||||
|
@ -137,95 +133,3 @@ function WalletFields ({ wallet: { config, fields } }) {
|
|||
return null
|
||||
})
|
||||
}
|
||||
|
||||
function generateSchema (wallet, { me }) {
|
||||
if (wallet.schema) return wallet.schema
|
||||
|
||||
const fieldValidator = (field) => {
|
||||
if (!field.validate) {
|
||||
// default validation
|
||||
let validator = string()
|
||||
if (!field.optional) validator = validator.required('required')
|
||||
return validator
|
||||
}
|
||||
|
||||
if (field.validate.schema) {
|
||||
// complex validation
|
||||
return field.validate.schema
|
||||
}
|
||||
|
||||
const { type: validationType, words, min, max } = field.validate
|
||||
|
||||
let validator
|
||||
|
||||
const stringTypes = ['url', 'string']
|
||||
|
||||
if (stringTypes.includes(validationType)) {
|
||||
validator = string()
|
||||
|
||||
if (field.validate.length) {
|
||||
validator = validator.length(field.validate.length)
|
||||
}
|
||||
}
|
||||
|
||||
if (validationType === 'url') {
|
||||
validator = process.env.NODE_ENV === 'development'
|
||||
? validator
|
||||
.or([string().matches(/^(http:\/\/)?localhost:\d+$/), string().url()], 'invalid url')
|
||||
: validator
|
||||
.url()
|
||||
.test(async (url, context) => {
|
||||
if (field.validate.torAllowed && 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
|
||||
})
|
||||
}
|
||||
|
||||
if (words) {
|
||||
validator = array()
|
||||
.transform(function (value, originalValue) {
|
||||
if (this.isType(value) && value !== null) {
|
||||
return value
|
||||
}
|
||||
return originalValue ? originalValue.trim().split(/[\s]+/) : []
|
||||
})
|
||||
.test(async (values, context) => {
|
||||
for (const v of values) {
|
||||
try {
|
||||
await string().oneOf(words).validate(v)
|
||||
} catch {
|
||||
return context.createError({ message: `'${v}' is not a valid ${field.label} word` })
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
if (min !== undefined) validator = validator.min(min)
|
||||
if (max !== undefined) validator = validator.max(max)
|
||||
|
||||
if (!field.optional) validator = validator.required('required')
|
||||
|
||||
return validator
|
||||
}
|
||||
|
||||
return object(
|
||||
wallet.fields.reduce((acc, field) => {
|
||||
return {
|
||||
...acc,
|
||||
[field.name]: fieldValidator(field)
|
||||
}
|
||||
}, wallet.walletType ? autowithdrawSchemaMembers({ me }) : {})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { LNDAutowithdrawSchema } from '@/lib/validate'
|
||||
import { isInvoicableMacaroon, isInvoiceMacaroon } from '@/lib/macaroon'
|
||||
|
||||
export const name = 'lnd'
|
||||
|
||||
|
@ -9,7 +9,10 @@ export const fields = [
|
|||
type: 'text',
|
||||
placeholder: '55.5.555.55:10001',
|
||||
hint: 'tor or clearnet',
|
||||
clear: true
|
||||
clear: true,
|
||||
validate: {
|
||||
type: 'socket'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'macaroon',
|
||||
|
@ -21,7 +24,12 @@ export const fields = [
|
|||
type: 'text',
|
||||
placeholder: 'AgEDbG5kAlgDChCn7YgfWX7uTkQQgXZ2uahNEgEwGhYKB2FkZHJlc3MSBHJlYWQSBXdyaXRlGhcKCGludm9pY2VzEgRyZWFkEgV3cml0ZRoPCgdvbmNoYWluEgRyZWFkAAAGIJkMBrrDV0npU90JV0TGNJPrqUD8m2QYoTDjolaL6eBs',
|
||||
hint: 'hex or base64 encoded',
|
||||
clear: true
|
||||
clear: true,
|
||||
validate: {
|
||||
type: 'hexOrBase64',
|
||||
test: v => isInvoiceMacaroon(v) || isInvoicableMacaroon(v),
|
||||
message: 'not an invoice macaroon or an invoicable macaroon'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cert',
|
||||
|
@ -30,7 +38,10 @@ export const fields = [
|
|||
placeholder: 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNNVENDQWRpZ0F3SUJBZ0lRSHBFdFdrcGJwZHV4RVF2eVBPc3NWVEFLQmdncWhrak9QUVFEQWpBdk1SOHcKSFFZRFZRUUtFeFpzYm1RZ1lYVjBiMmRsYm1WeVlYUmxaQ0JqWlhKME1Rd3dDZ1lEVlFRREV3TmliMkl3SGhjTgpNalF3TVRBM01qQXhORE0wV2hjTk1qVXdNekF6TWpBeE5ETTBXakF2TVI4d0hRWURWUVFLRXhac2JtUWdZWFYwCmIyZGxibVZ5WVhSbFpDQmpaWEowTVF3d0NnWURWUVFERXdOaWIySXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak8KUFFNQkJ3TkNBQVJUS3NMVk5oZnhqb1FLVDlkVVdDbzUzSmQwTnBuL1BtYi9LUE02M1JxbU52dFYvdFk4NjJJZwpSbE41cmNHRnBEajhUeFc2OVhIK0pTcHpjWDdlN3N0Um80SFZNSUhTTUE0R0ExVWREd0VCL3dRRUF3SUNwREFUCkJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCVDAKMnh3V25GeHRUNzI0MWxwZlNoNm9FWi9UMWpCN0JnTlZIUkVFZERCeWdnTmliMktDQ1d4dlkyRnNhRzl6ZElJRApZbTlpZ2d4d2IyeGhjaTF1TVMxaWIyS0NGR2h2YzNRdVpHOWphMlZ5TG1sdWRHVnlibUZzZ2dSMWJtbDRnZ3AxCmJtbDRjR0ZqYTJWMGdnZGlkV1pqYjI1dWh3Ui9BQUFCaHhBQUFBQUFBQUFBQUFBQUFBQUFBQUFCaHdTc0VnQUQKTUFvR0NDcUdTTTQ5QkFNQ0EwY0FNRVFDSUEwUTlkRXdoNXpPRnpwL3hYeHNpemh5SkxNVG5yazU1VWx1NHJPRwo4WW52QWlBVGt4U3p3Y3hZZnFscGx0UlNIbmd0NUJFcDBzcXlHL05nenBzb2pmMGNqQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K',
|
||||
optional: 'optional if from [CA](https://en.wikipedia.org/wiki/Certificate_authority) (e.g. voltage)',
|
||||
hint: 'hex or base64 encoded',
|
||||
clear: true
|
||||
clear: true,
|
||||
validate: {
|
||||
type: 'hexOrBase64'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -40,8 +51,6 @@ export const card = {
|
|||
badges: ['receive only', 'non-custodial']
|
||||
}
|
||||
|
||||
export const schema = LNDAutowithdrawSchema
|
||||
|
||||
export const walletType = 'LND'
|
||||
|
||||
export const walletField = 'walletLND'
|
||||
|
|
Loading…
Reference in New Issue