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) + } + } + }) + } +}