diff --git a/solarkraft/package-lock.json b/solarkraft/package-lock.json index 973869de..3df8220a 100644 --- a/solarkraft/package-lock.json +++ b/solarkraft/package-lock.json @@ -12,6 +12,7 @@ "@sweet-monads/either": "^3.3.1", "axios": "^1.6.7", "chalk": "^5.3.0", + "immutable": "^5.0.0-beta.5", "json-bigint": "^1.0.0", "source-licenser": "^2.0.6", "yargs": "^17.7.2" @@ -1609,6 +1610,11 @@ "node": ">= 4" } }, + "node_modules/immutable": { + "version": "5.0.0-beta.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.0-beta.5.tgz", + "integrity": "sha512-1RO6wxfJdh/uyWb2MTn3RuCPXYmpRiAhoKm8vEnA50+2Gy0j++6GBtu5q6sq2d4tpcL+e1sCHzk8NkWnRhT2/Q==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -4177,6 +4183,11 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==" }, + "immutable": { + "version": "5.0.0-beta.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.0-beta.5.tgz", + "integrity": "sha512-1RO6wxfJdh/uyWb2MTn3RuCPXYmpRiAhoKm8vEnA50+2Gy0j++6GBtu5q6sq2d4tpcL+e1sCHzk8NkWnRhT2/Q==" + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", diff --git a/solarkraft/package.json b/solarkraft/package.json index 2f3c526f..84bfc9f8 100644 --- a/solarkraft/package.json +++ b/solarkraft/package.json @@ -43,6 +43,7 @@ "@sweet-monads/either": "^3.3.1", "axios": "^1.6.7", "chalk": "^5.3.0", + "immutable": "^5.0.0-beta.5", "json-bigint": "^1.0.0", "source-licenser": "^2.0.6", "yargs": "^17.7.2" diff --git a/solarkraft/src/state/value.ts b/solarkraft/src/state/value.ts index c0b1fbe7..a47fd4c2 100644 --- a/solarkraft/src/state/value.ts +++ b/solarkraft/src/state/value.ts @@ -1,48 +1,79 @@ +/** + * @license + * [Apache-2.0](https://github.com/freespek/solarkraft/blob/main/LICENSE) + */ +import { OrderedMap } from 'immutable' + export type Value = { type: string -} & (IntValue | BoolValue | SymbValue | AddrValue) +} & ( + | IntValue + | BoolValue + | SymbValue + | AddrValue + | ArrValue + | VecValue + | MapValue +) export function isValid(v: Value): boolean { switch (v.type) { - case "u32": { - const val = (v as IntValue).val - return (0n <= val) && (val <= 2n ** 32n) + // Integers are valid iff their values lie within the [0, 2^n) or [-2^{n-1},2^{n-1}) intervals + case 'u32': { + const val = v.val + return 0n <= val && val < 2n ** 32n } - case "i32": { - const val = (v as IntValue).val - return (-(2n ** 31n) <= val) && (val < (2n ** 31n)) + case 'i32': { + const val = v.val + return -(2n ** 31n) <= val && val < 2n ** 31n } - case "u64": { - const val = (v as IntValue).val - return (0n <= val) && (val <= 2n ** 64n) + case 'u64': { + const val = v.val + return 0n <= val && val < 2n ** 64n } - case "i64": { - const val = (v as IntValue).val - return (-(2n ** 63n) <= val) && (val < (2n ** 63n)) + case 'i64': { + const val = v.val + return -(2n ** 63n) <= val && val < 2n ** 63n } - case "u128": { - const val = (v as IntValue).val - return (0n <= val) && (val <= 2n ** 128n) + case 'u128': { + const val = v.val + return 0n <= val && val < 2n ** 128n } - case "i128": { - const val = (v as IntValue).val - return (-(2n ** 127n) <= val) && (val < (2n ** 127n)) + case 'i128': { + const val = v.val + return -(2n ** 127n) <= val && val < 2n ** 127n } - case "symb": { + // Symbols are valid iff they have at most 32 alphanumeric or underscore characters + case 'symb': { const regex: RegExp = /^[a-zA-Z0-9_]{0,32}$/ - return regex.test((v as SymbValue).val) + return regex.test(v.val) } - case "addr": { + // Addresses are valid iff they have _exactly_ 56 uppercase alphanumeric characters + case 'addr': { const regex: RegExp = /^[A-Z0-9]{56}$/ - return regex.test((v as AddrValue).val) + return regex.test(v.val) + } + // Fixed-length byte arrays are valid only if their declared length matches their actual length + case 'arr': { + return typeof v.len === 'undefined' || v.val.length === v.len + } + // Vectors are valid iff their elements are all valid. + case 'vec': { + return !v.val.some((elem) => !isValid(elem)) + } + // Maps are valid iff their keys and values are all valid. + case 'map': { + return ![...v.val.entries()].some( + ([key, value]) => !isValid(key) || !isValid(value) + ) } + // Booleans are always valid, under TS type constraints. default: return true - } } -/** +/** * Any of the follwing: * - Unsigned 32-bit Integer (u32) * - Signed 32-bit Integer (i32) @@ -50,104 +81,176 @@ export function isValid(v: Value): boolean { * - Signed 64-bit Integer (i64) * - Unsigned 128-bit Integer (u128) * - Signed 128-bit Integer (i128) - * + * * Example: 2u32 would be represented as { val: 2, type: "u32" }, whereas 2i32 would be { val: 2, type: "i32" } -*/ + */ -export type ValidIntT = "u32" | "i32" | "u64" | "i64" | "u128" | "i128" +export type ValidIntT = 'u32' | 'i32' | 'u64' | 'i64' | 'u128' | 'i128' export type IntValue = { val: bigint type: ValidIntT } - -function mkInt(type: ValidIntT, val: bigint) { - const obj = { type: type, val: val } as Value +// Internal function for creating values of integer types. +// Calls isValid() to check whether `val` belongs to the range determined by `type` +function mkInt(type: ValidIntT, val: bigint): IntValue { + const obj: IntValue = { type: type, val: val } if (!isValid(obj)) { throw new RangeError(`${val} lies outside the ${type} range.`) } return obj } -export function u32(v: bigint): Value { - return mkInt("u32", v) +// Safe constructor for u32-typed `Value`s. Throws a `RangeError` if `v` lies outside [0, 2^32) +export function u32(v: bigint): IntValue { + return mkInt('u32', v) } -export function i32(v: bigint): Value { - return mkInt("i32", v) +// Safe constructor for i32-typed `Value`s. Throws a `RangeError` if `v` lies outside [-2^31, 2^31) +export function i32(v: bigint): IntValue { + return mkInt('i32', v) } -export function u64(v: bigint): Value { - return mkInt("u64", v) +// Safe constructor for u64-typed `Value`s. Throws a `RangeError` if `v` lies outside [0, 2^64) +export function u64(v: bigint): IntValue { + return mkInt('u64', v) } -export function i64(v: bigint): Value { - return mkInt("i64", v) +// Safe constructor for i64-typed `Value`s. Throws a `RangeError` if `v` lies outside [-2^63, 2^63) +export function i64(v: bigint): IntValue { + return mkInt('i64', v) } -export function u128(v: bigint): Value { - return mkInt("u128", v) +// Safe constructor for u128-typed `Value`s. Throws a `RangeError` if `v` lies outside [0, 2^128) +export function u128(v: bigint): IntValue { + return mkInt('u128', v) } -export function i128(v: bigint): Value { - return mkInt("i128", v) +// Safe constructor for i128-typed `Value`s. Throws a `RangeError` if `v` lies outside [-2^127, 2^127) +export function i128(v: bigint): IntValue { + return mkInt('i128', v) } // true or false export type BoolValue = { val: boolean - type: "bool" + type: 'bool' } -export function bool(v: boolean): Value { - return { type: "bool", val: v } +// Wrapper around a `boolean`. Can never throw an exception, and is always valid. +export function bool(v: boolean): BoolValue { + return { type: 'bool', val: v } } -// Symbols are small efficient strings up to 32 characters in length and limited to a-z A-Z 0-9 _ that are encoded into 64-bit integers. -// We store the string representation, and optionally the number. -// TODO: determine _how_ the strings are encoded as numbers +// Symbols are small efficient strings up to 32 characters in length and limited to `a-z`, `A-Z`, `0-9`, and `_`, +// that are encoded into 64-bit integers. We store the string representation, and optionally the number. +// TODO: determine _how_ the strings are encoded as numbers? export type SymbValue = { val: string - type: "symb" + type: 'symb' num?: number } -export function symb(s: string): Value { - const obj = { type: "symb", val: s } as Value +// Safe constructor for symb-typed `Value`s. Throws a `TypeError` if `s` is too long, or contains illegal characters. +export function symb(s: string): SymbValue { + const obj: SymbValue = { type: 'symb', val: s } if (!isValid(obj)) { - throw new TypeError(`Symbols must be up to 32 alphanumeric characters or underscores, found: ${s}.`) + throw new TypeError( + `Symbols must be up to 32 alphanumeric characters or underscores, found: ${s}.` + ) } return obj } -// Addresses are always length-56 +// Addresses are always length-56, and limited to `A-Z`, `0-9`. export type AddrValue = { val: string - type: "addr" + type: 'addr' } -export function addr(s: string): Value { - const obj = { type: "addr", val: s } as Value +// Safe constructor for addr-typed `Value`s. Throws a `TypeError` if `s` is not 56 characters long or contains illegal characters. +export function addr(s: string): AddrValue { + const obj: AddrValue = { type: 'addr', val: s } if (!isValid(obj)) { - throw new TypeError(`Symbols must be up to 32 alphanumeric characters or underscores, found: ${s}.`) + throw new TypeError( + `Symbols must be up to 32 alphanumeric characters or underscores, found: ${s}.` + ) } return obj } +export type byte = 0 | 1 + // Byte arrays (Bytes, BytesN) // The `len` field is present iff the length is fixed (i.e. for BytesN) -// TODO: tests export type ArrValue = { - val: { type: "u32", val: bigint }[] - type: "arr" + val: byte[] + type: 'arr' len?: number - // if (typeof (l) !== 'undefined' && v.length !== l) { - // throw new TypeError(`Array declared as fixed-length ${l}, but actual length is ${v.length}.`) - // } } +// Safe constructor for arr-typed `Value`s representing non-fixed width byte arrays. Cannot throw. +export function bytes(v: byte[]): Value { + return { type: 'arr', val: v } +} + +// Safe constructor for arr-typed `Value`s representing fixed width byte arrays. Cannot throw. +export function bytesN(v: byte[]): ArrValue { + OrderedMap() + return { type: 'arr', val: v, len: v.length } +} + +// Vectors are an ordered collection of `Value`s, with possible duplicates. +// The values in a Vec are not guaranteed to be of any specific type. <-- from the docs +export type VecValue = { + val: Value[] + type: 'vec' +} + +// Safe constructor for vec-typed `Value`s. Throws a `TypeError` if `v` contains an invalid value. +export function vec(v: Value[]): VecValue { + const obj: VecValue = { type: 'vec', val: v } + if (!isValid(obj)) { + throw new TypeError(`Some element of ${v} is not valid.`) + } + return obj +} + +// Soroban Map is an ordered key-value dictionary (note that JS maps are in principle unordered, but will iterate in insertion order). +// Maps have at most one entry per key. Setting a value for a key in the map that already has a value for that key replaces the value. <-- docs +export type MapValue = { + val: OrderedMap + type: 'map' +} + +export type KeyValuePair = [Value, Value] +// Safe constructor for map-typed `Value`s. Throws a `TypeError` if `v` contains an invalid key or value. +export function map(v: OrderedMap): MapValue { + const obj: MapValue = { type: 'map', val: v } + if (!isValid(obj)) { + throw new TypeError(`Some key or value of ${v} is not valid.`) + } + return obj +} +// Safe constructor for vec-typed `Value`s. Throws a `TypeError` if `v` contains an invalid key or value, or if it contains duplicate keys. +export function mapFromKV(a: KeyValuePair[]): MapValue { + let partialMap = OrderedMap() + for (const [k, v] of a) { + if (partialMap.has(k)) { + throw new TypeError( + `Pairs must have unique keys, found duplicate ${k}` + ) + } + partialMap = partialMap.set(k, v) + } + return map(partialMap) +} +// Helper function, returns the contents of the map as an array of key-value pairs for serialization +export function toArr(m: MapValue): KeyValuePair[] { + return Array.from(m.val.entries()) +} diff --git a/solarkraft/test/state/value.test.ts b/solarkraft/test/state/value.test.ts index af9b41d9..9b81991a 100644 --- a/solarkraft/test/state/value.test.ts +++ b/solarkraft/test/state/value.test.ts @@ -1,82 +1,271 @@ import { assert } from 'chai' import { describe, it } from 'mocha' -import { isValid, u32, i32, u64, i64, u128, i128, symb, addr } from '../../src/state/value.js' +import { OrderedMap } from 'immutable' +import { + isValid, + u32, + i32, + u64, + i64, + u128, + i128, + symb, + addr, + bytes, + bytesN, + vec, + map, + bool, + Value, + byte, + toArr, + KeyValuePair, + mapFromKV, +} from '../../src/state/value.js' describe('Integer tests', () => { it('asserts 32-bit integer constructors respect bounds', () => { - assert.throws(() => { u32(-1n) }, RangeError) - assert.throws(() => { u32(2n ** 32n + 1n) }, RangeError) - assert.throws(() => { i32(-(2n ** 31n) - 1n) }, RangeError) - assert.throws(() => { i32(2n ** 31n) }, RangeError) + assert.throws(() => { + u32(-1n) + }, RangeError) + assert.throws(() => { + u32(2n ** 32n + 1n) + }, RangeError) + assert.throws(() => { + i32(-(2n ** 31n) - 1n) + }, RangeError) + assert.throws(() => { + i32(2n ** 31n) + }, RangeError) const x_u32 = u32(2n ** 20n) assert(isValid(x_u32)) - assert(x_u32.type === "u32") + assert(x_u32.type === 'u32') assert(x_u32.val === 2n ** 20n) const x_i32 = i32(-(2n ** 20n)) assert(isValid(x_i32)) - assert(x_i32.type === "i32") + assert(x_i32.type === 'i32') assert(x_i32.val === -(2n ** 20n)) }) it('asserts 64-bit integer constructors respect bounds', () => { - assert.throws(() => { u64(-1n) }, RangeError) - assert.throws(() => { u64(2n ** 64n + 1n) }, RangeError) - assert.throws(() => { i64(-(2n ** 63n) - 1n) }, RangeError) - assert.throws(() => { i64(2n ** 63n) }, RangeError) + assert.throws(() => { + u64(-1n) + }, RangeError) + assert.throws(() => { + u64(2n ** 64n + 1n) + }, RangeError) + assert.throws(() => { + i64(-(2n ** 63n) - 1n) + }, RangeError) + assert.throws(() => { + i64(2n ** 63n) + }, RangeError) const x_u64 = u64(2n ** 40n) assert(isValid(x_u64)) - assert(x_u64.type === "u64") + assert(x_u64.type === 'u64') assert(x_u64.val === 2n ** 40n) const x_i64 = i64(-(2n ** 40n)) assert(isValid(x_i64)) - assert(x_i64.type === "i64") + assert(x_i64.type === 'i64') assert(x_i64.val === -(2n ** 40n)) }) it('asserts 128-bit integer constructors respect bounds', () => { - assert.throws(() => { u128(-1n) }, RangeError) - assert.throws(() => { u128(2n ** 128n + 1n) }, RangeError) - assert.throws(() => { i128(-(2n ** 127n) - 1n) }, RangeError) - assert.throws(() => { i128(2n ** 127n) }, RangeError) + assert.throws(() => { + u128(-1n) + }, RangeError) + assert.throws(() => { + u128(2n ** 128n + 1n) + }, RangeError) + assert.throws(() => { + i128(-(2n ** 127n) - 1n) + }, RangeError) + assert.throws(() => { + i128(2n ** 127n) + }, RangeError) const x_u128 = u128(2n ** 80n) assert(isValid(x_u128)) - assert(x_u128.type === "u128") + assert(x_u128.type === 'u128') assert(x_u128.val === 2n ** 80n) const x_i128 = i128(-(2n ** 80n)) assert(isValid(x_i128)) - assert(x_i128.type === "i128") + assert(x_i128.type === 'i128') assert(x_i128.val === -(2n ** 80n)) }) }) describe('Stringlike tests', () => { it('asserts SymValue constructors properly check requirements', () => { - assert.throws(() => { symb("waaaaaaaaaaaaaaaaay tooooooooooooooooooooooooo loooooooooooooooooooooooooooooong") }, TypeError) - assert.throws(() => { symb('\u2615') }, TypeError) + assert.throws(() => { + symb( + 'waaaaaaaaaaaaaaaaay tooooooooooooooooooooooooo loooooooooooooooooooooooooooooong' + ) + }, TypeError) + assert.throws(() => { + symb('\u2615') + }, TypeError) - const s = symb("FOO") + const s = symb('FOO') assert(s.val === 'FOO') }) it('asserts Address constructors properly check requirements', () => { - assert.throws(() => { addr("LOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOONG") }, TypeError) - assert.throws(() => { addr('========================================================') }, TypeError) - assert.throws(() => { addr('FOO') }, TypeError) + assert.throws(() => { + addr( + 'LOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOONG' + ) + }, TypeError) + assert.throws(() => { + addr('========================================================') + }, TypeError) + assert.throws(() => { + addr('FOO') + }, TypeError) - const s = addr("ALICE000000000000000000000000000000000000000000000000000") - assert(s.val === "ALICE000000000000000000000000000000000000000000000000000") + const s = addr( + 'ALICE000000000000000000000000000000000000000000000000000' + ) + assert( + s.val === 'ALICE000000000000000000000000000000000000000000000000000' + ) }) }) -describe('Array tests', () => { - it('asserts something about arrays (TODO)', () => { - // todo +describe('Collection tests', () => { + it('asserts array valididty chekcs properly assert length equality', () => { + const arr3: byte[] = [0, 1, 0] + + const customVariable: Value = { type: 'arr', val: [] } + const customFixed: Value = { type: 'arr', val: [], len: 10 } + + assert(isValid(customVariable)) + assert(!isValid(customFixed)) + + const variableArr = bytes(arr3) + const fixedArr = bytesN(arr3) + + assert(isValid(variableArr)) + assert(isValid(fixedArr)) + + assert(variableArr.val === fixedArr.val) + assert(variableArr.val === arr3) + assert(variableArr.type === fixedArr.type) + assert(variableArr.type === 'arr') + + assert(!Object.keys(variableArr).includes('len')) + assert(fixedArr.len === arr3.length) + }) + + it('asserts vector constructors properly assert child validity', () => { + const homogeneousArr = [u64(0n), u64(0n), u64(0n)] + const heterogeneousArr = [bool(false), symb('fOo')] + const vecWithInvalid: Value[] = [{ type: 'u64', val: -1n }] + + assert.throws(() => { + vec(vecWithInvalid) + }, TypeError) + + const homVec = vec(homogeneousArr) + const hetVec = vec(heterogeneousArr) + + assert(homVec.val.length == homogeneousArr.length) + assert(hetVec.val.length == heterogeneousArr.length) + }) + + it('asserts basic map constructors properly assert child validity', () => { + const k0 = u32(0n) + const k1 = u32(1n) + const alice = addr( + 'ALICE000000000000000000000000000000000000000000000000000' + ) + const bob = addr( + 'BOB00000000000000000000000000000000000000000000000000000' + ) + + const mapValid: OrderedMap = OrderedMap() + .set(k0, alice) + .set(k1, bob) + + const mapInvalidVal: OrderedMap = OrderedMap< + Value, + Value + >().set(k0, { type: 'addr', val: 'ALICE' }) + const mapInvalidKey: OrderedMap = OrderedMap< + Value, + Value + >().set({ type: 'u32', val: -1n }, bob) + + assert.throws(() => { + map(mapInvalidKey) + }, TypeError) + assert.throws(() => { + map(mapInvalidVal) + }, TypeError) + + const valdiMap = map(mapValid) + const asArr = toArr(valdiMap) + + assert(asArr.length === 2) + assert(asArr[0].length === 2) + assert(asArr[1].length === 2) + + assert(asArr[0][0] === k0) + assert(asArr[0][1] === alice) + assert(asArr[1][0] === k1) + assert(asArr[1][1] === bob) + }) + + it('asserts map array constructors properly assert child validity', () => { + const k0 = u32(0n) + const k1 = u32(1n) + const alice = addr( + 'ALICE000000000000000000000000000000000000000000000000000' + ) + const bob = addr( + 'BOB00000000000000000000000000000000000000000000000000000' + ) + + const arrValid: KeyValuePair[] = [ + [k0, alice], + [k1, bob], + ] + + const arrInvalidVal: KeyValuePair[] = [ + [k0, { type: 'addr', val: 'ALICE' }], + ] + const arrInvalidKey: KeyValuePair[] = [[{ type: 'u32', val: -1n }, bob]] + const arrInvalidDup: KeyValuePair[] = [ + [k0, alice], + [k0, bob], + ] + + assert.throws(() => { + mapFromKV(arrInvalidVal) + }, TypeError) + assert.throws(() => { + mapFromKV(arrInvalidKey) + }, TypeError) + assert.throws(() => { + mapFromKV(arrInvalidDup) + }, TypeError) + + const valdiMap = mapFromKV(arrValid) + const asArr = toArr(valdiMap) + + assert(asArr.length === arrValid.length) + assert(asArr[0].length === arrValid[0].length) + assert(asArr[1].length === arrValid[1].length) + + assert(asArr[0][0] === arrValid[0][0]) + assert(asArr[0][1] === arrValid[0][1]) + assert(asArr[1][0] === arrValid[1][0]) + assert(asArr[1][1] === arrValid[1][1]) }) -}) \ No newline at end of file +})