From 22b0bfed2b31148c19e146471f58087c7328ec60 Mon Sep 17 00:00:00 2001 From: Dan Volkov Date: Wed, 31 Jul 2024 21:36:46 +0300 Subject: [PATCH] feat: add SendExternalMessageWaitTransaction api for APIClient --- ton/api.go | 1 + ton/sendmessagewait.go | 127 ++++++++++++++++++++++++++++++++++++++ ton/wallet/wallet_test.go | 43 +++++++------ 3 files changed, 152 insertions(+), 19 deletions(-) create mode 100644 ton/sendmessagewait.go 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/sendmessagewait.go b/ton/sendmessagewait.go new file mode 100644 index 00000000..5ba36fe4 --- /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 (api *APIClient) SendExternalMessageWaitTransaction(ctx context.Context, ext *tlb.ExternalMessage) (*tlb.Transaction, *BlockIDExt, []byte, error) { + block, err := api.CurrentMasterchainInfo(ctx) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to get block: %w", err) + } + + acc, err := api.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 = api.SendExternalMessage(ctx, ext); err != nil { + return nil, nil, nil, fmt.Errorf("failed to send message: %w", err) + } + + tx, block, err := api.waitConfirmation(ctx, block, acc, ext) + if err != nil { + return nil, nil, nil, err + } + + return tx, block, inMsgHash, nil +} + +func (api *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 = api.Client().StickyContext(ctx) + + for time.Now().Before(till) { + blockNew, err := api.WaitForBlock(block.SeqNo + 1).GetMasterchainInfo(ctx) + if err != nil { + continue + } + + accNew, err := api.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 = 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 := api.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/wallet_test.go b/ton/wallet/wallet_test.go index 47aca381..1d3cf3ac 100644 --- a/ton/wallet/wallet_test.go +++ b/ton/wallet/wallet_test.go @@ -462,25 +462,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 +522,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) }