diff --git a/ledger/alonzo/alonzo.go b/ledger/alonzo/alonzo.go index dd4c3693..e1ce1400 100644 --- a/ledger/alonzo/alonzo.go +++ b/ledger/alonzo/alonzo.go @@ -447,6 +447,21 @@ func (o AlonzoTransactionOutput) Utxorpc() (*utxorpc.TxOutput, error) { nil } +func (o AlonzoTransactionOutput) String() string { + assets := "" + if o.OutputAmount.Assets != nil { + if as := o.OutputAmount.Assets.String(); as != "[]" { + assets = " assets=" + as + } + } + return fmt.Sprintf( + "(AlonzoTransactionOutput address=%s amount=%d%s)", + o.OutputAddress.String(), + o.OutputAmount.Amount, + assets, + ) +} + type AlonzoRedeemer struct { cbor.StructAsArray Tag common.RedeemerTag diff --git a/ledger/alonzo/alonzo_test.go b/ledger/alonzo/alonzo_test.go index ef4e04f7..a3d6d8b7 100644 --- a/ledger/alonzo/alonzo_test.go +++ b/ledger/alonzo/alonzo_test.go @@ -15,6 +15,7 @@ package alonzo import ( + "encoding/hex" "math/big" "reflect" "testing" @@ -277,3 +278,24 @@ func TestAlonzoRedeemersIter(t *testing.T) { iterIdx++ } } + +func TestAlonzoTransactionOutputString(t *testing.T) { + addrStr := "addr1qytna5k2fq9ler0fuk45j7zfwv7t2zwhp777nvdjqqfr5tz8ztpwnk8zq5ngetcz5k5mckgkajnygtsra9aej2h3ek5seupmvd" + addr, _ := common.NewAddress(addrStr) + ma := common.NewMultiAsset[common.MultiAssetTypeOutput]( + map[common.Blake2b224]map[cbor.ByteString]uint64{ + common.NewBlake2b224(make([]byte, 28)): {cbor.NewByteString([]byte("t")): 2}, + }, + ) + out := AlonzoTransactionOutput{ + OutputAddress: addr, + OutputAmount: mary.MaryTransactionOutputValue{Amount: 456, Assets: &ma}, + } + s := out.String() + policyStr := common.NewBlake2b224(make([]byte, 28)).String() + assetsStr := "[" + policyStr + "." + hex.EncodeToString([]byte("t")) + "=2]" + expected := "(AlonzoTransactionOutput address=" + addrStr + " amount=456 assets=" + assetsStr + ")" + if s != expected { + t.Fatalf("unexpected string: %s", s) + } +} diff --git a/ledger/babbage/babbage.go b/ledger/babbage/babbage.go index 4723b359..8fc155cd 100644 --- a/ledger/babbage/babbage.go +++ b/ledger/babbage/babbage.go @@ -649,6 +649,21 @@ func (o BabbageTransactionOutput) Utxorpc() (*utxorpc.TxOutput, error) { nil } +func (o BabbageTransactionOutput) String() string { + assets := "" + if o.OutputAmount.Assets != nil { + if as := o.OutputAmount.Assets.String(); as != "[]" { + assets = " assets=" + as + } + } + return fmt.Sprintf( + "(BabbageTransactionOutput address=%s amount=%d%s)", + o.OutputAddress.String(), + o.OutputAmount.Amount, + assets, + ) +} + type BabbageTransactionWitnessSet struct { cbor.DecodeStoreCbor VkeyWitnesses []common.VkeyWitness `cbor:"0,keyasint,omitempty"` diff --git a/ledger/babbage/babbage_test.go b/ledger/babbage/babbage_test.go index 74a99433..c6bd7c63 100644 --- a/ledger/babbage/babbage_test.go +++ b/ledger/babbage/babbage_test.go @@ -2970,3 +2970,22 @@ func TestBabbageTransactionOutputToPlutusDataCoinAssets(t *testing.T) { ) } } + +func TestBabbageTransactionOutputString(t *testing.T) { + addrStr := "addr1qytna5k2fq9ler0fuk45j7zfwv7t2zwhp777nvdjqqfr5tz8ztpwnk8zq5ngetcz5k5mckgkajnygtsra9aej2h3ek5seupmvd" + addr, _ := common.NewAddress(addrStr) + ma := common.NewMultiAsset[common.MultiAssetTypeOutput]( + map[common.Blake2b224]map[cbor.ByteString]uint64{ + common.NewBlake2b224(make([]byte, 28)): {cbor.NewByteString([]byte("x")): 2}, + }, + ) + out := BabbageTransactionOutput{ + OutputAddress: addr, + OutputAmount: mary.MaryTransactionOutputValue{Amount: 456, Assets: &ma}, + } + s := out.String() + expected := "(BabbageTransactionOutput address=" + addrStr + " amount=456 assets=" + ma.String() + ")" + if s != expected { + t.Fatalf("unexpected string: %s", s) + } +} diff --git a/ledger/byron/byron.go b/ledger/byron/byron.go index 25939879..3c397c19 100644 --- a/ledger/byron/byron.go +++ b/ledger/byron/byron.go @@ -462,6 +462,14 @@ func (o ByronTransactionOutput) Utxorpc() (*utxorpc.TxOutput, error) { nil } +func (o ByronTransactionOutput) String() string { + return fmt.Sprintf( + "(ByronTransactionOutput address=%s amount=%d)", + o.OutputAddress.String(), + o.OutputAmount, + ) +} + type ByronBlockVersion struct { cbor.StructAsArray Major uint16 diff --git a/ledger/byron/byron_test.go b/ledger/byron/byron_test.go index b2c0c4d9..e577f308 100644 --- a/ledger/byron/byron_test.go +++ b/ledger/byron/byron_test.go @@ -104,3 +104,24 @@ func TestByronTransaction_Utxorpc_Empty(t *testing.T) { t.Errorf("Expected fee = 0, got %d", result.Fee) } } + +func TestByronTransactionOutputString(t *testing.T) { + addr, err := common.NewByronAddressFromParts( + 0, + make([]byte, common.AddressHashSize), + common.ByronAddressAttributes{}, + ) + if err != nil { + t.Fatalf("address: %v", err) + } + addrStr := addr.String() + out := byron.ByronTransactionOutput{ + OutputAddress: addr, + OutputAmount: 456, + } + s := out.String() + expected := "(ByronTransactionOutput address=" + addrStr + " amount=456)" + if s != expected { + t.Fatalf("unexpected string: %s", s) + } +} diff --git a/ledger/common/common.go b/ledger/common/common.go index 9d156176..716d6dcb 100644 --- a/ledger/common/common.go +++ b/ledger/common/common.go @@ -22,6 +22,7 @@ import ( "maps" "math/big" "slices" + "strings" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/plutigo/data" @@ -325,6 +326,44 @@ func (m *MultiAsset[T]) normalize() map[Blake2b224]map[cbor.ByteString]T { return ret } +// String returns a stable, human-friendly representation of the MultiAsset. +// Output format: [.=, ...] sorted by policyId, then asset name +func (m *MultiAsset[T]) String() string { + if m == nil { + return "[]" + } + norm := m.normalize() + if len(norm) == 0 { + return "[]" + } + + policies := slices.Collect(maps.Keys(norm)) + slices.SortFunc(policies, func(a, b Blake2b224) int { return bytes.Compare(a.Bytes(), b.Bytes()) }) + + var b strings.Builder + b.WriteByte('[') + first := true + for _, pid := range policies { + assets := norm[pid] + names := slices.Collect(maps.Keys(assets)) + slices.SortFunc(names, func(a, b cbor.ByteString) int { return bytes.Compare(a.Bytes(), b.Bytes()) }) + + for _, name := range names { + if !first { + b.WriteString(", ") + } + first = false + b.WriteString(pid.String()) + b.WriteByte('.') + b.WriteString(hex.EncodeToString(name.Bytes())) + b.WriteByte('=') + fmt.Fprintf(&b, "%d", assets[name]) + } + } + b.WriteByte(']') + return b.String() +} + type AssetFingerprint struct { policyId []byte assetName []byte diff --git a/ledger/common/tx.go b/ledger/common/tx.go index 570db12d..93d509d7 100644 --- a/ledger/common/tx.go +++ b/ledger/common/tx.go @@ -77,6 +77,7 @@ type TransactionOutput interface { Utxorpc() (*utxorpc.TxOutput, error) ScriptRef() Script ToPlutusData() data.PlutusData + String() string } type TransactionWitnessSet interface { diff --git a/ledger/mary/errors.go b/ledger/mary/errors.go index 607a35de..8e12b81e 100644 --- a/ledger/mary/errors.go +++ b/ledger/mary/errors.go @@ -15,7 +15,6 @@ package mary import ( - "fmt" "strings" "github.com/blinklabs-io/gouroboros/ledger/common" @@ -28,7 +27,7 @@ type OutputTooBigUtxoError struct { func (e OutputTooBigUtxoError) Error() string { tmpOutputs := make([]string, len(e.Outputs)) for idx, tmpOutput := range e.Outputs { - tmpOutputs[idx] = fmt.Sprintf("%#v", tmpOutput) + tmpOutputs[idx] = tmpOutput.String() } return "output value too large: " + strings.Join(tmpOutputs, ", ") } diff --git a/ledger/mary/mary.go b/ledger/mary/mary.go index 6562d3be..a7ff2cb9 100644 --- a/ledger/mary/mary.go +++ b/ledger/mary/mary.go @@ -490,6 +490,21 @@ func (o MaryTransactionOutput) Utxorpc() (*utxorpc.TxOutput, error) { err } +func (o MaryTransactionOutput) String() string { + assets := "" + if o.OutputAmount.Assets != nil { + if as := o.OutputAmount.Assets.String(); as != "[]" { + assets = " assets=" + as + } + } + return fmt.Sprintf( + "(MaryTransactionOutput address=%s amount=%d%s)", + o.OutputAddress.String(), + o.OutputAmount.Amount, + assets, + ) +} + type MaryTransactionOutputValue struct { cbor.StructAsArray Amount uint64 diff --git a/ledger/mary/mary_test.go b/ledger/mary/mary_test.go index 6bbf4f3f..ef4ad79c 100644 --- a/ledger/mary/mary_test.go +++ b/ledger/mary/mary_test.go @@ -16,6 +16,7 @@ package mary import ( "encoding/hex" + "fmt" "reflect" "testing" @@ -115,3 +116,36 @@ func TestMaryTransactionOutputValueEncodeDecode(t *testing.T) { } } } + +func TestMaryTransactionOutputString(t *testing.T) { + addrStr := "addr1qytna5k2fq9ler0fuk45j7zfwv7t2zwhp777nvdjqqfr5tz8ztpwnk8zq5ngetcz5k5mckgkajnygtsra9aej2h3ek5seupmvd" + addr, _ := common.NewAddress(addrStr) + ma := common.NewMultiAsset[common.MultiAssetTypeOutput]( + map[common.Blake2b224]map[cbor.ByteString]uint64{ + common.NewBlake2b224(make([]byte, 28)): {cbor.NewByteString([]byte("token")): 2}, + }, + ) + out := MaryTransactionOutput{ + OutputAddress: addr, + OutputAmount: MaryTransactionOutputValue{Amount: 456, Assets: &ma}, + } + s := out.String() + expected := fmt.Sprintf("(MaryTransactionOutput address=%s amount=456 assets=%s)", addrStr, ma.String()) + if s != expected { + t.Fatalf("unexpected string: %s", s) + } +} + +func TestMaryOutputTooBigErrorFormatting(t *testing.T) { + addrStr := "addr1qytna5k2fq9ler0fuk45j7zfwv7t2zwhp777nvdjqqfr5tz8ztpwnk8zq5ngetcz5k5mckgkajnygtsra9aej2h3ek5seupmvd" + addr, _ := common.NewAddress(addrStr) + out := &MaryTransactionOutput{ + OutputAddress: addr, + OutputAmount: MaryTransactionOutputValue{Amount: 456}, + } + errStr := OutputTooBigUtxoError{Outputs: []common.TransactionOutput{out}}.Error() + expected := fmt.Sprintf("output value too large: (MaryTransactionOutput address=%s amount=456)", addrStr) + if errStr != expected { + t.Fatalf("unexpected error: %s", errStr) + } +} diff --git a/ledger/shelley/errors.go b/ledger/shelley/errors.go index f1343cb6..3da5dc0b 100644 --- a/ledger/shelley/errors.go +++ b/ledger/shelley/errors.go @@ -111,7 +111,7 @@ type OutputTooSmallUtxoError struct { func (e OutputTooSmallUtxoError) Error() string { tmpOutputs := make([]string, len(e.Outputs)) for idx, tmpOutput := range e.Outputs { - tmpOutputs[idx] = fmt.Sprintf("%#v", tmpOutput) + tmpOutputs[idx] = tmpOutput.String() } return "output too small: " + strings.Join(tmpOutputs, ", ") } @@ -123,7 +123,7 @@ type OutputBootAddrAttrsTooBigError struct { func (e OutputBootAddrAttrsTooBigError) Error() string { tmpOutputs := make([]string, len(e.Outputs)) for idx, tmpOutput := range e.Outputs { - tmpOutputs[idx] = fmt.Sprintf("%#v", tmpOutput) + tmpOutputs[idx] = tmpOutput.String() } return "output bootstrap address attributes too big: " + strings.Join( tmpOutputs, diff --git a/ledger/shelley/shelley.go b/ledger/shelley/shelley.go index 7920cfdf..046f0556 100644 --- a/ledger/shelley/shelley.go +++ b/ledger/shelley/shelley.go @@ -444,6 +444,14 @@ func (o ShelleyTransactionOutput) Utxorpc() (*utxorpc.TxOutput, error) { }, nil } +func (o ShelleyTransactionOutput) String() string { + return fmt.Sprintf( + "(ShelleyTransactionOutput address=%s amount=%d)", + o.OutputAddress.String(), + o.OutputAmount, + ) +} + type ShelleyTransactionWitnessSet struct { cbor.DecodeStoreCbor VkeyWitnesses []common.VkeyWitness `cbor:"0,keyasint,omitempty"` diff --git a/ledger/shelley/shelley_test.go b/ledger/shelley/shelley_test.go new file mode 100644 index 00000000..dbc2ba67 --- /dev/null +++ b/ledger/shelley/shelley_test.go @@ -0,0 +1,37 @@ +package shelley_test + +import ( + "fmt" + "testing" + + "github.com/blinklabs-io/gouroboros/ledger/common" + "github.com/blinklabs-io/gouroboros/ledger/shelley" +) + +func TestShelleyTransactionOutputString(t *testing.T) { + addrStr := "addr1qytna5k2fq9ler0fuk45j7zfwv7t2zwhp777nvdjqqfr5tz8ztpwnk8zq5ngetcz5k5mckgkajnygtsra9aej2h3ek5seupmvd" + addr, _ := common.NewAddress(addrStr) + out := shelley.ShelleyTransactionOutput{ + OutputAddress: addr, + OutputAmount: 456, + } + s := out.String() + expected := fmt.Sprintf("(ShelleyTransactionOutput address=%s amount=456)", addrStr) + if s != expected { + t.Fatalf("unexpected string: %s", s) + } +} + +func TestShelleyOutputTooSmallErrorFormatting(t *testing.T) { + addrStr := "addr1qytna5k2fq9ler0fuk45j7zfwv7t2zwhp777nvdjqqfr5tz8ztpwnk8zq5ngetcz5k5mckgkajnygtsra9aej2h3ek5seupmvd" + addr, _ := common.NewAddress(addrStr) + out := &shelley.ShelleyTransactionOutput{ + OutputAddress: addr, + OutputAmount: 456, + } + errStr := shelley.OutputTooSmallUtxoError{Outputs: []common.TransactionOutput{out}}.Error() + expected := fmt.Sprintf("output too small: (ShelleyTransactionOutput address=%s amount=456)", addrStr) + if errStr != expected { + t.Fatalf("unexpected error: %s", errStr) + } +}