Skip to content

Commit 18ba039

Browse files
wolf31o2GitHub Copilot
andcommitted
feat: implement CBOR marshal/unmarshal for ScriptsNotPaidUtxo error
Add custom CBOR encoding/decoding for ScriptsNotPaidUtxo that matches the Haskell cardano-ledger specification. The implementation uses `[constructor_index, utxo_map]` format with multi-strategy decoding to handle complex CBOR map structures. - Add `MarshalCBOR` converting slice to map format - Add `UnmarshalCBOR` with fallback decoding strategies - Add comprehensive test with mock interfaces - Maintain compatibility with existing error handling Signed-off-by: Chris Gianelloni <[email protected]> Co-authored-by: GitHub Copilot <[email protected]>
1 parent 39d18fd commit 18ba039

File tree

2 files changed

+630
-3
lines changed

2 files changed

+630
-3
lines changed

ledger/error.go

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import (
2121
"strings"
2222

2323
"github.com/blinklabs-io/gouroboros/cbor"
24+
"github.com/blinklabs-io/gouroboros/ledger/byron"
25+
"github.com/blinklabs-io/gouroboros/ledger/common"
26+
"github.com/blinklabs-io/gouroboros/ledger/shelley"
2427
)
2528

2629
const (
@@ -534,14 +537,115 @@ func (e *InsufficientCollateral) Error() string {
534537
)
535538
}
536539

540+
// ScriptsNotPaidUtxo represents the ScriptsNotPaidUTxO error from cardano-ledger.
541+
// Haskell: ScriptsNotPaidUTxO !(UTxO era) where UTxO era = Map TxIn TxOut
542+
// CBOR: [14, utxo_map]
537543
type ScriptsNotPaidUtxo struct {
538544
UtxoFailureErrorBase
539-
// TODO: determine content/structure of this value (#847)
540-
Value cbor.Value
545+
Utxos []common.Utxo // Each Utxo contains Id (input) and Output
546+
}
547+
548+
func (e *ScriptsNotPaidUtxo) MarshalCBOR() ([]byte, error) {
549+
// Set the struct tag for consistency
550+
e.Type = UtxoFailureScriptsNotPaidUtxo
551+
552+
utxoMap := make(
553+
map[common.TransactionInput]common.TransactionOutput,
554+
len(e.Utxos),
555+
)
556+
for _, u := range e.Utxos {
557+
// Return error for nil entries instead of silently skipping
558+
if u.Id == nil || u.Output == nil {
559+
return nil, errors.New(
560+
"ScriptsNotPaidUtxo: cannot marshal UTxO with nil Id or Output",
561+
)
562+
}
563+
utxoMap[u.Id] = u.Output
564+
}
565+
arr := []any{UtxoFailureScriptsNotPaidUtxo, utxoMap}
566+
return cbor.Encode(arr)
567+
}
568+
569+
func (e *ScriptsNotPaidUtxo) UnmarshalCBOR(data []byte) error {
570+
type tScriptsNotPaidUtxo struct {
571+
cbor.StructAsArray
572+
ConstructorIdx uint64
573+
UtxoMapCbor cbor.RawMessage
574+
}
575+
var tmp tScriptsNotPaidUtxo
576+
if _, err := cbor.Decode(data, &tmp); err != nil {
577+
return fmt.Errorf("failed to decode ScriptsNotPaidUtxo: %w", err)
578+
}
579+
580+
if tmp.ConstructorIdx != UtxoFailureScriptsNotPaidUtxo {
581+
return fmt.Errorf(
582+
"ScriptsNotPaidUtxo: expected constructor index %d, got %d",
583+
UtxoFailureScriptsNotPaidUtxo,
584+
tmp.ConstructorIdx,
585+
)
586+
}
587+
588+
// Set the struct tag to match the decoded constructor
589+
e.Type = UtxoFailureScriptsNotPaidUtxo
590+
591+
// For era-agnostic decoding, we need to handle the map structure carefully
592+
// Since we can't use cbor.RawMessage as map keys, we'll decode to a concrete type first
593+
// and then convert to era-agnostic types. Try different era input types until one works.
594+
595+
// Try Shelley-family transaction inputs first (most common from Shelley onwards)
596+
var shelleyUtxoMap map[shelley.ShelleyTransactionInput]cbor.RawMessage
597+
if _, err := cbor.Decode(tmp.UtxoMapCbor, &shelleyUtxoMap); err == nil {
598+
// Successfully decoded as Shelley-family inputs
599+
e.Utxos = make([]common.Utxo, 0, len(shelleyUtxoMap))
600+
for input, outputCbor := range shelleyUtxoMap {
601+
// Decode output using era-agnostic function (handles all eras)
602+
output, err := NewTransactionOutputFromCbor(outputCbor)
603+
if err != nil {
604+
return fmt.Errorf(
605+
"failed to decode transaction output: %w",
606+
err,
607+
)
608+
}
609+
610+
e.Utxos = append(e.Utxos, common.Utxo{
611+
Id: input,
612+
Output: output,
613+
})
614+
}
615+
return nil
616+
}
617+
618+
// Try Byron transaction inputs (for Byron era)
619+
var byronUtxoMap map[byron.ByronTransactionInput]cbor.RawMessage
620+
if _, err := cbor.Decode(tmp.UtxoMapCbor, &byronUtxoMap); err == nil {
621+
// Successfully decoded as Byron inputs
622+
e.Utxos = make([]common.Utxo, 0, len(byronUtxoMap))
623+
for input, outputCbor := range byronUtxoMap {
624+
// Decode output using era-agnostic function (handles all eras)
625+
output, err := NewTransactionOutputFromCbor(outputCbor)
626+
if err != nil {
627+
return fmt.Errorf(
628+
"failed to decode transaction output: %w",
629+
err,
630+
)
631+
}
632+
633+
e.Utxos = append(e.Utxos, common.Utxo{
634+
Id: input,
635+
Output: output,
636+
})
637+
}
638+
return nil
639+
}
640+
641+
// If both failed, return an error
642+
return errors.New(
643+
"failed to decode UTxO map as either Shelley-family or Byron transaction inputs",
644+
)
541645
}
542646

543647
func (e *ScriptsNotPaidUtxo) Error() string {
544-
return fmt.Sprintf("ScriptsNotPaidUtxo (%v)", e.Value.Value())
648+
return fmt.Sprintf("ScriptsNotPaidUtxo (%d UTxOs)", len(e.Utxos))
545649
}
546650

547651
type ExUnitsTooBigUtxo struct {

0 commit comments

Comments
 (0)