diff --git a/src/activity/exe-script-executor.ts b/src/activity/exe-script-executor.ts index ff5948b8a..602e8c900 100644 --- a/src/activity/exe-script-executor.ts +++ b/src/activity/exe-script-executor.ts @@ -10,6 +10,7 @@ import retry from "async-retry"; import { Result, ResultData, StreamingBatchEvent } from "./results"; import sleep from "../shared/utils/sleep"; import { Activity } from "./activity"; +import { getMessageFromApiError } from "../shared/utils/apiErrorMessage"; export interface ExeScriptRequest { text: string; @@ -80,7 +81,7 @@ export class ExeScriptExecutor { ? this.streamingBatch(batchId, batchSize, startTime, timeout) : this.pollingBatch(batchId, startTime, timeout, maxRetries); } catch (error) { - const message = error?.response?.data?.message || error.message || error; + const message = getMessageFromApiError(error); this.logger.error("Execution of script failed.", { reason: message, diff --git a/src/market/error.ts b/src/market/error.ts index a0b761224..a84ec760b 100644 --- a/src/market/error.ts +++ b/src/market/error.ts @@ -1,6 +1,8 @@ import { GolemModuleError } from "../shared/error/golem-error"; export enum MarketErrorCode { + CouldNotGetAgreement = "CouldNotGetAgreement", + CouldNotGetProposal = "CouldNotGetProposal", ServiceNotInitialized = "ServiceNotInitialized", MissingAllocation = "MissingAllocation", SubscriptionFailed = "SubscriptionFailed", diff --git a/src/payment/agreement_payment_process.ts b/src/payment/agreement_payment_process.ts index f3494b3e4..fec8f5c04 100644 --- a/src/payment/agreement_payment_process.ts +++ b/src/payment/agreement_payment_process.ts @@ -9,6 +9,7 @@ import AsyncLock from "async-lock"; import { GolemPaymentError, PaymentErrorCode } from "./error"; import { GolemUserError } from "../shared/error/golem-error"; import { IPaymentApi } from "./types"; +import { getMessageFromApiError } from "../shared/utils/apiErrorMessage"; /** * Process manager that controls the logic behind processing events related to an agreement which result with payments @@ -125,10 +126,10 @@ export class AgreementPaymentProcess { return true; } catch (error) { - const reason = error?.response?.data?.message || error; + const message = getMessageFromApiError(error); // this.events.emit("paymentFailed", { id: this.id, agreementId: this.agreementId, reason }); throw new GolemPaymentError( - `Unable to accept debit note ${debitNote.id}. ${reason}`, + `Unable to accept debit note ${debitNote.id}. ${message}`, PaymentErrorCode.DebitNoteAcceptanceFailed, undefined, debitNote.provider, @@ -149,8 +150,9 @@ export class AgreementPaymentProcess { // await this.paymentApi.rejectDebitNote(debitNote, rejectMessage); this.logger.warn(`DebitNote rejected`, { reason: rejectMessage }); } catch (error) { + const message = getMessageFromApiError(error); throw new GolemPaymentError( - `Unable to reject debit note ${debitNote.id}. ${error?.response?.data?.message || error}`, + `Unable to reject debit note ${debitNote.id}. ${message}`, PaymentErrorCode.DebitNoteRejectionFailed, undefined, debitNote.provider, @@ -221,12 +223,12 @@ export class AgreementPaymentProcess { // provider: invoice.provider, // }); } catch (error) { - const reason = error?.response?.data?.message || error; + const message = getMessageFromApiError(error); // this.events.emit("paymentFailed", { invoiceId: invoice.id, agreementId: invoice.agreementId, reason }); throw new GolemPaymentError( - `Unable to accept invoice ${invoice.id} ${reason}`, + `Unable to accept invoice ${invoice.id} ${message}`, PaymentErrorCode.InvoiceAcceptanceFailed, undefined, invoice.provider, @@ -246,8 +248,9 @@ export class AgreementPaymentProcess { await this.paymentApi.rejectInvoice(invoice, message); this.logger.warn(`Invoice rejected`, { reason: message }); } catch (error) { + const message = getMessageFromApiError(error); throw new GolemPaymentError( - `Unable to reject invoice ${invoice.id} ${error?.response?.data?.message || error}`, + `Unable to reject invoice ${invoice.id} ${message}`, PaymentErrorCode.InvoiceRejectionFailed, undefined, invoice.provider, diff --git a/src/payment/error.ts b/src/payment/error.ts index e5d4d0538..0a566a1f0 100644 --- a/src/payment/error.ts +++ b/src/payment/error.ts @@ -11,6 +11,8 @@ export enum PaymentErrorCode { DebitNoteAcceptanceFailed = "DebitNoteAcceptanceFailed", InvoiceRejectionFailed = "InvoiceRejectionFailed", DebitNoteRejectionFailed = "DebitNoteRejectionFailed", + CouldNotGetDebitNote = "CouldNotGetDebitNote", + CouldNotGetInvoice = "CouldNotGetInvoice", PaymentStatusQueryFailed = "PaymentStatusQueryFailed", AgreementAlreadyPaid = "AgreementAlreadyPaid", InvoiceAlreadyReceived = "InvoiceAlreadyReceived", diff --git a/src/shared/utils/apiErrorMessage.ts b/src/shared/utils/apiErrorMessage.ts new file mode 100644 index 000000000..e5ac9cef6 --- /dev/null +++ b/src/shared/utils/apiErrorMessage.ts @@ -0,0 +1,22 @@ +import YaTsClient from "ya-ts-client"; +function isApiError(error: unknown): error is YaTsClient.ActivityApi.ApiError { + return typeof error == "object" && error !== null && "name" in error && error.name === "ApiError"; +} +/** + * Try to extract a message from a yagna API error. + * If the error is not an instance of `ApiError`, return the error message. + */ +export function getMessageFromApiError(error: unknown): string { + if (!(error instanceof Error)) { + return String(error); + } + + if (isApiError(error)) { + try { + return JSON.stringify(error.body, null, 2); + } catch (_jsonParseError) { + return error.message; + } + } + return error.message; +} diff --git a/src/shared/yagna/adapters/activity-api-adapter.ts b/src/shared/yagna/adapters/activity-api-adapter.ts index 57983c7b3..23f61a293 100644 --- a/src/shared/yagna/adapters/activity-api-adapter.ts +++ b/src/shared/yagna/adapters/activity-api-adapter.ts @@ -1,7 +1,8 @@ import { Agreement } from "../../../agreement"; import { ActivityApi } from "ya-ts-client"; -import { Activity, ActivityStateEnum, IActivityApi } from "../../../activity"; +import { Activity, ActivityStateEnum, GolemWorkError, IActivityApi, WorkErrorCode } from "../../../activity"; import { IActivityRepository } from "../../../activity/activity"; +import { getMessageFromApiError } from "../../utils/apiErrorMessage"; export class ActivityApiAdapter implements IActivityApi { constructor( @@ -15,19 +16,41 @@ export class ActivityApiAdapter implements IActivityApi { } async createActivity(agreement: Agreement): Promise { - // TODO: Use options - // @ts-expect-error: FIXME #yagna ts types - const { activityId } = await this.control.createActivity({ - agreementId: agreement.id, - }); + try { + // TODO: Use options + // @ts-expect-error: FIXME #yagna ts types + const { activityId } = await this.control.createActivity({ + agreementId: agreement.id, + }); - return this.activityRepo.getById(activityId); + return this.activityRepo.getById(activityId); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemWorkError( + `Failed to create activity: ${message}`, + WorkErrorCode.ActivityCreationFailed, + agreement, + undefined, + agreement.getProviderInfo(), + ); + } } async destroyActivity(activity: Activity): Promise { - await this.control.destroyActivity(activity.id, 30); + try { + await this.control.destroyActivity(activity.id, 30); - return this.activityRepo.getById(activity.id); + return this.activityRepo.getById(activity.id); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemWorkError( + `Failed to destroy activity: ${message}`, + WorkErrorCode.ActivityDestroyingFailed, + activity.agreement, + activity, + activity.agreement.getProviderInfo(), + ); + } } async getActivityState(id: string): Promise { diff --git a/src/shared/yagna/adapters/agreement-api-adapter.ts b/src/shared/yagna/adapters/agreement-api-adapter.ts index cd30eb56c..e91a88733 100644 --- a/src/shared/yagna/adapters/agreement-api-adapter.ts +++ b/src/shared/yagna/adapters/agreement-api-adapter.ts @@ -5,6 +5,7 @@ import { withTimeout } from "../../utils/timeout"; import { Logger } from "../../utils"; import { AgreementApiConfig } from "../../../agreement"; import { GolemUserError } from "../../error/golem-error"; +import { getMessageFromApiError } from "../../utils/apiErrorMessage"; export class AgreementApiAdapter implements IAgreementApi { constructor( @@ -73,8 +74,9 @@ export class AgreementApiAdapter implements IAgreementApi { return this.repository.getById(agreementId); } catch (error) { + const message = getMessageFromApiError(error); throw new GolemMarketError( - `Unable to create agreement ${error?.response?.data?.message || error?.response?.data || error}`, + `Unable to create agreement ${message}`, MarketErrorCode.LeaseProcessCreationFailed, error, ); @@ -134,8 +136,9 @@ export class AgreementApiAdapter implements IAgreementApi { return this.repository.getById(agreement.id); } catch (error) { + const message = getMessageFromApiError(error); throw new GolemMarketError( - `Unable to terminate agreement ${agreement.id}. ${error.response?.data?.message || error.response?.data || error}`, + `Unable to terminate agreement ${agreement.id}. ${message}`, MarketErrorCode.LeaseProcessTerminationFailed, error, ); diff --git a/src/shared/yagna/adapters/market-api-adapter.ts b/src/shared/yagna/adapters/market-api-adapter.ts index c4e059ff6..9337c8e97 100644 --- a/src/shared/yagna/adapters/market-api-adapter.ts +++ b/src/shared/yagna/adapters/market-api-adapter.ts @@ -13,6 +13,7 @@ import YaTsClient from "ya-ts-client"; import { GolemInternalError } from "../../error/golem-error"; import { Logger } from "../../utils"; import { DemandBodyPrototype, DemandPropertyValue } from "../../../market/demand/demand-body-builder"; +import { getMessageFromApiError } from "../../utils/apiErrorMessage"; /** * A bit more user-friendly type definition of DemandOfferBaseDTO from ya-ts-client @@ -124,8 +125,9 @@ export class MarketApiAdapter implements MarketApi { this.logger.debug("Proposal rejection result from yagna", { response: result }); } catch (error) { + const message = getMessageFromApiError(error); throw new GolemMarketError( - `Failed to reject proposal. ${error?.response?.data?.message || error}`, + `Failed to reject proposal. ${message}`, MarketErrorCode.ProposalRejectionFailed, error, ); diff --git a/src/shared/yagna/adapters/payment-api-adapter.ts b/src/shared/yagna/adapters/payment-api-adapter.ts index 1f05ba772..69593bc27 100644 --- a/src/shared/yagna/adapters/payment-api-adapter.ts +++ b/src/shared/yagna/adapters/payment-api-adapter.ts @@ -11,6 +11,7 @@ import { import { IInvoiceRepository } from "../../../payment/invoice"; import { Logger, YagnaApi } from "../../utils"; import { IDebitNoteRepository } from "../../../payment/debit_note"; +import { getMessageFromApiError } from "../../utils/apiErrorMessage"; export class PaymentApiAdapter implements IPaymentApi { public receivedInvoices$ = new Subject(); @@ -76,41 +77,81 @@ export class PaymentApiAdapter implements IPaymentApi { } async acceptInvoice(invoice: Invoice, allocation: Allocation, amount: string): Promise { - await this.yagna.payment.acceptInvoice(invoice.id, { - totalAmountAccepted: amount, - allocationId: allocation.id, - }); + try { + await this.yagna.payment.acceptInvoice(invoice.id, { + totalAmountAccepted: amount, + allocationId: allocation.id, + }); - return this.invoiceRepo.getById(invoice.id); + return this.invoiceRepo.getById(invoice.id); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemPaymentError( + `Could not accept invoice. ${message}`, + PaymentErrorCode.InvoiceAcceptanceFailed, + allocation, + invoice.provider, + ); + } } async rejectInvoice(invoice: Invoice, reason: string): Promise { - await this.yagna.payment.rejectInvoice(invoice.id, { - rejectionReason: "BAD_SERVICE", - totalAmountAccepted: "0.00", - message: reason, - }); + try { + await this.yagna.payment.rejectInvoice(invoice.id, { + rejectionReason: "BAD_SERVICE", + totalAmountAccepted: "0.00", + message: reason, + }); - return this.invoiceRepo.getById(invoice.id); + return this.invoiceRepo.getById(invoice.id); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemPaymentError( + `Could not reject invoice. ${message}`, + PaymentErrorCode.InvoiceRejectionFailed, + undefined, + invoice.provider, + ); + } } async acceptDebitNote(debitNote: DebitNote, allocation: Allocation, amount: string): Promise { - await this.yagna.payment.acceptDebitNote(debitNote.id, { - totalAmountAccepted: amount, - allocationId: allocation.id, - }); + try { + await this.yagna.payment.acceptDebitNote(debitNote.id, { + totalAmountAccepted: amount, + allocationId: allocation.id, + }); - return this.debitNoteRepo.getById(debitNote.id); + return this.debitNoteRepo.getById(debitNote.id); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemPaymentError( + `Could not accept debit note. ${message}`, + PaymentErrorCode.DebitNoteAcceptanceFailed, + allocation, + debitNote.provider, + ); + } } async rejectDebitNote(debitNote: DebitNote, reason: string): Promise { - await this.yagna.payment.rejectDebitNote(debitNote.id, { - rejectionReason: "BAD_SERVICE", - totalAmountAccepted: "0.00", - message: reason, - }); + try { + await this.yagna.payment.rejectDebitNote(debitNote.id, { + rejectionReason: "BAD_SERVICE", + totalAmountAccepted: "0.00", + message: reason, + }); - return this.debitNoteRepo.getById(debitNote.id); + return this.debitNoteRepo.getById(debitNote.id); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemPaymentError( + `Could not reject debit note. ${message}`, + PaymentErrorCode.DebitNoteRejectionFailed, + undefined, + debitNote.provider, + ); + } } async getAllocation(id: string) { @@ -118,8 +159,9 @@ export class PaymentApiAdapter implements IPaymentApi { const model = await this.yagna.payment.getAllocation(id); return new Allocation(model); } catch (error) { + const message = getMessageFromApiError(error); throw new GolemPaymentError( - `Could not retrieve allocation. ${error.response?.data?.message || error.response?.data || error}`, + `Could not retrieve allocation. ${message}`, PaymentErrorCode.AllocationCreationFailed, undefined, undefined, @@ -154,8 +196,9 @@ export class PaymentApiAdapter implements IPaymentApi { return allocation; } catch (error) { + const message = getMessageFromApiError(error); throw new GolemPaymentError( - `Could not create new allocation. ${error.response?.data?.message || error.response?.data || error}`, + `Could not create new allocation. ${message}`, PaymentErrorCode.AllocationCreationFailed, undefined, undefined, diff --git a/src/shared/yagna/repository/activity-repository.ts b/src/shared/yagna/repository/activity-repository.ts index e56cb148a..3f4e0cffd 100644 --- a/src/shared/yagna/repository/activity-repository.ts +++ b/src/shared/yagna/repository/activity-repository.ts @@ -1,6 +1,8 @@ import { Activity, ActivityStateEnum, IActivityRepository } from "../../../activity/activity"; import { ActivityApi } from "ya-ts-client"; import { IAgreementRepository } from "../../../agreement/agreement"; +import { getMessageFromApiError } from "../../utils/apiErrorMessage"; +import { GolemWorkError, WorkErrorCode } from "../../../activity"; export class ActivityRepository implements IActivityRepository { constructor( @@ -9,20 +11,44 @@ export class ActivityRepository implements IActivityRepository { ) {} async getById(id: string): Promise { - const agreementId = await this.state.getActivityAgreement(id); - const agreement = await this.agreementRepo.getById(agreementId); - const state = await this.getStateOfActivity(id); - const usage = await this.state.getActivityUsage(id); + try { + const agreementId = await this.state.getActivityAgreement(id); + const agreement = await this.agreementRepo.getById(agreementId); + const state = await this.getStateOfActivity(id); + const usage = await this.state.getActivityUsage(id); - return new Activity(id, agreement, state ?? ActivityStateEnum.Unknown, usage); + return new Activity(id, agreement, state ?? ActivityStateEnum.Unknown, usage); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemWorkError( + `Failed to get activity: ${message}`, + WorkErrorCode.ActivityStatusQueryFailed, + undefined, + undefined, + undefined, + error, + ); + } } async getStateOfActivity(id: string): Promise { - const state = await this.state.getActivityState(id); - if (!state || state.state[0] === null) { - return ActivityStateEnum.Unknown; - } + try { + const state = await this.state.getActivityState(id); + if (!state || state.state[0] === null) { + return ActivityStateEnum.Unknown; + } - return ActivityStateEnum[state.state[0]]; + return ActivityStateEnum[state.state[0]]; + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemWorkError( + `Failed to get activity state: ${message}`, + WorkErrorCode.ActivityStatusQueryFailed, + undefined, + undefined, + undefined, + error, + ); + } } } diff --git a/src/shared/yagna/repository/agreement-repository.ts b/src/shared/yagna/repository/agreement-repository.ts index 307aa4cc1..aeda1ae67 100644 --- a/src/shared/yagna/repository/agreement-repository.ts +++ b/src/shared/yagna/repository/agreement-repository.ts @@ -3,6 +3,8 @@ import { MarketApi } from "ya-ts-client"; import { GolemInternalError } from "../../error/golem-error"; import { IDemandRepository } from "../../../market/demand"; import { CacheService } from "../../cache/CacheService"; +import { getMessageFromApiError } from "../../utils/apiErrorMessage"; +import { GolemMarketError, MarketErrorCode } from "../../../market"; export class AgreementRepository implements IAgreementRepository { private readonly cache = new CacheService(); @@ -13,8 +15,13 @@ export class AgreementRepository implements IAgreementRepository { ) {} async getById(id: string): Promise { - const dto = await this.api.getAgreement(id); - + let dto; + try { + dto = await this.api.getAgreement(id); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemMarketError(`Failed to get agreement: ${message}`, MarketErrorCode.CouldNotGetAgreement, error); + } const { demandId } = dto.demand; const demand = this.demandRepo.getById(demandId); diff --git a/src/shared/yagna/repository/debit-note-repository.ts b/src/shared/yagna/repository/debit-note-repository.ts index d7f4f1cbd..d2df1a10d 100644 --- a/src/shared/yagna/repository/debit-note-repository.ts +++ b/src/shared/yagna/repository/debit-note-repository.ts @@ -1,6 +1,9 @@ import { DebitNote, IDebitNoteRepository } from "../../../payment/debit_note"; import { MarketApi, PaymentApi } from "ya-ts-client"; import { ProposalProperties } from "../../../market/offer-proposal"; +import { getMessageFromApiError } from "../../utils/apiErrorMessage"; +import { GolemPaymentError, PaymentErrorCode } from "../../../payment"; +import { GolemMarketError, MarketErrorCode } from "../../../market"; export class DebitNoteRepository implements IDebitNoteRepository { constructor( @@ -9,8 +12,31 @@ export class DebitNoteRepository implements IDebitNoteRepository { ) {} async getById(id: string): Promise { - const model = await this.paymentClient.getDebitNote(id); - const agreement = await this.marketClient.getAgreement(model.agreementId); + let model; + let agreement; + try { + model = await this.paymentClient.getDebitNote(id); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemPaymentError( + `Failed to get debit note: ${message}`, + PaymentErrorCode.CouldNotGetDebitNote, + undefined, + undefined, + error, + ); + } + + try { + agreement = await this.marketClient.getAgreement(model.agreementId); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemMarketError( + `Failed to get agreement for debit note: ${message}`, + MarketErrorCode.CouldNotGetAgreement, + error, + ); + } const providerInfo = { id: model.issuerId, diff --git a/src/shared/yagna/repository/invoice-repository.ts b/src/shared/yagna/repository/invoice-repository.ts index 8f616f8d3..e286801ab 100644 --- a/src/shared/yagna/repository/invoice-repository.ts +++ b/src/shared/yagna/repository/invoice-repository.ts @@ -1,6 +1,9 @@ import { IInvoiceRepository, Invoice } from "../../../payment/invoice"; import { MarketApi, PaymentApi } from "ya-ts-client"; import { ProposalProperties } from "../../../market/offer-proposal"; +import { getMessageFromApiError } from "../../utils/apiErrorMessage"; +import { GolemPaymentError, PaymentErrorCode } from "../../../payment"; +import { GolemMarketError, MarketErrorCode } from "../../../market"; export class InvoiceRepository implements IInvoiceRepository { constructor( @@ -9,9 +12,31 @@ export class InvoiceRepository implements IInvoiceRepository { ) {} async getById(id: string): Promise { - const model = await this.paymentClient.getInvoice(id); - const agreement = await this.marketClient.getAgreement(model.agreementId); + let model; + let agreement; + try { + model = await this.paymentClient.getInvoice(id); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemPaymentError( + `Failed to get debit note: ${message}`, + PaymentErrorCode.CouldNotGetInvoice, + undefined, + undefined, + error, + ); + } + try { + agreement = await this.marketClient.getAgreement(model.agreementId); + } catch (error) { + const message = getMessageFromApiError(error); + throw new GolemMarketError( + `Failed to get agreement for invoice: ${message}`, + MarketErrorCode.CouldNotGetAgreement, + error, + ); + } const providerInfo = { id: model.issuerId, walletAddress: model.payeeAddr, diff --git a/src/shared/yagna/repository/proposal-repository.ts b/src/shared/yagna/repository/proposal-repository.ts index e8089a584..2c968145d 100644 --- a/src/shared/yagna/repository/proposal-repository.ts +++ b/src/shared/yagna/repository/proposal-repository.ts @@ -1,6 +1,6 @@ import { IProposalRepository, OfferProposal } from "../../../market/offer-proposal"; import { MarketApi } from "ya-ts-client"; -import { Demand } from "../../../market"; +import { Demand, GolemMarketError, MarketErrorCode } from "../../../market"; import { CacheService } from "../../cache/CacheService"; export class ProposalRepository implements IProposalRepository { @@ -19,7 +19,12 @@ export class ProposalRepository implements IProposalRepository { } async getByDemandAndId(demand: Demand, id: string): Promise { - const dto = await this.api.getProposalOffer(demand.id, id); - return new OfferProposal(dto, demand); + try { + const dto = await this.api.getProposalOffer(demand.id, id); + return new OfferProposal(dto, demand); + } catch (error) { + const message = error.message; + throw new GolemMarketError(`Failed to get proposal: ${message}`, MarketErrorCode.CouldNotGetProposal, error); + } } } diff --git a/tests/e2e/leaseProcessPool.spec.ts b/tests/e2e/leaseProcessPool.spec.ts index 019146891..ae01ec75d 100644 --- a/tests/e2e/leaseProcessPool.spec.ts +++ b/tests/e2e/leaseProcessPool.spec.ts @@ -1,5 +1,5 @@ import { Subscription } from "rxjs"; -import { Allocation, DraftOfferProposalPool, GolemNetwork, YagnaApi } from "../../src"; +import { Allocation, DraftOfferProposalPool, GolemNetwork } from "../../src"; describe("LeaseProcessPool", () => { const glm = new GolemNetwork(); @@ -15,7 +15,11 @@ describe("LeaseProcessPool", () => { beforeAll(async () => { await glm.connect(); - allocation = await modules.payment.createAllocation({ budget: 1, expirationSec: 60 }); + allocation = await modules.payment.createAllocation({ + budget: 1, + // 30 minutes + expirationSec: 60 * 30, + }); }); afterAll(async () => {