From 8ffd3f5040db314bc1358f05946780af1c03df1a Mon Sep 17 00:00:00 2001 From: Jeff Doyle Date: Fri, 2 Feb 2024 16:35:16 -0800 Subject: [PATCH] Support v1.1.0 of InteractionTemplate and InteractionTemplateInterface (#1614) * PKG -- [fcl] InteractionTempalteUtils support v1.1.0 of InteractionTemplate and InteractionTemplateInterface ID generation * PKG -- [fcl] InteractionTemplateNormalizers support v1.1.0 of InteractionTemplate and InteractionTemplateInterface * PKG -- [fcl] InteractionTempalteUtils support v1.1.0 of InteractionTemplate and InteractionTemplateInterface ID generation * PKG -- [fcl] Adds tests for InteractionTemplateUtils * PKG -- [fcl] exec utils prep-template-opts support v1.1.0 InteractionTemplate * update test template with generated 1.1.0 template, fix tests, fix typos * checkpoint * fixed generate template id for v1.1.0 * create helper method for verifying the template id has correctly been generated * Update packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network-1.0.0.js Co-authored-by: Alex <12097569+nialexsan@users.noreply.github.com> * Update packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin-1.1.0.js Co-authored-by: Alex <12097569+nialexsan@users.noreply.github.com> * Update packages/fcl-core/src/interaction-template-utils/get-template-argument-message.js Co-authored-by: Alex <12097569+nialexsan@users.noreply.github.com> * Update packages/fcl-core/src/interaction-template-utils/get-template-message.test.js Co-authored-by: Alex <12097569+nialexsan@users.noreply.github.com> * Update packages/fcl-core/src/exec/utils/prep-template-opts.js Co-authored-by: Alex <12097569+nialexsan@users.noreply.github.com> * Update packages/fcl-core/src/normalizers/interaction-template/interaction-template-interface-1.0.0-1.1.1.js Co-authored-by: Alex <12097569+nialexsan@users.noreply.github.com> * merge down file * fix path to generate template id * remove generate template interface functionality, deprecated * remove async that are not needed * remove checking account at a specific block height, since access nodes are limited on history * remove async, method doesn't need it * Update packages/fcl-core/src/interaction-template-utils/get-template-argument-message.test.js Co-authored-by: Alex <12097569+nialexsan@users.noreply.github.com> * Update packages/fcl-core/src/interaction-template-utils/get-template-argument-message.test.js Co-authored-by: Alex <12097569+nialexsan@users.noreply.github.com> * Update packages/fcl-core/src/interaction-template-utils/get-template-argument-message.test.js Co-authored-by: Alex <12097569+nialexsan@users.noreply.github.com> * Update packages/fcl-core/src/interaction-template-utils/get-template-argument-message.test.js Co-authored-by: Alex <12097569+nialexsan@users.noreply.github.com> * Update packages/fcl-core/src/interaction-template-utils/get-template-argument-message.test.js Co-authored-by: Alex <12097569+nialexsan@users.noreply.github.com> * Update packages/fcl-core/src/interaction-template-utils/get-template-argument-message.test.js Co-authored-by: Alex <12097569+nialexsan@users.noreply.github.com> * use const instead of let * changeset --------- Co-authored-by: Alex Ni <12097569+nialexsan@users.noreply.github.com> Co-authored-by: Tom Haile --- .changeset/blue-rats-leave.md | 5 + packages/fcl-core/src/exec/query.md | 6 +- .../src/exec/utils/prep-template-opts.js | 4 +- .../derive-cadence-by-network.js | 112 ----- .../derive-cadence-by-network.test.js | 310 ------------ .../derive-cadence-by-network-1.0.0.js | 49 ++ .../derive-cadence-by-network-1.1.0.js | 57 +++ .../derive-cadence-by-network.js | 46 ++ .../derive-cadence-by-network.test.js | 224 +++++++++ .../generate-dependency-pin.js | 103 ---- .../generate-dependency-pin-1.0.0.js | 71 +++ .../generate-dependency-pin-1.1.0.js | 73 +++ .../generate-dependency-pin.js | 72 +++ .../generate-dependency-pin.test.js | 302 ++++++++++++ .../generate-template-id.js | 158 ------- .../generate-template-id-1.0.0.js | 152 ++++++ .../generate-template-id-1.1.0.js | 122 +++++ .../generate-template-id-1.1.0.test.js | 444 ++++++++++++++++++ .../generate-template-id.js | 46 ++ .../generate-template-interface-id.js | 53 --- .../get-interaction-template-audits.js | 7 +- .../get-template-argument-message.js | 24 +- .../get-template-argument-message.test.js | 188 +++++++- .../get-template-message.js | 19 +- .../get-template-message.test.js | 178 ++++++- .../src/interaction-template-utils/index.js | 10 +- .../utils/find-imports.js | 6 +- .../utils/find-imports.test.js | 6 +- .../interaction-template-utils/utils/hash.js | 2 +- .../utils/replace-string-imports.js | 16 + .../utils/replace-string-imports.test.js | 26 + .../verify-dependency-pin-same-at-block.js | 68 ++- ...erify-dependency-pin-same-at-block.test.js | 444 ++++++++++++++++++ .../interaction-template-1.0.0-1.1.0.js | 70 +++ .../interaction-template-interface.js | 13 - .../interaction-template.js | 8 +- 36 files changed, 2690 insertions(+), 804 deletions(-) create mode 100644 .changeset/blue-rats-leave.md delete mode 100644 packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network.js delete mode 100644 packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network.test.js create mode 100644 packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network-1.0.0.js create mode 100644 packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network-1.1.0.js create mode 100644 packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network.js create mode 100644 packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network.test.js delete mode 100644 packages/fcl-core/src/interaction-template-utils/generate-dependency-pin.js create mode 100644 packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin-1.0.0.js create mode 100644 packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin-1.1.0.js create mode 100644 packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin.js create mode 100644 packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin.test.js delete mode 100644 packages/fcl-core/src/interaction-template-utils/generate-template-id.js create mode 100644 packages/fcl-core/src/interaction-template-utils/generate-template-id/generate-template-id-1.0.0.js create mode 100644 packages/fcl-core/src/interaction-template-utils/generate-template-id/generate-template-id-1.1.0.js create mode 100644 packages/fcl-core/src/interaction-template-utils/generate-template-id/generate-template-id-1.1.0.test.js create mode 100644 packages/fcl-core/src/interaction-template-utils/generate-template-id/generate-template-id.js delete mode 100644 packages/fcl-core/src/interaction-template-utils/generate-template-interface-id.js create mode 100644 packages/fcl-core/src/interaction-template-utils/utils/replace-string-imports.js create mode 100644 packages/fcl-core/src/interaction-template-utils/utils/replace-string-imports.test.js create mode 100644 packages/fcl-core/src/interaction-template-utils/verify-dependency-pin-same-at-block.test.js create mode 100644 packages/fcl-core/src/normalizers/interaction-template/interaction-template-1.0.0-1.1.0.js delete mode 100644 packages/fcl-core/src/normalizers/interaction-template/interaction-template-interface.js diff --git a/.changeset/blue-rats-leave.md b/.changeset/blue-rats-leave.md new file mode 100644 index 000000000..3f3bb3291 --- /dev/null +++ b/.changeset/blue-rats-leave.md @@ -0,0 +1,5 @@ +--- +"@onflow/fcl-core": minor +--- + +Support v1.1.0 of InteractionTemplate and InteractionTemplateInterface diff --git a/packages/fcl-core/src/exec/query.md b/packages/fcl-core/src/exec/query.md index d917c9efd..80a9c0973 100644 --- a/packages/fcl-core/src/exec/query.md +++ b/packages/fcl-core/src/exec/query.md @@ -2,7 +2,7 @@ Programmatically Query Flow. A light, stable and approachable higher level wrapper around the script variant of the FCL/JS-SDK interaction. -> The primary idea here is to make querying the Flow Blockchain more approachable by providing an interface that hides most of the complexity. Eventually this way of querying the chain will be able to pull stored script interactions from on chain repositories of scripts/transactions, this interface provides some inital steps/foundation in that direction. +> The primary idea here is to make querying the Flow Blockchain more approachable by providing an interface that hides most of the complexity. Eventually this way of querying the chain will be able to pull stored script interactions from on chain repositories of scripts/transactions, this interface provides some initial steps/foundation in that direction. ## Status @@ -106,7 +106,7 @@ Just like before we would pass this to our query function as the `cadence` value The function will be passed two values (`arg` and `t`) for us to construct our return value in a way that FCL understands. The first value is a function called `arg`, it is going to take our value and our type and build out the underlying argument. The second value is an object that includes a corresponding type constructor for every valid cadence type we can pass in as an argument. -Writing it out in text makes it so much more complicated than it is, just remember we need to tell the query the order of the arguments (the array) and the type of each argument (`t`), that requires a special datastructure that you shouldn't need to care about so that is hidden in a function (`arg`). +Writing it out in text makes it so much more complicated than it is, just remember we need to tell the query the order of the arguments (the array) and the type of each argument (`t`), that requires a special data structure that you shouldn't need to care about so that is hidden in a function (`arg`). As an example lets look at our first argument passed into the public `main` function (`a: Int`), and say we want its value to be `7`. Because it is the first argument, it should be the first argument returned in the array. @@ -151,7 +151,7 @@ await fcl.query({ ## A more real world example -The above examples are a bit contrived. We tried to distil things down to convey the concepts, but in practice those examples are a bit useless because you could just do them in javascript. +The above examples are a bit contrived. We tried to distill things down to convey the concepts, but in practice those examples are a bit useless because you could just do them in javascript. The following example should hopefully highlight a bit better what is possible. In this example we are going to point FCL at testnet, and use a profile contract that is deployed there to query a couple Flow accounts that may or may not have Profile resources. The first thing we need to do is to configure FCL, as mentioned above this only needs to happen once before we make our first query. In this case we are wanting two configurations values, the access node api and the profile contract address. diff --git a/packages/fcl-core/src/exec/utils/prep-template-opts.js b/packages/fcl-core/src/exec/utils/prep-template-opts.js index 29f5e0bc6..215ca814f 100644 --- a/packages/fcl-core/src/exec/utils/prep-template-opts.js +++ b/packages/fcl-core/src/exec/utils/prep-template-opts.js @@ -1,6 +1,6 @@ import {retrieve} from "../../document/document.js" -import {normalizeInteractionTemplate} from "../../normalizers/interaction-template/interaction-template" -import {deriveCadenceByNetwork} from "../../interaction-template-utils/derive-cadence-by-network.js" +import {normalizeInteractionTemplate} from "../../normalizers/interaction-template/interaction-template.js" +import {deriveCadenceByNetwork} from "../../interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network.js" import {deriveDependencies} from "./derive-dependencies" import {isString} from "../../utils/is" import {getChainId} from "../../utils" diff --git a/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network.js b/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network.js deleted file mode 100644 index a20be3852..000000000 --- a/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network.js +++ /dev/null @@ -1,112 +0,0 @@ -import {invariant} from "@onflow/sdk" -import {normalizeInteractionTemplate} from "../normalizers/interaction-template/interaction-template" - -/** - * @description Fills import addresses in Cadence for network - * - * @param {object} params - * @param {string} params.network - Network to derive Cadence for - * @param {object} params.template - Interaction Template to derive Cadence from - * @returns {string} - Cadence - */ -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) - - case "1.1.0": - // get network dependencies from template dependencies, use new string import format - const networkDeps = {} - - template?.data?.dependencies.forEach(dependency => { - dependency.contracts.forEach(contract => { - const contractName = contract.contract - contract.networks.forEach(net => { - if (net.network === network) { - networkDeps[contractName] = net.address - } - }) - - invariant( - networkDeps[contractName], - `networkAddress -- Could not find contracts Network Address: ${network} ${contractName}` - ) - }) - }) - - invariant( - Object.keys(networkDeps).length === template?.data?.dependencies.length, - `networkDeps -- Could not find contracts for import dependencies: ${networkDeps}` - ) - - invariant( - Object.keys(networkDeps).length === Object.values(networkDeps).length, - `networkDeps -- Could not find all addresses for network ${network} dependencies: ${networkDeps}` - ) - - return Object.keys(networkDeps).reduce((cadence, contractName) => { - const test = new RegExp(`\\bimport\\b\\s*\\\"${contractName}\\\"`, "g") - return cadence.replace(test, `import ${contractName} from ${networkDeps[contractName]}`) - }, template.data.cadence.body) - - default: - throw new Error( - "deriveCadenceByNetwork Error: Unsupported template version" - ) - } -} diff --git a/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network.test.js b/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network.test.js deleted file mode 100644 index 518012ac9..000000000 --- a/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network.test.js +++ /dev/null @@ -1,310 +0,0 @@ -import {deriveCadenceByNetwork} from "./derive-cadence-by-network.js" - -describe("Derive cadence by network 1.0.0", () => { - 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) - }) -}) - -describe("Derive cadence by network 1.1.0 single import", () => { - let templatev11 = { - f_type: "InteractionTemplate", - f_version: "1.1.0", - id: "a2b2d73def...aabc5472d2", - data: { - type: "transaction", - interface: "asadf23234...fas234234", - messages: [], - cadence: { - body: "import \"FlowToken\"\n transaction(amount: UFix64, to: Address) {\n let vault: @FungibleToken.Vault\n prepare(signer: AuthAccount) {\n %%self.vault <- signer\n .borrow<&{FungibleToken.Provider}>(from: /storage/flowTokenVault)!\n .withdraw(amount: amount)\n self.vault <- FungibleToken.getVault(signer)\n }\n execute {\n getAccount(to)\n .getCapability(/public/flowTokenReceiver)!\n .borrow<&{FungibleToken.Receiver}>()!\n .deposit(from: <-self.vault)\n }\n }", - pins: [ - { - network: "mainnet", - pin: "186e262ce6fe06b5075ec6569a0e5482a79c471881182612d8e4a665c2977f3e", - }, - { - network: "testnet", - pin: "f93977d7a297f559e97259cb2a95fed0f87cfeec46c5257a26adc26a260d6c4c", - }, - ], - }, - dependencies: [ - { - contracts: [ - { - contract: "FlowToken", - networks: [ - { - network: "mainnet", - address: "0x1654653399040a61", - dependency_pin_block_height: 10123123123, - dependency_pin: { - pin: "c8cb7cc7a1c2a329de65d83455016bc3a9b53f9668c74ef555032804bac0b25b", - pin_self: - "38d0cca4b74c4e88213df636b4cfc2eb6e86fd8b2b84579d3b9bffab3e0b1fcb", - pin_contract_name: "FlowToken", - imports: [ - { - pin: "b8a3ed26c222ed67016a28021d8fee5603b948533cbc992b3c90f71a61b2b312", - pin_self: - "7bc3056ba5d39d130f45411c2c05bb549db8ce727c11a1cb821136a621be27fb", - pin_contract_name: "FungibleToken", - pin_contract_address: "0xf233dcee88fe0abe", - imports: [], - }, - ], - }, - }, - { - network: "testnet", - address: "0x7e60df042a9c0868", - dependency_pin_block_height: 10123123123, - dependency_pin: { - pin: "c8cb7cc7a1c2a329de65d83455016bc3a9b53f9668c74ef555032804bac0b25b", - pin_self: - "38d0cca4b74c4e88213df636b4cfc2eb6e86fd8b2b84579d3b9bffab3e0b1fcb", - pin_contract_name: "FlowToken", - pin_contract_address: "0x7e60df042a9c0868", - imports: [], - }, - }, - ], - }, - ], - }, - ], - parameters: [ - { - label: "amount", - index: 0, - type: "UFix64", - messages: [], - balance: "FlowToken", - }, - { - label: "to", - index: 1, - type: "Address", - messages: [], - }, - ], - }, - } - - const mainnetAddressReplaced = "import FlowToken from 0x1654653399040a61\n transaction(amount: UFix64, to: Address) {\n let vault: @FungibleToken.Vault\n prepare(signer: AuthAccount) {\n %%self.vault <- signer\n .borrow<&{FungibleToken.Provider}>(from: /storage/flowTokenVault)!\n .withdraw(amount: amount)\n self.vault <- FungibleToken.getVault(signer)\n }\n execute {\n getAccount(to)\n .getCapability(/public/flowTokenReceiver)!\n .borrow<&{FungibleToken.Receiver}>()!\n .deposit(from: <-self.vault)\n }\n }" - test("1.1.0 derives cadence correctly for a given mainnet", async () => { - let cadence = deriveCadenceByNetwork({ - network: "mainnet", - template: templatev11, - }) - - expect(cadence).toEqual(mainnetAddressReplaced) - - - }) - - const testnetAddressReplaced = "import FlowToken from 0x7e60df042a9c0868\n transaction(amount: UFix64, to: Address) {\n let vault: @FungibleToken.Vault\n prepare(signer: AuthAccount) {\n %%self.vault <- signer\n .borrow<&{FungibleToken.Provider}>(from: /storage/flowTokenVault)!\n .withdraw(amount: amount)\n self.vault <- FungibleToken.getVault(signer)\n }\n execute {\n getAccount(to)\n .getCapability(/public/flowTokenReceiver)!\n .borrow<&{FungibleToken.Receiver}>()!\n .deposit(from: <-self.vault)\n }\n }" - test("1.1.0 derives cadence correctly for a given testnet", async () => { - let cadence = deriveCadenceByNetwork({ - network: "testnet", - template: templatev11, - }) - - expect(cadence).toEqual(testnetAddressReplaced) - }) - - test("It fails to derive cadence for unknown network", async () => { - expect(() => - deriveCadenceByNetwork({ - network: "randomnet", - templatev11, - }) - ).toThrow(Error) - }) -}) - - -describe("Derive cadence by network 1.1.0 multiple import", () => { - let templatev11 = { - f_type: "InteractionTemplate", - f_version: "1.1.0", - id: "a2b2d73def...aabc5472d2", - data: { - type: "transaction", - interface: "asadf23234...fas234234", - messages: [], - cadence: { - body: "import \"FlowToken\"\n import \"FungibleToken\"\n transaction(amount: UFix64, to: Address) {\n let vault: @FungibleToken.Vault\n prepare(signer: AuthAccount) {\n %%self.vault <- signer\n .borrow<&{FungibleToken.Provider}>(from: /storage/flowTokenVault)!\n .withdraw(amount: amount)\n self.vault <- FungibleToken.getVault(signer)\n }\n execute {\n getAccount(to)\n .getCapability(/public/flowTokenReceiver)!\n .borrow<&{FungibleToken.Receiver}>()!\n .deposit(from: <-self.vault)\n }\n }", - pins: [ - { - network: "mainnet", - pin: "186e262ce6fe06b5075ec6569a0e5482a79c471881182612d8e4a665c2977f3e", - }, - { - network: "testnet", - pin: "f93977d7a297f559e97259cb2a95fed0f87cfeec46c5257a26adc26a260d6c4c", - }, - ], - }, - dependencies: [ - { - contracts: [ - { - contract: "FlowToken", - networks: [ - { - network: "mainnet", - address: "0x1654653399040a61", - dependency_pin_block_height: 10123123123, - dependency_pin: { - pin: "c8cb7cc7a1c2a329de65d83455016bc3a9b53f9668c74ef555032804bac0b25b", - pin_self: - "38d0cca4b74c4e88213df636b4cfc2eb6e86fd8b2b84579d3b9bffab3e0b1fcb", - pin_contract_name: "FlowToken", - imports: [], - }, - }, - { - network: "testnet", - address: "0x7e60df042a9c0868", - dependency_pin_block_height: 10123123123, - dependency_pin: { - pin: "c8cb7cc7a1c2a329de65d83455016bc3a9b53f9668c74ef555032804bac0b25b", - pin_self: - "38d0cca4b74c4e88213df636b4cfc2eb6e86fd8b2b84579d3b9bffab3e0b1fcb", - pin_contract_name: "FlowToken", - pin_contract_address: "0x7e60df042a9c0868", - imports: [], - }, - }, - ], - }, - { - contract: "FungibleToken", - networks: [ - { - network: "mainnet", - address: "0xf233dcee88fe0abe", - dependency_pin_block_height: 10123123123, - dependency_pin: { - pin: "c8cb7cc7a1c2a329de65d83455016bc3a9b53f9668c74ef555032804bac0b25b", - pin_self: - "38d0cca4b74c4e88213df636b4cfc2eb6e86fd8b2b84579d3b9bffab3e0b1fcb", - pin_contract_name: "FlowToken", - imports: [], - }, - }, - { - network: "testnet", - address: "0x11111111111", - dependency_pin_block_height: 10123123123, - dependency_pin: { - pin: "c8cb7cc7a1c2a329de65d83455016bc3a9b53f9668c74ef555032804bac0b25b", - pin_self: - "38d0cca4b74c4e88213df636b4cfc2eb6e86fd8b2b84579d3b9bffab3e0b1fcb", - pin_contract_name: "FlowToken", - pin_contract_address: "0x7e60df042a9c0868", - imports: [], - }, - }, - ], - }, - ], - }, - ], - parameters: [ - { - label: "amount", - index: 0, - type: "UFix64", - messages: [], - balance: "FlowToken", - }, - { - label: "to", - index: 1, - type: "Address", - messages: [], - }, - ], - }, - } - - const mainnetAddressReplaced = "import FlowToken from 0x1654653399040a61\n import FungibleToken from 0xf233dcee88fe0abe\n transaction(amount: UFix64, to: Address) {\n let vault: @FungibleToken.Vault\n prepare(signer: AuthAccount) {\n %%self.vault <- signer\n .borrow<&{FungibleToken.Provider}>(from: /storage/flowTokenVault)!\n .withdraw(amount: amount)\n self.vault <- FungibleToken.getVault(signer)\n }\n execute {\n getAccount(to)\n .getCapability(/public/flowTokenReceiver)!\n .borrow<&{FungibleToken.Receiver}>()!\n .deposit(from: <-self.vault)\n }\n }" - test("1.1.0 multiple imports derives cadence correctly for a given mainnet", async () => { - let cadence = deriveCadenceByNetwork({ - network: "mainnet", - template: templatev11, - }) - - expect(cadence).toEqual(mainnetAddressReplaced) - - - }) - - const testnetAddressReplaced = "import FlowToken from 0x7e60df042a9c0868\n import FungibleToken from 0x11111111111\n transaction(amount: UFix64, to: Address) {\n let vault: @FungibleToken.Vault\n prepare(signer: AuthAccount) {\n %%self.vault <- signer\n .borrow<&{FungibleToken.Provider}>(from: /storage/flowTokenVault)!\n .withdraw(amount: amount)\n self.vault <- FungibleToken.getVault(signer)\n }\n execute {\n getAccount(to)\n .getCapability(/public/flowTokenReceiver)!\n .borrow<&{FungibleToken.Receiver}>()!\n .deposit(from: <-self.vault)\n }\n }" - test("1.1.0 multiple imports derives cadence correctly for a given testnet", async () => { - let cadence = deriveCadenceByNetwork({ - network: "testnet", - template: templatev11, - }) - - expect(cadence).toEqual(testnetAddressReplaced) - }) - - test("1.1.0 multiple imports fails to derive cadence for unknown network", async () => { - expect(() => - deriveCadenceByNetwork({ - network: "randomnet", - templatev11, - }) - ).toThrow(Error) - }) -}) \ No newline at end of file diff --git a/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network-1.0.0.js b/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network-1.0.0.js new file mode 100644 index 000000000..a40a9f357 --- /dev/null +++ b/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network-1.0.0.js @@ -0,0 +1,49 @@ +import {invariant} from "@onflow/util-invariant" + +/** + * @description Fills import addresses in Cadence for network + * + * @param {object} params + * @param {string} params.network - Network to derive Cadence for + * @param {object} params.template - Interaction Template to derive Cadence from + * @returns {string} - Cadence + */ +export async function deriveCadenceByNetwork100({network, template}) { + invariant( + template.f_version === "1.0.0", + "deriveCadenceByNetwork100({ template }) -- template must be version 1.0.0" + ) + + const networkDependencies = Object.keys(template?.data?.dependencies).map( + dependencyPlaceholder => { + const dependencyNetworkContracts = Object.values( + template?.data?.dependencies?.[dependencyPlaceholder] + ) + + invariant( + dependencyNetworkContracts !== undefined, + `deriveCadenceByNetwork100 -- Could not find contracts for dependency placeholder: ${dependencyPlaceholder}` + ) + + invariant( + dependencyNetworkContracts.length > 0, + `deriveCadenceByNetwork100 -- Could not find contracts for dependency placeholder: ${dependencyPlaceholder}` + ) + + const dependencyContract = dependencyNetworkContracts[0] + const dependencyContractForNetwork = dependencyContract?.[network] + + invariant( + dependencyContractForNetwork, + `deriveCadenceByNetwork100 -- 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) +} diff --git a/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network-1.1.0.js b/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network-1.1.0.js new file mode 100644 index 000000000..db5056c4c --- /dev/null +++ b/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network-1.1.0.js @@ -0,0 +1,57 @@ +import {invariant} from "@onflow/util-invariant" +import {replaceStringImports} from "../utils/replace-string-imports.js" +/** + * @description Fills import addresses in Cadence for network + * + * @param {object} params + * @param {string} params.network - Network to derive Cadence for + * @param {object} params.template - Interaction Template to derive Cadence from + * @returns {string} - Cadence + */ +export async function deriveCadenceByNetwork110({network, template}) { + invariant( + template.f_version === "1.1.0", + "deriveCadenceByNetwork110({ template }) -- template must be version 1.0.0" + ) + + // get network dependencies from template dependencies, use new string import format + const networkDependencies = {} + + template?.data?.dependencies.forEach(dependency => { + dependency.contracts.forEach(contract => { + const contractName = contract.contract + contract.networks.forEach(net => { + if (net.network === network) { + networkDependencies[contractName] = net.address + } + }) + + invariant( + networkDependencies[contractName] !== undefined, + `deriveCadenceByNetwork110 -- Could not find contracts Network Address: ${network} ${contractName}` + ) + }) + }) + + invariant( + Object.keys(networkDependencies).length === + template?.data?.dependencies.length, + `deriveCadenceByNetwork110 -- Could not find contracts for import dependencies: ${networkDependencies}` + ) + + invariant( + Object.keys(networkDependencies).length === + Object.values(networkDependencies).length, + `deriveCadenceByNetwork110 -- Could not find all addresses for network ${network} dependencies: ${networkDependencies}` + ) + + invariant( + template?.data?.cadence?.body, + `no cadence found -- Could not replace import dependencies: ${networkDependencies}` + ) + + return replaceStringImports({ + cadence: template?.data?.cadence?.body, + networkDependencies, + }) +} diff --git a/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network.js b/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network.js new file mode 100644 index 000000000..b8704dad6 --- /dev/null +++ b/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network.js @@ -0,0 +1,46 @@ +import {invariant} from "@onflow/util-invariant" +import {deriveCadenceByNetwork100} from "./derive-cadence-by-network-1.0.0.js" +import {deriveCadenceByNetwork110} from "./derive-cadence-by-network-1.1.0.js" + +/** + * @description Fills import addresses in Cadence for network + * + * @param {object} params + * @param {string} params.network - Network to derive Cadence for + * @param {object} params.template - Interaction Template to derive Cadence from + * @returns {string} - Cadence + */ +export async 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, + "deriveCadenceByNetwork({ template }) -- template must be defined" + ) + invariant( + typeof template === "object", + "deriveCadenceByNetwork({ template }) -- template must be an object" + ) + invariant( + template.f_type === "InteractionTemplate", + "deriveCadenceByNetwork({ template }) -- template must be an InteractionTemplate" + ) + + switch (template.f_version) { + case "1.1.0": + return await deriveCadenceByNetwork110({network, template}) + case "1.0.0": + return await deriveCadenceByNetwork100({network, template}) + default: + throw new Error( + "deriveCadenceByNetwork Error: Unsupported template version" + ) + } +} diff --git a/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network.test.js b/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network.test.js new file mode 100644 index 000000000..78a86e2a3 --- /dev/null +++ b/packages/fcl-core/src/interaction-template-utils/derive-cadence-by-network/derive-cadence-by-network.test.js @@ -0,0 +1,224 @@ +import {deriveCadenceByNetwork} from "./derive-cadence-by-network.js" + +describe("Derive cadence by network 1.0.0", () => { + const 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 () => { + const cadence = await deriveCadenceByNetwork({ + network: "mainnet", + template, + }) + + expect(cadence).toEqual("import FungibleToken from 0xf233dcee88fe0abe\n") + }) + + test("It fails to derive cadence for unknown network", async () => { + await expect(() => + deriveCadenceByNetwork({ + network: "randomnet", + template, + }) + ).rejects.toThrow(Error) + }) +}) + +describe("Derive cadence by network 1.1.0", () => { + const template11 = { + f_type: "InteractionTemplate", + f_version: "1.1.0", + id: "3a99af243b85f3f6af28304af2ed53a37fb913782b3efc483e6f0162a47720a0", + data: { + type: "transaction", + interface: "", + messages: [ + { + key: "title", + i18n: [ + { + tag: "en-US", + translation: "Transfer Tokens", + }, + ], + }, + { + key: "description", + i18n: [ + { + tag: "en-US", + translation: "Transfer Flow to account", + }, + ], + }, + ], + cadence: { + body: 'import "FungibleToken"\n\n#interaction(\n version: "1.1.0",\n title: "Transfer Flow",\n description: "Transfer Flow to account",\n language: "en-US",\n parameters: [\n Parameter(\n name: "amount", \n title: "Amount", \n description: "The amount of FLOW tokens to send"\n ),\n Parameter(\n name: "to", \n title: "To",\n description: "The Flow account the tokens will go to"\n )\n ],\n)\n\ntransaction(amount: UFix64, to: Address) {\n let vault: @FungibleToken.Vault\n \n prepare(signer: AuthAccount) {\n self.vault \u003c- signer\n .borrow\u003c\u0026{FungibleToken.Provider}\u003e(from: /storage/flowTokenVault)!\n .withdraw(amount: amount)\n }\n\n execute {\n getAccount(to)\n .getCapability(/public/flowTokenReceiver)!\n .borrow\u003c\u0026{FungibleToken.Receiver}\u003e()!\n .deposit(from: \u003c-self.vault)\n }\n}', + network_pins: [ + { + network: "mainnet", + pin_self: + "dd046de8ef442e4d708124d5710cb78962eb884a4387df1f0b1daf374bd28278", + }, + { + network: "testnet", + pin_self: + "4089786f5e19fe66b39e347634ca28229851f4de1fd469bd8f327d79510e771f", + }, + ], + }, + dependencies: [ + { + contracts: [ + { + contract: "FungibleToken", + networks: [ + { + network: "mainnet", + address: "0xf233dcee88fe0abe", + dependency_pin_block_height: 70493190, + dependency_pin: { + pin: "ac0208f93d07829ec96584d618ddbec6af3cf4e2866bd5071249e8ec93c7e0dc", + pin_self: + "cdadd5b5897f2dfe35d8b25f4e41fea9f8fca8f40f8a8b506b33701ef5033076", + pin_contract_name: "FungibleToken", + pin_contract_address: "0xf233dcee88fe0abe", + imports: [], + }, + }, + { + network: "testnet", + address: "0x9a0766d93b6608b7", + dependency_pin_block_height: 149595558, + dependency_pin: { + pin: "ac0208f93d07829ec96584d618ddbec6af3cf4e2866bd5071249e8ec93c7e0dc", + pin_self: + "cdadd5b5897f2dfe35d8b25f4e41fea9f8fca8f40f8a8b506b33701ef5033076", + pin_contract_name: "FungibleToken", + pin_contract_address: "0x9a0766d93b6608b7", + imports: [], + }, + }, + { + network: "emulator", + address: "0xee82856bf20e2aa6", + dependency_pin_block_height: 0, + }, + ], + }, + ], + }, + ], + parameters: [ + { + label: "amount", + index: 0, + type: "UFix64", + messages: [ + { + key: "title", + i18n: [ + { + tag: "en-US", + translation: "Amount", + }, + ], + }, + { + key: "description", + i18n: [ + { + tag: "en-US", + translation: "The amount of FLOW tokens to send", + }, + ], + }, + ], + }, + { + label: "to", + index: 1, + type: "Address", + messages: [ + { + key: "title", + i18n: [ + { + tag: "en-US", + translation: "To", + }, + ], + }, + { + key: "description", + i18n: [ + { + tag: "en-US", + translation: "The Flow account the tokens will go to", + }, + ], + }, + ], + }, + ], + }, + } + + test("v1.1.0, It derives cadence correctly for a given network", async () => { + const cadence = await deriveCadenceByNetwork({ + network: "mainnet", + template: template11, + }) + + const expectedCadence = `import FungibleToken from 0xf233dcee88fe0abe\n\n#interaction(\n version: "1.1.0",\n title: "Transfer Flow",\n description: "Transfer Flow to account",\n language: "en-US",\n parameters: [\n Parameter(\n name: "amount", \n title: "Amount", \n description: "The amount of FLOW tokens to send"\n ),\n Parameter(\n name: "to", \n title: "To",\n description: "The Flow account the tokens will go to"\n )\n ],\n)\n\ntransaction(amount: UFix64, to: Address) {\n let vault: @FungibleToken.Vault\n \n prepare(signer: AuthAccount) {\n self.vault \u003c- signer\n .borrow\u003c\u0026{FungibleToken.Provider}\u003e(from: /storage/flowTokenVault)!\n .withdraw(amount: amount)\n }\n\n execute {\n getAccount(to)\n .getCapability(/public/flowTokenReceiver)!\n .borrow\u003c\u0026{FungibleToken.Receiver}\u003e()!\n .deposit(from: \u003c-self.vault)\n }\n}` + expect(cadence).toEqual(expectedCadence) + }) + + test("v1.1.0, Incorrect template version", async () => { + await expect(() => + deriveCadenceByNetwork({ + network: "mainnet", + template: {f_type: "InteractionTemplate", f_version: "0.0.0"}, + }) + ).rejects.toThrow(Error) + }) + + test("v1.1.0, It fails to derive cadence for unknown network", async () => { + await expect(() => + deriveCadenceByNetwork({ + network: "randomnet", + template: template11, + }) + ).rejects.toThrow(Error) + }) + +}) diff --git a/packages/fcl-core/src/interaction-template-utils/generate-dependency-pin.js b/packages/fcl-core/src/interaction-template-utils/generate-dependency-pin.js deleted file mode 100644 index 58f48c149..000000000 --- a/packages/fcl-core/src/interaction-template-utils/generate-dependency-pin.js +++ /dev/null @@ -1,103 +0,0 @@ -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" - -/** - * @description Produces a dependency pin for a contract at a given block height - * @param {object} params - * @param {string} params.address - The address of the account containing the contract - * @param {string} params.contractName - The name of the contract - * @param {number} params.blockHeight - The block height to produce the dependency pin for - * @param {object} opts - Options to pass to the interaction - * @returns {Promise} - The dependency pin - */ -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) -} - -/** - * @description Produces a dependency pin for a contract at latest sealed block - * @param {object} params - * @param {string} params.address - The address of the account containing the contract - * @param {string} params.contractName - The name of the contract - * @param {object} opts - Options to pass to the interaction - * @returns {Promise} - The dependency pin - */ -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-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin-1.0.0.js b/packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin-1.0.0.js new file mode 100644 index 000000000..bd2efcf31 --- /dev/null +++ b/packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin-1.0.0.js @@ -0,0 +1,71 @@ +import { + invariant, + send, + getAccount, + 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" + + /** + * @description Produces a dependency pin for a contract at current state of chain + * @param {object} params + * @param {string} params.address - The address of the account containing the contract + * @param {string} params.contractName - The name of the contract + * @param {object} opts - Options to pass to the interaction + * @returns {Promise} - The dependency pin + */ + export async function generateDependencyPin100( + {address, contractName}, + opts = {} + ) { + invariant( + address != undefined, + "generateDependencyPin({ address }) -- address must be defined" + ) + invariant( + contractName != undefined, + "generateDependencyPin({ contractName }) -- contractName 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" + ) + + const horizon = [generateImport({contractName, address})] + + for (const horizonImport of horizon) { + const account = await send( + [ + getAccount( + await config().get(horizonImport.address, horizonImport.address) + ), + ], + 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") + } + + const contractImports = findImports(horizonImport.contract) + + horizon.push(...contractImports) + } + + const contractHashes = horizon.map(iport => genHash(iport.contract)) + + const contractHashesJoined = contractHashes.join("") + + return genHash(contractHashesJoined) + } + \ No newline at end of file diff --git a/packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin-1.1.0.js b/packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin-1.1.0.js new file mode 100644 index 000000000..a8a73a4cd --- /dev/null +++ b/packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin-1.1.0.js @@ -0,0 +1,73 @@ +import { + invariant, + send, + getAccount, + 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" + +/** + * @description Produces a dependency pin for a contract at current state of chain + * @param {object} params + * @param {string} params.address - The address of the account containing the contract + * @param {string} params.contractName - The name of the contract + * @param {object} opts - Options to pass to the interaction + * @returns {Promise} - The dependency pin + */ +export async function generateDependencyPin110( + {address, contractName}, + opts = {} +) { + invariant( + address != undefined, + "generateDependencyPin({ address }) -- address must be defined" + ) + invariant( + contractName != undefined, + "generateDependencyPin({ contractName }) -- contractName 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" + ) + + const horizon = [generateImport({contractName, address})] + + for (const horizonImport of horizon) { + const account = await send( + [ + getAccount( + await config().get(horizonImport.address, horizonImport.address) + ), + ], + 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") + } + + const contractImports = findImports(horizonImport.contract) + + horizon.push(...contractImports) + } + + const contractPinSelfHashesPromises = horizon.map(iport => + genHash(iport.contract) + ) + // genHash returns a promise, so we need to await the results of all the promises + const contractPinSelfHashes = await Promise.all(contractPinSelfHashesPromises) + const contractPinHashes = contractPinSelfHashes.join("") + + return genHash(contractPinHashes) +} diff --git a/packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin.js b/packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin.js new file mode 100644 index 000000000..475dfa013 --- /dev/null +++ b/packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin.js @@ -0,0 +1,72 @@ +import { + block, + invariant, +} from "@onflow/sdk" +import {generateDependencyPin110} from "./generate-dependency-pin-1.1.0.js" +import {generateDependencyPin100} from "./generate-dependency-pin-1.0.0.js" + + /** + * @description Produces a dependency pin for a contract at current state of chain + * @param {object} params + * @param {string} params.version - The version of the interaction template + * @param {string} params.address - The address of the account containing the contract + * @param {string} params.contractName - The name of the contract + * @param {object} opts - Options to pass to the interaction + * @returns {Promise} - The dependency pin + */ + export async function generateDependencyPin( + {version, address, contractName }, + opts = {} + ) { + invariant( + address != undefined, + "generateDependencyPin({ address }) -- address must be defined" + ) + invariant( + contractName != undefined, + "generateDependencyPin({ contractName }) -- contractName 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" + ) + + switch (version) { + case "1.1.0": + return await generateDependencyPin110({address, contractName}) + case "1.0.0": + return await generateDependencyPin100({address, contractName }) + default: + throw new Error( + "deriveCadenceByNetwork Error: Unsupported template version" + ) + } +} + + + /** + * @description Produces a dependency pin for a contract at latest sealed block + * @param {object} params + * @param {string} params.version - The version of the interaction template + * @param {string} params.address - The address of the account containing the contract + * @param {string} params.contractName - The name of the contract + * @param {object} opts - Options to pass to the interaction + * @returns {Promise} - The dependency pin + */ + export async function generateDependencyPinAtLatestSealedBlock( + {version, address, contractName}, + opts = {} + ) { + const latestSealedBlock = await block({sealed: true}, opts) + const latestSealedBlockHeight = latestSealedBlock?.height + + return generateDependencyPin( + {version, address, contractName, blockHeight: latestSealedBlockHeight}, + opts + ) + } + \ No newline at end of file diff --git a/packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin.test.js b/packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin.test.js new file mode 100644 index 000000000..b4a0196ff --- /dev/null +++ b/packages/fcl-core/src/interaction-template-utils/generate-dependency-pin/generate-dependency-pin.test.js @@ -0,0 +1,302 @@ +import {generateDependencyPin110, generateDependencySelfPin} from "./generate-dependency-pin-1.1.0.js" +import {config} from "@onflow/config" + +const returnedAccount = { + address: "0xf233dcee88fe0abe", + keys: [], + balance: "10", + contracts: { + FungibleToken: +`/** + +# The Flow Fungible Token standard + +## \`FungibleToken\` contract interface + +The interface that all Fungible Token contracts would have to conform to. +If a users wants to deploy a new token contract, their contract +would need to implement the FungibleToken interface. + +Their contract would have to follow all the rules and naming +that the interface specifies. + +## \`Vault\` resource + +Each account that owns tokens would need to have an instance +of the Vault resource stored in their account storage. + +The Vault resource has methods that the owner and other users can call. + +## \`Provider\`, \`Receiver\`, and \`Balance\` resource interfaces + +These interfaces declare pre-conditions and post-conditions that restrict +the execution of the functions in the Vault. + +They are separate because it gives the user the ability to share +a reference to their Vault that only exposes the fields functions +in one or more of the interfaces. + +It also gives users the ability to make custom resources that implement +these interfaces to do various things with the tokens. +For example, a faucet can be implemented by conforming +to the Provider interface. + +By using resources and interfaces, users of Fungible Token contracts +can send and receive tokens peer-to-peer, without having to interact +with a central ledger smart contract. To send tokens to another user, +a user would simply withdraw the tokens from their Vault, then call +the deposit function on another user's Vault to complete the transfer. + +*/ + +/// The interface that Fungible Token contracts implement. +/// +pub contract interface FungibleToken { + + /// The total number of tokens in existence. + /// It is up to the implementer to ensure that the total supply + /// stays accurate and up to date + pub var totalSupply: UFix64 + + /// The event that is emitted when the contract is created + pub event TokensInitialized(initialSupply: UFix64) + + /// The event that is emitted when tokens are withdrawn from a Vault + pub event TokensWithdrawn(amount: UFix64, from: Address?) + + /// The event that is emitted when tokens are deposited into a Vault + pub event TokensDeposited(amount: UFix64, to: Address?) + + /// The interface that enforces the requirements for withdrawing + /// tokens from the implementing type. + /// + /// It does not enforce requirements on \`balance\` here, + /// because it leaves open the possibility of creating custom providers + /// that do not necessarily need their own balance. + /// + pub resource interface Provider { + + /// Subtracts tokens from the owner's Vault + /// and returns a Vault with the removed tokens. + /// + /// The function's access level is public, but this is not a problem + /// because only the owner storing the resource in their account + /// can initially call this function. + /// + /// The owner may grant other accounts access by creating a private + /// capability that allows specific other users to access + /// the provider resource through a reference. + /// + /// The owner may also grant all accounts access by creating a public + /// capability that allows all users to access the provider + /// resource through a reference. + /// + /// @param amount: The amount of tokens to be withdrawn from the vault + /// @return The Vault resource containing the withdrawn funds + /// + pub fun withdraw(amount: UFix64): @Vault { + post { + // \`result\` refers to the return value + result.balance == amount: + "Withdrawal amount must be the same as the balance of the withdrawn Vault" + } + } + } + + /// The interface that enforces the requirements for depositing + /// tokens into the implementing type. + /// + /// We do not include a condition that checks the balance because + /// we want to give users the ability to make custom receivers that + /// can do custom things with the tokens, like split them up and + /// send them to different places. + /// + pub resource interface Receiver { + + /// Takes a Vault and deposits it into the implementing resource type + /// + /// @param from: The Vault resource containing the funds that will be deposited + /// + pub fun deposit(from: @Vault) + + /// Below is referenced from the FLIP #69 https://github.com/onflow/flips/blob/main/flips/20230206-fungible-token-vault-type-discovery.md + /// + /// Returns the dictionary of Vault types that the the receiver is able to accept in its \`deposit\` method + /// this then it would return \`{Type<@FlowToken.Vault>(): true}\` and if any custom receiver + /// uses the default implementation then it would return empty dictionary as its parent + /// resource doesn't conform with the \`FungibleToken.Vault\` resource. + /// + /// Custom receiver implementations are expected to upgrade their contracts to add an implementation + /// that supports this method because it is very valuable for various applications to have. + /// + /// @return dictionary of supported deposit vault types by the implementing resource. + /// + pub fun getSupportedVaultTypes(): {Type: Bool} { + // Below check is implemented to make sure that run-time type would + // only get returned when the parent resource conforms with \`FungibleToken.Vault\`. + if self.getType().isSubtype(of: Type<@FungibleToken.Vault>()) { + return {self.getType(): true} + } else { + // Return an empty dictionary as the default value for resource who don't + // implement \`FungibleToken.Vault\`, such as \`FungibleTokenSwitchboard\`, \`TokenForwarder\` etc. + return {} + } + } + } + + /// The interface that contains the \`balance\` field of the Vault + /// and enforces that when new Vaults are created, the balance + /// is initialized correctly. + /// + pub resource interface Balance { + + /// The total balance of a vault + /// + pub var balance: UFix64 + + init(balance: UFix64) { + post { + self.balance == balance: + "Balance must be initialized to the initial balance" + } + } + + /// Function that returns all the Metadata Views implemented by a Fungible Token + /// + /// @return An array of Types defining the implemented views. This value will be used by + /// developers to know which parameter to pass to the resolveView() method. + /// + pub fun getViews(): [Type] { + return [] + } + + /// Function that resolves a metadata view for this fungible token by type. + /// + /// @param view: The Type of the desired view. + /// @return A structure representing the requested view. + /// + pub fun resolveView(_ view: Type): AnyStruct? { + return nil + } + } + + /// The resource that contains the functions to send and receive tokens. + /// The declaration of a concrete type in a contract interface means that + /// every Fungible Token contract that implements the FungibleToken interface + /// must define a concrete \`Vault\` resource that conforms to the \`Provider\`, \`Receiver\`, + /// and \`Balance\` interfaces, and declares their required fields and functions + /// + pub resource Vault: Provider, Receiver, Balance { + + /// The total balance of the vault + pub var balance: UFix64 + + // The conforming type must declare an initializer + // that allows providing the initial balance of the Vault + // + init(balance: UFix64) + + /// Subtracts \`amount\` from the Vault's balance + /// and returns a new Vault with the subtracted balance + /// + /// @param amount: The amount of tokens to be withdrawn from the vault + /// @return The Vault resource containing the withdrawn funds + /// + pub fun withdraw(amount: UFix64): @Vault { + pre { + self.balance >= amount: + "Amount withdrawn must be less than or equal than the balance of the Vault" + } + post { + // use the special function \`before\` to get the value of the \`balance\` field + // at the beginning of the function execution + // + self.balance == before(self.balance) - amount: + "New Vault balance must be the difference of the previous balance and the withdrawn Vault" + } + } + + /// Takes a Vault and deposits it into the implementing resource type + /// + /// @param from: The Vault resource containing the funds that will be deposited + /// + pub fun deposit(from: @Vault) { + // Assert that the concrete type of the deposited vault is the same + // as the vault that is accepting the deposit + pre { + from.isInstance(self.getType()): + "Cannot deposit an incompatible token type" + } + post { + self.balance == before(self.balance) + before(from.balance): + "New Vault balance must be the sum of the previous balance and the deposited Vault" + } + } + } + + /// Allows any user to create a new Vault that has a zero balance + /// + /// @return The new Vault resource + /// + pub fun createEmptyVault(): @Vault { + post { + result.balance == 0.0: "The newly created Vault must have zero balance" + } + } +} +`, + }, + code: null, +} + +jest.mock("@onflow/sdk", () => ({ + send: jest.fn().mockImplementation(({}) => { + // Adjusted mock implementation + const sanitized = returnedAccount.contracts.FungibleToken.replace(/\\/g, ''); + returnedAccount.contracts.FungibleToken = sanitized; + return Promise.resolve(returnedAccount) + }), + getAccount: jest.fn().mockImplementation(({}) => { + // Adjusted mock implementation + return Promise.resolve({data: returnedAccount}) + }), + invariant: jest.fn().mockImplementation(({}) => { + // Adjusted mock implementation + return + }), + config: jest.fn().mockImplementation(() => { + // Adjusted mock implementation + return { + get: jest.fn().mockImplementation(() => { + // Adjusted mock implementation + return Promise.resolve("0xf233dcee88fe0abe") + }), + } + }), +})) + +describe("1.1.0, generate dependency pin", () => { + beforeAll(() => { + jest.spyOn(console, "warn").mockImplementation(() => {}) + }) + + afterAll(() => { + console.warn.mockRestore() + }) + + test("v1.1.0, get dependency pin", async () => { + const pin = + "ac0208f93d07829ec96584d618ddbec6af3cf4e2866bd5071249e8ec93c7e0dc" + + config.put("flow.network", "mainnet") + config.put("accessNode.api", "https://rest-mainnet.onflow.org") + + const depPin = await generateDependencyPin110({ + address: "0xf233dcee88fe0abe", + contractName: "FungibleToken", + }) + + expect(depPin).toEqual(pin) + }) + +}) diff --git a/packages/fcl-core/src/interaction-template-utils/generate-template-id.js b/packages/fcl-core/src/interaction-template-utils/generate-template-id.js deleted file mode 100644 index f72f121b7..000000000 --- a/packages/fcl-core/src/interaction-template-utils/generate-template-id.js +++ /dev/null @@ -1,158 +0,0 @@ -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" - -/** - * @description Generates Interaction Template ID for a given Interaction Template - * - * @param {object} params - * @param {object} params.template - Interaction Template - * @returns {Promise} - Interaction Template ID - */ -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-core/src/interaction-template-utils/generate-template-id/generate-template-id-1.0.0.js b/packages/fcl-core/src/interaction-template-utils/generate-template-id/generate-template-id-1.0.0.js new file mode 100644 index 000000000..757ac931f --- /dev/null +++ b/packages/fcl-core/src/interaction-template-utils/generate-template-id/generate-template-id-1.0.0.js @@ -0,0 +1,152 @@ +import {invariant} from "@onflow/sdk" +import {encode as rlpEncode} from "@onflow/rlp" +import {genHash} from "../utils/hash.js" + +/** + * @description Generates Interaction Template ID for a given Interaction Template + * + * @param {object} params + * @param {object} params.template - Interaction Template + * @returns {Promise} - Interaction Template ID + */ +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( + template.f_type === "InteractionTemplate", + "generateTemplateId({ template }) -- template object must be an InteractionTemplate" + ) + invariant( + template.f_version === "1.0.0", + "generateTemplateId({ template }) -- template object must be an version 1.0.0" + ) + + const templateData = template.data + + const messages = await Promise.all( + Object.keys(templateData.messages).map(async messageKey => [ + genHash(messageKey), + await Promise.all( + Object.keys(templateData.messages?.[messageKey]?.i18n).map( + async i18nkeylanguage => [ + genHash(i18nkeylanguage), + genHash( + templateData.messages?.[messageKey]?.i18n?.[i18nkeylanguage] + ), + ] + ) + ), + ]) + ) + + const dependencies = await Promise.all( + Object.keys(templateData?.dependencies).map( + async dependencyAddressPlaceholder => [ + genHash(dependencyAddressPlaceholder), + await Promise.all( + Object.keys( + templateData?.dependencies?.[dependencyAddressPlaceholder] + ).map(async dependencyContract => [ + genHash(dependencyContract), + await Promise.all( + Object.keys( + templateData?.dependencies?.[dependencyAddressPlaceholder]?.[ + dependencyContract + ] + ).map(async dependencyContractNetwork => [ + genHash(dependencyContractNetwork), + [ + genHash( + templateData?.dependencies?.[ + dependencyAddressPlaceholder + ]?.[dependencyContract]?.[dependencyContractNetwork].address + ), + genHash( + templateData?.dependencies?.[ + dependencyAddressPlaceholder + ]?.[dependencyContract]?.[dependencyContractNetwork] + .contract + ), + genHash( + templateData?.dependencies?.[ + dependencyAddressPlaceholder + ]?.[dependencyContract]?.[dependencyContractNetwork] + .fq_address + ), + genHash( + templateData?.dependencies?.[ + dependencyAddressPlaceholder + ]?.[dependencyContract]?.[dependencyContractNetwork].pin + ), + genHash( + String( + templateData?.dependencies?.[ + dependencyAddressPlaceholder + ]?.[dependencyContract]?.[dependencyContractNetwork] + .pin_block_height + ) + ), + ], + ]) + ), + ]) + ), + ] + ) + ) + + const _arguments = await Promise.all( + Object.keys(templateData?.["arguments"]).map(async argumentLabel => [ + genHash(argumentLabel), + [ + genHash( + String(templateData?.["arguments"]?.[argumentLabel].index) + ), + genHash(templateData?.["arguments"]?.[argumentLabel].type), + genHash( + templateData?.["arguments"]?.[argumentLabel].balance || "" + ), + await Promise.all( + Object.keys( + templateData?.["arguments"]?.[argumentLabel].messages + ).map(async argumentMessageKey => [ + genHash(argumentMessageKey), + await Promise.all( + Object.keys( + templateData?.["arguments"]?.[argumentLabel].messages?.[ + argumentMessageKey + ].i18n + ).map(async i18nkeylanguage => [ + genHash(i18nkeylanguage), + genHash( + templateData?.["arguments"]?.[argumentLabel].messages?.[ + argumentMessageKey + ].i18n?.[i18nkeylanguage] + ), + ]) + ), + ]) + ), + ], + ]) + ) + + const encodedHex = rlpEncode([ + genHash("InteractionTemplate"), + genHash("1.0.0"), + genHash(templateData?.type), + genHash(templateData?.interface), + messages, + genHash(templateData?.cadence), + dependencies, + _arguments, + ]).toString("hex") + + return genHash(encodedHex) +} diff --git a/packages/fcl-core/src/interaction-template-utils/generate-template-id/generate-template-id-1.1.0.js b/packages/fcl-core/src/interaction-template-utils/generate-template-id/generate-template-id-1.1.0.js new file mode 100644 index 000000000..be8704e00 --- /dev/null +++ b/packages/fcl-core/src/interaction-template-utils/generate-template-id/generate-template-id-1.1.0.js @@ -0,0 +1,122 @@ +import {invariant} from "@onflow/util-invariant" +import {encode as rlpEncode} from "@onflow/rlp" +import {genHash} from "../utils/hash.js" +import {generateDependencyPin110} from "../generate-dependency-pin/generate-dependency-pin-1.1.0.js" + +async function generateContractNetworks(contractName, networks) { + const values = [] + for (const net of networks) { + const networkHashes = [genHash(net.network)] + const {address, dependency_pin_block_height} = net + if (net.dependency_pin) { + const hash = await generateDependencyPin110({ + address, + contractName, + blockHeight: dependency_pin_block_height, + }) + networkHashes.push(genHash(hash)) + } + values.push(networkHashes) + } + return values +} + +async function generateContractDependencies(dependencies) { + const values = [] + for (let i = 0; i < dependencies.length; i++) { + const dependency = dependencies[i] + const contracts = [] + for (let j = 0; j < dependency?.contracts.length; j++) { + const c = dependency?.contracts[j] + const contractName = c?.contract + contracts.push(genHash(contractName)) + const contractHashes = await generateContractNetworks( + contractName, + c?.networks + ) + contracts.push(contractHashes) + } + values.push(contracts) + } + return values +} + +/** + * @description Generates Interaction Template ID for a given Interaction Template + * + * @param {object} params + * @param {object} params.template - Interaction Template + * @returns {Promise} - Interaction Template ID + */ +export async function generateTemplateId({template}) { + invariant( + template, + "generateTemplateId({ template }) -- template must be defined" + ) + invariant( + typeof template === "object", + "generateTemplateId({ template }) -- template must be an object" + ) + invariant( + template.f_type === "InteractionTemplate", + "generateTemplateId({ template }) -- template object must be an InteractionTemplate" + ) + invariant( + template.f_version === "1.1.0", + "generateTemplateId({ template }) -- template object must be an version 1.1.0" + ) + + const templateData = template.data + + const messages = await Promise.all( + templateData.messages.map(async templateMessage => [ + genHash(templateMessage.key), + await Promise.all( + templateMessage.i18n.map(async templateMessagei18n => [ + genHash(templateMessagei18n.tag), + genHash(templateMessagei18n.translation), + ]) + ), + ]) + ) + + const params = await Promise.all( + templateData?.["parameters"] + .sort((a, b) => a.index - b.index) + .map(async arg => [ + genHash(arg.label), + [ + genHash(String(arg.index)), + genHash(arg.type), + await Promise.all( + arg.messages.map(async argumentMessage => [ + genHash(argumentMessage.key), + await Promise.all( + argumentMessage.i18n.map(async argumentMessagei18n => [ + genHash(argumentMessagei18n.tag), + genHash(argumentMessagei18n.translation), + ]) + ), + ]) + ), + ], + ]) + ) + + const dependencies = [ + await generateContractDependencies(templateData?.dependencies), + ] + + const encodedHex = rlpEncode([ + genHash(template?.f_type), + genHash(template?.f_version), + genHash(templateData?.type), + genHash(templateData?.interface), + messages, + genHash(templateData?.cadence?.body), + [dependencies], + params, + ]).toString("hex") + + return genHash(encodedHex) +} diff --git a/packages/fcl-core/src/interaction-template-utils/generate-template-id/generate-template-id-1.1.0.test.js b/packages/fcl-core/src/interaction-template-utils/generate-template-id/generate-template-id-1.1.0.test.js new file mode 100644 index 000000000..3245c05cb --- /dev/null +++ b/packages/fcl-core/src/interaction-template-utils/generate-template-id/generate-template-id-1.1.0.test.js @@ -0,0 +1,444 @@ +import {generateTemplateId} from "./generate-template-id.js" +import {replaceStringImports} from "../utils/replace-string-imports.js" +import {genHash} from "../utils/hash.js" + +const returnedAccount = { + address: "0xf233dcee88fe0abe", + keys: [], + balance: "10", + contracts: { + FungibleToken: +`/** + +# The Flow Fungible Token standard + +## \`FungibleToken\` contract interface + +The interface that all Fungible Token contracts would have to conform to. +If a users wants to deploy a new token contract, their contract +would need to implement the FungibleToken interface. + +Their contract would have to follow all the rules and naming +that the interface specifies. + +## \`Vault\` resource + +Each account that owns tokens would need to have an instance +of the Vault resource stored in their account storage. + +The Vault resource has methods that the owner and other users can call. + +## \`Provider\`, \`Receiver\`, and \`Balance\` resource interfaces + +These interfaces declare pre-conditions and post-conditions that restrict +the execution of the functions in the Vault. + +They are separate because it gives the user the ability to share +a reference to their Vault that only exposes the fields functions +in one or more of the interfaces. + +It also gives users the ability to make custom resources that implement +these interfaces to do various things with the tokens. +For example, a faucet can be implemented by conforming +to the Provider interface. + +By using resources and interfaces, users of Fungible Token contracts +can send and receive tokens peer-to-peer, without having to interact +with a central ledger smart contract. To send tokens to another user, +a user would simply withdraw the tokens from their Vault, then call +the deposit function on another user's Vault to complete the transfer. + +*/ + +/// The interface that Fungible Token contracts implement. +/// +pub contract interface FungibleToken { + + /// The total number of tokens in existence. + /// It is up to the implementer to ensure that the total supply + /// stays accurate and up to date + pub var totalSupply: UFix64 + + /// The event that is emitted when the contract is created + pub event TokensInitialized(initialSupply: UFix64) + + /// The event that is emitted when tokens are withdrawn from a Vault + pub event TokensWithdrawn(amount: UFix64, from: Address?) + + /// The event that is emitted when tokens are deposited into a Vault + pub event TokensDeposited(amount: UFix64, to: Address?) + + /// The interface that enforces the requirements for withdrawing + /// tokens from the implementing type. + /// + /// It does not enforce requirements on \`balance\` here, + /// because it leaves open the possibility of creating custom providers + /// that do not necessarily need their own balance. + /// + pub resource interface Provider { + + /// Subtracts tokens from the owner's Vault + /// and returns a Vault with the removed tokens. + /// + /// The function's access level is public, but this is not a problem + /// because only the owner storing the resource in their account + /// can initially call this function. + /// + /// The owner may grant other accounts access by creating a private + /// capability that allows specific other users to access + /// the provider resource through a reference. + /// + /// The owner may also grant all accounts access by creating a public + /// capability that allows all users to access the provider + /// resource through a reference. + /// + /// @param amount: The amount of tokens to be withdrawn from the vault + /// @return The Vault resource containing the withdrawn funds + /// + pub fun withdraw(amount: UFix64): @Vault { + post { + // \`result\` refers to the return value + result.balance == amount: + "Withdrawal amount must be the same as the balance of the withdrawn Vault" + } + } + } + + /// The interface that enforces the requirements for depositing + /// tokens into the implementing type. + /// + /// We do not include a condition that checks the balance because + /// we want to give users the ability to make custom receivers that + /// can do custom things with the tokens, like split them up and + /// send them to different places. + /// + pub resource interface Receiver { + + /// Takes a Vault and deposits it into the implementing resource type + /// + /// @param from: The Vault resource containing the funds that will be deposited + /// + pub fun deposit(from: @Vault) + + /// Below is referenced from the FLIP #69 https://github.com/onflow/flips/blob/main/flips/20230206-fungible-token-vault-type-discovery.md + /// + /// Returns the dictionary of Vault types that the the receiver is able to accept in its \`deposit\` method + /// this then it would return \`{Type<@FlowToken.Vault>(): true}\` and if any custom receiver + /// uses the default implementation then it would return empty dictionary as its parent + /// resource doesn't conform with the \`FungibleToken.Vault\` resource. + /// + /// Custom receiver implementations are expected to upgrade their contracts to add an implementation + /// that supports this method because it is very valuable for various applications to have. + /// + /// @return dictionary of supported deposit vault types by the implementing resource. + /// + pub fun getSupportedVaultTypes(): {Type: Bool} { + // Below check is implemented to make sure that run-time type would + // only get returned when the parent resource conforms with \`FungibleToken.Vault\`. + if self.getType().isSubtype(of: Type<@FungibleToken.Vault>()) { + return {self.getType(): true} + } else { + // Return an empty dictionary as the default value for resource who don't + // implement \`FungibleToken.Vault\`, such as \`FungibleTokenSwitchboard\`, \`TokenForwarder\` etc. + return {} + } + } + } + + /// The interface that contains the \`balance\` field of the Vault + /// and enforces that when new Vaults are created, the balance + /// is initialized correctly. + /// + pub resource interface Balance { + + /// The total balance of a vault + /// + pub var balance: UFix64 + + init(balance: UFix64) { + post { + self.balance == balance: + "Balance must be initialized to the initial balance" + } + } + + /// Function that returns all the Metadata Views implemented by a Fungible Token + /// + /// @return An array of Types defining the implemented views. This value will be used by + /// developers to know which parameter to pass to the resolveView() method. + /// + pub fun getViews(): [Type] { + return [] + } + + /// Function that resolves a metadata view for this fungible token by type. + /// + /// @param view: The Type of the desired view. + /// @return A structure representing the requested view. + /// + pub fun resolveView(_ view: Type): AnyStruct? { + return nil + } + } + + /// The resource that contains the functions to send and receive tokens. + /// The declaration of a concrete type in a contract interface means that + /// every Fungible Token contract that implements the FungibleToken interface + /// must define a concrete \`Vault\` resource that conforms to the \`Provider\`, \`Receiver\`, + /// and \`Balance\` interfaces, and declares their required fields and functions + /// + pub resource Vault: Provider, Receiver, Balance { + + /// The total balance of the vault + pub var balance: UFix64 + + // The conforming type must declare an initializer + // that allows providing the initial balance of the Vault + // + init(balance: UFix64) + + /// Subtracts \`amount\` from the Vault's balance + /// and returns a new Vault with the subtracted balance + /// + /// @param amount: The amount of tokens to be withdrawn from the vault + /// @return The Vault resource containing the withdrawn funds + /// + pub fun withdraw(amount: UFix64): @Vault { + pre { + self.balance >= amount: + "Amount withdrawn must be less than or equal than the balance of the Vault" + } + post { + // use the special function \`before\` to get the value of the \`balance\` field + // at the beginning of the function execution + // + self.balance == before(self.balance) - amount: + "New Vault balance must be the difference of the previous balance and the withdrawn Vault" + } + } + + /// Takes a Vault and deposits it into the implementing resource type + /// + /// @param from: The Vault resource containing the funds that will be deposited + /// + pub fun deposit(from: @Vault) { + // Assert that the concrete type of the deposited vault is the same + // as the vault that is accepting the deposit + pre { + from.isInstance(self.getType()): + "Cannot deposit an incompatible token type" + } + post { + self.balance == before(self.balance) + before(from.balance): + "New Vault balance must be the sum of the previous balance and the deposited Vault" + } + } + } + + /// Allows any user to create a new Vault that has a zero balance + /// + /// @return The new Vault resource + /// + pub fun createEmptyVault(): @Vault { + post { + result.balance == 0.0: "The newly created Vault must have zero balance" + } + } +} +`, + }, + code: null, +} + +jest.mock("@onflow/sdk", () => ({ + send: jest.fn().mockImplementation(({}) => { + // Adjusted mock implementation + const sanitized = returnedAccount.contracts.FungibleToken.replace(/\\/g, "") + returnedAccount.contracts.FungibleToken = sanitized + return Promise.resolve(returnedAccount) + }), + getAccount: jest.fn().mockImplementation(({}) => { + // Adjusted mock implementation + return Promise.resolve({data: returnedAccount}) + }), + invariant: jest.fn().mockImplementation(({}) => { + // Adjusted mock implementation + return + }), + config: jest.fn().mockImplementation(() => { + // Adjusted mock implementation + return { + get: jest.fn().mockImplementation(() => { + // Adjusted mock implementation + return Promise.resolve("0xf233dcee88fe0abe") + }), + } + }), + atBlockHeight: jest.fn().mockImplementation(({}) => { + // Adjusted mock implementation + return Promise.resolve({}) + }), +})) + +describe("Gen template id interaction template messages 1.1.0", () => { + const template = { + f_type: "InteractionTemplate", + f_version: "1.1.0", + id: "3accd8c0bf4c7b543a80287d6c158043b4c2e737c2205dba6e009abbbf1328a4", + data: { + type: "transaction", + interface: "", + messages: [ + { + key: "title", + i18n: [ + { + tag: "en-US", + translation: "Transfer Tokens", + }, + ], + }, + { + key: "description", + i18n: [ + { + tag: "en-US", + translation: "Transfer Flow to account", + }, + ], + }, + ], + cadence: { + body: 'import "FungibleToken"\n\n#interaction(\n version: "1.1.0",\n title: "Transfer Flow",\n description: "Transfer Flow to account",\n language: "en-US",\n parameters: [\n Parameter(\n name: "amount", \n title: "Amount", \n description: "The amount of FLOW tokens to send"\n ),\n Parameter(\n name: "to", \n title: "To",\n description: "The Flow account the tokens will go to"\n )\n ],\n)\n\ntransaction(amount: UFix64, to: Address) {\n let vault: @FungibleToken.Vault\n \n prepare(signer: AuthAccount) {\n self.vault \u003c- signer\n .borrow\u003c\u0026{FungibleToken.Provider}\u003e(from: /storage/flowTokenVault)!\n .withdraw(amount: amount)\n }\n\n execute {\n getAccount(to)\n .getCapability(/public/flowTokenReceiver)!\n .borrow\u003c\u0026{FungibleToken.Receiver}\u003e()!\n .deposit(from: \u003c-self.vault)\n }\n}', + network_pins: [ + { + network: "mainnet", + pin_self: + "dd046de8ef442e4d708124d5710cb78962eb884a4387df1f0b1daf374bd28278", + }, + { + network: "testnet", + pin_self: + "4089786f5e19fe66b39e347634ca28229851f4de1fd469bd8f327d79510e771f", + }, + ], + }, + dependencies: [ + { + contracts: [ + { + contract: "FungibleToken", + networks: [ + { + network: "mainnet", + address: "0xf233dcee88fe0abe", + dependency_pin_block_height: 70493190, + dependency_pin: { + pin: "ac0208f93d07829ec96584d618ddbec6af3cf4e2866bd5071249e8ec93c7e0dc", + pin_self: + "cdadd5b5897f2dfe35d8b25f4e41fea9f8fca8f40f8a8b506b33701ef5033076", + pin_contract_name: "FungibleToken", + pin_contract_address: "0xf233dcee88fe0abe", + imports: [], + }, + }, + { + network: "testnet", + address: "0x9a0766d93b6608b7", + dependency_pin_block_height: 149595558, + dependency_pin: { + pin: "ac0208f93d07829ec96584d618ddbec6af3cf4e2866bd5071249e8ec93c7e0dc", + pin_self: + "cdadd5b5897f2dfe35d8b25f4e41fea9f8fca8f40f8a8b506b33701ef5033076", + pin_contract_name: "FungibleToken", + pin_contract_address: "0x9a0766d93b6608b7", + imports: [], + }, + }, + { + network: "emulator", + address: "0xee82856bf20e2aa6", + dependency_pin_block_height: 0, + }, + ], + }, + ], + }, + ], + parameters: [ + { + label: "amount", + index: 0, + type: "UFix64", + messages: [ + { + key: "title", + i18n: [ + { + tag: "en-US", + translation: "Amount", + }, + ], + }, + { + key: "description", + i18n: [ + { + tag: "en-US", + translation: "The amount of FLOW tokens to send", + }, + ], + }, + ], + }, + { + label: "to", + index: 1, + type: "Address", + messages: [ + { + key: "title", + i18n: [ + { + tag: "en-US", + translation: "To", + }, + ], + }, + { + key: "description", + i18n: [ + { + tag: "en-US", + translation: "The Flow account the tokens will go to", + }, + ], + }, + ], + }, + ], + }, + } + + test("v1.1.0, mainnet network hash is derived correctly", async () => { + const networkDependencies = {FungibleToken: "0xf233dcee88fe0abe"} + + const popCadence = replaceStringImports({ + cadence: template.data.cadence.body, + networkDependencies, + }) + + const hash = genHash(popCadence) + + expect(hash).toEqual(template.data.cadence.network_pins[0].pin_self) + }) + + test("Test id generation and compare", async () => { + const testId = template.id + const id = await generateTemplateId({ + template, + }) + + expect(id).toEqual(testId) + }) +}) diff --git a/packages/fcl-core/src/interaction-template-utils/generate-template-id/generate-template-id.js b/packages/fcl-core/src/interaction-template-utils/generate-template-id/generate-template-id.js new file mode 100644 index 000000000..74e22809e --- /dev/null +++ b/packages/fcl-core/src/interaction-template-utils/generate-template-id/generate-template-id.js @@ -0,0 +1,46 @@ +import {invariant} from "@onflow/util-invariant" +import {generateTemplateId as generateTemplateId100} from "./generate-template-id-1.0.0.js" +import {generateTemplateId as generateTemplateId110} from "./generate-template-id-1.1.0.js" + +/** + * @description Generates Interaction Template ID for a given Interaction Template + * + * @param {object} params + * @param {object} params.template - Interaction Template + * @returns {Promise} - Interaction Template ID + */ +export async function generateTemplateId({template}) { + invariant( + template, + "generateTemplateId({ template }) -- template must be defined" + ) + invariant( + typeof template === "object", + "generateTemplateId({ template }) -- template must be an object" + ) + invariant( + template.f_type === "InteractionTemplate", + "generateTemplateId({ template }) -- template object must be an InteractionTemplate" + ) + + switch (template.f_version) { + case "1.1.0": + return await generateTemplateId110({template}) + case "1.0.0": + return await generateTemplateId100({template}) + default: + throw new Error("generateTemplateId Error: Unsupported template version") + } +} + +/** + * @description Verifies the given Interaction Template Id has been correctly generated + * + * @param {object} params + * @param {object} params.template - Interaction Template + * @returns {Promise} - true or false, Interaction Template ID + */ + +export async function verifyGeneratedTemplateId({template}) { + return template.id === await generateTemplateId({template}) +} \ No newline at end of file diff --git a/packages/fcl-core/src/interaction-template-utils/generate-template-interface-id.js b/packages/fcl-core/src/interaction-template-utils/generate-template-interface-id.js deleted file mode 100644 index 0a75932f8..000000000 --- a/packages/fcl-core/src/interaction-template-utils/generate-template-interface-id.js +++ /dev/null @@ -1,53 +0,0 @@ -import {invariant} from "@onflow/sdk" -import {encode as rlpEncode} from "@onflow/rlp" -import {genHash} from "./utils/hash.js" -import {normalizeInteractionTemplateInterface} from "../normalizers/interaction-template/interaction-template-interface.js" - -/** - * @description Generates Interaction Template Interface ID for a given Interaction Template Interface - * - * @param {object} params - * @param {object} params.templateInterface - Interaction Template Interface - * @returns {Promise} - Interaction Template Interface ID - */ -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-core/src/interaction-template-utils/get-interaction-template-audits.js b/packages/fcl-core/src/interaction-template-utils/get-interaction-template-audits.js index 9eec7799b..8c59d595a 100644 --- a/packages/fcl-core/src/interaction-template-utils/get-interaction-template-audits.js +++ b/packages/fcl-core/src/interaction-template-utils/get-interaction-template-audits.js @@ -1,8 +1,7 @@ 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" +import {generateTemplateId} from "./generate-template-id/generate-template-id.js" import {getChainId} from "../utils" /** @@ -23,9 +22,6 @@ export async function getInteractionTemplateAudits( template != undefined, "getInteractionTemplateAudits({ template }) -- template must be defined" ) - - template = normalizeInteractionTemplate(template) - invariant( template.f_type === "InteractionTemplate", "getInteractionTemplateAudits({ template }) -- template must be an InteractionTemplate" @@ -49,6 +45,7 @@ export async function getInteractionTemplateAudits( } switch (template.f_version) { + case "1.1.0": case "1.0.0": const _auditors = auditors || (await config().get("flow.auditors")) diff --git a/packages/fcl-core/src/interaction-template-utils/get-template-argument-message.js b/packages/fcl-core/src/interaction-template-utils/get-template-argument-message.js index 68129830f..9112ae7f1 100644 --- a/packages/fcl-core/src/interaction-template-utils/get-template-argument-message.js +++ b/packages/fcl-core/src/interaction-template-utils/get-template-argument-message.js @@ -2,7 +2,7 @@ import {invariant} from "@onflow/sdk" /** * @description Gets Interaction Template argument message by message key, argument label, and localization - * + * * @param {object} opts * @param {string} opts.localization [localization="en-US"] - Localization to get message for * @param {string} opts.argumentLabel - Argument label to get message for @@ -56,7 +56,23 @@ export function getTemplateArgumentMessage({ "getTemplateArgumentMessage({ template }) -- template object must be an InteractionTemplate" ) - const args = template?.data?.arguments - - return args?.[argumentLabel]?.messages?.[messageKey]?.i18n?.[localization] + switch (template.f_version) { + case "1.1.0": + const param = template?.data?.parameters?.find( + a => a.label === argumentLabel + ) + if (!param) return undefined + const message = param?.messages?.find(a => a.key === messageKey) + if (!message) return undefined + const lzn = message?.i18n?.find(a => a.tag === localization) + if (!lzn) return undefined + return lzn.translation + case "1.0.0": + return template?.data?.arguments?.[argumentLabel]?.messages?.[messageKey] + ?.i18n?.[localization] + default: + throw new Error( + "getTemplateArgumentMessage Error: Unsupported template version" + ) + } } diff --git a/packages/fcl-core/src/interaction-template-utils/get-template-argument-message.test.js b/packages/fcl-core/src/interaction-template-utils/get-template-argument-message.test.js index 6e8bd6334..e40e2715c 100644 --- a/packages/fcl-core/src/interaction-template-utils/get-template-argument-message.test.js +++ b/packages/fcl-core/src/interaction-template-utils/get-template-argument-message.test.js @@ -1,7 +1,7 @@ import {getTemplateArgumentMessage} from "./get-template-argument-message.js" describe("Get interaction template argument messages", () => { - let template = { + const templatev1 = { f_type: "InteractionTemplate", f_version: "1.0.0", id: "abc123", @@ -69,33 +69,207 @@ describe("Get interaction template argument messages", () => { } test("It gets argument message for given argument and internationalization", async () => { - let message = getTemplateArgumentMessage({ + const message = getTemplateArgumentMessage({ localization: "en-US", argumentLabel: "amount", messageKey: "title", - template, + template: templatev1, }) expect(message).toEqual("The amount of FLOW tokens to send") }) test("It fails to get message for an unknown argument", async () => { - let message = getTemplateArgumentMessage({ + const message = getTemplateArgumentMessage({ localization: "en-US", argumentLabel: "foo", messageKey: "title", - template, + template: templatev1, }) expect(message).toEqual(undefined) }) test("It fails to get message for an unknown message key", async () => { - let message = getTemplateArgumentMessage({ + const message = getTemplateArgumentMessage({ localization: "en-US", argumentLabel: "amount", messageKey: "baz", - template, + template: templatev1, + }) + + expect(message).toEqual(undefined) + }) +}) + +describe("Get interaction template v1.1.0 parameters messages", () => { + const templatev11 = { + f_type: "InteractionTemplate", + f_version: "1.1.0", + id: "3a99af243b85f3f6af28304af2ed53a37fb913782b3efc483e6f0162a47720a0", + data: { + type: "transaction", + interface: "", + messages: [ + { + key: "title", + i18n: [ + { + tag: "en-US", + translation: "Transfer Flow", + }, + ], + }, + { + key: "description", + i18n: [ + { + tag: "en-US", + translation: "Transfer Flow to account", + }, + ], + }, + ], + cadence: { + body: 'import "FungibleToken"\n\n#interaction(\n version: "1.1.0",\n title: "Transfer Flow",\n description: "Transfer Flow to account",\n language: "en-US",\n parameters: [\n Parameter(\n name: "amount", \n title: "Amount", \n description: "The amount of FLOW tokens to send"\n ),\n Parameter(\n name: "to", \n title: "To",\n description: "The Flow account the tokens will go to"\n )\n ],\n)\n\ntransaction(amount: UFix64, to: Address) {\n let vault: @FungibleToken.Vault\n \n prepare(signer: AuthAccount) {\n self.vault \u003c- signer\n .borrow\u003c\u0026{FungibleToken.Provider}\u003e(from: /storage/flowTokenVault)!\n .withdraw(amount: amount)\n }\n\n execute {\n getAccount(to)\n .getCapability(/public/flowTokenReceiver)!\n .borrow\u003c\u0026{FungibleToken.Receiver}\u003e()!\n .deposit(from: \u003c-self.vault)\n }\n}', + network_pins: [ + { + network: "mainnet", + pin_self: + "dd046de8ef442e4d708124d5710cb78962eb884a4387df1f0b1daf374bd28278", + }, + { + network: "testnet", + pin_self: + "4089786f5e19fe66b39e347634ca28229851f4de1fd469bd8f327d79510e771f", + }, + ], + }, + dependencies: [ + { + contracts: [ + { + contract: "FungibleToken", + networks: [ + { + network: "mainnet", + address: "0xf233dcee88fe0abe", + dependency_pin_block_height: 70493190, + dependency_pin: { + pin: "ac0208f93d07829ec96584d618ddbec6af3cf4e2866bd5071249e8ec93c7e0dc", + pin_self: + "cdadd5b5897f2dfe35d8b25f4e41fea9f8fca8f40f8a8b506b33701ef5033076", + pin_contract_name: "FungibleToken", + pin_contract_address: "0xf233dcee88fe0abe", + imports: [], + }, + }, + { + network: "testnet", + address: "0x9a0766d93b6608b7", + dependency_pin_block_height: 149595558, + dependency_pin: { + pin: "ac0208f93d07829ec96584d618ddbec6af3cf4e2866bd5071249e8ec93c7e0dc", + pin_self: + "cdadd5b5897f2dfe35d8b25f4e41fea9f8fca8f40f8a8b506b33701ef5033076", + pin_contract_name: "FungibleToken", + pin_contract_address: "0x9a0766d93b6608b7", + imports: [], + }, + }, + { + network: "emulator", + address: "0xee82856bf20e2aa6", + dependency_pin_block_height: 0, + }, + ], + }, + ], + }, + ], + parameters: [ + { + label: "amount", + index: 0, + type: "UFix64", + messages: [ + { + key: "title", + i18n: [ + { + tag: "en-US", + translation: "Amount", + }, + ], + }, + { + key: "description", + i18n: [ + { + tag: "en-US", + translation: "The amount of FLOW tokens to send", + }, + ], + }, + ], + }, + { + label: "to", + index: 1, + type: "Address", + messages: [ + { + key: "title", + i18n: [ + { + tag: "en-US", + translation: "To", + }, + ], + }, + { + key: "description", + i18n: [ + { + tag: "en-US", + translation: "The Flow account the tokens will go to", + }, + ], + }, + ], + }, + ], + }, + } + + test("It gets argument message for given argument and internationalization", async () => { + const message = getTemplateArgumentMessage({ + localization: "en-US", + argumentLabel: "amount", + messageKey: "title", + template: templatev11, + }) + + expect(message).toEqual("Amount") + }) + + test("It fails to get message for an unknown argument", async () => { + const message = getTemplateArgumentMessage({ + localization: "en-US", + argumentLabel: "foo", + messageKey: "title", + template: templatev11, + }) + + expect(message).toEqual(undefined) + }) + + test("It fails to get message for an unknown message key", async () => { + const message = getTemplateArgumentMessage({ + localization: "en-US", + argumentLabel: "amount", + messageKey: "baz", + template: templatev11, }) expect(message).toEqual(undefined) diff --git a/packages/fcl-core/src/interaction-template-utils/get-template-message.js b/packages/fcl-core/src/interaction-template-utils/get-template-message.js index 20bc43d7f..a88cb5de3 100644 --- a/packages/fcl-core/src/interaction-template-utils/get-template-message.js +++ b/packages/fcl-core/src/interaction-template-utils/get-template-message.js @@ -2,7 +2,7 @@ import {invariant} from "@onflow/sdk" /** * @description Get Interaction Template argument message - * + * * @param {object} params * @param {string} params.localization [localization="en-US"] - Localization code * @param {string} params.messageKey - Message key @@ -45,7 +45,18 @@ export function getTemplateMessage({ "getTemplateMessage({ template }) -- template object must be an InteractionTemplate" ) - const messages = template?.data?.messages - - return messages?.[messageKey]?.i18n?.[localization] + switch (template.f_version) { + case "1.1.0": + const msg = template?.data?.messages?.find(a => a.key === messageKey) + if (!msg) return undefined + const lzn = msg?.i18n?.find(a => a.tag === localization) + if (!lzn) return undefined + return lzn.translation + case "1.0.0": + return template?.data?.messages?.[messageKey]?.i18n?.[localization] + default: + throw new Error( + "getTemplateArgumentMessage Error: Unsupported template version" + ) + } } diff --git a/packages/fcl-core/src/interaction-template-utils/get-template-message.test.js b/packages/fcl-core/src/interaction-template-utils/get-template-message.test.js index bb9d10cba..ce4af4b6c 100644 --- a/packages/fcl-core/src/interaction-template-utils/get-template-message.test.js +++ b/packages/fcl-core/src/interaction-template-utils/get-template-message.test.js @@ -1,7 +1,7 @@ import {getTemplateMessage} from "./get-template-message.js" -describe("Get interaction template messages", () => { - let template = { +describe("Get interaction template messages 1.0.0", () => { + const template = { f_type: "InteractionTemplate", f_version: "1.0.0", id: "abc123", @@ -87,7 +87,179 @@ describe("Get interaction template messages", () => { }) test("It fails to get message for an unknown message key", async () => { - let message = getTemplateMessage({ + const message = getTemplateMessage({ + localization: "en-US", + messageKey: "foo", + template, + }) + + expect(message).toEqual(undefined) + }) +}) + +describe("Get interaction template messages 1.1.0", () => { + const template = { + f_type: "InteractionTemplate", + f_version: "1.1.0", + id: "3a99af243b85f3f6af28304af2ed53a37fb913782b3efc483e6f0162a47720a0", + data: { + type: "transaction", + interface: "", + messages: [ + { + key: "title", + i18n: [ + { + tag: "en-US", + translation: "Transfer Tokens", + }, + ], + }, + { + key: "description", + i18n: [ + { + tag: "en-US", + translation: "Transfer Flow to account", + }, + ], + }, + ], + cadence: { + body: 'import "FungibleToken"\n\n#interaction(\n version: "1.1.0",\n title: "Transfer Flow",\n description: "Transfer Flow to account",\n language: "en-US",\n parameters: [\n Parameter(\n name: "amount", \n title: "Amount", \n description: "The amount of FLOW tokens to send"\n ),\n Parameter(\n name: "to", \n title: "To",\n description: "The Flow account the tokens will go to"\n )\n ],\n)\n\ntransaction(amount: UFix64, to: Address) {\n let vault: @FungibleToken.Vault\n \n prepare(signer: AuthAccount) {\n self.vault \u003c- signer\n .borrow\u003c\u0026{FungibleToken.Provider}\u003e(from: /storage/flowTokenVault)!\n .withdraw(amount: amount)\n }\n\n execute {\n getAccount(to)\n .getCapability(/public/flowTokenReceiver)!\n .borrow\u003c\u0026{FungibleToken.Receiver}\u003e()!\n .deposit(from: \u003c-self.vault)\n }\n}', + network_pins: [ + { + network: "mainnet", + pin_self: + "dd046de8ef442e4d708124d5710cb78962eb884a4387df1f0b1daf374bd28278", + }, + { + network: "testnet", + pin_self: + "4089786f5e19fe66b39e347634ca28229851f4de1fd469bd8f327d79510e771f", + }, + ], + }, + dependencies: [ + { + contracts: [ + { + contract: "FungibleToken", + networks: [ + { + network: "mainnet", + address: "0xf233dcee88fe0abe", + dependency_pin_block_height: 70493190, + dependency_pin: { + pin: "ac0208f93d07829ec96584d618ddbec6af3cf4e2866bd5071249e8ec93c7e0dc", + pin_self: + "cdadd5b5897f2dfe35d8b25f4e41fea9f8fca8f40f8a8b506b33701ef5033076", + pin_contract_name: "FungibleToken", + pin_contract_address: "0xf233dcee88fe0abe", + imports: [], + }, + }, + { + network: "testnet", + address: "0x9a0766d93b6608b7", + dependency_pin_block_height: 149595558, + dependency_pin: { + pin: "ac0208f93d07829ec96584d618ddbec6af3cf4e2866bd5071249e8ec93c7e0dc", + pin_self: + "cdadd5b5897f2dfe35d8b25f4e41fea9f8fca8f40f8a8b506b33701ef5033076", + pin_contract_name: "FungibleToken", + pin_contract_address: "0x9a0766d93b6608b7", + imports: [], + }, + }, + { + network: "emulator", + address: "0xee82856bf20e2aa6", + dependency_pin_block_height: 0, + }, + ], + }, + ], + }, + ], + parameters: [ + { + label: "amount", + index: 0, + type: "UFix64", + messages: [ + { + key: "title", + i18n: [ + { + tag: "en-US", + translation: "Amount", + }, + ], + }, + { + key: "description", + i18n: [ + { + tag: "en-US", + translation: "The amount of FLOW tokens to send", + }, + ], + }, + ], + }, + { + label: "to", + index: 1, + type: "Address", + messages: [ + { + key: "title", + i18n: [ + { + tag: "en-US", + translation: "To", + }, + ], + }, + { + key: "description", + i18n: [ + { + tag: "en-US", + translation: "The Flow account the tokens will go to", + }, + ], + }, + ], + }, + ], + }, + } + + test("It gets template title message for given message key and internationalization", async () => { + const title = getTemplateMessage({ + localization: "en-US", + messageKey: "title", + template, + }) + + expect(title).toEqual("Transfer Tokens") + }) + + test("It gets template description message for given message key and internationalization", async () => { + const description = getTemplateMessage({ + localization: "en-US", + messageKey: "description", + template, + }) + + expect(description).toEqual("Transfer Flow to account") + }) + + + test("It fails to get message for an unknown message key", async () => { + const message = getTemplateMessage({ localization: "en-US", messageKey: "foo", template, diff --git a/packages/fcl-core/src/interaction-template-utils/index.js b/packages/fcl-core/src/interaction-template-utils/index.js index 6672f6f39..5187a1265 100644 --- a/packages/fcl-core/src/interaction-template-utils/index.js +++ b/packages/fcl-core/src/interaction-template-utils/index.js @@ -2,13 +2,15 @@ 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" +} from "./generate-dependency-pin/generate-dependency-pin.js" +export { + generateTemplateId, + verifyGeneratedTemplateId, +} from "./generate-template-id/generate-template-id.js" export { verifyDependencyPinsSame, verifyDependencyPinsSameAtLatestSealedBlock, } from "./verify-dependency-pin-same-at-block.js" -export {deriveCadenceByNetwork} from "./derive-cadence-by-network.js" +export {deriveCadenceByNetwork} from "./derive-cadence-by-network/derive-cadence-by-network.js" export {getTemplateMessage} from "./get-template-message.js" export {getTemplateArgumentMessage} from "./get-template-argument-message.js" diff --git a/packages/fcl-core/src/interaction-template-utils/utils/find-imports.js b/packages/fcl-core/src/interaction-template-utils/utils/find-imports.js index 228effdd1..87d55a764 100644 --- a/packages/fcl-core/src/interaction-template-utils/utils/find-imports.js +++ b/packages/fcl-core/src/interaction-template-utils/utils/find-imports.js @@ -1,10 +1,10 @@ import {generateImport} from "./generate-import.js" export function findImports(cadence) { - let imports = [] + const imports = [] - let importsReg = /import ((\w|,| )+)* from 0x\w+/g - let fileImports = cadence.match(importsReg) || [] + const importsReg = /import ((\w|,| )+)* from 0x\w+/g + const fileImports = cadence.match(importsReg) || [] for (const fileImport of fileImports) { const importLineReg = /import ((\w+|, |)*) from (0x\w+)/g diff --git a/packages/fcl-core/src/interaction-template-utils/utils/find-imports.test.js b/packages/fcl-core/src/interaction-template-utils/utils/find-imports.test.js index 4a9d5f615..af68a88d0 100644 --- a/packages/fcl-core/src/interaction-template-utils/utils/find-imports.test.js +++ b/packages/fcl-core/src/interaction-template-utils/utils/find-imports.test.js @@ -34,7 +34,7 @@ describe("Find imports", () => { ` test("It parses contracts correctly for cadence A", async () => { - let imports = findImports(cadenceA) + const imports = findImports(cadenceA) expect(imports).toEqual([ generateImport({ @@ -49,7 +49,7 @@ describe("Find imports", () => { }) test("It parses contracts correctly for cadence B", async () => { - let imports = findImports(cadenceA) + const imports = findImports(cadenceA) expect(imports).toEqual([ generateImport({ @@ -64,7 +64,7 @@ describe("Find imports", () => { }) test("It parses contracts correctly for cadence C", async () => { - let imports = findImports(cadenceC) + const imports = findImports(cadenceC) expect(imports).toEqual([ generateImport({ diff --git a/packages/fcl-core/src/interaction-template-utils/utils/hash.js b/packages/fcl-core/src/interaction-template-utils/utils/hash.js index c65c54884..14917c2b4 100644 --- a/packages/fcl-core/src/interaction-template-utils/utils/hash.js +++ b/packages/fcl-core/src/interaction-template-utils/utils/hash.js @@ -1,7 +1,7 @@ import {SHA3} from "sha3" import {Buffer} from "@onflow/rlp" -export async function genHash(utf8String) { +export function genHash(utf8String) { const sha = new SHA3(256) sha.update(Buffer.from(utf8String, "utf8")) return sha.digest("hex") diff --git a/packages/fcl-core/src/interaction-template-utils/utils/replace-string-imports.js b/packages/fcl-core/src/interaction-template-utils/utils/replace-string-imports.js new file mode 100644 index 000000000..a31e092c8 --- /dev/null +++ b/packages/fcl-core/src/interaction-template-utils/utils/replace-string-imports.js @@ -0,0 +1,16 @@ + +/** + * @description - Replaces string imports with the actual contract address + * + * @param {object} param + * @param {string} param.cadence + * @param {object} param.networkDependencies + * @returns {string} - Cadence + */ +export function replaceStringImports({cadence, networkDependencies}) { + return Object.keys(networkDependencies).reduce((c, contractName) => { + const address = networkDependencies[contractName] + const regex = new RegExp(`import "\\b${contractName}\\b"`, "g"); + return c.replace(regex, `import ${contractName} from ${address}`); + }, cadence) +} \ No newline at end of file diff --git a/packages/fcl-core/src/interaction-template-utils/utils/replace-string-imports.test.js b/packages/fcl-core/src/interaction-template-utils/utils/replace-string-imports.test.js new file mode 100644 index 000000000..7c0c0da93 --- /dev/null +++ b/packages/fcl-core/src/interaction-template-utils/utils/replace-string-imports.test.js @@ -0,0 +1,26 @@ +import {replaceStringImports} from "./replace-string-imports" + +describe("Verify replace imports works ", () => { + test("replace single import", async () => { + const cadence = await replaceStringImports({ + cadence: 'import "FungibleToken"\n\n', + networkDependencies: { + FungibleToken: "0xf233dcee88fe0abe", + }, + }) + expect(cadence).toEqual("import FungibleToken from 0xf233dcee88fe0abe\n\n") + }) + + + test("replace multiple import", async () => { + const cadence = await replaceStringImports({ + cadence: 'import "FungibleToken"\nimport "NonFungibleToken"\n\n', + networkDependencies: { + FungibleToken: "0xf233dcee88fe0abe", + NonFungibleToken: "0x1d7e57aa55817448" + }, + }) + + expect(cadence).toEqual("import FungibleToken from 0xf233dcee88fe0abe\nimport NonFungibleToken from 0x1d7e57aa55817448\n\n") + }) +}) diff --git a/packages/fcl-core/src/interaction-template-utils/verify-dependency-pin-same-at-block.js b/packages/fcl-core/src/interaction-template-utils/verify-dependency-pin-same-at-block.js index e87b6825a..b91dae62b 100644 --- a/packages/fcl-core/src/interaction-template-utils/verify-dependency-pin-same-at-block.js +++ b/packages/fcl-core/src/interaction-template-utils/verify-dependency-pin-same-at-block.js @@ -1,11 +1,10 @@ -import {generateDependencyPin} from "./generate-dependency-pin.js" +import {generateDependencyPin} from "./generate-dependency-pin/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" /** * @description Checks if an Interaction Template's pins match those generated at a block height - * + * * @param {object} params * @param {object} params.template - Interaction Template to check pins for * @param {number} params.blockHeight - Block height to check pins at @@ -30,8 +29,6 @@ export async function verifyDependencyPinsSame( "generateDependencyPin({ template }) -- template must be an InteractionTemplate" ) - template = normalizeInteractionTemplate(template) - invariant( network != undefined, "generateDependencyPin({ network }) network must be defined" @@ -47,29 +44,29 @@ export async function verifyDependencyPinsSame( switch (template.f_version) { case "1.0.0": - let templateDependenciesPlaceholderKeys = Object.keys( + const templateDependenciesPlaceholderKeys = Object.keys( template.data.dependencies ) for (let templateDependencyPlaceholderKey of templateDependenciesPlaceholderKeys) { - let templateDependencyPlaceholder = + const templateDependencyPlaceholder = template.data.dependencies[templateDependencyPlaceholderKey] - let templateDependencyPlaceholderContractNames = Object.keys( + const templateDependencyPlaceholderContractNames = Object.keys( templateDependencyPlaceholder ) for (let templateDependencyPlaceholderContractName of templateDependencyPlaceholderContractNames) { - let templateDependencyPlaceholderContractNetworks = + const templateDependencyPlaceholderContractNetworks = template.data.dependencies[templateDependencyPlaceholderKey][ templateDependencyPlaceholderContractName ] - let templateDependency = + const templateDependency = templateDependencyPlaceholderContractNetworks[network] if (typeof templateDependency === "undefined") continue - let pin = await generateDependencyPin( + const pin = await generateDependencyPin( { address: templateDependency.address, contractName: templateDependency.contract, @@ -95,6 +92,49 @@ export async function verifyDependencyPinsSame( return true + case "1.1.0": + let isVerified = false + // iterate over each dependency + for (let i = 0; i < template.data?.dependencies.length; i++) { + const dependency = template.data?.dependencies[i] + // iterate over each contract in dependency + for (let j = 0; j < dependency?.contracts.length; j++) { + const contract = dependency?.contracts[j] + // iterate over each network in contract + for (let k = 0; k < contract?.networks.length; k++) { + const net = contract?.networks[k] + // if network matches, generate pin and compare + if (net.network === network) { + const pin = await generateDependencyPin( + { + version: template.f_version, + address: net.address, + contractName: contract.contract, + blockHeight, + }, + opts + ) + + if (pin !== net.dependency_pin.pin) { + log({ + title: "verifyDependencyPinsSame Debug Error", + message: `Could not recompute and match dependency pin. + address: ${net.address} | contract: ${contract.contract} + computed: ${pin} + template: ${net.pin} + `, + level: LEVELS.debug, + }) + return false + } else { + isVerified = true + } + } + } + } + } + return isVerified + default: throw new Error( "verifyDependencyPinsSame Error: Unsupported template version" @@ -104,7 +144,7 @@ export async function verifyDependencyPinsSame( /** * @description Checks if an Interaction Template's pins match those generated at the latest block height - * + * * @param {object} params * @param {object} params.template - Interaction Template to check pins for * @param {string} params.network - Network to check pins on @@ -115,8 +155,8 @@ export async function verifyDependencyPinsSameAtLatestSealedBlock( {template, network}, opts = {} ) { - let latestSealedBlock = await block({sealed: true}) - let latestSealedBlockHeight = latestSealedBlock?.height + const latestSealedBlock = await block({sealed: true}) + const latestSealedBlockHeight = latestSealedBlock?.height return verifyDependencyPinsSame( {template, network, blockHeight: latestSealedBlockHeight}, diff --git a/packages/fcl-core/src/interaction-template-utils/verify-dependency-pin-same-at-block.test.js b/packages/fcl-core/src/interaction-template-utils/verify-dependency-pin-same-at-block.test.js new file mode 100644 index 000000000..009ffca14 --- /dev/null +++ b/packages/fcl-core/src/interaction-template-utils/verify-dependency-pin-same-at-block.test.js @@ -0,0 +1,444 @@ +import {verifyDependencyPinsSame} from "./verify-dependency-pin-same-at-block.js" +import {config} from "@onflow/config" + +const returnedAccount = { + address: "0xf233dcee88fe0abe", + keys: [], + balance: "10", + contracts: { + FungibleToken: +`/** + +# The Flow Fungible Token standard + +## \`FungibleToken\` contract interface + +The interface that all Fungible Token contracts would have to conform to. +If a users wants to deploy a new token contract, their contract +would need to implement the FungibleToken interface. + +Their contract would have to follow all the rules and naming +that the interface specifies. + +## \`Vault\` resource + +Each account that owns tokens would need to have an instance +of the Vault resource stored in their account storage. + +The Vault resource has methods that the owner and other users can call. + +## \`Provider\`, \`Receiver\`, and \`Balance\` resource interfaces + +These interfaces declare pre-conditions and post-conditions that restrict +the execution of the functions in the Vault. + +They are separate because it gives the user the ability to share +a reference to their Vault that only exposes the fields functions +in one or more of the interfaces. + +It also gives users the ability to make custom resources that implement +these interfaces to do various things with the tokens. +For example, a faucet can be implemented by conforming +to the Provider interface. + +By using resources and interfaces, users of Fungible Token contracts +can send and receive tokens peer-to-peer, without having to interact +with a central ledger smart contract. To send tokens to another user, +a user would simply withdraw the tokens from their Vault, then call +the deposit function on another user's Vault to complete the transfer. + +*/ + +/// The interface that Fungible Token contracts implement. +/// +pub contract interface FungibleToken { + + /// The total number of tokens in existence. + /// It is up to the implementer to ensure that the total supply + /// stays accurate and up to date + pub var totalSupply: UFix64 + + /// The event that is emitted when the contract is created + pub event TokensInitialized(initialSupply: UFix64) + + /// The event that is emitted when tokens are withdrawn from a Vault + pub event TokensWithdrawn(amount: UFix64, from: Address?) + + /// The event that is emitted when tokens are deposited into a Vault + pub event TokensDeposited(amount: UFix64, to: Address?) + + /// The interface that enforces the requirements for withdrawing + /// tokens from the implementing type. + /// + /// It does not enforce requirements on \`balance\` here, + /// because it leaves open the possibility of creating custom providers + /// that do not necessarily need their own balance. + /// + pub resource interface Provider { + + /// Subtracts tokens from the owner's Vault + /// and returns a Vault with the removed tokens. + /// + /// The function's access level is public, but this is not a problem + /// because only the owner storing the resource in their account + /// can initially call this function. + /// + /// The owner may grant other accounts access by creating a private + /// capability that allows specific other users to access + /// the provider resource through a reference. + /// + /// The owner may also grant all accounts access by creating a public + /// capability that allows all users to access the provider + /// resource through a reference. + /// + /// @param amount: The amount of tokens to be withdrawn from the vault + /// @return The Vault resource containing the withdrawn funds + /// + pub fun withdraw(amount: UFix64): @Vault { + post { + // \`result\` refers to the return value + result.balance == amount: + "Withdrawal amount must be the same as the balance of the withdrawn Vault" + } + } + } + + /// The interface that enforces the requirements for depositing + /// tokens into the implementing type. + /// + /// We do not include a condition that checks the balance because + /// we want to give users the ability to make custom receivers that + /// can do custom things with the tokens, like split them up and + /// send them to different places. + /// + pub resource interface Receiver { + + /// Takes a Vault and deposits it into the implementing resource type + /// + /// @param from: The Vault resource containing the funds that will be deposited + /// + pub fun deposit(from: @Vault) + + /// Below is referenced from the FLIP #69 https://github.com/onflow/flips/blob/main/flips/20230206-fungible-token-vault-type-discovery.md + /// + /// Returns the dictionary of Vault types that the the receiver is able to accept in its \`deposit\` method + /// this then it would return \`{Type<@FlowToken.Vault>(): true}\` and if any custom receiver + /// uses the default implementation then it would return empty dictionary as its parent + /// resource doesn't conform with the \`FungibleToken.Vault\` resource. + /// + /// Custom receiver implementations are expected to upgrade their contracts to add an implementation + /// that supports this method because it is very valuable for various applications to have. + /// + /// @return dictionary of supported deposit vault types by the implementing resource. + /// + pub fun getSupportedVaultTypes(): {Type: Bool} { + // Below check is implemented to make sure that run-time type would + // only get returned when the parent resource conforms with \`FungibleToken.Vault\`. + if self.getType().isSubtype(of: Type<@FungibleToken.Vault>()) { + return {self.getType(): true} + } else { + // Return an empty dictionary as the default value for resource who don't + // implement \`FungibleToken.Vault\`, such as \`FungibleTokenSwitchboard\`, \`TokenForwarder\` etc. + return {} + } + } + } + + /// The interface that contains the \`balance\` field of the Vault + /// and enforces that when new Vaults are created, the balance + /// is initialized correctly. + /// + pub resource interface Balance { + + /// The total balance of a vault + /// + pub var balance: UFix64 + + init(balance: UFix64) { + post { + self.balance == balance: + "Balance must be initialized to the initial balance" + } + } + + /// Function that returns all the Metadata Views implemented by a Fungible Token + /// + /// @return An array of Types defining the implemented views. This value will be used by + /// developers to know which parameter to pass to the resolveView() method. + /// + pub fun getViews(): [Type] { + return [] + } + + /// Function that resolves a metadata view for this fungible token by type. + /// + /// @param view: The Type of the desired view. + /// @return A structure representing the requested view. + /// + pub fun resolveView(_ view: Type): AnyStruct? { + return nil + } + } + + /// The resource that contains the functions to send and receive tokens. + /// The declaration of a concrete type in a contract interface means that + /// every Fungible Token contract that implements the FungibleToken interface + /// must define a concrete \`Vault\` resource that conforms to the \`Provider\`, \`Receiver\`, + /// and \`Balance\` interfaces, and declares their required fields and functions + /// + pub resource Vault: Provider, Receiver, Balance { + + /// The total balance of the vault + pub var balance: UFix64 + + // The conforming type must declare an initializer + // that allows providing the initial balance of the Vault + // + init(balance: UFix64) + + /// Subtracts \`amount\` from the Vault's balance + /// and returns a new Vault with the subtracted balance + /// + /// @param amount: The amount of tokens to be withdrawn from the vault + /// @return The Vault resource containing the withdrawn funds + /// + pub fun withdraw(amount: UFix64): @Vault { + pre { + self.balance >= amount: + "Amount withdrawn must be less than or equal than the balance of the Vault" + } + post { + // use the special function \`before\` to get the value of the \`balance\` field + // at the beginning of the function execution + // + self.balance == before(self.balance) - amount: + "New Vault balance must be the difference of the previous balance and the withdrawn Vault" + } + } + + /// Takes a Vault and deposits it into the implementing resource type + /// + /// @param from: The Vault resource containing the funds that will be deposited + /// + pub fun deposit(from: @Vault) { + // Assert that the concrete type of the deposited vault is the same + // as the vault that is accepting the deposit + pre { + from.isInstance(self.getType()): + "Cannot deposit an incompatible token type" + } + post { + self.balance == before(self.balance) + before(from.balance): + "New Vault balance must be the sum of the previous balance and the deposited Vault" + } + } + } + + /// Allows any user to create a new Vault that has a zero balance + /// + /// @return The new Vault resource + /// + pub fun createEmptyVault(): @Vault { + post { + result.balance == 0.0: "The newly created Vault must have zero balance" + } + } +} +`, + }, + code: null, +} + +jest.mock("@onflow/sdk", () => ({ + send: jest.fn().mockImplementation(({}) => { + // Adjusted mock implementation + const sanitized = returnedAccount.contracts.FungibleToken.replace(/\\/g, ''); + returnedAccount.contracts.FungibleToken = sanitized; + return Promise.resolve(returnedAccount) + }), + getAccount: jest.fn().mockImplementation(({}) => { + // Adjusted mock implementation + return Promise.resolve({data: returnedAccount}) + }), + invariant: jest.fn().mockImplementation(({}) => { + // Adjusted mock implementation + return + }), + config: jest.fn().mockImplementation(() => { + // Adjusted mock implementation + return { + get: jest.fn().mockImplementation(() => { + // Adjusted mock implementation + return Promise.resolve("0xf233dcee88fe0abe") + }), + } + }), + atBlockHeight: jest.fn().mockImplementation(({}) => { + // Adjusted mock implementation + return + }) +})) + +const template = { + f_type: "InteractionTemplate", + f_version: "1.1.0", + id: "3accd8c0bf4c7b543a80287d6c158043b4c2e737c2205dba6e009abbbf1328a4", + data: { + type: "transaction", + interface: "", + messages: [ + { + key: "title", + i18n: [ + { + tag: "en-US", + translation: "Transfer Tokens", + }, + ], + }, + { + key: "description", + i18n: [ + { + tag: "en-US", + translation: "Transfer Flow to account", + }, + ], + }, + ], + cadence: { + body: 'import "FungibleToken"\n\n#interaction(\n version: "1.1.0",\n title: "Transfer Flow",\n description: "Transfer Flow to account",\n language: "en-US",\n parameters: [\n Parameter(\n name: "amount", \n title: "Amount", \n description: "The amount of FLOW tokens to send"\n ),\n Parameter(\n name: "to", \n title: "To",\n description: "The Flow account the tokens will go to"\n )\n ],\n)\n\ntransaction(amount: UFix64, to: Address) {\n let vault: @FungibleToken.Vault\n \n prepare(signer: AuthAccount) {\n self.vault \u003c- signer\n .borrow\u003c\u0026{FungibleToken.Provider}\u003e(from: /storage/flowTokenVault)!\n .withdraw(amount: amount)\n }\n\n execute {\n getAccount(to)\n .getCapability(/public/flowTokenReceiver)!\n .borrow\u003c\u0026{FungibleToken.Receiver}\u003e()!\n .deposit(from: \u003c-self.vault)\n }\n}', + network_pins: [ + { + network: "mainnet", + pin_self: + "dd046de8ef442e4d708124d5710cb78962eb884a4387df1f0b1daf374bd28278", + }, + { + network: "testnet", + pin_self: + "4089786f5e19fe66b39e347634ca28229851f4de1fd469bd8f327d79510e771f", + }, + ], + }, + dependencies: [ + { + contracts: [ + { + contract: "FungibleToken", + networks: [ + { + network: "mainnet", + address: "0xf233dcee88fe0abe", + dependency_pin_block_height: 70493190, + dependency_pin: { + pin: "ac0208f93d07829ec96584d618ddbec6af3cf4e2866bd5071249e8ec93c7e0dc", + pin_self: + "cdadd5b5897f2dfe35d8b25f4e41fea9f8fca8f40f8a8b506b33701ef5033076", + pin_contract_name: "FungibleToken", + pin_contract_address: "0xf233dcee88fe0abe", + imports: [], + }, + }, + { + network: "testnet", + address: "0x9a0766d93b6608b7", + dependency_pin_block_height: 149595558, + dependency_pin: { + pin: "ac0208f93d07829ec96584d618ddbec6af3cf4e2866bd5071249e8ec93c7e0dc", + pin_self: + "cdadd5b5897f2dfe35d8b25f4e41fea9f8fca8f40f8a8b506b33701ef5033076", + pin_contract_name: "FungibleToken", + pin_contract_address: "0x9a0766d93b6608b7", + imports: [], + }, + }, + { + network: "emulator", + address: "0xee82856bf20e2aa6", + dependency_pin_block_height: 0, + }, + ], + }, + ], + }, + ], + parameters: [ + { + label: "amount", + index: 0, + type: "UFix64", + messages: [ + { + key: "title", + i18n: [ + { + tag: "en-US", + translation: "Amount", + }, + ], + }, + { + key: "description", + i18n: [ + { + tag: "en-US", + translation: "The amount of FLOW tokens to send", + }, + ], + }, + ], + }, + { + label: "to", + index: 1, + type: "Address", + messages: [ + { + key: "title", + i18n: [ + { + tag: "en-US", + translation: "To", + }, + ], + }, + { + key: "description", + i18n: [ + { + tag: "en-US", + translation: "The Flow account the tokens will go to", + }, + ], + }, + ], + }, + ], + }, + } + +describe("1.1.0, verify dependency pin same", () => { + beforeAll(() => { + jest.spyOn(console, "warn").mockImplementation(() => {}) + }) + + afterAll(() => { + console.warn.mockRestore() + }) + + test("v1.1.0, get dependency pin", async () => { + + config.put("flow.network", "mainnet") + config.put("accessNode.api", "https://rest-mainnet.onflow.org") + + const isVerified = await verifyDependencyPinsSame({ + template: template, + blockHeight: 70493190, + network: "mainnet", + }) + + expect(isVerified).toEqual(true) + }) + +}) diff --git a/packages/fcl-core/src/normalizers/interaction-template/interaction-template-1.0.0-1.1.0.js b/packages/fcl-core/src/normalizers/interaction-template/interaction-template-1.0.0-1.1.0.js new file mode 100644 index 000000000..f904c426d --- /dev/null +++ b/packages/fcl-core/src/normalizers/interaction-template/interaction-template-1.0.0-1.1.0.js @@ -0,0 +1,70 @@ +import {invariant} from "@onflow/sdk" + +export function normalize({template}) { + invariant( + template != undefined, + "generateTemplateId({ template }) -- template must be defined" + ) + invariant( + typeof template === "object", + "generateTemplateId({ template }) -- template must be an object" + ) + invariant( + template.f_type === "InteractionTemplate", + "generateTemplateId({ template }) -- template object must be an InteractionTemplate" + ) + invariant( + template.f_version === "1.0.0", + "generateTemplateId({ template }) -- template object must be version 1.0.0" + ) + + const templateData = template.data + + const messages = Object.keys(templateData.messages).map(messageKey => ({ + key: messageKey, + i18n: templateData.messages[messageKey]?.i18n.map( + messageKeyTranslationTag => ({ + tag: messageKeyTranslationTag, + translation: + templateData.messages[messageKey]?.i18n?.[messageKeyTranslationTag], + }) + ), + })) + + const _args = Object.keys(templateData?.["arguments"]).map( + async (argumentKey, i) => ({ + ...templateData?.["arguments"]?.[argumentKey], + label: argumentKey, + index: i, + type: templateData?.["arguments"]?.[argumentKey]?.type, + messages: Object.keys( + templateData?.["arguments"]?.[argumentKey].messages + ).map(argumentMessageKey => ({ + key: argumentMessageKey, + i18n: Object.keys( + templateData?.["arguments"]?.[argumentKey].messages?.[ + argumentMessageKey + ] + ).map(argumentMessageTag => ({ + tag: argumentMessageTag, + translation: + templateData?.["arguments"]?.[argumentKey].messages?.[ + argumentMessageKey + ]?.[argumentMessageTag], + })), + })), + }) + ) + + const newTemplate = { + ...template, + f_version: "1.1.0", + data: { + ...template.data, + messages, + parameters: _args, + }, + } + + return newTemplate +} diff --git a/packages/fcl-core/src/normalizers/interaction-template/interaction-template-interface.js b/packages/fcl-core/src/normalizers/interaction-template/interaction-template-interface.js deleted file mode 100644 index 109c3cd3f..000000000 --- a/packages/fcl-core/src/normalizers/interaction-template/interaction-template-interface.js +++ /dev/null @@ -1,13 +0,0 @@ -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-core/src/normalizers/interaction-template/interaction-template.js b/packages/fcl-core/src/normalizers/interaction-template/interaction-template.js index 3b32bd912..3fd43c137 100644 --- a/packages/fcl-core/src/normalizers/interaction-template/interaction-template.js +++ b/packages/fcl-core/src/normalizers/interaction-template/interaction-template.js @@ -1,9 +1,13 @@ -export function normalizeInteractionTemplate(template) { +import {normalize as normalize100to110} from "./interaction-template-1.0.0-1.1.0" + +export async function normalizeInteractionTemplate(template) { if (template == null) return null switch (template["f_version"]) { case "1.0.0": - return template + return normalizeInteractionTemplate( + normalize100to110({template}) + ) case "1.1.0": return template default: