mirror of
https://github.com/grassrootseconomics/cic-custodial.git
synced 2024-12-02 10: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")),
|
||||
GiftableToken: w3.A(ko.MustString("system.giftable_token_address")),
|
||||
GiftableTokenValue: big.NewInt(ko.MustInt64("system.giftable_token_value")),
|
||||
LockPrefix: ko.MustString("system.lock_prefix"),
|
||||
LockTimeout: 1 * time.Second,
|
||||
PublicKey: ko.MustString("system.public_key"),
|
||||
TokenDecimals: ko.MustInt("system.token_decimals"),
|
||||
|
@ -67,6 +67,7 @@ func main() {
|
||||
Noncestore: redisNoncestore,
|
||||
PgStore: pgStore,
|
||||
Pub: jsPub,
|
||||
RedisClient: redisPool.Client,
|
||||
SystemContainer: systemContainer,
|
||||
TaskerClient: taskerClient,
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ func initTasker(custodialContainer *custodial.Custodial, redisPool *redis.RedisP
|
||||
taskerServer.RegisterHandlers(tasker.AccountRegisterTask, task.AccountRegisterOnChainProcessor(custodialContainer))
|
||||
taskerServer.RegisterHandlers(tasker.AccountGiftGasTask, task.AccountGiftGasProcessor(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.SignTransferTask, task.SignTransfer(custodialContainer))
|
||||
taskerServer.RegisterHandlers(tasker.DispatchTxTask, task.DispatchTx(custodialContainer))
|
||||
|
@ -17,7 +17,6 @@ devnet = false
|
||||
|
||||
# All addresses MUST be checksumed
|
||||
account_index_address = ""
|
||||
lock_prefix = "lock:"
|
||||
gas_faucet_address = ""
|
||||
gas_refill_threshold = 2500000000000000
|
||||
gas_refill_value = 10000000000000000
|
||||
|
@ -40,6 +40,21 @@ func HandleSignTransfer(c echo.Context) error {
|
||||
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()
|
||||
taskPayload, err := json.Marshal(task.TransferPayload{
|
||||
TrackingId: trackingId,
|
||||
@ -65,6 +80,11 @@ func HandleSignTransfer(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cu.PgStore.DecrGasQuota(c.Request().Context(), req.From)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, OkResp{
|
||||
Ok: true,
|
||||
Result: H{
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/bsm/redislock"
|
||||
"github.com/celo-org/celo-blockchain/common"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/grassrootseconomics/celoutils"
|
||||
"github.com/grassrootseconomics/cic-custodial/internal/keystore"
|
||||
"github.com/grassrootseconomics/cic-custodial/internal/nonce"
|
||||
@ -26,7 +27,6 @@ type (
|
||||
GiftableGasValue *big.Int
|
||||
GiftableToken common.Address
|
||||
GiftableTokenValue *big.Int
|
||||
LockPrefix string
|
||||
LockTimeout time.Duration
|
||||
PrivateKey *ecdsa.PrivateKey
|
||||
PublicKey string
|
||||
@ -40,6 +40,7 @@ type (
|
||||
Noncestore nonce.Noncestore
|
||||
PgStore store.Store
|
||||
Pub *pub.Pub
|
||||
RedisClient *redis.Client
|
||||
SystemContainer *SystemContainer
|
||||
TaskerClient *tasker.TaskerClient
|
||||
}
|
||||
|
@ -11,10 +11,15 @@ type Queries struct {
|
||||
WriteKeyPair string `query:"write-key-pair"`
|
||||
LoadKeyPair string `query:"load-key-pair"`
|
||||
// Store
|
||||
CreateOTX string `query:"create-otx"`
|
||||
CreateDispatchStatus string `query:"create-dispatch-status"`
|
||||
UpdateChainStatus string `query:"update-chain-status"`
|
||||
GetTxStatusByTrackingId string `query:"get-tx-status-by-tracking-id"`
|
||||
CreateOTX string `query:"create-otx"`
|
||||
CreateDispatchStatus string `query:"create-dispatch-status"`
|
||||
ActivateAccount string `query:"activate-account"`
|
||||
UpdateChainStatus string `query:"update-chain-status"`
|
||||
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) {
|
||||
|
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
|
||||
}
|
||||
|
||||
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 (
|
||||
status = enum.SUCCESS
|
||||
)
|
||||
|
@ -40,5 +40,10 @@ type (
|
||||
CreateDispatchStatus(ctx context.Context, dispatch DispatchStatus) error
|
||||
GetTxStatusByTrackingId(ctx context.Context, trackingId string) ([]*TxStatus, 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 {
|
||||
case "CHAIN.gas":
|
||||
//
|
||||
if err := s.cu.PgStore.ResetGasQuota(ctx, checksum(chainEvent.To)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
task.Id = uuid.NewString()
|
||||
}
|
||||
|
||||
qTask := asynq.NewTask(
|
||||
string(taskName),
|
||||
task.Payload,
|
||||
defaultOpts := []asynq.Option{
|
||||
asynq.Queue(string(queueName)),
|
||||
asynq.TaskID(task.Id),
|
||||
asynq.Retention(taskRetention),
|
||||
asynq.Timeout(taskTimeout),
|
||||
}
|
||||
taskOpts := append(defaultOpts, extraOpts...)
|
||||
|
||||
qTask := asynq.NewTask(
|
||||
string(taskName),
|
||||
task.Payload,
|
||||
taskOpts...
|
||||
)
|
||||
|
||||
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"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/common/hexutil"
|
||||
"github.com/grassrootseconomics/celoutils"
|
||||
@ -16,6 +17,10 @@ import (
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
const (
|
||||
accountActivationCheckDelay = 5 * time.Second
|
||||
)
|
||||
|
||||
func AccountGiftGasProcessor(cu *custodial.Custodial) func(context.Context, *asynq.Task) error {
|
||||
return func(ctx context.Context, t *asynq.Task) error {
|
||||
var (
|
||||
@ -29,7 +34,7 @@ func AccountGiftGasProcessor(cu *custodial.Custodial) func(context.Context, *asy
|
||||
|
||||
lock, err := cu.LockProvider.Obtain(
|
||||
ctx,
|
||||
cu.SystemContainer.LockPrefix+cu.SystemContainer.PublicKey,
|
||||
lockPrefix+cu.SystemContainer.PublicKey,
|
||||
cu.SystemContainer.LockTimeout,
|
||||
nil,
|
||||
)
|
||||
@ -105,6 +110,19 @@ func AccountGiftGasProcessor(cu *custodial.Custodial) func(context.Context, *asy
|
||||
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{
|
||||
OtxId: id,
|
||||
TrackingId: payload.TrackingId,
|
||||
|
@ -28,7 +28,7 @@ func GiftVoucherProcessor(cu *custodial.Custodial) func(context.Context, *asynq.
|
||||
|
||||
lock, err := cu.LockProvider.Obtain(
|
||||
ctx,
|
||||
cu.SystemContainer.LockPrefix+cu.SystemContainer.PublicKey,
|
||||
lockPrefix+cu.SystemContainer.PublicKey,
|
||||
cu.SystemContainer.LockTimeout,
|
||||
nil,
|
||||
)
|
||||
|
@ -3,10 +3,12 @@ package task
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/celo-org/celo-blockchain/common/hexutil"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/grassrootseconomics/celoutils"
|
||||
"github.com/grassrootseconomics/cic-custodial/internal/custodial"
|
||||
"github.com/grassrootseconomics/cic-custodial/internal/pub"
|
||||
@ -14,14 +16,17 @@ import (
|
||||
"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/module/eth"
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
const (
|
||||
gasLockPrefix = "gas_lock:"
|
||||
gasLockExpiry = 1 * time.Hour
|
||||
)
|
||||
|
||||
func AccountRefillGasProcessor(cu *custodial.Custodial) func(context.Context, *asynq.Task) error {
|
||||
return func(ctx context.Context, t *asynq.Task) error {
|
||||
var (
|
||||
balance big.Int
|
||||
err error
|
||||
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)
|
||||
}
|
||||
|
||||
if err := cu.CeloProvider.Client.CallCtx(
|
||||
ctx,
|
||||
eth.Balance(w3.A(payload.PublicKey), nil).Returns(&balance),
|
||||
); err != nil {
|
||||
// TODO: Check eth-faucet whether we can request for a topup before signing the tx.
|
||||
_, gasQuota, err := cu.PgStore.GetAccountStatusByAddress(ctx, payload.PublicKey)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// TODO: Use eth-faucet.
|
||||
lock, err := cu.LockProvider.Obtain(
|
||||
ctx,
|
||||
cu.SystemContainer.LockPrefix+cu.SystemContainer.PublicKey,
|
||||
lockPrefix+cu.SystemContainer.PublicKey,
|
||||
cu.SystemContainer.LockTimeout,
|
||||
nil,
|
||||
)
|
||||
@ -134,6 +144,10 @@ func AccountRefillGasProcessor(cu *custodial.Custodial) func(context.Context, *a
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := cu.RedisClient.SetEX(ctx, gasLockPrefix+payload.PublicKey, true, gasLockExpiry).Result(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ func AccountRegisterOnChainProcessor(cu *custodial.Custodial) func(context.Conte
|
||||
|
||||
lock, err := cu.LockProvider.Obtain(
|
||||
ctx,
|
||||
cu.SystemContainer.LockPrefix+cu.SystemContainer.PublicKey,
|
||||
lockPrefix+cu.SystemContainer.PublicKey,
|
||||
cu.SystemContainer.LockTimeout,
|
||||
nil,
|
||||
)
|
||||
|
@ -47,7 +47,7 @@ func SignTransfer(cu *custodial.Custodial) func(context.Context, *asynq.Task) er
|
||||
|
||||
lock, err := cu.LockProvider.Obtain(
|
||||
ctx,
|
||||
cu.SystemContainer.LockPrefix+payload.From,
|
||||
lockPrefix+payload.From,
|
||||
cu.SystemContainer.LockTimeout,
|
||||
nil,
|
||||
)
|
||||
@ -136,7 +136,8 @@ func SignTransfer(cu *custodial.Custodial) func(context.Context, *asynq.Task) er
|
||||
}
|
||||
|
||||
gasRefillPayload, err := json.Marshal(AccountPayload{
|
||||
PublicKey: payload.From,
|
||||
PublicKey: payload.From,
|
||||
TrackingId: payload.TrackingId,
|
||||
})
|
||||
if err != nil {
|
||||
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"
|
||||
AccountGiftVoucherTask TaskName = "sys:gift_token"
|
||||
AccountRefillGasTask TaskName = "sys:refill_gas"
|
||||
AccountActivateTask TaskName = "sys:quorum_check"
|
||||
SignTransferTask TaskName = "usr:sign_transfer"
|
||||
DispatchTxTask TaskName = "rpc:dispatch"
|
||||
)
|
||||
|
@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS keystore (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
public_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
|
||||
);
|
||||
);
|
||||
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
|
||||
CREATE TABLE IF NOT EXISTS otx_sign (
|
||||
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,
|
||||
raw_tx TEXT NOT NULL,
|
||||
tx_hash TEXT NOT NULL,
|
||||
@ -24,8 +24,9 @@ CREATE TABLE IF NOT EXISTS otx_sign (
|
||||
nonce int NOT NULL,
|
||||
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 from_idx ON otx_sign USING hash("from");
|
||||
CREATE INDEX IF NOT EXISTS tracking_id_idx ON otx_sign (tracking_id);
|
||||
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
|
||||
-- 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
|
||||
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
|
||||
-- Create a new locally originating tx
|
||||
-- $1: tracking_id
|
||||
@ -51,7 +56,7 @@ INSERT INTO otx_dispatch(
|
||||
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
|
||||
WHERE otx_sign.tx_hash=$1
|
||||
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
|
||||
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
|
||||
WHERE otx_sign.tracking_id=$1
|
||||
|
||||
-- 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