Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { DeploymentHttpService } from "@akashnetwork/http-sdk";
import type { BalanceHttpService, DeploymentHttpService } from "@akashnetwork/http-sdk";
import createError from "http-errors";
import { describe, expect, it, vi } from "vitest";
import { mock } from "vitest-mock-extended";

import type { UserWalletRepository } from "@src/billing/repositories";
import type { BillingConfigService } from "@src/billing/services/billing-config/billing-config.service";
import { ChainErrorService } from "@src/billing/services/chain-error/chain-error.service";
import type { TxManagerService } from "@src/billing/services/tx-manager/tx-manager.service";
import type { LoggerService } from "@src/core/providers/logging.provider";
import { JOB_NAME, type JobPayload, type JobQueueService } from "@src/core/services/job-queue/job-queue.service";
import type { GetDeploymentResponse } from "@src/deployment/http-schemas/deployment.schema";
Expand Down Expand Up @@ -179,6 +182,44 @@ describe(CloseTrialDeploymentHandler.name, () => {
expect(jobQueueService.enqueue).not.toHaveBeenCalled();
});

it("logs unsettleable event and skips retry and notification when escrow cannot be settled", async () => {
const wallet = createUserWallet({
id: 123,
userId: "user-123",
address: "akash1test",
isTrialing: true
});

const closeError = createError(400, "Deployment escrow cannot be settled yet", {
originalError: new Error("Query failed with (6): rpc error: code = Unknown desc = recovered: negative decimal coin amount: -2.000000000000000000")
});

const { handler, jobQueueService, logger } = setup({
findWalletById: vi.fn().mockResolvedValue(wallet),
findDeployment: vi.fn().mockResolvedValue({ deployment: { state: "active" } } as GetDeploymentResponse["data"]),
closeDeployment: vi.fn().mockRejectedValue(closeError)
});

const payload: JobPayload<CloseTrialDeployment> = {
walletId: wallet.id,
dseq: "test-dseq",
version: 1
};

await expect(handler.handle(payload)).resolves.toBeUndefined();

expect(jobQueueService.enqueue).not.toHaveBeenCalled();
expect(logger.error).toHaveBeenCalledWith({
event: "CLOSE_TRIAL_DEPLOYMENT_UNSETTLEABLE",
reason: "Deployment escrow cannot be settled yet; chain rejects close until it settles",
job: CloseTrialDeployment[JOB_NAME],
walletId: payload.walletId,
dseq: payload.dseq,
owner: wallet.address,
userId: wallet.userId
});
});

it("logs error when find deployment returns an error", async () => {
const wallet = createUserWallet({ id: 123, userId: "user-123", address: "akash1test", isTrialing: true });

Expand Down Expand Up @@ -272,7 +313,8 @@ describe(CloseTrialDeploymentHandler.name, () => {
}),
billingConfig: mock<BillingConfigService>({
get: vi.fn().mockReturnValue(input?.trialDeploymentLifetimeInHours ?? 24)
})
}),
chainErrorService: new ChainErrorService(mock<BalanceHttpService>(), mock<BillingConfigService>(), mock<TxManagerService>())
};

const handler = new CloseTrialDeploymentHandler(
Expand All @@ -281,7 +323,8 @@ describe(CloseTrialDeploymentHandler.name, () => {
mocks.jobQueueService,
mocks.deploymentWriterService,
mocks.deploymentService,
mocks.billingConfig
mocks.billingConfig,
mocks.chainErrorService
);

return { handler, ...mocks };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { singleton } from "tsyringe";

import { UserWalletRepository } from "@src/billing/repositories";
import { BillingConfigService } from "@src/billing/services/billing-config/billing-config.service";
import { ChainErrorService } from "@src/billing/services/chain-error/chain-error.service";
import { Job, JOB_NAME, JobHandler, JobPayload, JobQueueService, LoggerService } from "@src/core";
import { DeploymentWriterService } from "@src/deployment/services/deployment-writer/deployment-writer.service";
import { RESOLVED_MARKER } from "@src/notifications/services/notification-data-resolver/notification-data-resolver.service";
Expand Down Expand Up @@ -33,7 +34,8 @@ export class CloseTrialDeploymentHandler implements JobHandler<CloseTrialDeploym
private readonly jobQueueService: JobQueueService,
private readonly deploymentWriterService: DeploymentWriterService,
private readonly deploymentService: DeploymentHttpService,
private readonly billingConfig: BillingConfigService
private readonly billingConfig: BillingConfigService,
private readonly chainErrorService: ChainErrorService
) {}

async handle(payload: JobPayload<CloseTrialDeployment>): Promise<void> {
Expand Down Expand Up @@ -115,7 +117,23 @@ export class CloseTrialDeploymentHandler implements JobHandler<CloseTrialDeploym
return;
}

await this.deploymentWriterService.close({ ...wallet, address }, payload.dseq);
try {
await this.deploymentWriterService.close({ ...wallet, address }, payload.dseq);
} catch (error) {
if (error instanceof Error && this.chainErrorService.isUnsettleableDeploymentError(error)) {
this.logger.error({
event: "CLOSE_TRIAL_DEPLOYMENT_UNSETTLEABLE",
reason: "Deployment escrow cannot be settled yet; chain rejects close until it settles",
job: CloseTrialDeployment[JOB_NAME],
walletId: payload.walletId,
dseq: payload.dseq,
owner: address,
userId: wallet.userId
});
return;
}
throw error;
}

await this.jobQueueService.enqueue(
new NotificationJob({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,15 @@ describe(ChainErrorService.name, () => {
expect(appErr.message).toBe("Insufficient balance");
});

it("returns 400 for escrow settlement underflow panic", async () => {
const { service } = setup();
const err = new Error("Query failed with (6): rpc error: code = Unknown desc = recovered: negative decimal coin amount: -2.000000000000000000");

const appErr = await service.toAppError(err, encodeMessages);
expect(appErr).toBeInstanceOf(BadRequest);
expect(appErr.message).toBe("Deployment escrow cannot be settled yet");
});

it("returns 502 when cause is an AxiosError with 502 status", async () => {
const { service } = setup();
const axiosError = new AxiosError("Request failed", "ERR_BAD_RESPONSE", undefined, undefined, {
Expand Down Expand Up @@ -280,6 +289,30 @@ describe(ChainErrorService.name, () => {
});
});

describe("isUnsettleableDeploymentError", () => {
it("returns true for the escrow settlement underflow panic", () => {
const { service } = setup();
const err = new Error("Query failed with (6): rpc error: code = Unknown desc = recovered: negative decimal coin amount: -2.000000000000000000");

expect(service.isUnsettleableDeploymentError(err)).toBe(true);
});

it("returns true for an error already normalized by toAppError", async () => {
const { service } = setup();
const rawPanic = new Error("Query failed with (6): rpc error: code = Unknown desc = recovered: negative decimal coin amount: -2.000000000000000000");
const appError = await service.toAppError(rawPanic, []);

expect(appError.message).toBe("Deployment escrow cannot be settled yet");
expect(service.isUnsettleableDeploymentError(appError)).toBe(true);
});

it("returns false for an unrelated error", () => {
const { service } = setup();

expect(service.isUnsettleableDeploymentError(new Error("insufficient balance"))).toBe(false);
});
});

function setup(): {
balanceHttpService: MockProxy<BalanceHttpService>;
billingConfigService: MockProxy<BillingConfigService>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { singleton } from "tsyringe";
import { BillingConfigService } from "@src/billing/services/billing-config/billing-config.service";
import { TxManagerService } from "@src/billing/services/tx-manager/tx-manager.service";

const ESCROW_SETTLEMENT_UNDERFLOW_MESSAGE = "negative decimal coin amount" as const;

@singleton()
export class ChainErrorService {
private readonly ERRORS = {
Expand Down Expand Up @@ -69,6 +71,10 @@ export class ChainErrorService {
"insufficient balance": {
code: 402,
message: "Insufficient balance"
},
[ESCROW_SETTLEMENT_UNDERFLOW_MESSAGE]: {
code: 400,
message: "Deployment escrow cannot be settled yet"
}
};

Expand Down Expand Up @@ -120,6 +126,12 @@ export class ChainErrorService {
return /account closed|deployment closed/i.test(error.message);
}

public isUnsettleableDeploymentError(error: Error): boolean {
const originalError = (error as { originalError?: unknown }).originalError;
const messages = [error.message, originalError instanceof Error ? originalError.message : undefined];
return messages.some(message => message?.toLowerCase().includes(ESCROW_SETTLEMENT_UNDERFLOW_MESSAGE));
}
Comment thread
baktun14 marked this conversation as resolved.
Comment thread
baktun14 marked this conversation as resolved.

public async isMasterWalletInsufficientFundsError(error: Error) {
if (!error.message.toLowerCase().includes("insufficient funds")) return false;

Expand Down
Loading