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
3 changes: 3 additions & 0 deletions config.env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ maxConcurrency: $MAX_CONCURRENCY
skipSweep: $SKIP_SWEEP
ownerProfile: $OWNER_PROFILE
selfFundVaults: $SELF_FUND_ORDERS
sweepWalletTime: $SWEEP_WALLET_TIME
convertToGasTime: $CONVERT_TO_GAS_TIME
rotateMultiWallet: $ROTATE_MULTI_WALLET
sgFilter:
includeOrders: $INCLUDE_ORDERS
excludeOrders: $EXCLUDE_ORDERS
Expand Down
9 changes: 9 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@ maxRatio: true
# The number of orders that will be processed concurrently, default is 1, i.e. no concurrency
maxConcurrency: 10

# Time (in days) to sweep multi wallet holdings back into main wallet, default is 0 meaning no sweep
sweepWalletTime: 0

# Time (in days) to convert main wallet holdings into gas token, default is 0 meaning no conversion
convertToGasTime: 10

# Determines if multi wallets should be rotated at runtime, meaning new ones to replace older ones once they runs out of gas, default is false
rotateMultiWallet: false

# list of tokens to skip sweeping
skipSweep:
- "0x123...456"
Expand Down
3 changes: 3 additions & 0 deletions src/cli/commands/sweep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ export async function sweepFunds(opts: SweepOptions) {
maxConcurrency: 0,
skipSweep: new Set(),
headroom: 0,
sweepWalletTime: 0,
convertToGasTime: 0,
rotateMultiWallet: false,
};

// prepare state config fields
Expand Down
56 changes: 40 additions & 16 deletions src/cli/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,16 +622,28 @@ describe("Test RainSolverCli", () => {
(mockWalletManager.retryPendingAddWorkers as Mock).mockResolvedValue(
mockRetryAddReports,
);
(mockWalletManager.retryPendingRemoveWorkers as Mock).mockResolvedValue([]);
(mockWalletManager.assessWorkers as Mock).mockResolvedValue(mockAssessReports);
(mockOrderManager.downscaleProtection as Mock).mockResolvedValue(undefined);

// enable rotation and elapse the assess timer, keep sweep and conversion disabled
(rainSolverCli as any).appOptions.rotateMultiWallet = true;
(rainSolverCli as any).nextAssesWorkerWalletTime = Date.now() - 1;
(rainSolverCli as any).nextSweepTime = 0;
(rainSolverCli as any).nextGasConversionTime = 0;

await rainSolverCli.runWalletOpsForRound(mockRoundCtx as any);

expect(mockWalletManager.retryPendingAddWorkers).toHaveBeenCalledTimes(1);
expect(mockWalletManager.retryPendingRemoveWorkers).toHaveBeenCalledTimes(1);
expect(mockWalletManager.assessWorkers).toHaveBeenCalledTimes(1);
expect(mockWalletManager.retryPendingRemoveWorkers).not.toHaveBeenCalled();
expect(mockWalletManager.convertHoldingsToGas).not.toHaveBeenCalled();
expect(mockOrderManager.downscaleProtection).not.toHaveBeenCalled();
expect(mockWalletManager.sweepWallet).not.toHaveBeenCalled();
expect(mockOrderManager.downscaleProtection).toHaveBeenCalled();

// assess timer should have been rearmed, so next round should not assess again
await rainSolverCli.runWalletOpsForRound(mockRoundCtx as any);
expect(mockWalletManager.assessWorkers).toHaveBeenCalledTimes(1);

expect(mockLogger.exportPreAssembledSpan).toHaveBeenCalledWith(
{ name: "retry-add-1" },
Expand Down Expand Up @@ -659,9 +671,9 @@ describe("Test RainSolverCli", () => {
);
});

it("should call downscale operation on round 250", async () => {
rainSolverCli.roundCount = 250;
it("should call downscale operation on timer", async () => {
const mockRoundCtx = { test: "context" };
(rainSolverCli as any).nextAssesWorkerWalletTime = Date.now() - 1;

(mockWalletManager.retryPendingAddWorkers as Mock).mockResolvedValue([]);
(mockWalletManager.assessWorkers as Mock).mockResolvedValue([]);
Expand All @@ -672,34 +684,30 @@ describe("Test RainSolverCli", () => {
expect(mockOrderManager.downscaleProtection).toHaveBeenCalledOnce();
});

it("should sweep every 5 days and convert to gas evey day", async () => {
it("should sweep and convert to gas per configured times", async () => {
const mockRoundCtx = { test: "context" };
const mockPendingRemoveReports = [{ name: "pending-remove-1" }];
const mockConvertHoldingsReport = { name: "convert-holdings" };
const mockSweepReport = { name: "sweep" };

(mockWalletManager.retryPendingAddWorkers as Mock).mockResolvedValue([]);
(mockWalletManager.assessWorkers as Mock).mockResolvedValue([]);
(mockWalletManager.retryPendingRemoveWorkers as Mock).mockResolvedValue(
mockPendingRemoveReports,
);
(mockWalletManager.retryPendingRemoveWorkers as Mock).mockResolvedValue([]);
(mockWalletManager.convertHoldingsToGas as Mock).mockResolvedValue(
mockConvertHoldingsReport,
);
(mockWalletManager.sweepWallet as Mock).mockResolvedValue(mockSweepReport);
(rainSolverCli as any).appOptions.sweepWalletTime = 5;
(rainSolverCli as any).appOptions.convertToGasTime = 1;
(rainSolverCli as any).nextSweepTime = Date.now() - 6 * DAY;
(rainSolverCli as any).nextGasConversionTime = Date.now() - 2 * DAY;

await rainSolverCli.runWalletOpsForRound(mockRoundCtx as any);

expect(mockWalletManager.retryPendingRemoveWorkers).toHaveBeenCalledTimes(1);
// pending remove workers retry only runs when rotateMultiWallet is enabled
expect(mockWalletManager.retryPendingRemoveWorkers).not.toHaveBeenCalled();
expect(mockWalletManager.convertHoldingsToGas).toHaveBeenCalledTimes(1);
expect(mockWalletManager.sweepWallet).toHaveBeenCalledTimes(1);

expect(mockLogger.exportPreAssembledSpan).toHaveBeenCalledWith(
{ name: "pending-remove-1" },
mockRoundCtx,
);
expect(mockLogger.exportPreAssembledSpan).toHaveBeenCalledWith(
mockConvertHoldingsReport,
mockRoundCtx,
Expand All @@ -709,9 +717,12 @@ describe("Test RainSolverCli", () => {
mockRoundCtx,
);

// timers should have been rearmed with the configured times
expect((rainSolverCli as any).nextSweepTime).toBeGreaterThan(Date.now() + 4 * DAY);
expect((rainSolverCli as any).nextGasConversionTime).toBeGreaterThan(Date.now());

// next time should not call these when time has not reached
await rainSolverCli.runWalletOpsForRound(mockRoundCtx as any);
expect(mockWalletManager.retryPendingRemoveWorkers).toHaveBeenCalledTimes(1);
expect(mockWalletManager.convertHoldingsToGas).toHaveBeenCalledTimes(1);
expect(mockWalletManager.sweepWallet).toHaveBeenCalledTimes(1);

Expand All @@ -720,10 +731,23 @@ describe("Test RainSolverCli", () => {
(rainSolverCli as any).nextGasConversionTime = Date.now() - 2 * DAY;

await rainSolverCli.runWalletOpsForRound(mockRoundCtx as any);
expect(mockWalletManager.retryPendingRemoveWorkers).toHaveBeenCalledTimes(2);
expect(mockWalletManager.convertHoldingsToGas).toHaveBeenCalledTimes(2);
expect(mockWalletManager.sweepWallet).toHaveBeenCalledTimes(2);
});

it("should not sweep or convert to gas when disabled", async () => {
const mockRoundCtx = { test: "context" };

(mockWalletManager.retryPendingAddWorkers as Mock).mockResolvedValue([]);
(mockWalletManager.assessWorkers as Mock).mockResolvedValue([]);
(rainSolverCli as any).nextSweepTime = 0;
(rainSolverCli as any).nextGasConversionTime = 0;

await rainSolverCli.runWalletOpsForRound(mockRoundCtx as any);

expect(mockWalletManager.convertHoldingsToGas).not.toHaveBeenCalled();
expect(mockWalletManager.sweepWallet).not.toHaveBeenCalled();
});
});

describe("Test processOrdersForRound method", () => {
Expand Down
80 changes: 49 additions & 31 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ config();
export * from "./commands";
export { main } from "./main";

/** Represents the duration of a minute in milliseconds */
export const MINUTE = 60 * 1000;

/** Represents the duration of a day in milliseconds */
export const DAY = 24 * 60 * 60 * 1000;
export const DAY = 24 * 60 * MINUTE;

/**
* The `RainSolverCli` class serves as the main entry point and orchestrator for the
Expand Down Expand Up @@ -62,10 +65,13 @@ export class RainSolverCli {

private nextGasReset = Date.now() + DAY;
private nextDatafetcherReset: number;

/** Wallet sweeper timer (once every 5 days) */
private nextSweepTime = Date.now() + 5 * DAY;
private nextSweepTime: number;
/** Convert bounties to gas timer (once every day) */
private nextGasConversionTime = Date.now() + DAY;
private nextGasConversionTime: number;
/** Time for next worker wallet assessment */
private nextAssesWorkerWalletTime = Date.now() + 15 * MINUTE;

private constructor(
state: SharedState,
Expand All @@ -85,6 +91,10 @@ export class RainSolverCli {
this.rainSolver = rainSolver;
this.logger = logger;
this.nextDatafetcherReset = nextDatafetcherReset;
this.nextSweepTime =
appOptions.sweepWalletTime === 0 ? 0 : Date.now() + appOptions.sweepWalletTime * DAY;
this.nextGasConversionTime =
appOptions.convertToGasTime === 0 ? 0 : Date.now() + appOptions.convertToGasTime * DAY;
}

/**
Expand Down Expand Up @@ -162,6 +172,8 @@ export class RainSolverCli {
const { orderManager, report } = orderManagerResult.value;
logger.exportPreAssembledSpan(report);

await orderManager.downscaleProtection();

// init wallet manager
const { walletManager, reports } = await WalletManager.init(state);
reports.forEach((statusReport) => logger.exportPreAssembledSpan(statusReport));
Expand Down Expand Up @@ -328,51 +340,57 @@ export class RainSolverCli {
* @param roundCtx - The otel context for the current round
*/
async runWalletOpsForRound(roundCtx: Context) {
// retry pending add workers
const retryPendingAddReports = await this.walletManager.retryPendingAddWorkers();
retryPendingAddReports.forEach((report) => {
this.logger.exportPreAssembledSpan(report, roundCtx);
});

// assess workers
const assessReports = await this.walletManager.assessWorkers();
assessReports.forEach((report) => {
this.logger.exportPreAssembledSpan(report.removeWorkerReport, roundCtx);
this.logger.exportPreAssembledSpan(report.addWorkerReport, roundCtx);
});

// rescale once every 250 rounds
if (this.roundCount % 250 === 0) {
// re-evaluate owner limits
await this.orderManager.downscaleProtection();
}

const now = Date.now();

if (this.nextGasConversionTime <= now) {
this.nextGasConversionTime = now + DAY;
if (this.appOptions.rotateMultiWallet) {
// retry pending add workers
const retryPendingAddReports = await this.walletManager.retryPendingAddWorkers();
retryPendingAddReports.forEach((report) => {
this.logger.exportPreAssembledSpan(report, roundCtx);
});

// retry pending remove workers
const pendingRemoveReports = await this.walletManager.retryPendingRemoveWorkers();
pendingRemoveReports.forEach((report) => {
this.logger.exportPreAssembledSpan(report, roundCtx);
});
}

// try to sweep main wallet's tokens back to gas
await this.walletManager.convertHoldingsToGas().then((convertHoldingsToGasReport) => {
this.logger.exportPreAssembledSpan(convertHoldingsToGasReport, roundCtx);
const now = Date.now();

// assess workers
if (this.nextAssesWorkerWalletTime <= now) {
this.nextAssesWorkerWalletTime = now + 15 * MINUTE;
const assessReports = await this.walletManager.assessWorkers();
assessReports.forEach((report) => {
if (report.removeWorkerReport)
this.logger.exportPreAssembledSpan(report.removeWorkerReport, roundCtx);
if (report.addWorkerReport)
this.logger.exportPreAssembledSpan(report.addWorkerReport, roundCtx);
if (report.topupWorkerReport)
this.logger.exportPreAssembledSpan(report.topupWorkerReport, roundCtx);
});

// re-evaluate owner limits
await this.orderManager.downscaleProtection();
}

if (this.nextSweepTime <= now) {
this.nextSweepTime = now + 5 * DAY;
if (this.nextSweepTime !== 0 && this.nextSweepTime <= now) {
this.nextSweepTime = now + this.appOptions.sweepWalletTime * DAY;
// sweep worker wallet bounties
for (const [, worker] of this.walletManager.workers.signers) {
await this.walletManager.sweepWallet(worker, false).then((report) => {
this.logger.exportPreAssembledSpan(report, roundCtx);
});
}
}

if (this.nextGasConversionTime !== 0 && this.nextGasConversionTime <= now) {
this.nextGasConversionTime = now + this.appOptions.convertToGasTime * DAY;

// try to sweep main wallet's tokens back to gas
await this.walletManager.convertHoldingsToGas().then((convertHoldingsToGasReport) => {
this.logger.exportPreAssembledSpan(convertHoldingsToGasReport, roundCtx);
});
}
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/config/yaml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ orderbookTradeTypes:
maxConcurrency: 15,
skipSweep: new Set([`0x${"8".repeat(40)}`, `0x${"9".repeat(40)}`]),
headroom: 102.5,
sweepWalletTime: 0,
convertToGasTime: 0,
rotateMultiWallet: false,
};

// AppOptions returned from fromYaml() should match expected
Expand Down Expand Up @@ -193,6 +196,9 @@ orderbookTradeTypes:
maxRatio: true,
maxConcurrency: undefined,
skipSweep: undefined,
sweepWalletTime: "10",
convertToGasTime: "2",
rotateMultiWallet: true,
ownerProfile: [
{ "0x4444444444444444444444444444444444444444": "100" },
{ "0x5555555555555555555555555555555555555555": "max" },
Expand Down Expand Up @@ -332,5 +338,8 @@ orderbookTradeTypes:
});

assert.deepEqual(result.skipSweep, new Set());
assert.equal(result.sweepWalletTime, 10);
assert.equal(result.convertToGasTime, 2);
assert.equal(result.rotateMultiWallet, true);
});
});
35 changes: 35 additions & 0 deletions src/config/yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ export type AppOptions = {
skipSweep: Set<string>;
/** Trade simulation profitablity headroom, default: 2.5 */
headroom: number;
/** Time (in days) to sweep multi wallet holdings back into main wallet, default is 0 meaning no sweep */
sweepWalletTime: number;
/** Time (in days) to convert main wallet holdings into gas token, default is 0 meaning no conversion */
convertToGasTime: number;
/** Determines if multi wallets should be rotated at runtime, meaning new ones to replace older ones once they runs out of gas, default is false */
rotateMultiWallet: boolean;
};

/** Provides methods to instantiate and validate AppOptions */
Expand Down Expand Up @@ -304,6 +310,35 @@ export namespace AppOptions {
"invalid headroom value, must be a number greater than equal to 0",
"2.5",
) + 100,
sweepWalletTime: Validator.resolveNumericValue(
input.sweepWalletTime,
INT_PATTERN,
"invalid sweepWalletTime, must be an integer greater than equal to 0",
"0",
undefined,
(sweepWalletTime) =>
assert(
sweepWalletTime >= 0,
"invalid sweepWalletTime, must be an integer greater than equal to 0",
),
),
convertToGasTime: Validator.resolveNumericValue(
input.convertToGasTime,
INT_PATTERN,
"invalid convertToGasTime, must be an integer greater than equal to 0",
"0",
undefined,
(convertToGasTime) =>
assert(
convertToGasTime >= 0,
"invalid convertToGasTime, must be an integer greater than equal to 0",
),
),
rotateMultiWallet: Validator.resolveBool(
input.rotateMultiWallet,
"expected a boolean value for rotateMultiWallet",
false,
),
} as AppOptions);
} catch (error: any) {
if (error instanceof AppOptionsError) {
Expand Down
2 changes: 1 addition & 1 deletion src/oracle/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export namespace OracleConstants {
/** Per-request timeout */
export const ORACLE_TIMEOUT_MS = 5_000 as const;
/** How long to skip a failing oracle (ms) */
export const COOLOFF_DURATION_MS = 5 * 60 * 1_000;
export const COOLOFF_DURATION_MS = 15 * 1_000;

/** List of known oracle URLs */
export const KnownUrls = [
Expand Down
Loading
Loading