feat: Implement FiatStrategy submit flow with order polling and relay execution#8347
feat: Implement FiatStrategy submit flow with order polling and relay execution#8347
FiatStrategy submit flow with order polling and relay execution#8347Conversation
| /** Order identifier - `orderCode` specifically used as RampsService:getOrder parameter in normalized format (/providers/{provider}/orders/{id}). */ | ||
| orderCode?: string; |
There was a problem hiding this comment.
Originally we talked Ramps team to add metadata into fiat orders and also filtering mechanism.
Due to limitation of timeframe this may not be option to deliver besides headless ramp, hence we are adding orderCode (meaning order id) into TransactionFiatPayment
There was a problem hiding this comment.
Minor, orderId or rampsId to be more generic?
There was a problem hiding this comment.
Done - renamed to orderId
| .filter((singleRequest) => { | ||
| const hasTargetMinimum = singleRequest.targetAmountMinimum !== '0'; | ||
| const isPostQuote = Boolean(singleRequest.isPostQuote); | ||
| const isExactInputRequest = | ||
| Boolean(singleRequest.isMaxAmount) && | ||
| new BigNumber(singleRequest.sourceTokenAmount).gt(0); | ||
|
|
||
| return hasTargetMinimum || isPostQuote || isExactInputRequest; | ||
| }) |
There was a problem hiding this comment.
The fiat re-quote request could get dropped by this filter if targetAmountMinimum happened to be '0' - it's not a post-quote, so the second condition wouldn't save it. The isExactInputRequest check distinguishes real exact-input requests (positive source amount + max amount flag) from empty gas fee token requests (zero source amount), ensuring the fiat re-quote passes through.
FiatStrategy submit flow with order polling and relay execution
|
|
||
| const log = createModuleLogger(projectLogger, 'fiat-submit'); | ||
|
|
||
| const ORDER_POLL_INTERVAL_MS = 1000; |
There was a problem hiding this comment.
Minor, I added feature flags for this on Relay, could extend for fiat in future.
There was a problem hiding this comment.
Good call, I'll extend the feature flags for fiat polling in a follow-up, not only this but also tokens per type.
| throw new Error('Missing wallet address for fiat submission'); | ||
| } | ||
|
|
||
| const state = messenger.call('TransactionPayController:getState'); |
There was a problem hiding this comment.
Minor, should this be in the request to make it more "pure"?
There was a problem hiding this comment.
Fair point - I can work on this on separate PR as this will touch beyond fiat scope.
|
|
||
| while (true) { | ||
| const order = await messenger.call( | ||
| 'RampsController:getOrder', |
There was a problem hiding this comment.
To clarify, does this trigger a network request in the RampsController?
Would it be more efficient if they owned the polling and we listened to a messenger event like RampsController:quoteStatusChange or RampsController:stateChange if it's updated in state?
There was a problem hiding this comment.
Yes it triggers a network request via RampsController:getOrder. Event-based via stateChange would be cleaner - agreed it's the better long-term approach. For now polling keeps fiat self-contained without coupling to RampsController's internal state updates.
In fact this we should be able to just subscribe -order status change-, I will bring this topic once we proceed with other blockers.
| const expectedChainId = expectedAssetId.split('/')[0]; | ||
| const orderChainId = orderCrypto?.chainId?.toLowerCase(); | ||
|
|
||
| if (orderAssetId && orderAssetId !== expectedAssetId) { |
There was a problem hiding this comment.
Oh we already have an order ID? Is this generated by the provider or Ramps?
There was a problem hiding this comment.
Generated by the provider (e.g Transak). The mobile app receives it after checkout completion and writes it to state via updateFiatPayment. The normalized format /providers/{provider}/orders/{id} is what Ramps uses internally.
So I expect them to execute a callback after successfully checkout passed while starting headless ramps (meaning we have orderId) then regularly fetch in the TPC.
| transactionId, | ||
| }); | ||
|
|
||
| const relayQuotes = await getRelayQuotes({ |
There was a problem hiding this comment.
Do we need to validate the final quote to check the fees are within our original slippage compared to what we showed the user?
Or is that a separate PR?
There was a problem hiding this comment.
Good catch - added validateRelaySlippage that compares the re-quoted relay's target output against the original. Fails if slippage exceeds 5%.
| /** Order identifier - `orderCode` specifically used as RampsService:getOrder parameter in normalized format (/providers/{provider}/orders/{id}). */ | ||
| orderCode?: string; |
There was a problem hiding this comment.
Minor, orderId or rampsId to be more generic?
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes 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.
packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.ts
Outdated
Show resolved
Hide resolved
| isPostQuote: false, | ||
| sourceBalanceRaw: sourceAmountRaw, | ||
| sourceTokenAmount: sourceAmountRaw, | ||
| }; |
There was a problem hiding this comment.
Re-quote may fail when transaction carries contract calldata
Medium Severity
The relay re-quote request sets isPostQuote: false with isMaxAmount: true. Since isPostQuote is false, getSingleQuote calls processTransactions on the original transaction. If that transaction has non-empty, non-token-transfer data (likely for predictDeposit or perpsDeposit contract calls), processTransactions throws "Max amount quotes do not support included transactions." The original fiat quote in fiat-quotes.ts avoids this by using isPostQuote: true to skip processTransactions entirely.


Explanation
Implements
FiatStrategysubmit flow with order polling and relay execution.submitFiatQuotesto poll the on-ramp order viaRampsController:getOrderuntil completion, then re-quote and submit the relay leg with the settled crypto amountorderCodefield toTransactionFiatPaymentandRampsControllerGetOrderActiontoAllowedActionsto enable order polling from the fiat strategyReferences
Checklist
Note
Medium Risk
Touches transaction submission flow by adding a new polling + re-quote path and a change to Relay quote filtering, which could affect execution behavior if request classification is incorrect, but it is guarded by strict validation and tests.
Overview
Implements the
FiatStrategysubmit path:submitFiatQuotesnow reads the stored fiatorderId, pollsRampsController:getOrderuntil completion (with timeout and fail-closed terminal statuses), validates the settled order asset/chain, converts the settled crypto amount to a raw source amount, then re-quotes and submits the Relay leg.Updates Relay quote request filtering to allow exact-input (max-amount) requests used by the fiat re-quote flow, extends messenger
AllowedActionsto includeRampsControllerGetOrderAction, and addsTransactionFiatPayment.orderIdto persist the normalized on-ramp order identifier. Adds extensive unit coverage for polling, validation, slippage guardrails, and error cases.Written by Cursor Bugbot for commit 6cd9e68. This will update automatically on new commits. Configure here.