diff --git a/cmd/service/api.go b/cmd/service/api.go index 66ad3df..03d183e 100644 --- a/cmd/service/api.go +++ b/cmd/service/api.go @@ -50,6 +50,7 @@ func initApiServer(custodialContainer *custodial.Custodial) *echo.Echo { apiRoute.POST("/account/create", api.HandleAccountCreate(custodialContainer)) apiRoute.GET("/account/status/:address", api.HandleNetworkAccountStatus(custodialContainer)) apiRoute.POST("/sign/transfer", api.HandleSignTransfer(custodialContainer)) + apiRoute.POST("/sign/transferAuth", api.HandleSignTranserAuthorization(custodialContainer)) apiRoute.GET("/track/:trackingId", api.HandleTrackTx(custodialContainer)) return server diff --git a/cmd/service/tasker.go b/cmd/service/tasker.go index e2b536d..b3fd640 100644 --- a/cmd/service/tasker.go +++ b/cmd/service/tasker.go @@ -37,6 +37,7 @@ func initTasker(custodialContainer *custodial.Custodial, redisPool *redis.RedisP taskerServer.RegisterHandlers(tasker.AccountRegisterTask, task.AccountRegisterOnChainProcessor(custodialContainer)) taskerServer.RegisterHandlers(tasker.AccountRefillGasTask, task.AccountRefillGasProcessor(custodialContainer)) taskerServer.RegisterHandlers(tasker.SignTransferTask, task.SignTransfer(custodialContainer)) + taskerServer.RegisterHandlers(tasker.SignTransferTaskAuth, task.SignTransferAuthorizationProcessor(custodialContainer)) taskerServer.RegisterHandlers(tasker.DispatchTxTask, task.DispatchTx(custodialContainer)) return taskerServer diff --git a/docs/docs.go b/docs/docs.go index c2a4b69..7a73f65 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -158,6 +158,66 @@ const docTemplate = `{ } } }, + "/sign/transferAuth": { + "post": { + "description": "Sign and dispatch a transfer authorization (approve) request.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "network" + ], + "summary": "Sign and dispatch a transfer authorization (approve) request.", + "parameters": [ + { + "description": "Sign Transfer Authorization (approve) Request", + "name": "signTransferAuthorzationRequest", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "amount": { + "type": "integer" + }, + "authorizedAddress": { + "type": "string" + }, + "authorizer": { + "type": "string" + }, + "voucherAddress": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.OkResp" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrResp" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.ErrResp" + } + } + } + } + }, "/track/{trackingId}": { "get": { "description": "Track an OTX (Origin transaction) status.", @@ -243,6 +303,8 @@ var SwaggerInfo = &swag.Spec{ Description: "Interact with CIC Custodial API", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", } func init() { diff --git a/docs/swagger.json b/docs/swagger.json index 03bcfcf..6d179fb 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -150,6 +150,66 @@ } } }, + "/sign/transferAuth": { + "post": { + "description": "Sign and dispatch a transfer authorization (approve) request.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "network" + ], + "summary": "Sign and dispatch a transfer authorization (approve) request.", + "parameters": [ + { + "description": "Sign Transfer Authorization (approve) Request", + "name": "signTransferAuthorzationRequest", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "amount": { + "type": "integer" + }, + "authorizedAddress": { + "type": "string" + }, + "authorizer": { + "type": "string" + }, + "voucherAddress": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/api.OkResp" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/api.ErrResp" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/api.ErrResp" + } + } + } + } + }, "/track/{trackingId}": { "get": { "description": "Track an OTX (Origin transaction) status.", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 56f98f6..22adf23 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -117,6 +117,45 @@ paths: summary: Sign and dispatch transfer request. tags: - network + /sign/transferAuth: + post: + consumes: + - application/json + description: Sign and dispatch a transfer authorization (approve) request. + parameters: + - description: Sign Transfer Authorization (approve) Request + in: body + name: signTransferAuthorzationRequest + required: true + schema: + properties: + amount: + type: integer + authorizedAddress: + type: string + authorizer: + type: string + voucherAddress: + type: string + type: object + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/api.OkResp' + "400": + description: Bad Request + schema: + $ref: '#/definitions/api.ErrResp' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/api.ErrResp' + summary: Sign and dispatch a transfer authorization (approve) request. + tags: + - network /track/{trackingId}: get: consumes: diff --git a/go.mod b/go.mod index 4997ba3..c6201a0 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,6 @@ require ( github.com/knadh/koanf/providers/file v0.1.0 github.com/knadh/koanf/v2 v2.0.1 github.com/labstack/echo/v4 v4.10.2 - github.com/labstack/gommon v0.4.0 github.com/nats-io/nats.go v1.25.0 github.com/redis/go-redis/v9 v9.0.4 github.com/swaggo/echo-swagger v1.4.0 @@ -77,6 +76,7 @@ require ( github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/labstack/gommon v0.4.0 // indirect github.com/leodido/go-urn v1.2.3 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/go.sum b/go.sum index 4b6a51e..f619860 100644 --- a/go.sum +++ b/go.sum @@ -275,10 +275,6 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= github.com/grassrootseconomics/asynq v0.25.0 h1:2zSz5YwNLu/oCTm/xfNixn86i9aw4zth9Dl0dc2kFEs= github.com/grassrootseconomics/asynq v0.25.0/go.mod h1:pe2XOdK1eIbTgTmRFHIYl75lvVuTPJxZq2T9Ocz/+2s= -github.com/grassrootseconomics/celoutils v1.2.1 h1:ndM4h7Df0d57m2kdRXRStrnunqOL61wQ51rnOanX1KI= -github.com/grassrootseconomics/celoutils v1.2.1/go.mod h1:Uo5YRy6AGLAHDZj9jaOI+AWoQ1H3L0v79728pPMkm9Q= -github.com/grassrootseconomics/celoutils v1.3.0 h1:0NTdYh0jboGlVnfML7fbgWLjrC0jA+F1J/Ze01IkNlY= -github.com/grassrootseconomics/celoutils v1.3.0/go.mod h1:Uo5YRy6AGLAHDZj9jaOI+AWoQ1H3L0v79728pPMkm9Q= github.com/grassrootseconomics/celoutils v1.4.0 h1:AJNKiOpfnQqZ3kRxeUlhWH/zlDDjhtbs/OzAMb5zU4A= github.com/grassrootseconomics/celoutils v1.4.0/go.mod h1:Uo5YRy6AGLAHDZj9jaOI+AWoQ1H3L0v79728pPMkm9Q= github.com/grassrootseconomics/w3-celo-patch v0.2.0 h1:YqibbPzX0tQKmxU1nUGzThPKk/fiYeYZY6Aif3eyu8U= diff --git a/internal/api/sign.go b/internal/api/sign_transfer.go similarity index 97% rename from internal/api/sign.go rename to internal/api/sign_transfer.go index 7cea418..1a37ed8 100644 --- a/internal/api/sign.go +++ b/internal/api/sign_transfer.go @@ -31,7 +31,7 @@ func HandleSignTransfer(cu *custodial.Custodial) func(echo.Context) error { From string `json:"from" validate:"required,eth_addr_checksum"` To string `json:"to" validate:"required,eth_addr_checksum"` VoucherAddress string `json:"voucherAddress" validate:"required,eth_addr_checksum"` - Amount uint64 `json:"amount" validate:"required"` + Amount uint64 `json:"amount" validate:"gt=0"` } ) diff --git a/internal/api/sign_transfer_auth.go b/internal/api/sign_transfer_auth.go new file mode 100644 index 0000000..410f166 --- /dev/null +++ b/internal/api/sign_transfer_auth.go @@ -0,0 +1,107 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/google/uuid" + "github.com/grassrootseconomics/cic-custodial/internal/custodial" + "github.com/grassrootseconomics/cic-custodial/internal/tasker" + "github.com/grassrootseconomics/cic-custodial/internal/tasker/task" + "github.com/labstack/echo/v4" +) + +// Max 10k vouchers per approval session +const approvalSafetyLimit = 10000 * 1000000 + +// HandleSignTransferAuthorization godoc +// +// @Summary Sign and dispatch a transfer authorization (approve) request. +// @Description Sign and dispatch a transfer authorization (approve) request. +// @Tags network +// @Accept json +// @Produce json +// @Param signTransferAuthorzationRequest body object{amount=uint64,authorizer=string,authorizedAddress=string,voucherAddress=string} true "Sign Transfer Authorization (approve) Request" +// @Success 200 {object} OkResp +// @Failure 400 {object} ErrResp +// @Failure 500 {object} ErrResp +// @Router /sign/transferAuth [post] +func HandleSignTranserAuthorization(cu *custodial.Custodial) func(echo.Context) error { + return func(c echo.Context) error { + var ( + req struct { + Amount uint64 `json:"amount" validate:"gte=0"` + Authorizer string `json:"authorizer" validate:"required,eth_addr_checksum"` + AuthorizedAddress string `json:"authorizedAddress" validate:"required,eth_addr_checksum"` + VoucherAddress string `json:"voucherAddress" validate:"required,eth_addr_checksum"` + } + ) + + if err := c.Bind(&req); err != nil { + return NewBadRequestError(ErrInvalidJSON) + } + + if err := c.Validate(req); err != nil { + return err + } + + accountActive, gasLock, err := cu.Store.GetAccountStatus(c.Request().Context(), req.Authorizer) + if err != nil { + return err + } + + if req.Amount > approvalSafetyLimit { + return c.JSON(http.StatusForbidden, ErrResp{ + Ok: false, + Message: "Approval amount per session exceeds 10k.", + }) + } + + if !accountActive { + return c.JSON(http.StatusForbidden, ErrResp{ + Ok: false, + Message: "Account pending activation. Try again later.", + }) + } + + if gasLock { + return c.JSON(http.StatusForbidden, ErrResp{ + Ok: false, + Message: "Gas lock. Gas balance unavailable. Try again later.", + }) + } + + trackingId := uuid.NewString() + + taskPayload, err := json.Marshal(task.TransferAuthPayload{ + TrackingId: trackingId, + Amount: req.Amount, + Authorizer: req.Authorizer, + AuthorizedAddress: req.AuthorizedAddress, + VoucherAddress: req.VoucherAddress, + }) + if err != nil { + return err + } + + _, err = cu.TaskerClient.CreateTask( + c.Request().Context(), + tasker.SignTransferTaskAuth, + tasker.DefaultPriority, + &tasker.Task{ + Id: trackingId, + Payload: taskPayload, + }, + ) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, OkResp{ + Ok: true, + Result: H{ + "trackingId": trackingId, + }, + }) + } +} diff --git a/internal/tasker/task/account_refill_gas.go b/internal/tasker/task/account_refill_gas.go index 1f4cff6..3e69005 100644 --- a/internal/tasker/task/account_refill_gas.go +++ b/internal/tasker/task/account_refill_gas.go @@ -17,10 +17,6 @@ import ( "github.com/hibiken/asynq" ) -const ( - gasGiveToLimit = 250000 -) - func AccountRefillGasProcessor(cu *custodial.Custodial) func(context.Context, *asynq.Task) error { return func(ctx context.Context, t *asynq.Task) error { var ( diff --git a/internal/tasker/task/sign_transfer_auth.go b/internal/tasker/task/sign_transfer_auth.go index 85c4faf..95acabc 100644 --- a/internal/tasker/task/sign_transfer_auth.go +++ b/internal/tasker/task/sign_transfer_auth.go @@ -4,9 +4,9 @@ import ( "context" "encoding/json" "math/big" + "time" "github.com/bsm/redislock" - "github.com/celo-org/celo-blockchain/accounts/abi" "github.com/celo-org/celo-blockchain/common/hexutil" "github.com/grassrootseconomics/celoutils" "github.com/grassrootseconomics/cic-custodial/internal/custodial" @@ -18,9 +18,9 @@ import ( ) type TransferAuthPayload struct { - AuthorizeFor string `json:"authorizeFor"` + Amount uint64 `json:"amount"` + Authorizer string `json:"authorizer"` AuthorizedAddress string `json:"authorizedAddress"` - Revoke bool `json:"revoke"` TrackingId string `json:"trackingId"` VoucherAddress string `json:"voucherAddress"` } @@ -39,7 +39,7 @@ func SignTransferAuthorizationProcessor(cu *custodial.Custodial) func(context.Co lock, err := cu.LockProvider.Obtain( ctx, - lockPrefix+payload.AuthorizeFor, + lockPrefix+payload.Authorizer, lockTimeout, &redislock.Options{ RetryStrategy: lockRetry(), @@ -50,31 +50,26 @@ func SignTransferAuthorizationProcessor(cu *custodial.Custodial) func(context.Co } defer lock.Release(ctx) - key, err := cu.Store.LoadPrivateKey(ctx, payload.AuthorizeFor) + key, err := cu.Store.LoadPrivateKey(ctx, payload.Authorizer) if err != nil { return err } - nonce, err := cu.Noncestore.Acquire(ctx, payload.AuthorizeFor) + nonce, err := cu.Noncestore.Acquire(ctx, payload.Authorizer) if err != nil { return err } defer func() { if err != nil { - if nErr := cu.Noncestore.Return(ctx, payload.AuthorizeFor); nErr != nil { + if nErr := cu.Noncestore.Return(ctx, payload.Authorizer); nErr != nil { err = nErr } } }() - authorizeAmount := big.NewInt(0).Sub(abi.MaxUint256, big.NewInt(1)) - if payload.Revoke { - authorizeAmount = big.NewInt(0) - } - input, err := cu.Abis[custodial.Approve].EncodeArgs( celoutils.HexToAddress(payload.AuthorizedAddress), - authorizeAmount, + new(big.Int).SetUint64(payload.Amount), ) if err != nil { return err @@ -118,7 +113,7 @@ func SignTransferAuthorizationProcessor(cu *custodial.Custodial) func(context.Co if err := cu.CeloProvider.Client.CallCtx( ctx, - eth.Balance(celoutils.HexToAddress(payload.AuthorizeFor), nil).Returns(&networkBalance), + eth.Balance(celoutils.HexToAddress(payload.Authorizer), nil).Returns(&networkBalance), ); err != nil { return err } @@ -143,8 +138,36 @@ func SignTransferAuthorizationProcessor(cu *custodial.Custodial) func(context.Co return err } + // Auto-revoke every session (15 min) + // Check if already a revoke request + if payload.Amount > 0 { + taskPayload, err := json.Marshal(TransferAuthPayload{ + TrackingId: payload.TrackingId, + Amount: 0, + Authorizer: payload.Authorizer, + AuthorizedAddress: payload.AuthorizedAddress, + VoucherAddress: payload.VoucherAddress, + }) + if err != nil { + return err + } + + _, err = cu.TaskerClient.CreateTask( + ctx, + tasker.SignTransferTaskAuth, + tasker.DefaultPriority, + &tasker.Task{ + Payload: taskPayload, + }, + asynq.ProcessIn(time.Minute*15), + ) + if err != nil { + return err + } + } + gasRefillPayload, err := json.Marshal(AccountPayload{ - PublicKey: payload.AuthorizeFor, + PublicKey: payload.Authorizer, TrackingId: payload.TrackingId, }) if err != nil { @@ -152,7 +175,7 @@ func SignTransferAuthorizationProcessor(cu *custodial.Custodial) func(context.Co } if !balanceCheck(networkBalance) { - if err := cu.Store.GasLock(ctx, payload.AuthorizeFor); err != nil { + if err := cu.Store.GasLock(ctx, payload.Authorizer); err != nil { return err } diff --git a/internal/tasker/types.go b/internal/tasker/types.go index a321ad8..bc8aacd 100644 --- a/internal/tasker/types.go +++ b/internal/tasker/types.go @@ -18,6 +18,7 @@ const ( AccountRegisterTask TaskName = "sys:register_account" AccountRefillGasTask TaskName = "sys:refill_gas" SignTransferTask TaskName = "usr:sign_transfer" + SignTransferTaskAuth TaskName = "usr:sign_transfer_auth" DispatchTxTask TaskName = "rpc:dispatch" ) diff --git a/pkg/enum/enum.go b/pkg/enum/enum.go index db8b186..841d687 100644 --- a/pkg/enum/enum.go +++ b/pkg/enum/enum.go @@ -21,6 +21,6 @@ const ( ACCOUNT_REGISTER OtxType = "ACCOUNT_REGISTER" REFILL_GAS OtxType = "REFILL_GAS" - TRANSFER_AUTH OtxType = "TRANFSER_AUTHORIZATION" + TRANSFER_AUTH OtxType = "TRANSFER_AUTHORIZATION" TRANSFER_VOUCHER OtxType = "TRANSFER_VOUCHER" )