Skip to content

Commit

Permalink
feat(binary-codec): add quality and ledger data codecs
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillemGarciaDev committed Nov 11, 2024
1 parent 7aaca42 commit 5a51ba2
Show file tree
Hide file tree
Showing 4 changed files with 443 additions and 0 deletions.
102 changes: 102 additions & 0 deletions binary-codec/ledger_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package binarycodec

import (
"encoding/binary"
"encoding/hex"
"strconv"
"strings"

"github.com/Peersyst/xrpl-go/binary-codec/definitions"
"github.com/Peersyst/xrpl-go/binary-codec/serdes"
)

// LedgerData represents the data of a ledger.
type LedgerData struct {
LedgerIndex uint32
TotalCoins string
ParentHash string
TransactionHash string
AccountHash string
ParentCloseTime uint32
CloseTime uint32
CloseTimeResolution uint8
CloseFlags uint8
}

// DecodeLedgerData decodes a hex string in the canonical binary format into a LedgerData object.
// The hex string should represent a ledger data object.
func DecodeLedgerData(data string) (LedgerData, error) {
decoded, err := hex.DecodeString(data)
if err != nil {
return LedgerData{}, err
}

parser := serdes.NewBinaryParser(decoded, definitions.Get())

var ledgerData LedgerData

ledgerIndex, err := parser.ReadBytes(4)
if err != nil {
return LedgerData{}, err
}

ledgerData.LedgerIndex = binary.BigEndian.Uint32(ledgerIndex)

totalCoins, err := parser.ReadBytes(8)
if err != nil {
return LedgerData{}, err
}

ledgerData.TotalCoins = strconv.FormatUint(binary.BigEndian.Uint64(totalCoins), 10)

parentHash, err := parser.ReadBytes(32)
if err != nil {
return LedgerData{}, err
}

ledgerData.ParentHash = strings.ToUpper(hex.EncodeToString(parentHash))

transactionHash, err := parser.ReadBytes(32)
if err != nil {
return LedgerData{}, err
}

ledgerData.TransactionHash = strings.ToUpper(hex.EncodeToString(transactionHash))

accountHash, err := parser.ReadBytes(32)
if err != nil {
return LedgerData{}, err
}

ledgerData.AccountHash = strings.ToUpper(hex.EncodeToString(accountHash))

parentCloseTime, err := parser.ReadBytes(4)
if err != nil {
return LedgerData{}, err
}

ledgerData.ParentCloseTime = binary.BigEndian.Uint32(parentCloseTime)

closeTime, err := parser.ReadBytes(4)
if err != nil {
return LedgerData{}, err
}

ledgerData.CloseTime = binary.BigEndian.Uint32(closeTime)

closeTimeResolution, err := parser.ReadByte()
if err != nil {
return LedgerData{}, err
}

ledgerData.CloseTimeResolution = uint8(closeTimeResolution)

closeFlags, err := parser.ReadByte()
if err != nil {
return LedgerData{}, err
}

ledgerData.CloseFlags = uint8(closeFlags)

return ledgerData, nil
}
97 changes: 97 additions & 0 deletions binary-codec/ledger_data_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package binarycodec

import (
"encoding/hex"
"testing"

"github.com/Peersyst/xrpl-go/binary-codec/serdes"
"github.com/stretchr/testify/require"
)

func TestDecodeLedgerData(t *testing.T) {
testcases := []struct {
name string
input string
expected LedgerData
expectedErr error
}{
{
name: "fail - invalid hex string",
input: "invalid",
expectedErr: hex.InvalidByteError(0x69),
},
{
name: "fail - invalid ledger index",
input: "01E914",
expectedErr: serdes.ErrParserOutOfBound,
},
{
name: "fail - invalid total coins",
input: "01E91435016340767BF1",
expectedErr: serdes.ErrParserOutOfBound,
},
{
name: "fail - invalid parent hash",
input: "01E91435016340767BF1C4A3EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DE",
expectedErr: serdes.ErrParserOutOfBound,
},
{
name: "fail - invalid transaction hash",
input: "01E91435016340767BF1C4A3EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DECE006DA18150CB18F6DD33",
expectedErr: serdes.ErrParserOutOfBound,
},
{
name: "fail - invalid account hash",
input: "01E91435016340767BF1C4A3EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DECE006DA18150CB18F6DD33F6F0990754C962A7CCE62F332FF9C13939B03B864117F0BDA86B6E9B4F873B5C3E520634D343EF5D",
expectedErr: serdes.ErrParserOutOfBound,
},
{
name: "fail - invalid parent close time",
input: "01E91435016340767BF1C4A3EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DECE006DA18150CB18F6DD33F6F0990754C962A7CCE62F332FF9C13939B03B864117F0BDA86B6E9B4F873B5C3E520634D343EF5D9D9A4246643D64DAD278BA95DC0EAC6EB5350CF970D521276C",
expectedErr: serdes.ErrParserOutOfBound,
},
{
name: "fail - invalid close time",
input: "01E91435016340767BF1C4A3EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DECE006DA18150CB18F6DD33F6F0990754C962A7CCE62F332FF9C13939B03B864117F0BDA86B6E9B4F873B5C3E520634D343EF5D9D9A4246643D64DAD278BA95DC0EAC6EB5350CF970D521276CDE21276C",
expectedErr: serdes.ErrParserOutOfBound,
},
{
name: "fail - invalid close time resolution",
input: "01E91435016340767BF1C4A3EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DECE006DA18150CB18F6DD33F6F0990754C962A7CCE62F332FF9C13939B03B864117F0BDA86B6E9B4F873B5C3E520634D343EF5D9D9A4246643D64DAD278BA95DC0EAC6EB5350CF970D521276CDE21276CE6",
expectedErr: serdes.ErrParserOutOfBound,
},
{
name: "fail - invalid close flags",
input: "01E91435016340767BF1C4A3EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DECE006DA18150CB18F6DD33F6F0990754C962A7CCE62F332FF9C13939B03B864117F0BDA86B6E9B4F873B5C3E520634D343EF5D9D9A4246643D64DAD278BA95DC0EAC6EB5350CF970D521276CDE21276CE60A",
expectedErr: serdes.ErrParserOutOfBound,
},
{
name: "pass - valid encoded ledger data",
input: "01E91435016340767BF1C4A3EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DECE006DA18150CB18F6DD33F6F0990754C962A7CCE62F332FF9C13939B03B864117F0BDA86B6E9B4F873B5C3E520634D343EF5D9D9A4246643D64DAD278BA95DC0EAC6EB5350CF970D521276CDE21276CE60A00",
expected: LedgerData{
AccountHash: "3B5C3E520634D343EF5D9D9A4246643D64DAD278BA95DC0EAC6EB5350CF970D5",
CloseFlags: 0,
CloseTime: 556231910,
CloseTimeResolution: 10,
LedgerIndex: 32052277,
ParentCloseTime: 556231902,
ParentHash: "EACEB081770D8ADE216C85445DD6FB002C6B5A2930F2DECE006DA18150CB18F6",
TotalCoins: "99994494362043555",
TransactionHash: "DD33F6F0990754C962A7CCE62F332FF9C13939B03B864117F0BDA86B6E9B4F87",
},
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
ledgerData, err := DecodeLedgerData(tc.input)
if tc.expectedErr != nil {
require.Error(t, err)
require.Equal(t, tc.expectedErr, err)
} else {
require.NoError(t, err)
require.Equal(t, tc.expected, ledgerData)
}
})
}
}
117 changes: 117 additions & 0 deletions binary-codec/quality.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package binarycodec

import (
"encoding/binary"
"encoding/hex"
"errors"
"strconv"
"strings"

bigdecimal "github.com/Peersyst/xrpl-go/pkg/big-decimal"
)

const (
// ZeroQualityHex is the hex representation of the zero quality.
ZeroQualityHex = 0x5500000000000000
// MaxIOUPrecision is the maximum precision for an IOU.
MaxIOUPrecision = 16
// MinIOUExponent is the minimum exponent for an IOU.
MinIOUExponent = -96
// MaxIOUExponent is the maximum exponent for an IOU.
MaxIOUExponent = 80
)

var (
ErrInvalidQuality = errors.New("invalid quality")
)

// EncodeQuality encodes a quality amount to a hex string.
func EncodeQuality(quality string) (string, error) {
if len(quality) == 0 {
return "", ErrInvalidQuality
}
if len(strings.Trim(strings.Trim(quality, "0"), ".")) == 0 {
zeroAmount := make([]byte, 8)
binary.BigEndian.PutUint64(zeroAmount, uint64(ZeroQualityHex))
return hex.EncodeToString(zeroAmount), nil
}

bigDecimal, err := bigdecimal.NewBigDecimal(quality)
if err != nil {
return "", err
}

if !isValidQuality(*bigDecimal) {
return "", ErrInvalidQuality
}

if bigDecimal.UnscaledValue == "" {
zeroAmount := make([]byte, 8)
binary.BigEndian.PutUint64(zeroAmount, uint64(ZeroQualityHex))
// if the value is zero, then return the zero currency amount hex
return hex.EncodeToString(zeroAmount), nil
}

// convert the unscaled value to an unsigned integer
mantissa, err := strconv.ParseUint(bigDecimal.UnscaledValue, 10, 64)

if err != nil {
return "", err
}

// get the scale
exp := bigDecimal.Scale

serialized := make([]byte, 8)
binary.BigEndian.PutUint64(serialized, mantissa)
serialized[0] += byte(exp) + 100
return strings.ToUpper(hex.EncodeToString(serialized)), nil
}

// Decode a quality amount from a hex string to a string.
func DecodeQuality(quality string) (string, error) {
if quality == "" {
return "", ErrInvalidQuality
}

decoded, err := hex.DecodeString(quality)
if err != nil {
return "", err
}

bytes := decoded[len(decoded)-8:]
exp := int(bytes[0]) - 100
mantissaBytes := append([]byte{0}, bytes[1:]...)
mantissa := binary.BigEndian.Uint64(mantissaBytes)

// Convert mantissa to string
mantissaStr := strconv.FormatUint(mantissa, 10)

// Add decimal point based on exponent
if exp < 0 {
// Need to add leading zeros
if len(mantissaStr) <= -exp {
zeros := strings.Repeat("0", -exp-len(mantissaStr)+1)
mantissaStr = "0." + zeros + mantissaStr
} else {
// Insert decimal point from right to left
insertPos := len(mantissaStr) + exp
mantissaStr = mantissaStr[:insertPos] + "." + mantissaStr[insertPos:]
}
} else if exp > 0 {
// Add trailing zeros
mantissaStr += strings.Repeat("0", exp)
}

// Trim trailing zeros after decimal point
if strings.Contains(mantissaStr, ".") {
mantissaStr = strings.TrimRight(mantissaStr, "0")
mantissaStr = strings.TrimRight(mantissaStr, ".")
}

return mantissaStr, nil
}

func isValidQuality(quality bigdecimal.BigDecimal) bool {
return quality.Precision <= MaxIOUPrecision && quality.Scale >= MinIOUExponent && quality.Scale <= MaxIOUExponent
}
Loading

0 comments on commit 5a51ba2

Please sign in to comment.