Repository: algorand/js-algorand-sdk
Pull request #490 (2021-12-28 20:51:52)

FOR REVIEW ONLY - js-algorand-sdk v1.13.0-beta.2
# 1.13.0-beta.2

## Added

- Support ABI reference types and other improvements (#482)
- Improve HTTP error messages (#485)
- Enabling a custom client for Algod and Indexer (#477)
- Export sdk subclasses to typedoc (#479)

## Fixed

- Fix wait for confirmation function (#480)


Patch:
diff --git a/.gitignore b/.gitignore
index 495e3de..5ac92e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@ tests/browser/bundle.*
 # Builds
 dist/
 docs/
+built/
 
 # Caches
 .eslintcache
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6969ec..3c8e50b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,16 @@
+# 1.13.0-beta.2
+
+## Added
+
+- Support ABI reference types and other improvements (#482)
+- Improve HTTP error messages (#485)
+- Enabling a custom client for Algod and Indexer (#477)
+- Export sdk subclasses to typedoc (#479)
+
+## Fixed
+
+- Fix wait for confirmation function (#480)
+
 # 1.13.0-beta.1
 
 ## Added
diff --git a/README.md b/README.md
index 3e35a87..4d3c938 100644
--- a/README.md
+++ b/README.md
@@ -20,8 +20,8 @@ Include a minified browser bundle directly in your HTML like so:
 
 ```html
 <script
-  src="https://unpkg.com/algosdk@1.13.0-beta.1/dist/browser/algosdk.min.js"
-  integrity="sha384-w9fodN00YjvWxdNoiyT+W6CqaATkXlfid9tnzBlToDqZWepVWLjI5wO7jNL4+aNs"
+  src="https://unpkg.com/algosdk@1.13.0-beta.2/dist/browser/algosdk.min.js"
+  integrity="sha384-ArIfXzQ4ARpkRJIn6EKgtqbJaPXhEEvNoguSPToHMg2VNl2rNc6QuuOTyDX7Krps"
   crossorigin="anonymous"
 ></script>
 ```
@@ -30,8 +30,8 @@ or
 
 ```html
 <script
-  src="https://cdn.jsdelivr.net/npm/algosdk@1.13.0-beta.1/dist/browser/algosdk.min.js"
-  integrity="sha384-w9fodN00YjvWxdNoiyT+W6CqaATkXlfid9tnzBlToDqZWepVWLjI5wO7jNL4+aNs"
+  src="https://cdn.jsdelivr.net/npm/algosdk@1.13.0-beta.2/dist/browser/algosdk.min.js"
+  integrity="sha384-ArIfXzQ4ARpkRJIn6EKgtqbJaPXhEEvNoguSPToHMg2VNl2rNc6QuuOTyDX7Krps"
   crossorigin="anonymous"
 ></script>
 ```
diff --git a/package-lock.json b/package-lock.json
index bd28b9b..24b64f7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "algosdk",
-  "version": "1.13.0-beta.1",
+  "version": "1.13.0-beta.2",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "algosdk",
-      "version": "1.13.0-beta.1",
+      "version": "1.13.0-beta.2",
       "license": "MIT",
       "dependencies": {
         "algo-msgpack-with-bigint": "^2.1.1",
@@ -51,7 +51,9 @@
         "source-map-loader": "^2.0.2",
         "ts-loader": "^8.3.0",
         "ts-node": "^10.0.0",
-        "typedoc": "^0.20.36",
+        "typedoc": "^0.22.10",
+        "typedoc-plugin-missing-exports": "^0.22.6",
+        "typedoc-plugin-rename-defaults": "^0.4.0",
         "typescript": "^4.2.3",
         "webpack": "^5.38.1",
         "webpack-cli": "^4.7.2"
@@ -1120,15 +1122,6 @@
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
       "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
     },
-    "node_modules/at-least-node": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
-      "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
-      "dev": true,
-      "engines": {
-        "node": ">= 4.0.0"
-      }
-    },
     "node_modules/available-typed-arrays": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.4.tgz",
@@ -3326,21 +3319,6 @@
         "node": ">= 0.6"
       }
     },
-    "node_modules/fs-extra": {
-      "version": "9.1.0",
-      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
-      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
-      "dev": true,
-      "dependencies": {
-        "at-least-node": "^1.0.0",
-        "graceful-fs": "^4.2.0",
-        "jsonfile": "^6.0.1",
-        "universalify": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
     "node_modules/fs-minipass": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -3587,36 +3565,6 @@
         "node": ">=4.x"
       }
     },
-    "node_modules/handlebars": {
-      "version": "4.7.7",
-      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
-      "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==",
-      "dev": true,
-      "dependencies": {
-        "minimist": "^1.2.5",
-        "neo-async": "^2.6.0",
-        "source-map": "^0.6.1",
-        "wordwrap": "^1.0.0"
-      },
-      "bin": {
-        "handlebars": "bin/handlebars"
-      },
-      "engines": {
-        "node": ">=0.4.7"
-      },
-      "optionalDependencies": {
-        "uglify-js": "^3.1.4"
-      }
-    },
-    "node_modules/handlebars/node_modules/source-map": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/har-schema": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
@@ -4083,15 +4031,6 @@
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
     },
-    "node_modules/interpret": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
-      "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
-      "dev": true,
-      "engines": {
-        "node": ">= 0.10"
-      }
-    },
     "node_modules/ip-regex": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz",
@@ -4618,17 +4557,11 @@
         "node": ">=6"
       }
     },
-    "node_modules/jsonfile": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
-      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
-      "dev": true,
-      "dependencies": {
-        "universalify": "^2.0.0"
-      },
-      "optionalDependencies": {
-        "graceful-fs": "^4.1.6"
-      }
+    "node_modules/jsonc-parser": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz",
+      "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==",
+      "dev": true
     },
     "node_modules/jsprim": {
       "version": "1.4.1",
@@ -5004,15 +4937,15 @@
       "dev": true
     },
     "node_modules/marked": {
-      "version": "2.0.7",
-      "resolved": "https://registry.npmjs.org/marked/-/marked-2.0.7.tgz",
-      "integrity": "sha512-BJXxkuIfJchcXOJWTT2DOL+yFWifFv2yGYOUzvXg8Qz610QKw+sHCvTMYwA+qWGhlA2uivBezChZ/pBy1tWdkQ==",
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.8.tgz",
+      "integrity": "sha512-0gVrAjo5m0VZSJb4rpL59K1unJAMb/hm8HRXqasD8VeC8m91ytDPMritgFSlKonfdt+rRYYpP/JfLxgIX8yoSw==",
       "dev": true,
       "bin": {
         "marked": "bin/marked"
       },
       "engines": {
-        "node": ">= 8.16.2"
+        "node": ">= 12"
       }
     },
     "node_modules/media-typer": {
@@ -5709,30 +5642,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/onigasm": {
-      "version": "2.2.5",
-      "resolved": "https://registry.npmjs.org/onigasm/-/onigasm-2.2.5.tgz",
-      "integrity": "sha512-F+th54mPc0l1lp1ZcFMyL/jTs2Tlq4SqIHKIXGZOR/VkHkF9A7Fr5rRr5+ZG/lWeRsyrClLYRq7s/yFQ/XhWCA==",
-      "dev": true,
-      "dependencies": {
-        "lru-cache": "^5.1.1"
-      }
-    },
-    "node_modules/onigasm/node_modules/lru-cache": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
-      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
-      "dev": true,
-      "dependencies": {
-        "yallist": "^3.0.2"
-      }
-    },
-    "node_modules/onigasm/node_modules/yallist": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
-      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
-      "dev": true
-    },
     "node_modules/opencollective-postinstall": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
@@ -6257,18 +6166,6 @@
         "node": ">=8.10.0"
       }
     },
-    "node_modules/rechoir": {
-      "version": "0.6.2",
-      "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
-      "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
-      "dev": true,
-      "dependencies": {
-        "resolve": "^1.1.6"
-      },
-      "engines": {
-        "node": ">= 0.10"
-      }
-    },
     "node_modules/regenerator-runtime": {
       "version": "0.13.7",
       "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
@@ -6699,31 +6596,15 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/shelljs": {
-      "version": "0.8.4",
-      "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz",
-      "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==",
-      "dev": true,
-      "dependencies": {
-        "glob": "^7.0.0",
-        "interpret": "^1.0.0",
-        "rechoir": "^0.6.2"
-      },
-      "bin": {
-        "shjs": "bin/shjs"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/shiki": {
-      "version": "0.9.3",
-      "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.3.tgz",
-      "integrity": "sha512-NEjg1mVbAUrzRv2eIcUt3TG7X9svX7l3n3F5/3OdFq+/BxUdmBOeKGiH4icZJBLHy354Shnj6sfBTemea2e7XA==",
+      "version": "0.9.14",
+      "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.14.tgz",
+      "integrity": "sha512-uLHjjyJdNsMzF9GOF8vlOuZ8BwigiYPraMN5yjC826k8K7Xu90JQcC5GUNrzRibLgT2EOk9597I1IX+jRdA8nw==",
       "dev": true,
       "dependencies": {
-        "onigasm": "^2.2.5",
-        "vscode-textmate": "^5.2.0"
+        "jsonc-parser": "^3.0.0",
+        "vscode-oniguruma": "^1.6.1",
+        "vscode-textmate": "5.2.0"
       }
     },
     "node_modules/side-channel": {
@@ -7649,40 +7530,63 @@
       }
     },
     "node_modules/typedoc": {
-      "version": "0.20.36",
-      "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.20.36.tgz",
-      "integrity": "sha512-qFU+DWMV/hifQ9ZAlTjdFO9wbUIHuUBpNXzv68ZyURAP9pInjZiO4+jCPeAzHVcaBCHER9WL/+YzzTt6ZlN/Nw==",
+      "version": "0.22.10",
+      "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.10.tgz",
+      "integrity": "sha512-hQYZ4WtoMZ61wDC6w10kxA42+jclWngdmztNZsDvIz7BMJg7F2xnT+uYsUa7OluyKossdFj9E9Ye4QOZKTy8SA==",
       "dev": true,
       "dependencies": {
-        "colors": "^1.4.0",
-        "fs-extra": "^9.1.0",
-        "handlebars": "^4.7.7",
-        "lodash": "^4.17.21",
+        "glob": "^7.2.0",
         "lunr": "^2.3.9",
-        "marked": "^2.0.3",
-        "minimatch": "^3.0.0",
-        "progress": "^2.0.3",
-        "shelljs": "^0.8.4",
-        "shiki": "^0.9.3",
-        "typedoc-default-themes": "^0.12.10"
+        "marked": "^3.0.8",
+        "minimatch": "^3.0.4",
+        "shiki": "^0.9.12"
       },
       "bin": {
         "typedoc": "bin/typedoc"
       },
       "engines": {
-        "node": ">= 10.8.0"
+        "node": ">= 12.10.0"
       },
       "peerDependencies": {
-        "typescript": "3.9.x || 4.0.x || 4.1.x || 4.2.x"
+        "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x"
       }
     },
-    "node_modules/typedoc-default-themes": {
-      "version": "0.12.10",
-      "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz",
-      "integrity": "sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA==",
+    "node_modules/typedoc-plugin-missing-exports": {
+      "version": "0.22.6",
+      "resolved": "https://registry.npmjs.org/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-0.22.6.tgz",
+      "integrity": "sha512-1uguGQqa+c5f33nWS3v1mm0uAx4Ii1lw4Kx2zQksmYFKNEWTmrmMXbMNBoBg4wu0p4dFCNC7JIWPoRzpNS6pFA==",
       "dev": true,
+      "peerDependencies": {
+        "typedoc": "0.22.x"
+      }
+    },
+    "node_modules/typedoc-plugin-rename-defaults": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/typedoc-plugin-rename-defaults/-/typedoc-plugin-rename-defaults-0.4.0.tgz",
+      "integrity": "sha512-7m7MbNZKcxCA4Z+0MW06sFEmuQ+he1xfc+rLRtvvQcQypPTd3fgFmkEJKVu42t8mONC+kKS/WkbmqwzsiqI1YQ==",
+      "dev": true,
+      "peerDependencies": {
+        "typedoc": "0.22.x"
+      }
+    },
+    "node_modules/typedoc/node_modules/glob": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+      "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
       "engines": {
-        "node": ">= 8"
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
       }
     },
     "node_modules/typescript": {
@@ -7698,19 +7602,6 @@
         "node": ">=4.2.0"
       }
     },
-    "node_modules/uglify-js": {
-      "version": "3.13.9",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.9.tgz",
-      "integrity": "sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g==",
-      "dev": true,
-      "optional": true,
-      "bin": {
-        "uglifyjs": "bin/uglifyjs"
-      },
-      "engines": {
-        "node": ">=0.8.0"
-      }
-    },
     "node_modules/uid-safe": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
@@ -7744,15 +7635,6 @@
       "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==",
       "dev": true
     },
-    "node_modules/universalify": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
-      "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
-      "dev": true,
-      "engines": {
-        "node": ">= 10.0.0"
-      }
-    },
     "node_modules/unpipe": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -7869,10 +7751,16 @@
         "extsprintf": "^1.2.0"
       }
     },
+    "node_modules/vscode-oniguruma": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.1.tgz",
+      "integrity": "sha512-vc4WhSIaVpgJ0jJIejjYxPvURJavX6QG41vu0mGhqywMkQqulezEqEQ3cO3gc8GvcOpX6ycmKGqRoROEMBNXTQ==",
+      "dev": true
+    },
     "node_modules/vscode-textmate": {
-      "version": "5.4.0",
-      "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.4.0.tgz",
-      "integrity": "sha512-c0Q4zYZkcLizeYJ3hNyaVUM2AA8KDhNCA3JvXY8CeZSJuBdAy3bAvSbv46RClC4P3dSO9BdwhnKEx2zOo6vP/w==",
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz",
+      "integrity": "sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==",
       "dev": true
     },
     "node_modules/watchpack": {
@@ -8268,12 +8156,6 @@
         "node": ">=0.10.0"
       }
     },
-    "node_modules/wordwrap": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
-      "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
-      "dev": true
-    },
     "node_modules/workerpool": {
       "version": "6.1.4",
       "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.4.tgz",
@@ -9308,12 +9190,6 @@
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
       "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
     },
-    "at-least-node": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
-      "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
-      "dev": true
-    },
     "available-typed-arrays": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.4.tgz",
@@ -11037,18 +10913,6 @@
       "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
       "dev": true
     },
-    "fs-extra": {
-      "version": "9.1.0",
-      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
-      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
-      "dev": true,
-      "requires": {
-        "at-least-node": "^1.0.0",
-        "graceful-fs": "^4.2.0",
-        "jsonfile": "^6.0.1",
-        "universalify": "^2.0.0"
-      }
-    },
     "fs-minipass": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -11232,27 +11096,6 @@
       "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
       "dev": true
     },
-    "handlebars": {
-      "version": "4.7.7",
-      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
-      "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==",
-      "dev": true,
-      "requires": {
-        "minimist": "^1.2.5",
-        "neo-async": "^2.6.0",
-        "source-map": "^0.6.1",
-        "uglify-js": "^3.1.4",
-        "wordwrap": "^1.0.0"
-      },
-      "dependencies": {
-        "source-map": {
-          "version": "0.6.1",
-          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "dev": true
-        }
-      }
-    },
     "har-schema": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
@@ -11580,12 +11423,6 @@
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
     },
-    "interpret": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
-      "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
-      "dev": true
-    },
     "ip-regex": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz",
@@ -11968,15 +11805,11 @@
         "minimist": "^1.2.5"
       }
     },
-    "jsonfile": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
-      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
-      "dev": true,
-      "requires": {
-        "graceful-fs": "^4.1.6",
-        "universalify": "^2.0.0"
-      }
+    "jsonc-parser": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz",
+      "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==",
+      "dev": true
     },
     "jsprim": {
       "version": "1.4.1",
@@ -12280,9 +12113,9 @@
       "dev": true
     },
     "marked": {
-      "version": "2.0.7",
-      "resolved": "https://registry.npmjs.org/marked/-/marked-2.0.7.tgz",
-      "integrity": "sha512-BJXxkuIfJchcXOJWTT2DOL+yFWifFv2yGYOUzvXg8Qz610QKw+sHCvTMYwA+qWGhlA2uivBezChZ/pBy1tWdkQ==",
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.8.tgz",
+      "integrity": "sha512-0gVrAjo5m0VZSJb4rpL59K1unJAMb/hm8HRXqasD8VeC8m91ytDPMritgFSlKonfdt+rRYYpP/JfLxgIX8yoSw==",
       "dev": true
     },
     "media-typer": {
@@ -12791,32 +12624,6 @@
         "mimic-fn": "^2.1.0"
       }
     },
-    "onigasm": {
-      "version": "2.2.5",
-      "resolved": "https://registry.npmjs.org/onigasm/-/onigasm-2.2.5.tgz",
-      "integrity": "sha512-F+th54mPc0l1lp1ZcFMyL/jTs2Tlq4SqIHKIXGZOR/VkHkF9A7Fr5rRr5+ZG/lWeRsyrClLYRq7s/yFQ/XhWCA==",
-      "dev": true,
-      "requires": {
-        "lru-cache": "^5.1.1"
-      },
-      "dependencies": {
-        "lru-cache": {
-          "version": "5.1.1",
-          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
-          "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
-          "dev": true,
-          "requires": {
-            "yallist": "^3.0.2"
-          }
-        },
-        "yallist": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
-          "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
-          "dev": true
-        }
-      }
-    },
     "opencollective-postinstall": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
@@ -13211,15 +13018,6 @@
         "picomatch": "^2.2.1"
       }
     },
-    "rechoir": {
-      "version": "0.6.2",
-      "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
-      "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
-      "dev": true,
-      "requires": {
-        "resolve": "^1.1.6"
-      }
-    },
     "regenerator-runtime": {
       "version": "0.13.7",
       "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
@@ -13551,25 +13349,15 @@
       "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
       "dev": true
     },
-    "shelljs": {
-      "version": "0.8.4",
-      "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz",
-      "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==",
-      "dev": true,
-      "requires": {
-        "glob": "^7.0.0",
-        "interpret": "^1.0.0",
-        "rechoir": "^0.6.2"
-      }
-    },
     "shiki": {
-      "version": "0.9.3",
-      "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.3.tgz",
-      "integrity": "sha512-NEjg1mVbAUrzRv2eIcUt3TG7X9svX7l3n3F5/3OdFq+/BxUdmBOeKGiH4icZJBLHy354Shnj6sfBTemea2e7XA==",
+      "version": "0.9.14",
+      "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.14.tgz",
+      "integrity": "sha512-uLHjjyJdNsMzF9GOF8vlOuZ8BwigiYPraMN5yjC826k8K7Xu90JQcC5GUNrzRibLgT2EOk9597I1IX+jRdA8nw==",
       "dev": true,
       "requires": {
-        "onigasm": "^2.2.5",
-        "vscode-textmate": "^5.2.0"
+        "jsonc-parser": "^3.0.0",
+        "vscode-oniguruma": "^1.6.1",
+        "vscode-textmate": "5.2.0"
       }
     },
     "side-channel": {
@@ -14286,29 +14074,47 @@
       }
     },
     "typedoc": {
-      "version": "0.20.36",
-      "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.20.36.tgz",
-      "integrity": "sha512-qFU+DWMV/hifQ9ZAlTjdFO9wbUIHuUBpNXzv68ZyURAP9pInjZiO4+jCPeAzHVcaBCHER9WL/+YzzTt6ZlN/Nw==",
+      "version": "0.22.10",
+      "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.10.tgz",
+      "integrity": "sha512-hQYZ4WtoMZ61wDC6w10kxA42+jclWngdmztNZsDvIz7BMJg7F2xnT+uYsUa7OluyKossdFj9E9Ye4QOZKTy8SA==",
       "dev": true,
       "requires": {
-        "colors": "^1.4.0",
-        "fs-extra": "^9.1.0",
-        "handlebars": "^4.7.7",
-        "lodash": "^4.17.21",
+        "glob": "^7.2.0",
         "lunr": "^2.3.9",
-        "marked": "^2.0.3",
-        "minimatch": "^3.0.0",
-        "progress": "^2.0.3",
-        "shelljs": "^0.8.4",
-        "shiki": "^0.9.3",
-        "typedoc-default-themes": "^0.12.10"
+        "marked": "^3.0.8",
+        "minimatch": "^3.0.4",
+        "shiki": "^0.9.12"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+          "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        }
       }
     },
-    "typedoc-default-themes": {
-      "version": "0.12.10",
-      "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz",
-      "integrity": "sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA==",
-      "dev": true
+    "typedoc-plugin-missing-exports": {
+      "version": "0.22.6",
+      "resolved": "https://registry.npmjs.org/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-0.22.6.tgz",
+      "integrity": "sha512-1uguGQqa+c5f33nWS3v1mm0uAx4Ii1lw4Kx2zQksmYFKNEWTmrmMXbMNBoBg4wu0p4dFCNC7JIWPoRzpNS6pFA==",
+      "dev": true,
+      "requires": {}
+    },
+    "typedoc-plugin-rename-defaults": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/typedoc-plugin-rename-defaults/-/typedoc-plugin-rename-defaults-0.4.0.tgz",
+      "integrity": "sha512-7m7MbNZKcxCA4Z+0MW06sFEmuQ+he1xfc+rLRtvvQcQypPTd3fgFmkEJKVu42t8mONC+kKS/WkbmqwzsiqI1YQ==",
+      "dev": true,
+      "requires": {}
     },
     "typescript": {
       "version": "4.2.4",
@@ -14316,13 +14122,6 @@
       "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==",
       "dev": true
     },
-    "uglify-js": {
-      "version": "3.13.9",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.9.tgz",
-      "integrity": "sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g==",
-      "dev": true,
-      "optional": true
-    },
     "uid-safe": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
@@ -14350,12 +14149,6 @@
       "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==",
       "dev": true
     },
-    "universalify": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
-      "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
-      "dev": true
-    },
     "unpipe": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -14456,10 +14249,16 @@
         "extsprintf": "^1.2.0"
       }
     },
+    "vscode-oniguruma": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.1.tgz",
+      "integrity": "sha512-vc4WhSIaVpgJ0jJIejjYxPvURJavX6QG41vu0mGhqywMkQqulezEqEQ3cO3gc8GvcOpX6ycmKGqRoROEMBNXTQ==",
+      "dev": true
+    },
     "vscode-textmate": {
-      "version": "5.4.0",
-      "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.4.0.tgz",
-      "integrity": "sha512-c0Q4zYZkcLizeYJ3hNyaVUM2AA8KDhNCA3JvXY8CeZSJuBdAy3bAvSbv46RClC4P3dSO9BdwhnKEx2zOo6vP/w==",
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz",
+      "integrity": "sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==",
       "dev": true
     },
     "watchpack": {
@@ -14739,12 +14538,6 @@
       "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
       "dev": true
     },
-    "wordwrap": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
-      "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
-      "dev": true
-    },
     "workerpool": {
       "version": "6.1.4",
       "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.4.tgz",
diff --git a/package.json b/package.json
index 80bfc79..af48bdc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "algosdk",
-  "version": "1.13.0-beta.1",
+  "version": "1.13.0-beta.2",
   "description": "The official JavaScript SDK for Algorand",
   "main": "dist/cjs/index.js",
   "module": "dist/esm/index.js",
@@ -59,7 +59,9 @@
     "source-map-loader": "^2.0.2",
     "ts-loader": "^8.3.0",
     "ts-node": "^10.0.0",
-    "typedoc": "^0.20.36",
+    "typedoc": "^0.22.10",
+    "typedoc-plugin-missing-exports": "^0.22.6",
+    "typedoc-plugin-rename-defaults": "^0.4.0",
     "typescript": "^4.2.3",
     "webpack": "^5.38.1",
     "webpack-cli": "^4.7.2"
@@ -69,7 +71,7 @@
     "prepare": "npm run build",
     "prepare-browser-tests": "npm run build && mkdir -p tests/cucumber/browser/build && cp dist/browser/algosdk.min.* tests/cucumber/browser/build/ && webpack --config tests/cucumber/browser/webpack.config.js",
     "build": "concurrently \"webpack --config webpack.config.js\" \"tsc -p tsconfig-esm.json\" \"tsc -p tsconfig-cjs.json\"",
-    "docs": "typedoc src/main.ts",
+    "docs": "typedoc src/main.ts --options typedoc.config.json",
     "lint": "eslint .",
     "lint:fix": "eslint --fix .",
     "format": "prettier --write .",
diff --git a/src/abi/abi_type.ts b/src/abi/abi_type.ts
index 08c9021..4c0d632 100644
--- a/src/abi/abi_type.ts
+++ b/src/abi/abi_type.ts
@@ -43,7 +43,7 @@ export abstract class ABIType {
   // Converts a ABIType object to a string
   abstract toString(): string;
   // Checks if two ABIType objects are equal in value
-  abstract equal(other: ABIType): boolean;
+  abstract equals(other: ABIType): boolean;
   // Checks if the ABIType object (or any of its child types) have dynamic length
   abstract isDynamic(): boolean;
   // Returns the size of the ABIType object in bytes
@@ -139,10 +139,8 @@ export class ABIUintType extends ABIType {
     return `uint${this.bitSize}`;
   }
 
-  equal(other: ABIUintType) {
-    return (
-      this.constructor === other.constructor && this.bitSize === other.bitSize
-    );
+  equals(other: ABIType) {
+    return other instanceof ABIUintType && this.bitSize === other.bitSize;
   }
 
   isDynamic() {
@@ -153,12 +151,11 @@ export class ABIUintType extends ABIType {
     return this.bitSize / 8;
   }
 
-  encode(value: bigint | number) {
-    if (
-      (typeof value !== 'bigint' && typeof value !== 'number') ||
-      value >= BigInt(2 ** this.bitSize) ||
-      value < BigInt(0)
-    ) {
+  encode(value: ABIValue) {
+    if (typeof value !== 'bigint' && typeof value !== 'number') {
+      throw new Error(`Cannot encode value as uint${this.bitSize}: ${value}`);
+    }
+    if (value >= BigInt(2 ** this.bitSize) || value < BigInt(0)) {
       throw new Error(
         `${value} is not a non-negative int or too big to fit in size uint${this.bitSize}`
       );
@@ -171,7 +168,7 @@ export class ABIUintType extends ABIType {
     return bigIntToBytes(value, this.bitSize / 8);
   }
 
-  decode(byteString: Uint8Array) {
+  decode(byteString: Uint8Array): bigint {
     if (byteString.length !== this.bitSize / 8) {
       throw new Error(`byte string must correspond to a uint${this.bitSize}`);
     }
@@ -199,9 +196,9 @@ export class ABIUfixedType extends ABIType {
     return `ufixed${this.bitSize}x${this.precision}`;
   }
 
-  equal(other: ABIUfixedType) {
+  equals(other: ABIType) {
     return (
-      this.constructor === other.constructor &&
+      other instanceof ABIUfixedType &&
       this.bitSize === other.bitSize &&
       this.precision === other.precision
     );
@@ -215,14 +212,13 @@ export class ABIUfixedType extends ABIType {
     return this.bitSize / 8;
   }
 
-  encode(value: bigint | number) {
-    if (
-      (typeof value !== 'bigint' && typeof value !== 'number') ||
-      value >= BigInt(2 ** this.bitSize) ||
-      value < BigInt(0)
-    ) {
+  encode(value: ABIValue) {
+    if (typeof value !== 'bigint' && typeof value !== 'number') {
+      throw new Error(`Cannot encode value as ${this.toString()}: ${value}`);
+    }
+    if (value >= BigInt(2 ** this.bitSize) || value < BigInt(0)) {
       throw new Error(
-        `${value} is not a non-negative int or too big to fit in size ufixed${this.bitSize}`
+        `${value} is not a non-negative int or too big to fit in size ${this.toString()}`
       );
     }
     if (typeof value === 'number' && !Number.isSafeInteger(value)) {
@@ -233,9 +229,9 @@ export class ABIUfixedType extends ABIType {
     return bigIntToBytes(value, this.bitSize / 8);
   }
 
-  decode(byteString: Uint8Array) {
+  decode(byteString: Uint8Array): bigint {
     if (byteString.length !== this.bitSize / 8) {
-      throw new Error(`byte string must correspond to a ufixed${this.bitSize}`);
+      throw new Error(`byte string must correspond to a ${this.toString()}`);
     }
     return bytesToBigInt(byteString);
   }
@@ -246,8 +242,8 @@ export class ABIAddressType extends ABIType {
     return 'address';
   }
 
-  equal(other: ABIAddressType) {
-    return this.constructor === other.constructor;
+  equals(other: ABIType) {
+    return other instanceof ABIAddressType;
   }
 
   isDynamic() {
@@ -258,20 +254,23 @@ export class ABIAddressType extends ABIType {
     return ADDR_BYTE_SIZE;
   }
 
-  encode(value: string | Uint8Array) {
+  encode(value: ABIValue) {
+    if (typeof value !== 'string' && !(value instanceof Uint8Array)) {
+      throw new Error(`Cannot encode value as ${this.toString()}: ${value}`);
+    }
     if (typeof value === 'string') {
       const decodedAddress = decodeAddress(value);
       return decodedAddress.publicKey;
     }
     // Return the address if it is already in bytes
-    if (value.length !== 32) {
+    if (value.byteLength !== 32) {
       throw new Error(`byte string must be 32 bytes long for an address`);
     }
     return value;
   }
 
-  decode(byteString: Uint8Array) {
-    if (byteString.length !== 32) {
+  decode(byteString: Uint8Array): string {
+    if (byteString.byteLength !== 32) {
       throw new Error(`byte string must be 32 bytes long for an address`);
     }
     return encodeAddress(byteString);
@@ -283,8 +282,8 @@ export class ABIBoolType extends ABIType {
     return 'bool';
   }
 
-  equal(other: ABIBoolType) {
-    return this.constructor === other.constructor;
+  equals(other: ABIType) {
+    return other instanceof ABIBoolType;
   }
 
   isDynamic() {
@@ -295,15 +294,18 @@ export class ABIBoolType extends ABIType {
     return SINGLE_BOOL_SIZE;
   }
 
-  encode(value: boolean) {
+  encode(value: ABIValue) {
+    if (typeof value !== 'boolean') {
+      throw new Error(`Cannot encode value as bool: ${value}`);
+    }
     if (value) {
       return new Uint8Array([128]);
     }
     return new Uint8Array([0]);
   }
 
-  decode(byteString: Uint8Array) {
-    if (byteString.length !== 1) {
+  decode(byteString: Uint8Array): boolean {
+    if (byteString.byteLength !== 1) {
       throw new Error(`bool string must be 1 byte long`);
     }
     const value = byteString[0];
@@ -322,8 +324,8 @@ export class ABIByteType extends ABIType {
     return 'byte';
   }
 
-  equal(other: ABIByteType) {
-    return this.constructor === other.constructor;
+  equals(other: ABIType) {
+    return other instanceof ABIByteType;
   }
 
   isDynamic() {
@@ -334,15 +336,22 @@ export class ABIByteType extends ABIType {
     return SINGLE_BYTE_SIZE;
   }
 
-  encode(value: number) {
+  encode(value: ABIValue) {
+    if (typeof value !== 'number' && typeof value !== 'bigint') {
+      throw new Error(`Cannot encode value as byte: ${value}`);
+    }
+    if (typeof value === 'bigint') {
+      // eslint-disable-next-line no-param-reassign
+      value = Number(value);
+    }
     if (value < 0 || value > 255) {
       throw new Error(`${value} cannot be encoded into a byte`);
     }
     return new Uint8Array([value]);
   }
 
-  decode(byteString: Uint8Array) {
-    if (byteString.length !== 1) {
+  decode(byteString: Uint8Array): number {
+    if (byteString.byteLength !== 1) {
       throw new Error(`byte string must be 1 byte long`);
     }
     return byteString[0];
@@ -354,8 +363,8 @@ export class ABIStringType extends ABIType {
     return 'string';
   }
 
-  equal(other: ABIStringType) {
-    return this.constructor === other.constructor;
+  equals(other: ABIType) {
+    return other instanceof ABIStringType;
   }
 
   isDynamic() {
@@ -366,7 +375,10 @@ export class ABIStringType extends ABIType {
     throw new Error(`${this.toString()} is a dynamic type`);
   }
 
-  encode(value: string) {
+  encode(value: ABIValue) {
+    if (typeof value !== 'string' && !(value instanceof Uint8Array)) {
+      throw new Error(`Cannot encode value as string: ${value}`);
+    }
     const encodedBytes = Buffer.from(value);
     const encodedLength = bigIntToBytes(value.length, LENGTH_ENCODE_BYTE_SIZE);
     const mergedBytes = new Uint8Array(value.length + LENGTH_ENCODE_BYTE_SIZE);
@@ -375,9 +387,11 @@ export class ABIStringType extends ABIType {
     return mergedBytes;
   }
 
-  decode(byteString: Uint8Array) {
+  decode(byteString: Uint8Array): string {
     if (byteString.length < LENGTH_ENCODE_BYTE_SIZE) {
-      throw new Error(`byte string is too short to be decoded: ${byteString}`);
+      throw new Error(
+        `byte string is too short to be decoded. Actual length is ${byteString.length}, but expected at least ${LENGTH_ENCODE_BYTE_SIZE}`
+      );
     }
     const buf = Buffer.from(byteString);
     const byteLength = buf.readUIntBE(0, LENGTH_ENCODE_BYTE_SIZE);
@@ -387,11 +401,10 @@ export class ABIStringType extends ABIType {
     );
     if (byteLength !== byteValue.length) {
       throw new Error(
-        `string length bytes do not match the actual length of string: ${byteString}`
+        `string length bytes do not match the actual length of string. Expected ${byteLength}, got ${byteValue.length}`
       );
     }
-    const stringValue = Buffer.from(byteValue).toString('utf-8');
-    return stringValue;
+    return Buffer.from(byteValue).toString('utf-8');
   }
 }
 
@@ -414,11 +427,11 @@ export class ABIArrayStaticType extends ABIType {
     return `${this.childType.toString()}[${this.staticLength}]`;
   }
 
-  equal(other: ABIArrayStaticType) {
+  equals(other: ABIType) {
     return (
-      this.constructor === other.constructor &&
-      this.childType === other.childType &&
-      this.staticLength === other.staticLength
+      other instanceof ABIArrayStaticType &&
+      this.staticLength === other.staticLength &&
+      this.childType.equals(other.childType)
     );
   }
 
@@ -433,15 +446,20 @@ export class ABIArrayStaticType extends ABIType {
     return this.staticLength * this.childType.byteLen();
   }
 
-  encode(values: ABIValue[]) {
-    if (values.length !== this.staticLength) {
-      throw new Error(`value array does not match static array length`);
+  encode(value: ABIValue) {
+    if (!Array.isArray(value) && !(value instanceof Uint8Array)) {
+      throw new Error(`Cannot encode value as ${this.toString()}: ${value}`);
+    }
+    if (value.length !== this.staticLength) {
+      throw new Error(
+        `Value array does not match static array length. Expected ${this.staticLength}, got ${value.length}`
+      );
     }
     const convertedTuple = this.toABITupleType();
-    return convertedTuple.encode(values);
+    return convertedTuple.encode(value);
   }
 
-  decode(byteString: Uint8Array) {
+  decode(byteString: Uint8Array): ABIValue[] {
     const convertedTuple = this.toABITupleType();
     return convertedTuple.decode(byteString);
   }
@@ -463,10 +481,10 @@ export class ABIArrayDynamicType extends ABIType {
     return `${this.childType.toString()}[]`;
   }
 
-  equal(other: ABIArrayDynamicType) {
+  equals(other: ABIType) {
     return (
-      this.constructor === other.constructor &&
-      this.childType === other.childType
+      other instanceof ABIArrayDynamicType &&
+      this.childType.equals(other.childType)
     );
   }
 
@@ -478,9 +496,12 @@ export class ABIArrayDynamicType extends ABIType {
     throw new Error(`${this.toString()} is a dynamic type`);
   }
 
-  encode(values: ABIValue[]) {
-    const convertedTuple = this.toABITupleType(values.length);
-    const encodedTuple = convertedTuple.encode(values);
+  encode(value: ABIValue) {
+    if (!Array.isArray(value) && !(value instanceof Uint8Array)) {
+      throw new Error(`Cannot encode value as ${this.toString()}: ${value}`);
+    }
+    const convertedTuple = this.toABITupleType(value.length);
+    const encodedTuple = convertedTuple.encode(value);
     const encodedLength = bigIntToBytes(
       convertedTuple.childTypes.length,
       LENGTH_ENCODE_BYTE_SIZE
@@ -489,7 +510,7 @@ export class ABIArrayDynamicType extends ABIType {
     return mergedBytes;
   }
 
-  decode(byteString: Uint8Array) {
+  decode(byteString: Uint8Array): ABIValue[] {
     const buf = Buffer.from(byteString);
     const byteLength = buf.readUIntBE(0, LENGTH_ENCODE_BYTE_SIZE);
     const convertedTuple = this.toABITupleType(byteLength);
@@ -524,10 +545,13 @@ export class ABITupleType extends ABIType {
     return `(${typeStrings.join(',')})`;
   }
 
-  equal(other: ABITupleType) {
+  equals(other: ABIType) {
     return (
-      this.constructor === other.constructor &&
-      this.childTypes === other.childTypes
+      other instanceof ABITupleType &&
+      this.childTypes.length === other.childTypes.length &&
+      this.childTypes.every((child, index) =>
+        child.equals(other.childTypes[index])
+      )
     );
   }
 
@@ -552,8 +576,12 @@ export class ABITupleType extends ABIType {
     return size;
   }
 
-  encode(values: ABIValue[]) {
-    if (values.length > MAX_LEN) {
+  encode(value: ABIValue) {
+    if (!Array.isArray(value) && !(value instanceof Uint8Array)) {
+      throw new Error(`Cannot encode value as ${this.toString()}: ${value}`);
+    }
+    const values = Array.from(value);
+    if (value.length > MAX_LEN) {
       throw new Error('length of tuple array should not exceed a uint16');
     }
     const tupleTypes = this.childTypes;
@@ -620,7 +648,7 @@ export class ABITupleType extends ABIType {
     return concatArrays(...heads, ...tails);
   }
 
-  decode(byteString: Uint8Array) {
+  decode(byteString: Uint8Array): ABIValue[] {
     const tupleTypes = this.childTypes;
     const dynamicSegments: Segment[] = [];
     const valuePartition: Uint8Array[] = [];
diff --git a/src/abi/contract.ts b/src/abi/contract.ts
index 2df7be2..81b1150 100644
--- a/src/abi/contract.ts
+++ b/src/abi/contract.ts
@@ -1,34 +1,46 @@
 import { ABIMethod, ABIMethodParams } from './method';
 
+export interface ABIContractNetworkInfo {
+  appID: number;
+}
+
+export interface ABIContractNetworks {
+  [network: string]: ABIContractNetworkInfo;
+}
+
 export interface ABIContractParams {
   name: string;
-  appId: number;
+  desc?: string;
+  networks?: ABIContractNetworks;
   methods: ABIMethodParams[];
 }
 
 export class ABIContract {
   public readonly name: string;
-  public readonly appId: number;
+  public readonly description?: string;
+  public readonly networks: ABIContractNetworks;
   public readonly methods: ABIMethod[];
 
   constructor(params: ABIContractParams) {
     if (
       typeof params.name !== 'string' ||
-      typeof params.appId !== 'number' ||
-      !Array.isArray(params.methods)
+      !Array.isArray(params.methods) ||
+      (params.networks && typeof params.networks !== 'object')
     ) {
       throw new Error('Invalid ABIContract parameters');
     }
 
     this.name = params.name;
-    this.appId = params.appId;
+    this.description = params.desc;
+    this.networks = params.networks ? { ...params.networks } : {};
     this.methods = params.methods.map((method) => new ABIMethod(method));
   }
 
   toJSON(): ABIContractParams {
     return {
       name: this.name,
-      appId: this.appId,
+      desc: this.description,
+      networks: this.networks,
       methods: this.methods.map((method) => method.toJSON()),
     };
   }
diff --git a/src/abi/index.ts b/src/abi/index.ts
index 2c24cf8..01c18da 100644
--- a/src/abi/index.ts
+++ b/src/abi/index.ts
@@ -2,3 +2,5 @@ export * from './abi_type';
 export * from './contract';
 export * from './interface';
 export * from './method';
+export * from './transaction';
+export * from './reference';
diff --git a/src/abi/interface.ts b/src/abi/interface.ts
index 1e6dc28..3959515 100644
--- a/src/abi/interface.ts
+++ b/src/abi/interface.ts
@@ -2,11 +2,13 @@ import { ABIMethod, ABIMethodParams } from './method';
 
 export interface ABIInterfaceParams {
   name: string;
+  desc?: string;
   methods: ABIMethodParams[];
 }
 
 export class ABIInterface {
   public readonly name: string;
+  public readonly description?: string;
   public readonly methods: ABIMethod[];
 
   constructor(params: ABIInterfaceParams) {
@@ -15,12 +17,14 @@ export class ABIInterface {
     }
 
     this.name = params.name;
+    this.description = params.desc;
     this.methods = params.methods.map((method) => new ABIMethod(method));
   }
 
   toJSON(): ABIInterfaceParams {
     return {
       name: this.name,
+      desc: this.description,
       methods: this.methods.map((method) => method.toJSON()),
     };
   }
diff --git a/src/abi/method.ts b/src/abi/method.ts
index 0c925d7..ecf7520 100644
--- a/src/abi/method.ts
+++ b/src/abi/method.ts
@@ -1,10 +1,7 @@
 import { genericHash } from '../nacl/naclWrappers';
-import { TransactionType } from '../types/transactions/base';
 import { ABIType, ABITupleType } from './abi_type';
-
-export function abiTypeIsTransaction(type: string): type is TransactionType {
-  return type === 'txn' || Object.keys(TransactionType).includes(type);
-}
+import { ABITransactionType, abiTypeIsTransaction } from './transaction';
+import { ABIReferenceType, abiTypeIsReference } from './reference';
 
 function parseMethodSignature(
   signature: string
@@ -48,14 +45,25 @@ function parseMethodSignature(
   };
 }
 
+export interface ABIMethodArgParams {
+  type: string;
+  name?: string;
+  desc?: string;
+}
+
+export interface ABIMethodReturnParams {
+  type: string;
+  desc?: string;
+}
+
 export interface ABIMethodParams {
   name: string;
   desc?: string;
-  args: Array<{ type: string; name?: string; desc?: string }>;
-  returns: { type: string; desc?: string };
+  args: ABIMethodArgParams[];
+  returns: ABIMethodReturnParams;
 }
 
-export type ABIArgumentType = ABIType | TransactionType;
+export type ABIArgumentType = ABIType | ABITransactionType | ABIReferenceType;
 
 export type ABIReturnType = ABIType | 'void';
 
@@ -82,7 +90,7 @@ export class ABIMethod {
     this.name = params.name;
     this.description = params.desc;
     this.args = params.args.map(({ type, name, desc }) => {
-      if (abiTypeIsTransaction(type)) {
+      if (abiTypeIsTransaction(type) || abiTypeIsReference(type)) {
         return {
           type,
           name,
diff --git a/src/abi/reference.ts b/src/abi/reference.ts
new file mode 100644
index 0000000..c3e0234
--- /dev/null
+++ b/src/abi/reference.ts
@@ -0,0 +1,24 @@
+export enum ABIReferenceType {
+  /**
+   * Account reference type
+   */
+  account = 'account',
+
+  /**
+   * Application reference type
+   */
+  application = 'application',
+
+  /**
+   * Asset reference type
+   */
+  asset = 'asset',
+}
+
+export function abiTypeIsReference(type: any): type is ABIReferenceType {
+  return (
+    type === ABIReferenceType.account ||
+    type === ABIReferenceType.application ||
+    type === ABIReferenceType.asset
+  );
+}
diff --git a/src/abi/transaction.ts b/src/abi/transaction.ts
new file mode 100644
index 0000000..880250f
--- /dev/null
+++ b/src/abi/transaction.ts
@@ -0,0 +1,61 @@
+import { Transaction } from '../transaction';
+
+export enum ABITransactionType {
+  /**
+   * Any transaction type
+   */
+  any = 'txn',
+
+  /**
+   * Payment transaction type
+   */
+  pay = 'pay',
+
+  /**
+   * Key registration transaction type
+   */
+  keyreg = 'keyreg',
+
+  /**
+   * Asset configuration transaction type
+   */
+  acfg = 'acfg',
+
+  /**
+   * Asset transfer transaction type
+   */
+  axfer = 'axfer',
+
+  /**
+   * Asset freeze transaction type
+   */
+  afrz = 'afrz',
+
+  /**
+   * Application transaction type
+   */
+  appl = 'appl',
+}
+
+export function abiTypeIsTransaction(type: any): type is ABITransactionType {
+  return (
+    type === ABITransactionType.any ||
+    type === ABITransactionType.pay ||
+    type === ABITransactionType.keyreg ||
+    type === ABITransactionType.acfg ||
+    type === ABITransactionType.axfer ||
+    type === ABITransactionType.afrz ||
+    type === ABITransactionType.appl
+  );
+}
+
+export function abiCheckTransactionType(
+  type: ABITransactionType,
+  txn: Transaction
+): boolean {
+  if (type === ABITransactionType.any) {
+    return true;
+  }
+
+  return txn.type && txn.type.toString() === type.toString();
+}
diff --git a/src/client/baseHTTPClient.ts b/src/client/baseHTTPClient.ts
new file mode 100644
index 0000000..39b1af6
--- /dev/null
+++ b/src/client/baseHTTPClient.ts
@@ -0,0 +1,58 @@
+export type Query<F> = {
+  format?: F;
+  [key: string]: any;
+};
+
+export interface BaseHTTPClientResponse {
+  body: Uint8Array;
+  status: number; // status must always be 200 except when the response is inside an error
+  headers: Record<string, string>;
+}
+
+/**
+ * BaseHTTPClientError is the interface that errors thrown
+ * by methods of BaseHTTPClient should be using
+ */
+export interface BaseHTTPClientError {
+  response: BaseHTTPClientResponse;
+}
+
+/**
+ * BaseHTTPClient is an interface abstracting the queries that can be
+ * made to an algod/indexer endpoint.
+ * The SDK normally uses the URLTokenBaseHTTPClient implementation.
+ * But when used via wallets, the wallet may provide a different object
+ * satisfying the HTTPClient interface. This is useful to allow
+ * wallets to provide access to paid API services without leaking
+ * the secret tokens/URLs.
+ *
+ * Note that post and delete also have an optional query parameter
+ * This is to allow future extension where post and delete may have queries
+ * Currently however HTTPClient does not make use of it
+ *
+ * Compared to HTTPClient, BaseHTTPClient does not deal with serialization/deserialization
+ * Everything is already string/Uint8Array
+ * and all the headers (including Accept/Content-Type) are assumed to be provided
+ *
+ * In case of non-200 status, all methods must throw an error of type
+ * BaseHTTPClientError
+ */
+export interface BaseHTTPClient {
+  get(
+    relativePath: string,
+    query?: Query<string>,
+    requestHeaders?: Record<string, string>
+  ): Promise<BaseHTTPClientResponse>;
+  post(
+    relativePath: string,
+    data: Uint8Array,
+    query?: Query<string>,
+    requestHeaders?: Record<string, string>
+  ): Promise<BaseHTTPClientResponse>;
+  delete(
+    relativePath: string,
+    data: Uint8Array,
+    query?: Query<string>,
+    requestHeaders?: Record<string, string>
+  ): Promise<BaseHTTPClientResponse>;
+}
diff --git a/src/client/client.ts b/src/client/client.ts
index 20f267d..170b487 100644
--- a/src/client/client.ts
+++ b/src/client/client.ts
@@ -1,54 +1,26 @@
-import path from 'path';
-import * as request from 'superagent';
-import Url from 'url-parse';
 import * as utils from '../utils/utils';
-import IntDecoding from '../types/intDecoding';
+import {
+  BaseHTTPClient,
+  BaseHTTPClientResponse,
+  Query,
+} from './baseHTTPClient';
+import { TokenHeader, URLTokenBaseHTTPClient } from './urlTokenBaseHTTPClient';
 
 interface ErrorWithAdditionalInfo extends Error {
   rawResponse: string | null;
   statusCode: number;
 }
 
-function createJSONParser(options: utils.JSONOptions) {
-  return (
-    res: request.Response,
-    // eslint-disable-next-line no-unused-vars
-    fnOrStr: string | ((err: Error, obj: any) => void)
-    // eslint-disable-next-line consistent-return
-  ) => {
-    if (typeof fnOrStr === 'string') {
-      // in browser
-      return fnOrStr && utils.parseJSON(fnOrStr, options);
-    }
-
-    // in node
-    // based off https://github.com/visionmedia/superagent/blob/1277a880c32191e300b229e352e0633e421046c8/src/node/parsers/json.js
-    res.text = '';
-    res.setEncoding('utf8');
-    res.on('data', (chunk) => {
-      res.text += chunk;
-    });
-    res.on('end', () => {
-      let body: any;
-      let err: ErrorWithAdditionalInfo | null;
-      try {
-        body = res.text && utils.parseJSON(res.text, options);
-      } catch (err_) {
-        err = err_;
-        // issue #675: return the raw response if the response parsing fails
-        err.rawResponse = res.text || null;
-        // issue #876: return the http status code if the response parsing fails
-        err.statusCode = res.status;
-      } finally {
-        fnOrStr(err, body);
-      }
-    });
-  };
+export interface HTTPClientResponse {
+  body: Uint8Array | any; // when content-type=JSON, body is a JSON object, otherwise it's a Uint8Array
+  text?: string;
+  headers: Record<string, string>;
+  status: number;
+  ok: boolean;
 }
 
 /**
  * Remove falsy values or values with a length of 0 from an object.
- * @param obj
  */
 function removeFalsyOrEmpty(obj: Record<string, any>) {
   for (const key in obj) {
@@ -60,15 +32,20 @@ function removeFalsyOrEmpty(obj: Record<string, any>) {
   return obj;
 }
 
-type Query<F> = {
-  format?: F;
-  [key: string]: any;
-};
+/**
+ * Create a new object with lower-case keys
+ * See https://codereview.stackexchange.com/a/162418
+ * Used to ensure all headers are lower-case and to work more easily with them
+ */
+function tolowerCaseKeys(o: object): object {
+  /* eslint-disable no-param-reassign,no-return-assign,no-sequences */
+  return Object.keys(o).reduce((c, k) => ((c[k.toLowerCase()] = o[k]), c), {});
+  /* eslint-enable no-param-reassign,no-return-assign,no-sequences */
+}
 
 /**
  * getAcceptFormat returns the correct Accept header depending on the
  * requested format.
- * @param query
  */
 function getAcceptFormat(
   query?: Query<'msgpack' | 'json'>
@@ -87,64 +64,173 @@ function getAcceptFormat(
   } else return 'application/json';
 }
 
-export interface AlgodTokenHeader {
-  'X-Algo-API-Token': string;
-}
-
-export interface IndexerTokenHeader {
-  'X-Indexer-API-Token': string;
-}
-
-export interface KMDTokenHeader {
-  'X-KMD-API-Token': string;
+/**
+ * Check is a response is JSON or not
+ * Inspired from superagent code
+ */
+function isResponseJSON(res: BaseHTTPClientResponse): boolean {
+  let contentType = tolowerCaseKeys(res.headers)['content-type'];
+  if (contentType) {
+    /* eslint-disable prefer-destructuring */
+    contentType = contentType.split(';')[0];
+    /* eslint-enable prefer-destructuring */
+  }
+  // regex should match /json or +json
+  // but not /json-seq
+  // from https://github.com/visionmedia/superagent/blob/048cf185d954028b1dccde0717d2488b2284c297/src/client.js#L276
+  return /[/+]json($|[^-\w])/i.test(contentType);
 }
 
-export interface CustomTokenHeader {
-  [headerName: string]: string;
+/**
+ * Check is a response is text
+ * Inspired from superagent code
+ */
+function isResponseText(res: BaseHTTPClientResponse): boolean {
+  const contentType =
+    tolowerCaseKeys(res.headers)['content-type'] || 'text/plain';
+  return /^\w*text\//i.test(contentType);
 }
 
-export type TokenHeader =
-  | AlgodTokenHeader
-  | IndexerTokenHeader
-  | KMDTokenHeader
-  | CustomTokenHeader;
-
+/**
+ * HTTPClient is a wrapper around a BaseHTTPClient
+ * It takes care of setting the proper "Accept" header and of
+ * decoding the JSON outputs.
+ */
 export default class HTTPClient {
-  private baseURL: Url;
-  private tokenHeader: TokenHeader;
-  public intDecoding: IntDecoding = IntDecoding.DEFAULT;
+  private bc: BaseHTTPClient;
 
+  /**
+   * Construct an HTTPClient from a BaseHTTPClient
+   * @param bc - the BaseHTTPClient used
+   */
+  constructor(bc: BaseHTTPClient);
+  /**
+   * Construct an HTTPClient from a URL (baseServer+port) and a token
+   */
   constructor(
     tokenHeader: TokenHeader,
     baseServer: string,
     port?: string | number,
-    private defaultHeaders: Record<string, any> = {}
+    defaultHeaders?: Record<string, string>
+  );
+
+  constructor(
+    bcOrTokenHeader: BaseHTTPClient | TokenHeader,
+    baseServer?: string,
+    port?: string | number,
+    defaultHeaders: Record<string, string> = {}
   ) {
-    const baseServerURL = new Url(baseServer, {});
-    if (typeof port !== 'undefined') {
-      baseServerURL.set('port', port.toString());
+    if (baseServer !== undefined) {
+      this.bc = new URLTokenBaseHTTPClient(
+        bcOrTokenHeader as TokenHeader,
+        baseServer,
+        port,
+        defaultHeaders
+      );
+    } else {
+      this.bc = bcOrTokenHeader as BaseHTTPClient;
     }
+  }
 
-    if (baseServerURL.protocol.length === 0) {
-      throw new Error('Invalid base server URL, protocol must be defined.');
+  /**
+   * Parse JSON using either the built-in JSON.parse or utils.parseJSON
+   * depending on whether jsonOptions are provided or not
+   *
+   * @param text JSON data
+   * @param status Status of the response (used in case parseJSON fails)
+   * @param jsonOptions Options object to use to decode JSON responses. See
+   *   utils.parseJSON for the options available.
+   * @private
+   */
+  private static parseJSON(
+    text: string,
+    status: number,
+    jsonOptions: utils.JSONOptions = {}
+  ) {
+    try {
+      if (Object.keys(jsonOptions).length !== 0) {
+        return text && JSON.parse(text);
+      }
+      return text && utils.parseJSON(text, jsonOptions);
+    } catch (err_) {
+      const err: ErrorWithAdditionalInfo = err_;
+      // return the raw response if the response parsing fails
+      err.rawResponse = text || null;
+      // return the http status code if the response parsing fails
+      err.statusCode = status;
+      throw err;
     }
-
-    this.baseURL = baseServerURL;
-    this.defaultHeaders = defaultHeaders;
-    this.tokenHeader = tokenHeader;
   }
 
   /**
-   * Compute the URL for a path relative to the instance's address
-   * @param relativePath - A path string
-   * @returns A URL string
+   * Serialize the data according to the requestHeaders
+   * Assumes that requestHeaders contain a key "content-type"
+   * If the content-type is "application/json", data is JSON serialized
+   * Otherwise, data needs to be either an UTF-8 string that is converted to an Uint8Array
+   * or an Uint8Array
+   * @private
    */
-  private addressWithPath(relativePath: string) {
-    const address = new Url(
-      path.posix.join(this.baseURL.pathname, relativePath),
-      this.baseURL
+  private static serializeData(
+    data: object,
+    requestHeaders: Record<string, string>
+  ): Uint8Array {
+    if (!data) {
+      return new Uint8Array(0); // empty Uint8Array
+    }
+    if (requestHeaders['content-type'] === 'application/json') {
+      return new Uint8Array(Buffer.from(JSON.stringify(data)));
+    }
+    if (typeof data === 'string') {
+      return new Uint8Array(Buffer.from(data));
+    }
+    if (data instanceof Uint8Array) {
+      return data;
+    }
+    throw new Error(
+      'provided data is neither a string nor a Uint8Array and content-type is not application/json'
     );
-    return address.toString();
+  }
+
+  /**
+   * Convert a BaseHTTPClientResponse into a full HTTPClientResponse
+   * Parse the body in
+   * Modifies in place res and return the result
+   */
+  private static prepareResponse(
+    res: BaseHTTPClientResponse,
+    jsonOptions: utils.JSONOptions = {}
+  ): HTTPClientResponse {
+    let { body } = res;
+    let text;
+    if (isResponseJSON(res)) {
+      text = (body && new TextDecoder().decode(body)) || '';
+      body = HTTPClient.parseJSON(text, res.status, jsonOptions);
+    } else if (isResponseText(res)) {
+      text = (body && new TextDecoder().decode(body)) || '';
+    }
+
+    return {
+      ...res,
+      body,
+      text,
+      ok: Math.trunc(res.status / 100) === 2,
+    };
+  }
+
+  /**
+   * Prepare an error with a response
+   * (the type of errors BaseHTTPClient are supposed to throw)
+   * by adding the status and preparing the internal response
+   * @private
+   */
+  private static prepareResponseError(err) {
+    if (err.response) {
+      // eslint-disable-next-line no-param-reassign
+      err.response = HTTPClient.prepareResponse(err.response);
+      // eslint-disable-next-line no-param-reassign
+      err.status = err.response.status;
+    }
+    return err;
   }
 
   /**
@@ -159,66 +245,76 @@ export default class HTTPClient {
   async get(
     relativePath: string,
     query?: Query<any>,
-    requestHeaders: Record<string, any> = {},
+    requestHeaders: Record<string, string> = {},
     jsonOptions: utils.JSONOptions = {}
-  ) {
+  ): Promise<HTTPClientResponse> {
     const format = getAcceptFormat(query);
-    let r = request
-      .get(this.addressWithPath(relativePath))
-      .set(this.tokenHeader)
-      .set(this.defaultHeaders)
-      .set(requestHeaders)
-      .set('Accept', format)
-      .query(removeFalsyOrEmpty(query));
-
-    if (format === 'application/msgpack') {
-      r = r.responseType('arraybuffer');
-    } else if (
-      format === 'application/json' &&
-      Object.keys(jsonOptions).length !== 0
-    ) {
-      if (utils.isNode()) {
-        // in node, need to set buffer
-        r = r.buffer(true);
-      }
-      r = r.parse(createJSONParser(jsonOptions));
-    }
+    const fullHeaders = { ...requestHeaders, accept: format };
 
-    const res = await r;
-    if (Buffer.isBuffer(res.body)) {
-      // In node res.body will be a Buffer, but in the browser it will be an ArrayBuffer
-      // (thanks superagent...), so convert it to an ArrayBuffer for consistency.
-      const underlyingArrayBuffer = res.body.buffer;
-      const start = res.body.byteOffset;
-      const end = start + res.body.byteLength;
-      res.body = underlyingArrayBuffer.slice(start, end);
+    try {
+      const res = await this.bc.get(
+        relativePath,
+        removeFalsyOrEmpty(query),
+        fullHeaders
+      );
+
+      return HTTPClient.prepareResponse(res, jsonOptions);
+    } catch (err) {
+      throw HTTPClient.prepareResponseError(err);
     }
-    return res;
   }
 
+  /**
+   * Send a POST request.
+   * If no content-type present, adds the header "content-type: application/json"
+   * and data is serialized in JSON (if not empty)
+   */
   async post(
     relativePath: string,
-    data: string | object,
-    requestHeaders: Record<string, any> = {}
-  ) {
-    return request
-      .post(this.addressWithPath(relativePath))
-      .set(this.tokenHeader)
-      .set(this.defaultHeaders)
-      .set(requestHeaders)
-      .send(data);
+    data: any,
+    requestHeaders: Record<string, string> = {}
+  ): Promise<HTTPClientResponse> {
+    const fullHeaders = {
+      'content-type': 'application/json',
+      ...tolowerCaseKeys(requestHeaders),
+    };
+
+    try {
+      const res = await this.bc.post(
+        relativePath,
+        HTTPClient.serializeData(data, fullHeaders),
+        undefined,
+        fullHeaders
+      );
+
+      return HTTPClient.prepareResponse(res);
+    } catch (err) {
+      throw HTTPClient.prepareResponseError(err);
+    }
   }
 
+  /**
+   * Send a DELETE request.
+   * If no content-type present, adds the header "content-type: application/json"
+   * and data is serialized in JSON (if not empty)
+   */
   async delete(
     relativePath: string,
-    data: string | object,
-    requestHeaders: Record<string, any> = {}
+    data: any,
+    requestHeaders: Record<string, string> = {}
   ) {
-    return request
-      .delete(this.addressWithPath(relativePath))
-      .set(this.tokenHeader)
-      .set(this.defaultHeaders)
-      .set(requestHeaders)
-      .send(data);
+    const fullHeaders = {
+      'content-type': 'application/json',
+      ...tolowerCaseKeys(requestHeaders),
+    };
+
+    const res = await this.bc.delete(
+      relativePath,
+      HTTPClient.serializeData(data, fullHeaders),
+      undefined,
+      fullHeaders
+    );
+
+    return HTTPClient.prepareResponse(res);
   }
 }
diff --git a/src/client/kmd.ts b/src/client/kmd.ts
index 82ba9fe..86f8884 100644
--- a/src/client/kmd.ts
+++ b/src/client/kmd.ts
@@ -1,6 +1,6 @@
 import ServiceClient from './v2/serviceClient';
 import * as txn from '../transaction';
-import { CustomTokenHeader, KMDTokenHeader } from './client';
+import { CustomTokenHeader, KMDTokenHeader } from './urlTokenBaseHTTPClient';
 
 export default class Kmd extends ServiceClient {
   constructor(
diff --git a/src/client/urlTokenBaseHTTPClient.ts b/src/client/urlTokenBaseHTTPClient.ts
new file mode 100644
index 0000000..f54ef8b
--- /dev/null
+++ b/src/client/urlTokenBaseHTTPClient.ts
@@ -0,0 +1,172 @@
+import Url from 'url-parse';
+import path from 'path';
+import * as request from 'superagent';
+import {
+  BaseHTTPClient,
+  BaseHTTPClientResponse,
+  Query,
+} from './baseHTTPClient';
+
+export interface AlgodTokenHeader {
+  'X-Algo-API-Token': string;
+}
+
+export interface IndexerTokenHeader {
+  'X-Indexer-API-Token': string;
+}
+
+export interface KMDTokenHeader {
+  'X-KMD-API-Token': string;
+}
+
+export interface CustomTokenHeader {
+  [headerName: string]: string;
+}
+
+export type TokenHeader =
+  | AlgodTokenHeader
+  | IndexerTokenHeader
+  | KMDTokenHeader
+  | CustomTokenHeader;
+
+/**
+ * Implementation of BaseHTTPClient that uses a URL and a token
+ * and make the REST queries using superagent.
+ * This is the default implementation of BaseHTTPClient.
+ */
+export class URLTokenBaseHTTPClient implements BaseHTTPClient {
+  private readonly baseURL: Url;
+  private readonly tokenHeader: TokenHeader;
+
+  constructor(
+    tokenHeader: TokenHeader,
+    baseServer: string,
+    port?: string | number,
+    private defaultHeaders: Record<string, any> = {}
+  ) {
+    const baseServerURL = new Url(baseServer, {});
+    if (typeof port !== 'undefined') {
+      baseServerURL.set('port', port.toString());
+    }
+
+    if (baseServerURL.protocol.length === 0) {
+      throw new Error('Invalid base server URL, protocol must be defined.');
+    }
+
+    this.baseURL = baseServerURL;
+    this.tokenHeader = tokenHeader;
+  }
+
+  /**
+   * Compute the URL for a path relative to the instance's address
+   * @param relativePath - A path string
+   * @returns A URL string
+   */
+  private addressWithPath(relativePath: string) {
+    const address = new Url(
+      path.posix.join(this.baseURL.pathname, relativePath),
+      this.baseURL
+    );
+    return address.toString();
+  }
+
+  /**
+   * Convert a superagent response to a valid BaseHTTPClientResponse
+   * Modify the superagent response
+   * @private
+   */
+  private static superagentToHTTPClientResponse(
+    res: request.Response
+  ): BaseHTTPClientResponse {
+    if (res.body instanceof ArrayBuffer) {
+      // Handle the case where the body is an arraybuffer which happens in the browser
+      res.body = new Uint8Array(res.body);
+    }
+    return res;
+  }
+
+  /**
+   * Make a superagent error more readable. For more info, see https://github.com/visionmedia/superagent/issues/1074
+   */
+  private static formatSuperagentError(err: any): Error {
+    if (err.response) {
+      try {
+        const decoded = JSON.parse(Buffer.from(err.response.body).toString());
+        // eslint-disable-next-line no-param-reassign
+        err.message = `Network request error. Received status ${err.response.status}: ${decoded.message}`;
+      } catch (err2) {
+        // ignore any error that happened while we are formatting the original error
+      }
+    }
+    return err;
+  }
+
+  async get(
+    relativePath: string,
+    query?: Query<string>,
+    requestHeaders: Record<string, string> = {}
+  ): Promise<BaseHTTPClientResponse> {
+    const r = request
+      .get(this.addressWithPath(relativePath))
+      .set(this.tokenHeader)
+      .set(this.defaultHeaders)
+      .set(requestHeaders)
+      .responseType('arraybuffer')
+      .query(query);
+
+    try {
+      const res = await r;
+      return URLTokenBaseHTTPClient.superagentToHTTPClientResponse(res);
+    } catch (err) {
+      throw URLTokenBaseHTTPClient.formatSuperagentError(err);
+    }
+  }
+
+  async post(
+    relativePath: string,
+    data: Uint8Array,
+    query?: Query<string>,
+    requestHeaders: Record<string, string> = {}
+  ): Promise<BaseHTTPClientResponse> {
+    const r = request
+      .post(this.addressWithPath(relativePath))
+      .set(this.tokenHeader)
+      .set(this.defaultHeaders)
+      .set(requestHeaders)
+      .query(query)
+      .serialize((o) => o) // disable serialization from superagent
+      .responseType('arraybuffer')
+      .send(Buffer.from(data)); // Buffer.from necessary for superagent
+
+    try {
+      const res = await r;
+      return URLTokenBaseHTTPClient.superagentToHTTPClientResponse(res);
+    } catch (err) {
+      throw URLTokenBaseHTTPClient.formatSuperagentError(err);
+    }
+  }
+
+  async delete(
+    relativePath: string,
+    data: Uint8Array,
+    query?: Query<string>,
+    requestHeaders: Record<string, string> = {}
+  ): Promise<BaseHTTPClientResponse> {
+    const r = request
+      .delete(this.addressWithPath(relativePath))
+      .set(this.tokenHeader)
+      .set(this.defaultHeaders)
+      .set(requestHeaders)
+      .query(query)
+      .serialize((o) => o) // disable serialization from superagent
+      .responseType('arraybuffer')
+      .send(Buffer.from(data)); // Buffer.from necessary for superagent
+
+    try {
+      const res = await r;
+      return URLTokenBaseHTTPClient.superagentToHTTPClientResponse(res);
+    } catch (err) {
+      throw URLTokenBaseHTTPClient.formatSuperagentError(err);
+    }
+  }
+}
diff --git a/src/client/v2/algod/algod.ts b/src/client/v2/algod/algod.ts
index 3bb682d..1ebcdc9 100644
--- a/src/client/v2/algod/algod.ts
+++ b/src/client/v2/algod/algod.ts
@@ -1,5 +1,4 @@
 import ServiceClient from '../serviceClient';
-import { AlgodTokenHeader, CustomTokenHeader } from '../../client';
 import * as modelsv2 from './models/types';
 import AccountInformation from './accountInformation';
 import Block from './block';
@@ -19,15 +18,29 @@ 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';
 
 export default class AlgodClient extends ServiceClient {
+  /**
+   * Create an AlgodClient from
+   * * either a token, baseServer, port, and optional headers
+   * * or a base client server for interoperability with external dApp wallets
+   */
   constructor(
-    token: string | AlgodTokenHeader | CustomTokenHeader,
+    tokenOrBaseClient:
+      | string
+      | AlgodTokenHeader
+      | CustomTokenHeader
+      | BaseHTTPClient,
     baseServer = 'http://r2.algorand.network',
     port: string | number = 4180,
-    headers = {}
+    headers: Record<string, string> = {}
   ) {
-    super('X-Algo-API-Token', token, baseServer, port, headers);
+    super('X-Algo-API-Token', tokenOrBaseClient, baseServer, port, headers);
   }
 
   healthCheck() {
diff --git a/src/client/v2/indexer/indexer.ts b/src/client/v2/indexer/indexer.ts
index 547dec3..d98f3fa 100644
--- a/src/client/v2/indexer/indexer.ts
+++ b/src/client/v2/indexer/indexer.ts
@@ -1,5 +1,4 @@
 import ServiceClient from '../serviceClient';
-import { CustomTokenHeader, IndexerTokenHeader } from '../../client';
 import MakeHealthCheck from './makeHealthCheck';
 import LookupAssetBalances from './lookupAssetBalances';
 import LookupAssetTransactions from './lookupAssetTransactions';
@@ -14,15 +13,29 @@ import SearchAccounts from './searchAccounts';
 import SearchForTransactions from './searchForTransactions';
 import SearchForAssets from './searchForAssets';
 import SearchForApplications from './searchForApplications';
+import { BaseHTTPClient } from '../../baseHTTPClient';
+import {
+  CustomTokenHeader,
+  IndexerTokenHeader,
+} from '../../urlTokenBaseHTTPClient';
 
 export default class IndexerClient extends ServiceClient {
+  /**
+   * Create an IndexerClient from
+   * * either a token, baseServer, port, and optional headers
+   * * or a base client server for interoperability with external dApp wallets
+   */
   constructor(
-    token: string | IndexerTokenHeader | CustomTokenHeader,
+    tokenOrBaseClient:
+      | string
+      | IndexerTokenHeader
+      | CustomTokenHeader
+      | BaseHTTPClient,
     baseServer = 'http://127.0.0.1',
     port: string | number = 8080,
-    headers = {}
+    headers: Record<string, string> = {}
   ) {
-    super('X-Indexer-API-Token', token, baseServer, port, headers);
+    super('X-Indexer-API-Token', tokenOrBaseClient, baseServer, port, headers);
   }
 
   /**
diff --git a/src/client/v2/serviceClient.ts b/src/client/v2/serviceClient.ts
index a4f0c23..4823db4 100644
--- a/src/client/v2/serviceClient.ts
+++ b/src/client/v2/serviceClient.ts
@@ -1,5 +1,7 @@
-import HTTPClient, { TokenHeader } from '../client';
+import HTTPClient from '../client';
 import IntDecoding from '../../types/intDecoding';
+import { BaseHTTPClient } from '../baseHTTPClient';
+import { TokenHeader } from '../urlTokenBaseHTTPClient';
 
 export type TokenHeaderIdentifier =
   | 'X-Indexer-API-Token'
@@ -21,6 +23,12 @@ function convertTokenStringToTokenHeader(
   return tokenHeader as TokenHeader;
 }
 
+function isBaseHTTPClient(
+  tbc: string | TokenHeader | BaseHTTPClient
+): tbc is BaseHTTPClient {
+  return typeof (tbc as BaseHTTPClient).get === 'function';
+}
+
 /**
  * Abstract service client to encapsulate shared AlgodClient and IndexerClient logic
  */
@@ -30,24 +38,30 @@ export default abstract class ServiceClient {
 
   constructor(
     tokenHeaderIdentifier: TokenHeaderIdentifier,
-    tokenHeaderOrStr: string | TokenHeader,
+    tokenHeaderOrStrOrBaseClient: string | TokenHeader | BaseHTTPClient,
     baseServer: string,
     port?: string | number,
     defaultHeaders: Record<string, any> = {}
   ) {
-    // Accept token header as string or object
-    // - workaround to allow backwards compatibility for multiple headers
-    let tokenHeader: TokenHeader;
-    if (typeof tokenHeaderOrStr === 'string') {
-      tokenHeader = convertTokenStringToTokenHeader(
-        tokenHeaderOrStr,
-        tokenHeaderIdentifier
-      );
+    if (isBaseHTTPClient(tokenHeaderOrStrOrBaseClient)) {
+      // we are using a base client
+      this.c = new HTTPClient(tokenHeaderOrStrOrBaseClient);
     } else {
-      tokenHeader = tokenHeaderOrStr;
+      // Accept token header as string or object
+      // - workaround to allow backwards compatibility for multiple headers
+      let tokenHeader: TokenHeader;
+      if (typeof tokenHeaderOrStrOrBaseClient === 'string') {
+        tokenHeader = convertTokenStringToTokenHeader(
+          tokenHeaderOrStrOrBaseClient,
+          tokenHeaderIdentifier
+        );
+      } else {
+        tokenHeader = tokenHeaderOrStrOrBaseClient;
+      }
+
+      this.c = new HTTPClient(tokenHeader, baseServer, port, defaultHeaders);
     }
 
-    this.c = new HTTPClient(tokenHeader, baseServer, port, defaultHeaders);
     this.intDecoding = IntDecoding.DEFAULT;
   }
 
diff --git a/src/composer.ts b/src/composer.ts
index 06c66f6..c9b5a2c 100644
--- a/src/composer.ts
+++ b/src/composer.ts
@@ -1,9 +1,14 @@
 import {
   ABIType,
   ABITupleType,
+  ABIUintType,
+  ABIAddressType,
   ABIValue,
   ABIMethod,
+  ABIReferenceType,
   abiTypeIsTransaction,
+  abiCheckTransactionType,
+  abiTypeIsReference,
 } from './abi';
 import { Transaction, decodeSignedTransaction } from './transaction';
 import { makeApplicationCallTxnFromObject } from './makeTxn';
@@ -20,6 +25,12 @@ import {
   SuggestedParams,
 } from './types/transactions/base';
 
+// First 4 bytes of SHA-512/256 hash of "return"
+const RETURN_PREFIX = Buffer.from([21, 31, 124, 117]);
+
+// The maximum number of arguments for an application call transaction
+const MAX_APP_ARGS = 16;
+
 export type ABIArgument = ABIValue | TransactionWithSigner;
 
 /** Represents the output from a successful ABI method call. */
@@ -57,6 +68,40 @@ export enum AtomicTransactionComposerStatus {
   COMMITTED,
 }
 
+/**
+ * Add a value to an application call's foreign array. The addition will be as compact as possible,
+ * and this function will return an index that can be used to reference `valueToAdd` in `array`.
+ *
+ * @param valueToAdd - The value to add to the array. If this value is already present in the array,
+ *   it will not be added again. Instead, the existing index will be returned.
+ * @param array - The existing foreign array. This input may be modified to append `valueToAdd`.
+ * @param zeroValue - If provided, this value indicated two things: the 0 value is special for this
+ *   array, so all indexes into `array` must start at 1; additionally, if `valueToAdd` equals
+ *   `zeroValue`, then `valueToAdd` will not be added to the array, and instead the 0 indexes will
+ *   be returned.
+ * @returns An index that can be used to reference `valueToAdd` in `array`.
+ */
+function populateForeignArray<Type>(
+  valueToAdd: Type,
+  array: Type[],
+  zeroValue?: Type
+): number {
+  if (zeroValue != null && valueToAdd === zeroValue) {
+    return 0;
+  }
+
+  const offset = zeroValue == null ? 0 : 1;
+
+  for (let i = 0; i < array.length; i++) {
+    if (valueToAdd === array[i]) {
+      return i + offset;
+    }
+  }
+
+  array.push(valueToAdd);
+  return array.length - 1 + offset;
+}
+
 /** A class used to construct and execute atomic transaction groups */
 export class AtomicTransactionComposer {
   /** The maximum size of an atomic transaction group. */
@@ -137,19 +182,26 @@ export class AtomicTransactionComposer {
    * for the given method.
    */
   addMethodCall({
-    appId,
+    appID,
     method,
     methodArgs,
     sender,
     suggestedParams,
     onComplete,
+    approvalProgram,
+    clearProgram,
+    numGlobalInts,
+    numGlobalByteSlices,
+    numLocalInts,
+    numLocalByteSlices,
+    extraPages,
     note,
     lease,
     rekeyTo,
     signer,
   }: {
-    /** The ID of the smart contract to call */
-    appId: number;
+    /** The ID of the smart contract to call. Set this to 0 to indicate an application creation call. */
+    appID: number;
     /** The method to call on the smart contract */
     method: ABIMethod;
     /** The arguments to include in the method call. If omitted, no arguments will be passed to the method. */
@@ -160,6 +212,20 @@ export class AtomicTransactionComposer {
     suggestedParams: SuggestedParams;
     /** The OnComplete action to take for this application call. If omitted, OnApplicationComplete.NoOpOC will be used. */
     onComplete?: OnApplicationComplete;
+    /** The approval program for this application call. Only set this if this is an application creation call, or if onComplete is OnApplicationComplete.UpdateApplicationOC */
+    approvalProgram?: Uint8Array;
+    /** The clear program for this application call. Only set this if this is an application creation call, or if onComplete is OnApplicationComplete.UpdateApplicationOC */
+    clearProgram?: Uint8Array;
+    /** The global integer schema size. Only set this if this is an application creation call. */
+    numGlobalInts?: number;
+    /** The global byte slice schema size. Only set this if this is an application creation call. */
+    numGlobalByteSlices?: number;
+    /** The local integer schema size. Only set this if this is an application creation call. */
+    numLocalInts?: number;
+    /** The local byte slice schema size. Only set this if this is an application creation call. */
+    numLocalByteSlices?: number;
+    /** The number of extra pages to allocate for the application's programs. Only set this if this is an application creation call. If omitted, defaults to 0. */
+    extraPages?: number;
     /** The note value for this application call */
     note?: Uint8Array;
     /** The lease value for this application call */
@@ -184,8 +250,48 @@ export class AtomicTransactionComposer {
       );
     }
 
-    if (appId === 0) {
-      throw new Error('Application create call not supported');
+    if (appID === 0) {
+      if (
+        approvalProgram == null ||
+        clearProgram == null ||
+        numGlobalInts == null ||
+        numGlobalByteSlices == null ||
+        numLocalInts == null ||
+        numLocalByteSlices == null
+      ) {
+        throw new Error(
+          'One of the following required parameters for application creation is missing: approvalProgram, clearProgram, numGlobalInts, numGlobalByteSlices, numLocalInts, numLocalByteSlices'
+        );
+      }
+    } else if (onComplete === OnApplicationComplete.UpdateApplicationOC) {
+      if (approvalProgram == null || clearProgram == null) {
+        throw new Error(
+          'One of the following required parameters for OnApplicationComplete.UpdateApplicationOC is missing: approvalProgram, clearProgram'
+        );
+      }
+      if (
+        numGlobalInts != null ||
+        numGlobalByteSlices != null ||
+        numLocalInts != null ||
+        numLocalByteSlices != null ||
+        extraPages != null
+      ) {
+        throw new Error(
+          'One of the following application creation parameters were set on a non-creation call: numGlobalInts, numGlobalByteSlices, numLocalInts, numLocalByteSlices, extraPages'
+        );
+      }
+    } else if (
+      approvalProgram != null ||
+      clearProgram != null ||
+      numGlobalInts != null ||
+      numGlobalByteSlices != null ||
+      numLocalInts != null ||
+      numLocalByteSlices != null ||
+      extraPages != null
+    ) {
+      throw new Error(
+        'One of the following application creation parameters were set on a non-creation call: approvalProgram, clearProgram, numGlobalInts, numGlobalByteSlices, numLocalInts, numLocalByteSlices, extraPages'
+      );
     }
 
     if (methodArgs == null) {
@@ -199,24 +305,24 @@ export class AtomicTransactionComposer {
       );
     }
 
-    let appArgTypes: ABIType[] = [];
-    let appArgValues: ABIValue[] = [];
+    let basicArgTypes: ABIType[] = [];
+    let basicArgValues: ABIValue[] = [];
     const txnArgs: TransactionWithSigner[] = [];
+    const refArgTypes: ABIReferenceType[] = [];
+    const refArgValues: ABIValue[] = [];
+    const refArgIndexToBasicArgIndex: Map<number, number> = new Map();
 
     for (let i = 0; i < methodArgs.length; i++) {
-      const argSpec = method.args[i];
+      let argType = method.args[i].type;
       const argValue = methodArgs[i];
 
-      if (
-        typeof argSpec.type === 'string' &&
-        abiTypeIsTransaction(argSpec.type)
-      ) {
+      if (abiTypeIsTransaction(argType)) {
         if (
           !isTransactionWithSigner(argValue) ||
-          argSpec.type !== argValue.txn.type
+          !abiCheckTransactionType(argType, argValue.txn)
         ) {
           throw new Error(
-            `Expected ${argSpec.type} transaction for argument at index ${i}`
+            `Expected ${argType} transaction for argument at index ${i}`
           );
         }
         if (argValue.txn.group && argValue.txn.group.some((v) => v !== 0)) {
@@ -232,33 +338,108 @@ export class AtomicTransactionComposer {
         );
       }
 
-      appArgTypes.push(argSpec.type);
-      appArgValues.push(argValue);
+      if (abiTypeIsReference(argType)) {
+        refArgIndexToBasicArgIndex.set(
+          refArgTypes.length,
+          basicArgTypes.length
+        );
+        refArgTypes.push(argType);
+        refArgValues.push(argValue);
+        // treat the reference as a uint8 for encoding purposes
+        argType = new ABIUintType(8);
+      }
+
+      if (typeof argType === 'string') {
+        throw new Error(`Unknown ABI type: ${argType}`);
+      }
+
+      basicArgTypes.push(argType);
+      basicArgValues.push(argValue);
+    }
+
+    const resolvedRefIndexes: number[] = [];
+    const foreignAccounts: string[] = [];
+    const foreignApps: number[] = [];
+    const foreignAssets: number[] = [];
+    for (let i = 0; i < refArgTypes.length; i++) {
+      const refType = refArgTypes[i];
+      const refValue = refArgValues[i];
+      let resolved = 0;
+
+      switch (refType) {
+        case ABIReferenceType.account: {
+          const addressType = new ABIAddressType();
+          const address = addressType.decode(addressType.encode(refValue));
+          resolved = populateForeignArray(address, foreignAccounts, sender);
+          break;
+        }
+        case ABIReferenceType.application: {
+          const uint64Type = new ABIUintType(64);
+          const refAppID = uint64Type.decode(uint64Type.encode(refValue));
+          if (refAppID > Number.MAX_SAFE_INTEGER) {
+            throw new Error(
+              `Expected safe integer for application value, got ${refAppID}`
+            );
+          }
+          resolved = populateForeignArray(Number(refAppID), foreignApps, appID);
+          break;
+        }
+        case ABIReferenceType.asset: {
+          const uint64Type = new ABIUintType(64);
+          const refAssetID = uint64Type.decode(uint64Type.encode(refValue));
+          if (refAssetID > Number.MAX_SAFE_INTEGER) {
+            throw new Error(
+              `Expected safe integer for asset value, got ${refAssetID}`
+            );
+          }
+          resolved = populateForeignArray(Number(refAssetID), foreignAssets);
+          break;
+        }
+        default:
+          throw new Error(`Unknown reference type: ${refType}`);
+      }
+
+      resolvedRefIndexes.push(resolved);
+    }
+
+    for (let i = 0; i < resolvedRefIndexes.length; i++) {
+      const basicArgIndex = refArgIndexToBasicArgIndex.get(i);
+      basicArgValues[basicArgIndex] = resolvedRefIndexes[i];
     }
 
-    if (appArgTypes.length > 14) {
-      const lastArgTupleTypes = appArgTypes.slice(14);
-      const lastArgTupleValues = appArgValues.slice(14);
+    if (basicArgTypes.length > MAX_APP_ARGS - 1) {
+      const lastArgTupleTypes = basicArgTypes.slice(MAX_APP_ARGS - 2);
+      const lastArgTupleValues = basicArgValues.slice(MAX_APP_ARGS - 2);
 
-      appArgTypes = appArgTypes.slice(0, 14);
-      appArgValues = appArgValues.slice(0, 14);
+      basicArgTypes = basicArgTypes.slice(0, MAX_APP_ARGS - 2);
+      basicArgValues = basicArgValues.slice(0, MAX_APP_ARGS - 2);
 
-      appArgTypes.push(new ABITupleType(lastArgTupleTypes));
-      appArgValues.push(lastArgTupleValues);
+      basicArgTypes.push(new ABITupleType(lastArgTupleTypes));
+      basicArgValues.push(lastArgTupleValues);
     }
 
     const appArgsEncoded: Uint8Array[] = [method.getSelector()];
-    for (let i = 0; i < appArgTypes.length; i++) {
-      appArgsEncoded.push(appArgTypes[i].encode(appArgValues[i]));
+    for (let i = 0; i < basicArgTypes.length; i++) {
+      appArgsEncoded.push(basicArgTypes[i].encode(basicArgValues[i]));
     }
 
     const appCall = {
       txn: makeApplicationCallTxnFromObject({
         from: sender,
-        appIndex: appId,
+        appIndex: appID,
         appArgs: appArgsEncoded,
+        accounts: foreignAccounts,
+        foreignApps,
+        foreignAssets,
         onComplete:
           onComplete == null ? OnApplicationComplete.NoOpOC : onComplete,
+        approvalProgram,
+        clearProgram,
+        numGlobalInts,
+        numGlobalByteSlices,
+        numLocalInts,
+        numLocalByteSlices,
+        extraPages,
         lease,
         note,
         rekeyTo,
@@ -281,9 +462,11 @@ export class AtomicTransactionComposer {
       if (this.transactions.length === 0) {
         throw new Error('Cannot build a group with 0 transactions');
       }
-      assignGroupID(
-        this.transactions.map((txnWithSigner) => txnWithSigner.txn)
-      );
+      if (this.transactions.length > 1) {
+        assignGroupID(
+          this.transactions.map((txnWithSigner) => txnWithSigner.txn)
+        );
+      }
       this.status = AtomicTransactionComposerStatus.BUILT;
     }
     return this.transactions;
@@ -423,6 +606,7 @@ export class AtomicTransactionComposer {
     }
 
     const txIDs = await this.submit(client);
+    this.status = AtomicTransactionComposerStatus.SUBMITTED;
 
     const firstMethodCallIndex = this.transactions.findIndex((_, index) =>
       this.methodCalls.has(index)
@@ -434,7 +618,6 @@ export class AtomicTransactionComposer {
       txIDs[indexToWaitFor],
       waitRounds
     );
-
     this.status = AtomicTransactionComposerStatus.COMMITTED;
 
     const confirmedRound: number = confirmedTxnInfo['confirmed-round'];
@@ -458,25 +641,19 @@ export class AtomicTransactionComposer {
                 await client.pendingTransactionInformation(txID).do();
 
           const logs: string[] = pendingInfo.logs || [];
-
-          // first 4 bytes of SHA-512/256 hash of "return"
-          const returnPrefix = Buffer.from([21, 31, 124, 117]);
-
-          let returnValueEncoded: Buffer | undefined;
-
-          for (let i = logs.length - 1; i >= 0; i--) {
-            const log = Buffer.from(logs[i], 'base64');
-            if (log.byteLength >= 4 && log.slice(0, 4).equals(returnPrefix)) {
-              returnValueEncoded = log.slice(4);
-              break;
-            }
+          if (logs.length === 0) {
+            throw new Error('App call transaction did not log a return value');
           }
 
-          if (returnValueEncoded == null) {
+          const lastLog = Buffer.from(logs[logs.length - 1], 'base64');
+          if (
+            lastLog.byteLength < 4 ||
+            !lastLog.slice(0, 4).equals(RETURN_PREFIX)
+          ) {
             throw new Error('App call transaction did not log a return value');
           }
 
-          methodResult.rawReturnValue = new Uint8Array(returnValueEncoded);
+          methodResult.rawReturnValue = new Uint8Array(lastLog.slice(4));
           methodResult.returnValue = method.returns.type.decode(
             methodResult.rawReturnValue
           );
diff --git a/src/main.ts b/src/main.ts
index b5bbfa0..d525b78 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -123,6 +123,11 @@ export { default as Kmd } from './client/kmd';
 export { default as IntDecoding } from './types/intDecoding';
 export { default as Account } from './types/account';
 export { default as Indexer } from './client/v2/indexer/indexer';
+export {
+  BaseHTTPClient,
+  BaseHTTPClientResponse,
+  BaseHTTPClientError,
+} from './client/baseHTTPClient';
 export { waitForConfirmation } from './wait';
 export {
   isValidAddress,
diff --git a/src/transaction.ts b/src/transaction.ts
index 2d01f79..55e4f16 100644
--- a/src/transaction.ts
+++ b/src/transaction.ts
@@ -772,8 +772,9 @@ export class Transaction implements TransactionStorageStructure {
       if (!txn.apap) delete txn.apap;
       if (!txn.apsu) delete txn.apsu;
       if (!txn.apan) delete txn.apan;
-      if (!txn.apfa) delete txn.apfa;
-      if (!txn.apas) delete txn.apas;
+      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;
diff --git a/src/wait.ts b/src/wait.ts
index b83573e..5ade09d 100644
--- a/src/wait.ts
+++ b/src/wait.ts
@@ -26,21 +26,33 @@ export async function waitForConfirmation(
 
   /* eslint-disable no-await-in-loop */
   while (currentRound < startRound + waitRounds) {
-    const pendingInfo = await client.pendingTransactionInformation(txid).do();
+    let poolError = false;
+    try {
+      const pendingInfo = await client.pendingTransactionInformation(txid).do();
 
-    if (pendingInfo['confirmed-round']) {
-      // Got the completed Transaction
-      return pendingInfo;
-    }
+      if (pendingInfo['confirmed-round']) {
+        // Got the completed Transaction
+        return pendingInfo;
+      }
 
-    if (pendingInfo['pool-error']) {
-      // If there was a pool error, then the transaction has been rejected!
-      throw new Error(`Transaction Rejected: ${pendingInfo['pool-error']}`);
+      if (pendingInfo['pool-error']) {
+        // If there was a pool error, then the transaction has been rejected
+        poolError = true;
+        throw new Error(`Transaction Rejected: ${pendingInfo['pool-error']}`);
+      }
+    } catch (err) {
+      // Ignore errors from PendingTransactionInformation, since it may return 404 if the algod
+      // instance is behind a load balancer and the request goes to a different algod than the
+      // one we submitted the transaction to
+      if (poolError) {
+        // Rethrow error only if it's because the transaction was rejected
+        throw err;
+      }
     }
 
     await client.statusAfterBlock(currentRound).do();
     currentRound += 1;
   }
   /* eslint-enable no-await-in-loop */
-  throw new Error(`Transaction not confirmed after ${waitRounds} rounds!`);
+  throw new Error(`Transaction not confirmed after ${waitRounds} rounds`);
 }
diff --git a/typedoc.config.json b/typedoc.config.json
new file mode 100644
index 0000000..4b53fab
--- /dev/null
+++ b/typedoc.config.json
@@ -0,0 +1,3 @@
+{
+  "excludeExternals": true
+}


Test patch:
diff --git a/tests/9.Client.ts b/tests/9.Client.ts
index b978bb2..67b1141 100644
--- a/tests/9.Client.ts
+++ b/tests/9.Client.ts
@@ -1,11 +1,12 @@
 import assert from 'assert';
 import HTTPClient from '../src/client/client';
+import { URLTokenBaseHTTPClient } from '../src/client/urlTokenBaseHTTPClient';
 
 describe('client', () => {
   describe('url construction', () => {
     /* eslint-disable dot-notation */
     it('should work with trivial paths', () => {
-      const client = new HTTPClient({}, 'http://localhost');
+      const client = new URLTokenBaseHTTPClient({}, 'http://localhost');
       const actual = client['addressWithPath']('/relative');
       const expected = 'http://localhost/relative';
 
@@ -13,7 +14,7 @@ describe('client', () => {
     });
 
     it('should work with number ports and trivial paths', () => {
-      const client = new HTTPClient({}, 'http://localhost', 3000);
+      const client = new URLTokenBaseHTTPClient({}, 'http://localhost', 3000);
       const actual = client['addressWithPath']('/relative');
       const expected = 'http://localhost:3000/relative';
 
@@ -21,7 +22,7 @@ describe('client', () => {
     });
 
     it('should work with complex base URLs and complex paths', () => {
-      const client = new HTTPClient(
+      const client = new URLTokenBaseHTTPClient(
         {},
         'https://testnet-algorand.api.purestake.io/ps2/',
         8080
@@ -35,8 +36,11 @@ describe('client', () => {
     it('should handle slash variations on complex paths', () => {
       const regularBase = 'https://localhost/absolute';
       const regularBaseWithFinalSlash = `${regularBase}/`;
-      const client = new HTTPClient({}, regularBase);
-      const clientWithSlash = new HTTPClient({}, regularBaseWithFinalSlash);
+      const client = new URLTokenBaseHTTPClient({}, regularBase);
+      const clientWithSlash = new URLTokenBaseHTTPClient(
+        {},
+        regularBaseWithFinalSlash
+      );
 
       const relativePath = 'relative';
       const relativePathWithInitialSlash = `/${relativePath}`;
@@ -57,8 +61,15 @@ describe('client', () => {
     it('should throw an error if protocol is the empty', () => {
       const baseServer = 'localhost'; // should be http://localhost
 
-      assert.throws(() => new HTTPClient({}, baseServer));
+      assert.throws(() => new URLTokenBaseHTTPClient({}, baseServer));
     });
     /* eslint-enable dot-notation */
   });
+  describe('HTTPClient construction', () => {
+    it('should throw an error if protocol is the empty', () => {
+      const baseServer = 'localhost'; // should be http://localhost
+
+      assert.throws(() => new HTTPClient({}, baseServer));
+    });
+  });
 });
diff --git a/tests/cucumber/steps/steps.js b/tests/cucumber/steps/steps.js
index 24f541f..b9c76d0 100644
--- a/tests/cucumber/steps/steps.js
+++ b/tests/cucumber/steps/steps.js
@@ -1874,7 +1874,7 @@ module.exports = function getSteps(options) {
           throw err;
         }
         if (this.expectedMockResponseCode === 500) {
-          if (!err.toString().includes('Internal Server Error')) {
+          if (!err.toString().includes('Received status 500')) {
             throw Error(
               `expected response code 500 implies error Internal Server Error but instead had error: ${err}`
             );
@@ -4655,11 +4655,12 @@ module.exports = function getSteps(options) {
   );
 
   When(
-    'I create an Interface object from the Method object with name {string}',
-    function (name) {
+    'I create an Interface object from the Method object with name {string} and description {string}',
+    function (name, desc) {
       this.interface = new algosdk.ABIInterface(
         makeObject({
           name,
+          desc,
           methods: makeArray(this.method.toJSON()),
         })
       );
@@ -4681,18 +4682,27 @@ module.exports = function getSteps(options) {
   );
 
   When(
-    'I create a Contract object from the Method object with name {string} and appId {int}',
-    function (name, appId) {
+    'I create a Contract object from the Method object with name {string} and description {string}',
+    function (name, desc) {
       this.contract = makeABIContract(
         makeObject({
           name,
-          appId: parseInt(appId, 10),
+          desc,
           methods: makeArray(this.method.toJSON()),
         })
       );
     }
   );
 
+  When(
+    "I set the Contract's appID to {int} for the network {string}",
+    function (appID, network) {
+      this.contract.networks[network] = makeObject({
+        appID: parseInt(appID, 10),
+      });
+    }
+  );
+
   When('I serialize the Contract object into json', function () {
     this.json = JSON.stringify(this.contract);
   });
@@ -4715,6 +4725,7 @@ module.exports = function getSteps(options) {
 
   Given('a new AtomicTransactionComposer', function () {
     this.composer = new algosdk.AtomicTransactionComposer();
+    this.composerMethods = [];
   });
 
   Given('an application id {int}', function (appId) {
@@ -4768,7 +4779,30 @@ module.exports = function getSteps(options) {
     }
   );
 
-  function addMethodCallToComposer(sender, onComplete) {
+  async function addMethodCallToComposer(
+    sender,
+    onComplete,
+    approvalProgramFile,
+    clearProgramFile,
+    globalBytes,
+    globalInts,
+    localBytes,
+    localInts,
+    extraPages
+  ) {
+    // open and load in approval program
+    let approvalProgramBytes;
+    if (approvalProgramFile !== '') {
+      const resouce = await loadResource(approvalProgramFile);
+      approvalProgramBytes = makeUint8Array(resouce);
+    }
+    // open and load in clear program
+    let clearProgramBytes;
+    if (clearProgramFile !== '') {
+      const resouce = await loadResource(clearProgramFile);
+      clearProgramBytes = makeUint8Array(resouce);
+    }
+
     const methodArgs = [];
 
     assert.strictEqual(
@@ -4780,42 +4814,178 @@ module.exports = function getSteps(options) {
       const argSpec = this.method.args[i];
       const encodedArg = this.encodedMethodArguments[i];
 
-      let preparedArg = encodedArg;
+      if (algosdk.abiTypeIsTransaction(argSpec.type)) {
+        methodArgs.push(encodedArg);
+        continue;
+      }
+
+      let typeToDecode = argSpec.type;
+
+      if (algosdk.abiTypeIsReference(argSpec.type)) {
+        switch (argSpec.type) {
+          case algosdk.ABIReferenceType.account:
+            typeToDecode = algosdk.ABIType.from('address');
+            break;
+          case algosdk.ABIReferenceType.application:
+          case algosdk.ABIReferenceType.asset:
+            typeToDecode = algosdk.ABIType.from('uint64');
+            break;
+          default:
+            throw new Error(`Unknown reference type: ${argSpec.type}`);
+        }
+      }
 
-      if (typeof argSpec.type !== 'string') {
-        // the argument is an encoded ABI value
-        preparedArg = argSpec.type.decode(encodedArg);
+      if (typeof typeToDecode === 'string') {
+        throw new Error(`Cannot decode with type: ${typeToDecode}`);
       }
 
-      methodArgs.push(preparedArg);
+      methodArgs.push(typeToDecode.decode(encodedArg));
     }
 
     this.composer.addMethodCall({
-      appId: this.currentApplicationIndex,
+      appID: this.currentApplicationIndex,
       method: this.method,
       methodArgs,
       sender,
       suggestedParams: this.suggestedParams,
       onComplete: operationStringToEnum(onComplete),
+      approvalProgram: approvalProgramBytes,
+      clearProgram: clearProgramBytes,
+      numGlobalInts: globalInts,
+      numGlobalByteSlices: globalBytes,
+      numLocalInts: localInts,
+      numLocalByteSlices: localBytes,
+      extraPages,
       signer: this.transactionSigner,
     });
+    this.composerMethods.push(this.method);
   }
 
   When(
     'I add a method call with the transient account, the current application, suggested params, on complete {string}, current transaction signer, current method arguments.',
-    function (onComplete) {
-      addMethodCallToComposer.call(
+    async function (onComplete) {
+      await addMethodCallToComposer.call(
         this,
         this.transientAccount.addr,
-        onComplete
+        onComplete,
+        '',
+        '',
+        undefined,
+        undefined,
+        undefined,
+        undefined,
+        undefined
       );
     }
   );
 
   When(
     'I add a method call with the signing account, the current application, suggested params, on complete {string}, current transaction signer, current method arguments.',
-    function (onComplete) {
-      addMethodCallToComposer.call(this, this.signingAccount.addr, onComplete);
+    async function (onComplete) {
+      await addMethodCallToComposer.call(
+        this,
+        this.signingAccount.addr,
+        onComplete,
+        '',
+        '',
+        undefined,
+        undefined,
+        undefined,
+        undefined,
+        undefined
+      );
+    }
+  );
+
+  When(
+    'I add a method call with the transient account, the current application, suggested params, on complete {string}, current transaction signer, current method arguments, approval-program {string}, clear-program {string}, global-bytes {int}, global-ints {int}, local-bytes {int}, local-ints {int}, extra-pages {int}.',
+    async function (
+      onComplete,
+      approvalProg,
+      clearProg,
+      globalBytes,
+      globalInts,
+      localBytes,
+      localInts,
+      extraPages
+    ) {
+      await addMethodCallToComposer.call(
+        this,
+        this.transientAccount.addr,
+        onComplete,
+        approvalProg,
+        clearProg,
+        parseInt(globalBytes, 10),
+        parseInt(globalInts, 10),
+        parseInt(localBytes, 10),
+        parseInt(localInts, 10),
+        parseInt(extraPages, 10)
+      );
+    }
+  );
+
+  When(
+    'I add a method call with the signing account, the current application, suggested params, on complete {string}, current transaction signer, current method arguments, approval-program {string}, clear-program {string}, global-bytes {int}, global-ints {int}, local-bytes {int}, local-ints {int}, extra-pages {int}.',
+    async function (
+      onComplete,
+      approvalProg,
+      clearProg,
+      globalBytes,
+      globalInts,
+      localBytes,
+      localInts,
+      extraPages
+    ) {
+      await addMethodCallToComposer.call(
+        this,
+        this.signingAccount.addr,
+        onComplete,
+        approvalProg,
+        clearProg,
+        parseInt(globalBytes, 10),
+        parseInt(globalInts, 10),
+        parseInt(localBytes, 10),
+        parseInt(localInts, 10),
+        parseInt(extraPages, 10)
+      );
+    }
+  );
+
+  When(
+    'I add a method call with the transient account, the current application, suggested params, on complete {string}, current transaction signer, current method arguments, approval-program {string}, clear-program {string}.',
+    async function (onCompletion, approvalProg, clearProg) {
+      assert.strictEqual(onCompletion, 'update');
+      await addMethodCallToComposer.call(
+        this,
+        this.transientAccount.addr,
+        onCompletion,
+        approvalProg,
+        clearProg,
+        undefined,
+        undefined,
+        undefined,
+        undefined,
+        undefined
+      );
+    }
+  );
+
+  When(
+    'I add a method call with the signing account, the current application, suggested params, on complete {string}, current transaction signer, current method arguments, approval-program {string}, clear-program {string}.',
+    async function (onCompletion, approvalProg, clearProg) {
+      assert.strictEqual(onCompletion, 'update');
+      await addMethodCallToComposer.call(
+        this,
+        this.signingAccount.addr,
+        onCompletion,
+        approvalProg,
+        clearProg,
+        undefined,
+        undefined,
+        undefined,
+        undefined,
+        undefined
+      );
     }
   );
 
@@ -4828,9 +4998,26 @@ module.exports = function getSteps(options) {
 
   When(
     'I build the transaction group with the composer. If there is an error it is {string}.',
-    function (errorMsg) {
-      assert.strictEqual(errorMsg, ''); // error checking not yet implemented
-      this.composerBuiltGroup = this.composer.buildGroup();
+    function (errorType) {
+      if (errorType === '') {
+        // no error expected
+        this.composerBuiltGroup = this.composer.buildGroup();
+        return;
+      }
+
+      let expectedMessage;
+      switch (errorType) {
+        case 'zero group size error':
+          expectedMessage = 'Cannot build a group with 0 transactions';
+          break;
+        default:
+          throw new Error(`Unknown error type: "${errorType}"`);
+      }
+
+      assert.throws(
+        () => this.composer.buildGroup(),
+        (err) => err.message === expectedMessage
+      );
     }
   );
 
@@ -4881,7 +5068,13 @@ module.exports = function getSteps(options) {
       const actualSignedTxns = this.composerSignedTransactions.map(
         (signedTxn) => Buffer.from(signedTxn)
       );
-      assert.deepStrictEqual([...actualSignedTxns], [...expectedSignedTxns]);
+      assert.deepStrictEqual(
+        [...actualSignedTxns],
+        [...expectedSignedTxns],
+        `Got ${actualSignedTxns
+          .map((stxn) => stxn.toString('base64'))
+          .join(',')}`
+      );
     }
   );
 
@@ -4898,34 +5091,43 @@ module.exports = function getSteps(options) {
 
   Then(
     'The app should have returned {string}.',
-    function (base64ExpectedReturnValue) {
+    function (expectedReturnValues) {
+      const b64ExpectedReturnValues = expectedReturnValues.split(',');
+
       const { methodResults } = this.composerExecuteResponse;
-      assert.strictEqual(methodResults.length, 1);
+      assert.strictEqual(methodResults.length, b64ExpectedReturnValues.length);
+      assert.strictEqual(methodResults.length, this.composerMethods.length);
+
+      for (let i = 0; i < methodResults.length; i++) {
+        const method = this.composerMethods[i];
+        const actualResult = methodResults[i];
+        const expectedReturnValue = Buffer.from(
+          b64ExpectedReturnValues[i],
+          'base64'
+        );
 
-      const actualResult = methodResults[0];
-      const expectedReturnValue = Buffer.from(
-        base64ExpectedReturnValue,
-        'base64'
-      );
+        if (actualResult.decodeError) {
+          throw actualResult.decodeError;
+        }
+        assert.deepStrictEqual(
+          Buffer.from(actualResult.rawReturnValue),
+          expectedReturnValue,
+          `Actual return value for method at index ${i} does not match expected. Actual: ${Buffer.from(
+            actualResult.rawReturnValue
+          ).toString('base64')}`
+        );
 
-      if (actualResult.decodeError) {
-        throw actualResult.decodeError;
-      }
-      assert.deepStrictEqual(
-        Buffer.from(actualResult.rawReturnValue),
-        expectedReturnValue
-      );
+        const returnType = method.returns.type;
+        if (returnType === 'void') {
+          assert.strictEqual(expectedReturnValue.byteLength, 0);
+          continue;
+        }
 
-      const returnType = this.method.returns.type;
-      if (returnType === 'void') {
-        assert.strictEqual(expectedReturnValue.byteLength, 0);
-        return;
+        assert.deepStrictEqual(
+          actualResult.returnValue,
+          returnType.decode(expectedReturnValue)
+        );
       }
-
-      assert.deepStrictEqual(
-        actualResult.returnValue,
-        returnType.decode(expectedReturnValue)
-      );
     }
   );