merge master
This commit is contained in:
commit
25facad5d9
@ -258,8 +258,10 @@ export async function createLightningInvoice (actionType, args, context) {
|
|||||||
expiry: INVOICE_EXPIRE_SECS
|
expiry: INVOICE_EXPIRE_SECS
|
||||||
}, { models })
|
}, { models })
|
||||||
|
|
||||||
|
// the sender (me) decides if the wrapped invoice has a description
|
||||||
|
// whereas the recipient decides if their invoice has a description
|
||||||
const { invoice: wrappedInvoice, maxFee } = await wrapInvoice(
|
const { invoice: wrappedInvoice, maxFee } = await wrapInvoice(
|
||||||
bolt11, { msats: cost, description }, { lnd })
|
bolt11, { msats: cost, description }, { me, lnd })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bolt11,
|
bolt11,
|
||||||
|
@ -180,7 +180,8 @@ export default function Comment ({
|
|||||||
</ActionTooltip>}
|
</ActionTooltip>}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
onEdit={e => { setEdit(!edit) }}
|
edit={edit}
|
||||||
|
toggleEdit={e => { setEdit(!edit) }}
|
||||||
editText={edit ? 'cancel' : 'edit'}
|
editText={edit ? 'cancel' : 'edit'}
|
||||||
/>}
|
/>}
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ import { useShowModal } from './modal'
|
|||||||
import { QRCodeSVG } from 'qrcode.react'
|
import { QRCodeSVG } from 'qrcode.react'
|
||||||
import { Scanner } from '@yudiel/react-qr-scanner'
|
import { Scanner } from '@yudiel/react-qr-scanner'
|
||||||
import { qrImageSettings } from './qr'
|
import { qrImageSettings } from './qr'
|
||||||
|
import { useIsClient } from './use-client'
|
||||||
|
|
||||||
export class SessionRequiredError extends Error {
|
export class SessionRequiredError extends Error {
|
||||||
constructor () {
|
constructor () {
|
||||||
@ -472,6 +473,7 @@ function InputInner ({
|
|||||||
const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props)
|
const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props)
|
||||||
const formik = noForm ? null : useFormikContext()
|
const formik = noForm ? null : useFormikContext()
|
||||||
const storageKeyPrefix = useContext(StorageKeyPrefixContext)
|
const storageKeyPrefix = useContext(StorageKeyPrefixContext)
|
||||||
|
const isClient = useIsClient()
|
||||||
|
|
||||||
const storageKey = storageKeyPrefix ? storageKeyPrefix + '-' + props.name : undefined
|
const storageKey = storageKeyPrefix ? storageKeyPrefix + '-' + props.name : undefined
|
||||||
|
|
||||||
@ -555,7 +557,7 @@ function InputInner ({
|
|||||||
isInvalid={invalid}
|
isInvalid={invalid}
|
||||||
isValid={showValid && meta.initialValue !== meta.value && meta.touched && !meta.error}
|
isValid={showValid && meta.initialValue !== meta.value && meta.touched && !meta.error}
|
||||||
/>
|
/>
|
||||||
{(clear && field.value) &&
|
{(isClient && clear && field.value && !props.readOnly) &&
|
||||||
<Button
|
<Button
|
||||||
variant={null}
|
variant={null}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@ -30,7 +30,7 @@ import classNames from 'classnames'
|
|||||||
|
|
||||||
export default function ItemInfo ({
|
export default function ItemInfo ({
|
||||||
item, full, commentsText = 'comments',
|
item, full, commentsText = 'comments',
|
||||||
commentTextSingular = 'comment', className, embellishUser, extraInfo, onEdit, editText,
|
commentTextSingular = 'comment', className, embellishUser, extraInfo, edit, toggleEdit, editText,
|
||||||
onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true,
|
onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true,
|
||||||
setDisableRetry, disableRetry
|
setDisableRetry, disableRetry
|
||||||
}) {
|
}) {
|
||||||
@ -151,8 +151,8 @@ export default function ItemInfo ({
|
|||||||
showActionDropdown &&
|
showActionDropdown &&
|
||||||
<>
|
<>
|
||||||
<EditInfo
|
<EditInfo
|
||||||
item={item} canEdit={canEdit}
|
item={item} edit={edit} canEdit={canEdit}
|
||||||
setCanEdit={setCanEdit} onEdit={onEdit} editText={editText} editThreshold={editThreshold}
|
setCanEdit={setCanEdit} toggleEdit={toggleEdit} editText={editText} editThreshold={editThreshold}
|
||||||
/>
|
/>
|
||||||
<PaymentInfo item={item} disableRetry={disableRetry} setDisableRetry={setDisableRetry} />
|
<PaymentInfo item={item} disableRetry={disableRetry} setDisableRetry={setDisableRetry} />
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
@ -311,7 +311,7 @@ function PaymentInfo ({ item, disableRetry, setDisableRetry }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditInfo ({ item, canEdit, setCanEdit, onEdit, editText, editThreshold }) {
|
function EditInfo ({ item, edit, canEdit, setCanEdit, toggleEdit, editText, editThreshold }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
if (canEdit) {
|
if (canEdit) {
|
||||||
@ -320,7 +320,7 @@ function EditInfo ({ item, canEdit, setCanEdit, onEdit, editText, editThreshold
|
|||||||
<span> \ </span>
|
<span> \ </span>
|
||||||
<span
|
<span
|
||||||
className='text-reset pointer fw-bold'
|
className='text-reset pointer fw-bold'
|
||||||
onClick={() => onEdit ? onEdit() : router.push(`/items/${item.id}/edit`)}
|
onClick={() => toggleEdit ? toggleEdit() : router.push(`/items/${item.id}/edit`)}
|
||||||
>
|
>
|
||||||
<span>{editText || 'edit'} </span>
|
<span>{editText || 'edit'} </span>
|
||||||
{(!item.invoice?.actionState || item.invoice?.actionState === 'PAID')
|
{(!item.invoice?.actionState || item.invoice?.actionState === 'PAID')
|
||||||
@ -334,5 +334,21 @@ function EditInfo ({ item, canEdit, setCanEdit, onEdit, editText, editThreshold
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (edit && !canEdit) {
|
||||||
|
// if we're still editing after timer ran out
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span> \ </span>
|
||||||
|
<span
|
||||||
|
className='text-reset pointer fw-bold'
|
||||||
|
onClick={() => toggleEdit ? toggleEdit() : router.push(`/items/${item.id}`)}
|
||||||
|
>
|
||||||
|
<span>cancel </span>
|
||||||
|
<span>00:00</span>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -43,10 +43,18 @@ export default function WalletCard ({ wallet, draggable, onDragStart, onDragEnte
|
|||||||
<Card.Title>{title}</Card.Title>
|
<Card.Title>{title}</Card.Title>
|
||||||
<Card.Subtitle className='mt-2'>
|
<Card.Subtitle className='mt-2'>
|
||||||
{badges?.map(
|
{badges?.map(
|
||||||
badge =>
|
badge => {
|
||||||
<Badge className={styles.badge} key={badge} bg={null}>
|
let style = ''
|
||||||
{badge}
|
switch (badge) {
|
||||||
</Badge>)}
|
case 'receive': style = styles.receive; break
|
||||||
|
case 'send': style = styles.send; break
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Badge className={`${styles.badge} ${style}`} key={badge} bg={null}>
|
||||||
|
{badge}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</Card.Subtitle>
|
</Card.Subtitle>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
<Link href={`/settings/wallets/${wallet.def.name}`}>
|
<Link href={`/settings/wallets/${wallet.def.name}`}>
|
||||||
|
@ -29,6 +29,18 @@ const ITEM_PAID_ACTION_FIELDS = gql`
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
const ITEM_PAID_ACTION_FIELDS_NO_CHILD_COMMENTS = gql`
|
||||||
|
${COMMENTS}
|
||||||
|
fragment ItemPaidActionFieldsNoChildComments on ItemPaidAction {
|
||||||
|
result {
|
||||||
|
id
|
||||||
|
deleteScheduledAt
|
||||||
|
reminderScheduledAt
|
||||||
|
...CommentFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const ITEM_ACT_PAID_ACTION_FIELDS = gql`
|
const ITEM_ACT_PAID_ACTION_FIELDS = gql`
|
||||||
fragment ItemActPaidActionFields on ItemActPaidAction {
|
fragment ItemActPaidActionFields on ItemActPaidAction {
|
||||||
result {
|
result {
|
||||||
@ -226,11 +238,11 @@ export const CREATE_COMMENT = gql`
|
|||||||
}`
|
}`
|
||||||
|
|
||||||
export const UPDATE_COMMENT = gql`
|
export const UPDATE_COMMENT = gql`
|
||||||
${ITEM_PAID_ACTION_FIELDS}
|
${ITEM_PAID_ACTION_FIELDS_NO_CHILD_COMMENTS}
|
||||||
${PAID_ACTION}
|
${PAID_ACTION}
|
||||||
mutation upsertComment($id: ID!, $text: String!, $boost: Int, ${HASH_HMAC_INPUT_1}) {
|
mutation upsertComment($id: ID!, $text: String!, $boost: Int, ${HASH_HMAC_INPUT_1}) {
|
||||||
upsertComment(id: $id, text: $text, boost: $boost, ${HASH_HMAC_INPUT_2}) {
|
upsertComment(id: $id, text: $text, boost: $boost, ${HASH_HMAC_INPUT_2}) {
|
||||||
...ItemPaidActionFields
|
...ItemPaidActionFieldsNoChildComments
|
||||||
...PaidActionFields
|
...PaidActionFields
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
@ -145,6 +145,10 @@ export const WALLET_FIELDS = gql`
|
|||||||
url
|
url
|
||||||
secondaryPassword
|
secondaryPassword
|
||||||
}
|
}
|
||||||
|
... on WalletBlink {
|
||||||
|
apiKeyRecv
|
||||||
|
currencyRecv
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
26
prisma/migrations/20241016171557_blinkreceive/migration.sql
Normal file
26
prisma/migrations/20241016171557_blinkreceive/migration.sql
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "WalletType" ADD VALUE 'BLINK';
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "WalletBlink" (
|
||||||
|
"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,
|
||||||
|
"apiKeyRecv" TEXT NOT NULL,
|
||||||
|
"currencyRecv" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "WalletBlink_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "WalletBlink_walletId_key" ON "WalletBlink"("walletId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "WalletBlink" ADD CONSTRAINT "WalletBlink_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
|
||||||
|
-- Update wallet json
|
||||||
|
CREATE TRIGGER wallet_blink_as_jsonb
|
||||||
|
AFTER INSERT OR UPDATE ON "WalletBlink"
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb();
|
@ -6,7 +6,6 @@
|
|||||||
-- the enum.
|
-- the enum.
|
||||||
|
|
||||||
|
|
||||||
ALTER TYPE "WalletType" ADD VALUE 'BLINK';
|
|
||||||
ALTER TYPE "WalletType" ADD VALUE 'LNC';
|
ALTER TYPE "WalletType" ADD VALUE 'LNC';
|
||||||
ALTER TYPE "WalletType" ADD VALUE 'WEBLN';
|
ALTER TYPE "WalletType" ADD VALUE 'WEBLN';
|
||||||
|
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
-- fix revenue for users who have multiple revenue entries for the same day
|
||||||
|
WITH revenue_days AS (
|
||||||
|
SELECT coalesce(sum(msats), 0) as revenue_msats, "userId", created_at
|
||||||
|
FROM "SubAct"
|
||||||
|
WHERE type = 'REVENUE'
|
||||||
|
GROUP BY "userId", created_at
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
),
|
||||||
|
revenue_total AS (
|
||||||
|
SELECT coalesce(sum(revenue_msats), 0) as revenue_msats, "userId"
|
||||||
|
FROM revenue_days
|
||||||
|
GROUP BY "userId"
|
||||||
|
)
|
||||||
|
UPDATE users SET msats = users.msats + revenue_total.revenue_msats
|
||||||
|
FROM revenue_total
|
||||||
|
WHERE users.id = revenue_total."userId";
|
||||||
|
|
||||||
|
-- fix stacked msats for users who have territory revenue
|
||||||
|
-- prior to this, we were not updating stacked msats for territory revenue
|
||||||
|
WITH territory_revenue AS (
|
||||||
|
SELECT coalesce(sum(msats), 0) as revenue_msats, "userId"
|
||||||
|
FROM "SubAct"
|
||||||
|
WHERE type = 'REVENUE'
|
||||||
|
GROUP BY "userId"
|
||||||
|
)
|
||||||
|
UPDATE users SET "stackedMsats" = users."stackedMsats" + territory_revenue.revenue_msats
|
||||||
|
FROM territory_revenue
|
||||||
|
WHERE users.id = territory_revenue."userId";
|
@ -211,6 +211,7 @@ model Wallet {
|
|||||||
walletLNbits WalletLNbits?
|
walletLNbits WalletLNbits?
|
||||||
walletNWC WalletNWC?
|
walletNWC WalletNWC?
|
||||||
walletPhoenixd WalletPhoenixd?
|
walletPhoenixd WalletPhoenixd?
|
||||||
|
walletBlink WalletBlink?
|
||||||
|
|
||||||
vaultEntries VaultEntry[] @relation("VaultEntries")
|
vaultEntries VaultEntry[] @relation("VaultEntries")
|
||||||
withdrawals Withdrawl[]
|
withdrawals Withdrawl[]
|
||||||
@ -298,6 +299,16 @@ model WalletNWC {
|
|||||||
nwcUrlRecv String
|
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 {
|
model WalletPhoenixd {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
walletId Int @unique
|
walletId Int @unique
|
||||||
|
@ -40,6 +40,14 @@
|
|||||||
margin-right: 0.2rem;
|
margin-right: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.receive {
|
||||||
|
color: #20c997 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send {
|
||||||
|
color: var(--bs-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.attach {
|
.attach {
|
||||||
color: var(--bs-body-color) !important;
|
color: var(--bs-body-color) !important;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -1,10 +1,19 @@
|
|||||||
import { galoyBlinkUrl } from 'wallets/blink'
|
import { getScopes, SCOPE_READ, SCOPE_WRITE, getWallet, request } from 'wallets/blink/common'
|
||||||
export * from 'wallets/blink'
|
export * from 'wallets/blink'
|
||||||
|
|
||||||
export async function testSendPayment ({ apiKey, currency }, { logger }) {
|
export async function testSendPayment ({ apiKey, currency }, { logger }) {
|
||||||
currency = currency ? currency.toUpperCase() : 'BTC'
|
|
||||||
logger.info('trying to fetch ' + currency + ' wallet')
|
logger.info('trying to fetch ' + currency + ' wallet')
|
||||||
|
const scopes = await getScopes(apiKey)
|
||||||
|
if (!scopes.includes(SCOPE_READ)) {
|
||||||
|
throw new Error('missing READ scope')
|
||||||
|
}
|
||||||
|
if (!scopes.includes(SCOPE_WRITE)) {
|
||||||
|
throw new Error('missing WRITE scope')
|
||||||
|
}
|
||||||
|
|
||||||
|
currency = currency ? currency.toUpperCase() : 'BTC'
|
||||||
await getWallet(apiKey, currency)
|
await getWallet(apiKey, currency)
|
||||||
|
|
||||||
logger.ok(currency + ' wallet found')
|
logger.ok(currency + ' wallet found')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,45 +152,3 @@ async function getTxInfo (authToken, wallet, invoice) {
|
|||||||
error: ''
|
error: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWallet (authToken, currency) {
|
|
||||||
const out = await request(authToken, `
|
|
||||||
query me {
|
|
||||||
me {
|
|
||||||
defaultAccount {
|
|
||||||
wallets {
|
|
||||||
id
|
|
||||||
walletCurrency
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, {})
|
|
||||||
const wallets = out.data.me.defaultAccount.wallets
|
|
||||||
for (const wallet of wallets) {
|
|
||||||
if (wallet.walletCurrency === currency) {
|
|
||||||
return wallet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error(`wallet ${currency} not found`)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function request (authToken, query, variables = {}) {
|
|
||||||
const options = {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-API-KEY': authToken
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ query, variables })
|
|
||||||
}
|
|
||||||
const res = await fetch(galoyBlinkUrl, options)
|
|
||||||
if (res.status >= 400 && res.status <= 599) {
|
|
||||||
if (res.status === 401) {
|
|
||||||
throw new Error('unauthorized')
|
|
||||||
} else {
|
|
||||||
throw new Error('API responded with HTTP ' + res.status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
62
wallets/blink/common.js
Normal file
62
wallets/blink/common.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
export const galoyBlinkUrl = 'https://api.blink.sv/graphql'
|
||||||
|
export const galoyBlinkDashboardUrl = 'https://dashboard.blink.sv/'
|
||||||
|
|
||||||
|
export const SCOPE_READ = 'READ'
|
||||||
|
export const SCOPE_WRITE = 'WRITE'
|
||||||
|
export const SCOPE_RECEIVE = 'RECEIVE'
|
||||||
|
|
||||||
|
export async function getWallet (authToken, currency) {
|
||||||
|
const out = await request(authToken, `
|
||||||
|
query me {
|
||||||
|
me {
|
||||||
|
defaultAccount {
|
||||||
|
wallets {
|
||||||
|
id
|
||||||
|
walletCurrency
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {})
|
||||||
|
const wallets = out.data.me.defaultAccount.wallets
|
||||||
|
for (const wallet of wallets) {
|
||||||
|
if (wallet.walletCurrency === currency) {
|
||||||
|
return wallet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`wallet ${currency} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function request (authToken, query, variables = {}) {
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-API-KEY': authToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ query, variables })
|
||||||
|
}
|
||||||
|
const res = await fetch(galoyBlinkUrl, options)
|
||||||
|
if (res.status >= 400 && res.status <= 599) {
|
||||||
|
// consume res
|
||||||
|
res.text().catch(() => {})
|
||||||
|
if (res.status === 401) {
|
||||||
|
throw new Error('unauthorized')
|
||||||
|
} else {
|
||||||
|
throw new Error('API responded with HTTP ' + res.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getScopes (authToken) {
|
||||||
|
const out = await request(authToken, `
|
||||||
|
query scopes {
|
||||||
|
authorization {
|
||||||
|
scopes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {})
|
||||||
|
const scopes = out?.data?.authorization?.scopes
|
||||||
|
return scopes || []
|
||||||
|
}
|
@ -1,7 +1,5 @@
|
|||||||
import { string } from '@/lib/yup'
|
import { string } from '@/lib/yup'
|
||||||
|
import { galoyBlinkDashboardUrl } from 'wallets/blink/common'
|
||||||
export const galoyBlinkUrl = 'https://api.blink.sv/graphql'
|
|
||||||
export const galoyBlinkDashboardUrl = 'https://dashboard.blink.sv/'
|
|
||||||
|
|
||||||
export const name = 'blink'
|
export const name = 'blink'
|
||||||
export const walletType = 'BLINK'
|
export const walletType = 'BLINK'
|
||||||
@ -12,30 +10,58 @@ export const fields = [
|
|||||||
name: 'apiKey',
|
name: 'apiKey',
|
||||||
label: 'api key',
|
label: 'api key',
|
||||||
type: 'password',
|
type: 'password',
|
||||||
help: `you can get an API key from [Blink Dashboard](${galoyBlinkDashboardUrl})`,
|
|
||||||
placeholder: 'blink_...',
|
placeholder: 'blink_...',
|
||||||
clientOnly: true,
|
clientOnly: true,
|
||||||
validate: string()
|
validate: string()
|
||||||
.matches(/^blink_[A-Za-z0-9]+$/, { message: 'must match pattern blink_A-Za-z0-9' })
|
.matches(/^blink_[A-Za-z0-9]+$/, { message: 'must match pattern blink_A-Za-z0-9' }),
|
||||||
|
help: `you can get an API key from [Blink Dashboard](${galoyBlinkDashboardUrl}).\nPlease make sure to select ONLY the 'Read' and 'Write' scopes when generating this API key.`,
|
||||||
|
optional: 'for sending',
|
||||||
|
requiredWithout: ['apiKeyRecv']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'currency',
|
name: 'currency',
|
||||||
label: 'wallet type',
|
label: 'wallet type',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
help: 'the blink wallet to use (BTC or USD for stablesats)',
|
help: 'the blink wallet to use for sending (BTC or USD for stablesats)',
|
||||||
placeholder: 'BTC',
|
placeholder: 'BTC',
|
||||||
optional: true,
|
|
||||||
clear: true,
|
clear: true,
|
||||||
autoComplete: 'off',
|
autoComplete: 'off',
|
||||||
clientOnly: true,
|
clientOnly: true,
|
||||||
validate: string()
|
validate: string()
|
||||||
.transform(value => value ? value.toUpperCase() : 'BTC')
|
.transform(value => value ? value.toUpperCase() : 'BTC')
|
||||||
.oneOf(['USD', 'BTC'], 'must be BTC or USD')
|
.oneOf(['USD', 'BTC'], 'must be BTC or USD'),
|
||||||
|
optional: 'for sending'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'apiKeyRecv',
|
||||||
|
label: 'api key',
|
||||||
|
type: 'password',
|
||||||
|
help: `you can get an API key from [Blink Dashboard](${galoyBlinkDashboardUrl}).\nPlease make sure to select ONLY the 'Read' and 'Receive' scopes when generating this API key.`,
|
||||||
|
placeholder: 'blink_...',
|
||||||
|
optional: 'for receiving',
|
||||||
|
serverOnly: true,
|
||||||
|
requiredWithout: ['apiKey'],
|
||||||
|
validate: string()
|
||||||
|
.matches(/^blink_[A-Za-z0-9]+$/, { message: 'must match pattern blink_A-Za-z0-9' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'currencyRecv',
|
||||||
|
label: 'wallet type',
|
||||||
|
type: 'text',
|
||||||
|
help: 'the blink wallet to use for receiving (only BTC available)',
|
||||||
|
value: 'BTC',
|
||||||
|
clear: true,
|
||||||
|
autoComplete: 'off',
|
||||||
|
optional: 'for receiving',
|
||||||
|
serverOnly: true,
|
||||||
|
validate: string()
|
||||||
|
.transform(value => value ? value.toUpperCase() : 'BTC')
|
||||||
|
.oneOf(['BTC'], 'must be BTC')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export const card = {
|
export const card = {
|
||||||
title: 'Blink',
|
title: 'Blink',
|
||||||
subtitle: 'use [Blink](https://blink.sv/) for payments',
|
subtitle: 'use [Blink](https://blink.sv/) for payments',
|
||||||
badges: ['send only']
|
badges: ['send', 'receive']
|
||||||
}
|
}
|
||||||
|
61
wallets/blink/server.js
Normal file
61
wallets/blink/server.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { withTimeout } from '@/lib/time'
|
||||||
|
import { getScopes, SCOPE_READ, SCOPE_RECEIVE, SCOPE_WRITE, getWallet, request } from 'wallets/blink/common'
|
||||||
|
import { msatsToSats } from '@/lib/format'
|
||||||
|
export * from 'wallets/blink'
|
||||||
|
|
||||||
|
export async function testCreateInvoice ({ apiKeyRecv, currencyRecv }) {
|
||||||
|
const scopes = await getScopes(apiKeyRecv)
|
||||||
|
if (!scopes.includes(SCOPE_READ)) {
|
||||||
|
throw new Error('missing READ scope')
|
||||||
|
}
|
||||||
|
if (scopes.includes(SCOPE_WRITE)) {
|
||||||
|
throw new Error('WRITE scope must not be present')
|
||||||
|
}
|
||||||
|
if (!scopes.includes(SCOPE_RECEIVE)) {
|
||||||
|
throw new Error('missing RECEIVE scope')
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = 15_000
|
||||||
|
currencyRecv = currencyRecv ? currencyRecv.toUpperCase() : 'BTC'
|
||||||
|
return await withTimeout(createInvoice({ msats: 1000, expiry: 1 }, { apiKeyRecv, currencyRecv }), timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createInvoice (
|
||||||
|
{ msats, description, expiry },
|
||||||
|
{ apiKeyRecv, currencyRecv }) {
|
||||||
|
currencyRecv = currencyRecv ? currencyRecv.toUpperCase() : 'BTC'
|
||||||
|
|
||||||
|
const wallet = await getWallet(apiKeyRecv, currencyRecv)
|
||||||
|
|
||||||
|
if (currencyRecv !== 'BTC') {
|
||||||
|
throw new Error('unsupported currency ' + currencyRecv)
|
||||||
|
}
|
||||||
|
const mutation = `
|
||||||
|
mutation LnInvoiceCreate($input: LnInvoiceCreateInput!) {
|
||||||
|
lnInvoiceCreate(input: $input) {
|
||||||
|
invoice {
|
||||||
|
paymentRequest
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const out = await request(apiKeyRecv, mutation, {
|
||||||
|
input: {
|
||||||
|
amount: msatsToSats(msats),
|
||||||
|
expiresIn: Math.floor(expiry / 60) || 1,
|
||||||
|
memo: description,
|
||||||
|
walletId: wallet.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const res = out.data.lnInvoiceCreate
|
||||||
|
const errors = res.errors
|
||||||
|
if (errors && errors.length > 0) {
|
||||||
|
throw new Error('failed to create invoice ' + errors.map(e => e.code + ' ' + e.message).join(', '))
|
||||||
|
}
|
||||||
|
const invoice = res.invoice.paymentRequest
|
||||||
|
return invoice
|
||||||
|
}
|
@ -63,5 +63,5 @@ export const fields = [
|
|||||||
export const card = {
|
export const card = {
|
||||||
title: 'CLN',
|
title: 'CLN',
|
||||||
subtitle: 'autowithdraw to your Core Lightning node via [CLNRest](https://docs.corelightning.org/docs/rest)',
|
subtitle: 'autowithdraw to your Core Lightning node via [CLNRest](https://docs.corelightning.org/docs/rest)',
|
||||||
badges: ['receive only']
|
badges: ['receive']
|
||||||
}
|
}
|
||||||
|
@ -23,5 +23,5 @@ export const fields = [
|
|||||||
export const card = {
|
export const card = {
|
||||||
title: 'lightning address',
|
title: 'lightning address',
|
||||||
subtitle: 'autowithdraw to a lightning address',
|
subtitle: 'autowithdraw to a lightning address',
|
||||||
badges: ['receive only']
|
badges: ['receive']
|
||||||
}
|
}
|
||||||
|
@ -55,5 +55,5 @@ export const fields = [
|
|||||||
export const card = {
|
export const card = {
|
||||||
title: 'LNbits',
|
title: 'LNbits',
|
||||||
subtitle: 'use [LNbits](https://lnbits.com/) for payments',
|
subtitle: 'use [LNbits](https://lnbits.com/) for payments',
|
||||||
badges: ['send & receive']
|
badges: ['send', 'receive']
|
||||||
}
|
}
|
||||||
|
@ -61,5 +61,5 @@ export const fields = [
|
|||||||
export const card = {
|
export const card = {
|
||||||
title: 'LNC',
|
title: 'LNC',
|
||||||
subtitle: 'use Lightning Node Connect for LND payments',
|
subtitle: 'use Lightning Node Connect for LND payments',
|
||||||
badges: ['send only', 'budgetable']
|
badges: ['send', 'budgetable']
|
||||||
}
|
}
|
||||||
|
@ -50,5 +50,5 @@ export const fields = [
|
|||||||
export const card = {
|
export const card = {
|
||||||
title: 'LND',
|
title: 'LND',
|
||||||
subtitle: 'autowithdraw to your Lightning Labs node',
|
subtitle: 'autowithdraw to your Lightning Labs node',
|
||||||
badges: ['receive only']
|
badges: ['receive']
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ export const fields = [
|
|||||||
export const card = {
|
export const card = {
|
||||||
title: 'NWC',
|
title: 'NWC',
|
||||||
subtitle: 'use Nostr Wallet Connect for payments',
|
subtitle: 'use Nostr Wallet Connect for payments',
|
||||||
badges: ['send & receive', 'budgetable']
|
badges: ['send', 'receive', 'budgetable']
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function nwcCall ({ nwcUrl, method, params }, { logger, timeout } = {}) {
|
export async function nwcCall ({ nwcUrl, method, params }, { logger, timeout } = {}) {
|
||||||
|
@ -38,5 +38,5 @@ export const fields = [
|
|||||||
export const card = {
|
export const card = {
|
||||||
title: 'phoenixd',
|
title: 'phoenixd',
|
||||||
subtitle: 'use [phoenixd](https://phoenix.acinq.co/server) for payments',
|
subtitle: 'use [phoenixd](https://phoenix.acinq.co/server) for payments',
|
||||||
badges: ['send & receive']
|
badges: ['send', 'receive']
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,9 @@ import * as lnAddr from 'wallets/lightning-address/server'
|
|||||||
import * as lnbits from 'wallets/lnbits/server'
|
import * as lnbits from 'wallets/lnbits/server'
|
||||||
import * as nwc from 'wallets/nwc/server'
|
import * as nwc from 'wallets/nwc/server'
|
||||||
import * as phoenixd from 'wallets/phoenixd/server'
|
import * as phoenixd from 'wallets/phoenixd/server'
|
||||||
|
import * as blink from 'wallets/blink/server'
|
||||||
|
|
||||||
// we import only the metadata of client side wallets
|
// we import only the metadata of client side wallets
|
||||||
import * as blink from 'wallets/blink'
|
|
||||||
import * as lnc from 'wallets/lnc'
|
import * as lnc from 'wallets/lnc'
|
||||||
import * as webln from 'wallets/webln'
|
import * as webln from 'wallets/webln'
|
||||||
|
|
||||||
|
@ -13,5 +13,5 @@ export const fields = []
|
|||||||
export const card = {
|
export const card = {
|
||||||
title: 'WebLN',
|
title: 'WebLN',
|
||||||
subtitle: 'use a [WebLN provider](https://www.webln.guide/ressources/webln-providers) for payments',
|
subtitle: 'use a [WebLN provider](https://www.webln.guide/ressources/webln-providers) for payments',
|
||||||
badges: ['send only']
|
badges: ['send']
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ const ZAP_SYBIL_FEE_MULT = 10 / 7 // the fee for the zap sybil service
|
|||||||
maxFee: number
|
maxFee: number
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
export default async function wrapInvoice (bolt11, { msats, description, descriptionHash }, { lnd }) {
|
export default async function wrapInvoice (bolt11, { msats, description, descriptionHash }, { me, lnd }) {
|
||||||
try {
|
try {
|
||||||
console.group('wrapInvoice', description)
|
console.group('wrapInvoice', description)
|
||||||
|
|
||||||
@ -112,6 +112,11 @@ export default async function wrapInvoice (bolt11, { msats, description, descrip
|
|||||||
wrapped.description = inv.description
|
wrapped.description = inv.description
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (me?.hideInvoiceDesc) {
|
||||||
|
wrapped.description = undefined
|
||||||
|
wrapped.description_hash = undefined
|
||||||
|
}
|
||||||
|
|
||||||
// validate the expiration
|
// validate the expiration
|
||||||
if (new Date(inv.expires_at) < new Date(Date.now() + INCOMING_EXPIRATION_BUFFER_MSECS)) {
|
if (new Date(inv.expires_at) < new Date(Date.now() + INCOMING_EXPIRATION_BUFFER_MSECS)) {
|
||||||
throw new Error('Invoice expiration is too soon')
|
throw new Error('Invoice expiration is too soon')
|
||||||
|
@ -72,10 +72,17 @@ export async function territoryRevenue ({ models }) {
|
|||||||
FROM revenue
|
FROM revenue
|
||||||
WHERE revenue > 1000
|
WHERE revenue > 1000
|
||||||
RETURNING *
|
RETURNING *
|
||||||
|
),
|
||||||
|
"SubActResultTotal" AS (
|
||||||
|
SELECT coalesce(sum(msats), 0) as total_msats, "userId"
|
||||||
|
FROM "SubActResult"
|
||||||
|
GROUP BY "userId"
|
||||||
)
|
)
|
||||||
UPDATE users SET msats = users.msats + "SubActResult".msats
|
UPDATE users
|
||||||
FROM "SubActResult"
|
SET msats = users.msats + "SubActResultTotal".total_msats,
|
||||||
WHERE users.id = "SubActResult"."userId"`,
|
"stackedMsats" = users."stackedMsats" + "SubActResultTotal".total_msats
|
||||||
|
FROM "SubActResultTotal"
|
||||||
|
WHERE users.id = "SubActResultTotal"."userId"`,
|
||||||
{ models }
|
{ models }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user