diff --git a/.env.example b/.env.example index ba6b5718..03d62ca5 100644 --- a/.env.example +++ b/.env.example @@ -30,6 +30,8 @@ FLOW_WALLET_ENABLED_TOKENS=FUSD:0xf8d6e0586b0a20c7:fusd,FlowToken:0x0ae53cb6e3f4 # This sets the number of proposal keys to be used on the admin account. # You can increase transaction throughput by using multiple proposal keys for # parallel transaction execution. +# WARNING: Increasing admin account's key count by more than 100 in a single transaction fails +# due to the large event size of the resulting transaction. Please increase key count in steps of 100. FLOW_WALLET_ADMIN_PROPOSAL_KEY_COUNT=50 # Sets the server request timeout @@ -42,7 +44,14 @@ FLOW_WALLET_ADMIN_PROPOSAL_KEY_COUNT=50 # Number of concurrent workers handling incoming jobs. # You can increase the number of workers if you're sending # too many transactions and find that the queue is often backlogged. -# FLOW_WALLET_WORKER_COUNT=100 (default) +# FLOW_WALLET_WORKER_COUNT=1 (default) # Max transactions per second, rate at which the service can submit transactions to Flow # FLOW_WALLET_MAX_TPS=10 (default) + + +# Init enabled fungible tokens on new account creation +INIT_FUNGIBLE_TOKEN_VAULTS_ON_ACCOUNT_CREATION=true + +# Service log level (Default=info) +# FLOW_WALLET_LOG_LEVEL=info \ No newline at end of file diff --git a/README.md b/README.md index 6b8d1956..13eccb72 100644 --- a/README.md +++ b/README.md @@ -110,16 +110,25 @@ When making `sync` requests it's sometimes required to adjust the server's reque ### Enabled fungible tokens -A comma separated list of _fungible tokens_ and their corresponding addresses enabled for this instance. Make sure to name each token exactly as it is in the corresponding cadence code (FlowToken, FUSD etc.). Include at least FlowToken as functionality without it is undetermined. +A comma separated list of _fungible tokens_ and their corresponding addresses and paths enabled for this instance. Make sure to name each token exactly as it is in the corresponding Cadence code (FlowToken, FUSD, etc). Include at least FlowToken as functionality without it is undetermined. Format is comma separated list of: -**NOTE:** It is necessary to add a 3rd parameter "lowercamelcase" name for each token. For FlowToken this would be "flowToken" and for FUSD "fusd". This is used to construct the vault name, receiver name and balance name in generic transaction templates. Consult the contract code for each token to derive the proper name (search for `.*Vault`, `.*Receiver`, `.*Balance`) +``` +TokenName:ContractAddress:ReceiverPublicPath:BalancePublicPath:VaultStoragePath +``` + +Example (mainnet): +``` +FiatToken:0xb19436aae4d94622:FiatToken.VaultReceiverPubPath:FiatToken.VaultBalancePubPath:FiatToken.VaultStoragePath +``` -**NOTE:** Non-fungible tokens can _not_ be enabled using environment variables. Use the API endpoints for that. +**DEPRECATION NOTICE:** You can optionally config each token with 3 parameters: a 3rd parameter "lowercamelcase" name for each token. For FlowToken this would be "flowToken" and for FUSD "fusd". This is used to construct the vault name, receiver name and balance name in generic transaction templates. Consult the contract code for each token to derive the proper name (search for `.*Vault`, `.*Receiver`, `.*Balance`).**THIS IS NOW DEPRECATED** It's best to grab paths from the token contract and set them explicitly here instead of generating them based on lowercase token name. The old format still works to maintain backward compatibility. Examples: FLOW_WALLET_ENABLED_TOKENS=FlowToken:0x0ae53cb6e3f42a79:flowToken,FUSD:0xf8d6e0586b0a20c7:fusd +**NOTE:** Non-fungible tokens _cannot_ be enabled using environment variables. Use the API endpoints for that. + ### Database | Config variable | Environment variable | Description | Default | Examples | diff --git a/accounts/account_added.go b/accounts/account_added.go index f7b48c4a..013538ed 100644 --- a/accounts/account_added.go +++ b/accounts/account_added.go @@ -1,12 +1,14 @@ package accounts import ( + "github.com/flow-hydraulics/flow-wallet-api/templates" "github.com/onflow/flow-go-sdk" log "github.com/sirupsen/logrus" ) type AccountAddedPayload struct { - Address flow.Address + Address flow.Address + InitializedFungibleTokens []templates.Token } type accountAddedHandler interface { diff --git a/accounts/service.go b/accounts/service.go index 4f0b8082..7acbdb50 100644 --- a/accounts/service.go +++ b/accounts/service.go @@ -13,9 +13,11 @@ import ( "github.com/flow-hydraulics/flow-wallet-api/flow_helpers" "github.com/flow-hydraulics/flow-wallet-api/jobs" "github.com/flow-hydraulics/flow-wallet-api/keys" + "github.com/flow-hydraulics/flow-wallet-api/templates" "github.com/flow-hydraulics/flow-wallet-api/templates/template_strings" "github.com/flow-hydraulics/flow-wallet-api/transactions" "github.com/onflow/cadence" + jsoncdc "github.com/onflow/cadence/encoding/json" "github.com/onflow/flow-go-sdk" flow_crypto "github.com/onflow/flow-go-sdk/crypto" flow_templates "github.com/onflow/flow-go-sdk/templates" @@ -43,6 +45,7 @@ type ServiceImpl struct { fc flow_helpers.FlowClient wp jobs.WorkerPool txs transactions.Service + temps templates.Service txRateLimiter ratelimit.Limiter } @@ -54,12 +57,13 @@ func NewService( fc flow_helpers.FlowClient, wp jobs.WorkerPool, txs transactions.Service, + temps templates.Service, opts ...ServiceOption, ) Service { var defaultTxRatelimiter = ratelimit.NewUnlimited() // TODO(latenssi): safeguard against nil config? - svc := &ServiceImpl{cfg, store, km, fc, wp, txs, defaultTxRatelimiter} + svc := &ServiceImpl{cfg, store, km, fc, wp, txs, temps, defaultTxRatelimiter} for _, opt := range opts { opt(svc) @@ -359,13 +363,29 @@ func (s *ServiceImpl) createAccount(ctx context.Context) (*Account, string, erro publicKeys = append(publicKeys, &clonedAccountKey) } - flowTx, err := flow_templates.CreateAccount( - publicKeys, - nil, - payer.Address, - ) - if err != nil { - return nil, "", err + var flowTx *flow.Transaction + var initializedFungibleTokens []templates.Token + if s.cfg.InitFungibleTokenVaultsOnAccountCreation { + + flowTx, initializedFungibleTokens, err = s.generateCreateAccountTransactionWithEnabledFungibleTokenVaults( + publicKeys, + payer.Address, + ) + if err != nil { + return nil, "", err + } + + } else { + + flowTx, err = flow_templates.CreateAccount( + publicKeys, + nil, + payer.Address, + ) + if err != nil { + return nil, "", err + } + } flowTx. @@ -441,10 +461,61 @@ func (s *ServiceImpl) createAccount(ctx context.Context) (*Account, string, erro } AccountAdded.Trigger(AccountAddedPayload{ - Address: flow.HexToAddress(account.Address), + Address: flow.HexToAddress(account.Address), + InitializedFungibleTokens: initializedFungibleTokens, }) - log.WithFields(log.Fields{"address": account.Address}).Debug("Account created") + log. + WithFields(log.Fields{"address": account.Address, "initialized-fungible-tokens": initializedFungibleTokens}). + Info("Account created") return account, flowTx.ID().String(), nil } + +// generateCreateAccountTransactionWithEnabledFungibleTokenVaults is a helper function that generates a templated +// account creation transaction that initializes all enabled fungible tokens. +func (s *ServiceImpl) generateCreateAccountTransactionWithEnabledFungibleTokenVaults( + publicKeys []*flow.AccountKey, + payerAddress flow.Address, +) ( + *flow.Transaction, + []templates.Token, + error, +) { + // Create custom cadence script to create account and init enabled fungible tokens vaults + tokens, err := s.temps.ListTokensFull(templates.FT) + if err != nil { + return nil, []templates.Token{}, nil + } + + var initializedTokens []templates.Token + tokensInfo := []template_strings.FungibleTokenInfo{} + for _, t := range tokens { + if t.Name != "FlowToken" { + tokensInfo = append(tokensInfo, templates.NewFungibleTokenInfo(t)) + initializedTokens = append(initializedTokens, t) + } + } + + txScript, err := templates.CreateAccountAndInitFungibleTokenVaultsCode(s.cfg.ChainID, tokensInfo) + if err != nil { + return nil, []templates.Token{}, err + } + + // Encode public key list + keyList := make([]cadence.Value, len(publicKeys)) + for i, key := range publicKeys { + keyList[i], err = flow_templates.AccountKeyToCadenceCryptoKey(key) + if err != nil { + return nil, []templates.Token{}, err + } + } + cadencePublicKeys := cadence.NewArray(keyList) + + flowTx := flow.NewTransaction(). + SetScript([]byte(txScript)). + AddAuthorizer(payerAddress). + AddRawArgument(jsoncdc.MustEncode(cadencePublicKeys)) + + return flowTx, initializedTokens, nil +} diff --git a/accounts/service_init.go b/accounts/service_init.go index 139cac12..22dd8397 100644 --- a/accounts/service_init.go +++ b/accounts/service_init.go @@ -55,6 +55,8 @@ func (s *ServiceImpl) InitAdminAccount(ctx context.Context) error { if _, err := s.km.InitAdminProposalKeys(ctx); err != nil { return err } + + log.Info("New admin account proposal keys created successfully") } } else { return err diff --git a/chain_events/listener.go b/chain_events/listener.go index 73257a68..294507fc 100644 --- a/chain_events/listener.go +++ b/chain_events/listener.go @@ -88,9 +88,19 @@ func (l *ListenerImpl) run(ctx context.Context, start, end uint64) error { if err != nil { return err } + count := 0 for _, b := range r { + count += len(b.Events) events = append(events, b.Events...) } + log. + WithFields(log.Fields{ + "type": t, + "startHeight": start, + "endHeight": end, + "resultCount": count, + }). + Debug("Fetching events") } for _, event := range events { diff --git a/configs/configs.go b/configs/configs.go index cab02950..acaac6ac 100644 --- a/configs/configs.go +++ b/configs/configs.go @@ -76,8 +76,9 @@ type Config struct { // -- Templates -- - EnabledTokens []string `env:"ENABLED_TOKENS" envSeparator:","` - ScriptPathCreateAccount string `env:"SCRIPT_PATH_CREATE_ACCOUNT" envDefault:""` + EnabledTokens []string `env:"ENABLED_TOKENS" envSeparator:","` + ScriptPathCreateAccount string `env:"SCRIPT_PATH_CREATE_ACCOUNT" envDefault:""` + InitFungibleTokenVaultsOnAccountCreation bool `env:"INIT_FUNGIBLE_TOKEN_VAULTS_ON_ACCOUNT_CREATION" envDefault:"false"` // -- Workerpool -- @@ -128,7 +129,7 @@ type Config struct { // For more info: https://pkg.go.dev/time#ParseDuration ChainListenerInterval time.Duration `env:"EVENTS_INTERVAL" envDefault:"10s"` - // Max transactions per second, rate at which the service can submit transactions to Flow + // Max transactions per second, rate at which the service can submit transactions to Flow (excluding ops) TransactionMaxSendRate int `env:"MAX_TPS" envDefault:"10"` // maxJobErrorCount is the maximum number of times a Job can be tried to @@ -152,6 +153,12 @@ type Config struct { PauseDuration time.Duration `env:"PAUSE_DURATION" envDefault:"60s"` GrpcMaxCallRecvMsgSize int `env:"GRPC_MAX_CALL_RECV_MSG_SIZE" envDefault:"16777216"` + + // -- ops --- + // WorkerCount for system jobs, max number of in-flight transactions + OpsWorkerCount uint `env:"OPS_WORKER_COUNT" envDefault:"200"` + // Capacity of buffered jobs queues for system jobs. + OpsWorkerQueueCapacity uint `env:"OPS_WORKER_QUEUE_CAPACITY" envDefault:"300000"` } // Parse parses environment variables and flags to a valid Config. diff --git a/configs/configs_test.go b/configs/configs_test.go index 6aa63343..c5959217 100644 --- a/configs/configs_test.go +++ b/configs/configs_test.go @@ -11,6 +11,7 @@ func TestParseConfig(t *testing.T) { t.Setenv("FLOW_WALLET_ENCRYPTION_KEY", "encryption-key") t.Setenv("FLOW_WALLET_ENCRYPTION_KEY_TYPE", "local") t.Setenv("FLOW_WALLET_ACCESS_API_HOST", "access-api-host") + t.Setenv("FLOW_WALLET_WORKER_COUNT", "1") cfg, err := Parse() diff --git a/docker-compose.test-suite.yml b/docker-compose.test-suite.yml index 7baa7512..760117c1 100644 --- a/docker-compose.test-suite.yml +++ b/docker-compose.test-suite.yml @@ -2,6 +2,8 @@ version: "3.9" services: db: image: postgres:13-alpine + ports: + - "5432:5432" environment: POSTGRES_DB: wallet_test POSTGRES_USER: wallet_test diff --git a/docker-compose.yml b/docker-compose.yml index 119f4d78..2f0a4288 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,8 @@ services: POSTGRES_PASSWORD: wallet networks: - private + ports: + - "5432:5432" healthcheck: test: [ @@ -27,7 +29,7 @@ services: redis: image: redis:6.2-alpine - command: redis-server /usr/local/etc/redis/redis.conf + command: redis-server /usr/local/etc/redis/redis.conf --loglevel warning volumes: - ./redis-config/redis.conf:/usr/local/etc/redis/redis.conf - ./redis-config/users.acl:/usr/local/etc/redis/users.acl @@ -50,6 +52,8 @@ services: - emulator-persist:/flowdb env_file: - ./.env + ports: + - "3569:3569" environment: FLOW_SERVICEPRIVATEKEY: ${FLOW_WALLET_ADMIN_PRIVATE_KEY} FLOW_SERVICEKEYSIGALGO: ECDSA_P256 diff --git a/flow_helpers/flow_helpers.go b/flow_helpers/flow_helpers.go index d086251f..2b5d833a 100644 --- a/flow_helpers/flow_helpers.go +++ b/flow_helpers/flow_helpers.go @@ -50,7 +50,7 @@ func WaitForSeal(ctx context.Context, flowClient FlowClient, id flow.Identifier, b := &backoff.Backoff{ Min: 100 * time.Millisecond, - Max: time.Minute, + Max: time.Second, Factor: 5, Jitter: true, } diff --git a/handlers/ops.go b/handlers/ops.go new file mode 100644 index 00000000..a9b7fe4e --- /dev/null +++ b/handlers/ops.go @@ -0,0 +1,27 @@ +package handlers + +import ( + "net/http" + + "github.com/flow-hydraulics/flow-wallet-api/ops" +) + +// Ops is a HTTP server for admin (system) operations. +type Ops struct { + service ops.Service +} + +// NewOps initiates a new ops server. +func NewOps(service ops.Service) *Ops { + return &Ops{service} +} + +// InitMissingFungibleVaults starts the job to initialize missing fungible token vaults. +func (s *Ops) InitMissingFungibleVaults() http.Handler { + return http.HandlerFunc(s.InitMissingFungibleVaultsFunc) +} + +// GetMissingFungibleVaults returns number of accounts that are missing a configured fungible token vault. +func (s *Ops) GetMissingFungibleVaults() http.Handler { + return http.HandlerFunc(s.GetMissingFungibleVaultsFunc) +} diff --git a/handlers/ops_func.go b/handlers/ops_func.go new file mode 100644 index 00000000..accb2fb9 --- /dev/null +++ b/handlers/ops_func.go @@ -0,0 +1,29 @@ +package handlers + +import ( + "net/http" +) + +// InitMissingFungibleVaultsFunc starts job to init missing fungible token vaults. +func (s *Ops) InitMissingFungibleVaultsFunc(rw http.ResponseWriter, r *http.Request) { + + result, err := s.service.InitMissingFungibleTokenVaults() + if err != nil { + handleError(rw, r, err) + return + } + + handleJsonResponse(rw, http.StatusOK, result) +} + +// GetMissingFungibleVaultsFunc returns number of accounts with missing fungible token vaults. +func (s *Ops) GetMissingFungibleVaultsFunc(rw http.ResponseWriter, r *http.Request) { + + result, err := s.service.GetMissingFungibleTokenVaults() + if err != nil { + handleError(rw, r, err) + return + } + + handleJsonResponse(rw, http.StatusOK, result) +} diff --git a/main.go b/main.go index 1ba5e5da..92480a4e 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "github.com/flow-hydraulics/flow-wallet-api/jobs" "github.com/flow-hydraulics/flow-wallet-api/keys" "github.com/flow-hydraulics/flow-wallet-api/keys/basic" + "github.com/flow-hydraulics/flow-wallet-api/ops" "github.com/flow-hydraulics/flow-wallet-api/system" "github.com/flow-hydraulics/flow-wallet-api/templates" "github.com/flow-hydraulics/flow-wallet-api/tokens" @@ -125,11 +126,15 @@ func runServer(cfg *configs.Config) { km := basic.NewKeyManager(cfg, keys.NewGormStore(db), fc) // Services - templateService := templates.NewService(cfg, templates.NewGormStore(db)) + templateService, err := templates.NewService(cfg, templates.NewGormStore(db)) + if err != nil { + log.Fatal(err) + } jobsService := jobs.NewService(jobs.NewGormStore(db)) transactionService := transactions.NewService(cfg, transactions.NewGormStore(db), km, fc, wp, transactions.WithTxRatelimiter(txRatelimiter)) - accountService := accounts.NewService(cfg, accounts.NewGormStore(db), km, fc, wp, transactionService, accounts.WithTxRatelimiter(txRatelimiter)) + accountService := accounts.NewService(cfg, accounts.NewGormStore(db), km, fc, wp, transactionService, templateService, accounts.WithTxRatelimiter(txRatelimiter)) tokenService := tokens.NewService(cfg, tokens.NewGormStore(db), km, fc, wp, transactionService, templateService, accountService) + opsService := ops.NewService(cfg, ops.NewGormStore(db), templateService, transactionService, tokenService) // Register a handler for account added events accounts.AccountAdded.Register(&tokens.AccountAddedHandler{ @@ -152,6 +157,7 @@ func runServer(cfg *configs.Config) { accountHandler := handlers.NewAccounts(accountService) transactionHandler := handlers.NewTransactions(transactionService) tokenHandler := handlers.NewTokens(tokenService) + opsHandler := handlers.NewOps(opsService) r := mux.NewRouter() @@ -241,6 +247,10 @@ func runServer(cfg *configs.Config) { log.Info("non-fungible tokens disabled") } + // Ops + rv.Handle("/ops/missing-fungible-token-vaults/start", opsHandler.InitMissingFungibleVaults()).Methods(http.MethodGet) // start retroactive init job + rv.Handle("/ops/missing-fungible-token-vaults/stats", opsHandler.GetMissingFungibleVaults()).Methods(http.MethodGet) // get number of accounts with missing fungible token vaults + h := http.TimeoutHandler(r, cfg.ServerRequestTimeout, "request timed out") h = handlers.UseCors(h) h = handlers.UseLogging(h) @@ -322,11 +332,11 @@ func runServer(cfg *configs.Config) { return nil, err } - token_count := len(*tt) + token_count := len(tt) event_types := make([]string, token_count) // Listen for enabled tokens deposit events - for i, token := range *tt { + for i, token := range tt { event_types[i] = templates.DepositEventTypeFromToken(token) } diff --git a/main_test.go b/main_test.go index e224f2e6..b94f5324 100644 --- a/main_test.go +++ b/main_test.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" + "os" "sort" "net/http" @@ -214,7 +214,7 @@ func TestAccountServices(t *testing.T) { fatal(t, err) if len(acc.Keys) != int(cfg2.DefaultAccountKeyCount) { - t.Fatalf("incorrect number of keys for new account, expected %d, got %d", len(acc.Keys), cfg2.DefaultAccountKeyCount) + t.Fatalf("incorrect number of keys for new account, expected %d, got %d", cfg2.DefaultAccountKeyCount, len(acc.Keys)) } // Keys should be clones w/ a running Index @@ -369,7 +369,8 @@ func TestAccountTransactionHandlers(t *testing.T) { token, err := templateSvc.GetTokenByName("FlowToken") fatal(t, err) - tFlow := templates.FungibleTransferCode(cfg.ChainID, token) + tFlow, err := templates.FungibleTransferCode(cfg.ChainID, token) + fatal(t, err) tFlowBytes, err := json.Marshal(tFlow) fatal(t, err) @@ -608,7 +609,8 @@ func TestTransactionHandlers(t *testing.T) { token, err := templateSvc.GetTokenByName("FlowToken") fatal(t, err) - transferFlow := templates.FungibleTransferCode(cfg.ChainID, token) + transferFlow, err := templates.FungibleTransferCode(cfg.ChainID, token) + fatal(t, err) _, transaction1, err := svc.Create( context.Background(), @@ -861,9 +863,7 @@ func TestTokenServices(t *testing.T) { // Make sure FUSD is deployed err := svc.DeployTokenContractForAccount(ctx, true, tokenName, cfg.AdminAddress) if err != nil { - if !strings.Contains(err.Error(), "cannot overwrite existing contract") { - t.Fatal(err) - } + t.Fatal(err) } // Setup the admin account to be able to handle FUSD @@ -886,7 +886,7 @@ func TestTokenServices(t *testing.T) { t.Errorf("expected %s but got %s", transactions.FtSetup, setupTx.TransactionType) } - // Create a withdrawal, should error as we can not mint FUSD right now + // Create a withdrawal _, _, err = svc.CreateWithdrawal( ctx, true, @@ -897,12 +897,7 @@ func TestTokenServices(t *testing.T) { FtAmount: "1.0", }, ) - if err != nil { - if !strings.Contains(err.Error(), "Amount withdrawn must be less than or equal than the balance of the Vault") { - t.Fatal(err) - } - } - + fatal(t, err) }) t.Run(("try to setup a non-existent token"), func(t *testing.T) { @@ -920,6 +915,40 @@ func TestTokenServices(t *testing.T) { t.Fatal("expected an error") } }) + + t.Run("init fungible token vaults on account creation", func(t *testing.T) { + cfg := test.LoadConfig(t) + cfg.InitFungibleTokenVaultsOnAccountCreation = true + app := test.GetServices(t, cfg) + + svc := app.GetTokens() + accountSvc := app.GetAccounts() + + ctx := context.Background() + + // Make sure FUSD is deployed + err := svc.DeployTokenContractForAccount(ctx, true, "FUSD", cfg.AdminAddress) + if err != nil { + t.Fatal(err) + } + + // Create an account + _, account, err := accountSvc.Create(ctx, true) + fatal(t, err) + + // Create a withdrawal + _, _, err = svc.CreateWithdrawal( + ctx, + true, + cfg.AdminAddress, + tokens.WithdrawalRequest{ + TokenName: "FUSD", + Recipient: account.Address, + FtAmount: "1.0", + }, + ) + fatal(t, err) + }) } func TestTokenHandlers(t *testing.T) { @@ -964,24 +993,20 @@ func TestTokenHandlers(t *testing.T) { // Make sure FUSD is deployed err = svc.DeployTokenContractForAccount(context.Background(), true, fusd.Name, fusd.Address) - if err != nil { - if !strings.Contains(err.Error(), "cannot overwrite existing contract") { - fatal(t, err) - } - } + fatal(t, err) // ExampleNFT - setupBytes, err := ioutil.ReadFile(filepath.Join(testCadenceTxBasePath, "setup_exampleNFT.cdc")) + setupBytes, err := os.ReadFile(filepath.Join(testCadenceTxBasePath, "setup_exampleNFT.cdc")) fatal(t, err) - transferBytes, err := ioutil.ReadFile(filepath.Join(testCadenceTxBasePath, "transfer_exampleNFT.cdc")) + transferBytes, err := os.ReadFile(filepath.Join(testCadenceTxBasePath, "transfer_exampleNFT.cdc")) fatal(t, err) - balanceBytes, err := ioutil.ReadFile(filepath.Join(testCadenceTxBasePath, "balance_exampleNFT.cdc")) + balanceBytes, err := os.ReadFile(filepath.Join(testCadenceTxBasePath, "balance_exampleNFT.cdc")) fatal(t, err) - mintBytes, err := ioutil.ReadFile(filepath.Join(testCadenceTxBasePath, "mint_exampleNFT.cdc")) + mintBytes, err := os.ReadFile(filepath.Join(testCadenceTxBasePath, "mint_exampleNFT.cdc")) fatal(t, err) exampleNft := templates.Token{ @@ -999,11 +1024,7 @@ func TestTokenHandlers(t *testing.T) { // Make sure ExampleNFT is deployed err = svc.DeployTokenContractForAccount(context.Background(), true, exampleNft.Name, exampleNft.Address) - if err != nil { - if !strings.Contains(err.Error(), "cannot overwrite existing contract") { - fatal(t, err) - } - } + fatal(t, err) // Create a few accounts testAccounts := make([]*accounts.Account, 2) @@ -1092,7 +1113,8 @@ func TestTokenHandlers(t *testing.T) { } // Mint ExampleNFTs for account 0 - mintCode := templates.TokenCode(cfg.ChainID, &exampleNft, string(mintBytes)) + mintCode, err := templates.TokenCode(cfg.ChainID, &exampleNft, string(mintBytes)) + fatal(t, err) for i := 0; i < 3; i++ { _, _, err := transactionSvc.Create(context.Background(), true, cfg.AdminAddress, mintCode, []transactions.Argument{cadence.NewAddress(flow.HexToAddress(testAccounts[0].Address))}, @@ -1425,9 +1447,7 @@ func TestNFTDeployment(t *testing.T) { err := svc.DeployTokenContractForAccount(context.Background(), true, "ExampleNFT", cfg.AdminAddress) if err != nil { - if !strings.Contains(err.Error(), "cannot overwrite existing contract") { - t.Fatal(err) - } + t.Fatal(err) } } @@ -1651,3 +1671,63 @@ func TestTemplateService(t *testing.T) { }) } + +func TestOpsServices(t *testing.T) { + cfg := test.LoadConfig(t) + app := test.GetServices(t, cfg) + + svc := app.GetOps() + accountSvc := app.GetAccounts() + + // Create an account + _, _, err := accountSvc.Create(context.Background(), true) + fatal(t, err) + + // Create another account + _, _, err = accountSvc.Create(context.Background(), true) + fatal(t, err) + + t.Run("get number of accounts with missing fungible vaults", func(t *testing.T) { + // Get missing vault count + result, err := svc.GetMissingFungibleTokenVaults() + fatal(t, err) + + if len(result) != 1 { + t.Errorf("GetMissingFungibleTokenVaults returns incorrect count: %d", len(result)) + } + + // 3 accounts with missing FUSD vault -> admin, account1, account2 + if result[0].TokenName != "FUSD" || result[0].Count != 3 { + t.Errorf("invalid GetMissingFungibleTokenVaults results: %+v", result) + } + }) + + t.Run("init missing fungible vaults job", func(t *testing.T) { + // Get missing vault count + result, err := svc.GetMissingFungibleTokenVaults() + fatal(t, err) + + if len(result) != 1 { + t.Errorf("GetMissingFungibleTokenVaults returns incorrect count: %d", len(result)) + } + + if result[0].TokenName != "FUSD" || result[0].Count == 0 { + t.Errorf("invalid GetMissingFungibleTokenVaults results: %+v", result) + } + + _, err = svc.InitMissingFungibleTokenVaults() + fatal(t, err) + + // Check count until 0 + for { + time.Sleep(1 * time.Second) + + result, err = svc.GetMissingFungibleTokenVaults() + fatal(t, err) + + if result[0].TokenName == "FUSD" && result[0].Count == 0 { + break + } + } + }) +} diff --git a/migrations/internal/m20221001/migration.go b/migrations/internal/m20221001/migration.go new file mode 100644 index 00000000..e97c9386 --- /dev/null +++ b/migrations/internal/m20221001/migration.go @@ -0,0 +1,61 @@ +package m20221001 + +import ( + "gorm.io/gorm" +) + +const ID = "20221001" + +type TokenType int + +const ( + NotSpecified TokenType = iota + FT + NFT +) + +type Token struct { + ID uint64 `json:"id,omitempty"` + Name string `json:"name" gorm:"uniqueIndex;not null"` // Declaration name + NameLowerCase string `json:"nameLowerCase,omitempty"` // (deprecated) For generic fungible token transaction templates + ReceiverPublicPath string `json:"receiverPublicPath,omitempty"` + BalancePublicPath string `json:"balancePublicPath,omitempty"` + VaultStoragePath string `json:"vaultStoragePath,omitempty"` + Address string `json:"address" gorm:"not null"` + Setup string `json:"setup,omitempty"` // Setup cadence code + Transfer string `json:"transfer,omitempty"` // Transfer cadence code + Balance string `json:"balance,omitempty"` // Balance cadence code + Type TokenType `json:"type"` +} + +func Migrate(tx *gorm.DB) error { + if err := tx.Migrator().AddColumn(&Token{}, "receiver_public_path"); err != nil { + return err + } + + if err := tx.Migrator().AddColumn(&Token{}, "balance_public_path"); err != nil { + return err + } + + if err := tx.Migrator().AddColumn(&Token{}, "vault_storage_path"); err != nil { + return err + } + + return nil +} + +func Rollback(tx *gorm.DB) error { + if err := tx.Migrator().DropColumn(&Token{}, "receiver_public_path"); err != nil { + return err + } + + if err := tx.Migrator().DropColumn(&Token{}, "balance_public_path"); err != nil { + return err + } + + if err := tx.Migrator().DropColumn(&Token{}, "vault_storage_path"); err != nil { + return err + } + + return nil +} diff --git a/migrations/migrations.go b/migrations/migrations.go index 19f724f6..10c63443 100644 --- a/migrations/migrations.go +++ b/migrations/migrations.go @@ -11,6 +11,7 @@ import ( "github.com/flow-hydraulics/flow-wallet-api/migrations/internal/m20211221_1" "github.com/flow-hydraulics/flow-wallet-api/migrations/internal/m20211221_2" "github.com/flow-hydraulics/flow-wallet-api/migrations/internal/m20220212" + "github.com/flow-hydraulics/flow-wallet-api/migrations/internal/m20221001" "github.com/go-gormigrate/gormigrate/v2" ) @@ -66,6 +67,11 @@ func List() []*gormigrate.Migration { Migrate: m20220212.Migrate, Rollback: m20220212.Rollback, }, + { + ID: m20221001.ID, + Migrate: m20221001.Migrate, + Rollback: m20221001.Rollback, + }, } return ms } diff --git a/openapi.yml b/openapi.yml index 389ea30e..5e79d990 100644 --- a/openapi.yml +++ b/openapi.yml @@ -34,6 +34,8 @@ tags: description: View the status of asynchronous tasks being completed by the Wallet API. - name: Watchlist description: View info for non-custodial accounts of interest. + - name: Ops + description: System operations and admin jobs. paths: /debug: get: @@ -898,6 +900,37 @@ paths: responses: '200': description: OK + '/ops/missing-fungible-token-vaults/stats': + get: + summary: Returns number of uninitialized accounts per enabled fungible token. + description: Returns number of uninitialized accounts per enabled fungible token based on the database. You can also use this endpoint to check the init job progress. + operationId: getMissingFungibleTokenVaults + tags: + - Ops + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/tokenCount' + '/ops/missing-fungible-token-vaults/start': + get: + summary: Starts the fungible token vault initialization job. + description: Starts the fungible token vault initialization job. The job is singleton. + operationId: initMissingFungibleTokenVaults + tags: + - Ops + responses: + '200': + description: OK + content: + text/plain: + schema: + type: string + components: schemas: jobState: @@ -1179,6 +1212,16 @@ components: nameLowerCase: type: string example: fusd + deprecated: true + receiverPublicPath: + type: string + example: /public/fusdReceiver + balancePublicPath: + type: string + example: /public/fusdBalance + vaultStoragePath: + type: string + example: /storage/fusdVault address: type: string example: '0xf8d6e0586b0a20c7' @@ -1460,6 +1503,14 @@ components: - google_kms example: local minLength: 1 + tokenCount: + type: object + properties: + token: + type: string + example: FUSD + count: + type: integer parameters: limit: name: limit diff --git a/ops/fungible_vaults.go b/ops/fungible_vaults.go new file mode 100644 index 00000000..9cd79755 --- /dev/null +++ b/ops/fungible_vaults.go @@ -0,0 +1,139 @@ +package ops + +import ( + "context" + "fmt" + "sync" + + "github.com/flow-hydraulics/flow-wallet-api/templates" + "github.com/flow-hydraulics/flow-wallet-api/templates/template_strings" + "github.com/flow-hydraulics/flow-wallet-api/transactions" + log "github.com/sirupsen/logrus" +) + +type TokenCount struct { + TokenName string `json:"token"` + Count uint `json:"count"` +} + +// GetMissingFungibleTokenVaults returns number of accounts that are missing a configured fungible token vault. +func (s *ServiceImpl) GetMissingFungibleTokenVaults() ([]TokenCount, error) { + + tokens, err := s.temps.ListTokensFull(templates.FT) + if err != nil { + return nil, err + } + + var result []TokenCount + for _, t := range tokens { + if t.Name != "FlowToken" { + accounts, err := s.store.ListAccountsWithMissingVault(t.Name) + if err != nil { + return nil, err + } + result = append(result, TokenCount{ + TokenName: t.Name, + Count: uint(len(*accounts)), + }) + } + } + + return result, nil +} + +// InitMissingFungibleTokenVaults starts job to init missing fungible token vaults. +func (s *ServiceImpl) InitMissingFungibleTokenVaults() (string, error) { + if s.initFungibleJobRunning { + return "Job is already running", nil + } + + s.initFungibleJobRunning = true + + log.Info("Starting new fungible token vault init job") + + tokens, err := s.temps.ListTokensFull(templates.FT) + if err != nil { + return "", err + } + + // mapping of user account address to list of fungible tokens that needs to be initialized + accountsMap := make(map[string][]string) + // token name -> FungibleTokenInfo for templating + tokenInfoMap := make(map[string]template_strings.FungibleTokenInfo) + for _, t := range tokens { + if t.Name != "FlowToken" { + tokenInfoMap[t.Name] = templates.NewFungibleTokenInfo(t) + + accounts, err := s.store.ListAccountsWithMissingVault(t.Name) + if err != nil { + return "", err + } + for _, a := range *accounts { + accountsMap[a.Address] = append(accountsMap[a.Address], t.Name) + } + } + } + + log.Infof("Number of accounts for vault init job: %d", len(accountsMap)) + + var txWg sync.WaitGroup + + for address, tokenList := range accountsMap { + + txWg.Add(1) + + // create a new job for each account to init token vaults + s.wp.AddFungibleInitJob( + OpsInitFungibleVaultsJob{ + Address: address, + TokenList: tokenList, + Func: func(address string, tokenList []string) error { + + defer txWg.Done() + + tList := []template_strings.FungibleTokenInfo{} + for _, t := range tokenList { + tList = append(tList, tokenInfoMap[t]) + } + + txScript, err := templates.InitFungibleTokenVaultsCode(s.cfg.ChainID, tList) + if err != nil { + return err + } + + // blocks until transaction is sealed + _, tx, err := s.txs.Create(context.Background(), true, address, txScript, nil, transactions.FtSetup) + if err != nil { + return err + } + + for _, t := range tokenList { + err := s.tokens.AddAccountToken(t, address) + if err != nil { + log.Errorf("Error adding AccountToken to store: %s", err) + } + } + + log. + WithFields(log.Fields{ + "account": address, + "txId": tx.TransactionId, + "tokens": tokenList, + }). + Info("Fungible token vaults initialized.") + + return nil + }, + }, + ) + } + + // unlock after all transactions are sealed + go func() { + txWg.Wait() + s.initFungibleJobRunning = false + log.Info("Fungible token vault init job finished") + }() + + return fmt.Sprintf("Job started! Accounts: %d, Workers: %d", len(accountsMap), s.wp.NumWorkers()), nil +} diff --git a/ops/service.go b/ops/service.go new file mode 100644 index 00000000..bddd9438 --- /dev/null +++ b/ops/service.go @@ -0,0 +1,50 @@ +package ops + +import ( + "github.com/flow-hydraulics/flow-wallet-api/configs" + "github.com/flow-hydraulics/flow-wallet-api/templates" + "github.com/flow-hydraulics/flow-wallet-api/tokens" + "github.com/flow-hydraulics/flow-wallet-api/transactions" +) + +// Service lists all functionality provided by ops service +type Service interface { + // Retroactive fungible token vault initialization + GetMissingFungibleTokenVaults() ([]TokenCount, error) + InitMissingFungibleTokenVaults() (string, error) + GetWorkerPool() OpsWorkerPoolService +} + +// ServiceImpl implements the ops Service +type ServiceImpl struct { + cfg *configs.Config + store Store + temps templates.Service + txs transactions.Service + tokens tokens.Service + wp OpsWorkerPoolService + + initFungibleJobRunning bool +} + +// NewService initiates a new ops service. +func NewService( + cfg *configs.Config, + store Store, + temps templates.Service, + txs transactions.Service, + tokens tokens.Service, +) Service { + + wp := NewWorkerPool( + cfg.OpsWorkerCount, + cfg.OpsWorkerQueueCapacity, + ) + wp.Start() + + return &ServiceImpl{cfg, store, temps, txs, tokens, wp, false} +} + +func (s *ServiceImpl) GetWorkerPool() OpsWorkerPoolService { + return s.wp +} diff --git a/ops/store.go b/ops/store.go new file mode 100644 index 00000000..37c03cb9 --- /dev/null +++ b/ops/store.go @@ -0,0 +1,10 @@ +package ops + +import ( + "github.com/flow-hydraulics/flow-wallet-api/accounts" +) + +// Store defines what ops needs from the database +type Store interface { + ListAccountsWithMissingVault(tokenName string) (*[]accounts.Account, error) +} diff --git a/ops/store_gorm.go b/ops/store_gorm.go new file mode 100644 index 00000000..03981528 --- /dev/null +++ b/ops/store_gorm.go @@ -0,0 +1,35 @@ +package ops + +import ( + "github.com/flow-hydraulics/flow-wallet-api/accounts" + "github.com/flow-hydraulics/flow-wallet-api/templates" + "github.com/flow-hydraulics/flow-wallet-api/tokens" + "gorm.io/gorm" +) + +type GormStore struct { + db *gorm.DB +} + +func NewGormStore(db *gorm.DB) Store { + return &GormStore{db} +} + +// ListAccountsWithMissingVault lists accounts that are not initialized for the given token name. +func (s *GormStore) ListAccountsWithMissingVault(tokenName string) (res *[]accounts.Account, err error) { + + // https://www.db-fiddle.com/f/hHoZ5P4FDDj3sVkRRiRMc2/0 + err = s.db. + Where( + "not exists (?)", + s.db. + Model(&tokens.AccountToken{}). + Where(&tokens.AccountToken{ + TokenName: tokenName, + TokenType: templates.FT, + }). + Where("account_address=address")). + Find(&res).Error + + return +} diff --git a/ops/workerpool.go b/ops/workerpool.go new file mode 100644 index 00000000..86bf66fb --- /dev/null +++ b/ops/workerpool.go @@ -0,0 +1,72 @@ +package ops + +import ( + "sync" + + log "github.com/sirupsen/logrus" +) + +type OpsInitFungibleVaultsJobFunc func(address string, tokenList []string) error +type OpsInitFungibleVaultsJob struct { + Func OpsInitFungibleVaultsJobFunc + Address string + TokenList []string +} + +type OpsWorkerPoolService interface { + Start() + Stop() + AddFungibleInitJob(job OpsInitFungibleVaultsJob) + NumWorkers() uint +} + +type workerPoolImpl struct { + numWorkers uint + capacity uint + + fungibleInitJobChan chan OpsInitFungibleVaultsJob + workersWaitGroup *sync.WaitGroup +} + +func NewWorkerPool( + numWorkers uint, + capacity uint, +) OpsWorkerPoolService { + return &workerPoolImpl{ + numWorkers: numWorkers, + capacity: capacity, + fungibleInitJobChan: make(chan OpsInitFungibleVaultsJob, capacity), + workersWaitGroup: &sync.WaitGroup{}, + } +} + +func (p *workerPoolImpl) Start() { + for i := uint(0); i < p.numWorkers; i++ { + p.workersWaitGroup.Add(1) + go func() { + defer p.workersWaitGroup.Done() + for job := range p.fungibleInitJobChan { + if job.Func == nil { + break + } + + if err := job.Func(job.Address, job.TokenList); err != nil { + log.Warnf("Error running ops job: %s", err) + } + } + }() + } +} + +func (p *workerPoolImpl) Stop() { + close(p.fungibleInitJobChan) + p.workersWaitGroup.Wait() +} + +func (p *workerPoolImpl) AddFungibleInitJob(job OpsInitFungibleVaultsJob) { + p.fungibleInitJobChan <- job +} + +func (p *workerPoolImpl) NumWorkers() uint { + return p.numWorkers +} diff --git a/templates/init_vars.go b/templates/init_vars.go index 8c0f6536..8fb9a246 100644 --- a/templates/init_vars.go +++ b/templates/init_vars.go @@ -2,20 +2,19 @@ package templates import "github.com/onflow/flow-go-sdk" -func init() { - t := make(templateVariables, 2) - - t["FungibleToken.cdc"] = knownAddresses{ +var KnownAddresses = templateVariables{ + "FungibleToken.cdc": knownAddresses{ flow.Emulator: "0xee82856bf20e2aa6", flow.Testnet: "0x9a0766d93b6608b7", flow.Mainnet: "0xf233dcee88fe0abe", - } - - t["NonFungibleToken.cdc"] = knownAddresses{ + }, + "NonFungibleToken.cdc": knownAddresses{ flow.Emulator: "0xf8d6e0586b0a20c7", flow.Testnet: "0x631e88ae7f1d7c20", flow.Mainnet: "0x1d7e57aa55817448", - } + }, +} - knownAddressesReplacers = makeReplacers(t) +func init() { + knownAddressesReplacers = makeReplacers(KnownAddresses) } diff --git a/templates/service.go b/templates/service.go index e0496171..67f04927 100644 --- a/templates/service.go +++ b/templates/service.go @@ -12,7 +12,8 @@ import ( type Service interface { AddToken(t *Token) error - ListTokens(tType TokenType) (*[]BasicToken, error) + ListTokens(tType TokenType) ([]BasicToken, error) + ListTokensFull(tType TokenType) ([]Token, error) GetTokenById(id uint64) (*Token, error) GetTokenByName(name string) (*Token, error) RemoveToken(id uint64) error @@ -29,17 +30,28 @@ func parseEnabledTokens(envEnabledTokens []string) map[string]Token { for _, s := range envEnabledTokens { ss := strings.Split(s, ":") token := Token{Name: ss[0], Address: ss[1]} - if len(ss) > 2 { + if len(ss) == 3 { + // Deprecated + if token.Name != "FlowToken" && token.Name != "FUSD" { + log.Warnf("ENABLED_TOKENS.%s is using deprecated config format: %s", ss[0], s) + } token.NameLowerCase = ss[2] + token.ReceiverPublicPath = fmt.Sprintf("/public/%sReceiver", token.NameLowerCase) + token.BalancePublicPath = fmt.Sprintf("/public/%sBalance", token.NameLowerCase) + token.VaultStoragePath = fmt.Sprintf("/storage/%sVault", token.NameLowerCase) + } else if len(ss) == 5 { + token.ReceiverPublicPath = ss[2] + token.BalancePublicPath = ss[3] + token.VaultStoragePath = ss[4] } - // Use all lowercase as the key so we can do case insenstive matchig in URLs + // Use all lowercase as the key so we can do case-insensitive matching in URLs key := strings.ToLower(ss[0]) enabledTokens[key] = token } return enabledTokens } -func NewService(cfg *configs.Config, store Store) Service { +func NewService(cfg *configs.Config, store Store) (Service, error) { // TODO(latenssi): safeguard against nil config? // Add all enabled tokens from config as fungible tokens @@ -53,22 +65,36 @@ func NewService(cfg *configs.Config, store Store) Service { } else { if !strings.Contains(err.Error(), "record not found") { // We got an error that is not "record not found" - panic(err) + return nil, err } } // Copy the value so we get an individual pointer, this is important token := t token.Type = FT // We only allow fungible tokens through env variables config - token.Setup = FungibleSetupCode(cfg.ChainID, &token) - token.Transfer = FungibleTransferCode(cfg.ChainID, &token) - token.Balance = FungibleBalanceCode(cfg.ChainID, &token) + + var err error + + token.Setup, err = FungibleSetupCode(cfg.ChainID, &token) + if err != nil { + return nil, err + } + + token.Transfer, err = FungibleTransferCode(cfg.ChainID, &token) + if err != nil { + return nil, err + } + + token.Balance, err = FungibleBalanceCode(cfg.ChainID, &token) + if err != nil { + return nil, err + } // Write to temp storage (memory), instead of database store.InsertTemp(&token) } - return &ServiceImpl{store, cfg} + return &ServiceImpl{store, cfg}, nil } func (s *ServiceImpl) AddToken(t *Token) error { @@ -85,17 +111,32 @@ func (s *ServiceImpl) AddToken(t *Token) error { } // Received code templates may have values that need replacing - t.Setup = TokenCode(s.cfg.ChainID, t, t.Setup) - t.Transfer = TokenCode(s.cfg.ChainID, t, t.Transfer) - t.Balance = TokenCode(s.cfg.ChainID, t, t.Balance) + t.Setup, err = TokenCode(s.cfg.ChainID, t, t.Setup) + if err != nil { + return err + } + + t.Transfer, err = TokenCode(s.cfg.ChainID, t, t.Transfer) + if err != nil { + return err + } + + t.Balance, err = TokenCode(s.cfg.ChainID, t, t.Balance) + if err != nil { + return err + } return s.store.Insert(t) } -func (s *ServiceImpl) ListTokens(tType TokenType) (*[]BasicToken, error) { +func (s *ServiceImpl) ListTokens(tType TokenType) ([]BasicToken, error) { return s.store.List(tType) } +func (s *ServiceImpl) ListTokensFull(tType TokenType) ([]Token, error) { + return s.store.ListFull(tType) +} + func (s *ServiceImpl) GetTokenById(id uint64) (*Token, error) { return s.store.GetById(id) } diff --git a/templates/store.go b/templates/store.go index bfb6ba09..1e554cbe 100644 --- a/templates/store.go +++ b/templates/store.go @@ -3,7 +3,8 @@ package templates // Store manages data regarding templates. type Store interface { Insert(*Token) error - List(TokenType) (*[]BasicToken, error) + List(TokenType) ([]BasicToken, error) + ListFull(TokenType) ([]Token, error) GetById(id uint64) (*Token, error) GetByName(name string) (*Token, error) Remove(id uint64) error diff --git a/templates/store_gorm.go b/templates/store_gorm.go index ae856332..b58e4280 100644 --- a/templates/store_gorm.go +++ b/templates/store_gorm.go @@ -20,17 +20,29 @@ func (s *GormStore) Insert(q *Token) error { return s.db.Omit("ID").Create(q).Error } -func (s *GormStore) List(tType TokenType) (*[]BasicToken, error) { - var err error +func (s *GormStore) List(tType TokenType) ([]BasicToken, error) { + listFull, err := s.ListFull(tType) + if err != nil { + return nil, err + } + + result := make([]BasicToken, 0, len(listFull)) + for _, t := range listFull { + result = append(result, t.BasicToken()) + } + + return result, nil +} - fromTemp := make([]BasicToken, 0, len(s.tempStore)) +func (s *GormStore) ListFull(tType TokenType) ([]Token, error) { + fromTemp := make([]Token, 0, len(s.tempStore)) for _, t := range s.tempStore { if tType == NotSpecified || t.Type == tType { - fromTemp = append(fromTemp, t.BasicToken()) + fromTemp = append(fromTemp, *t) } } - fromDB := []BasicToken{} + fromDB := []Token{} q := s.db.Model(&Token{}) @@ -39,15 +51,14 @@ func (s *GormStore) List(tType TokenType) (*[]BasicToken, error) { q = q.Where(&Token{Type: tType}) } - err = q.Find(&fromDB).Error - + err := q.Find(&fromDB).Error if err != nil { return nil, err } result := append(fromDB, fromTemp...) - return &result, nil + return result, nil } func (s *GormStore) GetById(id uint64) (*Token, error) { diff --git a/templates/template_strings/batched_fungible_token.go b/templates/template_strings/batched_fungible_token.go new file mode 100644 index 00000000..b9af4943 --- /dev/null +++ b/templates/template_strings/batched_fungible_token.go @@ -0,0 +1,102 @@ +package template_strings + +import ( + "bytes" + "text/template" +) + +type BatchedFungibleOpsInfo struct { + FungibleTokenContractAddress string + Tokens []FungibleTokenInfo +} + +type FungibleTokenInfo struct { + ContractName string + Address string + VaultStoragePath string + ReceiverPublicPath string + BalancePublicPath string +} + +func AddFungibleTokenVaultBatchTransaction(i BatchedFungibleOpsInfo) (string, error) { + return executeTemplate("AddFungibleTokens", AddFungibleTokenVaultBatchTransactionTemplate, i) +} + +func CreateAccountAndSetupTransaction(i BatchedFungibleOpsInfo) (string, error) { + return executeTemplate("CreateAccount", CreateAccountAndSetupTransactionTemplate, i) +} + +const CreateAccountAndSetupTransactionTemplate = ` +import Crypto +import FungibleToken from {{ .FungibleTokenContractAddress }} +{{ range .Tokens }} +import {{ .ContractName }} from {{ .Address }} +{{ end }} + +transaction(publicKeys: [Crypto.KeyListEntry]) { + prepare(signer: AuthAccount) { + let account = AuthAccount(payer: signer) + + // add all the keys to the account + for key in publicKeys { + account.keys.add(publicKey: key.publicKey, hashAlgorithm: key.hashAlgorithm, weight: key.weight) + } + + {{ range .Tokens }} + // initializing vault for {{ .ContractName }} + account.save(<-{{ .ContractName }}.createEmptyVault(), to: {{ .VaultStoragePath }}) + account.link<&{{ .ContractName }}.Vault{FungibleToken.Receiver}>( + {{ .ReceiverPublicPath }}, + target: {{ .VaultStoragePath }} + ) + account.link<&{{ .ContractName }}.Vault{FungibleToken.Balance}>( + {{ .BalancePublicPath }}, + target: {{ .VaultStoragePath }} + ) + {{ end }} + } +} +` + +const AddFungibleTokenVaultBatchTransactionTemplate = ` +import FungibleToken from {{ .FungibleTokenContractAddress }} +{{ range .Tokens }} +import {{ .ContractName }} from {{ .Address }} +{{ end }} + +transaction() { + prepare(account: AuthAccount) { + {{ range .Tokens }} + // initializing vault for {{ .ContractName }} + if account.borrow<&{{ .ContractName }}.Vault>(from: {{ .VaultStoragePath }}) == nil { + account.save(<-{{ .ContractName }}.createEmptyVault(), to: {{ .VaultStoragePath }}) + account.link<&{{ .ContractName }}.Vault{FungibleToken.Receiver}>( + {{ .ReceiverPublicPath }}, + target: {{ .VaultStoragePath }} + ) + account.link<&{{ .ContractName }}.Vault{FungibleToken.Balance}>( + {{ .BalancePublicPath }}, + target: {{ .VaultStoragePath }} + ) + } + {{ end }} + } +} +` + +func executeTemplate(name string, temp string, i BatchedFungibleOpsInfo) (string, error) { + template, err := template. + New(name). + Parse(temp) + if err != nil { + return "", err + } + + buf := new(bytes.Buffer) + err = template.Execute(buf, i) + if err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/templates/template_strings/batched_fungible_token_test.go b/templates/template_strings/batched_fungible_token_test.go new file mode 100644 index 00000000..cd692848 --- /dev/null +++ b/templates/template_strings/batched_fungible_token_test.go @@ -0,0 +1,84 @@ +package template_strings + +import ( + "fmt" + "strings" + "testing" +) + +var tokens = BatchedFungibleOpsInfo{ + FungibleTokenContractAddress: "0xFungibleTokenContractAddress", + Tokens: []FungibleTokenInfo{ + { + ContractName: "TokenA", + Address: "0x1", + VaultStoragePath: "TokenA.VaultStoragePath", + ReceiverPublicPath: "TokenA.VaultReceiverPubPath", + BalancePublicPath: "TokenA.VaultBalancePubPath", + }, + { + ContractName: "TokenB", + Address: "0x2", + VaultStoragePath: "/storage/tokenBVault", + ReceiverPublicPath: "/public/tokenBReceiver", + BalancePublicPath: "/public/tokenBBalance", + }, + }, +} + +func TestAccountCreation(t *testing.T) { + result, err := CreateAccountAndSetupTransaction(tokens) + if err != nil { + t.Error(err) + } + + checkStrings := []string{ + "import FungibleToken from 0xFungibleTokenContractAddress", + "import TokenA from 0x1", + "import TokenB from 0x2", + "account.save(<-TokenA.createEmptyVault(), to: TokenA.VaultStoragePath)", + "target: TokenA.VaultStoragePath", + "account.save(<-TokenB.createEmptyVault(), to: /storage/tokenBVault)", + "target: /storage/tokenBVault", + } + + ok, failedCheck := containsAll(result, checkStrings) + if !ok { + fmt.Println(result) + t.Errorf("result doesn't contain: %s", failedCheck) + } +} + +func TestAddFungibleTokens(t *testing.T) { + result, err := AddFungibleTokenVaultBatchTransaction(tokens) + if err != nil { + t.Error(err) + } + + checkStrings := []string{ + "import FungibleToken from 0xFungibleTokenContractAddress", + "import TokenA from 0x1", + "import TokenB from 0x2", + "account.save(<-TokenA.createEmptyVault(), to: TokenA.VaultStoragePath)", + "target: TokenA.VaultStoragePath", + "account.save(<-TokenB.createEmptyVault(), to: /storage/tokenBVault)", + "target: /storage/tokenBVault", + "if account.borrow<&TokenA.Vault>(from: TokenA.VaultStoragePath) == nil {", + "if account.borrow<&TokenB.Vault>(from: /storage/tokenBVault) == nil {", + } + + ok, failedCheck := containsAll(result, checkStrings) + if !ok { + fmt.Println(result) + t.Errorf("result doesn't contain: %s", failedCheck) + } +} + +func containsAll(result string, checks []string) (bool, string) { + for _, check := range checks { + if !strings.Contains(result, check) { + return false, check + } + } + return true, "" +} diff --git a/templates/template_strings/scripts.go b/templates/template_strings/scripts.go index 1863256d..290fad27 100644 --- a/templates/template_strings/scripts.go +++ b/templates/template_strings/scripts.go @@ -7,7 +7,7 @@ import TOKEN_DECLARATION_NAME from TOKEN_ADDRESS pub fun main(account: Address): UFix64 { let vaultRef = getAccount(account) - .getCapability(/public/TOKEN_BALANCE) + .getCapability(TOKEN_BALANCE) .borrow<&TOKEN_DECLARATION_NAME.Vault{FungibleToken.Balance}>() ?? panic("failed to borrow reference to vault") diff --git a/templates/template_strings/transactions.go b/templates/template_strings/transactions.go index d477337c..986644a5 100644 --- a/templates/template_strings/transactions.go +++ b/templates/template_strings/transactions.go @@ -29,7 +29,7 @@ transaction(amount: UFix64, recipient: Address) { prepare(signer: AuthAccount) { let vaultRef = signer - .borrow<&TOKEN_DECLARATION_NAME.Vault>(from: /storage/TOKEN_VAULT) + .borrow<&TOKEN_DECLARATION_NAME.Vault>(from: TOKEN_VAULT) ?? panic("failed to borrow reference to sender vault") self.sentVault <- vaultRef.withdraw(amount: amount) @@ -37,7 +37,7 @@ transaction(amount: UFix64, recipient: Address) { execute { let receiverRef = getAccount(recipient) - .getCapability(/public/TOKEN_RECEIVER) + .getCapability(TOKEN_RECEIVER) .borrow<&{FungibleToken.Receiver}>() ?? panic("failed to borrow reference to recipient vault") @@ -53,22 +53,22 @@ import TOKEN_DECLARATION_NAME from TOKEN_ADDRESS transaction { prepare(signer: AuthAccount) { - let existingVault = signer.borrow<&TOKEN_DECLARATION_NAME.Vault>(from: /storage/TOKEN_VAULT) + let existingVault = signer.borrow<&TOKEN_DECLARATION_NAME.Vault>(from: TOKEN_VAULT) if (existingVault != nil) { panic("vault exists") } - signer.save(<-TOKEN_DECLARATION_NAME.createEmptyVault(), to: /storage/TOKEN_VAULT) + signer.save(<-TOKEN_DECLARATION_NAME.createEmptyVault(), to: TOKEN_VAULT) signer.link<&TOKEN_DECLARATION_NAME.Vault{FungibleToken.Receiver}>( - /public/TOKEN_RECEIVER, - target: /storage/TOKEN_VAULT + TOKEN_RECEIVER, + target: TOKEN_VAULT ) signer.link<&TOKEN_DECLARATION_NAME.Vault{FungibleToken.Balance}>( - /public/TOKEN_BALANCE, - target: /storage/TOKEN_VAULT + TOKEN_BALANCE, + target: TOKEN_VAULT ) } } diff --git a/templates/templates.go b/templates/templates.go index 6d6fbe02..311ecbb8 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -7,17 +7,21 @@ import ( "github.com/flow-hydraulics/flow-wallet-api/templates/template_strings" "github.com/onflow/flow-go-sdk" + log "github.com/sirupsen/logrus" ) type Token struct { - ID uint64 `json:"id,omitempty"` - Name string `json:"name" gorm:"uniqueIndex;not null"` // Declaration name - NameLowerCase string `json:"nameLowerCase,omitempty"` // For generic fungible token transaction templates - Address string `json:"address" gorm:"not null"` - Setup string `json:"setup,omitempty"` // Setup cadence code - Transfer string `json:"transfer,omitempty"` // Transfer cadence code - Balance string `json:"balance,omitempty"` // Balance cadence code - Type TokenType `json:"type"` + ID uint64 `json:"id,omitempty"` + Name string `json:"name" gorm:"uniqueIndex;not null"` // Declaration name + NameLowerCase string `json:"nameLowerCase,omitempty"` // (deprecated) For generic fungible token transaction templates + ReceiverPublicPath string `json:"receiverPublicPath,omitempty"` + BalancePublicPath string `json:"balancePublicPath,omitempty"` + VaultStoragePath string `json:"vaultStoragePath,omitempty"` + Address string `json:"address" gorm:"not null"` + Setup string `json:"setup,omitempty"` // Setup cadence code + Transfer string `json:"transfer,omitempty"` // Transfer cadence code + Balance string `json:"balance,omitempty"` // Balance cadence code + Type TokenType `json:"type"` } // BasicToken is a simplifed representation of a Token used in listings @@ -61,7 +65,7 @@ func (token Token) BasicToken() BasicToken { } } -func TokenCode(chainId flow.ChainID, token *Token, tmplStr string) string { +func TokenCode(chainId flow.ChainID, token *Token, tmplStr string) (string, error) { // Regex that matches all references to cadence source files // For example: @@ -78,12 +82,20 @@ func TokenCode(chainId flow.ChainID, token *Token, tmplStr string) string { fmt.Sprintf("%s.cdc", token.Name), "TOKEN_ADDRESS", ) + tokenVault, tokenReceiver, tokenBalance, err := GetTokenPaths(token) + if err != nil && (strings.Contains(tmplStr, "TOKEN_VAULT") || + strings.Contains(tmplStr, "TOKEN_RECEIVER") || + strings.Contains(tmplStr, "TOKEN_BALANCE")) { + + return "", err + } + templateReplacer := strings.NewReplacer( "TOKEN_DECLARATION_NAME", token.Name, "TOKEN_ADDRESS", token.Address, - "TOKEN_VAULT", fmt.Sprintf("%sVault", token.NameLowerCase), - "TOKEN_RECEIVER", fmt.Sprintf("%sReceiver", token.NameLowerCase), - "TOKEN_BALANCE", fmt.Sprintf("%sBalance", token.NameLowerCase), + "TOKEN_VAULT", tokenVault, + "TOKEN_RECEIVER", tokenReceiver, + "TOKEN_BALANCE", tokenBalance, ) knownAddressesReplacer := knownAddressesReplacers[chainId] @@ -96,17 +108,77 @@ func TokenCode(chainId flow.ChainID, token *Token, tmplStr string) string { code = templateReplacer.Replace(code) code = knownAddressesReplacer.Replace(code) - return code + return code, nil +} + +func GetTokenPaths( + token *Token, +) ( + vaultPath string, + receiverPath string, + balancePath string, + err error, +) { + + if token.VaultStoragePath != "" && + token.ReceiverPublicPath != "" && + token.BalancePublicPath != "" { + + // All three paths are set explicitly. + + vaultPath = token.VaultStoragePath + receiverPath = token.ReceiverPublicPath + balancePath = token.BalancePublicPath + + } else if token.NameLowerCase != "" && + token.VaultStoragePath == "" && + token.ReceiverPublicPath == "" && + token.BalancePublicPath == "" { + + // Deprecated config format: None of the paths are set. + // Use token.NameLowerCase to generate paths. + + if token.Name != "FlowToken" && token.Name != "FUSD" { + log.Warnf("%s token is using deprecated config format", token.Name) + } + + vaultPath = fmt.Sprintf("/storage/%sVault", token.NameLowerCase) + receiverPath = fmt.Sprintf("/public/%sReceiver", token.NameLowerCase) + balancePath = fmt.Sprintf("/public/%sBalance", token.NameLowerCase) + + } else { + + // Configuration error in paths + + err = fmt.Errorf("invalid path configuration for token %s", token.Name) + return + } + + return } -func FungibleTransferCode(chainId flow.ChainID, token *Token) string { +func FungibleTransferCode(chainId flow.ChainID, token *Token) (string, error) { return TokenCode(chainId, token, template_strings.GenericFungibleTransfer) } -func FungibleSetupCode(chainId flow.ChainID, token *Token) string { +func FungibleSetupCode(chainId flow.ChainID, token *Token) (string, error) { return TokenCode(chainId, token, template_strings.GenericFungibleSetup) } -func FungibleBalanceCode(chainId flow.ChainID, token *Token) string { +func FungibleBalanceCode(chainId flow.ChainID, token *Token) (string, error) { return TokenCode(chainId, token, template_strings.GenericFungibleBalance) } + +func InitFungibleTokenVaultsCode(chainId flow.ChainID, tokens []template_strings.FungibleTokenInfo) (string, error) { + return template_strings.AddFungibleTokenVaultBatchTransaction(template_strings.BatchedFungibleOpsInfo{ + FungibleTokenContractAddress: KnownAddresses["FungibleToken.cdc"][chainId], + Tokens: tokens, + }) +} + +func CreateAccountAndInitFungibleTokenVaultsCode(chainId flow.ChainID, tokens []template_strings.FungibleTokenInfo) (string, error) { + return template_strings.CreateAccountAndSetupTransaction(template_strings.BatchedFungibleOpsInfo{ + FungibleTokenContractAddress: KnownAddresses["FungibleToken.cdc"][chainId], + Tokens: tokens, + }) +} diff --git a/templates/templates_test.go b/templates/templates_test.go index 1277bbc6..e6c8027a 100644 --- a/templates/templates_test.go +++ b/templates/templates_test.go @@ -11,7 +11,10 @@ import ( func TestParsing(t *testing.T) { t.Run("FlowToken", func(t *testing.T) { token := &Token{Name: "FlowToken", Address: "test-address", NameLowerCase: "flowToken"} - c := FungibleTransferCode(flow.Emulator, token) + c, err := FungibleTransferCode(flow.Emulator, token) + if err != nil { + t.Fatal(err) + } if strings.Contains(c, ".cdc") { t.Error("expected all cadence file references to have been replaced") } @@ -22,7 +25,10 @@ func TestParsing(t *testing.T) { t.Run("FUSD", func(t *testing.T) { token := &Token{Name: "FUSD", Address: "test-address", NameLowerCase: "fusd"} - c := FungibleTransferCode(flow.Emulator, token) + c, err := FungibleTransferCode(flow.Emulator, token) + if err != nil { + t.Fatal(err) + } if strings.Contains(c, ".cdc") { t.Error("expected all cadence file references to have been replaced") } @@ -33,7 +39,7 @@ func TestParsing(t *testing.T) { t.Run("ExampleNFT", func(t *testing.T) { token := &Token{Name: "ExampleNFT", Address: "test-address"} - c := TokenCode( + c, err := TokenCode( flow.Emulator, token, ` @@ -59,6 +65,9 @@ func TestParsing(t *testing.T) { } } `) + if err != nil { + t.Fatal(err) + } if strings.Contains(c, ".cdc") { t.Error("expected all cadence file references to have been replaced") } diff --git a/templates/util.go b/templates/util.go new file mode 100644 index 00000000..e05412d3 --- /dev/null +++ b/templates/util.go @@ -0,0 +1,13 @@ +package templates + +import "github.com/flow-hydraulics/flow-wallet-api/templates/template_strings" + +func NewFungibleTokenInfo(t Token) template_strings.FungibleTokenInfo { + return template_strings.FungibleTokenInfo{ + ContractName: t.Name, + Address: t.Address, + VaultStoragePath: t.VaultStoragePath, + ReceiverPublicPath: t.ReceiverPublicPath, + BalancePublicPath: t.BalancePublicPath, + } +} diff --git a/tests/account_handler_test.go b/tests/account_handler_test.go index 4811f722..c38a9395 100644 --- a/tests/account_handler_test.go +++ b/tests/account_handler_test.go @@ -7,7 +7,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "net/http/httptest" "testing" @@ -143,7 +142,7 @@ func TestWatchlistAccountManagement(t *testing.T) { func assertStatusCode(t *testing.T, res *http.Response, expected int) { t.Helper() if res.StatusCode != expected { - bs, err := ioutil.ReadAll(res.Body) + bs, err := io.ReadAll(res.Body) if err != nil { panic(err) } @@ -165,7 +164,7 @@ func asJson(v interface{}) []byte { func fromJsonBody(t *testing.T, res *http.Response, v interface{}) { t.Helper() - bs, err := ioutil.ReadAll(res.Body) + bs, err := io.ReadAll(res.Body) if err != nil { t.Fatal(err) } diff --git a/tests/system_test.go b/tests/system_test.go index dfb69a98..fce58432 100644 --- a/tests/system_test.go +++ b/tests/system_test.go @@ -4,7 +4,6 @@ import ( "bytes" "database/sql" "io" - "io/ioutil" "net/http" "strings" "testing" @@ -58,7 +57,7 @@ func TestSettingsE2E(t *testing.T) { for _, tt := range steps { res := send(router, tt.method, tt.path, tt.body) assertStatusCode(t, res, tt.expectedStatus) - if bs, err := ioutil.ReadAll(res.Body); err != nil || strings.TrimSpace(string(bs)) != tt.expectedBody { + if bs, err := io.ReadAll(res.Body); err != nil || strings.TrimSpace(string(bs)) != tt.expectedBody { if err != nil { t.Error(err) } else { diff --git a/tests/test/file.go b/tests/test/file.go index 46b26501..7fa81bf5 100644 --- a/tests/test/file.go +++ b/tests/test/file.go @@ -1,14 +1,14 @@ package test import ( - "io/ioutil" + "os" "testing" ) func ReadFile(t *testing.T, path string) []byte { t.Helper() - bs, err := ioutil.ReadFile(path) + bs, err := os.ReadFile(path) if err != nil { t.Fatalf("error file reading file %q: %#v", path, err) } diff --git a/tests/test/service.go b/tests/test/service.go index 3891455f..6d8c20bb 100644 --- a/tests/test/service.go +++ b/tests/test/service.go @@ -16,6 +16,7 @@ import ( "github.com/flow-hydraulics/flow-wallet-api/jobs" "github.com/flow-hydraulics/flow-wallet-api/keys" "github.com/flow-hydraulics/flow-wallet-api/keys/basic" + "github.com/flow-hydraulics/flow-wallet-api/ops" "github.com/flow-hydraulics/flow-wallet-api/system" "github.com/flow-hydraulics/flow-wallet-api/templates" "github.com/flow-hydraulics/flow-wallet-api/tokens" @@ -29,6 +30,7 @@ type Services interface { GetTokens() tokens.Service GetTransactions() transactions.Service GetSystem() system.Service + GetOps() ops.Service GetKeyManager() keys.Manager GetListener() chain_events.Listener @@ -42,6 +44,7 @@ type svcs struct { tokenService tokens.Service transactionService transactions.Service systemService system.Service + opsService ops.Service keyManager keys.Manager listener chain_events.Listener @@ -83,6 +86,7 @@ func GetServices(t *testing.T, cfg *configs.Config) Services { goleak.IgnoreTopFunction("google.golang.org/grpc.(*ccBalancerWrapper).watcher"), goleak.IgnoreTopFunction("google.golang.org/grpc/internal/transport.(*controlBuffer).get"), goleak.IgnoreTopFunction("github.com/flow-hydraulics/flow-wallet-api/jobs.(*WorkerPoolImpl).startWorkers.func1"), + goleak.IgnoreTopFunction("github.com/flow-hydraulics/flow-wallet-api/ops.(*workerPoolImpl).Start.func1"), goleak.IgnoreTopFunction("github.com/flow-hydraulics/flow-wallet-api/jobs.(*WorkerPoolImpl).startDBJobScheduler.func1"), goleak.IgnoreTopFunction("github.com/flow-hydraulics/flow-wallet-api/chain_events.(*ListenerImpl).Start.func1"), ) @@ -106,11 +110,15 @@ func GetServices(t *testing.T, cfg *configs.Config) Services { km := basic.NewKeyManager(cfg, keys.NewGormStore(db), fc) - templateService := templates.NewService(cfg, templates.NewGormStore(db)) + templateService, err := templates.NewService(cfg, templates.NewGormStore(db)) + if err != nil { + t.Fatal(err) + } transactionService := transactions.NewService(cfg, transactions.NewGormStore(db), km, fc, wp) - accountService := accounts.NewService(cfg, accounts.NewGormStore(db), km, fc, wp, transactionService) + accountService := accounts.NewService(cfg, accounts.NewGormStore(db), km, fc, wp, transactionService, templateService) jobService := jobs.NewService(jobs.NewGormStore(db)) tokenService := tokens.NewService(cfg, tokens.NewGormStore(db), km, fc, wp, transactionService, templateService, accountService) + opsService := ops.NewService(cfg, ops.NewGormStore(db), templateService, transactionService, tokenService) getTypes := func() ([]string, error) { // Get all enabled tokens @@ -119,11 +127,11 @@ func GetServices(t *testing.T, cfg *configs.Config) Services { return nil, err } - token_count := len(*tt) + token_count := len(tt) event_types := make([]string, token_count) // Listen for enabled tokens deposit events - for i, token := range *tt { + for i, token := range tt { event_types[i] = templates.DepositEventTypeFromToken(token) } @@ -145,7 +153,7 @@ func GetServices(t *testing.T, cfg *configs.Config) Services { TokenService: tokenService, }) - err := accountService.InitAdminAccount(context.Background()) + err = accountService.InitAdminAccount(context.Background()) if err != nil { t.Fatal(err) } @@ -157,6 +165,7 @@ func GetServices(t *testing.T, cfg *configs.Config) Services { t.Cleanup(func() { wp.Stop(false) + opsService.GetWorkerPool().Stop() listener.Stop() }) @@ -170,6 +179,7 @@ func GetServices(t *testing.T, cfg *configs.Config) Services { tokenService: tokenService, transactionService: transactionService, systemService: systemService, + opsService: opsService, keyManager: km, listener: listener, @@ -197,6 +207,10 @@ func (s *svcs) GetTransactions() transactions.Service { return s.transactionService } +func (s *svcs) GetOps() ops.Service { + return s.opsService +} + func (s *svcs) GetKeyManager() keys.Manager { return s.keyManager } diff --git a/tokens/account_events.go b/tokens/account_events.go index 0ac97ec3..b2fdcaf5 100644 --- a/tokens/account_events.go +++ b/tokens/account_events.go @@ -14,13 +14,16 @@ type AccountAddedHandler struct { func (h *AccountAddedHandler) Handle(payload accounts.AccountAddedPayload) { address := flow_helpers.FormatAddress(payload.Address) - h.addFlowToken(address) + h.addToken("FlowToken", address) + for _, t := range payload.InitializedFungibleTokens { + h.addToken(t.Name, address) + } } -func (h *AccountAddedHandler) addFlowToken(address string) { - if err := h.TokenService.AddAccountToken("FlowToken", address); err != nil { +func (h *AccountAddedHandler) addToken(name string, address string) { + if err := h.TokenService.AddAccountToken(name, address); err != nil { log. WithFields(log.Fields{"error": err}). - Warn("Error while adding FlowToken to new account") + Warnf("Error while adding %s token to new account", name) } } diff --git a/tokens/chain_events.go b/tokens/chain_events.go index f976427f..ca67fbbb 100644 --- a/tokens/chain_events.go +++ b/tokens/chain_events.go @@ -31,6 +31,9 @@ func (h *ChainEventHandler) handleDeposit(ctx context.Context, event flow.Event) // as we could not even listen to events for them token, err := h.TemplateService.TokenFromEvent(event) if err != nil { + log. + WithFields(log.Fields{"error": err}). + Warn("Failed to extract token from event") return } @@ -49,4 +52,12 @@ func (h *ChainEventHandler) handleDeposit(ctx context.Context, event flow.Event) Warn("Error while registering a deposit") return } + + log. + WithFields(log.Fields{ + "token": token.Name, + "account": accountAddress, + "amountOrNftID": amountOrNftID, + }). + Debug("New deposit") } diff --git a/tokens/test_helpers.go b/tokens/test_helpers.go index 22dcd5fb..b1947fd9 100644 --- a/tokens/test_helpers.go +++ b/tokens/test_helpers.go @@ -2,6 +2,7 @@ package tokens import ( "context" + "strings" "github.com/flow-hydraulics/flow-wallet-api/accounts" "github.com/flow-hydraulics/flow-wallet-api/flow_helpers" @@ -30,12 +31,15 @@ func (s *ServiceImpl) DeployTokenContractForAccount(ctx context.Context, runSync return err } - src := templates.TokenCode(s.cfg.ChainID, token, tmplStr) + src, err := templates.TokenCode(s.cfg.ChainID, token, tmplStr) + if err != nil { + return err + } c := flow_templates.Contract{Name: n, Source: src} err = accounts.AddContract(ctx, s.fc, s.km, address, c, s.cfg.TransactionTimeout) - if err != nil { + if err != nil && !strings.Contains(err.Error(), "cannot overwrite existing contract") { return err }