Skip to content

Commit

Permalink
Merge pull request #23 from xssnick/ext-msg
Browse files Browse the repository at this point in the history
External message support
  • Loading branch information
xssnick authored May 16, 2022
2 parents 75e1ca0 + ee6f62b commit 80d7fd2
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 34 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,25 @@ fmt.Printf("Balance: %s TON\n", res.State.Balance.TON())
fmt.Printf("Data: %s", res.Data.Dump())
```
You can find full working example at `example/account-state/main.go`

### Send external message
Using messages you can interact with contracts to modify state, for example it can be used to intercat with wallet and send transactions to others.

You can send message to contract like that:
```golang
data := cell.BeginCell().
MustStoreUInt(777, 64).
EndCell()

err = api.SendExternalMessage(context.Background(), address.MustParseAddr("kQBkh8dcas3_OB0uyFEDdVBBSpAWNEgdQ66OYF76N4cDXAFQ"), data)
if err != nil {
log.Printf("send err: %s", err.Error())
return
}
```
You can find full working example at `example/external-message/main.go` Wallet-like case is implemented there, but without signature.
### 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.
By default, standard reconnect method will be used - `c.DefaultReconnect(3*time.Second, 3)` which will do 3 tries and wait 3 seconds after each.

But you can use your own reconnection logic, this library support callbacks, in this case OnDisconnect callback can be used, you can set it like this:
```golang
Expand All @@ -104,7 +121,8 @@ client.SetOnDisconnect(func(addr, serverKey string) {
* ✅ Support cell and slice as arguments for run get method
* ✅ Reconnect on failure
* ✅ Get account state method
* Send external query method
* ✅ Send external message
* Deploy contract method
* Cell dictionaries support
* MustLoad methods
* Event subscriptions
Expand Down
6 changes: 4 additions & 2 deletions address/addr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,9 @@ func TestMustParseAddr(t *testing.T) {

func TestNewAddressFromBytes(t *testing.T) {
type args struct {
bytes []byte
flags byte
workchain byte
data []byte
}
tests := []struct {
name string
Expand All @@ -400,7 +402,7 @@ func TestNewAddressFromBytes(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := NewAddressFromBytes(tt.args.bytes); !reflect.DeepEqual(got, tt.want) {
if got := NewAddress(tt.args.flags, tt.args.workchain, tt.args.data); !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewAddressFromBytes() = %v, want %v", got, tt.want)
}
})
Expand Down
88 changes: 88 additions & 0 deletions example/external-message/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package main

import (
"context"
"log"

"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/ton"
"github.com/xssnick/tonutils-go/tvm/cell"
)

/*
This example is for such contract.
It is recommended to deploy your own before run this script
because this address can have not enough TON due to many executions of this example.
Or you can at least add some coins to contract address
() recv_external(slice in_msg) impure {
int seqno = in_msg~load_uint(64);
int n = in_msg.preload_uint(16);
var data = get_data().begin_parse();
int stored_seq = data~load_uint(64);
throw_if(409, seqno != stored_seq);
accept_message();
int total = data.preload_uint(64);
set_data(begin_cell().store_uint(stored_seq + 1, 64).store_uint(total + n, 64).end_cell());
}
(int, int) get_total() method_id {
var data = get_data().begin_parse();
int stored_seq = data~load_uint(64);
return (stored_seq, data.preload_uint(64));
}
*/

func main() {
client := liteclient.NewClient()

// connect to testnet lite server
err := client.Connect(context.Background(), "65.21.74.140:46427", "JhXt7H1dZTgxQTIyGiYV4f9VUARuDxFl/1kVBjLSMB8=")
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
block, err := api.GetBlockInfo(context.Background())
if err != nil {
log.Fatalln("get block err:", err.Error())
return
}

// call method to get seqno of contract
res, err := api.RunGetMethod(context.Background(), block, address.MustParseAddr("kQBkh8dcas3_OB0uyFEDdVBBSpAWNEgdQ66OYF76N4cDXAFQ"), "get_total")
if err != nil {
log.Fatalln("run get method err:", err.Error())
return
}

seqno := res[0].(uint64)
total := res[1].(uint64)

log.Printf("Current seqno = %d and total = %d", seqno, total)

data := cell.BeginCell().
MustStoreUInt(seqno, 64).
MustStoreUInt(1, 16). // add 1 to total
EndCell()

err = api.SendExternalMessage(context.Background(), address.MustParseAddr("kQBkh8dcas3_OB0uyFEDdVBBSpAWNEgdQ66OYF76N4cDXAFQ"), data)
if err != nil {
// FYI: it can fail if not enough balance on contract
log.Printf("send err: %s", err.Error())
return
}

log.Println("External message successfully processed and should be added to blockchain soon!")
log.Println("Rerun this script in a couple seconds and you should see total and seqno changed.")
}
47 changes: 47 additions & 0 deletions ton/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ package ton

import (
"context"
"encoding/binary"

"github.com/xssnick/tonutils-go/liteclient"
)

const _GetMasterchainInfo int32 = -1984567762
const _RunContractGetMethod int32 = 1556504018
const _GetAccountState int32 = 1804144165
const _SendMessage int32 = 1762317442

const _RunQueryResult int32 = -1550163605
const _AccountState int32 = 1887029073
const _SendMessageResult = 961602967

const _LSError int32 = -1146494648

Expand All @@ -28,3 +31,47 @@ func NewAPIClient(client LiteClient) *APIClient {
client: client,
}
}

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:]
}

func storableBytes(buf []byte) []byte {
var data []byte

// store buf length
if len(buf) >= 0xFE {
ln := make([]byte, 4)
binary.LittleEndian.PutUint32(data, uint32(len(buf)<<8)|0xFE)
data = append(data, ln...)
} else {
data = append(data, byte(len(buf)))
}

data = append(data, buf...)

// adjust actual length to fit % 4 = 0
if round := (len(buf) + 1) % 4; round != 0 {
data = append(data, make([]byte, 4-round)...)
}

return data
}
8 changes: 7 additions & 1 deletion ton/getstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ func (c *APIClient) GetAccount(ctx context.Context, block *tlb.BlockInfo, addr *
var state []byte
state, resp.Data = loadBytes(resp.Data)

if len(state) == 0 {
return &Account{
IsActive: false,
}, nil
}

cl, err := cell.FromBOC(state)
if err != nil {
return nil, err
Expand Down Expand Up @@ -89,7 +95,7 @@ func (c *APIClient) GetAccount(ctx context.Context, block *tlb.BlockInfo, addr *
}

return &Account{
IsActive: false,
IsActive: true,
State: &st,
Data: contractDataCell,
Code: contractCodeCell,
Expand Down
29 changes: 1 addition & 28 deletions ton/runmethod.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,7 @@ func (c *APIClient) RunGetMethod(ctx context.Context, blockInfo *tlb.BlockInfo,
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)...)
}
data = append(data, storableBytes(req)...)

resp, err := c.client.Do(ctx, _RunContractGetMethod, data)
if err != nil {
Expand Down Expand Up @@ -264,25 +259,3 @@ func (c *APIClient) RunGetMethod(ctx context.Context, blockInfo *tlb.BlockInfo,

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:]
}
63 changes: 63 additions & 0 deletions ton/sendmessage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package ton

import (
"context"
"encoding/binary"
"errors"
"fmt"

"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tvm/cell"
)

var ErrMessageNotAccepted = errors.New("message was not accepted by the contract")

func (c *APIClient) SendExternalMessage(ctx context.Context, addr *address.Address, msg *cell.Cell) error {
return c.sendExternalMessage(ctx, addr, msg)
}

func (c *APIClient) sendExternalMessage(ctx context.Context, addr *address.Address, msg any) error {
builder := cell.BeginCell().MustStoreUInt(0b10, 2).
MustStoreUInt(0b00, 2). // src addr_none
MustStoreAddr(addr). // dst addr
MustStoreCoins(0) // import fee 0

builder.MustStoreUInt(0, 1) // no state init

switch d := msg.(type) {
case []byte: // slice data
builder.MustStoreUInt(0, 1).MustStoreSlice(d, len(d)*8)
case *cell.Cell: // cell data
builder.MustStoreUInt(1, 1).MustStoreRef(d)
default:
return errors.New("unknown arg type")
}

req := builder.EndCell().ToBOCWithFlags(false)

resp, err := c.client.Do(ctx, _SendMessage, storableBytes(req))
if err != nil {
return err
}

switch resp.TypeID {
case _SendMessageResult:
// TODO: mode
status := binary.LittleEndian.Uint32(resp.Data)

if status != 1 {
return fmt.Errorf("status: %d", status)
}

return nil
case _LSError:
code := binary.LittleEndian.Uint32(resp.Data)
if code == 0 {
return ErrMessageNotAccepted
}

return fmt.Errorf("lite server error, code %d: %s", code, string(resp.Data[5:]))
}

return errors.New("unknown response type")
}
40 changes: 40 additions & 0 deletions tvm/cell/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package cell

import (
"math/big"

"github.com/xssnick/tonutils-go/address"
)

type Builder struct {
Expand Down Expand Up @@ -110,6 +112,44 @@ func (b *Builder) StoreBigInt(value *big.Int, sz int) error {
return b.StoreSlice(bytes, sz)
}

func (b *Builder) MustStoreAddr(addr *address.Address) *Builder {
err := b.StoreAddr(addr)
if err != nil {
panic(err)
}
return b
}

func (b *Builder) StoreAddr(addr *address.Address) error {
if addr == nil {
return b.StoreUInt(0, 2)
}

// addr std
err := b.StoreUInt(0b10, 2)
if err != nil {
return err
}

// anycast
err = b.StoreUInt(0b0, 1)
if err != nil {
return err
}

err = b.StoreUInt(uint64(addr.Workchain()), 8)
if err != nil {
return err
}

err = b.StoreSlice(addr.Data(), 256)
if err != nil {
return err
}

return nil
}

func (b *Builder) MustStoreRef(ref *Cell) *Builder {
err := b.StoreRef(ref)
if err != nil {
Expand Down
Loading

0 comments on commit 80d7fd2

Please sign in to comment.