Skip to content

Commit d827282

Browse files
authored
Merge pull request #284 from blinklabs-io/feat/ledger-tx-output-multiasset
feat: proper parsing for multiasset TX outputs
2 parents d1c28f1 + 6916a1a commit d827282

File tree

8 files changed

+194
-23
lines changed

8 files changed

+194
-23
lines changed

cbor/bytestring.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,11 @@ import (
1919
)
2020

2121
// Wrapper for bytestrings that allows them to be used as keys for a map
22-
type ByteString struct {
23-
// We use a string because []byte isn't comparable, which means it can't be used as a map key
24-
data string
25-
}
22+
// We use a string because []byte isn't comparable, which means it can't be used as a map key
23+
type ByteString string
2624

2725
func NewByteString(data []byte) ByteString {
28-
bs := ByteString{
29-
data: string(data),
30-
}
26+
bs := ByteString(data)
3127
return bs
3228
}
3329

@@ -36,14 +32,18 @@ func (bs *ByteString) UnmarshalCBOR(data []byte) error {
3632
if _, err := Decode(data, &tmpValue); err != nil {
3733
return err
3834
}
39-
bs.data = string(tmpValue)
35+
*bs = ByteString(tmpValue)
4036
return nil
4137
}
4238

39+
func (bs ByteString) MarshalCBOR() ([]byte, error) {
40+
return Encode([]byte(bs))
41+
}
42+
4343
func (bs ByteString) Bytes() []byte {
44-
return []byte(bs.data)
44+
return []byte(bs)
4545
}
4646

4747
func (bs ByteString) String() string {
48-
return hex.EncodeToString([]byte(bs.data))
48+
return hex.EncodeToString([]byte(bs))
4949
}

cbor/encode.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ package cbor
1616

1717
import (
1818
"bytes"
19+
"fmt"
20+
"reflect"
1921

2022
_cbor "github.com/fxamacker/cbor/v2"
23+
"github.com/jinzhu/copier"
2124
)
2225

2326
func Encode(data interface{}) ([]byte, error) {
@@ -30,3 +33,35 @@ func Encode(data interface{}) ([]byte, error) {
3033
err = enc.Encode(data)
3134
return buf.Bytes(), err
3235
}
36+
37+
// EncodeGeneric encodes the specified object to CBOR without using the source object's
38+
// MarshalCBOR() function
39+
func EncodeGeneric(src interface{}) ([]byte, error) {
40+
// Create a duplicate(-ish) struct from the destination
41+
// We do this so that we can bypass any custom UnmarshalCBOR() function on the
42+
// destination object
43+
valueSrc := reflect.ValueOf(src)
44+
if valueSrc.Kind() != reflect.Pointer || valueSrc.Elem().Kind() != reflect.Struct {
45+
return nil, fmt.Errorf("source must be a pointer to a struct")
46+
}
47+
typeSrcElem := valueSrc.Elem().Type()
48+
srcTypeFields := []reflect.StructField{}
49+
for i := 0; i < typeSrcElem.NumField(); i++ {
50+
tmpField := typeSrcElem.Field(i)
51+
if tmpField.IsExported() && tmpField.Name != "DecodeStoreCbor" {
52+
srcTypeFields = append(srcTypeFields, tmpField)
53+
}
54+
}
55+
// Create temporary object with the type created above
56+
tmpSrc := reflect.New(reflect.StructOf(srcTypeFields))
57+
// Copy values from source object into temporary object
58+
if err := copier.Copy(tmpSrc.Interface(), src); err != nil {
59+
return nil, err
60+
}
61+
// Encode temporary object into CBOR
62+
cborData, err := Encode(tmpSrc.Interface())
63+
if err != nil {
64+
return nil, err
65+
}
66+
return cborData, nil
67+
}

cbor/value.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (v *Value) UnmarshalCBOR(data []byte) error {
5353
v.Value = tmpValue
5454
case CBOR_TYPE_BYTE_STRING:
5555
// Use our custom type which stores the bytestring in a way that allows it to be used as a map key
56-
tmpValue := ByteString{}
56+
var tmpValue ByteString
5757
if _, err := Decode(data, &tmpValue); err != nil {
5858
return err
5959
}

internal/test/helpers.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package test
2+
3+
import (
4+
"encoding/hex"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
// DecodeHexString is a helper function for tests that decodes hex strings. It doesn't return
10+
// an error value, which makes it usable inline.
11+
func DecodeHexString(hexData string) []byte {
12+
// Strip off any leading/trailing whitespace in hex string
13+
hexData = strings.TrimSpace(hexData)
14+
decoded, err := hex.DecodeString(hexData)
15+
if err != nil {
16+
panic(fmt.Sprintf("error decoding hex: %s", err))
17+
}
18+
return decoded
19+
}

ledger/alonzo.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ type AlonzoTransactionOutput struct {
9595
cbor.StructAsArray
9696
cbor.DecodeStoreCbor
9797
Address Blake2b256
98-
Amount cbor.Value
98+
Amount MaryTransactionOutputValue
9999
DatumHash Blake2b256
100100
}
101101

ledger/babbage.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,10 @@ func (b *BabbageTransactionBody) UnmarshalCBOR(cborData []byte) error {
136136
}
137137

138138
type BabbageTransactionOutput struct {
139-
Address Blake2b256 `cbor:"0,keyasint,omitempty"`
140-
Amount cbor.Value `cbor:"1,keyasint,omitempty"`
141-
DatumOption []cbor.RawMessage `cbor:"2,keyasint,omitempty"`
142-
ScriptRef cbor.Tag `cbor:"3,keyasint,omitempty"`
139+
Address Blake2b256 `cbor:"0,keyasint,omitempty"`
140+
Amount MaryTransactionOutputValue `cbor:"1,keyasint,omitempty"`
141+
DatumOption []cbor.RawMessage `cbor:"2,keyasint,omitempty"`
142+
ScriptRef cbor.Tag `cbor:"3,keyasint,omitempty"`
143143
legacyOutput bool
144144
}
145145

ledger/mary.go

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,34 @@ type MaryTransaction struct {
9595
Metadata cbor.Value
9696
}
9797

98-
// TODO: support both forms
99-
/*
100-
transaction_output = [address, amount : value]
101-
value = coin / [coin,multiasset<uint>]
102-
*/
103-
10498
type MaryTransactionOutput struct {
10599
cbor.StructAsArray
106100
Address Blake2b256
107-
Amount cbor.Value
101+
Amount MaryTransactionOutputValue
102+
}
103+
104+
type MaryTransactionOutputValue struct {
105+
cbor.StructAsArray
106+
Amount uint64
107+
Assets map[Blake2b224]map[cbor.ByteString]uint64
108+
}
109+
110+
func (v *MaryTransactionOutputValue) UnmarshalCBOR(data []byte) error {
111+
if _, err := cbor.Decode(data, &(v.Amount)); err == nil {
112+
return nil
113+
}
114+
if err := cbor.DecodeGeneric(data, v); err != nil {
115+
return err
116+
}
117+
return nil
118+
}
119+
120+
func (v *MaryTransactionOutputValue) MarshalCBOR() ([]byte, error) {
121+
if v.Assets == nil {
122+
return cbor.Encode(v.Amount)
123+
} else {
124+
return cbor.EncodeGeneric(v)
125+
}
108126
}
109127

110128
func NewMaryBlockFromCbor(data []byte) (*MaryBlock, error) {

ledger/mary_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright 2023 Blink Labs, LLC.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package ledger
16+
17+
import (
18+
"encoding/hex"
19+
"reflect"
20+
"testing"
21+
22+
"github.com/blinklabs-io/gouroboros/cbor"
23+
"github.com/blinklabs-io/gouroboros/internal/test"
24+
)
25+
26+
func createMaryTransactionOutputValueAssets(policyId []byte, assetName []byte, amount uint64) map[Blake2b224]map[cbor.ByteString]uint64 {
27+
ret := map[Blake2b224]map[cbor.ByteString]uint64{}
28+
policyIdKey := Blake2b224{}
29+
copy(policyIdKey[:], policyId)
30+
assetKey := cbor.ByteString(assetName)
31+
ret[policyIdKey] = map[cbor.ByteString]uint64{
32+
assetKey: amount,
33+
}
34+
return ret
35+
}
36+
37+
func TestMaryTransactionOutputValueEncodeDecode(t *testing.T) {
38+
var tests = []struct {
39+
CborHex string
40+
Object interface{}
41+
}{
42+
{
43+
CborHex: "1a02d71996",
44+
Object: MaryTransactionOutputValue{Amount: 47651222},
45+
},
46+
{
47+
CborHex: "1b0000000129d2de56",
48+
Object: MaryTransactionOutputValue{Amount: 4996652630},
49+
},
50+
{
51+
CborHex: "821a003d0900a1581c00000002df633853f6a47465c9496721d2d5b1291b8398016c0e87aea1476e7574636f696e01",
52+
// [4000000, {h'00000002DF633853F6A47465C9496721D2D5B1291B8398016C0E87AE': {h'6E7574636F696E': 1}}]
53+
Object: MaryTransactionOutputValue{
54+
Amount: 4000000,
55+
Assets: createMaryTransactionOutputValueAssets(
56+
test.DecodeHexString("00000002DF633853F6A47465C9496721D2D5B1291B8398016C0E87AE"),
57+
test.DecodeHexString("6E7574636F696E"),
58+
1,
59+
),
60+
},
61+
},
62+
{
63+
CborHex: "821a004986e3a1581c3a9241cd79895e3a8d65261b40077d4437ce71e9d7c8c6c00e3f658ea1494669727374636f696e01",
64+
// [4818659, {h'3A9241CD79895E3A8D65261B40077D4437CE71E9D7C8C6C00E3F658E': {h'4669727374636F696E': 1}}]
65+
Object: MaryTransactionOutputValue{
66+
Amount: 4818659,
67+
Assets: createMaryTransactionOutputValueAssets(
68+
test.DecodeHexString("3A9241CD79895E3A8D65261B40077D4437CE71E9D7C8C6C00E3F658E"),
69+
test.DecodeHexString("4669727374636F696E"),
70+
1,
71+
),
72+
},
73+
},
74+
}
75+
for _, test := range tests {
76+
// Test decode
77+
cborData, err := hex.DecodeString(test.CborHex)
78+
if err != nil {
79+
t.Fatalf("failed to decode CBOR hex: %s", err)
80+
}
81+
tmpObj := MaryTransactionOutputValue{}
82+
_, err = cbor.Decode(cborData, &tmpObj)
83+
if err != nil {
84+
t.Fatalf("failed to decode CBOR: %s", err)
85+
}
86+
if !reflect.DeepEqual(tmpObj, test.Object) {
87+
t.Fatalf("CBOR did not decode to expected object\n got: %#v\n wanted: %#v", tmpObj, test.Object)
88+
}
89+
// Test encode
90+
cborData, err = cbor.Encode(test.Object)
91+
if err != nil {
92+
t.Fatalf("failed to encode object to CBOR: %s", err)
93+
}
94+
cborHex := hex.EncodeToString(cborData)
95+
if cborHex != test.CborHex {
96+
t.Fatalf("object did not encode to expected CBOR\n got: %s\n wanted: %s", cborHex, test.CborHex)
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)