@transia/ripple-binary-codec 2.5.5-alpha.0 → 2.5.5-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/enums/definitions.json +1313 -959
  2. package/dist/enums/src/enums/definitions.json +1318 -959
  3. package/dist/index.js +0 -3
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/enums/definitions.json +1313 -959
  6. package/dist/src/index.js +0 -3
  7. package/dist/src/index.js.map +1 -1
  8. package/dist/src/types/hash-128.js +1 -1
  9. package/dist/src/types/hash-128.js.map +1 -1
  10. package/dist/src/types/hash-160.js +1 -1
  11. package/dist/src/types/hash-160.js.map +1 -1
  12. package/dist/src/types/hash-192.js +1 -1
  13. package/dist/src/types/hash-192.js.map +1 -1
  14. package/dist/src/types/index.d.ts +2 -1
  15. package/dist/src/types/index.js +4 -1
  16. package/dist/src/types/index.js.map +1 -1
  17. package/dist/src/types/int-32.d.ts +33 -0
  18. package/dist/src/types/int-32.js +64 -0
  19. package/dist/src/types/int-32.js.map +1 -0
  20. package/dist/src/types/int.d.ts +38 -0
  21. package/dist/src/types/int.js +57 -0
  22. package/dist/src/types/int.js.map +1 -0
  23. package/dist/src/types/issue.d.ts +10 -13
  24. package/dist/src/types/issue.js +42 -29
  25. package/dist/src/types/issue.js.map +1 -1
  26. package/dist/src/types/st-number.js +88 -33
  27. package/dist/src/types/st-number.js.map +1 -1
  28. package/dist/src/types/st-object.d.ts +3 -3
  29. package/dist/src/types/st-object.js +12 -12
  30. package/dist/src/types/st-object.js.map +1 -1
  31. package/dist/src/types/uint-16.js +2 -2
  32. package/dist/src/types/uint-16.js.map +1 -1
  33. package/dist/src/types/uint-32.js +1 -1
  34. package/dist/src/types/uint-32.js.map +1 -1
  35. package/dist/src/types/uint-64.js +1 -1
  36. package/dist/src/types/uint-64.js.map +1 -1
  37. package/dist/src/types/uint-8.js +1 -1
  38. package/dist/src/types/uint-8.js.map +1 -1
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/dist/types/hash-128.js +1 -1
  41. package/dist/types/hash-128.js.map +1 -1
  42. package/dist/types/hash-160.js +1 -1
  43. package/dist/types/hash-160.js.map +1 -1
  44. package/dist/types/hash-192.js +1 -1
  45. package/dist/types/hash-192.js.map +1 -1
  46. package/dist/types/index.d.ts +2 -1
  47. package/dist/types/index.js +4 -1
  48. package/dist/types/index.js.map +1 -1
  49. package/dist/types/int-32.d.ts +33 -0
  50. package/dist/types/int-32.js +64 -0
  51. package/dist/types/int-32.js.map +1 -0
  52. package/dist/types/int.d.ts +38 -0
  53. package/dist/types/int.js +57 -0
  54. package/dist/types/int.js.map +1 -0
  55. package/dist/types/issue.d.ts +10 -13
  56. package/dist/types/issue.js +42 -29
  57. package/dist/types/issue.js.map +1 -1
  58. package/dist/types/st-number.js +88 -33
  59. package/dist/types/st-number.js.map +1 -1
  60. package/dist/types/st-object.d.ts +3 -3
  61. package/dist/types/st-object.js +12 -12
  62. package/dist/types/st-object.js.map +1 -1
  63. package/dist/types/uint-16.js +2 -2
  64. package/dist/types/uint-16.js.map +1 -1
  65. package/dist/types/uint-32.js +1 -1
  66. package/dist/types/uint-32.js.map +1 -1
  67. package/dist/types/uint-64.js +1 -1
  68. package/dist/types/uint-64.js.map +1 -1
  69. package/dist/types/uint-8.js +1 -1
  70. package/dist/types/uint-8.js.map +1 -1
  71. package/package.json +8 -5
  72. package/src/enums/definitions.json +1318 -959
  73. package/src/index.ts +1 -3
  74. package/src/types/hash-128.ts +1 -1
  75. package/src/types/hash-160.ts +1 -1
  76. package/src/types/hash-192.ts +1 -1
  77. package/src/types/index.ts +3 -0
  78. package/src/types/int-32.ts +72 -0
  79. package/src/types/int.ts +75 -0
  80. package/src/types/issue.ts +90 -57
  81. package/src/types/st-number.ts +107 -31
  82. package/src/types/st-object.ts +77 -73
  83. package/src/types/uint-16.ts +2 -2
  84. package/src/types/uint-32.ts +1 -1
  85. package/src/types/uint-64.ts +1 -1
  86. package/src/types/uint-8.ts +1 -1
package/src/index.ts CHANGED
@@ -100,9 +100,7 @@ function encodeForMultisigning(
100
100
  if (typeof json !== 'object') {
101
101
  throw new Error()
102
102
  }
103
- if (json['SigningPubKey'] !== '') {
104
- throw new Error()
105
- }
103
+
106
104
  const definitionsOpt = definitions ? { definitions } : undefined
107
105
  return bytesToHex(
108
106
  multiSigningData(json as JsonObject, signer, definitionsOpt),
@@ -10,7 +10,7 @@ class Hash128 extends Hash {
10
10
  static readonly ZERO_128: Hash128 = new Hash128(new Uint8Array(Hash128.width))
11
11
 
12
12
  constructor(bytes: Uint8Array) {
13
- if (bytes && bytes.byteLength === 0) {
13
+ if (bytes?.byteLength === 0) {
14
14
  bytes = Hash128.ZERO_128.bytes
15
15
  }
16
16
 
@@ -9,7 +9,7 @@ class Hash160 extends Hash {
9
9
  static readonly ZERO_160: Hash160 = new Hash160(new Uint8Array(Hash160.width))
10
10
 
11
11
  constructor(bytes?: Uint8Array) {
12
- if (bytes && bytes.byteLength === 0) {
12
+ if (bytes?.byteLength === 0) {
13
13
  bytes = Hash160.ZERO_160.bytes
14
14
  }
15
15
 
@@ -9,7 +9,7 @@ class Hash192 extends Hash {
9
9
  static readonly ZERO_192: Hash192 = new Hash192(new Uint8Array(Hash192.width))
10
10
 
11
11
  constructor(bytes?: Uint8Array) {
12
- if (bytes && bytes.byteLength === 0) {
12
+ if (bytes?.byteLength === 0) {
13
13
  bytes = Hash192.ZERO_192.bytes
14
14
  }
15
15
 
@@ -8,6 +8,7 @@ import { Hash128 } from './hash-128'
8
8
  import { Hash160 } from './hash-160'
9
9
  import { Hash192 } from './hash-192'
10
10
  import { Hash256 } from './hash-256'
11
+ import { Int32 } from './int-32'
11
12
  import { Issue } from './issue'
12
13
  import { STNumber } from './st-number'
13
14
  import { PathSet } from './path-set'
@@ -33,6 +34,7 @@ const coreTypes: Record<string, typeof SerializedType> = {
33
34
  Hash160,
34
35
  Hash192,
35
36
  Hash256,
37
+ Int32,
36
38
  Issue,
37
39
  Number: STNumber,
38
40
  PathSet,
@@ -63,6 +65,7 @@ export {
63
65
  Hash160,
64
66
  Hash192,
65
67
  Hash256,
68
+ Int32,
66
69
  PathSet,
67
70
  STArray,
68
71
  STObject,
@@ -0,0 +1,72 @@
1
+ import { Int } from './int'
2
+ import { BinaryParser } from '../serdes/binary-parser'
3
+ import { readInt32BE, writeInt32BE } from '../utils'
4
+
5
+ /**
6
+ * Derived Int class for serializing/deserializing signed 32-bit integers.
7
+ */
8
+ class Int32 extends Int {
9
+ protected static readonly width: number = 32 / 8 // 4 bytes
10
+ static readonly defaultInt32: Int32 = new Int32(new Uint8Array(Int32.width))
11
+
12
+ // Signed 32-bit integer range
13
+ static readonly MIN_VALUE: number = -2147483648 // -2^31
14
+ static readonly MAX_VALUE: number = 2147483647 // 2^31 - 1
15
+
16
+ constructor(bytes: Uint8Array) {
17
+ super(bytes ?? Int32.defaultInt32.bytes)
18
+ }
19
+
20
+ /**
21
+ * Construct an Int32 from a BinaryParser
22
+ *
23
+ * @param parser BinaryParser to read Int32 from
24
+ * @returns An Int32 object
25
+ */
26
+ static fromParser(parser: BinaryParser): Int {
27
+ return new Int32(parser.read(Int32.width))
28
+ }
29
+
30
+ /**
31
+ * Construct an Int32 object from a number or string
32
+ *
33
+ * @param val Int32 object, number, or string
34
+ * @returns An Int32 object
35
+ */
36
+ static from<T extends Int32 | number | string>(val: T): Int32 {
37
+ if (val instanceof Int32) {
38
+ return val
39
+ }
40
+
41
+ const buf = new Uint8Array(Int32.width)
42
+
43
+ if (typeof val === 'string') {
44
+ const num = Number(val)
45
+ if (!Number.isFinite(num) || !Number.isInteger(num)) {
46
+ throw new Error(`Cannot construct Int32 from string: ${val}`)
47
+ }
48
+ Int32.checkIntRange('Int32', num, Int32.MIN_VALUE, Int32.MAX_VALUE)
49
+ writeInt32BE(buf, num, 0)
50
+ return new Int32(buf)
51
+ }
52
+
53
+ if (typeof val === 'number' && Number.isInteger(val)) {
54
+ Int32.checkIntRange('Int32', val, Int32.MIN_VALUE, Int32.MAX_VALUE)
55
+ writeInt32BE(buf, val, 0)
56
+ return new Int32(buf)
57
+ }
58
+
59
+ throw new Error('Cannot construct Int32 from given value')
60
+ }
61
+
62
+ /**
63
+ * Get the value of the Int32 object
64
+ *
65
+ * @returns the signed 32-bit integer represented by this.bytes
66
+ */
67
+ valueOf(): number {
68
+ return readInt32BE(this.bytes, 0)
69
+ }
70
+ }
71
+
72
+ export { Int32 }
@@ -0,0 +1,75 @@
1
+ import { Comparable } from './serialized-type'
2
+
3
+ /**
4
+ * Compare numbers and bigInts n1 and n2
5
+ *
6
+ * @param n1 First object to compare
7
+ * @param n2 Second object to compare
8
+ * @returns -1, 0, or 1, depending on how the two objects compare
9
+ */
10
+ function compare(n1: number | bigint, n2: number | bigint): number {
11
+ return n1 < n2 ? -1 : n1 == n2 ? 0 : 1
12
+ }
13
+
14
+ /**
15
+ * Base class for serializing and deserializing signed integers.
16
+ */
17
+ abstract class Int extends Comparable<Int | number> {
18
+ protected static width: number
19
+
20
+ constructor(bytes: Uint8Array) {
21
+ super(bytes)
22
+ }
23
+
24
+ /**
25
+ * Overload of compareTo for Comparable
26
+ *
27
+ * @param other other Int to compare this to
28
+ * @returns -1, 0, or 1 depending on how the objects relate to each other
29
+ */
30
+ compareTo(other: Int | number): number {
31
+ return compare(this.valueOf(), other.valueOf())
32
+ }
33
+
34
+ /**
35
+ * Convert an Int object to JSON
36
+ *
37
+ * @returns number or string represented by this.bytes
38
+ */
39
+ toJSON(): number | string {
40
+ const val = this.valueOf()
41
+ return typeof val === 'number' ? val : val.toString()
42
+ }
43
+
44
+ /**
45
+ * Get the value of the Int represented by this.bytes
46
+ *
47
+ * @returns the value
48
+ */
49
+ abstract valueOf(): number | bigint
50
+
51
+ /**
52
+ * Validate that a number is within the specified signed integer range
53
+ *
54
+ * @param typeName The name of the type (for error messages)
55
+ * @param val The number to validate
56
+ * @param min The minimum allowed value
57
+ * @param max The maximum allowed value
58
+ * @throws Error if the value is out of range
59
+ */
60
+ // eslint-disable-next-line max-params -- for error clarity in browsers
61
+ static checkIntRange(
62
+ typeName: string,
63
+ val: number | bigint,
64
+ min: number | bigint,
65
+ max: number | bigint,
66
+ ): void {
67
+ if (val < min || val > max) {
68
+ throw new Error(
69
+ `Invalid ${typeName}: ${val} must be >= ${min} and <= ${max}`,
70
+ )
71
+ }
72
+ }
73
+ }
74
+
75
+ export { Int }
@@ -1,140 +1,173 @@
1
- import { concat } from '@transia/isomorphic/utils'
2
- import { BinaryParser } from '../serdes/binary-parser'
3
-
4
- import { AccountID } from './account-id'
5
- import { Currency } from './currency'
6
- import { JsonObject, SerializedType, SerializedTypeID } from './serialized-type'
7
- import { Hash192 } from './hash-192'
1
+ import { bytesToHex, concat } from "@transia/isomorphic/utils";
2
+ import { BinaryParser } from "../serdes/binary-parser";
3
+
4
+ import { AccountID } from "./account-id";
5
+ import { Currency } from "./currency";
6
+ import {
7
+ JsonObject,
8
+ SerializedType,
9
+ SerializedTypeID,
10
+ } from "./serialized-type";
11
+ import { Hash192 } from "./hash-192";
12
+ import { readUInt32BE, writeUInt32BE } from "../utils";
8
13
 
9
14
  interface XRPIssue extends JsonObject {
10
- currency: string
15
+ currency: string;
11
16
  }
12
17
 
13
18
  interface IOUIssue extends JsonObject {
14
- currency: string
15
- issuer: string
19
+ currency: string;
20
+ issuer: string;
16
21
  }
17
22
  interface MPTIssue extends JsonObject {
18
- mpt_issuance_id: string
23
+ mpt_issuance_id: string;
19
24
  }
20
25
  /**
21
26
  * Interface for JSON objects that represent issues
22
27
  */
23
- type IssueObject = XRPIssue | IOUIssue | MPTIssue
28
+ type IssueObject = XRPIssue | IOUIssue | MPTIssue;
24
29
 
25
30
  /**
26
31
  * Type guard for Issue Object
27
32
  */
28
33
  function isIssueObject(arg): arg is IssueObject {
29
- const keys = Object.keys(arg).sort()
30
- const isXRP = keys.length === 1 && keys[0] === 'currency'
34
+ const keys = Object.keys(arg).sort();
35
+ const isXRP = keys.length === 1 && keys[0] === "currency";
31
36
  const isIOU =
32
- keys.length === 2 && keys[0] === 'currency' && keys[1] === 'issuer'
33
- const isMPT = keys.length === 1 && keys[0] === 'mpt_issuance_id'
37
+ keys.length === 2 && keys[0] === "currency" && keys[1] === "issuer";
38
+ const isMPT = keys.length === 1 && keys[0] === "mpt_issuance_id";
34
39
 
35
- return isXRP || isIOU || isMPT
40
+ return isXRP || isIOU || isMPT;
36
41
  }
37
42
 
43
+ const MPT_WIDTH = 44;
44
+ const NO_ACCOUNT = AccountID.from("0000000000000000000000000000000000000001");
45
+
38
46
  /**
39
- * Class for serializing/Deserializing Amounts
47
+ * Class for serializing/Deserializing Issue
40
48
  */
41
49
  class Issue extends SerializedType {
42
- static readonly ZERO_ISSUED_CURRENCY: Issue = new Issue(new Uint8Array(20))
50
+ static readonly XRP_ISSUE: Issue = new Issue(new Uint8Array(20));
43
51
 
44
52
  constructor(bytes: Uint8Array) {
45
- super(bytes ?? Issue.ZERO_ISSUED_CURRENCY.bytes)
53
+ super(bytes ?? Issue.XRP_ISSUE.bytes);
46
54
  }
47
55
 
48
56
  /**
49
- * Construct an amount from an IOU or string amount
57
+ * Construct Issue from XRPIssue, IOUIssue or MPTIssue
50
58
  *
51
- * @param value An Amount, object representing an IOU, MPTAmount, or a string
52
- * representing an integer amount
59
+ * @param value An object representing an XRPIssue, IOUIssue or MPTIssue
53
60
  * @returns An Issue object
54
61
  */
55
62
  static from<T extends Issue | IssueObject>(value: T): Issue {
56
63
  if (value instanceof Issue) {
57
- return value
64
+ return value;
58
65
  }
59
66
 
60
67
  if (isIssueObject(value)) {
61
68
  if (value.currency) {
62
- const currency = Currency.from(value.currency.toString()).toBytes()
69
+ const currency = Currency.from(value.currency.toString()).toBytes();
63
70
 
64
71
  //IOU case
65
72
  if (value.issuer) {
66
- const issuer = AccountID.from(value.issuer.toString()).toBytes()
67
- return new Issue(concat([currency, issuer]))
73
+ const issuer = AccountID.from(value.issuer.toString()).toBytes();
74
+ return new Issue(concat([currency, issuer]));
68
75
  }
69
76
 
70
77
  //XRP case
71
- return new Issue(currency)
78
+ return new Issue(currency);
72
79
  }
73
80
 
74
81
  // MPT case
75
82
  if (value.mpt_issuance_id) {
76
83
  const mptIssuanceIdBytes = Hash192.from(
77
84
  value.mpt_issuance_id.toString(),
78
- ).toBytes()
79
- return new Issue(mptIssuanceIdBytes)
85
+ ).toBytes();
86
+ const issuerAccount = mptIssuanceIdBytes.slice(4);
87
+ const sequence = Number(
88
+ readUInt32BE(mptIssuanceIdBytes.slice(0, 4), 0),
89
+ ); // sequence is in Big-endian format in mpt_issuance_id
90
+
91
+ // Convert to Little-endian
92
+ const sequenceBuffer = new Uint8Array(4);
93
+ new DataView(sequenceBuffer.buffer).setUint32(0, sequence, true);
94
+
95
+ return new Issue(
96
+ concat([issuerAccount, NO_ACCOUNT.toBytes(), sequenceBuffer]),
97
+ );
80
98
  }
81
99
  }
82
100
 
83
- throw new Error('Invalid type to construct an Amount')
101
+ throw new Error("Invalid type to construct an Issue");
84
102
  }
85
103
 
86
104
  /**
87
- * Read an amount from a BinaryParser
105
+ * Read Issue from a BinaryParser
88
106
  *
89
- * @param parser BinaryParser to read the Amount from
90
- * @param hint The number of bytes to consume from the parser.
91
- * For an MPT amount, pass 24 (the fixed length for Hash192).
107
+ * @param parser BinaryParser to read the Issue from
92
108
  *
93
109
  * @returns An Issue object
94
110
  */
95
- static fromParser(parser: BinaryParser, hint?: number): Issue {
96
- if (hint === Hash192.width) {
97
- const mptBytes = parser.read(Hash192.width)
98
- return new Issue(mptBytes)
111
+ static fromParser(parser: BinaryParser): Issue {
112
+ // XRP
113
+ const currencyOrAccount = parser.read(20);
114
+ if (new Currency(currencyOrAccount).toJSON() === "XRP") {
115
+ return new Issue(currencyOrAccount);
99
116
  }
100
- const currency = parser.read(20)
101
- if (new Currency(currency).toJSON() === 'XRP') {
102
- return new Issue(currency)
117
+
118
+ // MPT
119
+ const issuerAccountId = new AccountID(parser.read(20));
120
+ if (NO_ACCOUNT.toHex() === issuerAccountId.toHex()) {
121
+ const sequence = parser.read(4);
122
+ return new Issue(
123
+ concat([currencyOrAccount, NO_ACCOUNT.toBytes(), sequence]),
124
+ );
103
125
  }
104
- const currencyAndIssuer = [currency, parser.read(20)]
105
- return new Issue(concat(currencyAndIssuer))
126
+
127
+ // IOU
128
+ return new Issue(concat([currencyOrAccount, issuerAccountId.toBytes()]));
106
129
  }
107
130
 
108
131
  /**
109
- * Get the JSON representation of this Amount
132
+ * Get the JSON representation of this IssueObject
110
133
  *
111
134
  * @returns the JSON interpretation of this.bytes
112
135
  */
113
136
  toJSON(): IssueObject {
114
- // If the buffer is exactly 24 bytes, treat it as an MPT amount.
115
- if (this.toBytes().length === Hash192.width) {
137
+ // If the buffer is exactly 44 bytes, treat it as an MPTIssue.
138
+ if (this.toBytes().length === MPT_WIDTH) {
139
+ const issuerAccount = this.toBytes().slice(0, 20);
140
+ const sequence = new DataView(this.toBytes().slice(40).buffer).getUint32(
141
+ 0,
142
+ true,
143
+ );
144
+
145
+ // sequence part of mpt_issuance_id should be in Big-endian
146
+ const sequenceBuffer = new Uint8Array(4);
147
+ writeUInt32BE(sequenceBuffer, sequence, 0);
148
+
116
149
  return {
117
- mpt_issuance_id: this.toHex().toUpperCase(),
118
- }
150
+ mpt_issuance_id: bytesToHex(concat([sequenceBuffer, issuerAccount])),
151
+ };
119
152
  }
120
153
 
121
- const parser = new BinaryParser(this.toString())
154
+ const parser = new BinaryParser(this.toString());
122
155
 
123
- const currency = Currency.fromParser(parser) as Currency
124
- if (currency.toJSON() === 'XRP') {
125
- return { currency: currency.toJSON() }
156
+ const currency = Currency.fromParser(parser) as Currency;
157
+ if (currency.toJSON() === "XRP") {
158
+ return { currency: currency.toJSON() };
126
159
  }
127
- const issuer = AccountID.fromParser(parser) as AccountID
160
+ const issuer = AccountID.fromParser(parser) as AccountID;
128
161
 
129
162
  return {
130
163
  currency: currency.toJSON(),
131
164
  issuer: issuer.toJSON(),
132
- }
165
+ };
133
166
  }
134
167
 
135
168
  getSType(): SerializedTypeID {
136
- return SerializedTypeID.STI_ISSUE
169
+ return SerializedTypeID.STI_ISSUE;
137
170
  }
138
171
  }
139
172
 
140
- export { Issue, IssueObject }
173
+ export { Issue, IssueObject };
@@ -1,3 +1,4 @@
1
+ /* eslint-disable complexity -- required for various checks */
1
2
  import { BinaryParser } from '../serdes/binary-parser'
2
3
  import { SerializedType, SerializedTypeID } from './serialized-type'
3
4
  import { writeInt32BE, writeInt64BE, readInt32BE, readInt64BE } from '../utils'
@@ -6,8 +7,9 @@ import { writeInt32BE, writeInt64BE, readInt32BE, readInt64BE } from '../utils'
6
7
  * Constants for mantissa and exponent normalization per XRPL Number spec.
7
8
  * These define allowed magnitude for mantissa and exponent after normalization.
8
9
  */
9
- const MIN_MANTISSA = BigInt('1000000000000000')
10
- const MAX_MANTISSA = BigInt('9999999999999999')
10
+ const MIN_MANTISSA = BigInt('1000000000000000000') // 10^18
11
+ const MAX_MANTISSA = BigInt('9999999999999999999') // 10^19 - 1
12
+ const MAX_INT64 = BigInt('9223372036854775807') // 2^63 - 1, max signed 64-bit integer
11
13
  const MIN_EXPONENT = -32768
12
14
  const MAX_EXPONENT = 32768
13
15
  const DEFAULT_VALUE_EXPONENT = -2147483648
@@ -62,6 +64,12 @@ function extractNumberPartsFromString(val: string): {
62
64
  }
63
65
  if (expPart) exponent += parseInt(expPart, 10)
64
66
 
67
+ // Remove trailing zeros from mantissa and adjust exponent
68
+ while (mantissaStr.length > 1 && mantissaStr.endsWith('0')) {
69
+ mantissaStr = mantissaStr.slice(0, -1)
70
+ exponent += 1
71
+ }
72
+
65
73
  let mantissa = BigInt(mantissaStr)
66
74
  if (sign === '-') mantissa = -mantissa
67
75
  const isNegative = mantissa < BigInt(0)
@@ -72,7 +80,7 @@ function extractNumberPartsFromString(val: string): {
72
80
  /**
73
81
  * Normalize the mantissa and exponent to XRPL constraints.
74
82
  *
75
- * Ensures that after normalization, the mantissa is between MIN_MANTISSA and MAX_MANTISSA (unless zero).
83
+ * Ensures that after normalization, the mantissa is between MIN_MANTISSA and MAX_INT64.
76
84
  * Adjusts the exponent as needed by shifting the mantissa left/right (multiplying/dividing by 10).
77
85
  *
78
86
  * @param mantissa - The unnormalized mantissa (BigInt).
@@ -87,16 +95,65 @@ function normalize(
87
95
  let m = mantissa < BigInt(0) ? -mantissa : mantissa
88
96
  const isNegative = mantissa < BigInt(0)
89
97
 
90
- while (m !== BigInt(0) && m < MIN_MANTISSA && exponent > MIN_EXPONENT) {
98
+ // Handle zero
99
+ if (m === BigInt(0)) {
100
+ return { mantissa: BigInt(0), exponent: DEFAULT_VALUE_EXPONENT }
101
+ }
102
+
103
+ // Grow mantissa until it reaches MIN_MANTISSA
104
+ while (m < MIN_MANTISSA && exponent > MIN_EXPONENT) {
91
105
  exponent -= 1
92
106
  m *= BigInt(10)
93
107
  }
108
+
109
+ let lastDigit: bigint | null = null
110
+
111
+ // Shrink mantissa until it fits within MAX_MANTISSA
94
112
  while (m > MAX_MANTISSA) {
95
- if (exponent >= MAX_EXPONENT)
113
+ if (exponent >= MAX_EXPONENT) {
96
114
  throw new Error('Mantissa and exponent are too large')
115
+ }
97
116
  exponent += 1
117
+ lastDigit = m % BigInt(10)
98
118
  m /= BigInt(10)
99
119
  }
120
+
121
+ // Handle underflow: if exponent too small or mantissa too small, throw error
122
+ if (exponent < MIN_EXPONENT || m < MIN_MANTISSA) {
123
+ throw new Error('Underflow: value too small to represent')
124
+ }
125
+
126
+ // Handle overflow: if exponent exceeds MAX_EXPONENT after growing.
127
+ if (exponent > MAX_EXPONENT) {
128
+ throw new Error('Exponent overflow: value too large to represent')
129
+ }
130
+
131
+ // Handle overflow: if mantissa exceeds MAX_INT64 (2^63-1) after growing.
132
+ if (m > MAX_INT64) {
133
+ if (exponent >= MAX_EXPONENT) {
134
+ throw new Error('Exponent overflow: value too large to represent')
135
+ }
136
+ exponent += 1
137
+ lastDigit = m % BigInt(10)
138
+ m /= BigInt(10)
139
+ }
140
+
141
+ if (lastDigit != null && lastDigit >= BigInt(5)) {
142
+ m += BigInt(1)
143
+ // After rounding, mantissa may exceed MAX_INT64 again
144
+ if (m > MAX_INT64) {
145
+ if (exponent >= MAX_EXPONENT) {
146
+ throw new Error('Exponent overflow: value too large to represent')
147
+ }
148
+ lastDigit = m % BigInt(10)
149
+ exponent += 1
150
+ m /= BigInt(10)
151
+ if (lastDigit >= BigInt(5)) {
152
+ m += BigInt(1)
153
+ }
154
+ }
155
+ }
156
+
100
157
  if (isNegative) m = -m
101
158
  return { mantissa: m, exponent }
102
159
  }
@@ -159,17 +216,9 @@ export class STNumber extends SerializedType {
159
216
  * @throws Error if val is not a valid number string.
160
217
  */
161
218
  static fromValue(val: string): STNumber {
162
- const { mantissa, exponent, isNegative } = extractNumberPartsFromString(val)
163
- let normalizedMantissa: bigint
164
- let normalizedExponent: number
165
-
166
- if (mantissa === BigInt(0) && exponent === 0 && !isNegative) {
167
- normalizedMantissa = BigInt(0)
168
- normalizedExponent = DEFAULT_VALUE_EXPONENT
169
- } else {
170
- ;({ mantissa: normalizedMantissa, exponent: normalizedExponent } =
171
- normalize(mantissa, exponent))
172
- }
219
+ const { mantissa, exponent } = extractNumberPartsFromString(val)
220
+ const { mantissa: normalizedMantissa, exponent: normalizedExponent } =
221
+ normalize(mantissa, exponent)
173
222
 
174
223
  const bytes = new Uint8Array(12)
175
224
  writeInt64BE(bytes, normalizedMantissa, 0)
@@ -193,39 +242,66 @@ export class STNumber extends SerializedType {
193
242
  *
194
243
  * @returns String representation of the value
195
244
  */
196
- // eslint-disable-next-line complexity -- required
197
245
  toJSON(): string {
198
246
  const b = this.bytes
199
- if (!b || b.length !== 12)
247
+ if (!b || b?.length !== 12)
200
248
  throw new Error('STNumber internal bytes not set or wrong length')
201
249
 
202
250
  // Signed 64-bit mantissa
203
251
  const mantissa = readInt64BE(b, 0)
204
252
  // Signed 32-bit exponent
205
- const exponent = readInt32BE(b, 8)
253
+ let exponent = readInt32BE(b, 8)
206
254
 
207
255
  // Special zero: XRPL encodes canonical zero as mantissa=0, exponent=DEFAULT_VALUE_EXPONENT.
208
256
  if (mantissa === BigInt(0) && exponent === DEFAULT_VALUE_EXPONENT) {
209
257
  return '0'
210
258
  }
211
- if (exponent === 0) return mantissa.toString()
212
259
 
213
- // Use scientific notation for small/large exponents, decimal otherwise
214
- if (exponent < -25 || exponent > -5) {
215
- return `${mantissa}e${exponent}`
260
+ const isNegative = mantissa < BigInt(0)
261
+ let mantissaAbs = isNegative ? -mantissa : mantissa
262
+
263
+ // If mantissa < MIN_MANTISSA, it was shrunk for int64 serialization (mantissa > 2^63-1).
264
+ // Restore it for proper string rendering to match rippled's internal representation.
265
+ if (mantissaAbs !== BigInt(0) && mantissaAbs < MIN_MANTISSA) {
266
+ mantissaAbs *= BigInt(10)
267
+ exponent -= 1
216
268
  }
217
269
 
218
- // Decimal rendering for -25 <= exp <= -5
219
- const isNegative = mantissa < BigInt(0)
220
- const mantissaAbs = mantissa < BigInt(0) ? -mantissa : mantissa
270
+ // For large mantissa range (default), rangeLog = 18
271
+ const rangeLog = 18
272
+
273
+ // Use scientific notation for exponents that are too small or too large
274
+ // Condition from rippled: exponent != 0 AND (exponent < -(rangeLog + 10) OR exponent > -(rangeLog - 10))
275
+ // For rangeLog = 18: exponent != 0 AND (exponent < -28 OR exponent > -8)
276
+ if (
277
+ exponent !== 0 &&
278
+ (exponent < -(rangeLog + 10) || exponent > -(rangeLog - 10))
279
+ ) {
280
+ // Strip trailing zeros from mantissa (matches rippled behavior)
281
+ let exp = exponent
282
+ while (
283
+ mantissaAbs !== BigInt(0) &&
284
+ mantissaAbs % BigInt(10) === BigInt(0) &&
285
+ exp < MAX_EXPONENT
286
+ ) {
287
+ mantissaAbs /= BigInt(10)
288
+ exp += 1
289
+ }
290
+ const sign = isNegative ? '-' : ''
291
+ return `${sign}${mantissaAbs}e${exp}`
292
+ }
293
+
294
+ // Decimal rendering for -(rangeLog + 10) <= exponent <= -(rangeLog - 10)
295
+ // i.e., -28 <= exponent <= -8, or exponent == 0
296
+ const padPrefix = rangeLog + 12 // 30
297
+ const padSuffix = rangeLog + 8 // 26
221
298
 
222
- const padPrefix = 27
223
- const padSuffix = 23
224
299
  const mantissaStr = mantissaAbs.toString()
225
300
  const rawValue = '0'.repeat(padPrefix) + mantissaStr + '0'.repeat(padSuffix)
226
- const OFFSET = exponent + 43
227
- const integerPart = rawValue.slice(0, OFFSET).replace(/^0+/, '') || '0'
228
- const fractionPart = rawValue.slice(OFFSET).replace(/0+$/, '')
301
+ const offset = exponent + padPrefix + rangeLog + 1 // exponent + 49
302
+
303
+ const integerPart = rawValue.slice(0, offset).replace(/^0+/, '') || '0'
304
+ const fractionPart = rawValue.slice(offset).replace(/0+$/, '')
229
305
 
230
306
  return `${isNegative ? '-' : ''}${integerPart}${
231
307
  fractionPart ? '.' + fractionPart : ''