mirror of
https://github.com/grassrootseconomics/cic-custodial.git
synced 2024-11-21 22:06:47 +01:00
feat: (wip) add account activation and gas quota lock
* This is a crude lock that restricts each account to the set gas quota.
This commit is contained in:
parent
ec14328d49
commit
341a760f02
@ -45,7 +45,6 @@ func initSystemContainer(ctx context.Context, noncestore nonce.Noncestore) *cust
|
|||||||
GiftableGasValue: big.NewInt(ko.MustInt64("system.giftable_gas_value")),
|
GiftableGasValue: big.NewInt(ko.MustInt64("system.giftable_gas_value")),
|
||||||
GiftableToken: w3.A(ko.MustString("system.giftable_token_address")),
|
GiftableToken: w3.A(ko.MustString("system.giftable_token_address")),
|
||||||
GiftableTokenValue: big.NewInt(ko.MustInt64("system.giftable_token_value")),
|
GiftableTokenValue: big.NewInt(ko.MustInt64("system.giftable_token_value")),
|
||||||
LockPrefix: ko.MustString("system.lock_prefix"),
|
|
||||||
LockTimeout: 1 * time.Second,
|
LockTimeout: 1 * time.Second,
|
||||||
PublicKey: ko.MustString("system.public_key"),
|
PublicKey: ko.MustString("system.public_key"),
|
||||||
TokenDecimals: ko.MustInt("system.token_decimals"),
|
TokenDecimals: ko.MustInt("system.token_decimals"),
|
||||||
|
@ -67,6 +67,7 @@ func main() {
|
|||||||
Noncestore: redisNoncestore,
|
Noncestore: redisNoncestore,
|
||||||
PgStore: pgStore,
|
PgStore: pgStore,
|
||||||
Pub: jsPub,
|
Pub: jsPub,
|
||||||
|
RedisClient: redisPool.Client,
|
||||||
SystemContainer: systemContainer,
|
SystemContainer: systemContainer,
|
||||||
TaskerClient: taskerClient,
|
TaskerClient: taskerClient,
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@ func initTasker(custodialContainer *custodial.Custodial, redisPool *redis.RedisP
|
|||||||
taskerServer.RegisterHandlers(tasker.AccountRegisterTask, task.AccountRegisterOnChainProcessor(custodialContainer))
|
taskerServer.RegisterHandlers(tasker.AccountRegisterTask, task.AccountRegisterOnChainProcessor(custodialContainer))
|
||||||
taskerServer.RegisterHandlers(tasker.AccountGiftGasTask, task.AccountGiftGasProcessor(custodialContainer))
|
taskerServer.RegisterHandlers(tasker.AccountGiftGasTask, task.AccountGiftGasProcessor(custodialContainer))
|
||||||
taskerServer.RegisterHandlers(tasker.AccountGiftVoucherTask, task.GiftVoucherProcessor(custodialContainer))
|
taskerServer.RegisterHandlers(tasker.AccountGiftVoucherTask, task.GiftVoucherProcessor(custodialContainer))
|
||||||
|
taskerServer.RegisterHandlers(tasker.AccountActivateTask, task.AccountActivateProcessor(custodialContainer))
|
||||||
taskerServer.RegisterHandlers(tasker.AccountRefillGasTask, task.AccountRefillGasProcessor(custodialContainer))
|
taskerServer.RegisterHandlers(tasker.AccountRefillGasTask, task.AccountRefillGasProcessor(custodialContainer))
|
||||||
taskerServer.RegisterHandlers(tasker.SignTransferTask, task.SignTransfer(custodialContainer))
|
taskerServer.RegisterHandlers(tasker.SignTransferTask, task.SignTransfer(custodialContainer))
|
||||||
taskerServer.RegisterHandlers(tasker.DispatchTxTask, task.DispatchTx(custodialContainer))
|
taskerServer.RegisterHandlers(tasker.DispatchTxTask, task.DispatchTx(custodialContainer))
|
||||||
|
@ -17,7 +17,6 @@ devnet = false
|
|||||||
|
|
||||||
# All addresses MUST be checksumed
|
# All addresses MUST be checksumed
|
||||||
account_index_address = ""
|
account_index_address = ""
|
||||||
lock_prefix = "lock:"
|
|
||||||
gas_faucet_address = ""
|
gas_faucet_address = ""
|
||||||
gas_refill_threshold = 2500000000000000
|
gas_refill_threshold = 2500000000000000
|
||||||
gas_refill_value = 10000000000000000
|
gas_refill_value = 10000000000000000
|
||||||
|
@ -40,6 +40,21 @@ func HandleSignTransfer(c echo.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accountActive, gasQuota, err := cu.PgStore.GetAccountStatusByAddress(c.Request().Context(), req.From)
|
||||||
|
if !accountActive {
|
||||||
|
return c.JSON(http.StatusForbidden, ErrResp{
|
||||||
|
Ok: false,
|
||||||
|
Message: "Account pending activation. Try again later.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if gasQuota < 1 {
|
||||||
|
return c.JSON(http.StatusForbidden, ErrResp{
|
||||||
|
Ok: false,
|
||||||
|
Message: "Out of gas, refill pending. Try again later.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
trackingId := uuid.NewString()
|
trackingId := uuid.NewString()
|
||||||
taskPayload, err := json.Marshal(task.TransferPayload{
|
taskPayload, err := json.Marshal(task.TransferPayload{
|
||||||
TrackingId: trackingId,
|
TrackingId: trackingId,
|
||||||
@ -65,6 +80,11 @@ func HandleSignTransfer(c echo.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = cu.PgStore.DecrGasQuota(c.Request().Context(), req.From)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, OkResp{
|
return c.JSON(http.StatusOK, OkResp{
|
||||||
Ok: true,
|
Ok: true,
|
||||||
Result: H{
|
Result: H{
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/bsm/redislock"
|
"github.com/bsm/redislock"
|
||||||
"github.com/celo-org/celo-blockchain/common"
|
"github.com/celo-org/celo-blockchain/common"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
"github.com/grassrootseconomics/celoutils"
|
"github.com/grassrootseconomics/celoutils"
|
||||||
"github.com/grassrootseconomics/cic-custodial/internal/keystore"
|
"github.com/grassrootseconomics/cic-custodial/internal/keystore"
|
||||||
"github.com/grassrootseconomics/cic-custodial/internal/nonce"
|
"github.com/grassrootseconomics/cic-custodial/internal/nonce"
|
||||||
@ -26,7 +27,6 @@ type (
|
|||||||
GiftableGasValue *big.Int
|
GiftableGasValue *big.Int
|
||||||
GiftableToken common.Address
|
GiftableToken common.Address
|
||||||
GiftableTokenValue *big.Int
|
GiftableTokenValue *big.Int
|
||||||
LockPrefix string
|
|
||||||
LockTimeout time.Duration
|
LockTimeout time.Duration
|
||||||
PrivateKey *ecdsa.PrivateKey
|
PrivateKey *ecdsa.PrivateKey
|
||||||
PublicKey string
|
PublicKey string
|
||||||
@ -40,6 +40,7 @@ type (
|
|||||||
Noncestore nonce.Noncestore
|
Noncestore nonce.Noncestore
|
||||||
PgStore store.Store
|
PgStore store.Store
|
||||||
Pub *pub.Pub
|
Pub *pub.Pub
|
||||||
|
RedisClient *redis.Client
|
||||||
SystemContainer *SystemContainer
|
SystemContainer *SystemContainer
|
||||||
TaskerClient *tasker.TaskerClient
|
TaskerClient *tasker.TaskerClient
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,13 @@ type Queries struct {
|
|||||||
// Store
|
// Store
|
||||||
CreateOTX string `query:"create-otx"`
|
CreateOTX string `query:"create-otx"`
|
||||||
CreateDispatchStatus string `query:"create-dispatch-status"`
|
CreateDispatchStatus string `query:"create-dispatch-status"`
|
||||||
|
ActivateAccount string `query:"activate-account"`
|
||||||
UpdateChainStatus string `query:"update-chain-status"`
|
UpdateChainStatus string `query:"update-chain-status"`
|
||||||
GetTxStatusByTrackingId string `query:"get-tx-status-by-tracking-id"`
|
GetTxStatusByTrackingId string `query:"get-tx-status-by-tracking-id"`
|
||||||
|
GetAccountActivationQuorum string `query:"get-account-activation-quorum"`
|
||||||
|
GetAccountStatus string `query:"get-account-status-by-address"`
|
||||||
|
DecrGasQuota string `query:"decr-gas-quota"`
|
||||||
|
ResetGasQuota string `query:"reset-gas-quota"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadQueries(q goyesql.Queries) (*Queries, error) {
|
func LoadQueries(q goyesql.Queries) (*Queries, error) {
|
||||||
|
66
internal/store/account.go
Normal file
66
internal/store/account.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *PostgresStore) GetAccountStatusByAddress(ctx context.Context, publicAddress string) (bool, int, error) {
|
||||||
|
var (
|
||||||
|
accountActive bool
|
||||||
|
gasQuota int
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := s.db.QueryRow(ctx, s.queries.GetAccountStatus, publicAddress).Scan(&accountActive, &gasQuota); err != nil {
|
||||||
|
return false, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return accountActive, gasQuota, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PostgresStore) GetAccountActivationQuorum(ctx context.Context, trackingId string) (int, error) {
|
||||||
|
var (
|
||||||
|
quorum int
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := s.db.QueryRow(ctx, s.queries.GetAccountActivationQuorum, trackingId).Scan(&quorum); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return quorum, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PostgresStore) DecrGasQuota(ctx context.Context, publicAddress string) error {
|
||||||
|
if _, err := s.db.Exec(
|
||||||
|
ctx,
|
||||||
|
s.queries.DecrGasQuota,
|
||||||
|
publicAddress,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PostgresStore) ResetGasQuota(ctx context.Context, publicAddress string) error {
|
||||||
|
if _, err := s.db.Exec(
|
||||||
|
ctx,
|
||||||
|
s.queries.ResetGasQuota,
|
||||||
|
publicAddress,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PostgresStore) ActivateAccount(ctx context.Context, publicAddress string) error {
|
||||||
|
if _, err := s.db.Exec(
|
||||||
|
ctx,
|
||||||
|
s.queries.ActivateAccount,
|
||||||
|
publicAddress,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,18 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *PostgresStore) CreateDispatchStatus(ctx context.Context, dispatch DispatchStatus) error {
|
|
||||||
if _, err := s.db.Exec(
|
|
||||||
ctx,
|
|
||||||
s.queries.CreateDispatchStatus,
|
|
||||||
dispatch.OtxId,
|
|
||||||
dispatch.Status,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -59,8 +59,20 @@ func (s *PostgresStore) GetTxStatusByTrackingId(ctx context.Context, trackingId
|
|||||||
return txs, nil
|
return txs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PostgresStore) UpdateOtxStatusFromChainEvent(ctx context.Context, chainEvent MinimalTxInfo) error {
|
func (s *PostgresStore) CreateDispatchStatus(ctx context.Context, dispatch DispatchStatus) error {
|
||||||
|
if _, err := s.db.Exec(
|
||||||
|
ctx,
|
||||||
|
s.queries.CreateDispatchStatus,
|
||||||
|
dispatch.OtxId,
|
||||||
|
dispatch.Status,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PostgresStore) UpdateOtxStatusFromChainEvent(ctx context.Context, chainEvent MinimalTxInfo) error {
|
||||||
var (
|
var (
|
||||||
status = enum.SUCCESS
|
status = enum.SUCCESS
|
||||||
)
|
)
|
||||||
|
@ -40,5 +40,10 @@ type (
|
|||||||
CreateDispatchStatus(ctx context.Context, dispatch DispatchStatus) error
|
CreateDispatchStatus(ctx context.Context, dispatch DispatchStatus) error
|
||||||
GetTxStatusByTrackingId(ctx context.Context, trackingId string) ([]*TxStatus, error)
|
GetTxStatusByTrackingId(ctx context.Context, trackingId string) ([]*TxStatus, error)
|
||||||
UpdateOtxStatusFromChainEvent(ctx context.Context, chainEvent MinimalTxInfo) error
|
UpdateOtxStatusFromChainEvent(ctx context.Context, chainEvent MinimalTxInfo) error
|
||||||
|
GetAccountStatusByAddress(ctx context.Context, publicAddress string) (bool, int, error)
|
||||||
|
GetAccountActivationQuorum(ctx context.Context, trackingId string) (int, error)
|
||||||
|
DecrGasQuota(ctx context.Context, publicAddress string) error
|
||||||
|
ResetGasQuota(ctx context.Context, publicAddress string) error
|
||||||
|
ActivateAccount(ctx context.Context, publicAddress string) error
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -23,7 +23,9 @@ func (s *Sub) handler(ctx context.Context, msg *nats.Msg) error {
|
|||||||
|
|
||||||
switch msg.Subject {
|
switch msg.Subject {
|
||||||
case "CHAIN.gas":
|
case "CHAIN.gas":
|
||||||
//
|
if err := s.cu.PgStore.ResetGasQuota(ctx, checksum(chainEvent.To)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
31
internal/sub/util.go
Normal file
31
internal/sub/util.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/sha3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: This should probably be used project wide
|
||||||
|
func checksum(address string) string {
|
||||||
|
address = strings.ToLower(address)
|
||||||
|
address = strings.Replace(address, "0x", "", 1)
|
||||||
|
|
||||||
|
sha := sha3.NewLegacyKeccak256()
|
||||||
|
sha.Write([]byte(address))
|
||||||
|
hash := sha.Sum(nil)
|
||||||
|
hashstr := hex.EncodeToString(hash)
|
||||||
|
result := []string{"0x"}
|
||||||
|
for i, v := range address {
|
||||||
|
res, _ := strconv.ParseInt(string(hashstr[i]), 16, 64)
|
||||||
|
if res > 7 {
|
||||||
|
result = append(result, strings.ToUpper(string(v)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, string(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(result, "")
|
||||||
|
}
|
@ -28,18 +28,23 @@ func NewTaskerClient(o TaskerClientOpts) *TaskerClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TaskerClient) CreateTask(ctx context.Context, taskName TaskName, queueName QueueName, task *Task) (*asynq.TaskInfo, error) {
|
func (c *TaskerClient) CreateTask(ctx context.Context, taskName TaskName, queueName QueueName, task *Task, extraOpts ...asynq.Option) (*asynq.TaskInfo, error) {
|
||||||
if task.Id == "" {
|
if task.Id == "" {
|
||||||
task.Id = uuid.NewString()
|
task.Id = uuid.NewString()
|
||||||
}
|
}
|
||||||
|
|
||||||
qTask := asynq.NewTask(
|
defaultOpts := []asynq.Option{
|
||||||
string(taskName),
|
|
||||||
task.Payload,
|
|
||||||
asynq.Queue(string(queueName)),
|
asynq.Queue(string(queueName)),
|
||||||
asynq.TaskID(task.Id),
|
asynq.TaskID(task.Id),
|
||||||
asynq.Retention(taskRetention),
|
asynq.Retention(taskRetention),
|
||||||
asynq.Timeout(taskTimeout),
|
asynq.Timeout(taskTimeout),
|
||||||
|
}
|
||||||
|
taskOpts := append(defaultOpts, extraOpts...)
|
||||||
|
|
||||||
|
qTask := asynq.NewTask(
|
||||||
|
string(taskName),
|
||||||
|
task.Payload,
|
||||||
|
taskOpts...
|
||||||
)
|
)
|
||||||
|
|
||||||
taskInfo, err := c.Client.EnqueueContext(ctx, qTask)
|
taskInfo, err := c.Client.EnqueueContext(ctx, qTask)
|
||||||
|
45
internal/tasker/task/account_activate.go
Normal file
45
internal/tasker/task/account_activate.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/grassrootseconomics/cic-custodial/internal/custodial"
|
||||||
|
"github.com/hibiken/asynq"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
requiredQuorum = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrQuorumNotReached = errors.New("Account activation quorum not reached.")
|
||||||
|
)
|
||||||
|
|
||||||
|
func AccountActivateProcessor(cu *custodial.Custodial) func(context.Context, *asynq.Task) error {
|
||||||
|
return func(ctx context.Context, t *asynq.Task) error {
|
||||||
|
var (
|
||||||
|
payload AccountPayload
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
quorum, err := cu.PgStore.GetAccountActivationQuorum(ctx, payload.TrackingId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if quorum < requiredQuorum {
|
||||||
|
return ErrQuorumNotReached
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cu.PgStore.ActivateAccount(ctx, payload.PublicKey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/celo-org/celo-blockchain/common/hexutil"
|
"github.com/celo-org/celo-blockchain/common/hexutil"
|
||||||
"github.com/grassrootseconomics/celoutils"
|
"github.com/grassrootseconomics/celoutils"
|
||||||
@ -16,6 +17,10 @@ import (
|
|||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
accountActivationCheckDelay = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
func AccountGiftGasProcessor(cu *custodial.Custodial) func(context.Context, *asynq.Task) error {
|
func AccountGiftGasProcessor(cu *custodial.Custodial) func(context.Context, *asynq.Task) error {
|
||||||
return func(ctx context.Context, t *asynq.Task) error {
|
return func(ctx context.Context, t *asynq.Task) error {
|
||||||
var (
|
var (
|
||||||
@ -29,7 +34,7 @@ func AccountGiftGasProcessor(cu *custodial.Custodial) func(context.Context, *asy
|
|||||||
|
|
||||||
lock, err := cu.LockProvider.Obtain(
|
lock, err := cu.LockProvider.Obtain(
|
||||||
ctx,
|
ctx,
|
||||||
cu.SystemContainer.LockPrefix+cu.SystemContainer.PublicKey,
|
lockPrefix+cu.SystemContainer.PublicKey,
|
||||||
cu.SystemContainer.LockTimeout,
|
cu.SystemContainer.LockTimeout,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
@ -105,6 +110,19 @@ func AccountGiftGasProcessor(cu *custodial.Custodial) func(context.Context, *asy
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = cu.TaskerClient.CreateTask(
|
||||||
|
ctx,
|
||||||
|
tasker.AccountActivateTask,
|
||||||
|
tasker.DefaultPriority,
|
||||||
|
&tasker.Task{
|
||||||
|
Payload: t.Payload(),
|
||||||
|
},
|
||||||
|
asynq.ProcessIn(accountActivationCheckDelay),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
eventPayload := &pub.EventPayload{
|
eventPayload := &pub.EventPayload{
|
||||||
OtxId: id,
|
OtxId: id,
|
||||||
TrackingId: payload.TrackingId,
|
TrackingId: payload.TrackingId,
|
||||||
|
@ -28,7 +28,7 @@ func GiftVoucherProcessor(cu *custodial.Custodial) func(context.Context, *asynq.
|
|||||||
|
|
||||||
lock, err := cu.LockProvider.Obtain(
|
lock, err := cu.LockProvider.Obtain(
|
||||||
ctx,
|
ctx,
|
||||||
cu.SystemContainer.LockPrefix+cu.SystemContainer.PublicKey,
|
lockPrefix+cu.SystemContainer.PublicKey,
|
||||||
cu.SystemContainer.LockTimeout,
|
cu.SystemContainer.LockTimeout,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
@ -3,10 +3,12 @@ package task
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"time"
|
||||||
|
|
||||||
"github.com/celo-org/celo-blockchain/common/hexutil"
|
"github.com/celo-org/celo-blockchain/common/hexutil"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
"github.com/grassrootseconomics/celoutils"
|
"github.com/grassrootseconomics/celoutils"
|
||||||
"github.com/grassrootseconomics/cic-custodial/internal/custodial"
|
"github.com/grassrootseconomics/cic-custodial/internal/custodial"
|
||||||
"github.com/grassrootseconomics/cic-custodial/internal/pub"
|
"github.com/grassrootseconomics/cic-custodial/internal/pub"
|
||||||
@ -14,14 +16,17 @@ import (
|
|||||||
"github.com/grassrootseconomics/cic-custodial/internal/tasker"
|
"github.com/grassrootseconomics/cic-custodial/internal/tasker"
|
||||||
"github.com/grassrootseconomics/cic-custodial/pkg/enum"
|
"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/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
gasLockPrefix = "gas_lock:"
|
||||||
|
gasLockExpiry = 1 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
func AccountRefillGasProcessor(cu *custodial.Custodial) func(context.Context, *asynq.Task) error {
|
func AccountRefillGasProcessor(cu *custodial.Custodial) func(context.Context, *asynq.Task) error {
|
||||||
return func(ctx context.Context, t *asynq.Task) error {
|
return func(ctx context.Context, t *asynq.Task) error {
|
||||||
var (
|
var (
|
||||||
balance big.Int
|
|
||||||
err error
|
err error
|
||||||
payload AccountPayload
|
payload AccountPayload
|
||||||
)
|
)
|
||||||
@ -30,20 +35,25 @@ func AccountRefillGasProcessor(cu *custodial.Custodial) func(context.Context, *a
|
|||||||
return fmt.Errorf("account: failed %v: %w", err, asynq.SkipRetry)
|
return fmt.Errorf("account: failed %v: %w", err, asynq.SkipRetry)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cu.CeloProvider.Client.CallCtx(
|
// TODO: Check eth-faucet whether we can request for a topup before signing the tx.
|
||||||
ctx,
|
_, gasQuota, err := cu.PgStore.GetAccountStatusByAddress(ctx, payload.PublicKey)
|
||||||
eth.Balance(w3.A(payload.PublicKey), nil).Returns(&balance),
|
if err != nil {
|
||||||
); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if belowThreshold := balance.Cmp(cu.SystemContainer.GasRefillThreshold); belowThreshold > 0 {
|
gasLock, err := cu.RedisClient.Get(ctx, gasLockPrefix+payload.PublicKey).Bool()
|
||||||
|
if !errors.Is(err, redis.Nil) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if gasQuota > 0 || gasLock {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Use eth-faucet.
|
||||||
lock, err := cu.LockProvider.Obtain(
|
lock, err := cu.LockProvider.Obtain(
|
||||||
ctx,
|
ctx,
|
||||||
cu.SystemContainer.LockPrefix+cu.SystemContainer.PublicKey,
|
lockPrefix+cu.SystemContainer.PublicKey,
|
||||||
cu.SystemContainer.LockTimeout,
|
cu.SystemContainer.LockTimeout,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
@ -134,6 +144,10 @@ func AccountRefillGasProcessor(cu *custodial.Custodial) func(context.Context, *a
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := cu.RedisClient.SetEX(ctx, gasLockPrefix+payload.PublicKey, true, gasLockExpiry).Result(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ func AccountRegisterOnChainProcessor(cu *custodial.Custodial) func(context.Conte
|
|||||||
|
|
||||||
lock, err := cu.LockProvider.Obtain(
|
lock, err := cu.LockProvider.Obtain(
|
||||||
ctx,
|
ctx,
|
||||||
cu.SystemContainer.LockPrefix+cu.SystemContainer.PublicKey,
|
lockPrefix+cu.SystemContainer.PublicKey,
|
||||||
cu.SystemContainer.LockTimeout,
|
cu.SystemContainer.LockTimeout,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
@ -47,7 +47,7 @@ func SignTransfer(cu *custodial.Custodial) func(context.Context, *asynq.Task) er
|
|||||||
|
|
||||||
lock, err := cu.LockProvider.Obtain(
|
lock, err := cu.LockProvider.Obtain(
|
||||||
ctx,
|
ctx,
|
||||||
cu.SystemContainer.LockPrefix+payload.From,
|
lockPrefix+payload.From,
|
||||||
cu.SystemContainer.LockTimeout,
|
cu.SystemContainer.LockTimeout,
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
@ -137,6 +137,7 @@ func SignTransfer(cu *custodial.Custodial) func(context.Context, *asynq.Task) er
|
|||||||
|
|
||||||
gasRefillPayload, err := json.Marshal(AccountPayload{
|
gasRefillPayload, err := json.Marshal(AccountPayload{
|
||||||
PublicKey: payload.From,
|
PublicKey: payload.From,
|
||||||
|
TrackingId: payload.TrackingId,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
5
internal/tasker/task/utils.go
Normal file
5
internal/tasker/task/utils.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
const (
|
||||||
|
lockPrefix = "lock:"
|
||||||
|
)
|
@ -20,6 +20,7 @@ const (
|
|||||||
AccountGiftGasTask TaskName = "sys:gift_gas"
|
AccountGiftGasTask TaskName = "sys:gift_gas"
|
||||||
AccountGiftVoucherTask TaskName = "sys:gift_token"
|
AccountGiftVoucherTask TaskName = "sys:gift_token"
|
||||||
AccountRefillGasTask TaskName = "sys:refill_gas"
|
AccountRefillGasTask TaskName = "sys:refill_gas"
|
||||||
|
AccountActivateTask TaskName = "sys:quorum_check"
|
||||||
SignTransferTask TaskName = "usr:sign_transfer"
|
SignTransferTask TaskName = "usr:sign_transfer"
|
||||||
DispatchTxTask TaskName = "rpc:dispatch"
|
DispatchTxTask TaskName = "rpc:dispatch"
|
||||||
)
|
)
|
||||||
|
@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS keystore (
|
|||||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
public_key TEXT NOT NULL,
|
public_key TEXT NOT NULL,
|
||||||
private_key TEXT NOT NULL,
|
private_key TEXT NOT NULL,
|
||||||
active BOOLEAN DEFAULT true,
|
active BOOLEAN DEFAULT false,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS public_key_idx ON keystore(public_key);
|
@ -12,7 +12,7 @@ INSERT INTO otx_tx_type (value) VALUES
|
|||||||
-- Origin tx table
|
-- Origin tx table
|
||||||
CREATE TABLE IF NOT EXISTS otx_sign (
|
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 uuid NOT NULL,
|
||||||
"type" TEXT REFERENCES otx_tx_type(value) 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,
|
||||||
@ -24,8 +24,9 @@ CREATE TABLE IF NOT EXISTS otx_sign (
|
|||||||
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_sign USING hash(tx_hash);
|
CREATE INDEX IF NOT EXISTS tracking_id_idx ON otx_sign (tracking_id);
|
||||||
CREATE INDEX IF NOT EXISTS from_idx ON otx_sign USING hash("from");
|
CREATE INDEX IF NOT EXISTS tx_hash_idx ON otx_sign (tx_hash);
|
||||||
|
CREATE INDEX IF NOT EXISTS from_idx ON otx_sign ("from");
|
||||||
|
|
||||||
-- Otx dispatch status enum table
|
-- Otx dispatch status enum table
|
||||||
-- Enforces referential integrity on the dispatch table
|
-- Enforces referential integrity on the dispatch table
|
||||||
|
28
migrations/003_gas_quota.sql
Normal file
28
migrations/003_gas_quota.sql
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-- Gas quota meta table
|
||||||
|
CREATE TABLE IF NOT EXISTS gas_quota_meta (
|
||||||
|
default_quota INT NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO gas_quota_meta (default_quota) VALUES (25);
|
||||||
|
|
||||||
|
-- Gas quota table
|
||||||
|
CREATE TABLE IF NOT EXISTS gas_quota (
|
||||||
|
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
key_id INT REFERENCES keystore(id) NOT NULL,
|
||||||
|
quota INT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Gas quota trigger on keystore insert to default 0 quota
|
||||||
|
-- We wait for the event handler to correctly set the chain quota
|
||||||
|
create function insert_gas_quota()
|
||||||
|
returns trigger
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
insert into gas_quota (key_id) values (new.id);
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
||||||
|
|
||||||
|
create trigger insert_gas_quota
|
||||||
|
after insert on keystore
|
||||||
|
for each row
|
||||||
|
execute procedure insert_gas_quota()
|
43
queries.sql
43
queries.sql
@ -9,6 +9,11 @@ INSERT INTO keystore(public_key, private_key) VALUES($1, $2) RETURNING id
|
|||||||
-- $1: public_key
|
-- $1: public_key
|
||||||
SELECT private_key FROM keystore WHERE public_key=$1
|
SELECT private_key FROM keystore WHERE public_key=$1
|
||||||
|
|
||||||
|
--name: activate-account
|
||||||
|
-- Activate an account following successful quorum
|
||||||
|
-- $1: public_key
|
||||||
|
UPDATE keystore SET active = true WHERE public_key=$1
|
||||||
|
|
||||||
--name: create-otx
|
--name: create-otx
|
||||||
-- Create a new locally originating tx
|
-- Create a new locally originating tx
|
||||||
-- $1: tracking_id
|
-- $1: tracking_id
|
||||||
@ -51,7 +56,7 @@ INSERT INTO otx_dispatch(
|
|||||||
UPDATE otx_dispatch SET "status" = $2, "block" = $3 WHERE otx_dispatch.id = (
|
UPDATE otx_dispatch SET "status" = $2, "block" = $3 WHERE otx_dispatch.id = (
|
||||||
SELECT otx_dispatch.id FROM otx_dispatch
|
SELECT otx_dispatch.id FROM otx_dispatch
|
||||||
INNER JOIN otx_sign ON otx_dispatch.otx_id = otx_sign.id
|
INNER JOIN otx_sign ON otx_dispatch.otx_id = otx_sign.id
|
||||||
WHERE otx_sign.tx_hash = $1
|
WHERE otx_sign.tx_hash=$1
|
||||||
AND otx_dispatch.status = 'IN_NETWORK'
|
AND otx_dispatch.status = 'IN_NETWORK'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -60,6 +65,40 @@ UPDATE otx_dispatch SET "status" = $2, "block" = $3 WHERE otx_dispatch.id = (
|
|||||||
-- $1: 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
|
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
|
INNER JOIN otx_dispatch ON otx_sign.id = otx_dispatch.otx_id
|
||||||
WHERE otx_sign.tracking_id = $1
|
WHERE otx_sign.tracking_id=$1
|
||||||
|
|
||||||
-- TODO: Scroll by status type with cursor pagination
|
-- TODO: Scroll by status type with cursor pagination
|
||||||
|
|
||||||
|
--name: get-account-activation-quorum
|
||||||
|
-- Gets quorum of required and confirmed system transactions for the account
|
||||||
|
-- $1: tracking_id
|
||||||
|
SELECT count(*) FROM otx_dispatch INNER JOIN
|
||||||
|
otx_sign ON otx_dispatch.otx_id = otx_sign.id
|
||||||
|
WHERE otx_sign.tracking_id=$1
|
||||||
|
AND otx_dispatch.status = 'SUCCESS'
|
||||||
|
|
||||||
|
--name: get-account-status-by-address
|
||||||
|
-- Gets current gas quota for an individual account by address
|
||||||
|
-- $1: public_key
|
||||||
|
SELECT keystore.active, gas_quota.quota FROM keystore
|
||||||
|
INNER JOIN gas_quota ON keystore.id = gas_quota.key_id
|
||||||
|
WHERE keystore.public_key=$1
|
||||||
|
|
||||||
|
--name: decr-gas-quota
|
||||||
|
-- Consumes a gas quota
|
||||||
|
-- $1: public_key
|
||||||
|
UPDATE gas_quota SET quota = quota - 1 WHERE key_id = (
|
||||||
|
SELECT id FROM keystore
|
||||||
|
WHERE public_key=$1
|
||||||
|
)
|
||||||
|
|
||||||
|
--name: reset-gas-quota
|
||||||
|
-- Resets the gas quota
|
||||||
|
-- 25 is the agreed upon quota
|
||||||
|
-- $1: public_key
|
||||||
|
UPDATE gas_quota SET quota = gas_quota_meta.default_quota
|
||||||
|
FROM gas_quota_meta
|
||||||
|
WHERE key_id = (
|
||||||
|
SELECT id FROM keystore
|
||||||
|
WHERE public_key=$1
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user