diff --git a/README.md b/README.md index 718a7c87..6a5c3451 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Based on TON][ton-svg]][ton] -![Coverage](https://img.shields.io/badge/Coverage-73.7%25-brightgreen) +![Coverage](https://img.shields.io/badge/Coverage-73.8%25-brightgreen) Golang library for interacting with TON blockchain. diff --git a/adnl/dht/client_test.go b/adnl/dht/client_test.go index 8763a823..43a433de 100644 --- a/adnl/dht/client_test.go +++ b/adnl/dht/client_test.go @@ -536,7 +536,7 @@ func TestClient_FindAddressesIntegration(t *testing.T) { } // restore after unit tests - testAddr := "516618cf6cbe9004f6883e742c9a2e3ca53ed02e3e36f4cef62a98ee1e449174" // ADNL address of foundation.ton + testAddr := "89bea091caf4273d38b0dc24944d8798e057abcfa6ac08d5e0b26284c5c0609a" // ADNL address of utils.ton ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() diff --git a/adnl/rldp/http/client_test.go b/adnl/rldp/http/client_test.go index a1bb30ff..32f8e402 100644 --- a/adnl/rldp/http/client_test.go +++ b/adnl/rldp/http/client_test.go @@ -420,7 +420,7 @@ func TestTransport_RoundTripIntegration(t *testing.T) { transport := NewTransport(dhtClient, getDNSResolver()) - req, err := http.NewRequest(http.MethodGet, "http://foundation.ton/", nil) + req, err := http.NewRequest(http.MethodGet, "http://utils.ton/", nil) if err != nil { t.Fatal(err) } diff --git a/example/deploy-nft-collection/main.go b/example/deploy-nft-collection/main.go index 10b5ae4f..d6a3692f 100644 --- a/example/deploy-nft-collection/main.go +++ b/example/deploy-nft-collection/main.go @@ -34,8 +34,8 @@ func main() { msgBody := cell.BeginCell().EndCell() fmt.Println("Deploying NFT collection contract to mainnet...") - addr, err := w.DeployContract(context.Background(), tlb.MustFromTON("0.02"), - msgBody, getNFTCollectionCode(), getContractData(w.WalletAddress(), w.WalletAddress()), true) + addr, _, _, err := w.DeployContractWaitTransaction(context.Background(), tlb.MustFromTON("0.02"), + msgBody, getNFTCollectionCode(), getContractData(w.WalletAddress(), w.WalletAddress())) if err != nil { panic(err) } diff --git a/example/dns/main.go b/example/dns/main.go index c81986ad..305a98d1 100644 --- a/example/dns/main.go +++ b/example/dns/main.go @@ -24,7 +24,7 @@ func main() { api := ton.NewAPIClient(client).WithRetry() // get root dns address from network config - root, err := dns.RootContractAddr(api) + root, err := dns.GetRootContractAddr(context.Background(), api) if err != nil { panic(err) } diff --git a/example/highload-wallet/main.go b/example/highload-wallet/main.go index 3bb8f32e..883a9b82 100644 --- a/example/highload-wallet/main.go +++ b/example/highload-wallet/main.go @@ -84,7 +84,7 @@ func main() { for addrStr, amtStr := range receivers { addr := address.MustParseAddr(addrStr) messages = append(messages, &wallet.Message{ - Mode: 1 + 2, // pay fee separately, ignore action errors + Mode: wallet.PayGasSeparately + wallet.IgnoreErrors, // pay fee separately, ignore action errors InternalMessage: &tlb.InternalMessage{ IHRDisabled: true, // disable hyper routing (currently not works in ton) Bounce: addr.IsBounceable(), diff --git a/example/send-to-contract/main.go b/example/send-to-contract/main.go index 41163a44..5ea514ac 100644 --- a/example/send-to-contract/main.go +++ b/example/send-to-contract/main.go @@ -82,7 +82,7 @@ func main() { log.Println("sending transaction and waiting for confirmation...") tx, block, err := w.SendWaitTransaction(context.Background(), &wallet.Message{ - Mode: 1, // pay fees separately (from balance, not from amount) + Mode: wallet.PayGasSeparately, // pay fees separately (from balance, not from amount) InternalMessage: &tlb.InternalMessage{ Bounce: true, // return amount in case of processing error DstAddr: address.MustParseAddr("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N"), diff --git a/example/site-request/main.go b/example/site-request/main.go index 65a58cf1..34b47cbd 100644 --- a/example/site-request/main.go +++ b/example/site-request/main.go @@ -63,7 +63,7 @@ func getDNSResolver() *dns.Client { api := ton.NewAPIClient(client) // get root dns address from network config - root, err := dns.RootContractAddr(api) + root, err := dns.GetRootContractAddr(context.Background(), api) if err != nil { panic(err) } diff --git a/liteclient/integration_test.go b/liteclient/integration_test.go index fb172f81..c03ff8e3 100644 --- a/liteclient/integration_test.go +++ b/liteclient/integration_test.go @@ -87,13 +87,17 @@ func Test_ConnSticky(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - ctx = client.StickyContext(ctx) err := client.AddConnectionsFromConfigUrl(ctx, "https://tonutils.com/global.config.json") if err != nil { t.Fatal("add connections err", err) } + ctx, err = client.StickyContextNextNodeBalanced(ctx) + if err != nil { + t.Fatal("next balanced err", err) + } + doReq := func(expErr error) { var resp tl.Serializable err := client.QueryLiteserver(ctx, GetMasterchainInf{}, &resp) diff --git a/liteclient/pool.go b/liteclient/pool.go index 09168372..67fd07d6 100644 --- a/liteclient/pool.go +++ b/liteclient/pool.go @@ -22,6 +22,7 @@ const _StickyCtxUsedNodesKey = "_ton_used_nodes_sticky" var ( ErrNoActiveConnections = errors.New("no active connections") ErrADNLReqTimeout = errors.New("adnl request timeout") + ErrNoNodesLeft = errors.New("no more active nodes left") ) type OnDisconnectCallback func(addr, key string) @@ -97,6 +98,7 @@ func (c *ConnectionPool) StickyContext(ctx context.Context) context.Context { return context.WithValue(ctx, _StickyCtxKey, id) } +// StickyContextNextNode - select next node in the available list (pseudo random) func (c *ConnectionPool) StickyContextNextNode(ctx context.Context) (context.Context, error) { nodeID, _ := ctx.Value(_StickyCtxKey).(uint32) usedNodes, _ := ctx.Value(_StickyCtxUsedNodesKey).([]uint32) @@ -118,7 +120,47 @@ iter: return context.WithValue(context.WithValue(ctx, _StickyCtxKey, node.id), _StickyCtxUsedNodesKey, usedNodes), nil } - return ctx, fmt.Errorf("no more active nodes left") + return ctx, ErrNoNodesLeft +} + +// StickyContextNextNodeBalanced - select next node based on its weight and availability +func (c *ConnectionPool) StickyContextNextNodeBalanced(ctx context.Context) (context.Context, error) { + nodeID, _ := ctx.Value(_StickyCtxKey).(uint32) + usedNodes, _ := ctx.Value(_StickyCtxUsedNodesKey).([]uint32) + if nodeID > 0 { + usedNodes = append(usedNodes, nodeID) + } + + c.nodesMx.RLock() + defer c.nodesMx.RUnlock() + + var reqNode *connection + +iter: + for _, node := range c.activeNodes { + for _, usedNode := range usedNodes { + if usedNode == node.id { + continue iter + } + } + + if reqNode == nil { + reqNode = node + continue + } + + // select best node on this moment + nw, old := atomic.LoadInt64(&node.weight), atomic.LoadInt64(&reqNode.weight) + if nw > old || (nw == old && atomic.LoadInt64(&node.lastRespTime) < atomic.LoadInt64(&reqNode.lastRespTime)) { + reqNode = node + } + } + + if reqNode != nil { + return context.WithValue(context.WithValue(ctx, _StickyCtxKey, reqNode.id), _StickyCtxUsedNodesKey, usedNodes), nil + } + + return ctx, ErrNoNodesLeft } func (c *ConnectionPool) StickyContextWithNodeID(ctx context.Context, nodeId uint32) context.Context { diff --git a/tlb/account.go b/tlb/account.go index d9c88e6e..ff884ae5 100644 --- a/tlb/account.go +++ b/tlb/account.go @@ -58,8 +58,8 @@ type AccountStorage struct { } type StorageUsed struct { - BitsUsed *big.Int `tlb:"var uint 7"` CellsUsed *big.Int `tlb:"var uint 7"` + BitsUsed *big.Int `tlb:"var uint 7"` PublicCellsUsed *big.Int `tlb:"var uint 7"` } diff --git a/tlb/shard.go b/tlb/shard.go index 972b791d..5e960ab8 100644 --- a/tlb/shard.go +++ b/tlb/shard.go @@ -1,6 +1,8 @@ package tlb import ( + "encoding/binary" + "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tvm/cell" ) @@ -13,6 +15,8 @@ func init() { Register(ShardStateUnsplit{}) } +type ShardID uint64 + type ShardStateUnsplit struct { _ Magic `tlb:"#9023afe2"` GlobalID int32 `tlb:"## 32"` @@ -146,3 +150,52 @@ type ShardDescB struct { FeesCollected CurrencyCollection `tlb:"."` FundsCreated CurrencyCollection `tlb:"."` } + +func (s ShardID) IsSibling(with ShardID) bool { + return (s^with) != 0 && ((s ^ with) == ((s & ShardID(bitsNegate64(uint64(s)))) << 1)) +} + +func (s ShardID) IsParent(of ShardID) bool { + y := lowerBit64(uint64(s)) + return y > 0 && of.GetParent() == s +} + +func (s ShardID) GetParent() ShardID { + y := lowerBit64(uint64(s)) + return ShardID((uint64(s) - y) | (y << 1)) +} + +func (s ShardID) GetChild(left bool) ShardID { + y := lowerBit64(uint64(s)) >> 1 + if left { + return s - ShardID(y) + } + return s + ShardID(y) +} + +func (s ShardID) ContainsAddress(addr *address.Address) bool { + x := lowerBit64(uint64(s)) + return ((uint64(s) ^ binary.BigEndian.Uint64(addr.Data())) & (bitsNegate64(x) << 1)) == 0 +} + +func (s ShardID) IsAncestor(of ShardID) bool { + x := lowerBit64(uint64(s)) + y := lowerBit64(uint64(of)) + return x >= y && uint64(s^of)&(bitsNegate64(x)<<1) == 0 +} + +func (s ShardIdent) IsSibling(with ShardIdent) bool { + return s.WorkchainID == with.WorkchainID && ShardID(s.ShardPrefix).IsSibling(ShardID(with.ShardPrefix)) +} + +func (s ShardIdent) IsAncestor(of ShardIdent) bool { + return s.WorkchainID == of.WorkchainID && ShardID(s.ShardPrefix).IsAncestor(ShardID(of.ShardPrefix)) +} + +func (s ShardIdent) IsParent(of ShardIdent) bool { + return s.WorkchainID == of.WorkchainID && ShardID(s.ShardPrefix).IsParent(ShardID(of.ShardPrefix)) +} + +func (s ShardIdent) GetShardID() ShardID { + return ShardID(s.ShardPrefix) +} diff --git a/tlb/shard_test.go b/tlb/shard_test.go index a01640a5..19f96904 100644 --- a/tlb/shard_test.go +++ b/tlb/shard_test.go @@ -2,6 +2,7 @@ package tlb import ( "encoding/hex" + "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tvm/cell" "testing" ) @@ -62,3 +63,363 @@ func TestShardState_LoadFromCell(t *testing.T) { }) } } + +func TestShardIdent_IsSibling(t *testing.T) { + type fields struct { + _ Magic + WorkchainID int32 + ShardPrefix uint64 + } + type args struct { + with ShardIdent + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "true", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0xC000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0x4000000000000000, + }, + }, + want: true, + }, + { + name: "same", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0x4000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0x4000000000000000, + }, + }, + want: false, + }, + { + name: "next", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0x6000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0x4000000000000000, + }, + }, + want: false, + }, + { + name: "diff wc", + fields: fields{ + WorkchainID: -1, + ShardPrefix: 0xC000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0x4000000000000000, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := ShardIdent{ + WorkchainID: tt.fields.WorkchainID, + ShardPrefix: tt.fields.ShardPrefix, + } + if got := s.IsSibling(tt.args.with); got != tt.want { + t.Errorf("IsSibling() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestShardIdent_IsParent(t *testing.T) { + type fields struct { + _ Magic + WorkchainID int32 + ShardPrefix uint64 + } + type args struct { + with ShardIdent + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "parent", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0x8000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0xC000000000000000, + }, + }, + want: true, + }, + { + name: "child", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0xC000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0x8000000000000000, + }, + }, + want: false, + }, + { + name: "grand child", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0x8000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0xE000000000000000, + }, + }, + want: false, + }, + { + name: "diff wc", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0x8000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: -1, + ShardPrefix: 0xC000000000000000, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := ShardIdent{ + WorkchainID: tt.fields.WorkchainID, + ShardPrefix: tt.fields.ShardPrefix, + } + if got := s.IsParent(tt.args.with); got != tt.want { + t.Errorf("IsParent() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestShardIdent_IsAncestor(t *testing.T) { + type fields struct { + _ Magic + WorkchainID int32 + ShardPrefix uint64 + } + type args struct { + with ShardIdent + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "ancestor", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0x8000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0xE000000000000000, + }, + }, + want: true, + }, + { + name: "diff wc", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0x8000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: -1, + ShardPrefix: 0xE000000000000000, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := ShardIdent{ + WorkchainID: tt.fields.WorkchainID, + ShardPrefix: tt.fields.ShardPrefix, + } + if got := s.IsAncestor(tt.args.with); got != tt.want { + t.Errorf("IsAncestor() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestShardIdent_GetShardID(t *testing.T) { + type fields struct { + ShardPrefix uint64 + } + tests := []struct { + name string + fields fields + want ShardID + }{ + { + name: "ok", + fields: fields{ + ShardPrefix: 0xE000000000000000, + }, + want: 0xE000000000000000, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := ShardIdent{ + ShardPrefix: tt.fields.ShardPrefix, + } + if got := s.GetShardID(); got != tt.want { + t.Errorf("GetShardID() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestShardID_GetChild(t *testing.T) { + type args struct { + left bool + } + tests := []struct { + name string + s ShardID + args args + want ShardID + }{ + { + name: "ok", + s: 0xE000000000000000, + args: args{ + true, + }, + want: 0xD000000000000000, + }, + { + name: "ok", + s: 0xE000000000000000, + args: args{ + false, + }, + want: 0xF000000000000000, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.s.GetChild(tt.args.left); got != tt.want { + t.Errorf("GetChild() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestShardID_ContainsAddress(t *testing.T) { + type args struct { + addr *address.Address + } + tests := []struct { + name string + s ShardID + args args + want bool + }{ + { + name: "ok", + s: 0xA000000000000000, + args: args{ + address.MustParseAddr("EQCN6j4gO7D_9OBkWQy_BkW1peVqA0ikvcSgCd9yj1yxu7VD"), + }, + want: true, + }, + { + name: "ok 2", + s: 0x6000000000000000, + args: args{ + address.MustParseAddr("EQBTmKoKwypDGJFXf9FNwNdKG9Ei5C9KdKd85_ALPLRJbIR1"), + }, + want: true, + }, + { + name: "ok 3", + s: 0x8000000000000000, + args: args{ + address.MustParseAddr("EQBTmKoKwypDGJFXf9FNwNdKG9Ei5C9KdKd85_ALPLRJbIR1"), + }, + want: true, + }, + { + name: "ok 3", + s: 0x8000000000000000, + args: args{ + address.MustParseAddr("EQCN6j4gO7D_9OBkWQy_BkW1peVqA0ikvcSgCd9yj1yxu7VD"), + }, + want: true, + }, + { + name: "no", + s: 0x6000000000000000, + args: args{ + address.MustParseAddr("EQCN6j4gO7D_9OBkWQy_BkW1peVqA0ikvcSgCd9yj1yxu7VD"), + }, + want: false, + }, + { + name: "no 2", + s: 0xA000000000000000, + args: args{ + address.MustParseAddr("EQBTmKoKwypDGJFXf9FNwNdKG9Ei5C9KdKd85_ALPLRJbIR1"), + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.s.ContainsAddress(tt.args.addr); got != tt.want { + t.Errorf("ContainsAddress() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/ton/api.go b/ton/api.go index e86c74af..8ded6d8a 100644 --- a/ton/api.go +++ b/ton/api.go @@ -34,6 +34,7 @@ type LiteClient interface { QueryLiteserver(ctx context.Context, payload tl.Serializable, result tl.Serializable) error StickyContext(ctx context.Context) context.Context StickyContextNextNode(ctx context.Context) (context.Context, error) + StickyContextNextNodeBalanced(ctx context.Context) (context.Context, error) StickyNodeID(ctx context.Context) uint32 } diff --git a/ton/dns/integration_test.go b/ton/dns/integration_test.go index eee7ce62..2778b805 100644 --- a/ton/dns/integration_test.go +++ b/ton/dns/integration_test.go @@ -33,7 +33,7 @@ func TestDNSClient_Resolve(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) defer cancel() - ctx = client.StickyContext(ctx) + ctx, _ = client.StickyContextNextNodeBalanced(ctx) d, err := cli.Resolve(ctx, "foundation.ton") if err != nil { diff --git a/ton/dns/resolve.go b/ton/dns/resolve.go index 1eabfaea..cae592d2 100644 --- a/ton/dns/resolve.go +++ b/ton/dns/resolve.go @@ -44,13 +44,18 @@ var randomizer = func() uint64 { return binary.LittleEndian.Uint64(buf) } +// Deprecated: use GetRootContractAddr func RootContractAddr(api TonApi) (*address.Address, error) { - b, err := api.CurrentMasterchainInfo(context.Background()) + return GetRootContractAddr(context.Background(), api) +} + +func GetRootContractAddr(ctx context.Context, api TonApi) (*address.Address, error) { + b, err := api.CurrentMasterchainInfo(ctx) if err != nil { return nil, fmt.Errorf("failed to get masterchain info: %w", err) } - cfg, err := api.GetBlockchainConfig(context.Background(), b, 4) + cfg, err := api.GetBlockchainConfig(ctx, b, 4) if err != nil { return nil, fmt.Errorf("failed to get root address from network config: %w", err) } @@ -174,7 +179,11 @@ func (d *Domain) GetWalletRecord() *address.Address { if rec == nil { return nil } - p := rec.BeginParse() + + p, err := rec.BeginParse().LoadRef() + if err != nil { + return nil + } category, err := p.LoadUInt(16) if err != nil { @@ -198,9 +207,8 @@ func (d *Domain) GetSiteRecord() (_ []byte, inStorage bool) { if rec == nil { return nil, false } - p := rec.BeginParse() - p, err := p.LoadRef() + p, err := rec.BeginParse().LoadRef() if err != nil { return nil, false } diff --git a/ton/dns/resolve_test.go b/ton/dns/resolve_test.go index 37de98c3..0c810749 100644 --- a/ton/dns/resolve_test.go +++ b/ton/dns/resolve_test.go @@ -28,10 +28,10 @@ func TestDomain_GetRecords(t *testing.T) { h.Write([]byte("wallet")) addr := address.MustParseAddr("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N") records.Set(cell.BeginCell().MustStoreSlice(h.Sum(nil), 256).EndCell(), - cell.BeginCell(). + cell.BeginCell().MustStoreRef(cell.BeginCell(). MustStoreUInt(_CategoryContractAddr, 16). MustStoreAddr(addr). - EndCell()) + EndCell()).EndCell()) domain := Domain{ Records: records, diff --git a/ton/proof.go b/ton/proof.go index 60ab019c..9f287ed4 100644 --- a/ton/proof.go +++ b/ton/proof.go @@ -600,10 +600,11 @@ func (c *APIClient) VerifyProofChain(ctx context.Context, from, to *BlockIDExt) return fmt.Errorf("config proof boc parse err: %w", err) } - err = CheckForwardBlockProof(fwd.From, fwd.To, fwd.ToKeyBlock, configProof, destProof, fwd.SignatureSet) + err = CheckForwardBlockProof(from, fwd.To, fwd.ToKeyBlock, configProof, destProof, fwd.SignatureSet) if err != nil { return fmt.Errorf("invalid forward block from %d to %d proof: %w", fwd.From.SeqNo, fwd.To.SeqNo, err) } + from = fwd.To } } else { for _, step := range part.Steps { diff --git a/ton/retrier.go b/ton/retrier.go index e65191d8..70e17be5 100644 --- a/ton/retrier.go +++ b/ton/retrier.go @@ -25,26 +25,23 @@ func (w *retryClient) QueryLiteserver(ctx context.Context, payload tl.Serializab tries++ if err != nil { - if errors.Is(err, liteclient.ErrADNLReqTimeout) { - // try next node - ctx, err = w.original.StickyContextNextNode(ctx) - if err != nil { - return fmt.Errorf("timeout error received, but failed to try with next node, "+ - "looks like all active nodes was already tried, original error: %w", err) - } + if !errors.Is(err, liteclient.ErrADNLReqTimeout) && !errors.Is(err, context.DeadlineExceeded) { + return err + } - continue + err := ctx.Err() + if err != nil { + return err } - if errors.Is(err, context.DeadlineExceeded) { - err := ctx.Err() - if err != nil { - return err - } - - continue + + // try next node + ctx, err = w.original.StickyContextNextNode(ctx) + if err != nil { + return fmt.Errorf("timeout error received, but failed to try with next node, "+ + "looks like all active nodes was already tried, original error: %w", err) } - return err + continue } if tmp, ok := result.(*tl.Serializable); ok && tmp != nil { @@ -75,3 +72,7 @@ func (w *retryClient) StickyNodeID(ctx context.Context) uint32 { func (w *retryClient) StickyContextNextNode(ctx context.Context) (context.Context, error) { return w.original.StickyContextNextNode(ctx) } + +func (w *retryClient) StickyContextNextNodeBalanced(ctx context.Context) (context.Context, error) { + return w.original.StickyContextNextNodeBalanced(ctx) +} diff --git a/ton/timeouter.go b/ton/timeouter.go index cd617437..af850743 100644 --- a/ton/timeouter.go +++ b/ton/timeouter.go @@ -30,3 +30,7 @@ func (c *timeoutClient) StickyNodeID(ctx context.Context) uint32 { func (c *timeoutClient) StickyContextNextNode(ctx context.Context) (context.Context, error) { return c.original.StickyContextNextNode(ctx) } + +func (c *timeoutClient) StickyContextNextNodeBalanced(ctx context.Context) (context.Context, error) { + return c.original.StickyContextNextNodeBalanced(ctx) +} diff --git a/ton/waiter.go b/ton/waiter.go index ae36d584..3350cf33 100644 --- a/ton/waiter.go +++ b/ton/waiter.go @@ -49,3 +49,7 @@ func (w *waiterClient) StickyNodeID(ctx context.Context) uint32 { func (w *waiterClient) StickyContextNextNode(ctx context.Context) (context.Context, error) { return w.original.StickyContextNextNode(ctx) } + +func (w *waiterClient) StickyContextNextNodeBalanced(ctx context.Context) (context.Context, error) { + return w.original.StickyContextNextNodeBalanced(ctx) +} diff --git a/ton/wallet/address.go b/ton/wallet/address.go index bd162a33..01a1df13 100644 --- a/ton/wallet/address.go +++ b/ton/wallet/address.go @@ -55,9 +55,13 @@ func GetStateInit(pubKey ed25519.PublicKey, version VersionConfig, subWallet uin switch ver { case HighloadV3: return nil, fmt.Errorf("use ConfigHighloadV3 for highload v3 spec") + case V5R1: + return nil, fmt.Errorf("use ConfigV5R1 for v5 spec") } case ConfigHighloadV3: ver = HighloadV3 + case ConfigV5R1: + ver = V5R1 } code, ok := walletCode[ver] @@ -80,6 +84,18 @@ func GetStateInit(pubKey ed25519.PublicKey, version VersionConfig, subWallet uin MustStoreSlice(pubKey, 256). MustStoreDict(nil). // empty dict of plugins EndCell() + case V5R1: + config := version.(ConfigV5R1) + + data = cell.BeginCell(). + MustStoreUInt(0, 33). // seqno + MustStoreInt(int64(config.NetworkGlobalID), 32). + MustStoreInt(int64(config.Workchain), 8). + MustStoreUInt(0, 8). // version of v5 + MustStoreUInt(uint64(subWallet), 32). + MustStoreSlice(pubKey, 256). + MustStoreDict(nil). // empty dict of plugins + EndCell() case HighloadV2R2, HighloadV2Verified: data = cell.BeginCell(). MustStoreUInt(uint64(subWallet), 32). diff --git a/ton/wallet/highloadv3.go b/ton/wallet/highloadv3.go index 592d5a36..ead72ca0 100644 --- a/ton/wallet/highloadv3.go +++ b/ton/wallet/highloadv3.go @@ -58,13 +58,13 @@ func (s *SpecHighloadV3) BuildMessage(ctx context.Context, messages []*Message) if len(messages) > 254*254 { return nil, errors.New("for this type of wallet max 254*254 messages can be sent in the same time") - } else if len(messages) > 1 { + } else if len(messages) == 1 && messages[0].InternalMessage.StateInit == nil { // messages with state init must be packed because of external msg validation in contract + msg = messages[0] + } else if len(messages) > 0 { msg, err = s.packActions(uint64(queryID), messages) if err != nil { return nil, fmt.Errorf("failed to pack messages to cell: %w", err) } - } else if len(messages) == 1 { - msg = messages[0] } else { return nil, errors.New("should have at least one message") } @@ -88,13 +88,15 @@ func (s *SpecHighloadV3) BuildMessage(ctx context.Context, messages []*Message) MustStoreRef(payload).EndCell(), nil } -func (s *SpecHighloadV3) packActions(queryId uint64, messages []*Message) (*Message, error) { - if len(messages) > 253 { - rest, err := s.packActions(queryId, messages[253:]) +func (s *SpecHighloadV3) packActions(queryId uint64, messages []*Message) (_ *Message, err error) { + const messagesPerPack = 253 + + if len(messages) > messagesPerPack { + rest, err := s.packActions(queryId, messages[messagesPerPack:]) if err != nil { return nil, err } - messages = append(messages[:253], rest) + messages = append(messages[:messagesPerPack], rest) } var amt = big.NewInt(0) @@ -122,7 +124,7 @@ func (s *SpecHighloadV3) packActions(queryId uint64, messages []*Message) (*Mess } return &Message{ - Mode: 1 + 2, + Mode: PayGasSeparately + IgnoreErrors, InternalMessage: &tlb.InternalMessage{ IHRDisabled: true, Bounce: false, diff --git a/ton/wallet/integration_test.go b/ton/wallet/integration_test.go index 35bcc357..f785bb05 100644 --- a/ton/wallet/integration_test.go +++ b/ton/wallet/integration_test.go @@ -83,10 +83,40 @@ func Test_HighloadHeavyTransfer(t *testing.T) { t.Log("TX", base64.StdEncoding.EncodeToString(tx.Hash)) } +func Test_V5HeavyTransfer(t *testing.T) { + seed := strings.Split(_seed, " ") + + w, err := FromSeed(api, seed, ConfigV5R1{ + NetworkGlobalID: MainnetGlobalID, + }) + if err != nil { + t.Fatal("FromSeed err:", err.Error()) + return + } + + t.Log("test wallet address:", w.WalletAddress()) + + var list []*Message + for i := 0; i < 255; i++ { + com, _ := CreateCommentCell(fmt.Sprint(i)) + list = append(list, SimpleMessage(w.WalletAddress(), tlb.MustFromTON("0.001"), com)) + } + + tx, _, err := w.SendManyWaitTransaction(context.Background(), list) + if err != nil { + t.Fatal("Send err:", err.Error()) + return + } + + t.Log("TX", base64.StdEncoding.EncodeToString(tx.Hash)) +} + func Test_WalletTransfer(t *testing.T) { seed := strings.Split(_seed, " ") - for _, v := range []VersionConfig{V3R2, V4R2, HighloadV2R2, V3R1, V4R1, HighloadV2Verified, ConfigHighloadV3{ + for _, v := range []VersionConfig{ConfigV5R1{ + NetworkGlobalID: TestnetGlobalID, + }, V3R2, V4R2, HighloadV2R2, V3R1, V4R1, HighloadV2Verified, ConfigHighloadV3{ MessageTTL: 120, MessageBuilder: func(ctx context.Context, subWalletId uint32) (id uint32, createdAt int64, err error) { tm := time.Now().Unix() - 30 @@ -236,6 +266,52 @@ func TestWallet_DeployContract(t *testing.T) { } } +func TestWallet_DeployContractUsingHW3(t *testing.T) { + seed := strings.Split(_seed, " ") + ctx := api.Client().StickyContext(context.Background()) + + // init wallet + w, err := FromSeed(api, seed, ConfigHighloadV3{ + MessageTTL: 120, + MessageBuilder: func(ctx context.Context, subWalletId uint32) (id uint32, createdAt int64, err error) { + tm := time.Now().Unix() - 30 + return uint32(10000 + tm%(1<<23)), tm, nil + }, + }) + if err != nil { + t.Fatal("FromSeed err:", err.Error()) + } + t.Logf("wallet address: %s", w.Address().String()) + + codeBytes, _ := hex.DecodeString("b5ee9c72410104010020000114ff00f4a413f4bcf2c80b010203844003020009a1b63c43510007a0000061d2421bb1") + code, _ := cell.FromBOC(codeBytes) + + buf := make([]byte, 8) + _, _ = rand.Read(buf) + rnd := binary.LittleEndian.Uint64(buf) + + addr, _, block, err := w.DeployContractWaitTransaction(ctx, tlb.MustFromTON("0.005"), cell.BeginCell().EndCell(), code, cell.BeginCell().MustStoreUInt(rnd, 64).EndCell()) + if err != nil { + t.Fatal("deploy err:", err) + } + t.Logf("contract address: %s", addr.String()) + + // wait next block to be sure everything updated + block, err = api.WaitForBlock(block.SeqNo + 5).GetMasterchainInfo(ctx) + if err != nil { + t.Fatal("wait master err:", err.Error()) + } + + res, err := api.WaitForBlock(block.SeqNo).RunGetMethod(ctx, block, addr, "dappka", 5, 10) + if err != nil { + t.Fatal("run err:", err) + } + + if res.MustInt(0).Uint64() != 5 || res.MustInt(1).Uint64() != 50 { + t.Fatal("result err:", res.MustInt(0).Uint64(), res.MustInt(1).Uint64()) + } +} + func TestWallet_TransferEncrypted(t *testing.T) { seed := strings.Split(_seed, " ") ctx := api.Client().StickyContext(context.Background()) diff --git a/ton/wallet/v5r1.go b/ton/wallet/v5r1.go new file mode 100644 index 00000000..8c560ca8 --- /dev/null +++ b/ton/wallet/v5r1.go @@ -0,0 +1,92 @@ +package wallet + +import ( + "context" + "errors" + "fmt" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton" + "time" + + "github.com/xssnick/tonutils-go/tvm/cell" +) + +// https://github.com/tonkeeper/tonkeeper-ton/commit/e8a7f3415e241daf4ac723f273fbc12776663c49#diff-c20d462b2e1ec616bbba2db39acc7a6c61edc3d5e768f5c2034a80169b1a56caR29 +const _V5R1CodeHex = "b5ee9c7241010101002300084202e4cf3b2f4c6d6a61ea0f2b5447d266785b26af3637db2deee6bcd1aa826f34120dcd8e11" + +type ConfigV5R1 struct { + NetworkGlobalID int32 + Workchain int8 +} + +type SpecV5R1 struct { + SpecRegular + SpecSeqno + + config ConfigV5R1 +} + +const MainnetGlobalID = -239 +const TestnetGlobalID = -3 + +func (s *SpecV5R1) BuildMessage(ctx context.Context, _ bool, _ *ton.BlockIDExt, messages []*Message) (_ *cell.Cell, err error) { + // TODO: remove block, now it is here for backwards compatibility + + if len(messages) > 255 { + return nil, errors.New("for this type of wallet max 4 messages can be sent in the same time") + } + + seq, err := s.seqnoFetcher(ctx, s.wallet.subwallet) + if err != nil { + return nil, fmt.Errorf("failed to fetch seqno: %w", err) + } + + actions, err := packV5Actions(messages) + if err != nil { + return nil, fmt.Errorf("failed to build actions: %w", err) + } + + payload := cell.BeginCell(). + MustStoreUInt(0x7369676e, 32). // external sign op code + MustStoreInt(int64(s.config.NetworkGlobalID), 32). + MustStoreInt(int64(s.config.Workchain), 8). + MustStoreUInt(0, 8). // version of v5 + MustStoreUInt(uint64(s.wallet.subwallet), 32). + MustStoreUInt(uint64(timeNow().Add(time.Duration(s.messagesTTL)*time.Second).UTC().Unix()), 32). + MustStoreUInt(uint64(seq), 32). + MustStoreBuilder(actions) + + sign := payload.EndCell().Sign(s.wallet.key) + msg := cell.BeginCell().MustStoreBuilder(payload).MustStoreSlice(sign, 512).EndCell() + + return msg, nil +} + +func packV5Actions(messages []*Message) (*cell.Builder, error) { + if len(messages) > 255 { + return nil, fmt.Errorf("max 255 messages allowed for v5") + } + + var list = cell.BeginCell().EndCell() + for _, message := range messages { + outMsg, err := tlb.ToCell(message.InternalMessage) + if err != nil { + return nil, err + } + + /* + out_list_empty$_ = OutList 0; + out_list$_ {n:#} prev:^(OutList n) action:OutAction + = OutList (n + 1); + action_send_msg#0ec3c86d mode:(## 8) + out_msg:^(MessageRelaxed Any) = OutAction; + */ + msg := cell.BeginCell().MustStoreUInt(0x0ec3c86d, 32). + MustStoreUInt(uint64(message.Mode), 8). + MustStoreRef(outMsg) + + list = cell.BeginCell().MustStoreRef(list).MustStoreBuilder(msg).EndCell() + } + + return cell.BeginCell().MustStoreUInt(0, 1).MustStoreRef(list), nil +} diff --git a/ton/wallet/wallet.go b/ton/wallet/wallet.go index 62a6f384..e571935d 100644 --- a/ton/wallet/wallet.go +++ b/ton/wallet/wallet.go @@ -36,6 +36,7 @@ const ( V3 = V3R2 V4R1 Version = 41 V4R2 Version = 42 + V5R1 Version = 51 HighloadV2R2 Version = 122 HighloadV2Verified Version = 123 HighloadV3 Version = 300 @@ -43,6 +44,14 @@ const ( Unknown Version = 0 ) +const ( + CarryAllRemainingBalance = 128 + CarryAllRemainingIncomingValue = 64 + DestroyAccountIfZero = 32 + IgnoreErrors = 2 + PayGasSeparately = 1 +) + func (v Version) String() string { if v == Unknown { return "unknown" @@ -70,6 +79,7 @@ var ( V2R1: _V2R1CodeHex, V2R2: _V2R2CodeHex, V3R1: _V3R1CodeHex, V3R2: _V3R2CodeHex, V4R1: _V4R1CodeHex, V4R2: _V4R2CodeHex, + V5R1: _V5R1CodeHex, HighloadV2R2: _HighloadV2R2CodeHex, HighloadV2Verified: _HighloadV2VerifiedCodeHex, HighloadV3: _HighloadV3CodeHex, Lockup: _LockupCodeHex, @@ -141,7 +151,15 @@ type Wallet struct { } func FromPrivateKey(api TonAPI, key ed25519.PrivateKey, version VersionConfig) (*Wallet, error) { - addr, err := AddressFromPubKey(key.Public().(ed25519.PublicKey), version, DefaultSubwallet) + var subwallet uint32 = DefaultSubwallet + + // default subwallet depends on wallet type + switch version.(type) { + case ConfigV5R1: + subwallet = 0 + } + + addr, err := AddressFromPubKey(key.Public().(ed25519.PublicKey), version, subwallet) if err != nil { return nil, err } @@ -151,7 +169,7 @@ func FromPrivateKey(api TonAPI, key ed25519.PrivateKey, version VersionConfig) ( key: key, addr: addr, ver: version, - subwallet: DefaultSubwallet, + subwallet: subwallet, } w.spec, err = getSpec(w) @@ -164,7 +182,7 @@ func FromPrivateKey(api TonAPI, key ed25519.PrivateKey, version VersionConfig) ( func getSpec(w *Wallet) (any, error) { switch v := w.ver.(type) { - case Version: + case Version, ConfigV5R1: regular := SpecRegular{ wallet: w, messagesTTL: 60 * 3, // default ttl 3 min @@ -191,6 +209,14 @@ func getSpec(w *Wallet) (any, error) { return uint32(iSeq.Uint64()), nil } + switch x := w.ver.(type) { + case ConfigV5R1: + if x.NetworkGlobalID == 0 { + return nil, fmt.Errorf("NetworkGlobalID should be set in v5 config") + } + return &SpecV5R1{SpecRegular: regular, SpecSeqno: SpecSeqno{seqnoFetcher: seqnoFetcher}, config: x}, nil + } + switch v { case V3R1, V3R2: return &SpecV3{regular, SpecSeqno{seqnoFetcher: seqnoFetcher}}, nil @@ -200,6 +226,8 @@ func getSpec(w *Wallet) (any, error) { return &SpecHighloadV2R2{regular, SpecQuery{}}, nil case HighloadV3: return nil, fmt.Errorf("use ConfigHighloadV3 for highload v3 spec") + case V5R1: + return nil, fmt.Errorf("use ConfigV5R1 for v5 spec") } case ConfigHighloadV3: return &SpecHighloadV3{wallet: w, config: v}, nil @@ -300,9 +328,13 @@ func (w *Wallet) PrepareExternalMessageForMany(ctx context.Context, withStateIni var msg *cell.Cell switch v := w.ver.(type) { - case Version: + case Version, ConfigV5R1: + if _, ok := v.(ConfigV5R1); ok { + v = V5R1 + } + switch v { - case V3R2, V3R1, V4R2, V4R1: + case V3R2, V3R1, V4R2, V4R1, V5R1: msg, err = w.spec.(RegularBuilder).BuildMessage(ctx, !withStateInit, nil, messages) if err != nil { return nil, fmt.Errorf("build message err: %w", err) @@ -343,7 +375,7 @@ func (w *Wallet) BuildTransfer(to *address.Address, amount tlb.Coins, bounce boo } return &Message{ - Mode: 1 + 2, + Mode: PayGasSeparately + IgnoreErrors, InternalMessage: &tlb.InternalMessage{ IHRDisabled: true, Bounce: bounce, @@ -369,7 +401,7 @@ func (w *Wallet) BuildTransferEncrypted(ctx context.Context, to *address.Address } return &Message{ - Mode: 1 + 2, + Mode: PayGasSeparately + IgnoreErrors, InternalMessage: &tlb.InternalMessage{ IHRDisabled: true, Bounce: bounce, @@ -710,7 +742,7 @@ func (w *Wallet) DeployContractWaitTransaction(ctx context.Context, amount tlb.C addr := address.NewAddress(0, 0, stateCell.Hash()) tx, block, err := w.SendWaitTransaction(ctx, &Message{ - Mode: 1 + 2, + Mode: PayGasSeparately + IgnoreErrors, InternalMessage: &tlb.InternalMessage{ IHRDisabled: true, Bounce: false, @@ -741,7 +773,7 @@ func (w *Wallet) DeployContract(ctx context.Context, amount tlb.Coins, msgBody, addr := address.NewAddress(0, 0, stateCell.Hash()) if err = w.Send(ctx, &Message{ - Mode: 1 + 2, + Mode: PayGasSeparately + IgnoreErrors, InternalMessage: &tlb.InternalMessage{ IHRDisabled: true, Bounce: false, @@ -769,7 +801,7 @@ func (w *Wallet) FindTransactionByInMsgHash(ctx context.Context, msgHash []byte, func SimpleMessage(to *address.Address, amount tlb.Coins, payload *cell.Cell) *Message { return &Message{ - Mode: 1 + 2, + Mode: PayGasSeparately + IgnoreErrors, InternalMessage: &tlb.InternalMessage{ IHRDisabled: true, Bounce: true, @@ -783,7 +815,7 @@ func SimpleMessage(to *address.Address, amount tlb.Coins, payload *cell.Cell) *M // SimpleMessageAutoBounce - will determine bounce flag from address func SimpleMessageAutoBounce(to *address.Address, amount tlb.Coins, payload *cell.Cell) *Message { return &Message{ - Mode: 1 + 2, + Mode: PayGasSeparately + IgnoreErrors, InternalMessage: &tlb.InternalMessage{ IHRDisabled: true, Bounce: to.IsBounceable(), diff --git a/ton/wallet/wallet_test.go b/ton/wallet/wallet_test.go index 0d8fae55..47aca381 100644 --- a/ton/wallet/wallet_test.go +++ b/ton/wallet/wallet_test.go @@ -258,7 +258,7 @@ func TestWallet_Send(t *testing.T) { } msg := &Message{ - Mode: 128, + Mode: CarryAllRemainingBalance, InternalMessage: intMsg, }