Repository: onflow/fcl-js
Pull request #1511 (2022-12-12 21:35:35)

Support new import syntax through config.load

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

Latest commit: 1ceb97c70701d9b86d8744ee11e881d1fd05ccf5

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/GH-1486-Support-new-flowjson-and-import-syntax-through-configload?filename=.changeset/polite-dingos-invent.md&value=---%0A%22%40onflow%2Fconfig%22%3A%20patch%0A---%0A%0AGh%201486%20support%20new%20flowjson%20and%20import%20syntax%20through%20configload%0A)


sideninja: Looks good, left couple of comments, I think it's good idea to add another test proving the resolution of address
chasefleming: I just found a bug in the load. It wasn't created in this PR, but can we fix it in it? The bug is that it still warns about private keys if you use a CLI env var like `$FLOW_EMULATOR_PRIVATE_KEY` instead of the from path. Can we also just make it not flag if you're using the $ method for env vars from CLI
fotescodev: @justinbarry sir! We just got another good piece of feedback on `flow dev` / `flow setup` commands, and it's dependent on the FCL's support of the new JSON Schema: https://github.com/onflow/flow-cli/issues/826

How can we move forward with this PR? 
justinbarry: > I just found a bug in the load. It wasn't created in this PR, but can we fix it in it? The bug is that it still warns about private keys if you use a CLI env var like `$FLOW_EMULATOR_PRIVATE_KEY` instead of the from path. Can we also just make it not flag if you're using the $ method for env vars from CLI

Let's make that another issue so I can get this out for testing. My hope was we would get this into master ready to release, but I don't think it is going to work out that way.
justinbarry: Given Flow.json V2 isn't out in the wild yet, I ripped out all the references to it, and we can add them back later at a future date. The only outstanding question I have is what happens when a contract in flow.json has both a alias and a deployment.... which one wine?  @sideninja Let's talk about this in the morning. Otherwise, this should be good to go.

sideninja: > 

It shouldn't have it, but even it does always prioritize the alias. Ofc it can have deployments and alias in case of a specific network. So you can imagine it has an alias for testnet but it's also in deployments because on emulator the deployments is used since there's no alias for emulator. So for the network you are using if the alias exists use that, otherwise you can use the deployment.

Patch:
diff --git a/.changeset/khaki-apricots-train.md b/.changeset/khaki-apricots-train.md
new file mode 100644
index 00000000..740fd445
--- /dev/null
+++ b/.changeset/khaki-apricots-train.md
@@ -0,0 +1,5 @@
+---
+"@onflow/config": minor
+---
+
+Put contract addr in `system.contracts.*` namespace
diff --git a/.changeset/shiny-bobcats-fold.md b/.changeset/shiny-bobcats-fold.md
new file mode 100644
index 00000000..960a2fd5
--- /dev/null
+++ b/.changeset/shiny-bobcats-fold.md
@@ -0,0 +1,5 @@
+---
+"@onflow/config": minor
+---
+
+Add support for `import "ContractName"` syntax in scripts and transactions.
diff --git a/packages/config/src/config.js b/packages/config/src/config.js
index 9ca1ee99..21434ebb 100644
--- a/packages/config/src/config.js
+++ b/packages/config/src/config.js
@@ -119,12 +119,9 @@ function resetConfig(oldConfig) {
 async function load(data) {
   const network = await get("flow.network")
   const cleanedNetwork = cleanNetwork(network)
-  const { flowJSON } = data
+  const {flowJSON} = data
 
-  invariant(
-    Boolean(flowJSON),
-    "config.load -- 'flowJSON' must be defined"
-  )
+  invariant(Boolean(flowJSON), "config.load -- 'flowJSON' must be defined")
 
   invariant(
     cleanedNetwork,
@@ -132,7 +129,7 @@ async function load(data) {
   )
 
   if (anyHasPrivateKeys(flowJSON)) {
-    const isEmulator = cleanedNetwork === 'emulator'
+    const isEmulator = cleanedNetwork === "emulator"
 
     logger.log({
       title: "Private Keys Detected",
@@ -143,25 +140,43 @@ async function load(data) {
     if (!isEmulator) return
   }
 
-  for (const [key, value] of Object.entries(getContracts(flowJSON, cleanedNetwork))) {
+  for (const [key, value] of Object.entries(
+    getContracts(flowJSON, cleanedNetwork)
+  )) {
     const contractConfigKey = `0x${key}`
     const existingContractConfigKey = await get(contractConfigKey)
-
-    if (existingContractConfigKey) {
+    if (existingContractConfigKey && existingContractConfigKey !== value) {
       logger.log({
         title: "Contract Placeholder Conflict Detected",
-        message: `A generated contract placeholder from config.load and a placeholder you've set manually in config have the same name.`,
+        message: `A generated contract placeholder from config.load conflicts with a placeholder you've set manually in config have the same name.`,
         level: logger.LEVELS.warn,
       })
     } else {
       put(contractConfigKey, value)
     }
+
+    const systemContractConfigKey = `system.contracts.${key}`
+    const systemExistingContractConfigKeyValue = await get(
+      systemContractConfigKey
+    )
+    if (
+      systemExistingContractConfigKeyValue &&
+      systemExistingContractConfigKeyValue !== value
+    ) {
+      logger.log({
+        title: "Contract Placeholder Conflict Detected",
+        message: `A generated contract placeholder from config.load conflicts with a placeholder you've set manually in config have the same name.`,
+        level: logger.LEVELS.warn,
+      })
+    } else {
+      put(systemContractConfigKey, value)
+    }
   }
 }
 
 /**
  * Takes an object of config keys and returns an object with config methods
- * 
+ *
  * @param {Object} values
  * @returns {Object} config
  * @returns {Function} config.put
@@ -174,11 +189,11 @@ async function load(data) {
  * @returns {Function} config.subscribe
  * @returns {Function} config.overload
  * @returns {Function} config.load
- * 
+ *
  * @example
  * import {config} from "@onflow/fcl"
  * config({ "flow.network": "testnet" })
- * 
+ *
  */
 function config(values) {
   if (values != null && typeof values === "object") {
diff --git a/packages/config/utils/utils.js b/packages/config/utils/utils.js
index 5867006a..ca99de77 100644
--- a/packages/config/utils/utils.js
+++ b/packages/config/utils/utils.js
@@ -1,20 +1,36 @@
-const pipe = (...funcs) => v => {
-  return funcs.reduce((res, func) => {
-    return func(res)
-  }, v)
-}
+const pipe =
+  (...funcs) =>
+  v => {
+    return funcs.reduce((res, func) => {
+      return func(res)
+    }, v)
+  }
+
+/***
+ * Merge multiple functions returning objects into one object.
+ * @param {...function(*): Object} funcs
+ * @return {Object}
+ */
+const mergePipe =
+  (...funcs) =>
+  v => {
+    return funcs.reduce((res, func) => {
+      return {...res, ...func(v)}
+    }, {})
+  }
 
 /**
  * Object check.
  * @param value
  * @returns {boolean}
  */
-const isObject = value => value && typeof value === 'object' && !Array.isArray(value)
+const isObject = value =>
+  value && typeof value === "object" && !Array.isArray(value)
 
 /**
  * Deep merge multiple objects.
  * @param {Object} target
- * @param {Object[]} ...sources
+ * @param {...Object[]} sources
  * @returns {Object}
  */
 const mergeDeep = (target, ...sources) => {
@@ -24,10 +40,10 @@ const mergeDeep = (target, ...sources) => {
   if (isObject(target) && isObject(source)) {
     for (const key in source) {
       if (isObject(source[key])) {
-        if (!target[key]) Object.assign(target, { [key]: {} })
+        if (!target[key]) Object.assign(target, {[key]: {}})
         mergeDeep(target[key], source[key])
       } else {
-        Object.assign(target, { [key]: source[key] })
+        Object.assign(target, {[key]: source[key]})
       }
     }
   }
@@ -35,26 +51,37 @@ const mergeDeep = (target, ...sources) => {
   return mergeDeep(target, ...sources)
 }
 
+/**
+ * Support if/then/else behavior in a function way.
+ * @param{function(Object): boolean} testFn
+ * @param{function(Object): *} posCond - Function to run if testFn is true
+ * @param{function(Object): *} negCond - Function to run it testFn is false
+ * @returns {function(*): *}
+ */
+export const ifElse = (testFn, posCond, negCond) => obj =>
+  testFn(obj) ? posCond(obj) : negCond(obj)
+
 /**
  * Deep merge multiple Flow JSON.
  * @param {Object|Object[]} value
  * @returns {Object}
  */
-const mergeFlowJSONs = value => Array.isArray(value) ? mergeDeep({}, ...value) : value
+const mergeFlowJSONs = value =>
+  Array.isArray(value) ? mergeDeep({}, ...value) : value
 
 /**
  * Filter out contracts section of flow.json.
  * @param {Object|Object[]} obj
  * @returns {Object}
  */
-const filterContracts = obj => obj.contracts ? obj.contracts : {}
+const filterContracts = obj => (obj.contracts ? obj.contracts : {})
 
 /**
  * Gathers contract addresses by network
  * @param {string} network emulator, testnet, mainnet
  * @returns {Object} { "HelloWorld": "0x123" }
  */
-const mapContractToNetworkAddress = network => contracts => {
+const mapContractAliasesToNetworkAddress = network => contracts => {
   return Object.entries(contracts).reduce((c, [key, value]) => {
     const networkContractAlias = value?.aliases?.[network]
     if (networkContractAlias) {
@@ -65,6 +92,24 @@ const mapContractToNetworkAddress = network => contracts => {
   }, {})
 }
 
+const mapDeploymentsToNetworkAddress =
+  network =>
+  ({deployments = {}, accounts = {}}) => {
+    const networkDeployment = deployments?.[network]
+    if (!networkDeployment) return {}
+
+    return Object.entries(networkDeployment).reduce((c, [key, value]) => {
+      // Resolve account address
+      const accountAddress = accounts[key]?.address
+      if (!accountAddress) return c
+
+      // Create an object assigning the address to the contract name.
+      return value.reduce((c, contract) => {
+        return {...c, [contract]: accountAddress}
+      }, {})
+    }, {})
+  }
+
 /**
  * Take in flow.json files and return contract to address mapping by network
  * @param {Object|Object[]} jsons
@@ -74,8 +119,10 @@ const mapContractToNetworkAddress = network => contracts => {
 export const getContracts = (jsons, network) => {
   return pipe(
     mergeFlowJSONs,
-    filterContracts,
-    mapContractToNetworkAddress(network)
+    mergePipe(
+      mapDeploymentsToNetworkAddress(network),
+      pipe(filterContracts, mapContractAliasesToNetworkAddress(network))
+    )
   )(jsons)
 }
 
@@ -85,10 +132,13 @@ export const getContracts = (jsons, network) => {
  * @returns {boolean}
  */
 const hasPrivateKeys = flowJSON => {
-  return Object.entries(flowJSON?.accounts).reduce((hasPrivateKey, [key, value]) => {
-    if (hasPrivateKey) return true
-    return value?.hasOwnProperty("key")
-  }, false)
+  return Object.entries(flowJSON?.accounts).reduce(
+    (hasPrivateKey, [key, value]) => {
+      if (hasPrivateKey) return true
+      return value?.hasOwnProperty("key") && !value?.key?.startsWith("$")
+    },
+    false
+  )
 }
 
 /**
@@ -106,4 +156,5 @@ export const anyHasPrivateKeys = value => {
  * @param {string} network 'local', 'emulator', 'testnet', 'mainnet'
  * @returns {string} 'emulator', 'testnet', 'mainnet'
  */
-export const cleanNetwork = network => network?.toLowerCase() === 'local' ? 'emulator' : network?.toLowerCase()
\ No newline at end of file
+export const cleanNetwork = network =>
+  network?.toLowerCase() === "local" ? "emulator" : network?.toLowerCase()
diff --git a/packages/sdk/src/resolve/resolve-cadence.js b/packages/sdk/src/resolve/resolve-cadence.js
index 2d741259..0b53077f 100644
--- a/packages/sdk/src/resolve/resolve-cadence.js
+++ b/packages/sdk/src/resolve/resolve-cadence.js
@@ -1,20 +1,42 @@
 import {isTransaction, isScript, get} from "../interaction/interaction.js"
 import {invariant} from "@onflow/util-invariant"
 import {config} from "@onflow/config"
+import * as logger from "@onflow/util-logger"
 
 const isFn = v => typeof v === "function"
 const isString = v => typeof v === "string"
 
+const oldIdentifierPatternFn = () => /\b(0x\w+)\b/g
+function isOldIdentifierSyntax(cadence) {
+  return oldIdentifierPatternFn().test(cadence)
+}
+
+const newIdentifierPatternFn = () => /import\s+"(\w+)"/g
+function isNewIdentifierSyntax(cadence) {
+  return newIdentifierPatternFn().test(cadence)
+}
+
+function getContractIdentifierSyntaxMatches(cadence) {
+  return cadence.matchAll(newIdentifierPatternFn())
+}
+
 export async function resolveCadence(ix) {
-  if (isTransaction(ix) || isScript(ix)) {
-    var cadence = get(ix, "ix.cadence")
-    invariant(
-      isFn(cadence) || isString(cadence),
-      "Cadence needs to be a function or a string."
-    )
-    if (isFn(cadence)) cadence = await cadence({})
-    invariant(isString(cadence), "Cadence needs to be a string at this point.")
-    ix.message.cadence = await config()
+  if (!isTransaction(ix) && !isScript(ix)) return ix
+
+  var cadence = get(ix, "ix.cadence")
+
+  invariant(
+    isFn(cadence) || isString(cadence),
+    "Cadence needs to be a function or a string."
+  )
+  if (isFn(cadence)) cadence = await cadence({})
+  invariant(isString(cadence), "Cadence needs to be a string at this point.")
+  invariant(
+    !isOldIdentifierSyntax(cadence) || !isNewIdentifierSyntax(cadence),
+    "Both account identifier and contract identifier syntax not simultaneously supported."
+  )
+  if (isOldIdentifierSyntax(cadence)) {
+    cadence = await config()
       .where(/^0x/)
       .then(d =>
         Object.entries(d).reduce((cadence, [key, value]) => {
@@ -24,5 +46,28 @@ export async function resolveCadence(ix) {
       )
   }
 
+  if (isNewIdentifierSyntax(cadence)) {
+    for (const [fullMatch, contractName] of getContractIdentifierSyntaxMatches(
+      cadence
+    )) {
+      const address = await config().get(`system.contracts.${contractName}`)
+      if (address) {
+        cadence = cadence.replace(
+          fullMatch,
+          `import ${contractName} from ${address}`
+        )
+      } else {
+        logger.log({
+          title: "Contract Placeholder not found",
+          message: `Cannot find a value for contract placeholder ${contractName}. Please add to your flow.json or explicitly add it to the config 'contracts.*' namespace.`,
+          level: logger.LEVELS.warn,
+        })
+      }
+    }
+  }
+
+  // We need to move this over in any case.
+  ix.message.cadence = cadence
+
   return ix
 }


Test patch:
diff --git a/packages/config/src/config.test.js b/packages/config/src/config.test.js
index 1e5e4a73..f385ccfa 100644
--- a/packages/config/src/config.test.js
+++ b/packages/config/src/config.test.js
@@ -3,86 +3,89 @@ import {config, clearConfig} from "./config"
 const idle = () => new Promise(resolve => setTimeout(resolve), 0)
 
 describe("config()", () => {
-  clearConfig()
-
-  config({
-    "config.test.init": "rawr",
+  beforeEach(async () => {
+    clearConfig()
   })
 
-  config()
-    .put("config.test.t", "t")
-    .put("config.test.z", "z")
-    .put("config.test.foo.bar", "bar")
-    .put("config.test.foo.baz", "baz")
-    .put("config.test.wat.bar", "foo")
+  describe("crud methods", () => {
+    beforeEach(async () => {
+      config({
+        "config.test.init": "rawr",
+      })
 
-  test("get", async () => {
-    expect(await config().get("config.test.foo.bar")).toBe("bar")
-    expect(await config().get("config.test.init")).toBe("rawr")
-  })
+      config()
+        .put("config.test.t", "t")
+        .put("config.test.z", "z")
+        .put("config.test.foo.bar", "bar")
+        .put("config.test.foo.baz", "baz")
+        .put("config.test.wat.bar", "foo")
+    })
 
-  test("get with fallback", async () => {
-    expect(await config().get("config.test.not.a.thing", "fallback")).toBe(
-      "fallback"
-    )
-  })
+    test("get", async () => {
+      expect(await config().get("config.test.foo.bar")).toBe("bar")
+      expect(await config().get("config.test.init")).toBe("rawr")
+    })
 
-  test("update", async () => {
-    config().update("config.test.t", v => v + v)
-    expect(await config().get("config.test.t")).toBe("tt")
-  })
+    test("get with fallback", async () => {
+      expect(await config().get("config.test.not.a.thing", "fallback")).toBe(
+        "fallback"
+      )
+    })
 
-  test("delete", async () => {
-    config().delete("config.test.z")
-    expect(await config().get("config.test.z")).toBe(undefined)
-  })
+    test("update", async () => {
+      config().update("config.test.t", v => v + v)
+      expect(await config().get("config.test.t")).toBe("tt")
+    })
 
-  test("where", async () => {
-    expect(await config().where(/^config.test.foo/)).toEqual({
-      "config.test.foo.bar": "bar",
-      "config.test.foo.baz": "baz",
+    test("delete", async () => {
+      config().delete("config.test.z")
+      expect(await config().get("config.test.z")).toBe(undefined)
     })
-  })
 
-  test("subscribe", async () => {
-    const fn1 = jest.fn()
-    const unsub = config().subscribe(fn1)
-    await idle()
+    test("where", async () => {
+      expect(await config().where(/^config.test.foo/)).toEqual({
+        "config.test.foo.bar": "bar",
+        "config.test.foo.baz": "baz",
+      })
+    })
 
-    config().put("config.test.y", "y").put("config.test.x", "x")
+    test("subscribe", async () => {
+      const fn1 = jest.fn()
+      const unsub = config().subscribe(fn1)
+      await idle()
 
-    await idle()
-    unsub()
-    await idle()
+      config().put("config.test.y", "y").put("config.test.x", "x")
 
-    config().update("config.test.y", v => v + v)
+      await idle()
+      unsub()
+      await idle()
 
-    await idle()
+      config().update("config.test.y", v => v + v)
 
-    expect(fn1).toHaveBeenCalledTimes(3)
-  })
+      await idle()
 
-  test("all", async () => {
-    expect(await config().all()).toEqual({
-      "config.test.foo.bar": "bar",
-      "config.test.foo.baz": "baz",
-      "config.test.init": "rawr",
-      "config.test.t": "tt",
-      "config.test.wat.bar": "foo",
-      "config.test.x": "x",
-      "config.test.y": "yy",
+      expect(fn1).toHaveBeenCalledTimes(3)
     })
 
-    await clearConfig()
+    test("all", async () => {
+      expect(await config().all()).toEqual({
+        "config.test.foo.bar": "bar",
+        "config.test.foo.baz": "baz",
+        "config.test.init": "rawr",
+        "config.test.t": "t",
+        "config.test.wat.bar": "foo",
+        "config.test.z": "z",
+      })
+    })
   })
 
   test("empty", async () => {
+    clearConfig()
+    await idle()
     expect(await config().all()).toEqual({})
   })
 
   describe("sans ()", () => {
-    afterEach(clearConfig)
-
     test("config(data)", async () => {
       const data = {
         foo: "bar",
@@ -101,8 +104,6 @@ describe("config()", () => {
   })
 
   describe("overload", () => {
-    afterEach(clearConfig)
-
     test("overload", async () => {
       const PRE = {
         yes: "yes",
@@ -118,7 +119,7 @@ describe("config()", () => {
 
       config(PRE)
       expect(await config.all()).toEqual(PRE)
-      const ret = await config.overload(POST, async d => {
+      const ret = await config.overload(POST, async () => {
         expect(await config.all()).toEqual({...PRE, ...POST})
         return "WOOT WOOT"
       })
@@ -153,4 +154,183 @@ describe("config()", () => {
       })
     }
   })
+
+  describe("load method", () => {
+    describe("with a set network", () => {
+      beforeEach(async () => {
+        // Just picked a random network. Behavior might differ based on network selection at a future date.
+        await config().put("flow.network", "emulator")
+      })
+
+      describe("flow.json v1", () => {
+        let flowJSON
+
+        describe("without aliases", () => {
+          beforeEach(async () => {
+            flowJSON = {
+              accounts: {
+                "emulator-account": {
+                  fromFile: "./emulator.private.json",
+                },
+              },
+              contracts: {
+                HelloWorld: {
+                  source: "./cadence/contracts/HelloWorld.cdc",
+                  aliases: {
+                    emulator: "0x1",
+                    testnet: "0x01",
+                    mainnet: "0x001",
+                  },
+                },
+                SecondLife: {
+                  aliases: {
+                    testnet: "0x02",
+                    mainnet: "0x002",
+                  },
+                },
+              },
+              deployments: {
+                emulator: {
+                  "emulator-account": ["HelloWorld"],
+                },
+              },
+              networks: {
+                emulator: "127.0.0.1:3569",
+              },
+            }
+          })
+
+          describe("with single config loaded", () => {
+            beforeEach(async () => {
+              await config().load({flowJSON})
+              await idle()
+            })
+
+            test("should load the contract location wrt the currently set network", async () => {
+              await expect(config().get("0xHelloWorld")).resolves.toBe("0x1")
+              await expect(
+                config().get("system.contracts.HelloWorld")
+              ).resolves.toBe("0x1")
+            })
+
+            test("should not set a contract if it does not have an alias", async () => {
+              await expect(
+                config().get("0xSecondLife")
+              ).resolves.toBeUndefined()
+              await expect(
+                config().get("system.contracts.SecondLife")
+              ).resolves.toBeUndefined()
+            })
+          })
+
+          describe("with an array of configs loaded", () => {
+            beforeEach(async () => {
+              let secondFlowJSON = {
+                accounts: {},
+                contracts: {
+                  ThirdContract: {
+                    aliases: {
+                      emulator: "0x3",
+                      testnet: "0x03",
+                      mainnet: "0x003",
+                    },
+                  },
+                },
+                deployments: {},
+                networks: {},
+              }
+
+              await config().load({flowJSON: [flowJSON, secondFlowJSON]})
+              await idle()
+            })
+
+            test("should load the contract locations from the secondFlowJSON", async () => {
+              await expect(config().get("0xThirdContract")).resolves.toBe("0x3")
+              await expect(
+                config().get("system.contracts.ThirdContract")
+              ).resolves.toBe("0x3")
+            })
+          })
+        })
+        describe("without contract aliases", () => {
+          beforeEach(async () => {
+            flowJSON = {
+              networks: {
+                emulator: "127.0.0.1:3569",
+              },
+              accounts: {
+                default: {
+                  address: "f8d6e0586b0a20c7",
+                  key: "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646",
+                },
+              },
+              contracts: {
+                HelloWorld: "./cadence/contracts/HelloWorld.cdc",
+              },
+              deployments: {
+                emulator: {
+                  default: ["HelloWorld"],
+                },
+              },
+            }
+            await config().load({flowJSON})
+            await idle()
+          })
+
+          test("should resolve to the default address through the deployment link", async () => {
+            await expect(config().get("0xHelloWorld")).resolves.toBe(
+              "f8d6e0586b0a20c7"
+            )
+            await expect(
+              config().get("system.contracts.HelloWorld")
+            ).resolves.toBe("f8d6e0586b0a20c7")
+          })
+        })
+
+        describe("with both contract aliases and deployments", () => {
+          beforeEach(async () => {
+            flowJSON = {
+              networks: {
+                emulator: "127.0.0.1:3569",
+              },
+              accounts: {
+                default: {
+                  address: "f8d6e0586b0a20c7",
+                  key: "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646",
+                },
+              },
+              contracts: {
+                HelloWorld: {
+                  source: "./cadence/contracts/HelloWorld.cdc",
+                  aliases: {
+                    emulator: "0x1",
+                  },
+                },
+              },
+              deployments: {
+                emulator: {
+                  default: ["HelloWorld"],
+                },
+              },
+            }
+            await config().load({flowJSON})
+            await idle()
+          })
+
+          test("should return the alias, not the deployment address", async () => {
+            await expect(config().get("0xHelloWorld")).resolves.toBe("0x1")
+            await expect(
+              config().get("system.contracts.HelloWorld")
+            ).resolves.toBe("0x1")
+          })
+        })
+      })
+    })
+  })
+
+  describe("without a network set", () => {
+    test("should throw an error", async () => {
+      await expect(() => config().load({flowJSON: {}})).rejects.toThrowError()
+    })
+  })
 })
diff --git a/packages/config/utils/utils.test.js b/packages/config/utils/utils.test.js
index 4e422891..e821253d 100644
--- a/packages/config/utils/utils.test.js
+++ b/packages/config/utils/utils.test.js
@@ -3,55 +3,55 @@ import {getContracts, anyHasPrivateKeys} from "./utils"
 describe("getContracts", () => {
   test("it should gather contract aliases for flow.json", () => {
     const flowJSON = {
-      "networks": {
-        "emulator": "127.0.0.1:3569",
-        "mainnet": "access.mainnet.nodes.onflow.org:9000",
-        "testnet": "access.devnet.nodes.onflow.org:9000"
+      networks: {
+        emulator: "127.0.0.1:3569",
+        mainnet: "access.mainnet.nodes.onflow.org:9000",
+        testnet: "access.devnet.nodes.onflow.org:9000",
       },
-      "contracts": {
-        "HelloWorld": {
-          "source": "./cadence/contracts/HelloWorld.cdc",
-          "aliases": {
-            "emulator": "0x123",
-            "testnet": "0x124",
-            "mainnet": "0x125"
-          }
+      contracts: {
+        HelloWorld: {
+          source: "./cadence/contracts/HelloWorld.cdc",
+          aliases: {
+            emulator: "0x123",
+            testnet: "0x124",
+            mainnet: "0x125",
+          },
+        },
+        FooBar: {
+          source: "./cadence/contracts/FooBar.cdc",
+          aliases: {
+            emulator: "0x223",
+            testnet: "0x224",
+            mainnet: "0x225",
+          },
         },
-        "FooBar": {
-          "source": "./cadence/contracts/FooBar.cdc",
-          "aliases": {
-            "emulator": "0x223",
-            "testnet": "0x224",
-            "mainnet": "0x225"
-          }
-        }
       },
-      "accounts": {
+      accounts: {
         "emulator-account": {
-          "address": "f8d6e0586b0a20c7",
-          "key": "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646"
-        }
+          address: "f8d6e0586b0a20c7",
+          key: "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646",
+        },
+      },
+      deployments: {
+        emulator: {
+          "emulator-account": ["HelloWorld"],
+        },
       },
-      "deployments": {
-        "emulator": {
-          "emulator-account": ["HelloWorld"]
-        }
-      }
     }
 
     const emulatorMappings = {
-      "HelloWorld": "0x123",
-      "FooBar": "0x223"
+      HelloWorld: "0x123",
+      FooBar: "0x223",
     }
 
     const testnetMappings = {
-      "HelloWorld": "0x124",
-      "FooBar": "0x224"
+      HelloWorld: "0x124",
+      FooBar: "0x224",
     }
 
     const mainnetMappings = {
-      "HelloWorld": "0x125",
-      "FooBar": "0x225"
+      HelloWorld: "0x125",
+      FooBar: "0x225",
     }
 
     expect(getContracts(flowJSON, "emulator")).toEqual(emulatorMappings)
@@ -68,29 +68,29 @@ describe("getContracts", () => {
 describe("anyHasPrivateKeys", () => {
   test("it should return true if private keys exist in account", () => {
     const flowJSON = {
-      "accounts": {
+      accounts: {
         "emulator-account": {
-          "address": "f8d6e0586b0a20c7",
-          "key": "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646"
-        }
-      }
+          address: "f8d6e0586b0a20c7",
+          key: "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646",
+        },
+      },
     }
 
     const flowJSONTwo = {
-      "accounts": {
+      accounts: {
         "testnet-account": {
-          "address": "f8d6e0586b0a20c7",
-          "key": "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646"
-        }
-      }
+          address: "f8d6e0586b0a20c7",
+          key: "ba68d45a5acaa52f3cacf4ad3a64d9523e0ce0ae3addb1ee6805385b380b7646",
+        },
+      },
     }
 
     const flowJSONThree = {
-      "accounts": {
+      accounts: {
         "emulator-account": {
-          "address": "f8d6e0586b0a20c7"
-        }
-      }
+          address: "f8d6e0586b0a20c7",
+        },
+      },
     }
 
     expect(anyHasPrivateKeys(flowJSON)).toBe(true)
@@ -98,24 +98,37 @@ describe("anyHasPrivateKeys", () => {
     expect(anyHasPrivateKeys([flowJSON, flowJSONTwo, flowJSONThree])).toBe(true)
   })
 
+  test("should return false for environmental variables", async () => {
+    const flowJSON = {
+      accounts: {
+        "emulator-account": {
+          address: "f8d6e0586b0a20c7",
+          key: "$FLOW_EMULATOR_PRIVATE_KEY",
+        },
+      },
+    }
+
+    expect(anyHasPrivateKeys(flowJSON)).toBe(false)
+  })
+
   test("it should return false if no private keys exist in account", () => {
     const flowJSON = {
-      "accounts": {
+      accounts: {
         "emulator-account": {
-          "address": "f8d6e0586b0a20c7"
-        }
-      }
+          address: "f8d6e0586b0a20c7",
+        },
+      },
     }
 
     const flowJSONTwo = {
-      "accounts": {
+      accounts: {
         "emulator-account": {
-          "address": "f8d6e0586b0a20c7"
-        }
-      }
+          address: "f8d6e0586b0a20c7",
+        },
+      },
     }
 
     expect(anyHasPrivateKeys(flowJSON)).toBe(false)
     expect(anyHasPrivateKeys([flowJSON, flowJSONTwo])).toBe(false)
   })
-})
\ No newline at end of file
+})
diff --git a/packages/sdk/src/resolve/resolve-cadence.test.js b/packages/sdk/src/resolve/resolve-cadence.test.js
index 8320d6fe..0c28c02d 100644
--- a/packages/sdk/src/resolve/resolve-cadence.test.js
+++ b/packages/sdk/src/resolve/resolve-cadence.test.js
@@ -2,70 +2,71 @@ import {interaction, pipe, put, makeScript} from "../interaction/interaction.js"
 import {resolveCadence} from "./resolve-cadence.js"
 import {config} from "@onflow/config"
 
-const log = msg => ix => (console.log(msg, ix), ix)
+const idle = () => new Promise(resolve => setTimeout(resolve), 0)
 
 describe("resolveCadence", () => {
-  test("cadence is a string", async () => {
-    const CADENCE = "CADENCE_STRING"
-
-    const ix = await pipe([
-      makeScript,
-      put("ix.cadence", CADENCE),
-      resolveCadence,
-    ])(interaction())
-
-    expect(ix.message.cadence).toBe(CADENCE)
-  })
-
-  test("cadence is a function", async () => {
-    const CADENCE = async function () {
-      return "CADENCE_ASYNC_FUNCTION"
-    }
-
-    const ix = await pipe([
-      makeScript,
-      put("ix.cadence", CADENCE),
-      resolveCadence,
-    ])(interaction())
-
-    expect(ix.message.cadence).toBe(await CADENCE())
-  })
-
-  test("replaces all addresses from config", async () => {
-    const CADENCE = async function () {
-      return `
+  describe("0xHelloWorld-style account identifier syntax", () => {
+    test("cadence is a string", async () => {
+      const CADENCE = "CADENCE_STRING"
+
+      const ix = await pipe([
+        makeScript,
+        put("ix.cadence", CADENCE),
+        resolveCadence,
+      ])(interaction())
+
+      expect(ix.message.cadence).toBe(CADENCE)
+    })
+
+    test("cadence is a function", async () => {
+      const CADENCE = async function () {
+        return "CADENCE_ASYNC_FUNCTION"
+      }
+
+      const ix = await pipe([
+        makeScript,
+        put("ix.cadence", CADENCE),
+        resolveCadence,
+      ])(interaction())
+
+      expect(ix.message.cadence).toBe(await CADENCE())
+    })
+
+    test("replaces all addresses from config", async () => {
+      const CADENCE = async function () {
+        return `
         import MyContract from 0xMY_CONTRACT_ADDRESS
   
         pub fun main(): Address {
           return 0xMY_CONTRACT_ADDRESS
         }
       `
-    }
+      }
 
-    const RESULT = async function () {
-      return `
+      const RESULT = async function () {
+        return `
         import MyContract from 0x123abc
   
         pub fun main(): Address {
           return 0x123abc
         }
       `
-    }
+      }
 
-    config.put("0xMY_CONTRACT_ADDRESS", "0x123abc")
+      await config.put("0xMY_CONTRACT_ADDRESS", "0x123abc")
 
-    const ix = await pipe([
-      makeScript,
-      put("ix.cadence", CADENCE),
-      resolveCadence,
-    ])(interaction())
+      const ix = await pipe([
+        makeScript,
+        put("ix.cadence", CADENCE),
+        resolveCadence,
+      ])(interaction())
 
-    expect(ix.message.cadence).toEqual(await RESULT())
-  })
+      expect(ix.message.cadence).toEqual(await RESULT())
+    })
 
-  test("similar config names do not replace each other", async () => {
-    const CADENCE = async function () {
-      return `
+    test("similar config names do not replace each other", async () => {
+      const CADENCE = async function () {
+        return `
         import FooBar from 0xFoo
         import FooBar from 0xFooBar
 
@@ -82,10 +83,10 @@ describe("resolveCadence", () => {
         pub fun otherTwo(): Address {return 0xFoo}
         pub fun otherThree(): Address {return 0xFooBar}
       `
-    }
+      }
 
-    const RESULT = async function () {
-      return `
+      const RESULT = async function () {
+        return `
         import FooBar from 0x123
         import FooBar from 0x456
 
@@ -102,16 +103,99 @@ describe("resolveCadence", () => {
         pub fun otherTwo(): Address {return 0x123}
         pub fun otherThree(): Address {return 0x456}
       `
-    }
+      }
+
+      config.put("0xFoo", "0x123").put("0xFooBar", "0x456")
+
+      const ix = await pipe([
+        makeScript,
+        put("ix.cadence", CADENCE),
+        resolveCadence,
+      ])(interaction())
+
+      expect(ix.message.cadence).toEqual(await RESULT())
+    })
+  })
+
+  describe("new Identifier syntax", () => {
+    test("single import statement", async () => {
+      const CADENCE = `import "Foo"
+
+pub fun main(): Address {
+  return "Foo"
+}`
+
+      const expected = `import Foo from 0x1
+
+pub fun main(): Address {
+  return "Foo"
+}`
+
+      await config().put("system.contracts.Foo", "0x1")
+      await idle()
+
+      const ix = await pipe([
+        makeScript,
+        put("ix.cadence", CADENCE),
+        resolveCadence,
+      ])(interaction())
+
+      expect(ix.message.cadence).toEqual(expected)
+    })
+
+    test("multiple import statements with only one defined", async () => {
+      const CADENCE = `import "Foo"
+import "Bar"
+
+pub fun main(): Address {
+  return "Foo"
+}`
+
+      const expected = `import Foo from 0x1
+import "Bar"
+
+pub fun main(): Address {
+  return "Foo"
+}`
+
+      await config().put("system.contracts.Foo", "0x1")
+      await idle()
+
+      const ix = await pipe([
+        makeScript,
+        put("ix.cadence", CADENCE),
+        resolveCadence,
+      ])(interaction())
+
+      expect(ix.message.cadence).toEqual(expected)
+    })
+
+    test("multiple import statements", async () => {
+      const CADENCE = `import "Foo"
+import "Bar"
+
+pub fun main(): Address {
+  return "Foo"
+}`
+
+      const expected = `import Foo from 0x1
+import Bar from 0x2
+
+pub fun main(): Address {
+  return "Foo"
+}`
 
-    config.put("0xFoo", "0x123").put("0xFooBar", "0x456")
+      await config().put("system.contracts.Foo", "0x1")
+      await config().put("system.contracts.Bar", "0x2")
+      await idle()
 
-    const ix = await pipe([
-      makeScript,
-      put("ix.cadence", CADENCE),
-      resolveCadence,
-    ])(interaction())
+      const ix = await pipe([
+        makeScript,
+        put("ix.cadence", CADENCE),
+        resolveCadence,
+      ])(interaction())
 
-    expect(ix.message.cadence).toEqual(await RESULT())
+      expect(ix.message.cadence).toEqual(expected)
+    })
   })
 })