feat: add otx tracking API, add enums, minor fixes

* otx can nw be tracked at /api/track/:trackingId
* moved queries to queries folder
* fixed validation error check in ErrorHandler
* added enum package with enum types
* updated migrations: added enum tables
This commit is contained in:
Mohamed Sohail 2023-02-21 17:34:22 +00:00
parent 140c887341
commit 40cb86f522
Signed by: kamikazechaser
GPG Key ID: 7DD45520C01CD85D
22 changed files with 314 additions and 147 deletions

View File

@ -30,7 +30,7 @@ func initApiServer(custodialContainer *custodial.Custodial) *echo.Echo {
return return
} }
if err.(validator.ValidationErrors) != nil { if _, ok := err.(validator.ValidationErrors); ok {
c.JSON(http.StatusForbidden, api.ErrResp{ c.JSON(http.StatusForbidden, api.ErrResp{
Ok: false, Ok: false,
Code: api.VALIDATION_ERROR, Code: api.VALIDATION_ERROR,
@ -66,6 +66,7 @@ func initApiServer(custodialContainer *custodial.Custodial) *echo.Echo {
apiRoute := server.Group("/api") apiRoute := server.Group("/api")
apiRoute.POST("/account/create", api.CreateAccountHandler(custodialContainer)) apiRoute.POST("/account/create", api.CreateAccountHandler(custodialContainer))
apiRoute.POST("/sign/transfer", api.SignTransferHandler(custodialContainer)) apiRoute.POST("/sign/transfer", api.SignTransferHandler(custodialContainer))
apiRoute.GET("/track/:trackingId", api.TxStatus(custodialContainer.PgStore))
return server return server
} }

View File

@ -28,7 +28,7 @@ var (
func init() { func init() {
flag.StringVar(&confFlag, "config", "config.toml", "Config file location") flag.StringVar(&confFlag, "config", "config.toml", "Config file location")
flag.BoolVar(&debugFlag, "log", false, "Enable debug logging") flag.BoolVar(&debugFlag, "log", false, "Enable debug logging")
flag.StringVar(&queriesFlag, "queries", "queries.sql", "Queries file location") flag.StringVar(&queriesFlag, "queries", "queries/queries.sql", "Queries file location")
flag.Parse() flag.Parse()
lo = initLogger(debugFlag) lo = initLogger(debugFlag)

2
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/VictoriaMetrics/metrics v1.23.1 github.com/VictoriaMetrics/metrics v1.23.1
github.com/bsm/redislock v0.7.2 github.com/bsm/redislock v0.7.2
github.com/celo-org/celo-blockchain v1.7.2 github.com/celo-org/celo-blockchain v1.7.2
github.com/georgysavva/scany/v2 v2.0.0
github.com/go-playground/validator/v10 v10.11.2 github.com/go-playground/validator/v10 v10.11.2
github.com/go-redis/redis/v8 v8.11.5 github.com/go-redis/redis/v8 v8.11.5
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
@ -70,7 +71,6 @@ require (
github.com/prometheus/tsdb v0.10.0 // indirect github.com/prometheus/tsdb v0.10.0 // indirect
github.com/rivo/uniseg v0.4.3 // indirect github.com/rivo/uniseg v0.4.3 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cast v1.3.1 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect

7
go.sum
View File

@ -136,6 +136,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304= github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs=
github.com/consensys/bavard v0.1.8-0.20210406032232-f3452dc9b572/go.mod h1:Bpd0/3mZuaj6Sj+PqrmIquiOKy397AKGThQPaGzNXAQ= github.com/consensys/bavard v0.1.8-0.20210406032232-f3452dc9b572/go.mod h1:Bpd0/3mZuaj6Sj+PqrmIquiOKy397AKGThQPaGzNXAQ=
github.com/consensys/gnark-crypto v0.4.1-0.20210426202927-39ac3d4b3f1f/go.mod h1:815PAHg3wvysy0SyIqanF8gZ0Y1wjk/hrDHD/iT88+Q= github.com/consensys/gnark-crypto v0.4.1-0.20210426202927-39ac3d4b3f1f/go.mod h1:815PAHg3wvysy0SyIqanF8gZ0Y1wjk/hrDHD/iT88+Q=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@ -175,6 +176,8 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/georgysavva/scany/v2 v2.0.0 h1:RGXqxDv4row7/FYoK8MRXAZXqoWF/NM+NP0q50k3DKU=
github.com/georgysavva/scany/v2 v2.0.0/go.mod h1:sigOdh+0qb/+aOs3TVhehVT10p8qJL7K/Zhyz8vWo38=
github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@ -218,6 +221,7 @@ github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
@ -424,6 +428,7 @@ github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -572,7 +577,6 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
@ -602,6 +606,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

View File

@ -29,7 +29,7 @@ func SignTransferHandler(cu *custodial.Custodial) func(echo.Context) error {
From string `json:"from" validate:"required,eth_checksum"` From string `json:"from" validate:"required,eth_checksum"`
To string `json:"to" validate:"required,eth_checksum"` To string `json:"to" validate:"required,eth_checksum"`
VoucherAddress string `json:"voucherAddress" validate:"required,eth_checksum"` VoucherAddress string `json:"voucherAddress" validate:"required,eth_checksum"`
Amount int64 `json:"amount" validate:"required,numeric"` Amount uint64 `json:"amount" validate:"required,numeric"`
} }
if err := c.Bind(&transferRequest); err != nil { if err := c.Bind(&transferRequest); err != nil {

37
internal/api/track.go Normal file
View File

@ -0,0 +1,37 @@
package api
import (
"net/http"
"github.com/grassrootseconomics/cic-custodial/internal/store"
"github.com/labstack/echo/v4"
)
func TxStatus(store store.Store) func(echo.Context) error {
return func(c echo.Context) error {
var txStatusRequest struct {
TrackingId string `param:"trackingId" validate:"required,uuid"`
}
if err := c.Bind(&txStatusRequest); err != nil {
return err
}
if err := c.Validate(txStatusRequest); err != nil {
return err
}
// TODO: handle potential timeouts
txs, err := store.GetTxStatusByTrackingId(c.Request().Context(), txStatusRequest.TrackingId)
if err != nil {
return err
}
return c.JSON(http.StatusOK, OkResp{
Ok: true,
Result: H{
"transactions": txs,
},
})
}
}

View File

@ -5,10 +5,8 @@ type EventEmitter interface {
Publish(subject string, dedupId string, eventPayload interface{}) error Publish(subject string, dedupId string, eventPayload interface{}) error
} }
type ( type EventPayload struct {
EventPayload struct { OtxId uint `json:"otxId"`
OtxId uint `json:"otxId"` TrackingId string `json:"trackingId"`
TrackingId string `json:"trackingId"` TxHash string `json:"txHash"`
TxHash string `json:"txHash"` }
}
)

View File

@ -10,10 +10,11 @@ type Queries struct {
// Keystore // Keystore
WriteKeyPair string `query:"write-key-pair"` WriteKeyPair string `query:"write-key-pair"`
LoadKeyPair string `query:"load-key-pair"` LoadKeyPair string `query:"load-key-pair"`
// OTX // Store
CreateOTX string `query:"create-otx"` CreateOTX string `query:"create-otx"`
// Dispatch CreateDispatchStatus string `query:"create-dispatch-status"`
CreateDispatchStatus string `query:"create-dispatch-status"` UpdateChainStatus string `query:"update-chain-status"`
GetTxStatusByTrackingId string `query:"get-tx-status-by-tracking-id"`
} }
func LoadQueries(q goyesql.Queries) (*Queries, error) { func LoadQueries(q goyesql.Queries) (*Queries, error) {

View File

@ -2,9 +2,9 @@ package store
import ( import (
"context" "context"
)
type Status string "github.com/grassrootseconomics/cic-custodial/pkg/enum"
)
func (s *PostgresStore) CreateDispatchStatus(ctx context.Context, dispatch DispatchStatus) error { func (s *PostgresStore) CreateDispatchStatus(ctx context.Context, dispatch DispatchStatus) error {
if _, err := s.db.Exec( if _, err := s.db.Exec(
@ -18,3 +18,17 @@ func (s *PostgresStore) CreateDispatchStatus(ctx context.Context, dispatch Dispa
return nil return nil
} }
func (s *PostgresStore) UpdateChainStatus(ctx context.Context, txHash string, status enum.OtxStatus, block uint64) error {
if _, err := s.db.Exec(
ctx,
s.queries.UpdateChainStatus,
txHash,
status,
block,
); err != nil {
return err
}
return nil
}

View File

@ -1,8 +1,21 @@
package store package store
import "context" import (
"context"
"time"
func (s *PostgresStore) CreateOTX(ctx context.Context, otx OTX) (uint, error) { "github.com/georgysavva/scany/v2/pgxscan"
)
type TxStatus struct {
Type string `db:"type" json:"txType"`
TxHash string `db:"tx_hash" json:"txHash"`
TransferValue uint64 `db:"transfer_value" json:"transferValue"`
CreatedAt time.Time `db:"created_at" json:"createdAt"`
Status string `db:"status" json:"status"`
}
func (s *PostgresStore) CreateOtx(ctx context.Context, otx OTX) (uint, error) {
var ( var (
id uint id uint
) )
@ -17,6 +30,8 @@ func (s *PostgresStore) CreateOTX(ctx context.Context, otx OTX) (uint, error) {
otx.From, otx.From,
otx.Data, otx.Data,
otx.GasPrice, otx.GasPrice,
otx.GasLimit,
otx.TransferValue,
otx.Nonce, otx.Nonce,
).Scan(&id); err != nil { ).Scan(&id); err != nil {
return id, err return id, err
@ -24,3 +39,21 @@ func (s *PostgresStore) CreateOTX(ctx context.Context, otx OTX) (uint, error) {
return id, nil return id, nil
} }
func (s *PostgresStore) GetTxStatusByTrackingId(ctx context.Context, trackingId string) ([]*TxStatus, error) {
var (
txs []*TxStatus
)
if err := pgxscan.Select(
ctx,
s.db,
&txs,
s.queries.GetTxStatusByTrackingId,
trackingId,
); err != nil {
return nil, err
}
return txs, nil
}

View File

@ -3,30 +3,32 @@ package store
import ( import (
"context" "context"
"github.com/grassrootseconomics/cic-custodial/pkg/status" "github.com/grassrootseconomics/cic-custodial/pkg/enum"
) )
type ( type (
OTX struct { OTX struct {
TrackingId string TrackingId string
Type string Type enum.OtxType
RawTx string RawTx string
TxHash string TxHash string
From string From string
Data string Data string
GasPrice uint64 GasLimit uint64
Nonce uint64 TransferValue uint64
GasPrice uint64
Nonce uint64
} }
DispatchStatus struct { DispatchStatus struct {
OtxId uint OtxId uint
Status status.Status Status enum.OtxStatus
} }
Store interface { Store interface {
// OTX (Custodial originating transactions). CreateOtx(ctx context.Context, otx OTX) (id uint, err error)
CreateOTX(ctx context.Context, otx OTX) (id uint, err error)
// Dispatch status.
CreateDispatchStatus(ctx context.Context, dispatch DispatchStatus) error CreateDispatchStatus(ctx context.Context, dispatch DispatchStatus) error
GetTxStatusByTrackingId(ctx context.Context, trackingId string) ([]*TxStatus, error)
UpdateChainStatus(ctx context.Context, txHash string, status enum.OtxStatus, block uint64) error
} }
) )

View File

@ -11,6 +11,7 @@ import (
"github.com/grassrootseconomics/cic-custodial/internal/events" "github.com/grassrootseconomics/cic-custodial/internal/events"
"github.com/grassrootseconomics/cic-custodial/internal/store" "github.com/grassrootseconomics/cic-custodial/internal/store"
"github.com/grassrootseconomics/cic-custodial/internal/tasker" "github.com/grassrootseconomics/cic-custodial/internal/tasker"
"github.com/grassrootseconomics/cic-custodial/pkg/enum"
"github.com/grassrootseconomics/w3-celo-patch" "github.com/grassrootseconomics/w3-celo-patch"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
) )
@ -67,15 +68,17 @@ func AccountGiftGasProcessor(cu *custodial.Custodial) func(context.Context, *asy
return err return err
} }
id, err := cu.PgStore.CreateOTX(ctx, store.OTX{ id, err := cu.PgStore.CreateOtx(ctx, store.OTX{
TrackingId: payload.TrackingId, TrackingId: payload.TrackingId,
Type: "GIFT_GAS", Type: enum.GIFT_GAS,
RawTx: hexutil.Encode(rawTx), RawTx: hexutil.Encode(rawTx),
TxHash: builtTx.Hash().Hex(), TxHash: builtTx.Hash().Hex(),
From: cu.SystemContainer.PublicKey, From: cu.SystemContainer.PublicKey,
Data: hexutil.Encode(builtTx.Data()), Data: hexutil.Encode(builtTx.Data()),
GasPrice: builtTx.GasPrice().Uint64(), GasPrice: builtTx.GasPrice().Uint64(),
Nonce: builtTx.Nonce(), GasLimit: builtTx.Gas(),
TransferValue: cu.SystemContainer.GiftableGasValue.Uint64(),
Nonce: builtTx.Nonce(),
}) })
if err != nil { if err != nil {
return err return err

View File

@ -10,6 +10,7 @@ import (
"github.com/grassrootseconomics/cic-custodial/internal/events" "github.com/grassrootseconomics/cic-custodial/internal/events"
"github.com/grassrootseconomics/cic-custodial/internal/store" "github.com/grassrootseconomics/cic-custodial/internal/store"
"github.com/grassrootseconomics/cic-custodial/internal/tasker" "github.com/grassrootseconomics/cic-custodial/internal/tasker"
"github.com/grassrootseconomics/cic-custodial/pkg/enum"
"github.com/grassrootseconomics/w3-celo-patch" "github.com/grassrootseconomics/w3-celo-patch"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
) )
@ -75,15 +76,17 @@ func GiftVoucherProcessor(cu *custodial.Custodial) func(context.Context, *asynq.
return err return err
} }
id, err := cu.PgStore.CreateOTX(ctx, store.OTX{ id, err := cu.PgStore.CreateOtx(ctx, store.OTX{
TrackingId: payload.TrackingId, TrackingId: payload.TrackingId,
Type: "GIFT_VOUCHER", Type: enum.ACCOUNT_REGISTER,
RawTx: hexutil.Encode(rawTx), RawTx: hexutil.Encode(rawTx),
TxHash: builtTx.Hash().Hex(), TxHash: builtTx.Hash().Hex(),
From: cu.SystemContainer.PublicKey, From: cu.SystemContainer.PublicKey,
Data: hexutil.Encode(builtTx.Data()), Data: hexutil.Encode(builtTx.Data()),
GasPrice: builtTx.GasPrice().Uint64(), GasPrice: builtTx.GasPrice().Uint64(),
Nonce: builtTx.Nonce(), GasLimit: builtTx.Gas(),
TransferValue: cu.SystemContainer.GiftableTokenValue.Uint64(),
Nonce: builtTx.Nonce(),
}) })
if err != nil { if err != nil {

View File

@ -12,6 +12,7 @@ import (
"github.com/grassrootseconomics/cic-custodial/internal/events" "github.com/grassrootseconomics/cic-custodial/internal/events"
"github.com/grassrootseconomics/cic-custodial/internal/store" "github.com/grassrootseconomics/cic-custodial/internal/store"
"github.com/grassrootseconomics/cic-custodial/internal/tasker" "github.com/grassrootseconomics/cic-custodial/internal/tasker"
"github.com/grassrootseconomics/cic-custodial/pkg/enum"
"github.com/grassrootseconomics/w3-celo-patch" "github.com/grassrootseconomics/w3-celo-patch"
"github.com/grassrootseconomics/w3-celo-patch/module/eth" "github.com/grassrootseconomics/w3-celo-patch/module/eth"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
@ -82,15 +83,17 @@ func AccountRefillGasProcessor(cu *custodial.Custodial) func(context.Context, *a
return err return err
} }
id, err := cu.PgStore.CreateOTX(ctx, store.OTX{ id, err := cu.PgStore.CreateOtx(ctx, store.OTX{
TrackingId: payload.TrackingId, TrackingId: payload.TrackingId,
Type: "REFILL_GAS", Type: enum.REFILL_GAS,
RawTx: hexutil.Encode(rawTx), RawTx: hexutil.Encode(rawTx),
TxHash: builtTx.Hash().Hex(), TxHash: builtTx.Hash().Hex(),
From: cu.SystemContainer.PublicKey, From: cu.SystemContainer.PublicKey,
Data: hexutil.Encode(builtTx.Data()), Data: hexutil.Encode(builtTx.Data()),
GasPrice: builtTx.GasPrice().Uint64(), GasPrice: builtTx.GasPrice().Uint64(),
Nonce: builtTx.Nonce(), GasLimit: builtTx.Gas(),
TransferValue: cu.SystemContainer.GiftableGasValue.Uint64(),
Nonce: builtTx.Nonce(),
}) })
if err != nil { if err != nil {
return err return err

View File

@ -11,6 +11,7 @@ import (
"github.com/grassrootseconomics/cic-custodial/internal/events" "github.com/grassrootseconomics/cic-custodial/internal/events"
"github.com/grassrootseconomics/cic-custodial/internal/store" "github.com/grassrootseconomics/cic-custodial/internal/store"
"github.com/grassrootseconomics/cic-custodial/internal/tasker" "github.com/grassrootseconomics/cic-custodial/internal/tasker"
"github.com/grassrootseconomics/cic-custodial/pkg/enum"
"github.com/grassrootseconomics/w3-celo-patch" "github.com/grassrootseconomics/w3-celo-patch"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
) )
@ -76,14 +77,15 @@ func AccountRegisterOnChainProcessor(cu *custodial.Custodial) func(context.Conte
return err return err
} }
id, err := cu.PgStore.CreateOTX(ctx, store.OTX{ id, err := cu.PgStore.CreateOtx(ctx, store.OTX{
TrackingId: payload.TrackingId, TrackingId: payload.TrackingId,
Type: "ACCOUNT_REGISTER", Type: enum.ACCOUNT_REGISTER,
RawTx: hexutil.Encode(rawTx), RawTx: hexutil.Encode(rawTx),
TxHash: builtTx.Hash().Hex(), TxHash: builtTx.Hash().Hex(),
From: cu.SystemContainer.PublicKey, From: cu.SystemContainer.PublicKey,
Data: hexutil.Encode(builtTx.Data()), Data: hexutil.Encode(builtTx.Data()),
GasPrice: builtTx.GasPrice().Uint64(), GasPrice: builtTx.GasPrice().Uint64(),
GasLimit: builtTx.Gas(),
Nonce: builtTx.Nonce(), Nonce: builtTx.Nonce(),
}) })
if err != nil { if err != nil {

View File

@ -11,7 +11,7 @@ import (
"github.com/grassrootseconomics/cic-custodial/internal/custodial" "github.com/grassrootseconomics/cic-custodial/internal/custodial"
"github.com/grassrootseconomics/cic-custodial/internal/events" "github.com/grassrootseconomics/cic-custodial/internal/events"
"github.com/grassrootseconomics/cic-custodial/internal/store" "github.com/grassrootseconomics/cic-custodial/internal/store"
"github.com/grassrootseconomics/cic-custodial/pkg/status" "github.com/grassrootseconomics/cic-custodial/pkg/enum"
"github.com/grassrootseconomics/w3-celo-patch/module/eth" "github.com/grassrootseconomics/w3-celo-patch/module/eth"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
) )
@ -43,15 +43,15 @@ func DispatchTx(cu *custodial.Custodial) func(context.Context, *asynq.Task) erro
ctx, ctx,
eth.SendTx(payload.Tx).Returns(&dispathchTx), eth.SendTx(payload.Tx).Returns(&dispathchTx),
); err != nil { ); err != nil {
dispatchStatus.Status = status.Unknown
switch err.Error() { switch err.Error() {
case celoutils.ErrGasPriceLow: case celoutils.ErrGasPriceLow:
dispatchStatus.Status = status.FailGasPrice dispatchStatus.Status = enum.FAIL_LOW_GAS_PRICE
case celoutils.ErrInsufficientGas: case celoutils.ErrInsufficientGas:
dispatchStatus.Status = status.FailInsufficientGas dispatchStatus.Status = enum.FAIL_NO_GAS
case celoutils.ErrNonceLow: case celoutils.ErrNonceLow:
dispatchStatus.Status = status.FailNonce dispatchStatus.Status = enum.FAIL_LOW_NONCE
default:
dispatchStatus.Status = enum.FAIL_UNKNOWN_RPC_ERROR
} }
if err := cu.PgStore.CreateDispatchStatus(ctx, dispatchStatus); err != nil { if err := cu.PgStore.CreateDispatchStatus(ctx, dispatchStatus); err != nil {
@ -65,7 +65,7 @@ func DispatchTx(cu *custodial.Custodial) func(context.Context, *asynq.Task) erro
return fmt.Errorf("dispatch: failed %v: %w", err, asynq.SkipRetry) return fmt.Errorf("dispatch: failed %v: %w", err, asynq.SkipRetry)
} }
dispatchStatus.Status = status.Successful dispatchStatus.Status = enum.IN_NETWORK
if err := cu.PgStore.CreateDispatchStatus(ctx, dispatchStatus); err != nil { if err := cu.PgStore.CreateDispatchStatus(ctx, dispatchStatus); err != nil {
return fmt.Errorf("dispatch: failed %v: %w", err, asynq.SkipRetry) return fmt.Errorf("dispatch: failed %v: %w", err, asynq.SkipRetry)

View File

@ -12,6 +12,7 @@ import (
"github.com/grassrootseconomics/cic-custodial/internal/events" "github.com/grassrootseconomics/cic-custodial/internal/events"
"github.com/grassrootseconomics/cic-custodial/internal/store" "github.com/grassrootseconomics/cic-custodial/internal/store"
"github.com/grassrootseconomics/cic-custodial/internal/tasker" "github.com/grassrootseconomics/cic-custodial/internal/tasker"
"github.com/grassrootseconomics/cic-custodial/pkg/enum"
"github.com/grassrootseconomics/w3-celo-patch" "github.com/grassrootseconomics/w3-celo-patch"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
) )
@ -22,7 +23,7 @@ type (
From string `json:"from" ` From string `json:"from" `
To string `json:"to"` To string `json:"to"`
VoucherAddress string `json:"voucherAddress"` VoucherAddress string `json:"voucherAddress"`
Amount int64 `json:"amount"` Amount uint64 `json:"amount"`
} }
transferEventPayload struct { transferEventPayload struct {
@ -71,7 +72,7 @@ func SignTransfer(cu *custodial.Custodial) func(context.Context, *asynq.Task) er
} }
}() }()
input, err := cu.SystemContainer.Abis["transfer"].EncodeArgs(w3.A(payload.To), big.NewInt(payload.Amount)) input, err := cu.SystemContainer.Abis["transfer"].EncodeArgs(w3.A(payload.To), new(big.Int).SetUint64(payload.Amount))
if err != nil { if err != nil {
return err return err
} }
@ -97,15 +98,17 @@ func SignTransfer(cu *custodial.Custodial) func(context.Context, *asynq.Task) er
return err return err
} }
id, err := cu.PgStore.CreateOTX(ctx, store.OTX{ id, err := cu.PgStore.CreateOtx(ctx, store.OTX{
TrackingId: payload.TrackingId, TrackingId: payload.TrackingId,
Type: "TRANSFER", Type: enum.TRANSFER_VOUCHER,
RawTx: hexutil.Encode(rawTx), RawTx: hexutil.Encode(rawTx),
TxHash: builtTx.Hash().Hex(), TxHash: builtTx.Hash().Hex(),
From: payload.From, From: payload.From,
Data: hexutil.Encode(builtTx.Data()), Data: hexutil.Encode(builtTx.Data()),
GasPrice: builtTx.GasPrice().Uint64(), GasPrice: builtTx.GasPrice().Uint64(),
Nonce: builtTx.Nonce(), GasLimit: builtTx.Gas(),
TransferValue: payload.Amount,
Nonce: builtTx.Nonce(),
}) })
if err != nil { if err != nil {
return err return err

View File

@ -1,23 +1,53 @@
-- Otx tx type enum table
CREATE TABLE IF NOT EXISTS otx_tx_type (
value TEXT PRIMARY KEY
);
INSERT INTO otx_tx_type (value) VALUES
('GIFT_GAS'),
('ACCOUNT_REGISTER'),
('GIFT_VOUCHER'),
('REFILL_GAS'),
('TRANSFER_VOUCHER');
-- Origin tx table -- Origin tx table
CREATE TABLE IF NOT EXISTS otx ( CREATE TABLE IF NOT EXISTS otx_sign (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tracking_id TEXT NOT NULL, tracking_id TEXT NOT NULL,
"type" TEXT NOT NULL, "type" TEXT REFERENCES otx_tx_type(value) NOT NULL,
raw_tx TEXT NOT NULL, raw_tx TEXT NOT NULL,
tx_hash TEXT NOT NULL, tx_hash TEXT NOT NULL,
"from" TEXT NOT NULL, "from" TEXT NOT NULL,
"data" TEXT NOT NULL, "data" TEXT NOT NULL,
gas_price bigint NOT NULL, gas_price bigint NOT NULL,
gas_limit bigint NOT NULL,
transfer_value bigint NOT NULL,
nonce int NOT NULL, nonce int NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX IF NOT EXISTS tx_hash_idx ON otx USING hash(tx_hash); CREATE INDEX IF NOT EXISTS tx_hash_idx ON otx_sign USING hash(tx_hash);
CREATE INDEX IF NOT EXISTS from_idx ON otx USING hash("from"); CREATE INDEX IF NOT EXISTS from_idx ON otx_sign USING hash("from");
-- Otx dispatch status enum table
-- Enforces referential integrity on the dispatch table
CREATE TABLE IF NOT EXISTS otx_dispatch_status_type (
value TEXT PRIMARY KEY
);
INSERT INTO otx_dispatch_status_type (value) VALUES
('IN_NETWORK'),
('OBSOLETE'),
('SUCCESS'),
('FAIL_NO_GAS'),
('FAIL_LOW_NONCE'),
('FAIL_LOW_GAS_PRICE'),
('FAIL_UNKNOWN_RPC_ERROR'),
('REVERTED');
-- Dispatch status table -- Dispatch status table
CREATE TABLE IF NOT EXISTS dispatch ( CREATE TABLE IF NOT EXISTS otx_dispatch (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
otx_id INT REFERENCES otx(id), otx_id INT REFERENCES otx_sign(id),
"status" TEXT NOT NULL, "status" TEXT REFERENCES otx_dispatch_status_type(value) NOT NULL,
"block" bigint,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE INDEX IF NOT EXISTS status_idx ON otx_dispatch("status");

27
pkg/enum/enum.go Normal file
View File

@ -0,0 +1,27 @@
package enum
type (
// OtxStatus represents enum-like values received in the dispatcher from the RPC node or Network callback.
// It includes a subset of well-known and likely failures the dispatcher may encounter.
OtxStatus string
// OtxType reprsents the specific type of signed transaction.
OtxType string
)
// NOTE: These values must also be inserted/updated into db to enforce referential integrity.
const (
IN_NETWORK OtxStatus = "IN_NETWORK"
OBSOLETE OtxStatus = "OBSOLETE"
SUCCESS OtxStatus = "SUCCESS"
FAIL_NO_GAS OtxStatus = "FAIL_NO_GAS"
FAIL_LOW_NONCE OtxStatus = "FAIL_LOW_NONCE"
FAIL_LOW_GAS_PRICE OtxStatus = "FAIL_LOW_GAS_PRICE"
FAIL_UNKNOWN_RPC_ERROR OtxStatus = "FAIL_UNKNOWN_RPC_ERROR"
REVERTED OtxStatus = "REVERTED"
GIFT_GAS OtxType = "GIFT_GAS"
ACCOUNT_REGISTER OtxType = "ACCOUNT_REGISTER"
GIFT_VOUCHER OtxType = "GIFT_VOUCHER"
REFILL_GAS OtxType = "REFILL_GAS"
TRANSFER_VOUCHER OtxType = "TRANSFER_VOUCHER"
)

View File

@ -1,13 +0,0 @@
package status
// Status represents enum-like values received in the dispatcher from the RPC node.
// It includes a subset of well-known and likely failures the dispatcher may encounter.
type Status string
const (
FailGasPrice = "FAIL_LOW_GAS_PRICE"
FailInsufficientGas = "FAIL_NO_GAS"
FailNonce = "FAIL_LOW_NONCE"
Successful = "SUCCESSFUL"
Unknown = "UNKNOWN"
)

View File

@ -1,47 +0,0 @@
-- Keystore queries
--name: write-key-pair
-- Save hex encoded private key
-- $1: public_key
-- $2: private_key
INSERT INTO keystore(public_key, private_key) VALUES($1, $2) RETURNING id
--name: load-key-pair
-- Load saved key pair
-- $1: public_key
SELECT private_key FROM keystore WHERE public_key=$1
-- OTX queries
--name: create-otx
-- Create a new locally originating tx
-- $1: tracking_id
-- $2: type
-- $3: raw_tx
-- $4: tx_hash
-- $5: from
-- $6: data
-- $7: gas_price
-- $8: nonce
INSERT INTO otx(
tracking_id,
"type",
raw_tx,
tx_hash,
"from",
"data",
gas_price,
nonce
) VALUES($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id
-- Dispatch status queries
--name: create-dispatch-status
-- Create a new dispatch status
-- $1: otx_id
-- $2: status
INSERT INTO dispatch(
otx_id,
"status"
) VALUES($1, $2) RETURNING id

65
queries/queries.sql Normal file
View File

@ -0,0 +1,65 @@
--name: write-key-pair
-- Save hex encoded private key
-- $1: public_key
-- $2: private_key
INSERT INTO keystore(public_key, private_key) VALUES($1, $2) RETURNING id
--name: load-key-pair
-- Load saved key pair
-- $1: public_key
SELECT private_key FROM keystore WHERE public_key=$1
--name: create-otx
-- Create a new locally originating tx
-- $1: tracking_id
-- $2: type
-- $3: raw_tx
-- $4: tx_hash
-- $5: from
-- $6: data
-- $7: gas_price
-- $8: gas_limit
-- $9: transfer_value
-- $10: nonce
INSERT INTO otx_sign(
tracking_id,
"type",
raw_tx,
tx_hash,
"from",
"data",
gas_price,
gas_limit,
transfer_value,
nonce
) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id
--name: create-dispatch-status
-- Create a new dispatch status
-- $1: otx_id
-- $2: status
INSERT INTO otx_dispatch(
otx_id,
"status"
) VALUES($1, $2) RETURNING id
--name: update-chain-status
-- Updates the status of the dispatched tx with the chain mine status
-- $1: tx_hash
-- $2: status
-- $3: block
UPDATE otx_dispatch SET "status" = $2, "block" = $3 WHERE otx_dispatch.id = (
SELECT otx_dispatch.id FROM otx_dispatch
INNER JOIN otx_sign ON otx_dispatch.otx_id = otx_sign.id
WHERE otx_sign.tx_hash = $1
AND otx_dispatch.status = 'IN_NETWORK'
)
--name: get-tx-status-by-tracking-id
-- Gets tx status's from possible multiple txs with the same tracking_id
-- $1: tracking_id
SELECT otx_sign.type, otx_sign.tx_hash, otx_sign.transfer_value, otx_sign.created_at, otx_dispatch.status FROM otx_sign
INNER JOIN otx_dispatch ON otx_sign.id = otx_dispatch.otx_id
WHERE otx_sign.tracking_id = $1
-- TODO: Scroll by status type with cursor pagination