From d6abfabbde35c3e42b3a716af327305a37710d12 Mon Sep 17 00:00:00 2001 From: Denis Subbotin Date: Tue, 26 May 2026 23:22:53 +0300 Subject: [PATCH 1/5] update tonutils-go --- blockchain/blockchain.go | 24 ++++++++++++++++++----- blockchain/shard_tracker.go | 2 +- core/block_scanner.go | 39 +++++++++++++++++++++++++++++-------- core/wallets.go | 5 ++++- go.mod | 14 ++++++------- go.sum | 12 ++++++++++++ 6 files changed, 74 insertions(+), 22 deletions(-) diff --git a/blockchain/blockchain.go b/blockchain/blockchain.go index e9066c2..c9726d4 100644 --- a/blockchain/blockchain.go +++ b/blockchain/blockchain.go @@ -140,8 +140,11 @@ func getConfigData(ctx context.Context, api ton.APIClientWrapped) (*address.Addr if data == nil { return nil, nil, fmt.Errorf("failed to get root address from blockchain config") } - - hash, err := data.BeginParse().LoadSlice(256) + slice, err := data.BeginParse() + if err != nil { + return nil, nil, fmt.Errorf("failed to get root address from blockchain config 2: %w", err) + } + hash, err := slice.LoadSlice(256) if err != nil { return nil, nil, fmt.Errorf("failed to get root address from blockchain config 4, failed to load hash: %w", err) } @@ -262,9 +265,12 @@ func getWalletRecord(d *dns.Domain) *address.Address { if rec == nil { return nil } - p := rec.BeginParse() + p, err := rec.BeginParse() + if err != nil { + return nil + } - p, err := p.LoadRef() + p, err = p.LoadRef() if err != nil { return nil } @@ -659,7 +665,15 @@ func getBlockchainConfig(ctx context.Context, client ton.LiteClient, block *ton. switch t := resp.(type) { case ton.ConfigAll: - stateExtra, err := ton.CheckShardMcStateExtraProof(block, []*cell.Cell{t.StateProof, t.ConfigProof}) + stateProof, err := cell.FromBOC(t.StateProof) + if err != nil { + return nil, err + } + configProof, err := cell.FromBOC(t.ConfigProof) + if err != nil { + return nil, err + } + stateExtra, err := ton.CheckShardMcStateExtraProof(block, []*cell.Cell{stateProof, configProof}) if err != nil { return nil, fmt.Errorf("incorrect proof: %w", err) } diff --git a/blockchain/shard_tracker.go b/blockchain/shard_tracker.go index 41ebda6..2c8adab 100644 --- a/blockchain/shard_tracker.go +++ b/blockchain/shard_tracker.go @@ -202,7 +202,7 @@ func filterByShard(headers []*ton.BlockIDExt, shard byte) *ton.BlockIDExt { } func convertBlockToShardHeader(block *tlb.Block, info *ton.BlockIDExt, shard byte) (core.ShardBlockHeader, error) { - parents, err := block.BlockInfo.GetParentBlocks() + parents, err := ton.GetParentBlocks(&block.BlockInfo) if err != nil { return core.ShardBlockHeader{}, err } diff --git a/core/block_scanner.go b/core/block_scanner.go index aaedfb2..6c5f98d 100644 --- a/core/block_scanner.go +++ b/core/block_scanner.go @@ -478,7 +478,11 @@ func decodeJettonTransferNotification(msg *tlb.InternalMessage) (jettonTransferN Sender *address.Address `tlb:"addr"` ForwardPayload *cell.Cell `tlb:"either . ^"` } - err := tlb.LoadFromCell(¬ification, payload.BeginParse()) + slice, err := payload.BeginParse() + if err != nil { + return jettonTransferNotificationMsg{}, err + } + err = tlb.LoadFromCell(¬ification, slice) if err != nil { return jettonTransferNotificationMsg{}, err } @@ -507,7 +511,11 @@ func DecodeJettonTransfer(msg *tlb.InternalMessage) (JettonTransferMsg, error) { ForwardTonAmount tlb.Coins `tlb:"."` ForwardPayload *cell.Cell `tlb:"either . ^"` } - err := tlb.LoadFromCell(&transfer, payload.BeginParse()) + slice, err := payload.BeginParse() + if err != nil { + return JettonTransferMsg{}, err + } + err = tlb.LoadFromCell(&transfer, slice) if err != nil { return JettonTransferMsg{}, err } @@ -530,7 +538,11 @@ func decodeJettonExcesses(msg *tlb.InternalMessage) (int64, error) { _ tlb.Magic `tlb:"#d53276db"` QueryID uint64 `tlb:"## 64"` } - err := tlb.LoadFromCell(&excesses, payload.BeginParse()) + slice, err := payload.BeginParse() + if err != nil { + return 0, err + } + err = tlb.LoadFromCell(&excesses, slice) if err != nil { return 0, err } @@ -552,13 +564,16 @@ func parseExternalMessage(msg *tlb.ExternalMessage) ( if err != nil { return uuid.UUID{}, nil, false, err } - - for _, m := range info.Messages.All() { + messages, err := info.Messages.LoadAll() + if err != nil { + return uuid.UUID{}, nil, false, err + } + for _, m := range messages { var ( intMsg tlb.InternalMessage addr Address ) - msgCell, err := m.Value.BeginParse().LoadRef() + msgCell, err := m.Value.LoadRef() if err != nil { return uuid.UUID{}, nil, false, err } @@ -619,7 +634,11 @@ func getHighLoadWalletExtMsgInfo(extMsg *tlb.ExternalMessage) (HighLoadWalletExt BoundedID uint64 `tlb:"## 64"` Messages *cell.Dictionary `tlb:"dict 16"` } - err = tlb.LoadFromCell(&data, body.BeginParse()) + slice, err := body.BeginParse() + if err != nil { + return HighLoadWalletExtMsgInfo{}, err + } + err = tlb.LoadFromCell(&data, slice) if err != nil { return HighLoadWalletExtMsgInfo{}, err } @@ -719,7 +738,11 @@ func (s *BlockScanner) processTonHotWalletExternalInMsg(tx *tlb.Transaction) (Ev func (s *BlockScanner) processTonHotWalletProxyMsg(msg *tlb.InternalMessage) (Events, error) { var events Events body := msg.Payload() - internalPayload, err := body.BeginParse().LoadRef() + slice, err := body.BeginParse() + if err != nil { + return Events{}, err + } + internalPayload, err := slice.LoadRef() if err != nil { return Events{}, fmt.Errorf("no internal payload to proxy contract: %v", err) } diff --git a/core/wallets.go b/core/wallets.go index 247a673..b2be9b1 100644 --- a/core/wallets.go +++ b/core/wallets.go @@ -208,7 +208,10 @@ func LoadComment(cell *cell.Cell) string { if cell == nil { return "" } - l := cell.BeginParse() + l, err := cell.BeginParse() + if err != nil { + return "" + } if val, err := l.LoadUInt(32); err == nil && val == 0 { str, err := l.LoadStringSnake() if err != nil { diff --git a/go.mod b/go.mod index b4e5939..c528922 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/gobicycle/bicycle -go 1.24.0 - -toolchain go1.24.11 +go 1.25.0 require ( github.com/caarlos0/env/v6 v6.10.1 @@ -13,11 +11,12 @@ require ( github.com/shopspring/decimal v1.4.0 github.com/sirupsen/logrus v1.9.3 github.com/tonkeeper/tongo v1.16.12 - github.com/xssnick/tonutils-go v1.13.1 + github.com/xssnick/tonutils-go v1.17.2 golang.org/x/time v0.10.0 ) require ( + filippo.io/edwards25519 v1.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect @@ -29,14 +28,15 @@ require ( github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/puddle v1.3.0 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect github.com/snksoft/crc v1.1.0 // indirect - golang.org/x/crypto v0.45.0 // indirect + golang.org/x/crypto v0.51.0 // indirect golang.org/x/exp v0.0.0-20230116083435-1de6713980de // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect google.golang.org/protobuf v1.33.0 // indirect ) diff --git a/go.sum b/go.sum index 725937d..7f148a6 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -91,6 +93,8 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae h1:7smdlrfdcZic4VfsGKD2ulWL804a4GVphr4s7WZxGiY= github.com/oasisprotocol/curve25519-voi v0.0.0-20220328075252-7dd334e3daae/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -136,6 +140,8 @@ github.com/tonkeeper/tongo v1.16.12 h1:oAS9z0Kj7okTwVUV7QKAq3E6XjYmx+6DXfYaP8gg4 github.com/tonkeeper/tongo v1.16.12/go.mod h1:MjgIgAytFarjCoVjMLjYEtpZNN1f2G/pnZhKjr28cWs= github.com/xssnick/tonutils-go v1.13.1 h1:eWMD3KoRDX29gjAQIcLJU2Lnnzojr97xpzaMOEFjOeE= github.com/xssnick/tonutils-go v1.13.1/go.mod h1:EDe/9D/HZpAenbR+WPMQHICOF0BZWAe01TU5+Vpg08k= +github.com/xssnick/tonutils-go v1.17.2 h1:WMBjyqJsgnZuc3UU/shxia47DQLjK+3oJtK0Xx2qgIg= +github.com/xssnick/tonutils-go v1.17.2/go.mod h1:Ind3q0JM2N/63zfWR7rSGmUpUZaiGxregm9nZc/xGx4= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -161,6 +167,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20230116083435-1de6713980de h1:DBWn//IJw30uYCgERoxCg84hWtA97F4wMiKOIh00Uf0= golang.org/x/exp v0.0.0-20230116083435-1de6713980de/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -187,6 +195,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -196,6 +206,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 968ed9e3d1947830e57a6a7a871f038c7496955d Mon Sep 17 00:00:00 2001 From: Denis Subbotin Date: Wed, 27 May 2026 00:50:41 +0300 Subject: [PATCH 2/5] add retries --- blockchain/blockchain.go | 100 +++++++++++++++++++++++------------- blockchain/shard_tracker.go | 17 +++--- config/config.go | 2 + 3 files changed, 77 insertions(+), 42 deletions(-) diff --git a/blockchain/blockchain.go b/blockchain/blockchain.go index c9726d4..9491674 100644 --- a/blockchain/blockchain.go +++ b/blockchain/blockchain.go @@ -4,6 +4,12 @@ import ( "context" "errors" "fmt" + "math" + "math/big" + "sort" + "strings" + "time" + "github.com/gobicycle/bicycle/config" "github.com/gobicycle/bicycle/core" log "github.com/sirupsen/logrus" @@ -20,11 +26,6 @@ import ( "github.com/xssnick/tonutils-go/ton/jetton" "github.com/xssnick/tonutils-go/ton/wallet" "github.com/xssnick/tonutils-go/tvm/cell" - "math" - "math/big" - "sort" - "strings" - "time" ) type Connection struct { @@ -52,6 +53,33 @@ type contract struct { Data *boc.Cell } +func retry[T any](f func() (T, error)) (T, error) { + var result T + var err error + for i := 1; i <= config.Config.LiteServerMaxRetries; i++ { + result, err = f() + if err == nil { + return result, nil + } + time.Sleep(time.Duration(config.Config.LiteServerRetryDelay*i) * time.Millisecond) + } + return result, err +} + +func retry2[T1 any, T2 any](fn func() (T1, T2, error)) (T1, T2, error) { + var res1 T1 + var res2 T2 + var err error + for i := 1; i <= config.Config.LiteServerMaxRetries; i++ { + res1, res2, err = fn() + if err == nil { + return res1, res2, nil + } + time.Sleep(time.Duration(config.Config.LiteServerRetryDelay*i) * time.Millisecond) + } + return res1, res2, err +} + // NewConnection creates new Blockchain connection func NewConnection(addr, key string, rateLimit int) (*Connection, error) { @@ -79,7 +107,7 @@ func NewConnection(addr, key string, rateLimit int) (*Connection, error) { return nil, fmt.Errorf("get network config from url err: %s", err.Error()) } - wrappedClient = ton.NewAPIClient(limitedClient, ton.ProofCheckPolicySecure).WithRetry() + wrappedClient = ton.NewAPIClient(limitedClient, ton.ProofCheckPolicySecure) wrappedClient.SetTrustedBlockFromConfig(cfg) log.Infof("Fetching and checking proofs since config init block ...") @@ -90,7 +118,7 @@ func NewConnection(addr, key string, rateLimit int) (*Connection, error) { log.Infof("Proof checks are completed") } else { - wrappedClient = ton.NewAPIClient(limitedClient, ton.ProofCheckPolicyUnsafe).WithRetry() + wrappedClient = ton.NewAPIClient(limitedClient, ton.ProofCheckPolicyUnsafe) } // TODO: replace after tonutils fix @@ -332,7 +360,7 @@ func (c *Connection) GenerateDepositJettonWalletForProxy( } func (c *Connection) getContract(ctx context.Context, addr *address.Address) (contract, error) { - block, err := c.client.CurrentMasterchainInfo(ctx) + block, err := c.CurrentMasterchainInfo(ctx) if err != nil { return contract{}, err } @@ -441,7 +469,7 @@ func (c *Connection) GetJettonBalance(ctx context.Context, address core.Address, // GetLastJettonBalance // Returns jetton balance for last block in basic units func (c *Connection) GetLastJettonBalance(ctx context.Context, address *address.Address) (*big.Int, error) { - masterID, err := c.client.CurrentMasterchainInfo(ctx) + masterID, err := c.CurrentMasterchainInfo(ctx) if err != nil { return nil, err } @@ -455,7 +483,7 @@ func (c *Connection) GetLastJettonBalance(ctx context.Context, address *address. // GetAccountCurrentState // Returns TON balance in nanoTONs and account status func (c *Connection) GetAccountCurrentState(ctx context.Context, address *address.Address) (*big.Int, tlb.AccountStatus, error) { - masterID, err := c.client.CurrentMasterchainInfo(ctx) + masterID, err := c.CurrentMasterchainInfo(ctx) if err != nil { return nil, "", err } @@ -465,7 +493,7 @@ func (c *Connection) GetAccountCurrentState(ctx context.Context, address *addres case <-ctx.Done(): return nil, "", core.ErrTimeoutExceeded default: - account, err := c.client.GetAccount(ctx, masterID, address) + account, err := c.GetAccount(ctx, masterID, address) if err != nil && isNotReadyError(err) { time.Sleep(time.Millisecond * 200) continue @@ -510,7 +538,9 @@ func (c *Connection) GetTransactionIDsFromBlock(ctx context.Context, blockID *to next = true ) for next { - fetchedIDs, more, err := c.client.GetBlockTransactionsV2(ctx, blockID, 256, after) + fetchedIDs, more, err := retry2(func() ([]ton.TransactionShortInfo, bool, error) { + return c.client.GetBlockTransactionsV2(ctx, blockID, 256, after) + }) if err != nil { return nil, err } @@ -531,7 +561,9 @@ func (c *Connection) GetTransactionIDsFromBlock(ctx context.Context, blockID *to // GetTransactionFromBlock // Gets transaction from block func (c *Connection) GetTransactionFromBlock(ctx context.Context, blockID *ton.BlockIDExt, txID ton.TransactionShortInfo) (*tlb.Transaction, error) { - tx, err := c.client.GetTransaction(ctx, blockID, address.NewAddress(0, byte(blockID.Workchain), txID.Account), txID.LT) + tx, err := retry(func() (*tlb.Transaction, error) { + return c.client.GetTransaction(ctx, blockID, address.NewAddress(0, byte(blockID.Workchain), txID.Account), txID.LT) + }) if err != nil { return nil, err } @@ -543,7 +575,9 @@ func inShard(addr core.Address, shard byte) bool { } func (c *Connection) getCurrentNodeTime(ctx context.Context) (time.Time, error) { - t, err := c.client.GetTime(ctx) + t, err := retry(func() (uint32, error) { + return c.client.GetTime(ctx) + }) if err != nil { return time.Time{}, err } @@ -596,16 +630,11 @@ func (c *Connection) WaitStatus(ctx context.Context, addr *address.Address, stat // GetAccount // The method is being redefined for more stable operation. -// Gets account from prev block if impossible to get it from current block. Be careful with diff calculation between blocks. func (c *Connection) GetAccount(ctx context.Context, block *ton.BlockIDExt, addr *address.Address) (*tlb.Account, error) { - res, err := c.client.GetAccount(ctx, block, addr) - if err != nil && isNotReadyError(err) { - prevBlock, err := c.client.LookupBlock(ctx, block.Workchain, block.Shard, block.SeqNo-1) - if err != nil { - return nil, err - } - return c.client.GetAccount(ctx, prevBlock, addr) - } + res, err := retry(func() (*tlb.Account, error) { + return c.client.GetAccount(ctx, block, addr) + }) + return res, err } @@ -622,30 +651,29 @@ func (c *Connection) RunGetMethod(ctx context.Context, block *ton.BlockIDExt, ad case <-ctx.Done(): return nil, core.ErrTimeoutExceeded default: - res, err := c.client.RunGetMethod(ctx, block, addr, method, params...) - if err != nil && isNotReadyError(err) { - time.Sleep(time.Millisecond * 200) - continue - } - return res, err + return retry(func() (*ton.ExecutionResult, error) { + return c.client.RunGetMethod(ctx, block, addr, method, params...) + }) } } } func (c *Connection) ListTransactions(ctx context.Context, addr *address.Address, num uint32, lt uint64, txHash []byte) ([]*tlb.Transaction, error) { - return c.client.ListTransactions(ctx, addr, num, lt, txHash) -} - -func (c *Connection) Client() ton.LiteClient { - return c.client.Client() + return retry(func() ([]*tlb.Transaction, error) { + return c.client.ListTransactions(ctx, addr, num, lt, txHash) + }) } func (c *Connection) CurrentMasterchainInfo(ctx context.Context) (*ton.BlockIDExt, error) { - return c.client.CurrentMasterchainInfo(ctx) + return retry(func() (*ton.BlockIDExt, error) { + return c.client.CurrentMasterchainInfo(ctx) + }) } func (c *Connection) GetMasterchainInfo(ctx context.Context) (*ton.BlockIDExt, error) { - return c.client.GetMasterchainInfo(ctx) + return retry(func() (*ton.BlockIDExt, error) { + return c.client.GetMasterchainInfo(ctx) + }) } func (c *Connection) SendExternalMessageWaitTransaction(ctx context.Context, ext *tlb.ExternalMessage) (*tlb.Transaction, *ton.BlockIDExt, []byte, error) { diff --git a/blockchain/shard_tracker.go b/blockchain/shard_tracker.go index 2c8adab..d700ad5 100644 --- a/blockchain/shard_tracker.go +++ b/blockchain/shard_tracker.go @@ -2,13 +2,14 @@ package blockchain import ( "context" + "math/bits" + "strings" + "time" + "github.com/gobicycle/bicycle/core" log "github.com/sirupsen/logrus" "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/ton" - "math/bits" - "strings" - "time" ) const ErrBlockNotApplied = "block is not applied" @@ -83,7 +84,7 @@ func (s *ShardTracker) getNext() *core.ShardBlockHeader { func (s *ShardTracker) getNextMasterBlockID(ctx context.Context) (*ton.BlockIDExt, error) { for { - masterBlockID, err := s.connection.client.GetMasterchainInfo(ctx) + masterBlockID, err := s.connection.GetMasterchainInfo(ctx) if err != nil { // exit by context timeout return nil, err @@ -109,7 +110,9 @@ func (s *ShardTracker) loadShardBlocksBatch(masterBlockID *ton.BlockIDExt) (bool ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) defer cancel() for { - shards, err = s.connection.client.GetBlockShardsInfo(ctx, masterBlockID) + shards, err = retry(func() ([]*ton.BlockIDExt, error) { + return s.connection.client.GetBlockShardsInfo(ctx, masterBlockID) + }) if err != nil && isNotReadyError(err) { // TODO: clarify error type time.Sleep(time.Second) continue @@ -224,7 +227,9 @@ func (c *Connection) getShardBlocksHeader(ctx context.Context, shardBlockID *ton block *tlb.Block ) for { - block, err = c.client.GetBlockData(ctx, shardBlockID) + block, err = retry(func() (*tlb.Block, error) { + return c.client.GetBlockData(ctx, shardBlockID) + }) if err != nil && isNotReadyError(err) { time.Sleep(time.Millisecond * 500) continue diff --git a/config/config.go b/config/config.go index 7a1ff5d..b95b4cb 100644 --- a/config/config.go +++ b/config/config.go @@ -41,6 +41,8 @@ var Config = struct { LiteServer string `env:"LITESERVER,required"` LiteServerKey string `env:"LITESERVER_KEY,required"` LiteServerRateLimit int `env:"LITESERVER_RATE_LIMIT" envDefault:"100"` + LiteServerMaxRetries int `env:"LITESERVER_MAX_RETRIES" envDefault:"10"` + LiteServerRetryDelay int `env:"LITESERVER_BASE_RETRY_DELAY" envDefault:"100"` // ms Seed string `env:"SEED,required"` DatabaseURI string `env:"DB_URI,required"` APIPort int `env:"API_PORT,required"` From 1b0f42ed5def529dec21ee302f9c003b02ce9d48 Mon Sep 17 00:00:00 2001 From: Denis Subbotin Date: Wed, 27 May 2026 11:41:57 +0300 Subject: [PATCH 3/5] rewrite recursion to cycle --- blockchain/shard_tracker.go | 73 +++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/blockchain/shard_tracker.go b/blockchain/shard_tracker.go index d700ad5..427fbe7 100644 --- a/blockchain/shard_tracker.go +++ b/blockchain/shard_tracker.go @@ -123,7 +123,7 @@ func (s *ShardTracker) loadShardBlocksBatch(masterBlockID *ton.BlockIDExt) (bool break } s.infoCounter = 0 - batch, exit, err := s.getShardBlocksRecursively(filterByShard(shards, s.shard), nil) + batch, exit, err := s.getShardBlocks(filterByShard(shards, s.shard)) if err != nil { return false, err } @@ -139,46 +139,49 @@ func (s *ShardTracker) loadShardBlocksBatch(masterBlockID *ton.BlockIDExt) (bool return false, nil } -func (s *ShardTracker) getShardBlocksRecursively(i *ton.BlockIDExt, batch []core.ShardBlockHeader) ([]core.ShardBlockHeader, bool, error) { - if s.gracefulShutdown { - return nil, true, nil - } - if s.lastKnownShardBlock == nil { - s.lastKnownShardBlock = i - } - isKnown := (s.lastKnownShardBlock.Shard == i.Shard) && (s.lastKnownShardBlock.SeqNo == i.SeqNo) - if isKnown { - return batch, false, nil - } +func (s *ShardTracker) getShardBlocks(i *ton.BlockIDExt) ([]core.ShardBlockHeader, bool, error) { + var batch []core.ShardBlockHeader + for { + if s.gracefulShutdown { + return nil, true, nil + } + if s.lastKnownShardBlock == nil { + s.lastKnownShardBlock = i + } + isKnown := (s.lastKnownShardBlock.Shard == i.Shard) && (s.lastKnownShardBlock.SeqNo == i.SeqNo) + if isKnown { + return batch, false, nil + } - // compare seqno with filtered shard block - // handle the case when a node may reference an old block - if s.lastKnownShardBlock.SeqNo > i.SeqNo { - return []core.ShardBlockHeader{}, false, nil - } + // compare seqno with filtered shard block + // handle the case when a node may reference an old block + if s.lastKnownShardBlock.SeqNo > i.SeqNo { + return []core.ShardBlockHeader{}, false, nil + } - seqnoDiff := int(i.SeqNo - s.lastKnownShardBlock.SeqNo) - if seqnoDiff > s.infoStep { - if s.infoCounter%s.infoStep == 0 { - estimatedTime := time.Duration(seqnoDiff/s.infoStep) * time.Since(s.infoLastTime) - s.infoLastTime = time.Now() - if s.infoCounter == 0 { - log.Printf("Shard tracker syncing... Seqno diff: %v Estimated time: unknown\n", seqnoDiff) - } else { - log.Printf("Shard tracker syncing... Seqno diff: %v Estimated time: %v\n", seqnoDiff, estimatedTime) + seqnoDiff := int(i.SeqNo - s.lastKnownShardBlock.SeqNo) + if seqnoDiff > s.infoStep { + if s.infoCounter%s.infoStep == 0 { + estimatedTime := time.Duration(seqnoDiff/s.infoStep) * time.Since(s.infoLastTime) + s.infoLastTime = time.Now() + if s.infoCounter == 0 { + log.Printf("Shard tracker syncing... Seqno diff: %v Estimated time: unknown\n", seqnoDiff) + } else { + log.Printf("Shard tracker syncing... Seqno diff: %v Estimated time: %v\n", seqnoDiff, estimatedTime) + } } + s.infoCounter++ } - s.infoCounter++ - } - ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) - defer cancel() - h, err := s.connection.getShardBlocksHeader(ctx, i, s.shard) - if err != nil { - return nil, false, err + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + h, err := s.connection.getShardBlocksHeader(ctx, i, s.shard) + cancel() + if err != nil { + return nil, false, err + } + batch = append(batch, h) + i = h.Parent } - batch = append(batch, h) - return s.getShardBlocksRecursively(h.Parent, batch) } func isInShard(blockShardPrefix uint64, shard byte) bool { From a20f5665af952e80c9865c4fe9d4e7099d064530 Mon Sep 17 00:00:00 2001 From: GoBicycle <120649456+gobicycle@users.noreply.github.com> Date: Wed, 27 May 2026 13:38:48 +0300 Subject: [PATCH 4/5] Update go.yml --- .github/workflows/go.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index b8b33bb..6d6583c 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,7 +2,7 @@ name: Go on: push: - branches: [ "master" ] + branches: [ "master", "develop" ] workflow_dispatch: {} jobs: @@ -67,4 +67,4 @@ jobs: KEY: ${{ secrets.KEY }} DB_URI: postgresql://pp_user:postgres@localhost:5432/payment_processor?sslmode=disable run: | - go test -v $(go list ./...) \ No newline at end of file + go test -v $(go list ./...) From 0e1aedf0d959850695b04a64127ff21d4c3cb394 Mon Sep 17 00:00:00 2001 From: Denis Subbotin Date: Wed, 27 May 2026 14:55:43 +0300 Subject: [PATCH 5/5] fix retries in tests and handle edge case with zero values --- blockchain/blockchain.go | 6 ++++++ cmd/testutil/main.go | 12 +++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/blockchain/blockchain.go b/blockchain/blockchain.go index 9491674..8c5f915 100644 --- a/blockchain/blockchain.go +++ b/blockchain/blockchain.go @@ -54,6 +54,9 @@ type contract struct { } func retry[T any](f func() (T, error)) (T, error) { + if config.Config.LiteServerMaxRetries == 0 { + return f() + } var result T var err error for i := 1; i <= config.Config.LiteServerMaxRetries; i++ { @@ -67,6 +70,9 @@ func retry[T any](f func() (T, error)) (T, error) { } func retry2[T1 any, T2 any](fn func() (T1, T2, error)) (T1, T2, error) { + if config.Config.LiteServerMaxRetries == 0 { + return fn() + } var res1 T1 var res2 T2 var err error diff --git a/cmd/testutil/main.go b/cmd/testutil/main.go index 7863e0d..3342bb6 100644 --- a/cmd/testutil/main.go +++ b/cmd/testutil/main.go @@ -2,14 +2,15 @@ package main import ( "context" - "github.com/gobicycle/bicycle/blockchain" - "github.com/gobicycle/bicycle/config" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/xssnick/tonutils-go/address" "log" "net/http" "os" "time" + + "github.com/gobicycle/bicycle/blockchain" + "github.com/gobicycle/bicycle/config" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/xssnick/tonutils-go/address" ) var ( @@ -41,7 +42,8 @@ func main() { if urlB == "" { log.Fatalf("empty HOST_B env var") } - + config.Config.LiteServerMaxRetries = 10 + config.Config.LiteServerRetryDelay = 100 hotWalletA, err := address.ParseAddr(os.Getenv("HOT_WALLET_A")) if err != nil { log.Fatalf("invalid HOT_WALLET_A env var")