* Migrate vault entries to new schema (#2092)

* Migrate existing vault entries to new schema

* Read+write new vault schema

* Drop VaultEntry table

* Refactor vaultPrismaFragments

* Remove wrong comment

* Remove TODO

* Fix possible race condition on update of vault key

* Remove lib/object.js

* Wallet schema v2 (#2146)

* Add wallet-v2 TODOs

* Update checkWallet

* Wallet list

* Delete almost all wallet v1 code

and add some code for wallet protocol forms

* Define protocol display name in JSON

* Show form per protocol

* Increase max-height of image in form

* Add JSdoc for protocols, form validation

* Use wallet cards again

My wallet list was quite ugly and I couldn't look at it anymore.

* Refactor hooks in wallet provider

* Fix PasswordInput not used

* Read encrypted wallets

* Decrypt wallets

* useWalletQuery now returns decrypted wallets
* Refactor useIndexedDB because its only purpose will be to store the key, so no need for pagination code etc.
* There is still a bug: if the wallet is not decrypted on first render, the form will not see the decrypted value. See TODO.

* Rename protocolJson to protocol

it no longer uses a JSON file

* Fix form not updated with decrypted API key

* Fix wallet template forms

* Fix optional shown as hint

* Rename to mapUserWalletResolveTypes

* Save LNbits send and recv

TODO:

* implement resolvers for other protocols
* fix double update required for trigger?
* add missing validation on server
* add missing network tests
* don't import from wallets/client on server

* Move definitions to lib/wallets.json and lib/protocols

* Fix ProtocolWallet.updated_at not updated by trigger

* Move wallet fragments into wallets/client/fragments/

* move invoice fragments to fragments/invoice.js
* remove some unused fragments that I don't think I also will not use
* move fragments that will be generated in own file

* Move wallet resolvers into wallets/server/resolvers

* Fix missing authorization check on wallet update

* Run all shared code in generic wallet update function

* Fix 'encrypt' flag not set for blink send currency

* Add mutations for all protocols

* Fix macaroon validation

* Fix CLN socket value not set

* Add server-side schema validation

* Fix JSDoc typedef for protocols

* Don't put JSDoc into separate file

* Create test invoices on save

* Also move type resolvers into wallets/server/resolvers

* Fix unconfigured protocols of UserWallet not found

* Fix Blink API key in wallet seed

* Test send payment on save (except LNC)

This does not include LNC because LNC cannot be saved yet

* Check if window.webln is defined on save

* Create new wallets from templates

* Separate protocols in wallets/lib into individual files

* Use justify-content-start for protocol tabs

and larger margin at the top

* Add LNC to client protocols

* Only return wallets from useWallets

* Query decrypted wallets

* Payments with new wallets

* More wallet logos

* Fix TypeError in useIndexedDB

* Add protocol attach docs

* Fix undefined useWalletRecvPrompt import

* Remove outdated TODOs

* First successful zap to new wallets

* Fix walletLogger imports

* Fix sequences

* the sequences for InvoiceForward and DirectPayment were still starting at 1
* when using setval() with two arguments, nextval() will return the second argument+1 (see https://www.postgresql.org/docs/current/functions-sequence.html)

* Rename ProtocolWallet columns

* Remove more outdated TODOs

* Update wallet indicator

* Fix page reset on route change

* Refactor __typename checks into functions

* Refactor protocol selection into own hook

* Add button to detach protocol

* Refetch wallet on save and detach

* Refetch wallets on change

* Always show all templates

* Refactor WalletLink component

* Also put wallet into forms context

* Remove outdated TODOs

* Use useMemo in wallets hooks

* Passphrase modals

* prompt for password if decryption failed
* add button to reveal passphrase on wallet page

TODO:
* remove button if passphrase was revealed or imported
* encrypt wallets with new key on passphrase reveal

* Fix protocol missing as callback dependency

* Encrypt wallets with new key on passphrase export

* Update 'unlock wallets' text

* Rename wallet mutation hooks

* Remove 'removeWallet' mutation

Wallets are automatically deleted when all protocols are deleted

* Passphrase reset

* Use 110px as minimum width for bip39 words

longest bip39 words are 8 characters and they fit into 103px so I rounded up to 110px.

* Also disable passphrase export on save

* Wallet settings

* Fix wallet receive prompt

* Remove unused parameters from postgres function

* Rename UserWallet to Wallet, ProtocolWallet to WalletProtocol

* Use danger variant for button to show passphrase

* Fix inconsistent imports and exports

* Remove outdated TODOs

* wallet logs

* Remove outdated comment

* Make sure wallets are used in priority order

* Separate wallets from templates in reducer

* Fix missing useCallback dependencies

* Refactor with useWalletLogger hook

* Move enabled to WalletProtocol

* Add checkbox to enable/disable protocol

* Fix migration with prod db dump

* Parse Coinos relay URLs

* Skip network tests if only enabled changed

* Allow IndexedDB calls without session

* Add code to migrate old CryptoKey

* first try to use existing CryptoKey before generating a new one
* bump IDB version to delete old object stores and create new ones
* return IDB callbacks with useMemo
* don't delete old IDB right away, wait until next release

* Fix ghost import error

*Sometimes*, I get import errors because it tries to resolve @/wallets/server to wallets/server.js instead of wallets/server/index.js.

For the files in wallets/server, it kind of makes sense because it's a circular import.

But I don't know why the files in worker/ have this problem.

Interestingly, it only seems to happen with walletLogger imports, so I guess its related to its import chain.

Anyway, this commit should make sure this never happens again ...

* Skip wallets queries if not logged in

* Split CUSTOM wallet into NWC and LN_ADDR

* Migrate local wallets

* Link to /wallets/:id/receive if send not supported

* Hide separator if there are no configured wallets

* Save LNC

* Add one-liner to attach LNC

* Update wallet priorities via DnD

* Wallet logs are part of protocol resolvers

* Fix logging to deleted protocol

* Fix trying to fetch logs for template

* also change type to Int so GraphQL layer can catch trying to fetch string IDs as is the case for templates

* Fix embedded flag for wallets logs not set

* Remove TODO

* Decrease max-height for embedded wallet logs on big screens

* Fix missing refetch on wallet priority update

* Set priorities of all wallets in one tx

* Fix nested state update

* Add DragIcon

* DnD mobile support and refactor

* Add CancelButton to wallet settings

* Remount form if path changes

This fixes the following warning in the console:

"""
Warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component.
"""

* Support string and object for wallet.image JSON

* Append domain to lightning address inputs

* Remove outdated TODOs

* Add template IDs to wallet JSON

* Fix missing callback dependency

* Implement lightning address save in receive prompt

* Update TODOs

* Fix missing check for enabled

* Pay QR codes with WebLN as anon

* Add logo for NWC

* Fix trying to save logs for template

* Add template logs

* Fix inconsistent margin

* Always throw on missing key

* Remove misleading comment

Wallets are returned even if decryption fails so we can show the unlock page if a wallet is stored as encrypted in the context.

Maybe I should rethink this.

* Check for existing wallets on local wallet migration

* Fix local wallet migration causing duplicates

* Fix protocol reattached on detach due to migration

* Fix form not centered

* Fix ZEUS lightning address domain

* Add placeholder, help, hints etc. to wallet form inputs

* Fix wallet badges not updated

* Remove unused declared variables

* Rename to ATTACH_PAGE

* Fix 500 error if no amount was given to LNURLp endpoint

* Tag log messages with wallet name

* Only skip network tests if we're disabling the wallet

* Rename var to networkTests

* Continue to store key hash in IndexedDB

* Rethink wallet state management

If decryption failed, the function to decrypt the wallets didn't throw but simply returned wallets that were still encrypted.

This was bad because it meant we could not rely on the wallets in the state being decrypted, even though this was the original idea behind the query hooks: hide the details of encryption and decryption inside them.

Because of this, we had to check if the wallets were still encrypted before we ran the wallet migration since we want to check if a protocol already exists.

This commit fixes this by making encryption and decryption always throw (and catching the errors), as well as returning a ready state from hooks. A hook might not be ready because it still needs to load something (in the case of the crypto hooks, it's loading the key from IndexedDB). Callers check that ready state before they call the function returned by the hook.

So now, the wallet migration hook can itself simply check if the hook to encrypt wallets is ready and if the wallets are no longer loading to let callers know if it itself is ready.

Since we also relied on wallets stored as encrypted in the context to show the unlock page, this was also changed by comparing the local and remote key hash.

* Add empty line

* Save new key hash during wallet reset

* Only receive protocol upserts require networkTests param

* Compare key hashes on server on each save

* Delete old code

* Fix card shows attach instead of configure

* Fix empty wallets created during migration

The old schema can contain '' instead of NULL in the columns of wallets for receiving.

* Update reset passphrase text

* Wrap passphrase reset in try/catch

* Fix migrate called multiple times

* Update key hash on migration if not set

* Fetch local wallets in migrate

* Fix missing await on setKey

* Let first device set key hash

* Fix indicator not shown if wallets locked

* Check if IndexedDB is available

* Fix inconsistent WebLN error message

* Disable WebLN if not available

* Remove outdated TODO

* Cursor-based pagination for wallet logs

* Fix log message x-overflow

* Add context to wallet logs

* Wrap errors are warnings in logs

* Rename wallet v2 migrations

* Update wallet status during logging

* Fix wallet logs loading state

The loading state would go from false -> true -> false because it's false when the lazy query wasn't called yet.

* Add wallet search

* Add Alby Go wallet

* Revert "Add Alby Go wallet"

This reverts commit 926c70638f1673756480c848237e52d5889dc037.

* Fix wallet logs sent by client don't update protocol status

* Fix mutation name

* put drag icon on opposite corner

* Add wallets/README.md

* Fix inconsistent case in wallets/README.md

* Fix autoprefixer warning about mixed support

This warning was in the app logs:

app     | Warning
app     |
app     | (31:3) autoprefixer: end value has mixed support, consider using flex-end instead
app     |
app     | Import trace for requested module:
app     | ./styles/wallet.module.css
app     | ./wallets/client/hooks/prompt.js
app     | ./wallets/client/hooks/index.js
app     | ./wallets/client/context/hooks.js
app     | ./wallets/client/context/provider.js
app     | ./wallets/client/context/index.js

* fix effect of wallet indicators on logo

* Fix deleting wallet template logs

* Use name as primary key of WalletTemplate

* Fix wallet_clear_vault trigger not mentioned in README

* Fix wallet receive prompt

Also remove no longer needed templateId from wallets.json and helper functions

* Use findUnique since name is now primary key

* Merge Alby wallets into one

* Remove unused name parameter from WalletsForm component

* Fix number check to decide if wallet or template

* Update wallet encryption on click, not as effect

* add cashu.me and lightning address logos

* add images

* Use recommended typeof to check if IDB available

* Also check if IDB available on delete

* Use constraint triggers

* Add indices on columns used for joins

* Fix inconsistent CLEAR OR REPLACE TRIGGER

* Attach wallet_check_support trigger to WalletProtocol table

* Update wallets/README.md

* Remove debugging code

* Refactor reducer: replace page with status

* Show 'wallets unavailable' if device does not support IndexedDB

* Remove duplicate ELSIF condition

* Fix hasSendWallet

The useSendWallets hook was not checking if the returned send wallets are enabled.

Since the components that used that hook only need to know if there is a send wallet, I replaced the useSendWallets hook with a useHasSendWallet hook.

* Add Cash App wallet

* fix changes loglevel enum

* Fix key init race condition in strict mode if no key exists yet

* Formatting

* Fix key init race condition via transactions in readwrite mode

* Replace Promise.withResolvers with regular promises

* replace generic spinner with our usual

---------

Co-authored-by: k00b <k00b@stacker.news>
This commit is contained in:
ekzyis 2025-07-15 23:36:43 +02:00 committed by GitHub
parent df299a226c
commit d89a4a429a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
208 changed files with 8568 additions and 4990 deletions

1
.gitignore vendored
View File

@ -53,6 +53,7 @@ docker-compose.*.yml
*.sql
!/prisma/migrations/*/*.sql
!/docker/db/seed.sql
!/docker/db/wallet-seed.sql
# nostr wallet connect
scripts/nwc-keys.json

View File

@ -227,7 +227,7 @@ async function performP2PAction (actionType, args, incomingContext) {
await assertBelowMaxPendingInvoices(incomingContext)
const description = await paidActions[actionType].describe(args, incomingContext)
const { invoice, wrappedInvoice, wallet, maxFee } = await createWrappedInvoice(userId, {
const { invoice, wrappedInvoice, protocol, maxFee } = await createWrappedInvoice(userId, {
msats: cost,
feePercent: sybilFeePercent,
description,
@ -239,7 +239,7 @@ async function performP2PAction (actionType, args, incomingContext) {
invoiceArgs: {
bolt11: invoice,
wrappedBolt11: wrappedInvoice,
wallet,
protocol,
maxFee
}
}
@ -269,7 +269,7 @@ async function performDirectAction (actionType, args, incomingContext) {
const description = actionDescription ?? await paidActions[actionType].describe(args, incomingContext)
for await (const { invoice, logger, wallet } of createUserInvoice(userId, {
for await (const { invoice, logger, protocol } of createUserInvoice(userId, {
msats: cost,
description,
expiry: INVOICE_EXPIRE_SECS
@ -293,7 +293,7 @@ async function performDirectAction (actionType, args, incomingContext) {
bolt11: invoice,
msats: cost,
hash,
walletId: wallet.id,
protocolId: protocol.id,
receiverId: userId
}
}),
@ -346,22 +346,26 @@ export async function retryPaidAction (actionType, args, incomingContext) {
invoiceId: failedInvoice.id
},
include: {
wallet: true
protocol: {
include: {
wallet: true
}
}
}
})
if (invoiceForward) {
// this is a wrapped invoice, we need to retry it with receiver fallbacks
try {
const { userId } = invoiceForward.wallet
const { userId } = invoiceForward.protocol.wallet
// this will return an invoice from the first receiver wallet that didn't fail yet and throw if none is available
const { invoice: bolt11, wrappedInvoice: wrappedBolt11, wallet, maxFee } = await createWrappedInvoice(userId, {
const { invoice: bolt11, wrappedInvoice: wrappedBolt11, protocol, maxFee } = await createWrappedInvoice(userId, {
msats: failedInvoice.msatsRequested,
feePercent: await action.getSybilFeePercent?.(actionArgs, retryContext),
description: await action.describe?.(actionArgs, retryContext),
expiry: INVOICE_EXPIRE_SECS
}, retryContext)
invoiceArgs = { bolt11, wrappedBolt11, wallet, maxFee }
invoiceArgs = { bolt11, wrappedBolt11, protocol, maxFee }
} catch (err) {
console.log('failed to retry wrapped invoice, falling back to SN:', err)
}
@ -429,7 +433,7 @@ async function createSNInvoice (actionType, args, context) {
async function createDbInvoice (actionType, args, context) {
const { me, models, tx, cost, optimistic, actionId, invoiceArgs, paymentAttempt, predecessorId } = context
const { bolt11, wrappedBolt11, preimage, wallet, maxFee } = invoiceArgs
const { bolt11, wrappedBolt11, preimage, protocol, maxFee } = invoiceArgs
const db = tx ?? models
@ -468,9 +472,9 @@ async function createDbInvoice (actionType, args, context) {
invoice: {
create: invoiceData
},
wallet: {
protocol: {
connect: {
id: wallet.id
id: protocol.id
}
}
}

View File

@ -39,11 +39,11 @@ export async function getInvoiceablePeer ({ id, sats, hasSendWallet }, { models,
return null
}
const wallets = await getInvoiceableWallets(item.userId, { models })
const protocols = await getInvoiceableWallets(item.userId, { models })
// request peer invoice if they have an attached wallet and have not forwarded the item
// and the receiver doesn't want to receive credits
if (wallets.length > 0 &&
if (protocols.length > 0 &&
item.itemForwards.length === 0 &&
sats >= item.user.receiveCreditsBelowSats) {
return item.userId

View File

@ -6,9 +6,9 @@ import { parsePaymentRequest, payViaPaymentRequest } from 'ln-service'
// paying actions are completely distinct from paid actions
// and there's only one paying action: send
// ... still we want the api to at least be similar
export default async function performPayingAction ({ bolt11, maxFee, walletId }, { me, models, lnd }) {
export default async function performPayingAction ({ bolt11, maxFee, protocolId }, { me, models, lnd }) {
try {
console.group('performPayingAction', `${bolt11.slice(0, 10)}...`, maxFee, walletId)
console.group('performPayingAction', `${bolt11.slice(0, 10)}...`, maxFee, protocolId)
if (!me) {
throw new Error('You must be logged in to perform this action')
@ -34,8 +34,8 @@ export default async function performPayingAction ({ bolt11, maxFee, walletId },
msatsPaying: toPositiveBigInt(decoded.mtokens),
msatsFeePaying: satsToMsats(maxFee),
userId: me.id,
walletId,
autoWithdraw: !!walletId
protocolId,
autoWithdraw: !!protocolId
}
})
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted })

View File

@ -1,7 +1,8 @@
import user from './user'
import message from './message'
import item from './item'
import wallet from './wallet'
import walletV1 from './wallet'
import walletV2 from '@/wallets/server/resolvers'
import lnurl from './lnurl'
import notifications from './notifications'
import invite from './invite'
@ -19,7 +20,6 @@ import chainFee from './chainFee'
import { GraphQLScalarType, Kind } from 'graphql'
import { createIntScalar } from 'graphql-scalar'
import paidAction from './paidAction'
import vault from './vault'
const date = new GraphQLScalarType({
name: 'Date',
@ -54,6 +54,6 @@ const limit = createIntScalar({
maximum: 1000
})
export default [user, item, message, wallet, lnurl, notifications, invite, sub,
export default [user, item, message, walletV1, walletV2, lnurl, notifications, invite, sub,
upload, search, growth, rewards, referrals, price, admin, blockHeight, chainFee,
{ JSONObject }, { Date: date }, { Limit: limit }, paidAction, vault]
{ JSONObject }, { Date: date }, { Limit: limit }, paidAction]

View File

@ -1,53 +0,0 @@
import { E_VAULT_KEY_EXISTS, GqlAuthenticationError, GqlInputError } from '@/lib/error'
export default {
Query: {
getVaultEntries: async (parent, args, { me, models }) => {
if (!me) throw new GqlAuthenticationError()
return await models.vaultEntry.findMany({ where: { userId: me.id } })
}
},
Mutation: {
// atomic vault migration
updateVaultKey: async (parent, { entries, hash }, { me, models }) => {
if (!me) throw new GqlAuthenticationError()
if (!hash) throw new GqlInputError('hash required')
const txs = []
const { vaultKeyHash: oldKeyHash } = await models.user.findUnique({ where: { id: me.id } })
if (oldKeyHash) {
if (oldKeyHash !== hash) {
throw new GqlInputError('vault key already set', E_VAULT_KEY_EXISTS)
} else {
return true
}
} else {
txs.push(models.user.update({
where: { id: me.id },
data: { vaultKeyHash: hash }
}))
}
for (const entry of entries) {
txs.push(models.vaultEntry.update({
where: { userId_key: { userId: me.id, key: entry.key } },
data: { value: entry.value, iv: entry.iv }
}))
}
await models.$transaction(txs)
return true
},
clearVault: async (parent, args, { me, models }) => {
if (!me) throw new GqlAuthenticationError()
const txs = []
txs.push(models.user.update({
where: { id: me.id },
data: { vaultKeyHash: '' }
}))
txs.push(models.vaultEntry.deleteMany({ where: { userId: me.id } }))
await models.$transaction(txs)
return true
}
}
}

View File

@ -8,7 +8,6 @@ import { SELECT, itemQueryWithMeta } from './item'
import { formatMsats, msatsToSats, msatsToSatsDecimal } from '@/lib/format'
import {
USER_ID, INVOICE_RETENTION_DAYS,
WALLET_CREATE_INVOICE_TIMEOUT_MS,
WALLET_RETRY_AFTER_MS,
WALLET_RETRY_BEFORE_MS,
WALLET_MAX_RETRIES
@ -18,76 +17,12 @@ import assertGofacYourself from './ofac'
import assertApiKeyNotPermitted from './apiKey'
import { bolt11Tags } from '@/lib/bolt11'
import { finalizeHodlInvoice } from '@/worker/wallet'
import walletDefs from '@/wallets/server'
import { generateResolverName, generateTypeDefName } from '@/wallets/graphql'
import { lnAddrOptions } from '@/lib/lnurl'
import { GqlAuthenticationError, GqlAuthorizationError, GqlInputError } from '@/lib/error'
import { getNodeSockets } from '../lnd'
import validateWallet from '@/wallets/validate'
import { canReceive, getWalletByType } from '@/wallets/common'
import performPaidAction from '../paidAction'
import performPayingAction from '../payingAction'
import { timeoutSignal, withTimeout } from '@/lib/time'
function injectResolvers (resolvers) {
console.group('injected GraphQL resolvers:')
for (const walletDef of walletDefs) {
const resolverName = generateResolverName(walletDef.walletField)
console.log(resolverName)
resolvers.Mutation[resolverName] = async (parent, { settings, validateLightning, vaultEntries, ...data }, { me, models }) => {
console.log('resolving', resolverName, { settings, validateLightning, vaultEntries, ...data })
let existingVaultEntries
if (typeof vaultEntries === 'undefined' && data.id) {
// this mutation was sent from an unsynced client
// to pass validation, we need to add the existing vault entries for validation
// in case the client is removing the receiving config
existingVaultEntries = await models.vaultEntry.findMany({
where: {
walletId: Number(data.id)
}
})
}
const validData = await validateWallet(walletDef,
{ ...data, ...settings, vaultEntries: vaultEntries ?? existingVaultEntries },
{ serverSide: true })
if (validData) {
data && Object.keys(validData).filter(key => key in data).forEach(key => { data[key] = validData[key] })
settings && Object.keys(validData).filter(key => key in settings).forEach(key => { settings[key] = validData[key] })
}
// wallet in shape of db row
const wallet = {
field: walletDef.walletField,
type: walletDef.walletType,
userId: me?.id
}
const logger = walletLogger({ wallet, models })
return await upsertWallet({
wallet,
walletDef,
testCreateInvoice:
walletDef.testCreateInvoice && validateLightning && canReceive({ def: walletDef, config: data })
? (data) => withTimeout(
walletDef.testCreateInvoice(data, {
logger,
signal: timeoutSignal(WALLET_CREATE_INVOICE_TIMEOUT_MS)
}),
WALLET_CREATE_INVOICE_TIMEOUT_MS)
: null
}, {
settings,
data,
vaultEntries
}, { logger, me, models })
}
}
console.groupEnd()
return resolvers
}
import { logContextFromBolt11 } from '@/wallets/server'
export async function getInvoice (parent, { id }, { me, models, lnd }) {
const inv = await models.invoice.findUnique({
@ -153,23 +88,6 @@ export function verifyHmac (hash, hmac) {
const resolvers = {
Query: {
invoice: getInvoice,
wallets: async (parent, args, { me, models }) => {
if (!me) {
throw new GqlAuthenticationError()
}
return await models.wallet.findMany({
include: {
vaultEntries: true
},
where: {
userId: me.id
},
orderBy: {
priority: 'asc'
}
})
},
withdrawl: getWithdrawl,
direct: async (parent, { id }, { me, models }) => {
if (!me) {
@ -375,67 +293,6 @@ const resolvers = {
facts: history
}
},
walletLogs: async (parent, { type, from, to, cursor }, { me, models }) => {
if (!me) {
throw new GqlAuthenticationError()
}
// we cursoring with the wallet logs on the client
// if we have from, don't use cursor
// regardless, store the state of the cursor for the next call
const decodedCursor = cursor ? decodeCursor(cursor) : { offset: 0, time: to ?? new Date() }
let logs = []
let nextCursor
if (from) {
logs = await models.walletLog.findMany({
where: {
userId: me.id,
wallet: type ?? undefined,
createdAt: {
gt: from ? new Date(Number(from)) : undefined,
lte: to ? new Date(Number(to)) : undefined
}
},
include: {
invoice: true,
withdrawal: true
},
orderBy: [
{ createdAt: 'desc' },
{ id: 'desc' }
]
})
nextCursor = nextCursorEncoded(decodedCursor, logs.length)
} else {
logs = await models.walletLog.findMany({
where: {
userId: me.id,
wallet: type ?? undefined,
createdAt: {
lte: decodedCursor.time
}
},
include: {
invoice: true,
withdrawal: true
},
orderBy: [
{ createdAt: 'desc' },
{ id: 'desc' }
],
take: LIMIT,
skip: decodedCursor.offset
})
nextCursor = logs.length === LIMIT ? nextCursorEncoded(decodedCursor, logs.length) : null
}
return {
cursor: nextCursor,
entries: logs
}
},
failedInvoices: async (parent, args, { me, models }) => {
if (!me) {
throw new GqlAuthenticationError()
@ -459,17 +316,6 @@ const resolvers = {
ORDER BY id DESC`
}
},
Wallet: {
wallet: async (wallet) => {
return {
...wallet.wallet,
__resolveType: generateTypeDefName(wallet.type)
}
}
},
WalletDetails: {
__resolveType: wallet => wallet.__resolveType
},
InvoiceOrDirect: {
__resolveType: invoiceOrDirect => invoiceOrDirect.__resolveType
},
@ -534,43 +380,6 @@ const resolvers = {
return true
},
setWalletPriority: async (parent, { id, priority }, { me, models }) => {
if (!me) {
throw new GqlAuthenticationError()
}
await models.wallet.update({ where: { userId: me.id, id: Number(id) }, data: { priority } })
return true
},
removeWallet: async (parent, { id }, { me, models }) => {
if (!me) {
throw new GqlAuthenticationError()
}
const wallet = await models.wallet.findUnique({ where: { userId: me.id, id: Number(id) } })
if (!wallet) {
throw new GqlInputError('wallet not found')
}
const logger = walletLogger({ wallet, models })
await models.wallet.delete({ where: { userId: me.id, id: Number(id) } })
if (canReceive({ def: getWalletByType(wallet.type), config: wallet.wallet })) {
logger.info('details for receiving deleted')
}
return true
},
deleteWalletLogs: async (parent, { wallet }, { me, models }) => {
if (!me) {
throw new GqlAuthenticationError()
}
await models.walletLog.deleteMany({ where: { userId: me.id, wallet } })
return true
},
buyCredits: async (parent, { credits }, { me, models, lnd }) => {
return await performPaidAction('BUY_CREDITS', { credits }, { models, me, lnd })
}
@ -736,205 +545,9 @@ const resolvers = {
}
}
export default injectResolvers(resolvers)
export default resolvers
const logContextFromBolt11 = async (bolt11) => {
const decoded = await parsePaymentRequest({ request: bolt11 })
return {
bolt11,
amount: formatMsats(decoded.mtokens),
payment_hash: decoded.id,
created_at: decoded.created_at,
expires_at: decoded.expires_at,
description: decoded.description
}
}
export const walletLogger = ({ wallet, models, me }) => {
// no-op logger if no wallet or user provided
if (!wallet && !me) {
return {
ok: () => {},
info: () => {},
error: () => {},
warn: () => {}
}
}
// server implementation of wallet logger interface on client
const log = (level) => async (message, ctx = {}) => {
try {
let { invoiceId, withdrawalId, ...context } = ctx
if (context.bolt11) {
// automatically populate context from bolt11 to avoid duplicating this code
context = {
...context,
...await logContextFromBolt11(context.bolt11)
}
}
await models.walletLog.create({
data: {
userId: wallet?.userId ?? me.id,
// system logs have no wallet
wallet: wallet?.type,
level,
message,
context,
invoiceId,
withdrawalId
}
})
} catch (err) {
console.error('error creating wallet log:', err)
}
}
return {
ok: (message, context) => log('SUCCESS')(message, context),
info: (message, context) => log('INFO')(message, context),
error: (message, context) => log('ERROR')(message, context),
warn: (message, context) => log('WARN')(message, context)
}
}
async function upsertWallet (
{ wallet, walletDef, testCreateInvoice }, { settings, data, vaultEntries }, { logger, me, models }) {
if (!me) {
throw new GqlAuthenticationError()
}
assertApiKeyNotPermitted({ me })
if (testCreateInvoice) {
try {
const pr = await testCreateInvoice(data)
if (!pr || typeof pr !== 'string' || !pr.startsWith('lnbc')) {
throw new GqlInputError('not a valid payment request')
}
} catch (err) {
const message = 'failed to create test invoice: ' + (err.message || err.toString?.())
logger.error(message)
throw new GqlInputError(message)
}
}
const { id, enabled, priority, ...recvConfig } = data
const txs = []
if (id) {
const oldVaultEntries = await models.vaultEntry.findMany({ where: { userId: me.id, walletId: Number(id) } })
// createMany is the set difference of the new - old
// deleteMany is the set difference of the old - new
// updateMany is the intersection of the old and new
const difference = (a = [], b = [], key = 'key') => a.filter(x => !b.find(y => y[key] === x[key]))
const intersectionMerge = (a = [], b = [], key = 'key') => a.filter(x => b.find(y => y[key] === x[key]))
.map(x => ({ [key]: x[key], ...b.find(y => y[key] === x[key]) }))
txs.push(
models.wallet.update({
where: { id: Number(id), userId: me.id },
data: {
enabled,
priority,
// client only wallets have no receive config and thus don't have their own table
...(Object.keys(recvConfig).length > 0
? {
[wallet.field]: {
upsert: {
create: recvConfig,
update: recvConfig
}
}
}
: {}),
...(vaultEntries
? {
vaultEntries: {
deleteMany: difference(oldVaultEntries, vaultEntries, 'key').map(({ key }) => ({
userId: me.id, key
})),
create: difference(vaultEntries, oldVaultEntries, 'key').map(({ key, iv, value }) => ({
key, iv, value, userId: me.id
})),
update: intersectionMerge(oldVaultEntries, vaultEntries, 'key').map(({ key, iv, value }) => ({
where: { userId_key: { userId: me.id, key } },
data: { value, iv }
}))
}
}
: {})
},
include: {
vaultEntries: true
}
})
)
} else {
txs.push(
models.wallet.create({
include: {
vaultEntries: true
},
data: {
enabled,
priority,
userId: me.id,
type: wallet.type,
// client only wallets have no receive config and thus don't have their own table
...(Object.keys(recvConfig).length > 0 ? { [wallet.field]: { create: recvConfig } } : {}),
...(vaultEntries
? {
vaultEntries: {
createMany: {
data: vaultEntries?.map(({ key, iv, value }) => ({ key, iv, value, userId: me.id }))
}
}
}
: {})
}
})
)
}
if (settings) {
txs.push(
models.user.update({
where: { id: me.id },
data: settings
})
)
}
if (canReceive({ def: walletDef, config: recvConfig })) {
txs.push(
models.walletLog.createMany({
data: {
userId: me.id,
wallet: wallet.type,
level: 'SUCCESS',
message: id ? 'details for receiving updated' : 'details for receiving saved'
}
}),
models.walletLog.create({
data: {
userId: me.id,
wallet: wallet.type,
level: enabled ? 'SUCCESS' : 'INFO',
message: enabled ? 'receiving enabled' : 'receiving disabled'
}
})
)
}
const [upsertedWallet] = await models.$transaction(txs)
return upsertedWallet
}
export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, headers, wallet, logger }) {
export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, headers, protocol, logger }) {
assertApiKeyNotPermitted({ me })
await validateSchema(withdrawlSchema, { invoice, maxFee })
await assertGofacYourself({ models, headers })
@ -984,7 +597,7 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model
throw new GqlInputError('SN cannot pay an invoice that SN is proxying')
}
return await performPayingAction({ bolt11: invoice, maxFee, walletId: wallet?.id }, { me, models, lnd })
return await performPayingAction({ bolt11: invoice, maxFee, protocolId: protocol?.id }, { me, models, lnd })
}
async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ...payer },

View File

@ -18,7 +18,6 @@ import admin from './admin'
import blockHeight from './blockHeight'
import chainFee from './chainFee'
import paidAction from './paidAction'
import vault from './vault'
const common = gql`
type Query {
@ -39,4 +38,4 @@ const common = gql`
`
export default [common, user, item, itemForward, message, wallet, lnurl, notifications, invite,
sub, upload, growth, rewards, referrals, price, admin, blockHeight, chainFee, paidAction, vault]
sub, upload, growth, rewards, referrals, price, admin, blockHeight, chainFee, paidAction]

View File

@ -124,9 +124,6 @@ export default gql`
zapUndos: Int
wildWestMode: Boolean!
withdrawMaxFeeDefault: Int!
proxyReceive: Boolean
receiveCreditsBelowSats: Int!
sendCreditsBelowSats: Int!
}
type AuthMethods {
@ -157,6 +154,7 @@ export default gql`
upvotePopover: Boolean!
hasInvites: Boolean!
apiKeyEnabled: Boolean!
showPassphrase: Boolean!
"""
mirrors SettingsInput
@ -203,14 +201,8 @@ export default gql`
wildWestMode: Boolean!
withdrawMaxFeeDefault: Int!
autoWithdrawThreshold: Int
autoWithdrawMaxFeePercent: Float
autoWithdrawMaxFeeTotal: Int
vaultKeyHash: String
walletsUpdatedAt: Date
proxyReceive: Boolean
directReceive: Boolean @deprecated
receiveCreditsBelowSats: Int!
sendCreditsBelowSats: Int!
}
type UserOptional {

View File

@ -1,28 +0,0 @@
import { gql } from 'graphql-tag'
export default gql`
type VaultEntry {
id: ID!
key: String!
iv: String!
value: String!
createdAt: Date!
updatedAt: Date!
}
input VaultEntryInput {
key: String!
iv: String!
value: String!
walletId: ID
}
extend type Query {
getVaultEntries: [VaultEntry!]!
}
extend type Mutation {
clearVault: Boolean
updateVaultKey(entries: [VaultEntryInput!]!, hash: String!): Boolean
}
`

View File

@ -1,66 +1,6 @@
import { gql } from 'graphql-tag'
import { fieldToGqlArg, fieldToGqlArgOptional, generateResolverName, generateTypeDefName } from '@/wallets/graphql'
import { isServerField } from '@/wallets/common'
import walletDefs from '@/wallets/server'
function injectTypeDefs (typeDefs) {
const injected = [rawTypeDefs(), mutationTypeDefs()]
return `${typeDefs}\n\n${injected.join('\n\n')}\n`
}
function mutationTypeDefs () {
console.group('injected GraphQL mutations:')
const typeDefs = walletDefs.map((w) => {
let args = 'id: ID, '
const serverFields = w.fields
.filter(isServerField)
.map(fieldToGqlArgOptional)
if (serverFields.length > 0) args += serverFields.join(', ') + ','
args += 'enabled: Boolean, priority: Int, vaultEntries: [VaultEntryInput!], settings: AutowithdrawSettings, validateLightning: Boolean'
const resolverName = generateResolverName(w.walletField)
const typeDef = `${resolverName}(${args}): Wallet`
console.log(typeDef)
return typeDef
})
console.groupEnd()
return `extend type Mutation {\n${typeDefs.join('\n')}\n}`
}
function rawTypeDefs () {
console.group('injected GraphQL type defs:')
const typeDefs = walletDefs.map((w) => {
let args = w.fields
.filter(isServerField)
.map(fieldToGqlArg)
.map(s => ' ' + s)
.join('\n')
if (!args) {
// add a placeholder arg so the type is not empty
args = ' _empty: Boolean'
}
const typeDefName = generateTypeDefName(w.walletType)
const typeDef = `type ${typeDefName} {\n${args}\n}`
console.log(typeDef)
return typeDef
})
let union = 'union WalletDetails = '
union += walletDefs.map((w) => {
const typeDefName = generateTypeDefName(w.walletType)
return typeDefName
}).join(' | ')
console.log(union)
console.groupEnd()
return typeDefs.join('\n\n') + union
}
const typeDefs = `
const typeDefs = gql`
extend type Query {
invoice(id: ID!): Invoice!
withdrawl(id: ID!): Withdrawl!
@ -68,8 +8,10 @@ const typeDefs = `
numBolt11s: Int!
connectAddress: String!
walletHistory(cursor: String, inc: String): History
wallets: [Wallet!]!
walletLogs(type: String, from: String, to: String, cursor: String): WalletLog!
wallets: [WalletOrTemplate!]!
wallet(id: ID, name: String): WalletOrTemplate
walletSettings: WalletSettings!
walletLogs(protocolId: Int, cursor: String): WalletLogs!
failedInvoices: [Invoice!]!
}
@ -79,9 +21,30 @@ const typeDefs = `
cancelInvoice(hash: String!, hmac: String, userCancel: Boolean): Invoice!
dropBolt11(hash: String!): Boolean
removeWallet(id: ID!): Boolean
deleteWalletLogs(wallet: String): Boolean
setWalletPriority(id: ID!, priority: Int!): Boolean
deleteWalletLogs(protocolId: Int): Boolean
setWalletPriorities(priorities: [WalletPriorityUpdate!]!): Boolean
buyCredits(credits: Int!): BuyCreditsPaidAction!
upsertWalletSendLNbits(walletId: ID, templateName: ID, enabled: Boolean!, url: String!, apiKey: VaultEntryInput!): WalletSendLNbits!
upsertWalletRecvLNbits(walletId: ID, templateName: ID, enabled: Boolean!, networkTests: Boolean, url: String!, apiKey: String!): WalletRecvLNbits!
upsertWalletSendPhoenixd(walletId: ID, templateName: ID, enabled: Boolean!, url: String!, apiKey: VaultEntryInput!): WalletSendPhoenixd!
upsertWalletRecvPhoenixd(walletId: ID, templateName: ID, enabled: Boolean!, networkTests: Boolean, url: String!, apiKey: String!): WalletRecvPhoenixd!
upsertWalletSendBlink(walletId: ID, templateName: ID, enabled: Boolean!, currency: VaultEntryInput!, apiKey: VaultEntryInput!): WalletSendBlink!
upsertWalletRecvBlink(walletId: ID, templateName: ID, enabled: Boolean!, networkTests: Boolean, currency: String!, apiKey: String!): WalletRecvBlink!
upsertWalletRecvLightningAddress(walletId: ID, templateName: ID, enabled: Boolean!, networkTests: Boolean, address: String!): WalletRecvLightningAddress!
upsertWalletSendNWC(walletId: ID, templateName: ID, enabled: Boolean!, url: VaultEntryInput!): WalletSendNWC!
upsertWalletRecvNWC(walletId: ID, templateName: ID, enabled: Boolean!, networkTests: Boolean, url: String!): WalletRecvNWC!
upsertWalletRecvCLNRest(walletId: ID, templateName: ID, enabled: Boolean!, networkTests: Boolean, socket: String!, rune: String!, cert: String): WalletRecvCLNRest!
upsertWalletRecvLNDGRPC(walletId: ID, templateName: ID, enabled: Boolean!, networkTests: Boolean, socket: String!, macaroon: String!, cert: String): WalletRecvLNDGRPC!
upsertWalletSendLNC(walletId: ID, templateName: ID, enabled: Boolean!, pairingPhrase: VaultEntryInput!, localKey: VaultEntryInput!, remoteKey: VaultEntryInput!, serverHost: VaultEntryInput!): WalletSendLNC!
upsertWalletSendWebLN(walletId: ID, templateName: ID, enabled: Boolean!): WalletSendWebLN!
removeWalletProtocol(id: ID!): Boolean
updateWalletEncryption(keyHash: String!, wallets: [WalletEncryptionUpdate!]!): Boolean
updateKeyHash(keyHash: String!): Boolean
resetWallets(newKeyHash: String!): Boolean
disablePassphraseExport: Boolean
setWalletSettings(settings: WalletSettingsInput!): Boolean
addWalletLog(protocolId: Int!, level: String!, message: String!, timestamp: Date!, invoiceId: Int): Boolean
}
type BuyCreditsResult {
@ -92,15 +55,155 @@ const typeDefs = `
id: ID!
}
union WalletOrTemplate = Wallet | WalletTemplate
enum WalletStatus {
OK
WARNING
ERROR
DISABLED
}
type Wallet {
id: ID!
createdAt: Date!
updatedAt: Date!
type: String!
enabled: Boolean!
name: String!
priority: Int!
wallet: WalletDetails!
vaultEntries: [VaultEntry!]!
template: WalletTemplate!
protocols: [WalletProtocol!]!
send: WalletStatus!
receive: WalletStatus!
}
type WalletTemplate {
name: ID!
protocols: [WalletProtocolTemplate!]!
send: WalletStatus!
receive: WalletStatus!
}
type WalletProtocol {
id: ID!
name: String!
send: Boolean!
enabled: Boolean!
config: WalletProtocolConfig!
status: WalletStatus!
}
type WalletProtocolTemplate {
id: ID!
name: String!
send: Boolean!
}
union WalletProtocolConfig =
| WalletSendNWC
| WalletSendLNbits
| WalletSendPhoenixd
| WalletSendBlink
| WalletSendWebLN
| WalletSendLNC
| WalletRecvNWC
| WalletRecvLNbits
| WalletRecvPhoenixd
| WalletRecvBlink
| WalletRecvLightningAddress
| WalletRecvCLNRest
| WalletRecvLNDGRPC
type WalletSettings {
receiveCreditsBelowSats: Int!
sendCreditsBelowSats: Int!
autoWithdrawThreshold: Int
autoWithdrawMaxFeePercent: Float
autoWithdrawMaxFeeTotal: Int
proxyReceive: Boolean!
}
input WalletSettingsInput {
receiveCreditsBelowSats: Int!
sendCreditsBelowSats: Int!
autoWithdrawThreshold: Int!
autoWithdrawMaxFeePercent: Float!
autoWithdrawMaxFeeTotal: Int!
proxyReceive: Boolean!
}
type WalletSendNWC {
id: ID!
url: VaultEntry!
}
type WalletSendLNbits {
id: ID!
url: String!
apiKey: VaultEntry!
}
type WalletSendPhoenixd {
id: ID!
url: String!
apiKey: VaultEntry!
}
type WalletSendBlink {
id: ID!
currency: VaultEntry!
apiKey: VaultEntry!
}
type WalletSendWebLN {
id: ID!
}
type WalletSendLNC {
id: ID!
pairingPhrase: VaultEntry!
localKey: VaultEntry!
remoteKey: VaultEntry!
serverHost: VaultEntry!
}
type WalletRecvNWC {
id: ID!
url: String!
}
type WalletRecvLNbits {
id: ID!
url: String!
apiKey: String!
}
type WalletRecvPhoenixd {
id: ID!
url: String!
apiKey: String!
}
type WalletRecvBlink {
id: ID!
currency: String!
apiKey: String!
}
type WalletRecvLightningAddress {
id: ID!
address: String!
}
type WalletRecvCLNRest {
id: ID!
socket: String!
rune: String!
cert: String
}
type WalletRecvLNDGRPC {
id: ID!
socket: String!
macaroon: String!
cert: String
}
input AutowithdrawSettings {
@ -109,6 +212,22 @@ const typeDefs = `
autoWithdrawMaxFeeTotal: Int!
}
input WalletEncryptionUpdate {
id: ID!
protocols: [WalletEncryptionUpdateProtocol!]!
}
input WalletEncryptionUpdateProtocol {
name: String!
send: Boolean!
config: JSONObject!
}
input WalletPriorityUpdate {
id: ID!
priority: Int!
}
type Invoice implements InvoiceOrDirect {
id: ID!
createdAt: Date!
@ -183,7 +302,7 @@ const typeDefs = `
cursor: String
}
type WalletLog {
type WalletLogs {
entries: [WalletLogEntry!]!
cursor: String
}
@ -191,11 +310,25 @@ const typeDefs = `
type WalletLogEntry {
id: ID!
createdAt: Date!
wallet: ID
wallet: Wallet
protocol: WalletProtocol
level: String!
message: String!
context: JSONObject
}
`
export default gql`${injectTypeDefs(typeDefs)}`
type VaultEntry {
id: ID!
iv: String!
value: String!
createdAt: Date!
updatedAt: Date!
}
input VaultEntryInput {
iv: String!
value: String!
keyHash: String!
}
`
export default typeDefs

View File

@ -1,76 +0,0 @@
import { InputGroup } from 'react-bootstrap'
import { Input } from './form'
import { useMe } from './me'
import { useEffect, useState } from 'react'
import { isNumber } from '@/lib/format'
import Link from 'next/link'
function autoWithdrawThreshold ({ me }) {
return isNumber(me?.privates?.autoWithdrawThreshold) ? me?.privates?.autoWithdrawThreshold : 10000
}
export function autowithdrawInitial ({ me }) {
return {
autoWithdrawThreshold: autoWithdrawThreshold({ me }),
autoWithdrawMaxFeePercent: isNumber(me?.privates?.autoWithdrawMaxFeePercent) ? me?.privates?.autoWithdrawMaxFeePercent : 1,
autoWithdrawMaxFeeTotal: isNumber(me?.privates?.autoWithdrawMaxFeeTotal) ? me?.privates?.autoWithdrawMaxFeeTotal : 1
}
}
export function AutowithdrawSettings () {
const { me } = useMe()
const threshold = autoWithdrawThreshold({ me })
const [sendThreshold, setSendThreshold] = useState(Math.max(Math.floor(threshold / 10), 1))
useEffect(() => {
setSendThreshold(Math.max(Math.floor(threshold / 10), 1))
}, [autoWithdrawThreshold])
return (
<>
<div className='my-4 border border-3 rounded'>
<div className='p-3'>
<h3 className='text-center text-muted'>desired balance</h3>
<h6 className='text-center pb-3'>applies globally to all autowithdraw methods</h6>
<Input
label='desired balance'
name='autoWithdrawThreshold'
onChange={(formik, e) => {
const value = e.target.value
setSendThreshold(Math.max(Math.floor(value / 10), 1))
}}
hint={isNumber(sendThreshold) ? `will attempt auto-withdraw when your balance exceeds ${sendThreshold * 11} sats` : undefined}
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
required
/>
<h3 className='text-center text-muted pt-3'>network fees</h3>
<h6 className='text-center pb-3'>
we'll use whichever setting is higher during{' '}
<Link
target='_blank'
href='https://docs.lightning.engineering/the-lightning-network/pathfinding'
rel='noreferrer'
>pathfinding
</Link>
</h6>
<Input
label='max fee rate'
name='autoWithdrawMaxFeePercent'
hint='max fee as percent of withdrawal amount'
append={<InputGroup.Text>%</InputGroup.Text>}
required
/>
<Input
label='max fee total'
name='autoWithdrawMaxFeeTotal'
hint='max fee for any withdrawal amount'
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
required
/>
</div>
</div>
</>
)
}

View File

@ -5,7 +5,6 @@ import { useMe } from '@/components/me'
import { useMutation } from '@apollo/client'
import { WELCOME_BANNER_MUTATION } from '@/fragments/users'
import { useToast } from '@/components/toast'
import Link from 'next/link'
export function WelcomeBanner ({ Banner }) {
const { me } = useMe()
@ -100,22 +99,6 @@ export function MadnessBanner ({ handleClose }) {
)
}
export function WalletSecurityBanner ({ isActive }) {
return (
<Alert className={styles.banner} key='info' variant='warning'>
<Alert.Heading>
Gunslingin' Safety Tips
</Alert.Heading>
<p className='mb-3 line-height-md'>
Listen up, pardner! Put a limit on yer spendin' wallet or hook up a wallet that's only for Stacker News. It'll keep them varmints from cleanin' out yer whole goldmine if they rustle up yer wallet.
</p>
<p className='line-height-md'>
Your spending wallet's credentials are never sent to our servers in plain text. To sync across devices, <Alert.Link as={Link} href='/settings/passphrase'>enable device sync in your settings</Alert.Link>.
</p>
</Alert>
)
}
export function AuthBanner () {
return (
<Alert className={`${styles.banner} mt-0`} key='info' variant='danger'>

View File

@ -34,12 +34,9 @@ import Info from './info'
import { useMe } from './me'
import classNames from 'classnames'
import Clipboard from '@/svgs/clipboard-line.svg'
import QrIcon from '@/svgs/qr-code-line.svg'
import QrScanIcon from '@/svgs/qr-scan-line.svg'
import { useShowModal } from './modal'
import { QRCodeSVG } from 'qrcode.react'
import dynamic from 'next/dynamic'
import { qrImageSettings } from './qr'
import { useIsClient } from './use-client'
import PageLoading from './page-loading'
@ -78,7 +75,7 @@ export function SubmitButton ({
)
}
function CopyButton ({ value, icon, ...props }) {
export function CopyButton ({ value, icon, ...props }) {
const toaster = useToast()
const [copied, setCopied] = useState(false)
@ -1333,33 +1330,6 @@ function PasswordHider ({ onClick, showPass }) {
)
}
function QrPassword ({ value }) {
const showModal = useShowModal()
const toaster = useToast()
const showQr = useCallback(() => {
showModal(close => (
<div>
<p className='line-height-md text-muted'>Import this passphrase into another device by navigating to device sync settings and scanning this QR code</p>
<div className='d-block p-3 mx-auto' style={{ background: 'white', maxWidth: '300px' }}>
<QRCodeSVG className='h-auto mw-100' value={value} size={300} imageSettings={qrImageSettings} />
</div>
</div>
))
}, [toaster, value, showModal])
return (
<>
<InputGroup.Text
style={{ cursor: 'pointer' }}
onClick={showQr}
>
<QrIcon height={16} width={16} />
</InputGroup.Text>
</>
)
}
function PasswordScanner ({ onScan, text }) {
const showModal = useShowModal()
const toaster = useToast()
@ -1422,12 +1392,12 @@ export function PasswordInput ({ newPass, qr, copy, readOnly, append, value: ini
{copy && (
<CopyButton icon value={field?.value} />
)}
{qr && (readOnly
? <QrPassword value={field?.value} />
: <PasswordScanner
text="Where'd you learn to square dance?"
onScan={v => helpers.setValue(v)}
/>)}
{qr && (
<PasswordScanner
text="Where'd you learn to square dance?"
onScan={v => helpers.setValue(v)}
/>
)}
{append}
</>
)

View File

@ -6,9 +6,9 @@ import { CompactLongCountdown } from './countdown'
import PayerData from './payer-data'
import Bolt11Info from './bolt11-info'
import { useQuery } from '@apollo/client'
import { INVOICE } from '@/fragments/wallet'
import { INVOICE } from '@/fragments/invoice'
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
import { WalletConfigurationError, WalletPaymentAggregateError } from '@/wallets/errors'
import { WalletConfigurationError, WalletPaymentAggregateError } from '@/wallets/client/errors'
import ItemJob from './item-job'
import Item from './item'
import { CommentFlat } from './comment'

View File

@ -12,7 +12,7 @@ import { usePaidMutation } from './use-paid-mutation'
import { ACT_MUTATION } from '@/fragments/paidAction'
import { meAnonSats } from '@/lib/apollo'
import { BoostItemInput } from './adv-post-form'
import { useSendWallets } from '@/wallets/index'
import { useHasSendWallet } from '@/wallets/client/hooks'
import { useAnimation } from '@/components/animation'
const defaultTips = [100, 1000, 10_000, 100_000]
@ -88,7 +88,7 @@ function BoostForm ({ step, onSubmit, children, item, oValue, inputRef, act = 'B
export default function ItemAct ({ onClose, item, act = 'TIP', step, children, abortSignal }) {
const inputRef = useRef(null)
const { me } = useMe()
const wallets = useSendWallets()
const hasSendWallet = useHasSendWallet()
const [oValue, setOValue] = useState()
useEffect(() => {
@ -116,7 +116,7 @@ export default function ItemAct ({ onClose, item, act = 'TIP', step, children, a
if (!me) setItemMeAnonSats({ id: item.id, amount })
}
const closeImmediately = wallets.length > 0 || me?.privates?.sats > Number(amount)
const closeImmediately = hasSendWallet || me?.privates?.sats > Number(amount)
if (closeImmediately) {
onPaid()
}
@ -126,7 +126,7 @@ export default function ItemAct ({ onClose, item, act = 'TIP', step, children, a
id: item.id,
sats: Number(amount),
act,
hasSendWallet: wallets.length > 0
hasSendWallet
},
optimisticResponse: me
? {
@ -143,7 +143,7 @@ export default function ItemAct ({ onClose, item, act = 'TIP', step, children, a
})
if (error) throw error
addCustomTip(Number(amount))
}, [me, actor, wallets.length, act, item.id, onClose, abortSignal, animate])
}, [me, actor, hasSendWallet, act, item.id, onClose, abortSignal, animate])
return act === 'BOOST'
? <BoostForm step={step} onSubmit={onSubmit} item={item} inputRef={inputRef} act={act}>{children}</BoostForm>
@ -263,13 +263,13 @@ export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
// because the mutation name we use varies,
// we need to extract the result/invoice from the response
const getPaidActionResult = data => Object.values(data)[0]
const wallets = useSendWallets()
const hasSendWallet = useHasSendWallet()
const [act] = usePaidMutation(query, {
waitFor: inv =>
// if we have attached wallets, we might be paying a wrapped invoice in which case we need to make sure
// we don't prematurely consider the payment as successful (important for receiver fallbacks)
wallets.length > 0
hasSendWallet
? inv?.actionState === 'PAID'
: inv?.satsReceived > 0,
...options,
@ -298,7 +298,7 @@ export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
}
export function useZap () {
const wallets = useSendWallets()
const hasSendWallet = useHasSendWallet()
const act = useAct()
const animate = useAnimation()
const toaster = useToast()
@ -309,14 +309,14 @@ export function useZap () {
// add current sats to next tip since idempotent zaps use desired total zap not difference
const sats = nextTip(meSats, { ...me?.privates })
const variables = { id: item.id, sats, act: 'TIP', hasSendWallet: wallets.length > 0 }
const variables = { id: item.id, sats, act: 'TIP', hasSendWallet }
const optimisticResponse = { act: { __typename: 'ItemActPaidAction', result: { path: item.path, ...variables } } }
try {
await abortSignal.pause({ me, amount: sats })
animate()
// batch zaps if wallet is enabled or using fee credits so they can be executed serially in a single request
const { error } = await act({ variables, optimisticResponse, context: { batch: wallets.length > 0 || me?.privates?.sats > sats } })
const { error } = await act({ variables, optimisticResponse, context: { batch: hasSendWallet || me?.privates?.sats > sats } })
if (error) throw error
} catch (error) {
if (error instanceof ActCanceledError) {
@ -327,7 +327,7 @@ export function useZap () {
// but right now this toast is noisy for optimistic zaps
console.error(error)
}
}, [act, toaster, animate, wallets])
}, [act, toaster, animate, hasSendWallet])
}
export class ActCanceledError extends Error {

View File

@ -1,62 +0,0 @@
import { timeSince } from '@/lib/time'
import styles from '@/styles/log.module.css'
import { Fragment, useState } from 'react'
export default function LogMessage ({ showWallet, wallet, level, message, context, ts }) {
const [show, setShow] = useState(false)
let className
switch (level.toLowerCase()) {
case 'ok':
case 'success':
level = 'ok'
className = 'text-success'; break
case 'error':
className = 'text-danger'; break
case 'warn':
className = 'text-warning'; break
default:
className = 'text-info'
}
const filtered = context
? Object.keys(context)
.filter(key => !['send', 'recv', 'status'].includes(key))
.reduce((obj, key) => {
obj[key] = context[key]
return obj
}, {})
: {}
const hasContext = context && Object.keys(filtered).length > 0
const handleClick = () => {
if (hasContext) { setShow(show => !show) }
}
const style = hasContext ? { cursor: 'pointer' } : { cursor: 'inherit' }
const indicator = hasContext ? (show ? '-' : '+') : <></>
return (
<>
<tr className={styles.tableRow} onClick={handleClick} style={style}>
<td className={styles.timestamp}>{timeSince(new Date(ts))}</td>
{showWallet ? <td className={styles.wallet}>[{wallet}]</td> : <td className='mx-1' />}
<td className={`${styles.level} ${className}`}>{level}</td>
<td>{message}</td>
<td>{indicator}</td>
</tr>
{show && hasContext && Object.entries(filtered)
.map(([key, value], i) => {
const last = i === Object.keys(filtered).length - 1
return (
<tr className={styles.line} key={i}>
<td />
<td className={last ? 'pb-2 pe-1' : 'pe-1'} colSpan='2'>{key}</td>
<td className={last ? 'text-break pb-2' : 'text-break'}>{value}</td>
</tr>
)
})}
</>
)
}

View File

@ -4,6 +4,12 @@ import BackArrow from '@/svgs/arrow-left-line.svg'
import { useRouter } from 'next/router'
import ActionDropdown from './action-dropdown'
export class ModalClosedError extends Error {
constructor () {
super('modal closed')
}
}
export const ShowModalContext = createContext(() => null)
export function ShowModalProvider ({ children }) {

View File

@ -19,8 +19,8 @@ import SearchIcon from '../../svgs/search-line.svg'
import classNames from 'classnames'
import SnIcon from '@/svgs/sn.svg'
import { useHasNewNotes } from '../use-has-new-notes'
import { useWallets } from '@/wallets/index'
import { useWalletIndicator } from '@/wallets/indicator'
// import { useWallets } from '@/wallets/client/hooks'
import { useWalletIndicator } from '@/wallets/client/hooks'
import SwitchAccountList, { nextAccount, useAccounts } from '@/components/account'
import { useShowModal } from '@/components/modal'
import { numWithUnits } from '@/lib/format'
@ -293,7 +293,7 @@ export default function LoginButton () {
function LogoutObstacle ({ onClose }) {
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
const { removeLocalWallets } = useWallets()
// const { removeLocalWallets } = useWallets()
const router = useRouter()
return (
@ -324,8 +324,6 @@ function LogoutObstacle ({ onClose }) {
await togglePushSubscription().catch(console.error)
}
removeLocalWallets()
await signOut({ callbackUrl: '/' })
}}
>

View File

@ -7,7 +7,7 @@ import AnonIcon from '@/svgs/spy-fill.svg'
import styles from './footer.module.css'
import canvasStyles from './offcanvas.module.css'
import classNames from 'classnames'
import { useWalletIndicator } from '@/wallets/indicator'
import { useWalletIndicator } from '@/wallets/client/hooks'
export default function OffCanvas ({ me, dropNavKey }) {
const [show, setShow] = useState(false)

View File

@ -8,7 +8,7 @@ import { useRoot } from './root'
import { ActCanceledError, useAct } from './item-act'
import { useAnimation } from '@/components/animation'
import { useToast } from './toast'
import { useSendWallets } from '@/wallets/index'
import { useHasSendWallet } from '@/wallets/client/hooks'
import { Form, SubmitButton } from './form'
export const payBountyCacheMods = {
@ -50,9 +50,9 @@ export default function PayBounty ({ children, item }) {
const root = useRoot()
const animate = useAnimation()
const toaster = useToast()
const wallets = useSendWallets()
const hasSendWallet = useHasSendWallet()
const variables = { id: item.id, sats: root.bounty, act: 'TIP', hasSendWallet: wallets.length > 0 }
const variables = { id: item.id, sats: root.bounty, act: 'TIP', hasSendWallet }
const act = useAct({
variables,
optimisticResponse: { act: { __typename: 'ItemActPaidAction', result: { ...variables, path: item.path } } },

View File

@ -1,300 +1,165 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { useMe } from '@/components/me'
import { useCallback, useMemo } from 'react'
export function getDbName (userId, name) {
return `app:storage:${userId ?? ''}${name ? `:${name}` : ''}`
}
const VERSION = 2
const DEFAULT_OPTIONS = { keyPath: 'id', autoIncrement: true }
const DEFAULT_INDICES = []
const DEFAULT_VERSION = 1
export function useIndexedDB (dbName) {
const { me } = useMe()
if (!dbName) dbName = me?.id ? `app:storage:${me.id}` : 'app:storage'
function useIndexedDB ({ dbName, storeName, options = DEFAULT_OPTIONS, indices = DEFAULT_INDICES, version = DEFAULT_VERSION }) {
const [db, setDb] = useState(null)
const [error, setError] = useState(null)
const [notSupported, setNotSupported] = useState(false)
const operationQueue = useRef([])
const handleError = useCallback((error) => {
console.error('IndexedDB error:', error)
setError(error)
}, [])
const processQueue = useCallback((db) => {
if (!db) return
const set = useCallback(async (storeName, key, value) => {
const db = await _open(dbName, VERSION)
try {
// try to run a noop to see if the db is ready
db.transaction(storeName)
while (operationQueue.current.length > 0) {
const operation = operationQueue.current.shift()
// if the db is the same as the one we're processing, run the operation
// else, we'll just clear the operation queue
// XXX this is a consquence of using a ref to store the queue and should be fixed
if (dbName === db.name) {
operation(db)
}
}
} catch (error) {
handleError(error)
return await _set(db, storeName, key, value)
} finally {
db.close()
}
}, [dbName, storeName, handleError, operationQueue])
}, [dbName])
useEffect(() => {
let isMounted = true
const get = useCallback(async (storeName, key) => {
const db = await _open(dbName, VERSION)
try {
return await _get(db, storeName, key)
} finally {
db.close()
}
}, [dbName])
const deleteDb = useCallback(async () => {
return await _delete(dbName)
}, [dbName])
const open = useCallback(async () => {
return await _open(dbName, VERSION)
}, [dbName])
return useMemo(() => ({ set, get, deleteDb, open }), [set, get, deleteDb, open])
}
async function _open (dbName, version = 1) {
return await new Promise((resolve, reject) => {
if (typeof window.indexedDB === 'undefined') {
return reject(new IndexedDBOpenError('IndexedDB unavailable'))
}
const request = window.indexedDB.open(dbName, version)
request.onupgradeneeded = (event) => {
try {
const db = event.target.result
if (!db.objectStoreNames.contains('vault')) db.createObjectStore('vault')
if (db.objectStoreNames.contains('wallet_logs')) db.deleteObjectStore('wallet_logs')
} catch (error) {
reject(new IndexedDBOpenError(`upgrade failed: ${error?.message}`))
}
}
request.onerror = (event) => {
reject(new IndexedDBOpenError(request.error?.message))
}
request.onsuccess = (event) => {
const db = request.result
resolve(db)
}
})
}
async function _set (db, storeName, key, value) {
return await new Promise((resolve, reject) => {
let request
try {
if (!window.indexedDB) {
console.log('IndexedDB is not supported')
setNotSupported(true)
return
}
request = window.indexedDB.open(dbName, version)
request.onerror = (event) => {
handleError(new Error('Error opening database'))
}
request.onsuccess = (event) => {
if (isMounted) {
const database = event.target.result
database.onversionchange = () => {
database.close()
setDb(null)
handleError(new Error('Database is outdated, please reload the page'))
}
setDb(database)
processQueue(database)
}
}
request.onupgradeneeded = (event) => {
const database = event.target.result
try {
const store = database.createObjectStore(storeName, options)
indices.forEach(index => {
store.createIndex(index.name, index.keyPath, index.options)
})
} catch (error) {
handleError(new Error('Error upgrading database: ' + error.message))
}
}
request = db
.transaction(storeName, 'readwrite')
.objectStore(storeName)
.put(value, key)
} catch (error) {
handleError(new Error('Error opening database: ' + error.message))
return reject(new IndexedDBSetError(error?.message))
}
return () => {
isMounted = false
if (db) {
db.close()
}
}
}, [dbName, storeName, version, indices, options, handleError, processQueue])
const queueOperation = useCallback((operation) => {
if (notSupported) {
return Promise.reject(new Error('IndexedDB is not supported'))
}
if (error) {
return Promise.reject(new Error('Database error: ' + error.message))
request.onerror = (event) => {
reject(new IndexedDBSetError(event.target?.error?.message))
}
return new Promise((resolve, reject) => {
const wrappedOperation = (db) => {
try {
const result = operation(db)
resolve(result)
} catch (error) {
reject(error)
}
}
operationQueue.current.push(wrappedOperation)
processQueue(db)
})
}, [processQueue, db, notSupported, error])
const add = useCallback((value) => {
return queueOperation((db) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite')
const store = transaction.objectStore(storeName)
const request = store.add(value)
request.onerror = () => reject(new Error('Error adding data'))
request.onsuccess = () => resolve(request.result)
})
})
}, [queueOperation, storeName])
const get = useCallback((key) => {
return queueOperation((db) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly')
const store = transaction.objectStore(storeName)
const request = store.get(key)
request.onerror = () => reject(new Error('Error getting data'))
request.onsuccess = () => resolve(request.result ? request.result : undefined)
})
})
}, [queueOperation, storeName])
const getAll = useCallback(() => {
return queueOperation((db) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly')
const store = transaction.objectStore(storeName)
const request = store.getAll()
request.onerror = () => reject(new Error('Error getting all data'))
request.onsuccess = () => resolve(request.result)
})
})
}, [queueOperation, storeName])
const set = useCallback((key, value) => {
return queueOperation((db) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite')
const store = transaction.objectStore(storeName)
const request = store.put(value, key)
request.onerror = () => reject(new Error('Error setting data'))
request.onsuccess = () => resolve(request.result)
})
})
}, [queueOperation, storeName])
const remove = useCallback((key) => {
return queueOperation((db) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite')
const store = transaction.objectStore(storeName)
const request = store.delete(key)
request.onerror = () => reject(new Error('Error removing data'))
request.onsuccess = () => resolve()
})
})
}, [queueOperation, storeName])
const clear = useCallback((indexName = null, query = null) => {
return queueOperation((db) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readwrite')
const store = transaction.objectStore(storeName)
if (!query) {
// Clear all data if no query is provided
const request = store.clear()
request.onerror = () => reject(new Error('Error clearing all data'))
request.onsuccess = () => resolve()
} else {
// Clear data based on the query
const index = indexName ? store.index(indexName) : store
const request = index.openCursor(query)
let deletedCount = 0
request.onerror = () => reject(new Error('Error clearing data based on query'))
request.onsuccess = (event) => {
const cursor = event.target.result
if (cursor) {
const deleteRequest = cursor.delete()
deleteRequest.onerror = () => reject(new Error('Error deleting item'))
deleteRequest.onsuccess = () => {
deletedCount++
cursor.continue()
}
} else {
resolve(deletedCount)
}
}
}
})
})
}, [queueOperation, storeName])
const getByIndex = useCallback((indexName, key) => {
return queueOperation((db) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly')
const store = transaction.objectStore(storeName)
const index = store.index(indexName)
const request = index.get(key)
request.onerror = () => reject(new Error('Error getting data by index'))
request.onsuccess = () => resolve(request.result)
})
})
}, [queueOperation, storeName])
const getAllByIndex = useCallback((indexName, query, direction = 'next', limit = Infinity) => {
return queueOperation((db) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly')
const store = transaction.objectStore(storeName)
const index = store.index(indexName)
const request = index.openCursor(query, direction)
const results = []
request.onerror = () => reject(new Error('Error getting data by index'))
request.onsuccess = (event) => {
const cursor = event.target.result
if (cursor && results.length < limit) {
results.push(cursor.value)
cursor.continue()
} else {
resolve(results)
}
}
})
})
}, [queueOperation, storeName])
const getPage = useCallback((page = 1, pageSize = 10, indexName = null, query = null, direction = 'next') => {
return queueOperation((db) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, 'readonly')
const store = transaction.objectStore(storeName)
const target = indexName ? store.index(indexName) : store
const request = target.openCursor(query, direction)
const results = []
let skipped = 0
let hasMore = false
request.onerror = () => reject(new Error('Error getting page'))
request.onsuccess = (event) => {
const cursor = event.target.result
if (cursor) {
if (skipped < (page - 1) * pageSize) {
skipped++
cursor.continue()
} else if (results.length < pageSize) {
results.push(cursor.value)
cursor.continue()
} else {
hasMore = true
}
}
if (hasMore || !cursor) {
const countRequest = target.count()
countRequest.onsuccess = () => {
resolve({
data: results,
total: countRequest.result,
hasMore
})
}
countRequest.onerror = () => reject(new Error('Error counting items'))
}
}
})
})
}, [queueOperation, storeName])
return { add, get, getAll, set, remove, clear, getByIndex, getAllByIndex, getPage, error, notSupported }
request.onsuccess = () => {
resolve(request.result)
}
})
}
export default useIndexedDB
async function _get (db, storeName, key) {
return await new Promise((resolve, reject) => {
let request
try {
request = db
.transaction(storeName)
.objectStore(storeName)
.get(key)
} catch (error) {
return reject(new IndexedDBGetError(error?.message))
}
request.onerror = (event) => {
reject(new IndexedDBGetError(event.target?.error?.message))
}
request.onsuccess = () => {
resolve(request.result)
}
})
}
async function _delete (dbName) {
return await new Promise((resolve, reject) => {
if (typeof window.indexedDB === 'undefined') {
return reject(new IndexedDBOpenError('IndexedDB unavailable'))
}
const request = window.indexedDB.deleteDatabase(dbName)
request.onerror = (event) => {
reject(new IndexedDBDeleteError(event.target?.error?.message))
}
request.onsuccess = () => {
resolve(request.result)
}
})
}
class IndexedDBError extends Error {
constructor (message) {
super(message)
this.name = 'IndexedDBError'
}
}
class IndexedDBOpenError extends IndexedDBError {
constructor (message) {
super(message)
this.name = 'IndexedDBOpenError'
}
}
class IndexedDBSetError extends IndexedDBError {
constructor (message) {
super(message)
this.name = 'IndexedDBSetError'
}
}
class IndexedDBGetError extends IndexedDBError {
constructor (message) {
super(message)
this.name = 'IndexedDBGetError'
}
}
class IndexedDBDeleteError extends IndexedDBError {
constructor (message) {
super(message)
this.name = 'IndexedDBDeleteError'
}
}

View File

@ -1,8 +1,8 @@
import { useApolloClient, useMutation } from '@apollo/client'
import { useCallback, useMemo } from 'react'
import { InvoiceCanceledError, InvoiceExpiredError, WalletReceiverError } from '@/wallets/errors'
import { InvoiceCanceledError, InvoiceExpiredError, WalletReceiverError } from '@/wallets/client/errors'
import { RETRY_PAID_ACTION } from '@/fragments/paidAction'
import { INVOICE, CANCEL_INVOICE } from '@/fragments/wallet'
import { INVOICE, CANCEL_INVOICE } from '@/fragments/invoice'
export default function useInvoice () {
const client = useApolloClient()

View File

@ -8,7 +8,7 @@ import { RETRY_PAID_ACTION } from '@/fragments/paidAction'
import gql from 'graphql-tag'
import { USER_ID } from '@/lib/constants'
import { useMe } from './me'
import { useWalletRecvPrompt, WalletPromptClosed } from '@/wallets/prompt'
import { useWalletRecvPrompt, WalletPromptClosed } from '@/wallets/client/hooks'
// this is intented to be compatible with upsert item mutations
// so that it can be reused for all post types and comments and we don't have

View File

@ -2,9 +2,9 @@ import { useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
import { useCallback, useState } from 'react'
import useQrPayment from '@/components/use-qr-payment'
import useInvoice from '@/components/use-invoice'
import { InvoiceCanceledError, InvoiceExpiredError, WalletError, WalletPaymentError } from '@/wallets/errors'
import { InvoiceCanceledError, InvoiceExpiredError, WalletError, WalletPaymentError } from '@/wallets/client/errors'
import { GET_PAID_ACTION } from '@/fragments/paidAction'
import { useWalletPayment } from '@/wallets/payment'
import { useWalletPayment } from '@/wallets/client/hooks'
/*
this is just like useMutation with a few changes:

View File

@ -1,9 +1,9 @@
import { useCallback } from 'react'
import Invoice from '@/components/invoice'
import { InvoiceCanceledError, InvoiceExpiredError, AnonWalletError } from '@/wallets/errors'
import { AnonWalletError, InvoiceCanceledError, InvoiceExpiredError } from '@/wallets/client/errors'
import { useShowModal } from '@/components/modal'
import useInvoice from '@/components/use-invoice'
import { sendPayment } from '@/wallets/webln/client'
import { sendPayment as weblnSendPayment } from '@/wallets/client/protocols/webln'
export default function useQrPayment () {
const invoice = useInvoice()
@ -19,7 +19,7 @@ export default function useQrPayment () {
) => {
// if anon user and webln is available, try to pay with webln
if (typeof window.webln !== 'undefined' && (walletError instanceof AnonWalletError)) {
sendPayment(inv.bolt11).catch(e => { console.error('WebLN payment failed:', e) })
weblnSendPayment(inv.bolt11).catch(e => { console.error('WebLN payment failed:', e) })
}
return await new Promise((resolve, reject) => {
let paid

View File

@ -1,175 +0,0 @@
import { useMutation, useQuery, makeVar, useReactiveVar } from '@apollo/client'
import { useMe } from '../me'
import { useToast } from '../toast'
import useIndexedDB, { getDbName } from '../use-indexeddb'
import { useCallback, useEffect, useMemo } from 'react'
import { E_VAULT_KEY_EXISTS } from '@/lib/error'
import { CLEAR_VAULT, GET_VAULT_ENTRIES, UPDATE_VAULT_KEY } from '@/fragments/vault'
import { toHex } from '@/lib/hex'
import { decryptValue, encryptValue } from './use-vault'
const useImperativeQuery = (query) => {
const { refetch } = useQuery(query, { skip: true })
const imperativelyCallQuery = (variables) => {
return refetch(variables)
}
return imperativelyCallQuery
}
// reactive variable to store the vault key shared by all vaults
// so all vaults can react to changes in the vault key
// an alternative is to create a vault context which may be more idiomatic(?)
const keyReactiveVar = makeVar(null)
export function useVaultConfigurator ({ onVaultKeySet, beforeDisconnectVault } = {}) {
const { me } = useMe()
const toaster = useToast()
const idbConfig = useMemo(() => ({ dbName: getDbName(me?.id, 'vault'), storeName: 'vault', options: {} }), [me?.id])
const { set, get, remove } = useIndexedDB(idbConfig)
const [updateVaultKey] = useMutation(UPDATE_VAULT_KEY)
const getVaultEntries = useImperativeQuery(GET_VAULT_ENTRIES)
const key = useReactiveVar(keyReactiveVar)
const disconnectVault = useCallback(async () => {
console.log('disconnecting vault')
beforeDisconnectVault?.()
await remove('key')
keyReactiveVar(null)
}, [remove, keyReactiveVar, beforeDisconnectVault])
useEffect(() => {
if (!me) return
(async () => {
try {
const localVaultKey = await get('key')
if (localVaultKey?.hash && localVaultKey?.hash !== me?.privates?.vaultKeyHash) {
// If the hash stored in the server does not match the hash of the local key,
// we can tell that the key is outdated (reset by another device or other reasons)
// in this case we clear the local key and let the user re-enter the passphrase
console.log('vault key hash mismatch, clearing local key', localVaultKey?.hash, '!=', me?.privates?.vaultKeyHash)
await disconnectVault()
return
}
keyReactiveVar(localVaultKey)
} catch (e) {
console.error('error loading vault configuration', e)
// toaster?.danger('error loading vault configuration ' + e.message)
}
})()
}, [me?.privates?.vaultKeyHash, get, remove, keyReactiveVar, disconnectVault])
// clear vault: remove everything and reset the key
const [clearVault] = useMutation(CLEAR_VAULT, {
onCompleted: async () => {
try {
await remove('key')
keyReactiveVar(null)
} catch (e) {
toaster.danger('error clearing vault ' + e.message)
}
}
})
// initialize the vault and set a vault key
const setVaultKey = useCallback(async (passphrase) => {
try {
const oldKeyValue = await get('key')
const vaultKey = await deriveKey(me.id, passphrase)
const { data } = await getVaultEntries()
const encrypt = async value => {
return await encryptValue(vaultKey.key, value)
}
const entries = []
if (oldKeyValue?.key) {
for (const { key, iv, value } of data.getVaultEntries) {
const plainValue = await decryptValue(oldKeyValue.key, { iv, value })
entries.push({ key, ...await encrypt(plainValue) })
}
}
await updateVaultKey({
variables: { entries, hash: vaultKey.hash },
update: (cache, { data }) => {
cache.modify({
id: `User:${me.id}`,
fields: {
privates: (existing) => ({
...existing,
vaultKeyHash: vaultKey.hash
})
}
})
},
onError: (error) => {
const errorCode = error.graphQLErrors[0]?.extensions?.code
if (errorCode === E_VAULT_KEY_EXISTS) {
throw new Error('wrong passphrase')
}
toaster.danger(error.graphQLErrors[0].message)
}
})
await set('key', vaultKey)
onVaultKeySet?.(encrypt).catch(console.error)
keyReactiveVar(vaultKey)
} catch (e) {
console.error('error setting vault key', e)
toaster.danger(e.message)
}
}, [getVaultEntries, updateVaultKey, set, get, remove, onVaultKeySet, keyReactiveVar, me?.id])
return { key, setVaultKey, clearVault, disconnectVault }
}
/**
* Derive a key to be used for the vault encryption
* @param {string | number} userId - the id of the user (used for salting)
* @param {string} passphrase - the passphrase to derive the key from
* @returns {Promise<{key: CryptoKey, hash: string, extractable: boolean}>} an un-extractable CryptoKey and its hash
*/
async function deriveKey (userId, passphrase) {
const enc = new TextEncoder()
const keyMaterial = await window.crypto.subtle.importKey(
'raw',
enc.encode(passphrase),
{ name: 'PBKDF2' },
false,
['deriveKey']
)
const key = await window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: enc.encode(`stacker${userId}`),
// 600,000 iterations is recommended by OWASP
// see https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2
iterations: 600_000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
)
const rawKey = await window.crypto.subtle.exportKey('raw', key)
const hash = toHex(await window.crypto.subtle.digest('SHA-256', rawKey))
const unextractableKey = await window.crypto.subtle.importKey(
'raw',
rawKey,
{ name: 'AES-GCM' },
false,
['encrypt', 'decrypt']
)
return {
key: unextractableKey,
hash
}
}

View File

@ -1,64 +0,0 @@
import { useCallback } from 'react'
import { useVaultConfigurator } from './use-vault-configurator'
import { fromHex, toHex } from '@/lib/hex'
export default function useVault () {
const { key } = useVaultConfigurator()
const encrypt = useCallback(async (value) => {
if (!key) throw new Error('no vault key set')
return await encryptValue(key.key, value)
}, [key])
const decrypt = useCallback(async ({ iv, value }) => {
if (!key) throw new Error('no vault key set')
return await decryptValue(key.key, { iv, value })
}, [key])
return { encrypt, decrypt, isActive: !!key?.key }
}
/**
* Encrypt data using AES-GCM
* @param {CryptoKey} sharedKey - the key to use for encryption
* @param {Object} value - the value to encrypt
* @returns {Promise<Object>} an object with iv and value properties, can be passed to decryptValue to get the original data back
*/
export async function encryptValue (sharedKey, value) {
// random IVs are _really_ important in GCM: reusing the IV once can lead to catastrophic failure
// see https://crypto.stackexchange.com/questions/26790/how-bad-it-is-using-the-same-iv-twice-with-aes-gcm
// 12 bytes (96 bits) is the recommended IV size for AES-GCM
const iv = window.crypto.getRandomValues(new Uint8Array(12))
const encoded = new TextEncoder().encode(JSON.stringify(value))
const encrypted = await window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv
},
sharedKey,
encoded
)
return {
iv: toHex(iv.buffer),
value: toHex(encrypted)
}
}
/**
* Decrypt data using AES-GCM
* @param {CryptoKey} sharedKey - the key to use for decryption
* @param {Object} encryptedValue - the encrypted value as returned by encryptValue
* @returns {Promise<Object>} the original unencrypted data
*/
export async function decryptValue (sharedKey, { iv, value }) {
const decrypted = await window.crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: fromHex(iv)
},
sharedKey,
fromHex(value)
)
const decoded = new TextDecoder().decode(decrypted)
return JSON.parse(decoded)
}

View File

@ -36,6 +36,7 @@ services:
env_file: *env_file
volumes:
- ./docker/db/seed.sql:/docker-entrypoint-initdb.d/seed.sql
- ./docker/db/wallet-seed.sql:/docker-entrypoint-initdb.d/wallet-seed.sql
- db:/var/lib/postgresql/data
labels:
CONNECT: "localhost:5431"

186
docker/db/wallet-seed.sql Normal file
View File

@ -0,0 +1,186 @@
/*
* This seed file inserts test wallets into the database to test wallet migrations.
* Only the wallets for which we could hardcode the configuration when this file was created will work to send or receive zaps.
* For example, NWC won't work for send or receive because it generates a random public key and secret every time the container is started for the first time.
*/
-- device sync passphrase: media fit youth secret combine live cupboard response enable loyal kitchen angle
COPY public."users" ("id", "name", "vaultKeyHash") FROM stdin;
21001 test_wallet_v2 0feb0e0ed8684eaf37a995c4decac6d360125d40ff3fffe26239bb7ffd810853
\.
-- triggers will update the wallet JSON column in the Wallet table when we insert rows into the other wallet tables
COPY public."Wallet" ("id", "userId", "type", "enabled") FROM stdin;
1 21001 LIGHTNING_ADDRESS true
2 21001 NWC true
3 21001 WEBLN true
4 21001 LNBITS true
5 21001 CLN true
6 21001 BLINK true
7 21001 PHOENIXD true
8 21001 LND true
9 21001 LNC true
10 21001 LIGHTNING_ADDRESS true
11 21001 LIGHTNING_ADDRESS true
12 21001 LIGHTNING_ADDRESS true
13 21001 LIGHTNING_ADDRESS true
14 21001 LIGHTNING_ADDRESS true
15 21001 LIGHTNING_ADDRESS true
16 21001 LIGHTNING_ADDRESS true
17 21001 LIGHTNING_ADDRESS true
18 21001 LIGHTNING_ADDRESS true
19 21001 LIGHTNING_ADDRESS true
20 21001 LIGHTNING_ADDRESS true
21 21001 LIGHTNING_ADDRESS true
22 21001 LIGHTNING_ADDRESS true
23 21001 LIGHTNING_ADDRESS true
24 21001 LIGHTNING_ADDRESS true
25 21001 LIGHTNING_ADDRESS true
26 21001 LIGHTNING_ADDRESS true
27 21001 LIGHTNING_ADDRESS true
28 21001 NWC true
29 21001 NWC true
\.
COPY public."WalletLightningAddress" ("id", "walletId", "address") FROM stdin;
1 1 john_doe@getalby.com
2 10 john_doe@rizful.com
3 11 john_doe@fountain.fm
4 12 john_doe@primal.net
5 13 john_doe@coinos.io
6 14 john_doe@speed.app
7 15 john_doe@tryspeed.com
8 16 john_doe@blink.sv
9 17 john_doe@zbd.gg
10 18 john_doe@strike.me
11 19 john_doe@minibits.cash
12 20 john_doe@npub.cash
13 21 john_doe@zeuspay.com
14 22 john_doe@fountain.fm
15 23 john_doe@lifpay.me
16 24 john_doe@rizful.com
17 25 john_doe@vlt.ge
19 26 john_doe@blixtwallet.com
20 27 john_doe@shockwallet.app
\.
COPY public."WalletNWC" ("id", "walletId", "nwcUrlRecv") FROM stdin;
1 2 nostr+walletconnect://8682ce552a852b5e21c8fe1235823a6f175641538f4c5431ec559a75dfb7f73a?relay=wss://relay.getalby.com/v1&secret=99669866becdbfacef4e9c3f0d00f085ee1174bc973135f158bab769f37152b9&lud16=john_doe@getalby.com
2 28 nostr+walletconnect://8682ce552a852b5e21c8fe1235823a6f175641538f4c5431ec559a75dfb7f73a?relay=wss://relay-nwc.rizful.com&secret=99669866becdbfacef4e9c3f0d00f085ee1174bc973135f158bab769f37152b9
\.
COPY public."WalletLNbits" ("id", "walletId", "url", "invoiceKey") FROM stdin;
1 4 http://localhost:5001 5deed7cd634e4306bb5e696f4a03cdac
\.
COPY public."WalletCLN" ("id", "walletId", "socket", "rune", "cert") FROM stdin;
1 5 cln:3010 Fz6ox9zLwTRfHSaKbxdr5SK4KyxAjL_UEniED6UEGRw9MCZtZXRob2Q9aW52b2ljZQ== LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlCY2pDQ0FSaWdBd0lCQWdJSkFOclN2UFovWTNLRU1Bb0dDQ3FHU000OUJBTUNNQll4RkRBU0JnTlZCQU1NDQpDMk5zYmlCU2IyOTBJRU5CTUNBWERUYzFNREV3TVRBd01EQXdNRm9ZRHpRd09UWXdNVEF4TURBd01EQXdXakFXDQpNUlF3RWdZRFZRUUREQXRqYkc0Z1VtOXZkQ0JEUVRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBDQpCQmptYUh1dWxjZ3dTR09ubExBSFlRbFBTUXdHWEROSld5ZnpWclY5aFRGYUJSZFFrMVl1Y3VqVFE5QXFybkVJDQpyRmR6MS9PeisyWFhENmdBMnhPbmIrNmpUVEJMTUJrR0ExVWRFUVFTTUJDQ0EyTnNib0lKYkc5allXeG9iM04wDQpNQjBHQTFVZERnUVdCQlNFY21OLzlyelMyaFI2RzdFSWdzWCs1MU4wQ2pBUEJnTlZIUk1CQWY4RUJUQURBUUgvDQpNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJSENlUHZOU3Z5aUJZYXdxS2dRcXV3OUoyV1Z5SnhuMk1JWUlxejlTDQpRTDE4QWlFQWg4QlZEejhwWDdOc2xsOHNiMGJPMFJaNDljdnFRb2NDZ1ZhYnFKdVN1aWs9DQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tDQo=
\.
COPY public."WalletBlink" ("id", "walletId", "apiKeyRecv", "currencyRecv") FROM stdin;
1 6 blink_IpGjMEmlLZrb3dx1RS5pcVm7Z6uKthb2UMg5bfGxcIV4Yae BTC
\.
COPY public."WalletPhoenixd" ("id", "walletId", "url", "secondaryPassword") FROM stdin;
1 7 https://phoenixd.ekzy.is abb6dc487e788fcfa2bdaf587aa3f96a5ee4a3e8d7d8068131182c5919d974cd
\.
COPY public."WalletLND" ("id", "walletId", "socket", "macaroon", "cert") FROM stdin;
1 8 lnd:10009 0201036c6e64022f030a1089912eeaa5f434e5265170565bcce0eb1201301a170a08696e766f6963657312047265616412057772697465000006200622e95cf2fe2d9a8976cbfb824809a9a5e8af861b659e396064f6de1dc79d04 LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNoVENDQWl1Z0F3SUJBZ0lSQUp5Zkg3cEdDZEhXTVJZVGo1d1pKSkF3Q2dZSUtvWkl6ajBFQXdJd09ERWYKTUIwR0ExVUVDaE1XYkc1a0lHRjFkRzluWlc1bGNtRjBaV1FnWTJWeWRERVZNQk1HQTFVRUF4TU1NR1V5T0dVNApPREkzTmpZd01CNFhEVEkxTURZd05URTRNak15TmxvWERUSTJNRGN6TVRFNE1qTXlObG93T0RFZk1CMEdBMVVFCkNoTVdiRzVrSUdGMWRHOW5aVzVsY21GMFpXUWdZMlZ5ZERFVk1CTUdBMVVFQXhNTU1HVXlPR1U0T0RJM05qWXcKTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFUE1lb2RYYTF2eXVxYXFaNklXbXgrNDVFdjBkUgpmQkY5SXZtMU5xQVNHUGlGT1JucEtxZVBVbm0xWmZlTUNETytwcGhQMHpGYVh4ZVBUU3BwaWMrYXlLT0NBUlF3CmdnRVFNQTRHQTFVZER3RUIvd1FFQXdJQ3BEQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBVEFQQmdOVkhSTUIKQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJUYkdKMlZDejN5WkFUd1JlUG1kckdvMnhkVmFqQ0J1QVlEVlIwUgpCSUd3TUlHdGdnd3daVEk0WlRnNE1qYzJOakNDQ1d4dlkyRnNhRzl6ZElJRGJHNWtnaFJvYjNOMExtUnZZMnRsCmNpNXBiblJsY201aGJJSStOelV5ZUdWNWIyeG1jSEJqTW5SbloybDZaSEZoYW1Kb2VXZHNjRzV4ZW10bGFtVmoKWW1oeGJIQnpNMjU0ZW5aMGMyZzNkMkZ0Y1dRdWIyNXBiMjZDQkhWdWFYaUNDblZ1YVhod1lXTnJaWFNDQjJKMQpabU52Ym02SEJIOEFBQUdIRUFBQUFBQUFBQUFBQUFBQUFBQUFBQUdIQkt3U0FBY3dDZ1lJS29aSXpqMEVBd0lEClNBQXdSUUlnY2pZZ2o5YVhpQjlOOVBmQUp0cWZRbStoYVdpbmZ0RTVXdkJ3Vis4NzgzTUNJUUNyaEx2Qys3RzQKN3NneENyYnlmLy9WdmxJN3BkakRlVFM0WGc4eHB2UmVEQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
\.
COPY public."VaultEntry" ("id", "key", "iv", "value", "userId", "walletId") FROM stdin;
1 nwcUrl 926622f7139d4b4506827549 b592906ea9c3ced5df077ca1ea0c787c2ea9173e39d062466b50723d8c0a568510ed42549215c6f6e15632601248f148fa18b87d7e0fe9ad667d10a12beb79e36d3cfcaa58ca65c78e8f41ee715c2b19ac8c638353cdc9098a784104eb9b1592b233d4327556de47d218f97991392105ff0868beada2667b308d544bf9e7199f056ecb8cd9c2f87a7f8f1eda7db7e80c880de12df4ce2dc5dcc16ec836d9a9f428f2c4e36f01bdbbd85084ecac308eefa1dbdfe89c2a321a3fafa1c35265a788a352c329f9e01d0988e47f05b8575fbcfb5814 21001 2
2 adminKey 1b911294853df2e94e4d9823 438ff80df2e58e3f7988ba828fab1e7def3934b908a58de9f5f16bb36e1ecc65e1cab43c0ec658f65bdccb0a241bb5614697 21001 4
3 apiKey b1f3500130b16bf4997fc370 3276869cf3d8c6d844e771688f8cd1a771279867165e4b1030b7ad90d537d5cfa0a6a82c2aaefe350db2a445b3b0c3b23a068edede3e78fe5957c1cfc6b5f1fd811786793c65aad90fe8ce 21001 6
4 currency 8174fe225f0d53957a4daced b912faacc32725b9ec01128911eb3922822fb21bdc 21001 6
5 primaryPassword 5e709c93ca34a135dad293d7 7509592ff463f886b7f7a621928a8ac0ca56b904d5835bf77ca5914a248fb70ad231f04aed893c0ef1dd7edd2d928d482d0eaeba7ae2381f3fd70ba25cb265de6091a11231a9cd3047f22ff2f838db046e67 21001 7
6 pairingPhrase 0196718758dea2bff7c89741 bd6ff716ec5b20dc74f6507b87ed0923c8b27e33204ae44cee47d8c7f78dd5976cc446f2c9dd918f2916611a71e20e87fb9245cacfdb35bbc527a42c0df765e2f9589e56b5b253c0d39f8e954b 21001 9
7 localKey 227bc46af405a40cb6697344 f4589aaca476b4905980b9dd834880926aff9e9c9217afa9b33152a74255698c9284015309ae19e10481843069a052dbe1a592e14db6aa13fce4e17fd9f5f2964720ba4686a4a45a1c72681248809e8de612 21001 9
8 remoteKey e63b62d8af6a1227129e8c7b ce97d971cdcd58b34ec2e998c6ce6df72b8c21a9cd07e69db96c9491b3d9a051cf557d721552c5cc565a4d7f1bf1ad70b20048b90e1b244e77f0b635b5dbd798e0538f85d7008b29918a7e589dc1c2bde465c50c 21001 9
9 serverHost a537d212e719810f6cbbc696 449c550ca2c24e761802087e5bc5637d0b4b231d9b771fbefbee6ed7c0a728862adc677cc283a373ec25f01003009f0c9cd18f884d08 21001 9
\.
COPY public."Invoice" ("id", "userId", "hash", "bolt11", "expiresAt", "msatsRequested") FROM stdin;
1 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82a1 lnbc 2025-05-16 00:00:00 1000000
2 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82a2 lnbc 2025-05-16 00:00:00 2000000
3 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82a3 lnbc 2025-05-16 00:00:00 3000000
4 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82a4 lnbc 2025-05-16 00:00:00 4000000
5 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82a5 lnbc 2025-05-16 00:00:00 1000000
6 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82a6 lnbc 2025-05-16 00:00:00 2000000
7 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82a7 lnbc 2025-05-16 00:00:00 3000000
8 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82a8 lnbc 2025-05-16 00:00:00 4000000
9 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82a9 lnbc 2025-05-16 00:00:00 4000000
10 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82aa lnbc 2025-05-16 00:00:00 4000000
11 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82ab lnbc 2025-05-16 00:00:00 4000000
12 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82ac lnbc 2025-05-16 00:00:00 4000000
13 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82ad lnbc 2025-05-16 00:00:00 4000000
14 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82ae lnbc 2025-05-16 00:00:00 4000000
15 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb82af lnbc 2025-05-16 00:00:00 4000000
16 21001 d1bc8d3ba4afc7e109612cb73acbdddac052c93025aa1f82942edabb7deb8210 lnbc 2025-05-16 00:00:00 4000000
\.
COPY public."Withdrawl" ("id", "userId", "walletId", "msatsPaying", "msatsFeePaying") FROM stdin;
1 21001 1 1000 0
2 21001 2 1000 0
3 21001 2 1000 0
4 21001 5 1000 0
5 21001 5 1000 0
6 21001 6 1000 0
7 21001 7 1000 0
8 21001 7 1000 0
9 21001 8 1000 0
10 21001 10 1000 0
11 21001 11 1000 0
12 21001 27 1000 0
13 21001 28 1000 0
14 21001 29 1000 0
15 21001 1 1000 0
16 21001 4 1000 0
17 21001 4 1000 0
18 21001 7 1000 0
19 21001 7 1000 0
20 21001 8 1000 0
\.
COPY public."InvoiceForward" ("id", "walletId", "bolt11", "maxFeeMsats", "invoiceId", "withdrawlId") FROM stdin;
1 1 lnbc 1000 1 1
2 2 lnbc 1000 2 2
3 4 lnbc 1000 3 3
4 4 lnbc 1000 4 4
5 5 lnbc 1000 5 5
6 6 lnbc 1000 6 6
7 7 lnbc 1000 7 7
8 8 lnbc 1000 8 8
9 27 lnbc 1000 9 9
10 28 lnbc 1000 10 10
11 29 lnbc 1000 11 11
12 4 lnbc 1000 12 12
13 4 lnbc 1000 13 13
14 5 lnbc 1000 14 14
15 6 lnbc 1000 15 15
16 7 lnbc 1000 16 16
\.
SELECT pg_catalog.setval('public."InvoiceForward_id_seq"', 16, true);
COPY public."DirectPayment" ("id", "walletId", "senderId", "receiverId", "msats") FROM stdin;
1 1 21001 21001 1000
2 2 21001 21001 1000
3 4 21001 21001 1000
4 5 21001 21001 1000
5 6 21001 21001 1000
6 7 21001 21001 1000
7 8 21001 21001 1000
8 16 21001 21001 1000
9 27 21001 21001 1000
10 28 21001 21001 1000
11 29 21001 21001 1000
12 7 21001 21001 1000
13 7 21001 21001 1000
14 5 21001 21001 1000
15 5 21001 21001 1000
16 4 21001 21001 1000
\.
SELECT pg_catalog.setval('public."DirectPayment_id_seq"', 16, true);

View File

@ -1,6 +1,5 @@
import { gql } from '@apollo/client'
import { ITEM_FULL_FIELDS } from './items'
import { VAULT_ENTRY_FIELDS } from './vault'
export const INVOICE_FIELDS = gql`
fragment InvoiceFields on Invoice {
@ -121,89 +120,6 @@ export const SEND_TO_LNADDR = gql`
}
}`
export const REMOVE_WALLET =
gql`
mutation removeWallet($id: ID!) {
removeWallet(id: $id)
}
`
// XXX [WALLET] this needs to be updated if another server wallet is added
export const WALLET_FIELDS = gql`
${VAULT_ENTRY_FIELDS}
fragment WalletFields on Wallet {
id
priority
type
updatedAt
enabled
vaultEntries {
...VaultEntryFields
}
wallet {
__typename
... on WalletLightningAddress {
address
}
... on WalletLnd {
socket
macaroon
cert
}
... on WalletCln {
socket
rune
cert
}
... on WalletLnbits {
url
invoiceKey
}
... on WalletNwc {
nwcUrlRecv
}
... on WalletPhoenixd {
url
secondaryPassword
}
... on WalletBlink {
apiKeyRecv
currencyRecv
}
}
}
`
export const WALLETS = gql`
${WALLET_FIELDS}
query Wallets {
wallets {
...WalletFields
}
}
`
export const WALLET_LOGS = gql`
query WalletLogs($type: String, $from: String, $to: String, $cursor: String) {
walletLogs(type: $type, from: $from, to: $to, cursor: $cursor) {
cursor
entries {
id
createdAt
wallet
level
message
context
}
}
}
`
export const SET_WALLET_PRIORITY = gql`
mutation SetWalletPriority($id: ID!, $priority: Int!) {
setWalletPriority(id: $id, priority: $priority)
}
`
export const CANCEL_INVOICE = gql`
${INVOICE_FIELDS}
mutation cancelInvoice($hash: String!, $hmac: String, $userCancel: Boolean) {

View File

@ -2,7 +2,7 @@ import { gql } from '@apollo/client'
import { ITEM_FULL_FIELDS, POLL_FIELDS } from './items'
import { INVITE_FIELDS } from './invites'
import { SUB_FIELDS } from './subs'
import { INVOICE_FIELDS } from './wallet'
import { INVOICE_FIELDS } from './invoice'
export const HAS_NOTIFICATIONS = gql`{ hasNewNotes }`

View File

@ -1,7 +1,7 @@
import gql from 'graphql-tag'
import { COMMENTS } from './comments'
import { SUB_FULL_FIELDS } from './subs'
import { INVOICE_FIELDS } from './wallet'
import { INVOICE_FIELDS } from './invoice'
const HASH_HMAC_INPUT_1 = '$hash: String, $hmac: String'
const HASH_HMAC_INPUT_2 = 'hash: $hash, hmac: $hmac'

View File

@ -25,9 +25,6 @@ ${STREAK_FIELDS}
autoDropBolt11s
noReferralLinks
fiatCurrency
autoWithdrawMaxFeePercent
autoWithdrawMaxFeeTotal
autoWithdrawThreshold
withdrawMaxFeeDefault
satsFilter
hideFromTopUsers
@ -52,7 +49,7 @@ ${STREAK_FIELDS}
disableFreebies
vaultKeyHash
walletsUpdatedAt
proxyReceive
showPassphrase
}
optional {
isContributor
@ -113,9 +110,6 @@ export const SETTINGS_FIELDS = gql`
apiKey
}
apiKeyEnabled
proxyReceive
receiveCreditsBelowSats
sendCreditsBelowSats
}
}`

View File

@ -1,33 +0,0 @@
import { gql } from '@apollo/client'
export const VAULT_ENTRY_FIELDS = gql`
fragment VaultEntryFields on VaultEntry {
id
key
iv
value
createdAt
updatedAt
}
`
export const GET_VAULT_ENTRIES = gql`
${VAULT_ENTRY_FIELDS}
query GetVaultEntries {
getVaultEntries {
...VaultEntryFields
}
}
`
export const CLEAR_VAULT = gql`
mutation ClearVault {
clearVault
}
`
export const UPDATE_VAULT_KEY = gql`
mutation updateVaultKey($entries: [VaultEntryInput!]!, $hash: String!) {
updateVaultKey(entries: $entries, hash: $hash)
}
`

View File

@ -95,6 +95,10 @@ function getClient (uri) {
'Reminder',
'ItemMention',
'Invoicification'
],
WalletOrTemplate: [
'Wallet',
'WalletTemplate'
]
},
typePolicies: {
@ -290,6 +294,12 @@ function getClient (uri) {
}
}
},
walletLogs: {
keyArgs: ['protocolId'],
merge (existing, incoming) {
return incoming
}
},
failedInvoices: {
keyArgs: [],
merge (existing, incoming) {

View File

@ -187,32 +187,6 @@ export function stripTrailingSlash (uri) {
return uri.endsWith('/') ? uri.slice(0, -1) : uri
}
export function parseNwcUrl (walletConnectUrl) {
if (!walletConnectUrl) return {}
walletConnectUrl = walletConnectUrl
.replace('nostrwalletconnect://', 'http://')
.replace('nostr+walletconnect://', 'http://') // makes it possible to parse with URL in the different environments (browser/node/...)
// XXX There is a bug in parsing since we use the URL constructor for parsing:
// A wallet pubkey matching /^[0-9a-fA-F]{64}$/ might not be a valid hostname.
// Example: 11111111111 (10 1's) is a valid hostname (gets parsed as IPv4) but 111111111111 (11 1's) is not.
// See https://stackoverflow.com/questions/56804936/how-does-only-numbers-in-url-resolve-to-a-domain
// However, this seems to only get triggered if a wallet pubkey only contains digits so this is pretty improbable.
const url = new URL(walletConnectUrl)
const params = {}
params.walletPubkey = url.host
const secret = url.searchParams.get('secret')
const relayUrls = url.searchParams.getAll('relay')
if (secret) {
params.secret = secret
}
if (relayUrls) {
params.relayUrls = relayUrls
}
return params
}
export class ResponseAssertError extends Error {
constructor (res, { message, method } = {}) {
const urlPart = method ? `${method} ${res.url}` : res.url

View File

@ -12,7 +12,6 @@ import { numWithUnits } from './format'
import { SUB } from '@/fragments/subs'
import { NAME_QUERY } from '@/fragments/users'
import { datePivot } from './time'
import bip39Words from './bip39-words'
export async function validateSchema (schema, data, args) {
try {
@ -52,12 +51,6 @@ export const lightningAddressValidator = process.env.NODE_ENV === 'development'
'address is no good')
: string().email('address is no good')
export const externalLightningAddressValidator = lightningAddressValidator.test({
name: 'address',
test: addr => !addr.toLowerCase().endsWith('@stacker.news'),
message: 'lightning address must be external'
})
async function usernameExists (name, { client, models }) {
if (!client && !models) {
throw new Error('cannot check for user')
@ -480,6 +473,15 @@ export const settingsSchema = object().shape({
// exclude from cyclic analysis. see https://github.com/jquense/yup/issues/720
}, [['tipRandomMax', 'tipRandomMin']])
export const walletSettingsSchema = object({
autoWithdrawThreshold: intValidator.min(0, 'must be greater or equal to 0').required('required'),
autoWithdrawMaxFeePercent: floatValidator.min(0, 'must be greater or equal to 0').required('required'),
autoWithdrawMaxFeeTotal: intValidator.min(0, 'must be greater or equal to 0').required('required'),
receiveCreditsBelowSats: intValidator.min(0, 'must be greater or equal to 0').required('required'),
sendCreditsBelowSats: intValidator.min(0, 'must be greater or equal to 0').required('required'),
proxyReceive: boolean().required('required')
})
const warningMessage = 'If I logout, even accidentally, I will never be able to access my account again'
export const lastAuthRemovalSchema = object({
warning: string().matches(warningMessage, 'does not match').required('required')
@ -513,23 +515,3 @@ export const lud18PayerDataSchema = (k1) => object({
email: string().email('bad email address'),
identifier: string()
})
export const deviceSyncSchema = object().shape({
passphrase: string().required('required')
.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.slice(0, 10)}${w.length > 10 ? '...' : ''}' is not a valid pairing phrase word` })
}
}
if (words.length < 12) {
return context.createError({ message: 'needs at least 12 words' })
}
return true
})
})

View File

@ -1,6 +1,4 @@
import { addMethod, string, mixed, array } from 'yup'
import { parseNwcUrl } from './url'
import { NOSTR_PUBKEY_HEX } from './nostr'
import { ensureB64, HEX_REGEX } from './format'
export * from 'yup'
@ -26,7 +24,7 @@ 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',
message: msg,
test: (val) => {
if (typeof val === 'undefined') return true
try {
@ -85,23 +83,6 @@ addMethod(string, 'ws', function (schemas, msg = 'invalid websocket') {
})
})
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',
@ -138,33 +119,6 @@ addMethod(string, 'hex', function (msg) {
})
})
addMethod(string, 'nwcUrl', function () {
return this.test({
test: (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 {
string().matches(/^nostr\+?walletconnect:\/\//, { message: 'must start with nostr+walletconnect://' }).validateSync(nwcUrl)
let relayUrls, walletPubkey, secret
try {
({ relayUrls, 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')
}
string().required('pubkey required').trim().matches(NOSTR_PUBKEY_HEX, 'pubkey must be 64 hex chars').validateSync(walletPubkey)
array().of(string().required('relay url required').trim().wss('relay must use wss://')).min(1, 'at least one relay required').validateSync(relayUrls)
string().required('secret required').trim().matches(/^[0-9a-fA-F]{64}$/, 'secret must be 64 hex chars').validateSync(secret)
} catch (err) {
return context.createError({ message: err.message })
}
return true
}
})
})
addMethod(array, 'equalto', function equals (
{ required, optional },
message

View File

@ -19,8 +19,7 @@ import 'nprogress/nprogress.css'
import { ChainFeeProvider } from '@/components/chain-fee.js'
import dynamic from 'next/dynamic'
import { HasNewNotesProvider } from '@/components/use-has-new-notes'
import { WebLnProvider } from '@/wallets/webln/client'
import { WalletsProvider } from '@/wallets/index'
import WalletsProvider from '@/wallets/client/context'
const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false })
@ -122,26 +121,24 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
<MeProvider me={me}>
<WalletsProvider>
<HasNewNotesProvider>
<WebLnProvider>
<ServiceWorkerProvider>
<PriceProvider price={price}>
<AnimationProvider>
<ToastProvider>
<ShowModalProvider>
<BlockHeightProvider blockHeight={blockHeight}>
<ChainFeeProvider chainFee={chainFee}>
<ErrorBoundary>
<Component ssrData={ssrData} {...otherProps} />
{!router?.query?.disablePrompt && <PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />}
</ErrorBoundary>
</ChainFeeProvider>
</BlockHeightProvider>
</ShowModalProvider>
</ToastProvider>
</AnimationProvider>
</PriceProvider>
</ServiceWorkerProvider>
</WebLnProvider>
<ServiceWorkerProvider>
<PriceProvider price={price}>
<AnimationProvider>
<ToastProvider>
<ShowModalProvider>
<BlockHeightProvider blockHeight={blockHeight}>
<ChainFeeProvider chainFee={chainFee}>
<ErrorBoundary>
<Component ssrData={ssrData} {...otherProps} />
{!router?.query?.disablePrompt && <PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />}
</ErrorBoundary>
</ChainFeeProvider>
</BlockHeightProvider>
</ShowModalProvider>
</ToastProvider>
</AnimationProvider>
</PriceProvider>
</ServiceWorkerProvider>
</HasNewNotesProvider>
</WalletsProvider>
</MeProvider>

View File

@ -8,7 +8,7 @@ import { formatMsats, toPositiveBigInt } from '@/lib/format'
import assertGofacYourself from '@/api/resolvers/ofac'
import performPaidAction from '@/api/paidAction'
import { validateSchema, lud18PayerDataSchema } from '@/lib/validate'
import { walletLogger } from '@/api/resolvers/wallet'
import { walletLogger } from '@/wallets/server'
export default async ({ query: { username, amount, nostr, comment, payerdata: payerData }, headers }, res) => {
const user = await models.user.findUnique({ where: { name: username } })
@ -16,7 +16,11 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa
return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` })
}
const logger = walletLogger({ models, me: user })
if (!amount || amount < 1000) {
return res.status(400).json({ status: 'ERROR', reason: 'amount must be >=1000 msats' })
}
const logger = walletLogger({ models, userId: user.id })
logger.info(`${user.name}@stacker.news payment attempt`, { amount: formatMsats(amount), nostr, comment })
try {
@ -46,10 +50,6 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa
descriptionHash = lnurlPayDescriptionHashForUser(username)
}
if (!amount || amount < 1000) {
return res.status(400).json({ status: 'ERROR', reason: 'amount must be >=1000 msats' })
}
if (comment?.length > LNURLP_COMMENT_MAX_LENGTH) {
return res.status(400).json({
status: 'ERROR',

View File

@ -1,7 +1,7 @@
import { useQuery } from '@apollo/client'
import { CenterLayout } from '@/components/layout'
import { useRouter } from 'next/router'
import { DIRECT } from '@/fragments/wallet'
import { DIRECT } from '@/fragments/invoice'
import { SSR, FAST_POLL_INTERVAL } from '@/lib/constants'
import Bolt11Info from '@/components/bolt11-info'
import { getGetServerSideProps } from '@/api/ssrApollo'

View File

@ -1,7 +1,7 @@
import Invoice from '@/components/invoice'
import { CenterLayout } from '@/components/layout'
import { useRouter } from 'next/router'
import { INVOICE_FULL } from '@/fragments/wallet'
import { INVOICE_FULL } from '@/fragments/invoice'
import { getGetServerSideProps } from '@/api/ssrApollo'
// force SSR to include CSP nonces

View File

@ -4,7 +4,7 @@ import { getGetServerSideProps } from '@/api/ssrApollo'
import Nav from 'react-bootstrap/Nav'
import Layout from '@/components/layout'
import MoreFooter from '@/components/more-footer'
import { WALLET_HISTORY } from '@/fragments/wallet'
import { WALLET_HISTORY } from '@/fragments/invoice'
import styles from '@/styles/satistics.module.css'
import Moon from '@/svgs/moon-fill.svg'
import Check from '@/svgs/check-double-line.svg'

View File

@ -76,11 +76,6 @@ export function SettingsHeader () {
<Nav.Link eventKey='mutes'>muted stackers</Nav.Link>
</Link>
</Nav.Item>
<Nav.Item>
<Link href='/settings/passphrase' passHref legacyBehavior>
<Nav.Link eventKey='passphrase'>device sync</Nav.Link>
</Link>
</Nav.Item>
</Nav>
</>
)
@ -155,16 +150,12 @@ export default function Settings ({ ssrData }) {
hideBookmarks: settings?.hideBookmarks,
hideWalletBalance: settings?.hideWalletBalance,
hideIsContributor: settings?.hideIsContributor,
noReferralLinks: settings?.noReferralLinks,
proxyReceive: settings?.proxyReceive,
receiveCreditsBelowSats: settings?.receiveCreditsBelowSats,
sendCreditsBelowSats: settings?.sendCreditsBelowSats
noReferralLinks: settings?.noReferralLinks
}}
schema={settingsSchema}
onSubmit={async ({
tipDefault, tipRandom, tipRandomMin, tipRandomMax, withdrawMaxFeeDefault,
zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, satsFilter,
receiveCreditsBelowSats, sendCreditsBelowSats,
...values
}) => {
if (nostrPubkey.length === 0) {
@ -190,8 +181,6 @@ export default function Settings ({ ssrData }) {
withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
satsFilter: Number(satsFilter),
zapUndos: zapUndosEnabled ? Number(zapUndos) : null,
receiveCreditsBelowSats: Number(receiveCreditsBelowSats),
sendCreditsBelowSats: Number(sendCreditsBelowSats),
nostrPubkey,
nostrRelays: nostrRelaysFiltered,
...values
@ -336,35 +325,6 @@ export default function Settings ({ ssrData }) {
name='noteCowboyHat'
/>
<div className='form-label'>wallet</div>
<Input
label='receive credits for zaps and deposits below'
name='receiveCreditsBelowSats'
required
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
<Input
label='send credits for zaps below'
name='sendCreditsBelowSats'
required
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
<Checkbox
label={
<div className='d-flex align-items-center'>enhance privacy of my lightning address
<Info>
<ul>
<li>Enabling this setting hides details (ie node pubkey) of your attached wallets when anyone pays your SN lightning address or lnurl-pay</li>
<li>The lightning invoice will appear to have SN's node as the destination to preserve your wallet's privacy</li>
<li>This will incur in a 10% fee</li>
<li>Disable this setting to receive payments directly to your attached wallets (which will reveal their details to the payer)</li>
<li>Note: this privacy behavior is standard for internal zaps/payments on SN, and this setting only applies to external payments</li>
</ul>
</Info>
</div>
}
name='proxyReceive'
groupClassName='mb-0'
/>
<Checkbox
label={
<div className='d-flex align-items-center'>hide invoice descriptions

View File

@ -1,211 +0,0 @@
import { getGetServerSideProps } from '@/api/ssrApollo'
import Layout from '@/components/layout'
import { SettingsHeader } from '../index'
import { useVaultConfigurator } from '@/components/vault/use-vault-configurator'
import { useMe } from '@/components/me'
import { Button, InputGroup } from 'react-bootstrap'
import bip39Words from '@/lib/bip39-words'
import { Form, PasswordInput, SubmitButton } from '@/components/form'
import { deviceSyncSchema } from '@/lib/validate'
import RefreshIcon from '@/svgs/refresh-line.svg'
import { useCallback, useEffect, useState } from 'react'
import { useToast } from '@/components/toast'
import { useWallets } from '@/wallets/index'
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
export default function DeviceSync ({ ssrData }) {
const { me } = useMe()
const { onVaultKeySet, beforeDisconnectVault } = useWallets()
const { key, setVaultKey, clearVault, disconnectVault } =
useVaultConfigurator({ onVaultKeySet, beforeDisconnectVault })
const [passphrase, setPassphrase] = useState()
const setSeedPassphrase = useCallback(async (passphrase) => {
await setVaultKey(passphrase)
setPassphrase(passphrase)
}, [setVaultKey])
const enabled = !!me?.privates?.vaultKeyHash
const connected = !!key
return (
<Layout>
<div className='pb-3 w-100 mt-2'>
<SettingsHeader />
<small className='line-height-md d-block mt-3' style={{ maxWidth: '600px' }}>
<p>
Device sync uses end-to-end encryption to securely synchronize your data across devices.
Your sensitive data remains private and inaccessible to our servers while being synced across all your connected devices using only a passphrase.
</p>
</small>
<div className='mt-4' style={{ maxWidth: '600px' }}>
{
(connected && passphrase && <Connect passphrase={passphrase} />) ||
(connected && <Connected disconnectVault={disconnectVault} />) ||
(enabled && <Enabled setVaultKey={setVaultKey} clearVault={clearVault} />) ||
<Setup setSeedPassphrase={setSeedPassphrase} />
}
</div>
</div>
</Layout>
)
}
function Connect ({ passphrase }) {
return (
<div>
<h2>Connect other devices</h2>
<p className='line-height-md'>
On your other devices, navigate to device sync settings and enter this exact passphrase.
</p>
<p className='line-height-md'>
<strong>Once you leave this page, this passphrase cannot be shown again.</strong> Connect all the devices you plan to use or write this passphrase down somewhere safe.
</p>
<PasswordInput
label='passphrase'
name='passphrase'
placeholder=''
required
autoFocus
as='textarea'
value={passphrase}
noForm
rows={3}
readOnly
copy
qr
/>
</div>
)
}
function Connected ({ disconnectVault }) {
return (
<div>
<h2>Device sync is enabled!</h2>
<p>
Sensitive data on this device is now securely synced between all connected devices.
</p>
<p className='text-muted text-sm'>
Disconnect to prevent this device from syncing data or to reset your passphrase.
</p>
<div className='d-flex justify-content-between'>
<div className='d-flex align-items-center ms-auto gap-2'>
<Button
variant='primary'
onClick={disconnectVault}
>disconnect
</Button>
</div>
</div>
</div>
)
}
function Enabled ({ setVaultKey, clearVault }) {
const toaster = useToast()
return (
<div>
<h2>Device sync is enabled</h2>
<p className='line-height-md'>
This device is not connected. Enter or scan your passphrase to connect. If you've lost your passphrase you may reset it.
</p>
<Form
schema={deviceSyncSchema}
initial={{ passphrase: '' }}
enableReinitialize
onSubmit={async ({ passphrase }) => {
try {
await setVaultKey(passphrase)
} catch (e) {
console.error(e)
toaster.danger('error setting vault key')
}
}}
>
<PasswordInput
label='passphrase'
name='passphrase'
placeholder=''
required
autoFocus
qr
/>
<div className='mt-3'>
<div className='d-flex justify-content-between align-items-center'>
<Button variant='danger' onClick={clearVault}>reset</Button>
<SubmitButton variant='primary'>enable</SubmitButton>
</div>
</div>
</Form>
</div>
)
}
const generatePassphrase = (n = 12) => {
const rand = new Uint32Array(n)
window.crypto.getRandomValues(rand)
return Array.from(rand).map(i => bip39Words[i % bip39Words.length]).join(' ')
}
function Setup ({ setSeedPassphrase }) {
const [passphrase, setPassphrase] = useState()
const toaster = useToast()
const newPassphrase = useCallback(() => {
setPassphrase(() => generatePassphrase(12))
}, [])
useEffect(() => {
setPassphrase(() => generatePassphrase(12))
}, [])
return (
<div>
<h2>Enable device sync</h2>
<p>
Enable secure sync of sensitive data (like wallet credentials) between your devices.
</p>
<p className='text-muted text-sm line-height-md'>
After enabled, your passphrase can be used to connect other devices.
</p>
<Form
schema={deviceSyncSchema}
initial={{ passphrase }}
enableReinitialize
onSubmit={async ({ passphrase }) => {
try {
await setSeedPassphrase(passphrase)
} catch (e) {
console.error(e)
toaster.danger('error setting passphrase')
}
}}
>
<PasswordInput
label='passphrase'
name='passphrase'
placeholder=''
required
autoFocus
as='textarea'
rows={3}
readOnly
append={
<InputGroup.Text style={{ cursor: 'pointer', userSelect: 'none' }} onClick={newPassphrase}>
<RefreshIcon width={16} height={16} />
</InputGroup.Text>
}
/>
<div className='mt-3'>
<div className='d-flex justify-content-between'>
<div className='d-flex align-items-center ms-auto gap-2'>
<SubmitButton variant='primary'>enable</SubmitButton>
</div>
</div>
</div>
</Form>
</div>
)
}

View File

@ -0,0 +1,20 @@
import { getGetServerSideProps } from '@/api/ssrApollo'
import { WalletForms as WalletFormsComponent } from '@/wallets/client/components'
import { unurlify } from '@/wallets/lib/util'
import { useParams } from 'next/navigation'
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
export default function WalletForms () {
const params = useParams()
const walletName = unurlify(params.slug[0])
// if the wallet name is a number, we are showing a configured wallet
// otherwise, we are showing a template
const isNumber = !Number.isNaN(Number(walletName))
if (isNumber) {
return <WalletFormsComponent id={Number(walletName)} />
}
return <WalletFormsComponent name={walletName} />
}

View File

@ -1,187 +0,0 @@
import { getGetServerSideProps } from '@/api/ssrApollo'
import { Form, ClientInput, PasswordInput, CheckboxGroup, Checkbox } from '@/components/form'
import { CenterLayout } from '@/components/layout'
import { WalletSecurityBanner } from '@/components/banners'
import { WalletLogs } from '@/wallets/logger'
import { useToast } from '@/components/toast'
import { useRouter } from 'next/router'
import { useWallet } from '@/wallets/index'
import Info from '@/components/info'
import Text from '@/components/text'
import { autowithdrawInitial, AutowithdrawSettings } from '@/components/autowithdraw-shared'
import { canReceive, canSend, isConfigured } from '@/wallets/common'
import { SSR } from '@/lib/constants'
import WalletButtonBar from '@/wallets/buttonbar'
import { useWalletConfigurator } from '@/wallets/config'
import { useCallback, useMemo } from 'react'
import { useMe } from '@/components/me'
import validateWallet from '@/wallets/validate'
import { ValidationError } from 'yup'
import { useFormikContext } from 'formik'
import { useWalletImage } from '@/wallets/image'
import styles from '@/styles/wallet.module.css'
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
export default function WalletSettings () {
const toaster = useToast()
const router = useRouter()
const { wallet: name } = router.query
const wallet = useWallet(name)
const { me } = useMe()
const { save, detach } = useWalletConfigurator(wallet)
const image = useWalletImage(wallet)
const initial = useMemo(() => {
const initial = wallet?.def.fields.reduce((acc, field) => {
// We still need to run over all wallet fields via reduce
// even though we use wallet.config as the initial value
// since wallet.config is empty when wallet is not configured.
// Also, wallet.config includes general fields like
// 'enabled' and 'priority' which are not defined in wallet.fields.
return {
...acc,
[field.name]: wallet?.config?.[field.name] || field.defaultValue || ''
}
}, wallet?.config)
if (wallet?.def.fields.every(f => f.clientOnly)) {
return initial
}
return {
...initial,
...autowithdrawInitial({ me })
}
}, [wallet, me])
const validate = useCallback(async (data) => {
try {
await validateWallet(wallet.def, data,
{ yupOptions: { abortEarly: false }, topLevel: false, skipGenerated: true })
} 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>
{image
? <img {...image} className={styles.walletBanner} />
: <h2 className='pb-2'>{wallet.def.card.title}</h2>}
<h6 className='text-muted text-center pb-3'><Text>{wallet.def.card.subtitle}</Text></h6>
<Form
initial={initial}
enableReinitialize
validate={validate}
onSubmit={async ({ amount, ...values }) => {
try {
const newConfig = !isConfigured(wallet)
// enable wallet if wallet was just configured
if (newConfig) {
values.enabled = true
}
await save(values, values.enabled)
toaster.success('saved settings')
router.push('/wallets')
} catch (err) {
console.error(err)
toaster.danger(err.message || err.toString?.())
}
}}
>
<SendWarningBanner walletDef={wallet.def} />
{wallet && <WalletFields 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 {
await detach()
toaster.success('saved settings')
router.push('/wallets')
} catch (err) {
console.error(err)
const message = 'failed to detach: ' + err.message || err.toString?.()
toaster.danger(message)
}
}}
/>
</Form>
<div className='mt-3 w-100'>
{wallet && <WalletLogs wallet={wallet} embedded />}
</div>
</CenterLayout>
)
}
function SendWarningBanner ({ walletDef }) {
const { values } = useFormikContext()
if (!canSend({ def: walletDef, config: values }) || !walletDef.requiresConfig) return null
return <WalletSecurityBanner />
}
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, requiredWithout,
validate, clientOnly, serverOnly, generated, ...props
}, i) => {
const rawProps = {
...props,
name,
initialValue: wallet.config?.[name],
readOnly: !SSR && isConfigured(wallet) && editable === false && !!wallet.config?.[name],
groupClassName: props.hidden ? 'd-none' : undefined,
label: label
? (
<div className='d-flex align-items-center'>
{label}
{/* help can be a string or object to customize the label */}
{help && (
<Info label={help.label}>
<Text>{help.text || help}</Text>
</Info>
)}
{optional && (
<small className='text-muted ms-2'>
{typeof optional === 'boolean' ? 'optional' : <Text>{optional}</Text>}
</small>
)}
</div>
)
: undefined,
required: !optional,
autoFocus: i === 0
}
if (type === 'text') {
return <ClientInput key={i} {...rawProps} />
}
if (type === 'password') {
return <PasswordInput key={i} {...rawProps} newPass />
}
return null
})
}

View File

@ -1,97 +1,76 @@
import { getGetServerSideProps } from '@/api/ssrApollo'
import Layout from '@/components/layout'
import styles from '@/styles/wallet.module.css'
import Link from 'next/link'
import { useWallets } from '@/wallets/index'
import { useCallback, useEffect, useState } from 'react'
import { useIsClient } from '@/components/use-client'
import WalletCard from '@/wallets/card'
import { useToast } from '@/components/toast'
import BootstrapForm from 'react-bootstrap/Form'
import RecvIcon from '@/svgs/arrow-left-down-line.svg'
import SendIcon from '@/svgs/arrow-right-up-line.svg'
import { useRouter } from 'next/router'
import { supportsReceive, supportsSend } from '@/wallets/common'
import { useWalletIndicator } from '@/wallets/indicator'
import { Button } from 'react-bootstrap'
import { useWallets, useTemplates, DndProvider, Status, useStatus } from '@/wallets/client/context'
import { WalletCard, WalletLayout, WalletLayoutHeader, WalletLayoutLink, WalletLayoutSubHeader } from '@/wallets/client/components'
import styles from '@/styles/wallet.module.css'
import { usePassphrasePrompt, useShowPassphrase, useSetWalletPriorities } from '@/wallets/client/hooks'
import { WalletSearch } from '@/wallets/client/components/search'
import { useMemo, useState } from 'react'
import { walletDisplayName } from '@/wallets/lib/util'
import Moon from '@/svgs/moon-fill.svg'
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
export default function Wallet ({ ssrData }) {
const { wallets, setPriorities } = useWallets()
const toast = useToast()
const isClient = useIsClient()
const [sourceIndex, setSourceIndex] = useState(null)
const [targetIndex, setTargetIndex] = useState(null)
export default function Wallet () {
const wallets = useWallets()
const status = useStatus()
const [showWallets, setShowWallets] = useState(false)
const templates = useTemplates()
const showPassphrase = useShowPassphrase()
const passphrasePrompt = usePassphrasePrompt()
const setWalletPriorities = useSetWalletPriorities()
const [searchFilter, setSearchFilter] = useState(() => (text) => true)
const router = useRouter()
const [filter, setFilter] = useState({
send: router.query.send === 'true',
receive: router.query.receive === 'true'
})
const reorder = useCallback(async (sourceIndex, targetIndex) => {
const newOrder = [...wallets.filter(w => w.config?.enabled)]
const [source] = newOrder.splice(sourceIndex, 1)
const priorities = newOrder.slice(0, targetIndex)
.concat(source)
.concat(newOrder.slice(targetIndex))
.map((w, i) => ({ wallet: w, priority: i }))
await setPriorities(priorities)
}, [setPriorities, wallets])
const onDragStart = useCallback((i) => (e) => {
// e.dataTransfer.dropEffect = 'move'
// We can only use the DataTransfer API inside the drop event
// see https://html.spec.whatwg.org/multipage/dnd.html#security-risks-in-the-drag-and-drop-model
// e.dataTransfer.setData('text/plain', name)
// That's why we use React state instead
setSourceIndex(i)
}, [setSourceIndex])
const onDragEnter = useCallback((i) => (e) => {
setTargetIndex(i)
}, [setTargetIndex])
const onReorderError = useCallback((err) => {
console.error(err)
toast.danger('failed to reorder wallets')
}, [toast])
const onDragEnd = useCallback((e) => {
setSourceIndex(null)
setTargetIndex(null)
if (sourceIndex === targetIndex) return
reorder(sourceIndex, targetIndex).catch(onReorderError)
}, [sourceIndex, targetIndex, reorder, onReorderError])
const onTouchStart = useCallback((i) => (e) => {
if (sourceIndex !== null) {
reorder(sourceIndex, i).catch(onReorderError)
setSourceIndex(null)
} else {
setSourceIndex(i)
const { wallets: filteredWallets, templates: filteredTemplates } = useMemo(() => {
const walletFilter = ({ name }) => searchFilter(walletDisplayName(name)) || searchFilter(name)
return {
wallets: wallets.filter(walletFilter),
templates: templates.filter(walletFilter)
}
}, [sourceIndex, reorder, onReorderError])
}, [wallets, templates, searchFilter])
const onFilterChange = useCallback((key) => {
return e => {
setFilter(old => ({ ...old, [key]: e.target.checked }))
router.replace({ query: { ...router.query, [key]: e.target.checked } }, undefined, { shallow: true })
}
}, [router])
const indicator = useWalletIndicator()
const [showWallets, setShowWallets] = useState(!indicator)
useEffect(() => { setShowWallets(!indicator) }, [indicator])
if (indicator && !showWallets) {
if (status === Status.LOADING_WALLETS) {
return (
<Layout>
<WalletLayout>
<div className='py-5 text-center d-flex flex-column align-items-center justify-content-center flex-grow-1 text-muted'>
<Moon className='spin fill-grey' height={28} width={28} />
<small className='d-block mt-3 text-muted'>loading wallets</small>
</div>
</WalletLayout>
)
}
if (status === Status.PASSPHRASE_REQUIRED) {
return (
<WalletLayout>
<div className='py-5 text-center d-flex flex-column align-items-center justify-content-center flex-grow-1'>
<Button
onClick={passphrasePrompt}
size='md' variant='secondary'
>unlock wallets
</Button>
<small className='d-block mt-3 text-muted'>your passphrase is required</small>
</div>
</WalletLayout>
)
}
if (status === Status.WALLETS_UNAVAILABLE) {
return (
<WalletLayout>
<div className='py-5 text-center d-flex flex-column align-items-center justify-content-center flex-grow-1'>
<span className='text-muted fw-bold my-1'>wallets unavailable</span>
<small className='d-block text-muted'>
this device does not support storage of cryptographic keys via IndexedDB
</small>
</div>
</WalletLayout>
)
}
if (status === Status.NO_WALLETS && !showWallets) {
return (
<WalletLayout>
<div className='py-5 text-center d-flex flex-column align-items-center justify-content-center flex-grow-1'>
<Button
onClick={() => setShowWallets(true)}
@ -100,70 +79,54 @@ export default function Wallet ({ ssrData }) {
</Button>
<small className='d-block mt-3 text-muted'>attach a wallet to send and receive sats</small>
</div>
</Layout>
</WalletLayout>
)
}
return (
<Layout>
<div className='py-5 w-100'>
<h2 className='mb-2 text-center'>wallets</h2>
<h6 className='text-muted text-center'>use real bitcoin</h6>
<WalletLayout>
<div className='py-5'>
<WalletLayoutHeader>wallets</WalletLayoutHeader>
<WalletLayoutSubHeader>use real bitcoin</WalletLayoutSubHeader>
<div className='text-center'>
<Link href='/wallets/logs' className='text-muted fw-bold text-underline'>
wallet logs
</Link>
<WalletLayoutLink href='/wallets/logs'>wallet logs</WalletLayoutLink>
<span className='mx-2'></span>
<WalletLayoutLink href='/wallets/settings'>settings</WalletLayoutLink>
{showPassphrase && (
<>
<span className='mx-2'></span>
<Button
variant='link'
className='text-muted fw-bold text-underline p-0 align-baseline'
onClick={showPassphrase}
>
passphrase
</Button>
</>
)}
</div>
<div className={styles.walletGrid} onDragEnd={onDragEnd}>
<div className={styles.walletFilters}>
<BootstrapForm.Check
inline
label={<span><RecvIcon width={16} height={16} /> receive</span>}
onChange={onFilterChange('receive')}
checked={filter.receive}
/>
<BootstrapForm.Check
inline
label={<span><SendIcon width={16} height={16} /> send</span>}
onChange={onFilterChange('send')}
checked={filter.send}
/>
</div>
{
wallets
.filter(w => {
return (!filter.send || (filter.send && supportsSend(w))) &&
(!filter.receive || (filter.receive && supportsReceive(w)))
})
.map((w, i) => {
const draggable = isClient && w.config?.enabled
return (
<div
key={w.def.name}
className={
!draggable
? ''
: (`${sourceIndex === i ? styles.drag : ''} ${draggable && targetIndex === i ? styles.drop : ''}`)
}
suppressHydrationWarning
>
<WalletCard
wallet={w}
draggable={draggable}
onDragStart={draggable ? onDragStart(i) : undefined}
onTouchStart={draggable ? onTouchStart(i) : undefined}
onDragEnter={draggable ? onDragEnter(i) : undefined}
sourceIndex={sourceIndex}
targetIndex={targetIndex}
index={i}
/>
</div>
)
})
}
<WalletSearch setSearchFilter={setSearchFilter} />
{filteredWallets.length > 0 && (
<>
<DndProvider items={filteredWallets} onReorder={setWalletPriorities}>
<div className={styles.walletGrid}>
{filteredWallets.map((wallet, index) => (
<WalletCard
key={wallet.id}
wallet={wallet}
index={index}
draggable
/>
))}
</div>
</DndProvider>
<div className={styles.separator} />
</>
)}
<div className={styles.walletGrid}>
{filteredTemplates.map((w, i) => <WalletCard key={i} wallet={w} />)}
</div>
</div>
</Layout>
</WalletLayout>
)
}

View File

@ -1,16 +1,15 @@
import { CenterLayout } from '@/components/layout'
import { getGetServerSideProps } from '@/api/ssrApollo'
import { WalletLogs } from '@/wallets/logger'
import { WalletLayout, WalletLayoutHeader, WalletLogs } from '@/wallets/client/components'
export const getServerSideProps = getGetServerSideProps({ query: null })
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
export default function () {
export default function WalletLogsPage () {
return (
<>
<CenterLayout>
<h2 className='text-center'>wallet logs</h2>
<WalletLayout>
<div className='py-5'>
<WalletLayoutHeader>wallet logs</WalletLayoutHeader>
<WalletLogs />
</CenterLayout>
</>
</div>
</WalletLayout>
)
}

185
pages/wallets/settings.js Normal file
View File

@ -0,0 +1,185 @@
import { getGetServerSideProps } from '@/api/ssrApollo'
import { Checkbox, Form, Input, SubmitButton } from '@/components/form'
import Info from '@/components/info'
import { isNumber } from '@/lib/format'
import { WalletLayout, WalletLayoutHeader, WalletLayoutSubHeader } from '@/wallets/client/components'
import { useMutation, useQuery } from '@apollo/client'
import Link from 'next/link'
import { useCallback, useMemo } from 'react'
import { InputGroup } from 'react-bootstrap'
import styles from '@/styles/wallet.module.css'
import classNames from 'classnames'
import { useField } from 'formik'
import { SET_WALLET_SETTINGS, WALLET_SETTINGS } from '@/wallets/client/fragments'
import { walletSettingsSchema } from '@/lib/validate'
import { useToast } from '@/components/toast'
import CancelButton from '@/components/cancel-button'
export const getServerSideProps = getGetServerSideProps({ query: WALLET_SETTINGS, authRequired: true })
export default function WalletSettings ({ ssrData }) {
const { data } = useQuery(WALLET_SETTINGS)
const [setSettings] = useMutation(SET_WALLET_SETTINGS)
const { walletSettings: settings } = useMemo(() => data ?? ssrData, [data, ssrData])
const toaster = useToast()
const initial = {
receiveCreditsBelowSats: settings?.receiveCreditsBelowSats,
sendCreditsBelowSats: settings?.sendCreditsBelowSats,
autoWithdrawThreshold: settings?.autoWithdrawThreshold ?? 10000,
autoWithdrawMaxFeePercent: settings?.autoWithdrawMaxFeePercent ?? 1,
autoWithdrawMaxFeeTotal: settings?.autoWithdrawMaxFeeTotal ?? 1,
proxyReceive: settings?.proxyReceive
}
const onSubmit = useCallback(async (values) => {
try {
await setSettings({
variables: {
settings: values
}
})
toaster.success('saved settings')
} catch (err) {
console.error(err)
toaster.danger('failed to save settings')
}
}, [toaster])
return (
<WalletLayout>
<div className='py-5 mx-auto w-100' style={{ maxWidth: '600px' }}>
<WalletLayoutHeader>wallet settings</WalletLayoutHeader>
<WalletLayoutSubHeader>apply globally to all wallets</WalletLayoutSubHeader>
<Form
enableReinitialize
initial={initial}
schema={walletSettingsSchema}
className='mt-3'
onSubmit={onSubmit}
>
<CowboyCreditsSettings />
<LightningAddressSettings />
<AutowithdrawSettings />
<LightningNetworkFeesSettings />
<div className='d-flex mt-1 justify-content-end'>
<CancelButton />
<SubmitButton variant='info'>save</SubmitButton>
</div>
</Form>
</div>
</WalletLayout>
)
}
function CowboyCreditsSettings () {
return (
<>
<Separator>cowboy credits</Separator>
<Input
label='receive credits for zaps below'
name='receiveCreditsBelowSats'
required
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
type='number'
min={0}
/>
<Input
label='send credits for zaps below'
name='sendCreditsBelowSats'
required
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
type='number'
min={0}
/>
</>
)
}
function LightningAddressSettings () {
return (
<>
<Separator>@stacker.news lightning address</Separator>
<Checkbox
label={
<div className='d-flex align-items-center'>enhance privacy of my lightning address
<Info>
<ul>
<li>Enabling this setting hides details (ie node pubkey) of your attached wallets when anyone pays your SN lightning address or lnurl-pay</li>
<li>The lightning invoice will appear to have SN's node as the destination to preserve your wallet's privacy</li>
<li>This will incur in a 10% fee</li>
<li>Disable this setting to receive payments directly to your attached wallets (which will reveal their details to the payer)</li>
<li>Note: this privacy behavior is standard for internal zaps/payments on SN, and this setting only applies to external payments</li>
</ul>
</Info>
</div>
}
name='proxyReceive'
groupClassName='mb-0'
/>
</>
)
}
function AutowithdrawSettings () {
const [{ value: threshold }] = useField('autoWithdrawThreshold')
const sendThreshold = Math.max(Math.floor(threshold / 10), 1)
return (
<>
<Separator>autowithdrawal</Separator>
<Input
label='desired balance'
name='autoWithdrawThreshold'
hint={isNumber(sendThreshold) ? `will attempt autowithdrawal when your balance exceeds ${sendThreshold * 11} sats` : undefined}
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
required
type='number'
min={0}
/>
</>
)
}
function LightningNetworkFeesSettings () {
return (
<>
<Separator className='mb-0'>lightning network fees</Separator>
<div className='text-center text-muted mb-2'>
we'll use whichever setting is higher during{' '}
<Link
target='_blank'
href='https://docs.lightning.engineering/the-lightning-network/pathfinding'
rel='noreferrer'
>pathfinding
</Link>
</div>
<Input
label='max fee rate'
name='autoWithdrawMaxFeePercent'
hint='max fee as percent of withdrawal amount'
append={<InputGroup.Text>%</InputGroup.Text>}
required
type='number'
min={0}
/>
<Input
label='max fee total'
name='autoWithdrawMaxFeeTotal'
hint='max fee for any withdrawal amount'
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
required
type='number'
min={0}
/>
</>
)
}
function Separator ({ children, className }) {
return (
<div className={classNames(styles.separator, 'fw-bold', className)}>{children}</div>
)
}

View File

@ -5,7 +5,7 @@ import { useRouter } from 'next/router'
import { InputGroup, Nav } from 'react-bootstrap'
import styles from '@/components/user-header.module.css'
import { gql, useMutation, useQuery } from '@apollo/client'
import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '@/fragments/wallet'
import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '@/fragments/invoice'
import { requestProvider } from 'webln'
import { useEffect, useState } from 'react'
import { useMe } from '@/components/me'

View File

@ -4,7 +4,7 @@ import { CopyInput, Input, InputSkeleton } from '@/components/form'
import InputGroup from 'react-bootstrap/InputGroup'
import InvoiceStatus from '@/components/invoice-status'
import { useRouter } from 'next/router'
import { WITHDRAWL } from '@/fragments/wallet'
import { WITHDRAWL } from '@/fragments/invoice'
import Link from 'next/link'
import { SSR, INVOICE_RETENTION_DAYS, FAST_POLL_INTERVAL } from '@/lib/constants'
import { numWithUnits } from '@/lib/format'

View File

@ -0,0 +1,208 @@
/*
Warnings:
- A unique constraint covering the columns `[apiKeyId]` on the table `WalletBlink` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[currencyId]` on the table `WalletBlink` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[adminKeyId]` on the table `WalletLNbits` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[nwcUrlId]` on the table `WalletNWC` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[primaryPasswordId]` on the table `WalletPhoenixd` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "WalletBlink"
ADD COLUMN "apiKeyId" INTEGER,
ADD COLUMN "currencyId" INTEGER;
-- AlterTable
ALTER TABLE "WalletLNbits" ADD COLUMN "adminKeyId" INTEGER;
-- AlterTable
ALTER TABLE "WalletNWC" ADD COLUMN "nwcUrlId" INTEGER;
-- AlterTable
ALTER TABLE "WalletPhoenixd" ADD COLUMN "primaryPasswordId" INTEGER;
-- CreateTable
CREATE TABLE "Vault" (
"id" SERIAL NOT NULL,
"iv" TEXT NOT NULL,
"value" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Vault_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "WalletLNC" (
"id" SERIAL NOT NULL,
"walletId" INTEGER NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"pairingPhraseId" INTEGER,
"localKeyId" INTEGER,
"remoteKeyId" INTEGER,
"serverHostId" INTEGER,
CONSTRAINT "WalletLNC_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "WalletWebLN" (
"id" SERIAL NOT NULL,
"walletId" INTEGER NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "WalletWebLN_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "WalletBlink_apiKeyId_key" ON "WalletBlink"("apiKeyId");
-- CreateIndex
CREATE UNIQUE INDEX "WalletBlink_currencyId_key" ON "WalletBlink"("currencyId");
-- CreateIndex
CREATE UNIQUE INDEX "WalletLNbits_adminKeyId_key" ON "WalletLNbits"("adminKeyId");
-- CreateIndex
CREATE UNIQUE INDEX "WalletNWC_nwcUrlId_key" ON "WalletNWC"("nwcUrlId");
-- CreateIndex
CREATE UNIQUE INDEX "WalletPhoenixd_primaryPasswordId_key" ON "WalletPhoenixd"("primaryPasswordId");
-- CreateIndex
CREATE UNIQUE INDEX "WalletLNC_walletId_key" ON "WalletLNC"("walletId");
-- CreateIndex
CREATE UNIQUE INDEX "WalletLNC_pairingPhraseId_key" ON "WalletLNC"("pairingPhraseId");
-- CreateIndex
CREATE UNIQUE INDEX "WalletLNC_localKeyId_key" ON "WalletLNC"("localKeyId");
-- CreateIndex
CREATE UNIQUE INDEX "WalletLNC_remoteKeyId_key" ON "WalletLNC"("remoteKeyId");
-- CreateIndex
CREATE UNIQUE INDEX "WalletLNC_serverHostId_key" ON "WalletLNC"("serverHostId");
-- CreateIndex
CREATE UNIQUE INDEX "WalletWebLN_walletId_key" ON "WalletWebLN"("walletId");
-- AddForeignKey
ALTER TABLE "WalletLNbits" ADD CONSTRAINT "WalletLNbits_adminKeyId_fkey" FOREIGN KEY ("adminKeyId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WalletNWC" ADD CONSTRAINT "WalletNWC_nwcUrlId_fkey" FOREIGN KEY ("nwcUrlId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WalletBlink" ADD CONSTRAINT "WalletBlink_apiKeyId_fkey" FOREIGN KEY ("apiKeyId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WalletBlink" ADD CONSTRAINT "WalletBlink_currencyId_fkey" FOREIGN KEY ("currencyId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WalletPhoenixd" ADD CONSTRAINT "WalletPhoenixd_primaryPasswordId_fkey" FOREIGN KEY ("primaryPasswordId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WalletLNC" ADD CONSTRAINT "WalletLNC_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WalletLNC" ADD CONSTRAINT "WalletLNC_pairingPhraseId_fkey" FOREIGN KEY ("pairingPhraseId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WalletLNC" ADD CONSTRAINT "WalletLNC_localKeyId_fkey" FOREIGN KEY ("localKeyId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WalletLNC" ADD CONSTRAINT "WalletLNC_remoteKeyId_fkey" FOREIGN KEY ("remoteKeyId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WalletLNC" ADD CONSTRAINT "WalletLNC_serverHostId_fkey" FOREIGN KEY ("serverHostId") REFERENCES "Vault"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "WalletWebLN" ADD CONSTRAINT "WalletWebLN_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TRIGGER wallet_lnc_as_jsonb
AFTER INSERT OR UPDATE ON "WalletLNC"
FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb();
CREATE TRIGGER wallet_webln_as_jsonb
AFTER INSERT OR UPDATE ON "WalletWebLN"
FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb();
CREATE OR REPLACE FUNCTION migrate_wallet_vault()
RETURNS void AS
$$
DECLARE
vaultEntry "VaultEntry"%ROWTYPE;
BEGIN
INSERT INTO "WalletWebLN"("walletId") SELECT id FROM "Wallet" WHERE type = 'WEBLN';
INSERT INTO "WalletLNC"("walletId") SELECT id from "Wallet" WHERE type = 'LNC';
FOR vaultEntry IN SELECT * FROM "VaultEntry" LOOP
DECLARE
vaultId INT;
walletType "WalletType";
BEGIN
INSERT INTO "Vault" ("iv", "value")
VALUES (vaultEntry."iv", vaultEntry."value")
RETURNING id INTO vaultId;
SELECT type INTO walletType
FROM "Wallet"
WHERE id = vaultEntry."walletId";
CASE walletType
WHEN 'LNBITS' THEN
UPDATE "WalletLNbits"
SET "adminKeyId" = vaultId
WHERE "walletId" = vaultEntry."walletId";
WHEN 'NWC' THEN
UPDATE "WalletNWC"
SET "nwcUrlId" = vaultId
WHERE "walletId" = vaultEntry."walletId";
WHEN 'BLINK' THEN
IF vaultEntry."key" = 'apiKey' THEN
UPDATE "WalletBlink"
SET "apiKeyId" = vaultId
WHERE "walletId" = vaultEntry."walletId";
ELSE
UPDATE "WalletBlink"
SET "currencyId" = vaultId
WHERE "walletId" = vaultEntry."walletId";
END IF;
WHEN 'PHOENIXD' THEN
UPDATE "WalletPhoenixd"
SET "primaryPasswordId" = vaultId
WHERE "walletId" = vaultEntry."walletId";
WHEN 'LNC' THEN
IF vaultEntry."key" = 'pairingPhrase' THEN
UPDATE "WalletLNC"
SET "pairingPhraseId" = vaultId
WHERE "walletId" = vaultEntry."walletId";
ELSIF vaultEntry."key" = 'localKey' THEN
UPDATE "WalletLNC"
SET "localKeyId" = vaultId
WHERE "walletId" = vaultEntry."walletId";
ELSIF vaultEntry."key" = 'remoteKey' THEN
UPDATE "WalletLNC"
SET "remoteKeyId" = vaultId
WHERE "walletId" = vaultEntry."walletId";
ELSIF vaultEntry."key" = 'serverHost' THEN
UPDATE "WalletLNC"
SET "serverHostId" = vaultId
WHERE "walletId" = vaultEntry."walletId";
END IF;
END CASE;
END;
END LOOP;
END;
$$ LANGUAGE plpgsql;
SELECT migrate_wallet_vault();
DROP FUNCTION migrate_wallet_vault();
ALTER TABLE "VaultEntry" DROP CONSTRAINT "VaultEntry_userId_fkey";
ALTER TABLE "VaultEntry" DROP CONSTRAINT "VaultEntry_walletId_fkey";
DROP TABLE "VaultEntry";

File diff suppressed because it is too large Load Diff

View File

@ -145,8 +145,8 @@ model User {
oneDayReferrals OneDayReferral[] @relation("OneDayReferral_referrer")
oneDayReferrees OneDayReferral[] @relation("OneDayReferral_referrees")
vaultKeyHash String @default("")
showPassphrase Boolean @default(true)
walletsUpdatedAt DateTime?
vaultEntries VaultEntry[] @relation("VaultEntries")
proxyReceive Boolean @default(true)
DirectPaymentReceived DirectPayment[] @relation("DirectPaymentReceived")
DirectPaymentSent DirectPayment[] @relation("DirectPaymentSent")
@ -202,157 +202,42 @@ model UserSubTrust {
@@id([userId, subName])
}
enum WalletType {
LIGHTNING_ADDRESS
LND
CLN
LNBITS
NWC
PHOENIXD
BLINK
LNC
WEBLN
}
model Wallet {
model Vault {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
userId Int
label String?
enabled Boolean @default(true)
priority Int @default(0)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// NOTE: this denormalized json field exists to make polymorphic joins efficient
// when reading wallets ... it is populated by a trigger when wallet descendants update
// otherwise reading wallets would require a join on every descendant table
// which might not be numerous for wallets but would be for other tables
// so this is a pattern we use only to be consistent with future polymorphic tables
// because it gives us fast reads and type safe writes
type WalletType
wallet Json? @db.JsonB
walletLightningAddress WalletLightningAddress?
walletLND WalletLND?
walletCLN WalletCLN?
walletLNbits WalletLNbits?
walletNWC WalletNWC?
walletPhoenixd WalletPhoenixd?
walletBlink WalletBlink?
vaultEntries VaultEntry[] @relation("VaultEntries")
withdrawals Withdrawl[]
InvoiceForward InvoiceForward[]
DirectPayment DirectPayment[]
@@unique([userId, type])
@@index([userId])
@@index([priority])
}
model VaultEntry {
id Int @id @default(autoincrement())
key String @db.Text
iv String @db.Text
value String @db.Text
userId Int
walletId Int?
user User @relation(fields: [userId], references: [id], onDelete: Cascade, name: "VaultEntries")
wallet Wallet? @relation(fields: [walletId], references: [id], onDelete: Cascade, name: "VaultEntries")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@unique([userId, key])
@@index([walletId])
walletSendNWC WalletSendNWC?
walletSendLNbits WalletSendLNbits?
walletSendPhoenixd WalletSendPhoenixd?
walletSendBlinkApiKey WalletSendBlink? @relation("blinkApiKeySend")
walletSendBlinkCurrency WalletSendBlink? @relation("blinkCurrencySend")
walletSendLNCPairingPhrase WalletSendLNC? @relation("lncPairingPhrase")
walletSendLNCLocalKey WalletSendLNC? @relation("lncLocalKey")
walletSendLNCRemoteKey WalletSendLNC? @relation("lncRemoteKey")
walletSendLNCServerHost WalletSendLNC? @relation("lncServerHost")
}
model WalletLog {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
wallet WalletType?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
protocolId Int?
protocol WalletProtocol? @relation(fields: [protocolId], references: [id], onDelete: Cascade)
level LogLevel
message String
invoiceId Int?
invoice Invoice? @relation(fields: [invoiceId], references: [id])
invoice Invoice? @relation(fields: [invoiceId], references: [id])
withdrawalId Int?
withdrawal Withdrawl? @relation(fields: [withdrawalId], references: [id])
context Json? @db.JsonB
withdrawal Withdrawl? @relation(fields: [withdrawalId], references: [id])
context Json? @db.JsonB
@@index([userId, createdAt])
}
model WalletLightningAddress {
id Int @id @default(autoincrement())
walletId Int @unique
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
address String
}
model WalletLND {
id Int @id @default(autoincrement())
walletId Int @unique
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
socket String
macaroon String
cert String?
}
model WalletCLN {
id Int @id @default(autoincrement())
walletId Int @unique
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
socket String
rune String
cert String?
}
model WalletLNbits {
id Int @id @default(autoincrement())
walletId Int @unique
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
url String
invoiceKey String?
}
model WalletNWC {
id Int @id @default(autoincrement())
walletId Int @unique
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
nwcUrlRecv String?
}
model WalletBlink {
id Int @id @default(autoincrement())
walletId Int @unique
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
apiKeyRecv String?
currencyRecv String?
}
model WalletPhoenixd {
id Int @id @default(autoincrement())
walletId Int @unique
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
url String
secondaryPassword String?
}
model Mute {
muterId Int
mutedId Int
@ -1005,22 +890,22 @@ model Invoice {
}
model DirectPayment {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
senderId Int?
receiverId Int?
preimage String? @unique
preimage String? @unique
bolt11 String?
hash String? @unique
hash String? @unique
desc String?
comment String?
lud18Data Json?
msats BigInt
walletId Int?
sender User? @relation("DirectPaymentSent", fields: [senderId], references: [id], onDelete: Cascade)
receiver User? @relation("DirectPaymentReceived", fields: [receiverId], references: [id], onDelete: Cascade)
wallet Wallet? @relation(fields: [walletId], references: [id], onDelete: SetNull)
protocolId Int?
sender User? @relation("DirectPaymentSent", fields: [senderId], references: [id], onDelete: Cascade)
receiver User? @relation("DirectPaymentReceived", fields: [receiverId], references: [id], onDelete: Cascade)
protocol WalletProtocol? @relation(fields: [protocolId], references: [id], onDelete: SetNull)
@@index([createdAt])
@@index([senderId])
@ -1033,7 +918,7 @@ model InvoiceForward {
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
bolt11 String
maxFeeMsats Int
walletId Int
protocolId Int
// we get these values when the invoice is held
expiryHeight Int?
@ -1043,12 +928,12 @@ model InvoiceForward {
invoiceId Int @unique
withdrawlId Int? @unique
invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
withdrawl Withdrawl? @relation(fields: [withdrawlId], references: [id], onDelete: SetNull)
invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
withdrawl Withdrawl? @relation(fields: [withdrawlId], references: [id], onDelete: SetNull)
@@index([invoiceId])
@@index([walletId])
@@index([protocolId])
@@index([withdrawlId])
}
@ -1066,16 +951,16 @@ model Withdrawl {
msatsFeePaid BigInt?
status WithdrawlStatus?
autoWithdraw Boolean @default(false)
walletId Int?
protocolId Int?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
wallet Wallet? @relation(fields: [walletId], references: [id], onDelete: SetNull)
protocol WalletProtocol? @relation(fields: [protocolId], references: [id], onDelete: SetNull)
invoiceForward InvoiceForward?
WalletLog WalletLog[]
@@index([createdAt], map: "Withdrawl.created_at_index")
@@index([userId], map: "Withdrawl.userId_index")
@@index([hash])
@@index([walletId])
@@index([protocolId])
@@index([autoWithdraw])
@@index([status])
}
@ -1292,9 +1177,283 @@ enum WithdrawlStatus {
}
enum LogLevel {
OK
DEBUG
INFO
WARN
WARNING
ERROR
SUCCESS
}
// ===================
// ==== WALLET V2 ====
// ===================
enum WalletProtocolName {
NWC
LNBITS
PHOENIXD
BLINK
WEBLN
LN_ADDR
LNC
CLN_REST
LND_GRPC
}
enum WalletSendProtocolName {
NWC
LNBITS
PHOENIXD
BLINK
WEBLN
LNC
}
enum WalletRecvProtocolName {
NWC
LNBITS
PHOENIXD
BLINK
LN_ADDR
CLN_REST
LND_GRPC
}
enum WalletProtocolStatus {
OK
WARNING
ERROR
}
enum WalletName {
ALBY
BLINK
BLIXT
CASHU_ME
CLN
COINOS
FOUNTAIN
LIFPAY
LNBITS
LND
MINIBITS
NPUB_CASH
PHOENIXD
PRIMAL
RIZFUL
SHOCKWALLET
SPEED
STRIKE
VOLTAGE
WALLET_OF_SATOSHI
ZBD
ZEUS
NWC
LN_ADDR
CASH_APP
}
model WalletTemplate {
name WalletName @id
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
sendProtocols WalletSendProtocolName[]
recvProtocols WalletRecvProtocolName[]
wallets Wallet[]
}
model Wallet {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
priority Int @default(0)
userId Int
templateName WalletName
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
template WalletTemplate @relation(fields: [templateName], references: [name], onDelete: Cascade)
protocols WalletProtocol[]
@@index([userId])
@@index([templateName])
}
model WalletProtocol {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
// NOTE: this denormalized json field exists to make polymorphic joins efficient
// when reading wallets ... it's populated by a trigger when wallet descendants update
// otherwise reading wallets would require a join on every descendant table.
// this pattern gives us fast reads and fast, type safe writes
config Json? @db.JsonB
walletId Int
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
send Boolean
name WalletProtocolName
enabled Boolean @default(true)
status WalletProtocolStatus @default(OK)
withdrawals Withdrawl[]
directPayments DirectPayment[]
invoiceForward InvoiceForward[]
logs WalletLog[]
walletSendNWC WalletSendNWC?
walletSendLNbits WalletSendLNbits?
walletSendPhoenixd WalletSendPhoenixd?
walletSendBlink WalletSendBlink?
walletSendWebLN WalletSendWebLN?
walletSendLNC WalletSendLNC?
walletRecvNWC WalletRecvNWC?
walletRecvLNbits WalletRecvLNbits?
walletRecvPhoenixd WalletRecvPhoenixd?
walletRecvBlink WalletRecvBlink?
walletRecvLightningAddress WalletRecvLightningAddress?
walletRecvCLNRest WalletRecvCLNRest?
walletRecvLNDGRPC WalletRecvLNDGRPC?
@@index([walletId])
@@unique(name: "WalletProtocol_walletId_send_name_key", [walletId, send, name])
}
model WalletSendNWC {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
protocolId Int @unique
protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
urlVaultId Int @unique
url Vault @relation(fields: [urlVaultId], references: [id], onDelete: Cascade)
}
model WalletSendLNbits {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
protocolId Int @unique
protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
url String
apiKeyVaultId Int @unique
apiKey Vault @relation(fields: [apiKeyVaultId], references: [id], onDelete: Cascade)
}
model WalletSendPhoenixd {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
protocolId Int @unique
protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
url String
apiKeyVaultId Int @unique
apiKey Vault @relation(fields: [apiKeyVaultId], references: [id], onDelete: Cascade)
}
model WalletSendBlink {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
protocolId Int @unique
protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
currencyVaultId Int @unique
currency Vault @relation("blinkCurrencySend", fields: [currencyVaultId], references: [id], onDelete: Cascade)
apiKeyVaultId Int @unique
apiKey Vault @relation("blinkApiKeySend", fields: [apiKeyVaultId], references: [id], onDelete: Cascade)
}
model WalletSendWebLN {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
protocolId Int @unique
protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
}
model WalletSendLNC {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
protocolId Int @unique
protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
pairingPhraseVaultId Int @unique
pairingPhrase Vault? @relation("lncPairingPhrase", fields: [pairingPhraseVaultId], references: [id])
localKeyVaultId Int @unique
localKey Vault? @relation("lncLocalKey", fields: [localKeyVaultId], references: [id])
remoteKeyVaultId Int @unique
remoteKey Vault? @relation("lncRemoteKey", fields: [remoteKeyVaultId], references: [id])
serverHostVaultId Int @unique
serverHost Vault? @relation("lncServerHost", fields: [serverHostVaultId], references: [id])
}
model WalletRecvNWC {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
protocolId Int @unique
protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
url String
}
model WalletRecvLNbits {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
protocolId Int @unique
protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
url String
apiKey String
}
model WalletRecvPhoenixd {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
protocolId Int @unique
protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
url String
apiKey String
}
model WalletRecvBlink {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
protocolId Int @unique
protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
currency String
apiKey String
}
model WalletRecvLightningAddress {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
protocolId Int @unique
protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
address String
}
model WalletRecvCLNRest {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
protocolId Int @unique
protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
socket String
rune String
cert String?
}
model WalletRecvLNDGRPC {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
protocolId Int @unique
protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
socket String
macaroon String
cert String?
}

View File

@ -0,0 +1,24 @@
<svg width="141" height="39" viewBox="0 0 141 39" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.6001 22.7998H36.8001V31.8998H13.6001V22.7998Z" fill="white"/>
<path d="M4.9001 10.2999C2.30436 10.2999 0.200098 8.19564 0.200098 5.5999C0.200098 3.00416 2.30436 0.899902 4.9001 0.899902C7.49584 0.899902 9.6001 3.00416 9.6001 5.5999C9.6001 8.19564 7.49584 10.2999 4.9001 10.2999Z" fill="#C5C7C8"/>
<path d="M4.9001 10.2999C2.30436 10.2999 0.200098 8.19564 0.200098 5.5999C0.200098 3.00416 2.30436 0.899902 4.9001 0.899902C7.49584 0.899902 9.6001 3.00416 9.6001 5.5999C9.6001 8.19564 7.49584 10.2999 4.9001 10.2999Z" fill="white" fill-opacity="0.8"/>
<path d="M13.5469 12.7617L12.0537 14.2383L3.45312 5.53809L4.94727 4.06152L13.5469 12.7617Z" fill="#C5C7C8"/>
<path d="M13.5469 12.7617L12.0537 14.2383L3.45312 5.53809L4.94727 4.06152L13.5469 12.7617Z" fill="white" fill-opacity="0.8"/>
<path d="M42.9002 10.1998C45.4959 10.1998 47.6002 8.09554 47.6002 5.4998C47.6002 2.90406 45.4959 0.799805 42.9002 0.799805C40.3045 0.799805 38.2002 2.90406 38.2002 5.4998C38.2002 8.09554 40.3045 10.1998 42.9002 10.1998Z" fill="#C5C7C8"/>
<path d="M42.9002 10.1998C45.4959 10.1998 47.6002 8.09554 47.6002 5.4998C47.6002 2.90406 45.4959 0.799805 42.9002 0.799805C40.3045 0.799805 38.2002 2.90406 38.2002 5.4998C38.2002 8.09554 40.3045 10.1998 42.9002 10.1998Z" fill="white" fill-opacity="0.8"/>
<path d="M44.5425 5.54199L35.8423 14.2422L34.3579 12.7578L43.0581 4.05762L44.5425 5.54199Z" fill="#C5C7C8"/>
<path d="M44.5425 5.54199L35.8423 14.2422L34.3579 12.7578L43.0581 4.05762L44.5425 5.54199Z" fill="white" fill-opacity="0.8"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 33.1001C4.86619 32.5745 3.93245 31.6968 3.33782 30.5977C2.74319 29.4985 2.51948 28.2367 2.7 27.0001C4.7 15.1001 13.5 6.1001 24.1 6.1001C34.7 6.1001 43.5 15.1001 45.4 27.1001C45.9 29.6001 44.5 32.1001 42.2 33.1001C36.5448 35.8504 30.3385 37.2795 24.05 37.2795C17.7615 37.2795 11.5552 35.8504 5.9 33.1001H6Z" fill="#FFDF6F"/>
<path d="M45.4001 27.0001L44.4001 27.3001L45.4001 27.1001V27.0001ZM3.80015 27.2001C5.80015 15.6001 14.2002 7.20009 24.1002 7.20009V5.1001C12.9002 5.1001 3.70014 14.6001 1.70014 26.8001L3.70014 27.2001H3.80015ZM24.1002 7.20009C34.0002 7.20009 42.5001 15.7001 44.4001 27.2001L46.5001 26.9001C44.5001 14.6001 35.3002 5.1001 24.1002 5.1001V7.20009ZM41.7001 32.2001C36.4001 34.8001 30.5001 36.2001 24.2001 36.2001V38.4001C30.8001 38.4001 37.1001 36.9001 42.7001 34.2001L41.7001 32.2001ZM24.2001 36.2001C17.8001 36.2001 11.8001 34.8001 6.40014 32.2001L5.40014 34.1001C11.1001 36.8001 17.4001 38.4001 24.2001 38.4001V36.2001ZM44.4001 27.2001C44.7001 29.3001 43.6001 31.3001 41.7001 32.2001L42.7001 34.2001C44.0443 33.5658 45.1436 32.5086 45.8299 31.1902C46.5161 29.8719 46.7516 28.365 46.5001 26.9001L44.4001 27.3001V27.2001ZM1.70014 26.8001C1.20014 29.8001 2.70015 32.8001 5.50015 34.1001L6.50015 32.1001C4.50015 31.2001 3.50015 29.2001 3.80015 27.1001L1.70014 26.8001Z" fill="#C5C7C8"/>
<path d="M45.4001 27.0001L44.4001 27.3001L45.4001 27.1001V27.0001ZM3.80015 27.2001C5.80015 15.6001 14.2002 7.20009 24.1002 7.20009V5.1001C12.9002 5.1001 3.70014 14.6001 1.70014 26.8001L3.70014 27.2001H3.80015ZM24.1002 7.20009C34.0002 7.20009 42.5001 15.7001 44.4001 27.2001L46.5001 26.9001C44.5001 14.6001 35.3002 5.1001 24.1002 5.1001V7.20009ZM41.7001 32.2001C36.4001 34.8001 30.5001 36.2001 24.2001 36.2001V38.4001C30.8001 38.4001 37.1001 36.9001 42.7001 34.2001L41.7001 32.2001ZM24.2001 36.2001C17.8001 36.2001 11.8001 34.8001 6.40014 32.2001L5.40014 34.1001C11.1001 36.8001 17.4001 38.4001 24.2001 38.4001V36.2001ZM44.4001 27.2001C44.7001 29.3001 43.6001 31.3001 41.7001 32.2001L42.7001 34.2001C44.0443 33.5658 45.1436 32.5086 45.8299 31.1902C46.5161 29.8719 46.7516 28.365 46.5001 26.9001L44.4001 27.3001V27.2001ZM1.70014 26.8001C1.20014 29.8001 2.70015 32.8001 5.50015 34.1001L6.50015 32.1001C4.50015 31.2001 3.50015 29.2001 3.80015 27.1001L1.70014 26.8001Z" fill="white" fill-opacity="0.8"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5002 30.1998C8.60018 29.4998 7.50019 27.4998 8.10019 25.5998C10.2002 19.0998 16.6002 14.2998 24.1002 14.2998C31.6002 14.2998 38.0002 19.0998 40.1002 25.5998C40.6002 27.4998 39.5002 29.4998 37.7002 30.1998C33.3917 31.9986 28.7691 32.9248 24.1002 32.9248C19.4313 32.9248 14.8087 31.9986 10.5002 30.1998Z" fill="#C5C7C8"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5002 30.1998C8.60018 29.4998 7.50019 27.4998 8.10019 25.5998C10.2002 19.0998 16.6002 14.2998 24.1002 14.2998C31.6002 14.2998 38.0002 19.0998 40.1002 25.5998C40.6002 27.4998 39.5002 29.4998 37.7002 30.1998C33.3917 31.9986 28.7691 32.9248 24.1002 32.9248C19.4313 32.9248 14.8087 31.9986 10.5002 30.1998Z" fill="#272828"/>
<path d="M29.4 27.4002C31.5539 27.4002 33.3 26.0123 33.3 24.3002C33.3 22.5881 31.5539 21.2002 29.4 21.2002C27.2461 21.2002 25.5 22.5881 25.5 24.3002C25.5 26.0123 27.2461 27.4002 29.4 27.4002Z" fill="#F8C455"/>
<path d="M29.4 27.4002C31.5539 27.4002 33.3 26.0123 33.3 24.3002C33.3 22.5881 31.5539 21.2002 29.4 21.2002C27.2461 21.2002 25.5 22.5881 25.5 24.3002C25.5 26.0123 27.2461 27.4002 29.4 27.4002Z" fill="white"/>
<path d="M18.2999 27.4002C20.4538 27.4002 22.1999 26.0123 22.1999 24.3002C22.1999 22.5881 20.4538 21.2002 18.2999 21.2002C16.146 21.2002 14.3999 22.5881 14.3999 24.3002C14.3999 26.0123 16.146 27.4002 18.2999 27.4002Z" fill="#F8C455"/>
<path d="M18.2999 27.4002C20.4538 27.4002 22.1999 26.0123 22.1999 24.3002C22.1999 22.5881 20.4538 21.2002 18.2999 21.2002C16.146 21.2002 14.3999 22.5881 14.3999 24.3002C14.3999 26.0123 16.146 27.4002 18.2999 27.4002Z" fill="white"/>
<path d="M124.842 37.6424C124.095 37.6424 123.395 37.5826 122.741 37.4628C122.096 37.3522 121.561 37.2094 121.137 37.0343L122.464 32.6388C123.155 32.8507 123.777 32.9659 124.33 32.9843C124.892 33.0028 125.376 32.8737 125.782 32.5973C126.196 32.3209 126.533 31.8509 126.791 31.1874L127.136 30.289L119.52 8.44971H125.712L130.108 24.0413H130.329L134.766 8.44971H141L132.748 31.9753C132.352 33.1179 131.813 34.1132 131.131 34.9609C130.458 35.8179 129.606 36.4768 128.574 36.9375C127.542 37.4075 126.298 37.6424 124.842 37.6424Z" fill="white"/>
<path d="M97.2563 29.6812V1.37305H103.145V12.0162H103.324C103.582 11.4449 103.956 10.8644 104.444 10.2746C104.942 9.67565 105.587 9.17805 106.379 8.78181C107.181 8.37636 108.176 8.17363 109.365 8.17363C110.913 8.17363 112.341 8.57908 113.65 9.38999C114.958 10.1917 116.004 11.4034 116.787 13.0253C117.571 14.6379 117.962 16.6605 117.962 19.0933C117.962 21.4615 117.58 23.4611 116.815 25.0921C116.059 26.714 115.027 27.9442 113.719 28.7827C112.419 29.612 110.963 30.0267 109.351 30.0267C108.208 30.0267 107.236 29.8378 106.434 29.46C105.642 29.0822 104.992 28.6076 104.485 28.0363C103.979 27.4558 103.592 26.8706 103.324 26.2809H103.062V29.6812H97.2563ZM103.02 19.0656C103.02 20.3281 103.195 21.4292 103.546 22.3692C103.896 23.3091 104.402 24.0417 105.066 24.5669C105.729 25.0829 106.536 25.341 107.485 25.341C108.443 25.341 109.254 25.0783 109.918 24.5531C110.581 24.0186 111.083 23.2814 111.424 22.3415C111.774 21.3924 111.949 20.3004 111.949 19.0656C111.949 17.84 111.779 16.7619 111.438 15.8312C111.097 14.9005 110.595 14.1725 109.931 13.6473C109.268 13.122 108.452 12.8594 107.485 12.8594C106.527 12.8594 105.716 13.1128 105.052 13.6196C104.398 14.1264 103.896 14.8452 103.546 15.7759C103.195 16.7066 103.02 17.8032 103.02 19.0656Z" fill="white"/>
<path d="M92.4283 1.37305V29.6812H86.54V1.37305H92.4283Z" fill="white"/>
<path d="M62.4136 29.6812H56L65.7724 1.37305H73.4852L83.2438 29.6812H76.8302L69.7394 7.84189H69.5182L62.4136 29.6812ZM62.0127 18.5542H77.162V23.2261H62.0127V18.5542Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

24
public/wallets/alby.svg Normal file
View File

@ -0,0 +1,24 @@
<svg width="141" height="38" viewBox="0 0 141 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.6001 22H36.8001V31.1H13.6001V22Z" fill="white"/>
<path d="M4.9001 9.5001C2.30436 9.5001 0.200098 7.39584 0.200098 4.8001C0.200098 2.20436 2.30436 0.100098 4.9001 0.100098C7.49584 0.100098 9.6001 2.20436 9.6001 4.8001C9.6001 7.39584 7.49584 9.5001 4.9001 9.5001Z" fill="#C5C7C8"/>
<path d="M4.9001 9.5001C2.30436 9.5001 0.200098 7.39584 0.200098 4.8001C0.200098 2.20436 2.30436 0.100098 4.9001 0.100098C7.49584 0.100098 9.6001 2.20436 9.6001 4.8001C9.6001 7.39584 7.49584 9.5001 4.9001 9.5001Z" fill="black" fill-opacity="0.8"/>
<path d="M13.5469 11.9619L12.0537 13.4385L3.45312 4.73828L4.94727 3.26172L13.5469 11.9619Z" fill="#C5C7C8"/>
<path d="M13.5469 11.9619L12.0537 13.4385L3.45312 4.73828L4.94727 3.26172L13.5469 11.9619Z" fill="black" fill-opacity="0.8"/>
<path d="M42.9002 9.39999C45.4959 9.39999 47.6002 7.29574 47.6002 4.7C47.6002 2.10426 45.4959 0 42.9002 0C40.3045 0 38.2002 2.10426 38.2002 4.7C38.2002 7.29574 40.3045 9.39999 42.9002 9.39999Z" fill="#C5C7C8"/>
<path d="M42.9002 9.39999C45.4959 9.39999 47.6002 7.29574 47.6002 4.7C47.6002 2.10426 45.4959 0 42.9002 0C40.3045 0 38.2002 2.10426 38.2002 4.7C38.2002 7.29574 40.3045 9.39999 42.9002 9.39999Z" fill="black" fill-opacity="0.8"/>
<path d="M44.5425 4.74219L35.8423 13.4424L34.3579 11.958L43.0581 3.25781L44.5425 4.74219Z" fill="#C5C7C8"/>
<path d="M44.5425 4.74219L35.8423 13.4424L34.3579 11.958L43.0581 3.25781L44.5425 4.74219Z" fill="black" fill-opacity="0.8"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 32.3003C4.86619 31.7747 3.93245 30.897 3.33782 29.7979C2.74319 28.6987 2.51948 27.4369 2.7 26.2003C4.7 14.3003 13.5 5.30029 24.1 5.30029C34.7 5.30029 43.5 14.3003 45.4 26.3003C45.9 28.8003 44.5 31.3003 42.2 32.3003C36.5448 35.0506 30.3385 36.4797 24.05 36.4797C17.7615 36.4797 11.5552 35.0506 5.9 32.3003H6Z" fill="#FFDF6F"/>
<path d="M45.4001 26.2003L44.4001 26.5003L45.4001 26.3003V26.2003ZM3.80015 26.4003C5.80015 14.8003 14.2002 6.40028 24.1002 6.40028V4.30029C12.9002 4.30029 3.70014 13.8003 1.70014 26.0003L3.70014 26.4003H3.80015ZM24.1002 6.40028C34.0002 6.40028 42.5001 14.9003 44.4001 26.4003L46.5001 26.1003C44.5001 13.8003 35.3002 4.30029 24.1002 4.30029V6.40028ZM41.7001 31.4003C36.4001 34.0003 30.5001 35.4003 24.2001 35.4003V37.6003C30.8001 37.6003 37.1001 36.1003 42.7001 33.4003L41.7001 31.4003ZM24.2001 35.4003C17.8001 35.4003 11.8001 34.0003 6.40014 31.4003L5.40014 33.3003C11.1001 36.0003 17.4001 37.6003 24.2001 37.6003V35.4003ZM44.4001 26.4003C44.7001 28.5003 43.6001 30.5003 41.7001 31.4003L42.7001 33.4003C44.0443 32.766 45.1436 31.7088 45.8299 30.3904C46.5161 29.072 46.7516 27.5651 46.5001 26.1003L44.4001 26.5003V26.4003ZM1.70014 26.0003C1.20014 29.0003 2.70015 32.0003 5.50015 33.3003L6.50015 31.3003C4.50015 30.4003 3.50015 28.4003 3.80015 26.3003L1.70014 26.0003Z" fill="#C5C7C8"/>
<path d="M45.4001 26.2003L44.4001 26.5003L45.4001 26.3003V26.2003ZM3.80015 26.4003C5.80015 14.8003 14.2002 6.40028 24.1002 6.40028V4.30029C12.9002 4.30029 3.70014 13.8003 1.70014 26.0003L3.70014 26.4003H3.80015ZM24.1002 6.40028C34.0002 6.40028 42.5001 14.9003 44.4001 26.4003L46.5001 26.1003C44.5001 13.8003 35.3002 4.30029 24.1002 4.30029V6.40028ZM41.7001 31.4003C36.4001 34.0003 30.5001 35.4003 24.2001 35.4003V37.6003C30.8001 37.6003 37.1001 36.1003 42.7001 33.4003L41.7001 31.4003ZM24.2001 35.4003C17.8001 35.4003 11.8001 34.0003 6.40014 31.4003L5.40014 33.3003C11.1001 36.0003 17.4001 37.6003 24.2001 37.6003V35.4003ZM44.4001 26.4003C44.7001 28.5003 43.6001 30.5003 41.7001 31.4003L42.7001 33.4003C44.0443 32.766 45.1436 31.7088 45.8299 30.3904C46.5161 29.072 46.7516 27.5651 46.5001 26.1003L44.4001 26.5003V26.4003ZM1.70014 26.0003C1.20014 29.0003 2.70015 32.0003 5.50015 33.3003L6.50015 31.3003C4.50015 30.4003 3.50015 28.4003 3.80015 26.3003L1.70014 26.0003Z" fill="black" fill-opacity="0.8"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5002 29.4C8.60018 28.7 7.50019 26.7 8.10019 24.8C10.2002 18.3 16.6002 13.5 24.1002 13.5C31.6002 13.5 38.0002 18.3 40.1002 24.8C40.6002 26.7 39.5002 28.7 37.7002 29.4C33.3917 31.1988 28.7691 32.125 24.1002 32.125C19.4313 32.125 14.8087 31.1988 10.5002 29.4Z" fill="#C5C7C8"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5002 29.4C8.60018 28.7 7.50019 26.7 8.10019 24.8C10.2002 18.3 16.6002 13.5 24.1002 13.5C31.6002 13.5 38.0002 18.3 40.1002 24.8C40.6002 26.7 39.5002 28.7 37.7002 29.4C33.3917 31.1988 28.7691 32.125 24.1002 32.125C19.4313 32.125 14.8087 31.1988 10.5002 29.4Z" fill="black" fill-opacity="0.8"/>
<path d="M29.4 26.6004C31.5539 26.6004 33.3 25.2125 33.3 23.5004C33.3 21.7883 31.5539 20.4004 29.4 20.4004C27.2461 20.4004 25.5 21.7883 25.5 23.5004C25.5 25.2125 27.2461 26.6004 29.4 26.6004Z" fill="#F8C455"/>
<path d="M29.4 26.6004C31.5539 26.6004 33.3 25.2125 33.3 23.5004C33.3 21.7883 31.5539 20.4004 29.4 20.4004C27.2461 20.4004 25.5 21.7883 25.5 23.5004C25.5 25.2125 27.2461 26.6004 29.4 26.6004Z" fill="white"/>
<path d="M18.2999 26.6004C20.4538 26.6004 22.1999 25.2125 22.1999 23.5004C22.1999 21.7883 20.4538 20.4004 18.2999 20.4004C16.146 20.4004 14.3999 21.7883 14.3999 23.5004C14.3999 25.2125 16.146 26.6004 18.2999 26.6004Z" fill="#F8C455"/>
<path d="M18.2999 26.6004C20.4538 26.6004 22.1999 25.2125 22.1999 23.5004C22.1999 21.7883 20.4538 20.4004 18.2999 20.4004C16.146 20.4004 14.3999 21.7883 14.3999 23.5004C14.3999 25.2125 16.146 26.6004 18.2999 26.6004Z" fill="white"/>
<path d="M124.842 36.8426C124.095 36.8426 123.395 36.7827 122.741 36.663C122.096 36.5524 121.561 36.4095 121.137 36.2345L122.464 31.839C123.155 32.0509 123.777 32.1661 124.33 32.1845C124.892 32.203 125.376 32.0739 125.782 31.7975C126.196 31.5211 126.533 31.0511 126.791 30.3876L127.136 29.4892L119.52 7.6499H125.712L130.108 23.2415H130.329L134.766 7.6499H141L132.748 31.1755C132.352 32.3181 131.813 33.3133 131.131 34.1611C130.458 35.0181 129.606 35.677 128.574 36.1377C127.542 36.6077 126.298 36.8426 124.842 36.8426Z" fill="#272828"/>
<path d="M97.2563 28.8814V0.573242H103.145V11.2164H103.324C103.582 10.6451 103.956 10.0646 104.444 9.47482C104.942 8.87585 105.587 8.37825 106.379 7.98201C107.181 7.57655 108.176 7.37382 109.365 7.37382C110.913 7.37382 112.341 7.77928 113.65 8.59019C114.958 9.39188 116.004 10.6036 116.787 12.2255C117.571 13.8381 117.962 15.8607 117.962 18.2935C117.962 20.6617 117.58 22.6613 116.815 24.2923C116.059 25.9142 115.027 27.1444 113.719 27.9829C112.419 28.8122 110.963 29.2269 109.351 29.2269C108.208 29.2269 107.236 29.038 106.434 28.6602C105.642 28.2824 104.992 27.8078 104.485 27.2365C103.979 26.656 103.592 26.0708 103.324 25.4811H103.062V28.8814H97.2563ZM103.02 18.2658C103.02 19.5283 103.195 20.6294 103.546 21.5693C103.896 22.5093 104.402 23.2419 105.066 23.7671C105.729 24.2831 106.536 24.5411 107.485 24.5411C108.443 24.5411 109.254 24.2785 109.918 23.7533C110.581 23.2188 111.083 22.4816 111.424 21.5417C111.774 20.5926 111.949 19.5006 111.949 18.2658C111.949 17.0402 111.779 15.9621 111.438 15.0314C111.097 14.1007 110.595 13.3727 109.931 12.8475C109.268 12.3222 108.452 12.0596 107.485 12.0596C106.527 12.0596 105.716 12.313 105.052 12.8198C104.398 13.3266 103.896 14.0454 103.546 14.9761C103.195 15.9068 103.02 17.0034 103.02 18.2658Z" fill="#272828"/>
<path d="M92.4283 0.573242V28.8814H86.54V0.573242H92.4283Z" fill="#272828"/>
<path d="M62.4136 28.8814H56L65.7724 0.573242H73.4852L83.2438 28.8814H76.8302L69.7394 7.04209H69.5182L62.4136 28.8814ZM62.0127 17.7544H77.162V22.4263H62.0127V17.7544Z" fill="#272828"/>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

20
public/wallets/blixt.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
public/wallets/cashapp.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
public/wallets/cashu.me.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="224" height="72" fill="none"><path d="M69.882 36c0 18.713-15.17 33.882-33.882 33.882-18.713 0-33.882-15.17-33.882-33.882C2.118 17.287 17.288 2.118 36 2.118c18.713 0 33.882 15.17 33.882 33.882Z"/><path fill="currentColor" fill-rule="evenodd" d="M36 4.235C18.457 4.235 4.235 18.457 4.235 36S18.457 67.765 36 67.765 67.765 53.543 67.765 36 53.543 4.235 36 4.235ZM0 36C0 16.118 16.118 0 36 0s36 16.118 36 36-16.118 36-36 36S0 55.882 0 36Z" clip-rule="evenodd"/><path fill="currentColor" fill-rule="evenodd" d="M36 58.588c12.475 0 22.588-10.113 22.588-22.588 0-12.475-10.113-22.588-22.588-22.588-12.475 0-22.588 10.113-22.588 22.588 0 12.475 10.113 22.588 22.588 22.588ZM36 54c9.941 0 18-8.059 18-18s-8.059-18-18-18-18 8.059-18 18 8.059 18 18 18Z" clip-rule="evenodd"/><path fill="currentColor" fill-rule="evenodd" d="M36 22.899c-7.236 0-13.101 5.865-13.101 13.101 0 7.236 5.865 13.101 13.101 13.101V22.9Z" clip-rule="evenodd"/><path fill="currentColor" d="M97.608 27.704c1.936 0 3.64.384 5.112 1.152 1.472.768 2.648 1.832 3.528 3.192.896 1.36 1.432 2.904 1.608 4.632h-4.992a5.382 5.382 0 0 0-.816-2.136 4.842 4.842 0 0 0-1.752-1.656c-.736-.432-1.624-.648-2.664-.648-1.856 0-3.368.688-4.536 2.064-1.152 1.376-1.728 3.488-1.728 6.336 0 2.624.552 4.712 1.656 6.264 1.104 1.552 2.68 2.328 4.728 2.328 1.024 0 1.896-.224 2.616-.672.736-.464 1.312-1.032 1.728-1.704.432-.688.712-1.376.84-2.064h4.848c-.144 1.68-.672 3.176-1.584 4.488-.896 1.312-2.08 2.344-3.552 3.096-1.456.736-3.136 1.104-5.04 1.104-2.272 0-4.296-.496-6.072-1.488-1.76-1.008-3.144-2.464-4.152-4.368-1.008-1.92-1.512-4.232-1.512-6.936 0-2.576.472-4.832 1.416-6.768.944-1.952 2.296-3.472 4.056-4.56 1.76-1.104 3.848-1.656 6.264-1.656ZM122.09 53.48c-2.368 0-4.432-.504-6.192-1.512-1.76-1.024-3.128-2.488-4.104-4.392-.96-1.904-1.44-4.192-1.44-6.864 0-2.592.464-4.864 1.392-6.816.944-1.952 2.296-3.472 4.056-4.56 1.76-1.088 3.864-1.632 6.312-1.632 2.368 0 4.424.52 6.168 1.56 1.744 1.04 3.088 2.528 4.032 4.464.96 1.936 1.44 4.264 1.44 6.984 0 2.512-.456 4.728-1.368 6.648-.896 1.904-2.216 3.4-3.96 4.488-1.728 1.088-3.84 1.632-6.336 1.632Zm.024-4.44c1.344 0 2.448-.368 3.312-1.104.88-.736 1.528-1.744 1.944-3.024.432-1.28.648-2.736.648-4.368 0-1.52-.192-2.92-.576-4.2-.368-1.28-.992-2.304-1.872-3.072-.864-.784-2.016-1.176-3.456-1.176-1.36 0-2.488.36-3.384 1.08-.88.704-1.544 1.696-1.992 2.976-.432 1.264-.648 2.728-.648 4.392 0 1.504.192 2.904.576 4.2.4 1.28 1.04 2.32 1.92 3.12.88.784 2.056 1.176 3.528 1.176Zm21.404-20.856V53h-5.616V28.184h5.616Zm.072-9.864v5.52h-5.784v-5.52h5.784ZM149.367 53V28.184h5.712v3.552c.384-.64.904-1.256 1.56-1.848.672-.592 1.496-1.072 2.472-1.44.976-.368 2.12-.552 3.432-.552 1.536 0 2.96.304 4.272.912 1.328.608 2.392 1.56 3.192 2.856.816 1.296 1.224 2.968 1.224 5.016V53h-5.856V37.376c0-1.632-.448-2.84-1.344-3.624-.896-.8-2.024-1.2-3.384-1.2a7.29 7.29 0 0 0-2.64.48c-.832.304-1.512.776-2.04 1.416-.512.624-.768 1.408-.768 2.352V53h-5.832Zm36.981.48c-2.368 0-4.432-.504-6.192-1.512-1.76-1.024-3.128-2.488-4.104-4.392-.96-1.904-1.44-4.192-1.44-6.864 0-2.592.464-4.864 1.392-6.816.944-1.952 2.296-3.472 4.056-4.56 1.76-1.088 3.864-1.632 6.312-1.632 2.368 0 4.424.52 6.168 1.56 1.744 1.04 3.088 2.528 4.032 4.464.96 1.936 1.44 4.264 1.44 6.984 0 2.512-.456 4.728-1.368 6.648-.896 1.904-2.216 3.4-3.96 4.488-1.728 1.088-3.84 1.632-6.336 1.632Zm.024-4.44c1.344 0 2.448-.368 3.312-1.104.88-.736 1.528-1.744 1.944-3.024.432-1.28.648-2.736.648-4.368 0-1.52-.192-2.92-.576-4.2-.368-1.28-.992-2.304-1.872-3.072-.864-.784-2.016-1.176-3.456-1.176-1.36 0-2.488.36-3.384 1.08-.88.704-1.544 1.696-1.992 2.976-.432 1.264-.648 2.728-.648 4.392 0 1.504.192 2.904.576 4.2.4 1.28 1.04 2.32 1.92 3.12.88.784 2.056 1.176 3.528 1.176Zm24.623 4.44c-1.728 0-3.368-.272-4.92-.816-1.536-.56-2.832-1.432-3.888-2.616-1.04-1.184-1.696-2.712-1.968-4.584h5.136c.224.912.616 1.656 1.176 2.232.576.576 1.256 1 2.04 1.272a7.685 7.685 0 0 0 2.4.384c1.488 0 2.688-.256 3.6-.768.928-.512 1.392-1.304 1.392-2.376 0-.784-.256-1.408-.768-1.872-.512-.464-1.328-.816-2.448-1.056l-4.728-1.08c-2.096-.464-3.784-1.232-5.064-2.304-1.264-1.072-1.904-2.584-1.92-4.536-.016-1.472.352-2.784 1.104-3.936.752-1.152 1.872-2.056 3.36-2.712 1.488-.672 3.328-1.008 5.52-1.008 2.896 0 5.216.648 6.96 1.944 1.744 1.28 2.64 3.112 2.688 5.496h-4.968c-.176-1.072-.68-1.904-1.512-2.496-.832-.608-1.912-.912-3.24-.912-1.376 0-2.504.264-3.384.792-.88.528-1.32 1.336-1.32 2.424 0 .752.336 1.344 1.008 1.776.672.432 1.68.8 3.024 1.104l4.464 1.08c1.28.32 2.328.752 3.144 1.296.816.544 1.456 1.144 1.92 1.8.464.64.784 1.304.96 1.992.192.672.288 1.296.288 1.872 0 1.6-.416 2.968-1.248 4.104-.816 1.12-1.976 1.984-3.48 2.592-1.504.608-3.28.912-5.328.912Z"/></svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/wallets/fountain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/wallets/lifpay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/wallets/lnaddr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
public/wallets/minibits.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

BIN
public/wallets/nwc-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
public/wallets/nwc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,25 @@
<svg width="124" height="36" viewBox="0 0 124 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.8684 35.583C20.6223 35.8559 19.3279 35.9997 17.9999 35.9997C14.3549 35.9997 10.963 34.9163 8.12856 33.054C7.42496 32.0476 7.10927 31.4935 6.87899 31.0894C6.76489 30.8892 6.67176 30.7257 6.5628 30.5623C5.48908 28.8355 4.9273 26.6174 4.80479 24.0113C4.42532 15.9363 9.3328 10.7798 14.3461 9.93346C17.5241 9.39695 20.0496 9.94028 21.9866 10.9762C20.2741 10.4998 18.2326 10.4712 15.9053 11.1374C10.2636 12.9558 8.38286 18.4852 9.18896 24.6085C10.5958 32.2811 18.1187 35.0311 21.8684 35.583Z" fill="url(#paint0_linear_24_382)"/>
<path d="M5.79919 31.235C4.74978 29.3716 3.79946 26.5912 3.68071 24.0649C3.27786 15.4924 8.51664 9.77741 14.1585 8.82493C21.8491 7.52657 26.1246 12.0794 27.5985 15.4587C27.6602 15.4171 27.6852 15.3348 27.6527 15.264C25.2293 9.99435 20.2169 6.37564 14.4271 6.37564C7.85277 6.37564 1.98661 11.0916 0 18.1005C0.028214 23.2941 2.25599 27.9669 5.79919 31.235Z" fill="url(#paint1_linear_24_382)"/>
<path d="M28.1246 32.8843C26.8263 33.7692 25.4062 34.4887 23.8946 35.0124C23.1688 34.886 22.3647 34.7286 21.7985 34.6178C21.5302 34.5653 21.3152 34.5232 21.1875 34.4998C17.7635 33.8744 11.6415 31.6681 10.3003 24.4334C9.9214 21.5244 10.1928 18.8466 11.1539 16.7225C12.1007 14.6299 13.7348 13.0257 16.2268 12.2158C19.1232 11.4267 21.5234 11.8178 23.3063 12.7753C22.8674 12.6828 22.4138 12.6343 21.9496 12.6343C18.0919 12.6343 14.9645 15.9849 14.9645 20.1182C14.9645 21.7686 15.4632 23.2943 16.3078 24.5316C16.3078 24.5316 18.7257 29.0989 25.3078 28.6907C31.1767 28.3267 34.2244 23.0624 34.5876 21.1179C34.7764 20.1074 34.8751 19.0652 34.8751 17.9999C34.8751 8.68016 27.32 1.12499 18.0002 1.12499C10.9479 1.12499 4.90605 5.45106 2.38354 11.5942C1.53151 12.7042 0.797846 13.9347 0.206665 15.2653C1.52404 6.62202 8.98879 0 18.0002 0C27.9413 0 36.0001 8.05884 36.0001 17.9999C36.0001 24.1869 32.8786 29.6444 28.1246 32.8843Z" fill="url(#paint2_linear_24_382)"/>
<path d="M46 32H50.3419V24.8894C50.7308 25.396 51.2555 25.7985 51.9158 26.0971C52.5852 26.3866 53.3721 26.5313 54.2767 26.5313C55.9592 26.5313 57.325 25.9207 58.3743 24.6994C59.4236 23.469 59.9482 21.8225 59.9482 19.7599V19.3257C59.9482 17.2359 59.4236 15.5939 58.3743 14.3998C57.3341 13.2056 55.9546 12.6086 54.236 12.6086C53.3405 12.6086 52.5354 12.7895 51.8208 13.1514C51.1062 13.5042 50.5906 13.9475 50.274 14.4812H50.2198L50.179 12.9207H46V32ZM50.3419 21.6868V17.453C50.5409 17.0007 50.8529 16.6026 51.2781 16.2589C51.7123 15.9151 52.2505 15.7432 52.8927 15.7432C53.7792 15.7432 54.4395 16.0463 54.8737 16.6524C55.3079 17.2495 55.525 18.0863 55.525 19.1628V19.9499C55.525 21.0174 55.3079 21.8678 54.8737 22.501C54.4486 23.1253 53.7882 23.4374 52.8927 23.4374C52.2776 23.4374 51.7439 23.2655 51.2917 22.9217C50.8484 22.5689 50.5318 22.1573 50.3419 21.6868Z" fill="#DDDDDD"/>
<path d="M62.1192 26.2599H66.5425V18.0637C66.8319 17.4666 67.2435 16.9826 67.7772 16.6117C68.3109 16.2408 68.8807 16.0553 69.4868 16.0553C69.7491 16.0553 69.9843 16.0689 70.1923 16.096C70.4094 16.1232 70.5994 16.1639 70.7622 16.2182V12.785C70.6718 12.7488 70.518 12.7126 70.3009 12.6764C70.0838 12.6402 69.8577 12.6221 69.6225 12.6221C68.9441 12.6221 68.3335 12.8076 67.7907 13.1785C67.248 13.5494 66.8183 14.0153 66.5018 14.5762H66.461L66.4068 12.9207H62.1192V26.2599Z" fill="#DDDDDD"/>
<path d="M72.4718 26.2599H76.8815V12.9207H72.4718V26.2599ZM72.1869 8.72756C72.1869 9.36082 72.3995 9.8991 72.8246 10.3424C73.2588 10.7857 73.8739 11.0073 74.6699 11.0073C75.4659 11.0073 76.081 10.7857 76.5152 10.3424C76.9494 9.8991 77.1665 9.36082 77.1665 8.72756C77.1665 8.08525 76.9539 7.54697 76.5288 7.11273C76.1036 6.6785 75.4885 6.46138 74.6835 6.46138C73.8875 6.46138 73.2724 6.6785 72.8382 7.11273C72.404 7.54697 72.1869 8.08525 72.1869 8.72756Z" fill="#DDDDDD"/>
<path d="M79.7852 26.2599H84.1406V17.6023C84.3667 17.0685 84.6743 16.6479 85.0632 16.3403C85.4522 16.0327 85.909 15.8789 86.4336 15.8789C87.0397 15.8789 87.501 16.0508 87.8176 16.3946C88.1432 16.7383 88.3061 17.3038 88.3061 18.0908V26.2599H92.6751V17.5887C92.9012 17.0731 93.2088 16.6614 93.5977 16.3539C93.9957 16.0372 94.457 15.8789 94.9817 15.8789C95.5877 15.888 96.0491 16.0644 96.3657 16.4081C96.6823 16.7429 96.8406 17.2947 96.8406 18.0637V26.2599H101.196V17.7651C101.196 15.9106 100.789 14.5898 99.9748 13.8027C99.1698 13.0066 98.0979 12.6086 96.7591 12.6086C95.8003 12.6086 94.9093 12.8121 94.0862 13.2192C93.2721 13.6263 92.6253 14.2098 92.1459 14.9697H92.1052C91.8157 14.2008 91.3228 13.6173 90.6263 13.2192C89.9388 12.8121 89.1156 12.6086 88.1568 12.6086C87.2794 12.6086 86.4879 12.7895 85.7824 13.1514C85.0859 13.5132 84.5205 14.0153 84.0863 14.6576H84.032L83.9913 12.9207H79.7852V26.2599Z" fill="#DDDDDD"/>
<path d="M103.299 22.3789C103.299 23.7359 103.724 24.7582 104.575 25.4457C105.434 26.1333 106.551 26.477 107.926 26.477C109.084 26.477 110.043 26.2735 110.802 25.8664C111.571 25.4593 112.105 25.0024 112.403 24.4958C112.431 24.8215 112.485 25.1427 112.566 25.4593C112.657 25.7759 112.761 26.0428 112.878 26.2599H117.166V26.0564C116.985 25.7035 116.845 25.2829 116.745 24.7944C116.655 24.2968 116.61 23.7766 116.61 23.2338V17.263C116.61 15.6256 116.089 14.4315 115.049 13.6806C114.018 12.9297 112.517 12.5543 110.545 12.5543C108.591 12.5543 107.026 12.9478 105.85 13.7349C104.683 14.5129 104.1 15.5216 104.1 16.761V17.0324L108.211 17.0459V16.8017C108.211 16.3222 108.378 15.9377 108.713 15.6482C109.048 15.3587 109.572 15.214 110.287 15.214C111.001 15.214 111.508 15.3859 111.806 15.7296C112.105 16.0734 112.254 16.5438 112.254 17.1409V17.9415H109.269C107.505 17.9506 106.067 18.3396 104.954 19.1086C103.851 19.8775 103.299 20.9676 103.299 22.3789ZM107.627 22.0397C107.627 21.4426 107.844 21.0038 108.279 20.7234C108.722 20.4429 109.405 20.3027 110.327 20.3027H112.254V21.4426C112.254 22.0849 111.992 22.6503 111.467 23.1388C110.952 23.6183 110.332 23.858 109.608 23.858C108.939 23.858 108.441 23.6997 108.116 23.3831C107.79 23.0574 107.627 22.6096 107.627 22.0397Z" fill="#DDDDDD"/>
<path d="M119.527 26.2599H123.909V6H119.527V26.2599Z" fill="#DDDDDD"/>
<defs>
<linearGradient id="paint0_linear_24_382" x1="11.2413" y1="14.9124" x2="11.2138" y2="30.9099" gradientUnits="userSpaceOnUse">
<stop offset="0.0297309" stop-color="#FA3C3C"/>
<stop offset="1" stop-color="#BC1870"/>
</linearGradient>
<linearGradient id="paint1_linear_24_382" x1="8.79045" y1="7.31564" x2="7.89914" y2="23.3177" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF9F2F"/>
<stop offset="1" stop-color="#FA3C3C"/>
</linearGradient>
<linearGradient id="paint2_linear_24_382" x1="21.375" y1="35.6248" x2="21.4244" y2="17.0626" gradientUnits="userSpaceOnUse">
<stop stop-color="#5B09AD"/>
<stop offset="1" stop-color="#BC1870"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

25
public/wallets/primal.svg Normal file
View File

@ -0,0 +1,25 @@
<svg width="124" height="36" viewBox="0 0 124 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.8683 35.583C20.6223 35.8559 19.3279 35.9997 17.9999 35.9997C14.3549 35.9997 10.963 34.9163 8.12855 33.054C7.42494 32.0476 7.10926 31.4935 6.87898 31.0894C6.76488 30.8892 6.67175 30.7257 6.56278 30.5623C5.48906 28.8355 4.92728 26.6174 4.80478 24.0113C4.4253 15.9363 9.33278 10.7798 14.3461 9.93346C17.524 9.39695 20.0496 9.94028 21.9866 10.9762C20.2741 10.4998 18.2326 10.4712 15.9053 11.1374C10.2636 12.9558 8.38284 18.4852 9.18894 24.6085C10.5957 32.2811 18.1187 35.0311 21.8683 35.583Z" fill="url(#paint0_linear_15_32)"/>
<path d="M5.79919 31.235C4.74978 29.3716 3.79946 26.5912 3.68071 24.0649C3.27786 15.4924 8.51664 9.7774 14.1585 8.82492C21.8491 7.52656 26.1246 12.0794 27.5985 15.4586C27.6602 15.4171 27.6852 15.3348 27.6527 15.264C25.2293 9.99435 20.2169 6.37563 14.4271 6.37563C7.85277 6.37563 1.98661 11.0916 0 18.1004C0.028214 23.2941 2.25599 27.9669 5.79919 31.235Z" fill="url(#paint1_linear_15_32)"/>
<path d="M28.1246 32.8843C26.8263 33.7692 25.4062 34.4887 23.8946 35.0124C23.1688 34.886 22.3647 34.7286 21.7985 34.6178C21.5302 34.5653 21.3152 34.5232 21.1875 34.4998C17.7635 33.8744 11.6415 31.6681 10.3003 24.4334C9.9214 21.5244 10.1928 18.8466 11.1539 16.7225C12.1007 14.6299 13.7348 13.0257 16.2268 12.2158C19.1232 11.4267 21.5234 11.8178 23.3063 12.7753C22.8674 12.6828 22.4138 12.6343 21.9496 12.6343C18.0919 12.6343 14.9645 15.9849 14.9645 20.1182C14.9645 21.7686 15.4632 23.2943 16.3078 24.5316C16.3078 24.5316 18.7257 29.0989 25.3078 28.6907C31.1767 28.3267 34.2244 23.0624 34.5876 21.1179C34.7764 20.1074 34.8751 19.0652 34.8751 17.9999C34.8751 8.68016 27.32 1.12499 18.0002 1.12499C10.9479 1.12499 4.90605 5.45106 2.38354 11.5942C1.53151 12.7042 0.797846 13.9347 0.206665 15.2653C1.52404 6.62202 8.98879 0 18.0002 0C27.9413 0 36.0001 8.05884 36.0001 17.9999C36.0001 24.1869 32.8786 29.6444 28.1246 32.8843Z" fill="url(#paint2_linear_15_32)"/>
<path d="M46 32H50.3419V24.8894C50.7308 25.396 51.2555 25.7985 51.9158 26.0971C52.5852 26.3866 53.3721 26.5313 54.2767 26.5313C55.9592 26.5313 57.325 25.9207 58.3743 24.6994C59.4236 23.469 59.9482 21.8225 59.9482 19.7599V19.3257C59.9482 17.2359 59.4236 15.5939 58.3743 14.3998C57.3341 13.2056 55.9546 12.6086 54.236 12.6086C53.3405 12.6086 52.5354 12.7895 51.8208 13.1514C51.1062 13.5042 50.5906 13.9475 50.274 14.4812H50.2198L50.179 12.9207H46V32ZM50.3419 21.6868V17.453C50.5409 17.0007 50.8529 16.6026 51.2781 16.2589C51.7123 15.9151 52.2505 15.7432 52.8927 15.7432C53.7792 15.7432 54.4395 16.0463 54.8737 16.6524C55.3079 17.2495 55.525 18.0863 55.525 19.1628V19.9499C55.525 21.0174 55.3079 21.8678 54.8737 22.501C54.4486 23.1253 53.7882 23.4374 52.8927 23.4374C52.2776 23.4374 51.7439 23.2655 51.2917 22.9217C50.8484 22.5689 50.5318 22.1573 50.3419 21.6868Z" fill="#333333"/>
<path d="M62.1192 26.2599H66.5425V18.0637C66.8319 17.4666 67.2435 16.9826 67.7772 16.6117C68.3109 16.2408 68.8807 16.0553 69.4868 16.0553C69.7491 16.0553 69.9843 16.0689 70.1923 16.096C70.4094 16.1232 70.5994 16.1639 70.7622 16.2182V12.785C70.6718 12.7488 70.518 12.7126 70.3009 12.6764C70.0838 12.6402 69.8577 12.6221 69.6225 12.6221C68.9441 12.6221 68.3335 12.8076 67.7907 13.1785C67.248 13.5494 66.8183 14.0153 66.5018 14.5762H66.461L66.4068 12.9207H62.1192V26.2599Z" fill="#333333"/>
<path d="M72.4718 26.2599H76.8815V12.9207H72.4718V26.2599ZM72.1869 8.72756C72.1869 9.36082 72.3995 9.8991 72.8246 10.3424C73.2588 10.7857 73.8739 11.0073 74.6699 11.0073C75.4659 11.0073 76.081 10.7857 76.5152 10.3424C76.9494 9.8991 77.1665 9.36082 77.1665 8.72756C77.1665 8.08525 76.9539 7.54697 76.5288 7.11273C76.1036 6.6785 75.4885 6.46138 74.6835 6.46138C73.8875 6.46138 73.2724 6.6785 72.8382 7.11273C72.404 7.54697 72.1869 8.08525 72.1869 8.72756Z" fill="#333333"/>
<path d="M79.7852 26.2599H84.1406V17.6023C84.3667 17.0685 84.6743 16.6479 85.0632 16.3403C85.4522 16.0327 85.909 15.8789 86.4336 15.8789C87.0397 15.8789 87.501 16.0508 87.8176 16.3946C88.1432 16.7383 88.3061 17.3038 88.3061 18.0908V26.2599H92.6751V17.5887C92.9012 17.0731 93.2088 16.6614 93.5977 16.3539C93.9957 16.0372 94.457 15.8789 94.9817 15.8789C95.5877 15.888 96.0491 16.0644 96.3657 16.4081C96.6823 16.7429 96.8406 17.2947 96.8406 18.0637V26.2599H101.196V17.7651C101.196 15.9106 100.789 14.5898 99.9748 13.8027C99.1698 13.0066 98.0979 12.6086 96.7591 12.6086C95.8003 12.6086 94.9093 12.8121 94.0862 13.2192C93.2721 13.6263 92.6253 14.2098 92.1459 14.9697H92.1052C91.8157 14.2008 91.3228 13.6173 90.6263 13.2192C89.9388 12.8121 89.1156 12.6086 88.1568 12.6086C87.2794 12.6086 86.4879 12.7895 85.7824 13.1514C85.0858 13.5132 84.5205 14.0153 84.0863 14.6576H84.032L83.9913 12.9207H79.7852V26.2599Z" fill="#333333"/>
<path d="M103.299 22.3789C103.299 23.7359 103.724 24.7582 104.575 25.4457C105.434 26.1333 106.551 26.477 107.926 26.477C109.084 26.477 110.043 26.2735 110.802 25.8664C111.571 25.4593 112.105 25.0024 112.403 24.4958C112.431 24.8215 112.485 25.1427 112.566 25.4593C112.657 25.7759 112.761 26.0428 112.878 26.2599H117.166V26.0564C116.985 25.7035 116.845 25.2829 116.745 24.7944C116.655 24.2968 116.61 23.7766 116.61 23.2338V17.263C116.61 15.6256 116.09 14.4315 115.049 13.6806C114.018 12.9297 112.517 12.5543 110.545 12.5543C108.591 12.5543 107.026 12.9478 105.85 13.7349C104.683 14.5129 104.1 15.5216 104.1 16.761V17.0324L108.211 17.0459V16.8017C108.211 16.3222 108.378 15.9377 108.713 15.6482C109.048 15.3587 109.572 15.214 110.287 15.214C111.001 15.214 111.508 15.3859 111.806 15.7296C112.105 16.0734 112.254 16.5438 112.254 17.1409V17.9415H109.269C107.505 17.9506 106.067 18.3396 104.954 19.1086C103.851 19.8775 103.299 20.9676 103.299 22.3789ZM107.627 22.0397C107.627 21.4426 107.844 21.0038 108.279 20.7234C108.722 20.4429 109.405 20.3027 110.327 20.3027H112.254V21.4426C112.254 22.0849 111.992 22.6503 111.467 23.1388C110.952 23.6183 110.332 23.858 109.608 23.858C108.939 23.858 108.441 23.6997 108.116 23.3831C107.79 23.0574 107.627 22.6096 107.627 22.0397Z" fill="#333333"/>
<path d="M119.527 26.2599H123.909V6H119.527V26.2599Z" fill="#333333"/>
<defs>
<linearGradient id="paint0_linear_15_32" x1="11.2413" y1="14.9124" x2="11.2138" y2="30.9099" gradientUnits="userSpaceOnUse">
<stop offset="0.0297309" stop-color="#FA3C3C"/>
<stop offset="1" stop-color="#BC1870"/>
</linearGradient>
<linearGradient id="paint1_linear_15_32" x1="8.79045" y1="7.31563" x2="7.89914" y2="23.3177" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF9F2F"/>
<stop offset="1" stop-color="#FA3C3C"/>
</linearGradient>
<linearGradient id="paint2_linear_15_32" x1="21.375" y1="35.6248" x2="21.4244" y2="17.0626" gradientUnits="userSpaceOnUse">
<stop stop-color="#5B09AD"/>
<stop offset="1" stop-color="#BC1870"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
public/wallets/rizful.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

9
public/wallets/speed.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/wallets/strike.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,18 @@
<svg width="1000" height="154" viewBox="0 0 1000 154" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4233_1168)">
<path d="M95.796 149.917L156.343 3.77344H108.029L78.1713 82.4814L48.3122 3.77344H0L60.5466 149.917H95.796Z" fill="white"/>
<path d="M420.31 109.415H364.325V3.77344H320.781V149.917H420.31V109.415Z" fill="white"/>
<path d="M443.741 149.917H487.285V43.4413H525.438V3.77344H405.588V43.4413H443.741V149.917Z" fill="white"/>
<path d="M568.776 3.77344L508.644 149.918H554.676L562.348 130.918H611.282L618.954 149.918H664.986L604.854 3.77344H568.776ZM573.96 96.6779L586.816 63.2747L599.671 96.6779H573.96Z" fill="white"/>
<path d="M743.786 69.3297L764.313 96.6779C758.715 106.492 748.97 112.337 736.321 112.337C714.963 112.337 702.73 96.2608 702.73 76.8445C702.73 57.4282 714.963 41.3538 736.321 41.3538C746.481 41.3538 754.567 44.6936 760.581 51.1657L792.927 22.5621C778.827 8.36559 758.922 0.0145304 736.321 0.0145304C691.326 -0.82068 657.735 34.4625 658.357 76.8445C657.735 119.226 691.326 154.51 736.321 153.674C780.072 154.51 814.077 119.226 813.248 76.8445C813.248 74.5485 813.041 72.0439 812.834 69.3297H743.786Z" fill="white"/>
<path d="M928.539 44.2755V3.77344H825.692V149.917H928.539V109.415H869.236V93.7552H921.904V59.9338H869.236V44.2755H928.539Z" fill="white"/>
<path d="M305.639 76.8443C306.11 44.7153 286.91 16.6821 257.995 5.35509L245.593 45.3451H264.427L223.617 119.581V63.5863L199.847 63.5842L208.392 1.97046C173.431 9.87628 149.182 40.7822 149.712 76.8443C149.089 119.226 182.681 154.51 227.676 153.674C272.67 154.51 306.26 119.226 305.639 76.8443Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M976.97 107.746C966.834 107.746 958.617 115.963 958.617 126.099C958.617 136.236 966.834 144.453 976.97 144.453C987.107 144.453 995.324 136.236 995.324 126.099C995.324 115.963 987.107 107.746 976.97 107.746ZM953.94 126.099C953.94 113.38 964.251 103.07 976.97 103.07C989.689 103.07 1000 113.38 1000 126.099C1000 138.818 989.689 149.129 976.97 149.129C964.251 149.129 953.94 138.818 953.94 126.099Z" fill="white"/>
<path d="M975.707 124.23C976.993 124.23 977.929 123.972 978.49 123.481C979.051 123.014 979.355 122.219 979.355 121.097C979.355 119.998 979.051 119.226 978.49 118.735C977.929 118.268 976.993 118.034 975.707 118.034H973.089V124.23H975.707ZM973.089 128.508V137.627H966.87V113.521H976.362C979.542 113.521 981.856 114.059 983.329 115.135C984.826 116.187 985.55 117.87 985.55 120.185C985.55 121.775 985.176 123.084 984.405 124.113C983.633 125.118 982.488 125.89 980.921 126.357C981.786 126.568 982.534 127.012 983.212 127.69C983.891 128.368 984.569 129.397 985.247 130.8L988.637 137.627H982.02L979.074 131.641C978.49 130.449 977.882 129.607 977.274 129.186C976.666 128.742 975.848 128.508 974.842 128.508H973.089Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_4233_1168">
<rect width="1000" height="154" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,11 @@
<svg width="1000" height="154" viewBox="0 0 1000 154" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M95.796 149.917L156.343 3.77348H108.029L78.1713 82.4814L48.3122 3.77348H0L60.5466 149.917H95.796Z" fill="#131626"/>
<path d="M420.31 109.415H364.325V3.77348H320.781V149.917H420.31V109.415Z" fill="#131626"/>
<path d="M443.741 149.917H487.285V43.4413H525.438V3.77348H405.588V43.4413H443.741V149.917Z" fill="#131626"/>
<path d="M568.776 3.77348L508.644 149.918H554.676L562.348 130.918H611.282L618.954 149.918H664.986L604.854 3.77348H568.776ZM573.96 96.6779L586.816 63.2747L599.671 96.6779H573.96Z" fill="#131626"/>
<path d="M743.786 69.3297L764.313 96.6779C758.715 106.492 748.97 112.337 736.321 112.337C714.963 112.337 702.73 96.2608 702.73 76.8445C702.73 57.4282 714.963 41.3538 736.321 41.3538C746.481 41.3538 754.567 44.6936 760.581 51.1657L792.927 22.5621C778.827 8.36559 758.922 0.0145304 736.321 0.0145304C691.326 -0.82068 657.735 34.4625 658.357 76.8445C657.735 119.226 691.326 154.51 736.321 153.674C780.072 154.51 814.077 119.226 813.248 76.8445C813.248 74.5485 813.041 72.0439 812.834 69.3297H743.786Z" fill="#131626"/>
<path d="M928.539 44.2755V3.77348H825.692V149.917H928.539V109.415H869.236V93.7552H921.904V59.9338H869.236V44.2755H928.539Z" fill="#131626"/>
<path d="M305.639 76.8444C306.11 44.7154 286.91 16.6822 257.995 5.35521L245.593 45.3452H264.427L223.617 119.581V63.5864L199.847 63.5843L208.392 1.97058C173.431 9.8764 149.182 40.7823 149.712 76.8444C149.089 119.226 182.681 154.51 227.676 153.674C272.67 154.51 306.26 119.226 305.639 76.8444Z" fill="#131626"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M976.97 107.746C966.834 107.746 958.617 115.963 958.617 126.099C958.617 136.236 966.834 144.453 976.97 144.453C987.107 144.453 995.324 136.236 995.324 126.099C995.324 115.963 987.107 107.746 976.97 107.746ZM953.94 126.099C953.94 113.38 964.251 103.07 976.97 103.07C989.689 103.07 1000 113.38 1000 126.099C1000 138.818 989.689 149.129 976.97 149.129C964.251 149.129 953.94 138.818 953.94 126.099Z" fill="#131626"/>
<path d="M975.707 124.23C976.993 124.23 977.929 123.972 978.49 123.481C979.051 123.014 979.355 122.219 979.355 121.097C979.355 119.998 979.051 119.226 978.49 118.735C977.929 118.268 976.993 118.034 975.707 118.034H973.089V124.23H975.707ZM973.089 128.508V137.627H966.87V113.521H976.362C979.542 113.521 981.856 114.059 983.329 115.135C984.826 116.187 985.55 117.87 985.55 120.185C985.55 121.775 985.176 123.084 984.405 124.113C983.633 125.118 982.488 125.89 980.921 126.357C981.786 126.568 982.534 127.012 983.212 127.69C983.891 128.368 984.569 129.397 985.247 130.8L988.637 137.627H982.02L979.074 131.641C978.49 130.449 977.882 129.607 977.274 129.186C976.666 128.742 975.848 128.508 974.842 128.508H973.089Z" fill="#131626"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

1
public/wallets/wos.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
public/wallets/zbd-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
public/wallets/zbd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

23
public/wallets/zbd.svg Normal file
View File

@ -0,0 +1,23 @@
<svg width="176" height="56" viewBox="0 0 176 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1995_141)">
<path opacity="1.01495" d="M164.279 4.34717L164.917 5.60721C165.116 6.00637 165.436 6.32704 165.854 6.54658L167.11 7.18658L165.854 7.82657C165.456 8.02616 165.136 8.34682 164.917 8.76595L164.279 10.026L163.641 8.76595C163.442 8.36678 163.122 8.04612 162.704 7.82657L161.448 7.18658L162.704 6.54658C163.102 6.34699 163.422 6.02633 163.641 5.60721L164.279 4.34717Z" fill="#0822F5"/>
<path opacity="1.09657" d="M81.6394 1.93066L82.2775 3.1907C82.4764 3.58987 82.7961 3.91053 83.214 4.13007L84.4702 4.77007L83.214 5.41007C82.816 5.60965 82.4963 5.93032 82.2775 6.34944L81.6394 7.60948L81.0013 6.34944C80.8024 5.95028 80.4827 5.62961 80.0648 5.41007L78.8086 4.77007L80.0648 4.13007C80.4628 3.93049 80.7825 3.60983 81.0013 3.1907L81.6394 1.93066Z" fill="#0822F5"/>
<path opacity="1.10383" d="M167.239 39.3579L167.877 40.6179C168.076 41.0171 168.396 41.3378 168.814 41.5573L170.07 42.1973L168.814 42.8373C168.416 43.0369 168.096 43.3576 167.877 43.7767L167.239 45.0367L166.601 43.7767C166.402 43.3775 166.082 43.0569 165.664 42.8373L164.408 42.1973L165.664 41.5573C166.062 41.3577 166.382 41.0371 166.601 40.6179L167.239 39.3579Z" fill="#0822F5"/>
<path opacity="0.905724" d="M2.83081 23.1975L3.46886 24.4575C3.66784 24.8567 3.98753 25.1774 4.40539 25.3969L5.66161 26.0369L4.40539 26.6769C4.00743 26.8765 3.68774 27.1972 3.46886 27.6163L2.83081 28.8763L2.19275 27.6163C1.99377 27.2171 1.67408 26.8965 1.25622 26.6769L0 26.0369L1.25622 25.3969C1.65418 25.1973 1.97387 24.8767 2.19275 24.4575L2.83081 23.1975Z" fill="#0822F5"/>
<path d="M14.5654 35.5643L42.8536 21.3619V20.0859H14.5654V9.99365H60.511V25.1327L32.2799 39.2207V40.4953H60.6277V50.5876H14.5654V35.567V35.5643Z" fill="black"/>
<path d="M96.2128 30.9251V29.5333C103.993 29.5333 109.165 26.2694 109.165 20.564C109.165 13.0557 103.698 10.0007 93.8887 10.0007H63.624V50.5801H93.8887C104.461 50.5801 109.165 46.7653 109.165 40.7885C109.165 34.8116 105.392 30.9251 96.2128 30.9251ZM88.1369 41.3606H79.017V34.5774H88.1369C91.0871 34.5774 92.6099 35.9147 92.6099 37.9411C92.6099 40.5636 90.8337 41.3593 88.1369 41.3593V41.3606ZM88.1369 25.9408H79.017V19.2122H88.1369C90.5034 19.2122 92.6099 19.8003 92.6099 22.6304C92.6099 25.4605 90.1824 25.9395 88.1369 25.9395V25.9408Z" fill="black"/>
<path d="M111.301 9.99854H139.126C156.021 9.99854 163.636 18.3438 163.636 30.2882C163.636 42.834 154.482 50.5779 139.126 50.5779H111.301V9.99854ZM147.199 30.2882C147.199 23.1245 142.218 20.4328 137.382 20.4328H127.392V40.1423H137.382C142.16 40.1423 147.199 37.4506 147.199 30.2869V30.2882Z" fill="black"/>
<path opacity="0.596148" d="M146.402 14.2175C146.402 14.2175 154.529 15.081 155.152 3.33887C155.152 3.33887 155.073 13.036 163.369 14.1842C163.369 14.1842 155.707 14.6393 155.401 24.8353C155.39 24.8473 155.459 15.2061 146.402 14.2175Z" fill="#F0FF27" stroke="black" stroke-linejoin="round"/>
<path opacity="0.324772" d="M1.16504 39.8337C1.16504 39.8337 9.29134 40.6972 9.9148 28.9551C9.9148 28.9551 9.83521 38.6522 18.1313 39.8004C18.1313 39.8004 10.4693 40.2555 10.1642 50.4516C10.1523 50.4635 10.2212 40.8223 1.16504 39.8337Z" fill="#F0FF27" stroke="black" stroke-linejoin="round"/>
<path opacity="0.151851" d="M164.346 17.9782C164.346 17.9782 169.266 18.5011 169.644 11.3906C169.644 11.3906 169.596 17.2624 174.618 17.9569C174.618 17.9569 169.979 18.2324 169.794 24.4061C169.787 24.4128 169.828 18.5756 164.346 17.9769V17.9782Z" fill="#F0FF27" stroke="black" stroke-linejoin="round"/>
<path opacity="0.0628216" d="M67.9766 7.58759C67.9766 7.58759 72.8966 8.1105 73.2747 1C73.2747 1 73.227 6.87175 78.2492 7.5663C78.2492 7.5663 73.6103 7.84173 73.4246 14.0155C73.418 14.0222 73.4591 8.18501 67.9766 7.58626V7.58759Z" fill="#F0FF27" stroke="black" stroke-linejoin="round"/>
<path opacity="0.0235497" d="M15.9355 23.2949C15.9355 23.2949 20.8556 23.8178 21.2337 16.7073C21.2337 16.7073 21.1859 22.579 26.2082 23.2736C26.2082 23.2736 21.5693 23.549 21.3836 29.7228C21.377 29.7294 21.4181 23.8923 15.9355 23.2935V23.2949Z" fill="#F0FF27" stroke="black" stroke-linejoin="round"/>
<path opacity="0.01" d="M102.614 50.321L103.252 51.5811C103.451 51.9802 103.771 52.3009 104.189 52.5205L105.445 53.1605L104.189 53.8005C103.791 54 103.471 54.3207 103.252 54.7398L102.614 55.9999L101.976 54.7398C101.777 54.3407 101.457 54.02 101.039 53.8005L99.7832 53.1605L101.039 52.5205C101.437 52.3209 101.757 52.0002 101.976 51.5811L102.614 50.321Z" fill="#0822F5"/>
<path opacity="0.01" d="M89.5137 48.7829C89.5137 48.7829 94.4338 49.3058 94.8118 42.1953C94.8118 42.1953 94.7641 48.0671 99.7863 48.7616C99.7863 48.7616 95.1474 49.037 94.9617 55.2108C94.9551 55.2175 94.9962 49.3803 89.5137 48.7816V48.7829Z" fill="#F0FF27" stroke="black" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_1995_141">
<rect width="176" height="56" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 258.12 100">
<defs>
<style>
.cls-1 {
fill: #ffa900;
stroke-width: 0px;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer 1">
<path class="cls-1" d="m13.83,27.6L0,0h46.47C29.48,4.39,19.86,16.16,13.83,27.6Zm12.86,72.4h46.47l-13.83-27.6c-6.03,11.43-15.65,23.21-32.64,27.6ZM50.12,0L0,100h23.05L73.17,0h-23.05Zm59.33,27.59L123.28,0h-46.47c16.99,4.39,26.62,16.16,32.64,27.6Zm-32.64,72.4h46.47l-13.83-27.6c-6.03,11.43-15.65,23.21-32.64,27.6Zm-3.14-89.97l-11.52,22.98,8.51,16.99-8.51,16.98,11.52,22.98,20.03-39.96-20.03-39.97ZM179.43.01h-23.05s25.06,49.99,25.06,49.99l-25.06,50h23.04l25.06-50L179.43.01Zm-75.68,49.99l25.06,50h23.04s-25.06-50-25.06-50L151.85,0h-23.04s-25.06,50-25.06,50Zm140.53-22.4L258.12,0h-46.47c16.99,4.39,26.62,16.16,32.64,27.6Zm-45.5,44.81l-13.83,27.6h46.47c-16.99-4.39-26.62-16.16-32.64-27.6ZM184.95,0l50.12,100h23.05S208.01.01,208.01.01h-23.06Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

14
public/wallets/zeus.svg Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 258.12 100">
<defs>
<style>
.cls-1 {
fill: #ffa900;
stroke-width: 0px;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer 1">
<path class="cls-1" d="m13.83,27.6L0,0h46.47C29.48,4.39,19.86,16.16,13.83,27.6Zm12.86,72.4h46.47l-13.83-27.6c-6.03,11.43-15.65,23.21-32.64,27.6ZM50.12,0L0,100h23.05L73.17,0h-23.05Zm59.33,27.59L123.28,0h-46.47c16.99,4.39,26.62,16.16,32.64,27.6Zm-32.64,72.4h46.47l-13.83-27.6c-6.03,11.43-15.65,23.21-32.64,27.6Zm-3.14-89.97l-11.52,22.98,8.51,16.99-8.51,16.98,11.52,22.98,20.03-39.96-20.03-39.97ZM179.43.01h-23.05s25.06,49.99,25.06,49.99l-25.06,50h23.04l25.06-50L179.43.01Zm-75.68,49.99l25.06,50h23.04s-25.06-50-25.06-50L151.85,0h-23.04s-25.06,50-25.06,50Zm140.53-22.4L258.12,0h-46.47c16.99,4.39,26.62,16.16,32.64,27.6Zm-45.5,44.81l-13.83,27.6h46.47c-16.99-4.39-26.62-16.16-32.64-27.6ZM184.95,0l50.12,100h23.05S208.01.01,208.01.01h-23.06Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

31
styles/dnd.module.css Normal file
View File

@ -0,0 +1,31 @@
.draggable {
cursor: grab;
transition: all 0.2s ease-out;
position: relative;
}
.dragging {
cursor: grabbing;
opacity: 0.3;
z-index: 1000;
}
.dragOver {
transform: scale(1.03);
box-shadow: 0 0 10px var(--bs-info);
}
@media (max-width: 768px) {
.draggable {
/* https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action */
touch-action: none;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.dragging {
touch-action: none;
}
}

View File

@ -1,4 +1,4 @@
.tableContainer {
.container {
width: 100%;
max-height: 60svh;
overflow-y: auto;
@ -9,34 +9,56 @@
}
@media screen and (min-width: 768px) {
.tableContainer {
.container {
max-height: 70svh;
}
.embedded {
max-height: 30svh;
max-height: 25svh;
}
}
.tableRow {
.row {
display: flex;
gap: 0.5rem;
font-family: monospace;
color: var(--theme-grey) !important; /* .text-muted */
}
.timestamp {
vertical-align: top;
text-wrap: nowrap;
justify-self: first baseline;
.row:hover {
background-color: rgba(128, 128, 128, 0.1);
}
.wallet {
vertical-align: top;
font-weight: bold;
.timestamp {
text-wrap: nowrap;
min-width: 20px;
text-align: right;
}
.level {
font-weight: bold;
vertical-align: top;
text-transform: uppercase;
padding-right: 0.5em;
min-width: 32px;
}
.tag {
vertical-align: top;
font-weight: bold;
}
.message {
word-break: break-word;
}
.indicator {
margin-left: auto;
}
.context {
flex-basis: 100%;
display: grid;
grid-template-columns: min-content auto;
column-gap: 0.5rem;
padding-left: 0.5rem;
padding-bottom: 0.5rem;
}

Some files were not shown because too many files have changed in this diff Show More