From 73202c560affc21a5e4c6551b6884ce41a9719bf Mon Sep 17 00:00:00 2001 From: Aurora Gaffney Date: Thu, 24 Jul 2025 09:42:26 -0400 Subject: [PATCH] feat: helper function to convert TransactionOutput(s) to PlutusData Fixes #1100 Signed-off-by: Aurora Gaffney --- ledger/alonzo/alonzo.go | 45 +++++++++ ledger/alonzo/alonzo_test.go | 172 +++++++++++++++++++++++++++++++++ ledger/babbage/babbage.go | 45 +++++++++ ledger/babbage/babbage_test.go | 150 ++++++++++++++++++++++++++++ ledger/byron/byron.go | 5 + ledger/common/tx.go | 1 + ledger/mary/mary.go | 6 ++ ledger/shelley/shelley.go | 5 + 8 files changed, 429 insertions(+) create mode 100644 ledger/alonzo/alonzo_test.go diff --git a/ledger/alonzo/alonzo.go b/ledger/alonzo/alonzo.go index f95562d7..dfbb4a8e 100644 --- a/ledger/alonzo/alonzo.go +++ b/ledger/alonzo/alonzo.go @@ -17,11 +17,13 @@ package alonzo import ( "encoding/json" "fmt" + "math/big" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger/common" "github.com/blinklabs-io/gouroboros/ledger/mary" "github.com/blinklabs-io/gouroboros/ledger/shelley" + "github.com/blinklabs-io/plutigo/pkg/data" utxorpc "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" ) @@ -332,6 +334,49 @@ func (o AlonzoTransactionOutput) MarshalJSON() ([]byte, error) { return json.Marshal(&tmpObj) } +func (o AlonzoTransactionOutput) ToPlutusData() data.PlutusData { + var valueData [][2]data.PlutusData + if o.OutputAmount.Amount > 0 { + valueData = append( + valueData, + [2]data.PlutusData{ + data.NewByteString(nil), + data.NewMap( + [][2]data.PlutusData{ + { + data.NewByteString(nil), + data.NewInteger(new(big.Int).SetUint64(o.OutputAmount.Amount)), + }, + }, + ), + }, + ) + } + if o.OutputAmount.Assets != nil { + assetData := o.OutputAmount.Assets.ToPlutusData() + assetDataMap, ok := assetData.(*data.Map) + if !ok { + return nil + } + valueData = append( + valueData, + assetDataMap.Pairs..., + ) + } + tmpData := data.NewConstr( + 0, + o.OutputAddress.ToPlutusData(), + data.NewMap(valueData), + // Empty datum option + // TODO: implement this + data.NewConstr(0), + // Empty script ref + // TODO: implement this + data.NewConstr(1), + ) + return tmpData +} + func (o AlonzoTransactionOutput) Address() common.Address { return o.OutputAddress } diff --git a/ledger/alonzo/alonzo_test.go b/ledger/alonzo/alonzo_test.go new file mode 100644 index 00000000..4fcddce7 --- /dev/null +++ b/ledger/alonzo/alonzo_test.go @@ -0,0 +1,172 @@ +// Copyright 2025 Blink Labs Software +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package alonzo + +import ( + "math/big" + "reflect" + "testing" + + "github.com/blinklabs-io/gouroboros/cbor" + "github.com/blinklabs-io/gouroboros/internal/test" + "github.com/blinklabs-io/gouroboros/ledger/common" + "github.com/blinklabs-io/gouroboros/ledger/mary" + "github.com/blinklabs-io/plutigo/pkg/data" +) + +func TestAlonzoTransactionOutputToPlutusDataCoinOnly(t *testing.T) { + testAddr := "addr_test1vqg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygxrcya6" + var testAmount uint64 = 123_456_789 + testTxOut := AlonzoTransactionOutput{ + OutputAddress: func() common.Address { foo, _ := common.NewAddress(testAddr); return foo }(), + OutputAmount: mary.MaryTransactionOutputValue{ + Amount: testAmount, + }, + } + expectedData := data.NewConstr( + 0, + // Address + data.NewConstr( + 0, + data.NewConstr( + 0, + data.NewConstr( + 0, + data.NewByteString( + []byte{ + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + }, + ), + ), + ), + data.NewConstr( + 1, + ), + ), + // Value + data.NewMap( + [][2]data.PlutusData{ + { + data.NewByteString(nil), + data.NewMap( + [][2]data.PlutusData{ + { + data.NewByteString(nil), + data.NewInteger(big.NewInt(int64(testAmount))), + }, + }, + ), + }, + }, + ), + // TODO: empty datum option + data.NewConstr(0), + // TODO: empty script ref + data.NewConstr(1), + ) + tmpData := testTxOut.ToPlutusData() + if !reflect.DeepEqual(tmpData, expectedData) { + t.Fatalf("did not get expected PlutusData\n got: %#v\n wanted: %#v", tmpData, expectedData) + } +} + +func TestAlonzoTransactionOutputToPlutusDataCoinAssets(t *testing.T) { + testAddr := "addr_test1vqg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygxrcya6" + var testAmount uint64 = 123_456_789 + testAssets := common.NewMultiAsset[common.MultiAssetTypeOutput]( + map[common.Blake2b224]map[cbor.ByteString]common.MultiAssetTypeOutput{ + common.NewBlake2b224(test.DecodeHexString("29a8fb8318718bd756124f0c144f56d4b4579dc5edf2dd42d669ac61")): { + cbor.NewByteString(test.DecodeHexString("6675726e697368613239686e")): 123456, + }, + common.NewBlake2b224(test.DecodeHexString("eaf8042c1d8203b1c585822f54ec32c4c1bb4d3914603e2cca20bbd5")): { + cbor.NewByteString(test.DecodeHexString("426f7764757261436f6e63657074733638")): 234567, + }, + }, + ) + testTxOut := AlonzoTransactionOutput{ + OutputAddress: func() common.Address { foo, _ := common.NewAddress(testAddr); return foo }(), + OutputAmount: mary.MaryTransactionOutputValue{ + Amount: testAmount, + Assets: &testAssets, + }, + } + expectedData := data.NewConstr( + 0, + // Address + data.NewConstr( + 0, + data.NewConstr( + 0, + data.NewConstr( + 0, + data.NewByteString( + []byte{ + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + }, + ), + ), + ), + data.NewConstr( + 1, + ), + ), + // Value + data.NewMap( + [][2]data.PlutusData{ + { + data.NewByteString(nil), + data.NewMap( + [][2]data.PlutusData{ + { + data.NewByteString(nil), + data.NewInteger(big.NewInt(int64(testAmount))), + }, + }, + ), + }, + { + data.NewByteString(test.DecodeHexString("29a8fb8318718bd756124f0c144f56d4b4579dc5edf2dd42d669ac61")), + data.NewMap( + [][2]data.PlutusData{ + { + data.NewByteString(test.DecodeHexString("6675726e697368613239686e")), + data.NewInteger(big.NewInt(123456)), + }, + }, + ), + }, + { + data.NewByteString(test.DecodeHexString("eaf8042c1d8203b1c585822f54ec32c4c1bb4d3914603e2cca20bbd5")), + data.NewMap( + [][2]data.PlutusData{ + { + data.NewByteString(test.DecodeHexString("426f7764757261436f6e63657074733638")), + data.NewInteger(big.NewInt(234567)), + }, + }, + ), + }, + }, + ), + // Empty datum option + data.NewConstr(0), + // Empty script ref + data.NewConstr(1), + ) + tmpData := testTxOut.ToPlutusData() + if !reflect.DeepEqual(tmpData, expectedData) { + t.Fatalf("did not get expected PlutusData\n got: %#v\n wanted: %#v", tmpData, expectedData) + } +} diff --git a/ledger/babbage/babbage.go b/ledger/babbage/babbage.go index 40d947de..0da4fe8f 100644 --- a/ledger/babbage/babbage.go +++ b/ledger/babbage/babbage.go @@ -18,12 +18,14 @@ import ( "encoding/json" "errors" "fmt" + "math/big" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger/alonzo" "github.com/blinklabs-io/gouroboros/ledger/common" "github.com/blinklabs-io/gouroboros/ledger/mary" "github.com/blinklabs-io/gouroboros/ledger/shelley" + "github.com/blinklabs-io/plutigo/pkg/data" utxorpc "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" ) @@ -487,6 +489,49 @@ func (o BabbageTransactionOutput) MarshalJSON() ([]byte, error) { return json.Marshal(&tmpObj) } +func (o BabbageTransactionOutput) ToPlutusData() data.PlutusData { + var valueData [][2]data.PlutusData + if o.OutputAmount.Amount > 0 { + valueData = append( + valueData, + [2]data.PlutusData{ + data.NewByteString(nil), + data.NewMap( + [][2]data.PlutusData{ + { + data.NewByteString(nil), + data.NewInteger(new(big.Int).SetUint64(o.OutputAmount.Amount)), + }, + }, + ), + }, + ) + } + if o.OutputAmount.Assets != nil { + assetData := o.OutputAmount.Assets.ToPlutusData() + assetDataMap, ok := assetData.(*data.Map) + if !ok { + return nil + } + valueData = append( + valueData, + assetDataMap.Pairs..., + ) + } + tmpData := data.NewConstr( + 0, + o.OutputAddress.ToPlutusData(), + data.NewMap(valueData), + // Empty datum option + // TODO: implement this + data.NewConstr(0), + // Empty script ref + // TODO: implement this + data.NewConstr(1), + ) + return tmpData +} + func (o BabbageTransactionOutput) Address() common.Address { return o.OutputAddress } diff --git a/ledger/babbage/babbage_test.go b/ledger/babbage/babbage_test.go index cd2b3cb6..c2dcfeae 100644 --- a/ledger/babbage/babbage_test.go +++ b/ledger/babbage/babbage_test.go @@ -15,10 +15,15 @@ package babbage import ( + "math/big" + "reflect" "testing" + "github.com/blinklabs-io/gouroboros/cbor" + "github.com/blinklabs-io/gouroboros/internal/test" "github.com/blinklabs-io/gouroboros/ledger/common" "github.com/blinklabs-io/gouroboros/ledger/mary" + "github.com/blinklabs-io/plutigo/pkg/data" "github.com/stretchr/testify/assert" ) @@ -2802,3 +2807,148 @@ func TestBabbageTransactionOutput_DatumHashReturnsNil(t *testing.T) { assert.Nil(t, datumHash) } + +func TestBabbageTransactionOutputToPlutusDataCoinOnly(t *testing.T) { + testAddr := "addr_test1vqg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygxrcya6" + var testAmount uint64 = 123_456_789 + testTxOut := BabbageTransactionOutput{ + OutputAddress: func() common.Address { foo, _ := common.NewAddress(testAddr); return foo }(), + OutputAmount: mary.MaryTransactionOutputValue{ + Amount: testAmount, + }, + } + expectedData := data.NewConstr( + 0, + // Address + data.NewConstr( + 0, + data.NewConstr( + 0, + data.NewConstr( + 0, + data.NewByteString( + []byte{ + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + }, + ), + ), + ), + data.NewConstr( + 1, + ), + ), + // Value + data.NewMap( + [][2]data.PlutusData{ + { + data.NewByteString(nil), + data.NewMap( + [][2]data.PlutusData{ + { + data.NewByteString(nil), + data.NewInteger(big.NewInt(int64(testAmount))), + }, + }, + ), + }, + }, + ), + // TODO: empty datum option + data.NewConstr(0), + // TODO: empty script ref + data.NewConstr(1), + ) + tmpData := testTxOut.ToPlutusData() + if !reflect.DeepEqual(tmpData, expectedData) { + t.Fatalf("did not get expected PlutusData\n got: %#v\n wanted: %#v", tmpData, expectedData) + } +} + +func TestBabbageTransactionOutputToPlutusDataCoinAssets(t *testing.T) { + testAddr := "addr_test1vqg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygxrcya6" + var testAmount uint64 = 123_456_789 + testAssets := common.NewMultiAsset[common.MultiAssetTypeOutput]( + map[common.Blake2b224]map[cbor.ByteString]common.MultiAssetTypeOutput{ + common.NewBlake2b224(test.DecodeHexString("29a8fb8318718bd756124f0c144f56d4b4579dc5edf2dd42d669ac61")): { + cbor.NewByteString(test.DecodeHexString("6675726e697368613239686e")): 123456, + }, + common.NewBlake2b224(test.DecodeHexString("eaf8042c1d8203b1c585822f54ec32c4c1bb4d3914603e2cca20bbd5")): { + cbor.NewByteString(test.DecodeHexString("426f7764757261436f6e63657074733638")): 234567, + }, + }, + ) + testTxOut := BabbageTransactionOutput{ + OutputAddress: func() common.Address { foo, _ := common.NewAddress(testAddr); return foo }(), + OutputAmount: mary.MaryTransactionOutputValue{ + Amount: testAmount, + Assets: &testAssets, + }, + } + expectedData := data.NewConstr( + 0, + // Address + data.NewConstr( + 0, + data.NewConstr( + 0, + data.NewConstr( + 0, + data.NewByteString( + []byte{ + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + }, + ), + ), + ), + data.NewConstr( + 1, + ), + ), + // Value + data.NewMap( + [][2]data.PlutusData{ + { + data.NewByteString(nil), + data.NewMap( + [][2]data.PlutusData{ + { + data.NewByteString(nil), + data.NewInteger(big.NewInt(int64(testAmount))), + }, + }, + ), + }, + { + data.NewByteString(test.DecodeHexString("29a8fb8318718bd756124f0c144f56d4b4579dc5edf2dd42d669ac61")), + data.NewMap( + [][2]data.PlutusData{ + { + data.NewByteString(test.DecodeHexString("6675726e697368613239686e")), + data.NewInteger(big.NewInt(123456)), + }, + }, + ), + }, + { + data.NewByteString(test.DecodeHexString("eaf8042c1d8203b1c585822f54ec32c4c1bb4d3914603e2cca20bbd5")), + data.NewMap( + [][2]data.PlutusData{ + { + data.NewByteString(test.DecodeHexString("426f7764757261436f6e63657074733638")), + data.NewInteger(big.NewInt(234567)), + }, + }, + ), + }, + }, + ), + // Empty datum option + data.NewConstr(0), + // Empty script ref + data.NewConstr(1), + ) + tmpData := testTxOut.ToPlutusData() + if !reflect.DeepEqual(tmpData, expectedData) { + t.Fatalf("did not get expected PlutusData\n got: %#v\n wanted: %#v", tmpData, expectedData) + } +} diff --git a/ledger/byron/byron.go b/ledger/byron/byron.go index 9c8884c3..6ef1836f 100644 --- a/ledger/byron/byron.go +++ b/ledger/byron/byron.go @@ -421,6 +421,11 @@ func (o *ByronTransactionOutput) UnmarshalCBOR(data []byte) error { return nil } +func (o ByronTransactionOutput) ToPlutusData() data.PlutusData { + // A Byron transaction output will never be used for Plutus scripts + return nil +} + func (o ByronTransactionOutput) Address() common.Address { return o.OutputAddress } diff --git a/ledger/common/tx.go b/ledger/common/tx.go index b6338942..c37b32e5 100644 --- a/ledger/common/tx.go +++ b/ledger/common/tx.go @@ -74,6 +74,7 @@ type TransactionOutput interface { Cbor() []byte Utxorpc() (*utxorpc.TxOutput, error) ScriptRef() Script + ToPlutusData() data.PlutusData } type TransactionWitnessSet interface { diff --git a/ledger/mary/mary.go b/ledger/mary/mary.go index d24dd032..36156a2c 100644 --- a/ledger/mary/mary.go +++ b/ledger/mary/mary.go @@ -21,6 +21,7 @@ import ( "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger/common" "github.com/blinklabs-io/gouroboros/ledger/shelley" + "github.com/blinklabs-io/plutigo/pkg/data" utxorpc "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" ) @@ -447,6 +448,11 @@ func (o MaryTransactionOutput) MarshalJSON() ([]byte, error) { return json.Marshal(&tmpObj) } +func (o MaryTransactionOutput) ToPlutusData() data.PlutusData { + // A Mary transaction output will never be used for Plutus scripts + return nil +} + func (o MaryTransactionOutput) Address() common.Address { return o.OutputAddress } diff --git a/ledger/shelley/shelley.go b/ledger/shelley/shelley.go index 99a7d7ee..a0709a24 100644 --- a/ledger/shelley/shelley.go +++ b/ledger/shelley/shelley.go @@ -403,6 +403,11 @@ func (o *ShelleyTransactionOutput) UnmarshalCBOR(cborData []byte) error { return nil } +func (o ShelleyTransactionOutput) ToPlutusData() data.PlutusData { + // A Shelley transaction output will never be used for Plutus scripts + return nil +} + func (o ShelleyTransactionOutput) Address() common.Address { return o.OutputAddress }