Skip to content

feat: Implement FiatStrategy submit flow with order polling and relay execution#8347

Open
OGPoyraz wants to merge 8 commits intomainfrom
ogp/CONF-960
Open

feat: Implement FiatStrategy submit flow with order polling and relay execution#8347
OGPoyraz wants to merge 8 commits intomainfrom
ogp/CONF-960

Conversation

@OGPoyraz
Copy link
Copy Markdown
Member

@OGPoyraz OGPoyraz commented Mar 31, 2026

Explanation

Implements FiatStrategy submit flow with order polling and relay execution.

  • Implement submitFiatQuotes to poll the on-ramp order via RampsController:getOrder until completion, then re-quote and submit the relay leg with the settled crypto amount
  • Add order validation that verifies the completed order's asset and chain match the expected fiat asset before proceeding with relay
  • Update relay quote request filtering to support exact-input max amount requests used by the fiat submit re-quote path
  • Add orderCode field to TransactionFiatPayment and RampsControllerGetOrderAction to AllowedActions to enable order polling from the fiat strategy
  • Fail-closed on terminal order statuses (failed/cancelled), polling timeout with last-status reporting, asset mismatches, and invalid crypto amounts

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

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 FiatStrategy submit path: submitFiatQuotes now reads the stored fiat orderId, polls RampsController:getOrder until 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 AllowedActions to include RampsControllerGetOrderAction, and adds TransactionFiatPayment.orderId to 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.

@OGPoyraz OGPoyraz marked this pull request as ready for review March 31, 2026 09:04
@OGPoyraz OGPoyraz requested review from a team as code owners March 31, 2026 09:04
Comment on lines +218 to +219
/** Order identifier - `orderCode` specifically used as RampsService:getOrder parameter in normalized format (/providers/{provider}/orders/{id}). */
orderCode?: string;
Copy link
Copy Markdown
Member Author

@OGPoyraz OGPoyraz Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, orderId or rampsId to be more generic?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - renamed to orderId

Comment on lines +81 to +89
.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;
})
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@OGPoyraz OGPoyraz changed the title feat: Implement fiat strategy submit flow with order polling and relay execution feat: Implement FiatStrategy submit flow with order polling and relay execution Mar 31, 2026
vinistevam
vinistevam previously approved these changes Mar 31, 2026

const log = createModuleLogger(projectLogger, 'fiat-submit');

const ORDER_POLL_INTERVAL_MS = 1000;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, I added feature flags for this on Relay, could extend for fiat in future.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, should this be in the request to make it more "pure"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh we already have an order ID? Is this generated by the provider or Ramps?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - added validateRelaySlippage that compares the re-quoted relay's target output against the original. Fails if slippage exceeds 5%.

Comment on lines +218 to +219
/** Order identifier - `orderCode` specifically used as RampsService:getOrder parameter in normalized format (/providers/{provider}/orders/{id}). */
orderCode?: string;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, orderId or rampsId to be more generic?

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

isPostQuote: false,
sourceBalanceRaw: sourceAmountRaw,
sourceTokenAmount: sourceAmountRaw,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Additional Locations (1)
Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants