diff --git a/README.md b/README.md index c598ee9e..60e34da9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Based on TON][ton-svg]][ton] [![Telegram Channel][tgc-svg]][tg-channel] -![Coverage](https://img.shields.io/badge/Coverage-73.8%25-brightgreen) +![Coverage](https://img.shields.io/badge/Coverage-73.5%25-brightgreen) Golang library for interacting with TON blockchain. diff --git a/adnl/adnl.go b/adnl/adnl.go index 0595a77d..54cb67b8 100644 --- a/adnl/adnl.go +++ b/adnl/adnl.go @@ -146,6 +146,24 @@ func (c *Channel) process(buf []byte) error { func (a *ADNL) processPacket(packet *PacketContent, ch *Channel) (err error) { a.mx.Lock() + + if packet.DstReinitDate != nil && *packet.DstReinitDate > 0 && *packet.DstReinitDate < a.reinitTime { + if packet.ReinitDate != nil { + a.dstReinit = *packet.ReinitDate + } + a.mx.Unlock() + + buf, err := a.buildRequest(ch, MessageNop{}) + if err != nil { + return fmt.Errorf("failed to create packet: %w", err) + } + if err = a.send(context.Background(), buf); err != nil { + return fmt.Errorf("failed to send ping reinit: %w", err) + } + + return nil + } + seqno := uint64(*packet.Seqno) a.lastReceiveAt = time.Now() @@ -161,17 +179,14 @@ func (a *ADNL) processPacket(packet *PacketContent, ch *Channel) (err error) { a.confirmSeqno = seqno } - if packet.ReinitDate != nil && *packet.ReinitDate > a.dstReinit { + if (packet.ReinitDate != nil && *packet.ReinitDate > a.dstReinit) && + (packet.DstReinitDate != nil && *packet.DstReinitDate == a.reinitTime) { // reset their seqno even if it is lower, // because other side could lose counter a.confirmSeqno = seqno a.loss = 0 - // a.dstReinit = *packet.ReinitDate - // a.seqno = 0 - // a.channel = nil - // a.confirmSeqno = 0 - // a.reinitTime = a.dstReinit + a.dstReinit = *packet.ReinitDate } if packet.RecvPriorityAddrListVersion != nil { @@ -329,6 +344,8 @@ func (a *ADNL) processMessage(message any, ch *Channel) error { return fmt.Errorf("failed to handle custom message: %w", err) } } + case MessageNop: + return nil default: return fmt.Errorf("skipped unprocessable message of type %s", reflect.TypeOf(message).String()) } diff --git a/adnl/gateway.go b/adnl/gateway.go index d7cfeaf5..e8aaf73d 100644 --- a/adnl/gateway.go +++ b/adnl/gateway.go @@ -279,7 +279,7 @@ func (g *Gateway) listen(rootId []byte) { g.mx.RUnlock() if proc == nil { - Logger("no processor for ADNL packet from", hex.EncodeToString(id)) + Logger("no processor for ADNL packet from", addr.String(), hex.EncodeToString(id)) continue } @@ -384,18 +384,17 @@ func (g *Gateway) registerClient(addr net.Addr, key ed25519.PublicKey, id string closer: ch.adnl.Close, } g.mx.Unlock() + }) - if oldId == "" { // connection = first channel initialisation - connHandler := g.connHandler - if connHandler != nil { - err := connHandler(peer) - if err != nil { - // close connection if connection handler reports an error - ch.adnl.Close() - } + connHandler := g.connHandler + if connHandler != nil { + go func() { + if err := connHandler(peer); err != nil { + // close connection if connection handler reports an error + a.Close() } - } - }) + }() + } return peer, nil } diff --git a/adnl/packet.go b/adnl/packet.go index 033f756f..2646e4f3 100644 --- a/adnl/packet.go +++ b/adnl/packet.go @@ -162,9 +162,9 @@ func parsePacket(data []byte) (_ *PacketContent, err error) { data = data[4:] packet.ReinitDate = &reinit - reinit = int32(binary.LittleEndian.Uint32(data)) + dstReinit := int32(binary.LittleEndian.Uint32(data)) data = data[4:] - packet.DstReinitDate = &reinit + packet.DstReinitDate = &dstReinit } if flags&_FlagSignature != 0 { diff --git a/adnl/rldp/client.go b/adnl/rldp/client.go index 9f3e0da1..94e9c667 100644 --- a/adnl/rldp/client.go +++ b/adnl/rldp/client.go @@ -357,6 +357,9 @@ func (r *RLDP) sendMessageParts(ctx context.Context, transferId, data []byte) er if symbolsSent > fastSymbols { x := (symbolsSent - fastSymbols) / 2 + if x > 70 { // 7 ms max delay + x = 70 + } select { case <-ctx.Done(): @@ -365,7 +368,7 @@ func (r *RLDP) sendMessageParts(ctx context.Context, transferId, data []byte) er case <-ch: // we got complete from receiver, finish sending return nil - case <-time.After(time.Duration(x) * _PacketWaitTime): + case <-time.After(time.Duration(x) * (time.Millisecond / 10)): // send additional FEC recovery parts until complete } } diff --git a/adnl/rldp/client_test.go b/adnl/rldp/client_test.go index d5079cb0..86f49dcf 100644 --- a/adnl/rldp/client_test.go +++ b/adnl/rldp/client_test.go @@ -377,10 +377,14 @@ func TestRDLP_sendMessageParts(t *testing.T) { } decoded, receivData, err := tDecoder.Decode() - if err != nil || decoded != true { + if err != nil { t.Fatal("failed to decode received test data, err: ", err) } + if decoded != true { + return nil + } + if !bytes.Equal(data, receivData) { t.Fatal("bad data received in 'sendCustomMessage'") } @@ -479,10 +483,14 @@ func TestRLDP_DoQuery(t *testing.T) { } decoded, receivData, err := tDecoder.Decode() - if err != nil || decoded != true { + if err != nil { t.Fatal("failed to decode received test data, err: ", err) } + if decoded != true { + return nil + } + var checkReq Query _, err = tl.Parse(&checkReq, receivData, true) if err != nil { @@ -568,10 +576,14 @@ func TestRLDP_SendAnswer(t *testing.T) { } decoded, receivData, err := tDecoder.Decode() - if err != nil || decoded != true { + if err != nil { t.Fatal("failed to decode received test data, err: ", err) } + if decoded != true { + return nil + } + var checkAnswer Answer _, err = tl.Parse(&checkAnswer, receivData, true) if err != nil { diff --git a/adnl/rldp/http/client.go b/adnl/rldp/http/client.go index e0d9e765..62262286 100644 --- a/adnl/rldp/http/client.go +++ b/adnl/rldp/http/client.go @@ -112,7 +112,7 @@ func (t *Transport) connectRLDP(ctx context.Context, key ed25519.PublicKey, addr } rCap := GetCapabilities{ - Capabilities: CapabilityRLDP2, + Capabilities: 0, } var caps Capabilities @@ -126,7 +126,7 @@ func (t *Transport) connectRLDP(ctx context.Context, key ed25519.PublicKey, addr switch query.Data.(type) { case GetCapabilities: ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - err := a.Answer(ctx, query.ID, &Capabilities{Value: CapabilityRLDP2}) + err := a.Answer(ctx, query.ID, &Capabilities{Value: 0}) cancel() if err != nil { return fmt.Errorf("failed to send capabilities answer: %w", err) @@ -284,7 +284,7 @@ func (t *Transport) RoundTrip(request *http.Request) (_ *http.Response, err erro req := Request{ ID: qid, Method: request.Method, - URL: request.URL.String(), + URL: request.URL.RequestURI(), Version: "HTTP/1.1", Headers: []Header{ { diff --git a/adnl/rldp/http/server.go b/adnl/rldp/http/server.go index 0ddaa080..ad187d2d 100644 --- a/adnl/rldp/http/server.go +++ b/adnl/rldp/http/server.go @@ -157,7 +157,7 @@ func (s *Server) ListenAndServe(listenAddr string) error { switch query.Data.(type) { case GetCapabilities: ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - err := client.Answer(ctx, query.ID, &Capabilities{Value: CapabilityRLDP2}) + err := client.Answer(ctx, query.ID, &Capabilities{Value: 0}) // CapabilityRLDP2 cancel() if err != nil { return fmt.Errorf("failed to send capabilities answer: %w", err) @@ -236,11 +236,15 @@ func (s *Server) handle(client RLDP, adnlId, addr string) func(transferId []byte if err != nil { return fmt.Errorf("failed to parse url `%s`: %w", uri, err) } + uri.Scheme = "http" contentLen := int64(-1) headers := http.Header{} for _, header := range req.Headers { - if header.Name == "Content-Length" { + name := http.CanonicalHeaderKey(header.Name) + if name == "Host" { + uri.Host = header.Value + } else if name == "Content-Length" { contentLen, err = strconv.ParseInt(header.Value, 10, 64) if err != nil { return fmt.Errorf("failed to parse content len `%s`: %w", header.Value, err) @@ -250,7 +254,8 @@ func (s *Server) handle(client RLDP, adnlId, addr string) func(transferId []byte return fmt.Errorf("failed to parse content len: should be >= 0") } } - headers[header.Name] = append(headers[header.Name], header.Value) + + headers[name] = append(headers[name], header.Value) } headers.Set("X-Adnl-Ip", netAddr.IP.String()) headers.Set("X-Adnl-Id", adnlId) diff --git a/example/nft-info/main.go b/example/nft-info/main.go index ecef810c..5ff3254c 100644 --- a/example/nft-info/main.go +++ b/example/nft-info/main.go @@ -44,7 +44,7 @@ func main() { case *nft.ContentOffchain: fmt.Println(" content offchain :", content.URI) case *nft.ContentOnchain: - fmt.Println(" content onchain :", content.Name) + fmt.Println(" content onchain :", content.GetAttribute("name")) } fmt.Println(" owner :", collectionData.OwnerAddress.String()) fmt.Println(" minted items num :", collectionData.NextItemIndex) @@ -69,7 +69,7 @@ func main() { fmt.Println(" full content :", nftContent.(*nft.ContentOffchain).URI) } case *nft.ContentOnchain: - fmt.Println(" content name :", content.Name) + fmt.Println(" content name :", content.GetAttribute("name")) } } else { fmt.Println(" empty content") diff --git a/tlb/coins.go b/tlb/coins.go index f7b5b1b4..3eb0fa48 100644 --- a/tlb/coins.go +++ b/tlb/coins.go @@ -222,3 +222,15 @@ func (g *Coins) UnmarshalJSON(data []byte) error { return nil } + +func (g *Coins) Compare(coins *Coins) int { + if g.decimals != coins.decimals { + panic("invalid comparsion") + } + + return g.Nano().Cmp(coins.Nano()) +} + +func (g *Coins) Decimals() int { + return g.decimals +} diff --git a/tlb/state-init.go b/tlb/state-init.go index be662235..6202ae75 100644 --- a/tlb/state-init.go +++ b/tlb/state-init.go @@ -1,6 +1,7 @@ package tlb import ( + "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tvm/cell" ) @@ -16,3 +17,8 @@ type StateInit struct { Data *cell.Cell `tlb:"maybe ^"` Lib *cell.Dictionary `tlb:"dict 256"` } + +func (s StateInit) CalcAddress(workchain int) *address.Address { + c, _ := ToCell(s) + return address.NewAddress(0, byte(workchain), c.Hash()) +} diff --git a/tlb/state-init_test.go b/tlb/state-init_test.go new file mode 100644 index 00000000..e74abb78 --- /dev/null +++ b/tlb/state-init_test.go @@ -0,0 +1,49 @@ +package tlb + +import ( + "github.com/xssnick/tonutils-go/tvm/cell" + "testing" +) + +func TestStateInit_CalcAddress(t *testing.T) { + tests := []struct { + name string + stateInit StateInit + workchain int + want string + }{ + { + name: "Base", + stateInit: StateInit{ + Code: cell.BeginCell().MustStoreUInt(0, 8).EndCell(), + Data: cell.BeginCell().MustStoreUInt(0, 8).EndCell(), + }, + workchain: 0, + want: "EQBPQF6r6-pUObVWu6RO05YwoHQRnjM95tRLAL_s2A6n0pvq", + }, + { + name: "Empty", + stateInit: StateInit{}, + workchain: 0, + want: "EQA_B407fiLIlE5VYZCaI2rki0in6kLyjdhhwitvZNfpe7eY", + }, + { + name: "Master", + stateInit: StateInit{ + Code: cell.BeginCell().MustStoreUInt(123, 8).EndCell(), + Data: cell.BeginCell().MustStoreUInt(456, 16).EndCell(), + }, + workchain: -1, + want: "Ef_jHHi5wLtyTaS56iIEPUc9mJuoD2keQPxZX87rl2FcVDZ1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.stateInit.CalcAddress(tt.workchain) + if got.String() != tt.want { + t.Errorf("StateInit.CalcAddress() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/ton/api.go b/ton/api.go index 8ded6d8a..1e61d21a 100644 --- a/ton/api.go +++ b/ton/api.go @@ -62,6 +62,7 @@ type APIClientWrapped interface { GetMasterchainInfo(ctx context.Context) (*BlockIDExt, error) GetAccount(ctx context.Context, block *BlockIDExt, addr *address.Address) (*tlb.Account, error) SendExternalMessage(ctx context.Context, msg *tlb.ExternalMessage) error + SendExternalMessageWaitTransaction(ctx context.Context, msg *tlb.ExternalMessage) (*tlb.Transaction, *BlockIDExt, []byte, error) RunGetMethod(ctx context.Context, blockInfo *BlockIDExt, addr *address.Address, method string, params ...interface{}) (*ExecutionResult, error) ListTransactions(ctx context.Context, addr *address.Address, num uint32, lt uint64, txHash []byte) ([]*tlb.Transaction, error) GetTransaction(ctx context.Context, block *BlockIDExt, addr *address.Address, lt uint64) (*tlb.Transaction, error) diff --git a/ton/getstate.go b/ton/getstate.go index 17fadc23..b4d5edc6 100644 --- a/ton/getstate.go +++ b/ton/getstate.go @@ -73,7 +73,8 @@ func (c *APIClient) GetAccount(ctx context.Context, block *BlockIDExt, addr *add } var shardHash []byte - if c.proofCheckPolicy != ProofCheckPolicyUnsafe && addr.Workchain() != address.MasterchainID { + if c.proofCheckPolicy != ProofCheckPolicyUnsafe && addr.Workchain() != address.MasterchainID && + block.Workchain == address.MasterchainID { if len(t.ShardProof) == 0 { return nil, ErrNoProof } diff --git a/ton/integration_test.go b/ton/integration_test.go index a8db93b6..5e4fbb43 100644 --- a/ton/integration_test.go +++ b/ton/integration_test.go @@ -3,6 +3,7 @@ package ton import ( "bytes" "context" + "encoding/base64" "encoding/hex" "errors" "fmt" @@ -204,7 +205,7 @@ func Test_RunMethod(t *testing.T) { } func Test_ExternalMessage(t *testing.T) { // need to deploy contract on test-net - > than change config to test-net. - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) defer cancel() ctx = apiTestNet.Client().StickyContext(ctx) @@ -229,7 +230,7 @@ func Test_ExternalMessage(t *testing.T) { // need to deploy contract on test-net MustStoreUInt(1, 16). // add 1 to total EndCell() - err = apiTestNet.SendExternalMessage(ctx, &tlb.ExternalMessage{ + tx, block, _, err := apiTestNet.SendExternalMessageWaitTransaction(ctx, &tlb.ExternalMessage{ DstAddr: testContractAddrTestNet, Body: data, }) @@ -239,9 +240,7 @@ func Test_ExternalMessage(t *testing.T) { // need to deploy contract on test-net return } - // TODO: wait for update and check result - - log.Printf("Current seqno = %d and total = %d", seqno, total) + log.Printf("Current seqno = %d and total = %d | block: %d tx: %d hash: %s", seqno, total, block.SeqNo, tx.LT, base64.URLEncoding.EncodeToString(tx.Hash)) } func Test_Account(t *testing.T) { @@ -601,8 +600,8 @@ func TestAccountStorage_LoadFromCell_ExtraCurrencies(t *testing.T) { t.Run("with proof", func(t *testing.T) { _, err := mainnetAPI.GetAccount(ctx, b, address.MustParseAddr("EQCYv992KVNNCKZHSLLJgM2GGzsgL0UgWP24BCQBaAdqSE2I")) - if err != ErrNoProof { - t.Fatal(err) + if err != nil { + t.Fatal("no proof") } }) @@ -783,7 +782,7 @@ func TestAPIClient_FindLastTransactionByInMsgHash(t *testing.T) { } func TestAPIClient_FindLastTransactionByOutMsgHash(t *testing.T) { - addr := address.MustParseAddr("UQCZ-7akCw_dvl_Q5xyriWqCXdWubIPbuN7aDQlzX45pa01R") + addr := address.MustParseAddr("EQB3ncyBUTjZUA5EnFKR5_EnOMI9V1tTEAAPaiU71gc4TiUt") block, err := api.CurrentMasterchainInfo(context.Background()) if err != nil { @@ -802,6 +801,10 @@ func TestAPIClient_FindLastTransactionByOutMsgHash(t *testing.T) { var hash []byte for i := len(list) - 1; i >= 0; i-- { + if list[i].IO.Out == nil { + continue + } + ls, err := list[i].IO.Out.ToSlice() if err != nil { continue diff --git a/ton/nft/content.go b/ton/nft/content.go index a325a09e..ca13e30b 100644 --- a/ton/nft/content.go +++ b/ton/nft/content.go @@ -16,11 +16,15 @@ type ContentOffchain struct { } type ContentOnchain struct { - Name string + // Deprecated: use GetAttribute("name") + Name string + // Deprecated: use GetAttribute("description") Description string - Image string - ImageData []byte - attributes *cell.Dictionary + // Deprecated: use GetAttribute("image") + Image string + // Deprecated: use GetAttributeBinary("image_data") + ImageData []byte + attributes *cell.Dictionary } type ContentSemichain struct { diff --git a/ton/proof.go b/ton/proof.go index 9f287ed4..e9cd54c4 100644 --- a/ton/proof.go +++ b/ton/proof.go @@ -23,6 +23,7 @@ func init() { } var ErrNoProof = fmt.Errorf("liteserver has no proof for this account in a given block, request newer block or disable proof checks") +var ErrNoAddrInProof = errors.New("no addr info in proof hashmap") func CheckShardMcStateExtraProof(master *BlockIDExt, shardProof []*cell.Cell) (*tlb.McStateExtra, error) { shardState, err := CheckBlockShardStateProof(shardProof, master.RootHash) @@ -113,7 +114,7 @@ func CheckAccountStateProof(addr *address.Address, block *BlockIDExt, stateProof if !skipBlockCheck { blockHash := block.RootHash // we need shard proof only for not masterchain - if len(shardHash) > 0 { + if len(shardHash) > 0 && block.Workchain == address.MasterchainID { if err := CheckShardInMasterProof(block, shardProof, addr.Workchain(), shardHash); err != nil { return nil, nil, fmt.Errorf("shard proof is incorrect: %w", err) } @@ -145,7 +146,7 @@ func CheckAccountStateProof(addr *address.Address, block *BlockIDExt, stateProof addrKey := cell.BeginCell().MustStoreSlice(addr.Data(), 256).EndCell() val := shardState.Accounts.ShardAccounts.Get(addrKey) if val == nil { - return nil, nil, errors.New("no addr info in proof hashmap") + return nil, nil, ErrNoAddrInProof } loadVal := val.BeginParse() diff --git a/ton/retrier.go b/ton/retrier.go index 70e17be5..6f09a31d 100644 --- a/ton/retrier.go +++ b/ton/retrier.go @@ -16,10 +16,14 @@ type retryClient struct { } func (w *retryClient) QueryLiteserver(ctx context.Context, payload tl.Serializable, result tl.Serializable) error { - tries := w.maxRetries + const maxRounds = 2 + + tries, rounds := 0, 0 + ctxBackup := ctx + for { err := w.original.QueryLiteserver(ctx, payload, result) - if w.maxRetries > 0 && tries == w.maxRetries { + if w.maxRetries > 0 && tries >= w.maxRetries { return err } tries++ @@ -37,6 +41,13 @@ func (w *retryClient) QueryLiteserver(ctx context.Context, payload tl.Serializab // try next node ctx, err = w.original.StickyContextNextNode(ctx) if err != nil { + rounds++ + if rounds < maxRounds { + // try same nodes one more time + ctx = ctxBackup + continue + } + 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) } @@ -50,7 +61,15 @@ func (w *retryClient) QueryLiteserver(ctx context.Context, payload tl.Serializab lsErr.Code == -400 || lsErr.Code == -503 || (lsErr.Code == 0 && strings.Contains(lsErr.Text, "Failed to get account state"))) { + if ctx, err = w.original.StickyContextNextNode(ctx); err != nil { // try next node + rounds++ + if rounds < maxRounds { + // try same nodes one more time + ctx = ctxBackup + continue + } + // no more nodes left, return as it is return nil } diff --git a/ton/runmethod.go b/ton/runmethod.go index 063be2bb..30c68776 100644 --- a/ton/runmethod.go +++ b/ton/runmethod.go @@ -95,9 +95,10 @@ func (c *APIClient) RunGetMethod(ctx context.Context, blockInfo *BlockIDExt, add var shardProof []*cell.Cell var shardHash []byte - if c.proofCheckPolicy != ProofCheckPolicyUnsafe && addr.Workchain() != address.MasterchainID { + if c.proofCheckPolicy != ProofCheckPolicyUnsafe && addr.Workchain() != address.MasterchainID && + blockInfo.Workchain == address.MasterchainID { if len(t.ShardProof) == 0 { - return nil, fmt.Errorf("liteserver has no proof for this account in a given block, request newer block or disable proof checks") + return nil, ErrNoProof } shardProof = t.ShardProof diff --git a/ton/sendmessagewait.go b/ton/sendmessagewait.go new file mode 100644 index 00000000..ec08198a --- /dev/null +++ b/ton/sendmessagewait.go @@ -0,0 +1,127 @@ +package ton + +import ( + "bytes" + "context" + "errors" + "fmt" + "time" + + "github.com/xssnick/tonutils-go/tlb" +) + +var ErrTxWasNotConfirmed = errors.New("transaction was not confirmed in a given deadline, but it may still be confirmed later") + +func (c *APIClient) SendExternalMessageWaitTransaction(ctx context.Context, ext *tlb.ExternalMessage) (*tlb.Transaction, *BlockIDExt, []byte, error) { + block, err := c.CurrentMasterchainInfo(ctx) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get block: %w", err) + } + + acc, err := c.WaitForBlock(block.SeqNo).GetAccount(ctx, block, ext.DstAddr) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get account state: %w", err) + } + + inMsgHash := ext.Body.Hash() + + if err = c.SendExternalMessage(ctx, ext); err != nil { + return nil, nil, nil, fmt.Errorf("failed to send message: %w", err) + } + + tx, block, err := c.waitConfirmation(ctx, block, acc, ext) + if err != nil { + return nil, nil, nil, err + } + + return tx, block, inMsgHash, nil +} + +func (c *APIClient) waitConfirmation(ctx context.Context, block *BlockIDExt, acc *tlb.Account, ext *tlb.ExternalMessage) (*tlb.Transaction, *BlockIDExt, error) { + if _, hasDeadline := ctx.Deadline(); !hasDeadline { + // fallback timeout to not stuck forever with background context + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), 180*time.Second) + defer cancel() + } + till, _ := ctx.Deadline() + + ctx = c.Client().StickyContext(ctx) + + for time.Now().Before(till) { + blockNew, err := c.WaitForBlock(block.SeqNo + 1).GetMasterchainInfo(ctx) + if err != nil { + continue + } + + accNew, err := c.WaitForBlock(blockNew.SeqNo).GetAccount(ctx, blockNew, ext.DstAddr) + if err != nil { + continue + } + block = blockNew + + if accNew.LastTxLT == acc.LastTxLT { + // if not in block, maybe LS lost our message, send it again + if err = c.SendExternalMessage(ctx, ext); err != nil { + continue + } + + continue + } + + lastLt, lastHash := accNew.LastTxLT, accNew.LastTxHash + + // it is possible that > 5 new not related transactions will happen, and we should not lose our scan offset, + // to prevent this we will scan till we reach last seen offset. + for time.Now().Before(till) { + // we try to get last 5 transactions, and check if we have our new there. + txList, err := c.WaitForBlock(block.SeqNo).ListTransactions(ctx, ext.DstAddr, 5, lastLt, lastHash) + if err != nil { + continue + } + + sawLastTx := false + for i, transaction := range txList { + if i == 0 { + // get previous of the oldest tx, in case if we need to scan deeper + lastLt, lastHash = txList[0].PrevTxLT, txList[0].PrevTxHash + } + + if !sawLastTx && transaction.PrevTxLT == acc.LastTxLT && + bytes.Equal(transaction.PrevTxHash, acc.LastTxHash) { + sawLastTx = true + } + + if transaction.IO.In != nil && transaction.IO.In.MsgType == tlb.MsgTypeExternalIn { + extIn := transaction.IO.In.AsExternalIn() + if ext.StateInit != nil { + if extIn.StateInit == nil { + continue + } + + if !bytes.Equal(ext.StateInit.Data.Hash(), extIn.StateInit.Data.Hash()) { + continue + } + + if !bytes.Equal(ext.StateInit.Code.Hash(), extIn.StateInit.Code.Hash()) { + continue + } + } + + if !bytes.Equal(extIn.Body.Hash(), ext.Body.Hash()) { + continue + } + + return transaction, block, nil + } + } + + if sawLastTx { + break + } + } + acc = accNew + } + + return nil, nil, ErrTxWasNotConfirmed +} diff --git a/ton/wallet/address.go b/ton/wallet/address.go index a3700618..1a7592e0 100644 --- a/ton/wallet/address.go +++ b/ton/wallet/address.go @@ -5,7 +5,6 @@ import ( "context" "crypto/ed25519" "fmt" - "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/tvm/cell" @@ -66,6 +65,8 @@ func GetStateInit(pubKey ed25519.PublicKey, version VersionConfig, subWallet uin ver = V5R1Beta case ConfigV5R1Final: ver = V5R1Final + case ConfigCustom: + return v.GetStateInit(pubKey, subWallet) } code, ok := walletCode[ver] diff --git a/ton/wallet/custom.go b/ton/wallet/custom.go new file mode 100644 index 00000000..679bc285 --- /dev/null +++ b/ton/wallet/custom.go @@ -0,0 +1,17 @@ +package wallet + +import ( + "context" + "crypto/ed25519" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/tvm/cell" +) + +type ConfigCustom interface { + GetStateInit(pubKey ed25519.PublicKey, subWallet uint32) (*tlb.StateInit, error) + GetSpec(w *Wallet) MessageBuilder +} + +type MessageBuilder interface { + BuildMessage(ctx context.Context, messages []*Message) (*cell.Cell, error) +} diff --git a/ton/wallet/custom_test.go b/ton/wallet/custom_test.go new file mode 100644 index 00000000..192dfe76 --- /dev/null +++ b/ton/wallet/custom_test.go @@ -0,0 +1,175 @@ +package wallet + +import ( + "context" + "crypto/ed25519" + "encoding/hex" + "fmt" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/tvm/cell" + "testing" +) + +type configCustomV5R1 struct { + code *cell.Cell + ConfigV5R1Final +} + +func newConfigCustomV5R1(code *cell.Cell) ConfigCustom { + return &configCustomV5R1{code: code, ConfigV5R1Final: ConfigV5R1Final{ + NetworkGlobalID: MainnetGlobalID, + }} +} + +type customSpecV5R1 struct { + SpecV5R1Final +} + +func (c *customSpecV5R1) BuildMessage(ctx context.Context, messages []*Message) (*cell.Cell, error) { + return c.SpecV5R1Final.BuildMessage(ctx, false, nil, messages) +} + +func (c *configCustomV5R1) GetSpec(w *Wallet) MessageBuilder { + return &customSpecV5R1{SpecV5R1Final: SpecV5R1Final{ + SpecRegular: SpecRegular{ + wallet: w, + messagesTTL: 60 * 3, + }, + SpecSeqno: SpecSeqno{seqnoFetcher: nil}, + config: c.ConfigV5R1Final, + }} +} + +func (c *configCustomV5R1) GetStateInit(pubKey ed25519.PublicKey, subWallet uint32) (*tlb.StateInit, error) { + walletId := V5R1ID{ + NetworkGlobalID: c.NetworkGlobalID, + WorkChain: c.Workchain, + SubwalletNumber: uint16(subWallet), + WalletVersion: 0, + } + + data := cell.BeginCell(). + MustStoreBoolBit(true). + MustStoreUInt(0, 32). + MustStoreUInt(uint64(walletId.Serialized()), 32). + MustStoreSlice(pubKey, 256). + MustStoreDict(nil). + EndCell() + + return &tlb.StateInit{ + Data: data, + Code: c.code, + }, nil +} + +func TestConfigCustom_CmpV5SubWalletAddress(t *testing.T) { + pkey := ed25519.NewKeyFromSeed([]byte("12345678901234567890123456789012")) + cfg := newConfigCustomV5R1(walletCode[V5R1Final]) + wCustom, err := FromPrivateKey(nil, pkey, cfg) + if err != nil { + t.Fatalf("failed to get custom v5r1 wallet from pk, err: %s", err) + } + + wCustomSub, err := wCustom.GetSubwallet(1) + if err != nil { + t.Fatalf("failed to get custom sub v5r1 wallet, err: %s", err) + } + + wOrig, err := FromPrivateKey(nil, pkey, ConfigV5R1Final{ + NetworkGlobalID: MainnetGlobalID, + }) + if err != nil { + t.Fatalf("failed to get orig v5r1 wallet from pk, err: %s", err) + } + + wOrigSub, err := wOrig.GetSubwallet(1) + if err != nil { + t.Fatalf("failed to get orig sub v5r1 wallet, err: %s", err) + } + + if !wCustomSub.WalletAddress().Equals(wOrigSub.WalletAddress()) { + t.Error("orig and custom v5r1 wallet address mismatch") + } +} + +type configCustomHighloadV3 struct { + code *cell.Cell + ConfigHighloadV3 +} + +func newConfigCustomHighloadV3(code *cell.Cell) ConfigCustom { + return &configCustomHighloadV3{code: code, ConfigHighloadV3: ConfigHighloadV3{ + MessageTTL: 60, + MessageBuilder: func(ctx context.Context, subWalletId uint32) (id uint32, createdAt int64, err error) { + return 1, 1733333333, nil + }, + }} +} + +func (c *configCustomHighloadV3) GetSpec(w *Wallet) MessageBuilder { + return &SpecHighloadV3{wallet: w, config: c.ConfigHighloadV3} +} + +func (c *configCustomHighloadV3) GetStateInit(pubKey ed25519.PublicKey, subWallet uint32) (*tlb.StateInit, error) { + timeout := c.MessageTTL + if timeout >= 1<<22 { + return nil, fmt.Errorf("too big timeout") + } + + data := cell.BeginCell(). + MustStoreSlice(pubKey, 256). + MustStoreUInt(uint64(subWallet), 32). + MustStoreUInt(0, 66). + MustStoreUInt(uint64(timeout), 22). + EndCell() + + return &tlb.StateInit{ + Data: data, + Code: c.code, + }, nil +} + +func TestConfigCustom_V3BocTx(t *testing.T) { + pkey := ed25519.NewKeyFromSeed([]byte("12345678901234567890123456789012")) + cfg := newConfigCustomHighloadV3(walletCode[HighloadV3]) + wCustom, err := FromPrivateKey(nil, pkey, cfg) + if err != nil { + t.Fatalf("failed to get custom HL3 wallet from pk, err: %s", err) + } + + wCustomSub, err := wCustom.GetSubwallet(1) + if err != nil { + t.Fatalf("failed to get custom sub HL3 wallet, err: %s", err) + } + + wCustomSubExtMgs, _ := wCustomSub.PrepareExternalMessageForMany(context.Background(), false, []*Message{SimpleMessage(wCustomSub.WalletAddress(), tlb.MustFromTON("0.5"), nil)}) + wCustomSubExtMgsCell, _ := tlb.ToCell(wCustomSubExtMgs) + wCustomSubExtMgsBocHex := hex.EncodeToString(wCustomSubExtMgsCell.ToBOCWithFlags(false)) + + wOrig, err := FromPrivateKey(nil, pkey, ConfigHighloadV3{ + MessageTTL: 60, + MessageBuilder: func(ctx context.Context, subWalletId uint32) (id uint32, createdAt int64, err error) { + return 1, 1733333333, nil + }, + }) + if err != nil { + t.Fatalf("failed to get orig HL3 wallet from pk, err: %s", err) + } + + wOrigSub, err := wOrig.GetSubwallet(1) + if err != nil { + t.Fatalf("failed to get orig sub HL3 wallet, err: %s", err) + } + + wOrigSubExtMgs, _ := wCustomSub.PrepareExternalMessageForMany(context.Background(), false, []*Message{SimpleMessage(wOrigSub.WalletAddress(), tlb.MustFromTON("0.5"), nil)}) + wOrigSubExtMgsCell, _ := tlb.ToCell(wOrigSubExtMgs) + wOrigSubExtMgsBocHex := hex.EncodeToString(wOrigSubExtMgsCell.ToBOCWithFlags(false)) + + if !wCustomSub.WalletAddress().Equals(wOrigSub.WalletAddress()) { + t.Error("orig and custom HL3 wallet address mismatch") + } + + if wCustomSubExtMgsBocHex != wOrigSubExtMgsBocHex { + t.Error("orig and custom ext boc msg mismatch") + } +} diff --git a/ton/wallet/highloadv2r2.go b/ton/wallet/highloadv2r2.go index 4078d6cd..9d47e289 100644 --- a/ton/wallet/highloadv2r2.go +++ b/ton/wallet/highloadv2r2.go @@ -22,7 +22,7 @@ type SpecHighloadV2R2 struct { SpecQuery } -func (s *SpecHighloadV2R2) BuildMessage(_ context.Context, messages []*Message) (*cell.Cell, error) { +func (s *SpecHighloadV2R2) BuildMessage(ctx context.Context, messages []*Message) (*cell.Cell, error) { if len(messages) > 254 { return nil, errors.New("for this type of wallet max 254 messages can be sent in the same time") } @@ -47,7 +47,11 @@ func (s *SpecHighloadV2R2) BuildMessage(_ context.Context, messages []*Message) var ttl, queryID uint32 if s.customQueryIDFetcher != nil { - ttl, queryID = s.customQueryIDFetcher() + var err error + ttl, queryID, err = s.customQueryIDFetcher(ctx, s.wallet.subwallet) + if err != nil { + return nil, fmt.Errorf("failed to fetch queryID: %w", err) + } } else { queryID = randUint32() ttl = uint32(timeNow().Add(time.Duration(s.messagesTTL) * time.Second).UTC().Unix()) diff --git a/ton/wallet/highloadv3.go b/ton/wallet/highloadv3.go index ead72ca0..821bf034 100644 --- a/ton/wallet/highloadv3.go +++ b/ton/wallet/highloadv3.go @@ -43,7 +43,7 @@ func (s *SpecHighloadV3) BuildMessage(ctx context.Context, messages []*Message) queryID, createdAt, err := s.config.MessageBuilder(ctx, s.wallet.subwallet) if err != nil { - return nil, fmt.Errorf("failed to convert msg to cell: %w", err) + return nil, fmt.Errorf("failed to fetch queryID: %w", err) } if queryID >= 1<<23 { @@ -123,6 +123,10 @@ func (s *SpecHighloadV3) packActions(queryId uint64, messages []*Message) (_ *Me list = cell.BeginCell().MustStoreRef(list).MustStoreBuilder(msg).EndCell() } + // attach some coins for internal message processing gas fees + fees := new(big.Int).Add(new(big.Int).Mul(tlb.MustFromTON("0.007").Nano(), big.NewInt(int64(len(messages)))), tlb.MustFromTON("0.01").Nano()) + amt = new(big.Int).Add(amt, fees) + return &Message{ Mode: PayGasSeparately + IgnoreErrors, InternalMessage: &tlb.InternalMessage{ diff --git a/ton/wallet/integration_test.go b/ton/wallet/integration_test.go index 80ce3fab..c83cfe37 100644 --- a/ton/wallet/integration_test.go +++ b/ton/wallet/integration_test.go @@ -21,6 +21,8 @@ import ( "github.com/xssnick/tonutils-go/tvm/cell" ) +const emptyWalletSeedEnvFatalMsg = "WALLET_SEED not found in environment" + var api = func() ton.APIClientWrapped { client := liteclient.NewConnectionPool() @@ -52,6 +54,9 @@ var apiMain = func() ton.APIClientWrapped { var _seed = os.Getenv("WALLET_SEED") func Test_HighloadHeavyTransfer(t *testing.T) { + if _seed == "" { + t.Fatal(emptyWalletSeedEnvFatalMsg) + } seed := strings.Split(_seed, " ") w, err := FromSeed(api, seed, ConfigHighloadV3{ @@ -84,6 +89,9 @@ func Test_HighloadHeavyTransfer(t *testing.T) { } func Test_V5HeavyTransfer(t *testing.T) { + if _seed == "" { + t.Fatal(emptyWalletSeedEnvFatalMsg) + } seed := strings.Split(_seed, " ") w, err := FromSeed(api, seed, ConfigV5R1Final{ @@ -112,6 +120,9 @@ func Test_V5HeavyTransfer(t *testing.T) { } func Test_WalletTransfer(t *testing.T) { + if _seed == "" { + t.Fatalf(emptyWalletSeedEnvFatalMsg) + } seed := strings.Split(_seed, " ") for _, v := range []VersionConfig{ConfigV5R1Final{ @@ -192,6 +203,9 @@ func Test_WalletTransfer(t *testing.T) { } func Test_WalletFindTransactionByInMsgHash(t *testing.T) { + if _seed == "" { + t.Fatal(emptyWalletSeedEnvFatalMsg) + } seed := strings.Split(_seed, " ") ctx := api.Client().StickyContext(context.Background()) @@ -232,7 +246,7 @@ func Test_WalletFindTransactionByInMsgHash(t *testing.T) { } // find tx hash - tx, err := w.FindTransactionByInMsgHash(ctx, inMsgHash, 30) + tx, err := w.api.FindLastTransactionByInMsgHash(ctx, w.addr, inMsgHash, 30) if err != nil { t.Fatal("cannot find tx:", err.Error()) } @@ -240,6 +254,9 @@ func Test_WalletFindTransactionByInMsgHash(t *testing.T) { } func TestWallet_DeployContract(t *testing.T) { + if _seed == "" { + t.Fatal(emptyWalletSeedEnvFatalMsg) + } seed := strings.Split(_seed, " ") ctx := api.Client().StickyContext(context.Background()) @@ -280,6 +297,9 @@ func TestWallet_DeployContract(t *testing.T) { } func TestWallet_DeployContractUsingHW3(t *testing.T) { + if _seed == "" { + t.Fatal(emptyWalletSeedEnvFatalMsg) + } seed := strings.Split(_seed, " ") ctx := api.Client().StickyContext(context.Background()) @@ -326,6 +346,9 @@ func TestWallet_DeployContractUsingHW3(t *testing.T) { } func TestWallet_TransferEncrypted(t *testing.T) { + if _seed == "" { + t.Fatal(emptyWalletSeedEnvFatalMsg) + } seed := strings.Split(_seed, " ") ctx := api.Client().StickyContext(context.Background()) @@ -342,6 +365,26 @@ func TestWallet_TransferEncrypted(t *testing.T) { } } +func TestWallet_TransferWaitTransaction(t *testing.T) { + seed := strings.Split(_seed, " ") + ctx := api.Client().StickyContext(context.Background()) + + // init wallet + w, err := FromSeed(api, seed, HighloadV2R2) + if err != nil { + t.Fatal("FromSeed err:", err.Error()) + } + t.Logf("wallet address: %s", w.Address().String()) + + tx, block, err := w.TransferWaitTransaction(ctx, address.MustParseAddr("EQC9bWZd29foipyPOGWlVNVCQzpGAjvi1rGWF7EbNcSVClpA"), tlb.MustFromTON("0.005"), "Hello from tonutils-go!") + if err != nil { + t.Fatal("transfer err:", err) + } + + t.Logf("Transaction: %v", tx) + t.Logf("Block: %v", block) +} + func TestGetWalletVersion(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() diff --git a/ton/wallet/regular.go b/ton/wallet/regular.go index cfd84683..1507c263 100644 --- a/ton/wallet/regular.go +++ b/ton/wallet/regular.go @@ -54,9 +54,16 @@ type SpecQuery struct { // Do not set ttl to high if you are sending many messages, // unexpired executed messages will be cached in contract, // and it may become too expensive to make transactions. - customQueryIDFetcher func() (ttl uint32, randPart uint32) + customQueryIDFetcher func(ctx context.Context, subWalletId uint32) (ttl uint32, randPart uint32, err error) } func (s *SpecQuery) SetCustomQueryIDFetcher(fetcher func() (ttl uint32, randPart uint32)) { + s.SetCustomQueryIDFetcherWithContext(func(ctx context.Context, subWalletId uint32) (ttl uint32, randPart uint32, err error) { + ttl, randPart = fetcher() + return ttl, randPart, nil + }) +} + +func (s *SpecQuery) SetCustomQueryIDFetcherWithContext(fetcher func(ctx context.Context, subWalletId uint32) (ttl uint32, randPart uint32, err error)) { s.customQueryIDFetcher = fetcher } diff --git a/ton/wallet/wallet.go b/ton/wallet/wallet.go index ba30e287..84a272df 100644 --- a/ton/wallet/wallet.go +++ b/ton/wallet/wallet.go @@ -68,6 +68,8 @@ func (v Version) String() string { return fmt.Sprintf("highload V2R2") case HighloadV2Verified: return fmt.Sprintf("highload V2R2 verified") + case HighloadV3: + return fmt.Sprintf("highload V3") } if v/100 == 2 { @@ -121,9 +123,6 @@ var timeNow = time.Now var ( ErrUnsupportedWalletVersion = errors.New("wallet version is not supported") - ErrTxWasNotConfirmed = errors.New("transaction was not confirmed in a given deadline, but it may still be confirmed later") - // Deprecated: use ton.ErrTxWasNotFound - ErrTxWasNotFound = errors.New("requested transaction is not found") ) type TonAPI interface { @@ -132,6 +131,7 @@ type TonAPI interface { CurrentMasterchainInfo(ctx context.Context) (*ton.BlockIDExt, error) GetAccount(ctx context.Context, block *ton.BlockIDExt, addr *address.Address) (*tlb.Account, error) SendExternalMessage(ctx context.Context, msg *tlb.ExternalMessage) error + SendExternalMessageWaitTransaction(ctx context.Context, ext *tlb.ExternalMessage) (*tlb.Transaction, *ton.BlockIDExt, []byte, error) RunGetMethod(ctx context.Context, blockInfo *ton.BlockIDExt, addr *address.Address, method string, params ...interface{}) (*ton.ExecutionResult, error) ListTransactions(ctx context.Context, addr *address.Address, num uint32, lt uint64, txHash []byte) ([]*tlb.Transaction, error) FindLastTransactionByInMsgHash(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) @@ -246,6 +246,8 @@ func getSpec(w *Wallet) (any, error) { } case ConfigHighloadV3: return &SpecHighloadV3{wallet: w, config: v}, nil + case ConfigCustom: + return v.GetSpec(w), nil } return nil, fmt.Errorf("cannot init spec: %w", ErrUnsupportedWalletVersion) @@ -289,6 +291,10 @@ func (w *Wallet) GetSubwallet(subwallet uint32) (*Wallet, error) { return sub, nil } +func (w *Wallet) GetSubwalletID() uint32 { + return w.subwallet +} + func (w *Wallet) GetBalance(ctx context.Context, block *ton.BlockIDExt) (tlb.Coins, error) { acc, err := w.api.WaitForBlock(block.SeqNo).GetAccount(ctx, block, w.addr) if err != nil { @@ -372,6 +378,11 @@ func (w *Wallet) PrepareExternalMessageForMany(ctx context.Context, withStateIni if err != nil { return nil, fmt.Errorf("build message err: %w", err) } + case ConfigCustom: + msg, err = w.spec.(MessageBuilder).BuildMessage(ctx, messages) + if err != nil { + return nil, fmt.Errorf("build message err: %w", err) + } default: return nil, fmt.Errorf("send is not yet supported: %w", ErrUnsupportedWalletVersion) } @@ -465,124 +476,30 @@ func (w *Wallet) SendWaitTransaction(ctx context.Context, message *Message) (*tl return w.SendManyWaitTransaction(ctx, []*Message{message}) } -func (w *Wallet) sendMany(ctx context.Context, messages []*Message, waitConfirmation ...bool) (tx *tlb.Transaction, block *ton.BlockIDExt, inMsgHash []byte, err error) { - block, err = w.api.CurrentMasterchainInfo(ctx) +// TransferWaitTransaction always waits for tx block confirmation and returns found tx. +func (w *Wallet) TransferWaitTransaction(ctx context.Context, to *address.Address, amount tlb.Coins, comment string) (*tlb.Transaction, *ton.BlockIDExt, error) { + transfer, err := w.BuildTransfer(to, amount, to.IsBounceable(), comment) if err != nil { - return nil, nil, nil, fmt.Errorf("failed to get block: %w", err) + return nil, nil, err } - acc, err := w.api.WaitForBlock(block.SeqNo).GetAccount(ctx, block, w.addr) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to get account state: %w", err) - } + return w.SendManyWaitTransaction(ctx, []*Message{transfer}) +} +func (w *Wallet) sendMany(ctx context.Context, messages []*Message, waitConfirmation ...bool) (tx *tlb.Transaction, block *ton.BlockIDExt, inMsgHash []byte, err error) { ext, err := w.BuildExternalMessageForMany(ctx, messages) if err != nil { return nil, nil, nil, err } - inMsgHash = ext.Body.Hash() - - if err = w.api.SendExternalMessage(ctx, ext); err != nil { - return nil, nil, nil, fmt.Errorf("failed to send message: %w", err) - } if len(waitConfirmation) > 0 && waitConfirmation[0] { - tx, block, err = w.waitConfirmation(ctx, block, acc, ext) - if err != nil { - return nil, nil, nil, err - } - } - - return tx, block, inMsgHash, nil -} - -func (w *Wallet) waitConfirmation(ctx context.Context, block *ton.BlockIDExt, acc *tlb.Account, ext *tlb.ExternalMessage) (*tlb.Transaction, *ton.BlockIDExt, error) { - if _, hasDeadline := ctx.Deadline(); !hasDeadline { - // fallback timeout to not stuck forever with background context - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(context.Background(), 180*time.Second) - defer cancel() + return w.api.SendExternalMessageWaitTransaction(ctx, ext) } - till, _ := ctx.Deadline() - - ctx = w.api.Client().StickyContext(ctx) - - for time.Now().Before(till) { - blockNew, err := w.api.WaitForBlock(block.SeqNo + 1).GetMasterchainInfo(ctx) - if err != nil { - continue - } - - accNew, err := w.api.WaitForBlock(blockNew.SeqNo).GetAccount(ctx, blockNew, w.addr) - if err != nil { - continue - } - block = blockNew - if accNew.LastTxLT == acc.LastTxLT { - // if not in block, maybe LS lost our message, send it again - if err = w.api.SendExternalMessage(ctx, ext); err != nil { - continue - } - - continue - } - - lastLt, lastHash := accNew.LastTxLT, accNew.LastTxHash - - // it is possible that > 5 new not related transactions will happen, and we should not lose our scan offset, - // to prevent this we will scan till we reach last seen offset. - for time.Now().Before(till) { - // we try to get last 5 transactions, and check if we have our new there. - txList, err := w.api.WaitForBlock(block.SeqNo).ListTransactions(ctx, w.addr, 5, lastLt, lastHash) - if err != nil { - continue - } - - sawLastTx := false - for i, transaction := range txList { - if i == 0 { - // get previous of the oldest tx, in case if we need to scan deeper - lastLt, lastHash = txList[0].PrevTxLT, txList[0].PrevTxHash - } - - if !sawLastTx && transaction.PrevTxLT == acc.LastTxLT && - bytes.Equal(transaction.PrevTxHash, acc.LastTxHash) { - sawLastTx = true - } - - if transaction.IO.In != nil && transaction.IO.In.MsgType == tlb.MsgTypeExternalIn { - extIn := transaction.IO.In.AsExternalIn() - if ext.StateInit != nil { - if extIn.StateInit == nil { - continue - } - - if !bytes.Equal(ext.StateInit.Data.Hash(), extIn.StateInit.Data.Hash()) { - continue - } - - if !bytes.Equal(ext.StateInit.Code.Hash(), extIn.StateInit.Code.Hash()) { - continue - } - } - - if !bytes.Equal(extIn.Body.Hash(), ext.Body.Hash()) { - continue - } - - return transaction, block, nil - } - } - - if sawLastTx { - break - } - } - acc = accNew + if err = w.api.SendExternalMessage(ctx, ext); err != nil { + return nil, nil, nil, fmt.Errorf("failed to send message: %w", err) } - - return nil, nil, ErrTxWasNotConfirmed + return nil, nil, ext.Body.Hash(), nil } // TransferNoBounce - can be used to transfer TON to not yet initialized contract/wallet @@ -807,16 +724,6 @@ func (w *Wallet) DeployContract(ctx context.Context, amount tlb.Coins, msgBody, return addr, nil } -// Deprecated: use ton.FindLastTransactionByInMsgHash -// FindTransactionByInMsgHash returns transaction in wallet account with incoming message hash equal to msgHash. -func (w *Wallet) FindTransactionByInMsgHash(ctx context.Context, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) { - tx, err := w.api.FindLastTransactionByInMsgHash(ctx, w.addr, msgHash, maxTxNumToScan...) - if err != nil && errors.Is(err, ton.ErrTxWasNotFound) { - return nil, ErrTxWasNotFound - } - return tx, err -} - func SimpleMessage(to *address.Address, amount tlb.Coins, payload *cell.Cell) *Message { return &Message{ Mode: PayGasSeparately + IgnoreErrors, diff --git a/ton/wallet/wallet_test.go b/ton/wallet/wallet_test.go index 47aca381..b7d6b434 100644 --- a/ton/wallet/wallet_test.go +++ b/ton/wallet/wallet_test.go @@ -20,15 +20,20 @@ import ( ) type MockAPI struct { - getBlockInfo func(ctx context.Context) (*ton.BlockIDExt, error) - getAccount func(ctx context.Context, block *ton.BlockIDExt, addr *address.Address) (*tlb.Account, error) - sendExternalMessage func(ctx context.Context, msg *tlb.ExternalMessage) error - runGetMethod func(ctx context.Context, blockInfo *ton.BlockIDExt, addr *address.Address, method string, params ...interface{}) (*ton.ExecutionResult, error) - listTransactions func(ctx context.Context, addr *address.Address, limit uint32, lt uint64, txHash []byte) ([]*tlb.Transaction, error) + getBlockInfo func(ctx context.Context) (*ton.BlockIDExt, error) + getAccount func(ctx context.Context, block *ton.BlockIDExt, addr *address.Address) (*tlb.Account, error) + sendExternalMessage func(ctx context.Context, msg *tlb.ExternalMessage) error + sendExternalMessageWait func(ctx context.Context, ext *tlb.ExternalMessage) (*tlb.Transaction, *ton.BlockIDExt, []byte, error) + runGetMethod func(ctx context.Context, blockInfo *ton.BlockIDExt, addr *address.Address, method string, params ...interface{}) (*ton.ExecutionResult, error) + listTransactions func(ctx context.Context, addr *address.Address, limit uint32, lt uint64, txHash []byte) ([]*tlb.Transaction, error) extMsgSent *tlb.ExternalMessage } +func (m MockAPI) SendExternalMessageWaitTransaction(ctx context.Context, ext *tlb.ExternalMessage) (*tlb.Transaction, *ton.BlockIDExt, []byte, error) { + return m.sendExternalMessageWait(ctx, ext) +} + func (m MockAPI) FindLastTransactionByInMsgHash(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) { //TODO implement me panic("implement me") @@ -41,11 +46,12 @@ func (m MockAPI) FindLastTransactionByOutMsgHash(ctx context.Context, addr *addr func (m MockAPI) WaitForBlock(seqno uint32) ton.APIClientWrapped { return &WaiterMock{ - MGetMasterchainInfo: m.getBlockInfo, - MGetAccount: m.getAccount, - MSendExternalMessage: m.sendExternalMessage, - MRunGetMethod: m.runGetMethod, - MListTransactions: m.listTransactions, + MGetMasterchainInfo: m.getBlockInfo, + MGetAccount: m.getAccount, + MSendExternalMessage: m.sendExternalMessage, + MRunGetMethod: m.runGetMethod, + MListTransactions: m.listTransactions, + MSendExternalMessageWaitTransaction: m.sendExternalMessageWait, } } @@ -257,6 +263,13 @@ func TestWallet_Send(t *testing.T) { return nil } + m.sendExternalMessageWait = func(ctx context.Context, ext *tlb.ExternalMessage) (*tlb.Transaction, *ton.BlockIDExt, []byte, error) { + if err := m.sendExternalMessage(ctx, ext); err != nil { + return nil, nil, nil, err + } + return &tlb.Transaction{}, &ton.BlockIDExt{}, make([]byte, 32), nil + } + msg := &Message{ Mode: CarryAllRemainingBalance, InternalMessage: intMsg, @@ -462,25 +475,26 @@ func checkHighloadV2R2(t *testing.T, p *cell.Slice, w *Wallet, intMsg *tlb.Inter } type WaiterMock struct { - MGetTime func(ctx context.Context) (uint32, error) - MLookupBlock func(ctx context.Context, workchain int32, shard int64, seqno uint32) (*ton.BlockIDExt, error) - MGetBlockData func(ctx context.Context, block *ton.BlockIDExt) (*tlb.Block, error) - MGetBlockTransactionsV2 func(ctx context.Context, block *ton.BlockIDExt, count uint32, after ...*ton.TransactionID3) ([]ton.TransactionShortInfo, bool, error) - MGetBlockShardsInfo func(ctx context.Context, master *ton.BlockIDExt) ([]*ton.BlockIDExt, error) - MGetBlockchainConfig func(ctx context.Context, block *ton.BlockIDExt, onlyParams ...int32) (*ton.BlockchainConfig, error) - MGetMasterchainInfo func(ctx context.Context) (*ton.BlockIDExt, error) - MGetAccount func(ctx context.Context, block *ton.BlockIDExt, addr *address.Address) (*tlb.Account, error) - MSendExternalMessage func(ctx context.Context, msg *tlb.ExternalMessage) error - MRunGetMethod func(ctx context.Context, blockInfo *ton.BlockIDExt, addr *address.Address, method string, params ...interface{}) (*ton.ExecutionResult, error) - MListTransactions func(ctx context.Context, addr *address.Address, num uint32, lt uint64, txHash []byte) ([]*tlb.Transaction, error) - MGetTransaction func(ctx context.Context, block *ton.BlockIDExt, addr *address.Address, lt uint64) (*tlb.Transaction, error) - MWaitForBlock func(seqno uint32) ton.APIClientWrapped - MWithRetry func(x ...int) ton.APIClientWrapped - MWithTimeout func(timeout time.Duration) ton.APIClientWrapped - MCurrentMasterchainInfo func(ctx context.Context) (_ *ton.BlockIDExt, err error) - MGetBlockProof func(ctx context.Context, known, target *ton.BlockIDExt) (*ton.PartialBlockProof, error) - MFindLastTransactionByInMsgHash func(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) - MFindLastTransactionByOutMsgHash func(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) + MGetTime func(ctx context.Context) (uint32, error) + MLookupBlock func(ctx context.Context, workchain int32, shard int64, seqno uint32) (*ton.BlockIDExt, error) + MGetBlockData func(ctx context.Context, block *ton.BlockIDExt) (*tlb.Block, error) + MGetBlockTransactionsV2 func(ctx context.Context, block *ton.BlockIDExt, count uint32, after ...*ton.TransactionID3) ([]ton.TransactionShortInfo, bool, error) + MGetBlockShardsInfo func(ctx context.Context, master *ton.BlockIDExt) ([]*ton.BlockIDExt, error) + MGetBlockchainConfig func(ctx context.Context, block *ton.BlockIDExt, onlyParams ...int32) (*ton.BlockchainConfig, error) + MGetMasterchainInfo func(ctx context.Context) (*ton.BlockIDExt, error) + MGetAccount func(ctx context.Context, block *ton.BlockIDExt, addr *address.Address) (*tlb.Account, error) + MSendExternalMessage func(ctx context.Context, msg *tlb.ExternalMessage) error + MRunGetMethod func(ctx context.Context, blockInfo *ton.BlockIDExt, addr *address.Address, method string, params ...interface{}) (*ton.ExecutionResult, error) + MListTransactions func(ctx context.Context, addr *address.Address, num uint32, lt uint64, txHash []byte) ([]*tlb.Transaction, error) + MGetTransaction func(ctx context.Context, block *ton.BlockIDExt, addr *address.Address, lt uint64) (*tlb.Transaction, error) + MWaitForBlock func(seqno uint32) ton.APIClientWrapped + MWithRetry func(x ...int) ton.APIClientWrapped + MWithTimeout func(timeout time.Duration) ton.APIClientWrapped + MCurrentMasterchainInfo func(ctx context.Context) (_ *ton.BlockIDExt, err error) + MGetBlockProof func(ctx context.Context, known, target *ton.BlockIDExt) (*ton.PartialBlockProof, error) + MFindLastTransactionByInMsgHash func(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) + MFindLastTransactionByOutMsgHash func(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) + MSendExternalMessageWaitTransaction func(ctx context.Context, msg *tlb.ExternalMessage) (*tlb.Transaction, *ton.BlockIDExt, []byte, error) } func (w WaiterMock) FindLastTransactionByInMsgHash(ctx context.Context, addr *address.Address, msgHash []byte, maxTxNumToScan ...int) (*tlb.Transaction, error) { @@ -521,6 +535,10 @@ func (w WaiterMock) Client() ton.LiteClient { panic("implement me") } +func (w WaiterMock) SendExternalMessageWaitTransaction(ctx context.Context, msg *tlb.ExternalMessage) (*tlb.Transaction, *ton.BlockIDExt, []byte, error) { + return w.MSendExternalMessageWaitTransaction(ctx, msg) +} + func (w WaiterMock) CurrentMasterchainInfo(ctx context.Context) (_ *ton.BlockIDExt, err error) { return w.MCurrentMasterchainInfo(ctx) } @@ -602,7 +620,7 @@ func TestCreateEncryptedCommentCell(t *testing.T) { return } - msg := randString(150 + i) + msg := "Hello, world!!!" sender := address.MustParseAddr("EQC9bWZd29foipyPOGWlVNVCQzpGAjvi1rGWF7EbNcSVClpA") c, err := CreateEncryptedCommentCell(msg, sender, priv1, pub2)