Skip to content

Commit 8099e72

Browse files
authored
revert(predict): remove Polymarket balance/allowance refresh workaround cp-7.69.0 (MetaMask#27162)
## **Description** Reverts MetaMask#26954. Polymarket has resolved the underlying CLOB infrastructure issue that was causing intermittent `not enough balance / allowance` 400 errors during order placement. The temporary `refreshBalanceAllowance()` workaround (calling `GET /balance-allowance/update` before each order) is no longer needed and is removed here. ### Changes: - **`utils.ts`**: Remove `refreshBalanceAllowance()` utility and related HMAC auth logic - **`PolymarketProvider.ts`**: Remove the preflight refresh call from `placeOrder()` - **`utils.test.ts`**: Remove associated unit tests - **`PolymarketProvider.test.ts`**: Remove associated integration tests ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: Polymarket order placement without refresh workaround Scenario: user places a BUY order on a prediction market Given the user has USDC deposited in their Polymarket proxy wallet When user places a BUY order on any market Then the order completes successfully without a preflight refresh call Scenario: user places a SELL order on a prediction market Given the user holds outcome tokens for a market When user places a SELL order Then the order completes successfully without a preflight refresh call ``` ## **Screenshots/Recordings** N/A — no UI changes ### **Before** <!-- N/A --> ### **After** <!-- N/A --> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes Polymarket order submission behavior by removing a preflight balance/allowance refresh; if the upstream CLOB issue is not fully resolved, this could reintroduce intermittent order failures under load. > > **Overview** > Removes the temporary Polymarket CLOB *balance/allowance refresh* workaround by deleting `refreshBalanceAllowance` from `utils.ts` and dropping the pre-submit call in `PolymarketProvider.placeOrder()`. > > Cleans up the associated unit/integration coverage by removing mocks and test cases that asserted the refresh call ordering and failure handling. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit db50f7d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent d881cec commit 8099e72

4 files changed

Lines changed: 0 additions & 278 deletions

File tree

app/components/UI/Predict/providers/polymarket/PolymarketProvider.test.ts

Lines changed: 0 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ import {
6868
parsePolymarketPositions,
6969
previewOrder,
7070
priceValid,
71-
refreshBalanceAllowance,
7271
submitClobOrder,
7372
} from './utils';
7473

@@ -109,7 +108,6 @@ jest.mock('./utils', () => {
109108
priceValid: jest.fn(),
110109
createApiKey: jest.fn(),
111110
submitClobOrder: jest.fn(),
112-
refreshBalanceAllowance: jest.fn(),
113111
getMarketPositions: jest.fn(),
114112
getBalance: jest.fn(),
115113
previewOrder: jest.fn(),
@@ -212,7 +210,6 @@ const mockParsePolymarketPositions = parsePolymarketPositions as jest.Mock;
212210
const mockPriceValid = priceValid as jest.Mock;
213211
const mockCreateApiKey = createApiKey as jest.Mock;
214212
const mockSubmitClobOrder = submitClobOrder as jest.Mock;
215-
const mockRefreshBalanceAllowance = refreshBalanceAllowance as jest.Mock;
216213
const mockEncodeClaim = encodeClaim as jest.Mock;
217214
const mockComputeProxyAddress = computeProxyAddress as jest.Mock;
218215
const mockCreatePermit2FeeAuthorization =
@@ -1576,97 +1573,6 @@ describe('PolymarketProvider', () => {
15761573
});
15771574
});
15781575

1579-
describe('placeOrder balance/allowance refresh workaround', () => {
1580-
beforeEach(() => {
1581-
jest.clearAllMocks();
1582-
mockRefreshBalanceAllowance.mockResolvedValue(undefined);
1583-
});
1584-
1585-
it('calls refreshBalanceAllowance with COLLATERAL before submitting a BUY order', async () => {
1586-
// Arrange
1587-
const { provider, mockSigner } = setupPlaceOrderTest();
1588-
const preview = createMockOrderPreview({ side: Side.BUY });
1589-
1590-
// Act
1591-
await provider.placeOrder({ signer: mockSigner, preview });
1592-
1593-
// Assert
1594-
expect(mockRefreshBalanceAllowance).toHaveBeenCalledWith({
1595-
address: mockSigner.address,
1596-
apiKey: expect.objectContaining({ apiKey: 'test-api-key' }),
1597-
side: Side.BUY,
1598-
outcomeTokenId: preview.outcomeTokenId,
1599-
});
1600-
});
1601-
1602-
it('calls refreshBalanceAllowance with CONDITIONAL before submitting a SELL order', async () => {
1603-
// Arrange
1604-
const { provider, mockSigner } = setupPlaceOrderTest();
1605-
const preview = createMockOrderPreview({ side: Side.SELL });
1606-
1607-
// Act
1608-
await provider.placeOrder({ signer: mockSigner, preview });
1609-
1610-
// Assert
1611-
expect(mockRefreshBalanceAllowance).toHaveBeenCalledWith({
1612-
address: mockSigner.address,
1613-
apiKey: expect.objectContaining({ apiKey: 'test-api-key' }),
1614-
side: Side.SELL,
1615-
outcomeTokenId: preview.outcomeTokenId,
1616-
});
1617-
});
1618-
1619-
it('calls refreshBalanceAllowance before submitClobOrder', async () => {
1620-
// Arrange
1621-
const { provider, mockSigner } = setupPlaceOrderTest();
1622-
const callOrder: string[] = [];
1623-
mockRefreshBalanceAllowance.mockImplementation(async () => {
1624-
callOrder.push('refresh');
1625-
});
1626-
mockSubmitClobOrder.mockImplementation(async () => {
1627-
callOrder.push('submit');
1628-
return {
1629-
success: true,
1630-
response: {
1631-
success: true,
1632-
makingAmount: '1000000',
1633-
orderID: 'order-123',
1634-
status: 'success',
1635-
takingAmount: '0',
1636-
transactionsHashes: [],
1637-
},
1638-
error: undefined,
1639-
};
1640-
});
1641-
const preview = createMockOrderPreview({ side: Side.BUY });
1642-
1643-
// Act
1644-
await provider.placeOrder({ signer: mockSigner, preview });
1645-
1646-
// Assert
1647-
expect(callOrder).toEqual(['refresh', 'submit']);
1648-
});
1649-
1650-
it('proceeds with order submission when refreshBalanceAllowance fails', async () => {
1651-
// Arrange
1652-
const { provider, mockSigner } = setupPlaceOrderTest();
1653-
mockRefreshBalanceAllowance.mockRejectedValue(
1654-
new Error('Network timeout'),
1655-
);
1656-
const preview = createMockOrderPreview({ side: Side.BUY });
1657-
1658-
// Act
1659-
const result = await provider.placeOrder({
1660-
signer: mockSigner,
1661-
preview,
1662-
});
1663-
1664-
// Assert - order still submitted despite refresh failure
1665-
expect(mockSubmitClobOrder).toHaveBeenCalled();
1666-
expect(result.success).toBe(true);
1667-
});
1668-
});
1669-
16701576
describe('placeOrder with Safe fee authorization', () => {
16711577
it('computes Safe address before creating order', async () => {
16721578
const { provider, mockSigner } = setupPlaceOrderTest();

app/components/UI/Predict/providers/polymarket/PolymarketProvider.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ import {
101101
parsePolymarketEvents,
102102
parsePolymarketPositions,
103103
previewOrder,
104-
refreshBalanceAllowance,
105104
roundOrderAmount,
106105
submitClobOrder,
107106
} from './utils';
@@ -1317,23 +1316,6 @@ export class PolymarketProvider implements PredictProvider {
13171316
apiKey: signerApiKey,
13181317
});
13191318

1320-
// TEMPORARY WORKAROUND: Refresh balance/allowance on Polymarket's CLOB
1321-
// before submitting the order. See refreshBalanceAllowance docs for details.
1322-
try {
1323-
await refreshBalanceAllowance({
1324-
address: signer.address,
1325-
apiKey: signerApiKey,
1326-
side,
1327-
outcomeTokenId,
1328-
});
1329-
} catch (refreshError) {
1330-
// Best-effort — don't block order submission if the refresh fails
1331-
DevLogger.log(
1332-
'PolymarketProvider: Pre-order balance/allowance refresh failed',
1333-
refreshError,
1334-
);
1335-
}
1336-
13371319
const { success, response, error } = await submitClobOrder({
13381320
headers,
13391321
clobOrder,

app/components/UI/Predict/providers/polymarket/utils.test.ts

Lines changed: 0 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ import {
6060
parsePolymarketPositions,
6161
parsePolymarketActivity,
6262
priceValid,
63-
refreshBalanceAllowance,
6463
submitClobOrder,
6564
decimalPlaces,
6665
roundNormal,
@@ -702,100 +701,6 @@ describe('polymarket utils', () => {
702701
});
703702
});
704703

705-
describe('refreshBalanceAllowance', () => {
706-
beforeEach(() => {
707-
mockFetch.mockReset();
708-
(global.crypto as any).createHmac.mockReturnValue({
709-
update: jest.fn().mockReturnThis(),
710-
digest: jest.fn().mockReturnValue('mock-digest-base64'),
711-
});
712-
});
713-
714-
it('sends COLLATERAL asset_type for BUY orders', async () => {
715-
mockFetch.mockResolvedValue({ ok: true });
716-
717-
await refreshBalanceAllowance({
718-
address: mockAddress,
719-
apiKey: mockApiKey,
720-
side: Side.BUY,
721-
outcomeTokenId: 'token-123',
722-
});
723-
724-
expect(mockFetch).toHaveBeenCalledWith(
725-
expect.stringContaining('/balance-allowance/update?'),
726-
expect.objectContaining({ method: 'GET' }),
727-
);
728-
const calledUrl = mockFetch.mock.calls[0][0] as string;
729-
expect(calledUrl).toContain('asset_type=COLLATERAL');
730-
expect(calledUrl).toContain('signature_type=2');
731-
expect(calledUrl).not.toContain('token_id=');
732-
});
733-
734-
it('sends CONDITIONAL asset_type with token_id for SELL orders', async () => {
735-
mockFetch.mockResolvedValue({ ok: true });
736-
737-
await refreshBalanceAllowance({
738-
address: mockAddress,
739-
apiKey: mockApiKey,
740-
side: Side.SELL,
741-
outcomeTokenId: 'token-456',
742-
});
743-
744-
const calledUrl = mockFetch.mock.calls[0][0] as string;
745-
expect(calledUrl).toContain('asset_type=CONDITIONAL');
746-
expect(calledUrl).toContain('token_id=token-456');
747-
expect(calledUrl).toContain('signature_type=2');
748-
});
749-
750-
it('calls CLOB_ENDPOINT with L2 auth headers', async () => {
751-
mockFetch.mockResolvedValue({ ok: true });
752-
753-
await refreshBalanceAllowance({
754-
address: mockAddress,
755-
apiKey: mockApiKey,
756-
side: Side.BUY,
757-
outcomeTokenId: 'token-123',
758-
});
759-
760-
const calledUrl = mockFetch.mock.calls[0][0] as string;
761-
expect(calledUrl).toMatch(/^https:\/\/clob\.polymarket\.com/);
762-
const calledOptions = mockFetch.mock.calls[0][1] as RequestInit;
763-
expect(calledOptions.headers).toEqual(
764-
expect.objectContaining({
765-
POLY_ADDRESS: mockAddress,
766-
}),
767-
);
768-
});
769-
770-
it('does not throw when response is not ok', async () => {
771-
mockFetch.mockResolvedValue({ ok: false, status: 500 });
772-
773-
await expect(
774-
refreshBalanceAllowance({
775-
address: mockAddress,
776-
apiKey: mockApiKey,
777-
side: Side.BUY,
778-
outcomeTokenId: 'token-123',
779-
}),
780-
).resolves.toBeUndefined();
781-
});
782-
783-
it('uses custom signatureType when provided', async () => {
784-
mockFetch.mockResolvedValue({ ok: true });
785-
786-
await refreshBalanceAllowance({
787-
address: mockAddress,
788-
apiKey: mockApiKey,
789-
side: Side.BUY,
790-
outcomeTokenId: 'token-123',
791-
signatureType: SignatureType.EOA,
792-
});
793-
794-
const calledUrl = mockFetch.mock.calls[0][0] as string;
795-
expect(calledUrl).toContain('signature_type=0');
796-
});
797-
});
798-
799704
describe('submitClobOrder', () => {
800705
const mockHeaders: ClobHeaders = {
801706
POLY_ADDRESS: mockAddress,

app/components/UI/Predict/providers/polymarket/utils.ts

Lines changed: 0 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ import {
6161
PolymarketApiMarket,
6262
PolymarketApiTeam,
6363
PolymarketPosition,
64-
SignatureType,
6564
TickSize,
6665
OrderBook,
6766
} from './types';
@@ -188,76 +187,6 @@ export const getL2Headers = async ({
188187
return headers;
189188
};
190189

191-
/**
192-
* TEMPORARY WORKAROUND for Polymarket infrastructure issue.
193-
*
194-
* Polymarket's CLOB infrastructure intermittently returns 400 errors with
195-
* "not enough balance / allowance" when placing orders at high request rates.
196-
* Calling this endpoint before each order refreshes the balance/allowance state
197-
* on their end and prevents most of these spurious failures.
198-
*
199-
* For BUY orders: refreshes COLLATERAL (USDC) balance/allowance.
200-
* For SELL orders: refreshes CONDITIONAL token balance/allowance.
201-
*
202-
* TODO: Remove this workaround once Polymarket resolves the underlying
203-
* infrastructure issue. Track removal in a follow-up ticket.
204-
*/
205-
export const refreshBalanceAllowance = async ({
206-
address,
207-
apiKey,
208-
side,
209-
outcomeTokenId,
210-
signatureType = SignatureType.POLY_GNOSIS_SAFE,
211-
}: {
212-
address: string;
213-
apiKey: ApiKeyCreds;
214-
side: Side;
215-
outcomeTokenId: string;
216-
signatureType?: SignatureType;
217-
}): Promise<void> => {
218-
const { CLOB_ENDPOINT } = getPolymarketEndpoints();
219-
220-
const queryParams = new URLSearchParams({
221-
signature_type: String(signatureType),
222-
});
223-
224-
if (side === Side.BUY) {
225-
queryParams.set('asset_type', 'COLLATERAL');
226-
} else {
227-
queryParams.set('asset_type', 'CONDITIONAL');
228-
queryParams.set('token_id', outcomeTokenId);
229-
}
230-
231-
const requestPath = `/balance-allowance/update`;
232-
233-
const headers = await getL2Headers({
234-
l2HeaderArgs: {
235-
method: 'GET',
236-
requestPath,
237-
},
238-
address,
239-
apiKey,
240-
});
241-
242-
const response = await fetch(
243-
`${CLOB_ENDPOINT}${requestPath}?${queryParams.toString()}`,
244-
{
245-
method: 'GET',
246-
headers,
247-
},
248-
);
249-
250-
if (!response.ok) {
251-
DevLogger.log(
252-
'refreshBalanceAllowance: Pre-order balance/allowance refresh failed',
253-
{
254-
status: response.status,
255-
side,
256-
},
257-
);
258-
}
259-
};
260-
261190
export const deriveApiKey = async ({ address }: { address: string }) => {
262191
const { CLOB_ENDPOINT } = getPolymarketEndpoints();
263192
const headers = await getL1Headers({ address });

0 commit comments

Comments
 (0)