Skip to content

Commit

Permalink
Add wallet v5 support
Browse files Browse the repository at this point in the history
  • Loading branch information
aleksej-paschenko committed May 1, 2024
1 parent 6ecb456 commit b01a2ca
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 3 deletions.
2 changes: 1 addition & 1 deletion tlb/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/tonkeeper/tongo/tlb/parser"
)

var bitsSizes = []int{96, 256, 264, 320, 352, 512}
var bitsSizes = []int{80, 96, 256, 264, 320, 352, 512}
var intSizes = []int{128, 256, 257}

func main() {
Expand Down
30 changes: 30 additions & 0 deletions tlb/integers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6100,6 +6100,36 @@ func (u *Int257) UnmarshalJSON(p []byte) error {
return nil
}

type Bits80 [10]byte

func (u Bits80) FixedSize() int {
return 80
}

func (u Bits80) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("\"%x\"", u[:])), nil
}

func (u *Bits80) UnmarshalJSON(b []byte) error {
bs, err := hex.DecodeString(strings.Trim(string(b), "\""))
if err != nil {
return err
}
if len(bs) != 10 {
return fmt.Errorf("can't parse Bits80 %v", string(b))
}
copy(u[:], bs)
return nil
}

func (u Bits80) Equal(other any) bool {
otherBits, ok := other.(Bits80)
if !ok {
return false
}
return u == otherBits
}

type Bits96 [12]byte

func (u Bits96) FixedSize() int {
Expand Down
142 changes: 141 additions & 1 deletion wallet/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package wallet

import (
"crypto/ed25519"
"errors"
"fmt"

"github.com/tonkeeper/tongo/boc"
"github.com/tonkeeper/tongo/tlb"
)

var ErrBadSignature = errors.New("failed to verify msg signature")

type MessageV3 struct {
SubWalletId uint32
ValidUntil uint32
Expand All @@ -24,6 +27,39 @@ type MessageV4 struct {
RawMessages PayloadV1toV4
}

type SendMessageAction struct {
Magic tlb.Magic `tlb:"#0ec3c86d"`
Mode uint8
Msg *boc.Cell `tlb:"^"`
}

type SendMessageList struct {
Actions []SendMessageAction
}

// MessageV5 is a message format used by wallet v5.
type MessageV5 struct {
tlb.SumType
// Sint is an internal message authenticated by a signature.
Sint struct {
SubWalletId tlb.Bits80
ValidUntil uint32
Seqno uint32
Op bool
Signature tlb.Bits512
Actions SendMessageList `tlb:"^"`
} `tlbSumType:"#73696e74"`
// Sign is an external message authenticated by a signature.
Sign struct {
SubWalletId tlb.Bits80
ValidUntil uint32
Seqno uint32
Op bool
Signature tlb.Bits512
Actions SendMessageList `tlb:"^"`
} `tlbSumType:"#7369676e"`
}

type HighloadV2Message struct {
SubWalletId uint32
BoundedQueryID uint64
Expand Down Expand Up @@ -56,7 +92,7 @@ func (body *SignedMsgBody) Verify(publicKey ed25519.PublicKey) error {
if ed25519.Verify(publicKey, hash, body.Sign[:]) {
return nil
}
return fmt.Errorf("failed to verify msg signature")
return ErrBadSignature
}

func extractSignedMsgBody(msg *boc.Cell) (*SignedMsgBody, error) {
Expand All @@ -72,6 +108,19 @@ func extractSignedMsgBody(msg *boc.Cell) (*SignedMsgBody, error) {
return &msgBody, nil
}

func DecodeMessageV5(msg *boc.Cell) (*MessageV5, error) {
var m tlb.Message
if err := tlb.Unmarshal(msg, &m); err != nil {
return nil, err
}
var msgv5 MessageV5
bodyCell := boc.Cell(m.Body.Value)
if err := tlb.Unmarshal(&bodyCell, &msgv5); err != nil {
return nil, err
}
return &msgv5, nil
}

func DecodeMessageV4(msg *boc.Cell) (*MessageV4, error) {
signedMsgBody, err := extractSignedMsgBody(msg)
if err != nil {
Expand Down Expand Up @@ -126,6 +175,12 @@ func decodeHighloadV2Message(body *SignedMsgBody) (*HighloadV2Message, error) {
// ExtractRawMessages extracts a list of RawMessages from an external message.
func ExtractRawMessages(ver Version, msg *boc.Cell) ([]RawMessage, error) {
switch ver {
case V5R1:
v5, err := DecodeMessageV5(msg)
if err != nil {
return nil, err
}
return v5.RawMessages(), nil
case V4R1, V4R2:
v4, err := DecodeMessageV4(msg)
if err != nil {
Expand Down Expand Up @@ -259,3 +314,88 @@ func (p *PayloadHighload) UnmarshalTLB(c *boc.Cell, decoder *tlb.Decoder) error
*p = rawMessages
return nil
}

func (l *SendMessageList) UnmarshalTLB(c *boc.Cell, decoder *tlb.Decoder) error {
var actions []SendMessageAction
for {
switch c.BitsAvailableForRead() {
case 0:
l.Actions = actions
return nil
case 40:
next, err := c.NextRef()
if err != nil {
return err
}
var action SendMessageAction
if err := decoder.Unmarshal(c, &action); err != nil {
return err
}
actions = append(actions, action)
c = next
default:
return fmt.Errorf("unexpected bits available: %v", c.BitsAvailableForRead())
}
}
}

func MessageV5VerifySignature(msgBody boc.Cell, publicKey ed25519.PublicKey) error {
totalBits := msgBody.BitsAvailableForRead()
if totalBits < 512 {
return fmt.Errorf("not enough bits in the cell")
}
bits, err := msgBody.ReadBits(totalBits - 512)
if err != nil {
return err
}
signature, err := msgBody.ReadBytes(64)
if err != nil {
return err
}
msgCopy := boc.NewCell()
if err := msgCopy.WriteBitString(bits); err != nil {
return err
}
for i := 0; i < msgBody.RefsSize(); i++ {
ref, err := msgBody.NextRef()
if err != nil {
return err
}
if err := msgCopy.AddRef(ref); err != nil {
return err
}
}
hash, err := msgCopy.Hash()
if err != nil {
return err
}
if ed25519.Verify(publicKey, hash, signature) {
return nil
}
return ErrBadSignature
}

func (m *MessageV5) RawMessages() []RawMessage {
switch m.SumType {
case "Sint":
msgs := make([]RawMessage, 0, len(m.Sint.Actions.Actions))
for _, action := range m.Sint.Actions.Actions {
msgs = append(msgs, RawMessage{
Message: action.Msg,
Mode: action.Mode,
})
}
return msgs
case "Sign":
msgs := make([]RawMessage, 0, len(m.Sign.Actions.Actions))
for _, action := range m.Sign.Actions.Actions {
msgs = append(msgs, RawMessage{
Message: action.Msg,
Mode: action.Mode,
})
}
return msgs
default:
return nil
}
}
87 changes: 87 additions & 0 deletions wallet/messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package wallet

import (
"crypto/ed25519"
"encoding/hex"
"fmt"
"reflect"
"testing"
Expand All @@ -18,6 +19,14 @@ func mustFromHex(msg string) *boc.Cell {
return c
}

func mustPubkeyFromHex(hexPubkey string) ed25519.PublicKey {
bytes, err := hex.DecodeString(hexPubkey)
if err != nil {
panic(err)
}
return ed25519.PublicKey(bytes)
}

func TestExtractRawMessages(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -59,6 +68,25 @@ func TestExtractRawMessages(t *testing.T) {
},
},
},
{
name: "v5",
boc: "te6ccgECCAEAAZ4AAfGIAehvqHPiQ2Ru+zkowjJx/7oJbqEYRnlCOuPe5+2gm24WA5tLO3f////oAAAAAAADMYd8kAAAAAEHzN670eqqNU3yWGkX1dOynyAbT7DN4cFDpE0r+nInTomGrifjPTaZvG3YxYzTHpLoNesGc9s5Q0tHlLNcFNQeAQIKDsPIbQMCAwIKDsPIbQMEBQCpaAHob6hz4kNkbvs5KMIycf+6CW6hGEZ5Qjrj3uftoJtuFwAbM0yWoWMN5aT+uK8qrHCkGgxpOEKbDu0Tui2Fbyh0iAy3GwAAAAAAAAAAAAAAAAAAQAIKDsPIbQMGBwCpaAHob6hz4kNkbvs5KMIycf+6CW6hGEZ5Qjrj3uftoJtuFwAbM0yWoWMN5aT+uK8qrHCkGgxpOEKbDu0Tui2Fbyh0iAx6EgAAAAAAAAAAAAAAAAAAQAAAAKloAehvqHPiQ2Ru+zkowjJx/7oJbqEYRnlCOuPe5+2gm24XABszTJahYw3lpP64ryqscKQaDGk4QpsO7RO6LYVvKHSIDD0JAAAAAAAAAAAAAAAAAABA",
ver: V5R1,
want: []RawMessage{
{
Message: mustFromHex("te6ccgEBAQEAVwAAqWgB6G+oc+JDZG77OSjCMnH/ugluoRhGeUI6497n7aCbbhcAGzNMlqFjDeWk/rivKqxwpBoMaThCmw7tE7othW8odIgMtxsAAAAAAAAAAAAAAAAAAEA="),
Mode: 3,
},
{
Message: mustFromHex("te6ccgEBAQEAVwAAqWgB6G+oc+JDZG77OSjCMnH/ugluoRhGeUI6497n7aCbbhcAGzNMlqFjDeWk/rivKqxwpBoMaThCmw7tE7othW8odIgMehIAAAAAAAAAAAAAAAAAAEA="),
Mode: 3,
},
{
Message: mustFromHex("te6ccgEBAQEAVwAAqWgB6G+oc+JDZG77OSjCMnH/ugluoRhGeUI6497n7aCbbhcAGzNMlqFjDeWk/rivKqxwpBoMaThCmw7tE7othW8odIgMPQkAAAAAAAAAAAAAAAAAAEA="),
Mode: 3,
},
},
},
{
name: "highload",
boc: "te6ccgECCQEAAUMAAUWIAbeTPaOhIeFpX00pVBankGP2F/kaObq5EAdGLvI+omE+DAEBmXzKceTPz+weyz8nYZbOkpsBYbvy6gN7h38ZVL6RTqln7XbUzHkQqxRp1B1ZYkBgMW1NtE7r8Jwg26HcS3qPiwYAAYiUZMJyTpfTrVXAAgIFngACAwQBAwDgBQEDAOAHAWJCADZmmS1CxhvLSf1xXlVY4Ug0GNJwhTYd2id0WwreUOkQCKAAAAAAAAAAAAAAAAABBgBQAAAAADcwMzBhYzQ2LWI5NWMtNDRjNy04ZDdiLTYxMjMyNmU2ZTUxMgFiQgA2ZpktQsYby0n9cV5VWOFINBjScIU2HdondFsK3lDpEAlAAAAAAAAAAAAAAAAAAQgAUAAAAAAzYjA2OTU1YS03YjRjLTQ1YWEtOTVlNy0wNTI4ZWZhYjAyM2E=",
Expand Down Expand Up @@ -161,3 +189,62 @@ func TestSignedMsgBody_Verify(t *testing.T) {
})
}
}

func TestMessageV5VerifySignature(t *testing.T) {
tests := []struct {
name string
boc string
publicKey ed25519.PublicKey
invalidPublicKeys []ed25519.PublicKey
wantErr bool
}{
{
name: "wallet v5",
boc: "te6ccgECCAEAAZ4AAfGIAehvqHPiQ2Ru+zkowjJx/7oJbqEYRnlCOuPe5+2gm24WA5tLO3f////oAAAAAAADMY8YiAAAADPkc94coPiaMQo1EI1uuJWlVQGxiffff96PyOTGiQhUjkr733UkT8rfdXxuYcb9SMykg8Tlo7LNBB187eI+ymw2AQIKDsPIbQMCAwIKDsPIbQMEBQCpaAHob6hz4kNkbvs5KMIycf+6CW6hGEZ5Qjrj3uftoJtuFwAbM0yWoWMN5aT+uK8qrHCkGgxpOEKbDu0Tui2Fbyh0iAy3GwAAAAAAAAAAAAAAAAAAQAIKDsPIbQMGBwCpaAHob6hz4kNkbvs5KMIycf+6CW6hGEZ5Qjrj3uftoJtuFwAbM0yWoWMN5aT+uK8qrHCkGgxpOEKbDu0Tui2Fbyh0iAx6EgAAAAAAAAAAAAAAAAAAQAAAAKloAehvqHPiQ2Ru+zkowjJx/7oJbqEYRnlCOuPe5+2gm24XABszTJahYw3lpP64ryqscKQaDGk4QpsO7RO6LYVvKHSIDD0JAAAAAAAAAAAAAAAAAABA",
publicKey: mustPubkeyFromHex("406b63856ff6913fe2170a5c128113c6bd8256438a43340ea3bf6e0bbc56f9ca"),
invalidPublicKeys: []ed25519.PublicKey{
mustPubkeyFromHex("406b63856ff6913fe2170a5c128113c6bd8256438a43340ea3bf6e0bbc56f9bb"),
mustPubkeyFromHex("406b63856ff6913fe2170a5c128113c6bd8256438a43340ea3bf6e0bbc560000"),
mustPubkeyFromHex("cfa50eeb1c3293c92bd33d5aa672c1717accd8a21b96033debb6d30b5bb230df"),
},
},
{
name: "wallet v5",
boc: "te6ccgECCAEAAZ4AAfGIAVjXuMKpIWGwKJenbsOOEh1AEZo6J5Zu0R8EDI37LVyKA5tLO3f////oAAAAAAADMY9YuAAAAAFs/6Zj178nNgWPsbSM2UaEwrcyYPF0kSqZ4d+fhPMfynWRWKBCiVh2PtDewtHZ5FW1luvfXHDqGX0DtYSHfVwGAQIKDsPIbQMCAwIKDsPIbQMEBQCpaAFY17jCqSFhsCiXp27DjhIdQBGaOieWbtEfBAyN+y1ciwAbM0yWoWMN5aT+uK8qrHCkGgxpOEKbDu0Tui2Fbyh0iAy3GwAAAAAAAAAAAAAAAAAAQAIKDsPIbQMGBwCpaAFY17jCqSFhsCiXp27DjhIdQBGaOieWbtEfBAyN+y1ciwAbM0yWoWMN5aT+uK8qrHCkGgxpOEKbDu0Tui2Fbyh0iAx6EgAAAAAAAAAAAAAAAAAAQAAAAKloAVjXuMKpIWGwKJenbsOOEh1AEZo6J5Zu0R8EDI37LVyLABszTJahYw3lpP64ryqscKQaDGk4QpsO7RO6LYVvKHSIDD0JAAAAAAAAAAAAAAAAAABA",
publicKey: mustPubkeyFromHex("cfa50eeb1c3293c92bd33d5aa672c1717accd8a21b96033debb6d30b5bb230df"),
invalidPublicKeys: []ed25519.PublicKey{
mustPubkeyFromHex("406b63856ff6913fe2170a5c128113c6bd8256438a43340ea3bf6e0bbc56f9bb"),
mustPubkeyFromHex("406b63856ff6913fe2170a5c128113c6bd8256438a43340ea3bf6e0bbc56f9ca"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cell := mustFromHex(tt.boc)
var m tlb.Message
if err := tlb.Unmarshal(cell, &m); err != nil {
t.Fatalf("Unmarshal() failed: %v", err)
}
msgBody := boc.Cell(m.Body.Value)
err := MessageV5VerifySignature(msgBody, tt.publicKey)
if tt.wantErr {
if err == nil {
t.Fatalf("MessageV5VerifySignature() had to fail but it didn't")
}
if err.Error() != ErrBadSignature.Error() {
t.Fatalf("MessageV5VerifySignature() failed: %v", err)
}
return
}
if err != nil {
t.Fatalf("MessageV5VerifySignature() failed: %v", err)
}

for _, publicKey := range tt.invalidPublicKeys {
if err = MessageV5VerifySignature(msgBody, publicKey); err == nil {
t.Fatalf("MessageV5VerifySignature() had to fail but it didn't")
}
}
})
}
}
4 changes: 3 additions & 1 deletion wallet/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
V3R2
V4R1
V4R2
V5R1
HighLoadV1R1
HighLoadV1R2
HighLoadV2
Expand Down Expand Up @@ -54,6 +55,7 @@ var codes = map[Version]string{
V3R2: "te6cckEBAQEAcQAA3v8AIN0gggFMl7ohggEznLqxn3Gw7UTQ0x/THzHXC//jBOCk8mCDCNcYINMf0x/TH/gjE7vyY+1E0NMf0x/T/9FRMrryoVFEuvKiBPkBVBBV+RDyo/gAkyDXSpbTB9QC+wDo0QGkyMsfyx/L/8ntVBC9ba0=",
V4R1: "te6cckECFQEAAvUAART/APSkE/S88sgLAQIBIAIDAgFIBAUE+PKDCNcYINMf0x/THwL4I7vyY+1E0NMf0x/T//QE0VFDuvKhUVG68qIF+QFUEGT5EPKj+AAkpMjLH1JAyx9SMMv/UhD0AMntVPgPAdMHIcAAn2xRkyDXSpbTB9QC+wDoMOAhwAHjACHAAuMAAcADkTDjDQOkyMsfEssfy/8REhMUA+7QAdDTAwFxsJFb4CHXScEgkVvgAdMfIYIQcGx1Z70ighBibG5jvbAighBkc3RyvbCSXwPgAvpAMCD6RAHIygfL/8nQ7UTQgQFA1yH0BDBcgQEI9ApvoTGzkl8F4ATTP8glghBwbHVnupEx4w0kghBibG5juuMABAYHCAIBIAkKAFAB+gD0BDCCEHBsdWeDHrFwgBhQBcsFJ88WUAP6AvQAEstpyx9SEMs/AFL4J28ighBibG5jgx6xcIAYUAXLBSfPFiT6AhTLahPLH1Iwyz8B+gL0AACSghBkc3Ryuo41BIEBCPRZMO1E0IEBQNcgyAHPFvQAye1UghBkc3Rygx6xcIAYUATLBVjPFiL6AhLLassfyz+UEDRfBOLJgED7AAIBIAsMAFm9JCtvaiaECAoGuQ+gIYRw1AgIR6STfSmRDOaQPp/5g3gSgBt4EBSJhxWfMYQCAVgNDgARuMl+1E0NcLH4AD2ynftRNCBAUDXIfQEMALIygfL/8nQAYEBCPQKb6ExgAgEgDxAAGa3OdqJoQCBrkOuF/8AAGa8d9qJoQBBrkOuFj8AAbtIH+gDU1CL5AAXIygcVy//J0Hd0gBjIywXLAiLPFlAF+gIUy2sSzMzJcfsAyEAUgQEI9FHypwIAbIEBCNcYyFQgJYEBCPRR8qeCEG5vdGVwdIAYyMsFywJQBM8WghAF9eEA+gITy2oSyx/JcfsAAgBygQEI1xgwUgKBAQj0WfKn+CWCEGRzdHJwdIAYyMsFywJQBc8WghAF9eEA+gIUy2oTyx8Syz/Jc/sAAAr0AMntVEap808=",
V4R2: "te6cckECFAEAAtQAART/APSkE/S88sgLAQIBIAIDAgFIBAUE+PKDCNcYINMf0x/THwL4I7vyZO1E0NMf0x/T//QE0VFDuvKhUVG68qIF+QFUEGT5EPKj+AAkpMjLH1JAyx9SMMv/UhD0AMntVPgPAdMHIcAAn2xRkyDXSpbTB9QC+wDoMOAhwAHjACHAAuMAAcADkTDjDQOkyMsfEssfy/8QERITAubQAdDTAyFxsJJfBOAi10nBIJJfBOAC0x8hghBwbHVnvSKCEGRzdHK9sJJfBeAD+kAwIPpEAcjKB8v/ydDtRNCBAUDXIfQEMFyBAQj0Cm+hMbOSXwfgBdM/yCWCEHBsdWe6kjgw4w0DghBkc3RyupJfBuMNBgcCASAICQB4AfoA9AQw+CdvIjBQCqEhvvLgUIIQcGx1Z4MesXCAGFAEywUmzxZY+gIZ9ADLaRfLH1Jgyz8gyYBA+wAGAIpQBIEBCPRZMO1E0IEBQNcgyAHPFvQAye1UAXKwjiOCEGRzdHKDHrFwgBhQBcsFUAPPFiP6AhPLassfyz/JgED7AJJfA+ICASAKCwBZvSQrb2omhAgKBrkPoCGEcNQICEekk30pkQzmkD6f+YN4EoAbeBAUiYcVnzGEAgFYDA0AEbjJftRNDXCx+AA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIA4PABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AAG7SB/oA1NQi+QAFyMoHFcv/ydB3dIAYyMsFywIizxZQBfoCFMtrEszMyXP7AMhAFIEBCPRR8qcCAHCBAQjXGPoA0z/IVCBHgQEI9FHyp4IQbm90ZXB0gBjIywXLAlAGzxZQBPoCFMtqEssfyz/Jc/sAAgBsgQEI1xj6ANM/MFIkgQEI9Fnyp4IQZHN0cnB0gBjIywXLAlAFzxZQA/oCE8tqyx8Syz/Jc/sAAAr0AMntVGliJeU=",
V5R1: "te6ccgEBAQEAIwAIQgLkzzsvTG1qYeoPK1RH0mZ4WyavNjfbLe7mvNGqgm80Eg==",
HighLoadV1R1: "te6ccgEBBgEAhgABFP8A9KQT9KDyyAsBAgEgAgMCAUgEBQC88oMI1xgg0x/TH9Mf+CMTu/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj9ATR+AB/jhghgBD0eG+hb6EgmALTB9QwAfsAkTLiAbPmWwGkyMsfyx/L/8ntVAAE0DAAEaCZL9qJoa4WPw==",
HighLoadV1R2: "te6ccgEBCAEAmQABFP8A9KQT9LzyyAsBAgEgAgMCAUgEBQC88oMI1xgg0x/TH9Mf+CMTu/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj9ATR+AB/jhghgBD0eG+hb6EgmALTB9QwAfsAkTLiAbPmWwGkyMsfyx/L/8ntVAAE0DACAUgGBwAXuznO1E0NM/MdcL/4ABG4yX7UTQ1wsfg=",
HighLoadV2: "te6ccgEBCQEA5QABFP8A9KQT9LzyyAsBAgEgAgcCAUgDBAAE0DACASAFBgAXvZznaiaGmvmOuF/8AEG+X5dqJoaY+Y6Z/p/5j6AmipEEAgegc30JjJLb/JXdHxQB6vKDCNcYINMf0z/4I6ofUyC58mPtRNDTH9M/0//0BNFTYIBA9A5voTHyYFFzuvKiB/kBVBCH+RDyowL0BNH4AH+OFiGAEPR4b6UgmALTB9QwAfsAkTLiAbPmW4MlochANIBA9EOK5jEByMsfE8s/y//0AMntVAgANCCAQPSWb6VsEiCUMFMDud4gkzM2AZJsIeKz",
Expand Down Expand Up @@ -142,7 +144,7 @@ func GetVerByCodeHash(hash tlb.Bits256) (Version, bool) {
}

func (v Version) ToString() string {
names := []string{"v1R1", "v1R2", "v1R3", "v2R1", "v2R2", "v3R1", "v3R2", "v4R1", "v4R2", "highload_v1R1", "highload_v1R2", "highload_v2", "highload_v2R1", "highload_v2R2"}
names := []string{"v1R1", "v1R2", "v1R3", "v2R1", "v2R2", "v3R1", "v3R2", "v4R1", "v4R2", "v5R1", "highload_v1R1", "highload_v1R2", "highload_v2", "highload_v2R1", "highload_v2R2"}
if int(v) > len(names) {
panic("to string conversion for this ver not supported")
}
Expand Down

0 comments on commit b01a2ca

Please sign in to comment.