Skip to content

Commit b770de4

Browse files
authored
fix: correct XdrLargeInt range errors, fix a dead test (#1659)
* fix: name the actual constraint in XdrLargeInt range errors * fix(test): make the muxed key assertion compare bytes again * refactor: reuse intRange for XdrLargeInt bounds * Updated docs
1 parent c78dcf4 commit b770de4

8 files changed

Lines changed: 255 additions & 103 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ A breaking change will get clearly marked in this log.
1919
+xdr.TransactionMeta.v3(transactionMetaV3);
2020
```
2121

22+
* `XdrLargeInt` encoding errors name the actual constraint. `toI64()`/`toI128()`/`toI256()` reported `value too large for i64: <v>` with no indication of the valid range, and now report `bigint value <v> for i64 out of range [<min>, <max>]`. The width check reported `value too large for 64 bits (i128)` even when the value was `1` — it rejects the declared type, not the value — and now reports `cannot encode i128 as 64 bits`. `toNumber()` printed its safe-integer range with the bounds reversed. For these range violations the error is unchanged apart from its text: the same `RangeError` on the same inputs.
23+
24+
* `XdrLargeInt` coerces a `toBigInt()` hook result with `BigInt(...)` instead of requiring an exact `bigint`. A custom object whose `toBigInt()` returns a bigint-convertible value — a `number` such as `5`, or `""` — is now accepted and normalized rather than throwing; a result that cannot be converted (`"abc"`, `NaN`, `null`, `{}`) still throws. This is what keeps `value` a genuine `bigint`: the range check compares with `<` and `>`, which yield `false` for a string or `NaN`, so without the coercion such a value passed every check and `toNumber()` returned `NaN` instead of throwing.
25+
2226
* `equals()` on XDR values is now callable from TypeScript on union types like `xdr.ScVal`, `xdr.TransactionEnvelope`, and `xdr.Memo` — which is what the SDK's accessors return ([#1630](https://github.com/stellar/js-stellar-sdk/issues/1630)). The parameter was typed as polymorphic `this`, which reduces to `never` on a union, so every call failed with TS2345 even though the runtime worked. The parameter is now `XdrValue`, so comparing two different XDR types compiles and returns `false`.
2327

2428
* `Keypair.verify` and `Keypair.verifyMessage` throw a `TypeError` for arguments whose type they don't accept, instead of returning `false` ([#1649](https://github.com/stellar/js-stellar-sdk/pull/1649)). Both previously swallowed every error and reported `false`, so a caller mistake was indistinguishable from an invalid signature. `verify` requires `data` to be a `Uint8Array` and `signature` to be either a `Uint8Array` or an `xdr.Signature`; `verifyMessage` takes the same `signature` and a `message` that is a string or a `Uint8Array`. Anything else now throws — a hex/base64 signature string, a plain array of byte values, the `xdr.DecoratedSignature` that `tx.signatures[0]` holds, or a `message` that is neither string nor bytes. A well-formed signature that doesn't match still returns `false`. Accepting an `xdr.Signature` — what `DecoratedSignature.signature` holds — means `kp.verify(tx.hash(), tx.signatures[0].signature)` works again. `authorizeEntry` likewise rejects a signer result it would have passed on unchecked — a callback returning none of its three shapes, a non-bytes `signature`, a non-string `publicKey`, or a `signatureScVal` that isn't an `xdr.ScVal`.

docs/reference/core-transactions.md

Lines changed: 45 additions & 45 deletions
Large diffs are not rendered by default.

src/base/numbers/sc_int.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ import { XdrLargeInt, type ScIntType } from "./xdr_large_int.js";
2424
* sci.toBigInt(); // gives the native BigInt value
2525
* sci.toU64(); // gives ScValType-specific XDR constructs (with size checks)
2626
*
27-
* // You have a number and want to shove it into a contract.
28-
* sci = new ScInt(0xdeadcafebabe);
29-
* sci.toBigInt() // returns 244838016400062n
30-
* sci.toNumber() // throws: too large
27+
* // You have a large value and want to shove it into a contract.
28+
* sci = new ScInt(0xdeadcafebabedeadn);
29+
* sci.toBigInt() // returns 16045704242794520237n
30+
* sci.toNumber() // throws: not in range for Number
3131
*
3232
* // Pass any to e.g. a Contract.call(), conversion happens automatically
3333
* // regardless of the initial type.

src/base/numbers/xdr_large_int.ts

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Uint128Parts,
66
Uint256Parts,
77
} from "../../xdr/index.js";
8+
import { intRange } from "../../xdr/values/bigint-parts.js";
89

910
type BigIntLike = { toBigInt(): bigint };
1011
type XdrLargeIntValues =
@@ -46,6 +47,34 @@ const SIGNED: Readonly<Record<ScIntType, boolean>> = {
4647
duration: false,
4748
};
4849

50+
/**
51+
* Throws a `RangeError` if `value` can't be represented as `name`, a `bits`-wide
52+
* integer.
53+
*
54+
* A negative value for an unsigned type is reported as a sign error rather than
55+
* as an overflow, and the valid range appears in the out-of-range message, so
56+
* the text states why the value was actually rejected. Both messages match the
57+
* ones `js-xdr` produced through v16.
58+
*/
59+
function assertInRange(
60+
value: bigint,
61+
name: string,
62+
bits: 64 | 128 | 256,
63+
signed: boolean,
64+
): void {
65+
if (!signed && value < 0n) {
66+
throw new RangeError(`expected a positive value, got: ${value}`);
67+
}
68+
69+
const [min, max] = intRange(signed, bits);
70+
71+
if (value < min || value > max) {
72+
throw new RangeError(
73+
`bigint value ${value} for ${name} out of range [${min}, ${max}]`,
74+
);
75+
}
76+
}
77+
4978
/**
5079
* A wrapper class to represent large XDR-encodable integers.
5180
*
@@ -83,7 +112,10 @@ export class XdrLargeInt {
83112
"toBigInt" in i &&
84113
typeof (i as BigIntLike).toBigInt === "function"
85114
) {
86-
return (i as BigIntLike).toBigInt();
115+
// `toBigInt()` is a consumer-supplied hook, so its result is coerced
116+
// rather than trusted: the range check below compares with `<` and
117+
// `>`, which yield `false` instead of throwing for a non-bigint.
118+
return BigInt((i as BigIntLike).toBigInt());
87119
}
88120
return BigInt(i as number | string);
89121
},
@@ -103,18 +135,7 @@ export class XdrLargeInt {
103135
// `LargeInt`-backed implementation enforced this at construction time and
104136
// several callers (notably `nativeToScVal` via `ScInt`) depend on it.
105137
if (parts.length === 1) {
106-
const bits = SIZE[type];
107-
if (SIGNED[type]) {
108-
if (BigInt.asIntN(bits, value) !== value) {
109-
throw new RangeError(
110-
`value too large for ${bits}-bit ${type}: ${value}`,
111-
);
112-
}
113-
} else if (value < 0n || BigInt.asUintN(bits, value) !== value) {
114-
throw new RangeError(
115-
`value too large for ${bits}-bit ${type}: ${value}`,
116-
);
117-
}
138+
assertInRange(value, type, SIZE[type], SIGNED[type]);
118139
}
119140

120141
this.value = value;
@@ -131,7 +152,7 @@ export class XdrLargeInt {
131152
if (bi > Number.MAX_SAFE_INTEGER || bi < Number.MIN_SAFE_INTEGER) {
132153
throw RangeError(
133154
`value ${bi} not in range for Number ` +
134-
`[${Number.MAX_SAFE_INTEGER}, ${Number.MIN_SAFE_INTEGER}]`,
155+
`[${Number.MIN_SAFE_INTEGER}, ${Number.MAX_SAFE_INTEGER}]`,
135156
);
136157
}
137158
return Number(bi);
@@ -150,9 +171,7 @@ export class XdrLargeInt {
150171
toI64(): ScVal {
151172
this._sizeCheck(64);
152173
const v = this.value;
153-
if (BigInt.asIntN(64, v) !== v) {
154-
throw RangeError(`value too large for i64: ${v}`);
155-
}
174+
assertInRange(v, "i64", 64, true);
156175
return ScVal.scvI64(v);
157176
}
158177

@@ -182,9 +201,7 @@ export class XdrLargeInt {
182201
toI128(): ScVal {
183202
this._sizeCheck(128);
184203
const v = this.value;
185-
if (BigInt.asIntN(128, v) !== v) {
186-
throw RangeError(`value too large for i128: ${v}`);
187-
}
204+
assertInRange(v, "i128", 128, true);
188205
return ScVal.scvI128(
189206
new Int128Parts({
190207
hi: BigInt.asIntN(64, v >> 64n),
@@ -216,9 +233,7 @@ export class XdrLargeInt {
216233
*/
217234
toI256(): ScVal {
218235
const v = this.value;
219-
if (BigInt.asIntN(256, v) !== v) {
220-
throw RangeError(`value too large for i256: ${v}`);
221-
}
236+
assertInRange(v, "i256", 256, true);
222237
return ScVal.scvI256(
223238
new Int256Parts({
224239
hiHi: BigInt.asIntN(64, v >> 192n),
@@ -297,8 +312,10 @@ export class XdrLargeInt {
297312
}
298313

299314
private _sizeCheck(bits: number): void {
315+
// This rejects the declared type, not the value: a small `i128` still can't
316+
// be encoded as 64 bits, so the message must not blame the value's size.
300317
if (SIZE[this.type] > bits) {
301-
throw RangeError(`value too large for ${bits} bits (${this.type})`);
318+
throw RangeError(`cannot encode ${this.type} as ${bits} bits`);
302319
}
303320
}
304321

test/unit/base/numbers/sc_int.test.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -553,8 +553,8 @@ describe("ScInt", () => {
553553
let b = new ScInt(sentinel);
554554
expect(b.toBigInt()).toBe(sentinel);
555555
expect(() => b.toNumber()).toThrow(/not in range/i);
556-
expect(() => b.toU64()).toThrow(/too large/i);
557-
expect(() => b.toI64()).toThrow(/too large/i);
556+
expect(() => b.toU64()).toThrow(/cannot encode u128 as 64 bits/);
557+
expect(() => b.toI64()).toThrow(/cannot encode u128 as 64 bits/);
558558

559559
let scvU = b.toU128();
560560
if (scvU.type !== "scvU128") throw new Error("expected scvU128");
@@ -672,15 +672,17 @@ describe("ScInt", () => {
672672
big = new ScInt(Number.MAX_SAFE_INTEGER + 1);
673673
expect(() => big.toNumber()).toThrow(/not in range/i);
674674

675+
// the value is 1: these fail on the declared type's width, not on the
676+
// value's magnitude, and the message says so
675677
big = new ScInt(1, { type: "i128" });
676-
expect(() => big.toU64()).toThrow(/too large/i);
677-
expect(() => big.toI64()).toThrow(/too large/i);
678+
expect(() => big.toU64()).toThrow(/cannot encode i128 as 64 bits/);
679+
expect(() => big.toI64()).toThrow(/cannot encode i128 as 64 bits/);
678680

679681
big = new ScInt(1, { type: "i256" });
680-
expect(() => big.toU64()).toThrow(/too large/i);
681-
expect(() => big.toI64()).toThrow(/too large/i);
682-
expect(() => big.toI128()).toThrow(/too large/i);
683-
expect(() => big.toU128()).toThrow(/too large/i);
682+
expect(() => big.toU64()).toThrow(/cannot encode i256 as 64 bits/);
683+
expect(() => big.toI64()).toThrow(/cannot encode i256 as 64 bits/);
684+
expect(() => big.toI128()).toThrow(/cannot encode i256 as 128 bits/);
685+
expect(() => big.toU128()).toThrow(/cannot encode i256 as 128 bits/);
684686
});
685687
});
686688
});

0 commit comments

Comments
 (0)