Skip to content

Commit

Permalink
Blink wallet sending attachment (#1293)
Browse files Browse the repository at this point in the history
* blink attachment

* support staging

* add staging dashboard link

* Revert "add staging dashboard link"

This reverts commit a43fa22.

* Revert "support staging"

This reverts commit 93c15aa.

* 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 <[email protected]>
  • Loading branch information
riccardobl and ekzyis authored Aug 18, 2024
1 parent 3d8ae4a commit 2d139be
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 5 deletions.
1 change: 1 addition & 0 deletions contributors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ mz
btcbagehot
felipe
benalleng
rblb
9 changes: 9 additions & 0 deletions lib/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
188 changes: 188 additions & 0 deletions wallets/blink/client.js
Original file line number Diff line number Diff line change
@@ -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()
}
34 changes: 34 additions & 0 deletions wallets/blink/index.js
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion wallets/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
8 changes: 4 additions & 4 deletions wallets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down

0 comments on commit 2d139be

Please sign in to comment.