Skip to content

Conversation

@lissavxo
Copy link
Collaborator

@lissavxo lissavxo commented Dec 23, 2025

Related to #44

Description

Description

This PR adds full support for token-based payments and updates the payment flow accordingly.

Changes included:

  • Accepts a token-id parameter to enable token-specific payments

  • Detects and validates payments for the specified token

  • Fetches the token icon and displays it in the QR code

  • Updates the dialog text to display the token ticker (e.g., Send 10 XECX)

  • Updates the payment URL to include token_id and token_decimalized_qty, removing unused parameters

Will continue the following remaining steps in another PR:

  • Convert the currency amount to the token amount

Test plan

Create a button with token-id param check dialog text, qrcode icon. Copy the payment url check if it contains the token-id, test the payment url in cashtab, make a payment with the specifications of the button and make sure the payment is detected.

Summary by CodeRabbit

  • New Features
    • Added token payment support enabling users to process payments using specific tokens.
    • Payment interfaces now display token-specific information, metadata, and icons in dialogs and QR codes.
    • Payment button configuration expanded to support token identification throughout the payment workflow.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 23, 2025

📝 Walkthrough

Walkthrough

This PR adds token support to the PayButton library by introducing an optional tokenId prop that flows through PayButton → PaymentDialog → Widget components, with utility functions extended to fetch token metadata, calculate token-specific transaction amounts, and construct token-aware URLs and QR codes.

Changes

Cohort / File(s) Summary
Component prop propagation
paybutton/src/index.tsx, react/lib/components/PayButton/PayButton.tsx, react/lib/components/PaymentDialog/PaymentDialog.tsx
Added optional tokenId prop to component interfaces and destructured signatures; propagated through component hierarchy from PayButton down to WidgetContainer via PaymentDialog.
Widget token integration
react/lib/components/Widget/Widget.tsx
Introduced tokenId prop and new tokenName state; fetches token info via getTokenInfo; updated URL resolution to accept and include tokenId parameter; conditional logic branches for token-specific amounts (using token_decimalized_qty), donation/amount text (using tokenName), and QR code icon source (token icon URL).
Chronik utility functions
react/lib/util/chronik.ts
Added getTokenAmount() to accumulate token outputs; added getTokenInfo() to fetch token metadata; extended getTransactionFromChronikTransaction(), parseWebsocketMessage(), and initializeChronikWebsocket() signatures with optional tokenId parameter; integrated token amount resolution where provided.
Socket setup
react/lib/util/socket.ts
Added optional tokenId to SetupTxsSocketParams interface; forwarded tokenId to initializeChronikWebsocket() call.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant PayButton
    participant Widget
    participant Chronik
    participant ChronikWS

    User->>PayButton: Pass tokenId prop
    PayButton->>PayButton: Destructure tokenId
    PayButton->>Widget: Pass tokenId to WidgetContainer
    Widget->>Widget: Store tokenId in props
    
    alt tokenId provided
        Widget->>Chronik: getTokenInfo(tokenId, address)
        Chronik-->>Widget: Return token metadata
        Widget->>Widget: Set tokenName state
        Widget->>Widget: resolveUrl with tokenId param
    else no tokenId
        Widget->>Widget: Use currency code
        Widget->>Widget: resolveUrl without tokenId
    end
    
    Widget->>ChronikWS: setupChronikWebSocket with tokenId
    ChronikWS->>ChronikWS: initializeChronikWebsocket with tokenId
    
    alt Token transaction received
        ChronikWS->>Chronik: parseWebsocketMessage with tokenId
        Chronik->>Chronik: getTokenAmount for matching outputs
        Chronik-->>ChronikWS: Return token-aware transaction
    else Regular transaction
        ChronikWS->>Chronik: parseWebsocketMessage standard flow
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • defaultProps no longer assigned. #551: Both PRs modify PayButton, PaymentDialog, and Widget component signatures; potential conflict when merging token-related props with inline destructuring defaults.

Suggested labels

enhancement (behind the scenes)

Suggested reviewers

  • chedieck

Poem

🐰 A token hops through components with glee,
From Button to Dialog to Widget so free,
Token info we fetch, amounts we calculate,
Icons and names appear—oh how they integrate!
Through Chronik we quest, by WebSocket we flow,
Token support blooms where components grow! 🌱

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'feat: prop tokenId' is vague and uses non-descriptive terminology that doesn't clearly convey the scope or purpose of the changeset. Consider using a more descriptive title like 'feat: add tokenId prop for token-based payments' to better summarize the main change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description follows the template structure with all required sections (Related to #44, Description, and Test plan) provided with substantial detail about changes and testing approach.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/token-id

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@lissavxo lissavxo marked this pull request as ready for review December 23, 2025 22:33
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
react/lib/util/chronik.ts (1)

166-184: Add error handling for token amount calculation.

The call to getTokenAmount (line 171) lacks error handling. If token amount calculation fails, it will throw an unhandled error.

🔎 Recommended fix
 const getTransactionFromChronikTransaction = async (transaction: Tx, address: string, tokenId?: string): Promise<Transaction> => {
     const { amount, opReturn } = await getTransactionAmountAndData(transaction, address)
     const parsedOpReturn = resolveOpReturn(opReturn)
     const networkSlug = getAddressPrefix(address)
     const inputAddresses = getSortedInputAddresses(networkSlug, transaction)
-    const tokenAmount = tokenId ? await getTokenAmount(transaction, tokenId, address, networkSlug) : undefined;
+    let tokenAmount: string | undefined;
+    if (tokenId) {
+      try {
+        // Fetch token decimals from token info
+        const tokenInfo = await getTokenInfo(tokenId, address);
+        const decimals = tokenInfo.genesisInfo.decimals;
+        tokenAmount = await getTokenAmount(transaction, tokenId, address, networkSlug, decimals);
+      } catch (err) {
+        console.error('Failed to calculate token amount:', err);
+        tokenAmount = '0';
+      }
+    }
     return {
       hash: transaction.txid,
       amount: tokenId ? tokenAmount! : amount,
       address,
       timestamp: transaction.block !== undefined ? transaction.block.timestamp : transaction.timeFirstSeen,
       confirmed: transaction.block !== undefined,
       opReturn,
       paymentId: parsedOpReturn?.paymentId ?? '',
       message: parsedOpReturn?.message ?? '',
       rawMessage: parsedOpReturn?.rawMessage ?? '',
       inputAddresses,
     }
 }
🧹 Nitpick comments (3)
react/lib/components/Widget/Widget.tsx (2)

501-503: Consider adding validation for the tokenId parameter.

The helper constructs an icon URL without validating the tokenId. While image loading failures are typically handled by the browser, consider adding basic validation (non-empty, expected format) to avoid constructing invalid URLs.

🔎 Optional: Add basic validation
 const getTokenIconUrl = useCallback((tokenId: string): string => {
+  if (!tokenId || typeof tokenId !== 'string' || tokenId.trim() === '') {
+    throw new Error('Invalid tokenId provided');
+  }
   return `https://icons.etokens.cash/128/${tokenId}.png`
 }, [])

845-845: Handle null tokenName in display text.

If tokenName is null (e.g., token info fetch failed or tokenTicker is undefined), the display text will show "null" to users. Consider providing a fallback.

🔎 Proposed fix
-setText(`Send ${amountToDisplay} ${tokenId ? tokenName : cur}`)
+setText(`Send ${amountToDisplay} ${tokenId ? (tokenName || 'token') : cur}`)
-setText(`Send any amount of ${tokenId ? tokenName : thisAddressType}`)
+setText(`Send any amount of ${tokenId ? (tokenName || 'token') : thisAddressType}`)

Also applies to: 849-850

react/lib/util/chronik.ts (1)

258-269: Consider documenting error handling expectations.

The getTokenInfo function lacks error handling and will propagate errors to callers. While this is acceptable, ensure all call sites properly handle potential failures (network errors, invalid tokenId, etc.).

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd9bbf5 and 3220fd0.

📒 Files selected for processing (6)
  • paybutton/src/index.tsx
  • react/lib/components/PayButton/PayButton.tsx
  • react/lib/components/PaymentDialog/PaymentDialog.tsx
  • react/lib/components/Widget/Widget.tsx
  • react/lib/util/chronik.ts
  • react/lib/util/socket.ts
🧰 Additional context used
🧬 Code graph analysis (2)
react/lib/util/socket.ts (2)
react/lib/util/chronik.ts (1)
  • initializeChronikWebsocket (296-323)
react/lib/util/types.ts (1)
  • Transaction (10-21)
react/lib/components/Widget/Widget.tsx (3)
react/lib/util/chronik.ts (1)
  • getTokenInfo (258-269)
react/lib/util/format.ts (1)
  • amount (5-12)
react/lib/util/address.ts (1)
  • isValidCashAddress (4-13)
🔇 Additional comments (10)
react/lib/util/socket.ts (1)

125-125: LGTM! Clean parameter propagation.

The tokenId parameter is properly threaded through the interface and passed to initializeChronikWebsocket, consistent with the token-aware flow.

Also applies to: 149-155

paybutton/src/index.tsx (1)

109-109: LGTM! Properly enables tokenId as a recognized prop.

The addition to allowedProps enables DOM attribute parsing and forwarding of tokenId to PayButton/Widget components.

react/lib/components/PaymentDialog/PaymentDialog.tsx (1)

70-70: LGTM! Clean prop forwarding.

The tokenId is properly threaded from PaymentDialogProps through to WidgetContainer, enabling token support in the dialog flow.

Also applies to: 134-134, 264-264

react/lib/components/PayButton/PayButton.tsx (1)

62-62: LGTM! Proper tokenId integration.

The tokenId is correctly added to PayButtonProps and forwarded to both the Chronik WebSocket setup (for transaction monitoring) and PaymentDialog (for UI rendering).

Also applies to: 97-97, 298-298, 456-456

react/lib/components/Widget/Widget.tsx (5)

1070-1070: LGTM! Proper QR code icon selection for tokens.

The image selection correctly prioritizes token icons when tokenId is present, falling back to the appropriate BCH or XEC logo based on the address format.


854-854: LGTM! Effect dependencies are complete.

The effect dependencies properly include both tokenName and tokenId, which are used within the effect for URL generation and display text.


546-546: LGTM! TokenId properly propagated to WebSocket setup.

The tokenId is correctly passed to the Chronik WebSocket setup for transaction monitoring in standalone widget mode.


55-55: LGTM! Proper setup for token support.

The imports, prop additions, and state initialization properly establish the foundation for token functionality in the Widget component.

Also applies to: 118-118, 179-179, 330-330


976-1021: Verify token payment URL parameter names with Cashtab protocol.

The code confirms that donation functionality is intentionally disabled for token payments (lines 993-1004 do not append donation address/amount parameters when tokenId is present), while donations work normally for XEC/BCH payments.

Please verify that token_decimalized_qty and token_id are the correct parameter names for the Cashtab wallet's token payment protocol.

react/lib/util/chronik.ts (1)

271-277: LGTM! Proper tokenId propagation through WebSocket flow.

The tokenId parameter is consistently threaded through the WebSocket initialization and message parsing functions, enabling token-aware transaction detection.

Also applies to: 287-287, 296-299, 314-315

Comment on lines +595 to +606
useEffect(() => {
;(async (): Promise<void> => {
if (tokenId && tokenId !== null && tokenId !== '' ) {
const tokenInfo = await getTokenInfo(tokenId, to)
const name = tokenInfo.genesisInfo.tokenTicker ?? null
setTokenName(name)

return
}
setLoading(false)
})()
}, [tokenId, to])
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add error handling for token info fetching.

The effect lacks error handling if getTokenInfo fails or returns unexpected data. This could leave the widget in a loading state or display incorrect information.

🔎 Recommended fix with error handling
 useEffect(() => {
   ;(async (): Promise<void> => {
-    if (tokenId && tokenId !== null && tokenId !== '' ) {
+    if (tokenId && tokenId !== '') {
+      try {
         const tokenInfo = await getTokenInfo(tokenId, to)
         const name = tokenInfo.genesisInfo.tokenTicker ?? null
         setTokenName(name)
-        
-        return
+      } catch (err) {
+        console.error('Failed to fetch token info:', err)
+        setTokenName(null)
+        setErrorMsg('Unable to load token information')
+      } finally {
+        setLoading(false)
+      }
+      return
     }
     setLoading(false)
   })()
 }, [tokenId, to])

Note: The redundant tokenId !== null check can be removed since the truthiness check already covers it.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
;(async (): Promise<void> => {
if (tokenId && tokenId !== null && tokenId !== '' ) {
const tokenInfo = await getTokenInfo(tokenId, to)
const name = tokenInfo.genesisInfo.tokenTicker ?? null
setTokenName(name)
return
}
setLoading(false)
})()
}, [tokenId, to])
useEffect(() => {
;(async (): Promise<void> => {
if (tokenId && tokenId !== '') {
try {
const tokenInfo = await getTokenInfo(tokenId, to)
const name = tokenInfo.genesisInfo.tokenTicker ?? null
setTokenName(name)
} catch (err) {
console.error('Failed to fetch token info:', err)
setTokenName(null)
setErrorMsg('Unable to load token information')
} finally {
setLoading(false)
}
return
}
setLoading(false)
})()
}, [tokenId, to])
🤖 Prompt for AI Agents
In react/lib/components/Widget/Widget.tsx around lines 595 to 606, the useEffect
that calls getTokenInfo lacks error handling and contains a redundant tokenId
!== null check; update it to remove the redundant null check, wrap the async
call in try/catch/finally, handle cases where tokenInfo or tokenInfo.genesisInfo
is missing (setTokenName to null or a safe default), log or surface the error as
appropriate, and ensure setLoading(false) is called in finally so the widget
never stays stuck in loading on failure.

Comment on lines +116 to +132
const getTokenAmount = async (transaction: Tx, tokenId: string, address: string, networkSlug: string): Promise<string> => {
let totalTokenOutput = BigInt(0);

for (const output of transaction.outputs) {
if (output.token?.tokenId === tokenId) {
const outputAddress = outputScriptToAddress(networkSlug, output.outputScript)

if(outputAddress === address) {
const atoms = BigInt(output.token.atoms);

totalTokenOutput += atoms / BigInt(100);
}
}
}

return totalTokenOutput.toString();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Hardcoded token decimals assumption.

Line 126 divides by BigInt(100), assuming all tokens have 2 decimal places. However, eCash tokens support 0-9 decimal places, specified in the token's genesis information. This will cause incorrect amount calculations for tokens with different decimal counts.

🔎 Recommended fix
-const getTokenAmount = async (transaction: Tx, tokenId: string, address: string, networkSlug: string): Promise<string> => {
+const getTokenAmount = async (transaction: Tx, tokenId: string, address: string, networkSlug: string, decimals: number): Promise<string> => {
   let totalTokenOutput = BigInt(0);
   
   for (const output of transaction.outputs) {
     if (output.token?.tokenId === tokenId) {
       const outputAddress = outputScriptToAddress(networkSlug, output.outputScript)

       if(outputAddress === address) {
         const atoms = BigInt(output.token.atoms);

-        totalTokenOutput += atoms / BigInt(100);
+        totalTokenOutput += atoms;
       }
     }
   }

-  return totalTokenOutput.toString();
+  // Convert from atoms to decimalized amount
+  const divisor = BigInt(10 ** decimals);
+  return (totalTokenOutput / divisor).toString();
 }

Then update the caller at line 171 to fetch and pass the decimals from token info.

🤖 Prompt for AI Agents
In react/lib/util/chronik.ts around lines 116 to 132, the code incorrectly
divides token atom amounts by BigInt(100) (line 126), hardcoding 2 decimals;
instead, read the token's decimals from the token genesis info and use that to
scale atoms (divide by BigInt(10) ** BigInt(decimals)) before summing. Update
the function signature to accept a decimals parameter (or retrieve decimals from
a passed token info object) and change the caller at line 171 to fetch and pass
the token's decimals from token info so amounts are computed using the correct
decimal precision.

@Klakurka Klakurka requested a review from chedieck December 23, 2025 22:39
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.

2 participants