Skip to content

Commit c83ebdb

Browse files
fix(predict): harden deposit wallet onboarding
Refresh deposit wallet account state and align relayer batch deadlines with Polymarket guidance while adding focused coverage for the new wallet paths. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d25110f commit c83ebdb

4 files changed

Lines changed: 207 additions & 22 deletions

File tree

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

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5426,18 +5426,35 @@ describe('PolymarketProvider', () => {
54265426
});
54275427
});
54285428

5429-
it('caches account state by owner address', async () => {
5430-
// Given an account state check
5429+
it('refreshes account state for repeated owner lookups', async () => {
54315430
const provider = createProvider();
5432-
(isSmartContractAddress as jest.Mock).mockResolvedValue(true);
5433-
(hasAllowances as jest.Mock).mockResolvedValue(true);
5431+
(isSmartContractAddress as jest.Mock)
5432+
.mockResolvedValueOnce(false)
5433+
.mockResolvedValueOnce(true);
5434+
(hasAllowances as jest.Mock)
5435+
.mockResolvedValueOnce(false)
5436+
.mockResolvedValueOnce(true);
54345437

5435-
// When getting account state twice
5436-
await provider.getAccountState({ ownerAddress: '0x123' });
5437-
await provider.getAccountState({ ownerAddress: '0x123' });
5438+
const firstResult = await provider.getAccountState({
5439+
ownerAddress: '0x123',
5440+
});
5441+
const secondResult = await provider.getAccountState({
5442+
ownerAddress: '0x123',
5443+
});
54385444

5439-
// Then Safe address is only computed once
5440-
expect(computeProxyAddress).toHaveBeenCalledTimes(1);
5445+
expect(firstResult).toEqual({
5446+
address: '0xSafeAddress',
5447+
isDeployed: false,
5448+
hasAllowances: false,
5449+
walletType: 'safe',
5450+
});
5451+
expect(secondResult).toEqual({
5452+
address: '0xSafeAddress',
5453+
isDeployed: true,
5454+
hasAllowances: true,
5455+
walletType: 'safe',
5456+
});
5457+
expect(computeProxyAddress).toHaveBeenCalledTimes(2);
54415458
});
54425459

54435460
it('computes Safe address for each unique owner', async () => {

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2391,11 +2391,6 @@ export class PolymarketProvider implements PredictProvider {
23912391
throw new Error('Owner address is required');
23922392
}
23932393

2394-
const cachedAccountState = this.#accountStateByAddress.get(ownerAddress);
2395-
if (cachedAccountState) {
2396-
return cachedAccountState;
2397-
}
2398-
23992394
const protocol = this.#getProtocol();
24002395
const { feeCollection: flagFeeCollection } = this.#getFeatureFlags();
24012396
const extraUsdcSpenders = flagFeeCollection.permit2Enabled

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

Lines changed: 180 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import {
2+
createDepositWalletPermit2FeeAuthorization,
23
DEPOSIT_WALLET_FACTORY_ADDRESS,
34
deriveDepositWalletAddress,
5+
executeDepositWalletBatch,
46
getDepositWalletNonce,
57
requestDepositWalletCreate,
68
syncDepositWalletClobBalanceAllowance,
79
toDepositWalletCalls,
10+
waitForDepositWalletTransaction,
811
} from './depositWallet';
12+
import { getPermit2Nonce } from './safe/utils';
913
import { getL2Headers } from './utils';
1014

1115
jest.mock('./utils', () => ({
@@ -15,8 +19,20 @@ jest.mock('./utils', () => ({
1519
})),
1620
}));
1721

22+
jest.mock('./safe/utils', () => ({
23+
getPermit2Nonce: jest.fn(),
24+
}));
25+
1826
describe('deposit wallet helpers', () => {
1927
const mockFetch = jest.fn();
28+
const ownerAddress = '0x1111111111111111111111111111111111111111';
29+
const depositWalletAddress = '0x2222222222222222222222222222222222222222';
30+
const typedSignature = `0x${'11'.repeat(32)}${'22'.repeat(32)}1b`;
31+
const signer = {
32+
address: ownerAddress,
33+
signTypedMessage: jest.fn().mockResolvedValue(typedSignature),
34+
signPersonalMessage: jest.fn(),
35+
};
2036

2137
beforeEach(() => {
2238
jest.clearAllMocks();
@@ -28,11 +44,17 @@ describe('deposit wallet helpers', () => {
2844
POLY_API_KEY: 'apiKey',
2945
POLY_PASSPHRASE: 'passphrase',
3046
});
47+
(getPermit2Nonce as jest.Mock).mockResolvedValue('7');
48+
signer.signTypedMessage.mockResolvedValue(typedSignature);
49+
});
50+
51+
afterEach(() => {
52+
jest.restoreAllMocks();
3153
});
3254

3355
it('derives the deterministic deposit wallet address for an owner', () => {
3456
expect(
35-
deriveDepositWalletAddress('0x1111111111111111111111111111111111111111'),
57+
deriveDepositWalletAddress(ownerAddress),
3658
).toBe('0xfAeA0f08159fcF2f573fE24E9E989B0d48f7651B');
3759
});
3860

@@ -44,7 +66,7 @@ describe('deposit wallet helpers', () => {
4466

4567
await expect(
4668
requestDepositWalletCreate({
47-
ownerAddress: '0x1111111111111111111111111111111111111111',
69+
ownerAddress,
4870
}),
4971
).resolves.toEqual({ transactionID: 'wallet-create-1' });
5072

@@ -53,7 +75,7 @@ describe('deposit wallet helpers', () => {
5375
expect.objectContaining({
5476
method: 'POST',
5577
body: JSON.stringify({
56-
owner: '0x1111111111111111111111111111111111111111',
78+
owner: ownerAddress,
5779
factory: DEPOSIT_WALLET_FACTORY_ADDRESS,
5880
}),
5981
}),
@@ -68,21 +90,125 @@ describe('deposit wallet helpers', () => {
6890

6991
await expect(
7092
getDepositWalletNonce({
71-
ownerAddress: '0x1111111111111111111111111111111111111111',
93+
ownerAddress,
7294
}),
7395
).resolves.toBe('42');
7496

7597
expect(mockFetch).toHaveBeenCalledWith(
7698
'https://predict.api.cx.metamask.io/wallet/nonce',
7799
expect.objectContaining({
78100
body: JSON.stringify({
79-
owner: '0x1111111111111111111111111111111111111111',
101+
owner: ownerAddress,
80102
type: 'WALLET',
81103
}),
82104
}),
83105
);
84106
});
85107

108+
it('signs and submits deposit wallet batches with the current wallet nonce', async () => {
109+
jest.spyOn(Date, 'now').mockReturnValue(1_700_000_000_000);
110+
mockFetch
111+
.mockResolvedValueOnce({
112+
ok: true,
113+
json: async () => ({ nonce: '42' }),
114+
})
115+
.mockResolvedValueOnce({
116+
ok: true,
117+
json: async () => ({ transactionID: 'wallet-batch-1' }),
118+
});
119+
120+
await expect(
121+
executeDepositWalletBatch({
122+
signer,
123+
walletAddress: depositWalletAddress,
124+
calls: [
125+
{
126+
target: '0x3333333333333333333333333333333333333333',
127+
value: '0',
128+
data: '0xabcdef',
129+
},
130+
],
131+
}),
132+
).resolves.toEqual({ transactionID: 'wallet-batch-1' });
133+
134+
expect(signer.signTypedMessage).toHaveBeenCalledWith(
135+
expect.objectContaining({
136+
from: ownerAddress,
137+
data: expect.objectContaining({
138+
domain: expect.objectContaining({
139+
name: 'DepositWallet',
140+
version: '1',
141+
verifyingContract: depositWalletAddress,
142+
}),
143+
primaryType: 'Batch',
144+
message: expect.objectContaining({
145+
wallet: depositWalletAddress,
146+
nonce: '42',
147+
deadline: '1700000240',
148+
}),
149+
}),
150+
}),
151+
'V4',
152+
);
153+
expect(mockFetch).toHaveBeenLastCalledWith(
154+
'https://predict.api.cx.metamask.io/wallet/execute',
155+
expect.objectContaining({
156+
body: JSON.stringify({
157+
owner: ownerAddress,
158+
factory: DEPOSIT_WALLET_FACTORY_ADDRESS,
159+
nonce: '42',
160+
signature: typedSignature,
161+
depositWalletParams: {
162+
depositWallet: depositWalletAddress,
163+
deadline: '1700000240',
164+
calls: [
165+
{
166+
target: '0x3333333333333333333333333333333333333333',
167+
value: '0',
168+
data: '0xabcdef',
169+
},
170+
],
171+
},
172+
}),
173+
}),
174+
);
175+
});
176+
177+
it('polls deposit wallet transactions until the relayer confirms them', async () => {
178+
mockFetch
179+
.mockResolvedValueOnce({
180+
ok: true,
181+
json: async () => [{ state: 'STATE_PENDING' }],
182+
})
183+
.mockResolvedValueOnce({
184+
ok: true,
185+
json: async () => [{ state: 'STATE_CONFIRMED' }],
186+
});
187+
188+
await expect(
189+
waitForDepositWalletTransaction({
190+
transactionID: 'wallet-batch-1',
191+
pollIntervalMs: 0,
192+
}),
193+
).resolves.toBeUndefined();
194+
195+
expect(mockFetch).toHaveBeenCalledTimes(2);
196+
});
197+
198+
it('rejects failed deposit wallet relayer transactions', async () => {
199+
mockFetch.mockResolvedValueOnce({
200+
ok: true,
201+
json: async () => ({ state: 'STATE_FAILED' }),
202+
});
203+
204+
await expect(
205+
waitForDepositWalletTransaction({
206+
transactionID: 'wallet-batch-1',
207+
pollIntervalMs: 0,
208+
}),
209+
).rejects.toThrow('Deposit wallet transaction failed: STATE_FAILED');
210+
});
211+
86212
it('maps internal Safe-style calls to deposit wallet calls', () => {
87213
expect(
88214
toDepositWalletCalls([
@@ -115,7 +241,7 @@ describe('deposit wallet helpers', () => {
115241
clobVersionHeader: '2',
116242
},
117243
},
118-
signerAddress: '0x1111111111111111111111111111111111111111',
244+
signerAddress: ownerAddress,
119245
apiKey: {
120246
apiKey: 'key',
121247
secret: 'secret',
@@ -129,7 +255,7 @@ describe('deposit wallet helpers', () => {
129255
requestPath:
130256
'/balance-allowance/update?asset_type=COLLATERAL&signature_type=3',
131257
},
132-
address: '0x1111111111111111111111111111111111111111',
258+
address: ownerAddress,
133259
apiKey: {
134260
apiKey: 'key',
135261
secret: 'secret',
@@ -143,4 +269,51 @@ describe('deposit wallet helpers', () => {
143269
}),
144270
);
145271
});
272+
273+
it('creates Permit2 fee authorizations signed by the deposit wallet owner', async () => {
274+
jest.spyOn(Date, 'now').mockReturnValue(1_700_000_000_000);
275+
276+
const authorization = await createDepositWalletPermit2FeeAuthorization({
277+
signer,
278+
amount: 123n,
279+
spender: '0x3333333333333333333333333333333333333333',
280+
tokenAddress: '0x4444444444444444444444444444444444444444',
281+
});
282+
283+
expect(signer.signTypedMessage).toHaveBeenCalledWith(
284+
expect.objectContaining({
285+
from: ownerAddress,
286+
data: expect.objectContaining({
287+
domain: expect.objectContaining({ name: 'Permit2' }),
288+
primaryType: 'PermitTransferFrom',
289+
message: {
290+
permitted: {
291+
token: '0x4444444444444444444444444444444444444444',
292+
amount: '123',
293+
},
294+
spender: '0x3333333333333333333333333333333333333333',
295+
nonce: '7',
296+
deadline: '1700003600',
297+
},
298+
}),
299+
}),
300+
'V4',
301+
);
302+
expect(authorization).toEqual({
303+
type: 'safe-permit2',
304+
authorization: {
305+
permit: {
306+
permitted: {
307+
token: '0x4444444444444444444444444444444444444444',
308+
amount: '123',
309+
},
310+
spender: '0x3333333333333333333333333333333333333333',
311+
nonce: '7',
312+
deadline: '1700003600',
313+
},
314+
spender: '0x3333333333333333333333333333333333333333',
315+
signature: typedSignature,
316+
},
317+
});
318+
});
146319
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const ERC1967_CONST1 =
3232
const ERC1967_CONST2 =
3333
'0x5155f3363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076';
3434
const ERC1967_PREFIX = 0x61003d3d8160233d3973n;
35-
const DEPOSIT_WALLET_BATCH_TTL_SECONDS = 15 * 60;
35+
const DEPOSIT_WALLET_BATCH_TTL_SECONDS = 240;
3636
const RELAYER_SUCCESS_STATES = new Set(['STATE_MINED', 'STATE_CONFIRMED']);
3737
const RELAYER_FAILED_STATES = new Set(['STATE_FAILED', 'STATE_INVALID']);
3838

0 commit comments

Comments
 (0)