-
-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4db2edb
commit 8b4b35c
Showing
8 changed files
with
203 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
prisma/migrations/20241219120508_zebedee_attachment/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |