mirror of
https://github.com/alexgo-io/gaze-indexer.git
synced 2026-04-29 20:25:24 +08:00
Merge branch 'develop' into feature/bitcoin-indexer
This commit is contained in:
@@ -6,7 +6,9 @@ type ErrorKind string
|
||||
|
||||
const (
|
||||
// NotFound is returned when a requested item is not found.
|
||||
NotFound = ErrorKind("Not Found")
|
||||
NotFound = ErrorKind("Not Found")
|
||||
OverflowUint64 = ErrorKind("overflow uint64")
|
||||
OverflowUint128 = ErrorKind("overflow uint128")
|
||||
)
|
||||
|
||||
// Error satisfies the error interface and prints human-readable errors.
|
||||
|
||||
1
go.mod
1
go.mod
@@ -8,6 +8,7 @@ require (
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
|
||||
github.com/cockroachdb/errors v1.11.1
|
||||
github.com/gaze-network/uint128 v1.2.0
|
||||
github.com/jackc/pgx/v5 v5.5.5
|
||||
github.com/samber/lo v1.39.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
|
||||
2
go.sum
2
go.sum
@@ -47,6 +47,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gaze-network/uint128 v1.2.0 h1:LRruR+EvzNv/eJg8nk0hztMJ4tOpYKolFYlQZqNvWmo=
|
||||
github.com/gaze-network/uint128 v1.2.0/go.mod h1:zAwwcnoRUNiiQj0vjLmHgNgJ+w2RUgzMAJgl8d7tRug=
|
||||
github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0=
|
||||
github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
|
||||
47
lib/leb128/leb128.go
Normal file
47
lib/leb128/leb128.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package leb128
|
||||
|
||||
import (
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/uint128"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrEmpty = errs.ErrorKind("leb128: empty byte sequence")
|
||||
ErrUnterminated = errs.ErrorKind("leb128: unterminated byte sequence")
|
||||
)
|
||||
|
||||
func EncodeLEB128(input uint128.Uint128) []byte {
|
||||
bytes := make([]byte, 0)
|
||||
// for n >> 7 > 0
|
||||
for !input.Rsh(7).IsZero() {
|
||||
last_7_bits := input.And64(0b0111_1111).Uint8()
|
||||
bytes = append(bytes, last_7_bits|0b1000_0000)
|
||||
input = input.Rsh(7)
|
||||
}
|
||||
last_byte := input.Uint8()
|
||||
bytes = append(bytes, last_byte)
|
||||
return bytes
|
||||
}
|
||||
|
||||
func DecodeLEB128(data []byte) (n uint128.Uint128, length int, err error) {
|
||||
if len(data) == 0 {
|
||||
return uint128.Uint128{}, 0, ErrEmpty
|
||||
}
|
||||
n = uint128.From64(0)
|
||||
|
||||
for i, b := range data {
|
||||
if i > 18 {
|
||||
return uint128.Uint128{}, 0, errs.OverflowUint128
|
||||
}
|
||||
value := uint128.New(uint64(b&0b0111_1111), 0)
|
||||
if i == 18 && !value.And64(0b0111_1100).IsZero() {
|
||||
return uint128.Uint128{}, 0, errs.OverflowUint128
|
||||
}
|
||||
n = n.Or(value.Lsh(uint(7 * i)))
|
||||
// if the high bit is not set, then this is the last byte
|
||||
if b&0b1000_0000 == 0 {
|
||||
return n, i + 1, nil
|
||||
}
|
||||
}
|
||||
return uint128.Uint128{}, 0, ErrUnterminated
|
||||
}
|
||||
83
lib/leb128/leb128_test.go
Normal file
83
lib/leb128/leb128_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package leb128
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
"github.com/gaze-network/uint128"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
test := func(n uint128.Uint128) {
|
||||
t.Run(n.String(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
encoded := EncodeLEB128(n)
|
||||
decoded, length, err := DecodeLEB128(encoded)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, n, decoded)
|
||||
assert.Equal(t, len(encoded), length)
|
||||
})
|
||||
}
|
||||
|
||||
test(uint128.Zero)
|
||||
// powers of two
|
||||
for i := 0; i < 128; i++ {
|
||||
n := uint128.From64(1)
|
||||
n = n.Lsh(uint(i))
|
||||
test(n)
|
||||
}
|
||||
|
||||
// alternating bits
|
||||
n := uint128.Zero
|
||||
for i := 0; i < 128; i++ {
|
||||
n = n.Lsh(1).Or(uint128.From64(uint64(i % 2)))
|
||||
test(n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeError(t *testing.T) {
|
||||
testError := func(name string, bytes []byte, expectedError error) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, _, err := DecodeLEB128(bytes)
|
||||
if expectedError == nil {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.ErrorIs(t, err, expectedError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
testError("empty", []byte{}, ErrEmpty)
|
||||
testError("unterminated", []byte{0b1000_0000}, ErrUnterminated)
|
||||
|
||||
// may not be longer than 19 bytes
|
||||
testError("valid 18 bytes", []byte{
|
||||
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 0,
|
||||
}, nil)
|
||||
testError("overflow 19 bytes", []byte{
|
||||
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128,
|
||||
128, 0,
|
||||
}, errs.OverflowUint128)
|
||||
|
||||
// may not overflow uint128
|
||||
testError("overflow 1", []byte{
|
||||
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 64,
|
||||
}, errs.OverflowUint128)
|
||||
testError("overflow 2", []byte{
|
||||
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 32,
|
||||
}, errs.OverflowUint128)
|
||||
testError("overflow 3", []byte{
|
||||
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 16,
|
||||
}, errs.OverflowUint128)
|
||||
testError("overflow 4", []byte{
|
||||
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 8,
|
||||
}, errs.OverflowUint128)
|
||||
testError("overflow 5", []byte{
|
||||
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 4,
|
||||
}, errs.OverflowUint128)
|
||||
testError("not overflow", []byte{
|
||||
128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 2,
|
||||
}, nil)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package runes
|
||||
|
||||
import "math/big"
|
||||
|
||||
type Edict struct {
|
||||
Amount big.Int
|
||||
Id RuneId
|
||||
Output int
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package runes
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/Cleverse/go-utilities/utils"
|
||||
)
|
||||
|
||||
type Terms struct {
|
||||
// Amount of the rune to be minted per transaction
|
||||
Amount *big.Int
|
||||
// Number of allowed mints
|
||||
Cap *big.Int
|
||||
// Block height at which the rune can start being minted. If both HeightStart and OffsetStart are set, use the higher value.
|
||||
HeightStart uint64
|
||||
// Block height at which the rune can no longer be minted. If both HeightEnd and OffsetEnd are set, use the lower value.
|
||||
HeightEnd uint64
|
||||
// Offset from etched block at which the rune can start being minted. If both HeightStart and OffsetStart are set, use the higher value.
|
||||
OffsetStart uint64
|
||||
// Offset from etched block at which the rune can no longer be minted. If both HeightEnd and OffsetEnd are set, use the lower value.
|
||||
OffsetEnd uint64
|
||||
}
|
||||
|
||||
type Etching struct {
|
||||
// Number of runes to be minted during etching
|
||||
Premine *big.Int
|
||||
// Rune name
|
||||
Rune Rune
|
||||
// Minting terms. If not provided, the rune is not mintable.
|
||||
Terms *Terms
|
||||
// Bitmap of spacers to be displayed between each letter of the rune name
|
||||
Spacers uint32
|
||||
// Single Unicode codepoint to represent the rune
|
||||
Symbol rune
|
||||
// Number of decimals when displaying the rune
|
||||
Divisibility uint8
|
||||
}
|
||||
|
||||
const (
|
||||
maxDivisibility uint8 = 38
|
||||
maxSpacers uint32 = 0b00000111_11111111_11111111_11111111
|
||||
)
|
||||
|
||||
func (e Etching) Supply() *big.Int {
|
||||
terms := utils.Default(e.Terms, &Terms{})
|
||||
|
||||
amount := utils.Default(terms.Amount, big.NewInt(0))
|
||||
cap := utils.Default(terms.Cap, big.NewInt(0))
|
||||
premine := utils.Default(e.Premine, big.NewInt(0))
|
||||
|
||||
result := new(big.Int).Mul(amount, cap)
|
||||
return result.Add(result, premine)
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package runes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TODO: add maxSpacers test
|
||||
|
||||
func TestSupply(t *testing.T) {
|
||||
testNumber := 0
|
||||
test := func(e Etching, expectedSupply *big.Int) {
|
||||
t.Run(fmt.Sprintf("case_%d", testNumber), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actualSupply := e.Supply()
|
||||
assert.Equal(t, expectedSupply, actualSupply)
|
||||
})
|
||||
testNumber++
|
||||
}
|
||||
|
||||
test(Etching{}, big.NewInt(0))
|
||||
|
||||
test(Etching{
|
||||
Premine: big.NewInt(0),
|
||||
Terms: nil,
|
||||
}, big.NewInt(0))
|
||||
|
||||
test(Etching{
|
||||
Premine: big.NewInt(1),
|
||||
Terms: nil,
|
||||
}, big.NewInt(1))
|
||||
|
||||
test(Etching{
|
||||
Premine: big.NewInt(1),
|
||||
Terms: &Terms{
|
||||
Amount: nil,
|
||||
Cap: nil,
|
||||
},
|
||||
}, big.NewInt(1))
|
||||
|
||||
test(Etching{
|
||||
Premine: big.NewInt(1000),
|
||||
Terms: &Terms{
|
||||
Amount: big.NewInt(100),
|
||||
Cap: big.NewInt(10),
|
||||
},
|
||||
}, big.NewInt(2000))
|
||||
|
||||
test(Etching{
|
||||
Premine: nil,
|
||||
Terms: &Terms{
|
||||
Amount: big.NewInt(100),
|
||||
Cap: big.NewInt(10),
|
||||
},
|
||||
}, big.NewInt(1000))
|
||||
|
||||
test(Etching{
|
||||
Premine: big.NewInt(1000),
|
||||
Terms: &Terms{
|
||||
Amount: big.NewInt(100),
|
||||
Cap: nil,
|
||||
},
|
||||
}, big.NewInt(1000))
|
||||
|
||||
test(Etching{
|
||||
Premine: big.NewInt(1000),
|
||||
Terms: &Terms{
|
||||
Amount: nil,
|
||||
Cap: big.NewInt(10),
|
||||
},
|
||||
}, big.NewInt(1000))
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
package runes
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"slices"
|
||||
|
||||
"github.com/Cleverse/go-utilities/utils"
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
)
|
||||
|
||||
type Rune big.Int
|
||||
|
||||
func NewRune(value int64) *Rune {
|
||||
return (*Rune)(big.NewInt(value))
|
||||
}
|
||||
|
||||
func NewRuneFromBigInt(value *big.Int) *Rune {
|
||||
return (*Rune)(new(big.Int).Set(value))
|
||||
}
|
||||
|
||||
var ErrInvalidBase10 = errs.ErrorKind("invalid base-10 character: must be in the range [0-9]")
|
||||
|
||||
// NewRuneFromString creates a new Rune from a string of base-10 integer
|
||||
func NewRuneFromString(value string) (*Rune, error) {
|
||||
bi, ok := new(big.Int).SetString(value, 10)
|
||||
if !ok {
|
||||
return nil, ErrInvalidBase10
|
||||
}
|
||||
return (*Rune)(bi), nil
|
||||
}
|
||||
|
||||
var ErrInvalidBase26 = errs.ErrorKind("invalid base-26 character: must be in the range [A-Z]")
|
||||
|
||||
// NewRuneFromBase26 creates a new Rune from a string of modified base-26 integer
|
||||
func NewRuneFromBase26(value string) (*Rune, error) {
|
||||
x := big.NewInt(0)
|
||||
one := big.NewInt(1)
|
||||
int26 := big.NewInt(26)
|
||||
for i, char := range value {
|
||||
if i > 0 {
|
||||
x = x.Add(x, one)
|
||||
}
|
||||
x = x.Mul(x, int26)
|
||||
if char < 'A' || char > 'Z' {
|
||||
return nil, ErrInvalidBase26
|
||||
}
|
||||
x = x.Add(x, big.NewInt(int64(char-'A')))
|
||||
}
|
||||
return (*Rune)(x), nil
|
||||
}
|
||||
|
||||
var firstReservedRune = utils.Must(NewRuneFromString("6402364363415443603228541259936211926"))
|
||||
|
||||
var unlockSteps = []*big.Int{
|
||||
utils.Must(new(big.Int).SetString("0", 10)), // A
|
||||
utils.Must(new(big.Int).SetString("26", 10)), // AA
|
||||
utils.Must(new(big.Int).SetString("702", 10)), // AAA
|
||||
utils.Must(new(big.Int).SetString("18278", 10)), // AAAA
|
||||
utils.Must(new(big.Int).SetString("475254", 10)), // AAAAA
|
||||
utils.Must(new(big.Int).SetString("12356630", 10)), // AAAAAA
|
||||
utils.Must(new(big.Int).SetString("321272406", 10)), // AAAAAAA
|
||||
utils.Must(new(big.Int).SetString("8353082582", 10)), // AAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("217180147158", 10)), // AAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("5646683826134", 10)), // AAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("146813779479510", 10)), // AAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("3817158266467286", 10)), // AAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("99246114928149462", 10)), // AAAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("2580398988131886038", 10)), // AAAAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("67090373691429037014", 10)), // AAAAAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("1744349715977154962390", 10)), // AAAAAAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("45353092615406029022166", 10)), // AAAAAAAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("1179180408000556754576342", 10)), // AAAAAAAAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("30658690608014475618984918", 10)), // AAAAAAAAAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("797125955808376366093607894", 10)), // AAAAAAAAAAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("20725274851017785518433805270", 10)), // AAAAAAAAAAAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("538857146126462423479278937046", 10)), // AAAAAAAAAAAAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("14010285799288023010461252363222", 10)), // AAAAAAAAAAAAAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("364267430781488598271992561443798", 10)), // AAAAAAAAAAAAAAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("9470953200318703555071806597538774", 10)), // AAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("246244783208286292431866971536008150", 10)), // AAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("6402364363415443603228541259936211926", 10)), // AAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
utils.Must(new(big.Int).SetString("166461473448801533683942072758341510102", 10)), // AAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
}
|
||||
|
||||
func (r Rune) IsReserved() bool {
|
||||
return (*big.Int)(&r).Cmp((*big.Int)(firstReservedRune)) >= 0
|
||||
}
|
||||
|
||||
func (r Rune) Add(value *big.Int) *Rune {
|
||||
bi := (*big.Int)(&r)
|
||||
return (*Rune)(bi.Add(bi, value))
|
||||
}
|
||||
|
||||
// Commitment returns the commitment of the rune. The commitment is the little-endian encoding of the rune.
|
||||
func (r Rune) Commitment() []byte {
|
||||
bytes := (*big.Int)(&r).Bytes()
|
||||
slices.Reverse(bytes)
|
||||
return bytes
|
||||
}
|
||||
|
||||
// String returns the string representation of the rune in modified base-26 integer
|
||||
func (r Rune) String() string {
|
||||
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
// value = r + 1
|
||||
value := new(big.Int).Add((*big.Int)(&r), big.NewInt(1))
|
||||
var encoded []byte
|
||||
for value.Sign() > 0 {
|
||||
// idx = (value - 1) % 26
|
||||
idx := new(big.Int).Mod(new(big.Int).Sub(value, big.NewInt(1)), big.NewInt(26)).Int64()
|
||||
encoded = append(encoded, chars[idx])
|
||||
// value = (value - 1) / 26
|
||||
value = new(big.Int).Div(new(big.Int).Sub(value, big.NewInt(1)), big.NewInt(26))
|
||||
}
|
||||
slices.Reverse(encoded)
|
||||
return string(encoded)
|
||||
}
|
||||
|
||||
func FirstRuneHeight(network common.Network) uint64 {
|
||||
switch network {
|
||||
case common.NetworkMainnet:
|
||||
return common.HalvingInterval * 4
|
||||
case common.NetworkTestnet:
|
||||
return common.HalvingInterval * 12
|
||||
}
|
||||
panic("invalid network")
|
||||
}
|
||||
|
||||
func MinimumRuneAtHeight(network common.Network, height uint64) *Rune {
|
||||
offset := height + 1
|
||||
interval := common.HalvingInterval / 12
|
||||
|
||||
// runes are gradually unlocked from rune activation height until the next halving
|
||||
start := FirstRuneHeight(network)
|
||||
end := start + common.HalvingInterval
|
||||
|
||||
if offset < start {
|
||||
return (*Rune)(unlockSteps[12])
|
||||
}
|
||||
if offset >= end {
|
||||
return (*Rune)(unlockSteps[0])
|
||||
}
|
||||
progress := offset - start
|
||||
length := 12 - progress/uint64(interval)
|
||||
|
||||
startRune := unlockSteps[length]
|
||||
endRune := unlockSteps[length-1] // length cannot be 0 because we checked that offset < end
|
||||
remainder := big.NewInt(int64(progress) % int64(interval))
|
||||
|
||||
runeRange := new(big.Int).Sub(startRune, endRune)
|
||||
result := new(big.Int).Mul(runeRange, remainder)
|
||||
result = result.Div(result, big.NewInt(int64(interval)))
|
||||
result = result.Sub(startRune, result)
|
||||
return (*Rune)(result)
|
||||
}
|
||||
|
||||
func GetReservedRune(blockHeight uint64, txIndex uint64) *Rune {
|
||||
// firstReservedRune + ((blockHeight << 32) | txIndex)
|
||||
increment := big.NewInt(int64(blockHeight))
|
||||
increment = increment.Lsh(increment, 32)
|
||||
increment = increment.Or(increment, big.NewInt(int64(txIndex)))
|
||||
return firstReservedRune.Add(increment)
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package runes
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/gaze-network/indexer-network/common/errs"
|
||||
)
|
||||
|
||||
type RuneId struct {
|
||||
BlockHeight uint64
|
||||
TxIndex uint64
|
||||
}
|
||||
|
||||
func NewRuneId(blockHeight uint64, txIndex uint64) RuneId {
|
||||
return RuneId{
|
||||
BlockHeight: blockHeight,
|
||||
TxIndex: txIndex,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidSeparator = errs.ErrorKind("invalid rune id: must contain exactly one separator")
|
||||
ErrCannotParseBlockHeight = errs.ErrorKind("invalid rune id: cannot parse block height")
|
||||
ErrCannotParseTxIndex = errs.ErrorKind("invalid rune id: cannot parse tx index")
|
||||
)
|
||||
|
||||
func NewRuneIdFromString(str string) (RuneId, error) {
|
||||
strs := strings.Split(str, ":")
|
||||
if len(strs) != 2 {
|
||||
return RuneId{}, ErrInvalidSeparator
|
||||
}
|
||||
blockHeightStr, txIndexStr := strs[0], strs[1]
|
||||
blockHeight, err := strconv.ParseUint(blockHeightStr, 10, 64)
|
||||
if err != nil {
|
||||
return RuneId{}, errors.WithStack(errors.Join(err, ErrCannotParseBlockHeight))
|
||||
}
|
||||
txIndex, err := strconv.ParseUint(txIndexStr, 10, 64)
|
||||
if err != nil {
|
||||
return RuneId{}, errors.WithStack(errors.Join(err, ErrCannotParseTxIndex))
|
||||
}
|
||||
return RuneId{
|
||||
BlockHeight: blockHeight,
|
||||
TxIndex: txIndex,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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, uint64) {
|
||||
blockDelta := next.BlockHeight - r.BlockHeight
|
||||
// if the block is the same, then tx index is the difference between the two
|
||||
if blockDelta == 0 {
|
||||
return 0, next.TxIndex - r.TxIndex
|
||||
}
|
||||
// otherwise, tx index is the tx index in the next block
|
||||
return blockDelta, next.TxIndex
|
||||
}
|
||||
|
||||
// Next calculates the next RuneId given a block delta and tx index delta.
|
||||
func (r RuneId) Next(blockDelta uint64, txIndexDelta uint64) RuneId {
|
||||
if blockDelta == 0 {
|
||||
return RuneId{
|
||||
BlockHeight: r.BlockHeight,
|
||||
TxIndex: r.TxIndex + txIndexDelta,
|
||||
}
|
||||
}
|
||||
return RuneId{
|
||||
BlockHeight: r.BlockHeight + blockDelta,
|
||||
TxIndex: txIndexDelta,
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package runes
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewRuneIdFromString(t *testing.T) {
|
||||
type testcase struct {
|
||||
name string
|
||||
input string
|
||||
expectedOutput RuneId
|
||||
shouldError bool
|
||||
}
|
||||
// TODO: test error instance match expected errors
|
||||
testcases := []testcase{
|
||||
{
|
||||
name: "valid rune id",
|
||||
input: "1:2",
|
||||
expectedOutput: RuneId{
|
||||
BlockHeight: 1,
|
||||
TxIndex: 2,
|
||||
},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "too many separators",
|
||||
input: "1:2:3",
|
||||
expectedOutput: RuneId{},
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
name: "too few separators",
|
||||
input: "1",
|
||||
expectedOutput: RuneId{},
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid tx index",
|
||||
input: "1:a",
|
||||
expectedOutput: RuneId{},
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid block",
|
||||
input: "a:1",
|
||||
expectedOutput: RuneId{},
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
name: "empty tx index",
|
||||
input: "1:",
|
||||
expectedOutput: RuneId{},
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
name: "empty block",
|
||||
input: ":1",
|
||||
expectedOutput: RuneId{},
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
name: "empty block and tx index",
|
||||
input: ":",
|
||||
expectedOutput: RuneId{},
|
||||
shouldError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
runeId, err := NewRuneIdFromString(tc.input)
|
||||
if tc.shouldError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedOutput, runeId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
package runes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gaze-network/indexer-network/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
test := func(rune *Rune, encoded string) {
|
||||
t.Run(encoded, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actualEncoded := rune.String()
|
||||
assert.Equal(t, encoded, actualEncoded)
|
||||
|
||||
actualRune, err := NewRuneFromBase26(encoded)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, rune, actualRune)
|
||||
})
|
||||
}
|
||||
|
||||
test(NewRune(0), "A")
|
||||
test(NewRune(1), "B")
|
||||
test(NewRune(2), "C")
|
||||
test(NewRune(3), "D")
|
||||
test(NewRune(4), "E")
|
||||
test(NewRune(5), "F")
|
||||
test(NewRune(6), "G")
|
||||
test(NewRune(7), "H")
|
||||
test(NewRune(8), "I")
|
||||
test(NewRune(9), "J")
|
||||
test(NewRune(10), "K")
|
||||
test(NewRune(11), "L")
|
||||
test(NewRune(12), "M")
|
||||
test(NewRune(13), "N")
|
||||
test(NewRune(14), "O")
|
||||
test(NewRune(15), "P")
|
||||
test(NewRune(16), "Q")
|
||||
test(NewRune(17), "R")
|
||||
test(NewRune(18), "S")
|
||||
test(NewRune(19), "T")
|
||||
test(NewRune(20), "U")
|
||||
test(NewRune(21), "V")
|
||||
test(NewRune(22), "W")
|
||||
test(NewRune(23), "X")
|
||||
test(NewRune(24), "Y")
|
||||
test(NewRune(25), "Z")
|
||||
test(NewRune(26), "AA")
|
||||
test(NewRune(27), "AB")
|
||||
test(NewRune(51), "AZ")
|
||||
test(NewRune(52), "BA")
|
||||
test(NewRune(53), "BB")
|
||||
maxUint128 := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
maxUint128 = maxUint128.Sub(maxUint128, big.NewInt(1))
|
||||
test(NewRuneFromBigInt(new(big.Int).Sub(maxUint128, big.NewInt(2))), "BCGDENLQRQWDSLRUGSNLBTMFIJAT")
|
||||
test(NewRuneFromBigInt(new(big.Int).Sub(maxUint128, big.NewInt(1))), "BCGDENLQRQWDSLRUGSNLBTMFIJAU")
|
||||
test(NewRuneFromBigInt(maxUint128), "BCGDENLQRQWDSLRUGSNLBTMFIJAV")
|
||||
}
|
||||
|
||||
func TestNewRuneFromBase26Error(t *testing.T) {
|
||||
_, err := NewRuneFromBase26("?")
|
||||
assert.ErrorIs(t, err, ErrInvalidBase26)
|
||||
}
|
||||
|
||||
func TestFirstRuneHeight(t *testing.T) {
|
||||
test := func(network common.Network, expected uint64) {
|
||||
t.Run(network.String(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actual := FirstRuneHeight(network)
|
||||
assert.Equal(t, expected, actual)
|
||||
})
|
||||
}
|
||||
|
||||
test(common.NetworkMainnet, 840_000)
|
||||
test(common.NetworkTestnet, 2_520_000)
|
||||
}
|
||||
|
||||
func TestMinimumRuneAtHeightMainnet(t *testing.T) {
|
||||
test := func(height uint64, encoded string) {
|
||||
t.Run(fmt.Sprintf("%d", height), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
rune, err := NewRuneFromBase26(encoded)
|
||||
assert.NoError(t, err)
|
||||
actual := MinimumRuneAtHeight(common.NetworkMainnet, height)
|
||||
assert.Equal(t, (*big.Int)(rune).String(), (*big.Int)(actual).String())
|
||||
})
|
||||
}
|
||||
|
||||
start := FirstRuneHeight(common.NetworkMainnet)
|
||||
end := start + common.HalvingInterval
|
||||
interval := uint64(common.HalvingInterval / 12)
|
||||
|
||||
test(0, "AAAAAAAAAAAAA")
|
||||
test(start/2, "AAAAAAAAAAAAA")
|
||||
test(start, "ZZYZXBRKWXVA")
|
||||
test(start+1, "ZZXZUDIVTVQA")
|
||||
test(end-1, "A")
|
||||
test(end, "A")
|
||||
test(end+1, "A")
|
||||
test(math.MaxUint32, "A")
|
||||
|
||||
test(start+interval*0-1, "AAAAAAAAAAAAA")
|
||||
test(start+interval*0, "ZZYZXBRKWXVA")
|
||||
test(start+interval*0+1, "ZZXZUDIVTVQA")
|
||||
|
||||
test(start+interval*1-1, "AAAAAAAAAAAA")
|
||||
test(start+interval*1, "ZZYZXBRKWXV")
|
||||
test(start+interval*1+1, "ZZXZUDIVTVQ")
|
||||
|
||||
test(start+interval*2-1, "AAAAAAAAAAA")
|
||||
test(start+interval*2, "ZZYZXBRKWY")
|
||||
test(start+interval*2+1, "ZZXZUDIVTW")
|
||||
|
||||
test(start+interval*3-1, "AAAAAAAAAA")
|
||||
test(start+interval*3, "ZZYZXBRKX")
|
||||
test(start+interval*3+1, "ZZXZUDIVU")
|
||||
|
||||
test(start+interval*4-1, "AAAAAAAAA")
|
||||
test(start+interval*4, "ZZYZXBRL")
|
||||
test(start+interval*4+1, "ZZXZUDIW")
|
||||
|
||||
test(start+interval*5-1, "AAAAAAAA")
|
||||
test(start+interval*5, "ZZYZXBS")
|
||||
test(start+interval*5+1, "ZZXZUDJ")
|
||||
|
||||
test(start+interval*6-1, "AAAAAAA")
|
||||
test(start+interval*6, "ZZYZXC")
|
||||
test(start+interval*6+1, "ZZXZUE")
|
||||
|
||||
test(start+interval*7-1, "AAAAAA")
|
||||
test(start+interval*7, "ZZYZY")
|
||||
test(start+interval*7+1, "ZZXZV")
|
||||
|
||||
test(start+interval*8-1, "AAAAA")
|
||||
test(start+interval*8, "ZZZA")
|
||||
test(start+interval*8+1, "ZZYA")
|
||||
|
||||
test(start+interval*9-1, "AAAA")
|
||||
test(start+interval*9, "ZZZ")
|
||||
test(start+interval*9+1, "ZZY")
|
||||
|
||||
test(start+interval*10-2, "AAC")
|
||||
test(start+interval*10-1, "AAA")
|
||||
test(start+interval*10, "AAA")
|
||||
test(start+interval*10+1, "AAA")
|
||||
|
||||
test(start+interval*10+interval/2, "NA")
|
||||
|
||||
test(start+interval*11-2, "AB")
|
||||
test(start+interval*11-1, "AA")
|
||||
test(start+interval*11, "AA")
|
||||
test(start+interval*11+1, "AA")
|
||||
|
||||
test(start+interval*11+interval/2, "N")
|
||||
|
||||
test(start+interval*12-2, "B")
|
||||
test(start+interval*12-1, "A")
|
||||
test(start+interval*12, "A")
|
||||
test(start+interval*12+1, "A")
|
||||
}
|
||||
|
||||
func TestMinimumRuneAtHeightTestnet(t *testing.T) {
|
||||
test := func(height uint64, runeStr string) {
|
||||
t.Run(fmt.Sprintf("%d", height), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
rune, err := NewRuneFromBase26(runeStr)
|
||||
assert.NoError(t, err)
|
||||
actual := MinimumRuneAtHeight(common.NetworkTestnet, height)
|
||||
assert.Equal(t, rune, actual)
|
||||
})
|
||||
}
|
||||
|
||||
start := FirstRuneHeight(common.NetworkTestnet)
|
||||
|
||||
test(start-1, "AAAAAAAAAAAAA")
|
||||
test(start, "ZZYZXBRKWXVA")
|
||||
test(start+1, "ZZXZUDIVTVQA")
|
||||
}
|
||||
|
||||
func TestIsReserved(t *testing.T) {
|
||||
test := func(runeStr string, expected bool) {
|
||||
t.Run(runeStr, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
rune, err := NewRuneFromBase26(runeStr)
|
||||
assert.NoError(t, err)
|
||||
actual := rune.IsReserved()
|
||||
assert.Equal(t, expected, actual)
|
||||
})
|
||||
}
|
||||
|
||||
test("A", false)
|
||||
test("B", false)
|
||||
test("ZZZZZZZZZZZZZZZZZZZZZZZZZZ", false)
|
||||
test("AAAAAAAAAAAAAAAAAAAAAAAAAAA", true)
|
||||
test("AAAAAAAAAAAAAAAAAAAAAAAAAAB", true)
|
||||
test("BCGDENLQRQWDSLRUGSNLBTMFIJAV", true)
|
||||
}
|
||||
|
||||
func TestGetReservedRune(t *testing.T) {
|
||||
test := func(blockHeight uint64, txIndex uint64, expected *Rune) {
|
||||
t.Run(fmt.Sprintf("blockHeight_%d_txIndex_%d", blockHeight, txIndex), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
rune := GetReservedRune(blockHeight, txIndex)
|
||||
assert.Equal(t, expected, rune)
|
||||
})
|
||||
}
|
||||
|
||||
test(0, 0, firstReservedRune)
|
||||
test(0, 1, firstReservedRune.Add(big.NewInt(1)))
|
||||
test(0, 2, firstReservedRune.Add(big.NewInt(2)))
|
||||
test(1, 0, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(1), 32)))
|
||||
test(1, 1, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(1), 32)).Add(big.NewInt(1)))
|
||||
test(1, 2, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(1), 32)).Add(big.NewInt(2)))
|
||||
test(2, 0, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(2), 32)))
|
||||
test(2, 1, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(2), 32)).Add(big.NewInt(1)))
|
||||
test(2, 2, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(2), 32)).Add(big.NewInt(2)))
|
||||
test(math.MaxInt64, 0, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(math.MaxInt64), 32)))
|
||||
test(math.MaxInt64, math.MaxInt64, firstReservedRune.Add(new(big.Int).Lsh(big.NewInt(math.MaxInt64), 32)).Add(big.NewInt(math.MaxInt64)))
|
||||
}
|
||||
|
||||
func TestUnlockSteps(t *testing.T) {
|
||||
for i := 0; i < len(unlockSteps); i++ {
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
encoded := (*Rune)(unlockSteps[i]).String()
|
||||
expected := strings.Repeat("A", i+1)
|
||||
assert.Equal(t, expected, encoded)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitment(t *testing.T) {
|
||||
test := func(rune *Rune, expected []byte) {
|
||||
t.Run((*big.Int)(rune).String(), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actual := rune.Commitment()
|
||||
assert.Equal(t, expected, actual)
|
||||
})
|
||||
}
|
||||
|
||||
test(NewRune(0), []byte{})
|
||||
test(NewRune(1), []byte{1})
|
||||
test(NewRune(2), []byte{2})
|
||||
test(NewRune(255), []byte{255})
|
||||
test(NewRune(256), []byte{0, 1})
|
||||
test(NewRune(257), []byte{1, 1})
|
||||
test(NewRune(65535), []byte{255, 255})
|
||||
test(NewRune(65536), []byte{0, 0, 1})
|
||||
}
|
||||
14
pkg/logger/attrs_keys.go
Normal file
14
pkg/logger/attrs_keys.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package logger
|
||||
|
||||
import "log/slog"
|
||||
|
||||
// Keys for log attributes.
|
||||
const (
|
||||
TimeKey = slog.TimeKey
|
||||
LevelKey = slog.LevelKey
|
||||
MessageKey = slog.MessageKey
|
||||
SourceKey = slog.SourceKey
|
||||
ErrorKey = "error"
|
||||
ErrorVerboseKey = "error_verbose"
|
||||
ErrorStackTraceKey = "error_stacktrace"
|
||||
)
|
||||
49
pkg/logger/chain_handlers.go
Normal file
49
pkg/logger/chain_handlers.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type (
|
||||
handleFunc func(context.Context, slog.Record) error
|
||||
middleware func(handleFunc) handleFunc
|
||||
)
|
||||
|
||||
type chainHandlers struct {
|
||||
h slog.Handler
|
||||
middlewares []middleware
|
||||
}
|
||||
|
||||
func newChainHandlers(handler slog.Handler, middlewares ...middleware) *chainHandlers {
|
||||
return &chainHandlers{
|
||||
h: handler,
|
||||
middlewares: middlewares,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *chainHandlers) Enabled(ctx context.Context, lvl slog.Level) bool {
|
||||
return c.h.Enabled(ctx, lvl)
|
||||
}
|
||||
|
||||
func (c *chainHandlers) Handle(ctx context.Context, rec slog.Record) error {
|
||||
h := c.h.Handle
|
||||
for i := len(c.middlewares) - 1; i >= 0; i-- {
|
||||
h = c.middlewares[i](h)
|
||||
}
|
||||
return h(ctx, rec)
|
||||
}
|
||||
|
||||
func (c *chainHandlers) WithGroup(group string) slog.Handler {
|
||||
return &chainHandlers{
|
||||
middlewares: c.middlewares,
|
||||
h: c.h.WithGroup(group),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *chainHandlers) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return &chainHandlers{
|
||||
middlewares: c.middlewares,
|
||||
h: c.h.WithAttrs(attrs),
|
||||
}
|
||||
}
|
||||
71
pkg/logger/error.go
Normal file
71
pkg/logger/error.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/cockroachdb/errors/errbase"
|
||||
)
|
||||
|
||||
// AttrError returns an attribute with error key.
|
||||
func AttrError(err error) slog.Attr {
|
||||
if err == nil {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return slog.Any(ErrorKey, err)
|
||||
}
|
||||
|
||||
func middlewareError() middleware {
|
||||
return func(next handleFunc) handleFunc {
|
||||
return func(ctx context.Context, rec slog.Record) error {
|
||||
rec.Attrs(func(attr slog.Attr) bool {
|
||||
if attr.Key == ErrorKey || attr.Key == "err" {
|
||||
err := attr.Value.Any()
|
||||
if err, ok := err.(error); ok && err != nil {
|
||||
rec.AddAttrs(slog.String("error_verbose", fmt.Sprintf("%+v", err)))
|
||||
if x, ok := err.(errbase.StackTraceProvider); ok {
|
||||
rec.AddAttrs(slog.Any("stack_trace", traceLines(x.StackTrace())))
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return next(ctx, rec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traceLines(frames errbase.StackTrace) []string {
|
||||
traceLines := make([]string, 0, len(frames))
|
||||
|
||||
// Iterate in reverse to skip uninteresting, consecutive runtime frames at
|
||||
// the bottom of the trace.
|
||||
skipping := true
|
||||
for i := len(frames) - 1; i >= 0; i-- {
|
||||
// Adapted from errors.Frame.MarshalText(), but avoiding repeated
|
||||
// calls to FuncForPC and FileLine.
|
||||
pc := uintptr(frames[i]) - 1
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
traceLines = append(traceLines, "unknown")
|
||||
skipping = false
|
||||
continue
|
||||
}
|
||||
|
||||
name := fn.Name()
|
||||
if skipping && strings.HasPrefix(name, "runtime.") {
|
||||
continue
|
||||
} else {
|
||||
skipping = false
|
||||
}
|
||||
|
||||
filename, lineNr := fn.FileLine(pc)
|
||||
traceLines = append(traceLines, fmt.Sprintf("%s %s:%d", name, filename, lineNr))
|
||||
}
|
||||
|
||||
return traceLines[:len(traceLines):len(traceLines)]
|
||||
}
|
||||
40
pkg/logger/gcp.go
Normal file
40
pkg/logger/gcp.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package logger
|
||||
|
||||
import "log/slog"
|
||||
|
||||
// GCPAttrReplacer replaces the default attribute keys with the GCP logging attribute keys.
|
||||
func GCPAttrReplacer(groups []string, attr slog.Attr) slog.Attr {
|
||||
switch attr.Key {
|
||||
case MessageKey:
|
||||
attr.Key = "message"
|
||||
case SourceKey:
|
||||
attr.Key = "logging.googleapis.com/sourceLocation"
|
||||
case LevelKey:
|
||||
attr.Key = "severity"
|
||||
lvl, ok := attr.Value.Any().(slog.Level)
|
||||
if ok {
|
||||
attr.Value = slog.StringValue(gcpSeverityMapping(lvl))
|
||||
}
|
||||
}
|
||||
return attr
|
||||
}
|
||||
|
||||
// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity
|
||||
func gcpSeverityMapping(lvl slog.Level) string {
|
||||
switch {
|
||||
case lvl < slog.LevelInfo:
|
||||
return "DEBUG"
|
||||
case lvl < slog.LevelWarn:
|
||||
return "INFO"
|
||||
case lvl < slog.LevelError:
|
||||
return "WARNING"
|
||||
case lvl < LevelCritical:
|
||||
return "ERROR"
|
||||
case lvl < LevelPanic:
|
||||
return "CRITICAL"
|
||||
case lvl < LevelFatal:
|
||||
return "ALERT"
|
||||
default:
|
||||
return "EMERGENCY"
|
||||
}
|
||||
}
|
||||
37
pkg/logger/level.go
Normal file
37
pkg/logger/level.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
const (
|
||||
LevelCritical = slog.Level(12)
|
||||
LevelPanic = slog.Level(14)
|
||||
LevelFatal = slog.Level(16)
|
||||
)
|
||||
|
||||
func levelAttrReplacer(groups []string, attr slog.Attr) slog.Attr {
|
||||
if len(groups) == 0 && attr.Key == "level" {
|
||||
str := func(base string, val slog.Level) string {
|
||||
if val == 0 {
|
||||
return base
|
||||
}
|
||||
return fmt.Sprintf("%s%+d", base, val)
|
||||
}
|
||||
|
||||
if l, ok := attr.Value.Any().(slog.Level); ok {
|
||||
switch {
|
||||
case l < LevelCritical:
|
||||
return attr
|
||||
case l < LevelPanic:
|
||||
return slog.Attr{Key: attr.Key, Value: slog.StringValue(str("CRITICAL", l-LevelCritical))}
|
||||
case l < LevelFatal:
|
||||
return slog.Attr{Key: attr.Key, Value: slog.StringValue(str("PANIC", l-LevelPanic))}
|
||||
default:
|
||||
return slog.Attr{Key: attr.Key, Value: slog.StringValue(str("FATAL", l-LevelFatal))}
|
||||
}
|
||||
}
|
||||
}
|
||||
return attr
|
||||
}
|
||||
155
pkg/logger/logger.go
Normal file
155
pkg/logger/logger.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// nolint: sloglint
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultLevel is the default minimum reporting level for the logger
|
||||
DefaultLevel = slog.LevelDebug
|
||||
|
||||
// logLevel set `log` output level to `DEBUG`.
|
||||
// `log` is allowed for debugging purposes only.
|
||||
//
|
||||
// NOTE: Please use `slog` for logging instead of `log`, and
|
||||
// do not use `log` for production code.
|
||||
logLevel = slog.LevelDebug
|
||||
)
|
||||
|
||||
var (
|
||||
// minimum reporting level for the logger
|
||||
lvl = new(slog.LevelVar)
|
||||
|
||||
// top-level logger
|
||||
logger *slog.Logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: lvl,
|
||||
ReplaceAttr: levelAttrReplacer,
|
||||
}))
|
||||
)
|
||||
|
||||
// Set default slog logger
|
||||
func init() {
|
||||
lvl.Set(DefaultLevel)
|
||||
slog.SetDefault(logger)
|
||||
}
|
||||
|
||||
// Set `log` output level
|
||||
func init() {
|
||||
slog.SetLogLoggerLevel(logLevel)
|
||||
}
|
||||
|
||||
// SetLevel sets the minimum reporting level for the logger
|
||||
func SetLevel(level slog.Level) (old slog.Level) {
|
||||
old = lvl.Level()
|
||||
lvl.Set(level)
|
||||
return old
|
||||
}
|
||||
|
||||
// Debug calls [Logger.Debug] on the default logger.
|
||||
func With(args ...any) *slog.Logger {
|
||||
return logger.With(args...)
|
||||
}
|
||||
|
||||
// Debug calls [Logger.Debug] on the default logger.
|
||||
func Debug(msg string, args ...any) {
|
||||
logger.Debug(msg, args...)
|
||||
}
|
||||
|
||||
// Info calls [Logger.Info] on the default logger.
|
||||
func Info(msg string, args ...any) {
|
||||
logger.Info(msg, args...)
|
||||
}
|
||||
|
||||
// Warn calls [Logger.Warn] on the default logger.
|
||||
func Warn(msg string, args ...any) {
|
||||
logger.Warn(msg, args...)
|
||||
}
|
||||
|
||||
// Error calls [Logger.Error] on the default logger.
|
||||
// TODO: support stack trace for error
|
||||
func Error(msg string, err error, args ...any) {
|
||||
logger.Error(msg, append(args, AttrError(err))...)
|
||||
}
|
||||
|
||||
// Panic calls [Logger.Log] with PANIC level on the default logger and then panic.
|
||||
func Panic(msg string, args ...any) {
|
||||
logger.Log(context.Background(), LevelPanic, msg, args...)
|
||||
panic(msg)
|
||||
}
|
||||
|
||||
// Log calls [Logger.Log] on the default logger.
|
||||
func Log(level slog.Level, msg string, args ...any) {
|
||||
logger.Log(context.Background(), level, msg, args...)
|
||||
}
|
||||
|
||||
// LogAttrs calls [Logger.LogAttrs] on the default logger.
|
||||
func LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) {
|
||||
logger.LogAttrs(ctx, level, msg, attrs...)
|
||||
}
|
||||
|
||||
// Config is the logger configuration.
|
||||
type Config struct {
|
||||
// Env is the logger environment.
|
||||
// - PRODUCTION, PROD: use JSON format, log level: INFO
|
||||
// - Default: use Text format, log level: DEBUG
|
||||
Env string `env:"ENV,expand" envDefault:"${ENV}"`
|
||||
|
||||
Platform string `env:"PLATFORM" envDefault:"none"`
|
||||
}
|
||||
|
||||
// Init initializes global logger and slog logger with given configuration.
|
||||
func Init(cfg Config) error {
|
||||
replacers := []func([]string, slog.Attr) slog.Attr{}
|
||||
|
||||
// Platform specific attr replacer
|
||||
switch strings.ToLower(cfg.Platform) {
|
||||
case "gcp":
|
||||
replacers = append(replacers, GCPAttrReplacer)
|
||||
}
|
||||
|
||||
// Default attr replacer
|
||||
replacers = append(replacers,
|
||||
levelAttrReplacer,
|
||||
)
|
||||
|
||||
var (
|
||||
handler slog.Handler
|
||||
level = new(slog.LevelVar)
|
||||
options = &slog.HandlerOptions{
|
||||
AddSource: true,
|
||||
Level: level,
|
||||
ReplaceAttr: attrReplacerChain(replacers...),
|
||||
}
|
||||
)
|
||||
|
||||
switch strings.ToLower(cfg.Env) {
|
||||
case "production", "prod":
|
||||
level.Set(slog.LevelInfo)
|
||||
handler = slog.NewJSONHandler(os.Stdout, options)
|
||||
default:
|
||||
level.Set(DefaultLevel)
|
||||
handler = slog.NewTextHandler(os.Stdout, options)
|
||||
}
|
||||
|
||||
logger = slog.New(newChainHandlers(handler, middlewareError()))
|
||||
|
||||
lvl = level
|
||||
slog.SetDefault(logger)
|
||||
|
||||
logger.Info("logger initialized", slog.String("environment", cfg.Env))
|
||||
return nil
|
||||
}
|
||||
|
||||
// attrReplacerChain returns a function that applies a chain of replacers to an attribute.
|
||||
func attrReplacerChain(replacers ...func([]string, slog.Attr) slog.Attr) func([]string, slog.Attr) slog.Attr {
|
||||
return func(groups []string, attr slog.Attr) slog.Attr {
|
||||
for _, replacer := range replacers {
|
||||
attr = replacer(groups, attr)
|
||||
}
|
||||
return attr
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user