diff --git a/README.md b/README.md index 5c39bfbb..48a2d23a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Based on TON][ton-svg]][ton] -![Coverage](https://img.shields.io/badge/Coverage-74.2%25-brightgreen) +![Coverage](https://img.shields.io/badge/Coverage-74.1%25-brightgreen) Golang library for interacting with TON blockchain. diff --git a/adnl/adnl.go b/adnl/adnl.go index ccdf39b0..0595a77d 100644 --- a/adnl/adnl.go +++ b/adnl/adnl.go @@ -352,6 +352,10 @@ func (a *ADNL) SetDisconnectHandler(handler func(addr string, key ed25519.Public a.onDisconnect = handler } +func (a *ADNL) GetDisconnectHandler() func(addr string, key ed25519.PublicKey) { + return a.onDisconnect +} + func (a *ADNL) SetChannelReadyHandler(handler func(ch *Channel)) { a.onChannel = handler } diff --git a/adnl/dht/client_test.go b/adnl/dht/client_test.go index bffb538c..7a9430bc 100644 --- a/adnl/dht/client_test.go +++ b/adnl/dht/client_test.go @@ -53,6 +53,10 @@ type MockADNL struct { close func() } +func (m MockADNL) GetDisconnectHandler() func(addr string, key ed25519.PublicKey) { + return nil +} + func (m MockADNL) GetQueryHandler() func(msg *adnl.MessageQuery) error { return nil } diff --git a/adnl/gateway.go b/adnl/gateway.go index 546a1061..d7cfeaf5 100644 --- a/adnl/gateway.go +++ b/adnl/gateway.go @@ -21,6 +21,7 @@ import ( type Peer interface { SetCustomMessageHandler(handler func(msg *MessageCustom) error) SetQueryHandler(handler func(msg *MessageQuery) error) + GetDisconnectHandler() func(addr string, key ed25519.PublicKey) SetDisconnectHandler(handler func(addr string, key ed25519.PublicKey)) SendCustomMessage(ctx context.Context, req tl.Serializable) error Query(ctx context.Context, req, result tl.Serializable) error @@ -44,6 +45,10 @@ type peerConn struct { client adnlClient } +func (p *peerConn) GetDisconnectHandler() func(addr string, key ed25519.PublicKey) { + return p.client.GetDisconnectHandler() +} + type srvProcessor struct { lastPacketAt time.Time processor func(buf []byte) error diff --git a/adnl/overlay/manager-adnl.go b/adnl/overlay/manager-adnl.go index 15ed6082..636723d1 100644 --- a/adnl/overlay/manager-adnl.go +++ b/adnl/overlay/manager-adnl.go @@ -23,6 +23,7 @@ type ADNL interface { SetCustomMessageHandler(handler func(msg *adnl.MessageCustom) error) SetQueryHandler(handler func(msg *adnl.MessageQuery) error) SetDisconnectHandler(handler func(addr string, key ed25519.PublicKey)) + GetDisconnectHandler() func(addr string, key ed25519.PublicKey) SendCustomMessage(ctx context.Context, req tl.Serializable) error Query(ctx context.Context, req, result tl.Serializable) error Answer(ctx context.Context, queryID []byte, result tl.Serializable) error @@ -43,6 +44,10 @@ type ADNLWrapper struct { ADNL } +func (a *ADNLWrapper) GetDisconnectHandler() func(addr string, key ed25519.PublicKey) { + return a.ADNL.GetDisconnectHandler() +} + func CreateExtendedADNL(adnl ADNL) *ADNLWrapper { w := &ADNLWrapper{ ADNL: adnl, diff --git a/adnl/overlay/manager-rldp.go b/adnl/overlay/manager-rldp.go index 09b69146..86c3e51c 100644 --- a/adnl/overlay/manager-rldp.go +++ b/adnl/overlay/manager-rldp.go @@ -2,6 +2,7 @@ package overlay import ( "context" + "crypto/ed25519" "encoding/hex" "fmt" "github.com/xssnick/tonutils-go/adnl/rldp" @@ -36,7 +37,13 @@ func CreateExtendedRLDP(rldp RLDP) *RLDPWrapper { overlays: map[string]*RLDPOverlayWrapper{}, } w.RLDP.SetOnQuery(w.queryHandler) - w.RLDP.SetOnDisconnect(w.disconnectHandler) + prev := w.GetADNL().GetDisconnectHandler() + w.GetADNL().SetDisconnectHandler(func(addr string, key ed25519.PublicKey) { + if prev != nil { + prev(addr, key) + } + w.disconnectHandler(addr, key) + }) return w } @@ -86,7 +93,7 @@ func (r *RLDPWrapper) queryHandler(transferId []byte, query *rldp.Query) error { return h(transferId, query) } -func (r *RLDPWrapper) disconnectHandler() { +func (r *RLDPWrapper) disconnectHandler(addr string, key ed25519.PublicKey) { var list []func() r.mx.RLock() diff --git a/adnl/rldp/client.go b/adnl/rldp/client.go index b9c1af86..45e6f822 100644 --- a/adnl/rldp/client.go +++ b/adnl/rldp/client.go @@ -20,6 +20,7 @@ type ADNL interface { GetID() []byte SetCustomMessageHandler(handler func(msg *adnl.MessageCustom) error) SetDisconnectHandler(handler func(addr string, key ed25519.PublicKey)) + GetDisconnectHandler() func(addr string, key ed25519.PublicKey) SendCustomMessage(ctx context.Context, req tl.Serializable) error Close() } @@ -68,7 +69,6 @@ func NewClient(a ADNL) *RLDP { } a.SetCustomMessageHandler(r.handleMessage) - a.SetDisconnectHandler(r.handleADNLDisconnect) return r } @@ -87,21 +87,18 @@ func (r *RLDP) SetOnQuery(handler func(transferId []byte, query *Query) error) { r.onQuery = handler } +// Deprecated: use GetADNL().SetDisconnectHandler +// WARNING: it overrides underlying adnl disconnect handler func (r *RLDP) SetOnDisconnect(handler func()) { - r.onDisconnect = handler + r.adnl.SetDisconnectHandler(func(addr string, key ed25519.PublicKey) { + handler() + }) } func (r *RLDP) Close() { r.adnl.Close() } -func (r *RLDP) handleADNLDisconnect(addr string, key ed25519.PublicKey) { - disc := r.onDisconnect - if disc != nil { - disc() - } -} - func (r *RLDP) handleMessage(msg *adnl.MessageCustom) error { isV2 := true switch m := msg.Data.(type) { diff --git a/adnl/rldp/client_test.go b/adnl/rldp/client_test.go index 50bceb77..72592359 100644 --- a/adnl/rldp/client_test.go +++ b/adnl/rldp/client_test.go @@ -41,6 +41,10 @@ func (m MockADNL) RemoteAddr() string { panic("implement me") } +func (m MockADNL) GetDisconnectHandler() func(addr string, key ed25519.PublicKey) { + return nil +} + func (m MockADNL) SetCustomMessageHandler(handler func(msg *adnl.MessageCustom) error) { } diff --git a/adnl/rldp/http/client.go b/adnl/rldp/http/client.go index 87487e3c..e0d9e765 100644 --- a/adnl/rldp/http/client.go +++ b/adnl/rldp/http/client.go @@ -49,6 +49,7 @@ type ADNL interface { RemoteAddr() string Query(ctx context.Context, req, result tl.Serializable) error SetDisconnectHandler(handler func(addr string, key ed25519.PublicKey)) + GetDisconnectHandler() func(addr string, key ed25519.PublicKey) SetCustomMessageHandler(handler func(msg *adnl.MessageCustom) error) SendCustomMessage(ctx context.Context, req tl.Serializable) error SetQueryHandler(handler func(msg *adnl.MessageQuery) error) diff --git a/adnl/rldp/http/client_test.go b/adnl/rldp/http/client_test.go index 9dd5e40f..037065d5 100644 --- a/adnl/rldp/http/client_test.go +++ b/adnl/rldp/http/client_test.go @@ -46,6 +46,10 @@ type MockADNL struct { close func() } +func (m MockADNL) GetDisconnectHandler() func(addr string, key ed25519.PublicKey) { + return nil +} + func (m MockADNL) GetID() []byte { //TODO implement me panic("implement me") diff --git a/example/highload-wallet/main.go b/example/highload-wallet/main.go index b24d0b5f..8db34934 100644 --- a/example/highload-wallet/main.go +++ b/example/highload-wallet/main.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "log" "strings" + "time" "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/liteclient" @@ -16,11 +17,11 @@ import ( func main() { client := liteclient.NewConnectionPool() - // connect to mainnet lite server - err := client.AddConnection(context.Background(), "135.181.140.212:13206", "K0t3+IWLOXHYMvMcrGZDPs+pn58a17LFbnXoQkKc2xw=") + // connect to testnet lite server + configUrl := "https://ton-blockchain.github.io/testnet-global.config.json" + err := client.AddConnectionsFromConfigUrl(context.Background(), configUrl) if err != nil { - log.Fatalln("connection err: ", err.Error()) - return + panic(err) } api := ton.NewAPIClient(client, ton.ProofCheckPolicyFast).WithRetry() @@ -29,7 +30,21 @@ func main() { words := strings.Split("birth pattern then forest walnut then phrase walnut fan pumpkin pattern then cluster blossom verify then forest velvet pond fiction pattern collect then then", " ") // initialize high-load wallet - w, err := wallet.FromSeed(api, words, wallet.HighloadV2R2) + w, err := wallet.FromSeed(api, words, wallet.ConfigHighloadV3{ + MessageTTL: 60 * 5, + MessageBuilder: func(ctx context.Context, subWalletId uint32) (id uint32, createdAt int64, err error) { + // Due to specific of externals emulation on liteserver, + // we need to take something less than or equals to block time, as message creation time, + // otherwise external message will be rejected, because time will be > than emulation time + // hope it will be fixed in the next LS versions + createdAt = time.Now().Unix() - 30 + + // example query id which will allow you to send 1 tx per second + // but you better to implement your own iterator in database, then you can send unlimited + // but make sure id is less than 1 << 23, when it is higher start from 0 again + return uint32(createdAt % (1 << 23)), createdAt, nil + }, + }) if err != nil { log.Fatalln("FromSeed err:", err.Error()) return @@ -65,13 +80,14 @@ func main() { } var messages []*wallet.Message - // generate message for each destination, in single transaction can be sent up to 254 messages + // generate message for each destination, in single batch can be sent up to 65k messages (but consider messages size, external size limit is 64kb) for addrStr, amtStr := range receivers { + addr := address.MustParseAddr(addrStr) messages = append(messages, &wallet.Message{ - Mode: 1, // pay fee separately + Mode: 1 + 2, // pay fee separately, ignore action errors InternalMessage: &tlb.InternalMessage{ - Bounce: false, // force send, even to uninitialized wallets - DstAddr: address.MustParseAddr(addrStr), + Bounce: addr.IsBounceable(), + DstAddr: addr, Amount: tlb.MustFromTON(amtStr), Body: comment, }, @@ -88,7 +104,7 @@ func main() { } log.Println("transaction sent, hash:", base64.StdEncoding.EncodeToString(txHash)) - log.Println("explorer link: https://tonscan.org/tx/" + base64.URLEncoding.EncodeToString(txHash)) + log.Println("explorer link: https://testnet.tonscan.org/tx/" + base64.URLEncoding.EncodeToString(txHash)) return } diff --git a/example/wallet-cold-alike/main.go b/example/wallet-cold-alike/main.go index 389f3198..b2959bae 100644 --- a/example/wallet-cold-alike/main.go +++ b/example/wallet-cold-alike/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/base64" "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/liteclient" "github.com/xssnick/tonutils-go/tlb" @@ -36,56 +37,42 @@ func main() { log.Println("wallet address:", w.WalletAddress()) - block, err := api.CurrentMasterchainInfo(ctx) - if err != nil { - log.Fatalln("CurrentMasterchainInfo err:", err.Error()) - return - } - - balance, err := w.GetBalance(ctx, block) - if err != nil { - log.Fatalln("GetBalance err:", err.Error()) - return - } + addr := address.MustParseAddr("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N") - if balance.Nano().Uint64() >= 3000000 { - addr := address.MustParseAddr("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N") + log.Println("sending transaction...") - log.Println("sending transaction...") + // default message ttl is 3 minutes, it is time during which you can send it to blockchain + // if you need to set longer TTL, you could use this method + // w.GetSpec().(*wallet.SpecV3).SetMessagesTTL(uint32((10 * time.Minute) / time.Second)) - // default message ttl is 3 minutes, it is time during which you can send it to blockchain - // if you need to set longer TTL, you could use this method - // w.GetSpec().(*wallet.SpecV3).SetMessagesTTL(uint32((10 * time.Minute) / time.Second)) + w.GetSpec().(*wallet.SpecV3).SetSeqnoFetcher(func(ctx context.Context, sub uint32) (uint32, error) { + // Get seqno from your database here, this func will be called during BuildTransfer to get seqno for transaction + return 1, nil + }) - // if destination wallet is not initialized you should set bounce = true - msg, err := w.BuildTransfer(addr, tlb.MustFromTON("0.003"), false, "Hello from tonutils-go!") - if err != nil { - log.Fatalln("BuildTransfer err:", err.Error()) - return - } + comment, _ := wallet.CreateCommentCell("Hello from tonutils-go!") + withStateInit := true // if wallet is initialized, you may set false to not send additional data - // pack message to send later or from other place - ext, err := w.BuildExternalMessage(ctx, msg) - if err != nil { - log.Fatalln("BuildExternalMessage err:", err.Error()) - return - } - - // if you wish to send it from diff source, or later, you could serialize it to BoC - // msgCell, _ := ext.ToCell() - // log.Println(base64.StdEncoding.EncodeToString(msgCell.ToBOC())) - - // send message to blockchain - err = api.SendExternalMessage(ctx, ext) - if err != nil { - log.Fatalln("Failed to send external message:", err.Error()) - return - } + // if destination wallet is not initialized you should set bounce = true + ext, err := w.PrepareExternalMessageForMany(context.Background(), withStateInit, []*wallet.Message{ + wallet.SimpleMessageAutoBounce(addr, tlb.MustFromTON("0.003"), comment), + }) + if err != nil { + log.Fatalln("BuildTransfer err:", err.Error()) + return + } - log.Println("transaction sent, we are not waiting for confirmation") + // if you wish to send message from diff source, or later, you could serialize it to BoC + msgCell, _ := tlb.ToCell(ext) + log.Println(base64.StdEncoding.EncodeToString(msgCell.ToBOC())) + // send message to blockchain + if err = api.SendExternalMessage(ctx, ext); err != nil { + log.Fatalln("Failed to send external message:", err.Error()) return } - log.Println("not enough balance:", balance.String()) + log.Println("transaction sent, we are not waiting for confirmation") + + return } diff --git a/example/wallet/main.go b/example/wallet/main.go index ccc4b4c6..d7e8464a 100644 --- a/example/wallet/main.go +++ b/example/wallet/main.go @@ -17,7 +17,7 @@ func main() { client := liteclient.NewConnectionPool() // get config - cfg, err := liteclient.GetConfigFromUrl(context.Background(), "https://ton.org/global.config.json") + cfg, err := liteclient.GetConfigFromUrl(context.Background(), "https://ton.org/testnet-global.config.json") if err != nil { log.Fatalln("get config err: ", err.Error()) return @@ -31,14 +31,14 @@ func main() { } // api client with full proof checks - api := ton.NewAPIClient(client, ton.ProofCheckPolicySecure).WithRetry() + api := ton.NewAPIClient(client, ton.ProofCheckPolicyFast).WithRetry() api.SetTrustedBlockFromConfig(cfg) // bound all requests to single ton node ctx := client.StickyContext(context.Background()) // seed words of account, you can generate them with any wallet or using wallet.NewSeed() method - words := strings.Split("birth pattern then forest walnut then phrase walnut fan pumpkin pattern then cluster blossom verify then forest velvet pond fiction pattern collect then then", " ") + words := strings.Split("diet diet attack autumn expose honey skate lounge holiday opinion village priority major enroll romance famous motor pact hello rubber express warfare rose whisper", " ") w, err := wallet.FromSeed(api, words, wallet.V4R2) if err != nil { diff --git a/ton/wallet/address.go b/ton/wallet/address.go index a44c45ee..bd162a33 100644 --- a/ton/wallet/address.go +++ b/ton/wallet/address.go @@ -13,8 +13,8 @@ import ( const DefaultSubwallet = 698983191 -func AddressFromPubKey(key ed25519.PublicKey, ver Version, subwallet uint32) (*address.Address, error) { - state, err := GetStateInit(key, ver, subwallet) +func AddressFromPubKey(key ed25519.PublicKey, version VersionConfig, subwallet uint32) (*address.Address, error) { + state, err := GetStateInit(key, version, subwallet) if err != nil { return nil, fmt.Errorf("failed to get state: %w", err) } @@ -47,7 +47,19 @@ func GetWalletVersion(account *tlb.Account) Version { return Unknown } -func GetStateInit(pubKey ed25519.PublicKey, ver Version, subWallet uint32) (*tlb.StateInit, error) { +func GetStateInit(pubKey ed25519.PublicKey, version VersionConfig, subWallet uint32) (*tlb.StateInit, error) { + var ver Version + switch v := version.(type) { + case Version: + ver = v + switch ver { + case HighloadV3: + return nil, fmt.Errorf("use ConfigHighloadV3 for highload v3 spec") + } + case ConfigHighloadV3: + ver = HighloadV3 + } + code, ok := walletCode[ver] if !ok { return nil, fmt.Errorf("cannot get code: %w", ErrUnsupportedWalletVersion) @@ -75,6 +87,18 @@ func GetStateInit(pubKey ed25519.PublicKey, ver Version, subWallet uint32) (*tlb MustStoreSlice(pubKey, 256). MustStoreDict(nil). // old queries EndCell() + case HighloadV3: + timeout := version.(ConfigHighloadV3).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() default: return nil, ErrUnsupportedWalletVersion } diff --git a/ton/wallet/highloadv3.go b/ton/wallet/highloadv3.go new file mode 100644 index 00000000..592d5a36 --- /dev/null +++ b/ton/wallet/highloadv3.go @@ -0,0 +1,138 @@ +package wallet + +import ( + "context" + "errors" + "fmt" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/tvm/cell" + "math/big" +) + +// code hex from https://github.com/ton-blockchain/highload-wallet-contract-v3/commit/3d2843747b14bc2a8915606df736d47490cd3d49 +const _HighloadV3CodeHex = "b5ee9c7241021001000228000114ff00f4a413f4bcf2c80b01020120020d02014803040078d020d74bc00101c060b0915be101d0d3030171b0915be0fa4030f828c705b39130e0d31f018210ae42e5a4ba9d8040d721d74cf82a01ed55fb04e030020120050a02027306070011adce76a2686b85ffc00201200809001aabb6ed44d0810122d721d70b3f0018aa3bed44d08307d721d70b1f0201200b0c001bb9a6eed44d0810162d721d70b15800e5b8bf2eda2edfb21ab09028409b0ed44d0810120d721f404f404d33fd315d1058e1bf82325a15210b99f326df82305aa0015a112b992306dde923033e2923033e25230800df40f6fa19ed021d721d70a00955f037fdb31e09130e259800df40f6fa19cd001d721d70a00937fdb31e0915be270801f6f2d48308d718d121f900ed44d0d3ffd31ff404f404d33fd315d1f82321a15220b98e12336df82324aa00a112b9926d32de58f82301de541675f910f2a106d0d31fd4d307d30cd309d33fd315d15168baf2a2515abaf2a6f8232aa15250bcf2a304f823bbf2a35304800df40f6fa199d024d721d70a00f2649130e20e01fe5309800df40f6fa18e13d05004d718d20001f264c858cf16cf8301cf168e1030c824cf40cf8384095005a1a514cf40e2f800c94039800df41704c8cbff13cb1ff40012f40012cb3f12cb15c9ed54f80f21d0d30001f265d3020171b0925f03e0fa4001d70b01c000f2a5fa4031fa0031f401fa0031fa00318060d721d300010f0020f265d2000193d431d19130e272b1fb00b585bf03" + +type ConfigHighloadV3 struct { + // MessageTTL must be > 5 and less than 1<<22 + MessageTTL uint32 + + // This function wil be used to get query id and creation time for the new message. + // ID can be iterator from your database, max id is 1<<23, when it is higher, start from 0 and repeat + // MessageBuilder should be defined if you want to send transactions + MessageBuilder func(ctx context.Context, subWalletId uint32) (id uint32, createdAt int64, err error) +} + +type SpecHighloadV3 struct { + wallet *Wallet + + config ConfigHighloadV3 +} + +func (s *SpecHighloadV3) BuildMessage(ctx context.Context, messages []*Message) (_ *cell.Cell, err error) { + if s.config.MessageBuilder == nil { + return nil, errors.New("query fetcher is not defined in spec config") + } + + if s.config.MessageTTL >= 1<<22 { + return nil, fmt.Errorf("too long ttl") + } + + if s.config.MessageTTL <= 5 { + return nil, fmt.Errorf("too short ttl") + } + + 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) + } + + if queryID >= 1<<23 { + return nil, fmt.Errorf("too big query id") + } + + if createdAt <= 0 { + return nil, fmt.Errorf("created at should be positive") + } + + var msg *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 { + 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") + } + + msgCell, err := tlb.ToCell(msg.InternalMessage) + if err != nil { + return nil, fmt.Errorf("failed to convert msg to cell: %w", err) + } + + payload := cell.BeginCell(). + MustStoreUInt(uint64(s.wallet.subwallet), 32). + MustStoreRef(msgCell). + MustStoreUInt(uint64(msg.Mode), 8). + MustStoreUInt(uint64(queryID), 23). + MustStoreUInt(uint64(createdAt), 64). + MustStoreUInt(uint64(s.config.MessageTTL), 22). + EndCell() + + return cell.BeginCell(). + MustStoreSlice(payload.Sign(s.wallet.key), 512). + 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:]) + if err != nil { + return nil, err + } + messages = append(messages[:253], rest) + } + + var amt = big.NewInt(0) + var list = cell.BeginCell().EndCell() + for _, message := range messages { + amt = amt.Add(amt, message.InternalMessage.Amount.Nano()) + + 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 &Message{ + Mode: 1 + 2, + InternalMessage: &tlb.InternalMessage{ + IHRDisabled: true, + Bounce: false, + DstAddr: s.wallet.addr, + Amount: tlb.FromNanoTON(amt), + Body: cell.BeginCell(). + MustStoreUInt(0xae42e5a4, 32). + MustStoreUInt(queryId, 64). + MustStoreRef(list). + EndCell(), + }, + }, nil +} diff --git a/ton/wallet/integration_test.go b/ton/wallet/integration_test.go index 5aa7f53a..9851343d 100644 --- a/ton/wallet/integration_test.go +++ b/ton/wallet/integration_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/rand" + "encoding/base64" "encoding/binary" "encoding/hex" "fmt" @@ -50,10 +51,48 @@ var apiMain = func() ton.APIClientWrapped { var _seed = os.Getenv("WALLET_SEED") +func Test_HighloadHeavyTransfer(t *testing.T) { + seed := strings.Split(_seed, " ") + + 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()) + return + } + + t.Log("test wallet address:", w.WalletAddress()) + + var list []*Message + for i := 0; i < 300; 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 []Version{V3R2, V4R2, HighloadV2R2, V3R1, V4R1, HighloadV2Verified} { + for _, v := range []VersionConfig{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 + return uint32(tm % (1 << 23)), tm, nil + }, + }} { ver := v for _, isSubwallet := range []bool{false, true} { isSubwallet := isSubwallet @@ -78,7 +117,7 @@ func Test_WalletTransfer(t *testing.T) { } } - log.Println(ver, "-> test wallet address:", w.Address(), isSubwallet) + log.Println(ver, "-> test wallet address:", w.WalletAddress(), isSubwallet) block, err := api.CurrentMasterchainInfo(ctx) if err != nil { @@ -128,7 +167,7 @@ func Test_WalletFindTransactionByInMsgHash(t *testing.T) { body := root.EndCell() // prepare simple transfer - msg := SimpleMessage( + msg := SimpleMessageAutoBounce( address.MustParseAddr("EQA8aJTl0jfFnUZBJjTeUxu9OcbsoPBp9UcHE9upyY_X35kE"), tlb.MustFromTON("0.0031337"), body, @@ -138,6 +177,17 @@ func Test_WalletFindTransactionByInMsgHash(t *testing.T) { inMsgHash, err := w.SendManyGetInMsgHash(ctx, []*Message{msg}, true) t.Logf("message hash: %s", hex.EncodeToString(inMsgHash)) + block, err := api.CurrentMasterchainInfo(ctx) + if err != nil { + t.Fatal(err) + } + + // wait next block to be sure everything updated + block, err = api.WaitForBlock(block.SeqNo + 2).GetMasterchainInfo(ctx) + if err != nil { + t.Fatal("wait master err:", err.Error()) + } + // find tx hash tx, err := w.FindTransactionByInMsgHash(ctx, inMsgHash, 30) if err != nil { diff --git a/ton/wallet/regular.go b/ton/wallet/regular.go index 079d176e..cfd84683 100644 --- a/ton/wallet/regular.go +++ b/ton/wallet/regular.go @@ -7,7 +7,7 @@ import ( ) type RegularBuilder interface { - BuildMessage(ctx context.Context, isInitialized bool, block *ton.BlockIDExt, messages []*Message) (*cell.Cell, error) + BuildMessage(ctx context.Context, isInitialized bool, _ *ton.BlockIDExt, messages []*Message) (*cell.Cell, error) } type SpecRegular struct { @@ -30,11 +30,18 @@ type SpecSeqno struct { // You may use it to set seqno according to your own logic, // for example for additional idempotency, // if build message is not enough, or for additional security - customSeqnoFetcher func() uint32 + seqnoFetcher func(ctx context.Context, subWallet uint32) (uint32, error) } +// Deprecated: Use SetSeqnoFetcher func (s *SpecSeqno) SetCustomSeqnoFetcher(fetcher func() uint32) { - s.customSeqnoFetcher = fetcher + s.seqnoFetcher = func(ctx context.Context, subWallet uint32) (uint32, error) { + return fetcher(), nil + } +} + +func (s *SpecSeqno) SetSeqnoFetcher(fetcher func(ctx context.Context, subWallet uint32) (uint32, error)) { + s.seqnoFetcher = fetcher } type SpecQuery struct { diff --git a/ton/wallet/seed.go b/ton/wallet/seed.go index 298dcd3f..5efcec4d 100644 --- a/ton/wallet/seed.go +++ b/ton/wallet/seed.go @@ -59,11 +59,13 @@ func NewSeedWithPassword(password string) []string { } } -func FromSeed(api TonAPI, seed []string, version Version) (*Wallet, error) { +type VersionConfig any + +func FromSeed(api TonAPI, seed []string, version VersionConfig) (*Wallet, error) { return FromSeedWithPassword(api, seed, "", version) } -func FromSeedWithPassword(api TonAPI, seed []string, password string, version Version) (*Wallet, error) { +func FromSeedWithPassword(api TonAPI, seed []string, password string, version VersionConfig) (*Wallet, error) { // validate seed if len(seed) < 12 { return nil, fmt.Errorf("seed should have at least 12 words") diff --git a/ton/wallet/v3.go b/ton/wallet/v3.go index 3d800a2c..770481bf 100644 --- a/ton/wallet/v3.go +++ b/ton/wallet/v3.go @@ -22,33 +22,21 @@ type SpecV3 struct { SpecSeqno } -func (s *SpecV3) BuildMessage(ctx context.Context, isInitialized bool, block *ton.BlockIDExt, messages []*Message) (*cell.Cell, error) { +func (s *SpecV3) 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) > 4 { return nil, errors.New("for this type of wallet max 4 messages can be sent in the same time") } - var seq uint64 - - if s.customSeqnoFetcher != nil { - seq = uint64(s.customSeqnoFetcher()) - } else { - if isInitialized { - resp, err := s.wallet.api.WaitForBlock(block.SeqNo).RunGetMethod(ctx, block, s.wallet.addr, "seqno") - if err != nil { - return nil, fmt.Errorf("get seqno err: %w", err) - } - - iSeq, err := resp.Int(0) - if err != nil { - return nil, fmt.Errorf("failed to parse seqno: %w", err) - } - seq = iSeq.Uint64() - } + seq, err := s.seqnoFetcher(ctx, s.wallet.subwallet) + if err != nil { + return nil, fmt.Errorf("failed to fetch seqno: %w", err) } payload := cell.BeginCell().MustStoreUInt(uint64(s.wallet.subwallet), 32). MustStoreUInt(uint64(timeNow().Add(time.Duration(s.messagesTTL)*time.Second).UTC().Unix()), 32). - MustStoreUInt(seq, 32) + MustStoreUInt(uint64(seq), 32) for i, message := range messages { intMsg, err := tlb.ToCell(message.InternalMessage) diff --git a/ton/wallet/v4r2.go b/ton/wallet/v4r2.go index 05288d0c..fc861cb0 100644 --- a/ton/wallet/v4r2.go +++ b/ton/wallet/v4r2.go @@ -22,33 +22,21 @@ type SpecV4R2 struct { SpecSeqno } -func (s *SpecV4R2) BuildMessage(ctx context.Context, isInitialized bool, block *ton.BlockIDExt, messages []*Message) (*cell.Cell, error) { +func (s *SpecV4R2) 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) > 4 { return nil, errors.New("for this type of wallet max 4 messages can be sent in the same time") } - var seq uint64 - - if s.customSeqnoFetcher != nil { - seq = uint64(s.customSeqnoFetcher()) - } else { - if isInitialized { - resp, err := s.wallet.api.WaitForBlock(block.SeqNo).RunGetMethod(ctx, block, s.wallet.addr, "seqno") - if err != nil { - return nil, fmt.Errorf("get seqno err: %w", err) - } - - iSeq, err := resp.Int(0) - if err != nil { - return nil, fmt.Errorf("failed to parse seqno: %w", err) - } - seq = iSeq.Uint64() - } + seq, err := s.seqnoFetcher(ctx, s.wallet.subwallet) + if err != nil { + return nil, fmt.Errorf("failed to fetch seqno: %w", err) } payload := cell.BeginCell().MustStoreUInt(uint64(s.wallet.subwallet), 32). MustStoreUInt(uint64(timeNow().Add(time.Duration(s.messagesTTL)*time.Second).UTC().Unix()), 32). - MustStoreUInt(seq, 32). + MustStoreUInt(uint64(seq), 32). MustStoreInt(0, 8) // op for i, message := range messages { diff --git a/ton/wallet/wallet.go b/ton/wallet/wallet.go index a3097d5f..22caec11 100644 --- a/ton/wallet/wallet.go +++ b/ton/wallet/wallet.go @@ -39,6 +39,7 @@ const ( V4R2 Version = 42 HighloadV2R2 Version = 122 HighloadV2Verified Version = 123 + HighloadV3 Version = 300 Lockup Version = 200 Unknown Version = 0 ) @@ -71,7 +72,8 @@ var ( V3R1: _V3R1CodeHex, V3R2: _V3R2CodeHex, V4R1: _V4R1CodeHex, V4R2: _V4R2CodeHex, HighloadV2R2: _HighloadV2R2CodeHex, HighloadV2Verified: _HighloadV2VerifiedCodeHex, - Lockup: _LockupCodeHex, + HighloadV3: _HighloadV3CodeHex, + Lockup: _LockupCodeHex, } walletCodeBOC = map[Version][]byte{} walletCode = map[Version]*cell.Cell{} @@ -126,7 +128,7 @@ type Wallet struct { api TonAPI key ed25519.PrivateKey addr *address.Address - ver Version + ver VersionConfig // Can be used to operate multiple wallets with the same key and version. // use GetSubwallet if you need it. @@ -136,7 +138,7 @@ type Wallet struct { spec any } -func FromPrivateKey(api TonAPI, key ed25519.PrivateKey, version Version) (*Wallet, error) { +func FromPrivateKey(api TonAPI, key ed25519.PrivateKey, version VersionConfig) (*Wallet, error) { addr, err := AddressFromPubKey(key.Public().(ed25519.PublicKey), version, DefaultSubwallet) if err != nil { return nil, err @@ -159,18 +161,46 @@ func FromPrivateKey(api TonAPI, key ed25519.PrivateKey, version Version) (*Walle } func getSpec(w *Wallet) (any, error) { - regular := SpecRegular{ - wallet: w, - messagesTTL: 60 * 3, // default ttl 3 min - } + switch v := w.ver.(type) { + case Version: + regular := SpecRegular{ + wallet: w, + messagesTTL: 60 * 3, // default ttl 3 min + } + + seqnoFetcher := func(ctx context.Context, subWallet uint32) (uint32, error) { + block, err := w.api.CurrentMasterchainInfo(ctx) + if err != nil { + return 0, fmt.Errorf("failed to get block: %w", err) + } + + resp, err := w.api.WaitForBlock(block.SeqNo).RunGetMethod(ctx, block, w.addr, "seqno") + if err != nil { + if cErr, ok := err.(ton.ContractExecError); ok && cErr.Code == ton.ErrCodeContractNotInitialized { + return 0, nil + } + return 0, fmt.Errorf("get seqno err: %w", err) + } - switch w.ver { - case V3R1, V3R2: - return &SpecV3{regular, SpecSeqno{}}, nil - case V4R1, V4R2: - return &SpecV4R2{regular, SpecSeqno{}}, nil - case HighloadV2R2, HighloadV2Verified: - return &SpecHighloadV2R2{regular, SpecQuery{}}, nil + iSeq, err := resp.Int(0) + if err != nil { + return 0, fmt.Errorf("failed to parse seqno: %w", err) + } + return uint32(iSeq.Uint64()), nil + } + + switch v { + case V3R1, V3R2: + return &SpecV3{regular, SpecSeqno{seqnoFetcher: seqnoFetcher}}, nil + case V4R1, V4R2: + return &SpecV4R2{regular, SpecSeqno{seqnoFetcher: seqnoFetcher}}, nil + case HighloadV2R2, HighloadV2Verified: + return &SpecHighloadV2R2{regular, SpecQuery{}}, nil + case HighloadV3: + return nil, fmt.Errorf("use ConfigHighloadV3 for highload v3 spec") + } + case ConfigHighloadV3: + return &SpecHighloadV3{wallet: w, config: v}, nil } return nil, fmt.Errorf("cannot init spec: %w", ErrUnsupportedWalletVersion) @@ -241,8 +271,6 @@ func (w *Wallet) BuildMessageForMany(ctx context.Context, messages []*Message) ( } func (w *Wallet) BuildExternalMessageForMany(ctx context.Context, messages []*Message) (*tlb.ExternalMessage, error) { - var stateInit *tlb.StateInit - block, err := w.api.CurrentMasterchainInfo(ctx) if err != nil { return nil, fmt.Errorf("failed to get block: %w", err) @@ -253,10 +281,15 @@ func (w *Wallet) BuildExternalMessageForMany(ctx context.Context, messages []*Me return nil, fmt.Errorf("failed to get account state: %w", err) } - initialized := true - if !acc.IsActive || acc.State.Status != tlb.AccountStatusActive { - initialized = false + initialized := acc.IsActive && acc.State.Status == tlb.AccountStatusActive + return w.PrepareExternalMessageForMany(ctx, !initialized, messages) +} +// PrepareExternalMessageForMany - Prepares external message for wallet +// can be used directly for offline signing but custom fetchers should be defined in this case +func (w *Wallet) PrepareExternalMessageForMany(ctx context.Context, withStateInit bool, messages []*Message) (_ *tlb.ExternalMessage, err error) { + var stateInit *tlb.StateInit + if withStateInit { stateInit, err = GetStateInit(w.key.Public().(ed25519.PublicKey), w.ver, w.subwallet) if err != nil { return nil, fmt.Errorf("failed to get state init: %w", err) @@ -264,14 +297,26 @@ func (w *Wallet) BuildExternalMessageForMany(ctx context.Context, messages []*Me } var msg *cell.Cell - switch w.ver { - case V3R2, V3R1, V4R2, V4R1: - msg, err = w.spec.(RegularBuilder).BuildMessage(ctx, initialized, block, messages) - if err != nil { - return nil, fmt.Errorf("build message err: %w", err) + switch v := w.ver.(type) { + case Version: + switch v { + case V3R2, V3R1, V4R2, V4R1: + msg, err = w.spec.(RegularBuilder).BuildMessage(ctx, !withStateInit, nil, messages) + if err != nil { + return nil, fmt.Errorf("build message err: %w", err) + } + case HighloadV2R2, HighloadV2Verified: + msg, err = w.spec.(*SpecHighloadV2R2).BuildMessage(ctx, messages) + if err != nil { + return nil, fmt.Errorf("build message err: %w", err) + } + case HighloadV3: + return nil, fmt.Errorf("use ConfigHighloadV3 for highload v3 spec") + default: + return nil, fmt.Errorf("send is not yet supported: %w", ErrUnsupportedWalletVersion) } - case HighloadV2R2, HighloadV2Verified: - msg, err = w.spec.(*SpecHighloadV2R2).BuildMessage(ctx, messages) + case ConfigHighloadV3: + msg, err = w.spec.(*SpecHighloadV3).BuildMessage(ctx, messages) if err != nil { return nil, fmt.Errorf("build message err: %w", err) } @@ -780,3 +825,17 @@ 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, + InternalMessage: &tlb.InternalMessage{ + IHRDisabled: true, + Bounce: to.IsBounceable(), + DstAddr: to, + Amount: amount, + Body: payload, + }, + } +} diff --git a/ton/wallet/wallet_test.go b/ton/wallet/wallet_test.go index 59f6f8a9..d1890214 100644 --- a/ton/wallet/wallet_test.go +++ b/ton/wallet/wallet_test.go @@ -194,6 +194,10 @@ func TestWallet_Send(t *testing.T) { return nil, nil } + if flow == SendWithInit1 || flow == SendWithInit2 { + return nil, ton.ContractExecError{Code: ton.ErrCodeContractNotInitialized} + } + if flow == SeqnoNotInt { return ton.NewExecutionResult([]any{"aaa"}), nil } @@ -295,7 +299,7 @@ func TestWallet_Send(t *testing.T) { continue } case SeqnoNotInt: - if strings.EqualFold(err.Error(), "build message err: failed to parse seqno: incorrect result type") { + if strings.EqualFold(err.Error(), "build message err: failed to fetch seqno: failed to parse seqno: incorrect result type") { continue } case TooMuchMessages: @@ -337,8 +341,8 @@ func checkV4R2(t *testing.T, p *cell.Slice, w *Wallet, flow int, intMsg *tlb.Int seq = 0 } - if p.MustLoadUInt(32) != seq { - t.Fatal("seqno incorrect") + if ld := p.MustLoadUInt(32); ld != seq { + t.Fatal("seqno incorrect", ld, seq) } if p.MustLoadUInt(8) != 0 {