Feat - Native Tokens (ETH/SOL/XLM) Payment support#47
Conversation
…end-to-end
Native sources were treated as 1:1 USD-pegged, so amounts were wrong across
the transfer, fee, create-payment, and display paths. Separate the three
distinct amounts (USD value / source-token amount / destination payout) and
thread them correctly.
- payWithToken/Solana/Stellar transfers: use required.amount (source-token
units) instead of required.usd
- add decimal-tolerant helpers tokenBaseAmountToDecimalString /
tokenAmountToBaseUnits (fixes "Cannot convert 0.0129… to a BigInt")
- createPayment payload: send source.amount via new preferredAmountUnits,
keep destination payout in toUnits (no longer conflated)
- getFee amount is now type-aware: source-token amount for exactIn,
USD/destination for exactOut
- formatPaymentResponseToHydratedOrder: derive usdValue/destFinalCallTokenAmount
from destination, not source.amount (fixes cancel → $0.02 + corrupted
usdRequired); surface source.amount via metadata for deposit flow
- default preferredSymbol now includes natives so they aren't filtered out
- keep Solana lamports/transferAmount as bigint (no Number() precision loss)
- tsconfig moduleResolution node → bundler (drops false ox@0.14.30 type error)
- native tokens render chain icon instead of logoURI on SelectToken + pay pages
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…d @rozoai/intent-pay v0.1.35-beta.1
There was a problem hiding this comment.
💡 Codex Review
intent-pay/packages/connectkit/src/hooks/usePaymentState.ts
Lines 792 to 795 in fc97bc8
When the user selects a native EVM token on a chain different from the wallet's current chain, this branch sends without either switchChainAsync({ chainId: required.token.chainId }) or a chainId on sendTransactionAsync. Unlike the ERC-20 branch below, the wallet can therefore submit the value transfer on the currently selected chain while the backend expects the payment on required.token.chainId, which can make native ETH/BNB/POL payments fail or land on the wrong chain.
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…ment transaction logic - Removed redundant imports for token formatting utilities. - Consolidated token chain ID checks and data suffix handling for improved clarity and efficiency. - Enhanced deep link generation for EVM payments by simplifying amount unit parsing.
AI review: 0 P0 / 6 P1 / 4 P2 — no blocking issues foundLabeled P0 (blocking)None. P1 (should fix)
P2 (nice to have)
ai-pr-review · claude-sonnet-5 |
|
ai-pr-review swept this head ( |
…d @rozoai/intent-pay v0.1.35-beta.2
AI review: 3 P0 (blocking) / 6 P1 / 4 P2P0 findings must be addressed before merge. This is advisory — no auto-approve, no auto-block; a human decides. P0 (blocking)
P1 (should fix)
P2 (nice to have)
ai-pr-review · claude-sonnet-5 |
AI review: 3 P0 (blocking) / 6 P1 / 5 P2P0 findings must be addressed before merge. This is advisory — no auto-approve, no auto-block; a human decides. P0 (blocking)
P1 (should fix)
P2 (nice to have)
ai-pr-review · claude-sonnet-5 |
|
ai-pr-review swept this head ( |
- Add getTokenPrices API (pay-common) for live USD conversion
- WaitingDepositAddress: convert USD → source-token units via live price
for native sources (SOL/ETH/XLM); getFee uses actual feeType (no
ExactOut override)
- buildCreatePaymentPayload: destination stays at toUnits — fee is on
source side, not destination (was: ExactOut branch subtracted fee)
- usePaymentState.payWithDepositAddress: source amount from fees response
(fee-inclusive) for both native and stablecoin; was bare toUnits
- fetchApi: surface BE structured error message, not raw JSON envelope
- PayWithToken/{Solana,Stellar}: scope native ExactOut override to
fee-quote only (createPayment keeps payParams.feeType)
- WaitingDepositAddress QR: nativeAsChainIcon for native tokens (single
chain badge, no redundant token+chain pair)
- Bump @rozoai/intent-common → 0.1.22-beta.3, @rozoai/intent-pay → 0.1.35-beta.3
- Add evm-native / solana-native / stellar-native e2e specs - Wire them into playwright.config.ts (sequential deps on existing merchant suite; reroute deposit-stellar-to-evm dep) - Extract APP_ID to app/const.ts (shared by Bridge/Checkout/DepositMode) - snippets.ts: import reorder + prettier semi-colons
AI review: 2 P0 (blocking) / 8 P1 / 5 P2P0 findings must be addressed before merge. This is advisory — no auto-approve, no auto-block; a human decides. P0 (blocking)
P1 (should fix)
P2 (nice to have)
ai-pr-review · claude-sonnet-5 |
- add AGENTS.md - pay-common: safe getChainName fallback, chain-aware normalizeTokenAddress - connectkit: dualChainConnect state + fallback QueryClientProvider - connectkit: migrate payment option hooks to @tanstack/react-query - connectkit: add generateStellarDeepLink helper - connectkit: remove refreshUtils.ts
- point @rozoai/intent-common/pay to workspace:* in package.json files - update pnpm-lock.yaml for workspace dependency resolution - add packages/connectkit/.oxfmtrc.json
…ization for multi-chain payments
AI review: 3 P0 (blocking) / 9 P1 / 4 P2P0 findings must be addressed before merge. This is advisory — no auto-approve, no auto-block; a human decides. P0 (blocking)
P1 (should fix)
P2 (nice to have)
ai-pr-review · claude-sonnet-5 |
- Add missing native specs: checkout/solana-native, deposit/evm-native, deposit/solana-native, merchant/polygon-native - Add pay-to-address payment-flows mode (merchant-evm, merchant-evm-native, merchant-solana fixme) with correct DepositAddressPaymentOptions testIds - Replace 30+ test:e2e:* npm scripts with single e2e/run.cjs runner (pnpm test:e2e -- <project>, --list, --mocked, --ui) - Update playwright.config.ts with all new projects in dependency chain - Update README with full E2E usage guide and maintenance contract fix(sdk): usdRequired=0 shows wrong deposit-address chain options - useDepositAddressOptions: guard enabled with usdRequired > 0 to prevent querying with zero value before order loads - startMerchantDepositAddressCheckout: wait for order sentinel before returning so usdRequired is populated when pay-to-address is clicked - getDepositAddressInfo: wait for data-value attributes to be non-empty before reading (async API population race) fix(sdk): native source amount corrupts usdValue in formatPaymentResponseToHydratedOrder - bridge-utils: detect native source token via address sentinel set (ethAddress/zeroAddress/SOL system program/XLM) instead of fiatISO; use destination.amount for native sources, source.amount for stablecoins refactor(pay-common): expose isNativeToken from pay-common, remove duplicate - token.ts: export NATIVE_TOKEN_ADDRESSES + isNativeToken(string) with zeroAddress added to sentinel set - bridge-utils: import isNativeToken from same package, drop inline set - connectkit/utils/token.ts: re-export isNativeToken from @rozoai/intent-common, drop local implementation - Update all isNativeToken callers to pass address string instead of Token object
AI review: 3 P0 (blocking) / 8 P1 / 6 P2P0 findings must be addressed before merge. This is advisory — no auto-approve, no auto-block; a human decides. P0 (blocking)
P1 (should fix)
P2 (nice to have)
ai-pr-review · claude-sonnet-5 |
- Type metadata.sourceAmountUnits in zRozoPayOrderMetadata Zod schema
- Remove as any casts: HydrateWalletOption type, store param narrowed to { dispatch }
- Native deposit source throws on missing sourceAmountUnits instead of wrong usdValue fallback
- Fix rozoPaymentResponse undefined fallback in PayWithSolanaToken hydrate path
- Price NaN guard + stale flag warning in WaitingDepositAddress
- Fix tokenBaseAmountToDecimalString param: BigIntStr -> string
- Fix bridge-utils prefferedToken typo
- Fix dualChainConnect flag not cleared in second useEffect — fixes Phantom mobile Solana-only pay-with-another-wallet doing nothing
- Atomic setRoute: merge route+routeMeta into single state in RozoPayProvider — fixes two-render race on Phantom mobile payId mode causing isExplicitBackNavigation to miss and re-navigate user to SELECT_TOKEN
- Pay to Address loading: disabled option + spinner replaces chain icons (Option.loading prop)
- ConnectorStellar: add Try Again + Cancel buttons on connection error
- ConnectorSolana: add error state via adapter error event + Try Again / Cancel buttons
- Add CHANGELOG.md to connectkit
- RozoPayModal: auto-trigger injected connector connect() when inside Phantom in-app browser with Solana-only auto-connected (deeplink case) - SelectMethod: extract disconnectAll() using Promise.allSettled with guards (isSolanaConnected/isStellarConnected) to prevent TypeError when disconnect is undefined on uninitialized adapter
Summary
Native-token payments (SOL, ETH, BNB, POL, XLM) as a payment source were broken across the SDK because the code assumed the source token was 1:1 with USD. This collapsed three distinct amounts into one:
1.3required.usd0.016885 SOLrequired.amount1.3 USDCdestFinalCallTokenAmountFor USDC/USDT the three coincide, so the bug only surfaced on native tokens.
This PR also includes a full code-review pass (P0–P2 fixes), UX improvements across connection and loading states, and several mobile flow fixes.
Changes
Native token payments (core)
payWithToken, Solana, Stellar): userequired.amount(source-token units), notrequired.usd.tokenBaseAmountToDecimalString/tokenAmountToBaseUnitshandle both integer base-units and decimal strings.preferredAmountUnitscarries the source amount;toUnitsstays the destination payout.amountis type-aware — source-token amount forexactIn, USD/destination forexactOut.formatPaymentResponseToHydratedOrder:usdValue/destFinalCallTokenAmountnow come from the destination. Source amount surfaced viametadata.sourceAmountUnits.lamports/transferAmountstaybigint(noNumber()truncation).logoURIon SelectToken and pay pages (nativeAsChainIcon).Code review fixes (P0/P1)
metadata.sourceAmountUnits: added tozRozoPayOrderMetadataZod schema — removes all(order.metadata as any)?.sourceAmountUnitscasts."sourceAmountUnits missing"instead of silently falling back tousdValue(the wrong USD destination payout).HydrateWalletOptiontype: narrower{ required: { token, amount: string }, fees: { usd } }replaces doubleas anycasts on thehydrateOrderdeposit-address path.store as anyeliminated: narrowedstoreparam type to{ dispatch }— the only field actually used.tokenBaseAmountToDecimalString: widened param fromBigIntStrtostring.bridge-utilsprefferedTokentypo fixed.rozoPaymentResponseundefined on hydrate path:PayWithSolanaTokenfalls back tohydratedOrder.metadata?.sourceAmountUnits.WaitingDepositAddressusesNumber.isFinite+ logs stale price warning.Dual-chain / mobile flow
dualChainConnectflag not cleared: restoredsetDualChainConnect(false)accidentally dropped in refactor — fixes Phantom mobile Solana-only → "Pay with another wallet" doing nothing.setRouteatomic update: mergedroute+routeMetainto a single state object inRozoPayProvider. Eliminates two-render race whereisExplicitBackNavigationcheck fired beforerouteMetaarrived, causing the auto-connect gate to re-navigate the user back toSELECT_TOKENon Phantom mobile payId flows.useWalletPaymentOptions,useSolanaPaymentOptions,useStellarPaymentOptions,useDepositAddressOptionsmigrated to@tanstack/react-query.dualChainConnectstate inPayContext/RozoPayProvider; mobile dual-chain wallet flow viaConnectorList.UX improvements
disabledwhile deposit chains load; chain icons replaced with a spinner (Option.loadingprop onOptionsList).ConnectorStellarerror recovery: "Connection Failed" now shows Try Again + Cancel instead of a dead-end message.ConnectorSolanaerror recovery: no error handling existed — spinner hung forever on rejection/expiry. Added adaptererrorevent listener + same Try Again / Cancel buttons.Infrastructure
generateStellarDeepLink;WaitingDepositAddressgenerates them for Stellar deposit addresses.normalizeTokenAddress(chainId, token)replaces ad-hoc.toLowerCase().workspace:*inpackages/connectkitandexamples/nextjs-app..oxfmtrc.json;AGENTS.mdupdated;CHANGELOG.mdcreated.Tested
pnpm build: 0 TypeScript errors (connectkit + pay-common).pnpm lint: 0 errors on both packages.pnpm --filter @rozoai/intent-common test: 41/41 pass.Follow-ups (not blocking)
WaitingWallet: no cancel path when wallet app doesn't open.WaitingExternal: no timeout/cancel for abandoned exchange flows.SelectTokenempty state: no guidance on why no tokens are available.