* clear

* feat: Add everything

* fix: Commenht
This commit is contained in:
Prad Nukala
2025-10-03 14:45:52 -04:00
committed by GitHub
parent 43b4a11c06
commit 13e6c3e84d
1935 changed files with 655061 additions and 40058 deletions
-112
View File
@@ -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)
}
-124
View File
@@ -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 }
-66
View File
@@ -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)
}
-157
View File
@@ -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
}
-17
View File
@@ -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)
}
-78
View File
@@ -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))
},
}
}
-66
View File
@@ -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
}
-18
View File
@@ -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)
}
-30
View File
@@ -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
}
-34
View File
@@ -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)
}
-231
View File
@@ -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
}
-140
View File
@@ -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)
}
-41
View File
@@ -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
View File
@@ -1 +0,0 @@
package ipfsnode
-1
View File
@@ -1 +0,0 @@
package ipfsnode
-29
View File
@@ -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
+33
View File
@@ -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
-223
View File
@@ -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
}
-16
View File
@@ -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)
}
-25
View File
@@ -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
}
-45
View File
@@ -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
}