mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
@@ -1,112 +0,0 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/sonr-io/snrd/internal/transaction"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoInitHandler = errors.New("no init handler")
|
||||
errNoExecuteHandler = errors.New("account does not accept messages")
|
||||
errInvalidMessage = errors.New("invalid message")
|
||||
)
|
||||
|
||||
// NewInitBuilder creates a new InitBuilder instance.
|
||||
func NewInitBuilder() *InitBuilder {
|
||||
return &InitBuilder{}
|
||||
}
|
||||
|
||||
// InitBuilder defines a smart account's initialisation handler builder.
|
||||
type InitBuilder struct {
|
||||
// handler is the handler function that will be called when the smart account is initialized.
|
||||
// Although the function here is defined to take an any, the smart account will work
|
||||
// with a typed version of it.
|
||||
handler func(ctx context.Context, initRequest transaction.Msg) (initResponse transaction.Msg, err error)
|
||||
|
||||
// schema is the schema of the message that will be passed to the handler function.
|
||||
schema HandlerSchema
|
||||
}
|
||||
|
||||
// makeHandler returns the handler function that will be called when the smart account is initialized.
|
||||
// It returns an error if no handler was registered.
|
||||
func (i *InitBuilder) makeHandler() (func(ctx context.Context, initRequest transaction.Msg) (initResponse transaction.Msg, err error), error) {
|
||||
if i.handler == nil {
|
||||
return nil, errNoInitHandler
|
||||
}
|
||||
return i.handler, nil
|
||||
}
|
||||
|
||||
// NewExecuteBuilder creates a new ExecuteBuilder instance.
|
||||
func NewExecuteBuilder() *ExecuteBuilder {
|
||||
return &ExecuteBuilder{
|
||||
handlers: make(map[string]func(ctx context.Context, executeRequest transaction.Msg) (executeResponse transaction.Msg, err error)),
|
||||
handlersSchema: make(map[string]HandlerSchema),
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteBuilder defines a smart account's execution router, it will be used to map an execution message
|
||||
// to a handler function for a specific account.
|
||||
type ExecuteBuilder struct {
|
||||
// handlers is a map of handler functions that will be called when the smart account is executed.
|
||||
handlers map[string]func(ctx context.Context, executeRequest transaction.Msg) (executeResponse transaction.Msg, err error)
|
||||
|
||||
// handlersSchema is a map of schemas for the messages that will be passed to the handler functions
|
||||
// and the messages that will be returned by the handler functions.
|
||||
handlersSchema map[string]HandlerSchema
|
||||
|
||||
// err is the error that occurred before building the handler function.
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *ExecuteBuilder) makeHandler() (func(ctx context.Context, executeRequest transaction.Msg) (executeResponse transaction.Msg, err error), error) {
|
||||
// if no handler is registered it's fine, it means the account will not be accepting execution or query messages.
|
||||
if len(r.handlers) == 0 {
|
||||
return func(ctx context.Context, _ transaction.Msg) (_ transaction.Msg, err error) {
|
||||
return nil, errNoExecuteHandler
|
||||
}, nil
|
||||
}
|
||||
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
|
||||
// build the real execution handler
|
||||
return func(ctx context.Context, executeRequest transaction.Msg) (executeResponse transaction.Msg, err error) {
|
||||
messageName := MessageName(executeRequest)
|
||||
handler, ok := r.handlers[messageName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: no handler for message %s", errInvalidMessage, messageName)
|
||||
}
|
||||
return handler(ctx, executeRequest)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewQueryBuilder creates a new QueryBuilder instance.
|
||||
func NewQueryBuilder() *QueryBuilder {
|
||||
return &QueryBuilder{
|
||||
er: NewExecuteBuilder(),
|
||||
}
|
||||
}
|
||||
|
||||
// QueryBuilder defines a smart account's query router, it will be used to map a query message
|
||||
// to a handler function for a specific account.
|
||||
type QueryBuilder struct {
|
||||
// er is the ExecuteBuilder, since there's no difference between the execution and query handlers API.
|
||||
er *ExecuteBuilder
|
||||
}
|
||||
|
||||
func (r *QueryBuilder) makeHandler() (func(ctx context.Context, queryRequest transaction.Msg) (queryResponse transaction.Msg, err error), error) {
|
||||
return r.er.makeHandler()
|
||||
}
|
||||
|
||||
// IsRoutingError returns true if the error is a routing error,
|
||||
// which typically occurs when a message cannot be matched to a handler.
|
||||
func IsRoutingError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return errors.Is(err, errInvalidMessage)
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
|
||||
"cosmossdk.io/collections"
|
||||
"cosmossdk.io/core/store"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/sonr-io/snrd/internal/prefixstore"
|
||||
"github.com/sonr-io/snrd/internal/transaction"
|
||||
)
|
||||
|
||||
var AccountStatePrefix = collections.NewPrefix(255)
|
||||
|
||||
type (
|
||||
ModuleExecFunc = func(ctx context.Context, sender []byte, msg transaction.Msg) (transaction.Msg, error)
|
||||
ModuleQueryFunc = func(ctx context.Context, queryReq transaction.Msg) (transaction.Msg, error)
|
||||
)
|
||||
|
||||
type contextKey struct{}
|
||||
|
||||
type contextValue struct {
|
||||
store store.KVStore // store is the prefixed store for the account.
|
||||
sender []byte // sender is the address of the entity invoking the account action.
|
||||
whoami []byte // whoami is the address of the account being invoked.
|
||||
funds sdk.Coins // funds reports the coins sent alongside the request.
|
||||
parentContext context.Context // parentContext that was used to build the account context.
|
||||
moduleExec ModuleExecFunc // moduleExec is a function that executes a module message, when the resp type is unknown.
|
||||
moduleQuery ModuleQueryFunc // moduleQuery is a function that queries a module.
|
||||
}
|
||||
|
||||
func addCtx(ctx context.Context, value contextValue) context.Context {
|
||||
return context.WithValue(ctx, contextKey{}, value)
|
||||
}
|
||||
|
||||
func getCtx(ctx context.Context) contextValue {
|
||||
return ctx.Value(contextKey{}).(contextValue)
|
||||
}
|
||||
|
||||
// MakeAccountContext creates a new account execution context given:
|
||||
// storeSvc: which fetches the x/accounts module store.
|
||||
// accountAddr: the address of the account being invoked, which is used to give the
|
||||
// account a prefixed storage.
|
||||
// sender: the address of entity invoking the account action.
|
||||
// moduleExec: a function that executes a module message.
|
||||
// moduleQuery: a function that queries a module.
|
||||
func MakeAccountContext(
|
||||
ctx context.Context,
|
||||
storeSvc store.KVStoreService,
|
||||
accNumber uint64,
|
||||
accountAddr []byte,
|
||||
sender []byte,
|
||||
funds sdk.Coins,
|
||||
moduleExec ModuleExecFunc,
|
||||
moduleQuery ModuleQueryFunc,
|
||||
) context.Context {
|
||||
return addCtx(ctx, contextValue{
|
||||
store: makeAccountStore(ctx, storeSvc, accNumber),
|
||||
sender: sender,
|
||||
whoami: accountAddr,
|
||||
funds: funds,
|
||||
parentContext: ctx,
|
||||
moduleExec: moduleExec,
|
||||
moduleQuery: moduleQuery,
|
||||
})
|
||||
}
|
||||
|
||||
func SetSender(ctx context.Context, sender []byte) context.Context {
|
||||
v := getCtx(ctx)
|
||||
v.sender = sender
|
||||
return addCtx(v.parentContext, v)
|
||||
}
|
||||
|
||||
// makeAccountStore creates the prefixed store for the account.
|
||||
// It uses the number of the account, this gives constant size
|
||||
// bytes prefixes for the account state.
|
||||
func makeAccountStore(ctx context.Context, storeSvc store.KVStoreService, accNum uint64) store.KVStore {
|
||||
prefix := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(prefix, accNum)
|
||||
return prefixstore.New(storeSvc.OpenKVStore(ctx), append(AccountStatePrefix, prefix...))
|
||||
}
|
||||
|
||||
// ExecModule can be used to execute a message towards a module, when the response type is unknown.
|
||||
func ExecModule(ctx context.Context, msg transaction.Msg) (transaction.Msg, error) {
|
||||
// get sender
|
||||
v := getCtx(ctx)
|
||||
|
||||
resp, err := v.moduleExec(v.parentContext, v.whoami, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// QueryModule can be used by an account to execute a module query.
|
||||
func QueryModule(ctx context.Context, req transaction.Msg) (transaction.Msg, error) {
|
||||
// we do not need to check the sender in a query because it is not a state transition.
|
||||
// we also unwrap the original context.
|
||||
v := getCtx(ctx)
|
||||
resp, err := v.moduleQuery(v.parentContext, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// openKVStore returns the prefixed store for the account given the context.
|
||||
func openKVStore(ctx context.Context) store.KVStore { return getCtx(ctx).store }
|
||||
|
||||
// Sender returns the address of the entity invoking the account action.
|
||||
func Sender(ctx context.Context) []byte {
|
||||
return getCtx(ctx).sender
|
||||
}
|
||||
|
||||
// Whoami returns the address of the account being invoked.
|
||||
func Whoami(ctx context.Context) []byte {
|
||||
return getCtx(ctx).whoami
|
||||
}
|
||||
|
||||
// Funds returns the funds associated with the execution context.
|
||||
func Funds(ctx context.Context) sdk.Coins { return getCtx(ctx).funds }
|
||||
@@ -1,66 +0,0 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
"github.com/cosmos/gogoproto/proto"
|
||||
|
||||
"github.com/sonr-io/snrd/internal/transaction"
|
||||
)
|
||||
|
||||
// ProtoMsgG is a generic interface for protobuf messages.
|
||||
type ProtoMsgG[T any] interface {
|
||||
*T
|
||||
transaction.Msg
|
||||
}
|
||||
|
||||
type Any = codectypes.Any
|
||||
|
||||
func FindMessageByName(name string) (transaction.Msg, error) {
|
||||
typ := proto.MessageType(name)
|
||||
if typ == nil {
|
||||
return nil, fmt.Errorf("no message type found for %s", name)
|
||||
}
|
||||
return reflect.New(typ.Elem()).Interface().(transaction.Msg), nil
|
||||
}
|
||||
|
||||
func MessageName(msg transaction.Msg) string {
|
||||
return proto.MessageName(msg)
|
||||
}
|
||||
|
||||
// PackAny packs a proto message into an anypb.Any.
|
||||
func PackAny(msg transaction.Msg) (*Any, error) {
|
||||
return codectypes.NewAnyWithValue(msg)
|
||||
}
|
||||
|
||||
// UnpackAny unpacks an anypb.Any into a proto message.
|
||||
func UnpackAny[T any, PT ProtoMsgG[T]](anyPB *Any) (PT, error) {
|
||||
to := new(T)
|
||||
return to, UnpackAnyTo(anyPB, PT(to))
|
||||
}
|
||||
|
||||
func UnpackAnyTo(anyPB *Any, to transaction.Msg) error {
|
||||
return proto.Unmarshal(anyPB.Value, to)
|
||||
}
|
||||
|
||||
func UnpackAnyRaw(anyPB *Any) (proto.Message, error) {
|
||||
split := strings.Split(anyPB.TypeUrl, "/")
|
||||
name := split[len(split)-1]
|
||||
typ := proto.MessageType(name)
|
||||
if typ == nil {
|
||||
return nil, fmt.Errorf("no message type found for %s", name)
|
||||
}
|
||||
to := reflect.New(typ.Elem()).Interface().(proto.Message)
|
||||
return to, UnpackAnyTo(anyPB, to)
|
||||
}
|
||||
|
||||
func Merge(a, b transaction.Msg) {
|
||||
proto.Merge(a, b)
|
||||
}
|
||||
|
||||
func Equal(a, b transaction.Msg) bool {
|
||||
return proto.Equal(a, b)
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"cosmossdk.io/collections"
|
||||
"cosmossdk.io/core/address"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
gogoproto "github.com/cosmos/gogoproto/proto"
|
||||
|
||||
"github.com/sonr-io/snrd/internal/appmodule"
|
||||
"github.com/sonr-io/snrd/internal/transaction"
|
||||
)
|
||||
|
||||
// Dependencies are passed to the constructor of a smart account.
|
||||
type Dependencies struct {
|
||||
SchemaBuilder *collections.SchemaBuilder
|
||||
AddressCodec address.Codec
|
||||
Environment appmodule.Environment
|
||||
LegacyStateCodec interface {
|
||||
Marshal(gogoproto.Message) ([]byte, error)
|
||||
Unmarshal([]byte, gogoproto.Message) error
|
||||
}
|
||||
}
|
||||
|
||||
// AccountCreatorFunc is a function that creates an account.
|
||||
type AccountCreatorFunc = func(deps Dependencies) (string, Account, error)
|
||||
|
||||
// MakeAccountsMap creates a map of account names to account implementations
|
||||
// from a list of account creator functions.
|
||||
func MakeAccountsMap(
|
||||
cdc codec.Codec,
|
||||
addressCodec address.Codec,
|
||||
env appmodule.Environment,
|
||||
accounts []AccountCreatorFunc,
|
||||
) (map[string]Implementation, error) {
|
||||
accountsMap := make(map[string]Implementation, len(accounts))
|
||||
for _, makeAccount := range accounts {
|
||||
stateSchemaBuilder := collections.NewSchemaBuilderFromAccessor(openKVStore)
|
||||
deps := Dependencies{
|
||||
SchemaBuilder: stateSchemaBuilder,
|
||||
AddressCodec: addressCodec,
|
||||
Environment: env,
|
||||
LegacyStateCodec: cdc,
|
||||
}
|
||||
name, accountInterface, err := makeAccount(deps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create account %s: %w", name, err)
|
||||
}
|
||||
if _, ok := accountsMap[name]; ok {
|
||||
return nil, fmt.Errorf("account %s is already registered", name)
|
||||
}
|
||||
impl, err := newImplementation(stateSchemaBuilder, accountInterface)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create implementation for account %s: %w", name, err)
|
||||
}
|
||||
accountsMap[name] = impl
|
||||
}
|
||||
|
||||
return accountsMap, nil
|
||||
}
|
||||
|
||||
// newImplementation creates a new Implementation instance given an Account implementer.
|
||||
func newImplementation(schemaBuilder *collections.SchemaBuilder, account Account) (Implementation, error) {
|
||||
// make init handler
|
||||
ir := NewInitBuilder()
|
||||
account.RegisterInitHandler(ir)
|
||||
initHandler, err := ir.makeHandler()
|
||||
if err != nil {
|
||||
return Implementation{}, err
|
||||
}
|
||||
|
||||
// make execute handler
|
||||
er := NewExecuteBuilder()
|
||||
account.RegisterExecuteHandlers(er)
|
||||
executeHandler, err := er.makeHandler()
|
||||
if err != nil {
|
||||
return Implementation{}, err
|
||||
}
|
||||
|
||||
// make query handler
|
||||
qr := NewQueryBuilder()
|
||||
account.RegisterQueryHandlers(qr)
|
||||
queryHandler, err := qr.makeHandler()
|
||||
if err != nil {
|
||||
return Implementation{}, err
|
||||
}
|
||||
|
||||
// build schema
|
||||
schema, err := schemaBuilder.Build()
|
||||
if err != nil {
|
||||
return Implementation{}, err
|
||||
}
|
||||
return Implementation{
|
||||
Init: initHandler,
|
||||
Execute: executeHandler,
|
||||
Query: queryHandler,
|
||||
CollectionsSchema: schema,
|
||||
InitHandlerSchema: ir.schema,
|
||||
QueryHandlersSchema: qr.er.handlersSchema,
|
||||
ExecuteHandlersSchema: er.handlersSchema,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Implementation wraps an Account implementer in order to provide a concrete
|
||||
// and non-generic implementation usable by the x/accounts module.
|
||||
type Implementation struct {
|
||||
// Init defines the initialisation handler for the smart account.
|
||||
Init func(ctx context.Context, msg transaction.Msg) (resp transaction.Msg, err error)
|
||||
// Execute defines the execution handler for the smart account.
|
||||
Execute func(ctx context.Context, msg transaction.Msg) (resp transaction.Msg, err error)
|
||||
// Query defines the query handler for the smart account.
|
||||
Query func(ctx context.Context, msg transaction.Msg) (resp transaction.Msg, err error)
|
||||
// CollectionsSchema represents the state schema.
|
||||
CollectionsSchema collections.Schema
|
||||
// InitHandlerSchema represents the init handler schema.
|
||||
InitHandlerSchema HandlerSchema
|
||||
// QueryHandlersSchema is the schema of the query handlers.
|
||||
QueryHandlersSchema map[string]HandlerSchema
|
||||
// ExecuteHandlersSchema is the schema of the execute handlers.
|
||||
ExecuteHandlersSchema map[string]HandlerSchema
|
||||
}
|
||||
|
||||
// HasExec returns true if the account can execute the given msg.
|
||||
func (i Implementation) HasExec(m transaction.Msg) bool {
|
||||
_, ok := i.ExecuteHandlersSchema[MessageName(m)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// HasQuery returns true if the account can execute the given request.
|
||||
func (i Implementation) HasQuery(m transaction.Msg) bool {
|
||||
_, ok := i.QueryHandlersSchema[MessageName(m)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// HasInit returns true if the account uses the provided init message.
|
||||
func (i Implementation) HasInit(m transaction.Msg) bool {
|
||||
return i.InitHandlerSchema.RequestSchema.Name == MessageName(m)
|
||||
}
|
||||
|
||||
// MessageSchema defines the schema of a message.
|
||||
// A message can also define a state schema.
|
||||
type MessageSchema struct {
|
||||
// Name identifies the message name, this must be queryable from some reflection service.
|
||||
Name string
|
||||
// New is used to create a new message instance for the schema.
|
||||
New func() transaction.Msg
|
||||
}
|
||||
|
||||
// HandlerSchema defines the schema of a handler.
|
||||
type HandlerSchema struct {
|
||||
// RequestSchema defines the schema of the request.
|
||||
RequestSchema MessageSchema
|
||||
// ResponseSchema defines the schema of the response.
|
||||
ResponseSchema MessageSchema
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package accounts
|
||||
|
||||
// Account defines a smart account interface.
|
||||
type Account interface {
|
||||
// RegisterInitHandler allows the smart account to register an initialisation handler, using
|
||||
// the provided InitBuilder. The handler will be called when the smart account is initialized
|
||||
// (deployed).
|
||||
RegisterInitHandler(builder *InitBuilder)
|
||||
|
||||
// RegisterExecuteHandlers allows the smart account to register execution handlers.
|
||||
// The smart account might also decide to not register any execution handler.
|
||||
RegisterExecuteHandlers(builder *ExecuteBuilder)
|
||||
|
||||
// RegisterQueryHandlers allows the smart account to register query handlers. The smart account
|
||||
// might also decide to not register any query handler.
|
||||
RegisterQueryHandlers(builder *QueryBuilder)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/sonr-io/snrd/internal/transaction"
|
||||
)
|
||||
|
||||
// RegisterInitHandler registers an initialisation handler for a smart account that uses protobuf.
|
||||
func RegisterInitHandler[
|
||||
Req any, ProtoReq ProtoMsgG[Req], Resp any, ProtoResp ProtoMsgG[Resp],
|
||||
](router *InitBuilder, handler func(ctx context.Context, req ProtoReq) (ProtoResp, error),
|
||||
) {
|
||||
reqName := MessageName(ProtoReq(new(Req)))
|
||||
|
||||
router.handler = func(ctx context.Context, initRequest transaction.Msg) (initResponse transaction.Msg, err error) {
|
||||
concrete, ok := initRequest.(ProtoReq)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: wanted %s, got %T", errInvalidMessage, reqName, initRequest)
|
||||
}
|
||||
return handler(ctx, concrete)
|
||||
}
|
||||
|
||||
router.schema = HandlerSchema{
|
||||
RequestSchema: *NewProtoMessageSchema[Req, ProtoReq](),
|
||||
ResponseSchema: *NewProtoMessageSchema[Resp, ProtoResp](),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterExecuteHandler registers an execution handler for a smart account that uses protobuf.
|
||||
func RegisterExecuteHandler[
|
||||
Req any, ProtoReq ProtoMsgG[Req], Resp any, ProtoResp ProtoMsgG[Resp],
|
||||
](router *ExecuteBuilder, handler func(ctx context.Context, req ProtoReq) (ProtoResp, error),
|
||||
) {
|
||||
reqName := MessageName(ProtoReq(new(Req)))
|
||||
// check if not registered already
|
||||
if _, ok := router.handlers[reqName]; ok {
|
||||
router.err = fmt.Errorf("handler already registered for message %s", reqName)
|
||||
return
|
||||
}
|
||||
|
||||
router.handlers[reqName] = func(ctx context.Context, executeRequest transaction.Msg) (executeResponse transaction.Msg, err error) {
|
||||
concrete, ok := executeRequest.(ProtoReq)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: wanted %s, got %T", errInvalidMessage, reqName, executeRequest)
|
||||
}
|
||||
return handler(ctx, concrete)
|
||||
}
|
||||
|
||||
router.handlersSchema[reqName] = HandlerSchema{
|
||||
RequestSchema: *NewProtoMessageSchema[Req, ProtoReq](),
|
||||
ResponseSchema: *NewProtoMessageSchema[Resp, ProtoResp](),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterQueryHandler registers a query handler for a smart account that uses protobuf.
|
||||
func RegisterQueryHandler[
|
||||
Req any, ProtoReq ProtoMsgG[Req], Resp any, ProtoResp ProtoMsgG[Resp],
|
||||
](router *QueryBuilder, handler func(ctx context.Context, req ProtoReq) (ProtoResp, error),
|
||||
) {
|
||||
RegisterExecuteHandler(router.er, handler)
|
||||
}
|
||||
|
||||
func NewProtoMessageSchema[T any, PT ProtoMsgG[T]]() *MessageSchema {
|
||||
msg := PT(new(T))
|
||||
if _, ok := (interface{}(msg)).(proto.Message); ok {
|
||||
panic("protov2 messages are not supported")
|
||||
}
|
||||
return &MessageSchema{
|
||||
Name: MessageName(msg),
|
||||
New: func() transaction.Msg {
|
||||
return PT(new(T))
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package address
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha512"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math/big"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
)
|
||||
|
||||
// ComputePublicKey computes the public key of a child key given the extended public key, chain code, coin type, and index.
|
||||
func ComputePublicKey(extPubKey []byte, chainCode []byte, coinType uint32, index int) ([]byte, error) {
|
||||
// Check if the index is a hardened child key
|
||||
if uint32(index) >= HardenedOffset {
|
||||
return nil, errors.New("cannot derive hardened child key from public key")
|
||||
}
|
||||
|
||||
// Serialize the public key
|
||||
pubKey, err := btcec.ParsePubKey(extPubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pubKeyBytes := pubKey.SerializeCompressed()
|
||||
|
||||
// Serialize the index
|
||||
indexBytes := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(indexBytes, uint32(index))
|
||||
|
||||
// Compute the HMAC-SHA512
|
||||
mac := hmac.New(sha512.New, chainCode)
|
||||
mac.Write(pubKeyBytes)
|
||||
mac.Write(indexBytes)
|
||||
I := mac.Sum(nil)
|
||||
|
||||
// Split I into two 32-byte sequences
|
||||
IL := I[:32]
|
||||
|
||||
// Convert IL to a big integer
|
||||
ilNum := new(big.Int).SetBytes(IL)
|
||||
|
||||
// Check if parse256(IL) >= n
|
||||
curve := btcec.S256()
|
||||
if ilNum.Cmp(curve.N) >= 0 {
|
||||
return nil, errors.New("invalid child key")
|
||||
}
|
||||
|
||||
// Compute the child public key: pubKey + IL * G
|
||||
ilx, ily := curve.ScalarBaseMult(IL)
|
||||
childX, childY := curve.Add(ilx, ily, pubKey.X(), pubKey.Y())
|
||||
lx := newBigIntFieldVal(childX)
|
||||
ly := newBigIntFieldVal(childY)
|
||||
|
||||
// Create the child public key
|
||||
childPubKey := btcec.NewPublicKey(lx, ly)
|
||||
childPubKeyBytes := childPubKey.SerializeCompressed()
|
||||
return childPubKeyBytes, nil
|
||||
}
|
||||
|
||||
// newBigIntFieldVal creates a new field value from a big integer.
|
||||
func newBigIntFieldVal(val *big.Int) *btcec.FieldVal {
|
||||
lx := new(btcec.FieldVal)
|
||||
lx.SetByteSlice(val.Bytes())
|
||||
return lx
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package address
|
||||
|
||||
type CoinType uint32
|
||||
|
||||
const (
|
||||
// Hardened offset for BIP-44 derivation
|
||||
HardenedOffset uint32 = 0x80000000
|
||||
|
||||
// Registered coin types for BIP-44
|
||||
CoinTypeBitcoin CoinType = CoinType(0 + HardenedOffset)
|
||||
CoinTypeEthereum CoinType = CoinType(60 + HardenedOffset)
|
||||
CoinTypeSonr CoinType = CoinType(703 + HardenedOffset)
|
||||
)
|
||||
|
||||
// Uint32 returns the coin type as a uint32.
|
||||
func (c CoinType) Uint32() uint32 {
|
||||
return uint32(c)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package appmodule
|
||||
|
||||
import (
|
||||
"cosmossdk.io/core/event"
|
||||
"cosmossdk.io/core/gas"
|
||||
"cosmossdk.io/core/header"
|
||||
"cosmossdk.io/core/store"
|
||||
|
||||
"github.com/sonr-io/snrd/internal/branch"
|
||||
"github.com/sonr-io/snrd/internal/log"
|
||||
"github.com/sonr-io/snrd/internal/router"
|
||||
"github.com/sonr-io/snrd/internal/transaction"
|
||||
)
|
||||
|
||||
// Environment is used to get all services to their respective module.
|
||||
// Contract: All fields of environment are always populated by runtime.
|
||||
type Environment struct {
|
||||
Logger log.Logger
|
||||
|
||||
BranchService branch.Service
|
||||
EventService event.Service
|
||||
GasService gas.Service
|
||||
HeaderService header.Service
|
||||
QueryRouterService router.Service
|
||||
MsgRouterService router.Service
|
||||
TransactionService transaction.Service
|
||||
|
||||
KVStoreService store.KVStoreService
|
||||
MemStoreService store.MemoryStoreService
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// Package branch contains the core branch service interface.
|
||||
package branch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// ErrGasLimitExceeded is returned when the gas limit is exceeded in a
|
||||
// Service.ExecuteWithGasLimit call.
|
||||
var ErrGasLimitExceeded = errors.New("branch: gas limit exceeded")
|
||||
|
||||
// Service is the branch service interface. It can be used to execute
|
||||
// code paths in an isolated execution context that can be reverted.
|
||||
// A revert typically means a rollback on events and state changes.
|
||||
type Service interface {
|
||||
// Execute executes the given function in an isolated context. If the
|
||||
// `f` function returns an error, the execution is considered failed,
|
||||
// and every change made affecting the execution context is rolled back.
|
||||
// If the function returns nil, the execution is considered successful, and
|
||||
// committed.
|
||||
// The context.Context passed to the `f` function is a child of the context
|
||||
// passed to the Execute function, and is what should be used with other
|
||||
// core services in order to ensure the execution remains isolated.
|
||||
Execute(ctx context.Context, f func(ctx context.Context) error) error
|
||||
// ExecuteWithGasLimit executes the given function `f` in an isolated context,
|
||||
// with the provided gas limit, this is advanced usage and is used to disallow
|
||||
// an execution path to consume an indefinite amount of gas.
|
||||
// If the execution fails or succeeds the gas limit is still applied to the
|
||||
// parent context, the function returns a gasUsed value which is the amount
|
||||
// of gas used by the execution path. If the execution path exceeds the gas
|
||||
// ErrGasLimitExceeded is returned.
|
||||
ExecuteWithGasLimit(ctx context.Context, gasLimit uint64, f func(ctx context.Context) error) (gasUsed uint64, err error)
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
crypto "github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
||||
"github.com/multiformats/go-multicodec"
|
||||
"github.com/multiformats/go-varint"
|
||||
)
|
||||
|
||||
// GenerateEd25519 generates an Ed25519 private key and the matching DID.
|
||||
// This is the RECOMMENDED algorithm.
|
||||
func GenerateEd25519() (crypto.PrivKey, DID, error) {
|
||||
priv, pub, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, Undef, nil
|
||||
}
|
||||
did, err := FromPubKey(pub)
|
||||
return priv, did, err
|
||||
}
|
||||
|
||||
// GenerateRSA generates a RSA private key and the matching DID.
|
||||
func GenerateRSA() (crypto.PrivKey, DID, error) {
|
||||
// NIST Special Publication 800-57 Part 1 Revision 5
|
||||
// Section 5.6.1.1 (Table 2)
|
||||
// Paraphrased: 2048-bit RSA keys are secure until 2030 and 3072-bit keys are recommended for longer-term security.
|
||||
const keyLength = 3072
|
||||
|
||||
priv, pub, err := crypto.GenerateRSAKeyPair(keyLength, rand.Reader)
|
||||
if err != nil {
|
||||
return nil, Undef, nil
|
||||
}
|
||||
did, err := FromPubKey(pub)
|
||||
return priv, did, err
|
||||
}
|
||||
|
||||
// GenerateSecp256k1 generates a Secp256k1 private key and the matching DID.
|
||||
func GenerateSecp256k1() (crypto.PrivKey, DID, error) {
|
||||
priv, pub, err := crypto.GenerateSecp256k1Key(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, Undef, nil
|
||||
}
|
||||
did, err := FromPubKey(pub)
|
||||
return priv, did, err
|
||||
}
|
||||
|
||||
// GenerateECDSA generates an ECDSA private key and the matching DID
|
||||
// for the default P256 curve.
|
||||
func GenerateECDSA() (crypto.PrivKey, DID, error) {
|
||||
return GenerateECDSAWithCurve(P256)
|
||||
}
|
||||
|
||||
// GenerateECDSAWithCurve generates an ECDSA private key and matching
|
||||
// DID for the user-supplied curve
|
||||
func GenerateECDSAWithCurve(code multicodec.Code) (crypto.PrivKey, DID, error) {
|
||||
var curve elliptic.Curve
|
||||
|
||||
switch code {
|
||||
case P256:
|
||||
curve = elliptic.P256()
|
||||
case P384:
|
||||
curve = elliptic.P384()
|
||||
case P521:
|
||||
curve = elliptic.P521()
|
||||
default:
|
||||
return nil, Undef, errors.New("unsupported ECDSA curve")
|
||||
}
|
||||
|
||||
priv, pub, err := crypto.GenerateECDSAKeyPairWithCurve(curve, rand.Reader)
|
||||
if err != nil {
|
||||
return nil, Undef, err
|
||||
}
|
||||
|
||||
did, err := FromPubKey(pub)
|
||||
|
||||
return priv, did, err
|
||||
}
|
||||
|
||||
// FromPrivKey is a convenience function that returns the DID associated
|
||||
// with the public key associated with the provided private key.
|
||||
func FromPrivKey(privKey crypto.PrivKey) (DID, error) {
|
||||
return FromPubKey(privKey.GetPublic())
|
||||
}
|
||||
|
||||
// FromPubKey returns a did:key constructed from the provided public key.
|
||||
func FromPubKey(pubKey crypto.PubKey) (DID, error) {
|
||||
var code multicodec.Code
|
||||
|
||||
switch pubKey.Type() {
|
||||
case pb.KeyType_Ed25519:
|
||||
code = multicodec.Ed25519Pub
|
||||
case pb.KeyType_RSA:
|
||||
code = RSA
|
||||
case pb.KeyType_Secp256k1:
|
||||
code = Secp256k1
|
||||
case pb.KeyType_ECDSA:
|
||||
var err error
|
||||
if code, err = codeForCurve(pubKey); err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
default:
|
||||
return Undef, errors.New("unsupported key type")
|
||||
}
|
||||
|
||||
if pubKey.Type() == pb.KeyType_ECDSA && code == Secp256k1 {
|
||||
var err error
|
||||
|
||||
pubKey, err = coerceECDSAToSecp256k1(pubKey)
|
||||
if err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
}
|
||||
|
||||
var bytes []byte
|
||||
|
||||
switch pubKey.Type() {
|
||||
case pb.KeyType_ECDSA:
|
||||
pkix, err := pubKey.Raw()
|
||||
if err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
|
||||
publicKey, err := x509.ParsePKIXPublicKey(pkix)
|
||||
if err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
|
||||
ecdsaPublicKey := publicKey.(*ecdsa.PublicKey)
|
||||
|
||||
bytes = elliptic.MarshalCompressed(ecdsaPublicKey.Curve, ecdsaPublicKey.X, ecdsaPublicKey.Y)
|
||||
case pb.KeyType_Ed25519, pb.KeyType_Secp256k1:
|
||||
var err error
|
||||
|
||||
if bytes, err = pubKey.Raw(); err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
case pb.KeyType_RSA:
|
||||
var err error
|
||||
|
||||
pkix, err := pubKey.Raw()
|
||||
if err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
|
||||
publicKey, err := x509.ParsePKIXPublicKey(pkix)
|
||||
if err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
|
||||
bytes = x509.MarshalPKCS1PublicKey(publicKey.(*rsa.PublicKey))
|
||||
}
|
||||
|
||||
return DID{
|
||||
code: code,
|
||||
bytes: string(append(varint.ToUvarint(uint64(code)), bytes...)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ToPubKey returns the crypto.PubKey encapsulated in the DID formed by
|
||||
// parsing the provided string.
|
||||
func ToPubKey(s string) (crypto.PubKey, error) {
|
||||
id, err := Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return id.PubKey()
|
||||
}
|
||||
|
||||
func codeForCurve(pubKey crypto.PubKey) (multicodec.Code, error) {
|
||||
stdPub, err := crypto.PubKeyToStdKey(pubKey)
|
||||
if err != nil {
|
||||
return multicodec.Identity, err
|
||||
}
|
||||
|
||||
ecdsaPub, ok := stdPub.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return multicodec.Identity, errors.New("failed to assert type for code to curve")
|
||||
}
|
||||
|
||||
switch ecdsaPub.Curve {
|
||||
case elliptic.P256():
|
||||
return P256, nil
|
||||
case elliptic.P384():
|
||||
return P384, nil
|
||||
case elliptic.P521():
|
||||
return P521, nil
|
||||
case secp256k1.S256():
|
||||
return Secp256k1, nil
|
||||
default:
|
||||
return multicodec.Identity, fmt.Errorf("unsupported ECDSA curve: %s", ecdsaPub.Curve.Params().Name)
|
||||
}
|
||||
}
|
||||
|
||||
// secp256k1.S256 is a valid ECDSA curve, but the go-libp2p/core/crypto
|
||||
// package treats it as a different type and has a different format for
|
||||
// the raw bytes of the public key.
|
||||
//
|
||||
// If a valid ECDSA public key was created using the secp256k1.S256 curve,
|
||||
// this function will "convert" it from a crypto.ECDSAPubKey to a
|
||||
// crypto.Secp256k1PublicKey.
|
||||
func coerceECDSAToSecp256k1(pubKey crypto.PubKey) (crypto.PubKey, error) {
|
||||
stdPub, err := crypto.PubKeyToStdKey(pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ecdsaPub, ok := stdPub.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to assert type for secp256k1 coersion")
|
||||
}
|
||||
|
||||
ecdsaPubBytes := append([]byte{0x04}, append(ecdsaPub.X.Bytes(), ecdsaPub.Y.Bytes()...)...)
|
||||
|
||||
secp256k1Pub, err := secp256k1.ParsePubKey(ecdsaPubBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cryptoPub := crypto.Secp256k1PublicKey(*secp256k1Pub)
|
||||
|
||||
return &cryptoPub, nil
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
crypto "github.com/libp2p/go-libp2p/core/crypto"
|
||||
mbase "github.com/multiformats/go-multibase"
|
||||
"github.com/multiformats/go-multicodec"
|
||||
varint "github.com/multiformats/go-varint"
|
||||
)
|
||||
|
||||
// Signature algorithms from the [did:key specification]
|
||||
//
|
||||
// [did:key specification]: https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm
|
||||
const (
|
||||
X25519 = multicodec.X25519Pub
|
||||
Ed25519 = multicodec.Ed25519Pub // UCAN required/recommended
|
||||
P256 = multicodec.P256Pub // UCAN required
|
||||
P384 = multicodec.P384Pub
|
||||
P521 = multicodec.P521Pub
|
||||
Secp256k1 = multicodec.Secp256k1Pub // UCAN required
|
||||
RSA = multicodec.RsaPub
|
||||
)
|
||||
|
||||
// Undef can be used to represent a nil or undefined DID, using DID{}
|
||||
// directly is also acceptable.
|
||||
var Undef = DID{}
|
||||
|
||||
// DID is a Decentralized Identifier of the did:key type, directly holding a cryptographic public key.
|
||||
// [did:key format]: https://w3c-ccg.github.io/did-method-key/
|
||||
type DID struct {
|
||||
code multicodec.Code
|
||||
bytes string // as string instead of []byte to allow the == operator
|
||||
}
|
||||
|
||||
// Parse returns the DID from the string representation or an error if
|
||||
// the prefix and method are incorrect, if an unknown encryption algorithm
|
||||
// is specified or if the method-specific-identifier's bytes don't
|
||||
// represent a public key for the specified encryption algorithm.
|
||||
func Parse(str string) (DID, error) {
|
||||
const keyPrefix = "did:key:"
|
||||
|
||||
if !strings.HasPrefix(str, keyPrefix) {
|
||||
return Undef, fmt.Errorf("must start with 'did:key'")
|
||||
}
|
||||
|
||||
baseCodec, bytes, err := mbase.Decode(str[len(keyPrefix):])
|
||||
if err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
if baseCodec != mbase.Base58BTC {
|
||||
return Undef, fmt.Errorf("not Base58BTC encoded")
|
||||
}
|
||||
code, _, err := varint.FromUvarint(bytes)
|
||||
if err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
switch multicodec.Code(code) {
|
||||
case Ed25519, P256, Secp256k1, RSA:
|
||||
return DID{bytes: string(bytes), code: multicodec.Code(code)}, nil
|
||||
default:
|
||||
return Undef, fmt.Errorf("unsupported did:key multicodec: 0x%x", code)
|
||||
}
|
||||
}
|
||||
|
||||
// MustParse is like Parse but panics instead of returning an error.
|
||||
func MustParse(str string) DID {
|
||||
did, err := Parse(str)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return did
|
||||
}
|
||||
|
||||
// Defined tells if the DID is defined, not equal to Undef.
|
||||
func (d DID) Defined() bool {
|
||||
return d.code != 0 || len(d.bytes) > 0
|
||||
}
|
||||
|
||||
// PubKey returns the public key encapsulated by the did:key.
|
||||
func (d DID) PubKey() (crypto.PubKey, error) {
|
||||
unmarshaler, ok := map[multicodec.Code]crypto.PubKeyUnmarshaller{
|
||||
X25519: crypto.UnmarshalEd25519PublicKey,
|
||||
Ed25519: crypto.UnmarshalEd25519PublicKey,
|
||||
P256: ecdsaPubKeyUnmarshaler(elliptic.P256()),
|
||||
P384: ecdsaPubKeyUnmarshaler(elliptic.P384()),
|
||||
P521: ecdsaPubKeyUnmarshaler(elliptic.P521()),
|
||||
Secp256k1: crypto.UnmarshalSecp256k1PublicKey,
|
||||
RSA: rsaPubKeyUnmarshaller,
|
||||
}[d.code]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported multicodec: %d", d.code)
|
||||
}
|
||||
|
||||
codeSize := varint.UvarintSize(uint64(d.code))
|
||||
return unmarshaler([]byte(d.bytes)[codeSize:])
|
||||
}
|
||||
|
||||
// String formats the decentralized identity document (DID) as a string.
|
||||
func (d DID) String() string {
|
||||
key, _ := mbase.Encode(mbase.Base58BTC, []byte(d.bytes))
|
||||
return "did:key:" + key
|
||||
}
|
||||
|
||||
func ecdsaPubKeyUnmarshaler(curve elliptic.Curve) crypto.PubKeyUnmarshaller {
|
||||
return func(data []byte) (crypto.PubKey, error) {
|
||||
x, y := elliptic.UnmarshalCompressed(curve, data)
|
||||
|
||||
ecdsaPublicKey := &ecdsa.PublicKey{
|
||||
Curve: curve,
|
||||
X: x,
|
||||
Y: y,
|
||||
}
|
||||
|
||||
pkix, err := x509.MarshalPKIXPublicKey(ecdsaPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return crypto.UnmarshalECDSAPublicKey(pkix)
|
||||
}
|
||||
}
|
||||
|
||||
func rsaPubKeyUnmarshaller(data []byte) (crypto.PubKey, error) {
|
||||
rsaPublicKey, err := x509.ParsePKCS1PublicKey(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pkix, err := x509.MarshalPKIXPublicKey(rsaPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return crypto.UnmarshalRsaPublicKey(pkix)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseDIDKey(t *testing.T) {
|
||||
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||
d, err := Parse(str)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, str, d.String())
|
||||
}
|
||||
|
||||
func TestMustParseDIDKey(t *testing.T) {
|
||||
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||
require.NotPanics(t, func() {
|
||||
d := MustParse(str)
|
||||
require.Equal(t, str, d.String())
|
||||
})
|
||||
str = "did:key:z7Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||
require.Panics(t, func() {
|
||||
MustParse(str)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEquivalence(t *testing.T) {
|
||||
undef0 := DID{}
|
||||
undef1 := Undef
|
||||
|
||||
did0, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
|
||||
require.NoError(t, err)
|
||||
did1, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, undef0 == undef1)
|
||||
require.False(t, undef0 == did0)
|
||||
require.True(t, did0 == did1)
|
||||
require.False(t, undef1 == did1)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package ipfsnode
|
||||
@@ -1 +0,0 @@
|
||||
package ipfsnode
|
||||
@@ -1,29 +0,0 @@
|
||||
package log
|
||||
|
||||
const ModuleKey = "module"
|
||||
|
||||
// Logger defines basic logger functionality that all previous versions of the Logger interface should
|
||||
// support. Library users should prefer to use this interface when possible, then type case to Logger
|
||||
// to see if WithContext is supported.
|
||||
type Logger interface {
|
||||
// Info takes a message and a set of key/value pairs and logs with level INFO.
|
||||
// The key of the tuple must be a string.
|
||||
Info(msg string, keyVals ...any)
|
||||
|
||||
// Warn takes a message and a set of key/value pairs and logs with level WARN.
|
||||
// The key of the tuple must be a string.
|
||||
Warn(msg string, keyVals ...any)
|
||||
|
||||
// Error takes a message and a set of key/value pairs and logs with level ERR.
|
||||
// The key of the tuple must be a string.
|
||||
Error(msg string, keyVals ...any)
|
||||
|
||||
// Debug takes a message and a set of key/value pairs and logs with level DEBUG.
|
||||
// The key of the tuple must be a string.
|
||||
Debug(msg string, keyVals ...any)
|
||||
|
||||
// Impl returns the underlying logger implementation.
|
||||
// It is used to access the full functionalities of the underlying logger.
|
||||
// Advanced users can type cast the returned value to the actual logger.
|
||||
Impl() any
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
number INTEGER NOT NULL,
|
||||
sequence INTEGER NOT NULL DEFAULT 0,
|
||||
address TEXT NOT NULL,
|
||||
public_key JSONB NOT NULL,
|
||||
chain_id TEXT NOT NULL,
|
||||
block_created INTEGER NOT NULL,
|
||||
controller TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
handle TEXT NOT NULL,
|
||||
is_subsidiary BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_validator BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_delegator BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_accountable BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
|
||||
|
||||
-- Unique constraints
|
||||
CREATE UNIQUE INDEX idx_accounts_address_unique ON accounts(address) WHERE deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX idx_accounts_handle_controller_unique ON accounts(handle, controller) WHERE deleted_at IS NULL;
|
||||
|
||||
-- Regular indexes for query performance
|
||||
CREATE INDEX idx_accounts_chain_id ON accounts(chain_id);
|
||||
CREATE INDEX idx_accounts_block_created ON accounts(block_created);
|
||||
CREATE INDEX idx_accounts_label ON accounts(label);
|
||||
CREATE INDEX idx_accounts_controller ON accounts(controller);
|
||||
CREATE INDEX idx_accounts_number ON accounts(number);
|
||||
CREATE INDEX idx_accounts_is_subsidiary ON accounts(is_subsidiary) WHERE is_subsidiary = TRUE;
|
||||
CREATE INDEX idx_accounts_is_validator ON accounts(is_validator) WHERE is_validator = TRUE;
|
||||
CREATE INDEX idx_accounts_is_delegator ON accounts(is_delegator) WHERE is_delegator = TRUE;
|
||||
CREATE INDEX idx_accounts_deleted_at ON accounts(deleted_at) WHERE deleted_at IS NOT NULL;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE accounts;
|
||||
-- +goose StatementEnd
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- Credentials store WebAuthn credentials
|
||||
CREATE TABLE credentials (
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
handle TEXT NOT NULL,
|
||||
raw_id TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'public-key',
|
||||
authenticator_attachment TEXT,
|
||||
transports TEXT[], -- Array of transport strings
|
||||
client_extension_results JSONB,
|
||||
attestation_object TEXT NOT NULL, -- Base64URL encoded
|
||||
client_data_json TEXT NOT NULL, -- Base64URL encoded
|
||||
public_key_alg INTEGER,
|
||||
public_key BYTEA
|
||||
);
|
||||
|
||||
-- Unique constraints
|
||||
CREATE UNIQUE INDEX idx_credentials_id_unique ON credentials(id) WHERE deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX idx_credentials_raw_id_unique ON credentials(raw_id) WHERE deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX idx_credentials_handle_unique ON credentials(handle) WHERE deleted_at IS NULL;
|
||||
|
||||
-- Regular indexes for query performance
|
||||
CREATE INDEX idx_credentials_handle ON credentials(handle);
|
||||
CREATE INDEX idx_credentials_type ON credentials(type);
|
||||
CREATE INDEX idx_credentials_authenticator_attachment ON credentials(authenticator_attachment);
|
||||
CREATE INDEX idx_credentials_deleted_at ON credentials(deleted_at) WHERE deleted_at IS NULL;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE credentials;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,30 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- Profiles represent user identities
|
||||
CREATE TABLE profiles (
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
address TEXT NOT NULL,
|
||||
handle TEXT NOT NULL,
|
||||
origin TEXT NOT NULL,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Unique constraints
|
||||
CREATE UNIQUE INDEX idx_profiles_handle_unique ON profiles(handle) WHERE deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX idx_profiles_address_origin_unique ON profiles(address, origin) WHERE deleted_at IS NULL;
|
||||
|
||||
-- Regular indexes for query performance
|
||||
CREATE INDEX idx_profiles_address ON profiles(address);
|
||||
CREATE INDEX idx_profiles_origin ON profiles(origin);
|
||||
CREATE INDEX idx_profiles_name ON profiles(name);
|
||||
CREATE INDEX idx_profiles_deleted_at ON profiles(deleted_at) WHERE deleted_at IS NOT NULL;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE profiles;
|
||||
-- +goose StatementEnd
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- Vaults store encrypted data
|
||||
CREATE TABLE vaults (
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
handle TEXT NOT NULL,
|
||||
origin TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
cid TEXT NOT NULL,
|
||||
config JSONB NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
redirect_uri TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Unique constraints
|
||||
CREATE UNIQUE INDEX idx_vaults_cid_unique ON vaults(cid) WHERE deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX idx_vaults_handle_origin_address_unique ON vaults(handle, origin, address) WHERE deleted_at IS NULL;
|
||||
|
||||
-- Regular indexes for query performance
|
||||
CREATE INDEX idx_vaults_handle ON vaults(handle);
|
||||
CREATE INDEX idx_vaults_origin ON vaults(origin);
|
||||
CREATE INDEX idx_vaults_address ON vaults(address);
|
||||
CREATE INDEX idx_vaults_session_id ON vaults(session_id);
|
||||
CREATE INDEX idx_vaults_deleted_at ON vaults(deleted_at) WHERE deleted_at IS NOT NULL;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE vaults;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,368 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- Create the http extension required for making HTTP requests
|
||||
CREATE EXTENSION IF NOT EXISTS http;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chains (
|
||||
chain_id VARCHAR(100) PRIMARY KEY,
|
||||
name VARCHAR(100),
|
||||
path VARCHAR(100),
|
||||
chain_name VARCHAR(100),
|
||||
network_type VARCHAR(50),
|
||||
pretty_name VARCHAR(200),
|
||||
status VARCHAR(50),
|
||||
bech32_prefix VARCHAR(20),
|
||||
slip44 INTEGER,
|
||||
symbol VARCHAR(20),
|
||||
display VARCHAR(20),
|
||||
denom VARCHAR(50),
|
||||
decimals INTEGER,
|
||||
image VARCHAR(500),
|
||||
website VARCHAR(500),
|
||||
height BIGINT,
|
||||
proxy_status JSONB,
|
||||
keywords TEXT[],
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chain_apis (
|
||||
id SERIAL PRIMARY KEY,
|
||||
chain_id VARCHAR(100) REFERENCES chains(chain_id) ON DELETE CASCADE,
|
||||
api_type VARCHAR(20), -- 'rest', 'rpc', 'grpc'
|
||||
address VARCHAR(500),
|
||||
provider VARCHAR(200),
|
||||
archive BOOLEAN DEFAULT FALSE,
|
||||
is_best_api BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chain_explorers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
chain_id VARCHAR(100) REFERENCES chains(chain_id) ON DELETE CASCADE,
|
||||
kind VARCHAR(50),
|
||||
url VARCHAR(500),
|
||||
tx_page VARCHAR(500),
|
||||
account_page VARCHAR(500),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 4. Create versions table
|
||||
CREATE TABLE IF NOT EXISTS chain_versions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
chain_id VARCHAR(100) REFERENCES chains(chain_id) ON DELETE CASCADE,
|
||||
application_name VARCHAR(100),
|
||||
application_version VARCHAR(50),
|
||||
consensus_name VARCHAR(100),
|
||||
consensus_version VARCHAR(50),
|
||||
cosmos_sdk_version VARCHAR(50),
|
||||
tendermint_version VARCHAR(50),
|
||||
ibc_go_version VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 5. Create assets table
|
||||
CREATE TABLE IF NOT EXISTS chain_assets (
|
||||
id SERIAL PRIMARY KEY,
|
||||
chain_id VARCHAR(100) REFERENCES chains(chain_id) ON DELETE CASCADE,
|
||||
asset_name VARCHAR(100),
|
||||
description VARCHAR(1000),
|
||||
denom_units JSONB,
|
||||
base VARCHAR(100),
|
||||
display VARCHAR(100),
|
||||
symbol VARCHAR(20),
|
||||
logo_uri VARCHAR(500),
|
||||
keywords TEXT[],
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 6. Create parameters table
|
||||
CREATE TABLE IF NOT EXISTS chain_params (
|
||||
id SERIAL PRIMARY KEY,
|
||||
chain_id VARCHAR(100) REFERENCES chains(chain_id) ON DELETE CASCADE,
|
||||
actual_block_time NUMERIC,
|
||||
actual_blocks_per_year NUMERIC,
|
||||
bonded_ratio NUMERIC,
|
||||
inflation JSONB,
|
||||
staking_apr NUMERIC,
|
||||
unbonding_time INTEGER,
|
||||
max_validators INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 7. Create indexes for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_chains_status ON chains(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_chains_network_type ON chains(network_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_chain_apis_type ON chain_apis(api_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_chain_apis_chain_id ON chain_apis(chain_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_chain_explorers_chain_id ON chain_explorers(chain_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_chain_versions_chain_id ON chain_versions(chain_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_chain_assets_chain_id ON chain_assets(chain_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_chain_params_chain_id ON chain_params(chain_id);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose StatementBegin
|
||||
-- 8. Main function to fetch and parse Cosmos chain data
|
||||
CREATE OR REPLACE FUNCTION fetch_and_parse_cosmos_chains()
|
||||
RETURNS TABLE(
|
||||
operation VARCHAR,
|
||||
chains_inserted INTEGER,
|
||||
apis_inserted INTEGER,
|
||||
explorers_inserted INTEGER,
|
||||
versions_inserted INTEGER,
|
||||
assets_inserted INTEGER,
|
||||
params_inserted INTEGER,
|
||||
total_processing_time INTERVAL
|
||||
) AS $$
|
||||
DECLARE
|
||||
start_time TIMESTAMP;
|
||||
cosmos_data JSONB;
|
||||
chains_count INTEGER := 0;
|
||||
apis_count INTEGER := 0;
|
||||
explorers_count INTEGER := 0;
|
||||
versions_count INTEGER := 0;
|
||||
assets_count INTEGER := 0;
|
||||
params_count INTEGER := 0;
|
||||
BEGIN
|
||||
start_time := clock_timestamp();
|
||||
|
||||
-- Fetch data from the Cosmos chain directory
|
||||
SELECT content::JSONB INTO cosmos_data
|
||||
FROM http_get('https://chains.cosmos.directory');
|
||||
|
||||
-- Clear existing data (optional - comment out if you want to preserve data)
|
||||
DELETE FROM chain_params;
|
||||
DELETE FROM chain_assets;
|
||||
DELETE FROM chain_versions;
|
||||
DELETE FROM chain_explorers;
|
||||
DELETE FROM chain_apis;
|
||||
DELETE FROM chains;
|
||||
|
||||
-- Insert chains data
|
||||
INSERT INTO chains (
|
||||
chain_id, name, path, chain_name, network_type, pretty_name,
|
||||
status, bech32_prefix, slip44, symbol, display, denom,
|
||||
decimals, image, website, height, proxy_status, keywords
|
||||
)
|
||||
SELECT
|
||||
(chain->>'chain_id')::VARCHAR(100),
|
||||
(chain->>'name')::VARCHAR(100),
|
||||
(chain->>'path')::VARCHAR(100),
|
||||
(chain->>'chain_name')::VARCHAR(100),
|
||||
(chain->>'network_type')::VARCHAR(50),
|
||||
(chain->>'pretty_name')::VARCHAR(200),
|
||||
(chain->>'status')::VARCHAR(50),
|
||||
(chain->>'bech32_prefix')::VARCHAR(20),
|
||||
(chain->>'slip44')::INTEGER,
|
||||
(chain->>'symbol')::VARCHAR(20),
|
||||
(chain->>'display')::VARCHAR(20),
|
||||
(chain->>'denom')::VARCHAR(50),
|
||||
(chain->>'decimals')::INTEGER,
|
||||
(chain->>'image')::VARCHAR(500),
|
||||
(chain->>'website')::VARCHAR(500),
|
||||
(chain->>'height')::BIGINT,
|
||||
chain->'proxy_status',
|
||||
CASE
|
||||
WHEN chain->'keywords' IS NOT NULL
|
||||
THEN ARRAY(SELECT jsonb_array_elements_text(chain->'keywords'))
|
||||
ELSE NULL
|
||||
END
|
||||
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain;
|
||||
|
||||
GET DIAGNOSTICS chains_count = ROW_COUNT;
|
||||
|
||||
-- Insert REST API endpoints
|
||||
INSERT INTO chain_apis (chain_id, api_type, address, provider, archive, is_best_api)
|
||||
SELECT
|
||||
(chain->>'chain_id')::VARCHAR(100),
|
||||
'rest'::VARCHAR(20),
|
||||
(api->>'address')::VARCHAR(500),
|
||||
(api->>'provider')::VARCHAR(200),
|
||||
COALESCE((api->>'archive')::BOOLEAN, false),
|
||||
true
|
||||
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain,
|
||||
JSONB_ARRAY_ELEMENTS(chain->'best_apis'->'rest') AS api
|
||||
WHERE chain->'best_apis'->'rest' IS NOT NULL;
|
||||
|
||||
-- Insert RPC API endpoints
|
||||
INSERT INTO chain_apis (chain_id, api_type, address, provider, archive, is_best_api)
|
||||
SELECT
|
||||
(chain->>'chain_id')::VARCHAR(100),
|
||||
'rpc'::VARCHAR(20),
|
||||
(api->>'address')::VARCHAR(500),
|
||||
(api->>'provider')::VARCHAR(200),
|
||||
COALESCE((api->>'archive')::BOOLEAN, false),
|
||||
true
|
||||
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain,
|
||||
JSONB_ARRAY_ELEMENTS(chain->'best_apis'->'rpc') AS api
|
||||
WHERE chain->'best_apis'->'rpc' IS NOT NULL;
|
||||
|
||||
-- Insert GRPC API endpoints
|
||||
INSERT INTO chain_apis (chain_id, api_type, address, provider, archive, is_best_api)
|
||||
SELECT
|
||||
(chain->>'chain_id')::VARCHAR(100),
|
||||
'grpc'::VARCHAR(20),
|
||||
(api->>'address')::VARCHAR(500),
|
||||
(api->>'provider')::VARCHAR(200),
|
||||
COALESCE((api->>'archive')::BOOLEAN, false),
|
||||
true
|
||||
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain,
|
||||
JSONB_ARRAY_ELEMENTS(chain->'best_apis'->'grpc') AS api
|
||||
WHERE chain->'best_apis'->'grpc' IS NOT NULL;
|
||||
|
||||
GET DIAGNOSTICS apis_count = ROW_COUNT;
|
||||
|
||||
-- Insert explorers
|
||||
INSERT INTO chain_explorers (chain_id, kind, url, tx_page, account_page)
|
||||
SELECT
|
||||
(chain->>'chain_id')::VARCHAR(100),
|
||||
(explorer->>'kind')::VARCHAR(50),
|
||||
(explorer->>'url')::VARCHAR(500),
|
||||
(explorer->>'tx_page')::VARCHAR(500),
|
||||
(explorer->>'account_page')::VARCHAR(500)
|
||||
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain,
|
||||
JSONB_ARRAY_ELEMENTS(chain->'explorers') AS explorer
|
||||
WHERE chain->'explorers' IS NOT NULL;
|
||||
|
||||
GET DIAGNOSTICS explorers_count = ROW_COUNT;
|
||||
|
||||
-- Insert version information
|
||||
INSERT INTO chain_versions (
|
||||
chain_id, application_name, application_version,
|
||||
consensus_name, consensus_version, cosmos_sdk_version,
|
||||
tendermint_version, ibc_go_version
|
||||
)
|
||||
SELECT
|
||||
(chain->>'chain_id')::VARCHAR(100),
|
||||
(chain->'versions'->>'name')::VARCHAR(100),
|
||||
(chain->'versions'->>'version')::VARCHAR(50),
|
||||
(chain->'versions'->'consensus'->>'name')::VARCHAR(100),
|
||||
(chain->'versions'->'consensus'->>'version')::VARCHAR(50),
|
||||
(chain->'versions'->>'cosmos_sdk_version')::VARCHAR(50),
|
||||
(chain->'versions'->>'tendermint_version')::VARCHAR(50),
|
||||
(chain->'versions'->>'ibc_go_version')::VARCHAR(50)
|
||||
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain
|
||||
WHERE chain->'versions' IS NOT NULL;
|
||||
|
||||
GET DIAGNOSTICS versions_count = ROW_COUNT;
|
||||
|
||||
-- Insert assets
|
||||
INSERT INTO chain_assets (
|
||||
chain_id, asset_name, description, denom_units,
|
||||
base, display, symbol, logo_uri, keywords
|
||||
)
|
||||
SELECT
|
||||
(chain->>'chain_id')::VARCHAR(100),
|
||||
(asset->>'name')::VARCHAR(100),
|
||||
(asset->>'description')::VARCHAR(1000),
|
||||
asset->'denom_units',
|
||||
(asset->>'base')::VARCHAR(100),
|
||||
(asset->>'display')::VARCHAR(100),
|
||||
(asset->>'symbol')::VARCHAR(20),
|
||||
(asset->'logo_URIs'->>'png')::VARCHAR(500),
|
||||
CASE
|
||||
WHEN asset->'keywords' IS NOT NULL
|
||||
THEN ARRAY(SELECT jsonb_array_elements_text(asset->'keywords'))
|
||||
ELSE NULL
|
||||
END
|
||||
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain,
|
||||
JSONB_ARRAY_ELEMENTS(chain->'assets') AS asset
|
||||
WHERE chain->'assets' IS NOT NULL;
|
||||
|
||||
GET DIAGNOSTICS assets_count = ROW_COUNT;
|
||||
|
||||
-- Insert parameters
|
||||
INSERT INTO chain_params (
|
||||
chain_id, actual_block_time, actual_blocks_per_year,
|
||||
bonded_ratio, inflation, staking_apr, unbonding_time, max_validators
|
||||
)
|
||||
SELECT
|
||||
(chain->>'chain_id')::VARCHAR(100),
|
||||
(chain->'params'->>'actual_block_time')::NUMERIC,
|
||||
(chain->'params'->>'actual_blocks_per_year')::NUMERIC,
|
||||
(chain->'params'->>'bonded_ratio')::NUMERIC,
|
||||
chain->'params'->'inflation',
|
||||
(chain->'params'->>'staking_apr')::NUMERIC,
|
||||
(chain->'params'->>'unbonding_time')::INTEGER,
|
||||
(chain->'params'->>'max_validators')::INTEGER
|
||||
FROM JSONB_ARRAY_ELEMENTS(cosmos_data->'chains') AS chain
|
||||
WHERE chain->'params' IS NOT NULL;
|
||||
|
||||
GET DIAGNOSTICS params_count = ROW_COUNT;
|
||||
|
||||
-- Return summary
|
||||
RETURN QUERY SELECT
|
||||
'SUCCESS'::VARCHAR as operation,
|
||||
chains_count,
|
||||
apis_count,
|
||||
explorers_count,
|
||||
versions_count,
|
||||
assets_count,
|
||||
params_count,
|
||||
clock_timestamp() - start_time as total_processing_time;
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
-- Return error information
|
||||
RETURN QUERY SELECT
|
||||
'ERROR'::VARCHAR as operation,
|
||||
0::INTEGER,
|
||||
0::INTEGER,
|
||||
0::INTEGER,
|
||||
0::INTEGER,
|
||||
0::INTEGER,
|
||||
0::INTEGER,
|
||||
clock_timestamp() - start_time as total_processing_time;
|
||||
RAISE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose StatementBegin
|
||||
-- 9. Convenience function to refresh data
|
||||
CREATE OR REPLACE FUNCTION refresh_cosmos_data()
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
PERFORM fetch_and_parse_cosmos_chains();
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose StatementBegin
|
||||
-- 10. Function to get chain summary statistics
|
||||
CREATE OR REPLACE FUNCTION get_cosmos_chain_stats()
|
||||
RETURNS TABLE(
|
||||
total_chains INTEGER,
|
||||
live_chains INTEGER,
|
||||
testnet_chains INTEGER,
|
||||
total_apis INTEGER,
|
||||
total_explorers INTEGER,
|
||||
chains_with_staking_info INTEGER,
|
||||
avg_staking_apr NUMERIC
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
(SELECT COUNT(*)::INTEGER FROM chains) as total_chains,
|
||||
(SELECT COUNT(*)::INTEGER FROM chains WHERE status = 'live') as live_chains,
|
||||
(SELECT COUNT(*)::INTEGER FROM chains WHERE network_type = 'testnet') as testnet_chains,
|
||||
(SELECT COUNT(*)::INTEGER FROM chain_apis) as total_apis,
|
||||
(SELECT COUNT(*)::INTEGER FROM chain_explorers) as total_explorers,
|
||||
(SELECT COUNT(*)::INTEGER FROM chain_params WHERE staking_apr IS NOT NULL) as chains_with_staking_info,
|
||||
(SELECT AVG(staking_apr) FROM chain_params WHERE staking_apr IS NOT NULL) as avg_staking_apr;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP FUNCTION IF EXISTS get_cosmos_chain_stats();
|
||||
DROP FUNCTION IF EXISTS refresh_cosmos_data();
|
||||
DROP FUNCTION IF EXISTS fetch_and_parse_cosmos_chains();
|
||||
DROP TABLE IF EXISTS chain_params CASCADE;
|
||||
DROP TABLE IF EXISTS chain_assets CASCADE;
|
||||
DROP TABLE IF EXISTS chain_versions CASCADE;
|
||||
DROP TABLE IF EXISTS chain_explorers CASCADE;
|
||||
DROP TABLE IF EXISTS chain_apis CASCADE;
|
||||
DROP TABLE IF EXISTS chains CASCADE;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,16 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- Step 1: Execute the main function to fetch and parse data
|
||||
SELECT * FROM fetch_and_parse_cosmos_chains();
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
-- Clean up data from all tables
|
||||
DELETE FROM chain_params;
|
||||
DELETE FROM chain_assets;
|
||||
DELETE FROM chain_versions;
|
||||
DELETE FROM chain_explorers;
|
||||
DELETE FROM chain_apis;
|
||||
DELETE FROM chains;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,29 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE OR REPLACE FUNCTION convert_webauthn_to_vc(
|
||||
webauthn_credential JSONB
|
||||
) RETURNS JSONB AS $$
|
||||
DECLARE
|
||||
vc_result JSONB;
|
||||
BEGIN
|
||||
-- Create the Verifiable Credential structure based on W3C standard
|
||||
vc_result = jsonb_build_object(
|
||||
'@context', jsonb_build_array('https://www.w3.org/2018/credentials/v1'),
|
||||
'type', jsonb_build_array('VerifiableCredential', 'WebAuthnCredential'),
|
||||
'issuer', jsonb_build_object(
|
||||
'id', 'did:example:' || encode(digest(webauthn_credential->>'rpId', 'sha256'), 'hex')
|
||||
),
|
||||
'issuanceDate', to_char(now() at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"'),
|
||||
'credentialSubject', jsonb_build_object(
|
||||
'id', 'did:key:' || encode(digest(webauthn_credential->>'id', 'sha256'), 'hex'),
|
||||
'webauthn', webauthn_credential
|
||||
)
|
||||
);
|
||||
|
||||
RETURN vc_result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
DROP FUNCTION convert_webauthn_to_vc;
|
||||
@@ -0,0 +1,486 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE IF NOT EXISTS crypto_global_market_data (
|
||||
id SERIAL PRIMARY KEY,
|
||||
market_cap_usd NUMERIC(20, 2),
|
||||
volume_24h_usd NUMERIC(20, 2),
|
||||
bitcoin_dominance_percentage NUMERIC(5, 2),
|
||||
cryptocurrencies_number INTEGER,
|
||||
market_cap_ath_value NUMERIC(20, 2),
|
||||
market_cap_ath_date TIMESTAMP WITH TIME ZONE,
|
||||
volume_24h_ath_value NUMERIC(20, 2),
|
||||
volume_24h_ath_date TIMESTAMP WITH TIME ZONE,
|
||||
market_cap_change_24h NUMERIC(10, 2),
|
||||
volume_24h_change_24h NUMERIC(10, 2),
|
||||
last_updated INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_crypto_global_market_data_last_updated ON crypto_global_market_data(last_updated DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_crypto_global_market_data_created_at ON crypto_global_market_data(created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crypto_coins (
|
||||
id VARCHAR(100) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
symbol VARCHAR(20) NOT NULL,
|
||||
rank INTEGER,
|
||||
is_new BOOLEAN DEFAULT FALSE,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
type VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_crypto_coins_symbol ON crypto_coins(symbol);
|
||||
CREATE INDEX IF NOT EXISTS idx_crypto_coins_rank ON crypto_coins(rank);
|
||||
CREATE INDEX IF NOT EXISTS idx_crypto_coins_is_active ON crypto_coins(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_crypto_coins_type ON crypto_coins(type);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crypto_coin_details (
|
||||
id VARCHAR(100) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
symbol VARCHAR(20) NOT NULL,
|
||||
parent_id VARCHAR(100),
|
||||
parent_name VARCHAR(255),
|
||||
parent_symbol VARCHAR(20),
|
||||
rank INTEGER,
|
||||
is_new BOOLEAN DEFAULT FALSE,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
type VARCHAR(50),
|
||||
logo VARCHAR(500),
|
||||
tags JSONB,
|
||||
team JSONB,
|
||||
description TEXT,
|
||||
message TEXT,
|
||||
open_source BOOLEAN,
|
||||
hardware_wallet BOOLEAN,
|
||||
started_at TIMESTAMP WITH TIME ZONE,
|
||||
development_status VARCHAR(100),
|
||||
proof_type VARCHAR(100),
|
||||
org_structure VARCHAR(100),
|
||||
hash_algorithm VARCHAR(100),
|
||||
contract VARCHAR(255),
|
||||
platform VARCHAR(255),
|
||||
contracts JSONB,
|
||||
links JSONB,
|
||||
links_extended JSONB,
|
||||
whitepaper JSONB,
|
||||
first_data_at TIMESTAMP WITH TIME ZONE,
|
||||
last_data_at TIMESTAMP WITH TIME ZONE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_crypto_coin_details_symbol ON crypto_coin_details(symbol);
|
||||
CREATE INDEX IF NOT EXISTS idx_crypto_coin_details_rank ON crypto_coin_details(rank);
|
||||
CREATE INDEX IF NOT EXISTS idx_crypto_coin_details_type ON crypto_coin_details(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_crypto_coin_details_started_at ON crypto_coin_details(started_at);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose StatementBegin
|
||||
CREATE OR REPLACE FUNCTION fetch_coinpaprika_global_market_data()
|
||||
RETURNS TABLE(
|
||||
operation VARCHAR,
|
||||
records_inserted INTEGER,
|
||||
last_market_cap NUMERIC,
|
||||
last_volume NUMERIC,
|
||||
processing_time INTERVAL
|
||||
) AS $$
|
||||
DECLARE
|
||||
start_time TIMESTAMP;
|
||||
market_data JSONB;
|
||||
insert_count INTEGER := 0;
|
||||
last_cap NUMERIC;
|
||||
last_vol NUMERIC;
|
||||
BEGIN
|
||||
start_time := clock_timestamp();
|
||||
SELECT content::JSONB INTO market_data
|
||||
FROM http_get('https://api.coinpaprika.com/v1/global');
|
||||
INSERT INTO crypto_global_market_data (
|
||||
market_cap_usd,
|
||||
volume_24h_usd,
|
||||
bitcoin_dominance_percentage,
|
||||
cryptocurrencies_number,
|
||||
market_cap_ath_value,
|
||||
market_cap_ath_date,
|
||||
volume_24h_ath_value,
|
||||
volume_24h_ath_date,
|
||||
market_cap_change_24h,
|
||||
volume_24h_change_24h,
|
||||
last_updated
|
||||
)
|
||||
SELECT
|
||||
(market_data->>'market_cap_usd')::NUMERIC,
|
||||
(market_data->>'volume_24h_usd')::NUMERIC,
|
||||
(market_data->>'bitcoin_dominance_percentage')::NUMERIC,
|
||||
(market_data->>'cryptocurrencies_number')::INTEGER,
|
||||
(market_data->>'market_cap_ath_value')::NUMERIC,
|
||||
(market_data->>'market_cap_ath_date')::TIMESTAMP WITH TIME ZONE,
|
||||
(market_data->>'volume_24h_ath_value')::NUMERIC,
|
||||
(market_data->>'volume_24h_ath_date')::TIMESTAMP WITH TIME ZONE,
|
||||
(market_data->>'market_cap_change_24h')::NUMERIC,
|
||||
(market_data->>'volume_24h_change_24h')::NUMERIC,
|
||||
(market_data->>'last_updated')::INTEGER
|
||||
RETURNING market_cap_usd, volume_24h_usd INTO last_cap, last_vol;
|
||||
|
||||
GET DIAGNOSTICS insert_count = ROW_COUNT;
|
||||
|
||||
RETURN QUERY SELECT
|
||||
'SUCCESS'::VARCHAR as operation,
|
||||
insert_count as records_inserted,
|
||||
last_cap as last_market_cap,
|
||||
last_vol as last_volume,
|
||||
clock_timestamp() - start_time as processing_time;
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN QUERY SELECT
|
||||
'ERROR: ' || SQLERRM::VARCHAR as operation,
|
||||
0::INTEGER as records_inserted,
|
||||
0::NUMERIC as last_market_cap,
|
||||
0::NUMERIC as last_volume,
|
||||
clock_timestamp() - start_time as processing_time;
|
||||
RAISE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose StatementBegin
|
||||
CREATE OR REPLACE FUNCTION refresh_market_data()
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
PERFORM fetch_coinpaprika_global_market_data();
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose StatementBegin
|
||||
CREATE OR REPLACE FUNCTION fetch_coinpaprika_coins()
|
||||
RETURNS TABLE(
|
||||
operation VARCHAR,
|
||||
records_upserted INTEGER,
|
||||
new_coins INTEGER,
|
||||
updated_coins INTEGER,
|
||||
processing_time INTERVAL
|
||||
) AS $$
|
||||
DECLARE
|
||||
start_time TIMESTAMP;
|
||||
coins_data JSONB;
|
||||
coin_record JSONB;
|
||||
upsert_count INTEGER := 0;
|
||||
new_count INTEGER := 0;
|
||||
update_count INTEGER := 0;
|
||||
existing_id VARCHAR;
|
||||
BEGIN
|
||||
start_time := clock_timestamp();
|
||||
SELECT content::JSONB INTO coins_data
|
||||
FROM http_get('https://api.coinpaprika.com/v1/coins');
|
||||
FOR coin_record IN SELECT * FROM jsonb_array_elements(coins_data)
|
||||
LOOP
|
||||
SELECT id INTO existing_id FROM crypto_coins WHERE id = coin_record->>'id';
|
||||
INSERT INTO crypto_coins (
|
||||
id,
|
||||
name,
|
||||
symbol,
|
||||
rank,
|
||||
is_new,
|
||||
is_active,
|
||||
type,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
coin_record->>'id',
|
||||
coin_record->>'name',
|
||||
coin_record->>'symbol',
|
||||
(coin_record->>'rank')::INTEGER,
|
||||
(coin_record->>'is_new')::BOOLEAN,
|
||||
(coin_record->>'is_active')::BOOLEAN,
|
||||
coin_record->>'type',
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
symbol = EXCLUDED.symbol,
|
||||
rank = EXCLUDED.rank,
|
||||
is_new = EXCLUDED.is_new,
|
||||
is_active = EXCLUDED.is_active,
|
||||
type = EXCLUDED.type,
|
||||
updated_at = CURRENT_TIMESTAMP;
|
||||
|
||||
upsert_count := upsert_count + 1;
|
||||
|
||||
IF existing_id IS NULL THEN
|
||||
new_count := new_count + 1;
|
||||
ELSE
|
||||
update_count := update_count + 1;
|
||||
END IF;
|
||||
END LOOP;
|
||||
RETURN QUERY SELECT
|
||||
'SUCCESS'::VARCHAR as operation,
|
||||
upsert_count as records_upserted,
|
||||
new_count as new_coins,
|
||||
update_count as updated_coins,
|
||||
clock_timestamp() - start_time as processing_time;
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN QUERY SELECT
|
||||
'ERROR: ' || SQLERRM::VARCHAR as operation,
|
||||
0::INTEGER as records_upserted,
|
||||
0::INTEGER as new_coins,
|
||||
0::INTEGER as updated_coins,
|
||||
clock_timestamp() - start_time as processing_time;
|
||||
RAISE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose StatementBegin
|
||||
CREATE OR REPLACE FUNCTION refresh_all_coinpaprika_data()
|
||||
RETURNS TABLE(
|
||||
operation VARCHAR,
|
||||
market_data_result VARCHAR,
|
||||
coins_result VARCHAR,
|
||||
total_processing_time INTERVAL
|
||||
) AS $$
|
||||
DECLARE
|
||||
start_time TIMESTAMP;
|
||||
market_result RECORD;
|
||||
coins_result RECORD;
|
||||
BEGIN
|
||||
start_time := clock_timestamp();
|
||||
|
||||
-- Fetch market data
|
||||
SELECT * INTO market_result FROM fetch_coinpaprika_global_market_data();
|
||||
|
||||
-- Fetch coins data
|
||||
SELECT * INTO coins_result FROM fetch_coinpaprika_coins();
|
||||
|
||||
-- Return combined results
|
||||
RETURN QUERY SELECT
|
||||
'COMPLETE'::VARCHAR as operation,
|
||||
market_result.operation as market_data_result,
|
||||
coins_result.operation as coins_result,
|
||||
clock_timestamp() - start_time as total_processing_time;
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN QUERY SELECT
|
||||
'ERROR'::VARCHAR as operation,
|
||||
'ERROR: ' || SQLERRM::VARCHAR as market_data_result,
|
||||
'ERROR: ' || SQLERRM::VARCHAR as coins_result,
|
||||
clock_timestamp() - start_time as total_processing_time;
|
||||
RAISE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose StatementBegin
|
||||
-- Function to fetch and store detailed coin information
|
||||
CREATE OR REPLACE FUNCTION fetch_coinpaprika_coin_details(coin_id VARCHAR)
|
||||
RETURNS TABLE(
|
||||
operation VARCHAR,
|
||||
coin_name VARCHAR,
|
||||
coin_symbol VARCHAR,
|
||||
processing_time INTERVAL
|
||||
) AS $$
|
||||
DECLARE
|
||||
start_time TIMESTAMP;
|
||||
coin_data JSONB;
|
||||
parent_data JSONB;
|
||||
BEGIN
|
||||
start_time := clock_timestamp();
|
||||
|
||||
-- Fetch data from CoinPaprika coin details endpoint
|
||||
SELECT content::JSONB INTO coin_data
|
||||
FROM http_get('https://api.coinpaprika.com/v1/coins/' || coin_id);
|
||||
|
||||
-- Extract parent data if exists
|
||||
parent_data := coin_data->'parent';
|
||||
|
||||
-- Insert or update the coin details
|
||||
INSERT INTO crypto_coin_details (
|
||||
id,
|
||||
name,
|
||||
symbol,
|
||||
parent_id,
|
||||
parent_name,
|
||||
parent_symbol,
|
||||
rank,
|
||||
is_new,
|
||||
is_active,
|
||||
type,
|
||||
logo,
|
||||
tags,
|
||||
team,
|
||||
description,
|
||||
message,
|
||||
open_source,
|
||||
hardware_wallet,
|
||||
started_at,
|
||||
development_status,
|
||||
proof_type,
|
||||
org_structure,
|
||||
hash_algorithm,
|
||||
contract,
|
||||
platform,
|
||||
contracts,
|
||||
links,
|
||||
links_extended,
|
||||
whitepaper,
|
||||
first_data_at,
|
||||
last_data_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
coin_data->>'id',
|
||||
coin_data->>'name',
|
||||
coin_data->>'symbol',
|
||||
CASE WHEN parent_data IS NOT NULL THEN parent_data->>'id' ELSE NULL END,
|
||||
CASE WHEN parent_data IS NOT NULL THEN parent_data->>'name' ELSE NULL END,
|
||||
CASE WHEN parent_data IS NOT NULL THEN parent_data->>'symbol' ELSE NULL END,
|
||||
(coin_data->>'rank')::INTEGER,
|
||||
(coin_data->>'is_new')::BOOLEAN,
|
||||
(coin_data->>'is_active')::BOOLEAN,
|
||||
coin_data->>'type',
|
||||
coin_data->>'logo',
|
||||
coin_data->'tags',
|
||||
coin_data->'team',
|
||||
coin_data->>'description',
|
||||
coin_data->>'message',
|
||||
(coin_data->>'open_source')::BOOLEAN,
|
||||
(coin_data->>'hardware_wallet')::BOOLEAN,
|
||||
(coin_data->>'started_at')::TIMESTAMP WITH TIME ZONE,
|
||||
coin_data->>'development_status',
|
||||
coin_data->>'proof_type',
|
||||
coin_data->>'org_structure',
|
||||
coin_data->>'hash_algorithm',
|
||||
coin_data->>'contract',
|
||||
coin_data->>'platform',
|
||||
coin_data->'contracts',
|
||||
coin_data->'links',
|
||||
coin_data->'links_extended',
|
||||
coin_data->'whitepaper',
|
||||
(coin_data->>'first_data_at')::TIMESTAMP WITH TIME ZONE,
|
||||
(coin_data->>'last_data_at')::TIMESTAMP WITH TIME ZONE,
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
symbol = EXCLUDED.symbol,
|
||||
parent_id = EXCLUDED.parent_id,
|
||||
parent_name = EXCLUDED.parent_name,
|
||||
parent_symbol = EXCLUDED.parent_symbol,
|
||||
rank = EXCLUDED.rank,
|
||||
is_new = EXCLUDED.is_new,
|
||||
is_active = EXCLUDED.is_active,
|
||||
type = EXCLUDED.type,
|
||||
logo = EXCLUDED.logo,
|
||||
tags = EXCLUDED.tags,
|
||||
team = EXCLUDED.team,
|
||||
description = EXCLUDED.description,
|
||||
message = EXCLUDED.message,
|
||||
open_source = EXCLUDED.open_source,
|
||||
hardware_wallet = EXCLUDED.hardware_wallet,
|
||||
started_at = EXCLUDED.started_at,
|
||||
development_status = EXCLUDED.development_status,
|
||||
proof_type = EXCLUDED.proof_type,
|
||||
org_structure = EXCLUDED.org_structure,
|
||||
hash_algorithm = EXCLUDED.hash_algorithm,
|
||||
contract = EXCLUDED.contract,
|
||||
platform = EXCLUDED.platform,
|
||||
contracts = EXCLUDED.contracts,
|
||||
links = EXCLUDED.links,
|
||||
links_extended = EXCLUDED.links_extended,
|
||||
whitepaper = EXCLUDED.whitepaper,
|
||||
first_data_at = EXCLUDED.first_data_at,
|
||||
last_data_at = EXCLUDED.last_data_at,
|
||||
updated_at = CURRENT_TIMESTAMP;
|
||||
|
||||
-- Return summary
|
||||
RETURN QUERY SELECT
|
||||
'SUCCESS'::VARCHAR as operation,
|
||||
coin_data->>'name' as coin_name,
|
||||
coin_data->>'symbol' as coin_symbol,
|
||||
clock_timestamp() - start_time as processing_time;
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
-- Return error information
|
||||
RETURN QUERY SELECT
|
||||
'ERROR: ' || SQLERRM::VARCHAR as operation,
|
||||
NULL::VARCHAR as coin_name,
|
||||
NULL::VARCHAR as coin_symbol,
|
||||
clock_timestamp() - start_time as processing_time;
|
||||
RAISE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose StatementBegin
|
||||
-- Function to fetch details for top N coins
|
||||
CREATE OR REPLACE FUNCTION fetch_top_coins_details(limit_count INTEGER DEFAULT 10)
|
||||
RETURNS TABLE(
|
||||
operation VARCHAR,
|
||||
coins_processed INTEGER,
|
||||
successful_fetches INTEGER,
|
||||
failed_fetches INTEGER,
|
||||
processing_time INTERVAL
|
||||
) AS $$
|
||||
DECLARE
|
||||
start_time TIMESTAMP;
|
||||
coin_rec RECORD;
|
||||
success_count INTEGER := 0;
|
||||
fail_count INTEGER := 0;
|
||||
total_count INTEGER := 0;
|
||||
fetch_result RECORD;
|
||||
BEGIN
|
||||
start_time := clock_timestamp();
|
||||
|
||||
-- Fetch details for each top coin
|
||||
FOR coin_rec IN
|
||||
SELECT id FROM crypto_coins
|
||||
WHERE is_active = TRUE AND rank IS NOT NULL
|
||||
ORDER BY rank
|
||||
LIMIT limit_count
|
||||
LOOP
|
||||
total_count := total_count + 1;
|
||||
|
||||
-- Try to fetch coin details
|
||||
BEGIN
|
||||
SELECT * INTO fetch_result FROM fetch_coinpaprika_coin_details(coin_rec.id);
|
||||
IF fetch_result.operation = 'SUCCESS' THEN
|
||||
success_count := success_count + 1;
|
||||
ELSE
|
||||
fail_count := fail_count + 1;
|
||||
END IF;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
fail_count := fail_count + 1;
|
||||
END;
|
||||
END LOOP;
|
||||
|
||||
-- Return summary
|
||||
RETURN QUERY SELECT
|
||||
'COMPLETE'::VARCHAR as operation,
|
||||
total_count as coins_processed,
|
||||
success_count as successful_fetches,
|
||||
fail_count as failed_fetches,
|
||||
clock_timestamp() - start_time as processing_time;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP FUNCTION IF EXISTS fetch_top_coins_details(INTEGER);
|
||||
DROP FUNCTION IF EXISTS fetch_coinpaprika_coin_details(VARCHAR);
|
||||
DROP FUNCTION IF EXISTS refresh_all_coinpaprika_data();
|
||||
DROP FUNCTION IF EXISTS fetch_coinpaprika_coins();
|
||||
DROP FUNCTION IF EXISTS refresh_market_data();
|
||||
DROP FUNCTION IF EXISTS fetch_coinpaprika_global_market_data();
|
||||
DROP TABLE IF EXISTS crypto_coin_details CASCADE;
|
||||
DROP TABLE IF EXISTS crypto_coins CASCADE;
|
||||
DROP TABLE IF EXISTS crypto_global_market_data CASCADE;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,139 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
|
||||
-- Enable pgcrypto extension for gen_random_bytes if not already enabled
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- Function to generate WebAuthn registration options with largeBlob and payment extensions
|
||||
CREATE OR REPLACE FUNCTION generate_webauthn_register_options(
|
||||
session_id UUID,
|
||||
vault_id UUID
|
||||
) RETURNS JSONB AS $$
|
||||
DECLARE
|
||||
challenge_bytes BYTEA;
|
||||
challenge_base64 TEXT;
|
||||
rp_id TEXT;
|
||||
rp_name TEXT;
|
||||
user_name TEXT;
|
||||
user_display_name TEXT;
|
||||
BEGIN
|
||||
-- Generate a random challenge based on session_id
|
||||
challenge_bytes = gen_random_bytes(32);
|
||||
challenge_base64 = encode(challenge_bytes, 'base64');
|
||||
|
||||
-- Set default user information
|
||||
user_name = 'user@' || session_id::TEXT;
|
||||
user_display_name = 'User';
|
||||
|
||||
-- Set relying party information
|
||||
rp_id = 'localhost'; -- This should be configurable
|
||||
rp_name = 'Sonr Highway';
|
||||
|
||||
-- Build and return the registration options
|
||||
RETURN jsonb_build_object(
|
||||
'challenge', challenge_base64,
|
||||
'rp', jsonb_build_object(
|
||||
'id', rp_id,
|
||||
'name', rp_name
|
||||
),
|
||||
'user', jsonb_build_object(
|
||||
'id', encode(session_id::TEXT::BYTEA, 'base64'),
|
||||
'name', user_name,
|
||||
'displayName', user_display_name
|
||||
),
|
||||
'pubKeyCredParams', jsonb_build_array(
|
||||
jsonb_build_object('type', 'public-key', 'alg', -7), -- ES256
|
||||
jsonb_build_object('type', 'public-key', 'alg', -257) -- RS256
|
||||
),
|
||||
'timeout', 60000,
|
||||
'attestation', 'direct',
|
||||
'authenticatorSelection', jsonb_build_object(
|
||||
'authenticatorAttachment', 'platform',
|
||||
'userVerification', 'required',
|
||||
'residentKey', 'required',
|
||||
'requireResidentKey', true
|
||||
),
|
||||
'extensions', jsonb_build_object(
|
||||
'largeBlob', jsonb_build_object(
|
||||
'support', 'required'
|
||||
),
|
||||
'payment', jsonb_build_object(
|
||||
'isPayment', true
|
||||
)
|
||||
)
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Function to generate WebAuthn login options with largeBlob and payment extensions
|
||||
CREATE OR REPLACE FUNCTION generate_webauthn_login_options(
|
||||
session_id UUID,
|
||||
vault_id UUID
|
||||
) RETURNS JSONB AS $$
|
||||
DECLARE
|
||||
challenge_bytes BYTEA;
|
||||
challenge_base64 TEXT;
|
||||
allow_credentials JSONB;
|
||||
rp_id TEXT;
|
||||
BEGIN
|
||||
-- Generate a random challenge based on session_id
|
||||
challenge_bytes = gen_random_bytes(32);
|
||||
challenge_base64 = encode(challenge_bytes, 'base64');
|
||||
|
||||
-- Set relying party information
|
||||
rp_id = 'localhost'; -- This should be configurable
|
||||
|
||||
-- Set empty allowed credentials (no vault_id column exists)
|
||||
allow_credentials = '[]'::JSONB;
|
||||
|
||||
-- Build and return the login options
|
||||
RETURN jsonb_build_object(
|
||||
'challenge', challenge_base64,
|
||||
'timeout', 60000,
|
||||
'rpId', rp_id,
|
||||
'allowCredentials', allow_credentials,
|
||||
'userVerification', 'required',
|
||||
'extensions', jsonb_build_object(
|
||||
'largeBlob', jsonb_build_object(
|
||||
'support', 'required',
|
||||
'read', true,
|
||||
'write', true
|
||||
),
|
||||
'payment', jsonb_build_object(
|
||||
'isPayment', true,
|
||||
'payeeName', 'Sonr Network',
|
||||
'payeeOrigin', 'https://sonr.io',
|
||||
'total', jsonb_build_object(
|
||||
'currency', 'USD',
|
||||
'value', '0.00'
|
||||
),
|
||||
'instrument', jsonb_build_object(
|
||||
'displayName', 'Sonr Wallet',
|
||||
'icon', 'https://sonr.io/icon.png'
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Add comments for documentation
|
||||
COMMENT ON FUNCTION generate_webauthn_register_options(UUID, UUID) IS
|
||||
'Generates WebAuthn registration options with largeBlob and payment extensions.
|
||||
Parameters:
|
||||
- session_id: UUID for generating unique challenge
|
||||
- vault_id: UUID to associate with user information
|
||||
Returns: JSONB object with WebAuthn registration options including required extensions';
|
||||
|
||||
COMMENT ON FUNCTION generate_webauthn_login_options(UUID, UUID) IS
|
||||
'Generates WebAuthn login options with largeBlob and payment extensions.
|
||||
Parameters:
|
||||
- session_id: UUID for generating unique challenge
|
||||
- vault_id: UUID to lookup allowed credentials
|
||||
Returns: JSONB object with WebAuthn login options including read/write largeBlob and SPC payment extensions';
|
||||
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
DROP FUNCTION IF EXISTS generate_webauthn_login_options(UUID, UUID);
|
||||
DROP FUNCTION IF EXISTS generate_webauthn_register_options(UUID, UUID);
|
||||
@@ -0,0 +1,176 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- Create a junction table to link chain assets with crypto coins by symbol
|
||||
CREATE TABLE IF NOT EXISTS asset_symbol_links (
|
||||
id SERIAL PRIMARY KEY,
|
||||
chain_asset_id INTEGER REFERENCES chain_assets(id) ON DELETE CASCADE,
|
||||
crypto_coin_id VARCHAR(100) REFERENCES crypto_coins(id) ON DELETE CASCADE,
|
||||
symbol VARCHAR(20) NOT NULL,
|
||||
match_confidence VARCHAR(20) DEFAULT 'exact', -- 'exact', 'case_insensitive', 'manual'
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(chain_asset_id, crypto_coin_id)
|
||||
);
|
||||
|
||||
-- Create indexes for efficient lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_asset_symbol_links_symbol ON asset_symbol_links(symbol);
|
||||
CREATE INDEX IF NOT EXISTS idx_asset_symbol_links_chain_asset ON asset_symbol_links(chain_asset_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_asset_symbol_links_crypto_coin ON asset_symbol_links(crypto_coin_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_asset_symbol_links_confidence ON asset_symbol_links(match_confidence);
|
||||
|
||||
-- Create a composite index on both tables' symbol columns for faster joins
|
||||
CREATE INDEX IF NOT EXISTS idx_chain_assets_symbol_lower ON chain_assets(LOWER(symbol));
|
||||
CREATE INDEX IF NOT EXISTS idx_crypto_coins_symbol_lower ON crypto_coins(LOWER(symbol));
|
||||
|
||||
-- Function to populate asset symbol links based on matching symbols
|
||||
CREATE OR REPLACE FUNCTION populate_asset_symbol_links()
|
||||
RETURNS TABLE(
|
||||
operation VARCHAR,
|
||||
exact_matches INTEGER,
|
||||
case_insensitive_matches INTEGER,
|
||||
total_links_created INTEGER,
|
||||
processing_time INTERVAL
|
||||
) AS $$
|
||||
DECLARE
|
||||
start_time TIMESTAMP;
|
||||
exact_count INTEGER := 0;
|
||||
case_count INTEGER := 0;
|
||||
BEGIN
|
||||
start_time := clock_timestamp();
|
||||
|
||||
-- First, clear existing automatic matches (preserve manual matches)
|
||||
DELETE FROM asset_symbol_links WHERE match_confidence IN ('exact', 'case_insensitive');
|
||||
|
||||
-- Insert exact symbol matches
|
||||
INSERT INTO asset_symbol_links (chain_asset_id, crypto_coin_id, symbol, match_confidence)
|
||||
SELECT DISTINCT
|
||||
ca.id,
|
||||
cc.id,
|
||||
ca.symbol,
|
||||
'exact'
|
||||
FROM chain_assets ca
|
||||
INNER JOIN crypto_coins cc ON ca.symbol = cc.symbol
|
||||
WHERE ca.symbol IS NOT NULL
|
||||
AND cc.symbol IS NOT NULL
|
||||
AND cc.is_active = TRUE
|
||||
ON CONFLICT (chain_asset_id, crypto_coin_id) DO NOTHING;
|
||||
|
||||
GET DIAGNOSTICS exact_count = ROW_COUNT;
|
||||
|
||||
-- Insert case-insensitive matches (excluding already matched)
|
||||
INSERT INTO asset_symbol_links (chain_asset_id, crypto_coin_id, symbol, match_confidence)
|
||||
SELECT DISTINCT
|
||||
ca.id,
|
||||
cc.id,
|
||||
UPPER(ca.symbol),
|
||||
'case_insensitive'
|
||||
FROM chain_assets ca
|
||||
INNER JOIN crypto_coins cc ON LOWER(ca.symbol) = LOWER(cc.symbol)
|
||||
WHERE ca.symbol IS NOT NULL
|
||||
AND cc.symbol IS NOT NULL
|
||||
AND cc.is_active = TRUE
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM asset_symbol_links asl
|
||||
WHERE asl.chain_asset_id = ca.id
|
||||
AND asl.crypto_coin_id = cc.id
|
||||
)
|
||||
ON CONFLICT (chain_asset_id, crypto_coin_id) DO NOTHING;
|
||||
|
||||
GET DIAGNOSTICS case_count = ROW_COUNT;
|
||||
|
||||
RETURN QUERY SELECT
|
||||
'SUCCESS'::VARCHAR as operation,
|
||||
exact_count as exact_matches,
|
||||
case_count as case_insensitive_matches,
|
||||
exact_count + case_count as total_links_created,
|
||||
clock_timestamp() - start_time as processing_time;
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN QUERY SELECT
|
||||
'ERROR: ' || SQLERRM::VARCHAR as operation,
|
||||
0::INTEGER as exact_matches,
|
||||
0::INTEGER as case_insensitive_matches,
|
||||
0::INTEGER as total_links_created,
|
||||
clock_timestamp() - start_time as processing_time;
|
||||
RAISE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- View to easily query linked assets with their market data
|
||||
CREATE OR REPLACE VIEW v_linked_crypto_assets AS
|
||||
SELECT
|
||||
ca.chain_id,
|
||||
ca.asset_name,
|
||||
ca.symbol AS chain_symbol,
|
||||
ca.description AS chain_description,
|
||||
ca.logo_uri AS chain_logo,
|
||||
cc.id AS crypto_coin_id,
|
||||
cc.name AS crypto_coin_name,
|
||||
cc.symbol AS crypto_symbol,
|
||||
cc.rank AS market_rank,
|
||||
cc.type AS crypto_type,
|
||||
asl.match_confidence,
|
||||
asl.created_at AS link_created_at
|
||||
FROM asset_symbol_links asl
|
||||
INNER JOIN chain_assets ca ON asl.chain_asset_id = ca.id
|
||||
INNER JOIN crypto_coins cc ON asl.crypto_coin_id = cc.id
|
||||
ORDER BY cc.rank;
|
||||
|
||||
-- Function to get market data for a specific chain asset
|
||||
CREATE OR REPLACE FUNCTION get_chain_asset_market_data(p_chain_id VARCHAR, p_symbol VARCHAR)
|
||||
RETURNS TABLE(
|
||||
chain_id VARCHAR,
|
||||
asset_name VARCHAR,
|
||||
symbol VARCHAR,
|
||||
crypto_coin_id VARCHAR,
|
||||
crypto_coin_name VARCHAR,
|
||||
market_rank INTEGER,
|
||||
coin_type VARCHAR,
|
||||
match_confidence VARCHAR
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
ca.chain_id,
|
||||
ca.asset_name,
|
||||
ca.symbol,
|
||||
cc.id AS crypto_coin_id,
|
||||
cc.name AS crypto_coin_name,
|
||||
cc.rank AS market_rank,
|
||||
cc.type AS coin_type,
|
||||
asl.match_confidence
|
||||
FROM chain_assets ca
|
||||
INNER JOIN asset_symbol_links asl ON ca.id = asl.chain_asset_id
|
||||
INNER JOIN crypto_coins cc ON asl.crypto_coin_id = cc.id
|
||||
WHERE ca.chain_id = p_chain_id
|
||||
AND ca.symbol = p_symbol;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Trigger to update the updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_asset_symbol_links_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_update_asset_symbol_links_updated_at
|
||||
BEFORE UPDATE ON asset_symbol_links
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_asset_symbol_links_updated_at();
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TRIGGER IF EXISTS trigger_update_asset_symbol_links_updated_at ON asset_symbol_links;
|
||||
DROP FUNCTION IF EXISTS update_asset_symbol_links_updated_at();
|
||||
DROP FUNCTION IF EXISTS get_chain_asset_market_data(VARCHAR, VARCHAR);
|
||||
DROP VIEW IF EXISTS v_linked_crypto_assets;
|
||||
DROP FUNCTION IF EXISTS populate_asset_symbol_links();
|
||||
DROP INDEX IF EXISTS idx_crypto_coins_symbol_lower;
|
||||
DROP INDEX IF EXISTS idx_chain_assets_symbol_lower;
|
||||
DROP TABLE IF EXISTS asset_symbol_links CASCADE;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,21 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
|
||||
-- Create a common function to update the updated_at column
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
|
||||
-- Drop the function
|
||||
DROP FUNCTION IF EXISTS update_updated_at_column();
|
||||
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,507 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
|
||||
-- Enable TimescaleDB extension
|
||||
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
||||
|
||||
-- Enable pg_cron extension for scheduled jobs
|
||||
CREATE EXTENSION IF NOT EXISTS pg_cron;
|
||||
|
||||
-- Grant usage on pg_cron schema to postgres user
|
||||
GRANT USAGE ON SCHEMA cron TO postgres;
|
||||
|
||||
-- Create crypto_coin_price table for storing historical price data
|
||||
CREATE TABLE IF NOT EXISTS crypto_coin_price (
|
||||
id SERIAL,
|
||||
coin_id VARCHAR(255) NOT NULL REFERENCES crypto_coins(id) ON DELETE CASCADE,
|
||||
time_open TIMESTAMPTZ NOT NULL,
|
||||
time_close TIMESTAMPTZ NOT NULL,
|
||||
open DECIMAL(20, 8),
|
||||
high DECIMAL(20, 8),
|
||||
low DECIMAL(20, 8),
|
||||
close DECIMAL(20, 8),
|
||||
volume DECIMAL(30, 2),
|
||||
market_cap DECIMAL(30, 2),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(coin_id, time_open, time_close)
|
||||
);
|
||||
|
||||
-- Convert to hypertable partitioned by time_close
|
||||
SELECT create_hypertable('crypto_coin_price', 'time_close',
|
||||
chunk_time_interval => INTERVAL '7 days',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
-- Create indexes optimized for TimescaleDB
|
||||
CREATE INDEX idx_crypto_coin_price_time_close_coin_id
|
||||
ON crypto_coin_price (time_close DESC, coin_id)
|
||||
WHERE time_close IS NOT NULL;
|
||||
|
||||
CREATE INDEX idx_crypto_coin_price_coin_id_time_close
|
||||
ON crypto_coin_price (coin_id, time_close DESC)
|
||||
WHERE coin_id IS NOT NULL;
|
||||
|
||||
-- Enable compression on older chunks
|
||||
ALTER TABLE crypto_coin_price SET (
|
||||
timescaledb.compress,
|
||||
timescaledb.compress_segmentby = 'coin_id',
|
||||
timescaledb.compress_orderby = 'time_close DESC'
|
||||
);
|
||||
|
||||
-- Create a policy to automatically compress chunks older than specified interval
|
||||
-- Default is 30 days, but can be configured via app.timescaledb_compress_after_days
|
||||
DO $$
|
||||
DECLARE
|
||||
compress_days INTEGER;
|
||||
compress_interval INTERVAL;
|
||||
BEGIN
|
||||
-- Try to get compression days from config, default to 30 days
|
||||
compress_days := COALESCE(
|
||||
current_setting('app.timescaledb_compress_after_days', true)::INTEGER,
|
||||
30
|
||||
);
|
||||
|
||||
compress_interval := compress_days || ' days';
|
||||
|
||||
PERFORM add_compression_policy('crypto_coin_price', compress_interval);
|
||||
|
||||
RAISE NOTICE 'Compression policy set to compress chunks older than % days', compress_days;
|
||||
END $$;
|
||||
|
||||
-- Add retention policy to drop chunks older than specified interval
|
||||
-- Default is 1 year, but can be configured via app.timescaledb_retention_days
|
||||
DO $$
|
||||
DECLARE
|
||||
retention_days INTEGER;
|
||||
retention_interval INTERVAL;
|
||||
BEGIN
|
||||
-- Try to get retention days from config, default to 365 days (1 year)
|
||||
retention_days := COALESCE(
|
||||
current_setting('app.timescaledb_retention_days', true)::INTEGER,
|
||||
365
|
||||
);
|
||||
|
||||
retention_interval := retention_days || ' days';
|
||||
|
||||
PERFORM add_retention_policy('crypto_coin_price', retention_interval);
|
||||
|
||||
RAISE NOTICE 'Retention policy set to % days', retention_days;
|
||||
END $$;
|
||||
|
||||
-- Create update trigger for updated_at
|
||||
CREATE TRIGGER update_crypto_coin_price_updated_at
|
||||
BEFORE UPDATE ON crypto_coin_price
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Create HTTP function to fetch OHLCV data from CoinPaprika
|
||||
CREATE OR REPLACE FUNCTION http_get_coinpaprika_ohlcv(coin_id TEXT, start_date TEXT DEFAULT NULL, end_date TEXT DEFAULT NULL)
|
||||
RETURNS TABLE (
|
||||
time_open TIMESTAMPTZ,
|
||||
time_close TIMESTAMPTZ,
|
||||
open NUMERIC,
|
||||
high NUMERIC,
|
||||
low NUMERIC,
|
||||
close NUMERIC,
|
||||
volume NUMERIC,
|
||||
market_cap NUMERIC
|
||||
) AS $$
|
||||
DECLARE
|
||||
api_url TEXT;
|
||||
response JSON;
|
||||
api_key TEXT;
|
||||
BEGIN
|
||||
-- Get API key from environment or use default
|
||||
api_key := COALESCE(current_setting('app.coinpaprika_api_key', true), 'dummy-key-for-free-tier');
|
||||
|
||||
-- Build URL based on parameters
|
||||
IF start_date IS NULL AND end_date IS NULL THEN
|
||||
-- Get today's data
|
||||
api_url := 'https://api.coinpaprika.com/v1/coins/' || coin_id || '/ohlcv/today';
|
||||
ELSIF start_date IS NOT NULL AND end_date IS NOT NULL THEN
|
||||
-- Get historical data for date range
|
||||
api_url := 'https://api.coinpaprika.com/v1/coins/' || coin_id || '/ohlcv/historical?start=' || start_date || '&end=' || end_date;
|
||||
ELSE
|
||||
-- Get latest data (last 24 hours)
|
||||
api_url := 'https://api.coinpaprika.com/v1/coins/' || coin_id || '/ohlcv/latest';
|
||||
END IF;
|
||||
|
||||
-- Make HTTP request
|
||||
response := http_get(
|
||||
api_url,
|
||||
JSONB_BUILD_OBJECT(
|
||||
'User-Agent', 'PostgreSQL/HTTP',
|
||||
'Accept', 'application/json'
|
||||
)
|
||||
)::JSON;
|
||||
|
||||
-- Parse and return the response
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
(item->>'time_open')::TIMESTAMPTZ,
|
||||
(item->>'time_close')::TIMESTAMPTZ,
|
||||
(item->>'open')::NUMERIC,
|
||||
(item->>'high')::NUMERIC,
|
||||
(item->>'low')::NUMERIC,
|
||||
(item->>'close')::NUMERIC,
|
||||
(item->>'volume')::NUMERIC,
|
||||
(item->>'market_cap')::NUMERIC
|
||||
FROM json_array_elements(response) AS item;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Function to fetch and store today's price data for a coin
|
||||
CREATE OR REPLACE FUNCTION refresh_coin_price_today(p_coin_id TEXT)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
-- Insert or update today's price data
|
||||
INSERT INTO crypto_coin_price (
|
||||
coin_id, time_open, time_close, open, high, low, close, volume, market_cap
|
||||
)
|
||||
SELECT
|
||||
p_coin_id, time_open, time_close, open, high, low, close, volume, market_cap
|
||||
FROM http_get_coinpaprika_ohlcv(p_coin_id)
|
||||
ON CONFLICT (coin_id, time_open, time_close)
|
||||
DO UPDATE SET
|
||||
open = EXCLUDED.open,
|
||||
high = EXCLUDED.high,
|
||||
low = EXCLUDED.low,
|
||||
close = EXCLUDED.close,
|
||||
volume = EXCLUDED.volume,
|
||||
market_cap = EXCLUDED.market_cap,
|
||||
updated_at = NOW();
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create continuous aggregates for different time intervals
|
||||
-- 1 hour aggregates
|
||||
CREATE MATERIALIZED VIEW crypto_coin_price_1h
|
||||
WITH (timescaledb.continuous) AS
|
||||
SELECT
|
||||
coin_id,
|
||||
time_bucket('1 hour', time_close) AS bucket,
|
||||
FIRST(open, time_open) AS open,
|
||||
MAX(high) AS high,
|
||||
MIN(low) AS low,
|
||||
LAST(close, time_close) AS close,
|
||||
SUM(volume) AS volume,
|
||||
AVG(market_cap) AS market_cap,
|
||||
COUNT(*) AS num_records
|
||||
FROM crypto_coin_price
|
||||
GROUP BY coin_id, bucket
|
||||
WITH NO DATA;
|
||||
|
||||
-- 4 hour aggregates
|
||||
CREATE MATERIALIZED VIEW crypto_coin_price_4h
|
||||
WITH (timescaledb.continuous) AS
|
||||
SELECT
|
||||
coin_id,
|
||||
time_bucket('4 hours', time_close) AS bucket,
|
||||
FIRST(open, time_open) AS open,
|
||||
MAX(high) AS high,
|
||||
MIN(low) AS low,
|
||||
LAST(close, time_close) AS close,
|
||||
SUM(volume) AS volume,
|
||||
AVG(market_cap) AS market_cap,
|
||||
COUNT(*) AS num_records
|
||||
FROM crypto_coin_price
|
||||
GROUP BY coin_id, bucket
|
||||
WITH NO DATA;
|
||||
|
||||
-- 1 day aggregates
|
||||
CREATE MATERIALIZED VIEW crypto_coin_price_1d
|
||||
WITH (timescaledb.continuous) AS
|
||||
SELECT
|
||||
coin_id,
|
||||
time_bucket('1 day', time_close) AS bucket,
|
||||
FIRST(open, time_open) AS open,
|
||||
MAX(high) AS high,
|
||||
MIN(low) AS low,
|
||||
LAST(close, time_close) AS close,
|
||||
SUM(volume) AS volume,
|
||||
AVG(market_cap) AS market_cap,
|
||||
COUNT(*) AS num_records
|
||||
FROM crypto_coin_price
|
||||
GROUP BY coin_id, bucket
|
||||
WITH NO DATA;
|
||||
|
||||
-- 1 week aggregates
|
||||
CREATE MATERIALIZED VIEW crypto_coin_price_1w
|
||||
WITH (timescaledb.continuous) AS
|
||||
SELECT
|
||||
coin_id,
|
||||
time_bucket('1 week', time_close) AS bucket,
|
||||
FIRST(open, time_open) AS open,
|
||||
MAX(high) AS high,
|
||||
MIN(low) AS low,
|
||||
LAST(close, time_close) AS close,
|
||||
SUM(volume) AS volume,
|
||||
AVG(market_cap) AS market_cap,
|
||||
COUNT(*) AS num_records
|
||||
FROM crypto_coin_price
|
||||
GROUP BY coin_id, bucket
|
||||
WITH NO DATA;
|
||||
|
||||
-- Create refresh policies for continuous aggregates
|
||||
-- The refresh window must be larger than the bucket interval
|
||||
-- For 1h buckets: window must be > 1 hour
|
||||
SELECT add_continuous_aggregate_policy('crypto_coin_price_1h',
|
||||
start_offset => INTERVAL '4 hours',
|
||||
end_offset => INTERVAL '1 hour',
|
||||
schedule_interval => INTERVAL '30 minutes');
|
||||
|
||||
-- For 4h buckets: window must be > 4 hours
|
||||
SELECT add_continuous_aggregate_policy('crypto_coin_price_4h',
|
||||
start_offset => INTERVAL '24 hours',
|
||||
end_offset => INTERVAL '4 hours',
|
||||
schedule_interval => INTERVAL '2 hours');
|
||||
|
||||
-- For 1d buckets: window must be > 1 day
|
||||
SELECT add_continuous_aggregate_policy('crypto_coin_price_1d',
|
||||
start_offset => INTERVAL '7 days',
|
||||
end_offset => INTERVAL '1 day',
|
||||
schedule_interval => INTERVAL '6 hours');
|
||||
|
||||
-- For 1w buckets: window must be > 1 week
|
||||
SELECT add_continuous_aggregate_policy('crypto_coin_price_1w',
|
||||
start_offset => INTERVAL '4 weeks',
|
||||
end_offset => INTERVAL '1 week',
|
||||
schedule_interval => INTERVAL '1 day');
|
||||
|
||||
-- Create indexes on continuous aggregates
|
||||
CREATE INDEX idx_crypto_coin_price_1h_coin_bucket ON crypto_coin_price_1h(coin_id, bucket DESC);
|
||||
CREATE INDEX idx_crypto_coin_price_4h_coin_bucket ON crypto_coin_price_4h(coin_id, bucket DESC);
|
||||
CREATE INDEX idx_crypto_coin_price_1d_coin_bucket ON crypto_coin_price_1d(coin_id, bucket DESC);
|
||||
CREATE INDEX idx_crypto_coin_price_1w_coin_bucket ON crypto_coin_price_1w(coin_id, bucket DESC);
|
||||
|
||||
-- Function to get latest price for a coin
|
||||
CREATE OR REPLACE FUNCTION get_latest_coin_price(p_coin_id TEXT)
|
||||
RETURNS TABLE (
|
||||
coin_id VARCHAR(255),
|
||||
price DECIMAL(20, 8),
|
||||
volume DECIMAL(30, 2),
|
||||
market_cap DECIMAL(30, 2),
|
||||
high_24h DECIMAL(20, 8),
|
||||
low_24h DECIMAL(20, 8),
|
||||
change_24h DECIMAL(20, 8),
|
||||
last_updated TIMESTAMPTZ
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
cp.coin_id,
|
||||
cp.close as price,
|
||||
cp.volume,
|
||||
cp.market_cap,
|
||||
cp.high as high_24h,
|
||||
cp.low as low_24h,
|
||||
CASE
|
||||
WHEN cp.open > 0 THEN ((cp.close - cp.open) / cp.open * 100)
|
||||
ELSE 0
|
||||
END as change_24h,
|
||||
cp.time_close as last_updated
|
||||
FROM crypto_coin_price cp
|
||||
WHERE cp.coin_id = p_coin_id
|
||||
ORDER BY cp.time_close DESC
|
||||
LIMIT 1;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Function to update TimescaleDB policies dynamically
|
||||
CREATE OR REPLACE FUNCTION update_timescaledb_policies(
|
||||
p_retention_days INTEGER DEFAULT NULL,
|
||||
p_compress_after_days INTEGER DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE (
|
||||
policy_type TEXT,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
status TEXT
|
||||
) AS $$
|
||||
DECLARE
|
||||
current_retention INTERVAL;
|
||||
current_compression INTERVAL;
|
||||
new_retention INTERVAL;
|
||||
new_compression INTERVAL;
|
||||
BEGIN
|
||||
-- Update retention policy if provided
|
||||
IF p_retention_days IS NOT NULL THEN
|
||||
-- Get current retention policy
|
||||
SELECT schedule_interval INTO current_retention
|
||||
FROM timescaledb_information.policies
|
||||
WHERE hypertable = 'crypto_coin_price'::regclass
|
||||
AND policy_name LIKE '%retention%'
|
||||
LIMIT 1;
|
||||
|
||||
new_retention := p_retention_days || ' days';
|
||||
|
||||
-- Remove old policy and add new one
|
||||
PERFORM remove_retention_policy('crypto_coin_price', if_exists => TRUE);
|
||||
PERFORM add_retention_policy('crypto_coin_price', new_retention);
|
||||
|
||||
RETURN QUERY SELECT
|
||||
'retention'::TEXT,
|
||||
COALESCE(current_retention::TEXT, 'none'),
|
||||
new_retention::TEXT,
|
||||
'updated'::TEXT;
|
||||
END IF;
|
||||
|
||||
-- Update compression policy if provided
|
||||
IF p_compress_after_days IS NOT NULL THEN
|
||||
-- Get current compression policy
|
||||
SELECT schedule_interval INTO current_compression
|
||||
FROM timescaledb_information.policies
|
||||
WHERE hypertable = 'crypto_coin_price'::regclass
|
||||
AND policy_name LIKE '%compress%'
|
||||
LIMIT 1;
|
||||
|
||||
new_compression := p_compress_after_days || ' days';
|
||||
|
||||
-- Remove old policy and add new one
|
||||
PERFORM remove_compression_policy('crypto_coin_price', if_exists => TRUE);
|
||||
PERFORM add_compression_policy('crypto_coin_price', new_compression);
|
||||
|
||||
RETURN QUERY SELECT
|
||||
'compression'::TEXT,
|
||||
COALESCE(current_compression::TEXT, 'none'),
|
||||
new_compression::TEXT,
|
||||
'updated'::TEXT;
|
||||
END IF;
|
||||
|
||||
-- Save configuration to database settings
|
||||
IF p_retention_days IS NOT NULL THEN
|
||||
PERFORM set_config('app.timescaledb_retention_days', p_retention_days::TEXT, FALSE);
|
||||
END IF;
|
||||
|
||||
IF p_compress_after_days IS NOT NULL THEN
|
||||
PERFORM set_config('app.timescaledb_compress_after_days', p_compress_after_days::TEXT, FALSE);
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create function to refresh price data for all active coins
|
||||
CREATE OR REPLACE FUNCTION refresh_all_coin_prices()
|
||||
RETURNS VOID AS $$
|
||||
DECLARE
|
||||
coin RECORD;
|
||||
success_count INT := 0;
|
||||
error_count INT := 0;
|
||||
BEGIN
|
||||
-- Iterate through all coins that have been linked to Cosmos assets
|
||||
FOR coin IN
|
||||
SELECT DISTINCT cc.id as coin_id
|
||||
FROM crypto_coins cc
|
||||
JOIN asset_symbol_links asl ON asl.crypto_coin_id = cc.id
|
||||
WHERE cc.is_active = TRUE
|
||||
LIMIT 100 -- Process in batches to avoid overwhelming the API
|
||||
LOOP
|
||||
BEGIN
|
||||
PERFORM refresh_coin_price_today(coin.coin_id);
|
||||
success_count := success_count + 1;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
error_count := error_count + 1;
|
||||
RAISE NOTICE 'Error refreshing price for coin %: %', coin.coin_id, SQLERRM;
|
||||
END;
|
||||
END LOOP;
|
||||
|
||||
RAISE NOTICE 'Price refresh completed. Success: %, Errors: %', success_count, error_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create pg_cron jobs for automated data fetching
|
||||
-- Refresh prices every 5 minutes during market hours
|
||||
SELECT cron.schedule(
|
||||
'refresh-coin-prices-frequent',
|
||||
'*/5 * * * *', -- Every 5 minutes
|
||||
$$SELECT refresh_all_coin_prices();$$
|
||||
);
|
||||
|
||||
-- Refresh continuous aggregates every hour
|
||||
SELECT cron.schedule(
|
||||
'refresh-continuous-aggregates-1h',
|
||||
'0 * * * *', -- Every hour at minute 0
|
||||
$$CALL refresh_continuous_aggregate('crypto_coin_price_1h', NULL, NULL);$$
|
||||
);
|
||||
|
||||
-- Function to get cosmos asset price through symbol linking
|
||||
CREATE OR REPLACE FUNCTION get_cosmos_asset_price(p_chain_name TEXT, p_base_denom TEXT)
|
||||
RETURNS TABLE (
|
||||
chain_name TEXT,
|
||||
base_denom TEXT,
|
||||
symbol TEXT,
|
||||
coin_id VARCHAR(255),
|
||||
price DECIMAL(20, 8),
|
||||
volume DECIMAL(30, 2),
|
||||
market_cap DECIMAL(30, 2),
|
||||
high_24h DECIMAL(20, 8),
|
||||
low_24h DECIMAL(20, 8),
|
||||
change_24h DECIMAL(20, 8),
|
||||
last_updated TIMESTAMPTZ
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
ca.chain_id as chain_name,
|
||||
ca.base as base_denom,
|
||||
asl.symbol,
|
||||
lcp.coin_id,
|
||||
lcp.price,
|
||||
lcp.volume,
|
||||
lcp.market_cap,
|
||||
lcp.high_24h,
|
||||
lcp.low_24h,
|
||||
lcp.change_24h,
|
||||
lcp.last_updated
|
||||
FROM asset_symbol_links asl
|
||||
JOIN chain_assets ca ON asl.chain_asset_id = ca.id
|
||||
JOIN LATERAL get_latest_coin_price(asl.crypto_coin_id) lcp ON TRUE
|
||||
WHERE ca.chain_id = p_chain_name
|
||||
AND ca.base = p_base_denom;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
|
||||
-- Drop cron jobs
|
||||
SELECT cron.unschedule('refresh-coin-prices-frequent');
|
||||
SELECT cron.unschedule('refresh-continuous-aggregates-1h');
|
||||
|
||||
-- Drop functions
|
||||
DROP FUNCTION IF EXISTS get_cosmos_asset_price(TEXT, TEXT);
|
||||
DROP FUNCTION IF EXISTS get_latest_coin_price(TEXT);
|
||||
DROP FUNCTION IF EXISTS update_timescaledb_policies(INTEGER, INTEGER);
|
||||
DROP FUNCTION IF EXISTS refresh_all_coin_prices();
|
||||
DROP FUNCTION IF EXISTS refresh_coin_price_today(TEXT);
|
||||
DROP FUNCTION IF EXISTS http_get_coinpaprika_ohlcv(TEXT, TEXT, TEXT);
|
||||
|
||||
-- Drop continuous aggregate policies
|
||||
SELECT remove_continuous_aggregate_policy('crypto_coin_price_1h', if_exists => TRUE);
|
||||
SELECT remove_continuous_aggregate_policy('crypto_coin_price_4h', if_exists => TRUE);
|
||||
SELECT remove_continuous_aggregate_policy('crypto_coin_price_1d', if_exists => TRUE);
|
||||
SELECT remove_continuous_aggregate_policy('crypto_coin_price_1w', if_exists => TRUE);
|
||||
|
||||
-- Drop continuous aggregates
|
||||
DROP MATERIALIZED VIEW IF EXISTS crypto_coin_price_1h CASCADE;
|
||||
DROP MATERIALIZED VIEW IF EXISTS crypto_coin_price_4h CASCADE;
|
||||
DROP MATERIALIZED VIEW IF EXISTS crypto_coin_price_1d CASCADE;
|
||||
DROP MATERIALIZED VIEW IF EXISTS crypto_coin_price_1w CASCADE;
|
||||
|
||||
-- Drop TimescaleDB policies
|
||||
SELECT remove_retention_policy('crypto_coin_price', if_exists => TRUE);
|
||||
SELECT remove_compression_policy('crypto_coin_price', if_exists => TRUE);
|
||||
|
||||
-- Drop triggers
|
||||
DROP TRIGGER IF EXISTS update_crypto_coin_price_updated_at ON crypto_coin_price;
|
||||
|
||||
-- Drop hypertable (this will drop all associated objects)
|
||||
DROP TABLE IF EXISTS crypto_coin_price CASCADE;
|
||||
|
||||
-- Drop extensions
|
||||
DROP EXTENSION IF EXISTS pg_cron CASCADE;
|
||||
DROP EXTENSION IF EXISTS timescaledb CASCADE;
|
||||
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,420 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
|
||||
-- Add status and quality fields to chain_assets table
|
||||
ALTER TABLE chain_assets
|
||||
ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT true,
|
||||
ADD COLUMN IF NOT EXISTS is_verified BOOLEAN DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS verification_score INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW();
|
||||
|
||||
-- Create trigger to update updated_at
|
||||
CREATE TRIGGER update_chain_assets_updated_at
|
||||
BEFORE UPDATE ON chain_assets
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Create indexes for efficient filtering
|
||||
CREATE INDEX IF NOT EXISTS idx_chain_assets_active ON chain_assets(is_active) WHERE is_active = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_chain_assets_verified ON chain_assets(is_verified) WHERE is_verified = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_chain_assets_score ON chain_assets(verification_score) WHERE verification_score > 0;
|
||||
|
||||
-- Function to calculate verification score for assets
|
||||
CREATE OR REPLACE FUNCTION calculate_asset_verification_score(p_asset_id INTEGER)
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
v_score INTEGER := 0;
|
||||
v_chain_status VARCHAR(50);
|
||||
v_has_symbol BOOLEAN;
|
||||
v_has_logo BOOLEAN;
|
||||
v_has_description BOOLEAN;
|
||||
v_linked_coin_rank INTEGER;
|
||||
v_linked_coin_active BOOLEAN;
|
||||
v_match_confidence VARCHAR(20);
|
||||
BEGIN
|
||||
-- Get asset details
|
||||
SELECT
|
||||
c.status,
|
||||
ca.symbol IS NOT NULL AND ca.symbol != '',
|
||||
ca.logo_uri IS NOT NULL AND ca.logo_uri != '',
|
||||
ca.description IS NOT NULL AND ca.description != ''
|
||||
INTO v_chain_status, v_has_symbol, v_has_logo, v_has_description
|
||||
FROM chain_assets ca
|
||||
JOIN chains c ON ca.chain_id = c.chain_id
|
||||
WHERE ca.id = p_asset_id;
|
||||
|
||||
-- Base scoring
|
||||
IF v_chain_status = 'live' THEN
|
||||
v_score := v_score + 30;
|
||||
ELSIF v_chain_status = 'upcoming' THEN
|
||||
v_score := v_score + 10;
|
||||
END IF;
|
||||
|
||||
IF v_has_symbol THEN
|
||||
v_score := v_score + 20;
|
||||
END IF;
|
||||
|
||||
IF v_has_logo THEN
|
||||
v_score := v_score + 10;
|
||||
END IF;
|
||||
|
||||
IF v_has_description THEN
|
||||
v_score := v_score + 10;
|
||||
END IF;
|
||||
|
||||
-- Check for linked market data
|
||||
SELECT
|
||||
cc.rank,
|
||||
cc.is_active,
|
||||
asl.match_confidence
|
||||
INTO v_linked_coin_rank, v_linked_coin_active, v_match_confidence
|
||||
FROM asset_symbol_links asl
|
||||
JOIN crypto_coins cc ON asl.crypto_coin_id = cc.id
|
||||
WHERE asl.chain_asset_id = p_asset_id
|
||||
LIMIT 1;
|
||||
|
||||
IF v_linked_coin_rank IS NOT NULL THEN
|
||||
-- Has market data
|
||||
v_score := v_score + 30;
|
||||
|
||||
-- Bonus for high rank coins
|
||||
IF v_linked_coin_rank <= 100 THEN
|
||||
v_score := v_score + 20;
|
||||
ELSIF v_linked_coin_rank <= 500 THEN
|
||||
v_score := v_score + 10;
|
||||
END IF;
|
||||
|
||||
-- Match confidence bonus
|
||||
IF v_match_confidence = 'exact' THEN
|
||||
v_score := v_score + 10;
|
||||
ELSIF v_match_confidence = 'manual' THEN
|
||||
v_score := v_score + 15; -- Manual verification is most trusted
|
||||
END IF;
|
||||
|
||||
IF v_linked_coin_active THEN
|
||||
v_score := v_score + 10;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN v_score;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Function to update asset quality flags
|
||||
CREATE OR REPLACE FUNCTION update_asset_quality_flags()
|
||||
RETURNS TABLE(
|
||||
total_assets INTEGER,
|
||||
deactivated_count INTEGER,
|
||||
verified_count INTEGER,
|
||||
high_quality_count INTEGER
|
||||
) AS $$
|
||||
DECLARE
|
||||
v_total INTEGER;
|
||||
v_deactivated INTEGER := 0;
|
||||
v_verified INTEGER := 0;
|
||||
v_high_quality INTEGER := 0;
|
||||
BEGIN
|
||||
-- Count total assets
|
||||
SELECT COUNT(*) INTO v_total FROM chain_assets;
|
||||
|
||||
-- Update verification scores
|
||||
UPDATE chain_assets
|
||||
SET verification_score = calculate_asset_verification_score(id);
|
||||
|
||||
-- Mark as inactive: assets with very low scores or from non-live chains
|
||||
UPDATE chain_assets ca
|
||||
SET is_active = false
|
||||
WHERE ca.verification_score < 20
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM chains c
|
||||
WHERE c.chain_id = ca.chain_id
|
||||
AND c.status NOT IN ('live', 'upcoming')
|
||||
);
|
||||
|
||||
GET DIAGNOSTICS v_deactivated = ROW_COUNT;
|
||||
|
||||
-- Mark as verified: high quality assets with market data
|
||||
UPDATE chain_assets ca
|
||||
SET is_verified = true
|
||||
WHERE ca.verification_score >= 70
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM asset_symbol_links asl
|
||||
JOIN crypto_coins cc ON asl.crypto_coin_id = cc.id
|
||||
WHERE asl.chain_asset_id = ca.id
|
||||
AND cc.is_active = true
|
||||
AND cc.rank IS NOT NULL
|
||||
);
|
||||
|
||||
GET DIAGNOSTICS v_verified = ROW_COUNT;
|
||||
|
||||
-- Count high quality assets
|
||||
SELECT COUNT(*) INTO v_high_quality
|
||||
FROM chain_assets
|
||||
WHERE verification_score >= 50;
|
||||
|
||||
RETURN QUERY SELECT v_total, v_deactivated, v_verified, v_high_quality;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Update the populate_asset_symbol_links function to respect filters
|
||||
CREATE OR REPLACE FUNCTION populate_asset_symbol_links()
|
||||
RETURNS TABLE(
|
||||
operation VARCHAR,
|
||||
exact_matches INTEGER,
|
||||
case_insensitive_matches INTEGER,
|
||||
total_links_created INTEGER,
|
||||
processing_time INTERVAL
|
||||
) AS $$
|
||||
DECLARE
|
||||
start_time TIMESTAMP;
|
||||
exact_count INTEGER := 0;
|
||||
case_count INTEGER := 0;
|
||||
BEGIN
|
||||
start_time := clock_timestamp();
|
||||
|
||||
-- First, update asset quality flags
|
||||
PERFORM update_asset_quality_flags();
|
||||
|
||||
-- Clear existing automatic matches (preserve manual matches)
|
||||
DELETE FROM asset_symbol_links WHERE match_confidence IN ('exact', 'case_insensitive');
|
||||
|
||||
-- Insert exact symbol matches (only for active assets)
|
||||
INSERT INTO asset_symbol_links (chain_asset_id, crypto_coin_id, symbol, match_confidence)
|
||||
SELECT DISTINCT
|
||||
ca.id,
|
||||
cc.id,
|
||||
ca.symbol,
|
||||
'exact'
|
||||
FROM chain_assets ca
|
||||
INNER JOIN crypto_coins cc ON ca.symbol = cc.symbol
|
||||
INNER JOIN chains c ON ca.chain_id = c.chain_id
|
||||
WHERE ca.symbol IS NOT NULL
|
||||
AND cc.symbol IS NOT NULL
|
||||
AND cc.is_active = TRUE
|
||||
AND ca.is_active = TRUE
|
||||
AND c.status IN ('live', 'upcoming')
|
||||
AND ca.verification_score >= 30
|
||||
ON CONFLICT (chain_asset_id, crypto_coin_id) DO NOTHING;
|
||||
|
||||
GET DIAGNOSTICS exact_count = ROW_COUNT;
|
||||
|
||||
-- Insert case-insensitive matches (excluding already matched, only for quality assets)
|
||||
INSERT INTO asset_symbol_links (chain_asset_id, crypto_coin_id, symbol, match_confidence)
|
||||
SELECT DISTINCT
|
||||
ca.id,
|
||||
cc.id,
|
||||
UPPER(ca.symbol),
|
||||
'case_insensitive'
|
||||
FROM chain_assets ca
|
||||
INNER JOIN crypto_coins cc ON LOWER(ca.symbol) = LOWER(cc.symbol)
|
||||
INNER JOIN chains c ON ca.chain_id = c.chain_id
|
||||
WHERE ca.symbol IS NOT NULL
|
||||
AND cc.symbol IS NOT NULL
|
||||
AND cc.is_active = TRUE
|
||||
AND ca.is_active = TRUE
|
||||
AND c.status IN ('live', 'upcoming')
|
||||
AND ca.verification_score >= 30
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM asset_symbol_links asl
|
||||
WHERE asl.chain_asset_id = ca.id
|
||||
AND asl.crypto_coin_id = cc.id
|
||||
)
|
||||
ON CONFLICT (chain_asset_id, crypto_coin_id) DO NOTHING;
|
||||
|
||||
GET DIAGNOSTICS case_count = ROW_COUNT;
|
||||
|
||||
RETURN QUERY SELECT
|
||||
'SUCCESS'::VARCHAR as operation,
|
||||
exact_count as exact_matches,
|
||||
case_count as case_insensitive_matches,
|
||||
exact_count + case_count as total_links_created,
|
||||
clock_timestamp() - start_time as processing_time;
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN QUERY SELECT
|
||||
'ERROR: ' || SQLERRM::VARCHAR as operation,
|
||||
0::INTEGER as exact_matches,
|
||||
0::INTEGER as case_insensitive_matches,
|
||||
0::INTEGER as total_links_created,
|
||||
clock_timestamp() - start_time as processing_time;
|
||||
RAISE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create view for quality assets only
|
||||
CREATE OR REPLACE VIEW v_quality_linked_assets AS
|
||||
SELECT
|
||||
ca.chain_id,
|
||||
ca.asset_name,
|
||||
ca.symbol,
|
||||
ca.description,
|
||||
ca.logo_uri,
|
||||
ca.is_verified,
|
||||
ca.verification_score,
|
||||
cc.id as crypto_coin_id,
|
||||
cc.name as crypto_coin_name,
|
||||
cc.rank as market_rank,
|
||||
cc.type as coin_type,
|
||||
asl.match_confidence,
|
||||
cp.close as current_price,
|
||||
cp.volume as volume_24h,
|
||||
cp.market_cap,
|
||||
CASE
|
||||
WHEN cp.open > 0 THEN ((cp.close - cp.open) / cp.open * 100)
|
||||
ELSE 0
|
||||
END as change_24h
|
||||
FROM chain_assets ca
|
||||
INNER JOIN asset_symbol_links asl ON ca.id = asl.chain_asset_id
|
||||
INNER JOIN crypto_coins cc ON asl.crypto_coin_id = cc.id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT * FROM crypto_coin_price
|
||||
WHERE coin_id = cc.id
|
||||
ORDER BY time_close DESC
|
||||
LIMIT 1
|
||||
) cp ON true
|
||||
WHERE ca.is_active = true
|
||||
AND ca.verification_score >= 50
|
||||
AND cc.is_active = true
|
||||
ORDER BY cc.rank NULLS LAST;
|
||||
|
||||
-- Function to manually mark an asset as junk/inactive
|
||||
CREATE OR REPLACE FUNCTION mark_asset_as_junk(p_chain_id TEXT, p_base_denom TEXT)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
UPDATE chain_assets
|
||||
SET
|
||||
is_active = false,
|
||||
is_verified = false,
|
||||
verification_score = 0,
|
||||
updated_at = NOW()
|
||||
WHERE chain_id = p_chain_id AND base = p_base_denom;
|
||||
|
||||
-- Remove any symbol links for this asset
|
||||
DELETE FROM asset_symbol_links
|
||||
WHERE chain_asset_id IN (
|
||||
SELECT id FROM chain_assets
|
||||
WHERE chain_id = p_chain_id AND base = p_base_denom
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Function to manually verify an asset
|
||||
CREATE OR REPLACE FUNCTION verify_asset(p_chain_id TEXT, p_base_denom TEXT)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
UPDATE chain_assets
|
||||
SET
|
||||
is_active = true,
|
||||
is_verified = true,
|
||||
verification_score = GREATEST(verification_score, 80),
|
||||
updated_at = NOW()
|
||||
WHERE chain_id = p_chain_id AND base = p_base_denom;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
|
||||
-- Drop views
|
||||
DROP VIEW IF EXISTS v_quality_linked_assets;
|
||||
|
||||
-- Drop functions
|
||||
DROP FUNCTION IF EXISTS verify_asset(TEXT, TEXT);
|
||||
DROP FUNCTION IF EXISTS mark_asset_as_junk(TEXT, TEXT);
|
||||
DROP FUNCTION IF EXISTS update_asset_quality_flags();
|
||||
DROP FUNCTION IF EXISTS calculate_asset_verification_score(INTEGER);
|
||||
|
||||
-- Restore original populate_asset_symbol_links function
|
||||
CREATE OR REPLACE FUNCTION populate_asset_symbol_links()
|
||||
RETURNS TABLE(
|
||||
operation VARCHAR,
|
||||
exact_matches INTEGER,
|
||||
case_insensitive_matches INTEGER,
|
||||
total_links_created INTEGER,
|
||||
processing_time INTERVAL
|
||||
) AS $$
|
||||
DECLARE
|
||||
start_time TIMESTAMP;
|
||||
exact_count INTEGER := 0;
|
||||
case_count INTEGER := 0;
|
||||
BEGIN
|
||||
start_time := clock_timestamp();
|
||||
|
||||
-- First, clear existing automatic matches (preserve manual matches)
|
||||
DELETE FROM asset_symbol_links WHERE match_confidence IN ('exact', 'case_insensitive');
|
||||
|
||||
-- Insert exact symbol matches
|
||||
INSERT INTO asset_symbol_links (chain_asset_id, crypto_coin_id, symbol, match_confidence)
|
||||
SELECT DISTINCT
|
||||
ca.id,
|
||||
cc.id,
|
||||
ca.symbol,
|
||||
'exact'
|
||||
FROM chain_assets ca
|
||||
INNER JOIN crypto_coins cc ON ca.symbol = cc.symbol
|
||||
WHERE ca.symbol IS NOT NULL
|
||||
AND cc.symbol IS NOT NULL
|
||||
AND cc.is_active = TRUE
|
||||
ON CONFLICT (chain_asset_id, crypto_coin_id) DO NOTHING;
|
||||
|
||||
GET DIAGNOSTICS exact_count = ROW_COUNT;
|
||||
|
||||
-- Insert case-insensitive matches (excluding already matched)
|
||||
INSERT INTO asset_symbol_links (chain_asset_id, crypto_coin_id, symbol, match_confidence)
|
||||
SELECT DISTINCT
|
||||
ca.id,
|
||||
cc.id,
|
||||
UPPER(ca.symbol),
|
||||
'case_insensitive'
|
||||
FROM chain_assets ca
|
||||
INNER JOIN crypto_coins cc ON LOWER(ca.symbol) = LOWER(cc.symbol)
|
||||
WHERE ca.symbol IS NOT NULL
|
||||
AND cc.symbol IS NOT NULL
|
||||
AND cc.is_active = TRUE
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM asset_symbol_links asl
|
||||
WHERE asl.chain_asset_id = ca.id
|
||||
AND asl.crypto_coin_id = cc.id
|
||||
)
|
||||
ON CONFLICT (chain_asset_id, crypto_coin_id) DO NOTHING;
|
||||
|
||||
GET DIAGNOSTICS case_count = ROW_COUNT;
|
||||
|
||||
RETURN QUERY SELECT
|
||||
'SUCCESS'::VARCHAR as operation,
|
||||
exact_count as exact_matches,
|
||||
case_count as case_insensitive_matches,
|
||||
exact_count + case_count as total_links_created,
|
||||
clock_timestamp() - start_time as processing_time;
|
||||
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RETURN QUERY SELECT
|
||||
'ERROR: ' || SQLERRM::VARCHAR as operation,
|
||||
0::INTEGER as exact_matches,
|
||||
0::INTEGER as case_insensitive_matches,
|
||||
0::INTEGER as total_links_created,
|
||||
clock_timestamp() - start_time as processing_time;
|
||||
RAISE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Drop triggers
|
||||
DROP TRIGGER IF EXISTS update_chain_assets_updated_at ON chain_assets;
|
||||
|
||||
-- Drop indexes
|
||||
DROP INDEX IF EXISTS idx_chain_assets_score;
|
||||
DROP INDEX IF EXISTS idx_chain_assets_verified;
|
||||
DROP INDEX IF EXISTS idx_chain_assets_active;
|
||||
|
||||
-- Drop columns
|
||||
ALTER TABLE chain_assets
|
||||
DROP COLUMN IF EXISTS updated_at,
|
||||
DROP COLUMN IF EXISTS verification_score,
|
||||
DROP COLUMN IF EXISTS is_verified,
|
||||
DROP COLUMN IF EXISTS is_active;
|
||||
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,26 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
account_id TEXT NOT NULL,
|
||||
user_agent TEXT,
|
||||
ip_address TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
session_data JSONB,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX idx_sessions_account_id ON sessions(account_id);
|
||||
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
|
||||
CREATE INDEX idx_sessions_is_active ON sessions(is_active) WHERE is_active = TRUE;
|
||||
CREATE INDEX idx_sessions_created_at ON sessions(created_at);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE sessions;
|
||||
-- +goose StatementEnd
|
||||
@@ -1,223 +0,0 @@
|
||||
// Package prefixstore provides a store that prefixes all keys with a given
|
||||
// prefix. It is used to isolate storage reads and writes for an account.
|
||||
// Implementation taken from cosmossdk.io/store/prefix, and adapted to
|
||||
// the cosmossdk.io/core/store.KVStore interface.
|
||||
package prefixstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
|
||||
"cosmossdk.io/core/store"
|
||||
)
|
||||
|
||||
// New creates a new prefix store using the provided bytes prefix.
|
||||
func New(store store.KVStore, prefix []byte) store.KVStore {
|
||||
return Store{
|
||||
parent: store,
|
||||
prefix: prefix,
|
||||
}
|
||||
}
|
||||
|
||||
var _ store.KVStore = Store{}
|
||||
|
||||
// Store is similar with cometbft/cometbft-db/blob/v1.0.1/prefixdb.go
|
||||
// both gives access only to the limited subset of the store
|
||||
// for convenience or safety
|
||||
type Store struct {
|
||||
parent store.KVStore
|
||||
prefix []byte
|
||||
}
|
||||
|
||||
func cloneAppend(bz, tail []byte) (res []byte) {
|
||||
res = make([]byte, len(bz)+len(tail))
|
||||
copy(res, bz)
|
||||
copy(res[len(bz):], tail)
|
||||
return
|
||||
}
|
||||
|
||||
func (s Store) key(key []byte) (res []byte) {
|
||||
if key == nil {
|
||||
panic("nil key on Store")
|
||||
}
|
||||
res = cloneAppend(s.prefix, key)
|
||||
return
|
||||
}
|
||||
|
||||
// Implements KVStore
|
||||
func (s Store) Get(key []byte) ([]byte, error) {
|
||||
return s.parent.Get(s.key(key))
|
||||
}
|
||||
|
||||
// Implements KVStore
|
||||
func (s Store) Has(key []byte) (bool, error) {
|
||||
return s.parent.Has(s.key(key))
|
||||
}
|
||||
|
||||
// Implements KVStore
|
||||
func (s Store) Set(key, value []byte) error {
|
||||
return s.parent.Set(s.key(key), value)
|
||||
}
|
||||
|
||||
// Implements KVStore
|
||||
func (s Store) Delete(key []byte) error { return s.parent.Delete(s.key(key)) }
|
||||
|
||||
// Implements KVStore
|
||||
// Check https://github.com/cometbft/cometbft-db/blob/v1.0.1/prefixdb.go#L109
|
||||
func (s Store) Iterator(start, end []byte) (store.Iterator, error) {
|
||||
newstart := cloneAppend(s.prefix, start)
|
||||
|
||||
var newend []byte
|
||||
if end == nil {
|
||||
newend = cpIncr(s.prefix)
|
||||
} else {
|
||||
newend = cloneAppend(s.prefix, end)
|
||||
}
|
||||
|
||||
iter, err := s.parent.Iterator(newstart, newend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newPrefixIterator(s.prefix, start, end, iter), nil
|
||||
}
|
||||
|
||||
// ReverseIterator implements KVStore
|
||||
// Check https://github.com/cometbft/cometbft-db/blob/v1.0.1/prefixdb.go#L132
|
||||
func (s Store) ReverseIterator(start, end []byte) (store.Iterator, error) {
|
||||
newstart := cloneAppend(s.prefix, start)
|
||||
|
||||
var newend []byte
|
||||
if end == nil {
|
||||
newend = cpIncr(s.prefix)
|
||||
} else {
|
||||
newend = cloneAppend(s.prefix, end)
|
||||
}
|
||||
|
||||
iter, err := s.parent.ReverseIterator(newstart, newend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newPrefixIterator(s.prefix, start, end, iter), nil
|
||||
}
|
||||
|
||||
var _ store.Iterator = (*prefixIterator)(nil)
|
||||
|
||||
type prefixIterator struct {
|
||||
prefix []byte
|
||||
start []byte
|
||||
end []byte
|
||||
iter store.Iterator
|
||||
valid bool
|
||||
}
|
||||
|
||||
func newPrefixIterator(prefix, start, end []byte, parent store.Iterator) *prefixIterator {
|
||||
return &prefixIterator{
|
||||
prefix: prefix,
|
||||
start: start,
|
||||
end: end,
|
||||
iter: parent,
|
||||
valid: parent.Valid() && bytes.HasPrefix(parent.Key(), prefix),
|
||||
}
|
||||
}
|
||||
|
||||
// Implements Iterator
|
||||
func (pi *prefixIterator) Domain() ([]byte, []byte) {
|
||||
return pi.start, pi.end
|
||||
}
|
||||
|
||||
// Implements Iterator
|
||||
func (pi *prefixIterator) Valid() bool {
|
||||
return pi.valid && pi.iter.Valid()
|
||||
}
|
||||
|
||||
// Implements Iterator
|
||||
func (pi *prefixIterator) Next() {
|
||||
if !pi.valid {
|
||||
panic("prefixIterator invalid, cannot call Next()")
|
||||
}
|
||||
|
||||
if pi.iter.Next(); !pi.iter.Valid() || !bytes.HasPrefix(pi.iter.Key(), pi.prefix) {
|
||||
// TODO: shouldn't pi be set to nil instead?
|
||||
pi.valid = false
|
||||
}
|
||||
}
|
||||
|
||||
// Implements Iterator
|
||||
func (pi *prefixIterator) Key() (key []byte) {
|
||||
if !pi.valid {
|
||||
panic("prefixIterator invalid, cannot call Key()")
|
||||
}
|
||||
|
||||
key = pi.iter.Key()
|
||||
key = stripPrefix(key, pi.prefix)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Implements Iterator
|
||||
func (pi *prefixIterator) Value() []byte {
|
||||
if !pi.valid {
|
||||
panic("prefixIterator invalid, cannot call Value()")
|
||||
}
|
||||
|
||||
return pi.iter.Value()
|
||||
}
|
||||
|
||||
// Implements Iterator
|
||||
func (pi *prefixIterator) Close() error {
|
||||
return pi.iter.Close()
|
||||
}
|
||||
|
||||
// Error returns an error if the prefixIterator is invalid defined by the Valid
|
||||
// method.
|
||||
func (pi *prefixIterator) Error() error {
|
||||
if !pi.Valid() {
|
||||
return errors.New("invalid prefixIterator")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// copied from github.com/cometbft/cometbft-db/blob/v1.0.1/prefixdb.go
|
||||
func stripPrefix(key, prefix []byte) []byte {
|
||||
if len(key) < len(prefix) || !bytes.Equal(key[:len(prefix)], prefix) {
|
||||
panic("should not happen")
|
||||
}
|
||||
|
||||
return key[len(prefix):]
|
||||
}
|
||||
|
||||
// wrapping types.PrefixEndBytes
|
||||
func cpIncr(bz []byte) []byte {
|
||||
return prefixEndBytes(bz)
|
||||
}
|
||||
|
||||
// prefixEndBytes returns the []byte that would end a
|
||||
// range query for all []byte with a certain prefix
|
||||
// Deals with last byte of prefix being FF without overflowing
|
||||
func prefixEndBytes(prefix []byte) []byte {
|
||||
if len(prefix) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
end := make([]byte, len(prefix))
|
||||
copy(end, prefix)
|
||||
|
||||
for {
|
||||
if end[len(end)-1] != byte(255) {
|
||||
end[len(end)-1]++
|
||||
break
|
||||
}
|
||||
|
||||
end = end[:len(end)-1]
|
||||
|
||||
if len(end) == 0 {
|
||||
end = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return end
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sonr-io/snrd/internal/transaction"
|
||||
)
|
||||
|
||||
// Service is the interface that wraps the basic methods for a router.
|
||||
// A router can be a query router or a message router.
|
||||
type Service interface {
|
||||
// CanInvoke returns an error if the given request cannot be invoked.
|
||||
CanInvoke(ctx context.Context, typeURL string) error
|
||||
// Invoke execute a message or query. The response should be type casted by the caller to the expected response.
|
||||
Invoke(ctx context.Context, req transaction.Msg) (res transaction.Msg, err error)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package transaction
|
||||
|
||||
import "context"
|
||||
|
||||
// ExecMode defines the execution mode
|
||||
type ExecMode uint8
|
||||
|
||||
// All possible execution modes.
|
||||
// For backwards compatibility and easier casting, the exec mode values must be the same as in cosmos/cosmos-sdk/types package.
|
||||
const (
|
||||
ExecModeCheck ExecMode = iota
|
||||
ExecModeReCheck
|
||||
ExecModeSimulate
|
||||
_
|
||||
_
|
||||
_
|
||||
_
|
||||
ExecModeFinalize
|
||||
)
|
||||
|
||||
// Service creates a transaction service.
|
||||
type Service interface {
|
||||
// ExecMode returns the current execution mode.
|
||||
ExecMode(ctx context.Context) ExecMode
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package transaction
|
||||
|
||||
type (
|
||||
// Msg uses structural types to define the interface for a message.
|
||||
Msg = interface {
|
||||
Reset()
|
||||
String() string
|
||||
ProtoMessage()
|
||||
}
|
||||
Identity = []byte
|
||||
)
|
||||
|
||||
// GenericMsg defines a generic version of a Msg.
|
||||
// The GenericMsg refers to the non pointer version of Msg,
|
||||
// and is required to allow its instantiations in generic contexts.
|
||||
type GenericMsg[T any] interface {
|
||||
*T
|
||||
Msg
|
||||
}
|
||||
|
||||
// Codec defines the TX codec, which converts a TX from bytes to its concrete representation.
|
||||
type Codec[T Tx] interface {
|
||||
// Decode decodes the tx bytes into a DecodedTx, containing
|
||||
// both concrete and bytes representation of the tx.
|
||||
Decode([]byte) (T, error)
|
||||
// DecodeJSON decodes the tx JSON bytes into a DecodedTx
|
||||
DecodeJSON([]byte) (T, error)
|
||||
}
|
||||
|
||||
// Tx defines the interface for a transaction.
|
||||
// All custom transactions must implement this interface.
|
||||
type Tx interface {
|
||||
// Hash returns the unique identifier for the Tx.
|
||||
Hash() [32]byte
|
||||
// GetMessages returns the list of state transitions of the Tx.
|
||||
GetMessages() ([]Msg, error)
|
||||
// GetSenders returns the tx state transition sender.
|
||||
GetSenders() ([]Identity, error) // TODO reduce this to a single identity if accepted
|
||||
// GetGasLimit returns the gas limit of the tx. Must return math.MaxUint64 for infinite gas
|
||||
// txs.
|
||||
GetGasLimit() (uint64, error)
|
||||
// Bytes returns the encoded version of this tx. Note: this is ideally cached
|
||||
// from the first instance of the decoding of the tx.
|
||||
Bytes() []byte
|
||||
}
|
||||
Reference in New Issue
Block a user