feat: implement runes data reader funcs

This commit is contained in:
Gaze
2024-04-10 15:48:20 +07:00
parent 4d580c09ed
commit 535e5b7223
9 changed files with 368 additions and 103 deletions

View File

@@ -27,15 +27,17 @@ CREATE TABLE IF NOT EXISTS "runes_entries" (
"mints" DECIMAL NOT NULL,
"premine" DECIMAL NOT NULL,
"symbol" INT NOT NULL,
"term_amount" DECIMAL,
"term_cap" DECIMAL,
"term_height_start" DECIMAL, -- using DECIMAL because it is uint64 but postgres only supports up to int64
"term_height_end" DECIMAL,
"term_offset_start" DECIMAL,
"term_offset_end" DECIMAL,
"divisibility" SMALLINT NOT NULL,
"terms" BOOLEAN NOT NULL, -- if true, then minting term exists for this entry
"terms_amount" DECIMAL,
"terms_cap" DECIMAL,
"terms_height_start" DECIMAL, -- using DECIMAL because it is uint64 but postgres only supports up to int64
"terms_height_end" DECIMAL,
"terms_offset_start" DECIMAL,
"terms_offset_end" DECIMAL,
"completion_time" TIMESTAMP NOT NULL
);
CREATE INDEX IF NOT EXISTS runes_entries_rune_idx ON "runes_entries" USING HASH ("rune");
CREATE UNIQUE INDEX IF NOT EXISTS runes_entries_rune_idx ON "runes_entries" USING BTREE ("rune");
CREATE TABLE IF NOT EXISTS "runes_outpoint_balances" (
"rune_id" TEXT NOT NULL,

View File

@@ -5,10 +5,10 @@ SELECT DISTINCT ON (rune_id) * FROM runes_balances WHERE pkscript = $1 AND block
SELECT DISTINCT ON (pkscript) * FROM runes_balances WHERE rune_id = $1 AND block_height <= $2 ORDER BY pkscript, block_height DESC;
-- name: GetOutPointBalances :many
SELECT * FROM runes_outpoint_balances WHERE rune_id = $1 AND tx_hash = $2 AND tx_idx = $3;
SELECT * FROM runes_outpoint_balances WHERE tx_hash = $1 AND tx_idx = $2;
-- name: GetRuneEntriesByRuneIds :many
SELECT * FROM runes_entries WHERE rune_id = ANY(@rune_ids::text[]);
-- name: GetRuneEntryByRuneId :one
SELECT * FROM runes_entries WHERE rune_id = $1;
-- name: GetRuneEntriesByRunes :many
SELECT * FROM runes_entries WHERE rune = ANY(@rune_ids::text[]);
-- name: GetRuneEntryByRune :one
SELECT * FROM runes_entries WHERE rune = $1;

View File

@@ -78,17 +78,16 @@ func (q *Queries) GetBalancesByRuneId(ctx context.Context, arg GetBalancesByRune
}
const getOutPointBalances = `-- name: GetOutPointBalances :many
SELECT rune_id, tx_hash, tx_idx, value FROM runes_outpoint_balances WHERE rune_id = $1 AND tx_hash = $2 AND tx_idx = $3
SELECT rune_id, tx_hash, tx_idx, value FROM runes_outpoint_balances WHERE tx_hash = $1 AND tx_idx = $2
`
type GetOutPointBalancesParams struct {
RuneID string
TxHash string
TxIdx int32
}
func (q *Queries) GetOutPointBalances(ctx context.Context, arg GetOutPointBalancesParams) ([]RunesOutpointBalance, error) {
rows, err := q.db.Query(ctx, getOutPointBalances, arg.RuneID, arg.TxHash, arg.TxIdx)
rows, err := q.db.Query(ctx, getOutPointBalances, arg.TxHash, arg.TxIdx)
if err != nil {
return nil, err
}
@@ -112,80 +111,58 @@ func (q *Queries) GetOutPointBalances(ctx context.Context, arg GetOutPointBalanc
return items, nil
}
const getRuneEntriesByRuneIds = `-- name: GetRuneEntriesByRuneIds :many
SELECT rune_id, rune, spacers, burned_amount, mints, premine, symbol, term_amount, term_cap, term_height_start, term_height_end, term_offset_start, term_offset_end, completion_time FROM runes_entries WHERE rune_id = ANY($1::text[])
const getRuneEntryByRune = `-- name: GetRuneEntryByRune :one
SELECT rune_id, rune, spacers, burned_amount, mints, premine, symbol, divisibility, terms, terms_amount, terms_cap, terms_height_start, terms_height_end, terms_offset_start, terms_offset_end, completion_time FROM runes_entries WHERE rune = $1
`
func (q *Queries) GetRuneEntriesByRuneIds(ctx context.Context, runeIds []string) ([]RunesEntry, error) {
rows, err := q.db.Query(ctx, getRuneEntriesByRuneIds, runeIds)
if err != nil {
return nil, err
}
defer rows.Close()
var items []RunesEntry
for rows.Next() {
var i RunesEntry
if err := rows.Scan(
&i.RuneID,
&i.Rune,
&i.Spacers,
&i.BurnedAmount,
&i.Mints,
&i.Premine,
&i.Symbol,
&i.TermAmount,
&i.TermCap,
&i.TermHeightStart,
&i.TermHeightEnd,
&i.TermOffsetStart,
&i.TermOffsetEnd,
&i.CompletionTime,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
func (q *Queries) GetRuneEntryByRune(ctx context.Context, rune string) (RunesEntry, error) {
row := q.db.QueryRow(ctx, getRuneEntryByRune, rune)
var i RunesEntry
err := row.Scan(
&i.RuneID,
&i.Rune,
&i.Spacers,
&i.BurnedAmount,
&i.Mints,
&i.Premine,
&i.Symbol,
&i.Divisibility,
&i.Terms,
&i.TermsAmount,
&i.TermsCap,
&i.TermsHeightStart,
&i.TermsHeightEnd,
&i.TermsOffsetStart,
&i.TermsOffsetEnd,
&i.CompletionTime,
)
return i, err
}
const getRuneEntriesByRunes = `-- name: GetRuneEntriesByRunes :many
SELECT rune_id, rune, spacers, burned_amount, mints, premine, symbol, term_amount, term_cap, term_height_start, term_height_end, term_offset_start, term_offset_end, completion_time FROM runes_entries WHERE rune = ANY($1::text[])
const getRuneEntryByRuneId = `-- name: GetRuneEntryByRuneId :one
SELECT rune_id, rune, spacers, burned_amount, mints, premine, symbol, divisibility, terms, terms_amount, terms_cap, terms_height_start, terms_height_end, terms_offset_start, terms_offset_end, completion_time FROM runes_entries WHERE rune_id = $1
`
func (q *Queries) GetRuneEntriesByRunes(ctx context.Context, runeIds []string) ([]RunesEntry, error) {
rows, err := q.db.Query(ctx, getRuneEntriesByRunes, runeIds)
if err != nil {
return nil, err
}
defer rows.Close()
var items []RunesEntry
for rows.Next() {
var i RunesEntry
if err := rows.Scan(
&i.RuneID,
&i.Rune,
&i.Spacers,
&i.BurnedAmount,
&i.Mints,
&i.Premine,
&i.Symbol,
&i.TermAmount,
&i.TermCap,
&i.TermHeightStart,
&i.TermHeightEnd,
&i.TermOffsetStart,
&i.TermOffsetEnd,
&i.CompletionTime,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
func (q *Queries) GetRuneEntryByRuneId(ctx context.Context, runeID string) (RunesEntry, error) {
row := q.db.QueryRow(ctx, getRuneEntryByRuneId, runeID)
var i RunesEntry
err := row.Scan(
&i.RuneID,
&i.Rune,
&i.Spacers,
&i.BurnedAmount,
&i.Mints,
&i.Premine,
&i.Symbol,
&i.Divisibility,
&i.Terms,
&i.TermsAmount,
&i.TermsCap,
&i.TermsHeightStart,
&i.TermsHeightEnd,
&i.TermsOffsetStart,
&i.TermsOffsetEnd,
&i.CompletionTime,
)
return i, err
}

View File

@@ -16,20 +16,22 @@ type RunesBalance struct {
}
type RunesEntry struct {
RuneID string
Rune string
Spacers int32
BurnedAmount pgtype.Numeric
Mints pgtype.Numeric
Premine pgtype.Numeric
Symbol int32
TermAmount pgtype.Numeric
TermCap pgtype.Numeric
TermHeightStart pgtype.Numeric
TermHeightEnd pgtype.Numeric
TermOffsetStart pgtype.Numeric
TermOffsetEnd pgtype.Numeric
CompletionTime pgtype.Timestamp
RuneID string
Rune string
Spacers int32
BurnedAmount pgtype.Numeric
Mints pgtype.Numeric
Premine pgtype.Numeric
Symbol int32
Divisibility int16
Terms bool
TermsAmount pgtype.Numeric
TermsCap pgtype.Numeric
TermsHeightStart pgtype.Numeric
TermsHeightEnd pgtype.Numeric
TermsOffsetStart pgtype.Numeric
TermsOffsetEnd pgtype.Numeric
CompletionTime pgtype.Timestamp
}
type RunesIndexerDbVersion struct {
@@ -39,10 +41,11 @@ type RunesIndexerDbVersion struct {
}
type RunesIndexerStat struct {
Id int64
ClientVersion string
Network string
CreatedAt pgtype.Timestamptz
Id int64
ClientVersion string
Network string
LatestBlockHeight int32
CreatedAt pgtype.Timestamptz
}
type RunesOutpointBalance struct {

View File

@@ -0,0 +1,108 @@
package postgres
import (
"time"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/modules/runes/internal/repository/postgres/gen"
"github.com/gaze-network/indexer-network/modules/runes/internal/runes"
"github.com/gaze-network/uint128"
"github.com/jackc/pgx/v5/pgtype"
"github.com/samber/lo"
)
func uint128FromNumeric(src pgtype.Numeric) (uint128.Uint128, error) {
bytes, err := src.MarshalJSON()
if err != nil {
return uint128.Uint128{}, errors.WithStack(err)
}
result, err := uint128.FromString(string(bytes))
if err != nil {
return uint128.Uint128{}, errors.WithStack(err)
}
return result, nil
}
func mapRuneEntryModelToType(src gen.RunesEntry) (*runes.RuneEntry, error) {
runeId, err := runes.NewRuneIdFromString(src.RuneID)
if err != nil {
return nil, errors.Wrap(err, "failed to parse rune id")
}
burnedAmount, err := uint128FromNumeric(src.BurnedAmount)
if err != nil {
return nil, errors.Wrap(err, "failed to parse burned amount")
}
rune, err := runes.NewRuneFromString(src.Rune)
if err != nil {
return nil, errors.Wrap(err, "failed to parse rune")
}
mints, err := uint128FromNumeric(src.Mints)
if err != nil {
return nil, errors.Wrap(err, "failed to parse mints")
}
premine, err := uint128FromNumeric(src.Premine)
if err != nil {
return nil, errors.Wrap(err, "failed to parse premine")
}
var completionTime time.Time
if src.CompletionTime.Valid {
completionTime = src.CompletionTime.Time
}
var terms *runes.Terms
if src.Terms {
terms = &runes.Terms{}
if src.TermsAmount.Valid {
amount, err := uint128FromNumeric(src.TermsAmount)
if err != nil {
return nil, errors.Wrap(err, "failed to parse terms amount")
}
terms.Amount = &amount
}
if src.TermsCap.Valid {
cap, err := uint128FromNumeric(src.TermsCap)
if err != nil {
return nil, errors.Wrap(err, "failed to parse terms cap")
}
terms.Cap = &cap
}
if src.TermsHeightStart.Valid {
heightStart, err := uint128FromNumeric(src.TermsHeightStart)
if err != nil {
return nil, errors.Wrap(err, "failed to parse terms height start")
}
terms.HeightStart = lo.ToPtr((heightStart.Uint64()))
}
if src.TermsHeightEnd.Valid {
heightEnd, err := uint128FromNumeric(src.TermsHeightEnd)
if err != nil {
return nil, errors.Wrap(err, "failed to parse terms height end")
}
terms.HeightEnd = lo.ToPtr((heightEnd.Uint64()))
}
if src.TermsOffsetStart.Valid {
offsetStart, err := uint128FromNumeric(src.TermsOffsetStart)
if err != nil {
return nil, errors.Wrap(err, "failed to parse terms offset start")
}
terms.OffsetStart = lo.ToPtr((offsetStart.Uint64()))
}
if src.TermsOffsetEnd.Valid {
offsetEnd, err := uint128FromNumeric(src.TermsOffsetEnd)
if err != nil {
return nil, errors.Wrap(err, "failed to parse terms offset end")
}
terms.OffsetEnd = lo.ToPtr((offsetEnd.Uint64()))
}
}
return &runes.RuneEntry{
RuneId: runeId,
SpacedRune: runes.NewSpacedRune(rune, uint32(src.Spacers)),
Mints: mints,
BurnedAmount: burnedAmount,
Premine: premine,
Symbol: src.Symbol,
Divisibility: uint8(src.Divisibility),
CompletionTime: completionTime,
Terms: terms,
}, nil
}

View File

@@ -0,0 +1,21 @@
package postgres
import (
"testing"
"github.com/gaze-network/uint128"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/assert"
)
func TestUint128FromNumeric(t *testing.T) {
numeric := pgtype.Numeric{}
numeric.ScanInt64(pgtype.Int8{
Int64: 1000,
Valid: true,
})
result, err := uint128FromNumeric(numeric)
assert.NoError(t, err)
assert.Equal(t, uint128.From64(1000), result)
}

View File

@@ -0,0 +1,61 @@
package postgres
import (
"context"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/internal/postgres"
"github.com/gaze-network/indexer-network/modules/runes/internal/repository/postgres/gen"
"github.com/jackc/pgx/v5"
)
type Repository struct {
db postgres.DB
queries *gen.Queries
tx pgx.Tx
}
func NewRepository(db postgres.DB) *Repository {
return &Repository{
db: db,
queries: gen.New(db),
}
}
var ErrTxAlreadyExists = errs.ErrorKind("Transaction already exists. Call Commit() or Rollback() first.")
func (r *Repository) Begin(ctx context.Context) (err error) {
if r.tx != nil {
return errors.WithStack(ErrTxAlreadyExists)
}
r.tx, err = r.db.Begin(ctx)
if err != nil {
return errors.Wrap(err, "failed to begin transaction")
}
return nil
}
func (r *Repository) Commit(ctx context.Context) error {
if r.tx == nil {
return nil
}
err := r.tx.Commit(ctx)
if err != nil {
return errors.Wrap(err, "failed to commit transaction")
}
r.tx = nil
return nil
}
func (r *Repository) Rollback(ctx context.Context) error {
if r.tx == nil {
return nil
}
err := r.tx.Rollback(ctx)
if err != nil {
return errors.Wrap(err, "failed to rollback transaction")
}
r.tx = nil
return nil
}

View File

@@ -0,0 +1,88 @@
package postgres
import (
"context"
"github.com/btcsuite/btcd/wire"
"github.com/cockroachdb/errors"
"github.com/gaze-network/indexer-network/common/errs"
"github.com/gaze-network/indexer-network/modules/runes/internal/datagateway"
"github.com/gaze-network/indexer-network/modules/runes/internal/repository/postgres/gen"
"github.com/gaze-network/indexer-network/modules/runes/internal/runes"
"github.com/gaze-network/uint128"
"github.com/jackc/pgx/v5"
)
var _ datagateway.RunesDataGateway = (*Repository)(nil)
func (r *Repository) GetRunesBalancesAtOutPoint(ctx context.Context, outPoint wire.OutPoint) (map[runes.RuneId]uint128.Uint128, error) {
balances, err := r.queries.GetOutPointBalances(ctx, gen.GetOutPointBalancesParams{
TxHash: outPoint.Hash.String(),
TxIdx: int32(outPoint.Index),
})
if err != nil {
return nil, errors.Wrap(err, "error during query")
}
result := make(map[runes.RuneId]uint128.Uint128, len(balances))
for _, balance := range balances {
runeId, err := runes.NewRuneIdFromString(balance.RuneID)
if err != nil {
return nil, errors.Wrap(err, "failed to parse RuneId")
}
value, err := uint128FromNumeric(balance.Value)
if err != nil {
return nil, errors.Wrap(err, "failed to parse balance")
}
result[runeId] = value
}
return result, nil
}
func (r *Repository) GetRuneIdByRune(ctx context.Context, rune runes.Rune) (runes.RuneId, error) {
runeEntryModel, err := r.queries.GetRuneEntryByRune(ctx, rune.String())
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return runes.RuneId{}, errors.WithStack(errs.NotFound)
}
return runes.RuneId{}, errors.Wrap(err, "error during query")
}
runeId, err := runes.NewRuneIdFromString(runeEntryModel.RuneID)
if err != nil {
return runes.RuneId{}, errors.Wrap(err, "failed to parse rune id")
}
return runeId, nil
}
func (r *Repository) GetRuneEntryByRuneId(ctx context.Context, runeId runes.RuneId) (*runes.RuneEntry, error) {
runeEntryModel, err := r.queries.GetRuneEntryByRuneId(ctx, runeId.String())
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, errors.WithStack(errs.NotFound)
}
return nil, errors.Wrap(err, "error during query")
}
runeEntry, err := mapRuneEntryModelToType(runeEntryModel)
if err != nil {
return nil, errors.Wrap(err, "failed to parse rune entry model")
}
return runeEntry, nil
}
func (r *Repository) SetRuneEntry(ctx context.Context, entry *runes.RuneEntry) error {
panic("implement me")
}
func (r *Repository) CreateRunesBalanceAtOutPoint(ctx context.Context, outPoint wire.OutPoint, balances map[runes.RuneId]uint128.Uint128) error {
panic("implement me")
}
func (r *Repository) CreateRuneBalance(ctx context.Context, pkScript string, runeId runes.RuneId, blockHeight uint64, balance uint128.Uint128) error {
panic("implement me")
}
func (r *Repository) UpdateLatestBlockHeight(ctx context.Context, blockHeight uint64) error {
panic("implement me")
}

View File

@@ -1,6 +1,7 @@
package runes
import (
"fmt"
"strconv"
"strings"
@@ -51,6 +52,10 @@ func NewRuneIdFromString(str string) (RuneId, error) {
}, nil
}
func (r RuneId) String() string {
return fmt.Sprintf("%d:%d", r.BlockHeight, r.TxIndex)
}
// Delta calculates the delta encoding between two RuneIds. If the two RuneIds are in the same block, then the block delta is 0 and the tx index delta is the difference between the two tx indices.
// If the two RuneIds are in different blocks, then the block delta is the difference between the two block indices and the tx index delta is the tx index in the other block.
func (r RuneId) Delta(next RuneId) (uint64, uint32) {