Skip to content

Commit 0853c39

Browse files
authored
feat(xdr): add validateXdr static to every generated type (#1597)
* feat(xdr): add validateXdr static to every generated type * docs(xdr): file validateXdr under renames, not removals
1 parent 9159189 commit 0853c39

4 files changed

Lines changed: 92 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ A breaking change will get clearly marked in this log.
1111
* Enums are singletons, not factory calls: `xdr.ContractDataDurability.persistent()` becomes `xdr.ContractDataDurability.persistent`.
1212
* Primitives are plain JS values. Integers are `number` or `bigint` instead of class wrappers, `LargeInt` subclasses are gone, byte fields are `Uint8Array`, and fields are `readonly`.
1313
* Absent optional fields decode to `null` instead of `undefined`, so `=== undefined` checks silently stop matching. Prefer `== null`.
14-
* Acronyms in method names collapse to single-initial-cap form, with no back-compat aliases. This reaches beyond the `xdr` namespace to the wrapper classes: `Transaction#toXDR()`, `TransactionBuilder.fromXDR()`, `Operation.fromXDRObject()`, `Asset#toXDRObject()`, `contract.AssembledTransaction#toXDR()` and others all gained the `Xdr` spelling.
14+
* Acronyms in method names collapse to single-initial-cap form, with no back-compat aliases (e.g. `validateXDR()` is now `validateXdr()`). This reaches beyond the `xdr` namespace to the wrapper classes: `Transaction#toXDR()`, `TransactionBuilder.fromXDR()`, `Operation.fromXDRObject()`, `Asset#toXDRObject()`, `contract.AssembledTransaction#toXDR()` and others all gained the `Xdr` spelling.
1515
* Struct field names are unchanged, but a few type names moved: `UInt128Parts` / `UInt256Parts` are now `Uint128Parts` / `Uint256Parts`, `ThresholdIndices` is now `ThresholdIndexes`, and the typedef aliases `Duration`, `TimePoint`, `SequenceNumber`, `ScVec`, `ScMap`, `LedgerEntryChanges`, `ContractCostParams`, `SorobanAuthorizationEntries`, `ScString`, `ScSymbol`, `String32`, `String64`, and `SponsorshipDescriptor` are gone in favor of what they stood for.
1616
* New: `toJson()` / `fromJson()` for [SEP-0051](https://stellar.org/protocol/sep-51) JSON, `toXdrObject()` / `fromXdrObject()` on XDR values, and `equals()` for structural comparison. Failures throw `xdr.XdrError`, which is now exported.
17-
* Removed: `Reader` and `Writer`; the v4 runtime type constructors (`Hyper`, `UnsignedHyper`, `Option`, `Opaque`, `VarOpaque`, `XDRArray`, `XDRString`, `Bool`, `SignedInt`, `UnsignedInt`), plus top-level `Hyper` / `UnsignedHyper` / `cereal`; the `validateXDR()` static on every type (use `try`/`catch` around `fromXdr`); and `xdr.scvSortedMap` (use the top-level `scvSortedMap`).
17+
* Removed: `Reader` and `Writer`; the v4 runtime type constructors (`Hyper`, `UnsignedHyper`, `Option`, `Opaque`, `VarOpaque`, `XDRArray`, `XDRString`, `Bool`, `SignedInt`, `UnsignedInt`), plus top-level `Hyper` / `UnsignedHyper` / `cereal`; and `xdr.scvSortedMap` (use the top-level `scvSortedMap`).
1818
* `ScInt` and `XdrLargeInt` lost their `.int` property; read `.value` (a `bigint`) instead, and note `valueOf()` now returns a `bigint`.
1919

2020
[`docs/XDR_MIGRATION.md`](./docs/XDR_MIGRATION.md) covers every change with before/after examples and a quick-reference table.

docs/XDR_MIGRATION.md

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ error; `TypeError: … is not a function` in plain JavaScript):
3434
| -------------------------------- | -------------------------------- |
3535
| `value.toXDR()` | `value.toXdr()` |
3636
| `Class.fromXDR(…)` | `Class.fromXdr(…)` |
37+
| `Class.validateXDR(…)` | `Class.validateXdr(…)` |
3738
| `value.toXDRObject()` | `value.toXdrObject()` |
3839
| `Class.fromXDRObject(…)` | `Class.fromXdrObject(…)` |
3940
| `asset.toChangeTrustXDRObject()` | `asset.toChangeTrustXdrObject()` |
@@ -531,6 +532,7 @@ new xdr.Int32(v) → Number(v)
531532
.toXDR() → .toXdr()
532533
.toXDR().toString("base64") → .toXdr("base64")
533534
.fromXDR(buf, "base64") → .fromXdr(buf, "base64")
535+
.validateXDR(s, "base64") → .validateXdr(s, "base64")
534536

535537
// ============== METHODS (new — no legacy equivalent) ==============
536538
.toXdrObject() / .fromXdrObject(wire)
@@ -565,7 +567,6 @@ scInt.int / xli.int → scInt.value / xli.value (bigint) — `.int`
565567
x === undefinedx == null // decoded absent = null now
566568

567569
// ============== REMOVED (§ 13) ==============
568-
Type.validateXDR(s, "base64") → try { Type.fromXdr(s, "base64") } catch {}
569570
xdr.scvSortedMap(entries) → scvSortedMap(entries) // top-level export
570571
xdr.Hyper / xdr.Option / xdr.Opaque / xdr.XDRString / … → (gone; see § 13)
571572
Hyper, UnsignedHyper, cereal → (gone from top-level)
@@ -832,22 +833,19 @@ them; the schema builders behind them are internal.
832833
### `validateXDR` and `xdr.scvSortedMap`
833834

834835
- **`validateXDR(input, format)`** was a static on every generated type — a
835-
cheap "is this decodable?" check. It has no replacement; wrap `fromXdr` in a
836-
`try`/`catch` instead (§ 15).
836+
"is this decodable?" check. It survives as **`validateXdr`** (casing now
837+
matches `fromXdr`/`toXdr`): `Uint8Array` input, or a string with
838+
`"hex" | "base64"`. As with `fromXdr`, the optional `"raw"` format argument
839+
is gone — pass bytes alone (§ 7). It does a full decode and returns a
840+
boolean; it never throws. When you need the failure reason, call `fromXdr`
841+
in a `try`/`catch` instead (§ 15).
837842

838843
```ts
839844
// Before
840845
if (xdr.TransactionEnvelope.validateXDR(str, "base64")) { … }
841846

842847
// After
843-
function isValid(str: string): boolean {
844-
try {
845-
xdr.TransactionEnvelope.fromXdr(str, "base64");
846-
return true;
847-
} catch {
848-
return false;
849-
}
850-
}
848+
if (xdr.TransactionEnvelope.validateXdr(str, "base64")) { … }
851849
```
852850

853851
- **`xdr.scvSortedMap()`** is gone. The legacy SDK monkey-patched it onto the
@@ -943,7 +941,8 @@ consume (`"source buffer not entirely consumed"`), which is now
943941
fixed-size fields, out-of-range integers, unknown union discriminants, and
944942
unknown enum values all throw as before, with new wording.
945943

946-
There is no `validateXDR` any more — see § 13 for the try/catch replacement.
944+
`validateXDR` survives as `validateXdr` (casing matches `fromXdr`/`toXdr`) —
945+
see § 13. It returns a bare boolean; decode directly when you need the error.
947946

948947
### Enum lookup
949948

src/xdr/values/xdr-value.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export interface XdrValueConstructor<Wire, Instance extends XdrValue> {
3838
* - `static fromXdrObject(wire: Wire): InstanceType` going the other way
3939
* - `toJson(): JsonValue`
4040
*
41-
* Inherited helpers (`toXdr`, `fromXdr`, `toString`, `equals`) handle the rest.
41+
* Inherited helpers (`toXdr`, `fromXdr`, `validateXdr`, `toString`, `equals`)
42+
* handle the rest.
4243
*/
4344
export abstract class XdrValue {
4445
abstract toXdrObject(): unknown;
@@ -107,6 +108,35 @@ export abstract class XdrValue {
107108
return this.fromXdrObject(this.schema.decode(bytes));
108109
}
109110

111+
/**
112+
* Check whether `input` decodes as this type — {@link XdrValue.fromXdr}
113+
* without the throw. Returns `false` on any failure (bad hex/base64, wrong
114+
* shape, trailing bytes) and discards the error detail; decode directly
115+
* when you need the reason.
116+
*/
117+
static validateXdr<Wire, Instance extends XdrValue>(
118+
this: XdrValueConstructor<Wire, Instance>,
119+
input: Uint8Array,
120+
): boolean;
121+
static validateXdr<Wire, Instance extends XdrValue>(
122+
this: XdrValueConstructor<Wire, Instance>,
123+
input: string,
124+
format: "hex" | "base64",
125+
): boolean;
126+
static validateXdr<Wire, Instance extends XdrValue>(
127+
this: XdrValueConstructor<Wire, Instance>,
128+
input: Uint8Array | string,
129+
format?: "hex" | "base64",
130+
): boolean {
131+
let bytes: Uint8Array;
132+
try {
133+
bytes = decodeBytes(input, format);
134+
} catch {
135+
return false;
136+
}
137+
return this.schema.validateXdr(bytes);
138+
}
139+
110140
static fromJson<Wire, Instance extends XdrValue>(
111141
this: XdrValueConstructor<Wire, Instance>,
112142
json: JsonValue,

test/unit/xdr/validate_xdr.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, it, expect } from "vitest";
2+
3+
import {
4+
Price,
5+
ScVal,
6+
TransactionEnvelope,
7+
AssetType,
8+
} from "../../../src/xdr/index.js";
9+
10+
describe("validateXdr", () => {
11+
it("returns true for valid raw bytes", () => {
12+
const bytes = new Price({ n: 1, d: 2 }).toXdr();
13+
expect(Price.validateXdr(bytes)).toBe(true);
14+
});
15+
16+
it("returns true for valid base64 and hex strings", () => {
17+
const val = ScVal.scvSymbol("hi");
18+
expect(ScVal.validateXdr(val.toXdr("base64"), "base64")).toBe(true);
19+
expect(ScVal.validateXdr(val.toXdr("hex"), "hex")).toBe(true);
20+
});
21+
22+
it("works on enums", () => {
23+
const bytes = AssetType.assetTypeNative.toXdr();
24+
expect(AssetType.validateXdr(bytes)).toBe(true);
25+
expect(AssetType.validateXdr(new Uint8Array([0, 0, 0, 99]))).toBe(false);
26+
});
27+
28+
it("returns false for bytes of the wrong type", () => {
29+
const bytes = new Price({ n: 1, d: 2 }).toXdr();
30+
expect(TransactionEnvelope.validateXdr(bytes)).toBe(false);
31+
});
32+
33+
it("returns false for trailing bytes", () => {
34+
const bytes = new Price({ n: 1, d: 2 }).toXdr();
35+
const padded = new Uint8Array([...bytes, 0, 0, 0, 0]);
36+
expect(Price.validateXdr(padded)).toBe(false);
37+
});
38+
39+
it("returns false for truncated input", () => {
40+
const bytes = new Price({ n: 1, d: 2 }).toXdr().slice(0, -1);
41+
expect(Price.validateXdr(bytes)).toBe(false);
42+
});
43+
44+
it("returns false (not a throw) on malformed base64 and hex", () => {
45+
expect(ScVal.validateXdr("not base64!!!", "base64")).toBe(false);
46+
expect(ScVal.validateXdr("zz-not-hex", "hex")).toBe(false);
47+
});
48+
});

0 commit comments

Comments
 (0)