@fintekkers/ledger-models 0.4.31 → 0.4.32

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.
@@ -0,0 +1,20 @@
1
+ import { PriceProto } from '../../../fintekkers/models/price/price_pb';
2
+ import { FieldViolationProto } from '../../../fintekkers/requests/util/errors/field_violation_pb';
3
+ /**
4
+ * Price input rules (LM-289): JS counterpart of Java's PriceRules
5
+ * (docs/adr/typed-input-errors.md). Pure: proto in, violations out.
6
+ *
7
+ * Three rules, checked in this order: `security` has a non-empty UUID,
8
+ * `price` has a non-empty arbitrary_precision_value (zero is valid, see the
9
+ * PriceProto comment), and `price_type` is not UNKNOWN_PRICE_TYPE. The price
10
+ * value is not parsed. Never throws on bad or unset input; null or undefined
11
+ * is checked as an empty PriceProto. Unlike security_rules, a link price is
12
+ * not skipped: hydrate it first. The Price wrapper does not call these rules.
13
+ */
14
+ export declare const SECURITY = "security";
15
+ export declare const PRICE = "price";
16
+ export declare const PRICE_TYPE = "price_type";
17
+ /** Returns every field-level violation on this price, or an empty array when it is valid. */
18
+ export declare function validate(proto: PriceProto | null | undefined): FieldViolationProto[];
19
+ /** Throws ModelValidationError carrying every violation if `validate` finds any. */
20
+ export declare function requireValid(proto: PriceProto | null | undefined): void;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireValid = exports.validate = exports.PRICE_TYPE = exports.PRICE = exports.SECURITY = void 0;
4
+ const price_pb_1 = require("../../../fintekkers/models/price/price_pb");
5
+ const price_type_pb_1 = require("../../../fintekkers/models/price/price_type_pb");
6
+ const errors_1 = require("../errors");
7
+ const uuid_1 = require("../utils/uuid");
8
+ /**
9
+ * Price input rules (LM-289): JS counterpart of Java's PriceRules
10
+ * (docs/adr/typed-input-errors.md). Pure: proto in, violations out.
11
+ *
12
+ * Three rules, checked in this order: `security` has a non-empty UUID,
13
+ * `price` has a non-empty arbitrary_precision_value (zero is valid, see the
14
+ * PriceProto comment), and `price_type` is not UNKNOWN_PRICE_TYPE. The price
15
+ * value is not parsed. Never throws on bad or unset input; null or undefined
16
+ * is checked as an empty PriceProto. Unlike security_rules, a link price is
17
+ * not skipped: hydrate it first. The Price wrapper does not call these rules.
18
+ */
19
+ exports.SECURITY = 'security';
20
+ exports.PRICE = 'price';
21
+ exports.PRICE_TYPE = 'price_type';
22
+ /** Returns every field-level violation on this price, or an empty array when it is valid. */
23
+ function validate(proto) {
24
+ var _a, _b, _c;
25
+ const p = proto !== null && proto !== void 0 ? proto : new price_pb_1.PriceProto();
26
+ const id = objectIdOf(p);
27
+ const out = [];
28
+ if (!((_b = (_a = p.getSecurity()) === null || _a === void 0 ? void 0 : _a.getUuid()) === null || _b === void 0 ? void 0 : _b.getRawUuid_asU8().length)) {
29
+ out.push((0, errors_1.violation)(exports.SECURITY, id, 'security with a non-empty uuid is required'));
30
+ }
31
+ if (!((_c = p.getPrice()) === null || _c === void 0 ? void 0 : _c.getArbitraryPrecisionValue())) {
32
+ out.push((0, errors_1.violation)(exports.PRICE, id, 'price is required'));
33
+ }
34
+ if (p.getPriceType() === price_type_pb_1.PriceTypeProto.UNKNOWN_PRICE_TYPE) {
35
+ out.push((0, errors_1.violation)(exports.PRICE_TYPE, id, 'price_type must not be UNKNOWN_PRICE_TYPE'));
36
+ }
37
+ return out;
38
+ }
39
+ exports.validate = validate;
40
+ /** Throws ModelValidationError carrying every violation if `validate` finds any. */
41
+ function requireValid(proto) {
42
+ const violations = validate(proto);
43
+ if (violations.length > 0)
44
+ throw new errors_1.ModelValidationError(violations);
45
+ }
46
+ exports.requireValid = requireValid;
47
+ /** The price's UUID, or undefined when it has none or it is not 16 bytes. */
48
+ function objectIdOf(proto) {
49
+ var _a;
50
+ const raw = (_a = proto.getUuid()) === null || _a === void 0 ? void 0 : _a.getRawUuid_asU8();
51
+ return raw && raw.length === 16 ? uuid_1.UUID.fromU8Array(raw) : undefined;
52
+ }
53
+ //# sourceMappingURL=price_rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"price_rules.js","sourceRoot":"","sources":["price_rules.ts"],"names":[],"mappings":";;;AAAA,wEAAuE;AACvE,kFAAgF;AAEhF,sCAA4D;AAC5D,wCAAqC;AAErC;;;;;;;;;;GAUG;AAEU,QAAA,QAAQ,GAAG,UAAU,CAAC;AACtB,QAAA,KAAK,GAAG,OAAO,CAAC;AAChB,QAAA,UAAU,GAAG,YAAY,CAAC;AAEvC,6FAA6F;AAC7F,SAAgB,QAAQ,CAAC,KAAoC;;IACzD,MAAM,CAAC,GAAG,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,IAAI,qBAAU,EAAE,CAAC;IACpC,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;IACzB,MAAM,GAAG,GAA0B,EAAE,CAAC;IACtC,IAAI,CAAC,CAAA,MAAA,MAAA,CAAC,CAAC,WAAW,EAAE,0CAAE,OAAO,EAAE,0CAAE,eAAe,GAAG,MAAM,CAAA,EAAE;QACvD,GAAG,CAAC,IAAI,CAAC,IAAA,kBAAS,EAAC,gBAAQ,EAAE,EAAE,EAAE,4CAA4C,CAAC,CAAC,CAAC;KACnF;IACD,IAAI,CAAC,CAAA,MAAA,CAAC,CAAC,QAAQ,EAAE,0CAAE,0BAA0B,EAAE,CAAA,EAAE;QAC7C,GAAG,CAAC,IAAI,CAAC,IAAA,kBAAS,EAAC,aAAK,EAAE,EAAE,EAAE,mBAAmB,CAAC,CAAC,CAAC;KACvD;IACD,IAAI,CAAC,CAAC,YAAY,EAAE,KAAK,8BAAc,CAAC,kBAAkB,EAAE;QACxD,GAAG,CAAC,IAAI,CAAC,IAAA,kBAAS,EAAC,kBAAU,EAAE,EAAE,EAAE,2CAA2C,CAAC,CAAC,CAAC;KACpF;IACD,OAAO,GAAG,CAAC;AACf,CAAC;AAdD,4BAcC;AAED,oFAAoF;AACpF,SAAgB,YAAY,CAAC,KAAoC;IAC7D,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACnC,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,6BAAoB,CAAC,UAAU,CAAC,CAAC;AAC1E,CAAC;AAHD,oCAGC;AAED,6EAA6E;AAC7E,SAAS,UAAU,CAAC,KAAiB;;IACjC,MAAM,GAAG,GAAG,MAAA,KAAK,CAAC,OAAO,EAAE,0CAAE,eAAe,EAAE,CAAC;IAC/C,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,WAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACxE,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ // LM-289: the shared price rule set. The same table runs in Java, Rust and Python.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const price_pb_1 = require("../../../fintekkers/models/price/price_pb");
5
+ const price_type_pb_1 = require("../../../fintekkers/models/price/price_type_pb");
6
+ const security_pb_1 = require("../../../fintekkers/models/security/security_pb");
7
+ const decimal_value_pb_1 = require("../../../fintekkers/models/util/decimal_value_pb");
8
+ const uuid_pb_1 = require("../../../fintekkers/models/util/uuid_pb");
9
+ const errors_1 = require("../errors");
10
+ const uuid_1 = require("../utils/uuid");
11
+ const price_rules_1 = require("./price_rules");
12
+ const PRICE_ID = '6a1d2c3e-4b5f-4a6d-8e7f-9a0b1c2d3e4f';
13
+ const SECURITY_ID = '1f0e8a4c-58b1-4d0e-9a3c-7d2b6e5f4a10';
14
+ function decimal(value) {
15
+ return new decimal_value_pb_1.DecimalValueProto().setArbitraryPrecisionValue(value);
16
+ }
17
+ /** A price that passes every rule. */
18
+ function valid() {
19
+ return new price_pb_1.PriceProto()
20
+ .setUuid(new uuid_1.UUID(uuid_1.UUID.fromString(PRICE_ID)).toUUIDProto())
21
+ .setSecurity(new security_pb_1.SecurityProto().setUuid(new uuid_1.UUID(uuid_1.UUID.fromString(SECURITY_ID)).toUUIDProto()))
22
+ .setPrice(decimal('100.25'))
23
+ .setPriceType(price_type_pb_1.PriceTypeProto.PERCENTAGE);
24
+ }
25
+ /** `valid()` with `change` applied; the generated clear* setters return void. */
26
+ function validWith(change) {
27
+ const p = valid();
28
+ change(p);
29
+ return p;
30
+ }
31
+ function fields(proto) {
32
+ return (0, price_rules_1.validate)(proto).map(v => v.getField());
33
+ }
34
+ describe('price_rules.validate', () => {
35
+ test('security absent gives one violation on security', () => {
36
+ expect(fields(validWith(p => p.clearSecurity()))).toEqual(['security']);
37
+ });
38
+ test('empty security uuid gives one violation on security', () => {
39
+ expect(fields(valid().setSecurity(new security_pb_1.SecurityProto().setUuid(new uuid_pb_1.UUIDProto())))).toEqual(['security']);
40
+ });
41
+ test('security present but uuid unset gives one violation on security', () => {
42
+ expect(fields(valid().setSecurity(new security_pb_1.SecurityProto()))).toEqual(['security']);
43
+ });
44
+ test('price absent gives one violation on price', () => {
45
+ expect(fields(validWith(p => p.clearPrice()))).toEqual(['price']);
46
+ });
47
+ test('empty price gives one violation on price', () => {
48
+ expect(fields(valid().setPrice(decimal('')))).toEqual(['price']);
49
+ });
50
+ test('zero price is valid', () => {
51
+ expect(fields(valid().setPrice(decimal('0')))).toEqual([]);
52
+ });
53
+ test.each(['-1', 'abc'])('price value %s is not checked', value => {
54
+ expect(fields(valid().setPrice(decimal(value)))).toEqual([]);
55
+ });
56
+ test('UNKNOWN_PRICE_TYPE gives one violation on price_type', () => {
57
+ expect(fields(valid().setPriceType(price_type_pb_1.PriceTypeProto.UNKNOWN_PRICE_TYPE))).toEqual(['price_type']);
58
+ });
59
+ test('unrecognised price_type is valid', () => {
60
+ expect(fields(valid().setPriceType(99))).toEqual([]);
61
+ });
62
+ test('all three problems give three violations in field order', () => {
63
+ const proto = validWith(p => { p.clearSecurity(); p.clearPrice(); p.setPriceType(price_type_pb_1.PriceTypeProto.UNKNOWN_PRICE_TYPE); });
64
+ const violations = (0, price_rules_1.validate)(proto);
65
+ expect(violations.map(v => v.getField())).toEqual(['security', 'price', 'price_type']);
66
+ for (const v of violations) {
67
+ expect(uuid_1.UUID.fromU8Array(v.getObjectId().getRawUuid_asU8()).toString()).toBe(PRICE_ID);
68
+ }
69
+ });
70
+ test('every known price type is valid', () => {
71
+ const values = Object.values(price_type_pb_1.PriceTypeProto).filter((v) => typeof v === 'number');
72
+ expect(values).toContain(price_type_pb_1.PriceTypeProto.PERCENTAGE);
73
+ for (const value of values) {
74
+ if (value === price_type_pb_1.PriceTypeProto.UNKNOWN_PRICE_TYPE)
75
+ continue;
76
+ expect(fields(valid().setPriceType(value))).toEqual([]);
77
+ }
78
+ });
79
+ test('empty, null or undefined proto gives three violations without throwing', () => {
80
+ const expected = ['security', 'price', 'price_type'];
81
+ expect(fields(new price_pb_1.PriceProto())).toEqual(expected);
82
+ expect(fields(null)).toEqual(expected);
83
+ expect(fields(undefined)).toEqual(expected);
84
+ expect((0, price_rules_1.validate)(null)[0].hasObjectId()).toBe(false);
85
+ });
86
+ });
87
+ describe('price_rules.requireValid', () => {
88
+ test('throws the typed error carrying the violations', () => {
89
+ const proto = validWith(p => p.clearPrice());
90
+ let err;
91
+ try {
92
+ (0, price_rules_1.requireValid)(proto);
93
+ }
94
+ catch (e) {
95
+ err = e;
96
+ }
97
+ expect(err).toBeInstanceOf(errors_1.ModelValidationError);
98
+ expect(err.violations.map(v => v.getField())).toEqual(['price']);
99
+ expect(() => (0, price_rules_1.requireValid)(valid())).not.toThrow();
100
+ });
101
+ });
102
+ describe('the published (compiled) price_rules.js', () => {
103
+ // npm publish runs no tsc: the tracked .js is what consumers get.
104
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
105
+ const compiled = require('./price_rules.js');
106
+ test('all three problems give three violations in field order', () => {
107
+ const proto = validWith(p => { p.clearSecurity(); p.clearPrice(); p.setPriceType(price_type_pb_1.PriceTypeProto.UNKNOWN_PRICE_TYPE); });
108
+ const violations = compiled.validate(proto);
109
+ expect(violations.map(v => v.getField())).toEqual(['security', 'price', 'price_type']);
110
+ });
111
+ });
112
+ //# sourceMappingURL=price_rules.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"price_rules.test.js","sourceRoot":"","sources":["price_rules.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,128 @@
1
+ // LM-289: the shared price rule set. The same table runs in Java, Rust and Python.
2
+
3
+ import { PriceProto } from '../../../fintekkers/models/price/price_pb';
4
+ import { PriceTypeProto } from '../../../fintekkers/models/price/price_type_pb';
5
+ import { SecurityProto } from '../../../fintekkers/models/security/security_pb';
6
+ import { DecimalValueProto } from '../../../fintekkers/models/util/decimal_value_pb';
7
+ import { UUIDProto } from '../../../fintekkers/models/util/uuid_pb';
8
+ import { ModelValidationError } from '../errors';
9
+ import { UUID } from '../utils/uuid';
10
+ import { requireValid, validate } from './price_rules';
11
+
12
+ const PRICE_ID = '6a1d2c3e-4b5f-4a6d-8e7f-9a0b1c2d3e4f';
13
+ const SECURITY_ID = '1f0e8a4c-58b1-4d0e-9a3c-7d2b6e5f4a10';
14
+
15
+ function decimal(value: string): DecimalValueProto {
16
+ return new DecimalValueProto().setArbitraryPrecisionValue(value);
17
+ }
18
+
19
+ /** A price that passes every rule. */
20
+ function valid(): PriceProto {
21
+ return new PriceProto()
22
+ .setUuid(new UUID(UUID.fromString(PRICE_ID)).toUUIDProto())
23
+ .setSecurity(new SecurityProto().setUuid(new UUID(UUID.fromString(SECURITY_ID)).toUUIDProto()))
24
+ .setPrice(decimal('100.25'))
25
+ .setPriceType(PriceTypeProto.PERCENTAGE);
26
+ }
27
+
28
+ /** `valid()` with `change` applied; the generated clear* setters return void. */
29
+ function validWith(change: (p: PriceProto) => void): PriceProto {
30
+ const p = valid();
31
+ change(p);
32
+ return p;
33
+ }
34
+
35
+ function fields(proto: PriceProto | null | undefined): string[] {
36
+ return validate(proto).map(v => v.getField());
37
+ }
38
+
39
+ describe('price_rules.validate', () => {
40
+ test('security absent gives one violation on security', () => {
41
+ expect(fields(validWith(p => p.clearSecurity()))).toEqual(['security']);
42
+ });
43
+
44
+ test('empty security uuid gives one violation on security', () => {
45
+ expect(fields(valid().setSecurity(new SecurityProto().setUuid(new UUIDProto())))).toEqual(['security']);
46
+ });
47
+
48
+ test('security present but uuid unset gives one violation on security', () => {
49
+ expect(fields(valid().setSecurity(new SecurityProto()))).toEqual(['security']);
50
+ });
51
+
52
+ test('price absent gives one violation on price', () => {
53
+ expect(fields(validWith(p => p.clearPrice()))).toEqual(['price']);
54
+ });
55
+
56
+ test('empty price gives one violation on price', () => {
57
+ expect(fields(valid().setPrice(decimal('')))).toEqual(['price']);
58
+ });
59
+
60
+ test('zero price is valid', () => {
61
+ expect(fields(valid().setPrice(decimal('0')))).toEqual([]);
62
+ });
63
+
64
+ test.each(['-1', 'abc'])('price value %s is not checked', value => {
65
+ expect(fields(valid().setPrice(decimal(value)))).toEqual([]);
66
+ });
67
+
68
+ test('UNKNOWN_PRICE_TYPE gives one violation on price_type', () => {
69
+ expect(fields(valid().setPriceType(PriceTypeProto.UNKNOWN_PRICE_TYPE))).toEqual(['price_type']);
70
+ });
71
+
72
+ test('unrecognised price_type is valid', () => {
73
+ expect(fields(valid().setPriceType(99 as PriceTypeProto))).toEqual([]);
74
+ });
75
+
76
+ test('all three problems give three violations in field order', () => {
77
+ const proto = validWith(p => { p.clearSecurity(); p.clearPrice(); p.setPriceType(PriceTypeProto.UNKNOWN_PRICE_TYPE); });
78
+ const violations = validate(proto);
79
+ expect(violations.map(v => v.getField())).toEqual(['security', 'price', 'price_type']);
80
+ for (const v of violations) {
81
+ expect(UUID.fromU8Array(v.getObjectId()!.getRawUuid_asU8()).toString()).toBe(PRICE_ID);
82
+ }
83
+ });
84
+
85
+ test('every known price type is valid', () => {
86
+ const values = Object.values(PriceTypeProto).filter((v): v is PriceTypeProto => typeof v === 'number');
87
+ expect(values).toContain(PriceTypeProto.PERCENTAGE);
88
+ for (const value of values) {
89
+ if (value === PriceTypeProto.UNKNOWN_PRICE_TYPE) continue;
90
+ expect(fields(valid().setPriceType(value))).toEqual([]);
91
+ }
92
+ });
93
+
94
+ test('empty, null or undefined proto gives three violations without throwing', () => {
95
+ const expected = ['security', 'price', 'price_type'];
96
+ expect(fields(new PriceProto())).toEqual(expected);
97
+ expect(fields(null)).toEqual(expected);
98
+ expect(fields(undefined)).toEqual(expected);
99
+ expect(validate(null)[0].hasObjectId()).toBe(false);
100
+ });
101
+ });
102
+
103
+ describe('price_rules.requireValid', () => {
104
+ test('throws the typed error carrying the violations', () => {
105
+ const proto = validWith(p => p.clearPrice());
106
+ let err: unknown;
107
+ try {
108
+ requireValid(proto);
109
+ } catch (e) {
110
+ err = e;
111
+ }
112
+ expect(err).toBeInstanceOf(ModelValidationError);
113
+ expect((err as ModelValidationError).violations.map(v => v.getField())).toEqual(['price']);
114
+ expect(() => requireValid(valid())).not.toThrow();
115
+ });
116
+ });
117
+
118
+ describe('the published (compiled) price_rules.js', () => {
119
+ // npm publish runs no tsc: the tracked .js is what consumers get.
120
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
121
+ const compiled = require('./price_rules.js');
122
+
123
+ test('all three problems give three violations in field order', () => {
124
+ const proto = validWith(p => { p.clearSecurity(); p.clearPrice(); p.setPriceType(PriceTypeProto.UNKNOWN_PRICE_TYPE); });
125
+ const violations = compiled.validate(proto) as { getField(): string }[];
126
+ expect(violations.map(v => v.getField())).toEqual(['security', 'price', 'price_type']);
127
+ });
128
+ });
@@ -0,0 +1,50 @@
1
+ import { PriceProto } from '../../../fintekkers/models/price/price_pb';
2
+ import { PriceTypeProto } from '../../../fintekkers/models/price/price_type_pb';
3
+ import { FieldViolationProto } from '../../../fintekkers/requests/util/errors/field_violation_pb';
4
+ import { ModelValidationError, violation } from '../errors';
5
+ import { UUID } from '../utils/uuid';
6
+
7
+ /**
8
+ * Price input rules (LM-289): JS counterpart of Java's PriceRules
9
+ * (docs/adr/typed-input-errors.md). Pure: proto in, violations out.
10
+ *
11
+ * Three rules, checked in this order: `security` has a non-empty UUID,
12
+ * `price` has a non-empty arbitrary_precision_value (zero is valid, see the
13
+ * PriceProto comment), and `price_type` is not UNKNOWN_PRICE_TYPE. The price
14
+ * value is not parsed. Never throws on bad or unset input; null or undefined
15
+ * is checked as an empty PriceProto. Unlike security_rules, a link price is
16
+ * not skipped: hydrate it first. The Price wrapper does not call these rules.
17
+ */
18
+
19
+ export const SECURITY = 'security';
20
+ export const PRICE = 'price';
21
+ export const PRICE_TYPE = 'price_type';
22
+
23
+ /** Returns every field-level violation on this price, or an empty array when it is valid. */
24
+ export function validate(proto: PriceProto | null | undefined): FieldViolationProto[] {
25
+ const p = proto ?? new PriceProto();
26
+ const id = objectIdOf(p);
27
+ const out: FieldViolationProto[] = [];
28
+ if (!p.getSecurity()?.getUuid()?.getRawUuid_asU8().length) {
29
+ out.push(violation(SECURITY, id, 'security with a non-empty uuid is required'));
30
+ }
31
+ if (!p.getPrice()?.getArbitraryPrecisionValue()) {
32
+ out.push(violation(PRICE, id, 'price is required'));
33
+ }
34
+ if (p.getPriceType() === PriceTypeProto.UNKNOWN_PRICE_TYPE) {
35
+ out.push(violation(PRICE_TYPE, id, 'price_type must not be UNKNOWN_PRICE_TYPE'));
36
+ }
37
+ return out;
38
+ }
39
+
40
+ /** Throws ModelValidationError carrying every violation if `validate` finds any. */
41
+ export function requireValid(proto: PriceProto | null | undefined): void {
42
+ const violations = validate(proto);
43
+ if (violations.length > 0) throw new ModelValidationError(violations);
44
+ }
45
+
46
+ /** The price's UUID, or undefined when it has none or it is not 16 bytes. */
47
+ function objectIdOf(proto: PriceProto): UUID | undefined {
48
+ const raw = proto.getUuid()?.getRawUuid_asU8();
49
+ return raw && raw.length === 16 ? UUID.fromU8Array(raw) : undefined;
50
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fintekkers/ledger-models",
3
3
  "todo": "Replace the version with build script version number",
4
- "version": "0.4.31",
4
+ "version": "0.4.32",
5
5
  "description": "ledger model protos ",
6
6
  "authors": [
7
7
  "David Doherty",