Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wrappers to easily deal with the myriad of ScVal integer types. #620

Merged
merged 34 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d3f125f
Add moved files from js-xdr
Shaptic May 31, 2023
65d5554
Add first draft of number-handling API
Shaptic Jun 1, 2023
f4e9609
Clean up interface and add examples
Shaptic Jun 1, 2023
ad32f62
Simplify interface, add working i64 -> i128 upscale
Shaptic Jun 3, 2023
b1d8550
Correctly upscale 64 -> 128, rename to ScInt
Shaptic Jun 5, 2023
d604e62
Add utility methods
Shaptic Jun 5, 2023
203ec70
Add upscaling from 64 -> anything
Shaptic Jun 5, 2023
758260d
Add ScVal interpretation helper, update docs
Shaptic Jun 5, 2023
77cab97
Add TypeScript definition
Shaptic Jun 5, 2023
59d5c76
Format types
Shaptic Jun 5, 2023
b307af1
Drop some local changes that snuck in there
Shaptic Jun 5, 2023
2da0279
Move XDR read/write tests
Shaptic Jun 5, 2023
e6579ad
Clarify that 32-bit ScVals aren't supported
Shaptic Jun 5, 2023
85c69a9
Add more conversion tests
Shaptic Jun 6, 2023
1800293
Small code simplification + code fmt
Shaptic Jun 6, 2023
0d14678
Add a way to build the abstraction from raw XDR
Shaptic Jun 7, 2023
30fb037
Add a type-required version of the abstraction
Shaptic Jun 7, 2023
8c5f027
Micro-optimization, tests for array construction
Shaptic Jun 7, 2023
0a02e68
File breakout, rename to conform to e.g. xdr.Int128Parts
Shaptic Jun 7, 2023
273b1bc
Finish file breakout
Shaptic Jun 7, 2023
cd7e2ec
Fix constructor signature
Shaptic Jun 7, 2023
fea88ed
Rename to match the rest of the repo
Shaptic Jun 8, 2023
ee42e30
Modify error message to include Number range
Shaptic Jun 8, 2023
66b7a65
Move fn since it isn't a static constructor
Shaptic Jun 8, 2023
229d856
Rename and do exports properly
Shaptic Jun 8, 2023
6f902ef
Fix docstring & relative filenames
Shaptic Jun 8, 2023
4f3d9d0
One last rename fix, I hope
Shaptic Jun 8, 2023
ab0b4af
Update all tests to match renames
Shaptic Jun 8, 2023
193f820
Renames per PR review suggestions
Shaptic Jun 13, 2023
688be0a
Add reference to js-xdr master commit, fixup test
Shaptic Jun 14, 2023
49747ff
Finish rename, plus clean up exports
Shaptic Jun 15, 2023
281f0e1
Update rename + fixup imports
Shaptic Jun 22, 2023
5ae75a4
Add real js-xdr version
Shaptic Jun 22, 2023
18aa173
Update yarn.lock w/ v3.0
Shaptic Jun 22, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
node-version: ${{ matrix.node-version }}

- name: Install Dependencies
run: yarn install
run: yarn install --network-concurrency 1

- name: Build All
run: yarn build:prod
Expand Down
3 changes: 2 additions & 1 deletion config/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = {
env: {
es6: true
es6: true,
es2020: true
},
extends: ['airbnb-base', 'prettier'],
plugins: ['@babel', 'prettier', 'prefer-import'],
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@
"build:browser:prod": "cross-env NODE_ENV=production yarn build:browser",
"build:prod": "cross-env NODE_ENV=production yarn build",
"test": "yarn build && yarn test:node && yarn test:browser",
"test:node": "nyc --nycrc-path ./config/.nycrc mocha",
"test:node": "yarn _nyc mocha",
"test:browser": "karma start ./config/karma.conf.js",
"docs": "jsdoc -c ./config/.jsdoc.json --verbose",
"lint": "eslint -c ./config/.eslintrc.js src/ && dtslint --localTs node_modules/typescript/lib types/",
"preversion": "yarn clean && yarn fmt && yarn lint && yarn build:prod && yarn test",
"fmt": "prettier --config ./config/prettier.config.js --ignore-path ./config/.prettierignore --write './**/*.js'",
"prepare": "yarn build:prod",
"clean": "rm -rf lib/ dist/ coverage/ .nyc_output/"
"clean": "rm -rf lib/ dist/ coverage/ .nyc_output/",
"_nyc": "nyc --nycrc-path ./config/.nycrc"
},
"mocha": {
"require": [
Expand Down Expand Up @@ -80,7 +81,6 @@
"@typescript-eslint/parser": "^5.59.5",
"babel-loader": "^9.1.2",
"babel-plugin-istanbul": "^6.1.1",
"buffer": "^6.0.3",
"chai": "^4.3.7",
"cross-env": "^7.0.3",
"eslint": "^8.40.0",
Expand Down Expand Up @@ -120,9 +120,10 @@
"dependencies": {
"base32.js": "^0.1.0",
"bignumber.js": "^9.1.1",
"buffer": "^6.0.3",
"crc": "^4.3.2",
"crypto-browserify": "^3.12.0",
"js-xdr": "^2.0.0",
"js-xdr": "^3.0.0",
"lodash": "^4.17.21",
"sha.js": "^2.3.6",
"tweetnacl": "^1.0.3"
Expand Down
12 changes: 6 additions & 6 deletions src/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ export class Contract {
*/
contractId(format = 'strkey') {
switch (format) {
case 'strkey':
return StrKey.encodeContract(this._id);
case 'hex':
return this._id.toString('hex');
default:
throw new Error(`Invalid format: ${format}`);
case 'strkey':
return StrKey.encodeContract(this._id);
case 'hex':
return this._id.toString('hex');
default:
throw new Error(`Invalid format: ${format}`);
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,14 @@ export {
encodeMuxedAccount
} from './util/decode_encode_muxed_account';

export {
ScInt,
XdrLargeInt,
scValToBigInt,
Uint256,
Int256,
Uint128,
Int128
} from './numbers/index';

export default module.exports;
64 changes: 64 additions & 0 deletions src/numbers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { XdrLargeInt } from './xdr_large_int';

import { Uint128 } from './uint128';
import { Uint256 } from './uint256';
import { Int128 } from './int128';
import { Int256 } from './int256';

export { Uint256, Int256, Uint128, Int128 };

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
* ```js
* 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 type = scv.switch().name.slice(3).toLowerCase();

switch (scv.switch().name) {
case 'scvU32':
case 'scvI32':
return BigInt(scv.value());

case 'scvU64':
case 'scvI64':
return new XdrLargeInt(type, scv.value()).toBigInt();

case 'scvU128':
case 'scvI128':
return new XdrLargeInt(type, [
scv.value().lo(),
scv.value().hi()
]).toBigInt();

case 'scvU256':
case 'scvI256':
return new XdrLargeInt(type, [
scv.value().loLo(),
scv.value().loHi(),
scv.value().hiLo(),
scv.value().hiHi()
]).toBigInt();

default:
throw TypeError(`expected integer type, got ${scv.switch()}`);
}
}
23 changes: 23 additions & 0 deletions src/numbers/int128.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { LargeInt } from 'js-xdr';

export class Int128 extends LargeInt {
/**
* Construct a signed 128-bit integer that can be XDR-encoded.
*
* @param {Array<number|bigint|string>} args - one or more slices to encode
* in big-endian format (i.e. earlier elements are higher bits)
*/
constructor(...args) {
super(args);
}

get unsigned() {
return false;
}

get size() {
return 128;
}
}

Int128.defineIntBoundaries();
23 changes: 23 additions & 0 deletions src/numbers/int256.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { LargeInt } from 'js-xdr';

export class Int256 extends LargeInt {
/**
* Construct a signed 256-bit integer that can be XDR-encoded.
*
* @param {Array<number|bigint|string>} args - one or more slices to encode
* in big-endian format (i.e. earlier elements are higher bits)
*/
constructor(...args) {
super(args);
}

get unsigned() {
return false;
}

get size() {
return 256;
}
}

Int256.defineIntBoundaries();
116 changes: 116 additions & 0 deletions src/numbers/sc_int.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { XdrLargeInt } from './xdr_large_int';

/**
* Provides an easier way to manipulate large numbers for Stellar operations.
*
* You can instantiate this 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
* ```js
* import sdk from "stellar-base";
*
* // You have an ScVal from a contract and want to parse it into JS native.
* const value = sdk.xdr.ScVal.fromXDR(someXdr, "base64");
* const bigi = sdk.ScInt.fromScVal(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 = sdk.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:
* sdk.ScInt("123456789123456789")
* sdk.ScInt(123456789123456789n);
* sdk.ScInt(1n << 140n);
* sdk.ScInt(-42);
* sdk.ScInt.fromScVal(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 = sdk.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|ScInt} 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 parts (${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;
}
23 changes: 23 additions & 0 deletions src/numbers/uint128.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { LargeInt } from 'js-xdr';

export class Uint128 extends LargeInt {
/**
* Construct an unsigned 128-bit integer that can be XDR-encoded.
*
* @param {Array<number|bigint|string>} args - one or more slices to encode
* in big-endian format (i.e. earlier elements are higher bits)
*/
constructor(...args) {
Copy link
Contributor

@sreuland sreuland Jun 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will inherit the same constructor from base class, can skip re-declaring here in the child classes..I think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, this should inherit the base class constructor automatically.

But doesn't hurt to double check that this still works if you remove it 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep I like that it's explicit bc it will document, added jsdocs to make this even more obvious to users

super(args);
}

get unsigned() {
return true;
}

get size() {
return 128;
}
}

Uint128.defineIntBoundaries();
23 changes: 23 additions & 0 deletions src/numbers/uint256.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { LargeInt } from 'js-xdr';

export class Uint256 extends LargeInt {
/**
* Construct an unsigned 256-bit integer that can be XDR-encoded.
*
* @param {Array<number|bigint|string>} args - one or more slices to encode
* in big-endian format (i.e. earlier elements are higher bits)
*/
constructor(...args) {
super(args);
}

get unsigned() {
return true;
}

get size() {
return 256;
}
}

Uint256.defineIntBoundaries();
Loading