Repository: algorand/js-algorand-sdk
Pull request #636 (2022-08-30 20:23:16)

FOR REVIEW ONLY - Release  v1.20.0
<!-- Release notes generated using configuration in .github/release.yml at release/v1.20.0 -->

## What's Changed
### Bugfixes
* Bug-Fix: Pass verbosity to the harness and sandbox by @tzaffi in https://github.com/algorand/js-algorand-sdk/pull/630
### Enhancements
* Enhancement: Use sandbox for SDK Testing and remove Indexer v1 steps by @algochoi in https://github.com/algorand/js-algorand-sdk/pull/623
* Tidy:  Ignore algorand-sdk-testing test-harness dir by @michaeldiamant in https://github.com/algorand/js-algorand-sdk/pull/634
* Enhancement: Deprecating use of langspec by @ahangsu in https://github.com/algorand/js-algorand-sdk/pull/632
* enhancement: Initial stateproofs support by @Eric-Warehime in https://github.com/algorand/js-algorand-sdk/pull/629

## New Contributors
* @ahangsu made their first contribution in https://github.com/algorand/js-algorand-sdk/pull/632

**Full Changelog**: https://github.com/algorand/js-algorand-sdk/compare/v1.19.1...v1.20.0-beta.1


Patch:
diff --git a/.gitignore b/.gitignore
index 5ac92e3..47d5bc3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@ temp
 # Environment information
 examples/.env
 
+test-harness/
 tests/cucumber/features/
 tests/cucumber/browser/build
 tests/browser/bundle.*
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d0059da..a506677 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,41 @@
+# v1.20.0
+
+## What's Changed
+
+### Bugfixes
+
+- Bug-Fix: Pass verbosity to the harness and sandbox by @tzaffi in https://github.com/algorand/js-algorand-sdk/pull/630
+
+### Enhancements
+
+- Enhancement: Use sandbox for SDK Testing and remove Indexer v1 steps by @algochoi in https://github.com/algorand/js-algorand-sdk/pull/623
+- Tidy: Ignore algorand-sdk-testing test-harness dir by @michaeldiamant in https://github.com/algorand/js-algorand-sdk/pull/634
+- Enhancement: Deprecating use of langspec by @ahangsu in https://github.com/algorand/js-algorand-sdk/pull/632
+- enhancement: Initial stateproofs support by @Eric-Warehime in https://github.com/algorand/js-algorand-sdk/pull/629
+
+**Full Changelog**: https://github.com/algorand/js-algorand-sdk/compare/v1.19.1...v1.20.0
+
+# v1.20.0-beta.1
+
+## What's Changed
+
+### Bugfixes
+
+- Bug-Fix: Pass verbosity to the harness and sandbox by @tzaffi in https://github.com/algorand/js-algorand-sdk/pull/630
+
+### Enhancements
+
+- Enhancement: Use sandbox for SDK Testing and remove Indexer v1 steps by @algochoi in https://github.com/algorand/js-algorand-sdk/pull/623
+- Tidy: Ignore algorand-sdk-testing test-harness dir by @michaeldiamant in https://github.com/algorand/js-algorand-sdk/pull/634
+- Enhancement: Deprecating use of langspec by @ahangsu in https://github.com/algorand/js-algorand-sdk/pull/632
+- enhancement: Initial stateproofs support by @Eric-Warehime in https://github.com/algorand/js-algorand-sdk/pull/629
+
+## New Contributors
+
+- @ahangsu made their first contribution in https://github.com/algorand/js-algorand-sdk/pull/632
+
+**Full Changelog**: https://github.com/algorand/js-algorand-sdk/compare/v1.19.1...v1.20.0-beta.1
+
 # v1.19.1
 
 ### Enhancements
diff --git a/Makefile b/Makefile
index f41f679..6f9bb46 100644
--- a/Makefile
+++ b/Makefile
@@ -1,11 +1,28 @@
+UNIT_TAGS :=  "$(subst :, or ,$(shell awk '{print $2}' tests/cucumber/unit.tags | paste -s -d: -))"
+INTEGRATIONS_TAGS := "$(subst :, or ,$(shell awk '{print $2}' tests/cucumber/integration.tags | paste -s -d: -))"
+
 unit:
-	node_modules/.bin/cucumber-js --tags "@unit.offline or @unit.algod or @unit.indexer or @unit.rekey or @unit.tealsign or @unit.dryrun or @unit.applications or @unit.responses or @unit.transactions or @unit.transactions.keyreg or @unit.transactions.payment or @unit.responses.231 or @unit.feetest or @unit.indexer.logs or @unit.abijson or @unit.abijson.byname or @unit.atomic_transaction_composer or @unit.responses.unlimited_assets or @unit.indexer.ledger_refactoring or @unit.algod.ledger_refactoring or @unit.dryrun.trace.application or @unit.sourcemap" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js
+	node_modules/.bin/cucumber-js --tags $(UNIT_TAGS) tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js
 	
 integration:
-	node_modules/.bin/cucumber-js --tags "@algod or @assets or @auction or @kmd or @send or @indexer or @rekey_v1 or @send.keyregtxn or @dryrun or @compile or @applications or @indexer.applications or @applications.verified or @indexer.231 or @abi or @c2c or @compile.sourcemap" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js
+	node_modules/.bin/cucumber-js --tags $(INTEGRATIONS_TAGS) tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js
+
+# The following assumes that all cucumber steps are defined in `./tests/cucumber/steps/steps.js` and begin past line 135 of that file.
+# Please note any deviations of the above before presuming correctness.
+display-all-js-steps:
+	tail -n +135 tests/cucumber/steps/steps.js | grep -v '^ *//' | awk "/(Given|Then|When)/,/',/" | grep -E "\'.+\'"  | sed "s/^[^']*'\([^']*\)'.*/\1/g"
+
+harness:
+	./test-harness.sh
+
+docker-build:
+	docker build -t js-sdk-testing -f tests/cucumber/docker/Dockerfile $(CURDIR) --build-arg TEST_BROWSER --build-arg CI=true
+
+docker-run:
+	docker ps -a
+	docker run -it --network host js-sdk-testing:latest
 
-docker-test:
-	./tests/cucumber/docker/run_docker.sh
+docker-test: harness docker-build docker-run
 
 format:
 	npm run format
diff --git a/README.md b/README.md
index 24bd04c..31e26cc 100644
--- a/README.md
+++ b/README.md
@@ -22,8 +22,8 @@ Include a minified browser bundle directly in your HTML like so:
 
 ```html
 <script
-  src="https://unpkg.com/algosdk@v1.19.1/dist/browser/algosdk.min.js"
-  integrity="sha384-vpY7inPLTrCOYSwaOYlQbFwoSY/t3lFMVjAh/iXN+86fNAQ39DeQjlX87aczChqD"
+  src="https://unpkg.com/algosdk@v1.20.0/dist/browser/algosdk.min.js"
+  integrity="sha384-MBUb3pc2VI86j87sLfTwm1kEs2PUDCOFXnXdgspn0lWSJOxr1gu4mZL6L7VE1ITH"
   crossorigin="anonymous"
 ></script>
 ```
@@ -32,8 +32,8 @@ or
 
 ```html
 <script
-  src="https://cdn.jsdelivr.net/npm/algosdk@v1.19.1/dist/browser/algosdk.min.js"
-  integrity="sha384-vpY7inPLTrCOYSwaOYlQbFwoSY/t3lFMVjAh/iXN+86fNAQ39DeQjlX87aczChqD"
+  src="https://cdn.jsdelivr.net/npm/algosdk@v1.20.0/dist/browser/algosdk.min.js"
+  integrity="sha384-MBUb3pc2VI86j87sLfTwm1kEs2PUDCOFXnXdgspn0lWSJOxr1gu4mZL6L7VE1ITH"
   crossorigin="anonymous"
 ></script>
 ```
diff --git a/package-lock.json b/package-lock.json
index 7b6856a..a44628a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "algosdk",
-  "version": "1.19.1",
+  "version": "1.20.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "algosdk",
-      "version": "1.19.1",
+      "version": "1.20.0",
       "license": "MIT",
       "dependencies": {
         "algo-msgpack-with-bigint": "^2.1.1",
diff --git a/package.json b/package.json
index 7a32341..2803b38 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "algosdk",
-  "version": "1.19.1",
+  "version": "1.20.0",
   "description": "The official JavaScript SDK for Algorand",
   "main": "dist/cjs/index.js",
   "module": "dist/esm/index.js",
diff --git a/src/client/v2/algod/algod.ts b/src/client/v2/algod/algod.ts
index c9d4b2b..c64bb49 100644
--- a/src/client/v2/algod/algod.ts
+++ b/src/client/v2/algod/algod.ts
@@ -6,25 +6,27 @@ import AccountApplicationInformation from './accountApplicationInformation';
 import Block from './block';
 import Compile from './compile';
 import Dryrun from './dryrun';
+import Genesis from './genesis';
 import GetAssetByID from './getAssetByID';
 import GetApplicationByID from './getApplicationByID';
 import HealthCheck from './healthCheck';
 import PendingTransactionInformation from './pendingTransactionInformation';
 import PendingTransactions from './pendingTransactions';
 import PendingTransactionsByAddress from './pendingTransactionsByAddress';
+import GetTransactionProof from './getTransactionProof';
 import SendRawTransaction from './sendRawTransaction';
 import Status from './status';
 import StatusAfterBlock from './statusAfterBlock';
 import SuggestedParams from './suggestedParams';
 import Supply from './supply';
 import Versions from './versions';
-import Genesis from './genesis';
-import Proof from './proof';
 import { BaseHTTPClient } from '../../baseHTTPClient';
 import {
   AlgodTokenHeader,
   CustomTokenHeader,
 } from '../../urlTokenBaseHTTPClient';
+import LightBlockHeaderProof from './lightBlockHeaderProof';
+import StateProof from './stateproof';
 
 /**
  * Algod client connects an application to the Algorand blockchain. The algod client requires a valid algod REST endpoint IP address and algod token from an Algorand node that is connected to the network you plan to interact with.
@@ -451,7 +453,7 @@ export default class AlgodClient extends ServiceClient {
    * ```typescript
    * const round = 18038133;
    * const txId = "MEUOC4RQJB23CQZRFRKYEI6WBO73VTTPST5A7B3S5OKBUY6LFUDA";
-   * const proof = await algodClient.getProof(round, txId).do();
+   * const proof = await algodClient.getTransactionProof(round, txId).do();
    * ```
    *
    * [Response data schema details](https://developer.algorand.org/docs/rest-apis/algod/v2/#get-v2blocksroundtransactionstxidproof)
@@ -459,7 +461,39 @@ export default class AlgodClient extends ServiceClient {
    * @param txID - The transaction ID for which to generate a proof.
    * @category GET
    */
-  getProof(round: number, txID: string) {
-    return new Proof(this.c, this.intDecoding, round, txID);
+  getTransactionProof(round: number, txID: string) {
+    return new GetTransactionProof(this.c, this.intDecoding, round, txID);
+  }
+
+  /**
+   * Gets a proof for a given light block header inside a state proof commitment.
+   *
+   * #### Example
+   * ```typescript
+   * const round = 11111111;
+   * const lightBlockHeaderProof = await algodClient.getLightBlockHeaderProof(round).do();
+   * ```
+   *
+   * [Response data schema details](https://developer.algorand.org/docs/rest-apis/algod/v2#get-v2blocksroundlightheaderproof)
+   * @param round
+   */
+  getLightBlockHeaderProof(round: number) {
+    return new LightBlockHeaderProof(this.c, this.intDecoding, round);
+  }
+
+  /**
+   * Gets a state proof that covers a given round.
+   *
+   * #### Example
+   * ```typescript
+   * const round = 11111111;
+   * const stateProof = await algodClient.getStateProof(round).do();
+   * ```
+   *
+   * [Response data schema details](https://developer.algorand.org/docs/rest-apis/algod/v2#get-v2stateproofsround)
+   * @param round
+   */
+  getStateProof(round: number) {
+    return new StateProof(this.c, this.intDecoding, round);
   }
 }
diff --git a/src/client/v2/algod/getTransactionProof.ts b/src/client/v2/algod/getTransactionProof.ts
new file mode 100644
index 0000000..398039a
--- /dev/null
+++ b/src/client/v2/algod/getTransactionProof.ts
@@ -0,0 +1,43 @@
+import JSONRequest from '../jsonrequest';
+import HTTPClient from '../../client';
+import IntDecoding from '../../../types/intDecoding';
+
+export default class GetTransactionProof extends JSONRequest {
+  constructor(
+    c: HTTPClient,
+    intDecoding: IntDecoding,
+    private round: number,
+    private txID: string
+  ) {
+    super(c, intDecoding);
+
+    this.round = round;
+    this.txID = txID;
+  }
+
+  path() {
+    return `/v2/blocks/${this.round}/transactions/${this.txID}/proof`;
+  }
+
+  /**
+   * Exclude assets and application data from results
+   * The type of hash function used to create the proof, must be one of: "sha512_256", "sha256"
+   *
+   * #### Example
+   * ```typescript
+   * const hashType = "sha256";
+   * const round = 123456;
+   * const txId = "abc123;
+   * const txProof = await algodClient.getTransactionProof(round, txId)
+   *        .hashType(hashType)
+   *        .do();
+   * ```
+   *
+   * @param hashType
+   * @category query
+   */
+  hashType(hashType: string) {
+    this.query.hashtype = hashType;
+    return this;
+  }
+}
diff --git a/src/client/v2/algod/lightBlockHeaderProof.ts b/src/client/v2/algod/lightBlockHeaderProof.ts
new file mode 100644
index 0000000..ac3794c
--- /dev/null
+++ b/src/client/v2/algod/lightBlockHeaderProof.ts
@@ -0,0 +1,15 @@
+import JSONRequest from '../jsonrequest';
+import HTTPClient from '../../client';
+import IntDecoding from '../../../types/intDecoding';
+
+export default class LightBlockHeaderProof extends JSONRequest {
+  constructor(c: HTTPClient, intDecoding: IntDecoding, private round: number) {
+    super(c, intDecoding);
+
+    this.round = round;
+  }
+
+  path() {
+    return `/v2/blocks/${this.round}/lightheader/proof`;
+  }
+}
diff --git a/src/client/v2/algod/models/types.ts b/src/client/v2/algod/models/types.ts
index d91d84c..f5d0fbf 100644
--- a/src/client/v2/algod/models/types.ts
+++ b/src/client/v2/algod/models/types.ts
@@ -1631,6 +1631,54 @@ export class EvalDeltaKeyValue extends BaseModel {
   }
 }
 
+/**
+ * Proof of membership and position of a light block header.
+ */
+export class LightBlockHeaderProof extends BaseModel {
+  /**
+   * The index of the light block header in the vector commitment tree
+   */
+  public index: number | bigint;
+
+  /**
+   * The encoded proof.
+   */
+  public proof: Uint8Array;
+
+  /**
+   * Represents the depth of the tree that is being proven, i.e. the number of edges
+   * from a leaf to the root.
+   */
+  public treedepth: number | bigint;
+
+  /**
+   * Creates a new `LightBlockHeaderProof` object.
+   * @param index - The index of the light block header in the vector commitment tree
+   * @param proof - The encoded proof.
+   * @param treedepth - Represents the depth of the tree that is being proven, i.e. the number of edges
+   * from a leaf to the root.
+   */
+  constructor(
+    index: number | bigint,
+    proof: string | Uint8Array,
+    treedepth: number | bigint
+  ) {
+    super();
+    this.index = index;
+    this.proof =
+      typeof proof === 'string'
+        ? new Uint8Array(Buffer.from(proof, 'base64'))
+        : proof;
+    this.treedepth = treedepth;
+
+    this.attribute_map = {
+      index: 'index',
+      proof: 'proof',
+      treedepth: 'treedepth',
+    };
+  }
+}
+
 /**
  *
  */
@@ -2038,80 +2086,112 @@ export class PostTransactionsResponse extends BaseModel {
 }
 
 /**
- * Proof of transaction in a block.
+ * Represents a state proof and its corresponding message
  */
-export class ProofResponse extends BaseModel {
+export class StateProof extends BaseModel {
   /**
-   * Index of the transaction in the block's payset.
+   * Represents the message that the state proofs are attesting to.
    */
-  public idx: number | bigint;
+  public message: StateProofMessage;
 
   /**
-   * Merkle proof of transaction membership.
+   * The encoded StateProof for the message.
    */
-  public proof: Uint8Array;
+  public stateproof: Uint8Array;
 
   /**
-   * Hash of SignedTxnInBlock for verifying proof.
+   * Creates a new `StateProof` object.
+   * @param message - Represents the message that the state proofs are attesting to.
+   * @param stateproof - The encoded StateProof for the message.
    */
-  public stibhash: Uint8Array;
+  constructor(message: StateProofMessage, stateproof: string | Uint8Array) {
+    super();
+    this.message = message;
+    this.stateproof =
+      typeof stateproof === 'string'
+        ? new Uint8Array(Buffer.from(stateproof, 'base64'))
+        : stateproof;
+
+    this.attribute_map = {
+      message: 'Message',
+      stateproof: 'StateProof',
+    };
+  }
+}
 
+/**
+ * Represents the message that the state proofs are attesting to.
+ */
+export class StateProofMessage extends BaseModel {
   /**
-   * Represents the depth of the tree that is being proven, i.e. the number of edges
-   * from a leaf to the root.
+   * The vector commitment root on all light block headers within a state proof
+   * interval.
    */
-  public treedepth: number | bigint;
+  public blockheaderscommitment: Uint8Array;
 
   /**
-   * The type of hash function used to create the proof, must be one of:
-   * * sha512_256
-   * * sha256
+   * The first round the message attests to.
    */
-  public hashtype?: string;
+  public firstattestedround: number | bigint;
 
   /**
-   * Creates a new `ProofResponse` object.
-   * @param idx - Index of the transaction in the block's payset.
-   * @param proof - Merkle proof of transaction membership.
-   * @param stibhash - Hash of SignedTxnInBlock for verifying proof.
-   * @param treedepth - Represents the depth of the tree that is being proven, i.e. the number of edges
-   * from a leaf to the root.
-   * @param hashtype - The type of hash function used to create the proof, must be one of:
-   * * sha512_256
-   * * sha256
+   * The last round the message attests to.
+   */
+  public lastattestedround: number | bigint;
+
+  /**
+   * An integer value representing the natural log of the proven weight with 16 bits
+   * of precision. This value would be used to verify the next state proof.
+   */
+  public lnprovenweight: number | bigint;
+
+  /**
+   * The vector commitment root of the top N accounts to sign the next StateProof.
+   */
+  public voterscommitment: Uint8Array;
+
+  /**
+   * Creates a new `StateProofMessage` object.
+   * @param blockheaderscommitment - The vector commitment root on all light block headers within a state proof
+   * interval.
+   * @param firstattestedround - The first round the message attests to.
+   * @param lastattestedround - The last round the message attests to.
+   * @param lnprovenweight - An integer value representing the natural log of the proven weight with 16 bits
+   * of precision. This value would be used to verify the next state proof.
+   * @param voterscommitment - The vector commitment root of the top N accounts to sign the next StateProof.
    */
   constructor({
-    idx,
-    proof,
-    stibhash,
-    treedepth,
-    hashtype,
+    blockheaderscommitment,
+    firstattestedround,
+    lastattestedround,
+    lnprovenweight,
+    voterscommitment,
   }: {
-    idx: number | bigint;
-    proof: string | Uint8Array;
-    stibhash: string | Uint8Array;
-    treedepth: number | bigint;
-    hashtype?: string;
+    blockheaderscommitment: string | Uint8Array;
+    firstattestedround: number | bigint;
+    lastattestedround: number | bigint;
+    lnprovenweight: number | bigint;
+    voterscommitment: string | Uint8Array;
   }) {
     super();
-    this.idx = idx;
-    this.proof =
-      typeof proof === 'string'
-        ? new Uint8Array(Buffer.from(proof, 'base64'))
-        : proof;
-    this.stibhash =
-      typeof stibhash === 'string'
-        ? new Uint8Array(Buffer.from(stibhash, 'base64'))
-        : stibhash;
-    this.treedepth = treedepth;
-    this.hashtype = hashtype;
+    this.blockheaderscommitment =
+      typeof blockheaderscommitment === 'string'
+        ? new Uint8Array(Buffer.from(blockheaderscommitment, 'base64'))
+        : blockheaderscommitment;
+    this.firstattestedround = firstattestedround;
+    this.lastattestedround = lastattestedround;
+    this.lnprovenweight = lnprovenweight;
+    this.voterscommitment =
+      typeof voterscommitment === 'string'
+        ? new Uint8Array(Buffer.from(voterscommitment, 'base64'))
+        : voterscommitment;
 
     this.attribute_map = {
-      idx: 'idx',
-      proof: 'proof',
-      stibhash: 'stibhash',
-      treedepth: 'treedepth',
-      hashtype: 'hashtype',
+      blockheaderscommitment: 'BlockHeadersCommitment',
+      firstattestedround: 'FirstAttestedRound',
+      lastattestedround: 'LastAttestedRound',
+      lnprovenweight: 'LnProvenWeight',
+      voterscommitment: 'VotersCommitment',
     };
   }
 }
@@ -2317,6 +2397,85 @@ export class TransactionParametersResponse extends BaseModel {
   }
 }
 
+/**
+ * Proof of transaction in a block.
+ */
+export class TransactionProofResponse extends BaseModel {
+  /**
+   * Index of the transaction in the block's payset.
+   */
+  public idx: number | bigint;
+
+  /**
+   * Proof of transaction membership.
+   */
+  public proof: Uint8Array;
+
+  /**
+   * Hash of SignedTxnInBlock for verifying proof.
+   */
+  public stibhash: Uint8Array;
+
+  /**
+   * Represents the depth of the tree that is being proven, i.e. the number of edges
+   * from a leaf to the root.
+   */
+  public treedepth: number | bigint;
+
+  /**
+   * The type of hash function used to create the proof, must be one of:
+   * * sha512_256
+   * * sha256
+   */
+  public hashtype?: string;
+
+  /**
+   * Creates a new `TransactionProofResponse` object.
+   * @param idx - Index of the transaction in the block's payset.
+   * @param proof - Proof of transaction membership.
+   * @param stibhash - Hash of SignedTxnInBlock for verifying proof.
+   * @param treedepth - Represents the depth of the tree that is being proven, i.e. the number of edges
+   * from a leaf to the root.
+   * @param hashtype - The type of hash function used to create the proof, must be one of:
+   * * sha512_256
+   * * sha256
+   */
+  constructor({
+    idx,
+    proof,
+    stibhash,
+    treedepth,
+    hashtype,
+  }: {
+    idx: number | bigint;
+    proof: string | Uint8Array;
+    stibhash: string | Uint8Array;
+    treedepth: number | bigint;
+    hashtype?: string;
+  }) {
+    super();
+    this.idx = idx;
+    this.proof =
+      typeof proof === 'string'
+        ? new Uint8Array(Buffer.from(proof, 'base64'))
+        : proof;
+    this.stibhash =
+      typeof stibhash === 'string'
+        ? new Uint8Array(Buffer.from(stibhash, 'base64'))
+        : stibhash;
+    this.treedepth = treedepth;
+    this.hashtype = hashtype;
+
+    this.attribute_map = {
+      idx: 'idx',
+      proof: 'proof',
+      stibhash: 'stibhash',
+      treedepth: 'treedepth',
+      hashtype: 'hashtype',
+    };
+  }
+}
+
 /**
  * algod version information.
  */
diff --git a/src/client/v2/algod/proof.ts b/src/client/v2/algod/proof.ts
deleted file mode 100644
index a8b78e2..0000000
--- a/src/client/v2/algod/proof.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import JSONRequest from '../jsonrequest';
-import HTTPClient from '../../client';
-import IntDecoding from '../../../types/intDecoding';
-
-export default class Proof extends JSONRequest {
-  constructor(
-    c: HTTPClient,
-    intDecoding: IntDecoding,
-    private round: number,
-    private txID: string
-  ) {
-    super(c, intDecoding);
-
-    this.round = round;
-    this.txID = txID;
-  }
-
-  path() {
-    return `/v2/blocks/${this.round}/transactions/${this.txID}/proof`;
-  }
-}
diff --git a/src/client/v2/algod/stateproof.ts b/src/client/v2/algod/stateproof.ts
new file mode 100644
index 0000000..98bab12
--- /dev/null
+++ b/src/client/v2/algod/stateproof.ts
@@ -0,0 +1,15 @@
+import JSONRequest from '../jsonrequest';
+import HTTPClient from '../../client';
+import IntDecoding from '../../../types/intDecoding';
+
+export default class StateProof extends JSONRequest {
+  constructor(c: HTTPClient, intDecoding: IntDecoding, private round: number) {
+    super(c, intDecoding);
+
+    this.round = round;
+  }
+
+  path() {
+    return `/v2/stateproofs/${this.round}`;
+  }
+}
diff --git a/src/client/v2/indexer/lookupAccountTransactions.ts b/src/client/v2/indexer/lookupAccountTransactions.ts
index 69f41cd..b2343f1 100644
--- a/src/client/v2/indexer/lookupAccountTransactions.ts
+++ b/src/client/v2/indexer/lookupAccountTransactions.ts
@@ -76,7 +76,7 @@ export default class LookupAccountTransactions extends JSONRequest {
    *        .do();
    * ```
    *
-   * @param type - one of `pay`, `keyreg`, `acfg`, `axfer`, `afrz`, `appl`
+   * @param type - one of `pay`, `keyreg`, `acfg`, `axfer`, `afrz`, `appl`, `stpf`
    * @category query
    */
   txType(type: string) {
diff --git a/src/client/v2/indexer/searchForTransactions.ts b/src/client/v2/indexer/searchForTransactions.ts
index 5afa84c..7583059 100644
--- a/src/client/v2/indexer/searchForTransactions.ts
+++ b/src/client/v2/indexer/searchForTransactions.ts
@@ -52,7 +52,7 @@ export default class SearchForTransactions extends JSONRequest {
    *        .do();
    * ```
    *
-   * @param type - one of `pay`, `keyreg`, `acfg`, `axfer`, `afrz`, `appl`
+   * @param type - one of `pay`, `keyreg`, `acfg`, `axfer`, `afrz`, `appl`, `stpf`
    * @category query
    */
   txType(type: string) {
diff --git a/src/logic/logic.ts b/src/logic/logic.ts
index 9f66b23..f425329 100644
--- a/src/logic/logic.ts
+++ b/src/logic/logic.ts
@@ -3,10 +3,12 @@
  * Utilities for working with program bytes.
  */
 
+/** @deprecated langspec.json is deprecated aross all SDKs */
 import langspec from './langspec.json';
 
 /**
  * Langspec Op Structure
+ * @deprecated for langspec.json is deprecated aross all SDKs
  */
 interface OpStructure {
   Opcode: number;
@@ -23,13 +25,17 @@ interface OpStructure {
   Groups: string[];
 }
 
+/** @deprecated for langspec.json is deprecated aross all SDKs */
 let opcodes: {
   [key: number]: OpStructure;
 };
 
+/** @deprecated for langspec.json is deprecated aross all SDKs */
 const maxCost = 20000;
+/** @deprecated for langspec.json is deprecated aross all SDKs */
 const maxLength = 1000;
 
+/** @deprecated for langspec.json is deprecated aross all SDKs */
 export function parseUvarint(
   array: Uint8Array
 ): [numberFound: number, size: number] {
@@ -49,6 +55,7 @@ export function parseUvarint(
   return [0, 0];
 }
 
+/** @deprecated for langspec.json is deprecated aross all SDKs */
 function readIntConstBlock(
   program: Uint8Array,
   pc: number
@@ -79,6 +86,7 @@ function readIntConstBlock(
   return [size, ints];
 }
 
+/** @deprecated for langspec.json is deprecated aross all SDKs */
 function readByteConstBlock(
   program: Uint8Array,
   pc: number
@@ -116,6 +124,7 @@ function readByteConstBlock(
   return [size, byteArrays];
 }
 
+/** @deprecated for langspec.json is deprecated aross all SDKs */
 function readPushIntOp(
   program: Uint8Array,
   pc: number
@@ -129,6 +138,7 @@ function readPushIntOp(
   return [size, numberFound];
 }
 
+/** @deprecated for langspec.json is deprecated aross all SDKs */
 function readPushByteOp(
   program: Uint8Array,
   pc: number
@@ -151,6 +161,12 @@ function readPushByteOp(
 
 /** readProgram validates program for length and running cost,
  * and additionally provides the found int variables and byte blocks
+ *
+ * @deprecated Validation relies on metadata (`langspec.json`) that
+ * does not accurately represent opcode behavior across program versions.
+ * The behavior of `readProgram` relies on `langspec.json`.
+ * Thus, this method is being deprecated.
+ *
  * @param program - Program to check
  * @param args - Program arguments as array of Uint8Array arrays
  * @throws
@@ -254,6 +270,12 @@ export function readProgram(
 
 /**
  * checkProgram validates program for length and running cost
+ *
+ * @deprecated Validation relies on metadata (`langspec.json`) that
+ * does not accurately represent opcode behavior across program versions.
+ * The behavior of `checkProgram` relies on `langspec.json`.
+ * Thus, this method is being deprecated.
+ *
  * @param program - Program to check
  * @param args - Program arguments as array of Uint8Array arrays
  * @throws
@@ -264,25 +286,31 @@ export function checkProgram(program: Uint8Array, args?: Uint8Array[]) {
   return success;
 }
 
+/** @deprecated for langspec.json is deprecated aross all SDKs */
 export function checkIntConstBlock(program: Uint8Array, pc: number) {
   const [size] = readIntConstBlock(program, pc);
   return size;
 }
 
+/** @deprecated for langspec.json is deprecated aross all SDKs */
 export function checkByteConstBlock(program: Uint8Array, pc: number) {
   const [size] = readByteConstBlock(program, pc);
   return size;
 }
 
+/** @deprecated for langspec.json is deprecated aross all SDKs */
 export function checkPushIntOp(program: Uint8Array, pc: number) {
   const [size] = readPushIntOp(program, pc);
   return size;
 }
 
+/** @deprecated for langspec.json is deprecated aross all SDKs */
 export function checkPushByteOp(program: Uint8Array, pc: number) {
   const [size] = readPushByteOp(program, pc);
   return size;
 }
 
+/** @deprecated for langspec.json is deprecated aross all SDKs */
 export const langspecEvalMaxVersion = langspec.EvalMaxVersion;
+/** @deprecated for langspec.json is deprecated aross all SDKs */
 export const langspecLogicSigVersion = langspec.LogicSigVersion;
diff --git a/src/logicsig.ts b/src/logicsig.ts
index 4904d3c..74c0e0b 100644
--- a/src/logicsig.ts
+++ b/src/logicsig.ts
@@ -1,10 +1,10 @@
 import * as nacl from './nacl/naclWrappers';
 import * as address from './encoding/address';
 import * as encoding from './encoding/encoding';
-import * as logic from './logic/logic';
 import { verifyMultisig } from './multisig';
 import * as utils from './utils/utils';
 import * as txnBuilder from './transaction';
+import { isValidAddress } from './encoding/address';
 import {
   EncodedLogicSig,
   EncodedLogicSigAccount,
@@ -20,6 +20,38 @@ interface LogicSigStorageStructure {
   msig?: EncodedMultisig;
 }
 
+/** sanityCheckProgram performs heuristic program validation:
+ * check if passed in bytes are Algorand address or is B64 encoded, rather than Teal bytes
+ *
+ * @param program - Program bytes to check
+ */
+export function sanityCheckProgram(program: Uint8Array) {
+  if (!program || program.length === 0) throw new Error('empty program');
+
+  const lineBreakOrd = '\n'.charCodeAt(0);
+  const blankSpaceOrd = ' '.charCodeAt(0);
+  const tildeOrd = '~'.charCodeAt(0);
+
+  const isPrintable = (x: number) => blankSpaceOrd <= x && x <= tildeOrd;
+  const isAsciiPrintable = program.every(
+    (x: number) => x === lineBreakOrd || isPrintable(x)
+  );
+
+  if (isAsciiPrintable) {
+    const programStr = Buffer.from(program).toString();
+
+    if (isValidAddress(programStr))
+      throw new Error('requesting program bytes, get Algorand address');
+
+    if (Buffer.from(programStr, 'base64').toString('base64') === programStr)
+      throw new Error('program should not be b64 encoded');
+
+    throw new Error(
+      'program bytes are all ASCII printable characters, not looking like Teal byte code'
+    );
+  }
+}
+
 /**
  LogicSig implementation
  */
@@ -49,9 +81,7 @@ export class LogicSig implements LogicSigStorageStructure {
     if (programArgs != null)
       args = programArgs.map((arg) => new Uint8Array(arg));
 
-    if (!logic.checkProgram(program, args)) {
-      throw new Error('Invalid program');
-    }
+    sanityCheckProgram(program);
 
     this.logic = program;
     this.args = args;
@@ -93,7 +123,7 @@ export class LogicSig implements LogicSigStorageStructure {
     }
 
     try {
-      logic.checkProgram(this.logic, this.args);
+      sanityCheckProgram(this.logic);
     } catch (e) {
       return false;
     }
diff --git a/src/transaction.ts b/src/transaction.ts
index 4f737a0..5d4f9d6 100644
--- a/src/transaction.ts
+++ b/src/transaction.ts
@@ -110,6 +110,9 @@ interface TransactionStorageStructure
   nonParticipation?: boolean;
   group?: Buffer;
   extraPages?: number;
+  stateProofType?: number | bigint;
+  stateProof?: Uint8Array;
+  stateProofMessage?: Uint8Array;
 }
 
 function getKeyregKey(
@@ -198,6 +201,9 @@ export class Transaction implements TransactionStorageStructure {
   nonParticipation?: boolean;
   group?: Buffer;
   extraPages?: number;
+  stateProofType?: number | bigint;
+  stateProof?: Uint8Array;
+  stateProofMessage?: Uint8Array;
 
   constructor({ ...transaction }: AnyTransaction) {
     // Populate defaults
@@ -528,6 +534,27 @@ export class Transaction implements TransactionStorageStructure {
 
     // say we are aware of groups
     this.group = undefined;
+
+    // stpf fields
+    if (
+      txn.stateProofType !== undefined &&
+      (!Number.isSafeInteger(txn.stateProofType) || txn.stateProofType < 0)
+    )
+      throw Error(
+        'State Proof type must be a positive number and smaller than 2^53-1'
+      );
+    if (txn.stateProofMessage !== undefined) {
+      if (txn.stateProofMessage.constructor !== Uint8Array)
+        throw Error('stateProofMessage must be a Uint8Array.');
+    } else {
+      txn.stateProofMessage = new Uint8Array(0);
+    }
+    if (txn.stateProof !== undefined) {
+      if (txn.stateProof.constructor !== Uint8Array)
+        throw Error('stateProof must be a Uint8Array.');
+    } else {
+      txn.stateProof = new Uint8Array(0);
+    }
   }
 
   // eslint-disable-next-line camelcase
@@ -826,6 +853,42 @@ export class Transaction implements TransactionStorageStructure {
       if (txn.grp === undefined) delete txn.grp;
       return txn;
     }
+    if (this.type === 'stpf') {
+      // state proof txn
+      const txn: EncodedTransaction = {
+        fee: this.fee,
+        fv: this.firstRound,
+        lv: this.lastRound,
+        note: Buffer.from(this.note),
+        snd: Buffer.from(this.from.publicKey),
+        type: this.type,
+        gen: this.genesisID,
+        gh: this.genesisHash,
+        lx: Buffer.from(this.lease),
+        sptype: this.stateProofType,
+        spmsg: Buffer.from(this.stateProofMessage),
+        sp: Buffer.from(this.stateProof),
+      };
+      // allowed zero values
+      if (!txn.sptype) delete txn.sptype;
+      if (!txn.note.length) delete txn.note;
+      if (!txn.lx.length) delete txn.lx;
+      if (!txn.amt) delete txn.amt;
+      if (!txn.fee) delete txn.fee;
+      if (!txn.fv) delete txn.fv;
+      if (!txn.gen) delete txn.gen;
+      if (!txn.apid) delete txn.apid;
+      if (!txn.apaa || !txn.apaa.length) delete txn.apaa;
+      if (!txn.apap) delete txn.apap;
+      if (!txn.apsu) delete txn.apsu;
+      if (!txn.apan) delete txn.apan;
+      if (!txn.apfa || !txn.apfa.length) delete txn.apfa;
+      if (!txn.apas || !txn.apas.length) delete txn.apas;
+      if (!txn.apat || !txn.apat.length) delete txn.apat;
+      if (!txn.apep) delete txn.apep;
+      if (txn.grp === undefined) delete txn.grp;
+      return txn;
+    }
 
     return undefined;
   }
@@ -993,6 +1056,16 @@ export class Transaction implements TransactionStorageStructure {
       if (txnForEnc.apas !== undefined) {
         txn.appForeignAssets = txnForEnc.apas;
       }
+    } else if (txnForEnc.type === 'stpf') {
+      if (txnForEnc.sptype !== undefined) {
+        txn.stateProofType = txnForEnc.sptype;
+      }
+      if (txnForEnc.sp !== undefined) {
+        txn.stateProof = txnForEnc.sp;
+      }
+      if (txnForEnc.spmsg !== undefined) {
+        txn.stateProofMessage = txnForEnc.spmsg;
+      }
     }
     return txn;
   }
diff --git a/src/types/blockHeader.ts b/src/types/blockHeader.ts
index 7fa1c2e..96f2af2 100644
--- a/src/types/blockHeader.ts
+++ b/src/types/blockHeader.ts
@@ -65,7 +65,17 @@ export default interface BlockHeader {
   ts: number;
 
   /**
-   * Transaction root
+   * Transaction root SHA512_256
    */
   txn: string;
+
+  /**
+   * Transaction root SHA256
+   */
+  txn256: string;
+
+  /**
+   * StateProofTracking map of type to tracking data
+   */
+  spt: Map<number, Uint8Array>;
 }
diff --git a/src/types/transactions/base.ts b/src/types/transactions/base.ts
index 739c88e..8c6db32 100644
--- a/src/types/transactions/base.ts
+++ b/src/types/transactions/base.ts
@@ -33,6 +33,10 @@ export enum TransactionType {
    * Application transaction
    */
   appl = 'appl',
+  /**
+   * State proof transaction
+   */
+  stpf = 'stpf',
 }
 
 export function isTransactionType(s: string): s is TransactionType {
@@ -42,7 +46,8 @@ export function isTransactionType(s: string): s is TransactionType {
     s === TransactionType.acfg ||
     s === TransactionType.axfer ||
     s === TransactionType.afrz ||
-    s === TransactionType.appl
+    s === TransactionType.appl ||
+    s === TransactionType.stpf
   );
 }
 
@@ -385,4 +390,19 @@ export interface TransactionParams {
    * Int representing extra pages of memory to rent during an application create transaction.
    */
   extraPages?: number;
+
+  /**
+   * Uint64 identifying a particular configuration of state proofs.
+   */
+  stateProofType?: number | bigint;
+
+  /**
+   * Byte array containing the state proof.
+   */
+  stateProof?: Uint8Array;
+
+  /**
+   * Byte array containing the state proof message.
+   */
+  stateProofMessage?: Uint8Array;
 }
diff --git a/src/types/transactions/encoded.ts b/src/types/transactions/encoded.ts
index bc5b9e9..1fe8fff 100644
--- a/src/types/transactions/encoded.ts
+++ b/src/types/transactions/encoded.ts
@@ -296,6 +296,21 @@ export interface EncodedTransaction {
    * extraPages
    */
   apep?: number;
+
+  /**
+   * stateProofType
+   */
+  sptype?: number | bigint;
+
+  /**
+   * stateProof
+   */
+  sp?: Buffer;
+
+  /**
+   * stateProofMessage
+   */
+  spmsg?: Buffer;
 }
 
 export interface EncodedSubsig {
diff --git a/src/types/transactions/index.ts b/src/types/transactions/index.ts
index 3202259..d73e46f 100644
--- a/src/types/transactions/index.ts
+++ b/src/types/transactions/index.ts
@@ -16,6 +16,7 @@ import {
   ApplicationClearStateTransaction as AppClearStateTxn,
   ApplicationNoOpTransaction as AppNoOpTxn,
 } from './application';
+import StateProofTxn from './stateproof';
 
 // Utilities
 export { TransactionParams, TransactionType, SuggestedParams } from './base';
@@ -44,6 +45,7 @@ export {
   ApplicationClearStateTransaction as AppClearStateTxn,
   ApplicationNoOpTransaction as AppNoOpTxn,
 } from './application';
+export { default as StateProofTxn } from './stateproof';
 
 // All possible transaction types
 type AnyTransaction =
@@ -60,5 +62,6 @@ type AnyTransaction =
   | AppOptInTxn
   | AppCloseOutTxn
   | AppClearStateTxn
-  | AppNoOpTxn;
+  | AppNoOpTxn
+  | StateProofTxn;
 export default AnyTransaction;
diff --git a/src/types/transactions/stateproof.ts b/src/types/transactions/stateproof.ts
new file mode 100644
index 0000000..7f2e116
--- /dev/null
+++ b/src/types/transactions/stateproof.ts
@@ -0,0 +1,17 @@
+import { TransactionType, TransactionParams } from './base';
+import { ConstructTransaction } from './builder';
+
+type SpecificParameters = Pick<
+  TransactionParams,
+  'stateProofType' | 'stateProof' | 'stateProofMessage'
+>;
+
+interface Overwrites {
+  type?: TransactionType.stpf;
+}
+
+type StateProofTransaction = ConstructTransaction<
+  SpecificParameters,
+  Overwrites
+>;
+export default StateProofTransaction;


Test patch:
diff --git a/.test-env b/.test-env
new file mode 100644
index 0000000..3b17bb1
--- /dev/null
+++ b/.test-env
@@ -0,0 +1,14 @@
+# Configs for testing repo download:
+SDK_TESTING_URL="https://github.com/algorand/algorand-sdk-testing"
+SDK_TESTING_BRANCH="master"
+SDK_TESTING_HARNESS="test-harness"
+
+VERBOSE_HARNESS=0
+
+# WARNING: If set to 1, new features will be LOST when downloading the test harness.
+# REGARDLESS: modified features are ALWAYS overwritten.
+REMOVE_LOCAL_FEATURES=0
+
+# WARNING: Be careful when turning on the next variable.
+# In that case you'll need to provide all variables expected by `algorand-sdk-testing`'s `.env`
+OVERWRITE_TESTING_ENVIRONMENT=0
diff --git a/test-harness.sh b/test-harness.sh
new file mode 100755
index 0000000..ad68eaf
--- /dev/null
+++ b/test-harness.sh
@@ -0,0 +1,69 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+START=$(date "+%s")
+
+THIS=$(basename "$0")
+ENV_FILE=".test-env"
+TEST_DIR="tests/cucumber"
+
+set -a
+source "$ENV_FILE"
+set +a
+
+rootdir=$(dirname "$0")
+pushd "$rootdir"
+
+echo "$THIS: VERBOSE_HARNESS=$VERBOSE_HARNESS"
+
+## Reset test harness
+if [ -d "$SDK_TESTING_HARNESS" ]; then
+  pushd "$SDK_TESTING_HARNESS"
+  ./scripts/down.sh
+  popd
+  rm -rf "$SDK_TESTING_HARNESS"
+else
+  echo "$THIS: directory $SDK_TESTING_HARNESS does not exist - NOOP"
+fi
+
+git clone --depth 1 --single-branch --branch "$SDK_TESTING_BRANCH" "$SDK_TESTING_URL" "$SDK_TESTING_HARNESS"
+
+
+echo "$THIS: OVERWRITE_TESTING_ENVIRONMENT=$OVERWRITE_TESTING_ENVIRONMENT"
+if [[ $OVERWRITE_TESTING_ENVIRONMENT == 1 ]]; then
+  echo "$THIS: OVERWRITE downloaded $SDK_TESTING_HARNESS/.env with $ENV_FILE:"
+  cp "$ENV_FILE" "$SDK_TESTING_HARNESS"/.env
+fi
+
+echo "$THIS: REMOVE_LOCAL_FEATURES=$REMOVE_LOCAL_FEATURES"
+## Copy feature files into the project resources
+if [[ $REMOVE_LOCAL_FEATURES == 1 ]]; then
+  echo "$THIS: OVERWRITE wipes clean $TEST_DIR/features"
+  if [[ $VERBOSE_HARNESS == 1 ]]; then
+    ( tree $TEST_DIR/features && echo "$THIS: see the previous for files deleted" ) || true
+  fi
+  rm -rf $TEST_DIR/features
+fi
+mkdir -p $TEST_DIR/features
+cp -r "$SDK_TESTING_HARNESS"/features/* $TEST_DIR/features
+if [[ $VERBOSE_HARNESS == 1 ]]; then
+  ( tree $TEST_DIR/features && echo "$THIS: see the previous for files copied over" ) || true
+fi
+echo "$THIS: seconds it took to get to end of cloning and copying: $(($(date "+%s") - START))s"
+
+## Start test harness environment
+pushd "$SDK_TESTING_HARNESS"
+
+[[ "$VERBOSE_HARNESS" = 1 ]] && V_FLAG="-v" || V_FLAG=""
+echo "$THIS: standing up harnness with command [./up.sh $V_FLAG]"
+./scripts/up.sh "$V_FLAG"
+
+popd
+echo "$THIS: seconds it took to finish testing sdk's up.sh: $(($(date "+%s") - START))s"
+echo ""
+echo "--------------------------------------------------------------------------------"
+echo "|"
+echo "|    To run sandbox commands, cd into $SDK_TESTING_HARNESS/.sandbox             "
+echo "|"
+echo "--------------------------------------------------------------------------------"
diff --git a/tests/5.Transaction.js b/tests/5.Transaction.js
index 710d16e..eeaadc5 100644
--- a/tests/5.Transaction.js
+++ b/tests/5.Transaction.js
@@ -341,6 +341,40 @@ describe('Sign', () => {
       assert.deepStrictEqual(reencRep, encRep);
     });
 
+    it('should correctly serialize and deserialize a state proof transaction from msgpack representation', () => {
+      const o = {
+        from: 'XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU',
+        fee: 10,
+        firstRound: 51,
+        lastRound: 61,
+        note: new Uint8Array([123, 12, 200]),
+        genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=',
+        voteKey: '5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKE=',
+        selectionKey: 'oImqaSLjuZj63/bNSAjd+eAh5JROOJ6j1cY4eGaJGX4=',
+        voteFirst: 123,
+        voteLast: 456,
+        voteKeyDilution: 1234,
+        genesisID: '',
+        type: 'stpf',
+        stateProofType: 0,
+        stateProof: new Uint8Array([1, 1, 1, 1]),
+        stateProofMessage: new Uint8Array([0, 0, 0, 0]),
+      };
+      const expectedTxn = new algosdk.Transaction(o);
+      console.log(
+        `${expectedTxn.stateProofType} ${expectedTxn.stateProofMessage} ${expectedTxn.stateProof} ${expectedTxn.type}`
+      );
+      const encRep = expectedTxn.get_obj_for_encoding();
+      console.log(
+        `${encRep.sptype} ${encRep.spmsg} ${encRep.sp} ${encRep.type}`
+      );
+      const encTxn = algosdk.encodeObj(encRep);
+      const decEncRep = algosdk.decodeObj(encTxn);
+      const decTxn = algosdk.Transaction.from_obj_for_encoding(decEncRep);
+      const reencRep = decTxn.get_obj_for_encoding();
+      assert.deepStrictEqual(reencRep, encRep);
+    });
+
     it('should correctly serialize and deserialize a key registration transaction from msgpack representation', () => {
       const o = {
         from: 'XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU',
diff --git a/tests/7.AlgoSDK.js b/tests/7.AlgoSDK.js
index e831786..6cc0c67 100644
--- a/tests/7.AlgoSDK.js
+++ b/tests/7.AlgoSDK.js
@@ -830,11 +830,6 @@ describe('Algosdk (AKA end to end)', () => {
       assert.equal(lsig.logic, program);
       assert.deepEqual(lsig.args, args);
     });
-    it('should throw on invalid program', () => {
-      const program = Uint8Array.from([1, 32, 1, 1, 34]);
-      program[0] = 128;
-      assert.throws(() => algosdk.makeLogicSig(program));
-    });
   });
   describe('Single logic sig', () => {
     it('should work on valid program', () => {
diff --git a/tests/8.LogicSig.ts b/tests/8.LogicSig.ts
index e7e7a5f..08be74a 100644
--- a/tests/8.LogicSig.ts
+++ b/tests/8.LogicSig.ts
@@ -65,11 +65,6 @@ describe('LogicSig', () => {
       const verified = lsig.verify(pk);
       assert.strictEqual(verified, false);
     });
-    it('should fail on invalid program', () => {
-      const program = Uint8Array.from([1, 32, 1, 1, 34]);
-      program[0] = 128;
-      assert.throws(() => algosdk.makeLogicSig(program));
-    });
   });
 
   describe('address', () => {
@@ -129,11 +124,6 @@ describe('LogicSigAccount', () => {
       const decoded = algosdk.LogicSigAccount.fromByte(encoded);
       assert.deepStrictEqual(decoded, lsigAccount);
     });
-    it('should fail on invalid program', () => {
-      const program = Uint8Array.from([1, 32, 1, 1, 34]);
-      program[0] = 128;
-      assert.throws(() => new algosdk.LogicSigAccount(program));
-    });
   });
 
   describe('sign', () => {
diff --git a/tests/cucumber/docker/run_docker.sh b/tests/cucumber/docker/run_docker.sh
deleted file mode 100755
index 72e6bb9..0000000
--- a/tests/cucumber/docker/run_docker.sh
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-# cleanup last test run
-rm -rf test-harness
-rm -rf tests/cucumber/features
-
-# clone test harness
-git clone --single-branch --branch master https://github.com/algorand/algorand-sdk-testing.git test-harness
-
-# move feature files and example files to destination
-mv test-harness/features tests/cucumber/features
-
-if [ $TEST_BROWSER == "chrome" ]; then
-  # use latest version of chromedriver for compatability with the current Chrome version
-  npm install chromedriver@latest
-  # print the version installed
-  npm ls chromedriver
-fi
-
-# build test environment
-docker build -t js-sdk-testing -f tests/cucumber/docker/Dockerfile "$(pwd)" --build-arg TEST_BROWSER --build-arg CI=true
-
-# Start test harness environment
-./test-harness/scripts/up.sh
-
-docker run -it \
-     --network host \
-     js-sdk-testing:latest
diff --git a/tests/cucumber/integration.tags b/tests/cucumber/integration.tags
new file mode 100644
index 0000000..03cb481
--- /dev/null
+++ b/tests/cucumber/integration.tags
@@ -0,0 +1,14 @@
+@abi
+@algod
+@applications
+@applications.verified
+@assets
+@auction
+@c2c
+@compile
+@compile.sourcemap
+@dryrun
+@kmd
+@rekey_v1
+@send
+@send.keyregtxn
\ No newline at end of file
diff --git a/tests/cucumber/steps/index.js b/tests/cucumber/steps/index.js
index 0c9c51f..0117dc8 100644
--- a/tests/cucumber/steps/index.js
+++ b/tests/cucumber/steps/index.js
@@ -347,6 +347,16 @@ if (browser) {
             rpcArgs = args.slice(0, rpcArgs.length - 1);
           }
 
+          for (const arg of rpcArgs) {
+            if (arg instanceof Uint8Array) {
+              // cannot send Uint8Array or Buffer objects because the arguments will get JSON
+              // encoded when transmitted to the browser
+              throw new Error(
+                `Attempted to send binary data to the browser when invoking test '${type} ${name}'`
+              );
+            }
+          }
+
           const { error } = await driver.executeAsyncScript(
             // variables are `scoped` because they exist in the upper scope
             async (scopedType, scopedName, ...rest) => {
@@ -391,34 +401,54 @@ for (const name of Object.keys(steps.given)) {
   const fn = steps.given[name];
   if (name === 'mock http responses in {string} loaded from {string}') {
     Given(name, function (fileName, jsonDirectory) {
-      const body1 = setupMockServerForResponses(
+      let body1 = setupMockServerForResponses(
         fileName,
         jsonDirectory,
         algodMockServerResponder
       );
-      const body2 = setupMockServerForResponses(
+      let body2 = setupMockServerForResponses(
         fileName,
         jsonDirectory,
         indexerMockServerResponder
       );
-      return fn.call(this, body2 || body1);
+      let format = 'json';
+      if (fileName.endsWith('base64')) {
+        format = 'msgp';
+      }
+      if (Buffer.isBuffer(body1)) {
+        body1 = body1.toString('base64');
+      }
+      if (Buffer.isBuffer(body2)) {
+        body2 = body2.toString('base64');
+      }
+      return fn.call(this, body2 || body1, format);
     });
   } else if (
     name ===
     'mock http responses in {string} loaded from {string} with status {int}.'
   ) {
     Given(name, function (fileName, jsonDirectory, status) {
-      const body1 = setupMockServerForResponses(
+      let body1 = setupMockServerForResponses(
         fileName,
         jsonDirectory,
         algodMockServerResponder
       );
-      const body2 = setupMockServerForResponses(
+      let body2 = setupMockServerForResponses(
         fileName,
         jsonDirectory,
         indexerMockServerResponder
       );
-      return fn.call(this, body2 || body1, status);
+      let format = 'json';
+      if (fileName.endsWith('base64')) {
+        format = 'msgp';
+      }
+      if (Buffer.isBuffer(body1)) {
+        body1 = body1.toString('base64');
+      }
+      if (Buffer.isBuffer(body2)) {
+        body2 = body2.toString('base64');
+      }
+      return fn.call(this, body2 || body1, status, format);
     });
   } else if (name === 'mock server recording request paths') {
     Given(name, function () {
diff --git a/tests/cucumber/steps/steps.js b/tests/cucumber/steps/steps.js
index bfd192f..22f8ddc 100644
--- a/tests/cucumber/steps/steps.js
+++ b/tests/cucumber/steps/steps.js
@@ -76,14 +76,6 @@ function parseJSON(json) {
 
 // END OBJECT CREATION FUNCTIONS
 
-function formatIncludeAll(includeAll) {
-  if (!['true', 'false'].includes(includeAll)) {
-    throw new Error(`Unknown value for includeAll: ${includeAll}`);
-  }
-
-  return includeAll === 'true';
-}
-
 const steps = {
   given: {},
   when: {},
@@ -630,26 +622,6 @@ module.exports = function getSteps(options) {
     );
   });
 
-  Then('I get transactions by address only', async function () {
-    const transactions = await this.acl.transactionByAddress(this.accounts[0]);
-    assert.deepStrictEqual(
-      true,
-      Object.entries(transactions).length === 0 ||
-        'transactions' in transactions
-    );
-  });
-
-  Then('I get transactions by address and date', async function () {
-    const transactions = await this.acl.transactionByAddressAndDate(
-      this.accounts[0]
-    );
-    assert.deepStrictEqual(
-      true,
-      Object.entries(transactions).length === 0 ||
-        'transactions' in transactions
-    );
-  });
-
   Then('I get pending transactions', async function () {
     const transactions = await this.acl.pendingTransactions(10);
     assert.deepStrictEqual(
@@ -881,19 +853,16 @@ module.exports = function getSteps(options) {
   });
 
   Then('the transaction should go through', async function () {
-    let info = await this.acl.pendingTransactionInformation(this.txid);
-    assert.deepStrictEqual(true, 'type' in info);
-    // let localParams = await this.acl.getTransactionParams();
-    // this.lastRound = localParams.lastRound;
     await waitForAlgodInDevMode();
-    info = await this.acl.transactionById(this.txid);
+    const info = await this.acl.pendingTransactionInformation(this.txid);
     assert.deepStrictEqual(true, 'type' in info);
-  });
 
-  Then('I can get the transaction by ID', async function () {
-    await waitForAlgodInDevMode();
-    const info = await this.acl.transactionById(this.txid);
-    assert.deepStrictEqual(true, 'type' in info);
+    // TODO: this needs to be modified/removed when v1 is no longer supported
+    // let localParams = await this.acl.getTransactionParams();
+    // this.lastRound = localParams.lastRound;
+    // await waitForAlgodInDevMode();
+    // info = await this.acl.transactionById(this.txid);
+    // assert.deepStrictEqual(true, 'type' in info);
   });
 
   Then('the transaction should not go through', function () {
@@ -1122,13 +1091,6 @@ module.exports = function getSteps(options) {
     }
   );
 
-  When(
-    'I get recent transactions, limited by {int} transactions',
-    function (int) {
-      this.acl.transactionByAddress(this.accounts[0], parseInt(int));
-    }
-  );
-
   /// /////////////////////////////////
   // begin asset tests
   /// /////////////////////////////////
@@ -1629,13 +1591,20 @@ module.exports = function getSteps(options) {
   } = options;
 
   let expectedMockResponse;
+  let responseFormat;
 
   Given(
     'mock http responses in {string} loaded from {string}',
-    function (expectedBody) {
+    function (expectedBody, format) {
       if (expectedBody !== null) {
         expectedMockResponse = expectedBody;
+        if (format === 'msgp') {
+          expectedMockResponse = new Uint8Array(
+            Buffer.from(expectedMockResponse, 'base64')
+          );
+        }
       }
+      responseFormat = format;
       this.v2Client = new algosdk.Algodv2(
         '',
         `http://${mockAlgodResponderHost}`,
@@ -1653,10 +1622,16 @@ module.exports = function getSteps(options) {
 
   Given(
     'mock http responses in {string} loaded from {string} with status {int}.',
-    function (expectedBody, status) {
+    function (expectedBody, status, format) {
       if (expectedBody !== null) {
         expectedMockResponse = expectedBody;
+        if (format === 'msgp') {
+          expectedMockResponse = new Uint8Array(
+            Buffer.from(expectedMockResponse, 'base64')
+          );
+        }
       }
+      responseFormat = format;
       this.v2Client = new algosdk.Algodv2(
         '',
         `http://${mockAlgodResponderHost}`,
@@ -1680,7 +1655,11 @@ module.exports = function getSteps(options) {
       try {
         if (client === 'algod') {
           // endpoints are ignored by mock server, see setupMockServerForResponses
-          this.actualMockResponse = await this.v2Client.status().do();
+          if (responseFormat === 'msgp') {
+            this.actualMockResponse = await this.v2Client.block(0).do();
+          } else {
+            this.actualMockResponse = await this.v2Client.status().do();
+          }
         } else if (client === 'indexer') {
           // endpoints are ignored by mock server, see setupMockServerForResponses
           this.actualMockResponse = await this.indexerClient
@@ -1706,10 +1685,22 @@ module.exports = function getSteps(options) {
 
   Then('the parsed response should equal the mock response.', function () {
     if (this.expectedMockResponseCode === 200) {
-      assert.strictEqual(
-        JSON.stringify(JSON.parse(expectedMockResponse)),
-        JSON.stringify(this.actualMockResponse)
-      );
+      // assert.deepStrictEqual considers a Buffer and Uint8Array with the same contents as unequal.
+      // These types are fairly interchangable in different parts of the SDK, so we need to normalize
+      // them before comparing, which is why we chain encoding/decoding below.
+      if (responseFormat === 'json') {
+        assert.strictEqual(
+          JSON.stringify(JSON.parse(expectedMockResponse)),
+          JSON.stringify(this.actualMockResponse)
+        );
+      } else {
+        assert.deepStrictEqual(
+          algosdk.decodeObj(
+            new Uint8Array(algosdk.encodeObj(this.actualMockResponse))
+          ),
+          algosdk.decodeObj(expectedMockResponse)
+        );
+      }
     }
   });
 
@@ -1762,13 +1753,6 @@ module.exports = function getSteps(options) {
     }
   );
 
-  When(
-    'we make a Pending Transaction Information against txid {string} with max {int}',
-    function (txid, max) {
-      this.v2Client.pendingTransactionInformation(txid).max(max).do();
-    }
-  );
-
   When(
     'we make a Pending Transaction Information against txid {string} with format {string}',
     async function (txid, format) {
@@ -2044,19 +2028,6 @@ module.exports = function getSteps(options) {
     }
   );
 
-  When(
-    'we make a Lookup Asset Balances call against asset index {int} with limit {int} nextToken {string} currencyGreaterThan {int} currencyLessThan {int}',
-    async function (index, limit, nextToken, currencyGreater, currencyLesser) {
-      await this.indexerClient
-        .lookupAssetBalances(index)
-        .limit(limit)
-        .currencyGreaterThan(currencyGreater)
-        .currencyLessThan(currencyLesser)
-        .nextToken(nextToken)
-        .do();
-    }
-  );
-
   When(
     'we make a Lookup Asset Balances call against asset index {int} with limit {int} afterAddress {string} currencyGreaterThan {int} currencyLessThan {int}',
     async function (
@@ -2075,51 +2046,6 @@ module.exports = function getSteps(options) {
     }
   );
 
-  When(
-    'we make a Lookup Asset Transactions call against asset index {int} with NotePrefix {string} TxType {string} SigType {string} txid {string} round {int} minRound {int} maxRound {int} limit {int} beforeTime {int} afterTime {int} currencyGreaterThan {int} currencyLessThan {int} address {string} addressRole {string} ExcluseCloseTo {string}',
-    async function (
-      assetIndex,
-      notePrefix,
-      txType,
-      sigType,
-      txid,
-      round,
-      minRound,
-      maxRound,
-      limit,
-      beforeTime,
-      afterTime,
-      currencyGreater,
-      currencyLesser,
-      address,
-      addressRole,
-      excludeCloseToAsString
-    ) {
-      let excludeCloseTo = false;
-      if (excludeCloseToAsString === 'true') {
-        excludeCloseTo = true;
-      }
-      await this.indexerClient
-        .lookupAssetTransactions(assetIndex)
-        .notePrefix(notePrefix)
-        .txType(txType)
-        .sigType(sigType)
-        .txid(txid)
-        .round(round)
-        .minRound(minRound)
-        .maxRound(maxRound)
-        .limit(limit)
-        .beforeTime(beforeTime)
-        .afterTime(afterTime)
-        .currencyGreaterThan(currencyGreater)
-        .currencyLessThan(currencyLesser)
-        .address(address)
-        .addressRole(addressRole)
-        .excludeCloseTo(excludeCloseTo)
-        .do();
-    }
-  );
-
   When(
     'we make a Lookup Asset Transactions call against asset index {int} with NotePrefix {string} TxType {string} SigType {string} txid {string} round {int} minRound {int} maxRound {int} limit {int} beforeTime {string} afterTime {string} currencyGreaterThan {int} currencyLessThan {int} address {string} addressRole {string} ExcluseCloseTo {string}',
     async function (
@@ -2324,13 +2250,6 @@ module.exports = function getSteps(options) {
     }
   );
 
-  When(
-    'we make a LookupApplications call with {int} and {int}',
-    async function (index, round) {
-      await this.indexerClient.lookupApplications(index).round(round).do();
-    }
-  );
-
   When(
     'we make a LookupApplicationLogsByID call with applicationID {int} limit {int} minRound {int} maxRound {int} nextToken {string} sender {string} and txID {string}',
     async function (appID, limit, minRound, maxRound, nextToken, sender, txID) {
@@ -2346,26 +2265,6 @@ module.exports = function getSteps(options) {
     }
   );
 
-  When(
-    'we make a Search Accounts call with assetID {int} limit {int} currencyGreaterThan {int} currencyLessThan {int} and nextToken {string}',
-    async function (
-      assetIndex,
-      limit,
-      currencyGreater,
-      currencyLesser,
-      nextToken
-    ) {
-      await this.indexerClient
-        .searchAccounts()
-        .assetID(assetIndex)
-        .limit(limit)
-        .currencyGreaterThan(currencyGreater)
-        .currencyLessThan(currencyLesser)
-        .nextToken(nextToken)
-        .do();
-    }
-  );
-
   When(
     'we make a Search Accounts call with assetID {int} limit {int} currencyGreaterThan {int} currencyLessThan {int} and round {int}',
     async function (assetIndex, limit, currencyGreater, currencyLesser, round) {
@@ -2409,63 +2308,6 @@ module.exports = function getSteps(options) {
     }
   );
 
-  When(
-    'we make a Search For Transactions call with account {string} NotePrefix {string} TxType {string} SigType {string} txid {string} round {int} minRound {int} maxRound {int} limit {int} beforeTime {int} afterTime {int} currencyGreaterThan {int} currencyLessThan {int} assetIndex {int} addressRole {string} ExcluseCloseTo {string}',
-    async function (
-      account,
-      notePrefix,
-      txType,
-      sigType,
-      txid,
-      round,
-      minRound,
-      maxRound,
-      limit,
-      beforeTime,
-      afterTime,
-      currencyGreater,
-      currencyLesser,
-      assetIndex,
-      addressRole,
-      excludeCloseToAsString
-    ) {
-      let excludeCloseTo = false;
-      if (excludeCloseToAsString === 'true') {
-        excludeCloseTo = true;
-      }
-      await this.indexerClient
-        .searchForTransactions()
-        .address(account)
-        .notePrefix(notePrefix)
-        .txType(txType)
-        .sigType(sigType)
-        .txid(txid)
-        .round(round)
-        .minRound(minRound)
-        .maxRound(maxRound)
-        .limit(limit)
-        .beforeTime(beforeTime)
-        .afterTime(afterTime)
-        .currencyGreaterThan(currencyGreater)
-        .currencyLessThan(currencyLesser)
-        .assetID(assetIndex)
-        .addressRole(addressRole)
-        .excludeCloseTo(excludeCloseTo)
-        .do();
-    }
-  );
-
-  When(
-    'we make a SearchForApplications call with {int} and {int}',
-    async function (index, round) {
-      await this.indexerClient
-        .searchForApplications()
-        .index(index)
-        .round(round)
-        .do();
-    }
-  );
-
   When(
     'we make a SearchForApplications call with creator {string}',
     async function (creator) {
@@ -2571,21 +2413,6 @@ module.exports = function getSteps(options) {
     }
   );
 
-  When(
-    'we make a SearchForAssets call with limit {int} creator {string} name {string} unit {string} index {int} and nextToken {string}',
-    async function (limit, creator, name, unit, index, nextToken) {
-      await this.indexerClient
-        .searchForAssets()
-        .limit(limit)
-        .creator(creator)
-        .name(name)
-        .unit(unit)
-        .index(index)
-        .nextToken(nextToken)
-        .do();
-    }
-  );
-
   When(
     'we make a SearchForAssets call with limit {int} creator {string} name {string} unit {string} index {int}',
     async function (limit, creator, name, unit, index) {
@@ -2607,13 +2434,6 @@ module.exports = function getSteps(options) {
     }
   );
 
-  When(
-    'we make a SearchForApplications call with creator {int}',
-    async function (index) {
-      await this.indexerClient.searchForApplications().creator(index).do();
-    }
-  );
-
   When(
     'we make a LookupApplications call with applicationID {int}',
     async function (index) {
@@ -2924,870 +2744,57 @@ module.exports = function getSteps(options) {
   );
 
   /// /////////////////////////////////
-  // begin indexer and integration tests
+  // begin rekey test helpers
   /// /////////////////////////////////
 
-  const indexerIntegrationClients = {};
-
-  Given(
-    'indexer client {int} at {string} port {int} with token {string}',
-    (clientNum, indexerHost, indexerPort, indexerToken) => {
-      let mutableIndexerHost = indexerHost;
+  When('I add a rekeyTo field with address {string}', function (address) {
+    this.txn.reKeyTo = address;
+  });
 
-      if (!mutableIndexerHost.startsWith('http')) {
-        mutableIndexerHost = `http://${mutableIndexerHost}`;
-      }
-      indexerIntegrationClients[clientNum] = new algosdk.Indexer(
-        indexerToken,
-        mutableIndexerHost,
-        indexerPort,
-        {}
-      );
+  When(
+    'I add a rekeyTo field with the private key algorand address',
+    function () {
+      const keypair = keyPairFromSecretKey(this.sk);
+      const pubKeyFromSk = keypair.publicKey;
+      this.txn.reKeyTo = algosdk.encodeAddress(pubKeyFromSk);
     }
   );
 
-  When('I use {int} to check the services health', async (clientNum) => {
-    const ic = indexerIntegrationClients[clientNum];
-    await ic.makeHealthCheck().do();
-  });
-
-  Then('I receive status code {int}', async (code) => {
-    // Currently only supports the good case. code != 200 should throw an exception.
-    assert.strictEqual(code, 200);
+  When('I set the from address to {string}', function (from) {
+    this.txn.from = from;
   });
 
-  let integrationBlockResponse;
+  let dryrunResponse;
 
-  When('I use {int} to lookup block {int}', async (clientNum, blockNum) => {
-    const ic = indexerIntegrationClients[clientNum];
-    integrationBlockResponse = await ic.lookupBlock(blockNum).do();
+  When('we make any Dryrun call', async function () {
+    const dr = new algosdk.modelsv2.DryrunRequest({});
+    dryrunResponse = await this.v2Client.dryrun(dr).do();
   });
 
   Then(
-    'The block was confirmed at {int}, contains {int} transactions, has the previous block hash {string}',
-    (timestamp, numTransactions, prevHash) => {
-      assert.strictEqual(timestamp, integrationBlockResponse.timestamp);
-      assert.strictEqual(
-        numTransactions,
-        integrationBlockResponse.transactions.length
-      );
+    'the parsed Dryrun Response should have global delta {string} with {int}',
+    (key, action) => {
+      assert.strictEqual(dryrunResponse.txns[0]['global-delta'][0].key, key);
       assert.strictEqual(
-        prevHash,
-        integrationBlockResponse['previous-block-hash']
+        dryrunResponse.txns[0]['global-delta'][0].value.action,
+        action
       );
     }
   );
 
-  let integrationLookupAccountResponse;
-
-  When(
-    'I use {int} to lookup account {string} at round {int}',
-    async (clientNum, account, round) => {
-      const ic = indexerIntegrationClients[clientNum];
-      integrationLookupAccountResponse = await ic
-        .lookupAccountByID(account)
-        .round(round)
-        .do();
-    }
-  );
-
-  Then(
-    'The account has {int} assets, the first is asset {int} has a frozen status of {string} and amount {int}.',
-    (numAssets, firstAssetIndex, firstAssetFrozenStatus, firstAssetAmount) => {
-      const firstAssetFrozenBool = firstAssetFrozenStatus === 'true';
-      assert.strictEqual(
-        numAssets,
-        integrationLookupAccountResponse.account.assets.length
-      );
-      if (numAssets === 0) {
-        return;
-      }
-      const scrutinizedAsset =
-        integrationLookupAccountResponse.account.assets[0];
-      assert.strictEqual(firstAssetIndex, scrutinizedAsset['asset-id']);
-      assert.strictEqual(firstAssetFrozenBool, scrutinizedAsset['is-frozen']);
-      assert.strictEqual(firstAssetAmount, scrutinizedAsset.amount);
-    }
-  );
-
-  Then(
-    'The account created {int} assets, the first is asset {int} is named {string} with a total amount of {int} {string}',
-    (
-      numCreatedAssets,
-      firstCreatedAssetIndex,
-      assetName,
-      assetIssuance,
-      assetUnit
-    ) => {
-      assert.strictEqual(
-        numCreatedAssets,
-        integrationLookupAccountResponse.account['created-assets'].length
-      );
-      const scrutinizedAsset =
-        integrationLookupAccountResponse.account['created-assets'][0];
-      assert.strictEqual(firstCreatedAssetIndex, scrutinizedAsset.index);
-      assert.strictEqual(assetName, scrutinizedAsset.params.name);
-      assert.strictEqual(assetIssuance, scrutinizedAsset.params.total);
-      assert.strictEqual(assetUnit, scrutinizedAsset.params['unit-name']);
-    }
-  );
-
-  Then(
-    'The account has {int} μalgos and {int} assets, {int} has {int}',
-    (microAlgos, numAssets, assetIndexToScrutinize, assetAmount) => {
-      assert.strictEqual(
-        microAlgos,
-        integrationLookupAccountResponse.account.amount
-      );
-      if (numAssets === 0) {
-        return;
-      }
-      assert.strictEqual(
-        numAssets,
-        integrationLookupAccountResponse.account.assets.length
-      );
-      if (assetIndexToScrutinize === 0) {
-        return;
-      }
-      for (
-        let idx = 0;
-        idx < integrationLookupAccountResponse.account.assets.length;
-        idx++
-      ) {
-        const scrutinizedAsset =
-          integrationLookupAccountResponse.account.assets[idx];
-        if (scrutinizedAsset.index === assetIndexToScrutinize) {
-          assert.strictEqual(assetAmount, scrutinizedAsset.amount);
-        }
-      }
-    }
-  );
-
-  let integrationLookupAssetResponse;
-
-  When('I use {int} to lookup asset {int}', async (clientNum, assetIndex) => {
-    const ic = indexerIntegrationClients[clientNum];
-    integrationLookupAssetResponse = await ic.lookupAssetByID(assetIndex).do();
-  });
-
-  Then(
-    'The asset found has: {string}, {string}, {string}, {int}, {string}, {int}, {string}',
-    (
-      name,
-      units,
-      creator,
-      decimals,
-      defaultFrozen,
-      totalIssuance,
-      clawback
-    ) => {
-      const assetParams = integrationLookupAssetResponse.asset.params;
-      assert.strictEqual(name, assetParams.name);
-      assert.strictEqual(units, assetParams['unit-name']);
-      assert.strictEqual(creator, assetParams.creator);
-      assert.strictEqual(decimals, assetParams.decimals);
-      const defaultFrozenBool = defaultFrozen === 'true';
-      assert.strictEqual(defaultFrozenBool, assetParams['default-frozen']);
-      assert.strictEqual(totalIssuance, assetParams.total);
-      assert.strictEqual(clawback, assetParams.clawback);
-    }
-  );
-
-  let integrationLookupAssetBalancesResponse;
-
-  When(
-    'I use {int} to lookup asset balances for {int} with {int}, {int}, {int} and token {string}',
-    async (
-      clientNum,
-      assetIndex,
-      currencyGreater,
-      currencyLesser,
-      limit,
-      nextToken
-    ) => {
-      const ic = indexerIntegrationClients[clientNum];
-      integrationLookupAssetBalancesResponse = await ic
-        .lookupAssetBalances(assetIndex)
-        .currencyGreaterThan(currencyGreater)
-        .currencyLessThan(currencyLesser)
-        .limit(limit)
-        .nextToken(nextToken)
-        .do();
-    }
-  );
-
-  When(
-    'I get the next page using {int} to lookup asset balances for {int} with {int}, {int}, {int}',
-    async (clientNum, assetIndex, currencyGreater, currencyLesser, limit) => {
-      const ic = indexerIntegrationClients[clientNum];
-      const nextToken = integrationLookupAssetBalancesResponse['next-token'];
-      integrationLookupAssetBalancesResponse = await ic
-        .lookupAssetBalances(assetIndex)
-        .currencyGreaterThan(currencyGreater)
-        .currencyLessThan(currencyLesser)
-        .limit(limit)
-        .nextToken(nextToken)
-        .do();
-    }
-  );
-
-  Then(
-    'There are {int} with the asset, the first is {string} has {string} and {int}',
-    (numAccounts, firstAccountAddress, isFrozenString, accountAmount) => {
-      assert.strictEqual(
-        numAccounts,
-        integrationLookupAssetBalancesResponse.balances.length
-      );
-      if (numAccounts === 0) {
-        return;
-      }
-      const firstHolder = integrationLookupAssetBalancesResponse.balances[0];
-      assert.strictEqual(firstAccountAddress, firstHolder.address);
-      const isFrozenBool = isFrozenString === 'true';
-      assert.strictEqual(isFrozenBool, firstHolder['is-frozen']);
-      assert.strictEqual(accountAmount, firstHolder.amount);
-    }
-  );
-
-  let integrationSearchAccountsResponse;
-
-  When(
-    'I use {int} to search for an account with {int}, {int}, {int}, {int} and token {string}',
-    async (
-      clientNum,
-      assetIndex,
-      limit,
-      currencyGreater,
-      currencyLesser,
-      nextToken
-    ) => {
-      const ic = indexerIntegrationClients[clientNum];
-      integrationSearchAccountsResponse = await ic
-        .searchAccounts()
-        .assetID(assetIndex)
-        .currencyGreaterThan(currencyGreater)
-        .currencyLessThan(currencyLesser)
-        .limit(limit)
-        .nextToken(nextToken)
-        .do();
-    }
-  );
-
-  When(
-    'I use {int} to search for an account with {int}, {int}, {int}, {int}, {string}, {int} and token {string}',
-    async function (
-      clientNum,
-      assetIndex,
-      limit,
-      currencyGreater,
-      currencyLesser,
-      authAddr,
-      appID,
-      nextToken
-    ) {
-      const ic = indexerIntegrationClients[clientNum];
-      integrationSearchAccountsResponse = await ic
-        .searchAccounts()
-        .assetID(assetIndex)
-        .currencyGreaterThan(currencyGreater)
-        .currencyLessThan(currencyLesser)
-        .limit(limit)
-        .authAddr(authAddr)
-        .applicationID(appID)
-        .nextToken(nextToken)
-        .do();
-      this.responseForDirectJsonComparison = integrationSearchAccountsResponse;
-    }
-  );
-
-  When(
-    'I use {int} to search for an account with {int}, {int}, {int}, {int}, {string}, {int}, {string} and token {string}',
-    async function (
-      clientNum,
-      assetIndex,
-      limit,
-      currencyGreater,
-      currencyLesser,
-      authAddr,
-      appID,
-      includeAll,
-      nextToken
-    ) {
-      const ic = indexerIntegrationClients[clientNum];
-      integrationSearchAccountsResponse = await ic
-        .searchAccounts()
-        .assetID(assetIndex)
-        .currencyGreaterThan(currencyGreater)
-        .currencyLessThan(currencyLesser)
-        .limit(limit)
-        .authAddr(authAddr)
-        .applicationID(appID)
-        .includeAll(formatIncludeAll(includeAll))
-        .nextToken(nextToken)
-        .do();
-      this.responseForDirectJsonComparison = integrationSearchAccountsResponse;
-    }
-  );
-
-  Then(
-    'There are {int}, the first has {int}, {int}, {int}, {int}, {string}, {int}, {string}, {string}',
-    (
-      numAccounts,
-      pendingRewards,
-      rewardsBase,
-      rewards,
-      withoutRewards,
-      address,
-      amount,
-      status,
-      type
-    ) => {
-      assert.strictEqual(
-        numAccounts,
-        integrationSearchAccountsResponse.accounts.length
-      );
-      if (numAccounts === 0) {
-        return;
-      }
-      const scrutinizedAccount = integrationSearchAccountsResponse.accounts[0];
-      assert.strictEqual(pendingRewards, scrutinizedAccount['pending-rewards']);
-      assert.strictEqual(rewardsBase, scrutinizedAccount['reward-base']);
-      assert.strictEqual(rewards, scrutinizedAccount.rewards);
-      assert.strictEqual(
-        withoutRewards,
-        scrutinizedAccount['amount-without-pending-rewards']
-      );
-      assert.strictEqual(address, scrutinizedAccount.address);
-      assert.strictEqual(amount, scrutinizedAccount.amount);
-      assert.strictEqual(status, scrutinizedAccount.status);
-      if (type) {
-        assert.strictEqual(type, scrutinizedAccount['sig-type']);
-      }
-    }
-  );
-
-  Then(
-    'I get the next page using {int} to search for an account with {int}, {int}, {int} and {int}',
-    async (clientNum, assetIndex, limit, currencyGreater, currencyLesser) => {
-      const ic = indexerIntegrationClients[clientNum];
-      const nextToken = integrationSearchAccountsResponse['next-token'];
-      integrationSearchAccountsResponse = await ic
-        .searchAccounts()
-        .assetID(assetIndex)
-        .currencyGreaterThan(currencyGreater)
-        .currencyLessThan(currencyLesser)
-        .limit(limit)
-        .nextToken(nextToken)
-        .do();
-    }
-  );
-
-  Then(
-    'The first account is online and has {string}, {int}, {int}, {int}, {string}, {string}',
-    (address, keyDilution, firstValid, lastValid, voteKey, selKey) => {
-      const scrutinizedAccount = integrationSearchAccountsResponse.accounts[0];
-      assert.strictEqual('Online', scrutinizedAccount.status);
-      assert.strictEqual(address, scrutinizedAccount.address);
-      assert.strictEqual(
-        keyDilution,
-        scrutinizedAccount.participation['vote-key-dilution']
-      );
-      assert.strictEqual(
-        firstValid,
-        scrutinizedAccount.participation['vote-first-valid']
-      );
-      assert.strictEqual(
-        lastValid,
-        scrutinizedAccount.participation['vote-last-valid']
-      );
-      assert.strictEqual(
-        voteKey,
-        scrutinizedAccount.participation['vote-participation-key']
-      );
-      assert.strictEqual(
-        selKey,
-        scrutinizedAccount.participation['selection-participation-key']
-      );
-    }
-  );
-
-  let integrationSearchTransactionsResponse;
-
-  When(
-    'I use {int} to search for transactions with {int}, {string}, {string}, {string}, {string}, {int}, {int}, {int}, {int}, {string}, {string}, {int}, {int}, {string}, {string}, {string} and token {string}',
-    async (
-      clientNum,
-      limit,
-      notePrefix,
-      txType,
-      sigType,
-      txid,
-      round,
-      minRound,
-      maxRound,
-      assetId,
-      beforeTime,
-      afterTime,
-      currencyGreater,
-      currencyLesser,
-      address,
-      addressRole,
-      excludeCloseToString,
-      nextToken
-    ) => {
-      const ic = indexerIntegrationClients[clientNum];
-      const excludeCloseToBool = excludeCloseToString === 'true';
-      integrationSearchTransactionsResponse = await ic
-        .searchForTransactions()
-        .limit(limit)
-        .notePrefix(notePrefix)
-        .txType(txType)
-        .sigType(sigType)
-        .txid(txid)
-        .round(round)
-        .minRound(minRound)
-        .maxRound(maxRound)
-        .assetID(assetId)
-        .beforeTime(beforeTime)
-        .afterTime(afterTime)
-        .currencyGreaterThan(currencyGreater)
-        .currencyLessThan(currencyLesser)
-        .address(address)
-        .addressRole(addressRole)
-        .excludeCloseTo(excludeCloseToBool)
-        .nextToken(nextToken)
-        .do();
-    }
-  );
-
-  When(
-    'I use {int} to search for transactions with {int}, {string}, {string}, {string}, {string}, {int}, {int}, {int}, {int}, {string}, {string}, {int}, {int}, {string}, {string}, {string}, {int} and token {string}',
-    async function (
-      clientNum,
-      limit,
-      notePrefix,
-      txType,
-      sigType,
-      txid,
-      round,
-      minRound,
-      maxRound,
-      assetId,
-      beforeTime,
-      afterTime,
-      currencyGreater,
-      currencyLesser,
-      address,
-      addressRole,
-      excludeCloseToString,
-      appID,
-      nextToken
-    ) {
-      const ic = indexerIntegrationClients[clientNum];
-      const excludeCloseToBool = excludeCloseToString === 'true';
-      integrationSearchTransactionsResponse = await ic
-        .searchForTransactions()
-        .limit(limit)
-        .notePrefix(notePrefix)
-        .txType(txType)
-        .sigType(sigType)
-        .txid(txid)
-        .round(round)
-        .minRound(minRound)
-        .maxRound(maxRound)
-        .assetID(assetId)
-        .beforeTime(beforeTime)
-        .afterTime(afterTime)
-        .currencyGreaterThan(currencyGreater)
-        .currencyLessThan(currencyLesser)
-        .address(address)
-        .addressRole(addressRole)
-        .excludeCloseTo(excludeCloseToBool)
-        .applicationID(appID)
-        .nextToken(nextToken)
-        .do();
-      this.responseForDirectJsonComparison = integrationSearchTransactionsResponse;
-    }
-  );
-
-  When(
-    'I use {int} to search for all {string} transactions',
-    async (clientNum, account) => {
-      const ic = indexerIntegrationClients[clientNum];
-      integrationSearchTransactionsResponse = await ic
-        .searchForTransactions()
-        .address(account)
-        .do();
-    }
-  );
-
-  When(
-    'I use {int} to search for all {int} asset transactions',
-    async (clientNum, assetIndex) => {
-      const ic = indexerIntegrationClients[clientNum];
-      integrationSearchTransactionsResponse = await ic
-        .searchForTransactions()
-        .assetID(assetIndex)
-        .do();
-    }
-  );
-
-  When(
-    'I use {int} to search for applications with {int}, {int}, and token {string}',
-    async function (clientNum, limit, appID, token) {
-      const ic = indexerIntegrationClients[clientNum];
-      this.responseForDirectJsonComparison = await ic
-        .searchForApplications()
-        .limit(limit)
-        .index(appID)
-        .nextToken(token)
-        .do();
-    }
-  );
-
-  When(
-    'I use {int} to search for applications with {int}, {int}, {string} and token {string}',
-    async function (clientNum, limit, appID, includeAll, token) {
-      const ic = indexerIntegrationClients[clientNum];
-      this.responseForDirectJsonComparison = await ic
-        .searchForApplications()
-        .limit(limit)
-        .index(appID)
-        .includeAll(formatIncludeAll(includeAll))
-        .nextToken(token)
-        .do();
-    }
-  );
-
-  When(
-    'I use {int} to lookup application with {int}',
-    async function (clientNum, appID) {
-      const ic = indexerIntegrationClients[clientNum];
-      this.responseForDirectJsonComparison = await ic
-        .lookupApplications(appID)
-        .do();
-    }
-  );
-
-  When(
-    'I use {int} to lookup application with {int} and {string}',
-    async function (clientNum, appID, includeAll) {
-      const ic = indexerIntegrationClients[clientNum];
-      try {
-        this.responseForDirectJsonComparison = await ic
-          .lookupApplications(appID)
-          .includeAll(formatIncludeAll(includeAll))
-          .do();
-      } catch (err) {
-        if (err.status !== 404) {
-          throw err;
-        }
-        this.responseForDirectJsonComparison = err.response.body;
-      }
-    }
-  );
-
-  function sortKeys(x) {
-    // recursively sorts on keys, unless the passed object is an array of dicts that all contain the property 'key',
-    // in which case it sorts on the value corresponding to key 'key'
-    if (typeof x !== 'object' || !x) return x;
-    if (Array.isArray(x)) {
-      if (
-        x.every(
-          (subobject) =>
-            typeof subobject === 'object' &&
-            Object.prototype.hasOwnProperty.call(subobject, 'key')
-        )
-      ) {
-        return x.sort((a, b) => (a.key > b.key ? 1 : -1));
-      }
-      return x.map(sortKeys);
-    }
-    return Object.keys(x)
-      .sort()
-      .reduce((o, k) => ({ ...o, [k]: sortKeys(x[k]) }), {});
-  }
-
-  Then('the parsed response should equal {string}.', async function (jsonFile) {
-    const rawResponse = await loadResource(jsonFile);
-    const responseFromFile = sortKeys(JSON.parse(rawResponse.toString()));
-    this.responseForDirectJsonComparison = sortKeys(
-      this.responseForDirectJsonComparison
-    );
-    assert.strictEqual(
-      JSON.stringify(this.responseForDirectJsonComparison),
-      JSON.stringify(responseFromFile)
-    );
-  });
-
-  When(
-    'I get the next page using {int} to search for transactions with {int} and {int}',
-    async (clientNum, limit, maxRound) => {
-      const ic = indexerIntegrationClients[clientNum];
-      const nextToken = integrationSearchTransactionsResponse['next-token'];
-      integrationSearchTransactionsResponse = await ic
-        .searchForTransactions()
-        .limit(limit)
-        .maxRound(maxRound)
-        .nextToken(nextToken)
-        .do();
-    }
-  );
-
-  Then(
-    'there are {int} transactions in the response, the first is {string}.',
-    (numTransactions, txid) => {
-      assert.strictEqual(
-        numTransactions,
-        integrationSearchTransactionsResponse.transactions.length
-      );
-      if (numTransactions === 0) {
-        return;
-      }
-      assert.strictEqual(
-        txid,
-        integrationSearchTransactionsResponse.transactions[0].id
-      );
-    }
-  );
-
-  Then('Every transaction has tx-type {string}', (txType) => {
-    for (
-      let idx = 0;
-      idx < integrationSearchTransactionsResponse.transactions.length;
-      idx++
-    ) {
-      const scrutinizedTxn =
-        integrationSearchTransactionsResponse.transactions[idx];
-      assert.strictEqual(txType, scrutinizedTxn['tx-type']);
-    }
-  });
-
-  Then('Every transaction has sig-type {string}', (sigType) => {
-    function getSigTypeFromTxnResponse(txn) {
-      if (txn.signature.logicsig) {
-        return 'lsig';
-      }
-      if (txn.signature.sig) {
-        return 'sig';
-      }
-      if (txn.signature.multisig) {
-        return 'msig';
-      }
-      return 'did not recognize sigtype of txn';
-    }
-    for (
-      let idx = 0;
-      idx < integrationSearchTransactionsResponse.transactions.length;
-      idx++
-    ) {
-      const scrutinizedTxn =
-        integrationSearchTransactionsResponse.transactions[idx];
-      assert.strictEqual(sigType, getSigTypeFromTxnResponse(scrutinizedTxn));
-    }
-  });
-
-  Then('Every transaction has round {int}', (round) => {
-    for (
-      let idx = 0;
-      idx < integrationSearchTransactionsResponse.transactions.length;
-      idx++
-    ) {
-      const scrutinizedTxn =
-        integrationSearchTransactionsResponse.transactions[idx];
-      assert.strictEqual(round, scrutinizedTxn['confirmed-round']);
-    }
-  });
-
-  Then('Every transaction has round >= {int}', (round) => {
-    for (
-      let idx = 0;
-      idx < integrationSearchTransactionsResponse.transactions.length;
-      idx++
-    ) {
-      const scrutinizedTxn =
-        integrationSearchTransactionsResponse.transactions[idx];
-      assert.ok(round <= scrutinizedTxn['confirmed-round']);
-    }
-  });
-
-  Then('Every transaction has round <= {int}', (round) => {
-    for (
-      let idx = 0;
-      idx < integrationSearchTransactionsResponse.transactions.length;
-      idx++
-    ) {
-      const scrutinizedTxn =
-        integrationSearchTransactionsResponse.transactions[idx];
-      assert.ok(round >= scrutinizedTxn['confirmed-round']);
-    }
-  });
-
-  Then('Every transaction works with asset-id {int}', (assetId) => {
-    function extractIdFromTransaction(txn) {
-      if (txn['created-asset-index']) {
-        return txn['created-asset-index'];
-      }
-      if (txn['asset-config-transaction']) {
-        return txn['asset-config-transaction']['asset-id'];
-      }
-      if (txn['asset-transfer-transaction']) {
-        return txn['asset-transfer-transaction']['asset-id'];
-      }
-      if (txn['asset-freeze-transaction']) {
-        return txn['asset-freeze-transaction']['asset-id'];
-      }
-      return 'could not find asset id within txn';
-    }
-    for (
-      let idx = 0;
-      idx < integrationSearchTransactionsResponse.transactions.length;
-      idx++
-    ) {
-      const scrutinizedTxn =
-        integrationSearchTransactionsResponse.transactions[idx];
-      assert.strictEqual(assetId, extractIdFromTransaction(scrutinizedTxn));
-    }
-  });
-
-  Then('Every transaction is older than {string}', (olderThan) => {
-    for (
-      let idx = 0;
-      idx < integrationSearchTransactionsResponse.transactions.length;
-      idx++
-    ) {
-      const scrutinizedTxn =
-        integrationSearchTransactionsResponse.transactions[idx];
-      assert.ok(scrutinizedTxn['round-time'] < Date.parse(olderThan) / 1000);
-    }
-  });
-
-  Then('Every transaction is newer than {string}', (newerThan) => {
-    for (
-      let idx = 0;
-      idx < integrationSearchTransactionsResponse.transactions.length;
-      idx++
-    ) {
-      const scrutinizedTxn =
-        integrationSearchTransactionsResponse.transactions[idx];
-      assert.ok(scrutinizedTxn['round-time'] > Date.parse(newerThan) / 1000);
-    }
-  });
-
-  Then(
-    'Every transaction moves between {int} and {int} currency',
-    (lowerBound, upperBound) => {
-      function getAmountMoved(txn) {
-        if (txn['payment-transaction']) {
-          return txn['payment-transaction'].amount;
-        }
-        if (txn['asset-transfer-transaction']) {
-          return txn['asset-transfer-transaction'].amount;
-        }
-        return 'could not get amount moved from txn';
-      }
-      for (
-        let idx = 0;
-        idx < integrationSearchTransactionsResponse.transactions.length;
-        idx++
-      ) {
-        const scrutinizedTxn =
-          integrationSearchTransactionsResponse.transactions[idx];
-        const amountMoved = getAmountMoved(scrutinizedTxn);
-        if (upperBound !== 0) {
-          assert.ok(amountMoved <= upperBound);
-        }
-        assert.ok(amountMoved >= lowerBound);
-      }
-    }
-  );
-
-  let integrationSearchAssetsResponse;
-
-  When(
-    'I use {int} to search for assets with {int}, {int}, {string}, {string}, {string}, and token {string}',
-    async (clientNum, zero, assetId, creator, name, unit, nextToken) => {
-      const ic = indexerIntegrationClients[clientNum];
-      integrationSearchAssetsResponse = await ic
-        .searchForAssets()
-        .index(assetId)
-        .creator(creator)
-        .name(name)
-        .unit(unit)
-        .nextToken(nextToken)
-        .do();
-    }
-  );
-
-  Then(
-    'there are {int} assets in the response, the first is {int}.',
-    (numAssets, firstAssetId) => {
-      assert.strictEqual(
-        numAssets,
-        integrationSearchAssetsResponse.assets.length
-      );
-      if (numAssets === 0) {
-        return;
-      }
-      assert.strictEqual(
-        firstAssetId,
-        integrationSearchAssetsResponse.assets[0].index
-      );
-    }
-  );
-
-  /// /////////////////////////////////
-  // begin rekey test helpers
-  /// /////////////////////////////////
-
-  When('I add a rekeyTo field with address {string}', function (address) {
-    this.txn.reKeyTo = address;
-  });
-
-  When(
-    'I add a rekeyTo field with the private key algorand address',
-    function () {
-      const keypair = keyPairFromSecretKey(this.sk);
-      const pubKeyFromSk = keypair.publicKey;
-      this.txn.reKeyTo = algosdk.encodeAddress(pubKeyFromSk);
-    }
-  );
-
-  When('I set the from address to {string}', function (from) {
-    this.txn.from = from;
-  });
-
-  let dryrunResponse;
-
-  When('we make any Dryrun call', async function () {
-    const dr = new algosdk.modelsv2.DryrunRequest({});
-    dryrunResponse = await this.v2Client.dryrun(dr).do();
-  });
-
-  Then(
-    'the parsed Dryrun Response should have global delta {string} with {int}',
-    (key, action) => {
-      assert.strictEqual(dryrunResponse.txns[0]['global-delta'][0].key, key);
-      assert.strictEqual(
-        dryrunResponse.txns[0]['global-delta'][0].value.action,
-        action
-      );
-    }
-  );
-
-  When('I dryrun a {string} program {string}', async function (kind, program) {
-    const data = await loadResource(program);
-    const algoTxn = new algosdk.Transaction({
-      from: 'UAPJE355K7BG7RQVMTZOW7QW4ICZJEIC3RZGYG5LSHZ65K6LCNFPJDSR7M',
-      fee: 1000,
-      amount: 1000,
-      firstRound: 1,
-      lastRound: 1000,
-      type: 'pay',
-      genesisHash: 'ZIkPs8pTDxbRJsFB1yJ7gvnpDu0Q85FRkl2NCkEAQLU=',
-    });
-    let txns;
-    let sources;
+  When('I dryrun a {string} program {string}', async function (kind, program) {
+    const data = await loadResource(program);
+    const algoTxn = new algosdk.Transaction({
+      from: 'UAPJE355K7BG7RQVMTZOW7QW4ICZJEIC3RZGYG5LSHZ65K6LCNFPJDSR7M',
+      fee: 1000,
+      amount: 1000,
+      firstRound: 1,
+      lastRound: 1000,
+      type: 'pay',
+      genesisHash: 'ZIkPs8pTDxbRJsFB1yJ7gvnpDu0Q85FRkl2NCkEAQLU=',
+    });
+    let txns;
+    let sources;
 
     switch (kind) {
       case 'compiled':
@@ -5476,6 +4483,43 @@ module.exports = function getSteps(options) {
     }
   );
 
+  When(
+    'we make a GetLightBlockHeaderProof call for round {int}',
+    async function (int) {
+      await this.v2Client.getLightBlockHeaderProof(int).do();
+    }
+  );
+
+  When('we make a GetStateProof call for round {int}', async function (int) {
+    await this.v2Client.getStateProof(int).do();
+  });
+
+  Given(
+    'a base64 encoded program bytes for heuristic sanity check {string}',
+    async function (programByteStr) {
+      this.seeminglyProgram = new Uint8Array(
+        Buffer.from(programByteStr, 'base64')
+      );
+    }
+  );
+
+  When('I start heuristic sanity check over the bytes', async function () {
+    this.actualErrMsg = undefined;
+    try {
+      new algosdk.LogicSigAccount(this.seeminglyProgram); // eslint-disable-line
+    } catch (e) {
+      this.actualErrMsg = e.message;
+    }
+  });
+
+  Then(
+    'if the heuristic sanity check throws an error, the error contains {string}',
+    async function (errMsg) {
+      if (errMsg !== '') assert.ok(this.actualErrMsg.includes(errMsg));
+      else assert.strictEqual(this.actualErrMsg, undefined);
+    }
+  );
+
   if (!options.ignoreReturn) {
     return steps;
   }
diff --git a/tests/cucumber/unit.tags b/tests/cucumber/unit.tags
new file mode 100644
index 0000000..2d3f46b
--- /dev/null
+++ b/tests/cucumber/unit.tags
@@ -0,0 +1,26 @@
+@unit.abijson
+@unit.abijson.byname
+@unit.algod
+@unit.algod.ledger_refactoring
+@unit.applications
+@unit.atomic_transaction_composer
+@unit.dryrun
+@unit.dryrun.trace.application
+@unit.feetest
+@unit.indexer
+@unit.indexer.ledger_refactoring
+@unit.indexer.logs
+@unit.offline
+@unit.program_sanity_check
+@unit.rekey
+@unit.responses
+@unit.responses.231
+@unit.responses.unlimited_assets
+@unit.sourcemap
+@unit.stateproof.paths
+@unit.stateproof.responses
+@unit.stateproof.responses.msgp
+@unit.tealsign
+@unit.transactions
+@unit.transactions.keyreg
+@unit.transactions.payment