Skip to content

Commit

Permalink
Merge pull request #277 from flow-hydraulics/feature/243-endpoint-for…
Browse files Browse the repository at this point in the history
…-adding-keys

Add "sync key count for existing account" endpoint
  • Loading branch information
nanuuki authored Mar 2, 2022
2 parents 6a82517 + fb55f9f commit 1c66f2d
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 7 deletions.
39 changes: 39 additions & 0 deletions accounts/jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package accounts

import (
"context"
"encoding/json"
"fmt"

"github.com/flow-hydraulics/flow-wallet-api/jobs"
"github.com/onflow/flow-go-sdk"
log "github.com/sirupsen/logrus"
)

const AccountCreateJobType = "account_create"
Expand All @@ -25,3 +29,38 @@ func (s *ServiceImpl) executeAccountCreateJob(ctx context.Context, j *jobs.Job)

return nil
}

const SyncAccountKeyCountJobType = "sync_account_key_count"

type syncAccountKeyCountJobAttributes struct {
Address flow.Address `json:"address"`
NumKeys int `json:"numkeys"`
}

func (s *ServiceImpl) executeSyncAccountKeyCountJob(ctx context.Context, j *jobs.Job) error {
entry := log.WithFields(log.Fields{"job": j, "function": "executeSyncAccountKeyCountJob"})
if j.Type != SyncAccountKeyCountJobType {
return jobs.ErrInvalidJobType
}

j.ShouldSendNotification = true

var attrs syncAccountKeyCountJobAttributes
err := json.Unmarshal(j.Attributes, &attrs)
if err != nil {
return err
}

entry.WithFields(log.Fields{"attrs": j.Attributes}).Trace("Unmarshaled attributes")

numKeys, txID, err := s.syncAccountKeyCount(ctx, attrs.Address, attrs.NumKeys)
entry.WithFields(log.Fields{"numKeys": numKeys, "txId": txID, "err": err}).Trace("s.syncAccountKeyCount complete")
if err != nil {
return err
}

j.TransactionID = txID
j.Result = fmt.Sprintf("%s:%d", attrs.Address, numKeys)

return nil
}
158 changes: 156 additions & 2 deletions accounts/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@ package accounts

import (
"context"
"encoding/json"
"fmt"
"os"
"sort"
"strings"

"github.com/flow-hydraulics/flow-wallet-api/configs"
"github.com/flow-hydraulics/flow-wallet-api/datastore"
"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/template_strings"
"github.com/flow-hydraulics/flow-wallet-api/transactions"
"github.com/onflow/cadence"
"github.com/onflow/flow-go-sdk"
flow_crypto "github.com/onflow/flow-go-sdk/crypto"
flow_templates "github.com/onflow/flow-go-sdk/templates"
log "github.com/sirupsen/logrus"
"go.uber.org/ratelimit"
Expand All @@ -24,6 +30,7 @@ type Service interface {
Create(ctx context.Context, sync bool) (*jobs.Job, *Account, error)
AddNonCustodialAccount(address string) (*Account, error)
DeleteNonCustodialAccount(address string) error
SyncAccountKeyCount(ctx context.Context, address flow.Address) (*jobs.Job, error)
Details(address string) (Account, error)
InitAdminAccount(ctx context.Context) error
}
Expand All @@ -35,6 +42,7 @@ type ServiceImpl struct {
km keys.Manager
fc flow_helpers.FlowClient
wp jobs.WorkerPool
txs transactions.Service
txRateLimiter ratelimit.Limiter
}

Expand All @@ -45,12 +53,13 @@ func NewService(
km keys.Manager,
fc flow_helpers.FlowClient,
wp jobs.WorkerPool,
txs transactions.Service,
opts ...ServiceOption,
) Service {
var defaultTxRatelimiter = ratelimit.NewUnlimited()

// TODO(latenssi): safeguard against nil config?
svc := &ServiceImpl{cfg, store, km, fc, wp, defaultTxRatelimiter}
svc := &ServiceImpl{cfg, store, km, fc, wp, txs, defaultTxRatelimiter}

for _, opt := range opts {
opt(svc)
Expand All @@ -60,8 +69,9 @@ func NewService(
panic("workerpool nil")
}

// Register asynchronous job executor.
// Register asynchronous job executors
wp.RegisterExecutor(AccountCreateJobType, svc.executeAccountCreateJob)
wp.RegisterExecutor(SyncAccountKeyCountJobType, svc.executeSyncAccountKeyCountJob)

return svc
}
Expand Down Expand Up @@ -160,6 +170,150 @@ func (s *ServiceImpl) Details(address string) (Account, error) {
return account, nil
}

// SyncKeyCount syncs number of keys for given account
func (s *ServiceImpl) SyncAccountKeyCount(ctx context.Context, address flow.Address) (*jobs.Job, error) {
// Validate address, they might be legit addresses but for the wrong chain
if !address.IsValid(s.cfg.ChainID) {
return nil, fmt.Errorf(`not a valid address for %s: "%s"`, s.cfg.ChainID, address)
}

// Prepare job attributes required for executing the job
attrs := syncAccountKeyCountJobAttributes{Address: address, NumKeys: int(s.cfg.DefaultAccountKeyCount)}
attrBytes, err := json.Marshal(attrs)
if err != nil {
return nil, err
}

// Create & schedule the "sync key count" job
job, err := s.wp.CreateJob(SyncAccountKeyCountJobType, "", jobs.WithAttributes(attrBytes))
if err != nil {
return nil, err
}
err = s.wp.Schedule(job)
if err != nil {
return nil, err
}

return job, nil
}

// syncAccountKeyCount syncs the number of account keys with the given numKeys and
// returns the number of keys, transaction ID and error.
func (s *ServiceImpl) syncAccountKeyCount(ctx context.Context, address flow.Address, numKeys int) (int, string, error) {
entry := log.WithFields(log.Fields{"address": address, "numKeys": numKeys, "function": "ServiceImpl.syncAccountKeyCount"})

if numKeys < 1 {
return 0, "", fmt.Errorf("invalid number of keys specified: %d, min. 1 expected", numKeys)
}

// Check on-chain keys
flowAccount, err := s.fc.GetAccount(ctx, address)
if err != nil {
entry.WithFields(log.Fields{"err": err}).Error("failed to get Flow account")
return 0, "", err
}

// Get stored account
dbAccount, err := s.store.Account(flow_helpers.FormatAddress(address))
if err != nil {
entry.WithFields(log.Fields{"err": err}).Error("failed to get account from database")
return 0, "", err
}

// Pick a source key that will be used to create the new keys & decode public key
sourceKey := dbAccount.Keys[0] // NOTE: Only valid (not revoked) keys should be stored in the database
sourceKeyPbkString := strings.TrimPrefix(sourceKey.PublicKey, "0x")
sourcePbk, err := flow_crypto.DecodePublicKeyHex(flow_crypto.StringToSignatureAlgorithm(sourceKey.SignAlgo), sourceKeyPbkString)
if err != nil {
entry.WithFields(log.Fields{"err": err, "sourceKeyPbkString": sourceKeyPbkString}).Error("failed to decode public key for source key")
return 0, "", err
}
entry.WithFields(log.Fields{"sourceKeyId": sourceKey.ID, "sourcePbk": sourcePbk}).Trace("source key selected")

// Count valid keys, as some keys might be revoked, assuming dbAccount.Keys are clones (all have same public key)
var validKeys []*flow.AccountKey
for i := range flowAccount.Keys {
key := flowAccount.Keys[i]
if !key.Revoked && key.PublicKey.Equals(sourcePbk) {
validKeys = append(validKeys, key)
}
}

if len(validKeys) != len(dbAccount.Keys) {
entry.WithFields(log.Fields{"onChain": len(validKeys), "database": len(dbAccount.Keys)}).Warn("on-chain vs. database key count mismatch")
}

entry.WithFields(log.Fields{"validKeys": validKeys}).Trace("filtered valid keys")

// Add keys by cloning the source key
if len(validKeys) < numKeys {

cloneCount := numKeys - len(validKeys)
code := template_strings.AddAccountKeysTransaction
pbks := []cadence.Value{}

entry.WithFields(log.Fields{"validKeys": len(validKeys), "numKeys": numKeys, "cloneCount": cloneCount}).Debug("going to add keys")

// Sort keys by index
sort.SliceStable(dbAccount.Keys, func(i, j int) bool {
return dbAccount.Keys[i].Index < dbAccount.Keys[j].Index
})

// Push publickeys to args and prepare db update
for i := 0; i < cloneCount; i++ {
pbk, err := cadence.NewString(sourceKey.PublicKey[2:]) // TODO: use a helper function to trim "0x" prefix
if err != nil {
return 0, "", err
}
pbks = append(pbks, pbk)

// Create cloned account key & update index
cloned := keys.Storable{
ID: 0, // Reset ID to create a new key to DB
AccountAddress: sourceKey.AccountAddress,
Index: dbAccount.Keys[len(dbAccount.Keys)-1].Index + 1,
Type: sourceKey.Type,
Value: sourceKey.Value,
PublicKey: sourceKey.PublicKey,
SignAlgo: sourceKey.SignAlgo,
HashAlgo: sourceKey.HashAlgo,
}

dbAccount.Keys = append(dbAccount.Keys, cloned)
}

// Prepare transaction arguments
x := cadence.NewArray(pbks)
args := []transactions.Argument{x}

entry.WithFields(log.Fields{"args": args}).Debug("args prepared")

// NOTE: sync, so will wait for transaction to be sent & sealed
_, tx, err := s.txs.Create(ctx, true, dbAccount.Address, code, args, transactions.General)
if err != nil {
entry.WithFields(log.Fields{"err": err}).Error("failed to create transaction")
return 0, tx.TransactionId, err
}

// Update account in database
// TODO: if update fails, should sync keys from chain later
err = s.store.SaveAccount(&dbAccount)
if err != nil {
entry.WithFields(log.Fields{"err": err}).Error("failed to update account in database")
return 0, tx.TransactionId, err
}

return len(dbAccount.Keys), tx.TransactionId, err
} else if len(validKeys) > numKeys {
entry.Debug("too many valid keys", len(validKeys), " vs. ", numKeys)
} else {
entry.Debug("correct number of keys")
return numKeys, "", nil
}

return 0, "", nil
}

// createAccount creates a new account on the flow blockchain. It generates a
// fresh key pair and constructs a flow transaction to create the account with
// generated key. Admin account is used to pay for the transaction.
Expand Down
3 changes: 3 additions & 0 deletions accounts/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ type Store interface {
// Insert a new account.
InsertAccount(a *Account) error

// Update an existing account.
SaveAccount(a *Account) error

// Permanently delete an account, despite of `DeletedAt` field.
HardDeleteAccount(a *Account) error
}
4 changes: 4 additions & 0 deletions accounts/store_gorm.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ func (s *GormStore) InsertAccount(a *Account) error {
return s.db.Create(a).Error
}

func (s *GormStore) SaveAccount(a *Account) error {
return s.db.Save(&a).Error
}

func (s *GormStore) HardDeleteAccount(a *Account) error {
return s.db.Unscoped().Delete(a).Error
}
10 changes: 10 additions & 0 deletions api-test-scripts/system.http
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,13 @@ idempotency-key: {{$guid}}
{
"maintenanceMode":false
}


### Sync account key counts
POST http://localhost:3000/v1/system/sync-account-key-count HTTP/1.1
content-type: application/json
idempotency-key: {{$guid}}

{
"address": "0x01"
}
10 changes: 10 additions & 0 deletions handlers/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"

"github.com/flow-hydraulics/flow-wallet-api/accounts"
"github.com/onflow/flow-go-sdk"
)

// Accounts is a HTTP server for account management.
Expand All @@ -13,6 +14,11 @@ type Accounts struct {
service accounts.Service
}

// SyncKeyCountRequest represents a JSON payload for a HTTP request
type SyncKeyCountRequest struct {
Address flow.Address `json:"address"`
}

// NewAccounts initiates a new accounts server.
func NewAccounts(service accounts.Service) *Accounts {
return &Accounts{service}
Expand All @@ -34,6 +40,10 @@ func (s *Accounts) DeleteNonCustodialAccount() http.Handler {
return http.HandlerFunc(s.DeleteNonCustodialAccountFunc)
}

func (s *Accounts) SyncAccountKeyCount() http.Handler {
return http.HandlerFunc(s.SyncAccountKeyCountFunc)
}

func (s *Accounts) Details() http.Handler {
return http.HandlerFunc(s.DetailsFunc)
}
24 changes: 24 additions & 0 deletions handlers/accounts_func.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,27 @@ func (s *Accounts) DeleteNonCustodialAccountFunc(rw http.ResponseWriter, r *http

rw.WriteHeader(http.StatusOK)
}

func (s *Accounts) SyncAccountKeyCountFunc(rw http.ResponseWriter, r *http.Request) {
// Check body is not empty
if err := checkNonEmptyBody(r); err != nil {
handleError(rw, r, err)
return
}

var req SyncKeyCountRequest
// Try to decode the request body.
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
err = &errors.RequestError{StatusCode: http.StatusBadRequest, Err: fmt.Errorf("invalid body")}
handleError(rw, r, err)
return
}

job, err := s.service.SyncAccountKeyCount(r.Context(), req.Address)
if err != nil {
handleError(rw, r, err)
return
}

handleJsonResponse(rw, http.StatusOK, job)
}
4 changes: 3 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func runServer(cfg *configs.Config) {
templateService := templates.NewService(cfg, templates.NewGormStore(db))
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, accounts.WithTxRatelimiter(txRatelimiter))
accountService := accounts.NewService(cfg, accounts.NewGormStore(db), km, fc, wp, transactionService, accounts.WithTxRatelimiter(txRatelimiter))
tokenService := tokens.NewService(cfg, tokens.NewGormStore(db), km, fc, wp, transactionService, templateService, accountService)

// Register a handler for account added events
Expand Down Expand Up @@ -160,6 +160,8 @@ func runServer(cfg *configs.Config) {
rv.Handle("/system/settings", systemHandler.GetSettings()).Methods(http.MethodGet)
rv.Handle("/system/settings", systemHandler.SetSettings()).Methods(http.MethodPost)

rv.Handle("/system/sync-account-key-count", accountHandler.SyncAccountKeyCount()).Methods(http.MethodPost)

// Jobs
rv.Handle("/jobs", jobsHandler.List()).Methods(http.MethodGet) // list
rv.Handle("/jobs/{jobId}", jobsHandler.Details()).Methods(http.MethodGet) // details
Expand Down
Loading

0 comments on commit 1c66f2d

Please sign in to comment.