diff --git a/README.md b/README.md index 8b9bc42c..6ad1f642 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,22 @@ if err != nil { // prints 56 println(val) ``` +### Account info +You can get full account information including balance, stored data and even code using GetAccount method, example: +```golang +// TON Foundation account +res, err := api.GetAccount(context.Background(), b, address.MustParseAddr("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N")) +if err != nil { + log.Fatalln("run get method err:", err.Error()) + return +} +// Balance: 66559946.09 TON +fmt.Printf("Balance: %s TON\n", res.State.Balance.TON()) +// Data: [0000003829a9a31772c9ed6b62a6e2eba14a93b90462e7a367777beb8a38fb15b9f33844d22ce2ff] +fmt.Printf("Data: %s", res.Data.Dump()) +``` +You can find full working example at `example/account-state/main.go` ### Custom reconnect policy By default, standard reconnect method will be used - `c.DefaultReconnect(3*time.Second, 3)` which will do 3 tries and wait 3 seconds before each. @@ -88,8 +103,7 @@ client.SetOnDisconnect(func(addr, serverKey string) { ### Features to implement * ✅ Support cell and slice as arguments for run get method * ✅ Reconnect on failure -* More tests -* Get account state method +* ✅ Get account state method * Send external query method * Cell dictionaries support * MustLoad methods diff --git a/example/account-state/main.go b/example/account-state/main.go new file mode 100644 index 00000000..ca471595 --- /dev/null +++ b/example/account-state/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/liteclient" + "github.com/xssnick/tonutils-go/ton" +) + +func main() { + client := liteclient.NewClient() + + // connect to mainnet lite server + err := client.Connect(context.Background(), "135.181.140.212:13206", "K0t3+IWLOXHYMvMcrGZDPs+pn58a17LFbnXoQkKc2xw=") + if err != nil { + log.Fatalln("connection err: ", err.Error()) + return + } + + // initialize ton api lite connection wrapper + api := ton.NewAPIClient(client) + + // we need fresh block info to run get methods + b, err := api.GetBlockInfo(context.Background()) + if err != nil { + log.Fatalln("get block err:", err.Error()) + return + } + + res, err := api.GetAccount(context.Background(), b, address.MustParseAddr("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N")) + if err != nil { + log.Fatalln("run get method err:", err.Error()) + return + } + + fmt.Printf("Balance: %s TON\n", res.State.Balance.TON()) + fmt.Printf("Data: %s", res.Data.Dump()) +} diff --git a/liteclient/tlb/account.go b/liteclient/tlb/account.go new file mode 100644 index 00000000..d7046832 --- /dev/null +++ b/liteclient/tlb/account.go @@ -0,0 +1,142 @@ +package tlb + +import ( + "math/big" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/tvm/cell" +) + +type AccountStorage struct { + LastTransactionLT uint64 + Balance Grams +} + +type StorageUsed struct { + BitsUsed uint64 + CellsUsed uint64 + PublicCellsUsed uint64 +} + +type StorageInfo struct { + StorageUsed StorageUsed + LastPaid uint32 + DuePayment *big.Int +} + +type AccountState struct { + IsValid bool + Address *address.Address + StorageInfo StorageInfo + + AccountStorage +} + +func (a *AccountState) LoadFromCell(loader *cell.LoadCell) error { + isAccount, err := loader.LoadUInt(1) + if err != nil { + return err + } + + if isAccount == 0 { + return nil + } + + addr, err := loader.LoadAddr() + if err != nil { + return err + } + + var info StorageInfo + err = info.LoadFromCell(loader) + if err != nil { + return err + } + + var store AccountStorage + err = store.LoadFromCell(loader) + if err != nil { + return err + } + + *a = AccountState{ + IsValid: true, + Address: addr, + StorageInfo: info, + AccountStorage: store, + } + + return nil +} + +func (s *StorageUsed) LoadFromCell(loader *cell.LoadCell) error { + cells, err := loader.LoadVarUInt(7) + if err != nil { + return err + } + + bits, err := loader.LoadVarUInt(7) + if err != nil { + return err + } + + pubCells, err := loader.LoadVarUInt(7) + if err != nil { + return err + } + + s.CellsUsed = cells.Uint64() + s.BitsUsed = bits.Uint64() + s.PublicCellsUsed = pubCells.Uint64() + + return nil +} + +func (s *StorageInfo) LoadFromCell(loader *cell.LoadCell) error { + var used StorageUsed + err := used.LoadFromCell(loader) + if err != nil { + return err + } + + lastPaid, err := loader.LoadUInt(32) + if err != nil { + return err + } + + isDuePayment, err := loader.LoadUInt(1) + if err != nil { + return err + } + + var duePayment *big.Int + if isDuePayment == 1 { + duePayment, err = loader.LoadBigCoins() + if err != nil { + return err + } + } + + s.StorageUsed = used + s.DuePayment = duePayment + s.LastPaid = uint32(lastPaid) + + return nil +} + +func (s *AccountStorage) LoadFromCell(loader *cell.LoadCell) error { + lastTransaction, err := loader.LoadUInt(64) + if err != nil { + return err + } + + coins, err := loader.LoadBigCoins() + if err != nil { + return err + } + + s.LastTransactionLT = lastTransaction + s.Balance = Grams{coins} + + return nil +} diff --git a/liteclient/tlb/grams.go b/liteclient/tlb/grams.go new file mode 100644 index 00000000..299e4e61 --- /dev/null +++ b/liteclient/tlb/grams.go @@ -0,0 +1,20 @@ +package tlb + +import ( + "math/big" +) + +type Grams struct { + val *big.Int +} + +func (g Grams) TON() string { + f := new(big.Float).SetInt(g.val) + t := new(big.Float).Quo(f, new(big.Float).SetUint64(1000000000)) + + return t.String() +} + +func (g Grams) NanoTON() *big.Int { + return g.val +} diff --git a/ton/api.go b/ton/api.go index 8ed720af..081b6dff 100644 --- a/ton/api.go +++ b/ton/api.go @@ -2,24 +2,18 @@ package ton import ( "context" - "encoding/binary" - "errors" - "fmt" - "math/big" - "github.com/sigurn/crc16" - "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/liteclient" - "github.com/xssnick/tonutils-go/liteclient/tlb" - "github.com/xssnick/tonutils-go/tvm/cell" ) const _GetMasterchainInfo int32 = -1984567762 const _RunContractGetMethod int32 = 1556504018 - -const LSError int32 = -1146494648 +const _GetAccountState int32 = 1804144165 const _RunQueryResult int32 = -1550163605 +const _AccountState int32 = 1887029073 + +const _LSError int32 = -1146494648 type LiteClient interface { Do(ctx context.Context, typeID int32, payload []byte) (*liteclient.LiteResponse, error) @@ -34,281 +28,3 @@ func NewAPIClient(client LiteClient) *APIClient { client: client, } } - -func (c *APIClient) GetBlockInfo(ctx context.Context) (*tlb.BlockInfo, error) { - resp, err := c.client.Do(ctx, _GetMasterchainInfo, nil) - if err != nil { - return nil, err - } - - block := new(tlb.BlockInfo) - _, err = block.Load(resp.Data) - if err != nil { - return nil, err - } - - return block, nil -} - -func (c *APIClient) RunGetMethod(ctx context.Context, blockInfo *tlb.BlockInfo, addr *address.Address, method string, params ...interface{}) ([]interface{}, error) { - data := make([]byte, 4) - binary.LittleEndian.PutUint32(data, 0b00000100) - - data = append(data, blockInfo.Serialize()...) - - chain := make([]byte, 4) - binary.LittleEndian.PutUint32(chain, uint32(addr.Workchain())) - - data = append(data, chain...) - data = append(data, addr.Data()...) - - // https://github.com/ton-blockchain/ton/blob/24dc184a2ea67f9c47042b4104bbb4d82289fac1/crypto/smc-envelope/SmartContract.h#L75 - mName := make([]byte, 8) - crc := uint64(crc16.Checksum([]byte(method), crc16.MakeTable(crc16.CRC16_XMODEM))) | 0x10000 - binary.LittleEndian.PutUint64(mName, crc) - data = append(data, mName...) - - builder := cell.BeginCell().MustStoreUInt(0, 16).MustStoreUInt(uint64(len(params)), 8) - if len(params) > 0 { - // we need to send in reverse order - reversed := make([]any, len(params)) - for i := 0; i < len(params); i++ { - reversed[(len(params)-1)-i] = params[i] - } - params = reversed - - var refNext = cell.BeginCell().EndCell() - - for i := len(params) - 1; i >= 0; i-- { - switch v := params[i].(type) { - case uint64, uint32, uint16, uint8, uint, int: - var val uint64 - switch x := params[i].(type) { - case int: - val = uint64(x) - case uint: - val = uint64(x) - case uint32: - val = uint64(x) - case uint16: - val = uint64(x) - case uint8: - val = uint64(x) - case uint64: - val = x - } - - if i == 0 { - builder.MustStoreUInt(1, 8).MustStoreUInt(val, 64).MustStoreRef(refNext) - break - } - refNext = cell.BeginCell().MustStoreUInt(1, 8).MustStoreUInt(val, 64).MustStoreRef(refNext).EndCell() - case *big.Int: - if i == 0 { - builder.MustStoreUInt(2, 8).MustStoreBigInt(v, 256).MustStoreRef(refNext) - break - } - refNext = cell.BeginCell().MustStoreUInt(2, 8).MustStoreBigInt(v, 256).MustStoreRef(refNext).EndCell() - case *cell.Cell: - if i == 0 { - builder.MustStoreUInt(3, 8).MustStoreRef(refNext).MustStoreRef(v) - break - } - refNext = cell.BeginCell().MustStoreUInt(3, 8).MustStoreRef(refNext).MustStoreRef(v).EndCell() - case []byte: - ln := 8 * len(v) - - sCell := cell.BeginCell() - if err := sCell.StoreSlice(v, ln); err != nil { - return nil, err - } - - if i == 0 { - builder.MustStoreUInt(4, 8). - MustStoreUInt(0, 10). - MustStoreUInt(uint64(ln), 10). - MustStoreUInt(0, 6). - MustStoreRef(refNext).MustStoreRef(sCell.EndCell()) - break - } - refNext = cell.BeginCell().MustStoreUInt(4, 8). - MustStoreUInt(0, 10). - MustStoreUInt(uint64(ln), 10). - MustStoreUInt(0, 6). - MustStoreRef(refNext).MustStoreRef(sCell.EndCell()).EndCell() - default: - // TODO: auto convert if possible - return nil, errors.New("only ints, uints, *big.Int, *cell.Cell, and []byte allowed as contract args") - } - } - } - - req := builder.EndCell().ToBOCWithFlags(false) - - // param - data = append(data, byte(len(req))) - data = append(data, req...) - - if round := (len(req) + 1) % 4; round != 0 { - data = append(data, make([]byte, 4-round)...) - } - - resp, err := c.client.Do(ctx, _RunContractGetMethod, data) - if err != nil { - return nil, err - } - - switch resp.TypeID { - case _RunQueryResult: - // TODO: mode - _ = binary.LittleEndian.Uint32(resp.Data) - - resp.Data = resp.Data[4:] - - b := new(tlb.BlockInfo) - resp.Data, err = b.Load(resp.Data) - if err != nil { - return nil, err - } - - shard := new(tlb.BlockInfo) - resp.Data, err = shard.Load(resp.Data) - if err != nil { - return nil, err - } - - // TODO: check proofs mode - - exitCode := binary.LittleEndian.Uint32(resp.Data) - resp.Data = resp.Data[4:] - - if exitCode != 0 { - return nil, fmt.Errorf("contract exit code: %d", exitCode) - } - - var state []byte - state, resp.Data = loadBytes(resp.Data) - - cl, err := cell.FromBOC(state) - if err != nil { - return nil, err - } - - loader := cl.BeginParse() - - // TODO: better parsing - // skip first 2 bytes, dont know exact meaning yet, but not so critical - _, err = loader.LoadUInt(16) - if err != nil { - return nil, err - } - - // return params num - num, err := loader.LoadUInt(8) - if err != nil { - return nil, err - } - - var result []any - - for i := 0; i < int(num); i++ { - // value type - typ, err := loader.LoadUInt(8) - if err != nil { - return nil, err - } - - next, err := loader.LoadRef() - if err != nil { - return nil, err - } - - switch typ { - case 1: // uint64 - val, err := loader.LoadUInt(64) - if err != nil { - return nil, err - } - - result = append(result, val) - case 2: // uint256 - val, err := loader.LoadBigInt(256) - if err != nil { - return nil, err - } - - result = append(result, val) - case 3: // cell - ref, err := loader.LoadRef() - if err != nil { - return nil, err - } - - c, err := ref.ToCell() - if err != nil { - return nil, err - } - - result = append(result, c) - case 4: // slice - begin, err := loader.LoadUInt(10) - if err != nil { - return nil, err - } - end, err := loader.LoadUInt(10) - if err != nil { - return nil, err - } - - // TODO: load ref ids - - ref, err := loader.LoadRef() - if err != nil { - return nil, err - } - - // seek to begin - _, err = ref.LoadSlice(int(begin)) - if err != nil { - return nil, err - } - - sz := int(end - begin) - payload, err := ref.LoadSlice(sz) - if err != nil { - return nil, err - } - - // represent slice as cell, to parse it easier - result = append(result, cell.BeginCell().MustStoreSlice(payload, sz).EndCell()) - default: - return nil, errors.New("unknown data type of result in response: " + fmt.Sprint(typ)) - } - - loader = next - } - - // it comes in reverse order from lite server, reverse it back - reversed := make([]any, len(result)) - for i := 0; i < len(result); i++ { - reversed[(len(result)-1)-i] = result[i] - } - - return reversed, nil - case LSError: - return nil, fmt.Errorf("lite server error, code %d: %s", binary.LittleEndian.Uint32(resp.Data), string(resp.Data[5:])) - } - - return nil, errors.New("unknown response type") -} - -func loadBytes(data []byte) (loaded []byte, buffer []byte) { - offset := 1 - ln := int(data[0]) - if ln == 0xFE { - ln = int(binary.LittleEndian.Uint32(data)) >> 8 - offset = 4 - } - - return data[offset : offset+ln], data[offset+ln:] -} diff --git a/ton/blockinfo.go b/ton/blockinfo.go new file mode 100644 index 00000000..717e98f8 --- /dev/null +++ b/ton/blockinfo.go @@ -0,0 +1,22 @@ +package ton + +import ( + "context" + + "github.com/xssnick/tonutils-go/liteclient/tlb" +) + +func (c *APIClient) GetBlockInfo(ctx context.Context) (*tlb.BlockInfo, error) { + resp, err := c.client.Do(ctx, _GetMasterchainInfo, nil) + if err != nil { + return nil, err + } + + block := new(tlb.BlockInfo) + _, err = block.Load(resp.Data) + if err != nil { + return nil, err + } + + return block, nil +} diff --git a/ton/getstate.go b/ton/getstate.go new file mode 100644 index 00000000..1957cbcf --- /dev/null +++ b/ton/getstate.go @@ -0,0 +1,102 @@ +package ton + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/liteclient/tlb" + "github.com/xssnick/tonutils-go/tvm/cell" +) + +type Account struct { + IsActive bool + State *tlb.AccountState + Data *cell.Cell + Code *cell.Cell +} + +func (c *APIClient) GetAccount(ctx context.Context, block *tlb.BlockInfo, addr *address.Address) (*Account, error) { + data := block.Serialize() + + chain := make([]byte, 4) + binary.LittleEndian.PutUint32(chain, uint32(addr.Workchain())) + + data = append(data, chain...) + data = append(data, addr.Data()...) + + resp, err := c.client.Do(ctx, _GetAccountState, data) + if err != nil { + return nil, err + } + + switch resp.TypeID { + case _AccountState: + b := new(tlb.BlockInfo) + resp.Data, err = b.Load(resp.Data) + if err != nil { + return nil, err + } + + shard := new(tlb.BlockInfo) + resp.Data, err = shard.Load(resp.Data) + if err != nil { + return nil, err + } + + var shardProof []byte + shardProof, resp.Data = loadBytes(resp.Data) + _ = shardProof + + var proof []byte + proof, resp.Data = loadBytes(resp.Data) + _ = proof + + var state []byte + state, resp.Data = loadBytes(resp.Data) + + cl, err := cell.FromBOC(state) + if err != nil { + return nil, err + } + + loader := cl.BeginParse() + + contractCode, err := loader.LoadRef() + if err != nil { + return nil, err + } + contractData, err := loader.LoadRef() + if err != nil { + return nil, err + } + + contractCodeCell, err := contractCode.ToCell() + if err != nil { + return nil, err + } + contractDataCell, err := contractData.ToCell() + if err != nil { + return nil, err + } + + var st tlb.AccountState + err = st.LoadFromCell(loader) + if err != nil { + return nil, err + } + + return &Account{ + IsActive: false, + State: &st, + Data: contractDataCell, + Code: contractCodeCell, + }, nil + case _LSError: + return nil, fmt.Errorf("lite server error, code %d: %s", binary.LittleEndian.Uint32(resp.Data), string(resp.Data[5:])) + } + + return nil, errors.New("unknown response type") +} diff --git a/ton/runmethod.go b/ton/runmethod.go new file mode 100644 index 00000000..bebacbbb --- /dev/null +++ b/ton/runmethod.go @@ -0,0 +1,288 @@ +package ton + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "math/big" + + "github.com/sigurn/crc16" + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/liteclient/tlb" + "github.com/xssnick/tonutils-go/tvm/cell" +) + +func (c *APIClient) RunGetMethod(ctx context.Context, blockInfo *tlb.BlockInfo, addr *address.Address, method string, params ...interface{}) ([]interface{}, error) { + data := make([]byte, 4) + binary.LittleEndian.PutUint32(data, 0b00000100) + + data = append(data, blockInfo.Serialize()...) + + chain := make([]byte, 4) + binary.LittleEndian.PutUint32(chain, uint32(addr.Workchain())) + + data = append(data, chain...) + data = append(data, addr.Data()...) + + // https://github.com/ton-blockchain/ton/blob/24dc184a2ea67f9c47042b4104bbb4d82289fac1/crypto/smc-envelope/SmartContract.h#L75 + mName := make([]byte, 8) + crc := uint64(crc16.Checksum([]byte(method), crc16.MakeTable(crc16.CRC16_XMODEM))) | 0x10000 + binary.LittleEndian.PutUint64(mName, crc) + data = append(data, mName...) + + builder := cell.BeginCell().MustStoreUInt(0, 16).MustStoreUInt(uint64(len(params)), 8) + if len(params) > 0 { + // we need to send in reverse order + reversed := make([]any, len(params)) + for i := 0; i < len(params); i++ { + reversed[(len(params)-1)-i] = params[i] + } + params = reversed + + var refNext = cell.BeginCell().EndCell() + + for i := len(params) - 1; i >= 0; i-- { + switch v := params[i].(type) { + case uint64, uint32, uint16, uint8, uint, int: + var val uint64 + switch x := params[i].(type) { + case int: + val = uint64(x) + case uint: + val = uint64(x) + case uint32: + val = uint64(x) + case uint16: + val = uint64(x) + case uint8: + val = uint64(x) + case uint64: + val = x + } + + if i == 0 { + builder.MustStoreUInt(1, 8).MustStoreUInt(val, 64).MustStoreRef(refNext) + break + } + refNext = cell.BeginCell().MustStoreUInt(1, 8).MustStoreUInt(val, 64).MustStoreRef(refNext).EndCell() + case *big.Int: + if i == 0 { + builder.MustStoreUInt(2, 8).MustStoreBigInt(v, 256).MustStoreRef(refNext) + break + } + refNext = cell.BeginCell().MustStoreUInt(2, 8).MustStoreBigInt(v, 256).MustStoreRef(refNext).EndCell() + case *cell.Cell: + if i == 0 { + builder.MustStoreUInt(3, 8).MustStoreRef(refNext).MustStoreRef(v) + break + } + refNext = cell.BeginCell().MustStoreUInt(3, 8).MustStoreRef(refNext).MustStoreRef(v).EndCell() + case []byte: + ln := 8 * len(v) + + sCell := cell.BeginCell() + if err := sCell.StoreSlice(v, ln); err != nil { + return nil, err + } + + if i == 0 { + builder.MustStoreUInt(4, 8). + MustStoreUInt(0, 10). + MustStoreUInt(uint64(ln), 10). + MustStoreUInt(0, 6). + MustStoreRef(refNext).MustStoreRef(sCell.EndCell()) + break + } + refNext = cell.BeginCell().MustStoreUInt(4, 8). + MustStoreUInt(0, 10). + MustStoreUInt(uint64(ln), 10). + MustStoreUInt(0, 6). + MustStoreRef(refNext).MustStoreRef(sCell.EndCell()).EndCell() + default: + // TODO: auto convert if possible + return nil, errors.New("only integers, uints, *big.Int, *cell.Cell, and []byte allowed as contract args") + } + } + } + + req := builder.EndCell().ToBOCWithFlags(false) + + // param + data = append(data, byte(len(req))) + data = append(data, req...) + + if round := (len(req) + 1) % 4; round != 0 { + data = append(data, make([]byte, 4-round)...) + } + + resp, err := c.client.Do(ctx, _RunContractGetMethod, data) + if err != nil { + return nil, err + } + + switch resp.TypeID { + case _RunQueryResult: + // TODO: mode + _ = binary.LittleEndian.Uint32(resp.Data) + + resp.Data = resp.Data[4:] + + b := new(tlb.BlockInfo) + resp.Data, err = b.Load(resp.Data) + if err != nil { + return nil, err + } + + shard := new(tlb.BlockInfo) + resp.Data, err = shard.Load(resp.Data) + if err != nil { + return nil, err + } + + // TODO: check proofs mode + + exitCode := binary.LittleEndian.Uint32(resp.Data) + resp.Data = resp.Data[4:] + + if exitCode != 0 { + return nil, fmt.Errorf("contract exit code: %d", exitCode) + } + + var state []byte + state, resp.Data = loadBytes(resp.Data) + + cl, err := cell.FromBOC(state) + if err != nil { + return nil, err + } + + loader := cl.BeginParse() + + // TODO: better parsing + // skip first 2 bytes, dont know exact meaning yet, but not so critical + _, err = loader.LoadUInt(16) + if err != nil { + return nil, err + } + + // return params num + num, err := loader.LoadUInt(8) + if err != nil { + return nil, err + } + + var result []any + + for i := 0; i < int(num); i++ { + // value type + typ, err := loader.LoadUInt(8) + if err != nil { + return nil, err + } + + next, err := loader.LoadRef() + if err != nil { + return nil, err + } + + switch typ { + case 1: // uint64 + val, err := loader.LoadUInt(64) + if err != nil { + return nil, err + } + + result = append(result, val) + case 2: // uint256 + val, err := loader.LoadBigInt(256) + if err != nil { + return nil, err + } + + result = append(result, val) + case 3: // cell + ref, err := loader.LoadRef() + if err != nil { + return nil, err + } + + c, err := ref.ToCell() + if err != nil { + return nil, err + } + + result = append(result, c) + case 4: // slice + begin, err := loader.LoadUInt(10) + if err != nil { + return nil, err + } + end, err := loader.LoadUInt(10) + if err != nil { + return nil, err + } + + // TODO: load ref ids + + ref, err := loader.LoadRef() + if err != nil { + return nil, err + } + + // seek to begin + _, err = ref.LoadSlice(int(begin)) + if err != nil { + return nil, err + } + + sz := int(end - begin) + payload, err := ref.LoadSlice(sz) + if err != nil { + return nil, err + } + + // represent slice as cell, to parse it easier + result = append(result, cell.BeginCell().MustStoreSlice(payload, sz).EndCell()) + default: + return nil, errors.New("unknown data type of result in response: " + fmt.Sprint(typ)) + } + + loader = next + } + + // it comes in reverse order from lite server, reverse it back + reversed := make([]any, len(result)) + for i := 0; i < len(result); i++ { + reversed[(len(result)-1)-i] = result[i] + } + + return reversed, nil + case _LSError: + return nil, fmt.Errorf("lite server error, code %d: %s", binary.LittleEndian.Uint32(resp.Data), string(resp.Data[5:])) + } + + return nil, errors.New("unknown response type") +} + +func loadBytes(data []byte) (loaded []byte, buffer []byte) { + offset := 1 + ln := int(data[0]) + if ln == 0xFE { + ln = int(binary.LittleEndian.Uint32(data)) >> 8 + offset = 4 + } + + // bytes length should be dividable by 4, add additional offset to buffer if it is not + bufSz := ln + if add := ln % 4; add != 0 { + bufSz += 4 - add + } + + // if its end, we don't need to align by 4 + if offset+bufSz >= len(data) { + return data[offset : offset+ln], nil + } + + return data[offset : offset+ln], data[offset+bufSz:] +} diff --git a/tvm/boc/boc.go b/tvm/boc/boc.go new file mode 100644 index 00000000..036a2ad1 --- /dev/null +++ b/tvm/boc/boc.go @@ -0,0 +1,21 @@ +package boc + +import ( + "github.com/xssnick/tonutils-go/utils" +) + +type BocFlags struct { + hasIndex bool + HasCrc32c bool + hasCacheBits bool +} + +var Magic = []byte{0xB5, 0xEE, 0x9C, 0x72} + +func ParseFlags(data byte) (BocFlags, int) { + return BocFlags{ + hasIndex: utils.HasBit(data, 7), + HasCrc32c: utils.HasBit(data, 6), + hasCacheBits: utils.HasBit(data, 5), + }, int(data & 0b00000111) +} diff --git a/tvm/cell/cell.go b/tvm/cell/cell.go index 66589d8b..b58af7ce 100644 --- a/tvm/cell/cell.go +++ b/tvm/cell/cell.go @@ -35,13 +35,14 @@ func (c *Cell) Dump() string { } func (c *Cell) dump(deep int) string { - str := strings.Repeat(" ", deep) + "[" + hex.EncodeToString(c.data) + "]" + " -> {" + str := strings.Repeat(" ", deep) + "[" + hex.EncodeToString(c.data) + "]" if len(c.refs) > 0 { + str += " -> {" for _, ref := range c.refs { str += "\n" + ref.dump(deep+1) + ", " + "\n" } str += strings.Repeat(" ", deep) + return str + "}" } - - return str + "}" + return str } diff --git a/tvm/cell/load.go b/tvm/cell/load.go index 76cc337d..fc41870b 100644 --- a/tvm/cell/load.go +++ b/tvm/cell/load.go @@ -84,6 +84,20 @@ func (c *LoadCell) LoadBigInt(sz int) (*big.Int, error) { return new(big.Int).SetBytes(b), nil } +func (c *LoadCell) LoadVarUInt(sz int) (*big.Int, error) { + ln, err := c.LoadUInt(big.NewInt(int64(sz - 1)).BitLen()) + if err != nil { + return nil, err + } + + value, err := c.LoadBigInt(int(ln * 8)) + if err != nil { + return nil, err + } + + return value, nil +} + func (c *LoadCell) LoadSlice(sz int) ([]byte, error) { if c.bitsSz-c.loadedSz < sz { return nil, ErrNotEnoughData diff --git a/tvm/cell/parse.go b/tvm/cell/parse.go index bcf1fbc7..eb24967d 100644 --- a/tvm/cell/parse.go +++ b/tvm/cell/parse.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "errors" "fmt" + "github.com/xssnick/tonutils-go/tvm/boc" "hash/crc32" ) @@ -31,34 +32,33 @@ func fromBOCMultiRoot(data []byte) ([]Cell, error) { r := newReader(data) - if !bytes.Equal(r.MustReadBytes(4), magic) { + if !bytes.Equal(r.MustReadBytes(4), boc.Magic) { return nil, errors.New("invalid boc magic header") } - flags := r.MustReadByte() + flags, cellNumSizeBytes := boc.ParseFlags(r.MustReadByte()) //has_idx:(## 1) has_crc32c:(## 1) has_cache_bits:(## 1) flags:(## 2) { flags = 0 } size:(## 3) { size <= 4 } + dataSizeBytes := int(r.MustReadByte()) //off_bytes:(## 8) { off_bytes <= 8 } - cellNumSizeBytes := int(flags & 0b111) - dataSizeBytes := int(r.MustReadByte()) + cellsNum := dynInt(r.MustReadBytes(cellNumSizeBytes)) //cells:(##(size * 8)) + rootsNum := dynInt(r.MustReadBytes(cellNumSizeBytes)) //roots:(##(size * 8)) { roots >= 1 } - cellsNum := dynInt(r.MustReadBytes(cellNumSizeBytes)) - rootsNum := int(r.MustReadByte()) + // complete BOCs - ??? (absent:(##(size * 8)) { roots + absent <= cells }) + _ = r.MustReadBytes(cellNumSizeBytes) - // complete BOCs - ??? - _ = r.MustReadByte() - - dataLen := dynInt(r.MustReadBytes(dataSizeBytes)) + dataLen := dynInt(r.MustReadBytes(dataSizeBytes)) //tot_cells_size:(##(off_bytes * 8)) // with checksum - if flags&0b01000000 != 0 { + if flags.HasCrc32c { crc := crc32.Checksum(data[:len(data)-4], crc32.MakeTable(crc32.Castagnoli)) if binary.LittleEndian.Uint32(data[len(data)-4:]) != crc { return nil, errors.New("checksum not matches") } } - rootIndex := r.MustReadByte() + rootList := r.MustReadBytes(rootsNum * cellNumSizeBytes) //root_list:(roots * ##(size * 8)) + rootIndex := dynInt(rootList[0:cellNumSizeBytes]) if rootIndex != 0 { - return nil, fmt.Errorf("first root index should be 0, but it is %d", rootIndex) + // return nil, fmt.Errorf("first root index should be 0, but it is %d", rootIndex) } payload, err := r.ReadBytes(dataLen) diff --git a/tvm/cell/serialize.go b/tvm/cell/serialize.go index 08bbe3f5..1a434d01 100644 --- a/tvm/cell/serialize.go +++ b/tvm/cell/serialize.go @@ -3,6 +3,7 @@ package cell import ( "encoding/binary" "errors" + "github.com/xssnick/tonutils-go/tvm/boc" "hash/crc32" "hash/crc64" ) @@ -14,8 +15,6 @@ var ErrTooMuchRefs = errors.New("too much refs") var ErNotFit1024 = errors.New("cell data size should fit into 1024 bits") var ErrNoMoreRefs = errors.New("no more refs exists") -var magic = []byte{0xB5, 0xEE, 0x9C, 0x72} - func (c *Cell) ToBOC() []byte { return c.ToBOCWithFlags(true) } @@ -74,7 +73,7 @@ func (c *Cell) ToBOCWithFlags(withCRC bool) []byte { var data []byte - data = append(data, magic...) + data = append(data, boc.Magic...) data = append(data, flags) // bytes needed to store size