diff --git a/go.mod b/go.mod index 896a6d895..d73c8cf14 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/onflow/flow-emulator v0.59.0 github.com/onflow/flow-go-sdk v0.41.17 github.com/onflowser/flowser/v3 v3.1.3 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/errors v0.9.1 github.com/psiemens/sconfig v0.1.0 github.com/radovskyb/watcher v1.0.7 @@ -87,6 +88,7 @@ require ( github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-stack/stack v1.8.1 // indirect + github.com/gobwas/ws v1.3.0 // indirect github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.1.2 // indirect diff --git a/go.sum b/go.sum index 4ab035819..a84212ff6 100644 --- a/go.sum +++ b/go.sum @@ -378,12 +378,15 @@ github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-test/deep v1.0.5/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= +github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= @@ -944,6 +947,8 @@ github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -1498,6 +1503,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/accounts/accounts.go b/internal/accounts/accounts.go index 31d22c00c..a88a25071 100644 --- a/internal/accounts/accounts.go +++ b/internal/accounts/accounts.go @@ -37,6 +37,10 @@ var Cmd = &cobra.Command{ GroupID: "resources", } +func testnetFaucetURL(address flow.Address) string { + return fmt.Sprintf("https://testnet-faucet.onflow.org/fund-account?address=%s", address) +} + func init() { addContractCommand.AddToParent(Cmd) removeCommand.AddToParent(Cmd) @@ -44,6 +48,7 @@ func init() { createCommand.AddToParent(Cmd) stakingCommand.AddToParent(Cmd) getCommand.AddToParent(Cmd) + fundCommand.AddToParent(Cmd) } // accountResult represent result from all account commands. @@ -86,6 +91,15 @@ func (r *accountResult) String() string { var b bytes.Buffer writer := util.CreateTabWriter(&b) + if r.Address.IsValid(flow.Testnet) { + _, _ = fmt.Fprintf( + writer, + "If you would like to fund the account with 1000 FLOW tokens for testing,"+ + " visit %s\n\n", + testnetFaucetURL(r.Address), + ) + } + _, _ = fmt.Fprintf(writer, "Address\t 0x%s\n", r.Address) _, _ = fmt.Fprintf(writer, "Balance\t %s\n", cadence.UFix64(r.Balance)) diff --git a/internal/accounts/create-interactive.go b/internal/accounts/create-interactive.go index c747d577c..95f300da3 100644 --- a/internal/accounts/create-interactive.go +++ b/internal/accounts/create-interactive.go @@ -30,14 +30,13 @@ import ( "strings" "time" - "github.com/onflow/flow-cli/flowkit/accounts" - flowsdk "github.com/onflow/flow-go-sdk" "github.com/onflow/flow-go-sdk/crypto" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "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" @@ -48,7 +47,7 @@ import ( // // This process takes the user through couple of steps with prompts asking for them to provide name and network, // and it then uses account creation APIs to automatically create the account on the network as well as save it. -func createInteractive(state *flowkit.State) error { +func createInteractive(state *flowkit.State) (*accountResult, error) { log := output.NewStdoutLogger(output.InfoLog) name := util.AccountNamePrompt(state.Accounts().Names()) networkName, selectedNetwork := util.CreateAccountNetworkPrompt() @@ -57,13 +56,13 @@ func createInteractive(state *flowkit.State) error { // create new gateway based on chosen network gw, err := gateway.NewGrpcGateway(selectedNetwork) if err != nil { - return err + return nil, err } flow := flowkit.NewFlowkit(state, selectedNetwork, gw, output.NewStdoutLogger(output.NoneLog)) key, err := flow.GenerateKey(context.Background(), defaultSignAlgo, "") if err != nil { - return err + return nil, err } log.StartProgress(fmt.Sprintf("Creating account %s on %s...", name, networkName)) @@ -79,7 +78,7 @@ func createInteractive(state *flowkit.State) error { log.StopProgress() } if err != nil { - return err + return nil, err } log.Info(fmt.Sprintf( @@ -93,7 +92,7 @@ func createInteractive(state *flowkit.State) error { state.Accounts().AddOrUpdate(account) err = state.SaveDefault() if err != nil { - return err + return nil, err } items := []string{ @@ -108,7 +107,14 @@ func createInteractive(state *flowkit.State) error { } outputList(log, items, false) - return nil + return &accountResult{ + Account: &flowsdk.Account{ + Address: account.Address, + Balance: 0, + Keys: []*flowsdk.AccountKey{flowsdk.NewAccountKey().FromPrivateKey(key)}, + }, + include: nil, + }, nil } // createNetworkAccount using the account creation API and return the newly created account address. diff --git a/internal/accounts/create.go b/internal/accounts/create.go index 8d1fa8f9d..436321fad 100644 --- a/internal/accounts/create.go +++ b/internal/accounts/create.go @@ -61,15 +61,22 @@ func create( flow flowkit.Services, state *flowkit.State, ) (command.Result, error) { + if len(createFlags.Keys) == 0 { // if user doesn't provide any flags go into interactive mode + return createInteractive(state) + } else { + return createManual(state, flow) + } +} + +func createManual( + state *flowkit.State, + flow flowkit.Services, +) (*accountResult, error) { sigsFlag := createFlags.SigAlgo hashFlag := createFlags.HashAlgo keysFlag := createFlags.Keys weightFlag := createFlags.Weights - if len(keysFlag) == 0 { // if user doesn't provide any flags go into interactive mode - return nil, createInteractive(state) - } - signer, err := state.Accounts().ByName(createFlags.Signer) if err != nil { return nil, err diff --git a/internal/accounts/fund.go b/internal/accounts/fund.go new file mode 100644 index 000000000..4f161a20c --- /dev/null +++ b/internal/accounts/fund.go @@ -0,0 +1,79 @@ +/* + * Flow CLI + * + * Copyright 2019 Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package accounts + +import ( + "fmt" + "time" + + flowsdk "github.com/onflow/flow-go-sdk" + + "github.com/pkg/browser" + "github.com/spf13/cobra" + + "github.com/onflow/flow-cli/flowkit" + "github.com/onflow/flow-cli/flowkit/output" + "github.com/onflow/flow-cli/internal/command" +) + +type flagsFund struct { + Include []string `default:"" flag:"include" info:"Fields to include in the output. Valid values: contracts."` +} + +var fundFlags = flagsFund{} + +var fundCommand = &command.Command{ + Cmd: &cobra.Command{ + Use: "fund
", + Short: "Funds an account by address through the Testnet Faucet", + Example: "flow accounts fund 8e94eaa81771313a", + Args: cobra.ExactArgs(1), + }, + Flags: &fundFlags, + Run: fund, +} + +func fund( + args []string, + _ command.GlobalFlags, + logger output.Logger, + _ flowkit.ReaderWriter, + flow flowkit.Services, +) (command.Result, error) { + address := flowsdk.HexToAddress(args[0]) + if !address.IsValid(flowsdk.Testnet) { + return nil, fmt.Errorf("unsupported address %s, faucet can only work for valid Testnet addresses", address.String()) + } + + logger.Info( + fmt.Sprintf( + "Opening the Testnet faucet to fund 0x%s on your native browser."+ + "\n\nIf there is an issue, please use this link instead: %s", + address.String(), + testnetFaucetURL(address), + )) + // wait for the user to read the message + time.Sleep(5 * time.Second) + + if err := browser.OpenURL(testnetFaucetURL(address)); err != nil { + return nil, err + } + + return nil, nil +}