From 5dc267381255d7bcafc7ab0a39031c4640e3a063 Mon Sep 17 00:00:00 2001 From: Aurora Gaffney Date: Sun, 17 Aug 2025 14:51:28 -0400 Subject: [PATCH] feat: initial Plutus script context support This supports building a V3 script context from a transaction for a limited set of use cases. This is copied straight from blinklabs-io/plutigo, with minimal modifications for package names, type compatibility, and appeasing the linter Signed-off-by: Aurora Gaffney --- ledger/common/script/scriptcontext.go | 455 ++++++++++++++++++ ledger/common/script/scriptcontext_test.go | 146 ++++++ .../simple_send_expected_structure.txt | 231 +++++++++ ledger/common/script/wrappers.go | 139 ++++++ 4 files changed, 971 insertions(+) create mode 100644 ledger/common/script/scriptcontext.go create mode 100644 ledger/common/script/scriptcontext_test.go create mode 100644 ledger/common/script/testdata/simple_send_expected_structure.txt create mode 100644 ledger/common/script/wrappers.go diff --git a/ledger/common/script/scriptcontext.go b/ledger/common/script/scriptcontext.go new file mode 100644 index 00000000..fe1fc60a --- /dev/null +++ b/ledger/common/script/scriptcontext.go @@ -0,0 +1,455 @@ +// 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 script + +import ( + "math/big" + "slices" + "strings" + + lcommon "github.com/blinklabs-io/gouroboros/ledger/common" + "github.com/blinklabs-io/plutigo/data" +) + +type ScriptContext interface { + isScriptContext() + ToPlutusData() data.PlutusData +} + +type ScriptContextV1V2 struct { + TxInfo TxInfo + // Purpose ScriptPurpose +} + +func (ScriptContextV1V2) isScriptContext() {} + +func (s ScriptContextV1V2) ToPlutusData() data.PlutusData { + // TODO + return nil +} + +type ScriptContextV3 struct { + TxInfo TxInfo + Redeemer Redeemer + Purpose ScriptInfo +} + +func (ScriptContextV3) isScriptContext() {} + +func (s ScriptContextV3) ToPlutusData() data.PlutusData { + return data.NewConstr( + 0, + s.TxInfo.ToPlutusData(), + s.Redeemer.ToPlutusData(), + s.Purpose.ToPlutusData(), + ) +} + +func NewScriptContextV3(txInfo TxInfo, redeemer Redeemer, purpose ScriptInfo) ScriptContext { + return ScriptContextV3{ + TxInfo: txInfo, + Redeemer: redeemer, + Purpose: purpose, + } +} + +type TxInfo interface { + isTxInfo() + ToPlutusData() data.PlutusData +} + +type TxInfoV1 struct { + Inputs []lcommon.Utxo + Outputs []lcommon.Utxo + Fee uint64 + Mint lcommon.MultiAsset[lcommon.MultiAssetTypeMint] + Certificates []lcommon.Certificate + Withdrawals KeyValuePairs[*lcommon.Address, Coin] + ValidRange TimeRange + Signatories []lcommon.Blake2b224 + Data KeyValuePairs[lcommon.Blake2b256, data.PlutusData] + Redeemers KeyValuePairs[ScriptInfo, Redeemer] + Id lcommon.Blake2b256 +} + +func (TxInfoV1) isTxInfo() {} + +func (t TxInfoV1) ToPlutusData() data.PlutusData { + // TODO + return nil +} + +type TxInfoV2 struct { + Inputs []lcommon.Utxo + ReferenceInputs []lcommon.Utxo + Outputs []lcommon.Utxo + Fee uint64 + Mint lcommon.MultiAsset[lcommon.MultiAssetTypeMint] + Certificates []lcommon.Certificate + Withdrawals KeyValuePairs[*lcommon.Address, Coin] + ValidRange TimeRange + Signatories []lcommon.Blake2b224 + Redeemers KeyValuePairs[ScriptInfo, Redeemer] + Data KeyValuePairs[lcommon.Blake2b256, data.PlutusData] + Id lcommon.Blake2b256 +} + +func (TxInfoV2) isTxInfo() {} + +func (t TxInfoV2) ToPlutusData() data.PlutusData { + // TODO + return nil +} + +type TxInfoV3 struct { + Inputs []ResolvedInput + ReferenceInputs []ResolvedInput + Outputs []lcommon.TransactionOutput + Fee uint64 + Mint lcommon.MultiAsset[lcommon.MultiAssetTypeMint] + Certificates []lcommon.Certificate + Withdrawals map[*lcommon.Address]uint64 + ValidRange TimeRange + Signatories []lcommon.Blake2b224 + Redeemers KeyValuePairs[ScriptInfo, Redeemer] + Data KeyValuePairs[lcommon.Blake2b256, data.PlutusData] + Id lcommon.Blake2b256 + Votes KeyValuePairs[lcommon.Voter, KeyValuePairs[lcommon.GovActionId, lcommon.VotingProcedure]] + ProposalProcedures []lcommon.ProposalProcedure + CurrentTreasuryAmount Option[Coin] + TreasuryDonation Option[PositiveCoin] +} + +func (TxInfoV3) isTxInfo() {} + +func (t TxInfoV3) ToPlutusData() data.PlutusData { + return data.NewConstr( + 0, + toPlutusData(t.Inputs), + toPlutusData(t.ReferenceInputs), + toPlutusData(t.Outputs), + data.NewInteger(new(big.Int).SetUint64(t.Fee)), + t.Mint.ToPlutusData(), + // TODO: certs + toPlutusData([]any{}), + toPlutusData(t.Withdrawals), + t.ValidRange.ToPlutusData(), + // TODO: signatories + toPlutusData([]any{}), + t.Redeemers.ToPlutusData(), + t.Data.ToPlutusData(), + data.NewByteString(t.Id.Bytes()), + // TODO: votes + data.NewMap([][2]data.PlutusData{}), + // TODO: proposal procedures + toPlutusData([]any{}), + // TODO: current treasury amount + data.NewConstr(1), + // TODO: treasury donation + data.NewConstr(1), + ) +} + +func NewTxInfoV3FromTransaction(tx lcommon.Transaction, resolvedInputs []lcommon.Utxo) TxInfoV3 { + assetMint := tx.AssetMint() + if assetMint == nil { + assetMint = &lcommon.MultiAsset[lcommon.MultiAssetTypeMint]{} + } + inputs := sortInputs(tx.Consumed()) + redeemers := redeemersInfo( + tx.Witnesses(), + scriptPurposeBuilder( + inputs, + *assetMint, + // TODO: certificates + tx.Withdrawals(), + // TODO: proposal procedures + // TODO: votes + ), + ) + ret := TxInfoV3{ + Inputs: expandInputs(inputs, resolvedInputs), + ReferenceInputs: expandInputs(sortInputs(tx.ReferenceInputs()), resolvedInputs), + Outputs: collapseOutputs(tx.Produced()), + Fee: tx.Fee(), + Mint: *assetMint, + ValidRange: TimeRange{ + tx.TTL(), + tx.ValidityIntervalStart(), + }, + Withdrawals: tx.Withdrawals(), + // TODO: Signatories + Redeemers: redeemers, + // TODO: Data + Id: tx.Hash(), + // TODO: Votes + // TODO: ProposalProcedures + // TODO: CurrentTreasuryAmount + // TODO: TreasuryDonation + } + return ret +} + +type TimeRange struct { + lowerBound uint64 + upperBound uint64 +} + +func (t TimeRange) ToPlutusData() data.PlutusData { + bound := func(bound uint64, isLower bool) data.PlutusData { + if bound > 0 { + return data.NewConstr( + 0, + data.NewConstr( + 1, + data.NewInteger( + new(big.Int).SetUint64(bound), + ), + ), + toPlutusData(isLower), + ) + } else { + var constrType uint = 0 + if !isLower { + constrType = 2 + } + return data.NewConstr( + 0, + data.NewConstr(constrType), + // NOTE: Infinite bounds are always exclusive, by convention. + toPlutusData(true), + ) + } + } + return data.NewConstr( + 0, + bound(t.lowerBound, true), + bound(t.upperBound, false), + ) +} + +type ScriptInfo interface { + isScriptInfo() + ToPlutusData +} + +type ScriptInfoMinting struct { + PolicyId lcommon.Blake2b224 +} + +func (ScriptInfoMinting) isScriptInfo() {} + +func (s ScriptInfoMinting) ToPlutusData() data.PlutusData { + return data.NewConstr( + 0, + data.NewByteString(s.PolicyId.Bytes()), + ) +} + +type ScriptInfoSpending struct { + Input lcommon.TransactionInput + Datum data.PlutusData +} + +func (ScriptInfoSpending) isScriptInfo() {} + +func (s ScriptInfoSpending) ToPlutusData() data.PlutusData { + if s.Datum == nil { + return data.NewConstr( + 1, + s.Input.ToPlutusData(), + ) + } + return data.NewConstr( + 1, + s.Input.ToPlutusData(), + data.NewConstr( + 0, + s.Datum, + ), + ) +} + +type ScriptInfoRewarding struct { + StakeCredential lcommon.Credential +} + +func (ScriptInfoRewarding) isScriptInfo() {} + +func (s ScriptInfoRewarding) ToPlutusData() data.PlutusData { + // TODO + return nil +} + +type ScriptInfoCertifying struct { + Size uint64 + Certificate lcommon.Certificate +} + +func (ScriptInfoCertifying) isScriptInfo() {} + +func (s ScriptInfoCertifying) ToPlutusData() data.PlutusData { + // TODO + return nil +} + +type ScriptInfoVoting struct { + Voter lcommon.Voter +} + +func (ScriptInfoVoting) isScriptInfo() {} + +func (s ScriptInfoVoting) ToPlutusData() data.PlutusData { + // TODO + return nil +} + +type ScriptInfoProposing struct { + Size uint64 + ProposalProcedure lcommon.ProposalProcedure +} + +func (ScriptInfoProposing) isScriptInfo() {} + +func (s ScriptInfoProposing) ToPlutusData() data.PlutusData { + // TODO + return nil +} + +func sortInputs(inputs []lcommon.TransactionInput) []lcommon.TransactionInput { + ret := make([]lcommon.TransactionInput, len(inputs)) + copy(ret, inputs) + slices.SortFunc( + ret, + func(a, b lcommon.TransactionInput) int { + // Compare TX ID + x := strings.Compare(a.Id().String(), b.Id().String()) + if x != 0 { + return x + } + if a.Index() < b.Index() { + return -1 + } else if a.Index() > b.Index() { + return 1 + } + return 0 + }, + ) + return ret +} + +func expandInputs(inputs []lcommon.TransactionInput, resolvedInputs []lcommon.Utxo) []ResolvedInput { + ret := make([]ResolvedInput, len(inputs)) + for i, input := range inputs { + for _, resolvedInput := range resolvedInputs { + if input.String() == resolvedInput.Id.String() { + ret[i] = ResolvedInput(resolvedInput) + break + } + } + } + return ret +} + +func collapseOutputs(outputs []lcommon.Utxo) []lcommon.TransactionOutput { + ret := make([]lcommon.TransactionOutput, len(outputs)) + for i, item := range outputs { + ret[i] = item.Output + } + return ret +} + +func sortedRedeemerKeys(redeemers lcommon.TransactionWitnessRedeemers) []lcommon.RedeemerKey { + tags := []lcommon.RedeemerTag{lcommon.RedeemerTagSpend, lcommon.RedeemerTagMint, lcommon.RedeemerTagCert, lcommon.RedeemerTagReward, lcommon.RedeemerTagVoting, lcommon.RedeemerTagProposing} + ret := make([]lcommon.RedeemerKey, 0) + for _, tag := range tags { + idxs := redeemers.Indexes(tag) + slices.Sort(idxs) + for _, idx := range idxs { + ret = append( + ret, + lcommon.RedeemerKey{ + Tag: tag, + // nolint:gosec + Index: uint32(idx), + }, + ) + } + } + return ret +} + +func redeemersInfo(witnessSet lcommon.TransactionWitnessSet, toScriptPurpose toScriptPurposeFunc) KeyValuePairs[ScriptInfo, Redeemer] { + var ret KeyValuePairs[ScriptInfo, Redeemer] + redeemers := witnessSet.Redeemers() + redeemerKeys := sortedRedeemerKeys(redeemers) + for _, key := range redeemerKeys { + redeemerValue := redeemers.Value(uint(key.Index), key.Tag) + datum := redeemerValue.Data.Data + purpose := toScriptPurpose(key, datum) + ret = append( + ret, + KeyValuePair[ScriptInfo, Redeemer]{ + Key: purpose, + Value: Redeemer{ + Tag: key.Tag, + Index: key.Index, + Data: datum, + ExUnits: redeemerValue.ExUnits, + }, + }, + ) + } + return ret +} + +type toScriptPurposeFunc func(lcommon.RedeemerKey, data.PlutusData) ScriptInfo + +// scriptPurposeBuilder creates a reusable function preloaded with information about a particular transaction +func scriptPurposeBuilder( + inputs []lcommon.TransactionInput, + mint lcommon.MultiAsset[lcommon.MultiAssetTypeMint], + // TODO: certificates + withdrawals map[*lcommon.Address]uint64, + // TODO: proposal procedures + // TODO: votes +) toScriptPurposeFunc { + return func(redeemerKey lcommon.RedeemerKey, datum data.PlutusData) ScriptInfo { + // TODO: implement additional redeemer tags + // https://github.com/aiken-lang/aiken/blob/af4e04b91e54dbba3340de03fc9e65a90f24a93b/crates/uplc/src/tx/script_context.rs#L771-L826 + switch redeemerKey.Tag { + case lcommon.RedeemerTagSpend: + return ScriptInfoSpending{ + Input: inputs[redeemerKey.Index], + Datum: datum, + } + case lcommon.RedeemerTagMint: + // TODO: fix this to work for more than one minted policy + mintPolicies := mint.Policies() + return ScriptInfoMinting{ + PolicyId: mintPolicies[0], + } + case lcommon.RedeemerTagCert: + return nil + case lcommon.RedeemerTagReward: + return nil + case lcommon.RedeemerTagVoting: + return nil + case lcommon.RedeemerTagProposing: + return nil + } + return nil + } +} diff --git a/ledger/common/script/scriptcontext_test.go b/ledger/common/script/scriptcontext_test.go new file mode 100644 index 00000000..6b29110c --- /dev/null +++ b/ledger/common/script/scriptcontext_test.go @@ -0,0 +1,146 @@ +// 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 script + +import ( + "encoding/hex" + "errors" + "fmt" + "os" + "regexp" + "strings" + "testing" + + "github.com/blinklabs-io/gouroboros/cbor" + "github.com/blinklabs-io/gouroboros/ledger/babbage" + lcommon "github.com/blinklabs-io/gouroboros/ledger/common" + "github.com/blinklabs-io/gouroboros/ledger/conway" + "github.com/blinklabs-io/gouroboros/ledger/shelley" + "github.com/blinklabs-io/plutigo/data" +) + +func formatPlutusData(pd data.PlutusData) string { + var builder strings.Builder + pdStr := pd.String() + indentLevel := 0 + needsNewline := false + for _, c := range pdStr { + if c == ' ' && needsNewline { + builder.WriteString("\n" + strings.Repeat(` `, indentLevel*4)) + continue + } + needsNewline = false + if c == '{' || c == '[' { + indentLevel++ + needsNewline = true + } + if c == '}' || c == ']' { + indentLevel-- + builder.WriteString("\n" + strings.Repeat(` `, indentLevel*4)) + needsNewline = true + } + if c == ')' { + needsNewline = true + } + if c == ',' { + needsNewline = true + } + builder.WriteRune(c) + if needsNewline { + builder.WriteString("\n" + strings.Repeat(` `, indentLevel*4)) + } + } + ret := builder.String() + // Strip out empty lines + tmpRe := regexp.MustCompile(`\n +\n`) + ret = tmpRe.ReplaceAllString(ret, "\n") + return ret +} + +func buildTxInfoV3(txHex string, inputsHex string, inputOutputsHex string) (TxInfo, error) { + // Transaction + txBytes, err := hex.DecodeString(txHex) + if err != nil { + return nil, fmt.Errorf("decode transaction hex: %w", err) + } + tx, err := conway.NewConwayTransactionFromCbor(txBytes) + if err != nil { + return nil, err + } + // Inputs + inputsBytes, err := hex.DecodeString(inputsHex) + if err != nil { + return nil, fmt.Errorf("decode inputs hex: %w", err) + } + var tmpInputs []shelley.ShelleyTransactionInput + if _, err := cbor.Decode(inputsBytes, &tmpInputs); err != nil { + return nil, fmt.Errorf("decode inputs: %w", err) + } + // Input outputs + inputOutputsBytes, err := hex.DecodeString(inputOutputsHex) + if err != nil { + return nil, fmt.Errorf("decode input outputs hex: %w", err) + } + var tmpOutputs []babbage.BabbageTransactionOutput + if _, err := cbor.Decode(inputOutputsBytes, &tmpOutputs); err != nil { + return nil, fmt.Errorf("decode input outputs: %w", err) + } + // Build resolved inputs + var resolvedInputs []lcommon.Utxo + if len(tmpInputs) != len(tmpOutputs) { + return nil, errors.New("input and output length don't match") + } + for i := range tmpInputs { + resolvedInputs = append( + resolvedInputs, + lcommon.Utxo{ + Id: tmpInputs[i], + Output: tmpOutputs[i], + }, + ) + } + // Build TxInfo + txInfo := NewTxInfoV3FromTransaction(tx, resolvedInputs) + return txInfo, nil +} + +func TestScriptContextV3SimpleSend(t *testing.T) { + // NOTE: these values come from the Aiken tests + // https://github.com/aiken-lang/aiken/blob/af4e04b91e54dbba3340de03fc9e65a90f24a93b/crates/uplc/src/tx/script_context.rs#L1189-L1225 + txInfo, err := buildTxInfoV3( + `84a70081825820000000000000000000000000000000000000000000000000000000000000000000018182581d60111111111111111111111111111111111111111111111111111111111a3b9aca0002182a0b5820ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0d818258200000000000000000000000000000000000000000000000000000000000000000001082581d60000000000000000000000000000000000000000000000000000000001a3b9aca001101a20581840000d87980821a000f42401a05f5e100078152510101003222253330044a229309b2b2b9a1f5f6`, + `81825820000000000000000000000000000000000000000000000000000000000000000000`, + `81a300581d7039f47fd3b388ef53c48f08de24766d3e55dade6cae908cc24e0f4f3e011a3b9aca00028201d81843d87980`, + ) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + // Extract purpose and redeemer from first redeemer in TxInfo + redeemerPair := txInfo.(TxInfoV3).Redeemers[0] + purpose := redeemerPair.Key + redeemer := redeemerPair.Value + // Build script context + sc := NewScriptContextV3(txInfo, redeemer, purpose) + // Read expected structure from file + expectedBytes, err := os.ReadFile(`testdata/simple_send_expected_structure.txt`) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + expected := strings.TrimSpace(string(expectedBytes)) + scPd := strings.TrimSpace(formatPlutusData(sc.ToPlutusData())) + if scPd != expected { + t.Fatalf("did not get expected structure\n\n got:\n\n%s\n\n wanted:\n\n%s", scPd, expected) + } +} diff --git a/ledger/common/script/testdata/simple_send_expected_structure.txt b/ledger/common/script/testdata/simple_send_expected_structure.txt new file mode 100644 index 00000000..01e0cbc4 --- /dev/null +++ b/ledger/common/script/testdata/simple_send_expected_structure.txt @@ -0,0 +1,231 @@ +Constr{ + tag: 0, + fields: [ + Constr{ + tag: 0, + fields: [ + List[ + Constr{ + tag: 0, + fields: [ + Constr{ + tag: 0, + fields: [ + ByteString(0000000000000000000000000000000000000000000000000000000000000000) + Integer(0) + ] + } + Constr{ + tag: 0, + fields: [ + Constr{ + tag: 0, + fields: [ + Constr{ + tag: 1, + fields: [ + ByteString(39f47fd3b388ef53c48f08de24766d3e55dade6cae908cc24e0f4f3e) + ] + } + Constr{ + tag: 1, + fields: [ + ] + } + ] + } + Map[ + [ + ByteString() + Map[ + [ + ByteString() + Integer(1000000000) + ] + ] + ] + ] + Constr{ + tag: 0, + fields: [ + ] + } + Constr{ + tag: 1, + fields: [ + ] + } + ] + } + ] + } + ] + List[ + ] + List[ + Constr{ + tag: 0, + fields: [ + Constr{ + tag: 0, + fields: [ + Constr{ + tag: 0, + fields: [ + ByteString(11111111111111111111111111111111111111111111111111111111) + ] + } + Constr{ + tag: 1, + fields: [ + ] + } + ] + } + Map[ + [ + ByteString() + Map[ + [ + ByteString() + Integer(1000000000) + ] + ] + ] + ] + Constr{ + tag: 0, + fields: [ + ] + } + Constr{ + tag: 1, + fields: [ + ] + } + ] + } + ] + Integer(42) + Map[ + ] + List[ + ] + Map[ + ] + Constr{ + tag: 0, + fields: [ + Constr{ + tag: 0, + fields: [ + Constr{ + tag: 0, + fields: [ + ] + } + Constr{ + tag: 1, + fields: [ + ] + } + ] + } + Constr{ + tag: 0, + fields: [ + Constr{ + tag: 2, + fields: [ + ] + } + Constr{ + tag: 1, + fields: [ + ] + } + ] + } + ] + } + List[ + ] + Map[ + [ + Constr{ + tag: 1, + fields: [ + Constr{ + tag: 0, + fields: [ + ByteString(0000000000000000000000000000000000000000000000000000000000000000) + Integer(0) + ] + } + Constr{ + tag: 0, + fields: [ + Constr{ + tag: 0, + fields: [ + ] + } + ] + } + ] + } + Constr{ + tag: 0, + fields: [ + ] + } + ] + ] + Map[ + ] + ByteString(78ec148ea647cf9969446891af31939c5d57b275a2455706782c6183ef0b62f1) + Map[ + ] + List[ + ] + Constr{ + tag: 1, + fields: [ + ] + } + Constr{ + tag: 1, + fields: [ + ] + } + ] + } + Constr{ + tag: 0, + fields: [ + ] + } + Constr{ + tag: 1, + fields: [ + Constr{ + tag: 0, + fields: [ + ByteString(0000000000000000000000000000000000000000000000000000000000000000) + Integer(0) + ] + } + Constr{ + tag: 0, + fields: [ + Constr{ + tag: 0, + fields: [ + ] + } + ] + } + ] + } + ] +} diff --git a/ledger/common/script/wrappers.go b/ledger/common/script/wrappers.go new file mode 100644 index 00000000..f1fce760 --- /dev/null +++ b/ledger/common/script/wrappers.go @@ -0,0 +1,139 @@ +// 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 script + +import ( + "math/big" + "reflect" + + lcommon "github.com/blinklabs-io/gouroboros/ledger/common" + "github.com/blinklabs-io/plutigo/data" +) + +// ToPlutusData is an interface that represents types that support serialization to PlutusData when building a ScriptContext +type ToPlutusData interface { + ToPlutusData() data.PlutusData +} + +type Option[T ToPlutusData] struct { + Value ToPlutusData +} + +func (o Option[T]) ToPlutusData() data.PlutusData { + if o.Value == nil { + return data.NewConstr(0) + } + return data.NewConstr( + 1, + o.Value.ToPlutusData(), + ) +} + +type KeyValuePairs[K any, V any] []KeyValuePair[K, V] + +func (k KeyValuePairs[K, V]) ToPlutusData() data.PlutusData { + pairs := make([][2]data.PlutusData, len(k)) + for i, tmpPair := range k { + pairs[i] = [2]data.PlutusData{ + toPlutusData(tmpPair.Key), + toPlutusData(tmpPair.Value), + } + } + return data.NewMap(pairs) +} + +type KeyValuePair[K any, V any] struct { + Key K + Value V +} + +func toPlutusData(val any) data.PlutusData { + if pd, ok := val.(ToPlutusData); ok { + return pd.ToPlutusData() + } + switch v := val.(type) { + case bool: + if v { + return data.NewConstr(1) + } + return data.NewConstr(0) + case int64: + return data.NewInteger(new(big.Int).SetInt64(v)) + case uint64: + return data.NewInteger(new(big.Int).SetUint64(v)) + case []ToPlutusData: + tmpItems := make([]data.PlutusData, len(v)) + for i, item := range v { + tmpItems[i] = item.ToPlutusData() + } + return data.NewList(tmpItems...) + default: + rv := reflect.ValueOf(v) + // nolint:exhaustive + switch rv.Kind() { + case reflect.Slice: + tmpItems := make([]data.PlutusData, rv.Len()) + for i := range rv.Len() { + item := rv.Index(i) + tmpItems[i] = toPlutusData(item.Interface()) + } + return data.NewList(tmpItems...) + case reflect.Map: + tmpPairs := make([][2]data.PlutusData, rv.Len()) + for i, k := range rv.MapKeys() { + v := rv.MapIndex(k) + tmpPairs[i] = [2]data.PlutusData{ + toPlutusData(k.Interface()), + toPlutusData(v.Interface()), + } + } + return data.NewMap(tmpPairs) + } + } + return nil +} + +type Coin int64 + +func (c Coin) ToPlutusData() data.PlutusData { + return data.NewInteger(new(big.Int).SetInt64(int64(c))) +} + +type PositiveCoin uint64 + +func (c PositiveCoin) ToPlutusData() data.PlutusData { + return data.NewInteger(new(big.Int).SetUint64(uint64(c))) +} + +type ResolvedInput lcommon.Utxo + +func (r ResolvedInput) ToPlutusData() data.PlutusData { + return data.NewConstr( + 0, + r.Id.ToPlutusData(), + r.Output.ToPlutusData(), + ) +} + +type Redeemer struct { + Tag lcommon.RedeemerTag + Index uint32 + Data data.PlutusData + ExUnits lcommon.ExUnits +} + +func (r Redeemer) ToPlutusData() data.PlutusData { + return r.Data +}