commit
fee9a96186
|
@ -141,6 +141,10 @@ export const WALLET = gql`
|
|||
url
|
||||
secondaryPassword
|
||||
}
|
||||
... on WalletBlink {
|
||||
apiKeyRecv
|
||||
currencyRecv
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -181,6 +185,10 @@ export const WALLET_BY_TYPE = gql`
|
|||
url
|
||||
secondaryPassword
|
||||
}
|
||||
... on WalletBlink {
|
||||
apiKeyRecv
|
||||
currencyRecv
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -751,14 +751,33 @@ export const nwcSchema = object().shape({
|
|||
...autowithdrawSchemaMembers
|
||||
}, ['nwcUrl', 'nwcUrlRecv'])
|
||||
|
||||
export const blinkSchema = object({
|
||||
export const blinkSchema = object().shape({
|
||||
apiKey: string()
|
||||
.required('required')
|
||||
.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' })
|
||||
.when(['apiKeyRecv'], ([apiKeyRecv], schema) => {
|
||||
if (!apiKeyRecv) return schema.required('required if api key for receiving not set')
|
||||
return schema.test({
|
||||
test: apiKey => apiKey !== apiKeyRecv,
|
||||
message: 'api key for sending cannot be the same as for receiving'
|
||||
})
|
||||
}),
|
||||
apiKeyRecv: string()
|
||||
.matches(/^blink_[A-Za-z0-9]+$/, { message: 'must match pattern blink_A-Za-z0-9' })
|
||||
.when(['apiKey'], ([apiKey], schema) => {
|
||||
if (!apiKey) return schema.required('required if api key for sending not set')
|
||||
return schema.test({
|
||||
test: apiKeyRecv => apiKeyRecv !== apiKey,
|
||||
message: 'api key for receiving cannot be the same as for sending'
|
||||
})
|
||||
}),
|
||||
currency: string()
|
||||
.transform(value => value ? value.toUpperCase() : 'BTC')
|
||||
.oneOf(['USD', 'BTC'], 'must be BTC or USD')
|
||||
})
|
||||
.oneOf(['USD', 'BTC'], 'must be BTC or USD'),
|
||||
currencyRecv: string()
|
||||
.transform(value => value ? value.toUpperCase() : 'BTC')
|
||||
.oneOf(['BTC'], 'must be BTC'),
|
||||
...autowithdrawSchemaMembers
|
||||
}, ['apiKey', 'apiKeyRecv'])
|
||||
|
||||
export const lncSchema = object({
|
||||
pairingPhrase: string()
|
||||
|
|
|
@ -110,7 +110,7 @@ function WalletFields ({ wallet: { config, fields, isConfigured } }) {
|
|||
...props,
|
||||
name,
|
||||
initialValue: config?.[name],
|
||||
readOnly: isClient && isConfigured && editable === false && !!config?.[name],
|
||||
readOnly: isClient && editable === false && ((isConfigured && !!config?.[name]) || !!props.value),
|
||||
groupClassName: props.hidden ? 'd-none' : undefined,
|
||||
label: label
|
||||
? (
|
||||
|
|
|
@ -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();
|
|
@ -179,6 +179,7 @@ enum WalletType {
|
|||
LNBITS
|
||||
NWC
|
||||
PHOENIXD
|
||||
BLINK
|
||||
}
|
||||
|
||||
model Wallet {
|
||||
|
@ -205,6 +206,7 @@ model Wallet {
|
|||
walletLNbits WalletLNbits?
|
||||
walletNWC WalletNWC?
|
||||
walletPhoenixd WalletPhoenixd?
|
||||
walletBlink WalletBlink?
|
||||
withdrawals Withdrawl[]
|
||||
InvoiceForward InvoiceForward[]
|
||||
|
||||
|
@ -273,6 +275,16 @@ model WalletNWC {
|
|||
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
|
||||
|
|
|
@ -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 async function testSendPayment ({ apiKey, currency }, { logger }) {
|
||||
currency = currency ? currency.toUpperCase() : 'BTC'
|
||||
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)
|
||||
|
||||
logger.ok(currency + ' wallet found')
|
||||
}
|
||||
|
||||
|
@ -143,45 +152,3 @@ async function getTxInfo (authToken, wallet, invoice) {
|
|||
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()
|
||||
}
|
||||
|
|
|
@ -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,34 +1,61 @@
|
|||
import { blinkSchema } from '@/lib/validate'
|
||||
|
||||
export const galoyBlinkUrl = 'https://api.blink.sv/graphql'
|
||||
export const galoyBlinkDashboardUrl = 'https://dashboard.blink.sv/'
|
||||
import { galoyBlinkDashboardUrl } from 'wallets/blink/common'
|
||||
|
||||
export const name = 'blink'
|
||||
export const walletType = 'BLINK'
|
||||
export const walletField = 'walletBlink'
|
||||
export const fieldValidation = blinkSchema
|
||||
|
||||
export const fields = [
|
||||
{
|
||||
name: 'apiKey',
|
||||
label: 'api key',
|
||||
type: 'password',
|
||||
help: `you can get an API key from [Blink Dashboard](${galoyBlinkDashboardUrl})`,
|
||||
placeholder: 'blink_...'
|
||||
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.`,
|
||||
placeholder: 'blink_...',
|
||||
optional: 'for sending',
|
||||
clientOnly: true,
|
||||
editable: false
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
label: 'wallet type',
|
||||
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',
|
||||
optional: true,
|
||||
clear: true,
|
||||
autoComplete: 'off'
|
||||
autoComplete: 'off',
|
||||
optional: 'for sending',
|
||||
clientOnly: true,
|
||||
editable: false
|
||||
},
|
||||
{
|
||||
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,
|
||||
editable: false
|
||||
},
|
||||
{
|
||||
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,
|
||||
editable: false
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
export const card = {
|
||||
title: 'Blink',
|
||||
subtitle: 'use [Blink](https://blink.sv/) for payments',
|
||||
badges: ['send']
|
||||
badges: ['send & receive']
|
||||
}
|
||||
|
||||
export const fieldValidation = blinkSchema
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -4,13 +4,14 @@ import * as lnAddr from 'wallets/lightning-address/server'
|
|||
import * as lnbits from 'wallets/lnbits/server'
|
||||
import * as nwc from 'wallets/nwc/server'
|
||||
import * as phoenixd from 'wallets/phoenixd/server'
|
||||
import * as blink from 'wallets/blink/server'
|
||||
import { addWalletLog } from '@/api/resolvers/wallet'
|
||||
import walletDefs from 'wallets/server'
|
||||
import { parsePaymentRequest } from 'ln-service'
|
||||
import { toPositiveNumber } from '@/lib/validate'
|
||||
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
||||
import { withTimeout } from '@/lib/time'
|
||||
export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd]
|
||||
export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink]
|
||||
|
||||
const MAX_PENDING_INVOICES_PER_WALLET = 25
|
||||
|
||||
|
|
Loading…
Reference in New Issue