feat(client): expose trading approval state (DEV-564) - #288
Conversation
brunson-bot
left a comment
There was a problem hiding this comment.
Read the diff plus the surrounding decorator / action / rpc.ts layers. No blocker — CI is green and the read path itself is correct: ethCallBatch already guarantees a same-length HexString[] with every element validated (rpc.ts:150-186), so the offset slicing and as HexString casts are sound, and wrapping the ABI decode in UnexpectedResponseError is a genuine improvement to the pre-existing prepareTradingApprovals path.
Four comments, ranked. The first two are public-API shape and are cheapest to fix before this ships; the third is a pre-existing hazard that this PR newly exposes on a fresh entry point.
| ): Promise<TradingApprovalsState>; | ||
| }; | ||
|
|
||
| const SecureGetTradingApprovalsStateRequestSchema = z.object({ |
There was a problem hiding this comment.
[issue] This is the only decorator in the package that imports zod or calls parseUserInput (checked all of src/decorators/). account.ts already solves the exact same "secure client defaults the address to its own account" problem without a second schema — DefaultAccountWallet<TRequest> for the type (account.ts:50-58) plus a three-line withAccountWallet() for the runtime default (account.ts:510-518), used by eight secure methods at account.ts:533-547.
Two concrete costs of doing it with a schema instead:
z.objectstrips unknown keys, so the override forwards only{ wallet }. The momentGetTradingApprovalsStateRequestgrows a second field (a block tag, aninclude, anything), it will work on the public client and be silently dropped on the secure one — no type error, no runtime error, just ignored input. The spread inwithAccountWallethas no such failure mode.- The
@defaultValueTSDoc you wrote inside thez.objectdoesn't survivez.input<...>, soSecureGetTradingApprovalsStateRequest'swalletreaches consumers undocumented.DefaultAccountWalletputs that doc on a hand-written member, where it does survive.
DefaultAccountWallet is keyed on user so it isn't directly reusable here, but a sibling keyed on wallet (or generalising it over the key) gets you the same contract, one parse instead of two, and leaves the input contract owned by the action.
| * @throws {@link GetTradingApprovalsStateError} | ||
| * Thrown on failure. | ||
| */ | ||
| export async function getTradingApprovalsState( |
There was a problem hiding this comment.
[issue] get* isn't a prefix this package uses. packages/client/src/actions/ exports 47 fetch* and 31 list* actions, and this is the only get*. AGENTS.md ties the prefix to SDK behaviour — fetch* means "returns a direct item or direct collection, with no SDK pagination abstraction" — which is exactly what this is. get* reads like a local/synchronous accessor, and this is a batched eth_call round trip.
fetchTradingApprovalsState costs nothing today and is a breaking rename once this is published.
| missing: TradingApprovalRequirements; | ||
| }; | ||
|
|
||
| export type GetTradingApprovalsStateError = |
There was a problem hiding this comment.
[issue] UserInputError doesn't actually cover a malformed address. EvmAddressSchema is z.string().transform(toEvmAddress) (bindings/src/shared.ts:312), and toEvmAddress → expectEvmAddress → invariant(...), which throws InvariantError (types/src/refinements.ts:62-68). A throwing .transform() escapes safeParse as a raw exception rather than becoming a Zod issue, so parseUserInput never sees a failed result. Verified on zod 4.4.3:
const S = z.object({ wallet: z.string().transform(v => { throw new InvariantError('Expected an EVM address') }) });
S.safeParse({ wallet: 'nope' }) // → THROWS InvariantError out of safeParse
S.safeParse({ wallet: null }) // → { success: false } ✔
So { wallet: 'not-an-address' } throws InvariantError — the assertion-style error AGENTS.md explicitly says must not appear in a public union — while { wallet: null } correctly throws UserInputError.
This is pre-existing and package-wide (every EvmAddressSchema input has it; cf. the markets.test.ts:52 comment about a raw TypeError aborting a page), so not a blocker on this PR. Flagging it because this is a brand-new public entry point whose documented @throws and whose only validation test both assert UserInputError.
| const fetchSpy = vi.spyOn(globalThis, 'fetch'); | ||
|
|
||
| try { | ||
| for (const request of [null, { wallet: null }]) { |
There was a problem hiding this comment.
[issue] Both vectors here (null, { wallet: null }) fail at z.string() — the branch that works. Adding { wallet: 'not-an-address' } to the loop exercises the branch that doesn't (see the comment on GetTradingApprovalsStateError).
Separately, the secure default is untested: nothing calls secureClient.getTradingApprovalsState() with no argument and asserts it resolved against client.account.wallet. That default is the only behaviour this decorator override adds over the action, and it's also the behaviour that would silently regress if the request type ever grows a field (see the wallet.ts comment). The public-client test is good — the eth_call-only assertion is the right way to prove "no signer, no writes".
| "@polymarket/client": minor | ||
| --- | ||
|
|
||
| Add `getTradingApprovalsState` for reading a wallet's missing trading approvals without a signer or transaction workflow. |
There was a problem hiding this comment.
[nit] The changeset covers the new action but not the two other consumer-visible changes: decode failures on the existing prepareTradingApprovals / setupTradingApprovals path now surface as UnexpectedResponseError instead of a raw ox decode error (a strict improvement — it makes the already-documented PrepareTradingApprovalsError union true rather than aspirational), and Erc20TradingApproval / Erc1155TradingApproval / TradingApprovalRequirements are now public exported types.
Also, while you're in there: TradingApprovalsState's own members are undocumented, and missing.erc20[].amount is the required allowance (MAX_UINT256), not a shortfall — worth one line each so missing doesn't read as "the amount you're short by".
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 021a542. Configure here.
|
Round 2 — all three round-1 findings addressed. Two things to look at before merge. [issue] The secure decorator's
fetchTradingApprovalsState: (
request: SecureFetchTradingApprovalsStateRequest = {},
) =>
fetchTradingApprovalsState(client, {
...request,
wallet: request.wallet ?? client.account.wallet,
}),Two of the three vectors in
Neither is caught by CI: [issue]
Nothing else. The refine is exactly |
|
Round 3 — both round-2 findings are closed, and the merge from Invalid-input contract. Bindings changeset. Address validation. Dropping [nit] The defaulting logic is inline where the sibling pattern is a helper. [nit] Two Every code check is green. The one red check is Merge verification
|
…ing-approvals-state

Summary
Closes #286
Linear: DEV-564
Testing
Note
Medium Risk
Touches trading approval resolution shared with setup workflows and changes EVM address validation behavior across bindings consumers. The new API itself is read-only eth_call based.
Overview
Adds
fetchTradingApprovalsState, a read-only API that reports whether a wallet is fully approved for trading and which ERC-20/ERC-1155 approvals are still missing—without a signer or transactions.Public clients can inspect any wallet; secure clients default to the authenticated wallet. The existing setup workflow now reuses the same resolver, and malformed approval-check RPC results surface as
UnexpectedResponseError. Related approval requirement types are exported.Also updates
EvmAddressSchemaso bad addresses fail Zod validation instead of throwingInvariantError.Reviewed by Cursor Bugbot for commit 6407752. Bugbot is set up for automated code reviews on this repo. Configure here.