Merge pull request #844 from shocknet/blinded-paths

Blinded paths
This commit is contained in:
Justin (shocknet) 2025-10-06 14:06:35 -04:00 committed by GitHub
commit 517d08408c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 908 additions and 869 deletions

View file

@ -1385,6 +1385,7 @@ The nostr server will send back a message response, and inside the body there wi
### NewInvoiceRequest
- __amountSats__: _number_
- __blind__: _boolean_ *this field is optional
- __expiry__: _number_ *this field is optional
- __memo__: _string_
- __zap__: _string_ *this field is optional
@ -1393,6 +1394,7 @@ The nostr server will send back a message response, and inside the body there wi
- __invoice__: _string_
### OfferConfig
- __blind__: _boolean_ *this field is optional
- __callback_url__: _string_
- __createdAtUnix__: _number_
- __default_offer__: _boolean_

View file

@ -470,6 +470,7 @@ type NewAddressResponse struct {
}
type NewInvoiceRequest struct {
Amountsats int64 `json:"amountSats"`
Blind bool `json:"blind"`
Expiry int64 `json:"expiry"`
Memo string `json:"memo"`
Zap string `json:"zap"`
@ -478,6 +479,7 @@ type NewInvoiceResponse struct {
Invoice string `json:"invoice"`
}
type OfferConfig struct {
Blind bool `json:"blind"`
Callback_url string `json:"callback_url"`
Createdatunix int64 `json:"createdAtUnix"`
Default_offer bool `json:"default_offer"`

View file

@ -2768,15 +2768,17 @@ export const NewAddressResponseValidate = (o?: NewAddressResponse, opts: NewAddr
export type NewInvoiceRequest = {
amountSats: number
blind?: boolean
expiry?: number
memo: string
zap?: string
}
export type NewInvoiceRequestOptionalField = 'expiry' | 'zap'
export const NewInvoiceRequestOptionalFields: NewInvoiceRequestOptionalField[] = ['expiry', 'zap']
export type NewInvoiceRequestOptionalField = 'blind' | 'expiry' | 'zap'
export const NewInvoiceRequestOptionalFields: NewInvoiceRequestOptionalField[] = ['blind', 'expiry', 'zap']
export type NewInvoiceRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: NewInvoiceRequestOptionalField[]
amountSats_CustomCheck?: (v: number) => boolean
blind_CustomCheck?: (v?: boolean) => boolean
expiry_CustomCheck?: (v?: number) => boolean
memo_CustomCheck?: (v: string) => boolean
zap_CustomCheck?: (v?: string) => boolean
@ -2788,6 +2790,9 @@ export const NewInvoiceRequestValidate = (o?: NewInvoiceRequest, opts: NewInvoic
if (typeof o.amountSats !== 'number') return new Error(`${path}.amountSats: is not a number`)
if (opts.amountSats_CustomCheck && !opts.amountSats_CustomCheck(o.amountSats)) return new Error(`${path}.amountSats: custom check failed`)
if ((o.blind || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('blind')) && typeof o.blind !== 'boolean') return new Error(`${path}.blind: is not a boolean`)
if (opts.blind_CustomCheck && !opts.blind_CustomCheck(o.blind)) return new Error(`${path}.blind: custom check failed`)
if ((o.expiry || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('expiry')) && typeof o.expiry !== 'number') return new Error(`${path}.expiry: is not a number`)
if (opts.expiry_CustomCheck && !opts.expiry_CustomCheck(o.expiry)) return new Error(`${path}.expiry: custom check failed`)
@ -2819,6 +2824,7 @@ export const NewInvoiceResponseValidate = (o?: NewInvoiceResponse, opts: NewInvo
}
export type OfferConfig = {
blind?: boolean
callback_url: string
createdAtUnix: number
default_offer: boolean
@ -2831,9 +2837,11 @@ export type OfferConfig = {
token: string
updatedAtUnix: number
}
export const OfferConfigOptionalFields: [] = []
export type OfferConfigOptionalField = 'blind'
export const OfferConfigOptionalFields: OfferConfigOptionalField[] = ['blind']
export type OfferConfigOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
checkOptionalsAreSet?: OfferConfigOptionalField[]
blind_CustomCheck?: (v?: boolean) => boolean
callback_url_CustomCheck?: (v: string) => boolean
createdAtUnix_CustomCheck?: (v: number) => boolean
default_offer_CustomCheck?: (v: boolean) => boolean
@ -2850,6 +2858,9 @@ export const OfferConfigValidate = (o?: OfferConfig, opts: OfferConfigOptions =
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if ((o.blind || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('blind')) && typeof o.blind !== 'boolean') return new Error(`${path}.blind: is not a boolean`)
if (opts.blind_CustomCheck && !opts.blind_CustomCheck(o.blind)) return new Error(`${path}.blind: custom check failed`)
if (typeof o.callback_url !== 'string') return new Error(`${path}.callback_url: is not a string`)
if (opts.callback_url_CustomCheck && !opts.callback_url_CustomCheck(o.callback_url)) return new Error(`${path}.callback_url: custom check failed`)

View file

@ -450,6 +450,7 @@ message NewInvoiceRequest{
string memo = 2;
optional string zap = 3;
optional int64 expiry = 4;
optional bool blind = 5;
}
message NewInvoiceResponse{
@ -785,6 +786,7 @@ message OfferConfig {
bool rejectUnauthorized = 9;
int64 createdAtUnix = 10;
int64 updatedAtUnix = 11;
optional bool blind = 12;
}
message UserOffers {

View file

@ -1,9 +1,9 @@
import { OpenChannelRequest, Invoice } from "../../../proto/lnd/lightning";
export const AddInvoiceReq = (value: number, expiry = 60 * 60, privateHints = false, memo?: string): Invoice => ({
export const AddInvoiceReq = (value: number, expiry = 60 * 60, privateHints = false, memo?: string, blind = false): Invoice => ({
expiry: BigInt(expiry),
memo: memo || "",
private: privateHints,
private: blind ? false : privateHints,
value: BigInt(value),
fallbackAddr: "",
@ -28,7 +28,7 @@ export const AddInvoiceReq = (value: number, expiry = 60 * 60, privateHints = fa
settleDate: 0n,
settleIndex: 0n,
state: 0,
isBlinded: false,
isBlinded: blind,
amtPaid: 0n,
settled: false,
})

View file

@ -288,7 +288,7 @@ export default class {
}
}
async NewInvoice(value: number, memo: string, expiry: number, { useProvider, from }: TxActionOptions): Promise<Invoice> {
async NewInvoice(value: number, memo: string, expiry: number, { useProvider, from }: TxActionOptions, blind = false): Promise<Invoice> {
if (useProvider) {
console.log("using provider")
const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry)
@ -296,7 +296,7 @@ export default class {
return { payRequest: invoice, providerDst }
}
try {
const res = await this.lightning.addInvoice(AddInvoiceReq(value, expiry, true, memo), DeadLineMetadata())
const res = await this.lightning.addInvoice(AddInvoiceReq(value, expiry, true, memo, blind), DeadLineMetadata())
this.utils.stateBundler.AddTxPoint('addedInvoice', value, { from, used: 'lnd' })
return { payRequest: res.response.paymentRequest }
} catch (err) {

View file

@ -199,7 +199,8 @@ export default class {
const opts: InboundOptionals = {
callbackUrl: cbUrl, expiry: expiry, expectedPayer: payer.user, linkedApplication: app, zapInfo,
offerId: req.offer_string, payerData: req.payer_data?.data, rejectUnauthorized: req.rejectUnauthorized,
token: req.token
token: req.token,
blind: req.invoice_req.blind
}
const appUserInvoice = await this.paymentManager.NewInvoice(receiver.user.user_id, req.invoice_req, opts)
return {

View file

@ -26,7 +26,8 @@ const mapToOfferConfig = (appUserId: string, offer: UserOffer, { pubkey, relay }
createdAtUnix: offer.created_at.getTime(),
updatedAtUnix: offer.updated_at.getTime(),
token: offer.bearer_token,
rejectUnauthorized: offer.rejectUnauthorized
rejectUnauthorized: offer.rejectUnauthorized,
blind: offer.blind
}
}
export class OfferManager {
@ -65,6 +66,7 @@ export class OfferManager {
label: req.label,
price_sats: req.price_sats,
callback_url: req.callback_url,
blind: req.blind,
})
return {
offer_id: newOffer.offer_id
@ -81,6 +83,7 @@ export class OfferManager {
label: req.label,
price_sats: req.price_sats,
callback_url: req.callback_url,
blind: req.blind,
})
}
async GetUserOfferInvoices(ctx: Types.UserContext, req: Types.GetUserOfferInvoicesReq): Promise<Types.OfferInvoices> {
@ -242,7 +245,7 @@ export class OfferManager {
const memo = offerReq.description || userOffer.label
const res = await this.applicationManager.AddAppUserInvoice(appId, {
http_callback_url: userOffer.callback_url, payer_identifier: userOffer.app_user_id, receiver_identifier: userOffer.app_user_id,
invoice_req: { amountSats: amt, memo, zap: offerReq.zap, expiry },
invoice_req: { amountSats: amt, memo, zap: offerReq.zap, expiry, blind: userOffer.blind },
payer_data: validated ? { data: validated } : undefined,
offer_string: offer,
rejectUnauthorized: userOffer.rejectUnauthorized,

View file

@ -223,7 +223,7 @@ export default class {
throw new Error("user is banned, cannot generate invoice")
}
const use = await this.liquidityManager.beforeInvoiceCreation(req.amountSats)
const res = await this.lnd.NewInvoice(req.amountSats, req.memo, options.expiry, { useProvider: use === 'provider', from: 'user' })
const res = await this.lnd.NewInvoice(req.amountSats, req.memo, options.expiry, { useProvider: use === 'provider', from: 'user' }, req.blind)
const userInvoice = await this.storage.paymentStorage.AddUserInvoice(user, res.payRequest, options, res.providerDst)
const appId = options.linkedApplication ? options.linkedApplication.app_id : ""
this.storage.eventsLog.LogEvent({ type: 'new_invoice', userId: user.user_id, appUserId: "", appId, balance: user.balance_sats, data: userInvoice.invoice, amount: req.amountSats })

View file

@ -38,6 +38,9 @@ export class UserOffer {
@Column({ default: true })
rejectUnauthorized: boolean
@Column({ default: false })
blind: boolean
@CreateDateColumn()
created_at: Date

View file

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddBlindToUserOffer1760000000000 implements MigrationInterface {
name = 'AddBlindToUserOffer1760000000000'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_offer" ADD COLUMN "blind" boolean NOT NULL DEFAULT (0)`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_offer" DROP COLUMN "blind"`);
}
}

View file

@ -24,12 +24,13 @@ import { AppUserDevice1753285173175 } from './1753285173175-app_user_device.js'
import { OldSomethingLeftover1753106599604 } from './1753106599604-old_something_leftover.js'
import { UserReceivingInvoiceIdx1753109184611 } from './1753109184611-user_receiving_invoice_idx.js'
import { UserAccess1759426050669 } from './1759426050669-user_access.js'
import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_user_offer.js'
export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264,
DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, UserAccess1759426050669]
InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000]
export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411]
/* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {

View file

@ -14,7 +14,7 @@ import { Application } from './entity/Application.js';
import TransactionsQueue from "./db/transactionsQueue.js";
import { LoggedEvent } from './eventsLog.js';
import { StorageInterface } from './db/storageInterface.js';
export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record<string, string>, rejectUnauthorized?: boolean, token?: string }
export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record<string, string>, rejectUnauthorized?: boolean, token?: string, blind?: boolean }
export const defaultInvoiceExpiry = 60 * 60
export default class {
dbs: StorageInterface