Skip to content

Commit b1e139a

Browse files
authored
Add missing field types to lending protocol related objects and support Int32 serialized type (#3198)
* WIP * update history * fix lint errors * add Int32 type * add encode decode test * update history * fix tests * use typeName * fix lint errors
1 parent 0c89766 commit b1e139a

File tree

21 files changed

+880
-10
lines changed

21 files changed

+880
-10
lines changed

packages/ripple-binary-codec/HISTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
### Fixed
6+
* Add `Int32` serialized type.
67
* Fix STNumber serialization logic to work with large mantissa scale [10^18, 10^19-1].
78
* Error if a decimal is passed into a `UInt`-typed field.
89

packages/ripple-binary-codec/src/types/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Hash128 } from './hash-128'
66
import { Hash160 } from './hash-160'
77
import { Hash192 } from './hash-192'
88
import { Hash256 } from './hash-256'
9+
import { Int32 } from './int-32'
910
import { Issue } from './issue'
1011
import { STNumber } from './st-number'
1112
import { PathSet } from './path-set'
@@ -29,6 +30,7 @@ const coreTypes: Record<string, typeof SerializedType> = {
2930
Hash160,
3031
Hash192,
3132
Hash256,
33+
Int32,
3234
Issue,
3335
Number: STNumber,
3436
PathSet,
@@ -57,6 +59,7 @@ export {
5759
Hash160,
5860
Hash192,
5961
Hash256,
62+
Int32,
6063
PathSet,
6164
STArray,
6265
STObject,
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 }
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 }
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { Int32 } from '../src/types'
2+
import { encode, decode } from '../src'
3+
4+
describe('Int32', () => {
5+
describe('from()', () => {
6+
it('should create Int32 from positive number', () => {
7+
const int32 = Int32.from(12345)
8+
expect(int32.valueOf()).toBe(12345)
9+
})
10+
11+
it('should create Int32 from negative number', () => {
12+
const int32 = Int32.from(-12345)
13+
expect(int32.valueOf()).toBe(-12345)
14+
})
15+
16+
it('should create Int32 from zero', () => {
17+
const int32 = Int32.from(0)
18+
expect(int32.valueOf()).toBe(0)
19+
})
20+
21+
it('should create Int32 from positive string', () => {
22+
const int32 = Int32.from('67890')
23+
expect(int32.valueOf()).toBe(67890)
24+
})
25+
26+
it('should create Int32 from negative string', () => {
27+
const int32 = Int32.from('-67890')
28+
expect(int32.valueOf()).toBe(-67890)
29+
})
30+
31+
it('should create Int32 from another Int32', () => {
32+
const original = Int32.from(999)
33+
const copy = Int32.from(original)
34+
expect(copy.valueOf()).toBe(999)
35+
})
36+
37+
it('should handle MIN_VALUE', () => {
38+
const int32 = Int32.from(-2147483648)
39+
expect(int32.valueOf()).toBe(-2147483648)
40+
})
41+
42+
it('should handle MAX_VALUE', () => {
43+
const int32 = Int32.from(2147483647)
44+
expect(int32.valueOf()).toBe(2147483647)
45+
})
46+
})
47+
48+
describe('range validation', () => {
49+
it('should throw error when value exceeds MAX_VALUE', () => {
50+
expect(() => Int32.from(2147483648)).toThrow(
51+
new Error(
52+
'Invalid Int32: 2147483648 must be >= -2147483648 and <= 2147483647',
53+
),
54+
)
55+
})
56+
57+
it('should throw error when value is below MIN_VALUE', () => {
58+
expect(() => Int32.from(-2147483649)).toThrow(
59+
new Error(
60+
'Invalid Int32: -2147483649 must be >= -2147483648 and <= 2147483647',
61+
),
62+
)
63+
})
64+
65+
it('should throw error for string exceeding MAX_VALUE', () => {
66+
expect(() => Int32.from('2147483648')).toThrow(
67+
new Error(
68+
'Invalid Int32: 2147483648 must be >= -2147483648 and <= 2147483647',
69+
),
70+
)
71+
})
72+
73+
it('should throw error for string below MIN_VALUE', () => {
74+
expect(() => Int32.from('-2147483649')).toThrow(
75+
new Error(
76+
'Invalid Int32: -2147483649 must be >= -2147483648 and <= 2147483647',
77+
),
78+
)
79+
})
80+
})
81+
82+
describe('decimal validation', () => {
83+
it('should throw error when passed a decimal number', () => {
84+
expect(() => Int32.from(100.5)).toThrow(
85+
new Error('Cannot construct Int32 from given value'),
86+
)
87+
})
88+
89+
it('should throw error when passed a negative decimal', () => {
90+
expect(() => Int32.from(-100.5)).toThrow(
91+
new Error('Cannot construct Int32 from given value'),
92+
)
93+
})
94+
95+
it('should throw error when passed a small decimal', () => {
96+
expect(() => Int32.from(0.001)).toThrow(
97+
new Error('Cannot construct Int32 from given value'),
98+
)
99+
})
100+
101+
it('should throw error for decimal string', () => {
102+
expect(() => Int32.from('1.23')).toThrow(
103+
new Error('Cannot construct Int32 from string: 1.23'),
104+
)
105+
})
106+
107+
it('should throw error for non-numeric string', () => {
108+
expect(() => Int32.from('abc')).toThrow(
109+
new Error('Cannot construct Int32 from string: abc'),
110+
)
111+
})
112+
})
113+
114+
describe('compareTo()', () => {
115+
it('should return 0 for equal values', () => {
116+
expect(Int32.from(100).compareTo(Int32.from(100))).toBe(0)
117+
})
118+
119+
it('should return 1 when first value is greater', () => {
120+
expect(Int32.from(100).compareTo(Int32.from(50))).toBe(1)
121+
})
122+
123+
it('should return -1 when first value is smaller', () => {
124+
expect(Int32.from(50).compareTo(Int32.from(100))).toBe(-1)
125+
})
126+
127+
it('should compare with raw number', () => {
128+
expect(Int32.from(100).compareTo(100)).toBe(0)
129+
expect(Int32.from(100).compareTo(50)).toBe(1)
130+
expect(Int32.from(50).compareTo(100)).toBe(-1)
131+
})
132+
133+
it('should handle negative values', () => {
134+
expect(Int32.from(-100).compareTo(Int32.from(-50))).toBe(-1)
135+
expect(Int32.from(-50).compareTo(Int32.from(-100))).toBe(1)
136+
expect(Int32.from(-100).compareTo(Int32.from(-100))).toBe(0)
137+
})
138+
139+
it('should compare negative and positive values', () => {
140+
expect(Int32.from(-100).compareTo(Int32.from(100))).toBe(-1)
141+
expect(Int32.from(100).compareTo(Int32.from(-100))).toBe(1)
142+
})
143+
})
144+
145+
describe('toJSON()', () => {
146+
it('should return number for positive value', () => {
147+
expect(Int32.from(12345).toJSON()).toBe(12345)
148+
})
149+
150+
it('should return number for negative value', () => {
151+
expect(Int32.from(-12345).toJSON()).toBe(-12345)
152+
})
153+
154+
it('should return 0 for zero', () => {
155+
expect(Int32.from(0).toJSON()).toBe(0)
156+
})
157+
})
158+
159+
describe('valueOf()', () => {
160+
it('should allow bitwise operations', () => {
161+
const val = Int32.from(5)
162+
expect(val.valueOf() | 0x2).toBe(7)
163+
})
164+
165+
it('should handle negative values in bitwise operations', () => {
166+
const val = Int32.from(-1)
167+
expect(val.valueOf() & 0xff).toBe(255)
168+
})
169+
})
170+
171+
describe('encode/decode with Loan ledger entry', () => {
172+
// Loan object from Devnet with LoanScale field
173+
const loanWithScale = {
174+
Borrower: 'rs5fUokF7Y5bxNkstM4p4JYHgqzYkFamCg',
175+
GracePeriod: 60,
176+
LoanBrokerID:
177+
'18F91BD8009DAF09B5E4663BE7A395F5F193D0657B12F8D1E781EB3D449E8151',
178+
LoanScale: -11,
179+
LoanSequence: 1,
180+
NextPaymentDueDate: 822779431,
181+
PaymentInterval: 400,
182+
PaymentRemaining: 1,
183+
PeriodicPayment: '10000',
184+
PrincipalOutstanding: '10000',
185+
StartDate: 822779031,
186+
TotalValueOutstanding: '10000',
187+
LedgerEntryType: 'Loan',
188+
}
189+
190+
it('can encode and decode Loan with negative LoanScale', () => {
191+
const encoded = encode(loanWithScale)
192+
const decoded = decode(encoded)
193+
expect(decoded).toEqual(loanWithScale)
194+
})
195+
196+
it('can encode and decode Loan with positive LoanScale', () => {
197+
const loan = { ...loanWithScale, LoanScale: 5 }
198+
const encoded = encode(loan)
199+
const decoded = decode(encoded)
200+
expect(decoded).toEqual(loan)
201+
})
202+
})
203+
})

packages/xrpl/HISTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Subscribe to [the **xrpl-announce** mailing list](https://groups.google.com/g/xr
77
### Added
88
* Add `faucetProtocol` (http or https) option to `fundWallet` method. Makes `fundWallet` work with locally running faucet servers.
99
* Add `signLoanSetByCounterparty` and `combineLoanSetCounterpartySigners` helper functions to sign and combine LoanSet transactions signed by the counterparty.
10+
* Add newly added fields to `Loan`, `LoanBroker` and `Vault` ledger objects and lending protocol related transaction types.
1011

1112
## 4.5.0 (2025-12-16)
1213

0 commit comments

Comments
 (0)