Skip to content

Commit 36fb960

Browse files
authored
feat(predict): add Deposit Wallet order flow (MetaMask#29933)
## **Description** Adds Predict order placement support for Polymarket Deposit Wallet accounts on top of the Deposit Wallet deposit foundation. This PR: - Adds the `POLY_1271` Polymarket signature type. - Adds Deposit Wallet / ERC-1271 order signing via the `TypedDataSign` wrapper. - Routes order maker/signer fields from active Predict account state. - Uses Deposit Wallet maker/signer for Deposit Wallet users. - Preserves legacy Safe order signing for grandfathered Safe users. - Runs Deposit Wallet create/setup preflight before submitting Deposit Wallet orders. - Passes an optional signed legacy Safe migration sweep as `allowancesTx` for Deposit Wallet orders, so stranded pUSD/USDC.e can be swept before the backend relays the order. - Skips Safe trade allowance and Permit2 fee preflight for Deposit Wallet orders. - Continues signing CLOB L2 headers with the EOA owner address. This PR is temporarily stacked on `predict/dw-deposit-foundation` while PRs 1 and 2 are under review, and should be rebased/retargeted after those merge. Validation run locally: ```bash yarn jest app/components/UI/Predict/providers/polymarket/protocol/orderCodec.test.ts app/components/UI/Predict/providers/polymarket/PolymarketProvider.test.ts --runInBand yarn lint:tsc ``` ## **Changelog** CHANGELOG entry: Fixed Predict order placement for Polymarket Deposit Wallet accounts ## **Related issues** Fixes: [PRED-860](https://consensyssoftware.atlassian.net/browse/PRED-860) ## **Manual testing steps** ```gherkin Feature: Predict Deposit Wallet order flow Scenario: Deposit Wallet user places a Predict order Given a Predict user is routed to a Polymarket Deposit Wallet And the Deposit Wallet has enough pUSD balance and required setup from the deposit flow When the user places a Predict buy or sell order Then the order is signed with POLY_1271 semantics And the order is submitted successfully to the Polymarket CLOB Scenario: Deposit Wallet user places first order before wallet setup Given a Predict user is routed to a Polymarket Deposit Wallet And the Deposit Wallet still needs creation or allowance setup When the user places a Predict buy or sell order Then the app creates/sets up the Deposit Wallet before order submission And the order is submitted successfully to the Polymarket CLOB Scenario: Deposit Wallet user has funds stranded in legacy Safe Given a Predict user is routed to a Polymarket Deposit Wallet And the deterministic legacy Safe has sweepable pUSD or USDC.e When the user places a Predict buy or sell order Then the signed legacy Safe sweep is included as allowancesTx And the backend can submit the sweep before relaying the order Scenario: Legacy Safe user places a Predict order Given a Predict user is grandfathered to the legacy Safe wallet When the user places a Predict buy or sell order Then the order continues to use legacy Safe signing and preflight behavior ``` ## **Screenshots/Recordings** ### **Before** N/A - provider/order-signing change only. ### **After** N/A - provider/order-signing change only. ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **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. [PRED-860]: https://consensyssoftware.atlassian.net/browse/PRED-860?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes core Predict order submission/signing paths, adding a new ERC-1271 signing format and deposit-wallet preflight flows; mistakes could cause failed or incorrectly-signed orders. Legacy Safe behavior is preserved but now shares more conditional branching. > > **Overview** > Adds end-to-end order placement support for Polymarket **Deposit Wallet** accounts. > > Order submission now derives maker/signer from the resolved account state and, for deposit-wallet users, performs a create/allowance batch preflight before signing/submitting. Deposit-wallet orders use the new `SignatureType.POLY_1271` and are signed via `signProtocolOrder` using an ERC-7739 `TypedDataSign` wrapper, while Safe users keep legacy Safe signing. > > Fee collection and Safe trade preflight steps (Permit2 fee authorization + allowances tx) are skipped for deposit-wallet orders; instead an optional legacy Safe migration sweep can be attached as `allowancesTx`. L2 CLOB headers are consistently signed using the EOA owner address. Tests were expanded to cover both Safe and deposit-wallet order flows and the new signing payload. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit b2bf1ee. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent e124dd0 commit 36fb960

5 files changed

Lines changed: 773 additions & 155 deletions

File tree

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

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { OrderPreview } from '../types';
1212
import { Side } from '../../types';
1313
import type { PredictFeatureFlags } from '../../types/flags';
1414
import { PolymarketProvider } from './PolymarketProvider';
15+
import { OrderType, SignatureType } from './types';
1516
import {
1617
deriveDepositWalletAddress,
1718
executeDepositWalletBatch,
@@ -471,6 +472,9 @@ describe('PolymarketProvider', () => {
471472
expect.any(Object),
472473
SignTypedDataVersion.V4,
473474
);
475+
expect(mockGetL2Headers).toHaveBeenCalledWith(
476+
expect.objectContaining({ address: signer.address }),
477+
);
474478
expect(mockSubmitProtocolClobOrder).toHaveBeenCalledWith(
475479
expect.objectContaining({
476480
protocol: expect.objectContaining({
@@ -480,6 +484,13 @@ describe('PolymarketProvider', () => {
480484
clobVersionHeader: '2',
481485
}),
482486
}),
487+
clobOrder: expect.objectContaining({
488+
order: expect.objectContaining({
489+
maker: legacySafeAddress,
490+
signer: signer.address,
491+
signatureType: SignatureType.POLY_GNOSIS_SAFE,
492+
}),
493+
}),
483494
allowancesTx: {
484495
to: '0x9999999999999999999999999999999999999999',
485496
data: '0xallowances',
@@ -488,6 +499,177 @@ describe('PolymarketProvider', () => {
488499
);
489500
});
490501

502+
it('submits deposit-wallet orders with POLY_1271 payload and no Safe trade preflight fields', async () => {
503+
const innerSignature = `0x${'11'.repeat(65)}`;
504+
signer.signTypedMessage.mockResolvedValueOnce(innerSignature);
505+
mockIsSmartContractAddress
506+
.mockResolvedValueOnce(false)
507+
.mockResolvedValueOnce(true);
508+
509+
const provider = createProvider({
510+
fakOrdersEnabled: true,
511+
feeCollection: {
512+
...DEFAULT_FEE_COLLECTION_FLAG,
513+
permit2Enabled: true,
514+
executors: ['0x4444444444444444444444444444444444444444'],
515+
},
516+
});
517+
518+
const result = await provider.placeOrder({
519+
signer,
520+
preview: {
521+
...basePreview,
522+
fees: {
523+
metamaskFee: 0.05,
524+
providerFee: 0.05,
525+
totalFee: 0.1,
526+
totalFeePercentage: 1,
527+
collector: '0x3333333333333333333333333333333333333333',
528+
executors: ['0x4444444444444444444444444444444444444444'],
529+
permit2Enabled: true,
530+
},
531+
},
532+
});
533+
534+
expect(result.success).toBe(true);
535+
expect(mockCreateApiKey).toHaveBeenCalledWith({ address: signer.address });
536+
expect(mockBuildTradeAllowancesTx).not.toHaveBeenCalled();
537+
expect(mockCreatePermit2FeeAuthorization).not.toHaveBeenCalled();
538+
expect(mockGetL2Headers).toHaveBeenCalledWith(
539+
expect.objectContaining({ address: signer.address }),
540+
);
541+
expect(signer.signTypedMessage).toHaveBeenCalledWith(
542+
{
543+
from: signer.address,
544+
data: expect.objectContaining({
545+
primaryType: 'TypedDataSign',
546+
message: expect.objectContaining({
547+
verifyingContract: depositWalletAddress,
548+
}),
549+
}),
550+
},
551+
SignTypedDataVersion.V4,
552+
);
553+
expect(mockSubmitProtocolClobOrder).toHaveBeenCalledWith(
554+
expect.objectContaining({
555+
clobOrder: expect.objectContaining({
556+
orderType: OrderType.FAK,
557+
order: expect.objectContaining({
558+
maker: depositWalletAddress,
559+
signer: depositWalletAddress,
560+
signatureType: SignatureType.POLY_1271,
561+
signature: expect.stringMatching(/^0x11+/u),
562+
}),
563+
}),
564+
feeAuthorization: undefined,
565+
executor: undefined,
566+
allowancesTx: undefined,
567+
}),
568+
);
569+
});
570+
571+
it('runs deposit-wallet setup before submitting deposit-wallet orders', async () => {
572+
const repairTransaction = {
573+
to: '0x4444444444444444444444444444444444444444',
574+
data: '0xrepair',
575+
operation: OperationType.Call,
576+
value: '0',
577+
};
578+
mockIsSmartContractAddress
579+
.mockResolvedValueOnce(false)
580+
.mockResolvedValueOnce(false)
581+
.mockResolvedValueOnce(false)
582+
.mockResolvedValueOnce(false);
583+
mockPlanDepositWalletPreflight.mockResolvedValue({
584+
missingRequirements: [
585+
{
586+
type: 'erc20-allowance',
587+
tokenAddress: repairTransaction.to,
588+
spender: '0x5555555555555555555555555555555555555555',
589+
},
590+
],
591+
transactions: [repairTransaction],
592+
});
593+
594+
const result = await createProvider().placeOrder({
595+
signer,
596+
preview: basePreview,
597+
});
598+
599+
expect(result.success).toBe(true);
600+
expect(mockRequestDepositWalletCreate).toHaveBeenCalledWith({
601+
ownerAddress: signer.address,
602+
});
603+
expect(mockExecuteDepositWalletBatch).toHaveBeenCalledWith({
604+
signer,
605+
walletAddress: depositWalletAddress,
606+
calls: [
607+
{
608+
target: repairTransaction.to,
609+
data: repairTransaction.data,
610+
value: repairTransaction.value,
611+
},
612+
],
613+
});
614+
expect(
615+
mockSyncDepositWalletCollateralBalanceAllowance,
616+
).toHaveBeenCalledWith({
617+
protocol: expect.objectContaining({ key: 'v2' }),
618+
signerAddress: signer.address,
619+
apiKey: expect.objectContaining({ apiKey: 'api-key' }),
620+
});
621+
expect(
622+
mockExecuteDepositWalletBatch.mock.invocationCallOrder[0],
623+
).toBeLessThan(mockSubmitProtocolClobOrder.mock.invocationCallOrder[0]);
624+
});
625+
626+
it('passes legacy Safe migration sweep as allowancesTx for deposit-wallet orders', async () => {
627+
global.fetch = jest.fn().mockResolvedValue({
628+
ok: true,
629+
json: jest.fn().mockResolvedValue([]),
630+
});
631+
mockIsSmartContractAddress
632+
.mockResolvedValueOnce(true)
633+
.mockResolvedValueOnce(true)
634+
.mockResolvedValueOnce(true)
635+
.mockResolvedValueOnce(true);
636+
const sweepTransaction: SignedSafeExecution = {
637+
params: {
638+
to: legacySafeAddress as `0x${string}`,
639+
data: '0xsweep' as `0x${string}`,
640+
},
641+
type: TransactionType.contractInteraction,
642+
};
643+
mockBuildLegacySafeMigrationSweepTransaction.mockResolvedValue(
644+
sweepTransaction,
645+
);
646+
647+
const result = await createProvider().placeOrder({
648+
signer,
649+
preview: basePreview,
650+
});
651+
652+
expect(result.success).toBe(true);
653+
expect(mockBuildLegacySafeMigrationSweepTransaction).toHaveBeenCalledWith({
654+
signer,
655+
legacySafeAddress,
656+
depositWalletAddress,
657+
protocol: expect.objectContaining({ key: 'v2' }),
658+
});
659+
expect(mockSubmitProtocolClobOrder).toHaveBeenCalledWith(
660+
expect.objectContaining({
661+
clobOrder: expect.objectContaining({
662+
order: expect.objectContaining({
663+
maker: depositWalletAddress,
664+
signer: depositWalletAddress,
665+
signatureType: SignatureType.POLY_1271,
666+
}),
667+
}),
668+
allowancesTx: sweepTransaction.params,
669+
}),
670+
);
671+
});
672+
491673
it('uses pUSD Permit2 fee authorization when fees are present', async () => {
492674
mockCreatePermit2FeeAuthorization.mockResolvedValue({
493675
type: 'safe-permit2',

0 commit comments

Comments
 (0)