Skip to content

Commit

Permalink
fetch account resources BCS (#8)
Browse files Browse the repository at this point in the history
* note unresolved TODO from previous PR

* fetch account resources as BCS

* misc structs and utils for account resource BCS
  • Loading branch information
brianolson authored May 1, 2024
1 parent 83fd6a3 commit 0545a40
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 7 deletions.
2 changes: 2 additions & 0 deletions account.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

type AccountAddress [32]byte

// TODO: find nicer naming for this? Move account to a package so this can be account.ONE ? Wrap in a singleton struct for Account.One ?
var Account0x1 AccountAddress = AccountAddress{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}

// Returns whether the address is a "special" address. Addresses are considered
Expand Down Expand Up @@ -52,6 +53,7 @@ func (aa *AccountAddress) Random() {
rand.Read((*aa)[:])
}
func (aa *AccountAddress) FromEd25519PubKey(pubkey ed25519.PublicKey) {
// TODO: Other SDK implementations have an internal AuthenticationKey type to wrap this. Maybe follow that pattern later?
hasher := sha3.New256()
hasher.Write(pubkey[:])
hasher.Write([]byte{0})
Expand Down
28 changes: 28 additions & 0 deletions bcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,34 @@ func DeserializeSequence[T any](bcs *Deserializer) []T {
return out
}

// DeserializeMapToSlices returns two slices []K and []V of equal length that are equivalent to map[K]V but may represent types that are not valid Go map keys.
func DeserializeMapToSlices[K, V any](bcs *Deserializer) (keys []K, values []V) {
count := bcs.Uleb128()
keys = make([]K, 0, count)
values = make([]V, 0, count)
for _ = range count {
var nextk K
var nextv V
switch sv := any(&nextk).(type) {
case BCSStruct:
sv.UnmarshalBCS(bcs)
case *string:
*sv = bcs.ReadString()
}
switch sv := any(&nextv).(type) {
case BCSStruct:
sv.UnmarshalBCS(bcs)
case *string:
*sv = bcs.ReadString()
case *[]byte:
*sv = bcs.ReadBytes()
}
keys = append(keys, nextk)
values = append(values, nextv)
}
return
}

func BcsDeserialize(dest BCSStruct, bcsBlob []byte) error {
bcs := Deserializer{
source: bcsBlob,
Expand Down
73 changes: 69 additions & 4 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,17 @@ func (rc *RestClient) Account(address AccountAddress, ledger_version ...int) (in

// AccountResourceInfo is returned by #AccountResource() and #AccountResources()
type AccountResourceInfo struct {
Type string `json:"type"`
Data map[string]any `json:"data"` // TODO: what are these? Build a struct.
// e.g. "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>"
Type string `json:"type"`

// Decoded from Move contract data, could really be anything
Data map[string]any `json:"data"`
}

func (rc *RestClient) AccountResource(address AccountAddress, resourceType string, ledger_version ...int) (data map[string]any, err error) {
au := rc.baseUrl
// TODO: offer a list of known-good resourceType string constants
// TODO: set "Accept: application/x-bcs" and parse BCS objects for lossless (and faster) transmission
au.Path = path.Join(au.Path, "accounts", address.String(), "resource", resourceType)
if len(ledger_version) > 0 {
params := url.Values{}
Expand All @@ -163,6 +167,8 @@ func (rc *RestClient) AccountResource(address AccountAddress, resourceType strin
return
}

// AccountResources fetches resources for an account into a JSON-like map[string]any in AccountResourceInfo.Data
// For fetching raw Move structs as BCS, See #AccountResourcesBCS
func (rc *RestClient) AccountResources(address AccountAddress, ledger_version ...int) (resources []AccountResourceInfo, err error) {
au := rc.baseUrl
au.Path = path.Join(au.Path, "accounts", address.String(), "resources")
Expand Down Expand Up @@ -190,6 +196,64 @@ func (rc *RestClient) AccountResources(address AccountAddress, ledger_version ..
return
}

// DeserializeSequence[AccountResourceRecord](bcs) approximates the Rust side BTreeMap<StructTag,Vec<u8>>
// They should BCS the same with a prefix Uleb128 length followed by (StructTag,[]byte) pairs.
type AccountResourceRecord struct {
// Account::Module::Name
Tag StructTag

// BCS data as stored by Move contract
Data []byte
}

func (aar *AccountResourceRecord) MarshalBCS(bcs *Serializer) {
aar.Tag.MarshalBCS(bcs)
bcs.WriteBytes(aar.Data)
}
func (aar *AccountResourceRecord) UnmarshalBCS(bcs *Deserializer) {
aar.Tag.UnmarshalBCS(bcs)
aar.Data = bcs.ReadBytes()
}

func (rc *RestClient) GetBCS(getUrl string) (*http.Response, error) {
req, err := http.NewRequest("GET", getUrl, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/x-bcs")
return rc.client.Do(req)
}

// AccountResourcesBCS fetches account resources as raw Move struct BCS blobs in AccountResourceRecord.Data []byte
func (rc *RestClient) AccountResourcesBCS(address AccountAddress, ledger_version ...int) (resources []AccountResourceRecord, err error) {
au := rc.baseUrl
au.Path = path.Join(au.Path, "accounts", address.String(), "resources")
if len(ledger_version) > 0 {
params := url.Values{}
params.Set("ledger_version", strconv.Itoa(ledger_version[0]))
au.RawQuery = params.Encode()
}
response, err := rc.GetBCS(au.String())
if err != nil {
err = fmt.Errorf("GET %s, %w", au.String(), err)
return
}
if response.StatusCode >= 400 {
err = NewHttpError(response)
return
}
blob, err := io.ReadAll(response.Body)
if err != nil {
err = fmt.Errorf("error getting response data, %w", err)
return
}
response.Body.Close()
bcs := NewDeserializer(blob)
// See resource_test.go TestMoveResourceBCS
resources = DeserializeSequence[AccountResourceRecord](bcs)
return
}

// TransactionByHash gets info on a transaction
// The transaction may be pending or recently committed.
//
Expand Down Expand Up @@ -312,8 +376,9 @@ func (rc *RestClient) Transactions(start *uint64, limit *uint64) (data []map[str
return
}

// Deprecated-ish, #SubmitTransaction() should be much faster and better in every way
func (rc *RestClient) TransactionEncode(request map[string]any) (data []byte, err error) {
// testing only
// There exists an aptos-node API for submitting JSON and having the node Rust code encode it to BCS, we should only use this for testing to validate our local BCS. Actual GO-SDK usage should use BCS encoding locally in Go code.
func (rc *RestClient) transactionEncode(request map[string]any) (data []byte, err error) {
rblob, err := json.Marshal(request)
if err != nil {
return
Expand Down
2 changes: 1 addition & 1 deletion cmd/goclient/goclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func main() {
fmt.Fprintf(os.Stdout, "new account %s funded for %d, privkey = %s\n", bob.Address.String(), amount, hex.EncodeToString(bob.PrivateKey.(ed25519.PrivateKey)[:]))

time.Sleep(2 * time.Second)
stxn, err := aptos.TransferTransaction(client, alice, bob.Address, 42)
stxn, err := aptos.APTTransferTransaction(client, alice, bob.Address, 42)
maybefail(err, "could not make transfer txn, %s", err)
slog.Debug("transfer", "stxn", stxn)
result, err := client.SubmitTransaction(stxn)
Expand Down
57 changes: 57 additions & 0 deletions resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package aptos

type MoveResource struct {
Tag MoveStructTag
Value map[string]any // MoveStructValue // TODO: api/types/src/move_types.rs probably actually has more to say about what a MoveStructValue is, but at first read it effectively says map[string]any; there's probably convention elesewhere about what goes into those 'any' parts
}

func (mr *MoveResource) MarshalBCS(bcs *Serializer) {
panic("TODO")
}
func (mr *MoveResource) UnmarshalBCS(bcs *Deserializer) {
mr.Tag.UnmarshalBCS(bcs)
}

type MoveStructTag struct {
Address AccountAddress
Module string // TODO: IdentifierWrapper ?
Name string // TODO: IdentifierWrapper ?
GenericTypeParams []MoveType
}

func (mst *MoveStructTag) MarshalBCS(bcs *Serializer) {
panic("TODO")
}
func (mst *MoveStructTag) UnmarshalBCS(bcs *Deserializer) {
mst.Address.UnmarshalBCS(bcs)
mst.Module = bcs.ReadString()
mst.Name = bcs.ReadString()
mst.GenericTypeParams = DeserializeSequence[MoveType](bcs)
}

// enum
type MoveType uint8

const (
MoveType_Bool MoveType = 0
MoveType_U8 MoveType = 1
MoveType_U16 MoveType = 2
MoveType_U32 MoveType = 3
MoveType_U64 MoveType = 4
MoveType_U128 MoveType = 5
MoveType_U256 MoveType = 6
MoveType_Address MoveType = 7
MoveType_Signer MoveType = 8
MoveType_Vector MoveType = 9 // contains MoveType of items of vector
MoveType_MoveStructTag MoveType = 10 // contains a MoveStructTag
MoveType_GeneritTypeParam MoveType = 11 // contains a uint16
MoveType_Reference MoveType = 12 // {mutable bool, to MoveType}
MoveType_Unparsable MoveType = 13 // contains a string
)

func (mt *MoveType) MarshalBCS(bcs *Serializer) {
bcs.Uleb128(uint64(*mt))
}
func (mt *MoveType) UnmarshalBCS(bcs *Deserializer) {
*mt = MoveType(bcs.Uleb128())
}
36 changes: 36 additions & 0 deletions resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package aptos

import (
"encoding/base64"
"io"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func decodeB64(x string) ([]byte, error) {
reader := strings.NewReader(x)
dec := base64.NewDecoder(base64.StdEncoding, reader)
return io.ReadAll(dec)
}

func TestMoveResourceBCS(t *testing.T) {
// fetched from local aptos-node 20240501_152556
// curl -o /tmp/ar_bcs --header "Accept: application/x-bcs" http://127.0.0.1:8080/v1/accounts/{addr}/resources
// base64 < /tmp/ar_bcs
b64text := "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBGNvaW4JQ29pblN0b3JlAQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQphcHRvc19jb2luCUFwdG9zQ29pbgBpKsLrCwAAAAAAAgAAAAAAAAACAAAAAAAAANGdA6RyqwjAFP2cXRokfP3YJqHHNb55lM2GQFYwd6a7AAAAAAAAAAADAAAAAAAAANGdA6RyqwjAFP2cXRokfP3YJqHHNb55lM2GQFYwd6a7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEHYWNjb3VudAdBY2NvdW50AJMBINGdA6RyqwjAFP2cXRokfP3YJqHHNb55lM2GQFYwd6a7AAAAAAAAAAAEAAAAAAAAAAEAAAAAAAAAAAAAAAAAAADRnQOkcqsIwBT9nF0aJHz92CahxzW+eZTNhkBWMHemuwAAAAAAAAAAAQAAAAAAAADRnQOkcqsIwBT9nF0aJHz92CahxzW+eZTNhkBWMHemuwAA"
blob, err := decodeB64(b64text)
assert.NoError(t, err)
assert.NotNil(t, blob)

bcs := NewDeserializer(blob)
//resources := DeserializeSequence[MoveResource](bcs)
//resourceKeys, resourceValues := DeserializeMap[StructTag, []byte](bcs)
// like client.go AccountResourcesBCS
resources := DeserializeSequence[AccountResourceRecord](bcs)
assert.NoError(t, bcs.Error())
assert.Equal(t, 2, len(resources))
assert.Equal(t, "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>", resources[0].Tag.String())
assert.Equal(t, "0x1::account::Account", resources[1].Tag.String())
}
Loading

0 comments on commit 0545a40

Please sign in to comment.