diff --git a/CODEOWNERS b/CODEOWNERS index 8dab471..57ec5a6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @chasefleming @bjartek @bthaile \ No newline at end of file +* @bjartek @bthaile @ianthpun \ No newline at end of file diff --git a/README.md b/README.md index c83e966..bb7dfde 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # FlixKit -> FlixKit is a Go package that provides functionalities for interacting with Flow Interaction Templates (aka FLIX). Please note that this package is currently in alpha and may undergo significant changes. +> FlixKit is a Go package that provides functionalities for interacting with Flow Interaction Templates (aka FLIX). This package supports generating v1.1 FLIX template json, creating binding files for v1.0 and v1.1 and replacing import for v1.0 and v1.1. More information about FLIX [FLIX FLIP](https://github.com/onflow/flips/blob/main/application/20230330-interaction-templates-1.1.0.md) The `flixkit` package is a Go module designed to interact with Flow Interaction Templates (FLIX). It allows users to fetch, parse, generate and create binding files for Flow interaction templates aka FLIX, aka Verified Interactions. ## Structures -See FLIP that descibes json structure, [Here](https://github.com/onflow/flips/blob/main/application/20230330-interaction-templates-1.1.0.md) current version is v1.1.0 +See FLIP that describes json structure, [Here](https://github.com/onflow/flips/blob/main/application/20230330-interaction-templates-1.1.0.md) current version is v1.1.0 This package provides three functionalities. - Getting network specific Cadence from FLIX @@ -15,28 +15,41 @@ This package provides three functionalities. The package also defines the following interfaces: -- `FlixService`: This interface defines methods to interact with the FLIX service such as fetching raw data or parsed data by template name or template ID. -- `Generator`: This interface generates FLIX json given Cadence, metadata can be provided in two ways: - - prefilled out json - - Cadence docs in the form of a pragma -- `Bindings`: This interface has two implementations for javascript and typescript using fcl +- `FlixService`: This interface defines methods to fetch templates using template name or template id (from flix service), URL or local file path. + +### Methods +```go +// GetTemplate returns the raw flix template +GetTemplate(ctx context.Context, templateName string) (string, string, error) +// GetAndReplaceImports returns the raw flix template with cadence imports replaced +GetTemplateAndReplaceImports(ctx context.Context, templateName string, network string) (*FlowInteractionTemplateExecution, error) +// GenerateBinding returns the generated binding given the language +GetTemplateAndCreateBinding(ctx context.Context, templateName string, lang string, destFile string) (string, error) +// GenerateTemplate returns the generated raw template +CreateTemplate(ctx context.Context, contractInfos ContractInfos, code string, preFill string) (string, error) +``` ## Usage -The package provides a `FlixService` interface with a constructor function `NewFlixService(config *Config)`. `Config` contains `FlixServerURL` which should be provided. If no URL is provided, it defaults to `"https://flix.flow.com/v1/templates"`. +The package provides a `FlixService` interface with a constructor function `NewFlixService(config *FlixServiceConfig)`. `FlixServiceConfig` +contains + - `FlixServerURL` which is defaulted to `"https://flix.flow.com/v1/templates"`. User can provide their own service url endpoint + - `FileReader` which is used to read local FLIX json template files + - `Logger` which is used in creating `flowkit.NewFlowkit` for FLIX template generation The `FlixService` interface provides the following methods: -- `GetTemplate(ctx context.Context, templateName string) (string, error)`: Fetches template and returns as a string. -- `GetAndReplaceCadenceImports(ctx context.Context, templateName string, network string) (*FlowInteractionTemplateExecution, error)`: Fetches and parses a Flix template and provides the cadence for the network provided. +- `GetTemplate`: Fetches template and returns as a string. +- `GetTemplateAndReplaceImports` returns `FlowInteractionTemplateExecution`: Fetches and parses a Flix template and provides the cadence for the network provided. There are two helper methods to assist in determining if the Cadence is a transaction or a script. -- Note: `templateName` parameter can be the id or name of a template from the interactive template service. A local file or url to the FLIX json file. +- Note: `templateName` parameter can be the id or name of a template from the interactive template service. A local file or url to the FLIX json file or the template string itself. Result form GetAndReplaceCadenceImports is a `FlowInteractionTemplateExecution` instance also provides the following methods: - `IsScript() bool`: Checks if the template is of type "script". - `IsTransaction() bool`: Checks if the template is of type "transaction". -- `GetAndReplaceCadenceImports(networkName string) (string, error)`: Replaces cadence import statements in the cadence script or transaction with their respective network addresses. +- `Cadence`: Replaced cadence with respective network addresses. +- `Network`: Name of network used to get import addresses ## Examples @@ -58,86 +71,105 @@ Note: Remember to replace "transfer-flow" with the actual name of the template y To read more about Flow Interaction Templates, [see the docs](https://developers.flow.com/tooling/fcl-js/interaction-templates). -## Bindings +## Binding Files -> Binding files are code files that bind consuming code with FLIX. The `bindings` module in Flixkit generates code that calls the FLIX cadence code. FLIX cadence is primarily transactions and scripts. +> Binding files are client code files used to call Cadence contracts using the scripts or transactions in a FLIX. These client files can be created given a FLIX, currently TypeScript and JavaScript are supported. ### Usage The `bindings` module has two public methods `Generate` and `NewFclJSGenerator`. `FclJSGenerator` takes a template directory. `bindings` has fcl-js templates. - - - `NewFclJSGenerator() *FclJSGenerator` // uses default fcl-js vanilla javascript - - `Generate(template string, templateLocation string) (string, error)` // flix is the hydrated template struct, templateLocation is the file location of the flix json file, isLocal is a flag that indicates if the template is local or on remote server - -### Example - ```go +flixService := flixkit.NewFlixService(&flixkit.Config{ + FileReader: myFileReader +}) -// uses default fcl-js templates -fclJsGen := flixkit.NewFclJSGenerator() - -output, err := fclJsGen.Generate(template, flixQuery, isLocal) +binding, err := flixService.GetTemplateAndCreateBinding(context.Background(), "transfer-flow", "js", "./bindingFiles/transferFlow.js") if err != nil { log.Fatal(err) } -// output is the javascript binding code -fmt.Println(output]) +fmt.Println(binding) +``` +```go +GetTemplateAndCreateBinding(ctx context.Context, templateName string, lang string, destFile string) (string, error) ``` -## Generate + - `templateName` value can be template name, template id, url or local file. + - `lang` values supported are "js", "javascript", "ts", "typescript" + - `destFile` is the location of the destination binding file, this is used to create the relative path if the template is local. If the template is a template name, template id or url `destFile` isn't used -> Generate creates the newest ratified version of FLIX, as of this update, v1.1 has been passed. Version 1.0.0 will be supported with `FlixService` and `bindings`. +## Generate Templates -- `deployedContracts` is an array of v1_1.Contract structs of the contract dependencies the Cadence code depends on, Core contracts are already configured, look in `internal/contracts/core.go` for details +> CreateTemplate creates the newest ratified version of FLIX, as of this update, see link to FLIP Flip above for more information. +```go + CreateTemplate(ctx context.Context, contractInfos ContractInfos, code string, preFill string) (string, error) +``` -### Example +### Usage ```go -generator, err := flixkit.NewGenerator(deployedContracts, logger output.Logger) -// preFilledTemplate is a partially populated flix template with human readable messages -// see FLIX flip for details -prettyJSON, err := generator.Generate(ctx, string(code), preFilledTemplate) +flixService := flixkit.NewFlixService(&flixkit.Config{ + FileReader: myFileReader, + Logger: myLogger, +}) + +prettyJSON, err := flixService.CreateTemplate(ctx, depContracts, string(code), preFilled) fmt.Println(prettyJSON) ``` +- `contractInfos` is an array of v1_1.Contract struct. This provides the network information about the deployed contracts that are dependencies in the FLIX Cadence code. +- `code` is the actual Cadence code the template is based on +- `preFilled` is a partially filled out FLIX template. This can be a template name, template id, url or local file. Alternatively to using a prefilled template, the Cadence itself can provide metadata using a FLIX specific Cadence pragma, more on that below, [See Cadence Doc Flip](https://github.com/onflow/flips/blob/main/application/20230406-interaction-template-cadence-doc.md) + + ### Cadence docs pragma -> Using Cadence pragma the metadata can live with the Cadence code. Therefore a prefilled template isn't necessary +> Using Cadence pragma the metadata can exist along with the Cadence code. Therefore a prefilled template isn't necessary ### Example ```go #interaction( - version: "1.1.0", - title: "Transfer Flow", - description: "Transfer Flow to account", - language: "en-US", - parameters: [ - Parameter( - name: "amount", - title: "Amount", - description: "Amount of Flow to transfer" - ), - Parameter( - name: "to", - title: "Receiver", - description: "Destination address to receive Flow Tokens" - ) - ], -) - -import "FlowToken" - -transaction(amount: UFix64, to: Address) { - - let vault: @FlowToken.Vault + version: "1.1.0", + title: "Transfer Flow", + description: "Transfer Flow to account", + language: "en-US", + ) - prepare(signer: &Account) { - // ... + import "FlowToken" + transaction(amount: UFix64, to: Address) { + let vault: @FlowToken.Vault + prepare(signer: &Account) { + ... + } } -} +` + ``` -The pragma describes the transaction parameters and reason for the transaction. \ No newline at end of file +A `pragma` gives special instruction or processors, in this case FLIX specific information that describes the transaction or script. + +Notice: Nested structures in Cadence pragma will be supported in future, this will allow describing parameters +```go +... +#interaction( + version: "1.1.0", + title: "Transfer Flow", + description: "Transfer Flow to account", + language: "en-US", + parameters: [ + Parameter( + name: "amount", + title: "Amount", + description: "Amount of Flow to transfer" + ), + Parameter( + name: "to", + title: "Reciever", + description: "Destination address to receive Flow Tokens" + ) + ], + ) +... +``` diff --git a/flixkit/flix_service.go b/flixkit/flix_service.go new file mode 100644 index 0000000..4f0e3ac --- /dev/null +++ b/flixkit/flix_service.go @@ -0,0 +1,33 @@ +package flixkit + +import ( + "context" + + "github.com/onflow/flixkit-go/internal" +) + +type FlixService interface { + // GetTemplate returns the raw flix template + GetTemplate(ctx context.Context, templateName string) (string, string, error) + // GetAndReplaceImports returns the raw flix template with cadence imports replaced + GetTemplateAndReplaceImports(ctx context.Context, templateName string, network string) (*FlowInteractionTemplateExecution, error) + // GenerateBinding returns the generated binding given the language + GetTemplateAndCreateBinding(ctx context.Context, templateName string, lang string, destFile string) (string, error) + // GenerateTemplate returns the generated raw template + CreateTemplate(ctx context.Context, contractInfos ContractInfos, code string, preFill string) (string, error) +} + +// FlowInteractionTemplateCadence is the interface returned from Replacing imports, it provides helper methods to assist in executing the resulting Cadence. +type FlowInteractionTemplateExecution = internal.FlowInteractionTemplateExecution + +// ContractInfos is an input into generating a template, it is a map of contract name to network information of deployed contracts of the source Cadence code. +type ContractInfos = internal.ContractInfos +type NetworkAddressMap = internal.NetworkAddressMap + +// FlixServiceConfig is the configuration for the FlixService that provides a override for FlixServerURL and default values for FileReader and Logger. +type FlixServiceConfig = internal.FlixServiceConfig + +// NewFlixService returns a new FlixService given a FlixServiceConfig +func NewFlixService(config *FlixServiceConfig) FlixService { + return internal.NewFlixService(config) +} diff --git a/flixkit/flixkit.go b/flixkit/flixkit.go deleted file mode 100644 index 13d2d36..0000000 --- a/flixkit/flixkit.go +++ /dev/null @@ -1,276 +0,0 @@ -package flixkit - -import ( - "context" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "net/url" - "os" - - v1 "github.com/onflow/flixkit-go/flixkit/v1" - v1_1 "github.com/onflow/flixkit-go/flixkit/v1_1" -) - -type FlowInteractionTemplateExecution struct { - Network string - Cadence string - IsTransaciton bool - IsScript bool -} - -type FlowInteractionTemplateVersion struct { - FVersion string `json:"f_version"` -} - -type GenerateTemplate interface { - Generate(ctx context.Context, code string, preFill string) (string, error) -} - -type GenerateBinding interface { - Generate(flixString string, templateLocation string) (string, error) -} - -type FlowInteractionTemplateCadence interface { - GetAndReplaceCadenceImports(templateName string) (string, error) - IsTransaction() bool - IsScript() bool -} - -type FlixService interface { - GetTemplate(ctx context.Context, templateName string) (string, error) - GetAndReplaceCadenceImports(ctx context.Context, templateName string, network string) (*FlowInteractionTemplateExecution, error) -} - -type flixServiceImpl struct { - config *Config -} - -var _ FlixService = (*flixServiceImpl)(nil) - -type FileReader interface { - ReadFile(path string) ([]byte, error) -} - -type Config struct { - FlixServerURL string - FileReader FileReader -} - -func NewFlixService(config *Config) FlixService { - if config.FlixServerURL == "" { - config.FlixServerURL = "https://flix.flow.com/v1/templates" - } - - return &flixServiceImpl{ - config: config, - } -} - -func (s *flixServiceImpl) GetFlixRaw(ctx context.Context, templateName string) (string, error) { - url := fmt.Sprintf("%s?name=%s", s.config.FlixServerURL, templateName) - return FetchFlixWithContext(ctx, url) -} - -func (s *flixServiceImpl) GetFlix(ctx context.Context, templateName string) (string, error) { - template, err := s.GetFlixRaw(ctx, templateName) - if err != nil { - return "", err - } - - return template, nil -} - -func (s *flixServiceImpl) GetFlixByIDRaw(ctx context.Context, templateID string) (string, error) { - url := fmt.Sprintf("%s/%s", s.config.FlixServerURL, templateID) - return FetchFlixWithContext(ctx, url) -} - -func (s *flixServiceImpl) GetFlixByID(ctx context.Context, templateID string) (string, error) { - template, err := s.GetFlixByIDRaw(ctx, templateID) - if err != nil { - return "", err - } - return template, nil -} - -func GetTemplateVersion(template string) (string, error) { - var flowTemplate FlowInteractionTemplateVersion - - err := json.Unmarshal([]byte(template), &flowTemplate) - if err != nil { - return "", err - } - - if flowTemplate.FVersion == "" { - return "", fmt.Errorf("version not found") - } - - return flowTemplate.FVersion, nil -} - -func FetchFlixWithContext(ctx context.Context, url string) (string, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return "", err - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer func() { - if err := resp.Body.Close(); err != nil { - log.Printf("Warning: error while closing the response body: %v", err) - } - }() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - return string(body), nil -} - -func (s *flixServiceImpl) GetAndReplaceCadenceImports(ctx context.Context, templateName string, network string) (*FlowInteractionTemplateExecution, error) { - template, err := s.GetTemplate(ctx, templateName) - if err != nil { - return nil, err - } - var execution FlowInteractionTemplateExecution - var cadenceCode string - ver, err := GetTemplateVersion(template) - if err != nil { - return nil, fmt.Errorf("invalid flix template version, %w", err) - } - var replaceableCadence FlowInteractionTemplateCadence - switch ver { - case "1.1.0": - replaceableCadence, err = v1_1.ParseFlix(template) - if err != nil { - return nil, err - } - cadenceCode, err = replaceableCadence.GetAndReplaceCadenceImports(network) - if err != nil { - return nil, err - } - execution.Cadence = cadenceCode - execution.IsScript = replaceableCadence.IsScript() - execution.IsTransaciton = replaceableCadence.IsTransaction() - case "1.0.0": - replaceableCadence, err = v1.ParseFlix(template) - if err != nil { - return nil, err - } - cadenceCode, err = replaceableCadence.GetAndReplaceCadenceImports(network) - if err != nil { - return nil, err - } - execution.Cadence = cadenceCode - execution.IsScript = replaceableCadence.IsScript() - execution.IsTransaciton = replaceableCadence.IsTransaction() - default: - return nil, fmt.Errorf("flix template version: %s not supported", ver) - } - - if execution.Cadence == "" { - return nil, fmt.Errorf("could not parse template, invalid flix template") - } - - return &execution, nil -} - -type flixQueryTypes string - -const ( - flixName flixQueryTypes = "name" - flixPath flixQueryTypes = "path" - flixId flixQueryTypes = "id" - flixUrl flixQueryTypes = "url" - flixJson flixQueryTypes = "json" -) - -func isHex(str string) bool { - if len(str) != 64 { - return false - } - _, err := hex.DecodeString(str) - return err == nil -} - -func isPath(path string) bool { - _, err := os.Stat(path) - return err == nil -} - -func isUrl(str string) bool { - u, err := url.Parse(str) - return err == nil && u.Scheme != "" && u.Host != "" -} - -func isJson(str string) bool { - var js json.RawMessage - return json.Unmarshal([]byte(str), &js) == nil -} - -func getType(s string) flixQueryTypes { - switch { - case isPath(s): - return flixPath - case isHex(s): - return flixId - case isUrl(s): - return flixUrl - case isJson(s): - return flixJson - default: - return flixName - } -} - -func (s *flixServiceImpl) GetTemplate(ctx context.Context, flixQuery string) (string, error) { - var template string - var err error - - switch getType(flixQuery) { - case flixId: - template, err = s.GetFlixByID(ctx, flixQuery) - if err != nil { - return "", fmt.Errorf("could not find flix with id %s: %w", flixQuery, err) - } - - case flixName: - template, err = s.GetFlix(ctx, flixQuery) - if err != nil { - return "", fmt.Errorf("could not find flix with name %s: %w", flixQuery, err) - } - - case flixPath: - if s.config.FileReader == nil { - return "", fmt.Errorf("file reader not provided") - } - file, err := s.config.FileReader.ReadFile(flixQuery) - if err != nil { - return "", fmt.Errorf("could not read flix file %s: %w", flixQuery, err) - } - template = string(file) - if err != nil { - return "", fmt.Errorf("could not parse flix from file %s: %w", flixQuery, err) - } - - case flixUrl: - template, err = FetchFlixWithContext(ctx, flixQuery) - if err != nil { - return "", fmt.Errorf("could not parse flix from url %s: %w", flixQuery, err) - } - - default: - return "", fmt.Errorf("invalid flix query type: %s", flixQuery) - } - - return template, nil -} diff --git a/internal/common.go b/internal/common.go new file mode 100644 index 0000000..ea70917 --- /dev/null +++ b/internal/common.go @@ -0,0 +1,104 @@ +package internal + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "net/url" + "path/filepath" +) + +func getTemplateVersion(template string) (string, error) { + type FlowInteractionTemplateVersion struct { + FVersion string `json:"f_version"` + } + var flowTemplate FlowInteractionTemplateVersion + + err := json.Unmarshal([]byte(template), &flowTemplate) + if err != nil { + return "", err + } + + if flowTemplate.FVersion == "" { + return "", fmt.Errorf("version not found") + } + + return flowTemplate.FVersion, nil +} + +func isArrayParameter(arg FlixParameter) (isArray bool, cType string, jsType string) { + if arg.Type == "" || arg.Type[0] != '[' { + return false, "", "" + } + cadenceType := arg.Type[1 : len(arg.Type)-1] + javascriptType := "Array<" + convertCadenceTypeToJS(cadenceType) + ">" + return true, cadenceType, javascriptType +} + +func isUrl(str string) bool { + u, err := url.Parse(str) + return err == nil && u.Scheme != "" && u.Host != "" +} + +type flixQueryTypes string + +const ( + flixName flixQueryTypes = "name" + flixPath flixQueryTypes = "path" + flixId flixQueryTypes = "id" + flixUrl flixQueryTypes = "url" + flixJson flixQueryTypes = "json" +) + +func isHex(str string) bool { + if len(str) != 64 { + return false + } + _, err := hex.DecodeString(str) + return err == nil +} + +func isPath(path string, f FileReader) bool { + if f == nil { + return false + } + _, err := f.ReadFile(path) + return err == nil +} + +func isJson(str string) bool { + var js json.RawMessage + return json.Unmarshal([]byte(str), &js) == nil +} + +func getType(s string, f FileReader) flixQueryTypes { + switch { + case isPath(s, f): + return flixPath + case isHex(s): + return flixId + case isUrl(s): + return flixUrl + case isJson(s): + return flixJson + default: + return flixName + } +} + +// getRelativePath computes the relative path from generated file to flix json file. +// This path is used in the binding file to reference the flix json file. +func getRelativePath(configFile, bindingFile string) (string, error) { + relPath, err := filepath.Rel(filepath.Dir(bindingFile), configFile) + if err != nil { + return "", err + } + + // If the file is in the same directory and doesn't start with "./", prepend it. + if !filepath.IsAbs(relPath) && relPath[0] != '.' { + relPath = "./" + relPath + } + + // Currently binding files are js, we need to convert the path to unix style + return filepath.ToSlash(relPath), nil +} diff --git a/flixkit/fcl_bindings.go b/internal/fcl_binding_creator.go similarity index 84% rename from flixkit/fcl_bindings.go rename to internal/fcl_binding_creator.go index 535faa1..115980e 100644 --- a/flixkit/fcl_bindings.go +++ b/internal/fcl_binding_creator.go @@ -1,4 +1,4 @@ -package flixkit +package internal import ( "bytes" @@ -6,13 +6,13 @@ import ( "sort" "text/template" - v1 "github.com/onflow/flixkit-go/flixkit/v1" - v1_1 "github.com/onflow/flixkit-go/flixkit/v1_1" "github.com/onflow/flixkit-go/internal/templates" + v1 "github.com/onflow/flixkit-go/internal/v1" + v1_1 "github.com/onflow/flixkit-go/internal/v1_1" "github.com/stoewer/go-strcase" ) -func NewFclTSGenerator() *FclGenerator { +func NewFclTSCreator() *FclCreator { t := []string{ templates.GetTsFclMainTemplate(), templates.GetTsFclScriptTemplate(), @@ -21,12 +21,12 @@ func NewFclTSGenerator() *FclGenerator { templates.GetTsFclInterfaceTemplate(), } - return &FclGenerator{ - Templates: t, + return &FclCreator{ + templates: t, } } -func NewFclJSGenerator() *FclGenerator { +func NewFclJSCreator() *FclCreator { t := []string{ templates.GetJsFclMainTemplate(), templates.GetJsFclScriptTemplate(), @@ -34,13 +34,13 @@ func NewFclJSGenerator() *FclGenerator { templates.GetJsFclParamsTemplate(), } - return &FclGenerator{ - Templates: t, + return &FclCreator{ + templates: t, } } -func (g *FclGenerator) Generate(flixString string, templateLocation string) (string, error) { - tmpl, err := parseTemplates(g.Templates) +func (g *FclCreator) Create(flixString string, templateLocation string) (string, error) { + tmpl, err := parseTemplates(g.templates) if err != nil { return "", err } @@ -48,7 +48,7 @@ func (g *FclGenerator) Generate(flixString string, templateLocation string) (str return "", fmt.Errorf("no flix template provided") } - ver, err := GetTemplateVersion(flixString) + ver, err := getTemplateVersion(flixString) if err != nil { return "", fmt.Errorf("invalid flix template version, %w", err) } @@ -100,8 +100,8 @@ type templateData struct { IsLocalTemplate bool } -type FclGenerator struct { - Templates []string +type FclCreator struct { + templates []string } type FlixParameter struct { @@ -128,18 +128,28 @@ func getTemplateDataV1_1(flix *v1_1.InteractionTemplate, templateLocation string title := msgs.GetTitle("Request") methodName := strcase.LowerCamelCase(title) description := msgs.GetDescription("") - result := simpleParameter{} - if flix.Data.Output != nil { - o := transformParameters([]v1_1.Parameter{*flix.Data.Output}) + var sp simpleParameter + + if flix.Data.Type == "script" { + oTemp := flix.Data.Output + if flix.Data.Output == nil { + // if output is not defined, add default output + oTemp = &v1_1.Parameter{ + Label: "result", + Type: "String", + Messages: v1_1.InteractionTemplateMessages{}, + } + } + o := transformParameters([]v1_1.Parameter{*oTemp}) if len(o) > 0 { - result = o[0] + sp = o[0] } } data := templateData{ Version: flix.FVersion, Parameters: transformParameters(flix.Data.Parameters), ParametersPrefixName: strcase.UpperCamelCase(title), - Output: result, + Output: sp, Title: methodName, Description: description, Location: templateLocation, @@ -153,7 +163,14 @@ func getTemplateDataV1_0(flix *v1.FlowInteractionTemplate, templateLocation stri title := flix.Data.Messages.GetTitleValue("Request") methodName := strcase.LowerCamelCase(title) description := flix.GetDescription() - + var sp simpleParameter + // version 1.0 does not support output parameters, add default output + if flix.Data.Type == "script" { + sp = simpleParameter{ + Name: "result", + JsType: "string", + } + } data := templateData{ Version: flix.FVersion, Parameters: transformArguments(flix.Data.Arguments), @@ -163,6 +180,7 @@ func getTemplateDataV1_0(flix *v1.FlowInteractionTemplate, templateLocation stri Location: templateLocation, IsScript: flix.IsScript(), IsLocalTemplate: isLocal, + Output: sp, } return data } @@ -247,12 +265,3 @@ func transformArguments(args v1.Arguments) []simpleParameter { } return simpleArgs } - -func isArrayParameter(arg FlixParameter) (isArray bool, cType string, jsType string) { - if arg.Type == "" || arg.Type[0] != '[' { - return false, "", "" - } - cadenceType := arg.Type[1 : len(arg.Type)-1] - javascriptType := "Array<" + convertCadenceTypeToJS(cadenceType) + ">" - return true, cadenceType, javascriptType -} diff --git a/flixkit/fcl_bindings_test.go b/internal/fcl_binding_creator_test.go similarity index 60% rename from flixkit/fcl_bindings_test.go rename to internal/fcl_binding_creator_test.go index 7b93ddf..6834aa9 100644 --- a/flixkit/fcl_bindings_test.go +++ b/internal/fcl_binding_creator_test.go @@ -1,15 +1,16 @@ -package flixkit +package internal import ( "encoding/json" "testing" "github.com/hexops/autogold/v2" + "github.com/stretchr/testify/assert" - v1 "github.com/onflow/flixkit-go/flixkit/v1" - v1_1 "github.com/onflow/flixkit-go/flixkit/v1_1" "github.com/onflow/flixkit-go/internal/templates" + v1 "github.com/onflow/flixkit-go/internal/v1" + v1_1 "github.com/onflow/flixkit-go/internal/v1_1" ) var parsedTemplateTX = &v1.FlowInteractionTemplate{ @@ -198,15 +199,15 @@ var minimumNoParamTemplate = &v1.FlowInteractionTemplate{ func TestJSGenTransaction(t *testing.T) { ttemp, err := json.Marshal(parsedTemplateTX) assert.NoError(t, err, "marshal template to json should not return an error") - generator := FclGenerator{ - Templates: []string{ + generator := FclCreator{ + templates: []string{ templates.GetJsFclMainTemplate(), templates.GetJsFclScriptTemplate(), templates.GetJsFclTxTemplate(), templates.GetJsFclParamsTemplate(), }, } - got, _ := generator.Generate(string(ttemp), "./transfer_token.json") + got, _ := generator.Create(string(ttemp), "./transfer_token.json") autogold.ExpectFile(t, got) } @@ -214,8 +215,8 @@ func TestJSGenScript(t *testing.T) { ttemp, err := json.Marshal(parsedTemplateScript) assert.NoError(t, err, "marshal template to json should not return an error") - generator := FclGenerator{ - Templates: []string{ + generator := FclCreator{ + templates: []string{ templates.GetJsFclMainTemplate(), templates.GetJsFclScriptTemplate(), templates.GetJsFclTxTemplate(), @@ -223,7 +224,7 @@ func TestJSGenScript(t *testing.T) { }, } assert := assert.New(t) - got, err := generator.Generate(string(ttemp), "./multiply_two_integers.template.json") + got, err := generator.Create(string(ttemp), "./multiply_two_integers.template.json") assert.NoError(err, "ParseTemplate should not return an error") autogold.ExpectFile(t, got) } @@ -232,8 +233,8 @@ func TestJSGenArrayScript(t *testing.T) { ttemp, err := json.Marshal(ArrayTypeScript) assert.NoError(t, err, "marshal template to json should not return an error") - generator := FclGenerator{ - Templates: []string{ + generator := FclCreator{ + templates: []string{ templates.GetJsFclMainTemplate(), templates.GetJsFclScriptTemplate(), templates.GetJsFclTxTemplate(), @@ -242,7 +243,7 @@ func TestJSGenArrayScript(t *testing.T) { } assert := assert.New(t) - out, err := generator.Generate(string(ttemp), "./multiply-numbers.template.json") + out, err := generator.Create(string(ttemp), "./multiply-numbers.template.json") assert.NoError(err, "ParseTemplate should not return an error") autogold.ExpectFile(t, out) } @@ -251,10 +252,10 @@ func TestJSGenMinScript(t *testing.T) { ttemp, err := json.Marshal(minimumTemplate) assert.NoError(t, err, "marshal template to json should not return an error") - generator := NewFclJSGenerator() + generator := NewFclJSCreator() assert := assert.New(t) - out, err := generator.Generate(string(ttemp), "./min.template.json") + out, err := generator.Create(string(ttemp), "./min.template.json") assert.NoError(err, "ParseTemplate should not return an error") autogold.ExpectFile(t, out) } @@ -262,10 +263,10 @@ func TestJSGenNoParamsScript(t *testing.T) { ttemp, err := json.Marshal(minimumNoParamTemplate) assert.NoError(t, err, "marshal template to json should not return an error") - generator := NewFclJSGenerator() + generator := NewFclJSCreator() assert := assert.New(t) - out, err := generator.Generate(string(ttemp), "./min.template.json") + out, err := generator.Create(string(ttemp), "./min.template.json") assert.NoError(err, "ParseTemplate should not return an error") autogold.ExpectFile(t, out) } @@ -416,10 +417,10 @@ func TestTSGenNoParamsScript(t *testing.T) { ttemp, err := json.Marshal(minimumNoParamTemplateTS_SCRIPT) assert.NoError(t, err, "marshal template to json should not return an error") - generator := NewFclTSGenerator() + generator := NewFclTSCreator() assert := assert.New(t) - out, err := generator.Generate(string(ttemp), "./min.template.json") + out, err := generator.Create(string(ttemp), "./min.template.json") assert.NoError(err, "ParseTemplate should not return an error") autogold.ExpectFile(t, out) } @@ -428,10 +429,10 @@ func TestTSGenNoParamsTx(t *testing.T) { ttemp, err := json.Marshal(minimumNoParamTemplateTS_TX) assert.NoError(t, err, "marshal template to json should not return an error") - generator := NewFclTSGenerator() + generator := NewFclTSCreator() assert := assert.New(t) - out, err := generator.Generate(string(ttemp), "./min.template.json") + out, err := generator.Create(string(ttemp), "./min.template.json") assert.NoError(err, "ParseTemplate should not return an error") autogold.ExpectFile(t, out) } @@ -440,10 +441,10 @@ func TestTSGenParamsScript(t *testing.T) { ttemp, err := json.Marshal(minimumParamTemplateTS_SCRIPT) assert.NoError(t, err, "marshal template to json should not return an error") - generator := NewFclTSGenerator() + generator := NewFclTSCreator() assert := assert.New(t) - out, err := generator.Generate(string(ttemp), "./min.template.json") + out, err := generator.Create(string(ttemp), "./min.template.json") assert.NoError(err, "ParseTemplate should not return an error") autogold.ExpectFile(t, out) } @@ -452,10 +453,147 @@ func TestTSGenParamsTx(t *testing.T) { ttemp, err := json.Marshal(minimumParamTemplateTS_TX) assert.NoError(t, err, "marshal template to json should not return an error") - generator := NewFclTSGenerator() + generator := NewFclTSCreator() + assert := assert.New(t) + + out, err := generator.Create(string(ttemp), "./min.template.json") + assert.NoError(err, "ParseTemplate should not return an error") + autogold.ExpectFile(t, out) +} + +const ReadTokenScript = ` +{ + "f_type": "InteractionTemplate", + "f_version": "1.1.0", + "id": "29d03aafbbb5a02e0d5f4ffee685c12494915410812305c2858008d3e2902b72", + "data": { + "type": "script", + "interface": "", + "messages": null, + "cadence": { + "body": "import \"FungibleToken\"\nimport \"FlowToken\"\n\npub fun main(address: Address): UFix64 {\n let account = getAccount(address)\n\n let vaultRef = account\n .getCapability(/public/flowTokenBalance)\n .borrow\u003c\u0026FlowToken.Vault{FungibleToken.Balance}\u003e()\n ?? panic(\"Could not borrow balance reference to the Vault\")\n\n return vaultRef.balance\n}\n", + "network_pins": [ + { + "network": "mainnet", + "pin_self": "e0a1c0443b724d1238410c4a05c48441ee974160cad8cf1103c63b6999f81dd5" + }, + { + "network": "testnet", + "pin_self": "6fee459b35d7013a83070c9ac42ea43ee04a3925deca445c34614c1bd6dc4cb8" + } + ] + }, + "dependencies": [ + { + "contracts": [ + { + "contract": "FungibleToken", + "networks": [ + { + "network": "mainnet", + "address": "0xf233dcee88fe0abe", + "dependency_pin_block_height": 69539302, + "dependency_pin": { + "pin": "ac0208f93d07829ec96584d618ddbec6af3cf4e2866bd5071249e8ec93c7e0dc", + "pin_self": "cdadd5b5897f2dfe35d8b25f4e41fea9f8fca8f40f8a8b506b33701ef5033076", + "pin_contract_name": "FungibleToken", + "pin_contract_address": "0xf233dcee88fe0abe", + "imports": [] + } + }, + { + "network": "testnet", + "address": "0x9a0766d93b6608b7", + "dependency_pin_block_height": 146201102, + "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 + } + ] + } + ] + }, + { + "contracts": [ + { + "contract": "FlowToken", + "networks": [ + { + "network": "mainnet", + "address": "0x1654653399040a61", + "dependency_pin_block_height": 69539302, + "dependency_pin": { + "pin": "a341e772da413bfbcf43b0fc167bd50a20c9f40baf10e12d3dbc2f5181526de9", + "pin_self": "0e932728b73bff3c09dd58922f2529fc7b7fe7477f1dcc61169bc8f46948ad91", + "pin_contract_name": "FlowToken", + "pin_contract_address": "0x1654653399040a61", + "imports": [ + { + "pin": "ac0208f93d07829ec96584d618ddbec6af3cf4e2866bd5071249e8ec93c7e0dc", + "pin_self": "cdadd5b5897f2dfe35d8b25f4e41fea9f8fca8f40f8a8b506b33701ef5033076", + "pin_contract_name": "FungibleToken", + "pin_contract_address": "0xf233dcee88fe0abe", + "imports": [] + } + ] + } + }, + { + "network": "testnet", + "address": "0x7e60df042a9c0868", + "dependency_pin_block_height": 146201102, + "dependency_pin": { + "pin": "9cc21a34a01486ebd6f044e99dbcdd58671850f81fcc345d071181c19f61aaa4", + "pin_self": "6f01c7001e2d6635b667a170d3ccbc13659c40d01bb35e56979fcc7fa2d18646", + "pin_contract_name": "FlowToken", + "pin_contract_address": "0x7e60df042a9c0868", + "imports": [ + { + "pin": "ac0208f93d07829ec96584d618ddbec6af3cf4e2866bd5071249e8ec93c7e0dc", + "pin_self": "cdadd5b5897f2dfe35d8b25f4e41fea9f8fca8f40f8a8b506b33701ef5033076", + "pin_contract_name": "FungibleToken", + "pin_contract_address": "0x9a0766d93b6608b7", + "imports": [] + } + ] + } + }, + { + "network": "emulator", + "address": "0x0ae53cb6e3f42a79", + "dependency_pin_block_height": 0 + } + ] + } + ] + } + ], + "parameters": [ + { + "label": "address", + "index": 0, + "type": "Address", + "messages": [] + } + ] + } +} +` + +func TestBindingReadTokenBalance(t *testing.T) { + generator := NewFclTSCreator() assert := assert.New(t) - out, err := generator.Generate(string(ttemp), "./min.template.json") + out, err := generator.Create(ReadTokenScript, "./read-token-balance.template.json") assert.NoError(err, "ParseTemplate should not return an error") autogold.ExpectFile(t, out) } diff --git a/internal/flix_service.go b/internal/flix_service.go new file mode 100644 index 0000000..ed55ceb --- /dev/null +++ b/internal/flix_service.go @@ -0,0 +1,251 @@ +package internal + +import ( + "context" + "fmt" + "io" + "log" + "net/http" + "strings" + + v1 "github.com/onflow/flixkit-go/internal/v1" + v1_1 "github.com/onflow/flixkit-go/internal/v1_1" + "github.com/onflow/flow-cli/flowkit/output" +) + +type FileReader interface { + ReadFile(path string) ([]byte, error) +} + +type FlixServiceConfig struct { + FlixServerURL string + FileReader FileReader + Logger output.Logger +} + +func NewFlixService(config *FlixServiceConfig) flixService { + if config.FlixServerURL == "" { + config.FlixServerURL = "https://flix.flow.com/v1/templates" + } + + return flixService{ + config: config, + } +} + +type flixService struct { + config *FlixServiceConfig +} + +type FlowInteractionTemplateExecution struct { + Network string + Cadence string + IsTransaciton bool + IsScript bool +} + +/* +Deployed contracts to network addresses +*/ +type NetworkAddressMap = v1_1.NetworkAddressMap + +/* +contract name associated with network information +*/ +type ContractInfos = v1_1.ContractInfos + +type flowInteractionTemplateCadence interface { + ReplaceCadenceImports(templateName string) (string, error) + IsTransaction() bool + IsScript() bool +} + +func (s flixService) GetTemplate(ctx context.Context, flixQuery string) (string, string, error) { + var template string + source := flixQuery + var err error + + if flixQuery == "" { + return "", source, fmt.Errorf("flix query cannot be empty") + } + + switch getType(flixQuery, s.config.FileReader) { + case flixId: + template, source, err = s.getFlixByID(ctx, flixQuery) + if err != nil { + return "", source, fmt.Errorf("could not find flix with id %s: %w", flixQuery, err) + } + + case flixName: + template, source, err = s.getFlix(ctx, flixQuery) + if err != nil { + return "", source, fmt.Errorf("could not find flix with name %s: %w", flixQuery, err) + } + + case flixPath: + source = flixQuery + if s.config.FileReader == nil { + return "", source, fmt.Errorf("file reader not provided") + } + file, err := s.config.FileReader.ReadFile(flixQuery) + if err != nil { + return "", source, fmt.Errorf("could not read flix file %s: %w", flixQuery, err) + } + template = string(file) + if err != nil { + return "", source, fmt.Errorf("could not parse flix from file %s: %w", flixQuery, err) + } + + case flixUrl: + template, source, err = fetchFlixWithContext(ctx, flixQuery) + if err != nil { + return "", source, fmt.Errorf("could not parse flix from url %s: %w", flixQuery, err) + } + case flixJson: + template = flixQuery + source = "json" + default: + return "", source, fmt.Errorf("invalid flix query type: %s", flixQuery) + } + + return template, source, nil +} + +func (s flixService) GetTemplateAndReplaceImports(ctx context.Context, templateName string, network string) (*FlowInteractionTemplateExecution, error) { + template, _, err := s.GetTemplate(ctx, templateName) + if err != nil { + return nil, err + } + var execution FlowInteractionTemplateExecution + var cadenceCode string + ver, err := getTemplateVersion(template) + if err != nil { + return nil, fmt.Errorf("invalid flix template version, %w", err) + } + var replaceableCadence flowInteractionTemplateCadence + switch ver { + case "1.1.0": + replaceableCadence, err = v1_1.ParseFlix(template) + if err != nil { + return nil, err + } + cadenceCode, err = replaceableCadence.ReplaceCadenceImports(network) + if err != nil { + return nil, err + } + execution.Cadence = cadenceCode + execution.IsScript = replaceableCadence.IsScript() + execution.IsTransaciton = replaceableCadence.IsTransaction() + case "1.0.0": + replaceableCadence, err = v1.ParseFlix(template) + if err != nil { + return nil, err + } + cadenceCode, err = replaceableCadence.ReplaceCadenceImports(network) + if err != nil { + return nil, err + } + execution.Cadence = cadenceCode + execution.IsScript = replaceableCadence.IsScript() + execution.IsTransaciton = replaceableCadence.IsTransaction() + default: + return nil, fmt.Errorf("flix template version: %s not supported", ver) + } + + if execution.Cadence == "" { + return nil, fmt.Errorf("could not parse template, invalid flix template") + } + + return &execution, nil +} + +func (s flixService) GetTemplateAndCreateBinding(ctx context.Context, templateName string, lang string, destFileLocation string) (string, error) { + template, source, err := s.GetTemplate(ctx, templateName) + if err != nil { + return "", err + } + language := strings.ToLower(lang) + var gen *FclCreator + + switch language { + case "js", "javascript": + gen = NewFclJSCreator() + case "ts", "typescript": + gen = NewFclTSCreator() + default: + return "", fmt.Errorf("language %s not supported", lang) + } + + relativeTemplateLocation := source + flixType := getType(source, s.config.FileReader) + if flixType == flixPath && destFileLocation != "" { + relativeTemplateLocation, err = getRelativePath(templateName, destFileLocation) + if err != nil { + return "", err + } + } + + return gen.Create(template, relativeTemplateLocation) +} + +func (s flixService) CreateTemplate(ctx context.Context, deployedContracts ContractInfos, code string, preFill string) (string, error) { + template, _, _ := s.GetTemplate(ctx, preFill) + var gen *v1_1.Generator + var err2 error + gen, err2 = v1_1.NewTemplateGenerator(deployedContracts, s.config.Logger) + if err2 != nil { + return "", err2 + } + return gen.CreateTemplate(ctx, code, template) +} + +func (s flixService) getFlixRaw(ctx context.Context, templateName string) (string, string, error) { + url := fmt.Sprintf("%s?name=%s", s.config.FlixServerURL, templateName) + return fetchFlixWithContext(ctx, url) +} + +func (s flixService) getFlix(ctx context.Context, templateName string) (string, string, error) { + template, url, err := s.getFlixRaw(ctx, templateName) + if err != nil { + return "", url, err + } + + return template, url, nil +} + +func (s flixService) getFlixByIDRaw(ctx context.Context, templateID string) (string, string, error) { + url := fmt.Sprintf("%s/%s", s.config.FlixServerURL, templateID) + return fetchFlixWithContext(ctx, url) +} + +func (s flixService) getFlixByID(ctx context.Context, templateID string) (string, string, error) { + template, url, err := s.getFlixByIDRaw(ctx, templateID) + if err != nil { + return "", url, err + } + return template, url, nil +} + +func fetchFlixWithContext(ctx context.Context, url string) (template string, templateUrl string, err error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", url, err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", url, err + } + defer func() { + if err := resp.Body.Close(); err != nil { + log.Printf("Warning: error while closing the response body: %v", err) + } + }() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", url, err + } + template = string(body) + return template, url, err +} diff --git a/flixkit/flixkit_test.go b/internal/flix_service_test.go similarity index 86% rename from flixkit/flixkit_test.go rename to internal/flix_service_test.go index e3fedad..b5de76d 100644 --- a/flixkit/flixkit_test.go +++ b/internal/flix_service_test.go @@ -1,4 +1,4 @@ -package flixkit +package internal import ( "context" @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" - v1 "github.com/onflow/flixkit-go/flixkit/v1" + v1 "github.com/onflow/flixkit-go/internal/v1" ) var flix_template = `{ @@ -190,7 +190,7 @@ func TestGetAndReplaceCadenceImports(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cadence, err := parsedTemplate.GetAndReplaceCadenceImports(tt.network) + cadence, err := parsedTemplate.ReplaceCadenceImports(tt.network) if tt.wantErr { assert.Error(err, "GetCadenceWithReplacedImports should return an error") } else { @@ -248,9 +248,10 @@ func TestFetchFlix(t *testing.T) { defer server.Close() ctx := context.Background() - body, err := FetchFlixWithContext(ctx, server.URL) + body, source, err := fetchFlixWithContext(ctx, server.URL) assert.NoError(err, "GetFlix should not return an error") assert.Equal("Hello World", body, "GetFlix should return the correct body") + assert.Equal(server.URL, source, "GetFlix should return the correct source") } type DefaultReader struct{} @@ -268,11 +269,12 @@ func TestGetFlixRaw(t *testing.T) { })) defer server.Close() - flixService := NewFlixService(&Config{FlixServerURL: server.URL, FileReader: DefaultReader{}}) + flixService := NewFlixService(&FlixServiceConfig{FlixServerURL: server.URL, FileReader: nil}) ctx := context.Background() - body, err := flixService.GetTemplate(ctx, "templateName") + body, source, err := flixService.GetTemplate(ctx, "templateName") assert.NoError(err, "GetFlixByName should not return an error") assert.Equal("Hello World", body, "GetFlixByName should return the correct body") + assert.Equal(server.URL+"?name=templateName", source, "GetFlixByName should return the correct source") } func TestGetFlixFilename(t *testing.T) { @@ -282,13 +284,13 @@ func TestGetFlixFilename(t *testing.T) { rw.Write([]byte(flix_template)) })) defer server.Close() - - flixService := NewFlixService(&Config{FlixServerURL: server.URL, FileReader: DefaultReader{}}) + flixService := NewFlixService(&FlixServiceConfig{FlixServerURL: server.URL, FileReader: DefaultReader{}}) ctx := context.Background() - flix, err := flixService.GetTemplate(ctx, "./templateFileName") + flix, source, err := flixService.GetTemplate(ctx, "./templateFileName") assert.NoError(err, "GetParsedFlixByName should not return an error") assert.NotNil(flix, "GetParsedFlixByName should not return a nil Flix") assert.Equal(flix_template, flix, "GetParsedFlixByName should return the correct Flix") + assert.Equal("./templateFileName", source, "GetParsedFlixByName should return the correct source") } func TestGetFlixByIDRaw(t *testing.T) { @@ -300,11 +302,12 @@ func TestGetFlixByIDRaw(t *testing.T) { })) defer server.Close() - flixService := NewFlixService(&Config{FlixServerURL: server.URL}) + flixService := NewFlixService(&FlixServiceConfig{FlixServerURL: server.URL}) ctx := context.Background() - body, err := flixService.GetTemplate(ctx, "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF") + body, source, err := flixService.GetTemplate(ctx, "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF") assert.NoError(err, "GetFlixByID should not return an error") assert.Equal("Hello World", body, "GetFlixByID should return the correct body") + assert.Equal(server.URL+"/1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF", source, "GetFlixByID should return the correct source") } func TestGetFlixByID(t *testing.T) { @@ -315,12 +318,13 @@ func TestGetFlixByID(t *testing.T) { })) defer server.Close() - flixService := NewFlixService(&Config{FlixServerURL: server.URL}) + flixService := NewFlixService(&FlixServiceConfig{FlixServerURL: server.URL}) ctx := context.Background() - flix, err := flixService.GetTemplate(ctx, "templateID") + flix, source, err := flixService.GetTemplate(ctx, "templateID") assert.NoError(err, "GetParsedFlixByID should not return an error") assert.NotNil(flix, "GetParsedFlixByID should not return a nil Flix") assert.Equal(flix_template, flix, "GetParsedFlixByID should return the correct Flix") + assert.Equal(server.URL+"?name=templateID", source, "GetParsedFlixByID should return the correct source") } func TestTemplateVersion(t *testing.T) { @@ -356,7 +360,7 @@ func TestTemplateVersion(t *testing.T) { for _, tt := range tests { t.Run(tt.templateStr, func(t *testing.T) { - ver, err := GetTemplateVersion(tt.templateStr) + ver, err := getTemplateVersion(tt.templateStr) if tt.wantErr { assert.Error(err, "TemplateVersion should return an error") } else { diff --git a/internal/testdata/TestBindingReadTokenBalance.golden b/internal/testdata/TestBindingReadTokenBalance.golden new file mode 100644 index 0000000..68df772 --- /dev/null +++ b/internal/testdata/TestBindingReadTokenBalance.golden @@ -0,0 +1,33 @@ +`/** + This binding file was auto generated based on FLIX template v1.1.0. + Changes to this file might get overwritten. + Note fcl version 1.9.0 or higher is required to use templates. +**/ + +import * as fcl from "@onflow/fcl" +import flixTemplate from "./read-token-balance.template.json" + +interface RequestParams { + address: string; +} + +/** +* request: +* @param string address - +* @returns {Promise} - +*/ +export async function request({address}: RequestParams): Promise { + const info = await fcl.query({ + cadence: "", + template: flixTemplate, + args: (arg, t) => [arg(address, t.Address)] + }); + + return info +} + + + + + +` diff --git a/flixkit/testdata/TestJSGenArrayScript.golden b/internal/testdata/TestJSGenArrayScript.golden similarity index 100% rename from flixkit/testdata/TestJSGenArrayScript.golden rename to internal/testdata/TestJSGenArrayScript.golden diff --git a/flixkit/testdata/TestJSGenMinScript.golden b/internal/testdata/TestJSGenMinScript.golden similarity index 100% rename from flixkit/testdata/TestJSGenMinScript.golden rename to internal/testdata/TestJSGenMinScript.golden diff --git a/flixkit/testdata/TestJSGenNoParamsScript.golden b/internal/testdata/TestJSGenNoParamsScript.golden similarity index 100% rename from flixkit/testdata/TestJSGenNoParamsScript.golden rename to internal/testdata/TestJSGenNoParamsScript.golden diff --git a/flixkit/testdata/TestJSGenScript.golden b/internal/testdata/TestJSGenScript.golden similarity index 100% rename from flixkit/testdata/TestJSGenScript.golden rename to internal/testdata/TestJSGenScript.golden diff --git a/flixkit/testdata/TestJSGenTransaction.golden b/internal/testdata/TestJSGenTransaction.golden similarity index 100% rename from flixkit/testdata/TestJSGenTransaction.golden rename to internal/testdata/TestJSGenTransaction.golden diff --git a/flixkit/testdata/TestTSGenNoParamsScript.golden b/internal/testdata/TestTSGenNoParamsScript.golden similarity index 100% rename from flixkit/testdata/TestTSGenNoParamsScript.golden rename to internal/testdata/TestTSGenNoParamsScript.golden diff --git a/flixkit/testdata/TestTSGenNoParamsTx.golden b/internal/testdata/TestTSGenNoParamsTx.golden similarity index 100% rename from flixkit/testdata/TestTSGenNoParamsTx.golden rename to internal/testdata/TestTSGenNoParamsTx.golden diff --git a/flixkit/testdata/TestTSGenParamsScript.golden b/internal/testdata/TestTSGenParamsScript.golden similarity index 100% rename from flixkit/testdata/TestTSGenParamsScript.golden rename to internal/testdata/TestTSGenParamsScript.golden diff --git a/flixkit/testdata/TestTSGenParamsTx.golden b/internal/testdata/TestTSGenParamsTx.golden similarity index 100% rename from flixkit/testdata/TestTSGenParamsTx.golden rename to internal/testdata/TestTSGenParamsTx.golden diff --git a/flixkit/v1/types.go b/internal/v1/types.go similarity index 96% rename from flixkit/v1/types.go rename to internal/v1/types.go index ef373c6..fa16f07 100644 --- a/flixkit/v1/types.go +++ b/internal/v1/types.go @@ -1,4 +1,4 @@ -package flixkit +package v1 import ( "encoding/json" @@ -76,7 +76,7 @@ func ParseFlix(template string) (*FlowInteractionTemplate, error) { return &flowTemplate, nil } -func (t *FlowInteractionTemplate) GetAndReplaceCadenceImports(networkName string) (string, error) { +func (t *FlowInteractionTemplate) ReplaceCadenceImports(networkName string) (string, error) { var cadence string for dependencyAddress, c := range t.Data.Dependencies { diff --git a/flixkit/generator.go b/internal/v1_1/generator.go similarity index 78% rename from flixkit/generator.go rename to internal/v1_1/generator.go index cc0a609..10629ca 100644 --- a/flixkit/generator.go +++ b/internal/v1_1/generator.go @@ -1,4 +1,4 @@ -package flixkit +package v1_1 import ( "context" @@ -10,7 +10,6 @@ import ( "github.com/onflow/cadence/runtime/cmd" "github.com/onflow/cadence/runtime/common" "github.com/onflow/cadence/runtime/parser" - v1_1 "github.com/onflow/flixkit-go/flixkit/v1_1" "github.com/onflow/flixkit-go/internal/contracts" "github.com/onflow/flow-cli/flowkit" "github.com/onflow/flow-cli/flowkit/config" @@ -32,13 +31,13 @@ Same structure as core contracts, keyed by contract name type ContractInfos map[string]NetworkAddressMap type Generator struct { - deployedContracts []v1_1.Contract + deployedContracts []Contract testnetClient *flowkit.Flowkit mainnetClient *flowkit.Flowkit - template *v1_1.InteractionTemplate + template *InteractionTemplate } -func NewGenerator(contractInfos ContractInfos, logger output.Logger) (*Generator, error) { +func NewTemplateGenerator(contractInfos ContractInfos, logger output.Logger) (*Generator, error) { loader := afero.Afero{Fs: afero.NewOsFs()} gwt, err := gateway.NewGrpcGateway(config.TestnetNetwork) @@ -59,11 +58,11 @@ func NewGenerator(contractInfos ContractInfos, logger output.Logger) (*Generator mainnetClient := flowkit.NewFlowkit(state, config.MainnetNetwork, gwm, logger) // add core contracts to deployed contracts cc := contracts.GetCoreContracts() - deployedContracts := make([]v1_1.Contract, 0) + deployedContracts := make([]Contract, 0) for contractName, c := range cc { - contract := v1_1.Contract{ + contract := Contract{ Contract: contractName, - Networks: []v1_1.Network{ + Networks: []Network{ {Network: config.MainnetNetwork.Name, Address: c[config.MainnetNetwork.Name]}, {Network: config.TestnetNetwork.Name, Address: c[config.TestnetNetwork.Name]}, {Network: config.EmulatorNetwork.Name, Address: c[config.EmulatorNetwork.Name]}, @@ -73,14 +72,15 @@ func NewGenerator(contractInfos ContractInfos, logger output.Logger) (*Generator } // allow user contracts to override core contracts for contractInfo, networks := range contractInfos { - contract := v1_1.Contract{ + contract := Contract{ Contract: contractInfo, - Networks: make([]v1_1.Network, 0), + Networks: make([]Network, 0), } for network, address := range networks { - contract.Networks = append(contract.Networks, v1_1.Network{ + addr := flow.HexToAddress(address) + contract.Networks = append(contract.Networks, Network{ Network: network, - Address: address, + Address: "0x" + addr.Hex(), }) } deployedContracts = append(deployedContracts, contract) @@ -90,15 +90,15 @@ func NewGenerator(contractInfos ContractInfos, logger output.Logger) (*Generator deployedContracts: deployedContracts, testnetClient: testnetClient, mainnetClient: mainnetClient, - template: &v1_1.InteractionTemplate{}, + template: &InteractionTemplate{}, }, nil } -func (g Generator) Generate(ctx context.Context, code string, preFill string) (string, error) { - g.template = &v1_1.InteractionTemplate{} +func (g Generator) CreateTemplate(ctx context.Context, code string, preFill string) (string, error) { + g.template = &InteractionTemplate{} g.template.Init() if preFill != "" { - t, err := v1_1.ParseFlix(preFill) + t, err := ParseFlix(preFill) if err != nil { return "", err } @@ -134,7 +134,7 @@ func (g Generator) Generate(ctx context.Context, code string, preFill string) (s // need to process dependencies before calculating network pins _ = g.calculateNetworkPins(program) - id, _ := v1_1.GenerateFlixID(g.template) + id, _ := GenerateFlixID(g.template) g.template.ID = id templateJson, err := json.MarshalIndent(g.template, "", " ") @@ -147,15 +147,15 @@ func (g Generator) calculateNetworkPins(program *ast.Program) error { config.MainnetNetwork.Name, config.TestnetNetwork.Name, } - networkPins := make([]v1_1.NetworkPin, 0) + networkPins := make([]NetworkPin, 0) for _, netName := range networksOfInterest { - cad, err := g.template.GetAndReplaceCadenceImports(netName) + cad, err := g.template.ReplaceCadenceImports(netName) if err != nil { continue } - networkPins = append(networkPins, v1_1.NetworkPin{ + networkPins = append(networkPins, NetworkPin{ Network: netName, - PinSelf: v1_1.ShaHex(cad, ""), + PinSelf: ShaHex(cad, ""), }) } g.template.Data.Cadence.NetworkPins = networkPins @@ -170,9 +170,9 @@ func (g Generator) processDependencies(ctx context.Context, program *ast.Program } // fill in dependence information - g.template.Data.Dependencies = make([]v1_1.Dependency, 0) + g.template.Data.Dependencies = make([]Dependency, 0) for _, imp := range imports { - contractName, err := v1_1.ExtractContractName(imp.String()) + contractName, err := ExtractContractName(imp.String()) if err != nil { return err } @@ -180,12 +180,12 @@ func (g Generator) processDependencies(ctx context.Context, program *ast.Program if err != nil { return err } - c := v1_1.Contract{ + c := Contract{ Contract: contractName, Networks: networks, } - dep := v1_1.Dependency{ - Contracts: []v1_1.Contract{c}, + dep := Dependency{ + Contracts: []Contract{c}, } g.template.Data.Dependencies = append(g.template.Data.Dependencies, dep) } @@ -193,15 +193,15 @@ func (g Generator) processDependencies(ctx context.Context, program *ast.Program return nil } -func (g *Generator) generateDependenceInfo(ctx context.Context, contractName string) ([]v1_1.Network, error) { +func (g *Generator) generateDependenceInfo(ctx context.Context, contractName string) ([]Network, error) { // only support string import syntax contractNetworks := g.LookupImportContractInfo(contractName) if len(contractNetworks) == 0 { return nil, fmt.Errorf("could not find contract dependency %s", contractName) } - var networks []v1_1.Network + var networks []Network for _, n := range contractNetworks { - network := v1_1.Network{ + network := Network{ Network: n.Network, Address: n.Address, } @@ -228,7 +228,7 @@ func (g *Generator) generateDependenceInfo(ctx context.Context, contractName str return networks, nil } -func (g *Generator) LookupImportContractInfo(contractName string) []v1_1.Network { +func (g *Generator) LookupImportContractInfo(contractName string) []Network { for _, contract := range g.deployedContracts { if contractName == contract.Contract { return contract.Networks @@ -237,8 +237,8 @@ func (g *Generator) LookupImportContractInfo(contractName string) []v1_1.Network return nil } -func (g *Generator) GenerateDepPinDepthFirst(ctx context.Context, flowkit *flowkit.Flowkit, address string, name string, height uint64) (details *v1_1.PinDetail, err error) { - memoize := make(map[string]v1_1.PinDetail) +func (g *Generator) GenerateDepPinDepthFirst(ctx context.Context, flowkit *flowkit.Flowkit, address string, name string, height uint64) (details *PinDetail, err error) { + memoize := make(map[string]PinDetail) networkPinDetail, err := generateDependencyNetworks(ctx, flowkit, address, name, memoize, height) if err != nil { return nil, err @@ -247,30 +247,28 @@ func (g *Generator) GenerateDepPinDepthFirst(ctx context.Context, flowkit *flowk return networkPinDetail, nil } -func generateDependencyNetworks(ctx context.Context, flowkit *flowkit.Flowkit, address string, name string, cache map[string]v1_1.PinDetail, height uint64) (*v1_1.PinDetail, error) { - identifier := fmt.Sprintf("A.%s.%s", strings.ReplaceAll(address, "0x", ""), name) +func generateDependencyNetworks(ctx context.Context, flowkit *flowkit.Flowkit, address string, name string, cache map[string]PinDetail, height uint64) (*PinDetail, error) { + addr := flow.HexToAddress(address) + identifier := fmt.Sprintf("A.%s.%s", addr.Hex(), name) pinDetail, ok := cache[identifier] if ok { return &pinDetail, nil } - account, err := flowkit.GetAccount(ctx, flow.HexToAddress(address)) + account, err := flowkit.GetAccount(ctx, addr) if err != nil { return nil, err } code := account.Contracts[name] - if !strings.HasPrefix(address, "0x") { - address = "0x" + address - } - depend := v1_1.PinDetail{ + depend := PinDetail{ PinContractName: name, - PinContractAddress: address, - PinSelf: v1_1.ShaHex(code, ""), + PinContractAddress: "0x" + addr.Hex(), + PinSelf: ShaHex(code, ""), } depend.CalculatePin(height) pins := []string{depend.PinSelf} imports := getAddressImports(code, name) - detailImports := make([]v1_1.PinDetail, 0) + detailImports := make([]PinDetail, 0) for _, imp := range imports { split := strings.Split(imp, ".") address, name := split[0], split[1] @@ -285,7 +283,7 @@ func generateDependencyNetworks(ctx context.Context, flowkit *flowkit.Flowkit, a pins = append(pins, dep.PinSelf) } depend.Imports = detailImports - depend.Pin = v1_1.ShaHex(strings.Join(pins, ""), "") + depend.Pin = ShaHex(strings.Join(pins, ""), "") return &depend, nil } diff --git a/flixkit/generator_test.go b/internal/v1_1/generator_test.go similarity index 88% rename from flixkit/generator_test.go rename to internal/v1_1/generator_test.go index 43d0426..afbeb04 100644 --- a/flixkit/generator_test.go +++ b/internal/v1_1/generator_test.go @@ -1,4 +1,4 @@ -package flixkit +package v1_1 import ( "context" @@ -8,15 +8,14 @@ import ( "github.com/onflow/flow-cli/flowkit/config" "github.com/stretchr/testify/assert" - v1_1 "github.com/onflow/flixkit-go/flixkit/v1_1" "github.com/onflow/flixkit-go/internal/contracts" ) func TestHelloScript(t *testing.T) { - contracts := []v1_1.Contract{ + contracts := []Contract{ { Contract: "HelloWorld", - Networks: []v1_1.Network{ + Networks: []Network{ { Network: "testnet", Address: "0xee82856bf20e2aa6", @@ -38,6 +37,7 @@ func TestHelloScript(t *testing.T) { testnetClient: nil, mainnetClient: nil, } + assert := assert.New(t) code := ` #interaction( @@ -56,17 +56,17 @@ func TestHelloScript(t *testing.T) { } ` ctx := context.Background() - template, err := generator.Generate(ctx, code, "") + template, err := generator.CreateTemplate(ctx, code, "") assert.NoError(err, "Generate should not return an error") autogold.ExpectFile(t, template) } func TestTransactionValue(t *testing.T) { - contracts := []v1_1.Contract{ + contracts := []Contract{ { Contract: "HelloWorld", - Networks: []v1_1.Network{ + Networks: []Network{ { Network: "testnet", Address: "0xee82856bf20e2aa6", @@ -118,19 +118,19 @@ func TestTransactionValue(t *testing.T) { } ` ctx := context.Background() - template, err := generator.Generate(ctx, code, "") + template, err := generator.CreateTemplate(ctx, code, "") assert.NoError(err, "Generate should not return an error") autogold.ExpectFile(t, template) } func TestTransferFlowTransaction(t *testing.T) { - cs := []v1_1.Contract{} + cs := []Contract{} cc := contracts.GetCoreContracts() for contractName, c := range cc { - contract := v1_1.Contract{ + contract := Contract{ Contract: contractName, - Networks: []v1_1.Network{ + Networks: []Network{ {Network: config.MainnetNetwork.Name, Address: c[config.MainnetNetwork.Name]}, {Network: config.TestnetNetwork.Name, Address: c[config.TestnetNetwork.Name]}, {Network: config.EmulatorNetwork.Name, Address: c[config.EmulatorNetwork.Name]}, @@ -174,7 +174,7 @@ func TestTransferFlowTransaction(t *testing.T) { } ` ctx := context.Background() - template, err := generator.Generate(ctx, code, "") + template, err := generator.CreateTemplate(ctx, code, "") assert.NoError(err, "Generate should not return an error") autogold.ExpectFile(t, template) diff --git a/flixkit/v1_1/testdata/TestEmptyPragmaWithParameter.golden b/internal/v1_1/testdata/TestEmptyPragmaWithParameter.golden similarity index 100% rename from flixkit/v1_1/testdata/TestEmptyPragmaWithParameter.golden rename to internal/v1_1/testdata/TestEmptyPragmaWithParameter.golden diff --git a/flixkit/v1_1/testdata/TestGenerateParametersScripts.golden b/internal/v1_1/testdata/TestGenerateParametersScripts.golden similarity index 91% rename from flixkit/v1_1/testdata/TestGenerateParametersScripts.golden rename to internal/v1_1/testdata/TestGenerateParametersScripts.golden index a61e0ca..059ae36 100644 --- a/flixkit/v1_1/testdata/TestGenerateParametersScripts.golden +++ b/internal/v1_1/testdata/TestGenerateParametersScripts.golden @@ -56,6 +56,12 @@ } ] } - ] + ], + "output": { + "label": "result", + "index": 0, + "type": "UFix64", + "messages": [] + } } }` diff --git a/flixkit/testdata/TestHelloScript.golden b/internal/v1_1/testdata/TestHelloScript.golden similarity index 93% rename from flixkit/testdata/TestHelloScript.golden rename to internal/v1_1/testdata/TestHelloScript.golden index c2794eb..d9ceb2c 100644 --- a/flixkit/testdata/TestHelloScript.golden +++ b/internal/v1_1/testdata/TestHelloScript.golden @@ -64,6 +64,12 @@ ] } ], - "parameters": null + "parameters": null, + "output": { + "label": "result", + "index": 0, + "type": "String", + "messages": [] + } } }` diff --git a/flixkit/v1_1/testdata/TestParsePragma/Empty.golden b/internal/v1_1/testdata/TestParsePragma/Empty.golden similarity index 100% rename from flixkit/v1_1/testdata/TestParsePragma/Empty.golden rename to internal/v1_1/testdata/TestParsePragma/Empty.golden diff --git a/flixkit/v1_1/testdata/TestParsePragma/Minimum.golden b/internal/v1_1/testdata/TestParsePragma/Minimum.golden similarity index 100% rename from flixkit/v1_1/testdata/TestParsePragma/Minimum.golden rename to internal/v1_1/testdata/TestParsePragma/Minimum.golden diff --git a/flixkit/v1_1/testdata/TestParsePragma/WithParameters.golden b/internal/v1_1/testdata/TestParsePragma/WithParameters.golden similarity index 100% rename from flixkit/v1_1/testdata/TestParsePragma/WithParameters.golden rename to internal/v1_1/testdata/TestParsePragma/WithParameters.golden diff --git a/flixkit/v1_1/testdata/TestParsePragma/WithoutParameters.golden b/internal/v1_1/testdata/TestParsePragma/WithoutParameters.golden similarity index 100% rename from flixkit/v1_1/testdata/TestParsePragma/WithoutParameters.golden rename to internal/v1_1/testdata/TestParsePragma/WithoutParameters.golden diff --git a/flixkit/testdata/TestTransactionValue.golden b/internal/v1_1/testdata/TestTransactionValue.golden similarity index 100% rename from flixkit/testdata/TestTransactionValue.golden rename to internal/v1_1/testdata/TestTransactionValue.golden diff --git a/flixkit/testdata/TestTransferFlowTransaction.golden b/internal/v1_1/testdata/TestTransferFlowTransaction.golden similarity index 100% rename from flixkit/testdata/TestTransferFlowTransaction.golden rename to internal/v1_1/testdata/TestTransferFlowTransaction.golden diff --git a/flixkit/v1_1/types.go b/internal/v1_1/types.go similarity index 98% rename from flixkit/v1_1/types.go rename to internal/v1_1/types.go index 0df025d..9cea150 100644 --- a/flixkit/v1_1/types.go +++ b/internal/v1_1/types.go @@ -1,4 +1,4 @@ -package flixkit +package v1_1 import ( "bytes" @@ -154,7 +154,7 @@ func replaceImport(code string, from string, to string) string { return code } -func (t *InteractionTemplate) GetAndReplaceCadenceImports(networkName string) (string, error) { +func (t *InteractionTemplate) ReplaceCadenceImports(networkName string) (string, error) { cadence := t.Data.Cadence.Body // Compile regular expression to match and capture contract names re := regexp.MustCompile(`import\s*"([^"]+)"`) @@ -354,6 +354,12 @@ func (template *InteractionTemplate) ProcessParameters(program *ast.Program) err for _, d := range functionDeclaration { if d.Identifier.String() == "main" { parameterList = d.ParameterList.Parameters + r := d.ReturnTypeAnnotation.Type.String() + template.Data.Output = &Parameter{ + Label: "result", + Type: r, + Messages: make([]Message, 0), + } } } diff --git a/flixkit/v1_1/types_test.go b/internal/v1_1/types_test.go similarity index 99% rename from flixkit/v1_1/types_test.go rename to internal/v1_1/types_test.go index ca3eed2..e33a72a 100644 --- a/flixkit/v1_1/types_test.go +++ b/internal/v1_1/types_test.go @@ -1,4 +1,4 @@ -package flixkit +package v1_1 import ( "encoding/json" @@ -814,7 +814,7 @@ func TestGetAndReplaceCadenceImports(t *testing.T) { t.Fatal(err) } - cadenceCode, err := parsedTemplate.GetAndReplaceCadenceImports(tt.network) + cadenceCode, err := parsedTemplate.ReplaceCadenceImports(tt.network) if tt.wantErr { assert.Error(err, tt.name, "GetCadenceWithReplacedImports should return an error") } else { @@ -832,7 +832,7 @@ func TestGetAndReplaceCadenceImportsMultipleImports(t *testing.T) { if err != nil { t.Fatal(err) } - cadenceCode, err := template.GetAndReplaceCadenceImports("mainnet") + cadenceCode, err := template.ReplaceCadenceImports("mainnet") if err != nil { t.Fatal(err) } @@ -846,7 +846,7 @@ func TestGetAndReplaceCadenceImportsMultipleCoreImports(t *testing.T) { if err != nil { t.Fatal(err) } - cadenceCode, err := template.GetAndReplaceCadenceImports("mainnet") + cadenceCode, err := template.ReplaceCadenceImports("mainnet") if err != nil { t.Fatal(err) }