Swap in both directions through NEAR Intents 1Click - #675
Conversation
SwapService is now a 1Click client: token list, dry quote, live quote with a deposit address, and status by deposit address. The mock quotes, fake deposit address, hard-coded prices and the silent token fallback are gone. Server rejections surface as SwapApiException with the server's message. The HTTP client and partner API key are injectable. SwapToken carries the 1Click asset id and USD price. SwapQuote and SwapOrder hold base-unit amounts as BigInt. NumberFormattingService takes a decimals count so every token reuses the locale-aware parser and formatter. QTC is not listed on NEAR Intents yet, so quantusIntentsAssetId is a placeholder and every quote into it fails with "tokenOut is not valid". Tests cover the request shape, response parsing, all statuses and the error paths against a mock client.
The swap screen loads real tokens, quotes with the active account as recipient, and shows server errors as a toast. The review sheet shows the quoted amount out, minimum received and time estimate, then asks for a live quote on Confirm. The deposit screen shows the real deposit address and deadline, polls status every 5 seconds, and has a view for each status plus an expired one. Demo strings are removed, English and Indonesian updated. The home swap button stays behind showSwapButton.
# Conflicts: # mobile-app/lib/v2/screens/swap/review_quote_sheet.dart # mobile-app/lib/v2/screens/swap/swap_screen.dart # quantus_sdk/lib/src/services/swap_service.dart
Debug builds no longer show the migration notice on every launch with a testnet outcome picker. The notice now only shows when it is due, in debug and release alike. Drops AppConstants.debugMainnetMigration, the forced-outcome provider and the picker's test.
SwapToken knows whether it is the Quantus token and names its network
("Ethereum" for ETH). SwapOrder reads the origin chain transaction
hashes. SwapService tells 1Click which transaction paid a deposit
address (POST /v0/deposit/submit) and keeps saved addresses per network
instead of recent refund addresses.
LocaleNumberConfig.parseDecimal sizes its input cap by the token's
decimals, so a 24-decimal amount parses instead of returning null.
NumberFormattingService.formatExactAmount prints every significant
digit for amounts someone must send exactly. QuantusTextField can keep
room for a trailing widget wider than one icon button.
The swap form holds QTC in the account on one side and a token on another chain on the other; the arrows flip the direction. An address sheet asks for the recipient (swap out) or refund address (swap in), with saved addresses and QR scan, and quotes the swap. Review Swap is a full screen with rate, address, network fee, slippage allowance and guaranteed minimum. The slippage tolerance is picked from 0.5, 1, 2 and 3 percent. Confirming takes a live quote. When it guarantees less than the one reviewed, the new terms replace the old and need a second confirm. Swapping out, the app then signs a transfer of the QTC to the deposit address with the account's own key, using the regular send path, and hands the hash to 1Click. One progress screen follows the order: the deposit address while a swap in waits, then the steps, a details sheet, and Swap Complete or Swap Failed. The deposit amount shows at full precision. Swap is enabled only for transparent accounts that sign in the app.
n13
left a comment
There was a problem hiding this comment.
Reviewer model: GPT-6 Sol
Verdict (advisory): Request changes
Reviewed head 3e6344143974b842708b9fc5c28022ad34c6e5cc against base 900fa77912e74b00fe60678b48e6f61b556e2bdd.
-
[P1] Show required deposit memos for swap-in quotes. swap_progress_screen.dart:413 encodes only the address in the QR, and the screen's copy/share actions also provide only the address. Yet
SwapQuoteretainsdepositMemo, and 1Click's SDK says some origin-chain deposits require that memo to be processed. The token list includes Stellar XLM, for which the SDK describes memo-mode deposits. If a live quote has a memo, following this screen's instructions sends an incomplete deposit and risks stranded funds. Display and separately copy the memo, include it in sharing instructions, and test a memo-bearing live quote; otherwise reject that route before exposing the deposit address. -
[P1] Allow enough deposit time for slow origin chains. swap_service.dart:28 gives every quote a 20-minute deadline, while BTC is selectable from the live token list. 1Click's quote contract says the deadline must exceed origin-chain mining time and gives Bitcoin as an example that may require about an hour. A correctly sent BTC deposit can therefore reach confirmation after the quote's deadline and be refunded or delayed, with refund fees or loss exposure. Use a chain-appropriate window or exclude chains for which this deadline cannot be met.
-
[P2] Show the exact outgoing amount on Review Swap. swap_summary.dart:84 calls
formatAmount, which truncates ordinary values to four fractional digits. For example, a quoted payment of1.00009 QTCappears as1 QTCon the confirmation screen whilesubmitLocaltransfers the full1.00009 QTC. Use the existingformatExactAmountfor the pay side of the review card so the amount authorized is the amount shown.
Validation: git diff --check passed; formatting of 14 touched Dart source files made no changes; 18 focused SDK tests and 107 focused mobile widget/locale tests passed. GitHub's Analyze check passed for this head. No funded swap could be tested because QTC is not yet listed on 1Click; the production enableSwap flag must be disabled before release as the PR description notes.
Address the review on the swap-out PR. A live quote's depositMemo is now shown on the deposit screen with its own copy action, a warning that a deposit without it is not processed, and a place in the shared text. Deposits from BTC, LTC, DOGE, BCH, DASH and ZEC get a two-hour window instead of twenty minutes, since their confirmations can take an hour. The pay side of the swap card and the details sheet print every digit of the amount that is sent. Once 1Click lists QTC under AppConstants.quantusIntentsAssetId, its asset id, decimals and price take over from the app's own QTC metadata. A listing whose decimals differ from the chain's is refused, since every quoted amount would then be wrong on chain.
|
Addressed in cb52d04:
Also in the same commit: once 1Click lists QTC under |
…ures Checked against the 1Click OpenAPI spec (v0.1.10) and live dry quotes. Stellar refuses a quote unless depositMode is MEMO, and every other chain refuses MEMO, so the request names the mode per origin chain. Every quote carries referral "quantus", the distribution channel id 1Click records. SwapQuote keeps 1Click's signature over the quote, and the signed response of every live quote is saved on the device, which the spec requires for settling a dispute about a deposit address.
|
3e46fc1 checks the integration against the 1Click OpenAPI spec (v0.1.10,
Open, not in this PR: the about page says users must accept the 1Click Terms of Service; the app shows no such notice yet. Signature verification needs 1Click's public key, which the docs page does not give; the TypeScript SDK does it. Unauthenticated quotes carried a 20 bps |
Every quote response now has to carry 1Click's Ed25519 signature over the request and quote fields, checked the way verifyQuoteSignature in @defuse-protocol/one-click-sdk-typescript does it: key-sorted JSON of the signed fields plus the timestamp, SHA-256, base58, then the signature over the bytes of that string against the 1Click manager key. The echoed quoteRequest must also repeat the amount, assets, addresses, slippage and dry flag that were sent. Either failing throws SwapQuoteIntegrityException before the quote is shown or a deposit address is used. The SDK's staging fixtures verify byte for byte against its staging key. Test fakes sign their responses with a generated key. Adds crypto, ed25519_edwards and base_x as direct SDK dependencies; all three were already in the lock file through polkadart.
|
fd45f10 verifies quote signatures. Neither reference wallet does this: Vizor (chainapsis/vizor-wallet, Flutter) hard-codes
Tests: the SDK's staging fixtures (live, dry, and the status-endpoint variant with null routing fields) verify byte for byte against its staging key; a tampered deposit address, the wrong key and malformed signatures are rejected; a signed quote answering a different request is rejected. Mock 1Click servers in both test suites now sign their responses with a generated key. |
n13
left a comment
There was a problem hiding this comment.
Reviewer model: GPT-6 Sol
Verdict (advisory): Request changes
Reviewed head fd45f10d072eaceb985c3114ada8abb6aa6983c2 against base 900fa77912e74b00fe60678b48e6f61b556e2bdd. The three findings from the previous review (deposit memo, slow-chain deadline, exact outgoing display) are addressed.
-
[P1] Refuse an expired live quote before transferring QTC. SwapService.createSwap requires a deposit address but does not check the signed quote's deadline. ReviewSwapScreen only checks the deadline when reusing a previously held order; it can immediately transfer to a newly returned expired address. The
createSwaptest currently passes with a live deadline of 2026-09-20, already past on this review date. A stale or replayed signed response can therefore direct funds to an inactive deposit address. Verify the echoed request deadline and require a live deposit deadline with enough time for signing and inclusion before sending. -
[P1] Do not silently increase the amount sent after confirmation. ReviewSwapScreen._liveOrder compares only
minAmountOut. The reviewed card and affordability check use_quote.amountIn, but the transfer usesorder.quote.amountInfrom the live response. The echo check verifiesquoteRequest.amount, notquote.amountIn. A signed live response with a largeramountInand an unchanged or better minimum sends more QTC than the user reviewed. Require the live input amount to equal the reviewed amount, or show the changed amount for a second confirmation and repeat the balance and fee checks.
Validation: git diff --check passed; dart format --output=none --set-exit-if-changed changed no files; 29 focused SDK tests and 109 focused mobile tests passed; GitHub Analyze passed at this head. No funded swap was possible because QTC is not yet listed on 1Click. Disable the production enableSwap flag before releasing this feature, as the PR description notes.
A quote now has to price exactly the amount sent (quote.amountIn equals the request amount) and echo the deadline that was sent, on top of the signature and the other echoed fields, so a swap out transfers the amount the user reviewed and nothing else. createSwap refuses a live quote whose deposit address has less than five minutes left, so a stale or replayed signed response can no longer direct a deposit to an address that is about to go cold. The review screen reuses the same lead when deciding whether to keep a live order for a retry. The request deadline goes out at millisecond precision so the echo can be compared as an instant.
|
Both addressed in 47e99a1.
Analyzer clean; mobile 522 and SDK 598 tests pass. |
n13
left a comment
There was a problem hiding this comment.
Reviewer model: GPT-6 Sol
Verdict (advisory): Approve
Reviewed head 47e99a10d425e2dced59ad11689623071a5628b1 against base 900fa77912e74b00fe60678b48e6f61b556e2bdd. No blocking code findings remain. The latest commit checks that the signed live quote prices the requested input amount, echoes the requested deadline, and leaves at least five minutes before the deposit address expires. This addresses the two findings in my previous review; the earlier memo, slow-chain window, and exact-pay-display findings are also addressed.
Validation: git diff --check passed; formatting of 28 existing changed Dart files made no changes; 32 focused SDK tests and 110 focused mobile tests passed. GitHub's Analyze check passed for this exact head. A dry quote probe from this host received HTTP 403, and a funded end-to-end swap remains untested because QTC is not yet listed on 1Click.
Release gate: keep enableSwap off in production before shipping a build with this PR. Enable it only after QTC is listed and both swap directions have been tested against live quotes and funded transfers, as the PR description notes.
What this does
Swap now works both ways between QTC and tokens on other chains, following the Figma "Swap Out flow". It builds on the 1Click integration from #664: that PR's three commits are included, with main merged in, so this PR supersedes #664.
/v0/deposit/submit.swapOrderProvider. For swap in it first shows the deposit address. Then it shows the steps, the progress bar and a details sheet, and ends on Swap Complete or Swap Failed (refunded, with Start a New Swap and Contact Support).Review findings from #664, fixed here
formatExactAmount).parseDecimalsizes its input cap by the token's decimals, so 24-decimal amounts parse.Blocker: QTC is not listed on NEAR Intents
Right now every quote goes to the real 1Click API with the placeholder asset id
nep141:qtc.omft.near. The token list loads (197 assets, none Quantus), prices and estimates work, but every quote fails withtokenOut is not valid(swap in) ortokenIn is not valid(swap out). The app shows that as an error toast in the address sheet. Nothing is sent. The home swap card follows theenableSwapremote config flag, which is currentlytruein production.Release gating
Swap ships behind the
enableSwapremote config flag. Real swaps cannot be tested until QTC is listed on NEAR Intents. SetenableSwaptofalsein production before a build with this PR goes out, and turn it back on only after swaps have been tested against the live listing. The same flag also hides the demo swap in builds already released.Where this differs from Figma
Verification
melos run analyze: no issues in all four packages.melos run formatis clean.test/screens/swap_flow_test.dart. It covers the form's direction and balance states, slippage going into the quote, the swap-out confirm sending to the live deposit address, the price-moved re-confirm, the insufficient balance block, and the complete, refunded and deposit views.