@fintekkers/ledger-models 0.4.38 → 0.4.39
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.
- package/node/fintekkers/models/util/decimal_value_pb.d.ts +2 -0
- package/node/wrappers/models/position/position.d.ts +1 -1
- package/node/wrappers/models/position/position.js.map +1 -1
- package/node/wrappers/models/position/position.test.js +2 -2
- package/node/wrappers/models/position/position.test.js.map +1 -1
- package/node/wrappers/models/position/position.test.ts +2 -2
- package/node/wrappers/models/position/position.ts +2 -2
- package/node/wrappers/models/price/Price.js +16 -8
- package/node/wrappers/models/price/Price.js.map +1 -1
- package/node/wrappers/models/price/Price.ts +15 -6
- package/node/wrappers/models/price/price_rules.d.ts +2 -2
- package/node/wrappers/models/price/price_rules.js +6 -4
- package/node/wrappers/models/price/price_rules.js.map +1 -1
- package/node/wrappers/models/price/price_rules.ts +5 -3
- package/node/wrappers/models/security/BondSecurity.d.ts +1 -1
- package/node/wrappers/models/security/BondSecurity.js +5 -5
- package/node/wrappers/models/security/BondSecurity.js.map +1 -1
- package/node/wrappers/models/security/BondSecurity.ts +5 -4
- package/node/wrappers/models/security/FloatingRateNote.js +2 -2
- package/node/wrappers/models/security/FloatingRateNote.js.map +1 -1
- package/node/wrappers/models/security/FloatingRateNote.ts +2 -1
- package/node/wrappers/models/security/Issuance.js +3 -6
- package/node/wrappers/models/security/Issuance.js.map +1 -1
- package/node/wrappers/models/security/Issuance.ts +3 -2
- package/node/wrappers/models/security/MortgageBackedSecurity.js +7 -7
- package/node/wrappers/models/security/MortgageBackedSecurity.js.map +1 -1
- package/node/wrappers/models/security/MortgageBackedSecurity.ts +7 -6
- package/node/wrappers/models/security/TIPSBond.js +2 -2
- package/node/wrappers/models/security/TIPSBond.js.map +1 -1
- package/node/wrappers/models/security/TIPSBond.ts +2 -1
- package/node/wrappers/models/security/security_rules.js +10 -12
- package/node/wrappers/models/security/security_rules.js.map +1 -1
- package/node/wrappers/models/security/security_rules.ts +9 -9
- package/node/wrappers/models/transaction/transaction.js +16 -10
- package/node/wrappers/models/transaction/transaction.js.map +1 -1
- package/node/wrappers/models/transaction/transaction.ts +16 -7
- package/node/wrappers/models/utils/decimal.d.ts +96 -0
- package/node/wrappers/models/utils/decimal.js +328 -0
- package/node/wrappers/models/utils/decimal.js.map +1 -0
- package/node/wrappers/models/utils/decimal.legacy.test.d.ts +1 -0
- package/node/wrappers/models/utils/decimal.legacy.test.js +174 -0
- package/node/wrappers/models/utils/decimal.legacy.test.js.map +1 -0
- package/node/wrappers/models/utils/decimal.legacy.test.ts +158 -0
- package/node/wrappers/models/utils/decimal.test.d.ts +1 -0
- package/node/wrappers/models/utils/decimal.test.js +258 -0
- package/node/wrappers/models/utils/decimal.test.js.map +1 -0
- package/node/wrappers/models/utils/decimal.test.ts +272 -0
- package/node/wrappers/models/utils/decimal.ts +335 -0
- package/node/wrappers/models/utils/decimal.typings.test.d.ts +1 -0
- package/node/wrappers/models/utils/decimal.typings.test.js +39 -0
- package/node/wrappers/models/utils/decimal.typings.test.js.map +1 -0
- package/node/wrappers/models/utils/decimal.typings.test.ts +16 -0
- package/node/wrappers/models/utils/serialization.d.ts +2 -1
- package/node/wrappers/models/utils/serialization.js +12 -2
- package/node/wrappers/models/utils/serialization.js.map +1 -1
- package/node/wrappers/models/utils/serialization.ts +14 -3
- package/node/wrappers/services/security-service/security.maturityLadder.test.js +1 -1
- package/node/wrappers/services/security-service/security.maturityLadder.test.js.map +1 -1
- package/node/wrappers/services/security-service/security.maturityLadder.test.ts +3 -2
- package/package.json +1 -1
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { Decimal } from 'decimal.js';
|
|
4
|
+
import { DecimalValueProto } from '../../../fintekkers/models/util/decimal_value_pb';
|
|
5
|
+
import { ModelValidationError } from '../errors';
|
|
6
|
+
import { ProtoSerializationUtil } from './serialization';
|
|
7
|
+
import {
|
|
8
|
+
CompactDecimal,
|
|
9
|
+
MAX_DIGITS,
|
|
10
|
+
MAX_SCALE,
|
|
11
|
+
MAX_UNSCALED_BYTES,
|
|
12
|
+
decodeDecimal,
|
|
13
|
+
encodeDecimal,
|
|
14
|
+
fromDecimalJs,
|
|
15
|
+
parseDecimalString,
|
|
16
|
+
setDecimalLogger,
|
|
17
|
+
toCanonicalString,
|
|
18
|
+
withField,
|
|
19
|
+
} from './decimal';
|
|
20
|
+
|
|
21
|
+
/** Codec tests against the shared fixture (LM-306, JS side of LM-294). */
|
|
22
|
+
|
|
23
|
+
const FIXTURE = JSON.parse(fs.readFileSync(
|
|
24
|
+
path.join(__dirname, '../../../../../ledger-models-protos/fixtures/decimal_compact.json'), 'utf8'));
|
|
25
|
+
|
|
26
|
+
interface Case { name: string; string: string; canonical: string; unscaled_hex: string; scale: number; wire_hex: string }
|
|
27
|
+
const CASES: Case[] = FIXTURE.cases;
|
|
28
|
+
|
|
29
|
+
const hex = (bytes: Uint8Array) => Buffer.from(bytes).toString('hex');
|
|
30
|
+
const unhex = (s: string) => new Uint8Array(Buffer.from(s, 'hex'));
|
|
31
|
+
|
|
32
|
+
/** Two's-complement big-endian, written independently of the codec. */
|
|
33
|
+
function expectedUnscaled(unscaledHex: string): bigint {
|
|
34
|
+
const unsigned = BigInt('0x' + unscaledHex);
|
|
35
|
+
const negative = parseInt(unscaledHex.slice(0, 2), 16) >= 0x80;
|
|
36
|
+
return negative ? unsigned - (BigInt(1) << BigInt(unscaledHex.length * 4)) : unsigned;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Only fields 11 and 12 set. */
|
|
40
|
+
function compactOnly(c: Case): DecimalValueProto {
|
|
41
|
+
return new DecimalValueProto().setUnscaledValue(unhex(c.unscaled_hex)).setScale(c.scale);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function expectInvalid(fn: () => unknown): void {
|
|
45
|
+
expect(fn).toThrow(ModelValidationError);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
beforeAll(() => {
|
|
49
|
+
// String-only decodes below would warn once; keep the test output quiet.
|
|
50
|
+
setDecimalLogger({ warn: jest.fn() });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('metric 1: shared fixture', () => {
|
|
54
|
+
test('fixture limits match the codec', () => {
|
|
55
|
+
expect(FIXTURE.limits).toEqual({
|
|
56
|
+
max_digits: MAX_DIGITS, max_scale: MAX_SCALE, max_unscaled_bytes: MAX_UNSCALED_BYTES,
|
|
57
|
+
});
|
|
58
|
+
expect(CASES.length).toBeGreaterThanOrEqual(12);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test.each(CASES.map(c => [c.name, c] as [string, Case]))('%s encodes to wire_hex and decodes exactly', (_name, c) => {
|
|
62
|
+
const parsed = parseDecimalString(c.string);
|
|
63
|
+
expect(parsed).toEqual({ unscaled: expectedUnscaled(c.unscaled_hex), scale: c.scale });
|
|
64
|
+
expect(toCanonicalString(parsed)).toBe(c.canonical);
|
|
65
|
+
|
|
66
|
+
const proto = encodeDecimal(parsed);
|
|
67
|
+
expect(hex(proto.getUnscaledValue_asU8())).toBe(c.unscaled_hex);
|
|
68
|
+
expect(proto.getScale()).toBe(c.scale);
|
|
69
|
+
expect(hex(proto.serializeBinary())).toBe(c.wire_hex);
|
|
70
|
+
|
|
71
|
+
// The proto alone is enough to decode (metric 3).
|
|
72
|
+
const decoded = decodeDecimal(DecimalValueProto.deserializeBinary(unhex(c.wire_hex)));
|
|
73
|
+
expect(decoded).toEqual({ unscaled: expectedUnscaled(c.unscaled_hex), scale: c.scale });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const stringErrors: [string, string][] = FIXTURE.string_errors.map((c: { name: string; string: string }) => [c.name, c.string]);
|
|
77
|
+
test.each(stringErrors)('string error %s is a ModelValidationError', (_name, s) => {
|
|
78
|
+
expectInvalid(() => parseDecimalString(s));
|
|
79
|
+
expectInvalid(() => decodeDecimal(new DecimalValueProto().setArbitraryPrecisionValue(s)));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const decodeErrors: [string, any][] = FIXTURE.decode_errors.map((c: any) => [c.name, c]);
|
|
83
|
+
test.each(decodeErrors)('decode error %s is a ModelValidationError', (_name, c) => {
|
|
84
|
+
const bytes = c.unscaled_hex_repeat
|
|
85
|
+
? unhex(c.unscaled_hex_repeat.hex.repeat(c.unscaled_hex_repeat.count))
|
|
86
|
+
: unhex(c.unscaled_hex);
|
|
87
|
+
expectInvalid(() => decodeDecimal(new DecimalValueProto().setUnscaledValue(bytes).setScale(c.scale)));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('the 1702-byte case passes the byte check and fails on digits; 1703 fails on bytes', () => {
|
|
91
|
+
const message = (n: number) => {
|
|
92
|
+
try {
|
|
93
|
+
decodeDecimal(new DecimalValueProto().setUnscaledValue(unhex('7f'.repeat(n))));
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return (e as Error).message;
|
|
96
|
+
}
|
|
97
|
+
return '';
|
|
98
|
+
};
|
|
99
|
+
expect(message(1702)).toContain(`more than ${MAX_DIGITS} digits`);
|
|
100
|
+
expect(message(1703)).toContain('1703-byte');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('the committed decimal.js (what npm publishes) encodes and decodes fixture cases', () => {
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
105
|
+
const shipped = require(path.join(__dirname, 'decimal.js'));
|
|
106
|
+
for (const name of ['zero', 'two_pow_127', 'tiny_fraction']) {
|
|
107
|
+
const c = CASES.find(x => x.name === name)!;
|
|
108
|
+
const proto: DecimalValueProto = shipped.encodeDecimal(shipped.parseDecimalString(c.string));
|
|
109
|
+
expect(hex(proto.serializeBinary())).toBe(c.wire_hex);
|
|
110
|
+
expect(shipped.decodeDecimal(DecimalValueProto.deserializeBinary(unhex(c.wire_hex))))
|
|
111
|
+
.toEqual({ unscaled: expectedUnscaled(c.unscaled_hex), scale: c.scale });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('compact form wins over a conflicting string and does not warn', () => {
|
|
116
|
+
jest.isolateModules(() => {
|
|
117
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
118
|
+
const codec = require('./decimal');
|
|
119
|
+
const warn = jest.fn();
|
|
120
|
+
codec.setDecimalLogger({ warn });
|
|
121
|
+
const proto = new DecimalValueProto()
|
|
122
|
+
.setArbitraryPrecisionValue('9')
|
|
123
|
+
.setUnscaledValue(unhex('0096'))
|
|
124
|
+
.setScale(2);
|
|
125
|
+
expect(codec.toCanonicalString(codec.decodeDecimal(proto))).toBe('1.50');
|
|
126
|
+
expect(warn).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('negative zero from decimal.js encodes to 0x00 at scale 0', () => {
|
|
131
|
+
const v = fromDecimalJs(new Decimal('-0'));
|
|
132
|
+
expect(v).toEqual({ unscaled: BigInt(0), scale: 0 });
|
|
133
|
+
const proto = encodeDecimal(v);
|
|
134
|
+
expect(hex(proto.getUnscaledValue_asU8())).toBe('00');
|
|
135
|
+
expect(proto.getScale()).toBe(0);
|
|
136
|
+
expect(proto.getArbitraryPrecisionValue()).toBe('0');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('metric 3: decodeDecimal takes the proto only', () => {
|
|
141
|
+
test('a type or field argument does not type-check', () => {
|
|
142
|
+
const p = encodeDecimal(parseDecimalString('1.50'));
|
|
143
|
+
// ts-jest type-checks this file: an unused @ts-expect-error fails it.
|
|
144
|
+
const typeChecked = () => {
|
|
145
|
+
// @ts-expect-error decodeDecimal takes no type argument
|
|
146
|
+
decodeDecimal<number>(p);
|
|
147
|
+
// @ts-expect-error decodeDecimal takes no field argument
|
|
148
|
+
decodeDecimal(p, 'price');
|
|
149
|
+
};
|
|
150
|
+
expect(typeof typeChecked).toBe('function');
|
|
151
|
+
expect(decodeDecimal.length).toBe(1);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('withField names the field on a codec error', () => {
|
|
155
|
+
try {
|
|
156
|
+
withField('price', () => decodeDecimal(new DecimalValueProto()));
|
|
157
|
+
throw new Error('expected a ModelValidationError');
|
|
158
|
+
} catch (e) {
|
|
159
|
+
expect(e).toBeInstanceOf(ModelValidationError);
|
|
160
|
+
expect((e as ModelValidationError).field).toBe('price');
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('metric 4: one deprecation warning per process', () => {
|
|
166
|
+
test('a compact-only decode does not warn', () => {
|
|
167
|
+
jest.isolateModules(() => {
|
|
168
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
169
|
+
const codec = require('./decimal');
|
|
170
|
+
const warn = jest.fn();
|
|
171
|
+
codec.setDecimalLogger({ warn });
|
|
172
|
+
for (const c of CASES) codec.decodeDecimal(compactOnly(c));
|
|
173
|
+
expect(warn).toHaveBeenCalledTimes(0);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('three string-only prices read through the Price wrapper warn exactly once', () => {
|
|
178
|
+
jest.isolateModules(() => {
|
|
179
|
+
// The same isolated registry: Price loads this codec instance.
|
|
180
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
181
|
+
const codec = require('./decimal');
|
|
182
|
+
const warn = jest.fn();
|
|
183
|
+
codec.setDecimalLogger({ warn });
|
|
184
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
185
|
+
const { PriceProto } = require('../../../fintekkers/models/price/price_pb');
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
187
|
+
const Price = require('../price/Price').default;
|
|
188
|
+
const values = ['100.25', '-0.50', '1E+3'];
|
|
189
|
+
const read = values.map(s => new Price(
|
|
190
|
+
new PriceProto().setPrice(new DecimalValueProto().setArbitraryPrecisionValue(s))).getPrice());
|
|
191
|
+
expect(read.map((d: Decimal) => d.toString())).toEqual(['100.25', '-0.5', '1000']);
|
|
192
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
193
|
+
expect(warn.mock.calls[0][0]).toContain('arbitrary_precision_value');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('metric 5: writers set both forms', () => {
|
|
199
|
+
test.each(CASES.map(c => [c.name, c] as [string, Case]))('%s: both forms are set and decode equal', (_name, c) => {
|
|
200
|
+
const proto = encodeDecimal(parseDecimalString(c.string));
|
|
201
|
+
expect(proto.getArbitraryPrecisionValue()).toBe(c.canonical);
|
|
202
|
+
expect(proto.getUnscaledValue_asU8().length).toBeGreaterThan(0);
|
|
203
|
+
|
|
204
|
+
const stringOnly = new DecimalValueProto().setArbitraryPrecisionValue(proto.getArbitraryPrecisionValue());
|
|
205
|
+
const compact = new DecimalValueProto()
|
|
206
|
+
.setUnscaledValue(proto.getUnscaledValue_asU8())
|
|
207
|
+
.setScale(proto.getScale());
|
|
208
|
+
const expected: CompactDecimal = { unscaled: expectedUnscaled(c.unscaled_hex), scale: c.scale };
|
|
209
|
+
expect(decodeDecimal(stringOnly)).toEqual(expected);
|
|
210
|
+
expect(decodeDecimal(compact)).toEqual(expected);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('guardrail: lossless', () => {
|
|
215
|
+
const NODE = path.join(__dirname, '../../..');
|
|
216
|
+
const SCANNED = [
|
|
217
|
+
'wrappers/models/utils/decimal.ts',
|
|
218
|
+
'wrappers/models/utils/serialization.ts',
|
|
219
|
+
'wrappers/models/price/Price.ts',
|
|
220
|
+
'wrappers/models/price/price_rules.ts',
|
|
221
|
+
'wrappers/models/transaction/transaction.ts',
|
|
222
|
+
'wrappers/models/security/BondSecurity.ts',
|
|
223
|
+
'wrappers/models/security/Issuance.ts',
|
|
224
|
+
'wrappers/models/security/MortgageBackedSecurity.ts',
|
|
225
|
+
'wrappers/models/security/TIPSBond.ts',
|
|
226
|
+
'wrappers/models/security/FloatingRateNote.ts',
|
|
227
|
+
'wrappers/models/security/security_rules.ts',
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
test.each(SCANNED)('%s has no float conversion on the decimal path', (file) => {
|
|
231
|
+
const lines = fs.readFileSync(path.join(NODE, file), 'utf8').split('\n');
|
|
232
|
+
const allowed: number[] = [];
|
|
233
|
+
lines.forEach((line, n) => {
|
|
234
|
+
// Comment lines don't count.
|
|
235
|
+
const code = /^\s*(\*|\/\*)/.test(line) ? '' : line.split('//')[0];
|
|
236
|
+
expect({ file, line: n + 1, code, banned: /parseFloat|parseInt|\.toNumber\(/.test(code) })
|
|
237
|
+
.toEqual({ file, line: n + 1, code, banned: false });
|
|
238
|
+
if (/\bNumber\(/.test(code)) allowed.push(n + 1);
|
|
239
|
+
});
|
|
240
|
+
// The one documented, range-checked conversion in smallInt().
|
|
241
|
+
const expectedNumberCalls = file.endsWith('utils/decimal.ts') ? 1 : 0;
|
|
242
|
+
expect(allowed.length).toBe(expectedNumberCalls);
|
|
243
|
+
if (expectedNumberCalls) expect(lines[allowed[0] - 1]).toContain('lossless-allow');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('serialize(number) accepts only safe integers', () => {
|
|
247
|
+
expectInvalid(() => ProtoSerializationUtil.serialize(0.1));
|
|
248
|
+
expectInvalid(() => ProtoSerializationUtil.serialize(2 ** 53));
|
|
249
|
+
const proto = ProtoSerializationUtil.serialize(42) as DecimalValueProto;
|
|
250
|
+
expect(proto.getArbitraryPrecisionValue()).toBe('42');
|
|
251
|
+
expect(hex(proto.getUnscaledValue_asU8())).toBe('2a');
|
|
252
|
+
expect(proto.getScale()).toBe(0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('serialize(Decimal) writes both forms; deserialize returns a Decimal', () => {
|
|
256
|
+
const proto = ProtoSerializationUtil.serialize(new Decimal('-123.45')) as DecimalValueProto;
|
|
257
|
+
expect(proto.getArbitraryPrecisionValue()).toBe('-123.45');
|
|
258
|
+
expect(hex(proto.getUnscaledValue_asU8())).toBe('cfc7');
|
|
259
|
+
|
|
260
|
+
const big = CASES.find(c => c.name === 'two_pow_127')!;
|
|
261
|
+
const read = ProtoSerializationUtil.deserialize(compactOnly(big));
|
|
262
|
+
expect(read).toBeInstanceOf(Decimal);
|
|
263
|
+
expect((read as Decimal).toFixed()).toBe(big.canonical);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('a value over the limits is an error, never rounded', () => {
|
|
267
|
+
expectInvalid(() => fromDecimalJs(new Decimal('1e-5000')));
|
|
268
|
+
expectInvalid(() => fromDecimalJs(new Decimal('1e+5000')));
|
|
269
|
+
expectInvalid(() => fromDecimalJs(new Decimal(NaN)));
|
|
270
|
+
expectInvalid(() => encodeDecimal({ unscaled: BigInt(1), scale: 1.5 }));
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { Decimal } from 'decimal.js';
|
|
2
|
+
import { DecimalValueProto } from '../../../fintekkers/models/util/decimal_value_pb';
|
|
3
|
+
import { ModelValidationError, violation } from '../errors';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Codec for `DecimalValueProto` (LM-306, the JS side of LM-294; see
|
|
7
|
+
* docs/adr/compact_decimal.md and the Rust reference in
|
|
8
|
+
* ledger-models-rust/fintekkers/wrappers/models/utils/decimal.rs).
|
|
9
|
+
*
|
|
10
|
+
* value = unscaled x 10^-scale, where `unscaled_value` (field 11) is a minimal
|
|
11
|
+
* two's-complement big-endian integer of any size and `scale` is field 12. A
|
|
12
|
+
* non-empty `unscaled_value` means the compact form is set and it wins.
|
|
13
|
+
* Otherwise the deprecated `arbitrary_precision_value` string (field 10) is
|
|
14
|
+
* read, with a warning logged once per process (LM-307).
|
|
15
|
+
*
|
|
16
|
+
* Lossless: no binary floating point on any path (no JS `number` value, no
|
|
17
|
+
* `parseFloat`). A value that cannot be represented exactly is a
|
|
18
|
+
* `ModelValidationError`, never rounded. The codec does not know which field
|
|
19
|
+
* it is decoding: a caller that wants the field named wraps the call in
|
|
20
|
+
* `withField`.
|
|
21
|
+
*
|
|
22
|
+
* Stage 1 (until LM-295): writers set both forms, so readers on older
|
|
23
|
+
* ledger-models keep working. This module is the only code allowed to touch
|
|
24
|
+
* the deprecated string field.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Most decimal digits allowed in the unscaled value, and the largest scale.
|
|
29
|
+
* The same in every language (see the ADR).
|
|
30
|
+
*/
|
|
31
|
+
export const MAX_DIGITS = 4096;
|
|
32
|
+
export const MAX_SCALE = 4096;
|
|
33
|
+
/**
|
|
34
|
+
* A minimal unscaled_value longer than this always has more than
|
|
35
|
+
* `MAX_DIGITS` digits, so it is rejected before it is parsed.
|
|
36
|
+
*/
|
|
37
|
+
export const MAX_UNSCALED_BYTES = 1702;
|
|
38
|
+
|
|
39
|
+
/** The library release that first writes the compact form. */
|
|
40
|
+
const FIRST_COMPACT_RELEASE = 'v0.4.34';
|
|
41
|
+
|
|
42
|
+
// No bigint literals: tsconfig targets es2016.
|
|
43
|
+
const ZERO = BigInt(0);
|
|
44
|
+
const ONE = BigInt(1);
|
|
45
|
+
const EIGHT = BigInt(8);
|
|
46
|
+
|
|
47
|
+
/** An exact decimal: `unscaled x 10^-scale`. `scale` is a count, never a value. */
|
|
48
|
+
export interface CompactDecimal {
|
|
49
|
+
unscaled: bigint;
|
|
50
|
+
scale: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Where the once-per-process deprecation warning goes. Defaults to `console`. */
|
|
54
|
+
export interface DecimalLogger {
|
|
55
|
+
warn(message: string): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let logger: DecimalLogger = console;
|
|
59
|
+
let warned = false;
|
|
60
|
+
|
|
61
|
+
/** Sends the deprecation warning to `l` from now on. Tests use it to spy. */
|
|
62
|
+
export function setDecimalLogger(l: DecimalLogger): void {
|
|
63
|
+
logger = l;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** An unlabelled ModelValidationError: the field is empty until `withField` adds it. */
|
|
67
|
+
function invalid(message: string): ModelValidationError {
|
|
68
|
+
return new ModelValidationError([violation('', undefined, message)]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Runs `fn`, labelling any ModelValidationError it throws with `field` (for
|
|
73
|
+
* example `price`). Violations that already name a field get it as a prefix.
|
|
74
|
+
*/
|
|
75
|
+
export function withField<T>(field: string, fn: () => T): T {
|
|
76
|
+
try {
|
|
77
|
+
return fn();
|
|
78
|
+
} catch (e) {
|
|
79
|
+
if (!(e instanceof ModelValidationError)) throw e;
|
|
80
|
+
throw new ModelValidationError(e.violations.map(v => {
|
|
81
|
+
const labelled = v.clone();
|
|
82
|
+
labelled.setField(v.getField() ? `${field}.${v.getField()}` : field);
|
|
83
|
+
return labelled;
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** At most the first 64 characters of `s`, for error messages. */
|
|
89
|
+
function preview(s: string): string {
|
|
90
|
+
return JSON.stringify(s.slice(0, 64));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function overCap(s: string): ModelValidationError {
|
|
94
|
+
return invalid(`decimal is over the limit of ${MAX_DIGITS} digits or scale ${MAX_SCALE}: ${preview(s)}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The only conversion from bigint to a JS number: `b` must already be
|
|
99
|
+
* range-checked against MAX_DIGITS / MAX_SCALE, so it is a small integer and
|
|
100
|
+
* the conversion is exact.
|
|
101
|
+
*/
|
|
102
|
+
function smallInt(b: bigint): number {
|
|
103
|
+
const limit = BigInt(Math.max(MAX_DIGITS, MAX_SCALE));
|
|
104
|
+
if (b < -limit || b > limit) {
|
|
105
|
+
throw invalid(`decimal count ${b} is outside the limits`);
|
|
106
|
+
}
|
|
107
|
+
return Number(b); // lossless-allow: range-checked above, |b| <= 4096
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isDigit(c: string | undefined): boolean {
|
|
111
|
+
return c !== undefined && c >= '0' && c <= '9';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parses a decimal string, as written to `arbitrary_precision_value`.
|
|
116
|
+
*
|
|
117
|
+
* Grammar: `[+-]? (digits [. digits?] | . digits) ([eE] [+-]? digits)?`. No
|
|
118
|
+
* whitespace, no `NaN` or `Infinity`. The exponent is folded into the scale;
|
|
119
|
+
* a negative scale becomes scale 0 (`1E+3` is unscaled 1000, scale 0).
|
|
120
|
+
* Trailing zeros are kept: `1.50` is unscaled 150, scale 2. Anything else, or
|
|
121
|
+
* a result over `MAX_DIGITS` / `MAX_SCALE`, is a ModelValidationError.
|
|
122
|
+
*/
|
|
123
|
+
export function parseDecimalString(s: string): CompactDecimal {
|
|
124
|
+
const notADecimal = () => invalid(`decimal is not a finite decimal: ${preview(s)}`);
|
|
125
|
+
let i = 0;
|
|
126
|
+
let negative = false;
|
|
127
|
+
if (s[0] === '-' || s[0] === '+') {
|
|
128
|
+
negative = s[0] === '-';
|
|
129
|
+
i = 1;
|
|
130
|
+
}
|
|
131
|
+
const intStart = i;
|
|
132
|
+
while (isDigit(s[i])) i++;
|
|
133
|
+
const intDigits = s.slice(intStart, i);
|
|
134
|
+
let fracDigits = '';
|
|
135
|
+
if (s[i] === '.') {
|
|
136
|
+
i++;
|
|
137
|
+
const fracStart = i;
|
|
138
|
+
while (isDigit(s[i])) i++;
|
|
139
|
+
fracDigits = s.slice(fracStart, i);
|
|
140
|
+
}
|
|
141
|
+
if (intDigits === '' && fracDigits === '') throw notADecimal();
|
|
142
|
+
let exponent = ZERO;
|
|
143
|
+
if (s[i] === 'e' || s[i] === 'E') {
|
|
144
|
+
i++;
|
|
145
|
+
let expNegative = false;
|
|
146
|
+
if (s[i] === '-' || s[i] === '+') {
|
|
147
|
+
expNegative = s[i] === '-';
|
|
148
|
+
i++;
|
|
149
|
+
}
|
|
150
|
+
const expStart = i;
|
|
151
|
+
while (isDigit(s[i])) i++;
|
|
152
|
+
if (i === expStart) throw notADecimal();
|
|
153
|
+
const expDigits = s.slice(expStart, i).replace(/^0+/, '');
|
|
154
|
+
// Any exponent this long is far past the caps below.
|
|
155
|
+
if (expDigits.length > 12) throw overCap(s);
|
|
156
|
+
const magnitude = expDigits === '' ? ZERO : BigInt(expDigits);
|
|
157
|
+
exponent = expNegative ? -magnitude : magnitude;
|
|
158
|
+
}
|
|
159
|
+
if (i !== s.length) throw notADecimal();
|
|
160
|
+
|
|
161
|
+
const significant = (intDigits + fracDigits).replace(/^0+/, '');
|
|
162
|
+
const scale = BigInt(fracDigits.length) - exponent;
|
|
163
|
+
if (significant === '') {
|
|
164
|
+
// Zero has no sign and keeps its scale (`0.00` is scale 2); `0E+3` is scale 0.
|
|
165
|
+
const zeroScale = scale < ZERO ? ZERO : scale;
|
|
166
|
+
if (zeroScale > BigInt(MAX_SCALE)) throw overCap(s);
|
|
167
|
+
return { unscaled: ZERO, scale: smallInt(zeroScale) };
|
|
168
|
+
}
|
|
169
|
+
const zerosToAppend = scale < ZERO ? -scale : ZERO;
|
|
170
|
+
if (scale > BigInt(MAX_SCALE) || BigInt(significant.length) + zerosToAppend > BigInt(MAX_DIGITS)) {
|
|
171
|
+
throw overCap(s);
|
|
172
|
+
}
|
|
173
|
+
const magnitude = BigInt(significant + '0'.repeat(smallInt(zerosToAppend)));
|
|
174
|
+
return {
|
|
175
|
+
unscaled: negative ? -magnitude : magnitude,
|
|
176
|
+
scale: smallInt(scale < ZERO ? ZERO : scale),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Plain notation, never an exponent, keeping trailing zeros: unscaled 150 scale 2 is `1.50`. */
|
|
181
|
+
export function toCanonicalString(v: CompactDecimal): string {
|
|
182
|
+
const negative = v.unscaled < ZERO;
|
|
183
|
+
const digits = (negative ? -v.unscaled : v.unscaled).toString();
|
|
184
|
+
const sign = negative ? '-' : '';
|
|
185
|
+
if (v.scale === 0) return sign + digits;
|
|
186
|
+
if (digits.length > v.scale) {
|
|
187
|
+
return `${sign}${digits.slice(0, digits.length - v.scale)}.${digits.slice(digits.length - v.scale)}`;
|
|
188
|
+
}
|
|
189
|
+
return `${sign}0.${'0'.repeat(v.scale - digits.length)}${digits}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Minimal two's-complement big-endian bytes. Zero is the single byte 0x00. */
|
|
193
|
+
function bigIntToBytes(n: bigint): Uint8Array {
|
|
194
|
+
let hex: string;
|
|
195
|
+
if (n >= ZERO) {
|
|
196
|
+
hex = n.toString(16);
|
|
197
|
+
if (hex.length % 2 === 1) hex = '0' + hex;
|
|
198
|
+
// A leading byte >= 0x80 would read back as negative.
|
|
199
|
+
if (/^[89a-f]/.test(hex)) hex = '00' + hex;
|
|
200
|
+
} else {
|
|
201
|
+
const magnitude = -n;
|
|
202
|
+
let length = (magnitude.toString(16).length + 1) >> 1;
|
|
203
|
+
// The most negative value in `length` bytes is -(2^(8*length - 1)).
|
|
204
|
+
if (magnitude > ONE << (BigInt(length) * EIGHT - ONE)) length++;
|
|
205
|
+
hex = ((ONE << (BigInt(length) * EIGHT)) + n).toString(16);
|
|
206
|
+
hex = '0'.repeat(length * 2 - hex.length) + hex;
|
|
207
|
+
}
|
|
208
|
+
const out = new Uint8Array(hex.length / 2);
|
|
209
|
+
for (let i = 0; i < out.length; i++) {
|
|
210
|
+
out[i] = (hexValue(hex[2 * i]) << 4) | hexValue(hex[2 * i + 1]);
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function hexValue(c: string): number {
|
|
216
|
+
return '0123456789abcdef'.indexOf(c);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Reads two's-complement big-endian bytes of any length; non-minimal input is fine. */
|
|
220
|
+
function bytesToBigInt(bytes: Uint8Array): bigint {
|
|
221
|
+
if (bytes.length === 0) return ZERO;
|
|
222
|
+
let hex = '';
|
|
223
|
+
for (const b of bytes) hex += (b < 16 ? '0' : '') + b.toString(16);
|
|
224
|
+
const unsigned = BigInt('0x' + hex);
|
|
225
|
+
return bytes[0] & 0x80 ? unsigned - (ONE << (BigInt(bytes.length) * EIGHT)) : unsigned;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Decodes the compact form (fields 11 and 12). Rejects a scale over
|
|
230
|
+
* `MAX_SCALE`, then bytes over `MAX_UNSCALED_BYTES`, then more than
|
|
231
|
+
* `MAX_DIGITS` digits, before any large allocation (the same order as Rust).
|
|
232
|
+
*/
|
|
233
|
+
function decodeCompact(unscaledValue: Uint8Array, scale: number): CompactDecimal {
|
|
234
|
+
if (scale > MAX_SCALE) {
|
|
235
|
+
throw invalid(`decimal has scale ${scale}, over the limit of ${MAX_SCALE}`);
|
|
236
|
+
}
|
|
237
|
+
if (unscaledValue.length > MAX_UNSCALED_BYTES) {
|
|
238
|
+
throw invalid(`decimal has a ${unscaledValue.length}-byte unscaled_value, over the limit of ${MAX_UNSCALED_BYTES} bytes`);
|
|
239
|
+
}
|
|
240
|
+
const unscaled = bytesToBigInt(unscaledValue);
|
|
241
|
+
if ((unscaled < ZERO ? -unscaled : unscaled).toString().length > MAX_DIGITS) {
|
|
242
|
+
throw invalid(`decimal has more than ${MAX_DIGITS} digits`);
|
|
243
|
+
}
|
|
244
|
+
return { unscaled, scale };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** True when neither form is set: `unscaled_value` and `arbitrary_precision_value` are both empty. */
|
|
248
|
+
export function isUnsetDecimal(p: DecimalValueProto): boolean {
|
|
249
|
+
return p.getUnscaledValue_asU8().length === 0 && p.getArbitraryPrecisionValue() === '';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Decodes a `DecimalValueProto`. The compact form wins when set. Otherwise
|
|
254
|
+
* falls back to the deprecated string and logs a deprecation warning, once
|
|
255
|
+
* per process. Both forms empty is a ModelValidationError; so is a malformed
|
|
256
|
+
* or over-cap value. Takes the proto only (owner ruling, 12 Oct): label
|
|
257
|
+
* errors with `withField`.
|
|
258
|
+
*/
|
|
259
|
+
export function decodeDecimal(p: DecimalValueProto): CompactDecimal {
|
|
260
|
+
const unscaledValue = p.getUnscaledValue_asU8();
|
|
261
|
+
if (unscaledValue.length > 0) {
|
|
262
|
+
return decodeCompact(unscaledValue, p.getScale());
|
|
263
|
+
}
|
|
264
|
+
const legacy = p.getArbitraryPrecisionValue();
|
|
265
|
+
if (legacy === '') {
|
|
266
|
+
throw invalid('decimal is required but unset');
|
|
267
|
+
}
|
|
268
|
+
emitDeprecation();
|
|
269
|
+
return parseDecimalString(legacy);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Encodes both forms (stage 1): fields 11 and 12, plus the canonical string
|
|
274
|
+
* in field 10 for readers on ledger-models older than v0.4.34.
|
|
275
|
+
*/
|
|
276
|
+
export function encodeDecimal(v: CompactDecimal): DecimalValueProto {
|
|
277
|
+
if (typeof v.unscaled !== 'bigint') {
|
|
278
|
+
throw invalid('decimal unscaled value must be a bigint');
|
|
279
|
+
}
|
|
280
|
+
if (!Number.isInteger(v.scale) || v.scale < 0 || v.scale > MAX_SCALE) {
|
|
281
|
+
throw invalid(`decimal scale must be an integer from 0 to ${MAX_SCALE}: ${v.scale}`);
|
|
282
|
+
}
|
|
283
|
+
return new DecimalValueProto()
|
|
284
|
+
.setArbitraryPrecisionValue(toCanonicalString(v))
|
|
285
|
+
.setUnscaledValue(bigIntToBytes(v.unscaled))
|
|
286
|
+
.setScale(v.scale);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* `p` rewritten in both forms (stage 1), keeping its value and scale. Used
|
|
291
|
+
* when re-saving a message loaded from older bytes.
|
|
292
|
+
*/
|
|
293
|
+
export function reencodeDecimal(p: DecimalValueProto): DecimalValueProto {
|
|
294
|
+
return encodeDecimal(decodeDecimal(p));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The value as a decimal.js `Decimal`, built from the canonical string. The
|
|
299
|
+
* `Decimal` constructor is exact and ignores the global `precision`. A
|
|
300
|
+
* `Decimal` does not keep trailing zeros, so `1.50` reads back as `1.5`.
|
|
301
|
+
*/
|
|
302
|
+
export function toDecimalJs(v: CompactDecimal): Decimal {
|
|
303
|
+
return new Decimal(toCanonicalString(v));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* The exact value of a decimal.js `Decimal`, from `toFixed()`, which never
|
|
308
|
+
* rounds when given no argument. A `Decimal` does not keep trailing zeros, so
|
|
309
|
+
* `new Decimal('1.50')` becomes scale 1: callers that need scale 2 use
|
|
310
|
+
* `parseDecimalString('1.50')`. Negative zero becomes 0. NaN, Infinity and
|
|
311
|
+
* values over the limits are a ModelValidationError.
|
|
312
|
+
*/
|
|
313
|
+
export function fromDecimalJs(d: Decimal): CompactDecimal {
|
|
314
|
+
if (!d.isFinite()) {
|
|
315
|
+
throw invalid(`decimal is not a finite decimal: ${d.toString()}`);
|
|
316
|
+
}
|
|
317
|
+
// Checked before toFixed(), which would write every digit out.
|
|
318
|
+
if (d.e >= MAX_DIGITS || d.decimalPlaces() > MAX_SCALE) {
|
|
319
|
+
throw overCap(d.toString());
|
|
320
|
+
}
|
|
321
|
+
return parseDecimalString(d.toFixed());
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Logs that a decimal was read from the deprecated string, the first time
|
|
326
|
+
* only in this process.
|
|
327
|
+
*/
|
|
328
|
+
function emitDeprecation(): void {
|
|
329
|
+
if (warned) return;
|
|
330
|
+
warned = true;
|
|
331
|
+
logger.warn(
|
|
332
|
+
'DecimalValueProto read from deprecated arbitrary_precision_value; '
|
|
333
|
+
+ `writer predates ledger-models ${FIRST_COMPACT_RELEASE}`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
const fs = __importStar(require("fs"));
|
|
27
|
+
const path = __importStar(require("path"));
|
|
28
|
+
/**
|
|
29
|
+
* Metric 4 (LM-306): the published typings mark the deprecated string
|
|
30
|
+
* accessors. compile.sh adds the tags after protoc-gen-ts runs.
|
|
31
|
+
*/
|
|
32
|
+
const TYPINGS = path.join(__dirname, '../../../fintekkers/models/util/decimal_value_pb.d.ts');
|
|
33
|
+
test.each(['getArbitraryPrecisionValue', 'setArbitraryPrecisionValue'])('%s is @deprecated', (accessor) => {
|
|
34
|
+
const lines = fs.readFileSync(TYPINGS, 'utf8').split('\n');
|
|
35
|
+
const at = lines.findIndex(l => l.trim().startsWith(`${accessor}(`));
|
|
36
|
+
expect(at).toBeGreaterThan(0);
|
|
37
|
+
expect(lines[at - 1].trim()).toMatch(/^\/\*\* @deprecated .*\*\/$/);
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=decimal.typings.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decimal.typings.test.js","sourceRoot":"","sources":["decimal.typings.test.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uCAAyB;AACzB,2CAA6B;AAE7B;;;GAGG;AAEH,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,uDAAuD,CAAC,CAAC;AAE9F,IAAI,CAAC,IAAI,CAAC,CAAC,4BAA4B,EAAE,4BAA4B,CAAC,CAAC,CAAC,mBAAmB,EAAE,CAAC,QAAQ,EAAE,EAAE;IACtG,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3D,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,QAAQ,GAAG,CAAC,CAAC,CAAC;IACrE,MAAM,CAAC,EAAE,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IAC9B,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,6BAA6B,CAAC,CAAC;AACxE,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Metric 4 (LM-306): the published typings mark the deprecated string
|
|
6
|
+
* accessors. compile.sh adds the tags after protoc-gen-ts runs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const TYPINGS = path.join(__dirname, '../../../fintekkers/models/util/decimal_value_pb.d.ts');
|
|
10
|
+
|
|
11
|
+
test.each(['getArbitraryPrecisionValue', 'setArbitraryPrecisionValue'])('%s is @deprecated', (accessor) => {
|
|
12
|
+
const lines = fs.readFileSync(TYPINGS, 'utf8').split('\n');
|
|
13
|
+
const at = lines.findIndex(l => l.trim().startsWith(`${accessor}(`));
|
|
14
|
+
expect(at).toBeGreaterThan(0);
|
|
15
|
+
expect(lines[at - 1].trim()).toMatch(/^\/\*\* @deprecated .*\*\/$/);
|
|
16
|
+
});
|
|
@@ -9,7 +9,8 @@ import { ProtoEnum } from "./protoEnum";
|
|
|
9
9
|
import { UUID } from "./uuid";
|
|
10
10
|
import { StringValue } from 'google-protobuf/google/protobuf/wrappers_pb';
|
|
11
11
|
import { Identifier } from "../security/identifier";
|
|
12
|
+
import { Decimal } from "decimal.js";
|
|
12
13
|
export declare class ProtoSerializationUtil {
|
|
13
14
|
static serialize(obj: any): UUIDProto | LocalDateProto | LocalTimestampProto | DecimalValueProto | StringValue | IdentifierProto;
|
|
14
|
-
static deserialize(obj: any): UUID | Date | ZonedDateTime | Identifier | string |
|
|
15
|
+
static deserialize(obj: any): UUID | Date | ZonedDateTime | Identifier | string | Decimal | ProtoEnum | StrategyAllocationProto;
|
|
15
16
|
}
|