stacker.news/wallets/common.js

206 lines
6.2 KiB
JavaScript

import walletDefs from '@/wallets/client'
export const Status = {
Enabled: 'Enabled',
Disabled: 'Disabled',
Error: 'Error',
Warning: 'Warning'
}
export function getWalletByName (name) {
return walletDefs.find(def => def.name === name)
}
export function getWalletByType (type) {
return walletDefs.find(def => def.walletType === type)
}
export function getStorageKey (name, userId) {
let storageKey = `wallet:${name}`
// WebLN has no credentials we need to scope to users
// so we can use the same storage key for all users
if (userId && name !== 'webln') {
storageKey = `${storageKey}:${userId}`
}
return storageKey
}
export function walletPrioritySort (w1, w2) {
// enabled/configured wallets always come before disabled/unconfigured wallets
if ((w1.config?.enabled && !w2.config?.enabled) || (isConfigured(w1) && !isConfigured(w2))) {
return -1
} else if ((w2.config?.enabled && !w1.config?.enabled) || (isConfigured(w2) && !isConfigured(w1))) {
return 1
}
const delta = w1.config?.priority - w2.config?.priority
// delta is NaN if either priority is undefined
if (!Number.isNaN(delta) && delta !== 0) return delta
// if one wallet has a priority but the other one doesn't, the one with the priority comes first
if (w1.config?.priority !== undefined && w2.config?.priority === undefined) return -1
if (w1.config?.priority === undefined && w2.config?.priority !== undefined) return 1
// both wallets have no priority set, falling back to other methods
// if both wallets have an id, use that as tie breaker
// since that's the order in which autowithdrawals are attempted
if (w1.config?.id && w2.config?.id) return Number(w1.config.id) - Number(w2.config.id)
// else we will use the card title as tie breaker
return w1.def.card.title < w2.def.card.title ? -1 : 1
}
export function isServerField (f) {
return f.serverOnly || !f.clientOnly
}
export function isClientField (f) {
return f.clientOnly || !f.serverOnly
}
function checkFields ({ fields, config }) {
// a wallet is configured if all of its required fields are set
let val = fields.every(f => {
if ((f.optional || f.generated) && !f.requiredWithout) return true
return !!config?.[f.name]
})
// however, a wallet is not configured if all fields are optional and none are set
// since that usually means that one of them is required
if (val && fields.length > 0) {
val = !(fields.every(f => f.optional || f.generated) && fields.every(f => !config?.[f.name]))
}
return val
}
export function isConfigured ({ def, config }) {
return isSendConfigured({ def, config }) || isReceiveConfigured({ def, config })
}
function isSendConfigured ({ def, config }) {
const fields = def.fields.filter(isClientField)
return (fields.length > 0 || def.isAvailable?.()) && checkFields({ fields, config })
}
function isReceiveConfigured ({ def, config }) {
const fields = def.fields.filter(isServerField)
return fields.length > 0 && checkFields({ fields, config })
}
export function supportsSend ({ def, config }) {
return !!def.sendPayment
}
export function supportsReceive ({ def, config }) {
return def.fields.some(f => f.serverOnly)
}
export function canSend ({ def, config }) {
return (
supportsSend({ def, config }) &&
isSendConfigured({ def, config }) &&
(def.requiresConfig || config?.enabled)
)
}
export function canReceive ({ def, config }) {
return supportsReceive({ def, config }) && isReceiveConfigured({ def, config })
}
export function siftConfig (fields, config) {
const sifted = {
clientOnly: {},
serverOnly: {},
shared: {},
serverWithShared: {},
clientWithShared: {},
settings: null
}
for (const [key, value] of Object.entries(config)) {
if (['id'].includes(key)) {
sifted.serverOnly[key] = value
continue
}
if (['autoWithdrawMaxFeePercent', 'autoWithdrawThreshold', 'autoWithdrawMaxFeeTotal'].includes(key)) {
sifted.serverOnly[key] = Number(value)
sifted.settings = { ...sifted.settings, [key]: Number(value) }
continue
}
const field = fields.find(({ name }) => name === key)
if (field) {
if (field.serverOnly) {
sifted.serverOnly[key] = value
} else if (field.clientOnly) {
sifted.clientOnly[key] = value
} else {
sifted.shared[key] = value
}
} else if (['enabled', 'priority'].includes(key)) {
sifted.shared[key] = value
}
}
sifted.serverWithShared = { ...sifted.shared, ...sifted.serverOnly }
sifted.clientWithShared = { ...sifted.shared, ...sifted.clientOnly }
return sifted
}
export async function upsertWalletVariables ({ def, config }, encrypt, append = {}) {
const { serverWithShared, settings, clientOnly } = siftConfig(def.fields, config)
// if we are disconnected from the vault, we leave vaultEntries undefined so we don't
// delete entries from connected devices
let vaultEntries
if (clientOnly && encrypt) {
vaultEntries = []
for (const [key, value] of Object.entries(clientOnly)) {
if (value) {
vaultEntries.push({ key, ...await encrypt(value) })
}
}
}
return { ...serverWithShared, settings, vaultEntries, ...append }
}
export async function saveWalletLocally (name, config, userId) {
const storageKey = getStorageKey(name, userId)
window.localStorage.setItem(storageKey, JSON.stringify(config))
}
export const statusFromLog = (wallet, logs) => {
if (wallet.status.any === Status.Disabled) return wallet
// override status depending on if there have been warnings or errors in the logs recently
// find first log from which we can derive status (logs are sorted by recent first)
const walletLogs = logs.filter(l => l.wallet === wallet.def.name)
const sendLevel = walletLogs.find(l => l.context?.status && l.context?.send)?.level
const recvLevel = walletLogs.find(l => l.context?.status && l.context?.recv)?.level
const levelToStatus = (level) => {
switch (level?.toLowerCase()) {
case 'ok':
case 'success': return Status.Enabled
case 'error': return Status.Error
case 'warn': return Status.Warning
}
}
return {
...wallet,
status: {
...wallet.status,
send: levelToStatus(sendLevel) || wallet.status.send,
recv: levelToStatus(recvLevel) || wallet.status.recv
}
}
}