Repository: algorand/js-algorand-sdk
Pull request #477 (2021-12-02 23:56:57)

Enabling a custom client for Algod and Indexer
Currently the only way for a dApp to use the SDK with an API service is to know the URL and token of the API service.
This prevents dApps from using API services / algod / indexer provided by the user or by a wallet such as AlgoSigner.

In particular:
* When using a wallet such as AlgoSigner, a dApp either need to manually serialize data and write path for endpoints (https://github.com/PureStake/algosigner/blob/develop/docs/dApp-integration.md#algosigneralgod-ledger-mainnettestnet-path-algod-v2-path--) or need to not use at all the provided API service.
* A user cannot use their preferred (potentially more trusted) API service / algod / index / API services
* A dApp cannot easily connect to a private network of the user

(See also https://github.com/algorandfoundation/ARCs/pull/8 for more details on wallets and discussion around them.)

The goal of this PR is to allow the above scenarios.
It does so by introducing an intermediate interface `BaseHTTPClient` and to allow to construct `Algodv2Client` and `IndexerClient` from such a `BaseHTTPClient`.

A `BaseHTTPClient` is minimal: it does not perform any serialization/de-serialization, does not fix `accept` headers, does not even try to utf8-decode strings. Its only purpose is to execute directly REST queries and return the raw responses.

`BaseHTTPClient`  is meant to be stable over releases of algosdk.

fabrice102: Thanks! I've done all the proposed changes.

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/package-lock.json b/package-lock.json
index bd28b9b..f371e8c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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..584fb7d 100644
--- a/package.json
+++ b/package.json
@@ -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/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..ec75625
--- /dev/null
+++ b/src/client/urlTokenBaseHTTPClient.ts
@@ -0,0 +1,144 @@
+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;
+  }
+
+  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);
+
+    const res = await r;
+    return URLTokenBaseHTTPClient.superagentToHTTPClientResponse(res);
+  }
+
+  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
+
+    const res = await r;
+    return URLTokenBaseHTTPClient.superagentToHTTPClientResponse(res);
+  }
+
+  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
+
+    const res = await r;
+    return URLTokenBaseHTTPClient.superagentToHTTPClientResponse(res);
+  }
+}
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/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/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));
+    });
+  });
 });