diff --git a/abi/generated_test.go b/abi/generated_test.go
index cac1e973..9895183a 100644
--- a/abi/generated_test.go
+++ b/abi/generated_test.go
@@ -1876,6 +1876,27 @@ func TestMessageDecoder(t *testing.T) {
},
interfaces: []ContractInterface{WalletV5R1},
},
+ {
+ name: "highload V3 internal transfer",
+ boc: "te6ccgEBBQEAWQABGK5C5aQAAAAAADVe6gECCg7DyG0DAgQCCg7DyG0DAwQAAABoQgA2ZpktQsYby0n9cV5VWOFINBjScIU2HdondFsK3lDpECAvrwgAAAAAAAAAAAAAAAAAAA==",
+ wantOpName: HighloadWalletInternalTransferMsgOp,
+ wantValue: HighloadWalletInternalTransferMsgBody{
+ QueryId: 3497706,
+ Actions: W5Actions{
+ W5SendMessageAction{
+ Magic: 0xec3c86d,
+ Mode: 3,
+ Msg: mustBocToMessageRelaxed("b5ee9c7201010101003600006842003666992d42c61bcb49fd715e5558e1483418d27085361dda27745b0ade50e910202faf080000000000000000000000000000"),
+ },
+ W5SendMessageAction{
+ Magic: 0xec3c86d,
+ Mode: 3,
+ Msg: mustBocToMessageRelaxed("b5ee9c7201010101003600006842003666992d42c61bcb49fd715e5558e1483418d27085361dda27745b0ade50e910202faf080000000000000000000000000000"),
+ },
+ },
+ },
+ interfaces: []ContractInterface{WalletHighloadV3R1},
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
diff --git a/abi/interfaces.go b/abi/interfaces.go
index 88a991fd..092c6a40 100644
--- a/abi/interfaces.go
+++ b/abi/interfaces.go
@@ -1299,6 +1299,10 @@ func (c ContractInterface) IntMsgs() []msgDecoderFunc {
decodeFuncStormVaultInitMsgBody,
decodeFuncStormVaultRequestWithdrawPositionMsgBody,
}
+ case WalletHighloadV3R1:
+ return []msgDecoderFunc{
+ decodeFuncHighloadWalletInternalTransferMsgBody,
+ }
case WalletV5R1:
return []msgDecoderFunc{
decodeFuncWalletSignedInternalV5R1MsgBody,
diff --git a/abi/messages.md b/abi/messages.md
index c47a371c..a5191c17 100644
--- a/abi/messages.md
+++ b/abi/messages.md
@@ -50,6 +50,7 @@ The list below contains the supported message operations, their names and opcode
| GetStaticData| 0x2fcb26a2 |
| GramSubmitProofOfWork| 0x4d696e65 |
| HighloadWalletSignedV2| 0x00000000 |
+| HighloadWalletInternalTransfer| 0xae42e5a4 |
| HighloadWalletSignedV3| 0x00000000 |
| InitPaymentChannel| 0x0e0620c2 |
| JettonBurn| 0x595f07bc |
diff --git a/abi/messages_generated.go b/abi/messages_generated.go
index 5e5a276c..22313d53 100644
--- a/abi/messages_generated.go
+++ b/abi/messages_generated.go
@@ -251,6 +251,8 @@ var (
decodeFuncStorageRewardWithdrawalMsgBody = decodeMsg(tlb.Tag{Val: 0xa91baf56, Len: 32}, StorageRewardWithdrawalMsgOp, StorageRewardWithdrawalMsgBody{})
// 0xad4eb6f5
decodeFuncDedustPayoutFromPoolMsgBody = decodeMsg(tlb.Tag{Val: 0xad4eb6f5, Len: 32}, DedustPayoutFromPoolMsgOp, DedustPayoutFromPoolMsgBody{})
+ // 0xae42e5a4
+ decodeFuncHighloadWalletInternalTransferMsgBody = decodeMsg(tlb.Tag{Val: 0xae42e5a4, Len: 32}, HighloadWalletInternalTransferMsgOp, HighloadWalletInternalTransferMsgBody{})
// 0xafaf283e
decodeFuncMultisigApproveRejectedMsgBody = decodeMsg(tlb.Tag{Val: 0xafaf283e, Len: 32}, MultisigApproveRejectedMsgOp, MultisigApproveRejectedMsgBody{})
// 0xb1ebae06
@@ -709,6 +711,9 @@ var opcodedMsgInDecodeFunctions = map[uint32]msgDecoderFunc{
// 0xad4eb6f5
DedustPayoutFromPoolMsgOpCode: decodeFuncDedustPayoutFromPoolMsgBody,
+ // 0xae42e5a4
+ HighloadWalletInternalTransferMsgOpCode: decodeFuncHighloadWalletInternalTransferMsgBody,
+
// 0xafaf283e
MultisigApproveRejectedMsgOpCode: decodeFuncMultisigApproveRejectedMsgBody,
@@ -962,6 +967,7 @@ const (
ReportRoyaltyParamsMsgOp MsgOpName = "ReportRoyaltyParams"
StorageRewardWithdrawalMsgOp MsgOpName = "StorageRewardWithdrawal"
DedustPayoutFromPoolMsgOp MsgOpName = "DedustPayoutFromPool"
+ HighloadWalletInternalTransferMsgOp MsgOpName = "HighloadWalletInternalTransfer"
MultisigApproveRejectedMsgOp MsgOpName = "MultisigApproveRejected"
TonstakeImanagerRequestNotificationMsgOp MsgOpName = "TonstakeImanagerRequestNotification"
TonstakePoolDeployControllerMsgOp MsgOpName = "TonstakePoolDeployController"
@@ -1130,6 +1136,7 @@ const (
ReportRoyaltyParamsMsgOpCode MsgOpCode = 0xa8cb00ad
StorageRewardWithdrawalMsgOpCode MsgOpCode = 0xa91baf56
DedustPayoutFromPoolMsgOpCode MsgOpCode = 0xad4eb6f5
+ HighloadWalletInternalTransferMsgOpCode MsgOpCode = 0xae42e5a4
MultisigApproveRejectedMsgOpCode MsgOpCode = 0xafaf283e
TonstakeImanagerRequestNotificationMsgOpCode MsgOpCode = 0xb1ebae06
TonstakePoolDeployControllerMsgOpCode MsgOpCode = 0xb27edcad
@@ -1902,6 +1909,11 @@ type DedustPayoutFromPoolMsgBody struct {
Payload *tlb.Any `tlb:"maybe^"`
}
+type HighloadWalletInternalTransferMsgBody struct {
+ QueryId uint64
+ Actions W5Actions `tlb:"^"`
+}
+
type MultisigApproveRejectedMsgBody struct {
QueryId uint64
ExitCode uint32
@@ -2277,6 +2289,7 @@ var KnownMsgInTypes = map[string]any{
ReportRoyaltyParamsMsgOp: ReportRoyaltyParamsMsgBody{},
StorageRewardWithdrawalMsgOp: StorageRewardWithdrawalMsgBody{},
DedustPayoutFromPoolMsgOp: DedustPayoutFromPoolMsgBody{},
+ HighloadWalletInternalTransferMsgOp: HighloadWalletInternalTransferMsgBody{},
MultisigApproveRejectedMsgOp: MultisigApproveRejectedMsgBody{},
TonstakeImanagerRequestNotificationMsgOp: TonstakeImanagerRequestNotificationMsgBody{},
TonstakePoolDeployControllerMsgOp: TonstakePoolDeployControllerMsgBody{},
diff --git a/abi/schemas/wallets.xml b/abi/schemas/wallets.xml
index b1681d18..32ee142b 100644
--- a/abi/schemas/wallets.xml
+++ b/abi/schemas/wallets.xml
@@ -136,6 +136,7 @@
+
@@ -216,6 +217,9 @@
extension_action#6578746e query_id:uint64 actions:(Maybe ^W5Actions) extended:(Maybe W5ExtendedActions) = InternalMsgBody;
+
+ internal_transfer#ae42e5a4 query_id:uint64 actions:^W5Actions = InternalMsgBody;
+
signed#_ signature:bits512 subwallet_id:uint32 query_id:uint64 payload:(HashmapE 16 SendMessageAction) = ExternalMsgBody;
diff --git a/wallet/messages.go b/wallet/messages.go
index 0ad042ee..3e80c60f 100644
--- a/wallet/messages.go
+++ b/wallet/messages.go
@@ -4,6 +4,7 @@ import (
"crypto/ed25519"
"errors"
"fmt"
+ "github.com/tonkeeper/tongo/ton"
"github.com/tonkeeper/tongo/boc"
"github.com/tonkeeper/tongo/tlb"
@@ -245,6 +246,12 @@ func ExtractRawMessages(ver Version, msg *boc.Cell) ([]RawMessage, error) {
return nil, err
}
return hl.RawMessages, nil
+ case HighLoadV3R1:
+ hl, err := DecodeHighloadV3Message(msg)
+ if err != nil {
+ return nil, err
+ }
+ return hl.Messages, nil
default:
return nil, fmt.Errorf("wallet version is not supported: %v", ver)
}
@@ -552,3 +559,208 @@ func (extendedActions W5ExtendedActions) MarshalTLB(c *boc.Cell, encoder *tlb.En
}
return nil
}
+
+// HighloadV3InternalTransfer TLB: internal_transfer#ae42e5a4 {n:#} query_id:uint64 actions:^(OutList n) = InternalMsgBody n;
+type HighloadV3InternalTransfer struct {
+ Magic tlb.Magic `tlb:"#ae42e5a4"`
+ QueryId uint64
+ Actions W5Actions `tlb:"^"`
+}
+
+type HighloadV3MsgInner struct {
+ SubwalletID uint32
+ MessageToSend *boc.Cell `tlb:"^"`
+ SendMode uint8
+ QueryID tlb.Uint23 // _ shift:uint13 bit_number:(## 10) { bit_number >= 0 } { bit_number < 1023 } = QueryId;
+ CreatedAt uint64
+ Timeout tlb.Uint22
+}
+
+type HighloadV3Message struct {
+ SubwalletID uint32
+ Messages []RawMessage
+ SendMode uint8
+ QueryID tlb.Uint23
+ CreatedAt uint64
+ Timeout tlb.Uint22
+ wallet ton.AccountID // technical field for storing the wallet address for forming attached messages
+}
+
+func (p HighloadV3Message) MarshalTLB(c *boc.Cell, encoder *tlb.Encoder) error {
+ var (
+ msg RawMessage
+ err error
+ )
+ ln := len(p.Messages)
+ switch {
+ case ln > 254*254:
+ return fmt.Errorf("PayloadHighloadV3 supports only up to 254*254 messages")
+ case ln < 1:
+ return fmt.Errorf("must be at least one message")
+ case ln == 1:
+ var m tlb.Message
+ err := tlb.Unmarshal(p.Messages[0].Message, &m)
+ if err != nil {
+ return err
+ }
+ // IntMsg with state init and extOutMsg must be packed because of message validation
+ // throw_if(error::invalid_message_to_send, maybe_state_init); ;; throw if state-init included (state-init not supported)
+ // throw_if(error::invalid_message_to_send, message_slice~load_uint(1)); ;; int_msg_info$0
+ if !m.Init.Exists && m.Info.SumType == "IntMsgInfo" { // no need to pack
+ msg = p.Messages[0]
+ } else {
+ msg, err = packHighloadV3Messages(uint64(p.QueryID), p.wallet, p.Messages, p.SendMode)
+ if err != nil {
+ return err
+ }
+ }
+ default:
+ msg, err = packHighloadV3Messages(uint64(p.QueryID), p.wallet, p.Messages, p.SendMode)
+ if err != nil {
+ return err
+ }
+ }
+ return tlb.Marshal(c, HighloadV3MsgInner{
+ SubwalletID: p.SubwalletID,
+ MessageToSend: msg.Message,
+ SendMode: msg.Mode,
+ QueryID: p.QueryID,
+ CreatedAt: p.CreatedAt,
+ Timeout: p.Timeout,
+ })
+}
+
+func packHighloadV3Messages(queryID uint64, wallet ton.AccountID, msgs []RawMessage, mode uint8) (RawMessage, error) {
+ const messagesPerPack = 253
+ var (
+ totalAmount uint64 = 0
+ actions W5Actions
+ )
+ rawMsgs := make([]RawMessage, len(msgs))
+ copy(rawMsgs, msgs) // to prevent corruption of msgs
+ if len(rawMsgs) > messagesPerPack {
+ rest, err := packHighloadV3Messages(queryID, wallet, rawMsgs[messagesPerPack:], mode)
+ if err != nil {
+ return RawMessage{}, err
+ }
+ rawMsgs = append(rawMsgs[:messagesPerPack], rest)
+ }
+ for _, rawMsg := range rawMsgs {
+ var m tlb.Message
+ err := tlb.Unmarshal(rawMsg.Message, &m)
+ if err != nil {
+ return RawMessage{}, err
+ }
+ if m.Info.SumType == "IntMsgInfo" {
+ totalAmount += uint64(m.Info.IntMsgInfo.Value.Grams)
+ } else {
+ totalAmount += uint64(1_000_000) // add some amount for execution
+ }
+ actions = append(actions, W5SendMessageAction{
+ Mode: rawMsg.Mode,
+ Msg: rawMsg.Message,
+ })
+ }
+ body := boc.NewCell()
+ err := tlb.Marshal(body, HighloadV3InternalTransfer{
+ QueryId: queryID,
+ Actions: actions,
+ })
+ if err != nil {
+ return RawMessage{}, err
+ }
+ msgInt, _, err := Message{
+ Amount: tlb.Grams(totalAmount),
+ Bounce: false,
+ Address: wallet,
+ Body: body,
+ }.ToInternal()
+ if err != nil {
+ return RawMessage{}, err
+ }
+ c := boc.NewCell()
+ err = tlb.Marshal(c, msgInt)
+ if err != nil {
+ return RawMessage{}, err
+ }
+ return RawMessage{
+ Mode: mode,
+ Message: c,
+ }, nil
+}
+
+const highloadV3InternalTransferOp = 0xae42e5a4
+
+func (p *HighloadV3Message) UnmarshalTLB(c *boc.Cell, decoder *tlb.Decoder) error {
+ var msgInner HighloadV3MsgInner
+ err := tlb.Unmarshal(c, &msgInner)
+ if err != nil {
+ return err
+ }
+ res := HighloadV3Message{
+ SubwalletID: msgInner.SubwalletID,
+ SendMode: msgInner.SendMode,
+ QueryID: msgInner.QueryID,
+ CreatedAt: msgInner.CreatedAt,
+ Timeout: msgInner.Timeout,
+ }
+ var msgs []RawMessage
+ msgs, err = unpackHighloadV3Messages(msgInner.MessageToSend, msgInner.QueryID, msgInner.SendMode, msgs)
+ if err != nil {
+ return err
+ }
+ res.Messages = msgs
+ *p = res
+ return nil
+}
+
+func unpackHighloadV3Messages(msg *boc.Cell, queryID tlb.Uint23, mode uint8, messages []RawMessage) ([]RawMessage, error) {
+ var m tlb.Message
+ err := tlb.Unmarshal(msg, &m)
+ if err != nil {
+ return nil, err
+ }
+ if m.Info.SumType != "IntMsgInfo" {
+ // TODO: reset counters for msgInner.MessageToSend ?
+ messages = append(messages, RawMessage{msg, mode})
+ return messages, nil
+ }
+ body := boc.Cell(m.Body.Value)
+ op, err := body.PickUint(32)
+ if err != nil || op != highloadV3InternalTransferOp {
+ messages = append(messages, RawMessage{msg, mode})
+ return messages, nil
+ }
+ var intTransfer HighloadV3InternalTransfer
+ err = tlb.Unmarshal(&body, &intTransfer)
+ if err != nil {
+ return nil, err
+ }
+ if intTransfer.QueryId != uint64(queryID) {
+ return nil, errors.New("mismatch queryID for internal transfer") // TODO: need to check?
+ }
+ for _, a := range intTransfer.Actions {
+ messages, err = unpackHighloadV3Messages(a.Msg, queryID, a.Mode, messages)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return messages, nil
+}
+
+func DecodeHighloadV3Message(msg *boc.Cell) (*HighloadV3Message, error) {
+ var m tlb.Message
+ if err := tlb.Unmarshal(msg, &m); err != nil {
+ return nil, err
+ }
+ c := boc.Cell(m.Body.Value)
+ payloadCell, err := c.NextRef()
+ if err != nil {
+ return nil, err
+ }
+ var res HighloadV3Message
+ if err := tlb.Unmarshal(payloadCell, &res); err != nil {
+ return nil, err
+ }
+ return &res, nil
+}
diff --git a/wallet/models.go b/wallet/models.go
index db34be3f..838baddf 100644
--- a/wallet/models.go
+++ b/wallet/models.go
@@ -30,6 +30,7 @@ const (
HighLoadV2
HighLoadV2R1
HighLoadV2R2
+ HighLoadV3R1
// TODO: maybe add lockup wallet
)
@@ -64,6 +65,8 @@ var codes = map[Version]string{
HighLoadV2: "te6ccgEBCQEA5QABFP8A9KQT9LzyyAsBAgEgAgcCAUgDBAAE0DACASAFBgAXvZznaiaGmvmOuF/8AEG+X5dqJoaY+Y6Z/p/5j6AmipEEAgegc30JjJLb/JXdHxQB6vKDCNcYINMf0z/4I6ofUyC58mPtRNDTH9M/0//0BNFTYIBA9A5voTHyYFFzuvKiB/kBVBCH+RDyowL0BNH4AH+OFiGAEPR4b6UgmALTB9QwAfsAkTLiAbPmW4MlochANIBA9EOK5jEByMsfE8s/y//0AMntVAgANCCAQPSWb6VsEiCUMFMDud4gkzM2AZJsIeKz",
HighLoadV2R1: "te6ccgEBBwEA1gABFP8A9KQT9KDyyAsBAgEgAgMCAUgEBQHu8oMI1xgg0x/TP/gjqh9TILnyY+1E0NMf0z/T//QE0VNggED0Dm+hMfJgUXO68qIH+QFUEIf5EPKjAvQE0fgAf44YIYAQ9HhvoW+hIJgC0wfUMAH7AJEy4gGz5luDJaHIQDSAQPRDiuYxyBLLHxPLP8v/9ADJ7VQGAATQMABBoZfl2omhpj5jpn+n/mPoCaKkQQCB6BzfQmMktv8ld0fFADgggED0lm+hb6EyURCUMFMDud4gkzM2AZIyMOKz",
HighLoadV2R2: "te6ccgEBCQEA6QABFP8A9KQT9LzyyAsBAgEgAgMCAUgEBQHu8oMI1xgg0x/TP/gjqh9TILnyY+1E0NMf0z/T//QE0VNggED0Dm+hMfJgUXO68qIH+QFUEIf5EPKjAvQE0fgAf44YIYAQ9HhvoW+hIJgC0wfUMAH7AJEy4gGz5luDJaHIQDSAQPRDiuYxyBLLHxPLP8v/9ADJ7VQIAATQMAIBIAYHABe9nOdqJoaa+Y64X/wAQb5fl2omhpj5jpn+n/mPoCaKkQQCB6BzfQmMktv8ld0fFAA4IIBA9JZvoW+hMlEQlDBTA7neIJMzNgGSMjDisw==",
+ // HighLoadV3 code hex from https://github.com/ton-blockchain/highload-wallet-contract-v3/commit/3d2843747b14bc2a8915606df736d47490cd3d49
+ HighLoadV3R1: "te6cckECEAEAAigAART/APSkE/S88sgLAQIBIAINAgFIAwQAeNAg10vAAQHAYLCRW+EB0NMDAXGwkVvg+kAw+CjHBbORMODTHwGCEK5C5aS6nYBA1yHXTPgqAe1V+wTgMAIBIAUKAgJzBgcAEa3OdqJoa4X/wAIBIAgJABqrtu1E0IEBItch1ws/ABiqO+1E0IMH1yHXCx8CASALDAAbuabu1E0IEBYtch1wsVgA5bi/Ltou37IasJAoQJsO1E0IEBINch9AT0BNM/0xXRBY4b+CMloVIQuZ8ybfgjBaoAFaESuZIwbd6SMDPikjAz4lIwgA30D2+hntAh1yHXCgCVXwN/2zHgkTDiWYAN9A9voZzQAdch1woAk3/bMeCRW+JwgB9vLUgwjXGNEh+QDtRNDT/9Mf9AT0BNM/0xXR+CMhoVIguY4SM234IySqAKESuZJtMt5Y+CMB3lQWdfkQ8qEG0NMf1NMH0wzTCdM/0xXRUWi68qJRWrrypvgjKqFSULzyowT4I7vyo1MEgA30D2+hmdAk1yHXCgDyZJEw4g4B/lMJgA30D2+hjhPQUATXGNIAAfJkyFjPFs+DAc8WjhAwyCTPQM+DhAlQBaGlFM9A4vgAyUA5gA30FwTIy/8Tyx/0ABL0ABLLPxLLFcntVPgPIdDTAAHyZdMCAXGwkl8D4PpAAdcLAcAA8qX6QDH6ADH0AfoAMfoAMYBg1yHTAAEPACDyZdIAAZPUMdGRMOJysfsAtYW/Aw==",
}
// codeHashToVersion maps code's hash to a wallet version.
@@ -180,6 +183,8 @@ func (v Version) ToString() string {
return "highload_v2R1"
case HighLoadV2R2:
return "highload_v2R2"
+ case HighLoadV3R1:
+ return "highload_v3R1"
default:
panic("to string conversion for this ver not supported")
}
@@ -316,3 +321,36 @@ func (t *TextComment) UnmarshalTLB(c *boc.Cell, decoder *tlb.Decoder) error { //
*t = TextComment(text)
return nil
}
+
+type LogMessage struct {
+ Comment string
+ // TODO: add support of external address
+}
+
+func (m LogMessage) ToInternal() (message tlb.Message, mode uint8, err error) {
+ info := tlb.CommonMsgInfo{
+ SumType: "ExtOutMsgInfo",
+ }
+ info.ExtOutMsgInfo = &struct {
+ Src tlb.MsgAddress
+ Dest tlb.MsgAddress
+ CreatedLt uint64
+ CreatedAt uint32
+ }{
+ Src: (*ton.AccountID)(nil).ToMsgAddress(),
+ Dest: (*ton.AccountID)(nil).ToMsgAddress(),
+ }
+ extMsg := tlb.Message{
+ Info: info,
+ }
+ if m.Comment != "" {
+ body := boc.NewCell()
+ err := tlb.Marshal(body, TextComment(m.Comment))
+ if err != nil {
+ return tlb.Message{}, 0, err
+ }
+ extMsg.Body.IsRight = true //todo: check length and
+ extMsg.Body.Value = tlb.Any(*body)
+ }
+ return extMsg, DefaultMessageMode, nil
+}
diff --git a/wallet/wallet.go b/wallet/wallet.go
index 60b9585a..0301e897 100644
--- a/wallet/wallet.go
+++ b/wallet/wallet.go
@@ -67,7 +67,7 @@ func WithMessageLifetime(lifetime time.Duration) Option {
func applyOptions(opts ...Option) Options {
options := Options{
- MsgLifetime: DefaultMessageLifetime,
+ MsgLifetime: DefaultMessageLifetime, // TODO: default value maybe too small for HighloadV3
}
for _, o := range opts {
o(&options)
@@ -114,6 +114,8 @@ func newWallet(key ed25519.PublicKey, version Version, options Options) (wallet,
return NewWalletV5R1(key, options), nil
case HighLoadV2R2:
return newWalletHighloadV2(version, key, options), nil
+ case HighLoadV3R1:
+ return newWalletHighloadV3(version, key, options), nil
default:
return nil, fmt.Errorf("unsupported wallet version: %v", version)
}
diff --git a/wallet/wallet_highload_v3.go b/wallet/wallet_highload_v3.go
new file mode 100644
index 00000000..bce8cf16
--- /dev/null
+++ b/wallet/wallet_highload_v3.go
@@ -0,0 +1,139 @@
+package wallet
+
+/*
+
+HighLoad wallet V3
+Contract repo: https://github.com/ton-blockchain/highload-wallet-contract-v3
+
+TLB scheme:
+
+storage$_ public_key:bits256 subwallet_id:uint32 old_queries:(HashmapE 14 ^Cell)
+ queries:(HashmapE 14 ^Cell) last_clean_time:uint64 timeout:uint22
+ = Storage;
+
+_ shift:uint13 bit_number:(## 10) { bit_number >= 0 } { bit_number < 1023 } = QueryId;
+
+// crc32('internal_transfer n:# query_id:uint64 actions:^OutList n = InternalMsgBody n') = ae42e5a4
+
+internal_transfer#ae42e5a4 {n:#} query_id:uint64 actions:^(OutList n) = InternalMsgBody n;
+
+_ {n:#} subwallet_id:uint32 message_to_send:^Cell send_mode:uint8 query_id:QueryId created_at:uint64 timeout:uint22 = MsgInner;
+
+msg_body$_ {n:#} signature:bits512 ^(MsgInner) = ExternalInMsgBody;
+
+*/
+
+import (
+ "crypto/ed25519"
+ "fmt"
+ "github.com/tonkeeper/tongo/boc"
+ "github.com/tonkeeper/tongo/tlb"
+ "github.com/tonkeeper/tongo/ton"
+ "time"
+)
+
+// DefaultSubWalletHighloadV3 https://github.com/ton-blockchain/highload-wallet-contract-v3?tab=readme-ov-file#highload-wallet-contract-v3
+const DefaultSubWalletHighloadV3 = 0x10ad
+
+const DefaultTimeoutHighloadV3 = 60 * 60 // TODO: recommended 1 hour to 24 hours
+
+var _ wallet = &walletHighloadV3{}
+
+type walletHighloadV3 struct {
+ version Version
+ publicKey ed25519.PublicKey
+ workchain int
+ subWalletID uint32
+ timeout tlb.Uint22
+}
+
+// DataHighloadV3 represents storage data of a wallet contract.
+type DataHighloadV3 struct {
+ PublicKey tlb.Bits256
+ SubWalletId uint32
+ OldQueries tlb.HashmapE[tlb.Uint14, tlb.Any]
+ Queries tlb.HashmapE[tlb.Uint14, tlb.Any]
+ LastCleanTime uint64
+ Timeout tlb.Uint22
+}
+
+func newWalletHighloadV3(ver Version, key ed25519.PublicKey, options Options) *walletHighloadV3 {
+ workchain := defaultOr(options.Workchain, 0)
+ subWalletID := defaultOr(options.SubWalletID, uint32(DefaultSubWalletHighloadV3))
+ // TODO: add custom message lifetime with size check
+ return &walletHighloadV3{
+ version: ver,
+ publicKey: key,
+ workchain: workchain,
+ subWalletID: subWalletID,
+ timeout: DefaultTimeoutHighloadV3,
+ }
+}
+
+func (w *walletHighloadV3) generateAddress() (ton.AccountID, error) {
+ stateInit, err := w.generateStateInit()
+ if err != nil {
+ return ton.AccountID{}, err
+ }
+ return generateAddress(w.workchain, *stateInit)
+}
+
+func (w *walletHighloadV3) generateStateInit() (*tlb.StateInit, error) {
+ data := DataHighloadV3{
+ SubWalletId: w.subWalletID,
+ PublicKey: publicKeyToBits(w.publicKey),
+ Timeout: w.timeout,
+ }
+ return generateStateInit(w.version, data)
+}
+
+func (w *walletHighloadV3) maxMessageNumber() int {
+ return 254 * 254
+}
+
+func (w *walletHighloadV3) createSignedMsgBodyCell(privateKey ed25519.PrivateKey, internalMessages []RawMessage, msgConfig MessageConfig) (*boc.Cell, error) {
+ // TODO: or use special queryID generator function as option
+ now := time.Now().UnixMilli()
+ queryID := tlb.Uint23((now / 100) % (1 << 23)) // allow to send every 100ms. overflows after 233 hr
+ addr, err := w.generateAddress()
+ if err != nil {
+ return nil, err
+ }
+ msgInner := HighloadV3Message{
+ SubwalletID: w.subWalletID,
+ Messages: internalMessages,
+ SendMode: DefaultMessageMode,
+ QueryID: queryID,
+ CreatedAt: uint64(now/1000 - 30), // TODO: fix -30 after the liteservers are updated
+ Timeout: w.timeout,
+ wallet: addr,
+ }
+ innerCell := boc.NewCell()
+ if err := tlb.Marshal(innerCell, msgInner); err != nil {
+ return nil, err
+ }
+ signBytes, err := innerCell.Sign(privateKey)
+ if err != nil {
+ return nil, fmt.Errorf("can not sign wallet message body: %v", err)
+ }
+ // msg_body$_ {n:#} signature:bits512 ^(MsgInner) = ExternalInMsgBody;
+ signedBodyCell := boc.NewCell()
+ err = signedBodyCell.WriteBytes(signBytes)
+ if err != nil {
+ return nil, err
+ }
+ _ = signedBodyCell.AddRef(innerCell)
+ return signedBodyCell, nil
+}
+
+func (w *walletHighloadV3) NextMessageParams(state tlb.ShardAccount) (NextMsgParams, error) {
+ initRequired := state.Account.Status() == tlb.AccountUninit || state.Account.Status() == tlb.AccountNone
+ if !initRequired {
+ return NextMsgParams{}, nil
+ }
+ stateInit, err := w.generateStateInit()
+ if err != nil {
+ return NextMsgParams{}, err
+ }
+ return NextMsgParams{Init: stateInit}, nil
+}
diff --git a/wallet/wallet_highload_v3_test.go b/wallet/wallet_highload_v3_test.go
new file mode 100644
index 00000000..61724502
--- /dev/null
+++ b/wallet/wallet_highload_v3_test.go
@@ -0,0 +1,206 @@
+package wallet
+
+import (
+ "context"
+ "crypto/ed25519"
+ "encoding/hex"
+ "fmt"
+ "github.com/tonkeeper/tongo"
+ "github.com/tonkeeper/tongo/boc"
+ "github.com/tonkeeper/tongo/liteapi"
+ "github.com/tonkeeper/tongo/tlb"
+ "github.com/tonkeeper/tongo/ton"
+ "testing"
+ "time"
+)
+
+func Test_HighloadV3_Send(t *testing.T) {
+ t.Skip("Only for manual testing")
+ tests := []struct {
+ name string
+ msgQty int
+ specialMode string
+ }{
+ {
+ name: "single message",
+ msgQty: 1,
+ specialMode: "",
+ },
+ {
+ name: "single message with init",
+ msgQty: 1,
+ specialMode: "with_init",
+ },
+ {
+ name: "one batch",
+ msgQty: 100,
+ specialMode: "",
+ },
+ {
+ name: "few batches",
+ msgQty: 300,
+ specialMode: "",
+ },
+ {
+ name: "single ext out message",
+ msgQty: 1,
+ specialMode: "ext_out",
+ },
+ {
+ name: "ext out message few batches",
+ msgQty: 300,
+ specialMode: "ext_out",
+ },
+ }
+
+ client, err := liteapi.NewClientWithDefaultTestnet()
+ if err != nil {
+ t.Fatalf("Unable to create lite client: %v", err)
+ }
+ pk, _ := SeedToPrivateKey("birth pattern then forest walnut then phrase walnut fan pumpkin pattern then cluster blossom verify then forest velvet pond fiction pattern collect then then")
+ opts := []Option{WithSubWalletID(DefaultSubWallet)}
+ w, err := New(pk, HighLoadV3R1, client, opts...)
+ if err != nil {
+ t.Fatalf("Unable to create wallet: %v", err)
+ }
+ recipient := tongo.MustParseAccountID("kQBszTJahYw3lpP64ryqscKQaDGk4QpsO7RO6LYVvKHSIX8f")
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ transfers := make([]Sendable, tt.msgQty)
+ for i := 0; i < tt.msgQty; i++ {
+ switch tt.specialMode {
+ case "with_init":
+ transfers[i] = Message{
+ Amount: 1_000_000,
+ Address: recipient,
+ Code: boc.NewCell(),
+ Data: boc.NewCell(),
+ }
+ case "ext_out":
+ transfers[i] = LogMessage{
+ Comment: "123",
+ }
+ default:
+ transfers[i] = SimpleTransfer{
+ Amount: 100_000,
+ Address: recipient,
+ Comment: fmt.Sprintf("%d", i+1),
+ Bounceable: false,
+ }
+ }
+ }
+ err = w.Send(context.TODO(), transfers...)
+ if err != nil {
+ t.Fatalf("Sending err: %v", err)
+ }
+ time.Sleep(time.Second)
+ })
+ }
+}
+
+func Test_HighloadV3_generateAddress(t *testing.T) {
+ tests := []struct {
+ name string
+ privateKey string
+ opts []Option
+ want ton.AccountID
+ }{
+ {
+ name: "workchain 0",
+ privateKey: "7c94066ee822c97aa6992fa1c506bfd56d0d8fed2f1027070af7e0a683d46fb671ced1c4c69e53eb7ede24658375f56c142d22cdb21d0728138cb53b817e454e",
+ opts: []Option{
+ WithWorkchain(0),
+ WithSubWalletID(DefaultSubWallet),
+ },
+ want: ton.MustParseAccountID("0:41ad70d17c024e9b4e2e3a5948d3f5ff855e339c6d9d504c679abc4eb08c4b7c"), // TODO: depends of DefaultTimeoutHighloadV3
+ },
+ {
+ name: "workchain -1",
+ privateKey: "7c94066ee822c97aa6992fa1c506bfd56d0d8fed2f1027070af7e0a683d46fb671ced1c4c69e53eb7ede24658375f56c142d22cdb21d0728138cb53b817e454e",
+ opts: []Option{
+ WithWorkchain(-1),
+ WithSubWalletID(DefaultSubWallet),
+ },
+ want: ton.MustParseAccountID("-1:41ad70d17c024e9b4e2e3a5948d3f5ff855e339c6d9d504c679abc4eb08c4b7c"), // TODO: depends of DefaultTimeoutHighloadV3
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ privateKey, err := hex.DecodeString(tt.privateKey)
+ if err != nil {
+ t.Fatalf("hex.DecodeString() error = %v", err)
+ }
+ publicKey := ed25519.PrivateKey(privateKey).Public().(ed25519.PublicKey)
+ w := newWalletHighloadV3(HighLoadV3R1, publicKey, applyOptions(tt.opts...))
+ address, err := w.generateAddress()
+ if err != nil {
+ t.Fatalf("generateAddress() error = %v", err)
+ }
+ if address.ToRaw() != tt.want.ToRaw() {
+ t.Errorf("generateAddress() got = %v, want %v", address, tt.want)
+ }
+ })
+ }
+}
+
+func Test_DecodeHighloadV3Message(t *testing.T) {
+ tests := []struct {
+ name string
+ msgQty int
+ }{
+ {
+ name: "single message",
+ msgQty: 1,
+ },
+ {
+ name: "one batch",
+ msgQty: 200,
+ },
+ {
+ name: "few batches",
+ msgQty: 600,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ transfers := make([]SimpleTransfer, tt.msgQty)
+ raws := make([]RawMessage, tt.msgQty)
+ for i := 0; i < tt.msgQty; i++ {
+ transfers[i] = SimpleTransfer{
+ Amount: tlb.Grams(i * 100),
+ Address: tongo.MustParseAccountID("kQBszTJahYw3lpP64ryqscKQaDGk4QpsO7RO6LYVvKHSIX8f"),
+ Bounceable: false,
+ }
+ msg, _, _ := transfers[i].ToInternal()
+ c := boc.NewCell()
+ _ = tlb.Marshal(c, msg)
+ raws[i].Message = c
+ raws[i].Mode = DefaultMessageMode
+ }
+ pubKey, privKey, _ := ed25519.GenerateKey(nil)
+ w, _ := newWallet(pubKey, HighLoadV3R1, Options{})
+ signedBodyCell, err := w.createSignedMsgBodyCell(privKey, raws, MessageConfig{})
+ if err != nil {
+ t.Fatalf("Unable to createSignedMsgBodyCell: %v", err)
+ }
+ addr, _ := w.generateAddress()
+ extMsg, _ := ton.CreateExternalMessage(addr, signedBodyCell, nil, tlb.VarUInteger16{})
+ extMsgCell := boc.NewCell()
+ _ = tlb.Marshal(extMsgCell, extMsg)
+ decodedMsg, err := DecodeHighloadV3Message(extMsgCell)
+ if err != nil {
+ t.Fatalf("Unable to docode external message: %v", err)
+ }
+ for i, m := range raws {
+ if m.Mode != decodedMsg.Messages[i].Mode {
+ t.Fatalf("Invalid mode at step: %d", i)
+ }
+ h1, _ := m.Message.Hash256()
+ h2, _ := decodedMsg.Messages[i].Message.Hash256()
+ if h1 != h2 {
+ t.Fatalf("Invalid message hash at step: %d", i)
+ }
+ }
+ })
+ }
+}