-
Notifications
You must be signed in to change notification settings - Fork 568
feat: add currency name to currency code conversion utilities #3223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,143 @@ | ||||||||||||||||||||||||||||||||||||||
| import { convertStringToHex, convertHexToString } from './stringConversion' | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Convert a currency name to a properly formatted currency code for XRPL. | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * For currency names of 3 characters or less, returns the name as-is (ASCII). | ||||||||||||||||||||||||||||||||||||||
| * For longer names, converts to a 40-character hex-encoded string. | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * @param currencyName - The human-readable currency name (e.g., "USD", "MyCustomToken") | ||||||||||||||||||||||||||||||||||||||
| * @returns The properly formatted currency code for use in XRPL transactions | ||||||||||||||||||||||||||||||||||||||
| * @throws Error if the currency name is invalid or too long | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * @example | ||||||||||||||||||||||||||||||||||||||
| * ```typescript | ||||||||||||||||||||||||||||||||||||||
| * currencyNameToCode("USD") // Returns: "USD" | ||||||||||||||||||||||||||||||||||||||
| * currencyNameToCode("EUR") // Returns: "EUR" | ||||||||||||||||||||||||||||||||||||||
| * currencyNameToCode("MyCustomToken") // Returns: "4D79437573746F6D546F6B656E000000000000000000000000" | ||||||||||||||||||||||||||||||||||||||
| * ``` | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| export function currencyNameToCode(currencyName: string): string { | ||||||||||||||||||||||||||||||||||||||
| if (typeof currencyName !== 'string') { | ||||||||||||||||||||||||||||||||||||||
| throw new Error('Currency name must be a string') | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (currencyName.length === 0) { | ||||||||||||||||||||||||||||||||||||||
| throw new Error('Currency name cannot be empty') | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (currencyName === 'XRP') { | ||||||||||||||||||||||||||||||||||||||
| throw new Error('XRP cannot be used as a currency code') | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // For names 3 characters or less, use as-is (standard ASCII codes like USD, EUR) | ||||||||||||||||||||||||||||||||||||||
| if (currencyName.length <= 3) { | ||||||||||||||||||||||||||||||||||||||
| return currencyName.toUpperCase() | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // For longer names, convert to hex string | ||||||||||||||||||||||||||||||||||||||
| const hexString = convertStringToHex(currencyName).toUpperCase() | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Check if the hex string is too long (more than 40 characters = 20 bytes) | ||||||||||||||||||||||||||||||||||||||
| if (hexString.length > 40) { | ||||||||||||||||||||||||||||||||||||||
| throw new Error(`Currency name "${currencyName}" is too long. Maximum length is 20 bytes when UTF-8 encoded.`) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Pad to exactly 40 characters (20 bytes) with zeros | ||||||||||||||||||||||||||||||||||||||
| return hexString.padEnd(40, '0') | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Convert a currency code back to a human-readable currency name. | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * For 3-character ASCII codes, returns as-is. | ||||||||||||||||||||||||||||||||||||||
| * For 40-character hex codes, converts back to the original string. | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * @param currencyCode - The currency code from XRPL (e.g., "USD" or hex string) | ||||||||||||||||||||||||||||||||||||||
| * @returns The human-readable currency name | ||||||||||||||||||||||||||||||||||||||
| * @throws Error if the currency code is invalid | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * @example | ||||||||||||||||||||||||||||||||||||||
| * ```typescript | ||||||||||||||||||||||||||||||||||||||
| * currencyCodeToName("USD") // Returns: "USD" | ||||||||||||||||||||||||||||||||||||||
| * currencyCodeToName("4D79437573746F6D546F6B656E000000000000000000000000") // Returns: "MyCustomToken" | ||||||||||||||||||||||||||||||||||||||
| * ``` | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| export function currencyCodeToName(currencyCode: string): string { | ||||||||||||||||||||||||||||||||||||||
| if (typeof currencyCode !== 'string') { | ||||||||||||||||||||||||||||||||||||||
| throw new Error('Currency code must be a string') | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (currencyCode.length === 0) { | ||||||||||||||||||||||||||||||||||||||
| throw new Error('Currency code cannot be empty') | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (currencyCode === 'XRP') { | ||||||||||||||||||||||||||||||||||||||
| return 'XRP' | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // If it's a short code (3 characters or less), return as-is | ||||||||||||||||||||||||||||||||||||||
| if (currencyCode.length <= 3) { | ||||||||||||||||||||||||||||||||||||||
| return currencyCode | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // If it's a 40-character hex string, convert back to string | ||||||||||||||||||||||||||||||||||||||
| if (currencyCode.length === 40) { | ||||||||||||||||||||||||||||||||||||||
| // Check if it's valid hex | ||||||||||||||||||||||||||||||||||||||
| if (!/^[0-9A-Fa-f]+$/u.test(currencyCode)) { | ||||||||||||||||||||||||||||||||||||||
| throw new Error('Invalid currency code: not valid hexadecimal') | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| // Remove trailing zeros and convert from hex | ||||||||||||||||||||||||||||||||||||||
| const trimmedHex = currencyCode.replace(/0+$/u, '') | ||||||||||||||||||||||||||||||||||||||
| if (trimmedHex.length === 0) { | ||||||||||||||||||||||||||||||||||||||
| throw new Error('Invalid currency code: empty after removing padding') | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return convertHexToString(trimmedHex) | ||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+91
to
+99
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trim
🐛 Proposed fix- const trimmedHex = currencyCode.replace(/0+$/u, '')
+ const trimmedHex = currencyCode.replace(/(?:00)+$/u, '')📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| throw new Error(`Invalid currency code: ${error instanceof Error ? error.message : 'conversion failed'}`) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| throw new Error('Invalid currency code: must be 3 characters or less, or exactly 40 characters hex') | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Check if a currency code is in standard 3-character ASCII format. | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * @param currencyCode - The currency code to check | ||||||||||||||||||||||||||||||||||||||
| * @returns True if the code is a standard 3-character ASCII format | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * @example | ||||||||||||||||||||||||||||||||||||||
| * ```typescript | ||||||||||||||||||||||||||||||||||||||
| * isStandardCurrencyCode("USD") // Returns: true | ||||||||||||||||||||||||||||||||||||||
| * isStandardCurrencyCode("EUR") // Returns: true | ||||||||||||||||||||||||||||||||||||||
| * isStandardCurrencyCode("4D79437573746F6D546F6B656E000000000000000000000000") // Returns: false | ||||||||||||||||||||||||||||||||||||||
| * ``` | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| export function isStandardCurrencyCode(currencyCode: string): boolean { | ||||||||||||||||||||||||||||||||||||||
| return typeof currencyCode === 'string' && | ||||||||||||||||||||||||||||||||||||||
| currencyCode.length <= 3 && | ||||||||||||||||||||||||||||||||||||||
| currencyCode.length > 0 && | ||||||||||||||||||||||||||||||||||||||
| currencyCode !== 'XRP' | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Check if a currency code is in hex format (40-character string). | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * @param currencyCode - The currency code to check | ||||||||||||||||||||||||||||||||||||||
| * @returns True if the code is a valid 40-character hex format | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * @example | ||||||||||||||||||||||||||||||||||||||
| * ```typescript | ||||||||||||||||||||||||||||||||||||||
| * isHexCurrencyCode("USD") // Returns: false | ||||||||||||||||||||||||||||||||||||||
| * isHexCurrencyCode("4D79437573746F6D546F6B656E000000000000000000000000") // Returns: true | ||||||||||||||||||||||||||||||||||||||
| * ``` | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| export function isHexCurrencyCode(currencyCode: string): boolean { | ||||||||||||||||||||||||||||||||||||||
| return typeof currencyCode === 'string' && | ||||||||||||||||||||||||||||||||||||||
| currencyCode.length === 40 && | ||||||||||||||||||||||||||||||||||||||
| /^[0-9A-Fa-f]+$/u.test(currencyCode) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| // Simple test for currency conversion functions | ||
| const { convertStringToHex, convertHexToString } = require('./packages/xrpl/src/utils/stringConversion'); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| function currencyNameToCode(currencyName) { | ||
| if (typeof currencyName !== 'string') { | ||
| throw new Error('Currency name must be a string') | ||
| } | ||
|
|
||
| if (currencyName.length === 0) { | ||
| throw new Error('Currency name cannot be empty') | ||
| } | ||
|
|
||
| if (currencyName === 'XRP') { | ||
| throw new Error('XRP cannot be used as a currency code') | ||
| } | ||
|
|
||
| // For names 3 characters or less, use as-is (standard ASCII codes like USD, EUR) | ||
| if (currencyName.length <= 3) { | ||
| return currencyName.toUpperCase() | ||
| } | ||
|
|
||
| // For longer names, convert to 40-character hex string | ||
| const hexString = convertStringToHex(currencyName).toUpperCase() | ||
|
|
||
| // Pad to 40 characters (20 bytes) with zeros | ||
| return hexString.padEnd(40, '0') | ||
| } | ||
|
|
||
| function currencyCodeToName(currencyCode) { | ||
| if (typeof currencyCode !== 'string') { | ||
| throw new Error('Currency code must be a string') | ||
| } | ||
|
|
||
| if (currencyCode.length === 0) { | ||
| throw new Error('Currency code cannot be empty') | ||
| } | ||
|
|
||
| if (currencyCode === 'XRP') { | ||
| return 'XRP' | ||
| } | ||
|
|
||
| // If it's a short code (3 characters or less), return as-is | ||
| if (currencyCode.length <= 3) { | ||
| return currencyCode | ||
| } | ||
|
|
||
| // If it's a 40-character hex string, convert back to string | ||
| if (currencyCode.length === 40) { | ||
| // Check if it's valid hex | ||
| if (!/^[0-9A-Fa-f]+$/u.test(currencyCode)) { | ||
| throw new Error('Invalid currency code: not valid hexadecimal') | ||
| } | ||
|
|
||
| try { | ||
| // Remove trailing zeros and convert from hex | ||
| const trimmedHex = currencyCode.replace(/0+$/u, '') | ||
| if (trimmedHex.length === 0) { | ||
| throw new Error('Invalid currency code: empty after removing padding') | ||
| } | ||
|
|
||
| return convertHexToString(trimmedHex) | ||
| } catch (error) { | ||
| throw new Error(`Invalid currency code: ${error instanceof Error ? error.message : 'conversion failed'}`) | ||
| } | ||
| } | ||
|
|
||
| throw new Error('Invalid currency code: must be 3 characters or less, or exactly 40 characters hex') | ||
| } | ||
|
Comment on lines
+4
to
+68
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This still isn’t validating the shipped utility. The file redefines the conversion logic locally, only prints outputs, and already diverges from Also applies to: 70-92 🤖 Prompt for AI Agents |
||
|
|
||
| // Test cases | ||
| console.log('Testing currency conversion functions...\n'); | ||
|
|
||
| try { | ||
| // Test standard 3-char codes | ||
| console.log('USD ->', currencyNameToCode('USD')); | ||
| console.log('EUR ->', currencyNameToCode('EUR')); | ||
|
|
||
| // Test longer names | ||
| console.log('MyCustomToken ->', currencyNameToCode('MyCustomToken')); | ||
|
|
||
| // Test reverse conversion | ||
| const hexCode = currencyNameToCode('MyCustomToken'); | ||
| console.log('Reverse conversion:', hexCode, '->', currencyCodeToName(hexCode)); | ||
|
|
||
| // Test standard codes reverse | ||
| console.log('USD reverse:', currencyCodeToName('USD')); | ||
|
|
||
| console.log('\nAll tests passed!'); | ||
|
|
||
| } catch (error) { | ||
| console.error('Test failed:', error.message); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| // Simple standalone test for currency conversion functions | ||
| function stringToHex(str) { | ||
| return Buffer.from(str, 'utf8').toString('hex') | ||
| } | ||
|
|
||
| function hexToString(hex) { | ||
| return Buffer.from(hex, 'hex').toString('utf8') | ||
| } | ||
|
|
||
| function currencyNameToCode(currencyName) { | ||
| if (typeof currencyName !== 'string') { | ||
| throw new Error('Currency name must be a string') | ||
| } | ||
|
|
||
| if (currencyName.length === 0) { | ||
| throw new Error('Currency name cannot be empty') | ||
| } | ||
|
|
||
| if (currencyName === 'XRP') { | ||
| throw new Error('XRP cannot be used as a currency code') | ||
| } | ||
|
|
||
| // For names 3 characters or less, use as-is (standard ASCII codes like USD, EUR) | ||
| if (currencyName.length <= 3) { | ||
| return currencyName.toUpperCase() | ||
| } | ||
|
|
||
| // For longer names, convert to hex string | ||
| const hexString = stringToHex(currencyName).toUpperCase() | ||
|
|
||
| // Check if the hex string is too long (more than 40 characters = 20 bytes) | ||
| if (hexString.length > 40) { | ||
| throw new Error(`Currency name "${currencyName}" is too long. Maximum length is 20 bytes when UTF-8 encoded.`) | ||
| } | ||
|
|
||
| // Pad to exactly 40 characters (20 bytes) with zeros | ||
| return hexString.padEnd(40, '0') | ||
| } | ||
|
|
||
| function currencyCodeToName(currencyCode) { | ||
| if (typeof currencyCode !== 'string') { | ||
| throw new Error('Currency code must be a string') | ||
| } | ||
|
|
||
| if (currencyCode.length === 0) { | ||
| throw new Error('Currency code cannot be empty') | ||
| } | ||
|
|
||
| if (currencyCode === 'XRP') { | ||
| return 'XRP' | ||
| } | ||
|
|
||
| // If it's a short code (3 characters or less), return as-is | ||
| if (currencyCode.length <= 3) { | ||
| return currencyCode | ||
| } | ||
|
|
||
| // If it's a 40-character hex string, convert back to string | ||
| if (currencyCode.length === 40) { | ||
| // Check if it's valid hex | ||
| if (!/^[0-9A-Fa-f]+$/u.test(currencyCode)) { | ||
| throw new Error('Invalid currency code: not valid hexadecimal') | ||
| } | ||
|
|
||
| try { | ||
| // Remove trailing zeros and convert from hex | ||
| const trimmedHex = currencyCode.replace(/0+$/u, '') | ||
| if (trimmedHex.length === 0) { | ||
| throw new Error('Invalid currency code: empty after removing padding') | ||
| } | ||
|
|
||
| return hexToString(trimmedHex) | ||
| } catch (error) { | ||
| throw new Error(`Invalid currency code: ${error instanceof Error ? error.message : 'conversion failed'}`) | ||
| } | ||
| } | ||
|
|
||
| throw new Error('Invalid currency code: must be 3 characters or less, or exactly 40 characters hex') | ||
| } | ||
|
|
||
| // Test cases | ||
| console.log('Testing currency conversion functions...\n'); | ||
|
|
||
| try { | ||
| // Test standard 3-char codes | ||
| console.log('USD ->', currencyNameToCode('USD')); | ||
| console.log('EUR ->', currencyNameToCode('EUR')); | ||
| console.log('BTC ->', currencyNameToCode('BTC')); | ||
|
|
||
| // Test longer names | ||
| console.log('MyCustomToken ->', currencyNameToCode('MyCustomToken')); | ||
| console.log('GOLD ->', currencyNameToCode('GOLD')); | ||
| console.log('MediumLengthToken ->', currencyNameToCode('MediumLengthToken')); | ||
|
|
||
| // Test reverse conversion | ||
| const hexCode1 = currencyNameToCode('MyCustomToken'); | ||
| console.log('\nReverse conversions:'); | ||
| console.log(hexCode1, '->', currencyCodeToName(hexCode1)); | ||
|
|
||
| const hexCode2 = currencyNameToCode('MediumLengthToken'); | ||
| console.log(hexCode2, '->', currencyCodeToName(hexCode2)); | ||
|
|
||
| // Test standard codes reverse | ||
| console.log('USD reverse:', currencyCodeToName('USD')); | ||
| console.log('EUR reverse:', currencyCodeToName('EUR')); | ||
|
|
||
| // Test edge cases | ||
| console.log('\nEdge cases:'); | ||
| console.log('XRP reverse:', currencyCodeToName('XRP')); | ||
|
|
||
| // Test error case | ||
| console.log('\nTesting error case:'); | ||
| try { | ||
| currencyNameToCode('SomeVeryLongTokenNameThatExceedsTwentyBytes'); | ||
| } catch (error) { | ||
| console.log('Expected error for long name:', error.message); | ||
| } | ||
|
|
||
| console.log('\n✅ All tests passed!'); | ||
|
|
||
| } catch (error) { | ||
| console.error('❌ Test failed:', error.message); | ||
| } | ||
|
Comment on lines
+1
to
+123
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a demo script, not an automated test. The file reimplements the conversion helpers locally and only logs outputs, so regressions in 🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate short codes before taking the fast path.
Right now any 1–3 code-unit string is accepted as a “standard” code. That lets
currencyNameToCode('xrp')return reservedXRP, and helpers likeisStandardCurrencyCode('€')report true even though the API/docs describe these as ASCII codes. Normalize first, then gate the short-code path with a shared ASCII check.Also applies to: 79-81, 120-125
🤖 Prompt for AI Agents