Repository: onflow/fcl-js
Pull request #1323 (2022-07-15 17:53:08)

[FLIP-934] Interaction Template Integration
[FLIP-934] Interaction Template Integration

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 89034925a79c6aadd9ad77e49d57b5a6704fab60

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/onflow/fcl-js/new/fcl/templates?filename=.changeset/brave-pillows-type.md&value=---%0A%22%40onflow%2Ffcl%22%3A%20patch%0A%22%40onflow%2Fsdk%22%3A%20patch%0A---%0A%0A%5BNO-MERGE%5D%20%5BFLIP-934%5D%20Interaction%20Template%20Integration%0A)



Patch:
diff --git a/.changeset/unlucky-pears-wash.md b/.changeset/unlucky-pears-wash.md
new file mode 100644
index 00000000..835a5aa5
--- /dev/null
+++ b/.changeset/unlucky-pears-wash.md
@@ -0,0 +1,7 @@
+---
+"@onflow/fcl": minor
+---
+
+Adds FLIP-934 Interaction Template support to FCL.
+
+Interaction Template is a concept established in FLIP-934. Read more about the FLIP here: https://github.com/onflow/flips/blob/main/flips/20220503-interaction-templates.md
diff --git a/docs/reference/interaction-templates.mdx b/docs/reference/interaction-templates.mdx
new file mode 100644
index 00000000..de5618e0
--- /dev/null
+++ b/docs/reference/interaction-templates.mdx
@@ -0,0 +1,249 @@
+---
+title: Interaction Templates
+---
+
+# Interaction Templates
+
+> Interaction Templates are a concept established in FLIP-934. Read the FLIP [here](https://github.com/onflow/flips/blob/main/flips/20220503-interaction-templates.md)
+
+> "Interaction" in this context refers to the higher order term establised in FLIP-934 that encompases a transaction and script, things that _interact_ with the blockchain.
+
+## Overview
+
+Interaction Templates establish a format for metadata that exists about an interaction. Interaction Templates can include:
+
+- Human readable, internationalized messages about the interaction
+- The Cadence code to carry out the interaction
+- Information about arguments such as internationalized human readable messages and what the arguments act upon
+- Contract dependencies the Interaction engages with, pinned to a version of them and their dependency tree
+
+Applications and Wallets can use Interaction Templates and it's interaction metadata. 
+
+For example Applications and Wallets can extract the internationalized human readable messaging from an Interaction Template to display to their users prior to execution of the interaction.
+
+## For Applications
+
+FCL `mutate` and `query` can accept an Interaction Template. FCL `mutate` and `query` will use the Interaction Template to:
+
+- Extract the Cadence code to carry out the interaction
+- Extract dependency configuration for the interaction (eg: Information about contract import addresses)
+
+Here is an example of using `mutate` with an Interaction Template:
+```javascript
+import * as fcl from "@onflow/fcl"
+import myTransactionTemplate from "./my-transaction-template.template.json"
+
+const txId = await fcl.mutate({
+  template: myTransactionTemplate
+})
+```
+
+An Interaction Template can also be used with `query`:
+```javascript
+import * as fcl from "@onflow/fcl"
+import myScriptTemplate from "./my-script-template.template.json"
+
+const info = await fcl.query({
+  template: myScriptTemplate
+})
+```
+
+Interaction Templates can be resolved from remote locations:
+
+```javascript
+import * as fcl from "@onflow/fcl"
+
+const txId = await fcl.mutate({
+  template: "http://interactions.awesome-crypto-project.com/buy-nft"
+})
+
+const nftInfo = await fcl.query({
+  template: "http://interactions.awesome-crypto-project.com/read-nft",
+  args: (arg, t) => [arg("nft-id", t.String)]
+})
+```
+
+FCL will resolve the template from the remote location before using it to execute its underlying transaction or script.
+
+> 💡 By requesting an Interaction Template from an external location, applications have a mechanism to always retrieve the most up to date way of accomplishing an interaction.
+
+By default FCL supports resolving Interaction Templates over http/https, but FCL can also be configured with various other ways to resolve Interaction Templates:
+
+```javascript
+import * as fcl from "@onflow/fcl"
+
+await fcl.config().put("document.resolver.ipfs", async ({ url }) => {
+  const jsonTemplate = getDocumentFromIPFS(url) // resolve interaction template from ipfs
+  return jsonTemplate
+})
+
+const txId = await fcl.mutate({
+  template: "ipfs://IPFSHASHGOESHERE"
+})
+```
+
+## For Wallets
+
+Wallets can use Interaction Templates to:
+- Display internationalized human readable information about a transaction to their users during signing
+- Verify the dependencies of an Interaction Template have not changed since when the Interaction Template was created
+- Using Interaction Template Audits, gain confidence in the correctness and safety of an Interaction Template and it's underlying transaction
+
+When recieving a transaction to sign, wallets can query for an Interaction Template that corresponds to it.
+
+Flow operates an "Interaction Template Discovery Service" which wallets can use to query for Interaction Templates. Anyone can run an "Interaction Template Discovery Service" and wallets can choose to query from any of them.
+
+```javascript
+const cadence = cadenceFromTransactionToSign
+const network = "mainnet" // "mainnet" | "testnet"
+
+const cadenceBase64 = btoa(cadence)
+
+const interactionTemplate = await fetch(
+  `https://flix.flow.com/v1/templates?network=${network}&cadence=${cadenceBase64}`
+)
+```
+
+> 📖  For more on the "Interaction Template Discovery Service" that Flow operates, see [here](https://github.com/onflow/flow-interaction-template-service)
+
+> ❗️ Not all transactions will have a corresponding Interaction Template. Wallets are encouraged to always support signing transactions that do not have a corresponding Interaction Template, or if they fail to discover one.
+
+Once a wallet has a corresponding Interaction Template for a given transaction, they may also may wish to verify that the transaction it represents is safe to sign, and that the Interaction Template is accurate for that transaction.
+
+To do so, wallets can rely on themselves, along with external Interaction Template Auditors to gain confidence in the Interaction Template and it's underlying transaction. Interaction Template Auditors are entities that audit Interaction Templates for correctness and safety.
+
+> 💡 Anyone can be an Interaction Template Auditor. Wallets can choose auditors they trust, if any.
+
+Wallets can specify auditors it trusts to FCL by configuring FCL with the address of each auditor:
+
+```javascript
+import * as fcl from "@onflow/fcl"
+
+await fcl.config().put("flow.network", "mainnet")
+
+const auditorA_FlowAddress = "0xABC123DEF456"
+const auditorB_FlowAddress = "0xFFAA1212DEFF"
+
+await fcl.config().put("flow.auditors", [
+  auditorA_FlowAddress,
+  auditorB_FlowAddress
+])
+```
+
+Wallets can check if the auditors they configured FCL with have audited a given Interaction Template:
+
+```javascript
+import * as fcl from "@onflow/fcl"
+import myTransactionTemplate from "./my-transaction-template.template.json"
+
+const audits = await fcl.InteractionTemplateUtils
+  .getInteractionTemplateAudits({
+    template: myTransactionTemplate
+  })
+
+/**
+ * audits = {
+ *   "0xABC123DEF456": true,
+ *   "0xFFAA1212DEFF": false
+ * }
+ ** /
+```
+
+Since not all auditors that a wallet trusts may have audited a given Interaction Template, trusting multiple auditors can increase the chance that at least one of the trusted auditors has audited the Interaction Template.
+
+> ❗️ Auditors can revoke audits at any time, so be sure to always check an Interaction Template's audit status. 
+
+Since contracts on Flow are mutable, wallets may additionally wish to verify that none of the dependency tree for the transaction an Interaction Template represents has changed since when it was created and of what it was audited against.
+
+```javascript
+import * as fcl from "@onflow/fcl"
+import myTransactionTemplate from "./my-transaction-template.template.json"
+
+const hasDependencyTreeChanged = await fcl.InteractionTemplateUtils
+  .verifyDependencyPinsSameAtLatestSealedBlock({
+    template: myTransactionTemplate
+  })
+```
+
+If the dependency tree has changed, wallets may choose to disregard the Interaction Template (and it's audits).
+
+Once the Interaction Template has been sufficiently audited by auditors the wallet trusts, and it's dependency tree determined unchanged since the interaction was created and audited against, then the wallet can use the Interaction Template with greater confidence in it's correctness and safety.
+
+The wallet may then decide to render human readable information about the transaction such as:
+- Internationalized 'title' and 'description' of the transaction
+- Internationalized 'title' for each of the transactions arguments alongside the arguments value
+
+The wallet may then also make the status of it's audits known to the user in their UI. This allows the user to have greater confidence in the safety of the transaction. 
+
+## Data Structure
+
+The following is an example Interaction Template that corresponds to a "Transfer FLOW" transaction:
+
+```json
+{
+  "f_type": "InteractionTemplate",
+  "f_version": "1.0.0",
+  "id": "290b6b6222b2a77b16db896a80ddf29ebd1fa3038c9e6625a933fa213fce51fa",
+  "data": {
+    "type": "transaction",
+    "interface": "",
+    "messages": {
+      "title": {
+        "i18n": {
+          "en-US": "Transfer Tokens"
+        }
+      },
+      "description": {
+        "i18n": {
+          "en-US": "Transfer tokens from one account to another"
+        }
+      }
+    },
+    "cadence": "import FungibleToken from 0xFUNGIBLETOKENADDRESS\ntransaction(amount: UFix64, to: Address) {\nlet vault: @FungibleToken.Vault\nprepare(signer: AuthAccount) {\nself.vault <- signer\n.borrow<&{FungibleToken.Provider}>(from: /storage/flowTokenVault)!\n.withdraw(amount: amount)\n}\nexecute {\ngetAccount(to)\n.getCapability(/public/flowTokenReceiver)!\n.borrow<&{FungibleToken.Receiver}>()!\n.deposit(from: <-self.vault)\n}\n}",
+    "dependencies": {
+      "0xFUNGIBLETOKENADDRESS": {
+        "FungibleToken": {
+          "mainnet": {
+            "address": "0xf233dcee88fe0abe",
+            "fq_address": "A.0xf233dcee88fe0abe.FungibleToken",
+            "contract": "FungibleToken",
+            "pin": "83c9e3d61d3b5ebf24356a9f17b5b57b12d6d56547abc73e05f820a0ae7d9cf5",
+            "pin_block_height": 34166296
+          },
+          "testnet": {
+            "address": "0x9a0766d93b6608b7",
+            "fq_address": "A.0x9a0766d93b6608b7.FungibleToken",
+            "contract": "FungibleToken",
+            "pin": "83c9e3d61d3b5ebf24356a9f17b5b57b12d6d56547abc73e05f820a0ae7d9cf5",
+            "pin_block_height": 74776482
+          }
+        }
+      }
+    },
+    "arguments": {
+      "amount": {
+        "index": 0,
+        "type": "UFix64",
+        "messages": {
+          "title": {
+            "i18n": {
+              "en-US": "The amount of FLOW tokens to send"
+            }
+          }
+        }
+      },
+      "to": {
+        "index": 1,
+        "type": "Address",
+        "messages": {
+          "title": {
+            "i18n": {
+              "en-US": "The Flow account the tokens will go to"
+            }
+          }
+        }
+      }
+    }
+  }
+}
+```
diff --git a/packages/fcl-wc/package.json b/packages/fcl-wc/package.json
index 0ec7547d..e3dc51e7 100644
--- a/packages/fcl-wc/package.json
+++ b/packages/fcl-wc/package.json
@@ -15,6 +15,11 @@
   "jest": {
     "transform": {
       ".js": "jest-esm-transformer"
+    },
+    "testEnvironment": "./jest-test-environment.js",
+    "globals": {
+      "window": {},
+      "PACKAGE_CURRENT_VERSION": "TESTVERSION"
     }
   },
   "source": "src/fcl-wc.js",
diff --git a/packages/fcl/package.json b/packages/fcl/package.json
index 6e2bef68..eb9694a8 100644
--- a/packages/fcl/package.json
+++ b/packages/fcl/package.json
@@ -52,6 +52,7 @@
     "@onflow/util-invariant": "^1.0.2",
     "@onflow/util-logger": "^1.1.1",
     "@onflow/util-template": "^1.0.3",
-    "@onflow/util-uid": "^1.0.2"
+    "@onflow/util-uid": "^1.0.2",
+    "node-fetch": "^2.6.7"
   }
 }
diff --git a/packages/fcl/src/current-user/build-user.js b/packages/fcl/src/current-user/build-user.js
index 7a9a6d3b..f0b9414e 100644
--- a/packages/fcl/src/current-user/build-user.js
+++ b/packages/fcl/src/current-user/build-user.js
@@ -2,8 +2,8 @@ import {withPrefix} from "@onflow/util-address"
 import * as rlp from "@onflow/rlp"
 import {fetchServices} from "./fetch-services"
 import {mergeServices} from "./merge-services"
-import {USER_PRAGMA} from "./normalize/__vsn"
-import {normalizeService} from "./normalize/service"
+import {USER_PRAGMA} from "../normalizers/service/__vsn"
+import {normalizeService} from "../normalizers/service/service"
 
 function deriveCompositeId(authn) {
   return rlp
diff --git a/packages/fcl/src/current-user/exec-service/strategies/ext-rpc.js b/packages/fcl/src/current-user/exec-service/strategies/ext-rpc.js
index a0fa608e..bfd2775e 100644
--- a/packages/fcl/src/current-user/exec-service/strategies/ext-rpc.js
+++ b/packages/fcl/src/current-user/exec-service/strategies/ext-rpc.js
@@ -1,6 +1,6 @@
 import {invariant} from "@onflow/util-invariant"
 import {extension} from "./utils/extension"
-import {normalizePollingResponse} from "../../normalize/polling-response"
+import {normalizePollingResponse} from "../../../normalizers/service/polling-response"
 import {VERSION} from "../../../VERSION"
 
 export function execExtRPC({service, body, config, opts}) {
diff --git a/packages/fcl/src/current-user/exec-service/strategies/http-post.js b/packages/fcl/src/current-user/exec-service/strategies/http-post.js
index 9431be0a..18dd4515 100644
--- a/packages/fcl/src/current-user/exec-service/strategies/http-post.js
+++ b/packages/fcl/src/current-user/exec-service/strategies/http-post.js
@@ -1,6 +1,6 @@
 import {fetchService} from "./utils/fetch-service"
-import {normalizePollingResponse} from "../../normalize/polling-response"
-import {normalizeLocalView} from "../../normalize/local-view"
+import {normalizePollingResponse} from "../../../normalizers/service/polling-response"
+import {normalizeLocalView} from "../../../normalizers/service/local-view"
 import {poll} from "./utils/poll"
 import {execLocal} from "../exec-local"
 import {VERSION} from "../../../VERSION"
diff --git a/packages/fcl/src/current-user/exec-service/strategies/iframe-rpc.js b/packages/fcl/src/current-user/exec-service/strategies/iframe-rpc.js
index 078c5be4..1c819d83 100644
--- a/packages/fcl/src/current-user/exec-service/strategies/iframe-rpc.js
+++ b/packages/fcl/src/current-user/exec-service/strategies/iframe-rpc.js
@@ -1,6 +1,6 @@
 import {uid} from "@onflow/util-uid"
 import {frame} from "./utils/frame"
-import {normalizePollingResponse} from "../../normalize/polling-response"
+import {normalizePollingResponse} from "../../../normalizers/service/polling-response"
 import {VERSION} from "../../../VERSION"
 
 export function execIframeRPC({service, body, config, opts}) {
diff --git a/packages/fcl/src/current-user/exec-service/strategies/pop-rpc.js b/packages/fcl/src/current-user/exec-service/strategies/pop-rpc.js
index a78cdf90..a2641fe4 100644
--- a/packages/fcl/src/current-user/exec-service/strategies/pop-rpc.js
+++ b/packages/fcl/src/current-user/exec-service/strategies/pop-rpc.js
@@ -1,6 +1,6 @@
 import {uid} from "@onflow/util-uid"
 import {pop} from "./utils/pop"
-import {normalizePollingResponse} from "../../normalize/polling-response"
+import {normalizePollingResponse} from "../../../normalizers/service/polling-response"
 import {VERSION} from "../../../VERSION"
 
 export function execPopRPC({service, body, config, opts}) {
diff --git a/packages/fcl/src/current-user/exec-service/strategies/tab-rpc.js b/packages/fcl/src/current-user/exec-service/strategies/tab-rpc.js
index 4eef08d3..68b3830e 100644
--- a/packages/fcl/src/current-user/exec-service/strategies/tab-rpc.js
+++ b/packages/fcl/src/current-user/exec-service/strategies/tab-rpc.js
@@ -1,6 +1,6 @@
 import {uid} from "@onflow/util-uid"
 import {tab} from "./utils/tab"
-import {normalizePollingResponse} from "../../normalize/polling-response"
+import {normalizePollingResponse} from "../../../normalizers/service/polling-response"
 import {VERSION} from "../../../VERSION"
 
 export function execTabRPC({service, body, config, opts}) {
diff --git a/packages/fcl/src/current-user/exec-service/strategies/utils/poll.js b/packages/fcl/src/current-user/exec-service/strategies/utils/poll.js
index b62a45ee..685973d5 100644
--- a/packages/fcl/src/current-user/exec-service/strategies/utils/poll.js
+++ b/packages/fcl/src/current-user/exec-service/strategies/utils/poll.js
@@ -1,4 +1,4 @@
-import {normalizePollingResponse} from "../../../normalize/polling-response"
+import {normalizePollingResponse} from "../../../../normalizers/service/polling-response"
 import {serviceEndpoint} from "./service-endpoint"
 import {invariant} from "@onflow/util-invariant"
 import {fetchService} from "./fetch-service"
diff --git a/packages/fcl/src/current-user/index.js b/packages/fcl/src/current-user/index.js
index f17d7fb4..64c2b6cf 100644
--- a/packages/fcl/src/current-user/index.js
+++ b/packages/fcl/src/current-user/index.js
@@ -8,7 +8,7 @@ import {invariant} from "@onflow/util-invariant"
 import {buildUser} from "./build-user"
 import {serviceOfType} from "./service-of-type"
 import {execService} from "./exec-service"
-import {normalizeCompositeSignature} from "./normalize/composite-signature"
+import {normalizeCompositeSignature} from "../normalizers/service/composite-signature"
 import {configLens} from "../default-config"
 import {VERSION} from "../VERSION"
 import {getDiscoveryService, makeDiscoveryServices} from "../discovery"
diff --git a/packages/fcl/src/document/document.js b/packages/fcl/src/document/document.js
new file mode 100644
index 00000000..9a3a182b
--- /dev/null
+++ b/packages/fcl/src/document/document.js
@@ -0,0 +1,58 @@
+import {invariant} from "@onflow/util-invariant"
+import fetchTransport from "node-fetch"
+import {config} from "@onflow/config"
+
+async function httpDocumentResolver({url}) {
+  invariant(
+    typeof url !== "undefined",
+    "retrieve({ url }) -- url must be defined"
+  )
+
+  let res
+  try {
+    res = await fetchTransport(url)
+  } catch (e) {
+    throw new Error("httpDocumentResolver Error: Failed to retrieve document.")
+  }
+
+  let document = res.ok ? await res.json() : null
+
+  return document
+}
+
+const DOCUMENT_RESOLVERS = new Map([
+  ["http", httpDocumentResolver],
+  ["https", httpDocumentResolver],
+])
+
+export async function retrieve({url}) {
+  invariant(
+    typeof url !== "undefined",
+    "retrieve({ url }) -- url must be defined"
+  )
+  invariant(
+    typeof url === "string",
+    "retrieve({ url }) -- url must be a string"
+  )
+
+  const documentResolversFromConfig = await config().where(
+    /^document\.resolver\./
+  )
+  Object.keys(documentResolversFromConfig).map(key => {
+    const resolverFromConfig = documentResolversFromConfig[key]
+    const resolverProtocol = key.replace(/^document\.resolver\./, "")
+    DOCUMENT_RESOLVERS.set(resolverProtocol, resolverFromConfig)
+  })
+
+  const urlParts = /^(.*):\/\/([A-Za-z0-9\-\.]+)(:[0-9]+)?(.*)$/.exec(url)
+  invariant(urlParts, "Failed to parse URL")
+  const protocol = urlParts[1]
+  invariant(urlParts, "Failed to parse URL protocol")
+
+  const resolver = DOCUMENT_RESOLVERS.get(protocol)
+  invariant(resolver, `No resolver found for protcol=${protocol}`)
+
+  let document = await resolver({url})
+
+  return document
+}
diff --git a/packages/fcl/src/exec/mutate.js b/packages/fcl/src/exec/mutate.js
index 6d5bd797..e379730e 100644
--- a/packages/fcl/src/exec/mutate.js
+++ b/packages/fcl/src/exec/mutate.js
@@ -1,15 +1,17 @@
 import {invariant} from "@onflow/util-invariant"
 import * as sdk from "@onflow/sdk"
-import {isRequired, isObject, isString, isFunc, isNumber} from "./utils/is"
+import {isRequired, isObject, isString, isNumber} from "./utils/is"
 import {normalizeArgs} from "./utils/normalize-args"
 import {currentUser} from "../current-user"
-import {transaction} from "../transaction"
+import {prepTemplateOpts} from "./utils/prep-template-opts.js"
+import {preMutate} from "./utils/pre.js"
 
 /** As the current user Mutate the Flow Blockchain
  *
  *  @arg {Object} opts - Mutation Options and configuration
  *  @arg {string} opts.cadence - Cadence Transaction used to mutate Flow
  *  @arg {ArgsFn} opts.args - Arguments passed to cadence transaction
+ *  @arg {Object} opts.template - Interaction Template for a transaction
  *  @arg {number} opts.limit - Compute Limit for transaction
  *  @returns {string} Transaction Id
  *
@@ -46,6 +48,7 @@ import {transaction} from "../transaction"
  *
  *  Options:
  *    type Options = {
+ *      template: InteractionTemplate | String // InteractionTemplate or url to one
  *      cadence: String!,
  *      args: (arg, t) => Array<Arg>,
  *      limit: Number,
@@ -55,54 +58,38 @@ import {transaction} from "../transaction"
  *      authorizations: [AuthzFn], // an array of authorization functions used as authorizations signatory roles
  *    }
  */
-
-const noop = () => {}
-
 export async function mutate(opts = {}) {
   var txid
   try {
-    await prepMutation(opts)
+    await preMutate(opts)
+    opts = await prepTemplateOpts(opts)
 
     // Allow for a config to overwrite the authorization function.
     // prettier-ignore
     const authz = await sdk.config().get("fcl.authz", currentUser().authorization)
 
-    // prettier-ignore
-    txid = sdk.send([
-      sdk.transaction(opts.cadence),
+    txid = sdk.config().overload(opts.dependencies || {}, async () =>
+      // prettier-ignore
+      sdk.send([
+        sdk.transaction(opts.cadence),
 
-      sdk.args(normalizeArgs(opts.args || [])),
+        sdk.args(normalizeArgs(opts.args || [])),
 
-      opts.limit && isNumber(opts.limit) && sdk.limit(opts.limit),
+        opts.limit && isNumber(opts.limit) && sdk.limit(opts.limit),
 
-      // opts.proposer > opts.authz > authz
-      sdk.proposer(opts.proposer || opts.authz || authz),
+        // opts.proposer > opts.authz > authz
+        sdk.proposer(opts.proposer || opts.authz || authz),
 
-      // opts.payer > opts.authz > authz
-      sdk.payer(opts.payer || opts.authz || authz),
+        // opts.payer > opts.authz > authz
+        sdk.payer(opts.payer || opts.authz || authz),
 
-      // opts.authorizations > [opts.authz > authz]
-      sdk.authorizations(opts.authorizations || [opts.authz || authz]),
-    ]).then(sdk.decode)
+        // opts.authorizations > [opts.authz > authz]
+        sdk.authorizations(opts.authorizations || [opts.authz || authz]),
+      ]).then(sdk.decode)
+    )
 
     return txid
   } catch (error) {
     throw error
   }
 }
-
-async function prepMutation(opts) {
-  // prettier-ignore
-  invariant(isRequired(opts), "mutate(opts) -- opts is required")
-  // prettier-ignore
-  invariant(isObject(opts), "mutate(opts) -- opts must be an object")
-  // prettier-ignore
-  invariant(isRequired(opts.cadence), "mutate({ cadence }) -- cadence is required")
-  // prettier-ignore
-  invariant(isString(opts.cadence), "mutate({ cadence }) -- cadence must be a string")
-  // prettier-ignore
-  invariant(
-    await sdk.config.get("accessNode.api"),
-    `Required value for "accessNode.api" not defined in config. See: ${"https://github.com/onflow/flow-js-sdk/blob/master/packages/fcl/src/exec/query.md#configuration"}`
-  )
-}
diff --git a/packages/fcl/src/exec/mutate.md b/packages/fcl/src/exec/mutate.md
index 9997b287..b7d60335 100644
--- a/packages/fcl/src/exec/mutate.md
+++ b/packages/fcl/src/exec/mutate.md
@@ -6,7 +6,7 @@ Programmatically mutate state on Flow. A light, stable and approachable higher l
 
 ## Status
 
-- **Last Updated:** June 30th 2022
+- **Last Updated:** Aug 17th 2022
 - **Stable:** Yes
 - **Risk of Breaking Change:** Low
 - **Introduced:** `0.0.73`
@@ -17,10 +17,11 @@ Programmatically mutate state on Flow. A light, stable and approachable higher l
 You can learn more about configuring FCL here: [Configure FCL](https://github.com/onflow/flow-js-sdk/blob/master/docs/configure-fcl.mdx)
 Configuration only needs to happen once, but it must happen before mutate is called. We always recommend configuring FCL as early in your application as possible/reasonable.
 
-`mutate` requires two pieces of configuration.
+`mutate` requires a few pieces of configuration.
 
 - `accessNode.api` - How to talk to Flow.
 - `discovery.wallet` - How to connect to wallets.
+- `flow.network` - which network are you on ("mainnet", "testnet", etc).
 
 Below is an example of configuring FCL to talk to Flow (testnet)
 
@@ -31,6 +32,7 @@ import * as fcl from "@onflow/fcl"
 fcl.config()
   .put("accessNode.api", "https://rest-testnet.onflow.org")
   .put("discovery.wallet", "https://fcl-discovery.onflow.org/testnet/authn")
+  .put("flow.network", "testnet")
 ```
 
 `mutate` also respects [Address Replacement](https://github.com/onflow/flow-js-sdk/blob/master/docs/configure-fcl.mdx#address-replacement-in-scripts-and-transactions) configuration.
@@ -217,3 +219,32 @@ const txId = await fcl.mutate({
 ```
 
 More specific options overload less specific options. So `payer` will be used over `authz` which will be used over the default of current user.
+
+## Interaction Templates
+
+If you have an Interaction Template, you can use it with `mutate`:
+
+### Using Template JSON 
+
+```javascript
+import * as fcl from "@onflow/fcl"
+import myTransactionTemplate from "transaction-template.json"
+
+const txId = await fcl.mutate({
+  template: myTransactionTemplate,
+})
+```
+
+### Using Template URL 
+
+In place of a JSON template, you can specify a URL that points to one, and FCL will retrieve it from the remote location:
+
+```javascript
+import * as fcl from "@onflow/fcl"
+
+const txId = await fcl.mutate({
+  template: "https://interactions.my-project.com/buy-nft",
+})
+```
+
+`muatate` will use the Interaction Template to carry out it's underlying transaction. Read more on Interaction Templates with FCL [here](https://github.com/onflow/fcl-js/blob/master/docs/reference/interaction-templates.mdx)
diff --git a/packages/fcl/src/exec/query.js b/packages/fcl/src/exec/query.js
index c65162f1..e1c59436 100644
--- a/packages/fcl/src/exec/query.js
+++ b/packages/fcl/src/exec/query.js
@@ -1,14 +1,16 @@
 import {invariant} from "@onflow/util-invariant"
 import * as sdk from "@onflow/sdk"
-import * as t from "@onflow/types"
-import {isRequired, isObject, isString, isFunc} from "./utils/is"
+import {isRequired, isObject, isString} from "./utils/is"
 import {normalizeArgs} from "./utils/normalize-args"
+import {prepTemplateOpts} from "./utils/prep-template-opts.js"
+import {preQuery} from "./utils/pre.js"
 
 /** Query the Flow Blockchain
  *
  *  @arg {Object} opts         - Query Options and configuration
  *  @arg {string} opts.cadence - Cadence Script used to query Flow
  *  @arg {ArgsFn} opts.args    - Arguments passed to cadence script
+ *  @arg {Object} opts.template - Interaction Template for a script
  *  @arg {number} opts.limit   - Compute Limit for Query
  *  @returns {Promise<Response>}
  *
@@ -42,28 +44,14 @@ import {normalizeArgs} from "./utils/normalize-args"
  */
 export async function query(opts = {}) {
   await preQuery(opts)
+  opts = await prepTemplateOpts(opts)
 
-  // prettier-ignore
-  return sdk.send([
-    sdk.script(opts.cadence),
-    sdk.args(normalizeArgs(opts.args || [])),
-    opts.limit && typeof opts.limit === "number" && sdk.limit(opts.limit)
-  ]).then(sdk.decode)
-}
-
-async function preQuery(opts) {
-  invariant(
-    isRequired(opts.cadence),
-    "query({ cadence }) -- cadence is required"
-  )
-
-  invariant(
-    isString(opts.cadence),
-    "query({ cadence }) -- cadence must be a string"
-  )
-
-  invariant(
-    await sdk.config.get("accessNode.api"),
-    `Required value for "accessNode.api" not defined in config. See: ${"https://github.com/onflow/flow-js-sdk/blob/master/packages/fcl/src/exec/query.md#configuration"}`
+  return sdk.config().overload(opts.dependencies || {}, async () =>
+    // prettier-ignore
+    sdk.send([
+      sdk.script(opts.cadence),
+      sdk.args(normalizeArgs(opts.args || [])),
+      opts.limit && typeof opts.limit === "number" && sdk.limit(opts.limit)
+    ]).then(sdk.decode)
   )
 }
diff --git a/packages/fcl/src/exec/query.md b/packages/fcl/src/exec/query.md
index 026c9c10..d917c9ef 100644
--- a/packages/fcl/src/exec/query.md
+++ b/packages/fcl/src/exec/query.md
@@ -6,7 +6,7 @@ Programmatically Query Flow. A light, stable and approachable higher level wrapp
 
 ## Status
 
-- **Last Updated:** June 30th 2022
+- **Last Updated:** Aug 17th 2022
 - **Stable:** Yes
 - **Risk of Breaking Change:** Low
 - **Introduced:** `0.0.68`
@@ -17,14 +17,18 @@ Programmatically Query Flow. A light, stable and approachable higher level wrapp
 You can learn more about configuring FCL here: [Configure FCL](https://github.com/onflow/flow-js-sdk/blob/master/docs/configure-fcl.mdx)
 Configuration only needs to happen once, but it must happen before query is called. We always recommend configuring FCL as early in your application as possible/reasonable.
 
-The main thing you will want to configure is the `accessNode.api` which is the channel FCL will use to communicate with Flow.
+The main things you will need to configure is are:
+- `accessNode.api` - How to talk to Flow.
+- `flow.network` - which network are you on ("mainnet", "testnet", etc).
 
 Below is an example of configuring FCL to talk to Flow (testnet)
 
 ```javascript
 import * as fcl from "@onflow/fcl"
 
-fcl.config().put("accessNode.api", "https://rest-testnet.onflow.org")
+fcl.config()
+  .put("accessNode.api", "https://rest-testnet.onflow.org")
+  .put("flow.network", "testnet")
 ```
 
 `query` also respects [Address Replacement](https://github.com/onflow/flow-js-sdk/blob/master/docs/configure-fcl.mdx#address-replacement-in-scripts-and-transactions) configuration.
@@ -206,3 +210,33 @@ Which should return something like this:
   }
 }
 ```
+
+## Interaction Templates
+
+If you have an Interaction Template, you can use it with `query`:
+
+### Using Template JSON 
+
+```javascript
+import * as fcl from "@onflow/fcl"
+import myScriptTemplate from "script-template.json"
+
+const scriptResult = await fcl.query({
+  template: myScriptTemplate,
+})
+```
+
+### Using Template URL 
+
+In place of a JSON template, you can specify a URL that points to one, and FCL will retrieve it from the remote location:
+
+```javascript
+import * as fcl from "@onflow/fcl"
+
+const scriptResult = await fcl.mutate({
+  template: "https://interactions.my-project.com/read-nft",
+})
+```
+
+`query` will use the Interaction Template to carry out it's underlying script. Read more on Interaction Templates with FCL [here](https://github.com/onflow/fcl-js/blob/master/docs/reference/interaction-templates.mdx)
+
diff --git a/packages/fcl/src/exec/utils/derive-dependencies.js b/packages/fcl/src/exec/utils/derive-dependencies.js
new file mode 100644
index 00000000..a871062b
--- /dev/null
+++ b/packages/fcl/src/exec/utils/derive-dependencies.js
@@ -0,0 +1,56 @@
+import {config} from "@onflow/config"
+import {invariant} from "@onflow/util-invariant"
+import {withPrefix} from "@onflow/util-address"
+
+export async function deriveDependencies({template}) {
+  const network = await config.get("flow.network")
+
+  invariant(
+    network,
+    "FCL configureDependencies Error: Missing configuration value for 'flow.network'"
+  )
+
+  const derivedDependencies = {}
+
+  switch (template["f_version"]) {
+    case "1.0.0":
+      const dependencyPlaceholderKeys = Object.keys(
+        template?.data?.dependencies
+      )
+      for (let dependencyPlaceholderKey of dependencyPlaceholderKeys) {
+        let dependencyPlaceholder =
+          template?.data?.dependencies[dependencyPlaceholderKey]
+
+        const dependencyPlaceholderContractsKeys = Object.keys(
+          dependencyPlaceholder
+        )
+        invariant(
+          dependencyPlaceholderContractsKeys.length > 0,
+          `FCL configureDependencies Error: No contracts found in template for placeholder=${dependencyPlaceholderKey}`
+        )
+        const dependencyPlaceholderContract =
+          dependencyPlaceholder[dependencyPlaceholderContractsKeys[0]]
+        const dependency = dependencyPlaceholderContract[network]
+
+        invariant(
+          dependency,
+          `FCL configureDependencies Error: No dependency information for placeholder=${dependencyPlaceholderKey} contract=${dependencyPlaceholderContractsKeys[0]} network=${network}`
+        )
+        invariant(
+          dependency?.address,
+          `FCL configureDependencies Error: No address information for placeholder=${dependencyPlaceholderKey} contract=${dependencyPlaceholderContractsKeys[0]} network=${network}`
+        )
+
+        derivedDependencies[dependencyPlaceholderKey] = withPrefix(
+          dependency?.address
+        )
+      }
+
+      return derivedDependencies
+
+    default:
+      throw new Error(
+        "FCL configureDependencies Error: Unsupported template version"
+      )
+  }
+}
diff --git a/packages/fcl/src/exec/utils/pre.js b/packages/fcl/src/exec/utils/pre.js
new file mode 100644
index 00000000..af8496ff
--- /dev/null
+++ b/packages/fcl/src/exec/utils/pre.js
@@ -0,0 +1,37 @@
+import {invariant} from "@onflow/util-invariant"
+import * as sdk from "@onflow/sdk"
+import {isRequired, isObject, isString} from "./is"
+
+async function pre(type, opts) {
+  // prettier-ignore
+  invariant(isRequired(opts), `${type}(opts) -- opts is required`)
+  // prettier-ignore
+  invariant(isObject(opts), `${type}(opts) -- opts must be an object`)
+  // prettier-ignore
+  invariant(!(opts.cadence && opts.template), `${type}({ template, cadence }) -- cannot pass both cadence and template`)
+  // prettier-ignore
+  invariant(isRequired(opts.cadence || opts?.template), `${type}({ cadence }) -- cadence is required`)
+  // // prettier-ignore
+  invariant(
+    isString(opts.cadence) || opts?.template,
+    `${type}({ cadence }) -- cadence must be a string`
+  )
+  // prettier-ignore
+  invariant(
+    opts.cadence || (await sdk.config().get("flow.network")),
+    `${type}(opts) -- Required value for "flow.network" not defined in config. See: ${"https://github.com/onflow/flow-js-sdk/blob/master/packages/fcl/src/exec/query.md#configuration"}`
+  )
+  // prettier-ignore
+  invariant(
+    await sdk.config().get("accessNode.api"),
+    `${type}(opts) -- Required value for "accessNode.api" not defined in config. See: ${"https://github.com/onflow/flow-js-sdk/blob/master/packages/fcl/src/exec/query.md#configuration"}`
+  )
+}
+
+export async function preMutate(opts) {
+  return pre("mutate", opts)
+}
+
+export async function preQuery(opts) {
+  return pre("query", opts)
+}
diff --git a/packages/fcl/src/exec/utils/prep-template-opts.js b/packages/fcl/src/exec/utils/prep-template-opts.js
new file mode 100644
index 00000000..e5d9838d
--- /dev/null
+++ b/packages/fcl/src/exec/utils/prep-template-opts.js
@@ -0,0 +1,29 @@
+import {retrieve} from "../../document/document.js"
+import {normalizeInteractionTemplate} from "../../normalizers/interaction-template/interaction-template"
+import {deriveCadenceByNetwork} from "../../interaction-template-utils"
+import {deriveDependencies} from "./derive-dependencies"
+import {isString} from "./is"
+
+export async function prepTemplateOpts(opts) {
+  if (isString(opts?.template)) {
+    opts.template = await retrieve({url: opts?.template})
+  }
+
+  let dependencies = {}
+  if (opts?.template) {
+    opts.template = normalizeInteractionTemplate(opts?.template)
+    dependencies = await deriveDependencies({template: opts.template})
+  }
+
+  const cadence =
+    opts.cadence ||
+    deriveCadenceByNetwork({
+      template: opts.template,
+      network: await sdk.config().get("flow.network"),
+    })
+
+  opts.cadence = cadence
+  opts.dependencies = dependencies
+
+  return opts
+}
diff --git a/packages/fcl/src/fcl.js b/packages/fcl/src/fcl.js
index 4622b3c8..a385ec81 100644
--- a/packages/fcl/src/fcl.js
+++ b/packages/fcl/src/fcl.js
@@ -34,6 +34,9 @@ export {WalletUtils}
 import * as AppUtils from "./app-utils"
 export {AppUtils}
 
+import * as InteractionTemplateUtils from "./interaction-template-utils"
+export {InteractionTemplateUtils}
+
 export {TestUtils} from "@onflow/sdk"
 export {config} from "@onflow/config"
 export {send} from "@onflow/sdk"
diff --git a/packages/fcl/src/interaction-template-utils/derive-cadence-by-network.js b/packages/fcl/src/interaction-template-utils/derive-cadence-by-network.js
new file mode 100644
index 00000000..78dafc66
--- /dev/null
+++ b/packages/fcl/src/interaction-template-utils/derive-cadence-by-network.js
@@ -0,0 +1,69 @@
+import {invariant} from "@onflow/sdk"
+import {normalizeInteractionTemplate} from "../normalizers/interaction-template/interaction-template"
+
+export function deriveCadenceByNetwork({network, template}) {
+  invariant(
+    network != undefined,
+    "deriveCadenceByNetwork({ network }) -- network must be defined"
+  )
+  invariant(
+    typeof network === "string",
+    "deriveCadenceByNetwork({ network }) -- network must be a string"
+  )
+
+  invariant(
+    template != undefined,
+    "generateDependencyPin({ template }) -- template must be defined"
+  )
+  invariant(
+    typeof template === "object",
+    "generateDependencyPin({ template }) -- template must be an object"
+  )
+  invariant(
+    template.f_type === "InteractionTemplate",
+    "generateDependencyPin({ template }) -- template must be an InteractionTemplate"
+  )
+
+  template = normalizeInteractionTemplate(template)
+
+  switch (template.f_version) {
+    case "1.0.0":
+      let networkDependencies = Object.keys(template?.data?.dependencies).map(
+        dependencyPlaceholder => {
+          let dependencyNetworkContracts = Object.values(
+            template?.data?.dependencies?.[dependencyPlaceholder]
+          )
+
+          invariant(
+            dependencyNetworkContracts,
+            `deriveCadenceByNetwork -- Could not find contracts for dependency placeholder: ${dependencyPlaceholder}`
+          )
+
+          invariant(
+            dependencyNetworkContracts.length === 0,
+            `deriveCadenceByNetwork -- Could not find contracts for dependency placeholder: ${dependencyPlaceholder}`
+          )
+
+          let dependencyContract = dependencyNetworkContracts[0]
+          let dependencyContractForNetwork = dependencyContract?.[network]
+
+          invariant(
+            dependencyContractForNetwork,
+            `deriveCadenceByNetwork -- Could not find ${network} network information for dependency: ${dependencyPlaceholder}`
+          )
+
+          return [dependencyPlaceholder, dependencyContractForNetwork.address]
+        }
+      )
+
+      return networkDependencies.reduce((cadence, [placeholder, address]) => {
+        const regex = new RegExp("(\\b" + placeholder + "\\b)", "g")
+        return cadence.replace(regex, address)
+      }, template.data.cadence)
+
+    default:
+      throw new Error(
+        "deriveCadenceByNetwork Error: Unsupported template version"
+      )
+  }
+}
diff --git a/packages/fcl/src/interaction-template-utils/generate-dependency-pin.js b/packages/fcl/src/interaction-template-utils/generate-dependency-pin.js
new file mode 100644
index 00000000..39606cc3
--- /dev/null
+++ b/packages/fcl/src/interaction-template-utils/generate-dependency-pin.js
@@ -0,0 +1,86 @@
+import {
+  invariant,
+  block,
+  send,
+  getAccount,
+  atBlockHeight,
+  config,
+  decode,
+} from "@onflow/sdk"
+import {genHash} from "./utils/hash.js"
+import {findImports} from "./utils/find-imports.js"
+import {generateImport} from "./utils/generate-import.js"
+
+export async function generateDependencyPin(
+  {address, contractName, blockHeight},
+  opts = {}
+) {
+  invariant(
+    address != undefined,
+    "generateDependencyPin({ address }) -- address must be defined"
+  )
+  invariant(
+    contractName != undefined,
+    "generateDependencyPin({ contractName }) -- contractName must be defined"
+  )
+  invariant(
+    blockHeight != undefined,
+    "generateDependencyPin({ blockHeight }) -- blockHeight must be defined"
+  )
+  invariant(
+    typeof address === "string",
+    "generateDependencyPin({ address }) -- address must be a string"
+  )
+  invariant(
+    typeof contractName === "string",
+    "generateDependencyPin({ contractName }) -- contractName must be a string"
+  )
+  invariant(
+    typeof blockHeight === "number",
+    "generateDependencyPin({ blockHeight }) -- blockHeight must be a number"
+  )
+
+  let horizon = [generateImport({contractName, address})]
+
+  for (const horizonImport of horizon) {
+    let account = await send(
+      [
+        getAccount(
+          await config().get(horizonImport.address, horizonImport.address)
+        ),
+        atBlockHeight(blockHeight),
+      ],
+      opts
+    ).then(decode)
+
+    horizonImport.contract = account.contracts?.[horizonImport.contractName]
+
+    if (!horizonImport.contract) {
+      console.error("Did not find expected contract", horizonImport, account)
+      throw new Error("Did not find expected contract")
+    }
+
+    let contractImports = findImports(horizonImport.contract)
+
+    horizon.push(...contractImports)
+  }
+
+  let contractHashes = horizon.map(iport => genHash(iport.contract))
+
+  let contractHashesJoined = contractHashes.join("")
+
+  return genHash(contractHashesJoined)
+}
+
+export async function generateDependencyPinAtLatestSealedBlock(
+  {address, contractName},
+  opts = {}
+) {
+  let latestSealedBlock = await block({sealed: true}, opts)
+  let latestSealedBlockHeight = latestSealedBlock?.height
+
+  return generateDependencyPin(
+    {address, contractName, blockHeight: latestSealedBlockHeight},
+    opts
+  )
+}
diff --git a/packages/fcl/src/interaction-template-utils/generate-template-id.js b/packages/fcl/src/interaction-template-utils/generate-template-id.js
new file mode 100644
index 00000000..b0e61ca6
--- /dev/null
+++ b/packages/fcl/src/interaction-template-utils/generate-template-id.js
@@ -0,0 +1,151 @@
+import {invariant} from "@onflow/sdk"
+import {encode as rlpEncode} from "@onflow/rlp"
+import {genHash} from "./utils/hash.js"
+import {normalizeInteractionTemplate} from "../normalizers/interaction-template/interaction-template.js"
+
+export async function generateTemplateId({template}) {
+  invariant(
+    template != undefined,
+    "generateTemplateId({ template }) -- template must be defined"
+  )
+  invariant(
+    typeof template === "object",
+    "generateTemplateId({ template }) -- template must be an object"
+  )
+  invariant(
+    typeof template.f_type === "InteractionTemplate",
+    "generateTemplateId({ template }) -- template object must be an InteractionTemplate"
+  )
+
+  template = normalizeInteractionTemplate(template)
+
+  switch (template.f_version) {
+    case "1.0.0":
+      const templateData = template.data
+
+      const messages = await Promise.all(
+        Object.keys(templateData.messages).map(async messageKey => [
+          await genHash(messageKey),
+          await Promise.all(
+            Object.keys(templateData.messages?.[messageKey]?.i18n).map(
+              async i18nkeylanguage => [
+                await genHash(i18nkeylanguage),
+                await genHash(
+                  templateData.messages?.[messageKey]?.i18n?.[i18nkeylanguage]
+                ),
+              ]
+            )
+          ),
+        ])
+      )
+
+      const dependencies = await Promise.all(
+        Object.keys(templateData?.dependencies).map(
+          async dependencyAddressPlaceholder => [
+            await genHash(dependencyAddressPlaceholder),
+            await Promise.all(
+              Object.keys(
+                templateData?.dependencies?.[dependencyAddressPlaceholder]
+              ).map(async dependencyContract => [
+                await genHash(dependencyContract),
+                await Promise.all(
+                  Object.keys(
+                    templateData?.dependencies?.[
+                      dependencyAddressPlaceholder
+                    ]?.[dependencyContract]
+                  ).map(async dependencyContractNetwork => [
+                    await genHash(dependencyContractNetwork),
+                    [
+                      await genHash(
+                        templateData?.dependencies?.[
+                          dependencyAddressPlaceholder
+                        ]?.[dependencyContract]?.[dependencyContractNetwork]
+                          .address
+                      ),
+                      await genHash(
+                        templateData?.dependencies?.[
+                          dependencyAddressPlaceholder
+                        ]?.[dependencyContract]?.[dependencyContractNetwork]
+                          .contract
+                      ),
+                      await genHash(
+                        templateData?.dependencies?.[
+                          dependencyAddressPlaceholder
+                        ]?.[dependencyContract]?.[dependencyContractNetwork]
+                          .fq_address
+                      ),
+                      await genHash(
+                        templateData?.dependencies?.[
+                          dependencyAddressPlaceholder
+                        ]?.[dependencyContract]?.[dependencyContractNetwork].pin
+                      ),
+                      await genHash(
+                        String(
+                          templateData?.dependencies?.[
+                            dependencyAddressPlaceholder
+                          ]?.[dependencyContract]?.[dependencyContractNetwork]
+                            .pin_block_height
+                        )
+                      ),
+                    ],
+                  ])
+                ),
+              ])
+            ),
+          ]
+        )
+      )
+
+      const _arguments = await Promise.all(
+        Object.keys(templateData?.["arguments"]).map(async argumentLabel => [
+          await genHash(argumentLabel),
+          [
+            await genHash(
+              String(templateData?.["arguments"]?.[argumentLabel].index)
+            ),
+            await genHash(templateData?.["arguments"]?.[argumentLabel].type),
+            await genHash(
+              templateData?.["arguments"]?.[argumentLabel].balance || ""
+            ),
+            await Promise.all(
+              Object.keys(
+                templateData?.["arguments"]?.[argumentLabel].messages
+              ).map(async argumentMessageKey => [
+                await genHash(argumentMessageKey),
+                await Promise.all(
+                  Object.keys(
+                    templateData?.["arguments"]?.[argumentLabel].messages?.[
+                      argumentMessageKey
+                    ].i18n
+                  ).map(async i18nkeylanguage => [
+                    await genHash(i18nkeylanguage),
+                    await genHash(
+                      templateData?.["arguments"]?.[argumentLabel].messages?.[
+                        argumentMessageKey
+                      ].i18n?.[i18nkeylanguage]
+                    ),
+                  ])
+                ),
+              ])
+            ),
+          ],
+        ])
+      )
+
+      const encodedHex = rlpEncode([
+        await genHash("InteractionTemplate"),
+        await genHash("1.0.0"),
+        await genHash(templateData?.type),
+        await genHash(templateData?.interface),
+        messages,
+        await genHash(templateData?.cadence),
+        dependencies,
+        _arguments,
+      ]).toString("hex")
+
+      return genHash(encodedHex)
+
+    default:
+      throw new Error("generateTemplateId Error: Unsupported template version")
+  }
+}
diff --git a/packages/fcl/src/interaction-template-utils/generate-template-interface-id.js b/packages/fcl/src/interaction-template-utils/generate-template-interface-id.js
new file mode 100644
index 00000000..c6ce419a
--- /dev/null
+++ b/packages/fcl/src/interaction-template-utils/generate-template-interface-id.js
@@ -0,0 +1,45 @@
+import {encode as rlpEncode} from "@onflow/rlp"
+import {genHash} from "./utils/hash.js"
+import {normalizeInteractionTemplateInterface} from "../normalizers/interaction-template/interaction-template-interface.js"
+
+export async function generateTemplateInterfaceId({templateInterface}) {
+  invariant(
+    templateInterface != undefined,
+    "generateTemplateInterfaceId({ templateInterface }) -- templateInterface must be defined"
+  )
+  invariant(
+    typeof templateInterface === "object",
+    "generateTemplateInterfaceId({ templateInterface }) -- templateInterface must be an object"
+  )
+  invariant(
+    typeof templateInterface.f_type === "InteractionTemplateInterface",
+    "generateTemplateInterfaceId({ templateInterface }) -- templateInterface object must be an InteractionTemplate"
+  )
+
+  templateInterface = normalizeInteractionTemplateInterface(templateInterface)
+
+  switch (templateInterface.f_version) {
+    case "1.0.0":
+      const interfaceData = templateInterface.data
+
+      const encodedHex = rlpEncode([
+        await genHash("InteractionTemplateInterface"),
+        await genHash("1.0.0"),
+        await genHash(interfaceData.flip),
+        await Promise.all(
+          Object.keys(interfaceData.arguments).map(async argumentLabel => [
+            await genHash(argumentLabel),
+            await genHash(String(interfaceData.arguments[argumentLabel].index)),
+            await genHash(interfaceData.arguments[argumentLabel].type),
+          ])
+        ),
+      ]).toString("hex")
+
+      return genHash(encodedHex)
+
+    default:
+      throw new Error(
+        "generateTemplateInterfaceId Error: Unsupported templateInterface version"
+      )
+  }
+}
diff --git a/packages/fcl/src/interaction-template-utils/get-interaction-template-audits.js b/packages/fcl/src/interaction-template-utils/get-interaction-template-audits.js
new file mode 100644
index 00000000..c9b31a48
--- /dev/null
+++ b/packages/fcl/src/interaction-template-utils/get-interaction-template-audits.js
@@ -0,0 +1,84 @@
+import {config, invariant} from "@onflow/sdk"
+import {log, LEVELS} from "@onflow/util-logger"
+import {query} from "../exec/query.js"
+import {generateTemplateId} from "./generate-template-id.js"
+import {normalizeInteractionTemplate} from "../normalizers/interaction-template/interaction-template.js"
+
+export async function getInteractionTemplateAudits({template, auditors}) {
+  invariant(
+    template != undefined,
+    "getInteractionTemplateAudits({ template }) -- template must be defined"
+  )
+
+  template = normalizeInteractionTemplate(template)
+
+  invariant(
+    template.f_type === "InteractionTemplate",
+    "getInteractionTemplateAudits({ template }) -- template must be an InteractionTemplate"
+  )
+
+  // Recompute ID to be sure it matches
+  let recomputedTemplateID = await generateTemplateId({template})
+
+  if (recomputedTemplateID !== template.id) {
+    log({
+      title: "getInteractionTemplateAudits Debug Error",
+      message: `Could not recompute and match template ID
+                computed: ${recomputedTemplateID}
+                template: ${template.id}
+            `,
+      level: LEVELS.debug,
+    })
+    throw new Error(
+      "getInteractionTemplateAudits Error: Could not recompute and match template ID"
+    )
+  }
+
+  switch (template.f_version) {
+    case "1.0.0":
+      const _auditors = auditors || (await config().get("flow.auditors"))
+
+      invariant(
+        _auditors,
+        "getInteractionTemplateAudits Error: Required configuration for 'fcl.auditors' is not set"
+      )
+      invariant(
+        Array.isArray(_auditors),
+        "getInteractionTemplateAudits Error: Required configuration for 'fcl.auditors' is not an array"
+      )
+
+      let FlowInteractionAuditContract = opts.flowInteractionAuditContract
+      if (!FlowInteractionAuditContract) {
+        const fclNetwork = await config().get("flow.network")
+        invariant(
+          fclNetwork === "mainnet" || fclNetwork === "testnet",
+          "getInteractionTemplateAudits Error: Unable to determine address for FlowInteractionTemplateAudit contract. Set configuration for 'fcl.network' to 'mainnet' or 'testnet'"
+        )
+        if (fclNetwork === "mainnet") {
+          FlowInteractionAuditContract = "0xb4b82a1c9d21d284"
+        } else {
+          FlowInteractionAuditContract = "0x74daa6f9c7ef24b1"
+        }
+      }
+
+      const audits = await query({
+        cadence: `
+        import FlowInteractionTemplateAudit from ${FlowInteractionAuditContract}
+        pub fun main(templateId: String, auditors: [Address]): {Address:Bool} {
+          return FlowInteractionAudit.getHasTemplateBeenAuditedByAuditors(templateId: templateId, auditors: auditors)
+        }
+        `,
+        args: (arg, t) => [
+          arg(recomputedTemplateID, t.String),
+          arg(_auditors, t.Array(t.Address)),
+        ],
+      })
+
+      return audits
+
+    default:
+      throw new Error(
+        "getInteractionTemplateAudits Error: Unsupported template version"
+      )
+  }
+}
diff --git a/packages/fcl/src/interaction-template-utils/index.js b/packages/fcl/src/interaction-template-utils/index.js
new file mode 100644
index 00000000..b81bd26b
--- /dev/null
+++ b/packages/fcl/src/interaction-template-utils/index.js
@@ -0,0 +1,12 @@
+export {getInteractionTemplateAudits} from "./get-interaction-template-audits.js"
+export {
+  generateDependencyPin,
+  generateDependencyPinAtLatestSealedBlock,
+} from "./generate-dependency-pin.js"
+export {generateTemplateId} from "./generate-template-id.js"
+export {generateTemplateInterfaceId} from "./generate-template-interface-id.js"
+export {
+  verifyDependencyPinsSame,
+  verifyDependencyPinsSameAtLatestSealedBlock,
+} from "./verify-dependency-pin-same-at-block.js"
+export {deriveCadenceByNetwork} from "./derive-cadence-by-network.js"
diff --git a/packages/fcl/src/interaction-template-utils/utils/find-imports.js b/packages/fcl/src/interaction-template-utils/utils/find-imports.js
new file mode 100644
index 00000000..228effdd
--- /dev/null
+++ b/packages/fcl/src/interaction-template-utils/utils/find-imports.js
@@ -0,0 +1,27 @@
+import {generateImport} from "./generate-import.js"
+
+export function findImports(cadence) {
+  let imports = []
+
+  let importsReg = /import ((\w|,| )+)* from 0x\w+/g
+  let fileImports = cadence.match(importsReg) || []
+
+  for (const fileImport of fileImports) {
+    const importLineReg = /import ((\w+|, |)*) from (0x\w+)/g
+    const importLine = importLineReg.exec(fileImport)
+
+    const contractsReg = /((?:\w+)+),?/g
+    const contracts = importLine[1].match(contractsReg) || []
+
+    for (const contract of contracts) {
+      imports.push(
+        generateImport({
+          address: importLine[3],
+          contractName: contract.replace(/,/g, ""),
+        })
+      )
+    }
+  }
+
+  return imports
+}
diff --git a/packages/fcl/src/interaction-template-utils/utils/generate-import.js b/packages/fcl/src/interaction-template-utils/utils/generate-import.js
new file mode 100644
index 00000000..de0227de
--- /dev/null
+++ b/packages/fcl/src/interaction-template-utils/utils/generate-import.js
@@ -0,0 +1,3 @@
+export function generateImport({contractName, address}) {
+  return {contractName, address, contract: ""}
+}
diff --git a/packages/fcl/src/interaction-template-utils/utils/hash.js b/packages/fcl/src/interaction-template-utils/utils/hash.js
new file mode 100644
index 00000000..c65c5488
--- /dev/null
+++ b/packages/fcl/src/interaction-template-utils/utils/hash.js
@@ -0,0 +1,8 @@
+import {SHA3} from "sha3"
+import {Buffer} from "@onflow/rlp"
+
+export async function genHash(utf8String) {
+  const sha = new SHA3(256)
+  sha.update(Buffer.from(utf8String, "utf8"))
+  return sha.digest("hex")
+}
diff --git a/packages/fcl/src/interaction-template-utils/verify-dependency-pin-same-at-block.js b/packages/fcl/src/interaction-template-utils/verify-dependency-pin-same-at-block.js
new file mode 100644
index 00000000..d43f9685
--- /dev/null
+++ b/packages/fcl/src/interaction-template-utils/verify-dependency-pin-same-at-block.js
@@ -0,0 +1,106 @@
+import {generateDependencyPin} from "./generate-dependency-pin.js"
+import {invariant, block} from "@onflow/sdk"
+import {log, LEVELS} from "@onflow/util-logger"
+import {normalizeInteractionTemplate} from "../normalizers/interaction-template/interaction-template.js"
+
+export async function verifyDependencyPinsSame(
+  {template, blockHeight, network},
+  opts = {}
+) {
+  invariant(
+    template != undefined,
+    "generateDependencyPin({ template }) -- template must be defined"
+  )
+  invariant(
+    typeof template === "object",
+    "generateDependencyPin({ template }) -- template must be an object"
+  )
+  invariant(
+    template.f_type === "InteractionTemplate",
+    "generateDependencyPin({ template }) -- template must be an InteractionTemplate"
+  )
+
+  template = normalizeInteractionTemplate(template)
+
+  invariant(
+    network != undefined,
+    "generateDependencyPin({ network }) network must be defined"
+  )
+  invariant(
+    blockHeight != undefined,
+    "generateDependencyPin({ blockHeight }) blockHeight must be defined"
+  )
+  invariant(
+    typeof blockHeight === "number",
+    "generateDependencyPin({ blockHeight }) blockHeight must be a number"
+  )
+
+  switch (template.f_version) {
+    case "1.0.0":
+      let templateDependenciesPlaceholderKeys = Object.keys(
+        template.data.dependencies
+      )
+
+      for (let templateDependencyPlaceholderKey of templateDependenciesPlaceholderKeys) {
+        let templateDependencyPlaceholder =
+          template.data.dependencies[templateDependencyPlaceholderKey]
+
+        let templateDependencyPlaceholderContractNames = Object.keys(
+          templateDependencyPlaceholder
+        )
+
+        for (let templateDependencyPlaceholderContractName of templateDependencyPlaceholderContractNames) {
+          let templateDependencyPlaceholderContractNetworks =
+            template.data.dependencies[templateDependencyPlaceholderKey][
+              templateDependencyPlaceholderContractName
+            ]
+
+          let templateDependency =
+            templateDependencyPlaceholderContractNetworks[network]
+          if (typeof templateDependency === "undefined") continue
+
+          let pin = await generateDependencyPin(
+            {
+              address: templateDependency.address,
+              contractName: templateDependency.contract,
+              blockHeight,
+            },
+            opts
+          )
+
+          if (pin !== templateDependency.pin) {
+            log({
+              title: "verifyDependencyPinsSame Debug Error",
+              message: `Could not recompute and match dependency pin.
+                                address: ${templateDependency.address} | contract: ${templateDependency.contract}
+                                computed: ${pin}
+                                template: ${templateDependency.pin}
+                            `,
+              level: LEVELS.debug,
+            })
+            return false
+          }
+        }
+      }
+
+      return true
+
+    default:
+      throw new Error(
+        "verifyDependencyPinsSame Error: Unsupported template version"
+      )
+  }
+}
+
+export async function verifyDependencyPinsSameAtLatestSealedBlock(
+  {template, network},
+  opts = {}
+) {
+  let latestSealedBlock = await block({sealed: true})
+  let latestSealedBlockHeight = latestSealedBlock?.height
+
+  return verifyDependencyPinsSame(
+    {template, network, blockHeight: latestSealedBlockHeight},
+    opts
+  )
+}
diff --git a/packages/fcl/src/normalizers/interaction-template/__vsn.js b/packages/fcl/src/normalizers/interaction-template/__vsn.js
new file mode 100644
index 00000000..fda933d9
--- /dev/null
+++ b/packages/fcl/src/normalizers/interaction-template/__vsn.js
@@ -0,0 +1,9 @@
+export const INTERACTION_TEMPLATE_PRAGMA = {
+  f_type: "InteractionTemplate",
+  f_version: "1.0.0",
+}
+
+export const INTERACTION_TEMPLATE_INTERFACE_PRAGMA = {
+  f_type: "InteractionTemplateInterface",
+  f_version: "1.0.0",
+}
diff --git a/packages/fcl/src/normalizers/interaction-template/interaction-template-interface.js b/packages/fcl/src/normalizers/interaction-template/interaction-template-interface.js
new file mode 100644
index 00000000..109c3cd3
--- /dev/null
+++ b/packages/fcl/src/normalizers/interaction-template/interaction-template-interface.js
@@ -0,0 +1,13 @@
+export function normalizeInteractionTemplateInterface(templateInterface) {
+  if (templateInterface == null) return null
+
+  switch (templateInterface["f_version"]) {
+    case "1.0.0":
+      return templateInterface
+
+    default:
+      throw new Error(
+        "normalizeInteractionTemplateInterface Error: Invalid InteractionTemplateInterface"
+      )
+  }
+}
diff --git a/packages/fcl/src/normalizers/interaction-template/interaction-template.js b/packages/fcl/src/normalizers/interaction-template/interaction-template.js
new file mode 100644
index 00000000..459906df
--- /dev/null
+++ b/packages/fcl/src/normalizers/interaction-template/interaction-template.js
@@ -0,0 +1,13 @@
+export function normalizeInteractionTemplate(template) {
+  if (template == null) return null
+
+  switch (template["f_version"]) {
+    case "1.0.0":
+      return template
+
+    default:
+      throw new Error(
+        "normalizeInteractionTemplate Error: Invalid InteractionTemplate"
+      )
+  }
+}
diff --git a/packages/fcl/src/current-user/normalize/__vsn.js b/packages/fcl/src/normalizers/service/__vsn.js
similarity index 100%
rename from packages/fcl/src/current-user/normalize/__vsn.js
rename to packages/fcl/src/normalizers/service/__vsn.js
diff --git a/packages/fcl/src/current-user/normalize/account-proof.js b/packages/fcl/src/normalizers/service/account-proof.js
similarity index 100%
rename from packages/fcl/src/current-user/normalize/account-proof.js
rename to packages/fcl/src/normalizers/service/account-proof.js
diff --git a/packages/fcl/src/current-user/normalize/authn-refresh.js b/packages/fcl/src/normalizers/service/authn-refresh.js
similarity index 100%
rename from packages/fcl/src/current-user/normalize/authn-refresh.js
rename to packages/fcl/src/normalizers/service/authn-refresh.js
diff --git a/packages/fcl/src/current-user/normalize/authn.js b/packages/fcl/src/normalizers/service/authn.js
similarity index 100%
rename from packages/fcl/src/current-user/normalize/authn.js
rename to packages/fcl/src/normalizers/service/authn.js
diff --git a/packages/fcl/src/current-user/normalize/authz.js b/packages/fcl/src/normalizers/service/authz.js
similarity index 100%
rename from packages/fcl/src/current-user/normalize/authz.js
rename to packages/fcl/src/normalizers/service/authz.js
diff --git a/packages/fcl/src/current-user/normalize/back-channel-rpc.js b/packages/fcl/src/normalizers/service/back-channel-rpc.js
similarity index 100%
rename from packages/fcl/src/current-user/normalize/back-channel-rpc.js
rename to packages/fcl/src/normalizers/service/back-channel-rpc.js
diff --git a/packages/fcl/src/current-user/normalize/composite-signature.js b/packages/fcl/src/normalizers/service/composite-signature.js
similarity index 100%
rename from packages/fcl/src/current-user/normalize/composite-signature.js
rename to packages/fcl/src/normalizers/service/composite-signature.js
diff --git a/packages/fcl/src/current-user/normalize/frame.js b/packages/fcl/src/normalizers/service/frame.js
similarity index 100%
rename from packages/fcl/src/current-user/normalize/frame.js
rename to packages/fcl/src/normalizers/service/frame.js
diff --git a/packages/fcl/src/current-user/normalize/local-view.js b/packages/fcl/src/normalizers/service/local-view.js
similarity index 100%
rename from packages/fcl/src/current-user/normalize/local-view.js
rename to packages/fcl/src/normalizers/service/local-view.js
diff --git a/packages/fcl/src/current-user/normalize/open-id.js b/packages/fcl/src/normalizers/service/open-id.js
similarity index 100%
rename from packages/fcl/src/current-user/normalize/open-id.js
rename to packages/fcl/src/normalizers/service/open-id.js
diff --git a/packages/fcl/src/current-user/normalize/polling-response.js b/packages/fcl/src/normalizers/service/polling-response.js
similarity index 100%
rename from packages/fcl/src/current-user/normalize/polling-response.js
rename to packages/fcl/src/normalizers/service/polling-response.js
diff --git a/packages/fcl/src/current-user/normalize/pre-authz.js b/packages/fcl/src/normalizers/service/pre-authz.js
similarity index 100%
rename from packages/fcl/src/current-user/normalize/pre-authz.js
rename to packages/fcl/src/normalizers/service/pre-authz.js
diff --git a/packages/fcl/src/current-user/normalize/service.js b/packages/fcl/src/normalizers/service/service.js
similarity index 100%
rename from packages/fcl/src/current-user/normalize/service.js
rename to packages/fcl/src/normalizers/service/service.js
diff --git a/packages/fcl/src/current-user/normalize/user-signature.js b/packages/fcl/src/normalizers/service/user-signature.js
similarity index 100%
rename from packages/fcl/src/current-user/normalize/user-signature.js
rename to packages/fcl/src/normalizers/service/user-signature.js
diff --git a/packages/fcl/src/wallet-utils/CompositeSignature.js b/packages/fcl/src/wallet-utils/CompositeSignature.js
index 53e23efa..13b8e9bf 100644
--- a/packages/fcl/src/wallet-utils/CompositeSignature.js
+++ b/packages/fcl/src/wallet-utils/CompositeSignature.js
@@ -1,5 +1,5 @@
 import {withPrefix} from "@onflow/util-address"
-import {COMPOSITE_SIGNATURE_PRAGMA} from "../current-user/normalize/__vsn"
+import {COMPOSITE_SIGNATURE_PRAGMA} from "../normalizers/service/__vsn"
 
 export function CompositeSignature(addr, keyId, signature) {
   this.f_type = COMPOSITE_SIGNATURE_PRAGMA.f_type


Test patch:
diff --git a/packages/fcl-wc/jest-test-environment.js b/packages/fcl-wc/jest-test-environment.js
new file mode 100644
index 00000000..0393e0ed
--- /dev/null
+++ b/packages/fcl-wc/jest-test-environment.js
@@ -0,0 +1,12 @@
+const Environment = require("jest-environment-jsdom")
+
+module.exports = class CustomTestEnvironment extends Environment {
+  async setup() {
+    await super.setup()
+    if (typeof this.global.TextEncoder === "undefined") {
+      const {TextEncoder, TextDecoder} = require("util")
+      this.global.TextEncoder = TextEncoder
+      this.global.TextDecoder = TextDecoder
+    }
+  }
+}
diff --git a/packages/fcl/src/document/document.test.js b/packages/fcl/src/document/document.test.js
new file mode 100644
index 00000000..43a29330
--- /dev/null
+++ b/packages/fcl/src/document/document.test.js
@@ -0,0 +1,18 @@
+import {retrieve} from "./document.js"
+import {config} from "@onflow/config"
+
+describe("resolveArguments", () => {
+  
+    test("Should invoke resolver", async () => {
+        const templateResolver = jest.fn()
+
+        const ret = await config.overload({
+            "document.resolver.testprotocol": templateResolver
+        }, async d => {
+            await retrieve({ url: "testprotocol://example.test" })
+
+            expect(templateResolver.mock.calls.length).toEqual(1)
+        })
+    })
+
+})
\ No newline at end of file
diff --git a/packages/fcl/src/interaction-template-utils/derive-cadence-by-network.test.js b/packages/fcl/src/interaction-template-utils/derive-cadence-by-network.test.js
new file mode 100644
index 00000000..b21c67b6
--- /dev/null
+++ b/packages/fcl/src/interaction-template-utils/derive-cadence-by-network.test.js
@@ -0,0 +1,54 @@
+import {deriveCadenceByNetwork} from "./derive-cadence-by-network.js"
+
+describe("Derive cadence by network", () => {
+  let template = {
+    f_type: "InteractionTemplate",
+    f_version: "1.0.0",
+    id: "abc123",
+    data: {
+      type: "transaction",
+      interface: "",
+      messages: {},
+      cadence: "import FungibleToken from 0xFUNGIBLETOKENADDRESS\n",
+      dependencies: {
+        "0xFUNGIBLETOKENADDRESS": {
+          FungibleToken: {
+            mainnet: {
+              address: "0xf233dcee88fe0abe",
+              fq_address: "A.0xf233dcee88fe0abe.FungibleToken",
+              contract: "FungibleToken",
+              pin: "83c9e3d61d3b5ebf24356a9f17b5b57b12d6d56547abc73e05f820a0ae7d9cf5",
+              pin_block_height: 34166296,
+            },
+            testnet: {
+              address: "0x9a0766d93b6608b7",
+              fq_address: "A.0x9a0766d93b6608b7.FungibleToken",
+              contract: "FungibleToken",
+              pin: "83c9e3d61d3b5ebf24356a9f17b5b57b12d6d56547abc73e05f820a0ae7d9cf5",
+              pin_block_height: 74776482,
+            },
+          },
+        },
+      },
+      arguments: {},
+    },
+  }
+
+  test("It derives cadence correctly for a given network", async () => {
+    let cadence = deriveCadenceByNetwork({
+      network: "mainnet",
+      template,
+    })
+
+    expect(cadence).toEqual("import FungibleToken from 0xf233dcee88fe0abe\n")
+  })
+
+  test("It fails to derive cadence for unknown network", async () => {
+    expect(() =>
+      deriveCadenceByNetwork({
+        network: "randomnet",
+        template,
+      })
+    ).toThrow(Error)
+  })
+})
diff --git a/packages/fcl/src/interaction-template-utils/utils/find-imports.test.js b/packages/fcl/src/interaction-template-utils/utils/find-imports.test.js
new file mode 100644
index 00000000..d3fe4d8b
--- /dev/null
+++ b/packages/fcl/src/interaction-template-utils/utils/find-imports.test.js
@@ -0,0 +1,88 @@
+import {findImports} from "./find-imports.js"
+import {generateImport} from "./generate-import.js"
+
+describe("Find imports", () => {
+  const cadenceA = `
+        import ContractA from 0xABC123
+        import ContractB from 0xDEF456
+
+        pub fun main(): Int {
+            return 12
+        }
+    `
+
+  const cadenceB = `
+        import ContractA from 0xABC123
+        import ContractB from 0xDEF456
+        import Crypto
+
+        pub fun main(): Int {
+            return 12
+        }
+    `
+
+  const cadenceC = `
+    // Go Flow!
+    import Crypto
+    import ContractA, ContractB, ContractD from 0xABC123
+    import ContractC from 0xDEF456
+    import Crypto
+
+    pub fun main(): Int {
+        return 12
+    }
+`
+
+  test("It parses contracts correctly for cadence A", async () => {
+    let imports = findImports(cadenceA)
+
+    expect(imports).toEqual([
+      generateImport({
+        contractName: "ContractA",
+        address: "0xABC123",
+      }),
+      generateImport({
+        contractName: "ContractB",
+        address: "0xDEF456",
+      }),
+    ])
+  })
+
+  test("It parses contracts correctly for cadence B", async () => {
+    let imports = findImports(cadenceA)
+
+    expect(imports).toEqual([
+      generateImport({
+        contractName: "ContractA",
+        address: "0xABC123",
+      }),
+      generateImport({
+        contractName: "ContractB",
+        address: "0xDEF456",
+      }),
+    ])
+  })
+
+  test("It parses contracts correctly for cadence C", async () => {
+    let imports = findImports(cadenceC)
+
+    expect(imports).toEqual([
+      generateImport({
+        contractName: "ContractA",
+        address: "0xABC123",
+      }),
+      generateImport({
+        contractName: "ContractB",
+        address: "0xABC123",
+      }),
+      generateImport({
+        contractName: "ContractD",
+        address: "0xABC123",
+      }),
+      generateImport({
+        contractName: "ContractC",
+        address: "0xDEF456",
+      }),
+    ])
+  })
+})