From 1f5c3041bdfab0b29079a1d9bbecaccab555fe57 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 4 Dec 2024 20:27:06 +0000 Subject: [PATCH] custom ofers --- package-lock.json | 10 +- package.json | 2 +- proto/autogenerated/client.md | 82 ++++++ proto/autogenerated/go/http_client.go | 126 +++++++++ proto/autogenerated/go/types.go | 25 ++ proto/autogenerated/ts/express_server.ts | 165 ++++++++++++ proto/autogenerated/ts/http_client.ts | 64 +++++ proto/autogenerated/ts/nostr_client.ts | 67 +++++ proto/autogenerated/ts/nostr_transport.ts | 135 ++++++++++ proto/autogenerated/ts/types.ts | 155 +++++++++++- proto/service/methods.proto | 36 +++ proto/service/structs.proto | 27 ++ src/nostrMiddleware.ts | 2 +- src/services/main/applicationManager.ts | 5 +- src/services/main/index.ts | 69 +---- src/services/main/offerManager.ts | 239 ++++++++++++++++++ src/services/serverMethods/index.ts | 27 ++ src/services/storage/entity/UserOffer.ts | 37 +++ .../storage/entity/UserReceivingInvoice.ts | 9 + src/services/storage/index.ts | 3 + src/services/storage/offerStorage.ts | 41 +++ src/services/storage/paymentStorage.ts | 6 +- 22 files changed, 1254 insertions(+), 78 deletions(-) create mode 100644 src/services/main/offerManager.ts create mode 100644 src/services/storage/entity/UserOffer.ts create mode 100644 src/services/storage/offerStorage.ts diff --git a/package-lock.json b/package-lock.json index 6501617d..223d36de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "grpc-tools": "^1.12.4", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", - "nostr-tools": "github:shocknet/nostr-tools#da188cd4bd195f44cc690074a3898f354ae85100", + "nostr-tools": "github:shocknet/nostr-tools#27575ffb69d615691242df433a0ccc063f6b8346", "pg": "^8.4.0", "reflect-metadata": "^0.2.2", "rimraf": "^3.0.2", @@ -3796,9 +3796,9 @@ } }, "node_modules/nostr-tools": { - "version": "2.8.0", - "resolved": "git+ssh://git@github.com/shocknet/nostr-tools.git#da188cd4bd195f44cc690074a3898f354ae85100", - "integrity": "sha512-kc41K75rXEnLhqIwlQmjaGsZ9yYTbyP8VW7B2Q+0U/pqaMyt25Nt0QCWiIYS04m0sanvD77OhmddvI1s2ntKog==", + "version": "2.10.4", + "resolved": "git+ssh://git@github.com/shocknet/nostr-tools.git#27575ffb69d615691242df433a0ccc063f6b8346", + "integrity": "sha512-ZQxr1yalFLi5coqG5pHWmjHGehLgCZbQusE/59mre/CgqrFMbGJY77AGTyhDnaGgqRc7B/UJnIvqGVVKhTVRmQ==", "license": "Unlicense", "dependencies": { "@noble/ciphers": "^0.5.1", @@ -3809,7 +3809,7 @@ "@scure/bip39": "1.2.1" }, "optionalDependencies": { - "nostr-wasm": "v0.1.0" + "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" diff --git a/package.json b/package.json index e9c13a0e..8720fc99 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "grpc-tools": "^1.12.4", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", - "nostr-tools": "github:shocknet/nostr-tools#da188cd4bd195f44cc690074a3898f354ae85100", + "nostr-tools": "github:shocknet/nostr-tools#27575ffb69d615691242df433a0ccc063f6b8346", "pg": "^8.4.0", "reflect-metadata": "^0.2.2", "rimraf": "^3.0.2", diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index b2a729b4..4c5b223f 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -28,6 +28,11 @@ The nostr server will send back a message response, and inside the body there wi - input: [AddProductRequest](#AddProductRequest) - output: [Product](#Product) +- AddUserOffer + - auth type: __User__ + - input: [OfferConfig](#OfferConfig) + - output: [OfferId](#OfferId) + - AuthApp - auth type: __Admin__ - input: [AuthAppRequest](#AuthAppRequest) @@ -68,6 +73,11 @@ The nostr server will send back a message response, and inside the body there wi - input: [DecodeInvoiceRequest](#DecodeInvoiceRequest) - output: [DecodeInvoiceResponse](#DecodeInvoiceResponse) +- DeleteUserOffer + - auth type: __User__ + - input: [OfferId](#OfferId) + - This methods has an __empty__ __response__ body + - EditDebit - auth type: __User__ - input: [DebitAuthorizationRequest](#DebitAuthorizationRequest) @@ -153,6 +163,16 @@ The nostr server will send back a message response, and inside the body there wi - This methods has an __empty__ __request__ body - output: [UserInfo](#UserInfo) +- GetUserOffer + - auth type: __User__ + - input: [OfferId](#OfferId) + - output: [OfferConfig](#OfferConfig) + +- GetUserOffers + - auth type: __User__ + - This methods has an __empty__ __request__ body + - output: [UserOffers](#UserOffers) + - GetUserOperations - auth type: __User__ - input: [GetUserOperationsRequest](#GetUserOperationsRequest) @@ -225,6 +245,11 @@ The nostr server will send back a message response, and inside the body there wi - input: [UpdateChannelPolicyRequest](#UpdateChannelPolicyRequest) - This methods has an __empty__ __response__ body +- UpdateUserOffer + - auth type: __User__ + - input: [OfferConfig](#OfferConfig) + - This methods has an __empty__ __response__ body + - UseInviteLink - auth type: __GuestWithPub__ - input: [UseInviteLinkRequest](#UseInviteLinkRequest) @@ -311,6 +336,13 @@ The nostr server will send back a message response, and inside the body there wi - input: [AddProductRequest](#AddProductRequest) - output: [Product](#Product) +- AddUserOffer + - auth type: __User__ + - http method: __post__ + - http route: __/api/user/offer/add__ + - input: [OfferConfig](#OfferConfig) + - output: [OfferId](#OfferId) + - AuthApp - auth type: __Admin__ - http method: __post__ @@ -367,6 +399,13 @@ The nostr server will send back a message response, and inside the body there wi - input: [DecodeInvoiceRequest](#DecodeInvoiceRequest) - output: [DecodeInvoiceResponse](#DecodeInvoiceResponse) +- DeleteUserOffer + - auth type: __User__ + - http method: __post__ + - http route: __/api/user/offer/delete__ + - input: [OfferId](#OfferId) + - This methods has an __empty__ __response__ body + - EditDebit - auth type: __User__ - http method: __post__ @@ -539,6 +578,20 @@ The nostr server will send back a message response, and inside the body there wi - This methods has an __empty__ __request__ body - output: [UserInfo](#UserInfo) +- GetUserOffer + - auth type: __User__ + - http method: __get__ + - http route: __/api/user/offer/get__ + - input: [OfferId](#OfferId) + - output: [OfferConfig](#OfferConfig) + +- GetUserOffers + - auth type: __User__ + - http method: __get__ + - http route: __/api/user/offers/get__ + - This methods has an __empty__ __request__ body + - output: [UserOffers](#UserOffers) + - GetUserOperations - auth type: __User__ - http method: __post__ @@ -733,6 +786,13 @@ The nostr server will send back a message response, and inside the body there wi - input: [UpdateChannelPolicyRequest](#UpdateChannelPolicyRequest) - This methods has an __empty__ __response__ body +- UpdateUserOffer + - auth type: __User__ + - http method: __post__ + - http route: __/api/user/offer/update__ + - input: [OfferConfig](#OfferConfig) + - This methods has an __empty__ __response__ body + - UseInviteLink - auth type: __GuestWithPub__ - http method: __post__ @@ -764,6 +824,8 @@ The nostr server will send back a message response, and inside the body there wi ### AddAppUserInvoiceRequest - __http_callback_url__: _string_ - __invoice_req__: _[NewInvoiceRequest](#NewInvoiceRequest)_ + - __offer_string__: _string_ *this field is optional + - __payer_data__: _[PayerData](#PayerData)_ *this field is optional - __payer_identifier__: _string_ - __receiver_identifier__: _string_ @@ -1056,6 +1118,17 @@ The nostr server will send back a message response, and inside the body there wi ### NewInvoiceResponse - __invoice__: _string_ +### OfferConfig + - __callback_url__: _string_ + - __expected_data__: MAP with key: _string_ and value: _[OfferDataType](#OfferDataType)_ + - __label__: _string_ + - __noffer__: _string_ + - __offer_id__: _string_ + - __price_sats__: _number_ + +### OfferId + - __offer_id__: _string_ + ### OpenChannel - __active__: _boolean_ - __capacity__: _number_ @@ -1106,6 +1179,9 @@ The nostr server will send back a message response, and inside the body there wi - __preimage__: _string_ - __service_fee__: _number_ +### PayerData + - __data__: MAP with key: _string_ and value: _string_ + ### PaymentState - __amount__: _number_ - __network_fee__: _number_ @@ -1201,6 +1277,9 @@ The nostr server will send back a message response, and inside the body there wi - __userId__: _string_ - __user_identifier__: _string_ +### UserOffers + - __offers__: ARRAY of: _[OfferConfig](#OfferConfig)_ + ### UserOperation - __amount__: _number_ - __confirmed__: _boolean_ @@ -1239,6 +1318,9 @@ The nostr server will send back a message response, and inside the body there wi - __MONTH__ - __WEEK__ +### OfferDataType + - __DATA_STRING__ + ### OperationType - __CHAIN_OP__ - __INVOICE_OP__ diff --git a/proto/autogenerated/go/http_client.go b/proto/autogenerated/go/http_client.go index 06a87ff7..0289dfd3 100644 --- a/proto/autogenerated/go/http_client.go +++ b/proto/autogenerated/go/http_client.go @@ -60,6 +60,7 @@ type Client struct { AddAppUserInvoice func(req AddAppUserInvoiceRequest) (*NewInvoiceResponse, error) AddPeer func(req AddPeerRequest) error AddProduct func(req AddProductRequest) (*Product, error) + AddUserOffer func(req OfferConfig) (*OfferId, error) AuthApp func(req AuthAppRequest) (*AuthApp, error) AuthorizeDebit func(req DebitAuthorizationRequest) (*DebitAuthorization, error) BanDebit func(req DebitOperation) error @@ -68,6 +69,7 @@ type Client struct { CloseChannel func(req CloseChannelRequest) (*CloseChannelResponse, error) CreateOneTimeInviteLink func(req CreateOneTimeInviteLinkRequest) (*CreateOneTimeInviteLinkResponse, error) DecodeInvoice func(req DecodeInvoiceRequest) (*DecodeInvoiceResponse, error) + DeleteUserOffer func(req OfferId) error EditDebit func(req DebitAuthorizationRequest) error EncryptionExchange func(req EncryptionExchangeRequest) error EnrollAdminToken func(req EnrollAdminTokenRequest) error @@ -92,6 +94,8 @@ type Client struct { GetSeed func() (*LndSeed, error) GetUsageMetrics func() (*UsageMetrics, error) GetUserInfo func() (*UserInfo, error) + GetUserOffer func(req OfferId) (*OfferConfig, error) + GetUserOffers func() (*UserOffers, error) GetUserOperations func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error) HandleLnurlAddress func(routeParams HandleLnurlAddress_RouteParams) (*LnurlPayInfoResponse, error) HandleLnurlPay func(query HandleLnurlPay_Query) (*HandleLnurlPayResponse, error) @@ -118,6 +122,7 @@ type Client struct { SetMockInvoiceAsPaid func(req SetMockInvoiceAsPaidRequest) error UpdateCallbackUrl func(req CallbackUrl) (*CallbackUrl, error) UpdateChannelPolicy func(req UpdateChannelPolicyRequest) error + UpdateUserOffer func(req OfferConfig) error UseInviteLink func(req UseInviteLinkRequest) error UserHealth func() error } @@ -293,6 +298,35 @@ func NewClient(params ClientParams) *Client { } return &res, nil }, + AddUserOffer: func(req OfferConfig) (*OfferId, error) { + auth, err := params.RetrieveUserAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/user/offer/add" + 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 := OfferId{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, AuthApp: func(req AuthAppRequest) (*AuthApp, error) { auth, err := params.RetrieveAdminAuth() if err != nil { @@ -492,6 +526,30 @@ func NewClient(params ClientParams) *Client { } return &res, nil }, + DeleteUserOffer: func(req OfferId) error { + auth, err := params.RetrieveUserAuth() + if err != nil { + return err + } + finalRoute := "/api/user/offer/delete" + body, err := json.Marshal(req) + if err != nil { + return err + } + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return err + } + if result.Status == "ERROR" { + return fmt.Errorf(result.Reason) + } + return nil + }, EditDebit: func(req DebitAuthorizationRequest) error { auth, err := params.RetrieveUserAuth() if err != nil { @@ -1023,6 +1081,50 @@ func NewClient(params ClientParams) *Client { } return &res, nil }, + GetUserOffer: func(req OfferId) (*OfferConfig, error) { + auth, err := params.RetrieveUserAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/user/offer/get" + resBody, err := doGetRequest(params.BaseURL+finalRoute, auth) + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return nil, err + } + if result.Status == "ERROR" { + return nil, fmt.Errorf(result.Reason) + } + res := OfferConfig{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, + GetUserOffers: func() (*UserOffers, error) { + auth, err := params.RetrieveUserAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/user/offers/get" + resBody, err := doGetRequest(params.BaseURL+finalRoute, auth) + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return nil, err + } + if result.Status == "ERROR" { + return nil, fmt.Errorf(result.Reason) + } + res := UserOffers{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, GetUserOperations: func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error) { auth, err := params.RetrieveUserAuth() if err != nil { @@ -1717,6 +1819,30 @@ func NewClient(params ClientParams) *Client { } return nil }, + UpdateUserOffer: func(req OfferConfig) error { + auth, err := params.RetrieveUserAuth() + if err != nil { + return err + } + finalRoute := "/api/user/offer/update" + body, err := json.Marshal(req) + if err != nil { + return err + } + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return err + } + if result.Status == "ERROR" { + return fmt.Errorf(result.Reason) + } + return 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 9b84c889..1881e0be 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -64,6 +64,12 @@ const ( WEEK IntervalType = "WEEK" ) +type OfferDataType string + +const ( + DATA_STRING OfferDataType = "DATA_STRING" +) + type OperationType string const ( @@ -94,6 +100,8 @@ type AddAppRequest struct { type AddAppUserInvoiceRequest struct { Http_callback_url string `json:"http_callback_url"` Invoice_req *NewInvoiceRequest `json:"invoice_req"` + Offer_string string `json:"offer_string"` + Payer_data *PayerData `json:"payer_data"` Payer_identifier string `json:"payer_identifier"` Receiver_identifier string `json:"receiver_identifier"` } @@ -386,6 +394,17 @@ type NewInvoiceRequest struct { type NewInvoiceResponse struct { Invoice string `json:"invoice"` } +type OfferConfig struct { + Callback_url string `json:"callback_url"` + Expected_data map[string]OfferDataType `json:"expected_data"` + Label string `json:"label"` + Noffer string `json:"noffer"` + Offer_id string `json:"offer_id"` + Price_sats int64 `json:"price_sats"` +} +type OfferId struct { + Offer_id string `json:"offer_id"` +} type OpenChannel struct { Active bool `json:"active"` Capacity int64 `json:"capacity"` @@ -436,6 +455,9 @@ type PayInvoiceResponse struct { Preimage string `json:"preimage"` Service_fee int64 `json:"service_fee"` } +type PayerData struct { + Data map[string]string `json:"data"` +} type PaymentState struct { Amount int64 `json:"amount"` Network_fee int64 `json:"network_fee"` @@ -531,6 +553,9 @@ type UserInfo struct { Userid string `json:"userId"` User_identifier string `json:"user_identifier"` } +type UserOffers struct { + Offers []OfferConfig `json:"offers"` +} type UserOperation struct { Amount int64 `json:"amount"` Confirmed bool `json:"confirmed"` diff --git a/proto/autogenerated/ts/express_server.ts b/proto/autogenerated/ts/express_server.ts index cedcfc7d..603d93d5 100644 --- a/proto/autogenerated/ts/express_server.ts +++ b/proto/autogenerated/ts/express_server.ts @@ -166,6 +166,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.AddUserOffer) throw new Error('method: AddUserOffer is not implemented') + app.post('/api/user/offer/add', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'AddUserOffer', 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.AddUserOffer) throw new Error('method: AddUserOffer 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.OfferConfigValidate(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.AddUserOffer({rpcName:'AddUserOffer', 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.AuthApp) throw new Error('method: AuthApp is not implemented') app.post('/api/admin/app/auth', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'AuthApp', batch: false, nostr: false, batchSize: 0} @@ -287,6 +309,18 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'AddUserOffer': + if (!methods.AddUserOffer) { + throw new Error('method AddUserOffer not found' ) + } else { + const error = Types.OfferConfigValidate(operation.req) + opStats.validate = process.hrtime.bigint() + if (error !== null) throw error + const res = await methods.AddUserOffer({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'AuthorizeDebit': if (!methods.AuthorizeDebit) { throw new Error('method AuthorizeDebit not found' ) @@ -323,6 +357,18 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'DeleteUserOffer': + if (!methods.DeleteUserOffer) { + throw new Error('method DeleteUserOffer not found' ) + } else { + const error = Types.OfferIdValidate(operation.req) + opStats.validate = process.hrtime.bigint() + if (error !== null) throw error + await methods.DeleteUserOffer({...operation, ctx}); responses.push({ status: 'OK' }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'EditDebit': if (!methods.EditDebit) { throw new Error('method EditDebit not found' ) @@ -409,6 +455,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'GetUserOffer': + if (!methods.GetUserOffer) { + throw new Error('method GetUserOffer not found' ) + } else { + const error = Types.OfferIdValidate(operation.req) + opStats.validate = process.hrtime.bigint() + if (error !== null) throw error + const res = await methods.GetUserOffer({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break + case 'GetUserOffers': + if (!methods.GetUserOffers) { + throw new Error('method GetUserOffers not found' ) + } else { + opStats.validate = opStats.guard + const res = await methods.GetUserOffers({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'GetUserOperations': if (!methods.GetUserOperations) { throw new Error('method GetUserOperations not found' ) @@ -515,6 +583,18 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'UpdateUserOffer': + if (!methods.UpdateUserOffer) { + throw new Error('method UpdateUserOffer not found' ) + } else { + const error = Types.OfferConfigValidate(operation.req) + opStats.validate = process.hrtime.bigint() + if (error !== null) throw error + await methods.UpdateUserOffer({...operation, ctx}); responses.push({ status: 'OK' }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'UserHealth': if (!methods.UserHealth) { throw new Error('method UserHealth not found' ) @@ -601,6 +681,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.DeleteUserOffer) throw new Error('method: DeleteUserOffer is not implemented') + app.post('/api/user/offer/delete', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'DeleteUserOffer', 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.DeleteUserOffer) throw new Error('method: DeleteUserOffer 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.OfferIdValidate(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 + await methods.DeleteUserOffer({rpcName:'DeleteUserOffer', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK'}) + 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.EditDebit) throw new Error('method: EditDebit is not implemented') app.post('/api/user/debit/edit', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'EditDebit', batch: false, nostr: false, batchSize: 0} @@ -1011,6 +1113,47 @@ 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.GetUserOffer) throw new Error('method: GetUserOffer is not implemented') + app.get('/api/user/offer/get', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'GetUserOffer', 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.GetUserOffer) throw new Error('method: GetUserOffer 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.OfferIdValidate(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.GetUserOffer({rpcName:'GetUserOffer', 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.GetUserOffers) throw new Error('method: GetUserOffers is not implemented') + app.get('/api/user/offers/get', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'GetUserOffers', 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.GetUserOffers) throw new Error('method: GetUserOffers is not implemented') + const authContext = await opts.UserAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + stats.validate = stats.guard + const query = req.query + const params = req.params + const response = await methods.GetUserOffers({rpcName:'GetUserOffers', ctx:authContext }) + 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.GetUserOperations) throw new Error('method: GetUserOperations is not implemented') app.post('/api/user/operations', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'GetUserOperations', batch: false, nostr: false, batchSize: 0} @@ -1565,6 +1708,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.UpdateUserOffer) throw new Error('method: UpdateUserOffer is not implemented') + app.post('/api/user/offer/update', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'UpdateUserOffer', 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.UpdateUserOffer) throw new Error('method: UpdateUserOffer 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.OfferConfigValidate(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 + await methods.UpdateUserOffer({rpcName:'UpdateUserOffer', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK'}) + 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 1bc428a1..b2ed71f6 100644 --- a/proto/autogenerated/ts/http_client.ts +++ b/proto/autogenerated/ts/http_client.ts @@ -98,6 +98,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + AddUserOffer: async (request: Types.OfferConfig): Promise => { + const auth = await params.retrieveUserAuth() + if (auth === null) throw new Error('retrieveUserAuth() returned null') + let finalRoute = '/api/user/offer/add' + 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.OfferIdValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, AuthApp: async (request: Types.AuthAppRequest): Promise => { const auth = await params.retrieveAdminAuth() if (auth === null) throw new Error('retrieveAdminAuth() returned null') @@ -204,6 +218,17 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + DeleteUserOffer: async (request: Types.OfferId): Promise => { + const auth = await params.retrieveUserAuth() + if (auth === null) throw new Error('retrieveUserAuth() returned null') + let finalRoute = '/api/user/offer/delete' + 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') { + return data + } + return { status: 'ERROR', reason: 'invalid response' } + }, EditDebit: async (request: Types.DebitAuthorizationRequest): Promise => { const auth = await params.retrieveUserAuth() if (auth === null) throw new Error('retrieveUserAuth() returned null') @@ -483,6 +508,34 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + GetUserOffer: async (request: Types.OfferId): Promise => { + const auth = await params.retrieveUserAuth() + if (auth === null) throw new Error('retrieveUserAuth() returned null') + let finalRoute = '/api/user/offer/get' + const { data } = await axios.get(params.baseUrl + finalRoute, { 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.OfferConfigValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, + GetUserOffers: async (): Promise => { + const auth = await params.retrieveUserAuth() + if (auth === null) throw new Error('retrieveUserAuth() returned null') + let finalRoute = '/api/user/offers/get' + const { data } = await axios.get(params.baseUrl + finalRoute, { 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.UserOffersValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, GetUserOperations: async (request: Types.GetUserOperationsRequest): Promise => { const auth = await params.retrieveUserAuth() if (auth === null) throw new Error('retrieveUserAuth() returned null') @@ -821,6 +874,17 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + UpdateUserOffer: async (request: Types.OfferConfig): Promise => { + const auth = await params.retrieveUserAuth() + if (auth === null) throw new Error('retrieveUserAuth() returned null') + let finalRoute = '/api/user/offer/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') { + return data + } + 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 6a2c8d5d..94dd8852 100644 --- a/proto/autogenerated/ts/nostr_client.ts +++ b/proto/autogenerated/ts/nostr_client.ts @@ -54,6 +54,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + AddUserOffer: async (request: Types.OfferConfig): 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:'AddUserOffer',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.OfferIdValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, AuthApp: async (request: Types.AuthAppRequest): Promise => { const auth = await params.retrieveNostrAdminAuth() if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null') @@ -167,6 +182,18 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + DeleteUserOffer: async (request: Types.OfferId): 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:'DeleteUserOffer',authIdentifier:auth, ...nostrRequest }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + return data + } + return { status: 'ERROR', reason: 'invalid response' } + }, EditDebit: async (request: Types.DebitAuthorizationRequest): Promise => { const auth = await params.retrieveNostrUserAuth() if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') @@ -409,6 +436,34 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + GetUserOffer: async (request: Types.OfferId): Promise => { + const auth = await params.retrieveNostrUserAuth() + if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') + const nostrRequest: NostrRequest = {} + const data = await send(params.pubDestination, {rpcName:'GetUserOffer',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.OfferConfigValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, + GetUserOffers: async (): Promise => { + const auth = await params.retrieveNostrUserAuth() + if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') + const nostrRequest: NostrRequest = {} + const data = await send(params.pubDestination, {rpcName:'GetUserOffers',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.UserOffersValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, GetUserOperations: async (request: Types.GetUserOperationsRequest): Promise => { const auth = await params.retrieveNostrUserAuth() if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') @@ -606,6 +661,18 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + UpdateUserOffer: async (request: Types.OfferConfig): 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:'UpdateUserOffer',authIdentifier:auth, ...nostrRequest }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + return data + } + 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 48f824a7..e651e11f 100644 --- a/proto/autogenerated/ts/nostr_transport.ts +++ b/proto/autogenerated/ts/nostr_transport.ts @@ -80,6 +80,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 'AddUserOffer': + try { + if (!methods.AddUserOffer) throw new Error('method: AddUserOffer 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.OfferConfigValidate(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.AddUserOffer({rpcName:'AddUserOffer', 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 'AuthApp': try { if (!methods.AuthApp) throw new Error('method: AuthApp is not implemented') @@ -175,6 +191,18 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'AddUserOffer': + if (!methods.AddUserOffer) { + throw new Error('method not defined: AddUserOffer') + } else { + const error = Types.OfferConfigValidate(operation.req) + opStats.validate = process.hrtime.bigint() + if (error !== null) throw error + const res = await methods.AddUserOffer({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'AuthorizeDebit': if (!methods.AuthorizeDebit) { throw new Error('method not defined: AuthorizeDebit') @@ -211,6 +239,18 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'DeleteUserOffer': + if (!methods.DeleteUserOffer) { + throw new Error('method not defined: DeleteUserOffer') + } else { + const error = Types.OfferIdValidate(operation.req) + opStats.validate = process.hrtime.bigint() + if (error !== null) throw error + await methods.DeleteUserOffer({...operation, ctx}); responses.push({ status: 'OK' }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'EditDebit': if (!methods.EditDebit) { throw new Error('method not defined: EditDebit') @@ -297,6 +337,28 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'GetUserOffer': + if (!methods.GetUserOffer) { + throw new Error('method not defined: GetUserOffer') + } else { + const error = Types.OfferIdValidate(operation.req) + opStats.validate = process.hrtime.bigint() + if (error !== null) throw error + const res = await methods.GetUserOffer({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break + case 'GetUserOffers': + if (!methods.GetUserOffers) { + throw new Error('method not defined: GetUserOffers') + } else { + opStats.validate = opStats.guard + const res = await methods.GetUserOffers({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'GetUserOperations': if (!methods.GetUserOperations) { throw new Error('method not defined: GetUserOperations') @@ -403,6 +465,18 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break + case 'UpdateUserOffer': + if (!methods.UpdateUserOffer) { + throw new Error('method not defined: UpdateUserOffer') + } else { + const error = Types.OfferConfigValidate(operation.req) + opStats.validate = process.hrtime.bigint() + if (error !== null) throw error + await methods.UpdateUserOffer({...operation, ctx}); responses.push({ status: 'OK' }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'UserHealth': if (!methods.UserHealth) { throw new Error('method not defined: UserHealth') @@ -471,6 +545,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 'DeleteUserOffer': + try { + if (!methods.DeleteUserOffer) throw new Error('method: DeleteUserOffer 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.OfferIdValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + await methods.DeleteUserOffer({rpcName:'DeleteUserOffer', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK'}) + 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 'EditDebit': try { if (!methods.EditDebit) throw new Error('method: EditDebit is not implemented') @@ -710,6 +800,35 @@ 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 'GetUserOffer': + try { + if (!methods.GetUserOffer) throw new Error('method: GetUserOffer 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.OfferIdValidate(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.GetUserOffer({rpcName:'GetUserOffer', 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 'GetUserOffers': + try { + if (!methods.GetUserOffers) throw new Error('method: GetUserOffers is not implemented') + const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + stats.validate = stats.guard + const response = await methods.GetUserOffers({rpcName:'GetUserOffers', ctx:authContext }) + 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 'GetUserOperations': try { if (!methods.GetUserOperations) throw new Error('method: GetUserOperations is not implemented') @@ -928,6 +1047,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 'UpdateUserOffer': + try { + if (!methods.UpdateUserOffer) throw new Error('method: UpdateUserOffer 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.OfferConfigValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + await methods.UpdateUserOffer({rpcName:'UpdateUserOffer', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK'}) + 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 ac253234..814ded3a 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 | EditDebit_Input | EnrollAdminToken_Input | GetDebitAuthorizations_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetPaymentState_Input | GetUserInfo_Input | GetUserOperations_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | RespondToDebit_Input | UpdateCallbackUrl_Input | UserHealth_Input -export type UserMethodOutputs = AddProduct_Output | AuthorizeDebit_Output | BanDebit_Output | DecodeInvoice_Output | EditDebit_Output | EnrollAdminToken_Output | GetDebitAuthorizations_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetPaymentState_Output | GetUserInfo_Output | GetUserOperations_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | RespondToDebit_Output | UpdateCallbackUrl_Output | UserHealth_Output +export type UserMethodInputs = AddProduct_Input | AddUserOffer_Input | AuthorizeDebit_Input | BanDebit_Input | DecodeInvoice_Input | DeleteUserOffer_Input | EditDebit_Input | EnrollAdminToken_Input | GetDebitAuthorizations_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetPaymentState_Input | GetUserInfo_Input | GetUserOffer_Input | GetUserOffers_Input | GetUserOperations_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | RespondToDebit_Input | UpdateCallbackUrl_Input | UpdateUserOffer_Input | UserHealth_Input +export type UserMethodOutputs = AddProduct_Output | AddUserOffer_Output | AuthorizeDebit_Output | BanDebit_Output | DecodeInvoice_Output | DeleteUserOffer_Output | EditDebit_Output | EnrollAdminToken_Output | GetDebitAuthorizations_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetPaymentState_Output | GetUserInfo_Output | GetUserOffer_Output | GetUserOffers_Output | GetUserOperations_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | RespondToDebit_Output | UpdateCallbackUrl_Output | UpdateUserOffer_Output | UserHealth_Output export type AuthContext = AdminContext | AppContext | GuestContext | GuestWithPubContext | MetricsContext | UserContext export type AddApp_Input = {rpcName:'AddApp', req: AddAppRequest} @@ -56,6 +56,9 @@ export type AddPeer_Output = ResultError | { status: 'OK' } export type AddProduct_Input = {rpcName:'AddProduct', req: AddProductRequest} export type AddProduct_Output = ResultError | ({ status: 'OK' } & Product) +export type AddUserOffer_Input = {rpcName:'AddUserOffer', req: OfferConfig} +export type AddUserOffer_Output = ResultError | ({ status: 'OK' } & OfferId) + export type AuthApp_Input = {rpcName:'AuthApp', req: AuthAppRequest} export type AuthApp_Output = ResultError | ({ status: 'OK' } & AuthApp) @@ -80,6 +83,9 @@ export type CreateOneTimeInviteLink_Output = ResultError | ({ status: 'OK' } & C export type DecodeInvoice_Input = {rpcName:'DecodeInvoice', req: DecodeInvoiceRequest} export type DecodeInvoice_Output = ResultError | ({ status: 'OK' } & DecodeInvoiceResponse) +export type DeleteUserOffer_Input = {rpcName:'DeleteUserOffer', req: OfferId} +export type DeleteUserOffer_Output = ResultError | { status: 'OK' } + export type EditDebit_Input = {rpcName:'EditDebit', req: DebitAuthorizationRequest} export type EditDebit_Output = ResultError | { status: 'OK' } @@ -158,6 +164,12 @@ export type GetUsageMetrics_Output = ResultError | ({ status: 'OK' } & UsageMetr export type GetUserInfo_Input = {rpcName:'GetUserInfo'} export type GetUserInfo_Output = ResultError | ({ status: 'OK' } & UserInfo) +export type GetUserOffer_Input = {rpcName:'GetUserOffer', req: OfferId} +export type GetUserOffer_Output = ResultError | ({ status: 'OK' } & OfferConfig) + +export type GetUserOffers_Input = {rpcName:'GetUserOffers'} +export type GetUserOffers_Output = ResultError | ({ status: 'OK' } & UserOffers) + export type GetUserOperations_Input = {rpcName:'GetUserOperations', req: GetUserOperationsRequest} export type GetUserOperations_Output = ResultError | ({ status: 'OK' } & GetUserOperationsResponse) @@ -252,6 +264,9 @@ export type UpdateCallbackUrl_Output = ResultError | ({ status: 'OK' } & Callbac export type UpdateChannelPolicy_Input = {rpcName:'UpdateChannelPolicy', req: UpdateChannelPolicyRequest} export type UpdateChannelPolicy_Output = ResultError | { status: 'OK' } +export type UpdateUserOffer_Input = {rpcName:'UpdateUserOffer', req: OfferConfig} +export type UpdateUserOffer_Output = ResultError | { status: 'OK' } + export type UseInviteLink_Input = {rpcName:'UseInviteLink', req: UseInviteLinkRequest} export type UseInviteLink_Output = ResultError | { status: 'OK' } @@ -265,6 +280,7 @@ export type ServerMethods = { AddAppUserInvoice?: (req: AddAppUserInvoice_Input & {ctx: AppContext }) => Promise AddPeer?: (req: AddPeer_Input & {ctx: AdminContext }) => Promise AddProduct?: (req: AddProduct_Input & {ctx: UserContext }) => Promise + AddUserOffer?: (req: AddUserOffer_Input & {ctx: UserContext }) => Promise AuthApp?: (req: AuthApp_Input & {ctx: AdminContext }) => Promise AuthorizeDebit?: (req: AuthorizeDebit_Input & {ctx: UserContext }) => Promise BanDebit?: (req: BanDebit_Input & {ctx: UserContext }) => Promise @@ -272,6 +288,7 @@ export type ServerMethods = { CloseChannel?: (req: CloseChannel_Input & {ctx: AdminContext }) => Promise CreateOneTimeInviteLink?: (req: CreateOneTimeInviteLink_Input & {ctx: AdminContext }) => Promise DecodeInvoice?: (req: DecodeInvoice_Input & {ctx: UserContext }) => Promise + DeleteUserOffer?: (req: DeleteUserOffer_Input & {ctx: UserContext }) => Promise EditDebit?: (req: EditDebit_Input & {ctx: UserContext }) => Promise EncryptionExchange?: (req: EncryptionExchange_Input & {ctx: GuestContext }) => Promise EnrollAdminToken?: (req: EnrollAdminToken_Input & {ctx: UserContext }) => Promise @@ -296,6 +313,8 @@ export type ServerMethods = { GetSeed?: (req: GetSeed_Input & {ctx: AdminContext }) => Promise GetUsageMetrics?: (req: GetUsageMetrics_Input & {ctx: MetricsContext }) => Promise GetUserInfo?: (req: GetUserInfo_Input & {ctx: UserContext }) => Promise + GetUserOffer?: (req: GetUserOffer_Input & {ctx: UserContext }) => Promise + GetUserOffers?: (req: GetUserOffers_Input & {ctx: UserContext }) => Promise GetUserOperations?: (req: GetUserOperations_Input & {ctx: UserContext }) => Promise HandleLnurlAddress?: (req: HandleLnurlAddress_Input & {ctx: GuestContext }) => Promise HandleLnurlPay?: (req: HandleLnurlPay_Input & {ctx: GuestContext }) => Promise @@ -322,6 +341,7 @@ export type ServerMethods = { SetMockInvoiceAsPaid?: (req: SetMockInvoiceAsPaid_Input & {ctx: GuestContext }) => Promise UpdateCallbackUrl?: (req: UpdateCallbackUrl_Input & {ctx: UserContext }) => Promise UpdateChannelPolicy?: (req: UpdateChannelPolicy_Input & {ctx: AdminContext }) => Promise + UpdateUserOffer?: (req: UpdateUserOffer_Input & {ctx: UserContext }) => Promise UseInviteLink?: (req: UseInviteLink_Input & {ctx: GuestWithPubContext }) => Promise UserHealth?: (req: UserHealth_Input & {ctx: UserContext }) => Promise } @@ -344,6 +364,13 @@ export const enumCheckIntervalType = (e?: IntervalType): boolean => { for (const v in IntervalType) if (e === v) return true return false } +export enum OfferDataType { + DATA_STRING = 'DATA_STRING', +} +export const enumCheckOfferDataType = (e?: OfferDataType): boolean => { + for (const v in OfferDataType) if (e === v) return true + return false +} export enum OperationType { CHAIN_OP = 'CHAIN_OP', INVOICE_OP = 'INVOICE_OP', @@ -424,14 +451,19 @@ export const AddAppRequestValidate = (o?: AddAppRequest, opts: AddAppRequestOpti export type AddAppUserInvoiceRequest = { http_callback_url: string invoice_req: NewInvoiceRequest + offer_string?: string + payer_data?: PayerData payer_identifier: string receiver_identifier: string } -export const AddAppUserInvoiceRequestOptionalFields: [] = [] +export type AddAppUserInvoiceRequestOptionalField = 'offer_string' | 'payer_data' +export const AddAppUserInvoiceRequestOptionalFields: AddAppUserInvoiceRequestOptionalField[] = ['offer_string', 'payer_data'] export type AddAppUserInvoiceRequestOptions = OptionsBaseMessage & { - checkOptionalsAreSet?: [] + checkOptionalsAreSet?: AddAppUserInvoiceRequestOptionalField[] http_callback_url_CustomCheck?: (v: string) => boolean invoice_req_Options?: NewInvoiceRequestOptions + offer_string_CustomCheck?: (v?: string) => boolean + payer_data_Options?: PayerDataOptions payer_identifier_CustomCheck?: (v: string) => boolean receiver_identifier_CustomCheck?: (v: string) => boolean } @@ -446,6 +478,15 @@ export const AddAppUserInvoiceRequestValidate = (o?: AddAppUserInvoiceRequest, o if (invoice_reqErr !== null) return invoice_reqErr + if ((o.offer_string || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('offer_string')) && typeof o.offer_string !== 'string') return new Error(`${path}.offer_string: is not a string`) + if (opts.offer_string_CustomCheck && !opts.offer_string_CustomCheck(o.offer_string)) return new Error(`${path}.offer_string: custom check failed`) + + if (typeof o.payer_data === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('payer_data')) { + const payer_dataErr = PayerDataValidate(o.payer_data, opts.payer_data_Options, `${path}.payer_data`) + if (payer_dataErr !== null) return payer_dataErr + } + + if (typeof o.payer_identifier !== 'string') return new Error(`${path}.payer_identifier: is not a string`) if (opts.payer_identifier_CustomCheck && !opts.payer_identifier_CustomCheck(o.payer_identifier)) return new Error(`${path}.payer_identifier: custom check failed`) @@ -2201,6 +2242,69 @@ export const NewInvoiceResponseValidate = (o?: NewInvoiceResponse, opts: NewInvo return null } +export type OfferConfig = { + callback_url: string + expected_data: Record + label: string + noffer: string + offer_id: string + price_sats: number +} +export const OfferConfigOptionalFields: [] = [] +export type OfferConfigOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + callback_url_CustomCheck?: (v: string) => boolean + expected_data_CustomCheck?: (v: Record) => boolean + label_CustomCheck?: (v: string) => boolean + noffer_CustomCheck?: (v: string) => boolean + offer_id_CustomCheck?: (v: string) => boolean + price_sats_CustomCheck?: (v: number) => boolean +} +export const OfferConfigValidate = (o?: OfferConfig, opts: OfferConfigOptions = {}, path: string = 'OfferConfig::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.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.expected_data !== 'object' || o.expected_data === null) return new Error(`${path}.expected_data: is not an object or is null`) + for (const key in o.expected_data) { + if (!enumCheckOfferDataType(o.expected_data[key])) return new Error(`${path}.expected_data['${key}']: is not a OfferDataType`) + } + + if (typeof o.label !== 'string') return new Error(`${path}.label: is not a string`) + if (opts.label_CustomCheck && !opts.label_CustomCheck(o.label)) return new Error(`${path}.label: 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.offer_id !== 'string') return new Error(`${path}.offer_id: is not a string`) + if (opts.offer_id_CustomCheck && !opts.offer_id_CustomCheck(o.offer_id)) return new Error(`${path}.offer_id: 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`) + + return null +} + +export type OfferId = { + offer_id: string +} +export const OfferIdOptionalFields: [] = [] +export type OfferIdOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + offer_id_CustomCheck?: (v: string) => boolean +} +export const OfferIdValidate = (o?: OfferId, opts: OfferIdOptions = {}, path: string = 'OfferId::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.offer_id !== 'string') return new Error(`${path}.offer_id: is not a string`) + if (opts.offer_id_CustomCheck && !opts.offer_id_CustomCheck(o.offer_id)) return new Error(`${path}.offer_id: custom check failed`) + + return null +} + export type OpenChannel = { active: boolean capacity: number @@ -2482,6 +2586,26 @@ export const PayInvoiceResponseValidate = (o?: PayInvoiceResponse, opts: PayInvo return null } +export type PayerData = { + data: Record +} +export const PayerDataOptionalFields: [] = [] +export type PayerDataOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + data_CustomCheck?: (v: Record) => boolean +} +export const PayerDataValidate = (o?: PayerData, opts: PayerDataOptions = {}, path: string = 'PayerData::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.data !== 'object' || o.data === null) return new Error(`${path}.data: is not an object or is null`) + for (const key in o.data) { + if (typeof o.data[key] !== 'string') return new Error(`${path}.data['${key}']: is not a string`) + } + + return null +} + export type PaymentState = { amount: number network_fee: number @@ -3018,6 +3142,29 @@ export const UserInfoValidate = (o?: UserInfo, opts: UserInfoOptions = {}, path: return null } +export type UserOffers = { + offers: OfferConfig[] +} +export const UserOffersOptionalFields: [] = [] +export type UserOffersOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + offers_ItemOptions?: OfferConfigOptions + offers_CustomCheck?: (v: OfferConfig[]) => boolean +} +export const UserOffersValidate = (o?: UserOffers, opts: UserOffersOptions = {}, path: string = 'UserOffers::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 (!Array.isArray(o.offers)) return new Error(`${path}.offers: is not an array`) + for (let index = 0; index < o.offers.length; index++) { + const offersErr = OfferConfigValidate(o.offers[index], opts.offers_ItemOptions, `${path}.offers[${index}]`) + if (offersErr !== null) return offersErr + } + if (opts.offers_CustomCheck && !opts.offers_CustomCheck(o.offers)) return new Error(`${path}.offers: custom check failed`) + + return null +} + export type UserOperation = { amount: number confirmed: boolean diff --git a/proto/service/methods.proto b/proto/service/methods.proto index 36f9089e..a372331d 100644 --- a/proto/service/methods.proto +++ b/proto/service/methods.proto @@ -467,6 +467,42 @@ service LightningPub { option (http_route) = "/api/user/lnurl_channel/url"; option (nostr) = true; } + + rpc GetUserOffers(structs.Empty) returns (structs.UserOffers){ + option (auth_type) = "User"; + option (http_method) = "get"; + option (http_route) = "/api/user/offers/get"; + option (nostr) = true; + } + + rpc GetUserOffer(structs.OfferId) returns (structs.OfferConfig){ + option (auth_type) = "User"; + option (http_method) = "get"; + option (http_route) = "/api/user/offer/get"; + option (nostr) = true; + } + + rpc UpdateUserOffer(structs.OfferConfig) returns (structs.Empty){ + option (auth_type) = "User"; + option (http_method) = "post"; + option (http_route) = "/api/user/offer/update"; + option (nostr) = true; + } + + rpc DeleteUserOffer(structs.OfferId) returns (structs.Empty){ + option (auth_type) = "User"; + option (http_method) = "post"; + option (http_route) = "/api/user/offer/delete"; + option (nostr) = true; + } + + rpc AddUserOffer(structs.OfferConfig) returns (structs.OfferId){ + option (auth_type) = "User"; + option (http_method) = "post"; + option (http_route) = "/api/user/offer/add"; + option (nostr) = true; + } + rpc GetDebitAuthorizations(structs.Empty) returns (structs.DebitAuthorizations){ option (auth_type) = "User"; option (http_method) = "get"; diff --git a/proto/service/structs.proto b/proto/service/structs.proto index 79e43b44..deebabe4 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -269,6 +269,8 @@ message AddAppUserInvoiceRequest { string payer_identifier = 2; string http_callback_url = 3; NewInvoiceRequest invoice_req = 4; + optional PayerData payer_data = 5; + optional string offer_string = 6; } message GetAppUserRequest { @@ -331,6 +333,10 @@ message PayAddressResponse{ int64 network_fee = 4; } +message PayerData { + map data = 1; +} + message NewInvoiceRequest{ int64 amountSats = 1; string memo = 2; @@ -613,4 +619,25 @@ message DebitResponse { Empty denied = 3; string invoice = 4; } +} + +enum OfferDataType { + DATA_STRING = 0; +} + +message OfferId { + string offer_id = 1; +} + +message OfferConfig { + string offer_id = 1; + string label = 2; + int64 price_sats = 3; + string callback_url = 4; + map expected_data = 5; + string noffer = 6; +} + +message UserOffers { + repeated OfferConfig offers = 1; } \ No newline at end of file diff --git a/src/nostrMiddleware.ts b/src/nostrMiddleware.ts index 19a138af..541bb772 100644 --- a/src/nostrMiddleware.ts +++ b/src/nostrMiddleware.ts @@ -49,7 +49,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett } if (event.kind === 21001) { const offerReq = j as NofferData - mainHandler.handleNip69Noffer(offerReq, event) + mainHandler.offerManager.handleNip69Noffer(offerReq, event) return } else if (event.kind === 21002) { const debitReq = j as NdebitData diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index 627bb149..a271e04b 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -193,7 +193,10 @@ export default class { if (req.invoice_req.zap) { zapInfo = this.paymentManager.validateZapEvent(req.invoice_req.zap, req.invoice_req.amountSats) } - const opts: InboundOptionals = { callbackUrl: cbUrl, expiry: defaultInvoiceExpiry, expectedPayer: payer.user, linkedApplication: app, zapInfo } + const opts: InboundOptionals = { + callbackUrl: cbUrl, expiry: defaultInvoiceExpiry, expectedPayer: payer.user, linkedApplication: app, zapInfo, + offerId: req.offer_string, payerData: req.payer_data?.data + } const appUserInvoice = await this.paymentManager.NewInvoice(receiver.user.user_id, req.invoice_req, opts) return { invoice: appUserInvoice.invoice diff --git a/src/services/main/index.ts b/src/services/main/index.ts index bd9de9b9..45ef3d8f 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -24,6 +24,7 @@ import { Unlocker } from "./unlocker.js" import { defaultInvoiceExpiry } from "../storage/paymentStorage.js" import { DebitManager } from "./debitManager.js" import { NofferData } from "nostr-tools/lib/types/nip69.js" +import { OfferManager } from "./offerManager.js" type UserOperationsSub = { id: string @@ -49,6 +50,7 @@ export default class { liquidityManager: LiquidityManager liquidityProvider: LiquidityProvider debitManager: DebitManager + offerManager: OfferManager utils: Utils rugPullTracker: RugPullTracker unlocker: Unlocker @@ -71,6 +73,8 @@ export default class { this.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager) this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager) this.debitManager = new DebitManager(this.storage, this.lnd, this.applicationManager) + this.offerManager = new OfferManager(this.storage, this.lnd, this.applicationManager, this.productManager) + } Stop() { @@ -89,6 +93,7 @@ export default class { this.nostrSend = f this.liquidityProvider.attachNostrSend(f) this.debitManager.attachNostrSend(f) + this.offerManager.attachNostrSend(f) } htlcCb: HtlcCb = (e) => { @@ -286,70 +291,6 @@ export default class { log({ unsigned: event }) this.nostrSend({ type: 'app', appId: invoice.linkedApplication.app_id }, { type: 'event', event }, zapInfo.relays || undefined) } - - async getNofferInvoice(offerReq: NofferData, appId: string): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { - try { - - const { remote } = await this.lnd.ChannelBalance() - const { offer, amount } = offerReq - const split = offer.split(':') - if (split.length === 1) { - if (!amount || isNaN(amount) || amount < 10 || amount > remote) { - return { success: false, code: 5, max: remote } - } - const res = await this.applicationManager.AddAppUserInvoice(appId, { - http_callback_url: "", payer_identifier: split[0], receiver_identifier: split[0], - invoice_req: { amountSats: amount, memo: "Default NIP-69 Offer", zap: offerReq.zap } - }) - return { success: true, invoice: res.invoice } - } else if (split[0] === 'p') { - const product = await this.productManager.NewProductInvoice(split[1]) - return { success: true, invoice: product.invoice } - } else { - return { success: false, code: 1, max: remote } - } - } catch (e: any) { - getLogger({ component: "noffer" })(ERROR, e.message || e) - return { success: false, code: 1, max: 0 } - } - } - - async handleNip69Noffer(offerReq: NofferData, event: NostrEvent) { - const offerInvoice = await this.getNofferInvoice(offerReq, event.appId) - if (!offerInvoice.success) { - const code = offerInvoice.code - const e = newNofferResponse(JSON.stringify({ code, error: codeToMessage(code), range: { min: 10, max: offerInvoice.max } }), event) - this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) - return - } - const e = newNofferResponse(JSON.stringify({ bolt11: offerInvoice.invoice }), event) - this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) - return - } -} - -const codeToMessage = (code: number) => { - switch (code) { - case 1: return 'Invalid Offer' - case 2: return 'Temporary Failure' - case 3: return 'Expired Offer' - case 4: return 'Unsupported Feature' - case 5: return 'Invalid Amount' - default: throw new Error("unknown error code" + code) - } -} - -const newNofferResponse = (content: string, event: NostrEvent): UnsignedEvent => { - return { - content, - created_at: Math.floor(Date.now() / 1000), - kind: 21001, - pubkey: "", - tags: [ - ['p', event.pub], - ['e', event.id], - ], - } } diff --git a/src/services/main/offerManager.ts b/src/services/main/offerManager.ts new file mode 100644 index 00000000..0a1fea96 --- /dev/null +++ b/src/services/main/offerManager.ts @@ -0,0 +1,239 @@ +import crypto from 'crypto'; +import * as Types from "../../../proto/autogenerated/ts/types.js"; +import ApplicationManager from "./applicationManager.js"; +import ProductManager from "./productManager.js"; +import Storage from '../storage/index.js' +import LND from "../lnd/lnd.js" +import { ERROR, getLogger } from "../helpers/logger.js"; +import { DebitAccess, DebitAccessRules } from '../storage/entity/DebitAccess.js'; +import paymentManager from './paymentManager.js'; +import { Application } from '../storage/entity/Application.js'; +import { ApplicationUser } from '../storage/entity/ApplicationUser.js'; +import { NostrEvent, NostrSend, SendData, SendInitiator } from '../nostr/handler.js'; +import { UnsignedEvent } from 'nostr-tools'; +import { BudgetFrequency, NdebitData, NdebitFailure, NdebitSuccess, NdebitSuccessPayment, RecurringDebitTimeUnit } from 'nostr-tools/lib/types/nip68.js'; +import { NofferData } from "nostr-tools/lib/types/nip69.js" +import { UserOffer } from '../storage/entity/UserOffer.js'; +import { DeepPartial } from 'typeorm'; +import { nip19 } from 'nostr-tools'; +import { LoadNosrtSettingsFromEnv } from '../nostr/index.js'; + +const mapToOfferConfig = (offer: UserOffer, { pubkey, relay }: { pubkey: string, relay: string }): Types.OfferConfig => { + if (offer.expected_data) { + const keys = Object.keys(offer.expected_data) + for (const key of keys) { + const v = offer.expected_data[key] as Types.OfferDataType + if (!Types.OfferDataType[v]) { + offer.expected_data[key] = Types.OfferDataType.DATA_STRING + } + } + } + const offerStr = offer.offer_id + const priceType: nip19.OfferPriceType = offer.price_sats === 0 ? nip19.OfferPriceType.Spontaneous : nip19.OfferPriceType.Fixed + const noffer = nip19.nofferEncode({ pubkey, offer: offerStr, priceType, relay }) + return { + label: offer.label, + price_sats: offer.price_sats, + callback_url: offer.callback_url, + expected_data: (offer.expected_data || {}) as Record, + offer_id: offer.offer_id, + noffer: noffer + } +} +export class OfferManager { + + + + + + + + _nostrSend: NostrSend | null = null + + applicationManager: ApplicationManager + productManager: ProductManager + storage: Storage + lnd: LND + logger = getLogger({ component: 'DebitManager' }) + constructor(storage: Storage, lnd: LND, applicationManager: ApplicationManager, productManager: ProductManager) { + this.storage = storage + this.lnd = lnd + this.applicationManager = applicationManager + this.productManager = productManager + } + + attachNostrSend = (nostrSend: NostrSend) => { + this._nostrSend = nostrSend + } + nostrSend: NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => { + if (!this._nostrSend) { + throw new Error("No nostrSend attached") + } + this._nostrSend(initiator, data, relays) + } + + async AddUserOffer(ctx: Types.UserContext, req: Types.OfferConfig): Promise { + const newOffer = await this.storage.offerStorage.AddUserOffer(ctx.app_user_id, { + expected_data: req.expected_data, + label: req.label, + price_sats: req.price_sats, + callback_url: req.callback_url, + }) + return { + offer_id: newOffer.offer_id + } + } + + async DeleteUserOffer(ctx: Types.UserContext, req: Types.OfferId) { + await this.storage.offerStorage.DeleteUserOffer(ctx.app_user_id, req.offer_id) + } + + async UpdateUserOffer(ctx: Types.UserContext, req: Types.OfferConfig) { + await this.storage.offerStorage.UpdateUserOffer(ctx.app_user_id, { + expected_data: req.expected_data, + label: req.label, + price_sats: req.price_sats, + callback_url: req.callback_url, + }) + } + + async GetUserOffer(ctx: Types.UserContext, req: Types.OfferId): Promise { + const app = await this.applicationManager.GetApp(ctx.app_id) + if (!app) { + throw new Error("App not found") + } + const offer = await this.storage.offerStorage.GetUserOffer(ctx.app_user_id, req.offer_id) + if (!offer) { + throw new Error("Offer not found") + } + const nostrSettings = LoadNosrtSettingsFromEnv() + return mapToOfferConfig(offer, { pubkey: app.npub, relay: nostrSettings.relays[0] }) + } + + async GetUserOffers(ctx: Types.UserContext): Promise { + const app = await this.applicationManager.GetApp(ctx.app_id) + if (!app) { + throw new Error("App not found") + } + const offers = await this.storage.offerStorage.GetUserOffers(ctx.app_user_id) + const nostrSettings = LoadNosrtSettingsFromEnv() + return { + offers: offers.map(o => mapToOfferConfig(o, { pubkey: app.npub, relay: nostrSettings.relays[0] })) + } + } + + ValidateExpectedData(userOffer: UserOffer, payerData: any): { passed: false, validated: undefined } | { passed: true, validated: Record } { + const expected = userOffer.expected_data + if (!expected) { + return { passed: true, validated: {} } + } + const expectedKeys = Object.keys(expected) + if (expectedKeys.length === 0) { + return { passed: true, validated: {} } + } + if (typeof payerData !== 'object' || payerData === null) { + return { passed: false, validated: undefined } + } + const validated: Record = {} + for (const key of expectedKeys) { + if (typeof payerData[key] !== 'string') { + return { passed: false, validated: undefined } + } + validated[key] = payerData[key] + } + return { passed: true, validated } + } + + async handleNip69Noffer(offerReq: NofferData, event: NostrEvent) { + const offerInvoice = await this.getNofferInvoice(offerReq, event.appId) + if (!offerInvoice.success) { + const code = offerInvoice.code + const e = newNofferResponse(JSON.stringify({ code, error: codeToMessage(code), range: { min: 10, max: offerInvoice.max } }), event) + this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) + return + } + const e = newNofferResponse(JSON.stringify({ bolt11: offerInvoice.invoice }), event) + this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) + return + } + + async HandleDefaultUserOffer(offerReq: NofferData, appId: string, remote: number): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { + const { amount, offer } = offerReq + if (!amount || isNaN(amount) || amount < 10 || amount > remote) { + return { success: false, code: 5, max: remote } + } + const res = await this.applicationManager.AddAppUserInvoice(appId, { + http_callback_url: "", payer_identifier: offer, receiver_identifier: offer, + invoice_req: { amountSats: amount, memo: "Default NIP-69 Offer", zap: offerReq.zap }, + offer_string: 'offer' + }) + return { success: true, invoice: res.invoice } + } + + async HandleUserOffer(offerReq: NofferData, appId: string, remote: number): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { + const { amount, offer } = offerReq + const userOffer = await this.storage.offerStorage.GetOffer(offer) + if (!userOffer) { + return this.HandleDefaultUserOffer(offerReq, appId, remote) + } + let amt = userOffer.price_sats + if (userOffer.price_sats === 0) { + if (!amount || isNaN(amount) || amount < 10 || amount > remote) { + return { success: false, code: 5, max: remote } + } + amt = amount + } + const { passed, validated } = this.ValidateExpectedData(userOffer, offerReq.payer_data) + if (!passed) { + return { success: false, code: 1, max: remote } + } + const res = await this.applicationManager.AddAppUserInvoice(appId, { + http_callback_url: userOffer.callback_url, payer_identifier: offer, receiver_identifier: offer, + invoice_req: { amountSats: amt, memo: userOffer.label, zap: offerReq.zap }, + payer_data: validated ? { data: validated } : undefined, + offer_string: offer + }) + return { success: true, invoice: res.invoice } + } + + async getNofferInvoice(offerReq: NofferData, appId: string): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { + try { + const { remote } = await this.lnd.ChannelBalance() + const split = offerReq.offer.split(':') + if (split.length === 1) { + return this.HandleUserOffer(offerReq, appId, remote) + } else if (split[0] === 'p') { + const product = await this.productManager.NewProductInvoice(split[1]) + return { success: true, invoice: product.invoice } + } else { + return { success: false, code: 1, max: remote } + } + } catch (e: any) { + getLogger({ component: "noffer" })(ERROR, e.message || e) + return { success: false, code: 1, max: 0 } + } + } + +} +const newNofferResponse = (content: string, event: NostrEvent): UnsignedEvent => { + return { + content, + created_at: Math.floor(Date.now() / 1000), + kind: 21001, + pubkey: "", + tags: [ + ['p', event.pub], + ['e', event.id], + ], + } +} +const codeToMessage = (code: number) => { + switch (code) { + case 1: return 'Invalid Offer' + case 2: return 'Temporary Failure' + case 3: return 'Expired Offer' + case 4: return 'Unsupported Feature' + case 5: return 'Invalid Amount' + default: throw new Error("unknown error code" + code) + } +} \ No newline at end of file diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index a998d2ef..7a9b0e0c 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -323,6 +323,33 @@ export default (mainHandler: Main): Types.ServerMethods => { }, RespondToDebit: async ({ ctx, req }) => { return mainHandler.debitManager.RespondToDebit(ctx, req); + }, + AddUserOffer: async ({ ctx, req }) => { + const err = Types.OfferConfigValidate(req, { + label_CustomCheck: label => label !== '', + }) + if (err != null) throw new Error(err.message) + return mainHandler.offerManager.AddUserOffer(ctx, req) + }, + DeleteUserOffer: async ({ ctx, req }) => { + const err = Types.OfferIdValidate(req, { + offer_id_CustomCheck: id => id !== '' + }) + if (err != null) throw new Error(err.message) + return mainHandler.offerManager.DeleteUserOffer(ctx, req) + }, + UpdateUserOffer: async ({ ctx, req }) => { + return mainHandler.offerManager.UpdateUserOffer(ctx, req) + }, + GetUserOffers: async ({ ctx }) => { + return mainHandler.offerManager.GetUserOffers(ctx) + }, + GetUserOffer: async ({ ctx, req }) => { + const err = Types.OfferIdValidate(req, { + offer_id_CustomCheck: id => id !== '' + }) + if (err != null) throw new Error(err.message) + return mainHandler.offerManager.GetUserOffer(ctx, req) } } } \ No newline at end of file diff --git a/src/services/storage/entity/UserOffer.ts b/src/services/storage/entity/UserOffer.ts new file mode 100644 index 00000000..409c4e8d --- /dev/null +++ b/src/services/storage/entity/UserOffer.ts @@ -0,0 +1,37 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn, UpdateDateColumn } from "typeorm" +import { User } from "./User.js" + +@Entity() +export class UserOffer { + + @PrimaryGeneratedColumn() + serial_id: number + + @Column() + app_user_id: string + + @Column({ unique: true, nullable: false }) + offer_id: string + + @Column() + label: string + + @Column({ default: 0 }) + price_sats: number + + @Column({ default: "" }) + callback_url: string + + @Column({ + nullable: true, + type: 'simple-json', + default: null + }) + expected_data: Record | null + + @CreateDateColumn() + created_at: Date + + @UpdateDateColumn() + updated_at: Date +} diff --git a/src/services/storage/entity/UserReceivingInvoice.ts b/src/services/storage/entity/UserReceivingInvoice.ts index 356f4886..cef021bc 100644 --- a/src/services/storage/entity/UserReceivingInvoice.ts +++ b/src/services/storage/entity/UserReceivingInvoice.ts @@ -58,6 +58,15 @@ export class UserReceivingInvoice { }) zap_info?: ZapInfo + @Column({ + nullable: true, + type: 'simple-json' + }) + payer_data?: Record + + @Column({ default: "" }) + offer_id?: string + @Column({ nullable: true, }) diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index a37b1de1..f266fd01 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -11,6 +11,7 @@ import EventsLogManager from "./eventsLog.js"; import { LiquidityStorage } from "./liquidityStorage.js"; import { StateBundler } from "./stateBundler.js"; import DebitStorage from "./debitStorage.js" +import OfferStorage from "./offerStorage.js" export type StorageSettings = { dbSettings: DbSettings eventLogPath: string @@ -30,6 +31,7 @@ export default class { metricsStorage: MetricsStorage liquidityStorage: LiquidityStorage debitStorage: DebitStorage + offerStorage: OfferStorage eventsLog: EventsLogManager stateBundler: StateBundler constructor(settings: StorageSettings) { @@ -47,6 +49,7 @@ export default class { this.metricsStorage = new MetricsStorage(this.settings) this.liquidityStorage = new LiquidityStorage(this.DB, this.txQueue) this.debitStorage = new DebitStorage(this.DB, this.txQueue) + this.offerStorage = new OfferStorage(this.DB, this.txQueue) try { if (this.settings.dataDir) fs.mkdirSync(this.settings.dataDir) } catch (e) { } const executedMetricsMigrations = await this.metricsStorage.Connect(metricsMigrations) return { executedMigrations, executedMetricsMigrations }; diff --git a/src/services/storage/offerStorage.ts b/src/services/storage/offerStorage.ts new file mode 100644 index 00000000..f1941f86 --- /dev/null +++ b/src/services/storage/offerStorage.ts @@ -0,0 +1,41 @@ +import { DataSource, EntityManager } from "typeorm" +import crypto from 'crypto'; +import UserStorage from './userStorage.js'; +import TransactionsQueue from "./transactionsQueue.js"; +import { DebitAccess, DebitAccessRules } from "./entity/DebitAccess.js"; +import { UserOffer } from "./entity/UserOffer.js"; +export default class { + + + DB: DataSource | EntityManager + txQueue: TransactionsQueue + constructor(DB: DataSource | EntityManager, txQueue: TransactionsQueue) { + this.DB = DB + this.txQueue = txQueue + } + async AddUserOffer(appUserId: string, req: Partial): Promise { + const newUserOffer = this.DB.getRepository(UserOffer).create({ + ...req, + app_user_id: appUserId, + offer_id: crypto.randomBytes(34).toString('hex') + }) + return this.txQueue.PushToQueue({ exec: async db => db.getRepository(UserOffer).save(newUserOffer), dbTx: false, description: `add offer for ${appUserId}: ${req.label} ` }) + } + + async DeleteUserOffer(appUserId: string, offerId: string, entityManager = this.DB) { + await entityManager.getRepository(UserOffer).delete({ app_user_id: appUserId, offer_id: offerId }) + } + async UpdateUserOffer(app_user_id: string, req: Partial) { + return this.DB.getRepository(UserOffer).update({ app_user_id, offer_id: req.offer_id }, req) + } + + async GetUserOffers(app_user_id: string): Promise { + return this.DB.getRepository(UserOffer).find({ where: { app_user_id } }) + } + async GetUserOffer(app_user_id: string, offer_id: string): Promise { + return this.DB.getRepository(UserOffer).findOne({ where: { app_user_id, offer_id } }) + } + async GetOffer(offer_id: string): Promise { + return this.DB.getRepository(UserOffer).findOne({ where: { offer_id } }) + } +} \ No newline at end of file diff --git a/src/services/storage/paymentStorage.ts b/src/services/storage/paymentStorage.ts index 06e3043a..051b9943 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -13,7 +13,7 @@ import { UserToUserPayment } from './entity/UserToUserPayment.js'; import { Application } from './entity/Application.js'; import TransactionsQueue from "./transactionsQueue.js"; import { LoggedEvent } from './eventsLog.js'; -export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo } +export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record } export const defaultInvoiceExpiry = 60 * 60 export default class { DB: DataSource | EntityManager @@ -102,7 +102,9 @@ export default class { payer: options.expectedPayer, linkedApplication: options.linkedApplication, zap_info: options.zapInfo, - liquidityProvider: providerDestination + liquidityProvider: providerDestination, + offer_id: options.offerId, + payer_data: options.payerData, }) return this.txQueue.PushToQueue({ exec: async db => db.getRepository(UserReceivingInvoice).save(newUserInvoice), dbTx: false, description: `add invoice for ${user.user_id} linked to ${options.linkedApplication?.app_id}: ${invoice} ` }) }