UNPKG

pmarket-cli

Version:
182 lines 8.89 kB
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals'; import { ethers } from 'ethers'; import { RedeemStrategy } from './redeem-strategy.js'; const mockTx = { hash: '0xabc123', wait: jest.fn().mockResolvedValue({}), }; function makePosition(overrides = {}) { return { asset: 'token_123', conditionId: '0xcondition1', curPrice: 1.0, currentValue: 10, initialValue: 5, outcome: 'Yes', percentPnl: 100, cashPnl: 5, price: 1.0, pricePerShare: 0.5, proxyWallet: null, redeemable: true, size: 10, title: 'Test Market', ...overrides, }; } describe('RedeemStrategy', () => { let polymarketService; let contractService; let strategy; beforeEach(() => { jest.useFakeTimers(); polymarketService = { getWalletAddress: jest.fn().mockReturnValue('0xWALLET'), getPositions: jest.fn().mockResolvedValue([]), }; contractService = { isStandardRedemption: jest.fn().mockResolvedValue(true), redeemPositions: jest.fn().mockResolvedValue(mockTx), redeemNegRiskPositions: jest.fn().mockResolvedValue(mockTx), }; strategy = new RedeemStrategy(polymarketService, contractService); }); afterEach(() => { jest.useRealTimers(); }); it('should print message and return when no redeemable positions', async () => { const consoleSpy = jest.spyOn(console, 'log'); polymarketService.getPositions.mockResolvedValue([ makePosition({ redeemable: false }), ]); await strategy.execute(); expect(consoleSpy).toHaveBeenCalledWith('No redeemable positions found.'); expect(contractService.isStandardRedemption).not.toHaveBeenCalled(); consoleSpy.mockRestore(); }); it('should use standard CTF redemption when isStandardRedemption returns true', async () => { const consoleSpy = jest.spyOn(console, 'log'); polymarketService.getPositions.mockResolvedValue([ makePosition({ conditionId: '0xSTD', redeemable: true, size: 20 }), ]); contractService.isStandardRedemption.mockResolvedValue(true); const promise = strategy.execute(); await jest.runAllTimersAsync(); await promise; expect(contractService.isStandardRedemption).toHaveBeenCalledWith('0xSTD'); expect(contractService.redeemPositions).toHaveBeenCalledWith('0xSTD'); expect(contractService.redeemNegRiskPositions).not.toHaveBeenCalled(); expect(consoleSpy).toHaveBeenCalledWith(' Detected: standard market'); expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Redeemed 1/1')); consoleSpy.mockRestore(); }); it('should use NegRiskAdapter when isStandardRedemption returns false', async () => { const consoleSpy = jest.spyOn(console, 'log'); polymarketService.getPositions.mockResolvedValue([ makePosition({ conditionId: '0xNEG', redeemable: true, outcome: 'Yes', size: 15 }), ]); contractService.isStandardRedemption.mockResolvedValue(false); const promise = strategy.execute(); await jest.runAllTimersAsync(); await promise; expect(contractService.isStandardRedemption).toHaveBeenCalledWith('0xNEG'); expect(contractService.redeemNegRiskPositions).toHaveBeenCalledWith('0xNEG', [ ethers.parseUnits('15.000000', 6), ethers.parseUnits('0.000000', 6), ]); expect(contractService.redeemPositions).not.toHaveBeenCalled(); expect(consoleSpy).toHaveBeenCalledWith(' Detected: neg_risk market'); consoleSpy.mockRestore(); }); it('should group Yes and No positions by conditionId', async () => { polymarketService.getPositions.mockResolvedValue([ makePosition({ conditionId: '0xABC', outcome: 'Yes', size: 10, redeemable: true }), makePosition({ conditionId: '0xABC', outcome: 'No', size: 5, redeemable: true }), ]); contractService.isStandardRedemption.mockResolvedValue(false); const promise = strategy.execute(); await jest.runAllTimersAsync(); await promise; expect(contractService.redeemNegRiskPositions).toHaveBeenCalledTimes(1); expect(contractService.redeemNegRiskPositions).toHaveBeenCalledWith('0xABC', [ ethers.parseUnits('10.000000', 6), ethers.parseUnits('5.000000', 6), ]); }); it('should handle multiple conditions from different markets separately', async () => { polymarketService.getPositions.mockResolvedValue([ makePosition({ conditionId: '0xMARKET1', outcome: 'Yes', size: 15, redeemable: true, title: 'Market A' }), makePosition({ conditionId: '0xMARKET2', outcome: 'Yes', size: 5, redeemable: true, title: 'Market B' }), ]); contractService.isStandardRedemption.mockResolvedValue(false); const promise = strategy.execute(); await jest.runAllTimersAsync(); await promise; expect(contractService.isStandardRedemption).toHaveBeenCalledTimes(2); expect(contractService.redeemNegRiskPositions).toHaveBeenCalledTimes(2); expect(contractService.redeemNegRiskPositions).toHaveBeenCalledWith('0xMARKET1', [ethers.parseUnits('15.000000', 6), ethers.parseUnits('0.000000', 6)]); expect(contractService.redeemNegRiskPositions).toHaveBeenCalledWith('0xMARKET2', [ethers.parseUnits('5.000000', 6), ethers.parseUnits('0.000000', 6)]); }); it('should filter out non-redeemable positions', async () => { polymarketService.getPositions.mockResolvedValue([ makePosition({ conditionId: '0xREDEEM', redeemable: true, size: 10 }), makePosition({ conditionId: '0xOPEN', redeemable: false, size: 20 }), ]); contractService.isStandardRedemption.mockResolvedValue(true); const promise = strategy.execute(); await jest.runAllTimersAsync(); await promise; expect(contractService.isStandardRedemption).toHaveBeenCalledTimes(1); expect(contractService.isStandardRedemption).toHaveBeenCalledWith('0xREDEEM'); }); it('should handle redemption failure gracefully and continue', async () => { const consoleErrorSpy = jest.spyOn(console, 'error'); const consoleSpy = jest.spyOn(console, 'log'); polymarketService.getPositions.mockResolvedValue([ makePosition({ conditionId: '0xFAIL', redeemable: true, title: 'Failing Market' }), makePosition({ conditionId: '0xOK', redeemable: true, title: 'OK Market' }), ]); contractService.isStandardRedemption .mockResolvedValueOnce(true) .mockResolvedValueOnce(true); contractService.redeemPositions .mockRejectedValueOnce(new Error('tx reverted')) .mockResolvedValueOnce(mockTx); const promise = strategy.execute(); await jest.runAllTimersAsync(); await promise; expect(consoleErrorSpy).toHaveBeenCalledWith(' Failed to redeem: tx reverted'); expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Redeemed 1/2')); consoleErrorSpy.mockRestore(); consoleSpy.mockRestore(); }); it('should handle detection failure gracefully', async () => { const consoleErrorSpy = jest.spyOn(console, 'error'); polymarketService.getPositions.mockResolvedValue([ makePosition({ conditionId: '0xRPC_FAIL', redeemable: true }), ]); contractService.isStandardRedemption .mockRejectedValue(new Error('RPC rate limit')); const promise = strategy.execute(); await jest.runAllTimersAsync(); await promise; expect(consoleErrorSpy).toHaveBeenCalledWith(' Failed to redeem: RPC rate limit'); expect(contractService.redeemPositions).not.toHaveBeenCalled(); expect(contractService.redeemNegRiskPositions).not.toHaveBeenCalled(); consoleErrorSpy.mockRestore(); }); it('should pass correct amounts for No-only position in neg_risk market', async () => { polymarketService.getPositions.mockResolvedValue([ makePosition({ conditionId: '0xNO_ONLY', outcome: 'No', size: 25, redeemable: true }), ]); contractService.isStandardRedemption.mockResolvedValue(false); const promise = strategy.execute(); await jest.runAllTimersAsync(); await promise; expect(contractService.redeemNegRiskPositions).toHaveBeenCalledWith('0xNO_ONLY', [ ethers.parseUnits('0.000000', 6), ethers.parseUnits('25.000000', 6), ]); }); }); //# sourceMappingURL=redeem-strategy.spec.js.map