diff --git a/example/deploy-nft-collection/main.go b/example/deploy-nft-collection/main.go index 10b5ae4f..d6a3692f 100644 --- a/example/deploy-nft-collection/main.go +++ b/example/deploy-nft-collection/main.go @@ -34,8 +34,8 @@ func main() { msgBody := cell.BeginCell().EndCell() fmt.Println("Deploying NFT collection contract to mainnet...") - addr, err := w.DeployContract(context.Background(), tlb.MustFromTON("0.02"), - msgBody, getNFTCollectionCode(), getContractData(w.WalletAddress(), w.WalletAddress()), true) + addr, _, _, err := w.DeployContractWaitTransaction(context.Background(), tlb.MustFromTON("0.02"), + msgBody, getNFTCollectionCode(), getContractData(w.WalletAddress(), w.WalletAddress())) if err != nil { panic(err) } diff --git a/tlb/shard.go b/tlb/shard.go index 972b791d..5e960ab8 100644 --- a/tlb/shard.go +++ b/tlb/shard.go @@ -1,6 +1,8 @@ package tlb import ( + "encoding/binary" + "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tvm/cell" ) @@ -13,6 +15,8 @@ func init() { Register(ShardStateUnsplit{}) } +type ShardID uint64 + type ShardStateUnsplit struct { _ Magic `tlb:"#9023afe2"` GlobalID int32 `tlb:"## 32"` @@ -146,3 +150,52 @@ type ShardDescB struct { FeesCollected CurrencyCollection `tlb:"."` FundsCreated CurrencyCollection `tlb:"."` } + +func (s ShardID) IsSibling(with ShardID) bool { + return (s^with) != 0 && ((s ^ with) == ((s & ShardID(bitsNegate64(uint64(s)))) << 1)) +} + +func (s ShardID) IsParent(of ShardID) bool { + y := lowerBit64(uint64(s)) + return y > 0 && of.GetParent() == s +} + +func (s ShardID) GetParent() ShardID { + y := lowerBit64(uint64(s)) + return ShardID((uint64(s) - y) | (y << 1)) +} + +func (s ShardID) GetChild(left bool) ShardID { + y := lowerBit64(uint64(s)) >> 1 + if left { + return s - ShardID(y) + } + return s + ShardID(y) +} + +func (s ShardID) ContainsAddress(addr *address.Address) bool { + x := lowerBit64(uint64(s)) + return ((uint64(s) ^ binary.BigEndian.Uint64(addr.Data())) & (bitsNegate64(x) << 1)) == 0 +} + +func (s ShardID) IsAncestor(of ShardID) bool { + x := lowerBit64(uint64(s)) + y := lowerBit64(uint64(of)) + return x >= y && uint64(s^of)&(bitsNegate64(x)<<1) == 0 +} + +func (s ShardIdent) IsSibling(with ShardIdent) bool { + return s.WorkchainID == with.WorkchainID && ShardID(s.ShardPrefix).IsSibling(ShardID(with.ShardPrefix)) +} + +func (s ShardIdent) IsAncestor(of ShardIdent) bool { + return s.WorkchainID == of.WorkchainID && ShardID(s.ShardPrefix).IsAncestor(ShardID(of.ShardPrefix)) +} + +func (s ShardIdent) IsParent(of ShardIdent) bool { + return s.WorkchainID == of.WorkchainID && ShardID(s.ShardPrefix).IsParent(ShardID(of.ShardPrefix)) +} + +func (s ShardIdent) GetShardID() ShardID { + return ShardID(s.ShardPrefix) +} diff --git a/tlb/shard_test.go b/tlb/shard_test.go index a01640a5..19f96904 100644 --- a/tlb/shard_test.go +++ b/tlb/shard_test.go @@ -2,6 +2,7 @@ package tlb import ( "encoding/hex" + "github.com/xssnick/tonutils-go/address" "github.com/xssnick/tonutils-go/tvm/cell" "testing" ) @@ -62,3 +63,363 @@ func TestShardState_LoadFromCell(t *testing.T) { }) } } + +func TestShardIdent_IsSibling(t *testing.T) { + type fields struct { + _ Magic + WorkchainID int32 + ShardPrefix uint64 + } + type args struct { + with ShardIdent + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "true", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0xC000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0x4000000000000000, + }, + }, + want: true, + }, + { + name: "same", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0x4000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0x4000000000000000, + }, + }, + want: false, + }, + { + name: "next", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0x6000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0x4000000000000000, + }, + }, + want: false, + }, + { + name: "diff wc", + fields: fields{ + WorkchainID: -1, + ShardPrefix: 0xC000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0x4000000000000000, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := ShardIdent{ + WorkchainID: tt.fields.WorkchainID, + ShardPrefix: tt.fields.ShardPrefix, + } + if got := s.IsSibling(tt.args.with); got != tt.want { + t.Errorf("IsSibling() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestShardIdent_IsParent(t *testing.T) { + type fields struct { + _ Magic + WorkchainID int32 + ShardPrefix uint64 + } + type args struct { + with ShardIdent + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "parent", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0x8000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0xC000000000000000, + }, + }, + want: true, + }, + { + name: "child", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0xC000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0x8000000000000000, + }, + }, + want: false, + }, + { + name: "grand child", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0x8000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0xE000000000000000, + }, + }, + want: false, + }, + { + name: "diff wc", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0x8000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: -1, + ShardPrefix: 0xC000000000000000, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := ShardIdent{ + WorkchainID: tt.fields.WorkchainID, + ShardPrefix: tt.fields.ShardPrefix, + } + if got := s.IsParent(tt.args.with); got != tt.want { + t.Errorf("IsParent() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestShardIdent_IsAncestor(t *testing.T) { + type fields struct { + _ Magic + WorkchainID int32 + ShardPrefix uint64 + } + type args struct { + with ShardIdent + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "ancestor", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0x8000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: 0, + ShardPrefix: 0xE000000000000000, + }, + }, + want: true, + }, + { + name: "diff wc", + fields: fields{ + WorkchainID: 0, + ShardPrefix: 0x8000000000000000, + }, + args: args{ + with: ShardIdent{ + WorkchainID: -1, + ShardPrefix: 0xE000000000000000, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := ShardIdent{ + WorkchainID: tt.fields.WorkchainID, + ShardPrefix: tt.fields.ShardPrefix, + } + if got := s.IsAncestor(tt.args.with); got != tt.want { + t.Errorf("IsAncestor() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestShardIdent_GetShardID(t *testing.T) { + type fields struct { + ShardPrefix uint64 + } + tests := []struct { + name string + fields fields + want ShardID + }{ + { + name: "ok", + fields: fields{ + ShardPrefix: 0xE000000000000000, + }, + want: 0xE000000000000000, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := ShardIdent{ + ShardPrefix: tt.fields.ShardPrefix, + } + if got := s.GetShardID(); got != tt.want { + t.Errorf("GetShardID() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestShardID_GetChild(t *testing.T) { + type args struct { + left bool + } + tests := []struct { + name string + s ShardID + args args + want ShardID + }{ + { + name: "ok", + s: 0xE000000000000000, + args: args{ + true, + }, + want: 0xD000000000000000, + }, + { + name: "ok", + s: 0xE000000000000000, + args: args{ + false, + }, + want: 0xF000000000000000, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.s.GetChild(tt.args.left); got != tt.want { + t.Errorf("GetChild() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestShardID_ContainsAddress(t *testing.T) { + type args struct { + addr *address.Address + } + tests := []struct { + name string + s ShardID + args args + want bool + }{ + { + name: "ok", + s: 0xA000000000000000, + args: args{ + address.MustParseAddr("EQCN6j4gO7D_9OBkWQy_BkW1peVqA0ikvcSgCd9yj1yxu7VD"), + }, + want: true, + }, + { + name: "ok 2", + s: 0x6000000000000000, + args: args{ + address.MustParseAddr("EQBTmKoKwypDGJFXf9FNwNdKG9Ei5C9KdKd85_ALPLRJbIR1"), + }, + want: true, + }, + { + name: "ok 3", + s: 0x8000000000000000, + args: args{ + address.MustParseAddr("EQBTmKoKwypDGJFXf9FNwNdKG9Ei5C9KdKd85_ALPLRJbIR1"), + }, + want: true, + }, + { + name: "ok 3", + s: 0x8000000000000000, + args: args{ + address.MustParseAddr("EQCN6j4gO7D_9OBkWQy_BkW1peVqA0ikvcSgCd9yj1yxu7VD"), + }, + want: true, + }, + { + name: "no", + s: 0x6000000000000000, + args: args{ + address.MustParseAddr("EQCN6j4gO7D_9OBkWQy_BkW1peVqA0ikvcSgCd9yj1yxu7VD"), + }, + want: false, + }, + { + name: "no 2", + s: 0xA000000000000000, + args: args{ + address.MustParseAddr("EQBTmKoKwypDGJFXf9FNwNdKG9Ei5C9KdKd85_ALPLRJbIR1"), + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.s.ContainsAddress(tt.args.addr); got != tt.want { + t.Errorf("ContainsAddress() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/ton/wallet/address.go b/ton/wallet/address.go index bd162a33..d578fb6d 100644 --- a/ton/wallet/address.go +++ b/ton/wallet/address.go @@ -58,6 +58,8 @@ func GetStateInit(pubKey ed25519.PublicKey, version VersionConfig, subWallet uin } case ConfigHighloadV3: ver = HighloadV3 + case ConfigV5R1: + ver = V5R1 } code, ok := walletCode[ver] @@ -80,6 +82,18 @@ func GetStateInit(pubKey ed25519.PublicKey, version VersionConfig, subWallet uin MustStoreSlice(pubKey, 256). MustStoreDict(nil). // empty dict of plugins EndCell() + case V5R1: + config := version.(ConfigV5R1) + + 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). + MustStoreSlice(pubKey, 256). + MustStoreDict(nil). // empty dict of plugins + EndCell() case HighloadV2R2, HighloadV2Verified: data = cell.BeginCell(). MustStoreUInt(uint64(subWallet), 32). diff --git a/ton/wallet/integration_test.go b/ton/wallet/integration_test.go index 35bcc357..24a234ba 100644 --- a/ton/wallet/integration_test.go +++ b/ton/wallet/integration_test.go @@ -83,10 +83,40 @@ func Test_HighloadHeavyTransfer(t *testing.T) { t.Log("TX", base64.StdEncoding.EncodeToString(tx.Hash)) } +func Test_V5HeavyTransfer(t *testing.T) { + seed := strings.Split(_seed, " ") + + w, err := FromSeed(api, seed, ConfigV5R1{ + NetworkGlobalID: MainnetGlobalID, + }) + if err != nil { + t.Fatal("FromSeed err:", err.Error()) + return + } + + t.Log("test wallet address:", w.WalletAddress()) + + var list []*Message + for i := 0; i < 255; 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 []VersionConfig{V3R2, V4R2, HighloadV2R2, V3R1, V4R1, HighloadV2Verified, ConfigHighloadV3{ + for _, v := range []VersionConfig{ConfigV5R1{ + NetworkGlobalID: TestnetGlobalID, + }, 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 diff --git a/ton/wallet/v5r1.go b/ton/wallet/v5r1.go new file mode 100644 index 00000000..e94df6c0 --- /dev/null +++ b/ton/wallet/v5r1.go @@ -0,0 +1,96 @@ +package wallet + +import ( + "context" + "errors" + "fmt" + "github.com/xssnick/tonutils-go/tlb" + "github.com/xssnick/tonutils-go/ton" + "math/big" + "time" + + "github.com/xssnick/tonutils-go/tvm/cell" +) + +// https://github.com/tonkeeper/tonkeeper-ton/commit/e8a7f3415e241daf4ac723f273fbc12776663c49#diff-c20d462b2e1ec616bbba2db39acc7a6c61edc3d5e768f5c2034a80169b1a56caR29 +const _V5R1CodeHex = "b5ee9c7241010101002300084202e4cf3b2f4c6d6a61ea0f2b5447d266785b26af3637db2deee6bcd1aa826f34120dcd8e11" + +type ConfigV5R1 struct { + NetworkGlobalID int32 + Workchain int8 +} + +type SpecV5R1 struct { + SpecRegular + SpecSeqno + + config ConfigV5R1 +} + +const MainnetGlobalID = -239 +const TestnetGlobalID = -3 + +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 + + 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 := packV5Actions(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 packV5Actions(messages []*Message) (*cell.Builder, error) { + if len(messages) > 255 { + return nil, fmt.Errorf("max 255 messages allowed for v5") + } + + 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 cell.BeginCell().MustStoreUInt(0, 1).MustStoreRef(list), nil +} diff --git a/ton/wallet/wallet.go b/ton/wallet/wallet.go index 62a6f384..4477113e 100644 --- a/ton/wallet/wallet.go +++ b/ton/wallet/wallet.go @@ -36,6 +36,7 @@ const ( V3 = V3R2 V4R1 Version = 41 V4R2 Version = 42 + V5R1 Version = 51 HighloadV2R2 Version = 122 HighloadV2Verified Version = 123 HighloadV3 Version = 300 @@ -70,6 +71,7 @@ var ( V2R1: _V2R1CodeHex, V2R2: _V2R2CodeHex, V3R1: _V3R1CodeHex, V3R2: _V3R2CodeHex, V4R1: _V4R1CodeHex, V4R2: _V4R2CodeHex, + V5R1: _V5R1CodeHex, HighloadV2R2: _HighloadV2R2CodeHex, HighloadV2Verified: _HighloadV2VerifiedCodeHex, HighloadV3: _HighloadV3CodeHex, Lockup: _LockupCodeHex, @@ -164,7 +166,7 @@ func FromPrivateKey(api TonAPI, key ed25519.PrivateKey, version VersionConfig) ( func getSpec(w *Wallet) (any, error) { switch v := w.ver.(type) { - case Version: + case Version, ConfigV5R1: regular := SpecRegular{ wallet: w, messagesTTL: 60 * 3, // default ttl 3 min @@ -200,6 +202,16 @@ func getSpec(w *Wallet) (any, error) { return &SpecHighloadV2R2{regular, SpecQuery{}}, nil case HighloadV3: return nil, fmt.Errorf("use ConfigHighloadV3 for highload v3 spec") + case V5R1: + return nil, fmt.Errorf("use ConfigV5R1 for v5 spec") + default: + switch x := w.ver.(type) { + case ConfigV5R1: + if x.NetworkGlobalID == 0 { + return nil, fmt.Errorf("NetworkGlobalID should be set in v5 config") + } + return &SpecV5R1{SpecRegular: regular, SpecSeqno: SpecSeqno{seqnoFetcher: seqnoFetcher}, config: x}, nil + } } case ConfigHighloadV3: return &SpecHighloadV3{wallet: w, config: v}, nil @@ -300,9 +312,13 @@ func (w *Wallet) PrepareExternalMessageForMany(ctx context.Context, withStateIni var msg *cell.Cell switch v := w.ver.(type) { - case Version: + case Version, ConfigV5R1: + if _, ok := v.(ConfigV5R1); ok { + v = V5R1 + } + switch v { - case V3R2, V3R1, V4R2, V4R1: + case V3R2, V3R1, V4R2, V4R1, V5R1: msg, err = w.spec.(RegularBuilder).BuildMessage(ctx, !withStateInit, nil, messages) if err != nil { return nil, fmt.Errorf("build message err: %w", err)