Skip to content

Commit

Permalink
zbd
Browse files Browse the repository at this point in the history
  • Loading branch information
riccardobl committed Dec 19, 2024
1 parent 4db2edb commit 8b4b35c
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 2 deletions.
3 changes: 3 additions & 0 deletions fragments/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ export const WALLET_FIELDS = gql`
apiKeyRecv
currencyRecv
}
... on WalletZebedee {
gamerTagId
}
}
}
`
Expand Down
24 changes: 24 additions & 0 deletions prisma/migrations/20241219120508_zebedee_attachment/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-- AlterEnum
ALTER TYPE "WalletType" ADD VALUE 'ZEBEDEE';

-- CreateTable
CREATE TABLE "WalletZebedee" (
"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,
"gamerTagId" TEXT,

CONSTRAINT "WalletZebedee_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "WalletZebedee_walletId_key" ON "WalletZebedee"("walletId");

-- AddForeignKey
ALTER TABLE "WalletZebedee" ADD CONSTRAINT "WalletZebedee_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- Update wallet json
CREATE TRIGGER wallet_zebedee_as_jsonb
AFTER INSERT OR UPDATE ON "WalletZebedee"
FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb();
11 changes: 11 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ enum WalletType {
BLINK
LNC
WEBLN
ZEBEDEE
}

model Wallet {
Expand Down Expand Up @@ -216,6 +217,7 @@ model Wallet {
walletNWC WalletNWC?
walletPhoenixd WalletPhoenixd?
walletBlink WalletBlink?
walletZebedee WalletZebedee?
vaultEntries VaultEntry[] @relation("VaultEntries")
withdrawals Withdrawl[]
Expand Down Expand Up @@ -325,6 +327,15 @@ model WalletPhoenixd {
secondaryPassword String?
}

model WalletZebedee {
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")
gamerTagId String?
}

model Mute {
muterId Int
mutedId Int
Expand Down
3 changes: 2 additions & 1 deletion wallets/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
3 changes: 2 additions & 1 deletion wallets/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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 * as zebedee from '@/wallets/zebedee/server'

// we import only the metadata of client side wallets
import * as lnc from '@/wallets/lnc'
Expand All @@ -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

Expand Down
62 changes: 62 additions & 0 deletions wallets/zebedee/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { API_URL, PREIMAGE_AWAIT_TIMEOUT_MS } from '@/wallets/zebedee'
import { assertContentTypeJson } from '@/lib/url'
import { withTimeout } from '@/lib/time'
import { fetchWithTimeout } from '@/lib/fetch'

export * from '@/wallets/zebedee'

export async function testSendPayment ({ apiKey }, { signal }) {
const wallet = await apiCall('wallet', {}, { apiKey, method: 'GET' })
if (!wallet.data) throw new Error('wallet not found')
}

export async function sendPayment (bolt11, { apiKey }, { signal }) {
const res = await apiCall('payments', { invoice: bolt11 }, { apiKey })
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 })
}

async function waitForPreimage (id, { apiKey }) {
return await withTimeout(async () => {
let preimage
while (true) {
const res = await apiCall('payments/{id}', { id }, { apiKey, method: 'GET' })
preimage = res?.data?.preimage
if (preimage) break
await new Promise(resolve => setTimeout(resolve, 10))
}
return preimage
}, PREIMAGE_AWAIT_TIMEOUT_MS)
}

export async function apiCall (api, body, { apiKey, method = 'POST' }) {
const headers = {
apikey: apiKey,
'Content-Type': 'application/json'
}
if (method === 'GET') {
for (const [k, v] of Object.entries(body)) {
api = api.replace('{' + k + '}', v)
}
}
const res = await fetchWithTimeout(API_URL + api, {
method,
headers,
body: method === 'POST' ? JSON.stringify(body) : undefined
})
// https://zbd.dev/api-reference/errors
if (res.status !== 200) {
let error
try {
assertContentTypeJson(res)
const json = await res.json()
if (json?.error) error = json.error
} catch (e) {
error = res.statusText || 'error ' + res.status
}
throw new Error(error)
}
return res.json()
}
40 changes: 40 additions & 0 deletions wallets/zebedee/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { string } from '@/lib/yup'

export const PREIMAGE_AWAIT_TIMEOUT_MS = 1_200
export const STATIC_CHARGE_URL = 'https://api.zebedee.io/v0/process-static-charges/'
export const DASHBOARD_URL = 'https://dashboard.zebedee.io/'
export const GAMER_TAG_LNADDR_BASEURL = 'https://zbd.gg/.well-known/lnurlp/'
export const API_URL = 'https://api.zebedee.io/v0/'
export const ZEBEDEE_LNDOMAIN = 'zbd.gg'

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}).`,
clientOnly: true,
requiredWithout: 'gamerTagId',
validate: string()
},
{
name: 'gamerTagId',
label: 'gamer tag or id',
type: 'text',
optional: 'for receiving',
help: `you can find your Gamertag in the [Zebedee Dashboard](${DASHBOARD_URL}) under 'Account' -> 'Gamertag' section, or in the Zebedee app on the Wallet card.\nNote: You can also use your @${ZEBEDEE_LNDOMAIN} Lightning address here.`,
serverOnly: true,
requiredWithout: 'apiKey',
validate: string()
}
]

export const card = {
title: 'Zebedee',
subtitle: 'use [Zebedee](https://zebedee.io) for payments'
}
59 changes: 59 additions & 0 deletions wallets/zebedee/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { GAMER_TAG_LNADDR_BASEURL, STATIC_CHARGE_URL, ZEBEDEE_LNDOMAIN } from '@/wallets/zebedee'
import { fetchWithTimeout } from '@/lib/fetch'
import { assertContentTypeJson } from '@/lib/url'

export * from '@/wallets/zebedee'

async function fetchJson (url) {
let res = await fetchWithTimeout(url)
assertContentTypeJson(res)
if (!res.ok) {
res.text().catch(() => {})
throw new Error(res.statusText || 'error ' + res.status)
}
res = await res.json()
if (res.status?.toLowerCase() === 'error') {
throw new Error(res.reason)
}
return res
}

function isGamerTag (value) {
if (value.endsWith('@' + ZEBEDEE_LNDOMAIN)) return true
return value.length > 0 && value.length < 30
}

export async function fetchGamerId (value) {
if (isGamerTag(value)) {
const [gamerTag, domain] = value.split('@')
if (domain && domain !== ZEBEDEE_LNDOMAIN) throw new Error(`invalid gamer tag: not a @${ZEBEDEE_LNDOMAIN} lightning address`)
const url = GAMER_TAG_LNADDR_BASEURL + gamerTag
try {
const res = await fetchJson(url)
const callback = res.callback
if (!callback) throw new Error('cannot fetch gamer id: ' + (res.statusText || 'error ' + res.status))
const gamerId = callback.substring(callback.lastIndexOf('/') + 1)
return gamerId
} catch (e) {
throw new Error('cannot fetch gamer id: ' + e.message)
}
}
return value
}

export async function testCreateInvoice (credentials, { signal }) {
credentials.gamerTagId = await fetchGamerId(credentials.gamerTagId)
return await createInvoice({ msats: 1000, expiry: 1 }, credentials, { signal })
}

export async function createInvoice ({ msats, description, expiry }, { gamerTagId }, { signal }) {
try {
const url = STATIC_CHARGE_URL + gamerTagId + '?amount=' + msats + '&comment=' + description
const res = await fetchJson(url)
if (!res.pr) throw new Error('cannot fetch invoice')
console.log('INVOICE', res.pr)
return res.pr
} catch (e) {
throw new Error(e.message)
}
}

0 comments on commit 8b4b35c

Please sign in to comment.