Skip to content

Commit cc12a99

Browse files
committed
feat: add currency name to currency code conversion utilities
- Add currencyNameToCode() to convert human-readable names to XRPL currency codes - Add currencyCodeToName() to convert XRPL currency codes back to readable names - Add isStandardCurrencyCode() and isHexCurrencyCode() helper functions - Support both 3-character ASCII codes (USD, EUR) and hex-encoded longer names - Include proper validation and error handling for oversized names - Addresses issue #2185
1 parent cf9dd82 commit cc12a99

4 files changed

Lines changed: 368 additions & 0 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { convertStringToHex, convertHexToString } from './stringConversion'
2+
3+
/**
4+
* Convert a currency name to a properly formatted currency code for XRPL.
5+
*
6+
* For currency names of 3 characters or less, returns the name as-is (ASCII).
7+
* For longer names, converts to a 40-character hex-encoded string.
8+
*
9+
* @param currencyName - The human-readable currency name (e.g., "USD", "MyCustomToken")
10+
* @returns The properly formatted currency code for use in XRPL transactions
11+
* @throws Error if the currency name is invalid or too long
12+
*
13+
* @example
14+
* ```typescript
15+
* currencyNameToCode("USD") // Returns: "USD"
16+
* currencyNameToCode("EUR") // Returns: "EUR"
17+
* currencyNameToCode("MyCustomToken") // Returns: "4D79437573746F6D546F6B656E000000000000000000000000"
18+
* ```
19+
*/
20+
export function currencyNameToCode(currencyName: string): string {
21+
if (typeof currencyName !== 'string') {
22+
throw new Error('Currency name must be a string')
23+
}
24+
25+
if (currencyName.length === 0) {
26+
throw new Error('Currency name cannot be empty')
27+
}
28+
29+
if (currencyName === 'XRP') {
30+
throw new Error('XRP cannot be used as a currency code')
31+
}
32+
33+
// For names 3 characters or less, use as-is (standard ASCII codes like USD, EUR)
34+
if (currencyName.length <= 3) {
35+
return currencyName.toUpperCase()
36+
}
37+
38+
// For longer names, convert to hex string
39+
const hexString = convertStringToHex(currencyName).toUpperCase()
40+
41+
// Check if the hex string is too long (more than 40 characters = 20 bytes)
42+
if (hexString.length > 40) {
43+
throw new Error(`Currency name "${currencyName}" is too long. Maximum length is 20 bytes when UTF-8 encoded.`)
44+
}
45+
46+
// Pad to exactly 40 characters (20 bytes) with zeros
47+
return hexString.padEnd(40, '0')
48+
}
49+
50+
/**
51+
* Convert a currency code back to a human-readable currency name.
52+
*
53+
* For 3-character ASCII codes, returns as-is.
54+
* For 40-character hex codes, converts back to the original string.
55+
*
56+
* @param currencyCode - The currency code from XRPL (e.g., "USD" or hex string)
57+
* @returns The human-readable currency name
58+
* @throws Error if the currency code is invalid
59+
*
60+
* @example
61+
* ```typescript
62+
* currencyCodeToName("USD") // Returns: "USD"
63+
* currencyCodeToName("4D79437573746F6D546F6B656E000000000000000000000000") // Returns: "MyCustomToken"
64+
* ```
65+
*/
66+
export function currencyCodeToName(currencyCode: string): string {
67+
if (typeof currencyCode !== 'string') {
68+
throw new Error('Currency code must be a string')
69+
}
70+
71+
if (currencyCode.length === 0) {
72+
throw new Error('Currency code cannot be empty')
73+
}
74+
75+
if (currencyCode === 'XRP') {
76+
return 'XRP'
77+
}
78+
79+
// If it's a short code (3 characters or less), return as-is
80+
if (currencyCode.length <= 3) {
81+
return currencyCode
82+
}
83+
84+
// If it's a 40-character hex string, convert back to string
85+
if (currencyCode.length === 40) {
86+
// Check if it's valid hex
87+
if (!/^[0-9A-Fa-f]+$/u.test(currencyCode)) {
88+
throw new Error('Invalid currency code: not valid hexadecimal')
89+
}
90+
91+
try {
92+
// Remove trailing zeros and convert from hex
93+
const trimmedHex = currencyCode.replace(/0+$/u, '')
94+
if (trimmedHex.length === 0) {
95+
throw new Error('Invalid currency code: empty after removing padding')
96+
}
97+
98+
return convertHexToString(trimmedHex)
99+
} catch (error) {
100+
throw new Error(`Invalid currency code: ${error instanceof Error ? error.message : 'conversion failed'}`)
101+
}
102+
}
103+
104+
throw new Error('Invalid currency code: must be 3 characters or less, or exactly 40 characters hex')
105+
}
106+
107+
/**
108+
* Check if a currency code is in standard 3-character ASCII format.
109+
*
110+
* @param currencyCode - The currency code to check
111+
* @returns True if the code is a standard 3-character ASCII format
112+
*
113+
* @example
114+
* ```typescript
115+
* isStandardCurrencyCode("USD") // Returns: true
116+
* isStandardCurrencyCode("EUR") // Returns: true
117+
* isStandardCurrencyCode("4D79437573746F6D546F6B656E000000000000000000000000") // Returns: false
118+
* ```
119+
*/
120+
export function isStandardCurrencyCode(currencyCode: string): boolean {
121+
return typeof currencyCode === 'string' &&
122+
currencyCode.length <= 3 &&
123+
currencyCode.length > 0 &&
124+
currencyCode !== 'XRP'
125+
}
126+
127+
/**
128+
* Check if a currency code is in hex format (40-character string).
129+
*
130+
* @param currencyCode - The currency code to check
131+
* @returns True if the code is a valid 40-character hex format
132+
*
133+
* @example
134+
* ```typescript
135+
* isHexCurrencyCode("USD") // Returns: false
136+
* isHexCurrencyCode("4D79437573746F6D546F6B656E000000000000000000000000") // Returns: true
137+
* ```
138+
*/
139+
export function isHexCurrencyCode(currencyCode: string): boolean {
140+
return typeof currencyCode === 'string' &&
141+
currencyCode.length === 40 &&
142+
/^[0-9A-Fa-f]+$/u.test(currencyCode)
143+
}

packages/xrpl/src/utils/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ import {
6262
} from './quality'
6363
import signPaymentChannelClaim from './signPaymentChannelClaim'
6464
import { convertHexToString, convertStringToHex } from './stringConversion'
65+
import {
66+
currencyNameToCode,
67+
currencyCodeToName,
68+
isStandardCurrencyCode,
69+
isHexCurrencyCode,
70+
} from './currencyConversion'
6571
import {
6672
rippleTimeToISOTime,
6773
isoTimeToRippleTime,
@@ -247,4 +253,8 @@ export {
247253
getNFTokenID,
248254
parseNFTokenID,
249255
getXChainClaimID,
256+
currencyNameToCode,
257+
currencyCodeToName,
258+
isStandardCurrencyCode,
259+
isHexCurrencyCode,
250260
}

test-currency-conversion.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Simple test for currency conversion functions
2+
const { convertStringToHex, convertHexToString } = require('./packages/xrpl/src/utils/stringConversion');
3+
4+
function currencyNameToCode(currencyName) {
5+
if (typeof currencyName !== 'string') {
6+
throw new Error('Currency name must be a string')
7+
}
8+
9+
if (currencyName.length === 0) {
10+
throw new Error('Currency name cannot be empty')
11+
}
12+
13+
if (currencyName === 'XRP') {
14+
throw new Error('XRP cannot be used as a currency code')
15+
}
16+
17+
// For names 3 characters or less, use as-is (standard ASCII codes like USD, EUR)
18+
if (currencyName.length <= 3) {
19+
return currencyName.toUpperCase()
20+
}
21+
22+
// For longer names, convert to 40-character hex string
23+
const hexString = convertStringToHex(currencyName).toUpperCase()
24+
25+
// Pad to 40 characters (20 bytes) with zeros
26+
return hexString.padEnd(40, '0')
27+
}
28+
29+
function currencyCodeToName(currencyCode) {
30+
if (typeof currencyCode !== 'string') {
31+
throw new Error('Currency code must be a string')
32+
}
33+
34+
if (currencyCode.length === 0) {
35+
throw new Error('Currency code cannot be empty')
36+
}
37+
38+
if (currencyCode === 'XRP') {
39+
return 'XRP'
40+
}
41+
42+
// If it's a short code (3 characters or less), return as-is
43+
if (currencyCode.length <= 3) {
44+
return currencyCode
45+
}
46+
47+
// If it's a 40-character hex string, convert back to string
48+
if (currencyCode.length === 40) {
49+
// Check if it's valid hex
50+
if (!/^[0-9A-Fa-f]+$/u.test(currencyCode)) {
51+
throw new Error('Invalid currency code: not valid hexadecimal')
52+
}
53+
54+
try {
55+
// Remove trailing zeros and convert from hex
56+
const trimmedHex = currencyCode.replace(/0+$/u, '')
57+
if (trimmedHex.length === 0) {
58+
throw new Error('Invalid currency code: empty after removing padding')
59+
}
60+
61+
return convertHexToString(trimmedHex)
62+
} catch (error) {
63+
throw new Error(`Invalid currency code: ${error instanceof Error ? error.message : 'conversion failed'}`)
64+
}
65+
}
66+
67+
throw new Error('Invalid currency code: must be 3 characters or less, or exactly 40 characters hex')
68+
}
69+
70+
// Test cases
71+
console.log('Testing currency conversion functions...\n');
72+
73+
try {
74+
// Test standard 3-char codes
75+
console.log('USD ->', currencyNameToCode('USD'));
76+
console.log('EUR ->', currencyNameToCode('EUR'));
77+
78+
// Test longer names
79+
console.log('MyCustomToken ->', currencyNameToCode('MyCustomToken'));
80+
81+
// Test reverse conversion
82+
const hexCode = currencyNameToCode('MyCustomToken');
83+
console.log('Reverse conversion:', hexCode, '->', currencyCodeToName(hexCode));
84+
85+
// Test standard codes reverse
86+
console.log('USD reverse:', currencyCodeToName('USD'));
87+
88+
console.log('\nAll tests passed!');
89+
90+
} catch (error) {
91+
console.error('Test failed:', error.message);
92+
}

test-currency-simple.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Simple standalone test for currency conversion functions
2+
function stringToHex(str) {
3+
return Buffer.from(str, 'utf8').toString('hex')
4+
}
5+
6+
function hexToString(hex) {
7+
return Buffer.from(hex, 'hex').toString('utf8')
8+
}
9+
10+
function currencyNameToCode(currencyName) {
11+
if (typeof currencyName !== 'string') {
12+
throw new Error('Currency name must be a string')
13+
}
14+
15+
if (currencyName.length === 0) {
16+
throw new Error('Currency name cannot be empty')
17+
}
18+
19+
if (currencyName === 'XRP') {
20+
throw new Error('XRP cannot be used as a currency code')
21+
}
22+
23+
// For names 3 characters or less, use as-is (standard ASCII codes like USD, EUR)
24+
if (currencyName.length <= 3) {
25+
return currencyName.toUpperCase()
26+
}
27+
28+
// For longer names, convert to hex string
29+
const hexString = stringToHex(currencyName).toUpperCase()
30+
31+
// Check if the hex string is too long (more than 40 characters = 20 bytes)
32+
if (hexString.length > 40) {
33+
throw new Error(`Currency name "${currencyName}" is too long. Maximum length is 20 bytes when UTF-8 encoded.`)
34+
}
35+
36+
// Pad to exactly 40 characters (20 bytes) with zeros
37+
return hexString.padEnd(40, '0')
38+
}
39+
40+
function currencyCodeToName(currencyCode) {
41+
if (typeof currencyCode !== 'string') {
42+
throw new Error('Currency code must be a string')
43+
}
44+
45+
if (currencyCode.length === 0) {
46+
throw new Error('Currency code cannot be empty')
47+
}
48+
49+
if (currencyCode === 'XRP') {
50+
return 'XRP'
51+
}
52+
53+
// If it's a short code (3 characters or less), return as-is
54+
if (currencyCode.length <= 3) {
55+
return currencyCode
56+
}
57+
58+
// If it's a 40-character hex string, convert back to string
59+
if (currencyCode.length === 40) {
60+
// Check if it's valid hex
61+
if (!/^[0-9A-Fa-f]+$/u.test(currencyCode)) {
62+
throw new Error('Invalid currency code: not valid hexadecimal')
63+
}
64+
65+
try {
66+
// Remove trailing zeros and convert from hex
67+
const trimmedHex = currencyCode.replace(/0+$/u, '')
68+
if (trimmedHex.length === 0) {
69+
throw new Error('Invalid currency code: empty after removing padding')
70+
}
71+
72+
return hexToString(trimmedHex)
73+
} catch (error) {
74+
throw new Error(`Invalid currency code: ${error instanceof Error ? error.message : 'conversion failed'}`)
75+
}
76+
}
77+
78+
throw new Error('Invalid currency code: must be 3 characters or less, or exactly 40 characters hex')
79+
}
80+
81+
// Test cases
82+
console.log('Testing currency conversion functions...\n');
83+
84+
try {
85+
// Test standard 3-char codes
86+
console.log('USD ->', currencyNameToCode('USD'));
87+
console.log('EUR ->', currencyNameToCode('EUR'));
88+
console.log('BTC ->', currencyNameToCode('BTC'));
89+
90+
// Test longer names
91+
console.log('MyCustomToken ->', currencyNameToCode('MyCustomToken'));
92+
console.log('GOLD ->', currencyNameToCode('GOLD'));
93+
console.log('MediumLengthToken ->', currencyNameToCode('MediumLengthToken'));
94+
95+
// Test reverse conversion
96+
const hexCode1 = currencyNameToCode('MyCustomToken');
97+
console.log('\nReverse conversions:');
98+
console.log(hexCode1, '->', currencyCodeToName(hexCode1));
99+
100+
const hexCode2 = currencyNameToCode('MediumLengthToken');
101+
console.log(hexCode2, '->', currencyCodeToName(hexCode2));
102+
103+
// Test standard codes reverse
104+
console.log('USD reverse:', currencyCodeToName('USD'));
105+
console.log('EUR reverse:', currencyCodeToName('EUR'));
106+
107+
// Test edge cases
108+
console.log('\nEdge cases:');
109+
console.log('XRP reverse:', currencyCodeToName('XRP'));
110+
111+
// Test error case
112+
console.log('\nTesting error case:');
113+
try {
114+
currencyNameToCode('SomeVeryLongTokenNameThatExceedsTwentyBytes');
115+
} catch (error) {
116+
console.log('Expected error for long name:', error.message);
117+
}
118+
119+
console.log('\n✅ All tests passed!');
120+
121+
} catch (error) {
122+
console.error('❌ Test failed:', error.message);
123+
}

0 commit comments

Comments
 (0)