diff --git a/ton/wallet/address.go b/ton/wallet/address.go index 01a1df1..7d73d8a 100644 --- a/ton/wallet/address.go +++ b/ton/wallet/address.go @@ -55,11 +55,15 @@ func GetStateInit(pubKey ed25519.PublicKey, version VersionConfig, subWallet uin switch ver { case HighloadV3: return nil, fmt.Errorf("use ConfigHighloadV3 for highload v3 spec") + case V5Beta: + return nil, fmt.Errorf("use ConfigV5Beta for V5 spec") case V5R1: - return nil, fmt.Errorf("use ConfigV5R1 for v5 spec") + return nil, fmt.Errorf("use ConfigV5R1 for V5 spec") } case ConfigHighloadV3: ver = HighloadV3 + case ConfigV5Beta: + ver = V5Beta case ConfigV5R1: ver = V5R1 } @@ -84,18 +88,36 @@ func GetStateInit(pubKey ed25519.PublicKey, version VersionConfig, subWallet uin MustStoreSlice(pubKey, 256). MustStoreDict(nil). // empty dict of plugins EndCell() - case V5R1: - config := version.(ConfigV5R1) + case V5Beta: + config := version.(ConfigV5Beta) data = cell.BeginCell(). - MustStoreUInt(0, 33). // seqno - MustStoreInt(int64(config.NetworkGlobalID), 32). - MustStoreInt(int64(config.Workchain), 8). - MustStoreUInt(0, 8). // version of v5 - MustStoreUInt(uint64(subWallet), 32). + MustStoreUInt(0, 33). // seqno + MustStoreInt(int64(config.NetworkGlobalID), 32). // network id + MustStoreInt(int64(config.Workchain), 8). // workchain + MustStoreUInt(0, 8). // version of v5 + MustStoreUInt(uint64(subWallet), 32). // default 0 MustStoreSlice(pubKey, 256). MustStoreDict(nil). // empty dict of plugins EndCell() + case V5R1: + config := version.(ConfigV5R1) + + // Create WalletId instance + walletId := WalletId{ + NetworkGlobalID: config.NetworkGlobalID, // -3 Testnet, -239 Mainnet + WorkChain: config.Workchain, + SubwalletNumber: uint16(subWallet), + WalletVersion: 0, // Wallet Version + } + + data = cell.BeginCell(). + MustStoreBoolBit(true). // storeUint(1, 1) - boolean flag for context type + MustStoreUInt(0, 32). // Sequence number, hardcoded as 0 + MustStoreUInt(uint64(walletId.Serialized()), 32). // Serializing WalletId into 32-bit integer + MustStoreSlice(pubKey, 256). // Storing the public key + MustStoreDict(nil). // Storing an empty plugins dictionary + EndCell() case HighloadV2R2, HighloadV2Verified: data = cell.BeginCell(). MustStoreUInt(uint64(subWallet), 32). diff --git a/ton/wallet/v5beta.go b/ton/wallet/v5beta.go new file mode 100644 index 0000000..99c07c4 --- /dev/null +++ b/ton/wallet/v5beta.go @@ -0,0 +1,90 @@ +package wallet + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton" + + "github.com/xssnick/tonutils-go/tvm/cell" +) + +// https://github.com/tonkeeper/tonkeeper-ton/commit/e8a7f3415e241daf4ac723f273fbc12776663c49#diff-c20d462b2e1ec616bbba2db39acc7a6c61edc3d5e768f5c2034a80169b1a56caR29 +const _V5BetaCodeHex = "b5ee9c7241010101002300084202e4cf3b2f4c6d6a61ea0f2b5447d266785b26af3637db2deee6bcd1aa826f34120dcd8e11" + +type ConfigV5Beta struct { + NetworkGlobalID int32 + Workchain int8 +} + +type SpecV5Beta struct { + SpecRegular + SpecSeqno + + config ConfigV5Beta +} + +func (s *SpecV5Beta) BuildMessage(ctx context.Context, _ bool, _ *ton.BlockIDExt, messages []*Message) (_ *cell.Cell, err error) { + // TODO: remove block, now it is here for backwards compatibility + + if len(messages) > 255 { + return nil, errors.New("for this type of wallet max 4 messages can be sent in the same time") + } + + seq, err := s.seqnoFetcher(ctx, s.wallet.subwallet) + if err != nil { + return nil, fmt.Errorf("failed to fetch seqno: %w", err) + } + + actions, err := packV5BetaActions(messages) + if err != nil { + return nil, fmt.Errorf("failed to build actions: %w", err) + } + + payload := cell.BeginCell(). + MustStoreUInt(0x7369676e, 32). // external sign op code + MustStoreInt(int64(s.config.NetworkGlobalID), 32). + MustStoreInt(int64(s.config.Workchain), 8). + MustStoreUInt(0, 8). // version of v5 + MustStoreUInt(uint64(s.wallet.subwallet), 32). + MustStoreUInt(uint64(timeNow().Add(time.Duration(s.messagesTTL)*time.Second).UTC().Unix()), 32). + MustStoreUInt(uint64(seq), 32). + MustStoreBuilder(actions) + + sign := payload.EndCell().Sign(s.wallet.key) + msg := cell.BeginCell().MustStoreBuilder(payload).MustStoreSlice(sign, 512).EndCell() + + return msg, nil +} + +func packV5BetaActions(messages []*Message) (*cell.Builder, error) { + if len(messages) > 255 { + return nil, fmt.Errorf("max 255 messages allowed for v5") + } + + var list = cell.BeginCell().EndCell() + for _, message := range messages { + outMsg, err := tlb.ToCell(message.InternalMessage) + if err != nil { + return nil, err + } + + /* + out_list_empty$_ = OutList 0; + out_list$_ {n:#} prev:^(OutList n) action:OutAction + = OutList (n + 1); + action_send_msg#0ec3c86d mode:(## 8) + out_msg:^(MessageRelaxed Any) = OutAction; + */ + msg := cell.BeginCell().MustStoreUInt(0x0ec3c86d, 32). + MustStoreUInt(uint64(message.Mode), 8). + MustStoreRef(outMsg) + + list = cell.BeginCell().MustStoreRef(list).MustStoreBuilder(msg).EndCell() + } + + return cell.BeginCell().MustStoreUInt(0, 1).MustStoreRef(list), nil +} diff --git a/ton/wallet/v5r1.go b/ton/wallet/v5r1.go index 8c560ca..9e57c1c 100644 --- a/ton/wallet/v5r1.go +++ b/ton/wallet/v5r1.go @@ -4,15 +4,17 @@ import ( "context" "errors" "fmt" + "time" + "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/ton" - "time" "github.com/xssnick/tonutils-go/tvm/cell" ) -// https://github.com/tonkeeper/tonkeeper-ton/commit/e8a7f3415e241daf4ac723f273fbc12776663c49#diff-c20d462b2e1ec616bbba2db39acc7a6c61edc3d5e768f5c2034a80169b1a56caR29 -const _V5R1CodeHex = "b5ee9c7241010101002300084202e4cf3b2f4c6d6a61ea0f2b5447d266785b26af3637db2deee6bcd1aa826f34120dcd8e11" +// Contract source: +// https://github.com/ton-blockchain/wallet-contract-v5/blob/main/build/wallet_v5.compiled.json +const _V5R1CodeHex = "b5ee9c7241021401000281000114ff00f4a413f4bcf2c80b01020120020d020148030402dcd020d749c120915b8f6320d70b1f2082106578746ebd21821073696e74bdb0925f03e082106578746eba8eb48020d72101d074d721fa4030fa44f828fa443058bd915be0ed44d0810141d721f4058307f40e6fa1319130e18040d721707fdb3ce03120d749810280b99130e070e2100f020120050c020120060902016e07080019adce76a2684020eb90eb85ffc00019af1df6a2684010eb90eb858fc00201480a0b0017b325fb51341c75c875c2c7e00011b262fb513435c280200019be5f0f6a2684080a0eb90fa02c0102f20e011e20d70b1f82107369676ebaf2e08a7f0f01e68ef0eda2edfb218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ccf9109a28945f0adb31e1f2c087df02b35007b0f2d0845125baf2e0855036baf2e086f823bbf2d0882292f800de01a47fc8ca00cb1f01cf16c9ed542092f80fde70db3cd81003f6eda2edfb02f404216e926c218e4c0221d73930709421c700b38e2d01d72820761e436c20d749c008f2e09320d74ac002f2e09320d71d06c712c2005230b0f2d089d74cd7393001a4e86c128407bbf2e093d74ac000f2e093ed55e2d20001c000915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74a111213009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed54007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de20010935bdb31e1d74cd0b4d6c35e" type ConfigV5R1 struct { NetworkGlobalID int32 @@ -26,14 +28,57 @@ type SpecV5R1 struct { config ConfigV5R1 } -const MainnetGlobalID = -239 -const TestnetGlobalID = -3 +// Source: https://github.com/tonkeeper/tonkeeper-ton/commit/d9aec6adfdb853eb37e0bba7453d83ae52e2a170#diff-c8ee60dec2f4e3ee55ad5e40f56fd9a104f21df78086a114d33d62e4fa0ffee6R139 +/* + * schema: + * wallet_id -- int32 + * wallet_id = global_id ^ context_id + * context_id_client$1 = wc:int8 wallet_version:uint8 counter:uint15 + * context_id_backoffice$0 = counter:uint31 + * + * + * calculated default values serialisation: + * + * global_id = -239, workchain = 0, wallet_version = 0', subwallet_number = 0 (client context) + * gives wallet_id = 2147483409 + * + * global_id = -239, workchain = -1, wallet_version = 0', subwallet_number = 0 (client context) + * gives wallet_id = 8388369 + * + * global_id = -3, workchain = 0, wallet_version = 0', subwallet_number = 0 (client context) + * gives wallet_id = 2147483645 + * + * global_id = -3, workchain = -1, wallet_version = 0', subwallet_number = 0 (client context) + * gives wallet_id = 8388605 + */ +// Function to generate the context ID based on the given workchain +func genContextID(workchain int8) uint32 { + var context uint32 + + // Convert workchain to uint32 after ensuring it's correctly handled as an 8-bit value + context |= 1 << 31 // Write 1 bit as 1 at the leftmost bit + context |= (uint32(workchain) & 0xFF) << 23 // Write 8 bits of workchain, shifted to position + context |= uint32(0) << 15 // Write 8 bits of 0 (wallet version) + context |= uint32(0) // Write 15 bits of 0 (subwallet number) + + return context +} -func (s *SpecV5R1) BuildMessage(ctx context.Context, _ bool, _ *ton.BlockIDExt, messages []*Message) (_ *cell.Cell, err error) { - // TODO: remove block, now it is here for backwards compatibility +type WalletId struct { + NetworkGlobalID int32 + WorkChain int8 + SubwalletNumber uint16 + WalletVersion uint8 +} + +func (w WalletId) Serialized() uint32 { + context := genContextID(w.WorkChain) + return uint32(int32(context) ^ w.NetworkGlobalID) +} +func (s *SpecV5R1) BuildMessage(ctx context.Context, _ bool, _ *ton.BlockIDExt, messages []*Message) (_ *cell.Cell, err error) { if len(messages) > 255 { - return nil, errors.New("for this type of wallet max 4 messages can be sent in the same time") + return nil, errors.New("for this type of wallet max 255 messages can be sent at the same time") } seq, err := s.seqnoFetcher(ctx, s.wallet.subwallet) @@ -41,20 +86,24 @@ func (s *SpecV5R1) BuildMessage(ctx context.Context, _ bool, _ *ton.BlockIDExt, return nil, fmt.Errorf("failed to fetch seqno: %w", err) } - actions, err := packV5Actions(messages) + actions, err := packV5R1Actions(messages) if err != nil { return nil, fmt.Errorf("failed to build actions: %w", err) } + walletId := WalletId{ + NetworkGlobalID: s.config.NetworkGlobalID, + WorkChain: s.config.Workchain, + SubwalletNumber: uint16(s.wallet.subwallet), + WalletVersion: 0, + } + payload := cell.BeginCell(). - MustStoreUInt(0x7369676e, 32). // external sign op code - MustStoreInt(int64(s.config.NetworkGlobalID), 32). - MustStoreInt(int64(s.config.Workchain), 8). - MustStoreUInt(0, 8). // version of v5 - MustStoreUInt(uint64(s.wallet.subwallet), 32). - MustStoreUInt(uint64(timeNow().Add(time.Duration(s.messagesTTL)*time.Second).UTC().Unix()), 32). - MustStoreUInt(uint64(seq), 32). - MustStoreBuilder(actions) + MustStoreUInt(0x7369676e, 32). // external sign op code + MustStoreUInt(uint64(walletId.Serialized()), 32). // serialized WalletId + MustStoreUInt(uint64(time.Now().Add(time.Duration(s.messagesTTL)*time.Second).UTC().Unix()), 32). // validUntil + MustStoreUInt(uint64(seq), 32). // seq (block) + MustStoreBuilder(actions) // Action list sign := payload.EndCell().Sign(s.wallet.key) msg := cell.BeginCell().MustStoreBuilder(payload).MustStoreSlice(sign, 512).EndCell() @@ -62,9 +111,23 @@ func (s *SpecV5R1) BuildMessage(ctx context.Context, _ bool, _ *ton.BlockIDExt, return msg, nil } -func packV5Actions(messages []*Message) (*cell.Builder, error) { +// Validate messages +func validateMessageFields(messages []*Message) error { if len(messages) > 255 { - return nil, fmt.Errorf("max 255 messages allowed for v5") + return fmt.Errorf("max 255 messages allowed for v5") + } + for _, message := range messages { + if message.InternalMessage == nil { + return fmt.Errorf("internal message cannot be nil") + } + } + return nil +} + +// Pack Actions +func packV5R1Actions(messages []*Message) (*cell.Builder, error) { + if err := validateMessageFields(messages); err != nil { + return nil, err } var list = cell.BeginCell().EndCell() @@ -74,19 +137,13 @@ func packV5Actions(messages []*Message) (*cell.Builder, error) { 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) + msg := cell.BeginCell().MustStoreUInt(0x0ec3c86d, 32). // action_send_msg prefix + MustStoreUInt(uint64(message.Mode), 8). // mode + MustStoreRef(outMsg) // message reference list = cell.BeginCell().MustStoreRef(list).MustStoreBuilder(msg).EndCell() } - return cell.BeginCell().MustStoreUInt(0, 1).MustStoreRef(list), nil + // Ensure the action list ends with 0, 1 as per the new specification + return cell.BeginCell().MustStoreUInt(1, 1).MustStoreRef(list).MustStoreUInt(0, 1), nil } diff --git a/ton/wallet/wallet.go b/ton/wallet/wallet.go index e571935..6e4e2cd 100644 --- a/ton/wallet/wallet.go +++ b/ton/wallet/wallet.go @@ -13,9 +13,10 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/xssnick/tonutils-go/adnl" "time" + "github.com/xssnick/tonutils-go/adnl" + "github.com/xssnick/tonutils-go/ton" "github.com/xssnick/tonutils-go/address" @@ -25,6 +26,10 @@ import ( type Version int +// Network IDs +const MainnetGlobalID = -239 +const TestnetGlobalID = -3 + const ( V1R1 Version = 11 V1R2 Version = 12 @@ -36,7 +41,8 @@ const ( V3 = V3R2 V4R1 Version = 41 V4R2 Version = 42 - V5R1 Version = 51 + V5Beta Version = 51 // W5 Beta + V5R1 Version = 52 // W5 Final HighloadV2R2 Version = 122 HighloadV2Verified Version = 123 HighloadV3 Version = 300 @@ -79,6 +85,7 @@ var ( V2R1: _V2R1CodeHex, V2R2: _V2R2CodeHex, V3R1: _V3R1CodeHex, V3R2: _V3R2CodeHex, V4R1: _V4R1CodeHex, V4R2: _V4R2CodeHex, + V5Beta: _V5BetaCodeHex, V5R1: _V5R1CodeHex, HighloadV2R2: _HighloadV2R2CodeHex, HighloadV2Verified: _HighloadV2VerifiedCodeHex, HighloadV3: _HighloadV3CodeHex, @@ -155,6 +162,7 @@ func FromPrivateKey(api TonAPI, key ed25519.PrivateKey, version VersionConfig) ( // default subwallet depends on wallet type switch version.(type) { + case ConfigV5Beta: case ConfigV5R1: subwallet = 0 } @@ -182,7 +190,7 @@ func FromPrivateKey(api TonAPI, key ed25519.PrivateKey, version VersionConfig) ( func getSpec(w *Wallet) (any, error) { switch v := w.ver.(type) { - case Version, ConfigV5R1: + case Version, ConfigV5Beta, ConfigV5R1: regular := SpecRegular{ wallet: w, messagesTTL: 60 * 3, // default ttl 3 min @@ -210,9 +218,14 @@ func getSpec(w *Wallet) (any, error) { } switch x := w.ver.(type) { + case ConfigV5Beta: + if x.NetworkGlobalID == 0 { + return nil, fmt.Errorf("NetworkGlobalID should be set in V5 config") + } + return &SpecV5Beta{SpecRegular: regular, SpecSeqno: SpecSeqno{seqnoFetcher: seqnoFetcher}, config: x}, nil case ConfigV5R1: if x.NetworkGlobalID == 0 { - return nil, fmt.Errorf("NetworkGlobalID should be set in v5 config") + return nil, fmt.Errorf("NetworkGlobalID should be set in V5 config") } return &SpecV5R1{SpecRegular: regular, SpecSeqno: SpecSeqno{seqnoFetcher: seqnoFetcher}, config: x}, nil } @@ -226,8 +239,10 @@ func getSpec(w *Wallet) (any, error) { return &SpecHighloadV2R2{regular, SpecQuery{}}, nil case HighloadV3: return nil, fmt.Errorf("use ConfigHighloadV3 for highload v3 spec") + case V5Beta: + return nil, fmt.Errorf("use ConfigV5Beta for V5 spec") case V5R1: - return nil, fmt.Errorf("use ConfigV5R1 for v5 spec") + return nil, fmt.Errorf("use ConfigV5R1 for V5 spec") } case ConfigHighloadV3: return &SpecHighloadV3{wallet: w, config: v}, nil @@ -328,13 +343,16 @@ func (w *Wallet) PrepareExternalMessageForMany(ctx context.Context, withStateIni var msg *cell.Cell switch v := w.ver.(type) { - case Version, ConfigV5R1: + case Version, ConfigV5Beta, ConfigV5R1: + if _, ok := v.(ConfigV5Beta); ok { + v = V5Beta + } if _, ok := v.(ConfigV5R1); ok { v = V5R1 } switch v { - case V3R2, V3R1, V4R2, V4R1, V5R1: + case V3R2, V3R1, V4R2, V4R1, V5Beta, V5R1: msg, err = w.spec.(RegularBuilder).BuildMessage(ctx, !withStateInit, nil, messages) if err != nil { return nil, fmt.Errorf("build message err: %w", err)