From baca3d42ab439426593185097bebaacf3e11bb3b Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Sat, 21 Oct 2023 18:00:46 -0700 Subject: [PATCH 1/4] [LS] Allow importing aliased contracts using new import syntax --- languageserver/integration/flow.go | 32 +++- languageserver/test/FungibleToken.cdc | 237 ++++++++++++++++++++++++++ languageserver/test/flow.json | 10 +- languageserver/test/index.test.ts | 20 +++ 4 files changed, 289 insertions(+), 10 deletions(-) create mode 100644 languageserver/test/FungibleToken.cdc diff --git a/languageserver/integration/flow.go b/languageserver/integration/flow.go index fa3e6310..d8411baa 100644 --- a/languageserver/integration/flow.go +++ b/languageserver/integration/flow.go @@ -21,17 +21,17 @@ package integration import ( "context" "fmt" - "github.com/onflow/flow-cli/flowkit/accounts" - "github.com/onflow/flow-cli/flowkit/transactions" "net/url" "os" "path/filepath" "github.com/onflow/cadence" "github.com/onflow/flow-cli/flowkit" + "github.com/onflow/flow-cli/flowkit/accounts" "github.com/onflow/flow-cli/flowkit/config" "github.com/onflow/flow-cli/flowkit/gateway" "github.com/onflow/flow-cli/flowkit/output" + "github.com/onflow/flow-cli/flowkit/transactions" "github.com/onflow/flow-go-sdk" "github.com/onflow/flow-go-sdk/crypto" ) @@ -393,23 +393,37 @@ func (f *flowkitClient) createSigner(address flow.Address) (*accounts.Account, e } func (f *flowkitClient) GetCodeByName(name string) (string, error) { - contracts, err := f.state.DeploymentContractsByNetwork(config.EmulatorNetwork) + // Try to find the contract by name + contract, err := f.state.Contracts().ByName(name) if err != nil { - return "", err + return "", fmt.Errorf("couldn't find the contract by import identifier: %s", name) } - for _, contract := range contracts { - if name == contract.Name { - return string(contract.Code()), nil - } + // If no location is set, return an error + if contract.Location == "" { + return "", fmt.Errorf("source file could not be found for import identifier: %s", name) } - return "", fmt.Errorf(fmt.Sprintf("couldn't find the contract by import identifier: %s", name)) + // Resolve the contract source code from file location + code, err := f.getCodeFromLocation(name, contract.Location) + if err != nil { + return "", err + } + return code, nil } // Helpers // +// Helper function to get code from a source file location +func (f *flowkitClient) getCodeFromLocation(name, location string) (string, error) { + code, err := f.loader.ReadFile(filepath.Join(filepath.Dir(f.getConfigPath()), location)) + if err != nil { + return "", err + } + return string(code), nil +} + // resolveFilename helper converts the transaction file to a relative location to config file func resolveFilename(configPath string, path string) (string, error) { if filepath.Dir(configPath) == "." { // if flow.json is passed as relative use current dir diff --git a/languageserver/test/FungibleToken.cdc b/languageserver/test/FungibleToken.cdc new file mode 100644 index 00000000..abc05810 --- /dev/null +++ b/languageserver/test/FungibleToken.cdc @@ -0,0 +1,237 @@ +/** + +# 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" + } + } +} \ No newline at end of file diff --git a/languageserver/test/flow.json b/languageserver/test/flow.json index 78180465..012e9eeb 100644 --- a/languageserver/test/flow.json +++ b/languageserver/test/flow.json @@ -7,7 +7,15 @@ }, "contracts": { "Foo": "./foo.cdc", - "Bar": "./bar.cdc" + "Bar": "./bar.cdc", + "FungibleToken": { + "source": "./FungibleToken.cdc", + "aliases": { + "emulator": "0xee82856bf20e2aa6", + "testnet": "0x9a0766d93b6608b7", + "mainnet": "0xf233dcee88fe0abe" + } + } }, "networks": { "emulator": "127.0.0.1:3569", diff --git a/languageserver/test/index.test.ts b/languageserver/test/index.test.ts index 77cd8a33..2f4f2219 100644 --- a/languageserver/test/index.test.ts +++ b/languageserver/test/index.test.ts @@ -326,6 +326,7 @@ describe("diagnostics", () => { } const fooContractCode = fs.readFileSync('./foo.cdc', 'utf8') + const ftContractCode = fs.readFileSync('./FungibleToken.cdc', 'utf8') async function testImports(docs: TestDoc[]): Promise { return new Promise(resolve => { @@ -393,6 +394,25 @@ describe("diagnostics", () => { expect(script.diagnostics).toHaveLength(0) }) + test("script with string import aliased contract", async () => { + const contractName = "FungibleToken"; + const scriptName = "script"; + const scriptCode = ` + import "FungibleToken" + pub fun main() { } + `; + + let docNotifications = await testImports([ + { name: contractName, code: ftContractCode }, + { name: scriptName, code: scriptCode }, + ]); + + let script = await docNotifications.find((n) => n.name == scriptName) + .notification + expect(script.uri).toEqual(`file://${scriptName}.cdc`) + expect(script.diagnostics).toHaveLength(0) + }) + test("script import failure", async() => { const contractName = "foo" const scriptName = "script" From 9182ac33df7c1481813152b25f5f10d8b724f398 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Mon, 23 Oct 2023 08:57:27 -0700 Subject: [PATCH 2/4] Simplify test --- languageserver/test/FungibleToken.cdc | 237 -------------------------- languageserver/test/a.cdc | 1 + languageserver/test/flow.json | 10 +- languageserver/test/index.test.ts | 8 +- 4 files changed, 8 insertions(+), 248 deletions(-) delete mode 100644 languageserver/test/FungibleToken.cdc create mode 100644 languageserver/test/a.cdc diff --git a/languageserver/test/FungibleToken.cdc b/languageserver/test/FungibleToken.cdc deleted file mode 100644 index abc05810..00000000 --- a/languageserver/test/FungibleToken.cdc +++ /dev/null @@ -1,237 +0,0 @@ -/** - -# 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" - } - } -} \ No newline at end of file diff --git a/languageserver/test/a.cdc b/languageserver/test/a.cdc new file mode 100644 index 00000000..434b521f --- /dev/null +++ b/languageserver/test/a.cdc @@ -0,0 +1 @@ +pub contract A {} \ No newline at end of file diff --git a/languageserver/test/flow.json b/languageserver/test/flow.json index 012e9eeb..6640247e 100644 --- a/languageserver/test/flow.json +++ b/languageserver/test/flow.json @@ -8,13 +8,9 @@ "contracts": { "Foo": "./foo.cdc", "Bar": "./bar.cdc", - "FungibleToken": { - "source": "./FungibleToken.cdc", - "aliases": { - "emulator": "0xee82856bf20e2aa6", - "testnet": "0x9a0766d93b6608b7", - "mainnet": "0xf233dcee88fe0abe" - } + "A": { + "source": "./a.cdc", + "aliases": {} } }, "networks": { diff --git a/languageserver/test/index.test.ts b/languageserver/test/index.test.ts index 2f4f2219..83c14246 100644 --- a/languageserver/test/index.test.ts +++ b/languageserver/test/index.test.ts @@ -326,7 +326,7 @@ describe("diagnostics", () => { } const fooContractCode = fs.readFileSync('./foo.cdc', 'utf8') - const ftContractCode = fs.readFileSync('./FungibleToken.cdc', 'utf8') + const aContractCode = fs.readFileSync('./a.cdc', 'utf8') async function testImports(docs: TestDoc[]): Promise { return new Promise(resolve => { @@ -395,15 +395,15 @@ describe("diagnostics", () => { }) test("script with string import aliased contract", async () => { - const contractName = "FungibleToken"; + const contractName = "A"; const scriptName = "script"; const scriptCode = ` - import "FungibleToken" + import "A" pub fun main() { } `; let docNotifications = await testImports([ - { name: contractName, code: ftContractCode }, + { name: contractName, code: aContractCode }, { name: scriptName, code: scriptCode }, ]); From 46f089ca31e0084f7bbc14b382a21428d74980cd Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Mon, 23 Oct 2023 09:45:11 -0700 Subject: [PATCH 3/4] fixup test --- languageserver/test/index.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/languageserver/test/index.test.ts b/languageserver/test/index.test.ts index 83c14246..2f6af36d 100644 --- a/languageserver/test/index.test.ts +++ b/languageserver/test/index.test.ts @@ -326,7 +326,6 @@ describe("diagnostics", () => { } const fooContractCode = fs.readFileSync('./foo.cdc', 'utf8') - const aContractCode = fs.readFileSync('./a.cdc', 'utf8') async function testImports(docs: TestDoc[]): Promise { return new Promise(resolve => { @@ -394,8 +393,7 @@ describe("diagnostics", () => { expect(script.diagnostics).toHaveLength(0) }) - test("script with string import aliased contract", async () => { - const contractName = "A"; + test("script with string import non-deployment contract", async () => { const scriptName = "script"; const scriptCode = ` import "A" @@ -403,7 +401,6 @@ describe("diagnostics", () => { `; let docNotifications = await testImports([ - { name: contractName, code: aContractCode }, { name: scriptName, code: scriptCode }, ]); From a5c07654bb000343f13c393ba3c43c8bf7b35bee Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 26 Oct 2023 08:38:26 -0700 Subject: [PATCH 4/4] Update languageserver/integration/flow.go Co-authored-by: Supun Setunga --- languageserver/integration/flow.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/languageserver/integration/flow.go b/languageserver/integration/flow.go index d8411baa..372af368 100644 --- a/languageserver/integration/flow.go +++ b/languageserver/integration/flow.go @@ -417,7 +417,9 @@ func (f *flowkitClient) GetCodeByName(name string) (string, error) { // Helper function to get code from a source file location func (f *flowkitClient) getCodeFromLocation(name, location string) (string, error) { - code, err := f.loader.ReadFile(filepath.Join(filepath.Dir(f.getConfigPath()), location)) + dir := filepath.Dir(f.getConfigPath()) + path := filepath.Join(dir, location) + code, err := f.loader.ReadFile(path) if err != nil { return "", err }