Skip to content

Commit

Permalink
Monetary Attribute (#1259)
Browse files Browse the repository at this point in the history
* Monetary Attribute

* fix testworld test

* fix testworld test

* fix testworld test

* address comments

* adding type

* adding type
  • Loading branch information
mikiquantum committed Aug 22, 2019
1 parent ef2a075 commit 73f834c
Show file tree
Hide file tree
Showing 20 changed files with 435 additions and 87 deletions.
4 changes: 2 additions & 2 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Gopkg.toml
Expand Up @@ -28,7 +28,7 @@ required = ["github.com/centrifuge/centrifuge-ethereum-contracts", "github.com/r

[[constraint]]
name = "github.com/centrifuge/centrifuge-protobufs"
revision = "3d5bdf9902852de7da8e3c69d2b0fde36e3eb48b"
revision = "a4dc99bb9e43371800469049e6536cc8052ce5ef"

[[override]]
name = "github.com/centrifuge/centrifuge-ethereum-contracts"
Expand Down
86 changes: 79 additions & 7 deletions documents/attribute.go
@@ -1,11 +1,13 @@
package documents

import (
"fmt"
"strings"
"time"

"github.com/centrifuge/go-centrifuge/config"
"github.com/centrifuge/go-centrifuge/crypto"
"github.com/centrifuge/go-centrifuge/errors"
"github.com/centrifuge/go-centrifuge/identity"
"github.com/centrifuge/go-centrifuge/utils"
"github.com/ethereum/go-ethereum/common/hexutil"
Expand All @@ -20,6 +22,14 @@ func (a AttributeType) String() string {
return string(a)
}

// MonetaryType represents the monetary type of the attribute
type MonetaryType string

// String returns the readable name of the monetary type.
func (a MonetaryType) String() string {
return string(a)
}

const (
// AttrInt256 is the standard integer custom attribute type
AttrInt256 AttributeType = "integer"
Expand All @@ -38,12 +48,18 @@ const (

// AttrSigned is the custom signature attribute type
AttrSigned AttributeType = "signed"

// AttrMonetary is the monetary attribute type
AttrMonetary AttributeType = "monetary"

// MonetaryToken is the monetary type for tokens
MonetaryToken MonetaryType = "token"
)

// isAttrTypeAllowed checks if the given attribute type is implemented and returns its `reflect.Type` if allowed.
func isAttrTypeAllowed(attr AttributeType) bool {
switch attr {
case AttrInt256, AttrDecimal, AttrString, AttrBytes, AttrTimestamp, AttrSigned:
case AttrInt256, AttrDecimal, AttrString, AttrBytes, AttrTimestamp, AttrSigned, AttrMonetary:
return true
default:
return false
Expand Down Expand Up @@ -104,6 +120,27 @@ func (s Signed) String() string {
return s.Identity.String()
}

// Monetary is a custom attribute type for monetary values
type Monetary struct {
Value *Decimal
ChainID []byte
Type MonetaryType
ID []byte // Currency USD|0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2(DAI)|ETH
}

// String returns the readable representation of the monetary value
func (m Monetary) String() string {
chStr := ""
if len(m.ChainID) > 0 {
chStr = "@" + hexutil.Encode(m.ChainID)
}
mID := string(m.ID)
if m.Type == MonetaryToken {
mID = hexutil.Encode(m.ID)
}
return fmt.Sprintf("%s %s%s", m.Value.String(), mID, chStr)
}

// AttrVal represents a strongly typed value of an attribute
type AttrVal struct {
Type AttributeType
Expand All @@ -113,6 +150,7 @@ type AttrVal struct {
Bytes []byte
Timestamp *timestamp.Timestamp
Signed Signed
Monetary Monetary
}

// AttrValFromString converts the string value to necessary type based on the attribute type.
Expand All @@ -129,11 +167,10 @@ func AttrValFromString(attrType AttributeType, value string) (attrVal AttrVal, e
attrVal.Bytes, err = hexutil.Decode(value)
case AttrTimestamp:
var t time.Time
t, err = time.Parse(time.RFC3339, value)
t, err = time.Parse(time.RFC3339Nano, value)
if err != nil {
return attrVal, err
}

attrVal.Timestamp, err = utils.ToTimestamp(t.UTC())
default:
return attrVal, ErrNotValidAttrType
Expand Down Expand Up @@ -163,10 +200,11 @@ func (attrVal AttrVal) String() (str string, err error) {
if err != nil {
break
}

str = tp.UTC().Format(time.RFC3339)
str = tp.UTC().Format(time.RFC3339Nano)
case AttrSigned:
str = attrVal.Signed.String()
case AttrMonetary:
str = attrVal.Monetary.String()
}

return str, err
Expand All @@ -179,8 +217,8 @@ type Attribute struct {
Value AttrVal
}

// NewAttribute creates a new custom attribute.
func NewAttribute(keyLabel string, attrType AttributeType, value string) (attr Attribute, err error) {
// NewStringAttribute creates a new custom attribute.
func NewStringAttribute(keyLabel string, attrType AttributeType, value string) (attr Attribute, err error) {
attrKey, err := AttrKeyFromLabel(keyLabel)
if err != nil {
return attr, err
Expand All @@ -198,6 +236,40 @@ func NewAttribute(keyLabel string, attrType AttributeType, value string) (attr A
}, nil
}

// NewMonetaryAttribute creates new instance of Monetary Attribute
func NewMonetaryAttribute(keyLabel string, value *Decimal, chainID []byte, id string) (attr Attribute, err error) {
if value == nil {
return attr, errors.NewTypedError(ErrWrongAttrFormat, errors.New("empty value field"))
}

attrKey, err := AttrKeyFromLabel(keyLabel)
if err != nil {
return attr, err
}

token := MonetaryToken
idb, err := hexutil.Decode(id)
if err != nil {
token = ""
idb = []byte(id)
}

if len(idb) > monetaryIDLength {
return attr, errors.NewTypedError(ErrWrongAttrFormat, errors.New("monetaryIDLength exceeds 32 bytes"))
}

attrVal := AttrVal{
Type: AttrMonetary,
Monetary: Monetary{Value: value, Type: token, ChainID: chainID, ID: idb},
}

return Attribute{
KeyLabel: keyLabel,
Key: attrKey,
Value: attrVal,
}, nil
}

// NewSignedAttribute returns a new signed attribute
// takes keyLabel, signer identity, signer account, model and value
// doc version is next version of the document since that is the document version in which the attribute is added.
Expand Down
56 changes: 54 additions & 2 deletions documents/attribute_test.go
Expand Up @@ -4,6 +4,7 @@ package documents

import (
"encoding/json"
"fmt"
"testing"
"time"

Expand Down Expand Up @@ -108,7 +109,7 @@ func TestNewAttribute(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
attr, err := NewAttribute(test.readableKey, test.attrType, test.value)
attr, err := NewStringAttribute(test.readableKey, test.attrType, test.value)
if test.errs {
assert.Error(t, err)
assert.Equal(t, test.errStr, err.Error())
Expand Down Expand Up @@ -179,7 +180,12 @@ func TestAttrValFromString(t *testing.T) {
time.Now().UTC().Format(time.RFC3339),
false,
},

{
"timestamp_nano",
AttrTimestamp,
time.Now().UTC().Format(time.RFC3339Nano),
false,
},
{
"unknown type",
AttributeType("some type"),
Expand Down Expand Up @@ -261,3 +267,49 @@ func TestNewSignedAttribute(t *testing.T) {
acc.AssertExpectations(t)
model.AssertExpectations(t)
}

func TestNewMonetaryAttribute(t *testing.T) {
dec, err := NewDecimal("1001.1001")
assert.NoError(t, err)

// empty label
_, err = NewMonetaryAttribute("", dec, nil, "")
assert.Error(t, err)
assert.True(t, errors.IsOfType(ErrEmptyAttrLabel, err))

// monetary ID exceeded length
label := "invoice_amount"
chainID := []byte{1}
idd := "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a29f8f72aa9304c8b593d555f12ef6589cc3a579a2" // 40 bytes
_, err = NewMonetaryAttribute(label, dec, chainID, idd)
assert.Error(t, err)
assert.True(t, errors.IsOfType(ErrWrongAttrFormat, err))

// success fiat
idd = "USD"
attr, err := NewMonetaryAttribute(label, dec, chainID, idd)
assert.NoError(t, err)
assert.Equal(t, AttrMonetary, attr.Value.Type)
attrKey, err := AttrKeyFromLabel(label)
assert.NoError(t, err)
assert.Equal(t, attrKey, attr.Key)
assert.Equal(t, []byte(idd), attr.Value.Monetary.ID)
assert.Equal(t, chainID, attr.Value.Monetary.ChainID)
assert.Equal(t, "", attr.Value.Monetary.Type.String())
assert.Equal(t, fmt.Sprintf("%s %s@%s", dec.String(), idd, hexutil.Encode(chainID)), attr.Value.Monetary.String())

// success erc20
idd = "0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2"
attr, err = NewMonetaryAttribute(label, dec, chainID, idd)
assert.NoError(t, err)
assert.Equal(t, AttrMonetary, attr.Value.Type)
attrKey, err = AttrKeyFromLabel(label)
assert.NoError(t, err)
assert.Equal(t, attrKey, attr.Key)
decIdd, err := hexutil.Decode(idd)
assert.NoError(t, err)
assert.Equal(t, decIdd, attr.Value.Monetary.ID)
assert.Equal(t, chainID, attr.Value.Monetary.ChainID)
assert.Equal(t, MonetaryToken, attr.Value.Monetary.Type)
assert.Equal(t, fmt.Sprintf("%s %s@%s", dec.String(), idd, hexutil.Encode(chainID)), attr.Value.Monetary.String())
}
52 changes: 50 additions & 2 deletions documents/converters.go
Expand Up @@ -14,8 +14,14 @@ import (
"github.com/golang/protobuf/ptypes/timestamp"
)

// maxTimeByteLength is the max length of the byte representation of a timestamp attribute
const maxTimeByteLength = 12
const (
// maxTimeByteLength is the max length of the byte representation of a timestamp attribute
maxTimeByteLength = 12
// monetaryChainIDLength is the fixed length of the byte representation of ChainID
monetaryChainIDLength = 4
// monetaryIDLength is the fixed length of the byte representation of monetary ID
monetaryIDLength = 32
)

// BinaryAttachment represent a single file attached to invoice.
type BinaryAttachment struct {
Expand Down Expand Up @@ -223,6 +229,20 @@ func toProtocolAttributes(attrs map[AttrKey]Attribute) (pattrs []*coredocumentpb
Identity: signed.Identity[:],
},
}
case AttrMonetary:
monetary := attr.Value.Monetary
decBytes, err := monetary.Value.Bytes()
if err != nil {
return nil, err
}
pattr.Value = &coredocumentpb.Attribute_MonetaryVal{
MonetaryVal: &coredocumentpb.Monetary{
Type: getProtocolMonetaryType(monetary.Type),
Value: decBytes,
Chain: append(make([]byte, monetaryChainIDLength-len(monetary.ChainID)), monetary.ChainID...),
Id: append(make([]byte, monetaryIDLength-len(monetary.ID)), monetary.ID...),
},
}
}

pattrs = append(pattrs, pattr)
Expand All @@ -243,6 +263,22 @@ func getAttributeTypeFromProtocolType(attrType coredocumentpb.AttributeType) Att
return AttributeType(strings.ToLower(strings.TrimPrefix(str, attributeProtocolPrefix)))
}

func getProtocolMonetaryType(mType MonetaryType) []byte {
ret := []byte{1}
if mType == MonetaryToken {
ret = []byte{2}
}
return ret
}

func getMonetaryTypeFromProtocolType(mType []byte) MonetaryType {
var ret MonetaryType
if bytes.Equal(mType, []byte{2}) {
ret = MonetaryToken
}
return ret
}

// fromProtocolAttributes converts protocol attribute list to model attribute map
func fromProtocolAttributes(pattrs []*coredocumentpb.Attribute) (map[AttrKey]Attribute, error) {
m := make(map[AttrKey]Attribute)
Expand Down Expand Up @@ -312,6 +348,18 @@ func attrValFromProtocolAttribute(attrType AttributeType, attribute *coredocumen
Value: val.Value,
Signature: val.Signature,
}
case AttrMonetary:
val := attribute.GetMonetaryVal()
dec, err := DecimalFromBytes(val.Value)
if err != nil {
return attrVal, err
}
attrVal.Monetary = Monetary{
Type: getMonetaryTypeFromProtocolType(val.Type),
Value: dec,
ChainID: bytes.TrimLeft(val.Chain, "\x00"),
ID: bytes.TrimLeft(val.Id, "\x00"),
}
}

return attrVal, err
Expand Down

0 comments on commit 73f834c

Please sign in to comment.