diff --git a/prisma/migrations/20250106164328_zebedee_sender/migration.sql b/prisma/migrations/20250106164328_zebedee_sender/migration.sql new file mode 100644 index 000000000..973f61eac --- /dev/null +++ b/prisma/migrations/20250106164328_zebedee_sender/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "WalletType" ADD VALUE 'ZEBEDEE'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dcdb3b938..6ef4ed060 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -193,6 +193,7 @@ enum WalletType { BLINK LNC WEBLN + ZEBEDEE } model Wallet { diff --git a/public/wallets/zbd-dark.svg b/public/wallets/zbd-dark.svg new file mode 100644 index 000000000..d2b03bd9a --- /dev/null +++ b/public/wallets/zbd-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/wallets/zbd.svg b/public/wallets/zbd.svg new file mode 100644 index 000000000..b92c3ff5c --- /dev/null +++ b/public/wallets/zbd.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/wallets/client.js b/wallets/client.js index 8bd44698f..e5e5c3287 100644 --- a/wallets/client.js +++ b/wallets/client.js @@ -7,5 +7,6 @@ import * as lnd from '@/wallets/lnd/client' import * as webln from '@/wallets/webln/client' import * as blink from '@/wallets/blink/client' import * as phoenixd from '@/wallets/phoenixd/client' +import * as zebedee from '@/wallets/zebedee/client' -export default [nwc, lnbits, lnc, lnAddr, cln, lnd, webln, blink, phoenixd] +export default [nwc, lnbits, lnc, lnAddr, cln, lnd, webln, blink, phoenixd, zebedee] diff --git a/wallets/server.js b/wallets/server.js index f14e9fb36..fd96aa0cb 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -10,6 +10,7 @@ import * as blink from '@/wallets/blink/server' // we import only the metadata of client side wallets import * as lnc from '@/wallets/lnc' import * as webln from '@/wallets/webln' +import * as zebedee from '@/wallets/zebedee' import { walletLogger } from '@/api/resolvers/wallet' import walletDefs from '@/wallets/server' @@ -20,7 +21,7 @@ import { timeoutSignal, withTimeout } from '@/lib/time' import { canReceive } from './common' import wrapInvoice from './wrap' -export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln] +export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln, zebedee] const MAX_PENDING_INVOICES_PER_WALLET = 25 diff --git a/wallets/zebedee/client.js b/wallets/zebedee/client.js new file mode 100644 index 000000000..d5375d72e --- /dev/null +++ b/wallets/zebedee/client.js @@ -0,0 +1,73 @@ +import { API_URL } from '@/wallets/zebedee' +import { assertContentTypeJson } from '@/lib/url' +export * from '@/wallets/zebedee' + +export async function testSendPayment ({ apiKey }, { signal }) { + const wallet = await apiCall('wallet', { apiKey, method: 'GET' }, { signal }) + if (!wallet.data) throw new Error('wallet not found') +} + +export async function sendPayment (bolt11, { apiKey }, { signal }) { + const res = await apiCall('payments', { body: { invoice: bolt11 }, apiKey }, { signal }) + if (!res?.data) throw new Error('payment failed') + + const { id, preimage } = res.data + if (preimage) return preimage + + // the api might return before the invoice is paid, so we'll wait for the preimage + return await waitForPreimage(id, { apiKey }, { signal }) +} + +async function waitForPreimage (id, { apiKey }, { signal }) { + while (!signal.aborted) { + const res = await apiCall('payments/{id}', { body: { id }, apiKey, method: 'GET' }, { signal }) + + // return preimage if it's available + const preimage = res?.data?.preimage + if (preimage) return preimage + + // wait a before checking again + await new Promise(resolve => setTimeout(resolve, 30)) + } + return null +} + +export async function apiCall (api, { body, apiKey, method = 'POST' }, { signal }) { + // if get request, put params into the url + if (method === 'GET' && body) { + for (const [k, v] of Object.entries(body)) { + api = api.replace(`{${k}}`, v) + } + } + + const res = await fetch(API_URL + api, { + method, + headers: { + apikey: apiKey, + 'Content-Type': 'application/json' + }, + signal, + body: method === 'POST' ? JSON.stringify(body) : undefined + }) + + // Catch errors + // ref: https://zbd.dev/api-reference/errors + if (res.status < 200 || res.status > 299) { + // try to extract the error message from the response + let error + try { + assertContentTypeJson(res) + const json = await res.json() + if (json?.message) error = json.message + } catch (e) { + console.log('failed to parse error', e) + } + + // throw the error, if we don't have one, we try to use the request status + if (!error) error = res.statusText || `error ${res.status}` + throw new Error(error) + } + + assertContentTypeJson(res) + return await res.json() +} diff --git a/wallets/zebedee/index.js b/wallets/zebedee/index.js new file mode 100644 index 000000000..15b497804 --- /dev/null +++ b/wallets/zebedee/index.js @@ -0,0 +1,26 @@ +import { string } from '@/lib/yup' + +export const DASHBOARD_URL = 'https://dashboard.zebedee.io/' +export const API_URL = 'https://api.zebedee.io/v0/' + +export const name = 'zebedee' +export const walletType = 'ZEBEDEE' +export const walletField = 'walletZebedee' + +export const fields = [ + { + name: 'apiKey', + label: 'api key', + type: 'password', + optional: 'for sending', + help: `you can get an API key from [Zebedee Dashboard](${DASHBOARD_URL}) from \n\`Project->API->Live\``, + clientOnly: true, + validate: string().min(8, 'invalid api key').max(64, 'api key is too long') + } +] + +export const card = { + title: 'Zebedee', + subtitle: 'use [Zebedee](https://zebedee.io) for payments', + image: { src: '/wallets/zbd.svg' } +}