Blink wallet sending attachment (#1293)
* blink attachment * support staging * add staging dashboard link * Revert "add staging dashboard link" This reverts commit a43fa2204f03d74e733063aedd6862c6d71e4a46. * Revert "support staging" This reverts commit 93c15aa5083e60b1dafc77c30e999fb90fef8589. * handle pending payments, code cleanup and comments * stable sats -> stablesats * catch HTTP errors * print wallet currency in debug * disable autocomplete * schema without test() * Fix save since default is not applied for empty strings Formik validation must see 'currency' as undefined and apply the default but the validation before save sees an empty string. * Save transformed config * Remove unnecessary defaults * Prefix HTTP error with text --------- Co-authored-by: ekzyis <ek@stacker.news>
This commit is contained in:
parent
3d8ae4a7a3
commit
2d139bed85
|
@ -10,3 +10,4 @@ mz
|
|||
btcbagehot
|
||||
felipe
|
||||
benalleng
|
||||
rblb
|
|
@ -712,6 +712,15 @@ export const nwcSchema = object({
|
|||
})
|
||||
})
|
||||
|
||||
export const blinkSchema = object({
|
||||
apiKey: string()
|
||||
.required('required')
|
||||
.matches(/^blink_[A-Za-z0-9]+$/, { message: 'must match pattern blink_A-Za-z0-9' }),
|
||||
currency: string()
|
||||
.transform(value => value ? value.toUpperCase() : 'BTC')
|
||||
.oneOf(['USD', 'BTC'], 'must be BTC or USD')
|
||||
})
|
||||
|
||||
export const lncSchema = object({
|
||||
pairingPhrase: array()
|
||||
.transform(function (value, originalValue) {
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
import { galoyBlinkUrl } from 'wallets/blink'
|
||||
export * from 'wallets/blink'
|
||||
|
||||
export async function testConnectClient ({ apiKey, currency }, { logger }) {
|
||||
currency = currency ? currency.toUpperCase() : 'BTC'
|
||||
logger.info('trying to fetch ' + currency + ' wallet')
|
||||
await getWallet(apiKey, currency)
|
||||
logger.ok(currency + ' wallet found')
|
||||
}
|
||||
|
||||
export async function sendPayment (bolt11, { apiKey, currency }) {
|
||||
const wallet = await getWallet(apiKey, currency)
|
||||
const preImage = await payInvoice(apiKey, wallet, bolt11)
|
||||
return { preImage }
|
||||
}
|
||||
|
||||
async function payInvoice (authToken, wallet, invoice) {
|
||||
const walletId = wallet.id
|
||||
const out = await request(authToken, `
|
||||
mutation LnInvoicePaymentSend($input: LnInvoicePaymentInput!) {
|
||||
lnInvoicePaymentSend(input: $input) {
|
||||
status
|
||||
errors {
|
||||
message
|
||||
path
|
||||
code
|
||||
}
|
||||
transaction {
|
||||
settlementVia {
|
||||
... on SettlementViaIntraLedger {
|
||||
preImage
|
||||
}
|
||||
... on SettlementViaLn {
|
||||
preImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
input: {
|
||||
paymentRequest: invoice,
|
||||
walletId
|
||||
}
|
||||
})
|
||||
const status = out.data.lnInvoicePaymentSend.status
|
||||
const errors = out.data.lnInvoicePaymentSend.errors
|
||||
if (errors && errors.length > 0) {
|
||||
throw new Error('failed to pay invoice ' + errors.map(e => e.code + ' ' + e.message).join(', '))
|
||||
}
|
||||
|
||||
// payment was settled immediately
|
||||
if (status === 'SUCCESS') {
|
||||
const preimage = out.data.lnInvoicePaymentSend.transaction.settlementVia.preImage
|
||||
if (!preimage) throw new Error('no preimage')
|
||||
return preimage
|
||||
}
|
||||
|
||||
// payment failed immediately
|
||||
if (status === 'FAILED') {
|
||||
throw new Error('failed to pay invoice')
|
||||
}
|
||||
|
||||
// payment couldn't be settled (or fail) immediately, so we wait for a result
|
||||
if (status === 'PENDING') {
|
||||
while (true) {
|
||||
// at some point it should either be settled or fail on the backend, so the loop will exit
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
const txInfo = await getTxInfo(authToken, wallet, invoice)
|
||||
// settled
|
||||
if (txInfo.status === 'SUCCESS') {
|
||||
if (!txInfo.preImage) throw new Error('no preimage')
|
||||
return txInfo.preImage
|
||||
}
|
||||
// failed
|
||||
if (txInfo.status === 'FAILED') {
|
||||
throw new Error(txInfo.error || 'failed to pay invoice')
|
||||
}
|
||||
// still pending
|
||||
// retry later
|
||||
}
|
||||
}
|
||||
|
||||
// this should never happen
|
||||
throw new Error('unexpected error')
|
||||
}
|
||||
|
||||
async function getTxInfo (authToken, wallet, invoice) {
|
||||
const walletId = wallet.id
|
||||
let out
|
||||
try {
|
||||
out = await request(authToken, `
|
||||
query GetTxInfo($walletId: WalletId!, $paymentRequest: LnPaymentRequest!) {
|
||||
me {
|
||||
defaultAccount {
|
||||
walletById(walletId: $walletId) {
|
||||
transactionsByPaymentRequest(paymentRequest: $paymentRequest) {
|
||||
status
|
||||
direction
|
||||
settlementVia {
|
||||
... on SettlementViaIntraLedger {
|
||||
preImage
|
||||
}
|
||||
... on SettlementViaLn {
|
||||
preImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
paymentRequest: invoice,
|
||||
walletId
|
||||
})
|
||||
} catch (e) {
|
||||
// something went wrong during the query,
|
||||
// maybe the connection was lost, so we just return
|
||||
// a pending status, the caller can retry later
|
||||
return {
|
||||
status: 'PENDING',
|
||||
preImage: null,
|
||||
error: ''
|
||||
}
|
||||
}
|
||||
const tx = out.data.me.defaultAccount.walletById.transactionsByPaymentRequest.find(t => t.direction === 'SEND')
|
||||
if (!tx) {
|
||||
// the transaction was not found, something went wrong
|
||||
return {
|
||||
status: 'FAILED',
|
||||
preImage: null,
|
||||
error: 'transaction not found'
|
||||
}
|
||||
}
|
||||
const status = tx.status
|
||||
const preImage = tx.settlementVia.preImage
|
||||
return {
|
||||
status,
|
||||
preImage,
|
||||
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,34 @@
|
|||
import { blinkSchema } from '@/lib/validate'
|
||||
|
||||
export const galoyBlinkUrl = 'https://api.blink.sv/graphql'
|
||||
export const galoyBlinkDashboardUrl = 'https://dashboard.blink.sv/'
|
||||
|
||||
export const name = 'blink'
|
||||
|
||||
export const fields = [
|
||||
{
|
||||
name: 'apiKey',
|
||||
label: 'api key',
|
||||
type: 'password',
|
||||
help: `you can get an API key from [Blink Dashboard](${galoyBlinkDashboardUrl})`,
|
||||
placeholder: 'blink_...'
|
||||
},
|
||||
{
|
||||
name: 'currency',
|
||||
label: 'wallet type',
|
||||
type: 'text',
|
||||
help: 'the blink wallet to use (BTC or USD for stablesats)',
|
||||
placeholder: 'BTC',
|
||||
optional: true,
|
||||
clear: true,
|
||||
autoComplete: 'off'
|
||||
}
|
||||
]
|
||||
|
||||
export const card = {
|
||||
title: 'Blink',
|
||||
subtitle: 'use [Blink](https://blink.sv/) for payments',
|
||||
badges: ['send only']
|
||||
}
|
||||
|
||||
export const fieldValidation = blinkSchema
|
|
@ -5,5 +5,6 @@ import * as lnAddr from 'wallets/lightning-address/client'
|
|||
import * as cln from 'wallets/cln/client'
|
||||
import * as lnd from 'wallets/lnd/client'
|
||||
import * as webln from 'wallets/webln/client'
|
||||
import * as blink from 'wallets/blink/client'
|
||||
|
||||
export default [nwc, lnbits, lnc, lnAddr, cln, lnd, webln]
|
||||
export default [nwc, lnbits, lnc, lnAddr, cln, lnd, webln, blink]
|
||||
|
|
|
@ -187,11 +187,11 @@ function useConfig (wallet) {
|
|||
// Not optimal UX but the trade-off is saving invalid configurations
|
||||
// and maybe it's not that big of an issue.
|
||||
if (hasClientConfig) {
|
||||
const newClientConfig = extractClientConfig(wallet.fields, newConfig)
|
||||
let newClientConfig = extractClientConfig(wallet.fields, newConfig)
|
||||
|
||||
let valid = true
|
||||
try {
|
||||
await walletValidate(wallet, newClientConfig)
|
||||
newClientConfig = await walletValidate(wallet, newClientConfig)
|
||||
} catch {
|
||||
valid = false
|
||||
}
|
||||
|
@ -204,11 +204,11 @@ function useConfig (wallet) {
|
|||
}
|
||||
}
|
||||
if (hasServerConfig) {
|
||||
const newServerConfig = extractServerConfig(wallet.fields, newConfig)
|
||||
let newServerConfig = extractServerConfig(wallet.fields, newConfig)
|
||||
|
||||
let valid = true
|
||||
try {
|
||||
await walletValidate(wallet, newServerConfig)
|
||||
newServerConfig = await walletValidate(wallet, newServerConfig)
|
||||
} catch {
|
||||
valid = false
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue