|
1 | 1 | import { encodeFunctionData } from "viem"; |
2 | 2 | import { EvmWalletProvider } from "./wallet-providers"; |
3 | | -import { approve } from "./utils"; |
| 3 | +import { approve, applyGasMultiplier, retryWithExponentialBackoff } from "./utils"; |
4 | 4 |
|
5 | 5 | const MOCK_TOKEN_ADDRESS = "0x1234567890123456789012345678901234567890"; |
6 | 6 | const MOCK_SPENDER_ADDRESS = "0x9876543210987654321098765432109876543210"; |
@@ -67,4 +67,145 @@ describe("utils", () => { |
67 | 67 | expect(response).toBe(`Error approving tokens: ${error}`); |
68 | 68 | }); |
69 | 69 | }); |
| 70 | + |
| 71 | + describe("applyGasMultiplier", () => { |
| 72 | + it("should scale gas estimate by multiplier", () => { |
| 73 | + const gas = BigInt(21000); |
| 74 | + const multiplier = 1.2; |
| 75 | + const result = applyGasMultiplier(gas, multiplier); |
| 76 | + expect(result).toBe(BigInt(25200)); |
| 77 | + }); |
| 78 | + |
| 79 | + it("should handle multiplier of 1 (no change)", () => { |
| 80 | + const gas = BigInt(100000); |
| 81 | + const result = applyGasMultiplier(gas, 1); |
| 82 | + expect(result).toBe(BigInt(100000)); |
| 83 | + }); |
| 84 | + |
| 85 | + it("should handle multiplier less than 1", () => { |
| 86 | + const gas = BigInt(100000); |
| 87 | + const result = applyGasMultiplier(gas, 0.5); |
| 88 | + expect(result).toBe(BigInt(50000)); |
| 89 | + }); |
| 90 | + |
| 91 | + it("should round to nearest integer", () => { |
| 92 | + const gas = BigInt(100); |
| 93 | + const multiplier = 1.5; |
| 94 | + const result = applyGasMultiplier(gas, multiplier); |
| 95 | + expect(result).toBe(BigInt(150)); |
| 96 | + }); |
| 97 | + |
| 98 | + it("should handle very large gas values", () => { |
| 99 | + const gas = BigInt("1000000000000"); |
| 100 | + const multiplier = 1.1; |
| 101 | + const result = applyGasMultiplier(gas, multiplier); |
| 102 | + expect(result).toBe(BigInt("1100000000000")); |
| 103 | + }); |
| 104 | + |
| 105 | + it("should handle zero gas", () => { |
| 106 | + const gas = BigInt(0); |
| 107 | + const result = applyGasMultiplier(gas, 1.5); |
| 108 | + expect(result).toBe(BigInt(0)); |
| 109 | + }); |
| 110 | + }); |
| 111 | + |
| 112 | + describe("retryWithExponentialBackoff", () => { |
| 113 | + beforeEach(() => { |
| 114 | + jest.useFakeTimers(); |
| 115 | + }); |
| 116 | + |
| 117 | + afterEach(() => { |
| 118 | + jest.useRealTimers(); |
| 119 | + }); |
| 120 | + |
| 121 | + it("should return result on first successful attempt", async () => { |
| 122 | + const fn = jest.fn().mockResolvedValue("success"); |
| 123 | + const promise = retryWithExponentialBackoff(fn); |
| 124 | + const result = await promise; |
| 125 | + expect(result).toBe("success"); |
| 126 | + expect(fn).toHaveBeenCalledTimes(1); |
| 127 | + }); |
| 128 | + |
| 129 | + it("should retry on failure and succeed eventually", async () => { |
| 130 | + const fn = jest |
| 131 | + .fn() |
| 132 | + .mockRejectedValueOnce(new Error("fail1")) |
| 133 | + .mockRejectedValueOnce(new Error("fail2")) |
| 134 | + .mockResolvedValue("success"); |
| 135 | + |
| 136 | + const promise = retryWithExponentialBackoff(fn, 3, 100, 0); |
| 137 | + |
| 138 | + // First attempt fails immediately |
| 139 | + await jest.advanceTimersByTimeAsync(0); |
| 140 | + // Wait for first retry delay (100ms) |
| 141 | + await jest.advanceTimersByTimeAsync(100); |
| 142 | + // Wait for second retry delay (200ms) |
| 143 | + await jest.advanceTimersByTimeAsync(200); |
| 144 | + |
| 145 | + const result = await promise; |
| 146 | + expect(result).toBe("success"); |
| 147 | + expect(fn).toHaveBeenCalledTimes(3); |
| 148 | + }); |
| 149 | + |
| 150 | + it("should throw after max retries exceeded", async () => { |
| 151 | + const error = new Error("persistent failure"); |
| 152 | + const fn = jest.fn().mockRejectedValue(error); |
| 153 | + |
| 154 | + const promise = retryWithExponentialBackoff(fn, 2, 100, 0); |
| 155 | + |
| 156 | + // Advance through all retry delays |
| 157 | + await jest.advanceTimersByTimeAsync(100); |
| 158 | + await jest.advanceTimersByTimeAsync(200); |
| 159 | + |
| 160 | + await expect(promise).rejects.toThrow("persistent failure"); |
| 161 | + expect(fn).toHaveBeenCalledTimes(3); // initial + 2 retries |
| 162 | + }); |
| 163 | + |
| 164 | + it("should respect initial delay", async () => { |
| 165 | + const fn = jest.fn().mockResolvedValue("success"); |
| 166 | + const promise = retryWithExponentialBackoff(fn, 3, 1000, 500); |
| 167 | + |
| 168 | + // Function should not be called before initial delay |
| 169 | + expect(fn).not.toHaveBeenCalled(); |
| 170 | + |
| 171 | + // Advance past initial delay |
| 172 | + await jest.advanceTimersByTimeAsync(500); |
| 173 | + |
| 174 | + const result = await promise; |
| 175 | + expect(result).toBe("success"); |
| 176 | + expect(fn).toHaveBeenCalledTimes(1); |
| 177 | + }); |
| 178 | + |
| 179 | + it("should use exponential backoff delays", async () => { |
| 180 | + const fn = jest |
| 181 | + .fn() |
| 182 | + .mockRejectedValueOnce(new Error("fail1")) |
| 183 | + .mockRejectedValueOnce(new Error("fail2")) |
| 184 | + .mockRejectedValueOnce(new Error("fail3")) |
| 185 | + .mockResolvedValue("success"); |
| 186 | + |
| 187 | + const baseDelay = 100; |
| 188 | + const promise = retryWithExponentialBackoff(fn, 3, baseDelay, 0); |
| 189 | + |
| 190 | + // First attempt |
| 191 | + await jest.advanceTimersByTimeAsync(0); |
| 192 | + expect(fn).toHaveBeenCalledTimes(1); |
| 193 | + |
| 194 | + // First retry after 100ms (baseDelay * 2^0) |
| 195 | + await jest.advanceTimersByTimeAsync(100); |
| 196 | + expect(fn).toHaveBeenCalledTimes(2); |
| 197 | + |
| 198 | + // Second retry after 200ms (baseDelay * 2^1) |
| 199 | + await jest.advanceTimersByTimeAsync(200); |
| 200 | + expect(fn).toHaveBeenCalledTimes(3); |
| 201 | + |
| 202 | + // Third retry after 400ms (baseDelay * 2^2) |
| 203 | + await jest.advanceTimersByTimeAsync(400); |
| 204 | + expect(fn).toHaveBeenCalledTimes(4); |
| 205 | + |
| 206 | + const result = await promise; |
| 207 | + expect(result).toBe("success"); |
| 208 | + }); |
| 209 | + }); |
70 | 210 | }); |
| 211 | + |
0 commit comments