diff --git a/datasource.js b/datasource.js index f4dd3915..949c21f7 100644 --- a/datasource.js +++ b/datasource.js @@ -26,13 +26,14 @@ import { PaymentIndex1721760297610 } from './build/src/services/storage/migratio import { DebitAccess1726496225078 } from './build/src/services/storage/migrations/1726496225078-debit_access.js' import { DebitAccessFixes1726685229264 } from './build/src/services/storage/migrations/1726685229264-debit_access_fixes.js' import { DebitToPub1727105758354 } from './build/src/services/storage/migrations/1727105758354-debit_to_pub.js' +import { UserCbUrl1727112281043 } from './build/src/services/storage/migrations/1727112281043-user_cb_url.js' export default new DataSource({ type: "sqlite", database: "db.sqlite", // logging: true, - migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354], + migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043], entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, TrackedProvider, InviteToken, DebitAccess], // synchronize: true, }) -//npx typeorm migration:generate ./src/services/storage/migrations/debit_to_pub -d ./datasource.js \ No newline at end of file +//npx typeorm migration:generate ./src/services/storage/migrations/usert_cb_url -d ./datasource.js \ No newline at end of file diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index 6c0d00db..320f86b3 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -195,6 +195,11 @@ The nostr server will send back a message response, and inside the body there wi - input: [DebitOperation](#DebitOperation) - This methods has an __empty__ __response__ body +- UpdateCallbackUrl + - auth type: __User__ + - input: [CallbackUrl](#CallbackUrl) + - output: [CallbackUrl](#CallbackUrl) + - UseInviteLink - auth type: __GuestWithPub__ - input: [UseInviteLinkRequest](#UseInviteLinkRequest) @@ -654,6 +659,13 @@ The nostr server will send back a message response, and inside the body there wi - input: [SetMockInvoiceAsPaidRequest](#SetMockInvoiceAsPaidRequest) - This methods has an __empty__ __response__ body +- UpdateCallbackUrl + - auth type: __User__ + - http method: __post__ + - http route: __/api/user/cb/update__ + - input: [CallbackUrl](#CallbackUrl) + - output: [CallbackUrl](#CallbackUrl) + - UseInviteLink - auth type: __GuestWithPub__ - http method: __post__ @@ -748,6 +760,9 @@ The nostr server will send back a message response, and inside the body there wi - __nostr_pub__: _string_ - __user_identifier__: _string_ +### CallbackUrl + - __url__: _string_ + ### ClosedChannel - __capacity__: _number_ - __channel_id__: _string_ @@ -1055,6 +1070,7 @@ The nostr server will send back a message response, and inside the body there wi ### UserInfo - __balance__: _number_ + - __callback_url__: _string_ - __max_withdrawable__: _number_ - __ndebit__: _string_ - __network_max_fee_bps__: _number_ diff --git a/proto/autogenerated/go/http_client.go b/proto/autogenerated/go/http_client.go index e10f774d..8500be04 100644 --- a/proto/autogenerated/go/http_client.go +++ b/proto/autogenerated/go/http_client.go @@ -111,6 +111,7 @@ type Client struct { SetMockAppBalance func(req SetMockAppBalanceRequest) error SetMockAppUserBalance func(req SetMockAppUserBalanceRequest) error SetMockInvoiceAsPaid func(req SetMockInvoiceAsPaidRequest) error + UpdateCallbackUrl func(req CallbackUrl) (*CallbackUrl, error) UseInviteLink func(req UseInviteLinkRequest) error UserHealth func() error } @@ -1527,6 +1528,35 @@ func NewClient(params ClientParams) *Client { } return nil }, + UpdateCallbackUrl: func(req CallbackUrl) (*CallbackUrl, error) { + auth, err := params.RetrieveUserAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/user/cb/update" + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return nil, err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return nil, err + } + if result.Status == "ERROR" { + return nil, fmt.Errorf(result.Reason) + } + res := CallbackUrl{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, UseInviteLink: func(req UseInviteLinkRequest) error { auth, err := params.RetrieveGuestWithPubAuth() if err != nil { diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index eb8371bf..b2f9652b 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -150,6 +150,9 @@ type BannedAppUser struct { Nostr_pub string `json:"nostr_pub"` User_identifier string `json:"user_identifier"` } +type CallbackUrl struct { + Url string `json:"url"` +} type ClosedChannel struct { Capacity int64 `json:"capacity"` Channel_id string `json:"channel_id"` @@ -457,6 +460,7 @@ type UseInviteLinkRequest struct { } type UserInfo struct { Balance int64 `json:"balance"` + Callback_url string `json:"callback_url"` Max_withdrawable int64 `json:"max_withdrawable"` Ndebit string `json:"ndebit"` Network_max_fee_bps int64 `json:"network_max_fee_bps"` diff --git a/proto/autogenerated/ts/express_server.ts b/proto/autogenerated/ts/express_server.ts index d4186e7c..b8e9f2dd 100644 --- a/proto/autogenerated/ts/express_server.ts +++ b/proto/autogenerated/ts/express_server.ts @@ -469,6 +469,18 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'UpdateCallbackUrl': + if (!methods.UpdateCallbackUrl) { + throw new Error('method UpdateCallbackUrl not found' ) + } else { + const error = Types.CallbackUrlValidate(operation.req) + opStats.validate = process.hrtime.bigint() + if (error !== null) throw error + const res = await methods.UpdateCallbackUrl({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'UserHealth': if (!methods.UserHealth) { throw new Error('method UserHealth not found' ) @@ -1387,6 +1399,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) + if (!opts.allowNotImplementedMethods && !methods.UpdateCallbackUrl) throw new Error('method: UpdateCallbackUrl is not implemented') + app.post('/api/user/cb/update', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'UpdateCallbackUrl', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.UpdateCallbackUrl) throw new Error('method: UpdateCallbackUrl is not implemented') + const authContext = await opts.UserAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + const request = req.body + const error = Types.CallbackUrlValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) + const query = req.query + const params = req.params + const response = await methods.UpdateCallbackUrl({rpcName:'UpdateCallbackUrl', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) if (!opts.allowNotImplementedMethods && !methods.UseInviteLink) throw new Error('method: UseInviteLink is not implemented') app.post('/api/guest/invite', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'UseInviteLink', batch: false, nostr: false, batchSize: 0} diff --git a/proto/autogenerated/ts/http_client.ts b/proto/autogenerated/ts/http_client.ts index 982aa76d..84f54d1a 100644 --- a/proto/autogenerated/ts/http_client.ts +++ b/proto/autogenerated/ts/http_client.ts @@ -735,6 +735,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + UpdateCallbackUrl: async (request: Types.CallbackUrl): Promise => { + const auth = await params.retrieveUserAuth() + if (auth === null) throw new Error('retrieveUserAuth() returned null') + let finalRoute = '/api/user/cb/update' + const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.CallbackUrlValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, UseInviteLink: async (request: Types.UseInviteLinkRequest): Promise => { const auth = await params.retrieveGuestWithPubAuth() if (auth === null) throw new Error('retrieveGuestWithPubAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_client.ts b/proto/autogenerated/ts/nostr_client.ts index a160d576..3ac48d64 100644 --- a/proto/autogenerated/ts/nostr_client.ts +++ b/proto/autogenerated/ts/nostr_client.ts @@ -528,6 +528,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + UpdateCallbackUrl: async (request: Types.CallbackUrl): Promise => { + const auth = await params.retrieveNostrUserAuth() + if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') + const nostrRequest: NostrRequest = {} + nostrRequest.body = request + const data = await send(params.pubDestination, {rpcName:'UpdateCallbackUrl',authIdentifier:auth, ...nostrRequest }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.CallbackUrlValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, UseInviteLink: async (request: Types.UseInviteLinkRequest): Promise => { const auth = await params.retrieveNostrGuestWithPubAuth() if (auth === null) throw new Error('retrieveNostrGuestWithPubAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_transport.ts b/proto/autogenerated/ts/nostr_transport.ts index 1ff5846e..479aee05 100644 --- a/proto/autogenerated/ts/nostr_transport.ts +++ b/proto/autogenerated/ts/nostr_transport.ts @@ -363,6 +363,18 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'UpdateCallbackUrl': + if (!methods.UpdateCallbackUrl) { + throw new Error('method not defined: UpdateCallbackUrl') + } else { + const error = Types.CallbackUrlValidate(operation.req) + opStats.validate = process.hrtime.bigint() + if (error !== null) throw error + const res = await methods.UpdateCallbackUrl({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'UserHealth': if (!methods.UserHealth) { throw new Error('method not defined: UserHealth') @@ -808,6 +820,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break + case 'UpdateCallbackUrl': + try { + if (!methods.UpdateCallbackUrl) throw new Error('method: UpdateCallbackUrl is not implemented') + const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + const request = req.body + const error = Types.CallbackUrlValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + const response = await methods.UpdateCallbackUrl({rpcName:'UpdateCallbackUrl', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break case 'UseInviteLink': try { if (!methods.UseInviteLink) throw new Error('method: UseInviteLink is not implemented') diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index c85e309e..ce4982b5 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -34,8 +34,8 @@ export type UserContext = { app_user_id: string user_id: string } -export type UserMethodInputs = AddProduct_Input | AuthorizeDebit_Input | BanDebit_Input | DecodeInvoice_Input | EnrollAdminToken_Input | GetDebitAuthorizations_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetPaymentState_Input | GetUserInfo_Input | GetUserOperations_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | OpenChannel_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | UserHealth_Input -export type UserMethodOutputs = AddProduct_Output | AuthorizeDebit_Output | BanDebit_Output | DecodeInvoice_Output | EnrollAdminToken_Output | GetDebitAuthorizations_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetPaymentState_Output | GetUserInfo_Output | GetUserOperations_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | OpenChannel_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | UserHealth_Output +export type UserMethodInputs = AddProduct_Input | AuthorizeDebit_Input | BanDebit_Input | DecodeInvoice_Input | EnrollAdminToken_Input | GetDebitAuthorizations_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetPaymentState_Input | GetUserInfo_Input | GetUserOperations_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | OpenChannel_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | UpdateCallbackUrl_Input | UserHealth_Input +export type UserMethodOutputs = AddProduct_Output | AuthorizeDebit_Output | BanDebit_Output | DecodeInvoice_Output | EnrollAdminToken_Output | GetDebitAuthorizations_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetPaymentState_Output | GetUserInfo_Output | GetUserOperations_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | OpenChannel_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | UpdateCallbackUrl_Output | UserHealth_Output export type AuthContext = AdminContext | AppContext | GuestContext | GuestWithPubContext | MetricsContext | UserContext export type AddApp_Input = {rpcName:'AddApp', req: AddAppRequest} @@ -231,6 +231,9 @@ export type SetMockAppUserBalance_Output = ResultError | { status: 'OK' } export type SetMockInvoiceAsPaid_Input = {rpcName:'SetMockInvoiceAsPaid', req: SetMockInvoiceAsPaidRequest} export type SetMockInvoiceAsPaid_Output = ResultError | { status: 'OK' } +export type UpdateCallbackUrl_Input = {rpcName:'UpdateCallbackUrl', req: CallbackUrl} +export type UpdateCallbackUrl_Output = ResultError | ({ status: 'OK' } & CallbackUrl) + export type UseInviteLink_Input = {rpcName:'UseInviteLink', req: UseInviteLinkRequest} export type UseInviteLink_Output = ResultError | { status: 'OK' } @@ -294,6 +297,7 @@ export type ServerMethods = { SetMockAppBalance?: (req: SetMockAppBalance_Input & {ctx: AppContext }) => Promise SetMockAppUserBalance?: (req: SetMockAppUserBalance_Input & {ctx: AppContext }) => Promise SetMockInvoiceAsPaid?: (req: SetMockInvoiceAsPaid_Input & {ctx: GuestContext }) => Promise + UpdateCallbackUrl?: (req: UpdateCallbackUrl_Input & {ctx: UserContext }) => Promise UseInviteLink?: (req: UseInviteLink_Input & {ctx: GuestWithPubContext }) => Promise UserHealth?: (req: UserHealth_Input & {ctx: UserContext }) => Promise } @@ -776,6 +780,24 @@ export const BannedAppUserValidate = (o?: BannedAppUser, opts: BannedAppUserOpti return null } +export type CallbackUrl = { + url: string +} +export const CallbackUrlOptionalFields: [] = [] +export type CallbackUrlOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + url_CustomCheck?: (v: string) => boolean +} +export const CallbackUrlValidate = (o?: CallbackUrl, opts: CallbackUrlOptions = {}, path: string = 'CallbackUrl::root.'): Error | null => { + 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 (typeof o.url !== 'string') return new Error(`${path}.url: is not a string`) + if (opts.url_CustomCheck && !opts.url_CustomCheck(o.url)) return new Error(`${path}.url: custom check failed`) + + return null +} + export type ClosedChannel = { capacity: number channel_id: string @@ -2590,6 +2612,7 @@ export const UseInviteLinkRequestValidate = (o?: UseInviteLinkRequest, opts: Use export type UserInfo = { balance: number + callback_url: string max_withdrawable: number ndebit: string network_max_fee_bps: number @@ -2603,6 +2626,7 @@ export const UserInfoOptionalFields: [] = [] export type UserInfoOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] balance_CustomCheck?: (v: number) => boolean + callback_url_CustomCheck?: (v: string) => boolean max_withdrawable_CustomCheck?: (v: number) => boolean ndebit_CustomCheck?: (v: string) => boolean network_max_fee_bps_CustomCheck?: (v: number) => boolean @@ -2619,6 +2643,9 @@ export const UserInfoValidate = (o?: UserInfo, opts: UserInfoOptions = {}, path: if (typeof o.balance !== 'number') return new Error(`${path}.balance: is not a number`) if (opts.balance_CustomCheck && !opts.balance_CustomCheck(o.balance)) return new Error(`${path}.balance: 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`) + 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`) diff --git a/proto/service/methods.proto b/proto/service/methods.proto index 7918b97d..7b273f01 100644 --- a/proto/service/methods.proto +++ b/proto/service/methods.proto @@ -343,6 +343,12 @@ service LightningPub { option (http_route) = "/api/user/info"; option (nostr) = true; } + rpc UpdateCallbackUrl(structs.CallbackUrl)returns(structs.CallbackUrl){ + option (auth_type) = "User"; + option (http_method) = "post"; + option (http_route) = "/api/user/cb/update"; + option (nostr) = true; + } rpc AddProduct(structs.AddProductRequest) returns (structs.Product){ option (auth_type) = "User"; diff --git a/proto/service/structs.proto b/proto/service/structs.proto index b1245465..c4e90041 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -347,6 +347,10 @@ message HandleLnurlPayResponse { repeated Empty routes = 2; } +message CallbackUrl { + string url = 1; +} + message UserInfo{ string userId = 1; int64 balance = 2; @@ -357,8 +361,8 @@ message UserInfo{ int64 network_max_fee_fixed = 7; string noffer = 8; string ndebit = 9; + string callback_url = 10; } - message GetUserOperationsRequest{ int64 latestIncomingInvoice = 1; int64 latestOutgoingInvoice = 2; diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index 7114e19b..7d43d6bc 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -6,6 +6,7 @@ import { MainSettings } from './settings.js' import ApplicationManager from './applicationManager.js' import { encodeNdebit, encodeNoffer, PriceType } from '../../custom-nip19.js' export default class { + storage: Storage settings: MainSettings applicationManager: ApplicationManager @@ -51,7 +52,7 @@ export default class { const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const appUser = await this.storage.applicationStorage.GetAppUserFromUser(app, user.user_id) console.log("User Identifier/pointer here", appUser?.identifier) - + if (!appUser) { throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing } @@ -64,10 +65,17 @@ export default class { network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit, service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps, noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: appUser.identifier, priceType: PriceType.spontaneous, relay: "" }), - ndebit: encodeNdebit({ pubkey: app.nostr_public_key!, pointerId: appUser.identifier, relay: "" }) + ndebit: encodeNdebit({ pubkey: app.nostr_public_key!, pointerId: appUser.identifier, relay: "" }), + callback_url: appUser.callback_url } } + async UpdateCallbackUrl(ctx: Types.UserContext, req: Types.CallbackUrl): Promise { + const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) + await this.storage.applicationStorage.UpdateUserCallbackUrl(app, ctx.app_user_id, req.url) + return { url: req.url } + } + async NewInvoice(ctx: Types.UserContext, req: Types.NewInvoiceRequest): Promise { return this.applicationManager.AddAppUserInvoice(ctx.app_id, { http_callback_url: "", diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index 4384e343..a3df5e78 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -160,7 +160,8 @@ export default class { network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit, service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps, noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: PriceType.spontaneous, relay: "" }), - ndebit: encodeNdebit({ pubkey: app.nostr_public_key!, pointerId: u.identifier, relay: "" }) + ndebit: encodeNdebit({ pubkey: app.nostr_public_key!, pointerId: u.identifier, relay: "" }), + callback_url: u.callback_url }, max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true) @@ -181,7 +182,8 @@ export default class { const log = getLogger({ appName: app.name }) const receiver = await this.storage.applicationStorage.GetApplicationUser(app, req.receiver_identifier) const { user: payer } = await this.storage.applicationStorage.GetOrCreateApplicationUser(app, req.payer_identifier, 0) - const opts: InboundOptionals = { callbackUrl: req.http_callback_url, expiry: defaultInvoiceExpiry, expectedPayer: payer.user, linkedApplication: app } + const cbUrl = req.http_callback_url || receiver.callback_url || "" + const opts: InboundOptionals = { callbackUrl: cbUrl, expiry: defaultInvoiceExpiry, expectedPayer: payer.user, linkedApplication: app } const appUserInvoice = await this.paymentManager.NewInvoice(receiver.user.user_id, req.invoice_req, opts) return { invoice: appUserInvoice.invoice @@ -201,7 +203,8 @@ export default class { network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit, service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps, noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: PriceType.spontaneous, relay: "" }), - ndebit: encodeNdebit({ pubkey: app.nostr_public_key!, pointerId: user.identifier, relay: "" }) + ndebit: encodeNdebit({ pubkey: app.nostr_public_key!, pointerId: user.identifier, relay: "" }), + callback_url: user.callback_url }, } } diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 24859ab9..82dea6eb 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -228,7 +228,8 @@ export default class { return } try { - await fetch(url + "&ok=true") + const symbol = url.includes('?') ? "&" : "?" + await fetch(url + symbol + "ok=true") } catch (err: any) { log(ERROR, "error sending paid callback for invoice", err.message || "") } diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index 96cb035b..88fe05f8 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -41,6 +41,9 @@ export default (mainHandler: Main): Types.ServerMethods => { }, UserHealth: async () => { }, GetUserInfo: ({ ctx }) => mainHandler.appUserManager.GetUserInfo(ctx), + UpdateCallbackUrl: async ({ ctx, req }) => { + return mainHandler.appUserManager.UpdateCallbackUrl(ctx, req) + }, GetUserOperations: async ({ ctx, req }) => { return mainHandler.paymentManager.GetUserOperations(ctx.user_id, req) }, diff --git a/src/services/storage/applicationStorage.ts b/src/services/storage/applicationStorage.ts index fcfe18a4..4b4f9885 100644 --- a/src/services/storage/applicationStorage.ts +++ b/src/services/storage/applicationStorage.ts @@ -161,9 +161,11 @@ export default class { async AddNPubToApplicationUser(serialId: number, nPub: string, entityManager = this.DB) { return entityManager.getRepository(ApplicationUser).update(serialId, { nostr_public_key: nPub }) - } + async UpdateUserCallbackUrl(application: Application, userIdentifier: string, callbackUrl: string, entityManager = this.DB) { + return entityManager.getRepository(ApplicationUser).update({ application: { app_id: application.app_id }, identifier: userIdentifier }, { callback_url: callbackUrl }) + } async RemoveApplicationUserAndBaseUser(appUser: ApplicationUser, entityManager = this.DB) { const baseUser = appUser.user; diff --git a/src/services/storage/entity/ApplicationUser.ts b/src/services/storage/entity/ApplicationUser.ts index a885cb65..2284b9bd 100644 --- a/src/services/storage/entity/ApplicationUser.ts +++ b/src/services/storage/entity/ApplicationUser.ts @@ -23,6 +23,9 @@ export class ApplicationUser { @Column({ nullable: true, unique: true }) nostr_public_key?: string + @Column({ default: "" }) + callback_url: string + @CreateDateColumn() created_at: Date diff --git a/src/services/storage/migrations/1727112281043-user_cb_url.ts b/src/services/storage/migrations/1727112281043-user_cb_url.ts new file mode 100644 index 00000000..a0bfa74f --- /dev/null +++ b/src/services/storage/migrations/1727112281043-user_cb_url.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class UserCbUrl1727112281043 implements MigrationInterface { + name = 'UserCbUrl1727112281043' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_0a0dbb25a73306b037dec82251"`); + await queryRunner.query(`CREATE TABLE "temporary_application_user" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "identifier" varchar NOT NULL, "nostr_public_key" varchar, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userSerialId" integer, "applicationSerialId" integer, "callback_url" varchar NOT NULL DEFAULT (''), CONSTRAINT "REL_0796a381bcc624f52e9a155712" UNIQUE ("userSerialId"), CONSTRAINT "UQ_3175dc397c8285d1e532554dea5" UNIQUE ("nostr_public_key"), CONSTRAINT "FK_1b3bdb6f660cd99533a1e673ef1" FOREIGN KEY ("applicationSerialId") REFERENCES "application" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_0796a381bcc624f52e9a155712b" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_application_user"("serial_id", "identifier", "nostr_public_key", "created_at", "updated_at", "userSerialId", "applicationSerialId") SELECT "serial_id", "identifier", "nostr_public_key", "created_at", "updated_at", "userSerialId", "applicationSerialId" FROM "application_user"`); + await queryRunner.query(`DROP TABLE "application_user"`); + await queryRunner.query(`ALTER TABLE "temporary_application_user" RENAME TO "application_user"`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0a0dbb25a73306b037dec82251" ON "application_user" ("identifier") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_0a0dbb25a73306b037dec82251"`); + await queryRunner.query(`ALTER TABLE "application_user" RENAME TO "temporary_application_user"`); + await queryRunner.query(`CREATE TABLE "application_user" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "identifier" varchar NOT NULL, "nostr_public_key" varchar, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userSerialId" integer, "applicationSerialId" integer, CONSTRAINT "REL_0796a381bcc624f52e9a155712" UNIQUE ("userSerialId"), CONSTRAINT "UQ_3175dc397c8285d1e532554dea5" UNIQUE ("nostr_public_key"), CONSTRAINT "FK_1b3bdb6f660cd99533a1e673ef1" FOREIGN KEY ("applicationSerialId") REFERENCES "application" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_0796a381bcc624f52e9a155712b" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "application_user"("serial_id", "identifier", "nostr_public_key", "created_at", "updated_at", "userSerialId", "applicationSerialId") SELECT "serial_id", "identifier", "nostr_public_key", "created_at", "updated_at", "userSerialId", "applicationSerialId" FROM "temporary_application_user"`); + await queryRunner.query(`DROP TABLE "temporary_application_user"`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0a0dbb25a73306b037dec82251" ON "application_user" ("identifier") `); + } + +} diff --git a/src/services/storage/migrations/runner.ts b/src/services/storage/migrations/runner.ts index e7080920..8efd66d5 100644 --- a/src/services/storage/migrations/runner.ts +++ b/src/services/storage/migrations/runner.ts @@ -15,7 +15,8 @@ import { BalanceEvents1724860966825 } from './1724860966825-balance_events.js' import { DebitAccess1726496225078 } from './1726496225078-debit_access.js' import { DebitAccessFixes1726685229264 } from './1726685229264-debit_access_fixes.js' import { DebitToPub1727105758354 } from './1727105758354-debit_to_pub.js' -const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354] +import { UserCbUrl1727112281043 } from './1727112281043-user_cb_url.js' +const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043] const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825] export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise => { if (arg === 'fake_initial_migration') {