diff --git a/types/types.go b/types/types.go index 19e5280f..fa8c870e 100644 --- a/types/types.go +++ b/types/types.go @@ -400,6 +400,17 @@ type Transaction struct { Signatures []TransactionSignature `json:"signatures,omitempty"` } +// MarshalJSON implements json.Marshaller. +// +// For convenience, the transaction's ID is also calculated and included. This field is ignored during unmarshalling. +func (txn Transaction) MarshalJSON() ([]byte, error) { + type jsonTxn Transaction // prevent recursion + return json.Marshal(struct { + ID TransactionID `json:"id"` + jsonTxn + }{txn.ID(), jsonTxn(txn)}) +} + // ID returns the "semantic hash" of the transaction, covering all of the // transaction's effects, but not incidental data such as signatures. This // ensures that the ID will remain stable (i.e. non-malleable). @@ -689,6 +700,17 @@ type V2Transaction struct { MinerFee Currency `json:"minerFee"` } +// MarshalJSON implements json.Marshaller. +// +// For convenience, the transaction's ID is also calculated and included. This field is ignored during unmarshalling. +func (txn V2Transaction) MarshalJSON() ([]byte, error) { + type jsonTxn V2Transaction // prevent recursion + return json.Marshal(struct { + ID TransactionID `json:"id"` + jsonTxn + }{txn.ID(), jsonTxn(txn)}) +} + // ID returns the "semantic hash" of the transaction, covering all of the // transaction's effects, but not incidental data such as signatures or Merkle // proofs. This ensures that the ID will remain stable (i.e. non-malleable). diff --git a/types/types_test.go b/types/types_test.go index d6b2e65d..a48ad9aa 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -759,6 +759,80 @@ func TestParseCurrency(t *testing.T) { } } +func TestTransactionJSONMarshalling(t *testing.T) { + txn := Transaction{ + SiacoinOutputs: []SiacoinOutput{ + {Address: frand.Entropy256(), Value: Siacoins(uint32(frand.Uint64n(math.MaxUint32)))}, + }, + SiacoinInputs: []SiacoinInput{ + { + ParentID: frand.Entropy256(), + UnlockConditions: UnlockConditions{ + PublicKeys: []UnlockKey{ + PublicKey(frand.Entropy256()).UnlockKey(), + }, + SignaturesRequired: 1, + }, + }, + }, + } + expectedID := txn.ID() + + buf, err := json.Marshal(txn) + if err != nil { + t.Fatal(err) + } + + txnMap := make(map[string]any) + if err := json.Unmarshal(buf, &txnMap); err != nil { + t.Fatal(err) + } else if txnMap["id"] != expectedID.String() { + t.Fatalf("expected ID %q, got %q", expectedID.String(), txnMap["id"].(string)) + } + + var txn2 Transaction + if err := json.Unmarshal(buf, &txn2); err != nil { + t.Fatal(err) + } else if txn2.ID() != expectedID { + t.Fatalf("expected unmarshalled ID to be %q, got %q", expectedID, txn2.ID()) + } +} + +func TestV2TransactionJSONMarshalling(t *testing.T) { + txn := V2Transaction{ + SiacoinInputs: []V2SiacoinInput{ + { + Parent: SiacoinElement{ + ID: frand.Entropy256(), + StateElement: StateElement{ + LeafIndex: frand.Uint64n(math.MaxUint64), + }, + }, + }, + }, + } + expectedID := txn.ID() + + buf, err := json.Marshal(txn) + if err != nil { + t.Fatal(err) + } + + txnMap := make(map[string]any) + if err := json.Unmarshal(buf, &txnMap); err != nil { + t.Fatal(err) + } else if txnMap["id"] != expectedID.String() { + t.Fatalf("expected ID %q, got %q", expectedID.String(), txnMap["id"].(string)) + } + + var txn2 V2Transaction + if err := json.Unmarshal(buf, &txn2); err != nil { + t.Fatal(err) + } else if txn2.ID() != expectedID { + t.Fatalf("expected unmarshalled ID to be %q, got %q", expectedID, txn2.ID()) + } +} + func TestUnmarshalHex(t *testing.T) { for _, test := range []struct { data string