diff --git a/CHANGELOG.md b/CHANGELOG.md index 4862a470..1ae242e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,31 @@ ## Unreleased +### Breaking Changes +* `XdrLargeInt` has been renamed to just `Int` ([#809](https://github.com/stellar/js-stellar-base/pull/809)). +* `ScInt` and `scValToBigInt` have been removed to simplify the API surface. Please migrate in the following way ([#809](https://github.com/stellar/js-stellar-base/pull/809)): +```typescript +// before: +new ScInt(value); +new ScInt(value, { type: 'i128' }); +// after: +Int.fromValue(value); +new Int('i128', value); + +// before: +scValToBigInt(scv); +// after: +Int.fromScVal(scv).toBigInt(); +``` + +### Added +* `XdrLargeInt` (renamed to `Int`) has new features ([#809](https://github.com/stellar/js-stellar-base/pull/809)): + - `fromValue(x: number|string|BigInt)` will create an `Int` of an appopriate size for the given value + - `fromScVal(x: xdr.ScVal)` will create an `Int` from an `ScVal` + - `.toU32()` will return an `ScVal` of the type `scvU32` + - `.toI32()` will return an `ScVal` of the type `scvI32` + - the constructor now accepts `i32` and `u32` as the first `type` parameter + ## [`v14.0.0-rc.2`](https://github.com/stellar/js-stellar-base/compare/v13.1.0...v14.0.0-rc.2) diff --git a/src/numbers/index.js b/src/numbers/index.js index e2b6244a..9a453d8d 100644 --- a/src/numbers/index.js +++ b/src/numbers/index.js @@ -4,56 +4,4 @@ export { Uint128 } from './uint128'; export { Uint256 } from './uint256'; export { Int128 } from './int128'; export { Int256 } from './int256'; -export { ScInt } from './sc_int'; -export { XdrLargeInt }; - -/** - * Transforms an opaque {@link xdr.ScVal} into a native bigint, if possible. - * - * If you then want to use this in the abstractions provided by this module, - * you can pass it to the constructor of {@link XdrLargeInt}. - * - * @example - * let scv = contract.call("add", x, y); // assume it returns an xdr.ScVal - * let bigi = scValToBigInt(scv); - * - * new ScInt(bigi); // if you don't care about types, and - * new XdrLargeInt('i128', bigi); // if you do - * - * @param {xdr.ScVal} scv - the raw XDR value to parse into an integer - * @returns {bigint} the native value of this input value - * - * @throws {TypeError} if the `scv` input value doesn't represent an integer - */ -export function scValToBigInt(scv) { - const scIntType = XdrLargeInt.getType(scv.switch().name); - - switch (scv.switch().name) { - case 'scvU32': - case 'scvI32': - return BigInt(scv.value()); - - case 'scvU64': - case 'scvI64': - return new XdrLargeInt(scIntType, scv.value()).toBigInt(); - - case 'scvU128': - case 'scvI128': - return new XdrLargeInt(scIntType, [ - scv.value().lo(), - scv.value().hi() - ]).toBigInt(); - - case 'scvU256': - case 'scvI256': - return new XdrLargeInt(scIntType, [ - scv.value().loLo(), - scv.value().loHi(), - scv.value().hiLo(), - scv.value().hiHi() - ]).toBigInt(); - - default: - throw TypeError(`expected integer type, got ${scv.switch()}`); - } -} +export { XdrLargeInt as Int }; diff --git a/src/numbers/sc_int.js b/src/numbers/sc_int.js deleted file mode 100644 index 7f81d83a..00000000 --- a/src/numbers/sc_int.js +++ /dev/null @@ -1,112 +0,0 @@ -import { XdrLargeInt } from './xdr_large_int'; - -/** - * Provides an easier way to manipulate large numbers for Stellar operations. - * - * You can instantiate this "**s**mart **c**ontract integer" value either from - * bigints, strings, or numbers (whole numbers, or this will throw). - * - * If you need to create a native BigInt from a list of integer "parts" (for - * example, you have a series of encoded 32-bit integers that represent a larger - * value), you can use the lower level abstraction {@link XdrLargeInt}. For - * example, you could do `new XdrLargeInt('u128', bytes...).toBigInt()`. - * - * @example - * import { xdr, ScInt, scValToBigInt } from "@stellar/stellar-base"; - * - * // You have an ScVal from a contract and want to parse it into JS native. - * const value = xdr.ScVal.fromXDR(someXdr, "base64"); - * const bigi = scValToBigInt(value); // grab it as a BigInt - * let sci = new ScInt(bigi); - * - * sci.toNumber(); // gives native JS type (w/ size check) - * sci.toBigInt(); // gives the native BigInt value - * sci.toU64(); // gives ScValType-specific XDR constructs (with size checks) - * - * // You have a number and want to shove it into a contract. - * sci = ScInt(0xdeadcafebabe); - * sci.toBigInt() // returns 244838016400062n - * sci.toNumber() // throws: too large - * - * // Pass any to e.g. a Contract.call(), conversion happens automatically - * // regardless of the initial type. - * const scValU128 = sci.toU128(); - * const scValI256 = sci.toI256(); - * const scValU64 = sci.toU64(); - * - * // Lots of ways to initialize: - * ScInt("123456789123456789") - * ScInt(123456789123456789n); - * ScInt(1n << 140n); - * ScInt(-42); - * ScInt(scValToBigInt(scValU128)); // from above - * - * // If you know the type ahead of time (accessing `.raw` is faster than - * // conversions), you can specify the type directly (otherwise, it's - * // interpreted from the numbers you pass in): - * const i = ScInt(123456789n, { type: "u256" }); - * - * // For example, you can use the underlying `sdk.U256` and convert it to an - * // `xdr.ScVal` directly like so: - * const scv = new xdr.ScVal.scvU256(i.raw); - * - * // Or reinterpret it as a different type (size permitting): - * const scv = i.toI64(); - * - * @param {number|bigint|string} value - a single, integer-like value which will - * be interpreted in the smallest appropriate XDR type supported by Stellar - * (64, 128, or 256 bit integer values). signed values are supported, though - * they are sanity-checked against `opts.type`. if you need 32-bit values, - * you can construct them directly without needing this wrapper, e.g. - * `xdr.ScVal.scvU32(1234)`. - * - * @param {object} [opts] - an optional object controlling optional parameters - * @param {string} [opts.type] - force a specific data type. the type choices - * are: 'i64', 'u64', 'i128', 'u128', 'i256', and 'u256' (default: the - * smallest one that fits the `value`) - * - * @throws {RangeError} if the `value` is invalid (e.g. floating point), too - * large (i.e. exceeds a 256-bit value), or doesn't fit in the `opts.type` - * @throws {TypeError} on missing parameters, or if the "signedness" of `opts` - * doesn't match input `value`, e.g. passing `{type: 'u64'}` yet passing -1n - * @throws {SyntaxError} if a string `value` can't be parsed as a big integer - */ -export class ScInt extends XdrLargeInt { - constructor(value, opts) { - const signed = value < 0; - let type = opts?.type ?? ''; - if (type.startsWith('u') && signed) { - throw TypeError(`specified type ${opts.type} yet negative (${value})`); - } - - // If unspecified, we make a best guess at the type based on the bit length - // of the value, treating 64 as a minimum and 256 as a maximum. - if (type === '') { - type = signed ? 'i' : 'u'; - const bitlen = nearestBigIntSize(value); - - switch (bitlen) { - case 64: - case 128: - case 256: - type += bitlen.toString(); - break; - - default: - throw RangeError( - `expected 64/128/256 bits for input (${value}), got ${bitlen}` - ); - } - } - - super(type, value); - } -} - -function nearestBigIntSize(bigI) { - // Note: Even though BigInt.toString(2) includes the negative sign for - // negative values (???), the following is still accurate, because the - // negative sign would be represented by a sign bit. - const bitlen = bigI.toString(2).length; - return [64, 128, 256].find((len) => bitlen <= len) ?? bitlen; -} diff --git a/src/numbers/xdr_large_int.js b/src/numbers/xdr_large_int.js index 66c02818..7c516605 100644 --- a/src/numbers/xdr_large_int.js +++ b/src/numbers/xdr_large_int.js @@ -9,21 +9,55 @@ import { Int256 } from './int256'; import xdr from '../xdr'; /** - * A wrapper class to represent large XDR-encodable integers. + * Provides a way to manipulate large numbers for Stellar operations. * - * This operates at a lower level than {@link ScInt} by forcing you to specify - * the type / width / size in bits of the integer you're targeting, regardless - * of the input value(s) you provide. + * You must first specify the type / width / size in bits of the integer you're + * targeting, regardless of the input value(s) you provide. Then, you pass one, + * or a list of bigints, strings, or numbers (*whole* numbers, or this will + * throw). + * + * For example, if you have a series of encoded 32-bit integers that represent a + * larger value, you `new XdrLargeInt('u128', bytes...).toBigInt()`. + * + * @example + * import { xdr, Int } from "@stellar/stellar-base"; + * + * // You have an ScVal from a contract and want to parse it into JS native. + * const value = xdr.ScVal.fromXDR(someXdr, "base64"); + * const i = Int.fromScVal(value); + * + * i.toNumber(); // gives native JS type (w/ size check) + * i.toBigInt(); // gives the native BigInt value + * i.toU64(); // gives ScValType-specific XDR constructs (w/ size checks) + * + * // You have a number and want to shove it into a contract. + * i = new Int('i128', 0xdeadcafebabe); + * i.toBigInt() // returns 244838016400062n + * i.toNumber() // throws: too large + * + * // Pass any to e.g. a `Contract.call(...)`, conversion happens automatically + * // regardless of the initially-specified type. + * const scValU128 = i.toU128(); + * const scValI256 = i.toI256(); + * const scValU64 = i.toU64(); + * const scValU32 = i.toU32(); // throws: too large + * + * // Lots of ways to initialize: + * Int("i256", "123456789123456789") + * Int("u256", 123456789123456789n); + * Int("i256", 1n << 140n); + * Int("i32", -42); + * Int("i256", [1, "2", 3n]); * * @param {string} type - specifies a data type to use to represent the, one - * of: 'i64', 'u64', 'i128', 'u128', 'i256', and 'u256' (see + * of: 'i32', 'u32', i64', 'u64', 'i128', 'u128', 'i256', and 'u256' (see * {@link XdrLargeInt.isType}) * @param {number|bigint|string|Array} values a list of * integer-like values interpreted in big-endian order */ export class XdrLargeInt { - /** @type {xdr.LargeInt} */ - int; // child class of a jsXdr.LargeInt + /** @type {xdr.LargeInt|BigInt} */ + int; // child class of a jsXdr.LargeInt or a native BigInt in the case of <= 32 bits /** @type {string} */ type; @@ -46,6 +80,22 @@ export class XdrLargeInt { }); switch (type) { + // 32 bits is a special case because there's no jsXdr.LargeInt equivalent + // to rely on: use a BigInt and size-check it immediately. + case 'i32': + case 'u32': + if (values.length > 1 || !Number.isInteger(Number(values[0]))) { + throw new TypeError(`only 1 int is allowed for ${type}: ${values}`); + } + this.int = values[0]; + if ( + (type === 'i32' ? BigInt.asIntN : BigInt.asUintN)(32, this.int) !== + this.int + ) { + throw new RangeError(`${this.int} is more than 32 bits`); + } + break; + case 'i64': this.int = new Hyper(values); break; @@ -76,7 +126,7 @@ export class XdrLargeInt { * @throws {RangeError} if the value can't fit into a Number */ toNumber() { - const bi = this.int.toBigInt(); + const bi = this.toBigInt(); if (bi > Number.MAX_SAFE_INTEGER || bi < Number.MIN_SAFE_INTEGER) { throw RangeError( `value ${bi} not in range for Number ` + @@ -89,7 +139,23 @@ export class XdrLargeInt { /** @returns {bigint} */ toBigInt() { - return this.int.toBigInt(); + switch (this.type) { + case 'u32': + case 'i32': + return this.int; + default: + return this.int.toBigInt(); + } + } + + /** @returns {xdr.ScVal} the integer encoded with `ScValType = U32` */ + toU32() { + return xdr.ScVal.scvU32(this.toNumber()); + } + + /** @returns {xdr.ScVal} the integer encoded with `ScValType = I32` */ + toI32() { + return xdr.ScVal.scvU32(this.toNumber()); } /** @returns {xdr.ScVal} the integer encoded with `ScValType = I64` */ @@ -118,7 +184,7 @@ export class XdrLargeInt { toI128() { this._sizeCheck(128); - const v = this.int.toBigInt(); + const v = this.toBigInt(); const hi64 = BigInt.asIntN(64, v >> 64n); // encode top 64 w/ sign bit const lo64 = BigInt.asUintN(64, v); // grab btm 64, encode sign @@ -136,7 +202,7 @@ export class XdrLargeInt { */ toU128() { this._sizeCheck(128); - const v = this.int.toBigInt(); + const v = this.toBigInt(); return xdr.ScVal.scvU128( new xdr.UInt128Parts({ @@ -148,7 +214,7 @@ export class XdrLargeInt { /** @returns {xdr.ScVal} the integer encoded with `ScValType = I256` */ toI256() { - const v = this.int.toBigInt(); + const v = this.toBigInt(); const hiHi64 = BigInt.asIntN(64, v >> 192n); // keep sign bit const hiLo64 = BigInt.asUintN(64, v >> 128n); const loHi64 = BigInt.asUintN(64, v >> 64n); @@ -166,7 +232,7 @@ export class XdrLargeInt { /** @returns {xdr.ScVal} the integer encoded with `ScValType = U256` */ toU256() { - const v = this.int.toBigInt(); + const v = this.toBigInt(); const hiHi64 = BigInt.asUintN(64, v >> 192n); // encode sign bit const hiLo64 = BigInt.asUintN(64, v >> 128n); const loHi64 = BigInt.asUintN(64, v >> 64n); @@ -182,9 +248,13 @@ export class XdrLargeInt { ); } - /** @returns {xdr.ScVal} the smallest interpretation of the stored value */ + /** @returns {xdr.ScVal} the specified interpretation of the stored value */ toScVal() { switch (this.type) { + case 'i32': + return this.toI32(); + case 'u32': + return this.toU32(); case 'i64': return this.toI64(); case 'i128': @@ -218,16 +288,32 @@ export class XdrLargeInt { } _sizeCheck(bits) { - if (this.int.size > bits) { + let tooBig = false; + + // Special case for (i|u)32: no .size to rely on, so try casting to 32 bits + // and comparing to ensure no bits are lost. + if (typeof this.int === 'bigint') { + if (this.type === 'u32') { + tooBig = BigInt.asUintN(32, this.int) !== this.int; + } else { + tooBig = BigInt.asIntN(32, this.int) !== this.int; + } + } else { + tooBig = this.int.size > bits; + } + + if (tooBig) { throw RangeError(`value too large for ${bits} bits (${this.type})`); } } static isType(type) { switch (type) { + case 'i32': case 'i64': case 'i128': case 'i256': + case 'u32': case 'u64': case 'u128': case 'u256': @@ -247,4 +333,66 @@ export class XdrLargeInt { static getType(scvType) { return scvType.slice(3).toLowerCase(); } + + static fromScVal(scv) { + const scIntType = XdrLargeInt.getType(scv.switch().name); + + switch (scv.switch().name) { + case 'scvU32': + case 'scvI32': + case 'scvU64': + case 'scvI64': + return new XdrLargeInt(scIntType, scv.value()); + + case 'scvU128': + case 'scvI128': + return new XdrLargeInt(scIntType, [scv.value().lo(), scv.value().hi()]); + + case 'scvU256': + case 'scvI256': + return new XdrLargeInt(scIntType, [ + scv.value().loLo(), + scv.value().loHi(), + scv.value().hiLo(), + scv.value().hiHi() + ]); + + default: + throw TypeError(`expected integer type, got ${scv.switch()}`); + } + } + + static fromValue(value) { + value = BigInt(value); + + // If unspecified, we make a best guess at the type based on the bit length + // of the value, treating 32 as a minimum and 256 as a maximum. + let type = value < 0 ? 'i' : 'u'; + const bitlen = nearestBigIntSize(value); + + switch (bitlen) { + case 32: + case 64: + case 128: + case 256: + type += bitlen.toString(); + break; + + default: + throw RangeError( + `expected 32/64/128/256 bits for input (${value}), got ${bitlen}` + ); + } + + return new XdrLargeInt(type, value); + } +} + +function nearestBigIntSize(bigI) { + const bitlen = bigI.toString(2).length; + + // Note: Even though BigInt.toString(2) includes the negative sign for + // negative values (???), the following is still accurate, because the + // negative sign would be represented by a sign bit. + return [32, 64, 128, 256].find((len) => bitlen <= len) ?? bitlen; } diff --git a/src/scval.js b/src/scval.js index 5f0e355c..bff9eb3d 100644 --- a/src/scval.js +++ b/src/scval.js @@ -3,7 +3,7 @@ import xdr from './xdr'; import { Keypair } from './keypair'; import { Address } from './address'; import { Contract } from './contract'; -import { ScInt, XdrLargeInt, scValToBigInt } from './numbers/index'; +import { Int } from './numbers'; /** * Attempts to convert native types into smart contract values @@ -21,7 +21,7 @@ import { ScInt, XdrLargeInt, scValToBigInt } from './numbers/index'; * - boolean -> scvBool * * - number/bigint -> the smallest possible XDR integer type that will fit the - * input value (if you want a specific type, use {@link ScInt}) + * input value (if you want a specific type, use {@link Int}) * * - {@link Address} or {@link Contract} -> scvAddress (for contracts and * public keys) @@ -46,7 +46,7 @@ import { ScInt, XdrLargeInt, scValToBigInt } from './numbers/index'; * types for `val`: * * - when `val` is an integer-like type (i.e. number|bigint), this will be - * forwarded to {@link ScInt} or forced to be u32/i32. + * forwarded to {@link Int}. * * - when `val` is an array type, this is forwarded to the recursion * @@ -61,7 +61,7 @@ import { ScInt, XdrLargeInt, scValToBigInt } from './numbers/index'; * - when `val` is a bytes-like type, this can be 'string', 'symbol', or * 'bytes' to force a particular interpretation * - * As a simple example, `nativeToScVal("hello", {type: 'symbol'})` will + * As a simple example, `nativeToScVal('hello', {type: 'symbol'})` will * return an `scvSymbol`, whereas without the type it would have been an * `scvString`. * @@ -243,7 +243,11 @@ export function nativeToScVal(val, opts = {}) { break; } - return new ScInt(val, { type: opts?.type }).toScVal(); + if ((opts?.type ?? '') !== '') { + return new Int(opts.type, val).toScVal(); + } + + return Int.fromValue(val).toScVal(); case 'string': { const optType = opts?.type ?? 'string'; @@ -264,8 +268,8 @@ export function nativeToScVal(val, opts = {}) { return xdr.ScVal.scvI32(parseInt(val, 10)); default: - if (XdrLargeInt.isType(optType)) { - return new XdrLargeInt(optType, val).toScVal(); + if (Int.isType(optType)) { + return new Int(optType, val).toScVal(); } throw new TypeError( @@ -331,7 +335,7 @@ export function scValToNative(scv) { case xdr.ScValType.scvI128().value: case xdr.ScValType.scvU256().value: case xdr.ScValType.scvI256().value: - return scValToBigInt(scv); + return Int.fromScVal(scv).toBigInt(); case xdr.ScValType.scvVec().value: return (scv.vec() ?? []).map(scValToNative); diff --git a/test/unit/events_test.js b/test/unit/events_test.js index cdfcd060..352022f2 100644 --- a/test/unit/events_test.js +++ b/test/unit/events_test.js @@ -1,10 +1,4 @@ -const [nativeToScVal, scValToNative, ScInt, humanizeEvents, xdr] = [ - StellarBase.nativeToScVal, - StellarBase.scValToNative, - StellarBase.ScInt, - StellarBase.humanizeEvents, - StellarBase.xdr -]; // shorthand +const { nativeToScVal, scValToNative, humanizeEvents, xdr } = StellarBase; // shorthand describe('humanizing raw events', function () { const contractId = 'CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE'; diff --git a/test/unit/scint_test.js b/test/unit/int_test.js similarity index 81% rename from test/unit/scint_test.js rename to test/unit/int_test.js index 5666bef8..c92cc302 100644 --- a/test/unit/scint_test.js +++ b/test/unit/int_test.js @@ -2,18 +2,19 @@ const I128 = StellarBase.Int128; const U128 = StellarBase.Uint128; const I256 = StellarBase.Int256; const U256 = StellarBase.Uint256; -const xdr = StellarBase.xdr; // shorthand +const { xdr, Int } = StellarBase; // shorthand describe('creating large integers', function () { describe('picks the right types', function () { Object.entries({ - u64: [1, '1', 0xdeadbeef, (1n << 64n) - 1n], + u32: [1, '1', 0xdeadbeef, (1n << 32n) - 2n], + u64: [2 ** 32, (2 ** 32).toFixed(0), 0x200000000, (1n << 64n) - 1n], u128: [1n << 64n, (1n << 128n) - 1n], u256: [1n << 128n, (1n << 256n) - 1n] }).forEach(([type, values]) => { values.forEach((value) => { it(`picks ${type} for ${value}`, function () { - const bi = new StellarBase.ScInt(value); + const bi = Int.fromValue(value); expect(bi.type).to.equal(type); expect(bi.toBigInt()).to.equal(BigInt(value)); }); @@ -24,11 +25,9 @@ describe('creating large integers', function () { it('has correct utility methods', function () { const v = 123456789123456789123456789123456789123456789123456789123456789123456789n; - const i = new StellarBase.ScInt(v); + const i = Int.fromValue(v); expect(i.valueOf()).to.be.eql(new U256(v)); - expect(i.toString()).to.equal( - '123456789123456789123456789123456789123456789123456789123456789123456789' - ); + expect(i.toString()).to.equal(v.toString()); expect(i.toJSON()).to.be.eql({ value: v.toString(), type: 'u256' }); }); @@ -36,14 +35,14 @@ describe('creating large integers', function () { const sentinel = 800000085n; it('handles u64', function () { - let b = new StellarBase.ScInt(sentinel); + let b = Int.fromValue(sentinel); expect(b.toBigInt()).to.equal(sentinel); expect(b.toNumber()).to.equal(Number(sentinel)); let u64 = b.toU64().u64(); expect(u64.low).to.equal(Number(sentinel)); expect(u64.high).to.equal(0); - b = new StellarBase.ScInt(-sentinel); + b = Int.fromValue(-sentinel); expect(b.toBigInt()).to.equal(-sentinel); expect(b.toNumber()).to.equal(Number(-sentinel)); u64 = b.toU64().u64(); @@ -52,7 +51,7 @@ describe('creating large integers', function () { }); it('handles i64', function () { - let b = new StellarBase.ScInt(sentinel); + let b = Int.fromValue(sentinel); expect(b.toBigInt()).to.equal(sentinel); expect(b.toNumber()).to.equal(Number(sentinel)); let i64 = b.toI64().i64(); @@ -61,14 +60,14 @@ describe('creating large integers', function () { }); it(`upscales u64 to 128`, function () { - const b = new StellarBase.ScInt(sentinel); + const b = Int.fromValue(sentinel); const i128 = b.toI128().i128(); expect(i128.lo().toBigInt()).to.equal(sentinel); expect(i128.hi().toBigInt()).to.equal(0n); }); it(`upscales i64 to 128`, function () { - const b = new StellarBase.ScInt(-sentinel); + const b = Int.fromValue(-sentinel); const i128 = b.toI128().i128(); const hi = i128.hi().toBigInt(); const lo = i128.lo().toBigInt(); @@ -78,7 +77,7 @@ describe('creating large integers', function () { }); it(`upscales i64 to 256`, function () { - const b = new StellarBase.ScInt(sentinel); + const b = Int.fromValue(sentinel); const i = b.toI256().i256(); const [hiHi, hiLo, loHi, loLo] = [ @@ -101,7 +100,7 @@ describe('creating large integers', function () { }); it(`upscales i64 to 256`, function () { - const b = new StellarBase.ScInt(-sentinel); + const b = Int.fromValue(-sentinel); const i = b.toI256().i256(); const [hiHi, hiLo, loHi, loLo] = [ @@ -128,7 +127,7 @@ describe('creating large integers', function () { const sentinel = 800000000000000000000085n; // 80 bits long it('handles inputs', function () { - let b = new StellarBase.ScInt(sentinel); + let b = Int.fromValue(sentinel); expect(b.toBigInt()).to.equal(sentinel); expect(() => b.toNumber()).to.throw(/not in range/i); expect(() => b.toU64()).to.throw(/too large/i); @@ -144,7 +143,7 @@ describe('creating large integers', function () { ]).toBigInt() ).to.equal(sentinel); - b = new StellarBase.ScInt(-sentinel); + b = Int.fromValue(-sentinel); u128 = b.toU128().u128(); expect( new U128([ @@ -155,7 +154,7 @@ describe('creating large integers', function () { ]).toBigInt() ).to.equal(BigInt.asUintN(128, -sentinel)); - b = new StellarBase.ScInt(sentinel); + b = Int.fromValue(sentinel); let i128 = b.toI128().i128(); expect( new I128([ @@ -166,7 +165,7 @@ describe('creating large integers', function () { ]).toBigInt() ).to.equal(sentinel); - b = new StellarBase.ScInt(-sentinel); + b = Int.fromValue(-sentinel); i128 = b.toI128().i128(); expect( new I128([ @@ -179,7 +178,7 @@ describe('creating large integers', function () { }); it('upscales to 256 bits', function () { - let b = new StellarBase.ScInt(-sentinel); + let b = Int.fromValue(-sentinel); let i256 = b.toI256().i256(); let u256 = b.toU256().u256(); @@ -213,7 +212,7 @@ describe('creating large integers', function () { describe('conversion to/from ScVals', function () { const v = 80000085n; - const i = new StellarBase.ScInt(v); + const i = Int.fromValue(v); [ [i.toI64(), 'i64'], @@ -227,9 +226,9 @@ describe('creating large integers', function () { expect(scv.switch().name).to.equal(`scv${type.toUpperCase()}`); expect(typeof scv.toXDR('base64')).to.equal('string'); - const bigi = StellarBase.scValToBigInt(scv); + const bigi = StellarBase.Int.fromScVal(scv).toBigInt(); expect(bigi).to.equal(v); - expect(new StellarBase.ScInt(bigi, { type }).toJSON()).to.eql({ + expect(new StellarBase.Int(type, bigi).toJSON()).to.eql({ ...i.toJSON(), type }); @@ -240,42 +239,42 @@ describe('creating large integers', function () { const i32 = new xdr.ScVal.scvI32(Number(v)); const u32 = new xdr.ScVal.scvU32(Number(v)); - expect(StellarBase.scValToBigInt(i32)).to.equal(v); - expect(StellarBase.scValToBigInt(u32)).to.equal(v); + expect(Int.fromScVal(i32).toBigInt()).to.equal(v); + expect(Int.fromScVal(u32).toBigInt()).to.equal(v); }); it('throws for non-integers', function () { - expect(() => - StellarBase.scValToBigInt(new xdr.ScVal.scvString('hello')) - ).to.throw(/integer/i); + expect(() => Int.fromScVal(new xdr.ScVal.scvString('hello'))).to.throw( + /integer/i + ); }); }); describe('error handling', function () { ['u64', 'u128', 'u256'].forEach((type) => { it(`throws when signed parts and {type: '${type}'}`, function () { - expect(() => new StellarBase.ScInt(-2, { type })).to.throw(/negative/i); + expect(() => new StellarBase.Int(type, -2)).to.throw(/positive/i); }); }); it('throws when too big', function () { - expect(() => new StellarBase.ScInt(1n << 400n)).to.throw(/expected/i); + expect(() => Int.fromValue(1n << 400n)).to.throw(/expected/i); }); it('throws when big interpreted as small', function () { let big; - big = new StellarBase.ScInt(1n << 64n); + big = Int.fromValue(1n << 64n); expect(() => big.toNumber()).to.throw(/not in range/i); - big = new StellarBase.ScInt(Number.MAX_SAFE_INTEGER + 1); + big = Int.fromValue(Number.MAX_SAFE_INTEGER + 1); expect(() => big.toNumber()).to.throw(/not in range/i); - big = new StellarBase.ScInt(1, { type: 'i128' }); + big = new Int('i128', 1); expect(() => big.toU64()).to.throw(/too large/i); expect(() => big.toI64()).to.throw(/too large/i); - big = new StellarBase.ScInt(1, { type: 'i256' }); + big = new Int('i256', 1); expect(() => big.toU64()).to.throw(/too large/i); expect(() => big.toI64()).to.throw(/too large/i); expect(() => big.toI128()).to.throw(/too large/i); @@ -293,7 +292,7 @@ describe('creating raw large XDR integers', function () { ].forEach(([type, count], idx) => { it(`works for ${type}`, function () { const input = new Array(count).fill(1n); - const xdrI = new StellarBase.XdrLargeInt(type, input); + const xdrI = new StellarBase.Int(type, input); let expected = input.reduce((accum, v, i) => { return (accum << 32n) | v; diff --git a/test/unit/invocation_test.js b/test/unit/invocation_test.js index a7f6b7cf..c280990f 100644 --- a/test/unit/invocation_test.js +++ b/test/unit/invocation_test.js @@ -51,7 +51,7 @@ describe('parsing invocation trees', function () { 1, 2, 3, 4 ].map(() => { // ezpz method to generate random contract IDs - const buf = hash(Keypair.random().publicKey()); + const buf = hash(rk()); return new Contract(StrKey.encodeContract(buf)); }); @@ -125,11 +125,9 @@ describe('parsing invocation trees', function () { salt: Buffer.alloc(32, 0) }) ), - constructorArgs: [1, '2', 3].map((arg, i) => { - return nativeToScVal(arg, { - type: ['u32', 'string', 'i32'][i] - }); - }), + constructorArgs: nativeToScVal([1, '2', 3], { + type: ['u32', 'string', 'i32'] + }).vec(), executable: xdr.ContractExecutable.contractExecutableWasm( Buffer.alloc(32, '\x20') ) @@ -145,7 +143,7 @@ describe('parsing invocation trees', function () { args: { source: nftContract.contractId(), function: 'purchase', - args: [`SomeNft:${nftId}`, '7'] + args: [`SomeNft:${nftId}`, 7] }, invocations: [ { diff --git a/test/unit/scval_test.js b/test/unit/scval_test.js index 50db7a46..cc3fdc7c 100644 --- a/test/unit/scval_test.js +++ b/test/unit/scval_test.js @@ -1,9 +1,8 @@ const { xdr, - ScInt, Address, Keypair, - XdrLargeInt, + Int, scValToNative, nativeToScVal, scValToBigInt @@ -15,12 +14,12 @@ describe('parsing and building ScVals', function () { void: null, u32: xdr.ScVal.scvU32(1), i32: xdr.ScVal.scvI32(1), - u64: 1n, - i64: -1n, - u128: new ScInt(1).toU128(), - i128: new ScInt(1).toI128(), - u256: new ScInt(1).toU256(), - i256: new ScInt(1).toI256(), + u64: BigInt(2 ** 33), + i64: BigInt(-1 * 2 ** 33), + u128: Int.fromValue(1).toU128(), + i128: Int.fromValue(1).toI128(), + u256: Int.fromValue(1).toU256(), + i256: Int.fromValue(1).toI256(), map: { arbitrary: 1n, nested: 'values', @@ -32,16 +31,16 @@ describe('parsing and building ScVals', function () { const targetScv = xdr.ScVal.scvMap( [ ['bool', xdr.ScVal.scvBool(true)], - ['i128', new ScInt(1, { type: 'i128' }).toScVal()], - ['i256', new ScInt(1, { type: 'i256' }).toScVal()], + ['i128', new Int('i128', 1).toScVal()], + ['i256', new Int('i256', 1).toScVal()], ['i32', xdr.ScVal.scvI32(1)], - ['i64', xdr.ScVal.scvI64(new xdr.Int64(-1))], + ['i64', xdr.ScVal.scvI64(new xdr.Int64(-1 * 2 ** 33))], [ 'map', xdr.ScVal.scvMap([ new xdr.ScMapEntry({ key: xdr.ScVal.scvString('arbitrary'), - val: xdr.ScVal.scvU64(new xdr.Uint64(1)) + val: xdr.ScVal.scvU32(1) }), new xdr.ScMapEntry({ key: xdr.ScVal.scvString('etc'), @@ -53,10 +52,10 @@ describe('parsing and building ScVals', function () { }) ]) ], - ['u128', new ScInt(1, { type: 'u128' }).toScVal()], - ['u256', new ScInt(1, { type: 'u256' }).toScVal()], + ['u128', new Int('u128', 1).toScVal()], + ['u256', new Int('u256', 1).toScVal()], ['u32', xdr.ScVal.scvU32(1)], - ['u64', xdr.ScVal.scvU64(new xdr.Uint64(1))], + ['u64', xdr.ScVal.scvU64(new xdr.Uint64(2 ** 33))], [ 'vec', xdr.ScVal.scvVec(['same', 'type', 'list'].map(xdr.ScVal.scvString)) @@ -81,7 +80,13 @@ describe('parsing and building ScVals', function () { // iterate for granular errors on failures targetScv.value().forEach((entry, idx) => { const actual = scv.value()[idx]; - expect(entry).to.deep.equal(actual, `item ${idx} doesn't match`); + expect(JSON.stringify(entry, null, 2)).to.eql( + JSON.stringify(actual, null, 2) + ); + expect(entry).to.deep.equal( + actual, + `item ${idx} doesn't match: ${JSON.stringify(entry, null, 2)} != ${JSON.stringify(actual, null, 2)}` + ); }); expect(scv.toXDR('base64')).to.deep.equal(targetScv.toXDR('base64')); @@ -95,14 +100,14 @@ describe('parsing and building ScVals', function () { [xdr.ScVal.scvVoid(), null], [xdr.ScVal.scvBool(true), true], [xdr.ScVal.scvBool(false), false], - [xdr.ScVal.scvU32(1), 1], - [xdr.ScVal.scvI32(1), 1], - [new ScInt(11).toU64(), 11n], - [new ScInt(11).toI64(), 11n], - [new ScInt(22).toU128(), 22n], - [new ScInt(22).toI128(), 22n], - [new ScInt(33).toU256(), 33n], - [new ScInt(33).toI256(), 33n], + [Int.fromValue(1).toU32(), 1], + [Int.fromValue(1).toI32(), 1], + [Int.fromValue(11).toU64(), 11n], + [Int.fromValue(11).toI64(), 11n], + [Int.fromValue(22).toU128(), 22n], + [Int.fromValue(22).toI128(), 22n], + [Int.fromValue(33).toU256(), 33n], + [Int.fromValue(33).toI256(), 33n], [xdr.ScVal.scvTimepoint(new xdr.Uint64(44n)), 44n], [xdr.ScVal.scvDuration(new xdr.Uint64(55n)), 55n], [xdr.ScVal.scvBytes(Buffer.alloc(32, 123)), Buffer.from('{'.repeat(32))], @@ -122,7 +127,7 @@ describe('parsing and building ScVals', function () { [ xdr.ScVal.scvMap( [ - [new ScInt(0).toI256(), xdr.ScVal.scvBool(true)], + [Int.fromValue(0).toI256(), xdr.ScVal.scvBool(true)], [xdr.ScVal.scvBool(false), xdr.ScVal.scvString('second')], [ xdr.ScVal.scvU32(2), @@ -235,8 +240,8 @@ describe('parsing and building ScVals', function () { it('lets strings be large integer ScVals', function () { ['i64', 'i128', 'i256', 'u64', 'u128', 'u256'].forEach((type) => { const scv = nativeToScVal('12345', { type }); - expect(XdrLargeInt.getType(scv.switch().name)).to.equal(type); - expect(scValToBigInt(scv)).to.equal(BigInt(12345)); + expect(Int.getType(scv.switch().name)).to.equal(type); + expect(Int.fromScVal(scv).toBigInt()).to.equal(BigInt(12345)); }); expect(() => nativeToScVal('not a number', { type: 'i128' })).to.throw(); diff --git a/types/index.d.ts b/types/index.d.ts index b8b6ffaf..0f6b4c77 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1135,7 +1135,9 @@ export function encodeMuxedAccount(gAddress: string, id: string): xdr.MuxedAccou export function extractBaseAddress(address: string): string; export type IntLike = string | number | bigint; -export type ScIntType = +export type IntType = + | 'i32' + | 'u32' | 'i64' | 'u64' | 'i128' @@ -1143,15 +1145,17 @@ export type ScIntType = | 'i256' | 'u256'; -export class XdrLargeInt { +export class Int { constructor( - type: ScIntType, + type: IntType, values: IntLike | IntLike[] ); toNumber(): number; toBigInt(): bigint; + toI32(): xdr.ScVal; + toU32(): xdr.ScVal; toI64(): xdr.ScVal; toU64(): xdr.ScVal; toI128(): xdr.ScVal; @@ -1164,18 +1168,16 @@ export class XdrLargeInt { toString(): string; toJSON(): { value: string; - type: ScIntType; + type: IntType; }; - static isType(t: string): t is ScIntType; - static getType(scvType: string): ScIntType; -} + static isType(t: string): t is IntType; + static getType(scvType: string): IntType; -export class ScInt extends XdrLargeInt { - constructor(value: IntLike, opts?: { type: ScIntType }); + static fromValue(x: IntLike): Int; + static fromScVal(x: xdr.ScVal): Int; } -export function scValToBigInt(scv: xdr.ScVal): bigint; export function nativeToScVal(val: any, opts?: { type: any }): xdr.ScVal; export function scValToNative(scv: xdr.ScVal): any; diff --git a/types/test.ts b/types/test.ts index 1d9a1dfe..80f58944 100644 --- a/types/test.ts +++ b/types/test.ts @@ -351,11 +351,11 @@ const sk = StellarSdk.xdr.SignerKey.signerKeyTypeEd25519SignedPayload( StellarSdk.SignerKey.encodeSignerKey(sk); // $ExpectType string StellarSdk.SignerKey.decodeAddress(sourceKey.publicKey()); // $ExpectType SignerKey -new StellarSdk.ScInt(1234); // $ExpectType ScInt -new StellarSdk.ScInt('1234'); // $ExpectType ScInt -new StellarSdk.ScInt(BigInt(1234)); // $ExpectType ScInt -(['i64', 'u64', 'i128', 'u128', 'i256', 'u256'] as StellarSdk.ScIntType[]).forEach((type) => { - new StellarSdk.ScInt(1234, { type }); // $ExpectType ScInt +StellarSdk.Int.fromValue(1234); // $ExpectType Int +StellarSdk.Int.fromValue('1234'); // $ExpectType Int +StellarSdk.Int.fromValue(BigInt(1234)); // $ExpectType Int +(['i32', 'u32', 'i64', 'u64', 'i128', 'u128', 'i256', 'u256'] as StellarSdk.IntType[]).forEach((type) => { + new StellarSdk.Int(type, 1234); // $ExpectType Int }); const root = new StellarSdk.xdr.SorobanAuthorizedInvocation({