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

Lnadrr meta #728

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions proto/autogenerated/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,7 @@ The nostr server will send back a message response, and inside the body there wi
- __identifier__: _string_
- __info__: _[UserInfo](#UserInfo)_
- __max_withdrawable__: _number_
- __noffer__: _string_

### Application
- __balance__: _number_
Expand Down Expand Up @@ -897,6 +898,7 @@ The nostr server will send back a message response, and inside the body there wi
### Product
- __id__: _string_
- __name__: _string_
- __noffer__: _string_
- __price_sats__: _number_

### RelaysMigration
Expand Down
10 changes: 10 additions & 0 deletions proto/autogenerated/ts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,13 +510,15 @@ export type AppUser = {
identifier: string
info: UserInfo
max_withdrawable: number
noffer: string
}
export const AppUserOptionalFields: [] = []
export type AppUserOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
identifier_CustomCheck?: (v: string) => boolean
info_Options?: UserInfoOptions
max_withdrawable_CustomCheck?: (v: number) => boolean
noffer_CustomCheck?: (v: string) => boolean
}
export const AppUserValidate = (o?: AppUser, opts: AppUserOptions = {}, path: string = 'AppUser::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
Expand All @@ -532,6 +534,9 @@ export const AppUserValidate = (o?: AppUser, opts: AppUserOptions = {}, path: st
if (typeof o.max_withdrawable !== 'number') return new Error(`${path}.max_withdrawable: is not a number`)
if (opts.max_withdrawable_CustomCheck && !opts.max_withdrawable_CustomCheck(o.max_withdrawable)) return new Error(`${path}.max_withdrawable: custom check failed`)

if (typeof o.noffer !== 'string') return new Error(`${path}.noffer: is not a string`)
if (opts.noffer_CustomCheck && !opts.noffer_CustomCheck(o.noffer)) return new Error(`${path}.noffer: custom check failed`)

return null
}

Expand Down Expand Up @@ -1977,13 +1982,15 @@ export const PaymentStateValidate = (o?: PaymentState, opts: PaymentStateOptions
export type Product = {
id: string
name: string
noffer: string
price_sats: number
}
export const ProductOptionalFields: [] = []
export type ProductOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
id_CustomCheck?: (v: string) => boolean
name_CustomCheck?: (v: string) => boolean
noffer_CustomCheck?: (v: string) => boolean
price_sats_CustomCheck?: (v: number) => boolean
}
export const ProductValidate = (o?: Product, opts: ProductOptions = {}, path: string = 'Product::root.'): Error | null => {
Expand All @@ -1996,6 +2003,9 @@ export const ProductValidate = (o?: Product, opts: ProductOptions = {}, path: st
if (typeof o.name !== 'string') return new Error(`${path}.name: is not a string`)
if (opts.name_CustomCheck && !opts.name_CustomCheck(o.name)) return new Error(`${path}.name: custom check failed`)

if (typeof o.noffer !== 'string') return new Error(`${path}.noffer: is not a string`)
if (opts.noffer_CustomCheck && !opts.noffer_CustomCheck(o.noffer)) return new Error(`${path}.noffer: custom check failed`)

if (typeof o.price_sats !== 'number') return new Error(`${path}.price_sats: is not a number`)
if (opts.price_sats_CustomCheck && !opts.price_sats_CustomCheck(o.price_sats)) return new Error(`${path}.price_sats: custom check failed`)

Expand Down
2 changes: 2 additions & 0 deletions proto/service/structs.proto
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ message AppUser {
string identifier = 1;
UserInfo info = 2;
int64 max_withdrawable = 3;
string noffer = 4;
}

message AddAppInvoiceRequest {
Expand Down Expand Up @@ -409,6 +410,7 @@ message Product {
string id = 1;
string name = 2;
int64 price_sats = 3;
string noffer = 4;
}

message GetProductBuyLinkResponse {
Expand Down
78 changes: 50 additions & 28 deletions src/custom-nip19.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/*
This file contains functions that deal with encoding and decoding nprofiles,
but with he addition of bridge urls in the nprofile.
These functions are basically the same functions from nostr-tools package
but with some tweaks to allow for the bridge inclusion.
This file contains functions that deal with encoding and decoding nprofiles,
but with he addition of bridge urls in the nprofile.
These functions are basically the same functions from nostr-tools package
but with some tweaks to allow for the bridge inclusion.
*/
import { bytesToHex, concatBytes, hexToBytes } from '@noble/hashes/utils';
import { bech32 } from 'bech32';
import { LoadNosrtSettingsFromEnv } from './services/nostr/index.js';

export const utf8Decoder = new TextDecoder('utf-8')
export const utf8Encoder = new TextEncoder()
Expand All @@ -14,7 +15,13 @@ export const utf8Encoder = new TextEncoder()
export type CustomProfilePointer = {
pubkey: string
relays?: string[]
bridge?: string[] // one bridge
bridge?: string[] // one bridge
}

export type OfferPointer = {
pubkey: string,
relays?: string[],
offer: string
}


Expand All @@ -23,15 +30,15 @@ type TLV = { [t: number]: Uint8Array[] }


const encodeTLV = (tlv: TLV): Uint8Array => {
const entries: Uint8Array[] = []
const entries: Uint8Array[] = []

Object.entries(tlv)
/*
the original function does a reverse() here,
but here it causes the nprofile string to be different,
even though it would still decode to the correct original inputs
*/
//.reverse()
/*
the original function does a reverse() here,
but here it causes the nprofile string to be different,
even though it would still decode to the correct original inputs
*/
//.reverse()
.forEach(([t, vs]) => {
vs.forEach(v => {
const entry = new Uint8Array(v.length + 2)
Expand All @@ -41,19 +48,34 @@ const encodeTLV = (tlv: TLV): Uint8Array => {
entries.push(entry)
})
})
return concatBytes(...entries);
return concatBytes(...entries);
}

export const encodeNprofile = (profile: CustomProfilePointer): string => {
const data = encodeTLV({
const data = encodeTLV({
0: [hexToBytes(profile.pubkey)],
1: (profile.relays || []).map(url => utf8Encoder.encode(url)),
2: (profile.bridge || []).map(url => utf8Encoder.encode(url))
2: (profile.bridge || []).map(url => utf8Encoder.encode(url))
});
const words = bech32.toWords(data)
const words = bech32.toWords(data)
return bech32.encode("nprofile", words, 5000);
}

export const encodeNoffer = (offer: OfferPointer): string => {
let relays = offer.relays
if (!relays) {
const settings = LoadNosrtSettingsFromEnv()
relays = settings.relays
}
const data = encodeTLV({
0: [hexToBytes(offer.pubkey)],
1: (relays).map(url => utf8Encoder.encode(url)),
2: [utf8Encoder.encode(offer.offer)]
});
const words = bech32.toWords(data)
return bech32.encode("noffer", words, 5000);
}

const parseTLV = (data: Uint8Array): TLV => {
const result: TLV = {}
let rest = data
Expand All @@ -70,19 +92,19 @@ const parseTLV = (data: Uint8Array): TLV => {
}

export const decodeNprofile = (nprofile: string): CustomProfilePointer => {
const { prefix, words } = bech32.decode(nprofile, 5000)
if (prefix !== "nprofile") {
throw new Error ("Expected nprofile prefix");
}
const { prefix, words } = bech32.decode(nprofile, 5000)
if (prefix !== "nprofile") {
throw new Error("Expected nprofile prefix");
}
const data = new Uint8Array(bech32.fromWords(words))

const tlv = parseTLV(data);
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for nprofile')
if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes')
const tlv = parseTLV(data);
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for nprofile')
if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes')

return {
pubkey: bytesToHex(tlv[0][0]),
relays: tlv[1] ? tlv[1].map(d => utf8Decoder.decode(d)) : [],
bridge: tlv[2] ? tlv[2].map(d => utf8Decoder.decode(d)): []
}
return {
pubkey: bytesToHex(tlv[0][0]),
relays: tlv[1] ? tlv[1].map(d => utf8Decoder.decode(d)) : [],
bridge: tlv[2] ? tlv[2].map(d => utf8Decoder.decode(d)) : []
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const start = async () => {
log("manual process ended")
return
}

const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn
const serverMethods = GetServerMethods(mainHandler)
const nostrSettings = LoadNosrtSettingsFromEnv()
Expand Down
49 changes: 48 additions & 1 deletion src/nostrMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import Main from "./services/main/index.js"
import Nostr from "./services/nostr/index.js"
import { NostrSend, NostrSettings } from "./services/nostr/handler.js"
import { NostrEvent, NostrSend, NostrSettings } from "./services/nostr/handler.js"
import * as Types from '../proto/autogenerated/ts/types.js'
import NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js';
import { ERROR, getLogger } from "./services/helpers/logger.js";
import { UnsignedEvent } from "./services/nostr/tools/event.js";
import { defaultInvoiceExpiry } from "./services/storage/paymentStorage.js";
import { Application } from "./services/storage/entity/Application.js";

export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings, onClientEvent: (e: { requestId: string }, fromPub: string) => void): { Stop: () => void, Send: NostrSend } => {
const log = getLogger({})
Expand Down Expand Up @@ -45,6 +48,12 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
log(ERROR, "invalid json event received", event.content)
return
}
if (event.kind === 21001) {
const offerReq = j as { offer: string }
handleNofferEvent(mainHandler, offerReq, event)
.then(e => nostr.Send({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }))
return
}
if (!j.rpcName) {
onClientEvent(j as { requestId: string }, event.pub)
return
Expand All @@ -59,3 +68,41 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
})
return { Stop: () => nostr.Stop, Send: (...args) => nostr.Send(...args) }
}

// TODO: move this to paymentManager
const handleNofferEvent = async (mainHandler: Main, offerReq: { offer: string }, event: NostrEvent): Promise<UnsignedEvent> => {
const app = await mainHandler.storage.applicationStorage.GetApplication(event.appId)
try {
const offer = offerReq.offer
let invoice: string
const split = offer.split(':')
if (split.length === 1) {
const user = await mainHandler.storage.applicationStorage.GetApplicationUser(app, split[0])
//TODO: add prop def for amount
const userInvoice = await mainHandler.paymentManager.NewInvoice(user.user.user_id, { amountSats: 1000, memo: "free offer" }, { expiry: defaultInvoiceExpiry, linkedApplication: app })
invoice = userInvoice.invoice
} else if (split[0] === 'p') {
const product = await mainHandler.productManager.NewProductInvoice(split[1])
invoice = product.invoice
} else {
return newNofferResponse(JSON.stringify({ code: 1, message: 'Invalid Offer' }), app, event)
}
return newNofferResponse(JSON.stringify({ bolt11: invoice }), app, event)
} catch (e: any) {
getLogger({ component: "noffer" })(ERROR, e.message || e)
return newNofferResponse(JSON.stringify({ code: 1, message: 'Invalid Offer' }), app, event)
}
}

const newNofferResponse = (content: string, app: Application, event: NostrEvent): UnsignedEvent => {
return {
content,
created_at: Math.floor(Date.now() / 1000),
kind: 21001,
pubkey: app.nostr_public_key!,
tags: [
['p', event.pub],
['e', event.id],
],
}
}
6 changes: 4 additions & 2 deletions src/services/main/applicationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ApplicationUser } from '../storage/entity/ApplicationUser.js'
import { PubLogger, getLogger } from '../helpers/logger.js'
import crypto from 'crypto'
import { Application } from '../storage/entity/Application.js'
import { encodeNoffer } from '../../custom-nip19.js'

const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds

Expand Down Expand Up @@ -160,6 +161,7 @@ export default class {
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps

},
noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: u.identifier }),
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true)
}
}
Expand Down Expand Up @@ -197,7 +199,7 @@ export default class {
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps
}
}, noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: user.identifier })
}
}

Expand Down Expand Up @@ -226,7 +228,7 @@ export default class {
async GetAppUserLNURLInfo(appId: string, req: Types.GetAppUserLNURLInfoRequest): Promise<Types.LnurlPayInfoResponse> {
const app = await this.storage.applicationStorage.GetApplication(appId)
const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier)
return this.paymentManager.GetLnurlPayInfoFromUser(user.user.user_id, app, req.base_url_override)
return this.paymentManager.GetLnurlPayInfoFromUser(user.user.user_id, app, { baseUrl: req.base_url_override })
}
async RequestNPubLinkingToken(appId: string, req: Types.RequestNPubLinkingTokenRequest, reset: boolean): Promise<Types.RequestNPubLinkingTokenResponse> {
const app = await this.storage.applicationStorage.GetApplication(appId);
Expand Down
12 changes: 7 additions & 5 deletions src/services/main/paymentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ interface UserOperationInfo {
}
export type PendingTx = { type: 'incoming', tx: AddressReceivingTransaction } | { type: 'outgoing', tx: UserTransactionPayment }
const defaultLnurlPayMetadata = `[["text/plain", "lnurl pay to Lightning.pub"]]`
const defaultLnAddressMetadata = (id: string) => `[["text/plain", "lnurl pay to Lightning.pub"],["text/identifier", "${id}"]]`
const confInOne = 1000 * 1000
const confInTwo = 100 * 1000 * 1000
export default class {
Expand Down Expand Up @@ -135,7 +136,7 @@ export default class {
//})
return
}
console.log({p})
console.log({ p })
const paymentRes = await this.lnd.GetPayment(p.paymentIndex)
const payment = paymentRes.payments[0]
if (!payment || Number(payment.paymentIndex) !== p.paymentIndex) {
Expand Down Expand Up @@ -337,7 +338,7 @@ export default class {
const pendingPayment = await this.storage.txQueue.PushToQueue({
dbTx: true, description: "payment started", exec: async tx => {
await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice, tx)
return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider,tx)
return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider, tx)
}
})
this.log("ready to pay")
Expand Down Expand Up @@ -513,10 +514,11 @@ export default class {
}
}

async GetLnurlPayInfoFromUser(userId: string, linkedApplication: Application, baseUrl?: string): Promise<Types.LnurlPayInfoResponse> {
async GetLnurlPayInfoFromUser(userId: string, linkedApplication: Application, opts: { baseUrl?: string, metadata?: string } = {}): Promise<Types.LnurlPayInfoResponse> {
if (this.isDefaultServiceUrl()) {
throw new Error("Lnurl not enabled. Make sure to set SERVICE_URL env variable")
}
const { baseUrl, metadata } = opts
const payK1 = await this.storage.paymentStorage.AddUserEphemeralKey(userId, 'pay', linkedApplication)
const url = baseUrl ? baseUrl : `${this.settings.serviceUrl}/api/guest/lnurl_pay/handle`
const { remote } = await this.lnd.ChannelBalance()
Expand All @@ -525,7 +527,7 @@ export default class {
callback: `${url}?k1=${payK1.key}`,
maxSendable: remote * 1000,
minSendable: 10000,
metadata: defaultLnurlPayMetadata,
metadata: metadata ? metadata : defaultLnurlPayMetadata,
allowsNostr: !!linkedApplication.nostr_public_key,
nostrPubkey: linkedApplication.nostr_public_key || ""
}
Expand Down Expand Up @@ -634,7 +636,7 @@ export default class {
if (!linkedUser) {
throw new Error("this address is not linked to any user")
}
return this.GetLnurlPayInfoFromUser(linkedUser.user.user_id, linkedUser.application)
return this.GetLnurlPayInfoFromUser(linkedUser.user.user_id, linkedUser.application, { metadata: defaultLnAddressMetadata(addressName) })
}

async OpenChannel(userId: string, req: Types.OpenChannelRequest): Promise<Types.OpenChannelResponse> { throw new Error("WIP") }
Expand Down
Loading
Loading