Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zebedee send attachment #1742

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
1 change: 1 addition & 0 deletions public/wallets/zbd-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/wallets/zbd.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
63 changes: 63 additions & 0 deletions wallets/zebedee/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { API_URL, PREIMAGE_AWAIT_TIMEOUT_MS } from '@/wallets/zebedee'
import { assertContentTypeJson } from '@/lib/url'
import { callWithTimeout } 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' }, { 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 })
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 }) {
return await callWithTimeout(async () => {
let preimage
while (true) {
const res = await apiCall('payments/{id}', { body: { id }, apiKey, method: 'GET' }, { signal })
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' }, { signal }) {
const headers = {
apikey: apiKey,
'Content-Type': 'application/json'
}
if (method === 'GET' && body) {
for (const [k, v] of Object.entries(body)) {
api = api.replace('{' + k + '}', v)
}
}
const res = await fetchWithTimeout(API_URL + api, {
method,
headers,
signal,
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?.message) error = json.message
} catch (e) {
error = res.statusText || 'error ' + res.status
}
throw new Error(error)
}
return res.json()
}
42 changes: 42 additions & 0 deletions wallets/zebedee/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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}) from \n\`Project->API->Live\``,
clientOnly: true,
requiredWithout: 'gamerTagId',
validate: string()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect we could do a little better for validation.

Same for the gamer tag.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see the zebedee api key today is an alphanumeric 32 characters long string, however there is nothing in the doc that implies this is and always be its shape, so i just added a reasonable min and max length.
Are you happy with that?

},
{
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 \n\`Account->Gamertag\`\n 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',
image: { src: '/wallets/zbd.svg' }

}
Loading
Loading