Repository: algorand/js-algorand-sdk (ts)

Issue:
Add ABI encoding support
## Summary
Add support for encoding and decoding ABI values, as described in https://github.com/algorandfoundation/ARCs/pull/7

## Scope
Add ability to:
* Parse and serialize ABI type strings, e.g. `(uint64, byte[], address)`
* Encode and decode ABI values, using type information

Pull request description:
ABI Support for JS library
This PR adds ABI type encoding support for the JS SDK library.

Closes #425 

Patch:
diff --git a/src/abi/abi_type.ts b/src/abi/abi_type.ts
new file mode 100644
index 0000000..8609198
--- /dev/null
+++ b/src/abi/abi_type.ts
@@ -0,0 +1,810 @@
+/* eslint-disable no-bitwise */
+/* eslint-disable no-use-before-define */
+/* eslint-disable class-methods-use-this */
+
+/**
+    //ABI-Types: uint<N>: An N-bit unsigned integer (8 <= N <= 512 and N % 8 = 0).
+    // | byte (alias for uint8)
+    // | ufixed <N> x <M> (8 <= N <= 512, N % 8 = 0, and 0 < M <= 160)
+    // | bool
+    // | address (alias for byte[32])
+    // | <type> [<N>]
+    // | <type> []
+    // | string
+    // | (T1, ..., Tn)
+*/
+import { encodeAddress, decodeAddress } from '../encoding/address';
+import { bigIntToBytes, bytesToBigInt } from '../encoding/bigint';
+import { concatArrays } from '../utils/utils';
+
+export const MAX_LEN = 2 ** 16 - 1;
+export const ADDR_BYTE_SIZE = 32;
+export const SINGLE_BYTE_SIZE = 1;
+export const SINGLE_BOOL_SIZE = 1;
+export const LENGTH_ENCODE_BYTE_SIZE = 2;
+
+interface Segment {
+  left: number;
+  right: number;
+}
+
+const staticArrayRegexp = /^([a-z\d[\](),]+)\[([1-9][\d]*)]$/;
+const ufixedRegexp = /^ufixed([1-9][\d]*)x([1-9][\d]*)$/;
+
+type ABIValue = boolean | number | bigint | string | Uint8Array | ABIValue[];
+
+export abstract class ABIType {
+  // Converts a ABIType object to a string
+  abstract toString(): string;
+  // Checks if two ABIType objects are equal in value
+  abstract equal(other: ABIType): boolean;
+  // Checks if the ABIType object (or any of its child types) have dynamic length
+  abstract isDynamic(): boolean;
+  // Returns the size of the ABIType object in bytes
+  abstract byteLen(): number;
+  // Encodes a value for the ABIType object using the ABI specs
+  abstract encode(value: ABIValue): Uint8Array;
+  // Decodes a value for the ABIType object using the ABI specs
+  abstract decode(byteString: Uint8Array): ABIValue;
+  // De-serializes the ABI type from a string using the ABI specs
+  static from(str: string): ABIType {
+    if (str.endsWith('[]')) {
+      const arrayArgType = ABIType.from(str.slice(0, str.length - 2));
+      return new ABIArrayDynamicType(arrayArgType);
+    }
+    if (str.endsWith(']')) {
+      const stringMatches = str.match(staticArrayRegexp);
+      // Match the string itself, array element type, then array length
+      if (stringMatches.length !== 3) {
+        throw new Error(`malformed static array string: ${str}`);
+      }
+      // Parse static array using regex
+      const arrayLengthStr = stringMatches[2];
+      const arrayLength = parseInt(arrayLengthStr, 10);
+      if (arrayLength > MAX_LEN) {
+        throw new Error(`array length exceeds limit ${MAX_LEN}`);
+      }
+      // Parse the array element type
+      const arrayType = ABIType.from(stringMatches[1]);
+      return new ABIArrayStaticType(arrayType, arrayLength);
+    }
+    if (str.startsWith('uint')) {
+      // Checks if the parsed number contains only digits, no whitespaces
+      const digitsOnly = (string) =>
+        [...string].every((c) => '0123456789'.includes(c));
+      const typeSizeStr = str.slice(4, str.length);
+      if (!digitsOnly(typeSizeStr)) {
+        throw new Error(`malformed uint string: ${typeSizeStr}`);
+      }
+      const typeSize = parseInt(typeSizeStr, 10);
+      if (typeSize > MAX_LEN) {
+        throw new Error(`malformed uint string: ${typeSize}`);
+      }
+      return new ABIUintType(typeSize);
+    }
+    if (str === 'byte') {
+      return new ABIByteType();
+    }
+    if (str.startsWith('ufixed')) {
+      const stringMatches = str.match(ufixedRegexp);
+      if (stringMatches.length !== 3) {
+        throw new Error(`malformed ufixed type: ${str}`);
+      }
+      const ufixedSize = parseInt(stringMatches[1], 10);
+      const ufixedPrecision = parseInt(stringMatches[2], 10);
+      return new ABIUfixedType(ufixedSize, ufixedPrecision);
+    }
+    if (str === 'bool') {
+      return new ABIBoolType();
+    }
+    if (str === 'address') {
+      return new ABIAddressType();
+    }
+    if (str === 'string') {
+      return new ABIStringType();
+    }
+    if (str.length >= 2 && str[0] === '(' && str[str.length - 1] === ')') {
+      const tupleContent = ABITupleType.parseTupleContent(
+        str.slice(1, str.length - 1)
+      );
+      const tupleTypes: ABIType[] = [];
+      for (let i = 0; i < tupleContent.length; i++) {
+        const ti = ABIType.from(tupleContent[i]);
+        tupleTypes.push(ti);
+      }
+      return new ABITupleType(tupleTypes);
+    }
+    throw new Error(`cannot convert a string ${str} to an ABI type`);
+  }
+}
+
+export class ABIUintType extends ABIType {
+  bitSize: number;
+
+  constructor(size: number) {
+    super();
+    if (size % 8 !== 0 || size < 8 || size > 512) {
+      throw new Error(`unsupported uint type bitSize: ${size}`);
+    }
+    this.bitSize = size;
+  }
+
+  toString() {
+    return `uint${this.bitSize}`;
+  }
+
+  equal(other: ABIUintType) {
+    return (
+      this.constructor === other.constructor && this.bitSize === other.bitSize
+    );
+  }
+
+  isDynamic() {
+    return false;
+  }
+
+  byteLen() {
+    return this.bitSize / 8;
+  }
+
+  encode(value: bigint | number) {
+    if (
+      (typeof value !== 'bigint' && typeof value !== 'number') ||
+      value >= BigInt(2 ** this.bitSize) ||
+      value < BigInt(0)
+    ) {
+      throw new Error(
+        `${value} is not a non-negative int or too big to fit in size uint${this.bitSize}`
+      );
+    }
+    if (typeof value === 'number' && !Number.isSafeInteger(value)) {
+      throw new Error(
+        `${value} should be converted into a BigInt before it is encoded`
+      );
+    }
+    return bigIntToBytes(value, this.bitSize / 8);
+  }
+
+  decode(byteString: Uint8Array) {
+    if (byteString.length !== this.bitSize / 8) {
+      throw new Error(`byte string must correspond to a uint${this.bitSize}`);
+    }
+    return bytesToBigInt(byteString);
+  }
+}
+
+export class ABIUfixedType extends ABIType {
+  bitSize: number;
+  precision: number;
+
+  constructor(size: number, denominator: number) {
+    super();
+    if (size % 8 !== 0 || size < 8 || size > 512) {
+      throw new Error(`unsupported ufixed type bitSize: ${size}`);
+    }
+    if (denominator > 160 || denominator < 1) {
+      throw new Error(`unsupported ufixed type precision: ${denominator}`);
+    }
+    this.bitSize = size;
+    this.precision = denominator;
+  }
+
+  toString() {
+    return `ufixed${this.bitSize}x${this.precision}`;
+  }
+
+  equal(other: ABIUfixedType) {
+    return (
+      this.constructor === other.constructor &&
+      this.bitSize === other.bitSize &&
+      this.precision === other.precision
+    );
+  }
+
+  isDynamic() {
+    return false;
+  }
+
+  byteLen() {
+    return this.bitSize / 8;
+  }
+
+  encode(value: bigint | number) {
+    if (
+      (typeof value !== 'bigint' && typeof value !== 'number') ||
+      value >= BigInt(2 ** this.bitSize) ||
+      value < BigInt(0)
+    ) {
+      throw new Error(
+        `${value} is not a non-negative int or too big to fit in size ufixed${this.bitSize}`
+      );
+    }
+    if (typeof value === 'number' && !Number.isSafeInteger(value)) {
+      throw new Error(
+        `${value} should be converted into a BigInt before it is encoded`
+      );
+    }
+    return bigIntToBytes(value, this.bitSize / 8);
+  }
+
+  decode(byteString: Uint8Array) {
+    if (byteString.length !== this.bitSize / 8) {
+      throw new Error(`byte string must correspond to a ufixed${this.bitSize}`);
+    }
+    return bytesToBigInt(byteString);
+  }
+}
+
+export class ABIAddressType extends ABIType {
+  toString() {
+    return 'address';
+  }
+
+  equal(other: ABIAddressType) {
+    return this.constructor === other.constructor;
+  }
+
+  isDynamic() {
+    return false;
+  }
+
+  byteLen() {
+    return ADDR_BYTE_SIZE;
+  }
+
+  encode(value: string | Uint8Array) {
+    if (typeof value === 'string') {
+      const decodedAddress = decodeAddress(value);
+      return decodedAddress.publicKey;
+    }
+    // Return the address if it is already in bytes
+    if (value.length !== 32) {
+      throw new Error(`byte string must be 32 bytes long for an address`);
+    }
+    return value;
+  }
+
+  decode(byteString: Uint8Array) {
+    if (byteString.length !== 32) {
+      throw new Error(`byte string must be 32 bytes long for an address`);
+    }
+    return encodeAddress(byteString);
+  }
+}
+
+export class ABIBoolType extends ABIType {
+  toString() {
+    return 'bool';
+  }
+
+  equal(other: ABIBoolType) {
+    return this.constructor === other.constructor;
+  }
+
+  isDynamic() {
+    return false;
+  }
+
+  byteLen() {
+    return SINGLE_BOOL_SIZE;
+  }
+
+  encode(value: boolean) {
+    if (value) {
+      return new Uint8Array([128]);
+    }
+    return new Uint8Array([0]);
+  }
+
+  decode(byteString: Uint8Array) {
+    if (byteString.length !== 1) {
+      throw new Error(`bool string must be 1 byte long`);
+    }
+    const value = byteString[0];
+    if (value === 128) {
+      return true;
+    }
+    if (value === 0) {
+      return false;
+    }
+    throw new Error(`boolean could not be decoded from the byte string`);
+  }
+}
+
+export class ABIByteType extends ABIType {
+  toString() {
+    return 'byte';
+  }
+
+  equal(other: ABIByteType) {
+    return this.constructor === other.constructor;
+  }
+
+  isDynamic() {
+    return false;
+  }
+
+  byteLen() {
+    return SINGLE_BYTE_SIZE;
+  }
+
+  encode(value: number) {
+    if (value < 0 || value > 255) {
+      throw new Error(`${value} cannot be encoded into a byte`);
+    }
+    return new Uint8Array([value]);
+  }
+
+  decode(byteString: Uint8Array) {
+    if (byteString.length !== 1) {
+      throw new Error(`byte string must be 1 byte long`);
+    }
+    return byteString[0];
+  }
+}
+
+export class ABIStringType extends ABIType {
+  toString() {
+    return 'string';
+  }
+
+  equal(other: ABIStringType) {
+    return this.constructor === other.constructor;
+  }
+
+  isDynamic() {
+    return true;
+  }
+
+  byteLen(): never {
+    throw new Error(`${this.toString()} is a dynamic type`);
+  }
+
+  encode(value: string) {
+    const encodedBytes = Buffer.from(value);
+    const encodedLength = bigIntToBytes(value.length, LENGTH_ENCODE_BYTE_SIZE);
+    const mergedBytes = new Uint8Array(value.length + LENGTH_ENCODE_BYTE_SIZE);
+    mergedBytes.set(encodedLength);
+    mergedBytes.set(encodedBytes, LENGTH_ENCODE_BYTE_SIZE);
+    return mergedBytes;
+  }
+
+  decode(byteString: Uint8Array) {
+    if (byteString.length < LENGTH_ENCODE_BYTE_SIZE) {
+      throw new Error(`byte string is too short to be decoded: ${byteString}`);
+    }
+    const buf = Buffer.from(byteString);
+    const byteLength = buf.readUIntBE(0, LENGTH_ENCODE_BYTE_SIZE);
+    const byteValue = byteString.slice(
+      LENGTH_ENCODE_BYTE_SIZE,
+      byteString.length
+    );
+    if (byteLength !== byteValue.length) {
+      throw new Error(
+        `string length bytes do not match the actual length of string: ${byteString}`
+      );
+    }
+    const stringValue = Buffer.from(byteValue).toString('utf-8');
+    return stringValue;
+  }
+}
+
+export class ABIArrayStaticType extends ABIType {
+  childType: ABIType;
+  staticLength: number;
+
+  constructor(argType: ABIType, arrayLength: number) {
+    super();
+    if (arrayLength < 1) {
+      throw new Error(
+        `static array must have a length greater than 0: ${arrayLength}`
+      );
+    }
+    this.childType = argType;
+    this.staticLength = arrayLength;
+  }
+
+  toString() {
+    return `${this.childType.toString()}[${this.staticLength}]`;
+  }
+
+  equal(other: ABIArrayStaticType) {
+    return (
+      this.constructor === other.constructor &&
+      this.childType === other.childType &&
+      this.staticLength === other.staticLength
+    );
+  }
+
+  isDynamic() {
+    return this.childType.isDynamic();
+  }
+
+  byteLen() {
+    if (this.childType.constructor === ABIBoolType) {
+      return Math.ceil(this.staticLength / 8);
+    }
+    return this.staticLength * this.childType.byteLen();
+  }
+
+  encode(values: ABIValue[]) {
+    if (values.length !== this.staticLength) {
+      throw new Error(`value array does not match static array length`);
+    }
+    const convertedTuple = this.toABITupleType();
+    return convertedTuple.encode(values);
+  }
+
+  decode(byteString: Uint8Array) {
+    const convertedTuple = this.toABITupleType();
+    return convertedTuple.decode(byteString);
+  }
+
+  toABITupleType() {
+    return new ABITupleType(Array(this.staticLength).fill(this.childType));
+  }
+}
+
+export class ABIArrayDynamicType extends ABIType {
+  childType: ABIType;
+
+  constructor(argType: ABIType) {
+    super();
+    this.childType = argType;
+  }
+
+  toString() {
+    return `${this.childType.toString()}[]`;
+  }
+
+  equal(other: ABIArrayDynamicType) {
+    return (
+      this.constructor === other.constructor &&
+      this.childType === other.childType
+    );
+  }
+
+  isDynamic() {
+    return true;
+  }
+
+  byteLen(): never {
+    throw new Error(`${this.toString()} is a dynamic type`);
+  }
+
+  encode(values: ABIValue[]) {
+    const convertedTuple = this.toABITupleType(values.length);
+    const encodedTuple = convertedTuple.encode(values);
+    const encodedLength = bigIntToBytes(
+      convertedTuple.childTypes.length,
+      LENGTH_ENCODE_BYTE_SIZE
+    );
+    const mergedBytes = concatArrays(encodedLength, encodedTuple);
+    return mergedBytes;
+  }
+
+  decode(byteString: Uint8Array) {
+    const buf = Buffer.from(byteString);
+    const byteLength = buf.readUIntBE(0, LENGTH_ENCODE_BYTE_SIZE);
+    const convertedTuple = this.toABITupleType(byteLength);
+    return convertedTuple.decode(
+      byteString.slice(LENGTH_ENCODE_BYTE_SIZE, byteString.length)
+    );
+  }
+
+  toABITupleType(length: number) {
+    return new ABITupleType(Array(length).fill(this.childType));
+  }
+}
+
+export class ABITupleType extends ABIType {
+  childTypes: ABIType[];
+
+  constructor(argTypes: ABIType[]) {
+    super();
+    if (argTypes.length >= MAX_LEN) {
+      throw new Error(
+        'tuple type child type number larger than maximum uint16 error'
+      );
+    }
+    this.childTypes = argTypes;
+  }
+
+  toString() {
+    const typeStrings: string[] = [];
+    for (let i = 0; i < this.childTypes.length; i++) {
+      typeStrings[i] = this.childTypes[i].toString();
+    }
+    return `(${typeStrings.join(',')})`;
+  }
+
+  equal(other: ABITupleType) {
+    return (
+      this.constructor === other.constructor &&
+      this.childTypes === other.childTypes
+    );
+  }
+
+  isDynamic() {
+    const isDynamic = (child: ABIType) => child.isDynamic();
+    return this.childTypes.some(isDynamic);
+  }
+
+  byteLen() {
+    let size = 0;
+    for (let i = 0; i < this.childTypes.length; i++) {
+      if (this.childTypes[i].constructor === ABIBoolType) {
+        const after = findBoolLR(this.childTypes, i, 1);
+        const boolNum = after + 1;
+        i += after;
+        size += Math.trunc((boolNum + 7) / 8);
+      } else {
+        const childByteSize = this.childTypes[i].byteLen();
+        size += childByteSize;
+      }
+    }
+    return size;
+  }
+
+  encode(values: ABIValue[]) {
+    if (values.length > MAX_LEN) {
+      throw new Error('length of tuple array should not exceed a uint16');
+    }
+    const tupleTypes = this.childTypes;
+    const heads: Uint8Array[] = [];
+    const tails: Uint8Array[] = [];
+    const isDynamicIndex = new Map<number, boolean>();
+    let i = 0;
+
+    while (i < tupleTypes.length) {
+      const tupleType = tupleTypes[i];
+      if (tupleType.isDynamic()) {
+        // Head is not pre-determined for dynamic types; store a placeholder for now
+        isDynamicIndex.set(heads.length, true);
+        heads.push(new Uint8Array([0, 0]));
+        tails.push(tupleType.encode(values[i]));
+      } else {
+        if (tupleType.constructor === ABIBoolType) {
+          const before = findBoolLR(tupleTypes, i, -1);
+          let after = findBoolLR(tupleTypes, i, 1);
+
+          // Pack bytes to heads and tails
+          if (before % 8 !== 0) {
+            throw new Error(
+              'expected before index should have number of bool mod 8 equal 0'
+            );
+          }
+          after = Math.min(7, after);
+          const compressedInt = compressMultipleBool(
+            values.slice(i, i + after + 1)
+          );
+          heads.push(bigIntToBytes(compressedInt, 1));
+          i += after;
+        } else {
+          const encodedTupleValue = tupleType.encode(values[i]);
+          heads.push(encodedTupleValue);
+        }
+        isDynamicIndex.set(i, false);
+        tails.push(new Uint8Array());
+      }
+      i += 1;
+    }
+
+    // Adjust head lengths for dynamic types
+    let headLength = 0;
+    for (const headElement of heads) {
+      headLength += headElement.length;
+    }
+
+    // encode any placeholders for dynamic types
+    let tailLength = 0;
+    for (let j = 0; j < heads.length; j++) {
+      if (isDynamicIndex.get(j)) {
+        const headValue = headLength + tailLength;
+        if (headValue > MAX_LEN) {
+          throw new Error(
+            `byte length of ${headValue} should not exceed a uint16`
+          );
+        }
+        heads[j] = bigIntToBytes(headValue, LENGTH_ENCODE_BYTE_SIZE);
+      }
+      tailLength += tails[j].length;
+    }
+
+    return concatArrays(...heads, ...tails);
+  }
+
+  decode(byteString: Uint8Array) {
+    const tupleTypes = this.childTypes;
+    const dynamicSegments: Segment[] = [];
+    const valuePartition: Uint8Array[] = [];
+    let i = 0;
+    let iterIndex = 0;
+    const buf = Buffer.from(byteString);
+
+    while (i < tupleTypes.length) {
+      const tupleType = tupleTypes[i];
+      if (tupleType.isDynamic()) {
+        if (
+          byteString.slice(iterIndex, byteString.length).length <
+          LENGTH_ENCODE_BYTE_SIZE
+        ) {
+          throw new Error('dynamic type in tuple is too short to be decoded');
+        }
+        const dynamicIndex = buf.readUIntBE(iterIndex, LENGTH_ENCODE_BYTE_SIZE);
+        if (dynamicSegments.length > 0) {
+          dynamicSegments[dynamicSegments.length - 1].right = dynamicIndex;
+          // Check that right side of segment is greater than the left side
+          if (dynamicIndex < dynamicSegments[dynamicSegments.length - 1].left) {
+            throw new Error(
+              'dynamic index segment miscalculation: left is greater than right index'
+            );
+          }
+        }
+        // Since we do not know where the current dynamic element ends, put a placeholder and update later
+        const seg: Segment = {
+          left: dynamicIndex,
+          right: -1,
+        };
+        dynamicSegments.push(seg);
+        valuePartition.push(null);
+        iterIndex += LENGTH_ENCODE_BYTE_SIZE;
+      } else {
+        // eslint-disable-next-line no-lonely-if
+        if (tupleType.constructor === ABIBoolType) {
+          const before = findBoolLR(this.childTypes, i, -1);
+          let after = findBoolLR(this.childTypes, i, 1);
+
+          if (before % 8 !== 0) {
+            throw new Error('expected before bool number mod 8 === 0');
+          }
+          after = Math.min(7, after);
+          // Parse bool in a byte to multiple byte strings
+          for (let boolIndex = 0; boolIndex <= after; boolIndex++) {
+            const boolMask = 0x80 >> boolIndex;
+            if ((byteString[iterIndex] & boolMask) > 0) {
+              valuePartition.push(new Uint8Array([128]));
+            } else {
+              valuePartition.push(new Uint8Array([0]));
+            }
+          }
+          i += after;
+          iterIndex += 1;
+        } else {
+          const currLen = tupleType.byteLen();
+          valuePartition.push(byteString.slice(iterIndex, iterIndex + currLen));
+          iterIndex += currLen;
+        }
+      }
+      if (i !== tupleTypes.length - 1 && iterIndex >= byteString.length) {
+        throw new Error('input byte not enough to decode');
+      }
+      i += 1;
+    }
+    if (dynamicSegments.length > 0) {
+      dynamicSegments[dynamicSegments.length - 1].right = byteString.length;
+      iterIndex = byteString.length;
+    }
+    if (iterIndex < byteString.length) {
+      throw new Error('input byte not fully consumed');
+    }
+
+    // Check segment indices are valid
+    // If the dynamic segment are not consecutive and well-ordered, we return error
+    // eslint-disable-next-line no-shadow
+    for (let i = 0; i < dynamicSegments.length; i++) {
+      const seg = dynamicSegments[i];
+      if (seg.left > seg.right) {
+        throw new Error(
+          'dynamic segment should display a [l, r] space with l <= r'
+        );
+      }
+      if (
+        i !== dynamicSegments.length - 1 &&
+        seg.right !== dynamicSegments[i + 1].left
+      ) {
+        throw new Error('dynamic segment should be consecutive');
+      }
+    }
+
+    // Check dynamic element partitions
+    let segIndex = 0;
+    // eslint-disable-next-line no-shadow
+    for (let i = 0; i < tupleTypes.length; i++) {
+      if (tupleTypes[i].isDynamic()) {
+        valuePartition[i] = byteString.slice(
+          dynamicSegments[segIndex].left,
+          dynamicSegments[segIndex].right
+        );
+        segIndex += 1;
+      }
+    }
+
+    // Decode each tuple element
+    const returnValues: ABIValue[] = [];
+    // eslint-disable-next-line no-shadow
+    for (let i = 0; i < tupleTypes.length; i++) {
+      const valueTi = tupleTypes[i].decode(valuePartition[i]);
+      returnValues.push(valueTi);
+    }
+    return returnValues;
+  }
+
+  static parseTupleContent(str: string): string[] {
+    if (str.length === 0) {
+      return [];
+    }
+    if (str.endsWith(',') || str.startsWith(',')) {
+      throw new Error('tuple string should not start with comma');
+    }
+    if (str.includes(',,')) {
+      throw new Error('tuple string should not have consecutive commas');
+    }
+
+    const tupleStrings: string[] = [];
+    let depth = 0;
+    let word = '';
+
+    for (const char of str) {
+      word += char;
+      if (char === '(') {
+        depth += 1;
+      } else if (char === ')') {
+        depth -= 1;
+      } else if (char === ',') {
+        // If the comma is at depth 0, then append the word as token.
+        if (depth === 0) {
+          tupleStrings.push(word.slice(0, word.length - 1));
+          word = '';
+        }
+      }
+    }
+    if (word.length !== 0) {
+      tupleStrings.push(word);
+    }
+    if (depth !== 0) {
+      throw new Error('tuple string has mismatched parentheses');
+    }
+    return tupleStrings;
+  }
+}
+
+// compressMultipleBool compresses consecutive bool values into a byte in ABI tuple / array value.
+function compressMultipleBool(valueList: ABIValue[]): number {
+  let res = 0;
+  if (valueList.length > 8) {
+    throw new Error('value list passed in should be no greater than length 8');
+  }
+  for (let i = 0; i < valueList.length; i++) {
+    const boolVal = valueList[i];
+    if (typeof boolVal !== 'boolean') {
+      throw new Error('non-boolean values cannot be compressed into a byte');
+    }
+    if (boolVal) {
+      res |= 1 << (7 - i);
+    }
+  }
+  return res;
+}
+
+// Assume that the current index on the list of type is an ABI bool type.
+// It returns the difference between the current index and the index of the furthest consecutive Bool type.
+function findBoolLR(typeList: ABIType[], index: number, delta: -1 | 1): number {
+  let until = 0;
+  while (true) {
+    const curr = index + delta * until;
+    if (typeList[curr].constructor === ABIBoolType) {
+      if (curr !== typeList.length - 1 && delta === 1) {
+        until += 1;
+      } else if (curr > 0 && delta === -1) {
+        until += 1;
+      } else {
+        break;
+      }
+    } else {
+      until -= 1;
+      break;
+    }
+  }
+  return until;
+}
diff --git a/src/encoding/bigint.ts b/src/encoding/bigint.ts
new file mode 100644
index 0000000..1f1b15d
--- /dev/null
+++ b/src/encoding/bigint.ts
@@ -0,0 +1,33 @@
+/**
+ * bigIntToBytes converts a BigInt to a big-endian Uint8Array for encoding.
+ * @param bi - The bigint to convert.
+ * @param size - The size of the resulting byte array.
+ * @returns A byte array containing the big-endian encoding of the input bigint
+ */
+export function bigIntToBytes(bi: bigint | number, size: number) {
+  let hex = bi.toString(16);
+  // Pad the hex with zeros so it matches the size in bytes
+  if (hex.length !== size * 2) {
+    hex = hex.padStart(size * 2, '0');
+  }
+  const byteArray = new Uint8Array(hex.length / 2);
+  for (let i = 0, j = 0; i < hex.length / 2; i++, j += 2) {
+    byteArray[i] = parseInt(hex.slice(j, j + 2), 16);
+  }
+  return byteArray;
+}
+
+/**
+ * bytesToBigInt produces a bigint from a binary representation.
+ *
+ * @param bytes - The Uint8Array to convert.
+ * @returns The bigint that was encoded in the input data.
+ */
+export function bytesToBigInt(bytes: Uint8Array) {
+  let res = BigInt(0);
+  const buf = Buffer.from(bytes);
+  for (let i = 0; i < bytes.length; i++) {
+    res = BigInt(Number(buf.readUIntBE(i, 1))) + res * BigInt(256);
+  }
+  return res;
+}
diff --git a/src/main.ts b/src/main.ts
index f5050c4..7dd43dd 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -270,3 +270,4 @@ export const LogicTemplates = LogicTemplatesCommonJSExport.default;
 export * from './makeTxn';
 export * from './transaction';
 export * from './types';
+export * from './abi/abi_type';


Test patch:
diff --git a/tests/10.ABI.ts b/tests/10.ABI.ts
new file mode 100644
index 0000000..ca90f6f
--- /dev/null
+++ b/tests/10.ABI.ts
@@ -0,0 +1,586 @@
+import assert from 'assert';
+import {
+  ABIAddressType,
+  ABIArrayDynamicType,
+  ABIArrayStaticType,
+  ABIBoolType,
+  ABIByteType,
+  ABIStringType,
+  ABITupleType,
+  ABIUfixedType,
+  ABIUintType,
+  ABIType,
+} from '../src/abi/abi_type';
+import { decodeAddress } from '../src/encoding/address';
+
+describe('ABI type checking', () => {
+  it('should create the correct type from the string', () => {
+    for (let i = 8; i < 513; i += 8) {
+      let expected = new ABIUintType(i);
+      let actual = ABIType.from(`uint${i}`);
+      assert.deepStrictEqual(actual, expected);
+      for (let j = 1; j < 161; j++) {
+        expected = new ABIUfixedType(i, j);
+        actual = ABIType.from(`ufixed${i}x${j}`);
+        assert.deepStrictEqual(actual, expected);
+      }
+    }
+
+    const testCases = [
+      ['address', new ABIAddressType()],
+      ['bool', new ABIBoolType()],
+      ['byte', new ABIByteType()],
+      ['string', new ABIStringType()],
+      ['uint32[]', new ABIArrayDynamicType(new ABIUintType(32))],
+      [
+        'byte[][]',
+        new ABIArrayDynamicType(new ABIArrayDynamicType(new ABIByteType())),
+      ],
+      ['ufixed256x64[]', new ABIArrayDynamicType(new ABIUfixedType(256, 64))],
+      [
+        'ufixed128x10[100]',
+        new ABIArrayStaticType(new ABIUfixedType(128, 10), 100),
+      ],
+      [
+        'bool[256][100]',
+        new ABIArrayStaticType(
+          new ABIArrayStaticType(new ABIBoolType(), 256),
+          100
+        ),
+      ],
+      ['()', new ABITupleType([])],
+      [
+        '(uint16,(byte,address[10]))',
+        new ABITupleType([
+          new ABIUintType(16),
+          new ABITupleType([
+            new ABIByteType(),
+            new ABIArrayStaticType(new ABIAddressType(), 10),
+          ]),
+        ]),
+      ],
+      [
+        '(uint256,(byte,address[10]),(),bool)',
+        new ABITupleType([
+          new ABIUintType(256),
+          new ABITupleType([
+            new ABIByteType(),
+            new ABIArrayStaticType(new ABIAddressType(), 10),
+          ]),
+          new ABITupleType([]),
+          new ABIBoolType(),
+        ]),
+      ],
+      [
+        '(ufixed256x16,((string),bool,(address,uint8)))',
+        new ABITupleType([
+          new ABIUfixedType(256, 16),
+          new ABITupleType([
+            new ABITupleType([new ABIStringType()]),
+            new ABIBoolType(),
+            new ABITupleType([new ABIAddressType(), new ABIUintType(8)]),
+          ]),
+        ]),
+      ],
+    ];
+
+    for (const testCase of testCases) {
+      const actual = ABIType.from(testCase[0] as string);
+      assert.deepStrictEqual(actual, testCase[1]);
+    }
+  });
+  it('should fail for an invalid bit size or precision', () => {
+    const invalidSizes = [-1, 0, 9, 513, 1024];
+    const invalidPrecisions = [-1, 0, 161];
+
+    for (const size of invalidSizes) {
+      assert.throws(() => new ABIUintType(size));
+      assert.throws(() => new ABIUfixedType(size, 10));
+    }
+    for (const precision of invalidPrecisions) {
+      assert.throws(() => new ABIUfixedType(8, precision));
+    }
+  });
+  it('should fail for an invalid type string', () => {
+    const testCases = [
+      // uint
+      'uint 8',
+      'uint8 ',
+      'uint123x345',
+      'uint!8',
+      'uint[32]',
+      'uint-893',
+      'uint#120\\',
+      // ufixed
+      'ufixed000000000016x0000010',
+      'ufixed123x345',
+      'ufixed 128 x 100',
+      'ufixed64x10 ',
+      'ufixed!8x2 ',
+      'ufixed[32]x16',
+      'ufixed-64x+100',
+      'ufixed16x+12',
+      // dynamic array
+      'byte[] ',
+      '[][][]',
+      'stuff[]',
+      // static array
+      'ufixed32x10[0]',
+      'byte[10 ]',
+      'uint64[0x21]',
+      // tuple
+      '(ufixed128x10))',
+      '(,uint128,byte[])',
+      '(address,ufixed64x5,)',
+      '(byte[16],somethingwrong)',
+      '(                )',
+      '((uint32)',
+      '(byte,,byte)',
+      '((byte),,(byte))',
+    ];
+
+    for (const testCase of testCases) {
+      assert.throws(() => ABIType.from(testCase));
+    }
+  });
+  it('should properly return whether the type is dynamic', () => {
+    const testCases = [
+      [new ABIUintType(8).isDynamic(), false],
+      [new ABIUfixedType(16, 10).isDynamic(), false],
+      [new ABIByteType().isDynamic(), false],
+      [new ABIBoolType().isDynamic(), false],
+      [new ABIAddressType().isDynamic(), false],
+      [new ABIStringType().isDynamic(), true],
+      [new ABIArrayDynamicType(new ABIBoolType()).isDynamic(), true],
+      [
+        new ABIArrayDynamicType(
+          new ABIArrayDynamicType(new ABIByteType())
+        ).isDynamic(),
+        true,
+      ],
+      [ABIType.from('(string[100])').isDynamic(), true],
+      [ABIType.from('(address,bool,uint256)').isDynamic(), false],
+      [ABIType.from('(uint8,(byte[10]))').isDynamic(), false],
+      [ABIType.from('(string,uint256)').isDynamic(), true],
+      [ABIType.from('(bool,(ufixed16x10[],(byte,address)))').isDynamic(), true],
+      [
+        ABIType.from('(bool,(uint256,(byte,address,string)))').isDynamic(),
+        true,
+      ],
+    ];
+
+    for (const testCase of testCases) {
+      const actual = testCase[0];
+      const expected = testCase[1];
+      assert.deepStrictEqual(actual, expected);
+    }
+  });
+  it('should properly return the byte length of the type', () => {
+    const testCases = [
+      [new ABIAddressType().byteLen(), 32],
+      [new ABIByteType().byteLen(), 1],
+      [new ABIBoolType().byteLen(), 1],
+      [new ABIUintType(64).byteLen(), 8],
+      [new ABIUfixedType(256, 50).byteLen(), 32],
+      [ABIType.from('bool[81]').byteLen(), 11],
+      [ABIType.from('bool[80]').byteLen(), 10],
+      [ABIType.from('bool[88]').byteLen(), 11],
+      [ABIType.from('address[5]').byteLen(), 160],
+      [ABIType.from('uint16[20]').byteLen(), 40],
+      [ABIType.from('ufixed64x20[10]').byteLen(), 80],
+      //   [ABIType.from('(address,byte,ufixed16x20)').byteLen(), 35],
+      [
+        ABIType.from(
+          '((bool,address[10]),(bool,bool,bool),uint8[20])'
+        ).byteLen(),
+        342,
+      ],
+      [ABIType.from('(bool,bool)').byteLen(), 1],
+      [ABIType.from(`(${'bool,'.repeat(6)}uint8)`).byteLen(), 2],
+      [
+        ABIType.from(
+          `(${'bool,'.repeat(10)}uint8,${'bool,'.repeat(10)}byte)`
+        ).byteLen(),
+        6,
+      ],
+    ];
+
+    for (const testCase of testCases) {
+      const actual = testCase[0];
+      const expected = testCase[1];
+      assert.deepStrictEqual(actual, expected);
+    }
+
+    // Dynamic types should not have a byte length
+    assert.throws(() => new ABIStringType().byteLen());
+    assert.throws(() => new ABIArrayDynamicType(new ABIBoolType()).byteLen());
+  });
+});
+
+describe('ABI encoding', () => {
+  it('should encode the value correctly into bytes', () => {
+    const testCases = [
+      [new ABIUintType(8).encode(BigInt(0)), new Uint8Array([0])],
+      [new ABIUintType(16).encode(BigInt(3)), new Uint8Array([0, 3])],
+      [
+        new ABIUintType(64).encode(256),
+        new Uint8Array([0, 0, 0, 0, 0, 0, 1, 0]),
+      ],
+      [new ABIUfixedType(8, 30).encode(BigInt(255)), new Uint8Array([255])],
+      [new ABIUfixedType(32, 10).encode(33), new Uint8Array([0, 0, 0, 33])],
+      [
+        new ABIAddressType().encode(
+          'MO2H6ZU47Q36GJ6GVHUKGEBEQINN7ZWVACMWZQGIYUOE3RBSRVYHV4ACJI'
+        ),
+        decodeAddress(
+          'MO2H6ZU47Q36GJ6GVHUKGEBEQINN7ZWVACMWZQGIYUOE3RBSRVYHV4ACJI'
+        ).publicKey,
+      ],
+      [new ABIByteType().encode(10), new Uint8Array([10])],
+      [new ABIByteType().encode(255), new Uint8Array([255])],
+      [new ABIBoolType().encode(true), new Uint8Array([128])],
+      [new ABIBoolType().encode(false), new Uint8Array([0])],
+      [
+        new ABIStringType().encode('asdf'),
+        new Uint8Array([0, 4, 97, 115, 100, 102]),
+      ],
+      [
+        new ABIArrayStaticType(new ABIBoolType(), 3).encode([
+          true,
+          true,
+          false,
+        ]),
+        new Uint8Array([192]),
+      ],
+      [
+        new ABIArrayStaticType(new ABIBoolType(), 8).encode([
+          false,
+          true,
+          false,
+          false,
+          false,
+          false,
+          false,
+          false,
+        ]),
+        new Uint8Array([64]),
+      ],
+      [
+        new ABIArrayStaticType(new ABIBoolType(), 8).encode([
+          true,
+          true,
+          true,
+          true,
+          true,
+          true,
+          true,
+          true,
+        ]),
+        new Uint8Array([255]),
+      ],
+      [
+        new ABIArrayStaticType(new ABIBoolType(), 9).encode([
+          true,
+          false,
+          false,
+          true,
+          false,
+          false,
+          true,
+          false,
+          true,
+        ]),
+        new Uint8Array([146, 128]),
+      ],
+      [
+        new ABIArrayStaticType(new ABIUintType(64), 3).encode([
+          BigInt(1),
+          BigInt(2),
+          BigInt(3),
+        ]),
+        new Uint8Array([
+          0,
+          0,
+          0,
+          0,
+          0,
+          0,
+          0,
+          1,
+          0,
+          0,
+          0,
+          0,
+          0,
+          0,
+          0,
+          2,
+          0,
+          0,
+          0,
+          0,
+          0,
+          0,
+          0,
+          3,
+        ]),
+      ],
+      [
+        new ABIArrayDynamicType(new ABIBoolType()).encode([]),
+        new Uint8Array([0, 0]),
+      ],
+      [
+        new ABIArrayDynamicType(new ABIBoolType()).encode([true, true, false]),
+        new Uint8Array([0, 3, 192]),
+      ],
+      [
+        new ABIArrayDynamicType(new ABIBoolType()).encode([
+          false,
+          true,
+          false,
+          false,
+          false,
+          false,
+          false,
+          false,
+        ]),
+        new Uint8Array([0, 8, 64]),
+      ],
+      [
+        new ABIArrayDynamicType(new ABIBoolType()).encode([
+          true,
+          false,
+          false,
+          true,
+          false,
+          false,
+          true,
+          false,
+          true,
+        ]),
+        new Uint8Array([0, 9, 146, 128]),
+      ],
+      [ABIType.from('()').encode([]), new Uint8Array([])],
+      // 2^6 + 2^5 = 64 + 32 = 96
+      [
+        ABIType.from('(bool,bool,bool)').encode([false, true, true]),
+        new Uint8Array([96]),
+      ],
+      [
+        ABIType.from('(bool[3])').encode([[false, true, true]]),
+        new Uint8Array([96]),
+      ],
+      [
+        ABIType.from('(bool[])').encode([[false, true, true]]),
+        new Uint8Array([0, 2, 0, 3, 96]),
+      ],
+      [
+        ABIType.from('(bool[2],bool[])').encode([
+          [true, true],
+          [true, true],
+        ]),
+        new Uint8Array([192, 0, 3, 0, 2, 192]),
+      ],
+      [
+        ABIType.from('(bool[],bool[])').encode([[], []]),
+        new Uint8Array([0, 4, 0, 6, 0, 0, 0, 0]),
+      ],
+      [
+        ABIType.from('(string,bool,bool,bool,bool,string)').encode([
+          'AB',
+          true,
+          false,
+          true,
+          false,
+          'DE',
+        ]),
+        new Uint8Array([0, 5, 160, 0, 9, 0, 2, 65, 66, 0, 2, 68, 69]),
+      ],
+    ];
+
+    for (const testCase of testCases) {
+      const actual = testCase[0];
+      const expected = testCase[1];
+      assert.deepStrictEqual(actual, expected);
+    }
+  });
+
+  it('should fail for bad values during encoding', () => {
+    assert.throws(() => new ABIUintType(8).encode(BigInt(-1)));
+    assert.throws(() => new ABIUintType(512).encode(BigInt(2 ** 512)));
+    assert.throws(() => new ABIUfixedType(512, 10).encode(BigInt(-1)));
+    assert.throws(() => new ABIByteType().encode(-1));
+    assert.throws(() => new ABIByteType().encode(256));
+    assert.throws(() => new ABIAddressType().encode('BADADDRESS'));
+    assert.throws(() =>
+      new ABIArrayStaticType(new ABIBoolType(), 3).encode([true])
+    );
+    assert.throws(() =>
+      new ABIArrayStaticType(new ABIStringType(), 1).encode([true])
+    );
+    assert.throws(() =>
+      new ABIArrayStaticType(new ABIUintType(256), 1).encode(['hello'])
+    );
+    assert.throws(() =>
+      new ABIArrayDynamicType(new ABIAddressType()).encode([false])
+    );
+    assert.throws(() =>
+      new ABITupleType([new ABIBoolType(), new ABIUfixedType(128, 20)]).encode([
+        BigInt(3),
+        true,
+      ])
+    );
+  });
+
+  it('should decode the value correctly into bytes', () => {
+    const testCases = [
+      [new ABIUintType(8).decode(new Uint8Array([0])), BigInt(0)],
+      [new ABIUintType(16).decode(new Uint8Array([0, 3])), BigInt(3)],
+      [
+        new ABIUintType(64).decode(new Uint8Array([1, 0, 0, 0, 0, 0, 0, 0])),
+        BigInt(2 ** 56),
+      ],
+      [new ABIUfixedType(8, 30).decode(new Uint8Array([255])), BigInt(255)],
+      [
+        new ABIUfixedType(32, 10).decode(new Uint8Array([0, 0, 0, 33])),
+        BigInt(33),
+      ],
+      [
+        new ABIAddressType().decode(
+          decodeAddress(
+            'MO2H6ZU47Q36GJ6GVHUKGEBEQINN7ZWVACMWZQGIYUOE3RBSRVYHV4ACJI'
+          ).publicKey
+        ),
+        'MO2H6ZU47Q36GJ6GVHUKGEBEQINN7ZWVACMWZQGIYUOE3RBSRVYHV4ACJI',
+      ],
+      [new ABIByteType().decode(new Uint8Array([10])), 10],
+      [new ABIByteType().decode(new Uint8Array([255])), 255],
+      [new ABIBoolType().decode(new Uint8Array([128])), true],
+      [new ABIBoolType().decode(new Uint8Array([0])), false],
+      [
+        new ABIStringType().decode(new Uint8Array([0, 4, 97, 115, 100, 102])),
+        'asdf',
+      ],
+      [
+        new ABIArrayStaticType(new ABIBoolType(), 3).decode(
+          new Uint8Array([192])
+        ),
+        [true, true, false],
+      ],
+      [
+        new ABIArrayStaticType(new ABIBoolType(), 8).decode(
+          new Uint8Array([64])
+        ),
+        [false, true, false, false, false, false, false, false],
+      ],
+      [
+        new ABIArrayStaticType(new ABIBoolType(), 8).decode(
+          new Uint8Array([255])
+        ),
+        [true, true, true, true, true, true, true, true],
+      ],
+      [
+        new ABIArrayStaticType(new ABIBoolType(), 9).decode(
+          new Uint8Array([146, 128])
+        ),
+        [true, false, false, true, false, false, true, false, true],
+      ],
+      [
+        new ABIArrayStaticType(new ABIUintType(64), 3).decode(
+          new Uint8Array([
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            1,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            2,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            0,
+            3,
+          ])
+        ),
+        [BigInt(1), BigInt(2), BigInt(3)],
+      ],
+      [
+        new ABIArrayDynamicType(new ABIBoolType()).decode(
+          new Uint8Array([0, 0])
+        ),
+        [],
+      ],
+      [
+        new ABIArrayDynamicType(new ABIBoolType()).decode(
+          new Uint8Array([0, 3, 192])
+        ),
+        [true, true, false],
+      ],
+      [
+        new ABIArrayDynamicType(new ABIBoolType()).decode(
+          new Uint8Array([0, 8, 64])
+        ),
+        [false, true, false, false, false, false, false, false],
+      ],
+      [
+        new ABIArrayDynamicType(new ABIBoolType()).decode(
+          new Uint8Array([0, 9, 146, 128])
+        ),
+        [true, false, false, true, false, false, true, false, true],
+      ],
+      [ABIType.from('()').decode(new Uint8Array([])), []],
+      //   // 2^6 + 2^5 = 64 + 32 = 96
+      [
+        ABIType.from('(bool,bool,bool)').decode(new Uint8Array([96])),
+        [false, true, true],
+      ],
+      [
+        ABIType.from('(bool[3])').decode(new Uint8Array([96])),
+        [[false, true, true]],
+      ],
+      [
+        ABIType.from('(bool[])').decode(new Uint8Array([0, 2, 0, 3, 96])),
+        [[false, true, true]],
+      ],
+      [
+        ABIType.from('(bool[2],bool[])').decode(
+          new Uint8Array([192, 0, 3, 0, 2, 192])
+        ),
+        [
+          [true, true],
+          [true, true],
+        ],
+      ],
+      [
+        ABIType.from('(bool[],bool[])').decode(
+          new Uint8Array([0, 4, 0, 6, 0, 0, 0, 0])
+        ),
+        [[], []],
+      ],
+      [
+        ABIType.from('(string,bool,bool,bool,bool,string)').decode(
+          new Uint8Array([0, 5, 160, 0, 9, 0, 2, 65, 66, 0, 2, 68, 69])
+        ),
+        ['AB', true, false, true, false, 'DE'],
+      ],
+    ];
+
+    for (const testCase of testCases) {
+      const actual = testCase[0];
+      const expected = testCase[1];
+      assert.deepStrictEqual(actual, expected);
+    }
+  });
+});