Skip to content

Commit

Permalink
Merge pull request #2 from philippgille/feature/decode-account-state
Browse files Browse the repository at this point in the history
Feature/decode account state
  • Loading branch information
philippgille authored Jun 30, 2019
2 parents 2bc26e8 + 543911c commit 9c132a8
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 10 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ before_install:
script:
# Build
- go build -v ./...
# Run tests
- go test -v -race .
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
vNext
-----

- Added: Decoding of an account state and resource into proper objects (issue [#1](https://github.com/philippgille/libra-sdk-go/issues/1))
- New struct `libra.AccountState` contains the blob (raw bytes) as well as a decoded `AccountResource`
- New struct `libra.AccountResource` contains the account's balance, auth key, sent and received events, as well as sequence number
- New function: `libra.FromAccountStateBlob(accountStateBlob []byte) (AccountState, error)` decodes an account state blob into a `libra.AccountState`
- New function: `libra.FromAccountResourceBlob(accountResourceBlob []byte) (AccountResource, error)` decodes an account resource blob into a `libra.AccountResource`
- Improved: `Client.GetAccountState(accountAddr string)` now returns an object of the newly added `libra.AccountState`

### Breaking Changes

- The return type of `Client.GetAccountState(accountAddr string)` was changed from `([]byte, error)` to `(AccountState, error)` (for issue [#1](https://github.com/philippgille/libra-sdk-go/issues/1))

v0.1.0 (2019-06-23)
---------------------

Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Go SDK for the Libra cryptocurrency
Features
--------

- Get account state (raw bytes)
- Get account state with account resource (balance, auth key, sent and received events count, sequence no)
- Send transaction (raw bytes)

### Roadmap
Expand Down Expand Up @@ -50,14 +50,18 @@ func main() {
panic(err)
}

fmt.Printf("Account state: 0x%x", accState)
fmt.Printf("Raw account state: 0x%x\n", accState.Blob)
fmt.Println()
fmt.Printf("Account resource: %v\n", accState.AccountResource)
}
```

Currently prints:

```
Account state: 0x010000002100000001217da6c6b3e19f1825cfb2676daecce3bf3de03cf26647c78df00b371b25cc9744000000200000008cd377191fe0ef113455c8e8d769f0c0147d5bb618bf195c0af31a05fbfd0969a0acb90300000000010000000000000004000000000000000400000000000000
Raw account state: 0x010000002100000001217da6c6b3e19f1825cfb2676daecce3bf3de03cf26647c78df00b371b25cc9744000000200000008cd377191fe0ef113455c8e8d769f0c0147d5bb618bf195c0af31a05fbfd0969a0acb90300000000010000000000000004000000000000000400000000000000
Account resource: {"authentication_key": "0x8cd377191fe0ef113455c8e8d769f0c0147d5bb618bf195c0af31a05fbfd0969", "balance": "62500000", "received_events_count": "1", "sent_events_count": "4", "sequence_number": "4"}
```

Develop
Expand Down
159 changes: 159 additions & 0 deletions account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package libra

import (
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
)

const (
// See https://github.com/perfectmak/libra-core/blob/6dc45f7b72aee0162d52a9d53d99cdae1adae6f1/lib/constants/PathValues.ts
accResourceKey = "01217da6c6b3e19f1825cfb2676daecce3bf3de03cf26647c78df00b371b25cc97"
)

// AccountState represents the state of an account.
type AccountState struct {
// The whole account state as raw bytes
Blob []byte
// The account resource with balance etc.
AccountResource AccountResource
// TODO: AccountEvents?
}

// FromAccountStateBlob converts an account state blob into an object of the AccountState struct.
func FromAccountStateBlob(accountStateBlob []byte) (AccountState, error) {
result := AccountState{
Blob: accountStateBlob,
}

r := bytes.NewReader(accountStateBlob)

// Inspired by https://github.com/perfectmak/libra-core/blob/6dc45f7b72aee0162d52a9d53d99cdae1adae6f1/lib/client.ts#L250

var mapEntryCount uint32
err := binary.Read(r, binary.LittleEndian, &mapEntryCount)
if err != nil {
return result, err
}

m := make(map[string][]byte, mapEntryCount)

// For now we only decode the account state.
// Go through the whole blob to find it (it has a specific map key).
// TODO: Also take care of events.
accResourceKeyFound := false
for mapEntryNo := uint32(1); mapEntryNo <= mapEntryCount; mapEntryNo++ {
var mapKeyLen uint32
err := binary.Read(r, binary.LittleEndian, &mapKeyLen)
if err != nil {
return result, err
}
mapKey := make([]byte, mapKeyLen)
err = binary.Read(r, binary.LittleEndian, &mapKey)
if err != nil {
return result, err
}

var mapValLength uint32
err = binary.Read(r, binary.LittleEndian, &mapValLength)
if err != nil {
return result, err
}
mapVal := make([]byte, mapValLength)
err = binary.Read(r, binary.LittleEndian, &mapVal)
if err != nil {
return result, err
}

mapKeyAsString := hex.EncodeToString(mapKey)
m[mapKeyAsString] = mapVal

// TODO: Remove when adding handling of events
if mapKeyAsString == accResourceKey {
accResourceKeyFound = true
break
}
}

if !accResourceKeyFound {
return result, errors.New("The account state blob didn't contain the data of an account resource or there was an error decoding it")
}

accResource, err := FromAccountResourceBlob(m[accResourceKey])
if err != nil {
return result, err
}
result.AccountResource = accResource

return result, nil
}

// AccountResource represents an account with its balance etc.
type AccountResource struct {
AuthKey []byte
Balance uint64
ReceivedEvents uint64
SentEvents uint64
SequenceNo uint64
}

// String formats the account state similarly to the Libra CLI.
// Numbers are formatted as string because the numbers are uint64,
// whose max value exceeds JSON's "save integer",
// which can lead to parsing errors.
func (ar AccountResource) String() string {
return fmt.Sprintf("{\"authentication_key\": \"0x%x\", \"balance\": \"%d\", \"received_events_count\": \"%d\", \"sent_events_count\": \"%d\", \"sequence_number\": \"%d\"}",
ar.AuthKey, ar.Balance, ar.ReceivedEvents, ar.SentEvents, ar.SequenceNo)
}

// FromAccountResourceBlob converts an account resource blob into an object of the AccountState struct.
func FromAccountResourceBlob(accountResourceBlob []byte) (AccountResource, error) {
result := AccountResource{}

r := bytes.NewReader(accountResourceBlob)

var authKeyLen uint32
err := binary.Read(r, binary.LittleEndian, &authKeyLen)
if err != nil {
return result, err
}

authKey := make([]byte, authKeyLen)
err = binary.Read(r, binary.LittleEndian, &authKey)
if err != nil {
return result, err
}
result.AuthKey = authKey

var balance uint64
err = binary.Read(r, binary.LittleEndian, &balance)
if err != nil {
return result, err
}
result.Balance = balance

var receivedEvents uint64
err = binary.Read(r, binary.LittleEndian, &receivedEvents)
if err != nil {
return result, err
}
result.ReceivedEvents = receivedEvents

var sentEvents uint64
err = binary.Read(r, binary.LittleEndian, &sentEvents)
if err != nil {
return result, err
}
result.SentEvents = sentEvents

var sequenceNo uint64
err = binary.Read(r, binary.LittleEndian, &sequenceNo)
if err != nil {
return result, err
}
result.SequenceNo = sequenceNo

return result, nil
}
96 changes: 96 additions & 0 deletions account_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package libra_test

import (
"bytes"
"encoding/hex"
"testing"

"github.com/go-test/deep"

libra "github.com/philippgille/libra-sdk-go"
)

const (
// In the Libra CLI:
//
//Decoded: AccountResource {
// authentication_key: 0x8cd377191fe0ef113455c8e8d769f0c0147d5bb618bf195c0af31a05fbfd0969,
// balance: 62500000,
// received_events_count: 1,
// sent_events_count: 4,
// sequence_number: 4,
// }
testAcc1StateString = "010000002100000001217da6c6b3e19f1825cfb2676daecce3bf3de03cf26647c78df00b371b25cc9744000000200000008cd377191fe0ef113455c8e8d769f0c0147d5bb618bf195c0af31a05fbfd0969a0acb90300000000010000000000000004000000000000000400000000000000"
// The account resource part of the above account state blob
testAcc1ResString = "200000008cd377191fe0ef113455c8e8d769f0c0147d5bb618bf195c0af31a05fbfd0969a0acb90300000000010000000000000004000000000000000400000000000000"
// The auth key part of the above account resource blob
testAcc1AuthKey = "8cd377191fe0ef113455c8e8d769f0c0147d5bb618bf195c0af31a05fbfd0969"
)

// TestAccountStateDecoding tests if libra.FromAccountStateBlob(...) works correctly.
// It uses libra.FromAccountResourceBlob(...), so that function must be tested independently.
func TestAccountStateDecoding(t *testing.T) {
testAccState, err := hex.DecodeString(testAcc1StateString)
if err != nil {
t.Fatal(err)
}
accState, err := libra.FromAccountStateBlob(testAccState)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(accState.Blob, testAccState) {
t.Fatal("accState.Blob != expected account state blob")
}
accRes := accState.AccountResource
if diff := deep.Equal(accRes, libra.AccountResource{}); diff == nil {
t.Fatal("accState.AccountResource has a nil value, but should be an object with values")
}

// Construct expected account resource
expectedAccResBlob, err := hex.DecodeString(testAcc1ResString)
if err != nil {
t.Fatal(err)
}
expectedAccRes, err := libra.FromAccountResourceBlob(expectedAccResBlob)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(accRes, expectedAccRes); diff != nil {
t.Fatal(diff)
}
}

// TestAccountResourceDecoding tests if libra.FromAccountResourceBlob(...) works correctly.
func TestAccountResourceDecoding(t *testing.T) {
// The string is a substring of the account state tested elsewhere
testAccRes, err := hex.DecodeString(testAcc1ResString)
if err != nil {
t.Fatal(err)
}
accRes, err := libra.FromAccountResourceBlob(testAccRes)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(accRes, libra.AccountResource{}); diff == nil {
t.Fatal("accRes is a nil value, but should be an object with values")
}
expectedAuthKey, err := hex.DecodeString(testAcc1AuthKey)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(accRes.AuthKey, expectedAuthKey) {
t.Fatal("accRes.AuthKey != expected auth key")
}
if accRes.Balance != uint64(62500000) {
t.Fatal("accRes.Balance != 62500000")
}
if accRes.ReceivedEvents != uint64(1) {
t.Fatal("accRes.ReceivedEvents != 1")
}
if accRes.SentEvents != uint64(4) {
t.Fatal("accRes.SentEvents != 4")
}
if accRes.SequenceNo != uint64(4) {
t.Fatal("accRes.SequenceNo != 4")
}
}
12 changes: 6 additions & 6 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,10 @@ type Client struct {
}

// GetAccountState requests the state of the given account.
// The return value is the raw undecoded slice of bytes.
// No proof is validated.
func (c Client) GetAccountState(accountAddr string) ([]byte, error) {
func (c Client) GetAccountState(accountAddr string) (AccountState, error) {
accountAddrBytes, err := hex.DecodeString(accountAddr)
if err != nil {
return nil, err
return AccountState{}, err
}
// From the generated Go code:
//
Expand All @@ -54,11 +52,13 @@ func (c Client) GetAccountState(accountAddr string) ([]byte, error) {
}
updateLedgerResponse, err := c.acc.UpdateToLatestLedger(context.Background(), &updateLedgerRequest)
if err != nil {
return nil, err
return AccountState{}, err
}

// We only put one request item in the request, so there should only be one response.
return updateLedgerResponse.GetResponseItems()[0].GetGetAccountStateResponse().GetAccountStateWithProof().GetBlob().GetBlob(), nil
accStateBlob := updateLedgerResponse.GetResponseItems()[0].GetGetAccountStateResponse().GetAccountStateWithProof().GetBlob().GetBlob()

return FromAccountStateBlob(accStateBlob)
}

// SendTx sends a transaction to the connected validator node.
Expand Down
4 changes: 3 additions & 1 deletion examples/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@ func main() {
panic(err)
}

fmt.Printf("Account state: 0x%x", accState)
fmt.Printf("Raw account state: 0x%x\n", accState.Blob)
fmt.Println()
fmt.Printf("Account resource: %v\n", accState.AccountResource)
}

0 comments on commit 9c132a8

Please sign in to comment.