diff --git a/.gitignore b/.gitignore index 436b9da..74a98e5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ ftoken # Dependency directories (remove the comment below to include it) # vendor/ + +builds +ftoken diff --git a/cmd/deploy.go b/cmd/deploy.go index 020c008..82b1fff 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -14,17 +14,16 @@ var deployCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { ctx := context.Background() exec, _ := cmd.Flags().GetBool("e") - receiver, _ := cmd.Flags().GetString("receiver") tokenStr, _ := cmd.Flags().GetString("tokens") - var tokens core.Tokens + var tokens core.TokenItems if err := json.Unmarshal([]byte(tokenStr), &tokens); err != nil { cmd.PrintErr("unmarshal tokens failed: ", tokens) return } factory := provideQuorumFactory() - tx, err := factory.CreateTransaction(ctx, tokens, &core.Address{Destination: receiver}) + tx, err := factory.CreateTransaction(ctx, tokens, "") if err != nil { cmd.PrintErr("CreateTransaction failed:", err) return diff --git a/cmd/provide.go b/cmd/provide.go index 968d189..70dc1e5 100644 --- a/cmd/provide.go +++ b/cmd/provide.go @@ -5,7 +5,9 @@ import ( "github.com/fox-one/ftoken/core" "github.com/fox-one/ftoken/quorum" + assetz "github.com/fox-one/ftoken/service/asset" walletz "github.com/fox-one/ftoken/service/wallet" + "github.com/fox-one/ftoken/store/asset" "github.com/fox-one/ftoken/store/order" "github.com/fox-one/ftoken/store/transaction" "github.com/fox-one/ftoken/store/wallet" @@ -21,7 +23,6 @@ func provideSystem(ctx context.Context, client *mixin.Client, factories []core.F ClientID: cfg.Dapp.ClientID, ClientSecret: cfg.Dapp.ClientSecret, Version: rootCmd.Version, - Addresses: make(map[string]*core.Address, len(factories)), Gas: core.Gas{ Mins: make(number.Values, len(cfg.Gas.Mins)), Multiplier: cfg.Gas.Multiplier, @@ -33,17 +34,6 @@ func provideSystem(ctx context.Context, client *mixin.Client, factories []core.F system.Gas.Mins.Set(val.Platform, val.Min) } - for _, factory := range factories { - asset, err := client.ReadAsset(ctx, factory.GasAsset()) - if err != nil { - return system, err - } - system.Addresses[factory.GasAsset()] = &core.Address{ - Destination: asset.Destination, - Tag: asset.Tag, - } - } - return system, nil } @@ -56,10 +46,6 @@ func provideMixinClient() *mixin.Client { return c } -func provideWalletService(c *mixin.Client) core.WalletService { - return walletz.New(walletz.Config{Pin: cfg.Dapp.Pin}, c) -} - func provideDatabase() (*db.DB, error) { database, err := db.Open(cfg.DB) if err != nil { @@ -89,6 +75,18 @@ func provideTransactionStore(db *db.DB) core.TransactionStore { return transaction.New(db) } +func provideAssetStore(db *db.DB) core.AssetStore { + return asset.New(db) +} + +func provideWalletService(c *mixin.Client) core.WalletService { + return walletz.New(walletz.Config{Pin: cfg.Dapp.Pin}, c) +} + +func provideAssetService(c *mixin.Client) core.AssetService { + return assetz.New(c) +} + func provideAllFactories() []core.Factory { return []core.Factory{ provideQuorumFactory(), diff --git a/cmd/server.go b/cmd/server.go index aca468d..51a206c 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -66,6 +66,7 @@ var serverCmd = &cobra.Command{ svr := handler.New( system, + provideAssetStore(database), provideOrderStore(database), provideTransactionStore(database), provideWalletService(client), diff --git a/cmd/worker.go b/cmd/worker.go index fb361bd..7b4a49a 100644 --- a/cmd/worker.go +++ b/cmd/worker.go @@ -33,6 +33,7 @@ var workerCmd = &cobra.Command{ defer database.Close() client := provideMixinClient() + assets := provideAssetStore(database) wallets := provideWalletStore(database) walletz := provideWalletService(client) orders := provideOrderStore(database) @@ -51,6 +52,7 @@ var workerCmd = &cobra.Command{ payee.Config{ClientID: cfg.Dapp.ClientID}, system, properties, + assets, orders, transactions, wallets, diff --git a/core/asset.go b/core/asset.go new file mode 100644 index 0000000..4e39d10 --- /dev/null +++ b/core/asset.go @@ -0,0 +1,39 @@ +package core + +import ( + "context" + "errors" + "time" +) + +var ( + ErrAssetNotExist = errors.New("asset not exist") +) + +type ( + Asset struct { + ID uint64 `sql:"PRIMARY_KEY;" json:"id"` + AssetID string `sql:"size:36;" json:"asset_id,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` + Version int64 `sql:"not null" json:"version,omitempty"` + Verified bool `json:"verified"` + Name string `sql:"size:64" json:"name,omitempty"` + Symbol string `sql:"size:32" json:"symbol,omitempty"` + DisplaySymbol string `sql:"size:32;default:null;" json:"display_symbol,omitempty"` + ChainID string `sql:"size:36" json:"chain_id,omitempty"` + } + + // AssetStore defines operations for working with assets on db. + AssetStore interface { + Save(ctx context.Context, asset *Asset) error + Find(ctx context.Context, assetIDs ...string) ([]*Asset, error) + ListAll(ctx context.Context) ([]*Asset, error) + } + + // AssetService provides access to assets information + // in the remote system like mixin network. + AssetService interface { + Find(ctx context.Context, assetID string) (*Asset, error) + } +) diff --git a/core/factory.go b/core/factory.go index 9c9481d..c6a6b54 100644 --- a/core/factory.go +++ b/core/factory.go @@ -1,12 +1,7 @@ package core import ( - "bytes" "context" - "database/sql/driver" - "encoding/binary" - "encoding/json" - "errors" "time" "github.com/shopspring/decimal" @@ -22,16 +17,6 @@ const ( type ( TransactionState int - Token struct { - Name string `gorm:"size:255;" json:"name,omitempty"` - Symbol string `gorm:"size:255;" json:"symbol,omitempty"` - TotalSupply uint64 `json:"total_supply,omitempty"` - AssetKey string `gorm:"size:255;" json:"asset_key,omitempty"` - AssetID string `gorm:"size:36;" json:"asset_id,omitempty"` - } - - Tokens []*Token - Transaction struct { ID uint64 `sql:"PRIMARY_KEY;" json:"id"` CreatedAt time.Time `json:"created_at"` @@ -41,14 +26,14 @@ type ( Hash string `sql:"size:127;" json:"hash,omitempty"` Raw string `sql:"type:longtext;" json:"raw,omitempty"` State TransactionState `json:"state,omitempty"` - Tokens Tokens `sql:"type:longtext;" json:"tokens,omitempty"` + Tokens TokenItems `sql:"type:longtext;" json:"tokens,omitempty"` Gas decimal.Decimal `sql:"type:decimal(64,8)" json:"gas,omitempty"` } Factory interface { Platform() string GasAsset() string - CreateTransaction(ctx context.Context, tokens []*Token, trace string) (*Transaction, error) + CreateTransaction(ctx context.Context, tokens TokenItems, trace string) (*Transaction, error) SendTransaction(ctx context.Context, tx *Transaction) error ReadTransaction(ctx context.Context, hash string) (*Transaction, error) } @@ -60,123 +45,3 @@ type ( FindTrace(ctx context.Context, traceID string) ([]*Transaction, error) } ) - -func EncodeTokens(tokens Tokens) ([]byte, error) { - enc := bytes.NewBuffer(nil) - for _, token := range tokens { - enc.WriteByte(byte(len(token.Name))) - enc.Write([]byte(token.Name)) - enc.WriteByte(byte(len(token.Symbol))) - enc.Write([]byte(token.Symbol)) - enc.Write(uint64ToByte(token.TotalSupply)) - } - return enc.Bytes(), nil -} - -func EncodeToken(token Token) ([]byte, error) { - enc := bytes.NewBuffer(nil) - enc.WriteByte(byte(len(token.Name))) - enc.Write([]byte(token.Name)) - enc.WriteByte(byte(len(token.Symbol))) - enc.Write([]byte(token.Symbol)) - enc.Write(uint64ToByte(token.TotalSupply)) - return enc.Bytes(), nil -} - -func DecodeTokens(data []byte) Tokens { - var ( - token *Token - tokens Tokens - ) - - for len(data) > 10 { - if token, data = DecodeToken(data); token != nil { - tokens = append(tokens, token) - } - } - return tokens -} - -func DecodeToken(data []byte) (*Token, []byte) { - if len(data) <= 10 { - return nil, nil - } - - var token Token - offset := 0 - if size := int(data[offset]); len(data) > 10+size { - offset++ - token.Name = string(data[offset : offset+size]) - offset += size - } else { - return nil, nil - } - - if size := int(data[offset]); len(data) >= offset+size+9 { - offset++ - token.Symbol = string(data[offset : offset+size]) - offset += size - } else { - return nil, nil - } - - token.TotalSupply = binary.BigEndian.Uint64(data[offset : offset+8]) - offset += 8 - data = data[offset:] - if token.TotalSupply > 0 { - return &token, data - } - return nil, data -} - -func uint64ToByte(d uint64) []byte { - b := make([]byte, 8) - binary.BigEndian.PutUint64(b, d) - return b -} - -func (t Token) MarshalBinary() ([]byte, error) { - return EncodeToken(t) -} - -func (t *Token) UnmarshalBinary(data []byte) error { - token, _ := DecodeToken(data) - if token == nil { - return errors.New("unmarshal Token failed") - } - *t = *token - return nil -} - -func (t Tokens) MarshalBinary() ([]byte, error) { - return EncodeTokens(t) -} - -func (t *Tokens) UnmarshalBinary(data []byte) error { - tokens := DecodeTokens(data) - *t = tokens - return nil -} - -// Scan implements the sql.Scanner interface for database deserialization. -func (s *Tokens) Scan(value interface{}) error { - var d []byte - switch v := value.(type) { - case string: - d = []byte(v) - case []byte: - d = v - } - var tokens Tokens - if err := json.Unmarshal(d, &tokens); err != nil { - return err - } - *s = tokens - return nil -} - -// Value implements the driver.Valuer interface for database serialization. -func (s Tokens) Value() (driver.Value, error) { - data, err := json.Marshal(s) - return data, err -} diff --git a/core/order.go b/core/order.go index 9434a8f..e86a592 100644 --- a/core/order.go +++ b/core/order.go @@ -2,11 +2,8 @@ package core import ( "context" - "database/sql/driver" - "encoding/json" "time" - "github.com/fox-one/ftoken/pkg/mtg" "github.com/shopspring/decimal" ) @@ -21,27 +18,21 @@ const ( type ( OrderState int - Address struct { - Destination string `json:"destination,omitempty"` - Tag string `json:"tag,omitempty"` - } - Order struct { - ID uint64 `sql:"PRIMARY_KEY;" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Version int `json:"version,omitempty"` - TraceID string `sql:"size:36;" json:"trace_id,omitempty"` - State OrderState `json:"state"` - UserID string `sql:"size:36;" json:"user_id,omitempty"` - FeeAsset string `sql:"size:36;" json:"fee_asset,omitempty"` - FeeAmount decimal.Decimal `sql:"type:decimal(64,8)" json:"fee_amount,omitempty"` - GasUsage decimal.Decimal `sql:"type:decimal(64,8)" json:"gas_usage,omitempty"` - Platform string `sql:"size:255;" json:"platform,omitempty"` - Tokens Tokens `sql:"type:longtext;" json:"tokens,omitempty"` - Result Tokens `sql:"type:longtext;" json:"result,omitempty"` - Receiver *Address `sql:"size:255;" json:"receiver,omitempty"` - Transaction string `sql:"size:128;" json:"transaction,omitempty"` + ID uint64 `sql:"PRIMARY_KEY;" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Version int `json:"version,omitempty"` + TraceID string `sql:"size:36;" json:"trace_id,omitempty"` + State OrderState `json:"state"` + UserID string `sql:"size:36;" json:"user_id,omitempty"` + FeeAsset string `sql:"size:36;" json:"fee_asset,omitempty"` + FeeAmount decimal.Decimal `sql:"type:decimal(64,8)" json:"fee_amount,omitempty"` + GasUsage decimal.Decimal `sql:"type:decimal(64,8)" json:"gas_usage,omitempty"` + Platform string `sql:"size:255;" json:"platform,omitempty"` + TokenRequests TokenItems `sql:"type:longtext;" json:"token_requests,omitempty"` + Tokens TokenItems `sql:"type:longtext;" json:"tokens,omitempty"` + Transaction string `sql:"size:128;" json:"transaction,omitempty"` } OrderStore interface { @@ -51,42 +42,3 @@ type ( List(ctx context.Context, state OrderState, limit int) ([]*Order, error) } ) - -func (a Address) MarshalBinary() ([]byte, error) { - return mtg.Encode(a.Destination, a.Tag) -} - -func (a *Address) UnmarshalBinary(data []byte) error { - var ( - destination string - tag string - ) - if _, err := mtg.Scan(data, &destination, &tag); err != nil { - return err - } - a.Destination = destination - a.Tag = tag - return nil -} - -// Scan implements the sql.Scanner interface for database deserialization. -func (a *Address) Scan(value interface{}) error { - var d []byte - switch v := value.(type) { - case string: - d = []byte(v) - case []byte: - d = v - } - var address Address - if err := json.Unmarshal(d, &address); err != nil { - return err - } - *a = address - return nil -} - -// Value implements the driver.Valuer interface for database serialization. -func (a *Address) Value() (driver.Value, error) { - return json.Marshal(a) -} diff --git a/core/system.go b/core/system.go index 47154b7..74cd0f1 100644 --- a/core/system.go +++ b/core/system.go @@ -18,6 +18,5 @@ type ( ClientID string ClientSecret string Gas Gas - Addresses map[string]*Address } ) diff --git a/core/token_item.go b/core/token_item.go new file mode 100644 index 0000000..81ba4d4 --- /dev/null +++ b/core/token_item.go @@ -0,0 +1,141 @@ +package core + +import ( + "bytes" + "database/sql/driver" + "encoding/binary" + "encoding/json" + "errors" +) + +type ( + TokenItem struct { + AssetID string `json:"asset_id,omitempty"` + AssetKey string `json:"asset_key"` + Name string `json:"name"` + Symbol string `json:"symbol"` + TotalSupply uint64 `json:"total_supply"` + } + + TokenItems []*TokenItem +) + +func EncodeTokens(tokens TokenItems) ([]byte, error) { + enc := bytes.NewBuffer(nil) + for _, token := range tokens { + enc.WriteByte(byte(len(token.Name))) + enc.Write([]byte(token.Name)) + enc.WriteByte(byte(len(token.Symbol))) + enc.Write([]byte(token.Symbol)) + enc.Write(uint64ToByte(token.TotalSupply)) + } + return enc.Bytes(), nil +} + +func EncodeToken(token TokenItem) ([]byte, error) { + enc := bytes.NewBuffer(nil) + enc.WriteByte(byte(len(token.Name))) + enc.Write([]byte(token.Name)) + enc.WriteByte(byte(len(token.Symbol))) + enc.Write([]byte(token.Symbol)) + enc.Write(uint64ToByte(token.TotalSupply)) + return enc.Bytes(), nil +} + +func DecodeTokens(data []byte) TokenItems { + var ( + token *TokenItem + tokens TokenItems + ) + + for len(data) > 10 { + if token, data = DecodeToken(data); token != nil { + tokens = append(tokens, token) + } + } + return tokens +} + +func DecodeToken(data []byte) (*TokenItem, []byte) { + if len(data) <= 10 { + return nil, nil + } + + var token TokenItem + offset := 0 + if size := int(data[offset]); len(data) > 10+size { + offset++ + token.Name = string(data[offset : offset+size]) + offset += size + } else { + return nil, nil + } + + if size := int(data[offset]); len(data) >= offset+size+9 { + offset++ + token.Symbol = string(data[offset : offset+size]) + offset += size + } else { + return nil, nil + } + + token.TotalSupply = binary.BigEndian.Uint64(data[offset : offset+8]) + offset += 8 + data = data[offset:] + if token.TotalSupply > 0 { + return &token, data + } + return nil, data +} + +func uint64ToByte(d uint64) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, d) + return b +} + +func (t TokenItem) MarshalBinary() ([]byte, error) { + return EncodeToken(t) +} + +func (t *TokenItem) UnmarshalBinary(data []byte) error { + token, _ := DecodeToken(data) + if token == nil { + return errors.New("unmarshal Token failed") + } + *t = *token + return nil +} + +func (t TokenItems) MarshalBinary() ([]byte, error) { + return EncodeTokens(t) +} + +func (t *TokenItems) UnmarshalBinary(data []byte) error { + tokens := DecodeTokens(data) + *t = tokens + return nil +} + +// Scan implements the sql.Scanner interface for database deserialization. +func (s *TokenItems) Scan(value interface{}) error { + var d []byte + switch v := value.(type) { + case string: + d = []byte(v) + case []byte: + d = v + } + var tokens TokenItems + if err := json.Unmarshal(d, &tokens); err != nil { + return err + } + *s = tokens + return nil +} + +// Value implements the driver.Valuer interface for database serialization. +func (s TokenItems) Value() (driver.Value, error) { + data, err := json.Marshal(s) + return data, err +} diff --git a/core/token_request.go b/core/token_request.go new file mode 100644 index 0000000..661e704 --- /dev/null +++ b/core/token_request.go @@ -0,0 +1,46 @@ +package core + +import ( + "database/sql/driver" + "encoding/json" +) + +const ( + TokenTypeFswapLP TokenType = iota + TokenTypeRings +) + +type ( + TokenType int + + TokenRequest struct { + Type TokenType `json:"type"` + Asset1 string `json:"asset1,omitempty"` + Asset2 string `json:"asset2,omitempty"` + } + + TokenRequests []*TokenRequest +) + +// Scan implements the sql.Scanner interface for database deserialization. +func (s *TokenRequests) Scan(value interface{}) error { + var d []byte + switch v := value.(type) { + case string: + d = []byte(v) + case []byte: + d = v + } + var tokens TokenRequests + if err := json.Unmarshal(d, &tokens); err != nil { + return err + } + *s = tokens + return nil +} + +// Value implements the driver.Valuer interface for database serialization. +func (s TokenRequests) Value() (driver.Value, error) { + data, err := json.Marshal(s) + return data, err +} diff --git a/handler/action/action.go b/handler/action/action.go deleted file mode 100644 index ed8b274..0000000 --- a/handler/action/action.go +++ /dev/null @@ -1,80 +0,0 @@ -package action - -import ( - "encoding/base64" - "net/http" - - "github.com/fox-one/ftoken/core" - "github.com/fox-one/ftoken/handler/render" - "github.com/fox-one/ftoken/pkg/mtg" - "github.com/fox-one/pkg/httputil/param" - "github.com/fox-one/pkg/uuid" - "github.com/twitchtv/twirp" -) - -func HandleCreateAction(system core.System, walletz core.WalletService, factories []core.Factory) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - var body struct { - TraceID string `json:"trace_id,omitempty"` - Platform string `json:"platform,omitempty"` - Tokens core.Tokens `json:"tokens,omitempty"` - Receiver core.Address `json:"receiver,omitempty"` - } - - if err := param.Binding(r, &body); err != nil { - render.Error(w, twirp.InternalErrorWith(err)) - return - } - - if body.TraceID == "" { - body.TraceID = uuid.New() - } - - if len(body.Tokens) == 0 { - render.Error(w, twirp.RequiredArgumentError("tokens")) - return - } - - memoBts, err := mtg.Encode(body.Tokens, body.Receiver) - if err != nil { - render.Error(w, twirp.InternalErrorWith(err)) - return - } - memo := base64.StdEncoding.EncodeToString(memoBts) - - var factory core.Factory - for _, f := range factories { - if f.Platform() != body.Platform { - continue - } - factory = f - } - if factory == nil { - render.Error(w, twirp.RequiredArgumentError("platform")) - return - } - - if body.Receiver.Destination == "" { - body.Receiver = *system.Addresses[factory.GasAsset()] - } - - tx, err := factory.CreateTransaction(ctx, body.Tokens, &body.Receiver) - if err != nil { - render.Error(w, twirp.InternalErrorWith(err)) - return - } - gas := tx.Gas.Mul(system.Gas.Multiplier) - if min, ok := system.Gas.Mins[factory.Platform()]; ok && gas.LessThan(min) { - gas = min - } - - render.JSON(w, render.H{ - "opponent_id": system.ClientID, - "asset_id": factory.GasAsset(), - "amount": gas, - "memo": memo, - }) - } -} diff --git a/handler/order/order.go b/handler/order/order.go index 5845a03..daf4ac7 100644 --- a/handler/order/order.go +++ b/handler/order/order.go @@ -7,6 +7,7 @@ import ( "github.com/fox-one/ftoken/core" "github.com/fox-one/ftoken/handler/render" "github.com/fox-one/ftoken/handler/render/views" + "github.com/fox-one/ftoken/pkg/token" "github.com/fox-one/pkg/httputil/param" "github.com/fox-one/pkg/uuid" "github.com/go-chi/chi" @@ -39,9 +40,9 @@ func HandleEstimateGas(system core.System, factories []core.Factory) http.Handle factory = f } - var tokens = make(core.Tokens, body.Count) + var tokens = make(core.TokenItems, body.Count) for i := 0; i < body.Count; i++ { - tokens[i] = &core.Token{ + tokens[i] = &core.TokenItem{ TotalSupply: 1000000, } } @@ -51,7 +52,7 @@ func HandleEstimateGas(system core.System, factories []core.Factory) http.Handle return } - tx, err := factory.CreateTransaction(ctx, tokens, system.Addresses[factory.GasAsset()]) + tx, err := factory.CreateTransaction(ctx, tokens, "") if err != nil { render.Error(w, twirp.InternalErrorWith(err)) return @@ -68,15 +69,20 @@ func HandleEstimateGas(system core.System, factories []core.Factory) http.Handle } } -func HandleCreateOrder(system core.System, walletz core.WalletService, orders core.OrderStore, factories []core.Factory) http.HandlerFunc { +func HandleCreateOrder( + system core.System, + assets core.AssetStore, + walletz core.WalletService, + orders core.OrderStore, + factories []core.Factory, +) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var body struct { - TraceID string `json:"trace_id,omitempty"` - Platform string `json:"platform,omitempty"` - Tokens core.Tokens `json:"tokens,omitempty"` - ReceiverAddress *core.Address `json:"receiver_address,omitempty"` + TraceID string `json:"trace_id,omitempty"` + Platform string `json:"platform,omitempty"` + Tokens core.TokenRequests `json:"tokens,omitempty"` } if err := param.Binding(r, &body); err != nil { @@ -88,13 +94,8 @@ func HandleCreateOrder(system core.System, walletz core.WalletService, orders co body.TraceID = uuid.New() } - var tokens core.Tokens - for _, token := range body.Tokens { - if token.Name != "" && token.Symbol != "" && token.TotalSupply > 0 { - tokens = append(tokens, token) - } - } - if len(tokens) == 0 { + tokens, err := token.ExportTokenItems(ctx, assets, body.Tokens) + if err != nil { render.Error(w, twirp.RequiredArgumentError("tokens")) return } @@ -127,9 +128,6 @@ func HandleCreateOrder(system core.System, walletz core.WalletService, orders co Platform: body.Platform, Tokens: tokens, } - if body.ReceiverAddress != nil && body.ReceiverAddress.Destination != "" { - order.Receiver = body.ReceiverAddress - } if err := orders.Create(ctx, order); err != nil { render.Error(w, twirp.InternalErrorWith(err)) return @@ -137,13 +135,13 @@ func HandleCreateOrder(system core.System, walletz core.WalletService, orders co } else { t1, _ := core.EncodeTokens(order.Tokens) t2, _ := core.EncodeTokens(tokens) - if order.UserID != "" && order.Receiver.Destination != body.ReceiverAddress.Destination || !bytes.Equal(t1, t2) { + if !bytes.Equal(t1, t2) { render.Error(w, twirp.NewErrorf(twirp.AlreadyExists, "order with trace already exists")) return } } - tx, err := factory.CreateTransaction(ctx, tokens, system.Addresses[factory.GasAsset()]) + tx, err := factory.CreateTransaction(ctx, tokens, "") if err != nil { render.Error(w, twirp.InternalErrorWith(err)) return @@ -171,6 +169,6 @@ func HandleFetchOrder(orders core.OrderStore) http.HandlerFunc { return } - render.JSON(w, views.OrderView(*order, false)) + render.JSON(w, views.OrderView(*order)) } } diff --git a/handler/render/views/order.go b/handler/render/views/order.go index 05fdfcf..bf67dea 100644 --- a/handler/render/views/order.go +++ b/handler/render/views/order.go @@ -9,30 +9,29 @@ import ( type ( Order struct { - ID uint64 `json:"id,omitempty"` - CreatedAt *time.Time `json:"created_at,omitempty"` - UpdatedAt *time.Time `json:"updated_at,omitempty"` - TraceID string `json:"trace_id,omitempty"` - State core.OrderState `json:"state,omitempty"` - UserID string `json:"user_id,omitempty"` - FeeAsset string `json:"fee_asset,omitempty"` - FeeAmount decimal.Decimal `json:"fee_amount,omitempty"` - Platform string `json:"platform,omitempty"` - Tokens core.Tokens `json:"tokens,omitempty"` - Result *core.Tokens `json:"result,omitempty"` - Receiver *core.Address `json:"receiver,omitempty"` + ID uint64 `json:"id,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` + TraceID string `json:"trace_id,omitempty"` + State core.OrderState `json:"state,omitempty"` + UserID string `json:"user_id,omitempty"` + FeeAsset string `json:"fee_asset,omitempty"` + FeeAmount decimal.Decimal `json:"fee_amount,omitempty"` + Platform string `json:"platform,omitempty"` + TokenRequests core.TokenItems `json:"tokens,omitempty"` + Tokens *core.TokenItems `json:"result,omitempty"` } ) -func OrderView(o core.Order, self bool) Order { +func OrderView(o core.Order) Order { order := Order{ - ID: o.ID, - TraceID: o.TraceID, - State: o.State, - FeeAsset: o.FeeAsset, - FeeAmount: o.FeeAmount, - Platform: o.Platform, - Tokens: o.Tokens, + ID: o.ID, + TraceID: o.TraceID, + State: o.State, + FeeAsset: o.FeeAsset, + FeeAmount: o.FeeAmount, + Platform: o.Platform, + TokenRequests: o.TokenRequests, } if o.CreatedAt.IsZero() { @@ -41,13 +40,8 @@ func OrderView(o core.Order, self bool) Order { if o.UpdatedAt.IsZero() { order.UpdatedAt = &o.UpdatedAt } - if len(o.Result) > 0 { - order.Result = &o.Result - } - - if self { - order.UserID = o.UserID - order.Receiver = o.Receiver + if len(o.Tokens) > 0 { + order.Tokens = &o.Tokens } return order diff --git a/handler/server.go b/handler/server.go index 5b711c4..625011d 100644 --- a/handler/server.go +++ b/handler/server.go @@ -4,7 +4,6 @@ import ( "net/http" "github.com/fox-one/ftoken/core" - "github.com/fox-one/ftoken/handler/action" "github.com/fox-one/ftoken/handler/auth" "github.com/fox-one/ftoken/handler/ip" "github.com/fox-one/ftoken/handler/order" @@ -17,6 +16,7 @@ import ( type ( Server struct { system core.System + assets core.AssetStore orders core.OrderStore txStore core.TransactionStore walletz core.WalletService @@ -26,6 +26,7 @@ type ( func New( system core.System, + assets core.AssetStore, orders core.OrderStore, txStore core.TransactionStore, walletz core.WalletService, @@ -33,6 +34,7 @@ func New( ) Server { return Server{ system: system, + assets: assets, orders: orders, txStore: txStore, walletz: walletz, @@ -52,12 +54,10 @@ func (s Server) Handle() http.Handler { r.Post("/oauth", auth.HandleOauth(s.system)) - r.Post("/actions", action.HandleCreateAction(s.system, s.walletz, s.factories)) - r.Post("/estimate-gas", order.HandleEstimateGas(s.system, s.factories)) r.Route("/orders", func(r chi.Router) { - r.Post("/", order.HandleCreateOrder(s.system, s.walletz, s.orders, s.factories)) + r.Post("/", order.HandleCreateOrder(s.system, s.assets, s.walletz, s.orders, s.factories)) r.Get("/{trace_id}", order.HandleFetchOrder(s.orders)) }) diff --git a/pkg/token/fswap.go b/pkg/token/fswap.go new file mode 100644 index 0000000..dca7def --- /dev/null +++ b/pkg/token/fswap.go @@ -0,0 +1,55 @@ +package token + +import ( + "fmt" + "sort" + + "github.com/fox-one/ftoken/core" +) + +var ( + Assets = []string{ + "31d2ea9c-95eb-3355-b65b-ba096853bc18", // pUSD + "4d8c508b-91c5-375b-92b0-ee702ed2dac5", // USDT + "b91e18ff-a9ae-3dc7-8679-e935d9a4b34b", // USDT@TRON + "5dac5e28-ad13-31ea-869f-41770dfcee09", // USDT@EOS + "815b0b1a-2764-3736-8faa-42d694fa620a", // USDT@OMNI + "9b180ab6-6abe-3dc0-a13f-04169eb34bfa", // USDC + "0ff3f325-4f34-334d-b6c0-a3bd8850fc06", // JPYC + "c6d0c728-2624-429b-8e0d-d9d19b6592fa", // BTC + "43d61dcd-e413-450d-80b8-101d5e903357", // ETH + "c94ac88f-4671-3976-b60a-09064f1811e8", // XIN + } +) + +func assetIndex(assetID string) int { + for i, asset := range Assets { + if asset == assetID { + return i + } + } + return 1000000 +} + +func exportFswapLP(assets []*core.Asset) *core.TokenItem { + sort.Slice(assets, func(i, j int) bool { + i1 := assetIndex(assets[i].AssetID) + i2 := assetIndex(assets[j].AssetID) + if i1 > i2 { + return true + } else if i1 < i2 { + return false + } + + return assets[i].DisplaySymbol < assets[j].DisplaySymbol + }) + + pair := fmt.Sprintf("%s-%s", assets[0].DisplaySymbol, assets[1].DisplaySymbol) + + var supply uint64 = 10000000000 + return &core.TokenItem{ + Name: fmt.Sprintf("4swap LP Token %s", pair), + Symbol: fmt.Sprintf("s%s", pair), + TotalSupply: supply, + } +} diff --git a/pkg/token/rings.go b/pkg/token/rings.go new file mode 100644 index 0000000..1e803fd --- /dev/null +++ b/pkg/token/rings.go @@ -0,0 +1,16 @@ +package token + +import ( + "fmt" + + "github.com/fox-one/ftoken/core" +) + +func exportRings(asset *core.Asset) *core.TokenItem { + var supply uint64 = 10000000000 + return &core.TokenItem{ + Name: fmt.Sprintf("Pando Rings %s", asset.DisplaySymbol), + Symbol: fmt.Sprintf("r%s", asset.DisplaySymbol), + TotalSupply: supply, + } +} diff --git a/pkg/token/token.go b/pkg/token/token.go new file mode 100644 index 0000000..3355df6 --- /dev/null +++ b/pkg/token/token.go @@ -0,0 +1,59 @@ +package token + +import ( + "context" + "errors" + + "github.com/fox-one/ftoken/core" +) + +var ( + ErrInvalidType = errors.New("invalid token type") + ErrAssetUnverified = errors.New("asset unverified") +) + +func ExportTokenItems(ctx context.Context, assets core.AssetStore, reqs core.TokenRequests) (core.TokenItems, error) { + tokens := make(core.TokenItems, 0, len(reqs)) + for _, req := range reqs { + if token, err := ExportTokenItem(ctx, assets, req); err != nil { + return nil, err + } else { + tokens = append(tokens, token) + } + } + return tokens, nil +} + +func ExportTokenItem(ctx context.Context, assets core.AssetStore, req *core.TokenRequest) (*core.TokenItem, error) { + switch req.Type { + case core.TokenTypeFswapLP: + assets, err := assets.Find(ctx, req.Asset1, req.Asset2) + if err != nil { + return nil, err + } else if len(assets) == 2 && assetsVerified(ctx, assets) { + return exportFswapLP(assets), nil + } + return nil, ErrAssetUnverified + + case core.TokenTypeRings: + assets, err := assets.Find(ctx, req.Asset1) + if err != nil { + return nil, err + } else if len(assets) == 1 && assetsVerified(ctx, assets) { + return exportRings(assets[0]), nil + } + return nil, ErrAssetUnverified + + default: + return nil, ErrInvalidType + } +} + +func assetsVerified(ctx context.Context, assets []*core.Asset) bool { + for _, asset := range assets { + if !asset.Verified { + return false + } + } + return true +} diff --git a/quorum/factory.go b/quorum/factory.go index 87d3073..42f529e 100644 --- a/quorum/factory.go +++ b/quorum/factory.go @@ -74,7 +74,7 @@ func (*Factory) GasAsset() string { return EthAsset } -func (f *Factory) CreateTransaction(ctx context.Context, tokens []*core.Token, trace string) (*core.Transaction, error) { +func (f *Factory) CreateTransaction(ctx context.Context, tokens core.TokenItems, trace string) (*core.Transaction, error) { data, err := core.EncodeTokens(tokens) if err != nil { return nil, err @@ -202,7 +202,7 @@ func (f *Factory) ReadTransaction(ctx context.Context, hash string) (*core.Trans if contract.Cap == nil || contract.Cap.Sign() <= 0 { return nil, errors.New("invalid contract cap") } - tx.Tokens = append(tx.Tokens, &core.Token{ + tx.Tokens = append(tx.Tokens, &core.TokenItem{ Name: contract.Name, Symbol: contract.Symbol, TotalSupply: contract.Cap.Uint64(), diff --git a/service/asset/asset.go b/service/asset/asset.go new file mode 100644 index 0000000..2de99df --- /dev/null +++ b/service/asset/asset.go @@ -0,0 +1,38 @@ +package asset + +import ( + "context" + + "github.com/fox-one/ftoken/core" + "github.com/fox-one/mixin-sdk-go" +) + +func New(c *mixin.Client) core.AssetService { + return &assetService{c: c} +} + +type assetService struct { + c *mixin.Client +} + +func (s *assetService) Find(ctx context.Context, assetID string) (*core.Asset, error) { + asset, err := s.c.ReadAsset(ctx, assetID) + if err != nil { + if mixin.IsErrorCodes(err, 10002) { + err = core.ErrAssetNotExist + } + + return nil, err + } + + return convertAsset(asset), nil +} + +func convertAsset(asset *mixin.Asset) *core.Asset { + return &core.Asset{ + AssetID: asset.AssetID, + Name: asset.Name, + Symbol: asset.Symbol, + ChainID: asset.ChainID, + } +} diff --git a/store/asset/asset.go b/store/asset/asset.go new file mode 100644 index 0000000..34a577b --- /dev/null +++ b/store/asset/asset.go @@ -0,0 +1,76 @@ +package asset + +import ( + "context" + + "github.com/fox-one/ftoken/core" + "github.com/fox-one/pkg/store/db" +) + +func init() { + db.RegisterMigrate(func(db *db.DB) error { + tx := db.Update().Model(core.Asset{}) + + if err := tx.AutoMigrate(core.Asset{}).Error; err != nil { + return err + } + + return nil + }) +} + +func New(db *db.DB) core.AssetStore { + return &assetStore{ + db: db, + } +} + +type assetStore struct { + db *db.DB +} + +func (s *assetStore) Save(ctx context.Context, asset *core.Asset) error { + return s.db.Update().Model(asset).Assign(toUpdateParams(asset)).FirstOrCreate(asset).Error +} + +func (s *assetStore) Find(ctx context.Context, ids ...string) ([]*core.Asset, error) { + if len(ids) == 0 { + return nil, nil + } + + var assets []*core.Asset + if err := s.db.View().Where("asset_id IN (?)", ids).Find(&assets).Error; err != nil { + return nil, err + } + return assets, nil +} + +func (s *assetStore) ListAll(ctx context.Context) ([]*core.Asset, error) { + var assets []*core.Asset + if err := s.db.View().Find(&assets).Error; err != nil { + return nil, err + } + + return assets, nil +} + +func toUpdateParams(asset *core.Asset) map[string]interface{} { + params := map[string]interface{}{ + "version": asset.Version + 1, + "verified": asset.Verified, + } + if asset.DisplaySymbol != "" { + params["display_symbol"] = asset.DisplaySymbol + } + return params +} + +func (s *assetStore) Update(ctx context.Context, asset *core.Asset) error { + params := toUpdateParams(asset) + if tx := s.db.Update().Model(asset).Where("version = ?", asset.Version).Update(params); tx.Error != nil { + return tx.Error + } else if tx.RowsAffected == 0 { + return db.ErrOptimisticLock + } + return nil +} diff --git a/store/asset/cache.go b/store/asset/cache.go new file mode 100644 index 0000000..1f5e34c --- /dev/null +++ b/store/asset/cache.go @@ -0,0 +1,63 @@ +package asset + +import ( + "context" + "time" + + "github.com/fox-one/ftoken/core" + "github.com/patrickmn/go-cache" + "golang.org/x/sync/singleflight" +) + +func Cache(store core.AssetStore, exp time.Duration) core.AssetStore { + return &cacheAssetStore{ + AssetStore: store, + cache: cache.New(exp, cache.NoExpiration), + sf: &singleflight.Group{}, + } +} + +type cacheAssetStore struct { + core.AssetStore + cache *cache.Cache + sf *singleflight.Group +} + +func (s *cacheAssetStore) Save(ctx context.Context, asset *core.Asset) error { + s.cache.Delete(asset.AssetID) + if err := s.AssetStore.Save(ctx, asset); err != nil { + return err + } + + s.cache.Delete(asset.AssetID) + return nil +} + +func (s *cacheAssetStore) Find(ctx context.Context, ids ...string) ([]*core.Asset, error) { + if assets, ok := s.itemsFromCache(ctx, ids...); ok { + return assets, nil + } + + assets, err := s.AssetStore.Find(ctx, ids...) + if err != nil { + return nil, err + } + + for _, asset := range assets { + s.cache.SetDefault(asset.AssetID, asset) + } + + return assets, nil +} + +func (s *cacheAssetStore) itemsFromCache(ctx context.Context, ids ...string) ([]*core.Asset, bool) { + var assets = make([]*core.Asset, 0, len(ids)) + for i, id := range ids { + v, ok := s.cache.Get(id) + if !ok { + return nil, false + } + assets[i] = v.(*core.Asset) + } + return assets, true +} diff --git a/store/order/order.go b/store/order/order.go index 8eae5f7..c763cc5 100644 --- a/store/order/order.go +++ b/store/order/order.go @@ -50,7 +50,7 @@ func toUpdateParams(order *core.Order) map[string]interface{} { "version": order.Version + 1, "user_id": order.UserID, "state": order.State, - "result": order.Result, + "tokens": order.Tokens, "fee_amount": order.FeeAmount, "gas_usage": order.GasUsage, "transaction": order.Transaction, diff --git a/worker/order/paid.go b/worker/order/paid.go index 44c9d60..26869a1 100644 --- a/worker/order/paid.go +++ b/worker/order/paid.go @@ -66,11 +66,7 @@ func (w *Worker) handlePaidOrder(ctx context.Context, order *core.Order) error { } if tx.ID == 0 { - var receiver = order.Receiver - if receiver == nil || receiver.Destination == "" { - receiver = w.system.Addresses[order.FeeAsset] - } - tx, err = factory.CreateTransaction(ctx, order.Tokens, receiver) + tx, err = factory.CreateTransaction(ctx, order.TokenRequests, order.TraceID) if err != nil { log.WithError(err).Errorln("factory.CreateTransaction failed") return err diff --git a/worker/order/processing.go b/worker/order/processing.go index 0057f68..a4685fc 100644 --- a/worker/order/processing.go +++ b/worker/order/processing.go @@ -118,7 +118,7 @@ func (w *Worker) handleProcessingOrder(ctx context.Context, order *core.Order) e return err } case core.OrderStateDone: - order.Result = tx.Tokens + order.Tokens = tx.Tokens } if err := w.orders.Update(ctx, order); err != nil { diff --git a/worker/payee/deposit.go b/worker/payee/deposit.go deleted file mode 100644 index 5d75ba6..0000000 --- a/worker/payee/deposit.go +++ /dev/null @@ -1,55 +0,0 @@ -package payee - -import ( - "context" - - "github.com/fox-one/ftoken/core" - "github.com/fox-one/pkg/logger" - "github.com/fox-one/pkg/uuid" - "github.com/lib/pq" - "github.com/sirupsen/logrus" -) - -func (w *Worker) handleDepositSnapshot(ctx context.Context, snapshot *core.Snapshot) error { - log := logger.FromContext(ctx).WithFields(logrus.Fields{ - "snapshot_id": snapshot.SnapshotID, - "pay_asset": snapshot.AssetID, - "amount": snapshot.Amount, - "transaction": snapshot.TransactionHash, - }) - ctx = logger.WithContext(ctx, log) - - tx, err := w.transactions.Find(ctx, snapshot.TransactionHash) - if err != nil { - log.WithError(err).Errorln("transactions.Find") - return err - } - - if tx.ID == 0 { - return nil - } - - order, err := w.orders.Find(ctx, tx.TraceID) - if err != nil { - log.WithError(err).Errorln("orders.Find") - return err - } - - if order.ID > 0 && order.UserID != "" { - if err := w.wallets.CreateTransfers(ctx, []*core.Transfer{ - { - TraceID: uuid.Modify(snapshot.SnapshotID, "forward"), - AssetID: snapshot.AssetID, - Amount: snapshot.Amount, - Threshold: 1, - Opponents: pq.StringArray{order.UserID}, - }, - }); err != nil { - logger.FromContext(ctx).Errorln("CreateTransfers failed") - return err - } - return nil - } - - return nil -} diff --git a/worker/payee/payee.go b/worker/payee/payee.go index c33b0cf..4b9a10b 100644 --- a/worker/payee/payee.go +++ b/worker/payee/payee.go @@ -27,6 +27,7 @@ type ( system core.System properties property.Store + assets core.AssetStore wallets core.WalletStore walletz core.WalletService orders core.OrderStore @@ -40,6 +41,7 @@ func New( cfg Config, system core.System, properties property.Store, + assets core.AssetStore, orders core.OrderStore, transactions core.TransactionStore, wallets core.WalletStore, @@ -59,6 +61,7 @@ func New( clientID: cfg.ClientID, system: system, properties: properties, + assets: assets, wallets: wallets, walletz: walletz, cache: cache.New(time.Hour, time.Hour), @@ -116,16 +119,8 @@ func (w *Worker) run(ctx context.Context) error { snapshotKey := fmt.Sprintf("snapshot:%s", snapshot.SnapshotID) if _, ok := w.cache.Get(snapshotKey); !ok { - switch snapshot.Source { - case "DEPOSIT_CONFIRMED": - if err := w.handleDepositSnapshot(ctx, snapshot); err != nil { - return err - } - - default: - if err := w.handleSnapshot(ctx, snapshot); err != nil { - return err - } + if err := w.handleSnapshot(ctx, snapshot); err != nil { + return err } w.cache.SetDefault(snapshotKey, true) } diff --git a/worker/payee/snapshot.go b/worker/payee/snapshot.go index 4e3240c..5a4d345 100644 --- a/worker/payee/snapshot.go +++ b/worker/payee/snapshot.go @@ -2,10 +2,8 @@ package payee import ( "context" - "encoding/base64" "github.com/fox-one/ftoken/core" - "github.com/fox-one/ftoken/pkg/mtg" "github.com/fox-one/pkg/logger" "github.com/sirupsen/logrus" ) @@ -29,61 +27,20 @@ func (w *Worker) handleSnapshot(ctx context.Context, snapshot *core.Snapshot) er if err != nil { log.WithError(err).Errorln("orders.Find") return err + } else if order.ID == 0 { + log.WithField("order_id", order.ID).WithField("state", order.State).Infoln("skip: order not exist") + return nil } - if order.ID == 0 { - order = &core.Order{ - CreatedAt: snapshot.CreatedAt, - Version: 1, - TraceID: snapshot.TraceID, - State: core.OrderStateNew, - UserID: snapshot.OpponentID, - FeeAsset: snapshot.AssetID, - FeeAmount: snapshot.Amount, - Platform: factory.Platform(), - } - - data := []byte(snapshot.Memo) - if d, err := base64.StdEncoding.DecodeString(snapshot.Memo); err == nil { - data = d - } - - if data, err := mtg.Scan(data, &order.Tokens); err != nil || len(order.Tokens) == 0 { - log.Infoln("refund: scan tokens failed") - return w.refundOrder(ctx, order) - } else { - var receiver core.Address - if _, err := mtg.Scan(data, &receiver); err == nil && receiver.Destination != "" { - order.Receiver = &receiver - } - } - - if err := w.orders.Create(ctx, order); err != nil { - log.WithError(err).Errorln("orders.Create") - return err - } - } else { - if order.FeeAsset != snapshot.AssetID { - log.WithField("order_asset", order.FeeAsset).Infoln("skip: asset not matched") - return nil - } - if order.UserID == "" { - order.UserID = snapshot.OpponentID - } - } - if order.Receiver == nil && snapshot.OpponentID == "" { - log.Infoln("skip: empty reciever / address") + if order.FeeAsset != snapshot.AssetID { + log.WithField("order_asset", order.FeeAsset).Infoln("skip: asset not matched") return nil } if order.State == core.OrderStateNew { order.FeeAmount = snapshot.Amount - receiver := order.Receiver - if receiver == nil || receiver.Destination == "" { - receiver = w.system.Addresses[order.FeeAsset] - } - tx, err := factory.CreateTransaction(ctx, order.Tokens, receiver) + tx, err := factory.CreateTransaction(ctx, order.TokenRequests, order.TraceID) if err != nil { log.WithError(err).Errorln("factory.CreateTransaction") return err