diff --git a/address/addr.go b/address/addr.go index 5822628a..fd661377 100644 --- a/address/addr.go +++ b/address/addr.go @@ -1,6 +1,7 @@ package address import ( + "bytes" "encoding/base64" "encoding/binary" "encoding/hex" @@ -337,3 +338,7 @@ func (a *Address) Workchain() int32 { func (a *Address) Data() []byte { return a.data } + +func (a *Address) Equals(b *Address) bool { + return a.workchain == b.workchain && bytes.Equal(a.data, b.data) +} diff --git a/example/accept-payments/main.go b/example/accept-payments/main.go index 97765f25..a2fd672a 100644 --- a/example/accept-payments/main.go +++ b/example/accept-payments/main.go @@ -6,6 +6,7 @@ import ( "github.com/xssnick/tonutils-go/liteclient" "github.com/xssnick/tonutils-go/tlb" "github.com/xssnick/tonutils-go/ton" + "github.com/xssnick/tonutils-go/ton/jetton" "log" ) @@ -26,19 +27,17 @@ func main() { } // initialize ton api lite connection wrapper with full proof checks - api := ton.NewAPIClient(client, ton.ProofCheckPolicySecure).WithRetry() + api := ton.NewAPIClient(client, ton.ProofCheckPolicyFast).WithRetry() api.SetTrustedBlockFromConfig(cfg) - log.Println("fetching and checking proofs since config init block, it may take near a minute...") master, err := api.CurrentMasterchainInfo(context.Background()) // we fetch block just to trigger chain proof check if err != nil { log.Fatalln("get masterchain info err: ", err.Error()) return } - log.Println("master proof checks are completed successfully, now communication is 100% safe!") // address on which we are accepting payments - treasuryAddress := address.MustParseAddr("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N") + treasuryAddress := address.MustParseAddr("EQAYqo4u7VF0fa4DPAebk4g9lBytj2VFny7pzXR0trjtXQaO") acc, err := api.GetAccount(context.Background(), master, treasuryAddress) if err != nil { @@ -58,10 +57,38 @@ func main() { log.Println("waiting for transfers...") + // USDT master contract addr, but can be any jetton + usdt := jetton.NewJettonMasterClient(api, address.MustParseAddr("EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs")) + // get our jetton wallet address + treasuryJettonWallet, err := usdt.GetJettonWalletAtBlock(context.Background(), treasuryAddress, master) + if err != nil { + log.Fatalln("get jetton wallet address err: ", err.Error()) + return + } + // listen for new transactions from channel for tx := range transactions { - // process transaction here - log.Println(tx.String()) + // only internal messages can increase the balance + if tx.IO.In != nil && tx.IO.In.MsgType == tlb.MsgTypeInternal { + ti := tx.IO.In.AsInternal() + src := ti.SrcAddr + + // verify that event sender is our jetton wallet + if ti.SrcAddr.Equals(treasuryJettonWallet.Address()) { + var transfer jetton.TransferNotification + if err = tlb.LoadFromCell(&transfer, ti.Body.BeginParse()); err == nil { + // convert decimals to 6 for USDT (it can be fetched from jetton details too), default is 9 + amt := tlb.MustFromNano(transfer.Amount.Nano(), 6) + + // reassign sender to real jetton sender instead of its jetton wallet contract + src = transfer.Sender + log.Println("received", amt.String(), "USDT from", src.String()) + } + } + + // show received ton amount + log.Println("received", ti.Amount.String(), "TON from", src.String()) + } // update last processed lt and save it in db lastProcessedLT = tx.LT diff --git a/example/deploy-nft-collection/main.go b/example/deploy-nft-collection/main.go index d6a3692f..c3029e10 100644 --- a/example/deploy-nft-collection/main.go +++ b/example/deploy-nft-collection/main.go @@ -45,7 +45,7 @@ func main() { func getWallet(api ton.APIClientWrapped) *wallet.Wallet { words := strings.Split("birth pattern then forest walnut then phrase walnut fan pumpkin pattern then cluster blossom verify then forest velvet pond fiction pattern collect then then", " ") - w, err := wallet.FromSeed(api, words, wallet.V3) + w, err := wallet.FromSeed(api, words, wallet.V4R2) if err != nil { panic(err) } @@ -53,7 +53,7 @@ func getWallet(api ton.APIClientWrapped) *wallet.Wallet { } func getNFTCollectionCode() *cell.Cell { - var hexBOC = "b5ee9c72410213010001fe000114ff00f4a413f4bcf2c80b0102016204020201200e030025bc82df6a2687d20699fea6a6a182de86a182c40202cd0a050201200706003d45af0047021f005778018c8cb0558cf165004fa0213cb6b12ccccc971fb0080201200908001b3e401d3232c084b281f2fff27420002d007232cffe0a33c5b25c083232c044fd003d0032c0326003ebd10638048adf000e8698180b8d848adf07d201800e98fe99ff6a2687d20699fea6a6a184108349e9ca829405d47141baf8280e8410854658056b84008646582a802e78b127d010a65b509e58fe59f80e78b64c0207d80701b28b9e382f970c892e000f18112e001718119026001f1812f82c207f97840d0c0b002801fa40304144c85005cf1613cb3fccccccc9ed5400a6357003d4308e378040f4966fa5208e2906a4208100fabe93f2c18fde81019321a05325bbf2f402fa00d43022544b30f00623ba9302a402de04926c21e2b3e6303250444313c85005cf1613cb3fccccccc9ed5400603502d33f5313bbf2e1925313ba01fa00d43028103459f0068e1201a44343c85005cf1613cb3fccccccc9ed54925f05e2020120120f0201201110002db4f47da89a1f481a67fa9a9a86028be09e008e003e00b0002fb5dafda89a1f481a67fa9a9a860d883a1a61fa61ff4806100043b8b5d31ed44d0fa40d33fd4d4d43010245f04d0d431d430d071c8cb0701cf16ccc98f34ea10e" + var hexBOC = "b5ee9c724102140100021f000114ff00f4a413f4bcf2c80b0102016202030202cd04050201200e0f04e7d10638048adf000e8698180b8d848adf07d201800e98fe99ff6a2687d20699fea6a6a184108349e9ca829405d47141baf8280e8410854658056b84008646582a802e78b127d010a65b509e58fe59f80e78b64c0207d80701b28b9e382f970c892e000f18112e001718112e001f181181981e0024060708090201200a0b00603502d33f5313bbf2e1925313ba01fa00d43028103459f0068e1201a44343c85005cf1613cb3fccccccc9ed54925f05e200a6357003d4308e378040f4966fa5208e2906a4208100fabe93f2c18fde81019321a05325bbf2f402fa00d43022544b30f00623ba9302a402de04926c21e2b3e6303250444313c85005cf1613cb3fccccccc9ed54002c323401fa40304144c85005cf1613cb3fccccccc9ed54003c8e15d4d43010344130c85005cf1613cb3fccccccc9ed54e05f04840ff2f00201200c0d003d45af0047021f005778018c8cb0558cf165004fa0213cb6b12ccccc971fb008002d007232cffe0a33c5b25c083232c044fd003d0032c03260001b3e401d3232c084b281f2fff2742002012010110025bc82df6a2687d20699fea6a6a182de86a182c40043b8b5d31ed44d0fa40d33fd4d4d43010245f04d0d431d430d071c8cb0701cf16ccc980201201213002fb5dafda89a1f481a67fa9a9a860d883a1a61fa61ff480610002db4f47da89a1f481a67fa9a9a86028be09e008e003e00b01a500c6e" codeCellBytes, _ := hex.DecodeString(hexBOC) codeCell, err := cell.FromBOC(codeCellBytes) @@ -65,7 +65,7 @@ func getNFTCollectionCode() *cell.Cell { } func getNFTItemCode() *cell.Cell { - var hexBOC = "b5ee9c7241020d010001d0000114ff00f4a413f4bcf2c80b0102016203020009a11f9fe0050202ce050402012008060201200907001d00f232cfd633c58073c5b3327b552000113e910c1c2ebcb85360003b3b513434cffe900835d27080269fc07e90350c04090408f80c1c165b5b6002d70c8871c02497c0f83434c0c05c6c2497c0f83e903e900c7e800c5c75c87e800c7e800c3c00812ce3850c1b088d148cb1c17cb865407e90350c0408fc00f801b4c7f4cfe08417f30f45148c2ea3a1cc840dd78c9004f80c0d0d0d4d60840bf2c9a884aeb8c097c12103fcbc200b0a00727082108b77173505c8cbff5004cf1610248040708010c8cb055007cf165005fa0215cb6a12cb1fcb3f226eb39458cf17019132e201c901fb0001f65135c705f2e191fa4021f001fa40d20031fa00820afaf0801ba121945315a0a1de22d70b01c300209206a19136e220c2fff2e192218e3e821005138d91c85009cf16500bcf16712449145446a0708010c8cb055007cf165005fa0215cb6a12cb1fcb3f226eb39458cf17019132e201c901fb00104794102a375be20c0082028e3526f0018210d53276db103744006d71708010c8cb055007cf165005fa0215cb6a12cb1fcb3f226eb39458cf17019132e201c901fb0093303234e25502f003cc82807e" + var hexBOC = "b5ee9c7241020d010001d0000114ff00f4a413f4bcf2c80b0102016202030202ce04050009a11f9fe00502012006070201200b0c02d70c8871c02497c0f83434c0c05c6c2497c0f83e903e900c7e800c5c75c87e800c7e800c3c00812ce3850c1b088d148cb1c17cb865407e90350c0408fc00f801b4c7f4cfe08417f30f45148c2ea3a1cc840dd78c9004f80c0d0d0d4d60840bf2c9a884aeb8c097c12103fcbc20080900113e910c1c2ebcb8536001f65135c705f2e191fa4021f001fa40d20031fa00820afaf0801ba121945315a0a1de22d70b01c300209206a19136e220c2fff2e192218e3e821005138d91c85009cf16500bcf16712449145446a0708010c8cb055007cf165005fa0215cb6a12cb1fcb3f226eb39458cf17019132e201c901fb00104794102a375be20a00727082108b77173505c8cbff5004cf1610248040708010c8cb055007cf165005fa0215cb6a12cb1fcb3f226eb39458cf17019132e201c901fb000082028e3526f0018210d53276db103744006d71708010c8cb055007cf165005fa0215cb6a12cb1fcb3f226eb39458cf17019132e201c901fb0093303234e25502f003003b3b513434cffe900835d27080269fc07e90350c04090408f80c1c165b5b60001d00f232cfd633c58073c5b3327b5520bf75041b" codeCellBytes, _ := hex.DecodeString(hexBOC) codeCell, err := cell.FromBOC(codeCellBytes) @@ -86,9 +86,9 @@ func getContractData(collectionOwnerAddr, royaltyAddr *address.Address) *cell.Ce // = Storage; royalty := cell.BeginCell(). - MustStoreUInt(50, 16). // 5% royalty - MustStoreUInt(1000, 16). - MustStoreAddr(royaltyAddr). + MustStoreUInt(5, 16). // 5% royalty + MustStoreUInt(100, 16). // denominator + MustStoreAddr(royaltyAddr). // fee addr destination EndCell() // collection data diff --git a/example/nft-mint/main.go b/example/nft-mint/main.go index 00f39800..7f79f736 100644 --- a/example/nft-mint/main.go +++ b/example/nft-mint/main.go @@ -70,7 +70,7 @@ func main() { func getWallet(api *ton.APIClient) *wallet.Wallet { words := strings.Split("birth pattern then forest walnut then phrase walnut fan pumpkin pattern then cluster blossom verify then forest velvet pond fiction pattern collect then then", " ") - w, err := wallet.FromSeed(api, words, wallet.V3) + w, err := wallet.FromSeed(api, words, wallet.V4R2) if err != nil { panic(err) } diff --git a/tlb/transaction.go b/tlb/transaction.go index 974aa9a1..76b9da44 100644 --- a/tlb/transaction.go +++ b/tlb/transaction.go @@ -1,6 +1,7 @@ package tlb import ( + "encoding/hex" "fmt" "math/big" "reflect" @@ -290,17 +291,20 @@ func (t *Transaction) String() string { case TransactionDescriptionOrdinary: } if t.IO.In != nil { + build += fmt.Sprintf("LT: %d", t.LT) + if t.IO.In.MsgType == MsgTypeInternal { in = t.IO.In.AsInternal().Amount.Nano() - } - if in.Cmp(big.NewInt(0)) != 0 { intTx := t.IO.In.AsInternal() - build += fmt.Sprintf("LT: %d, In: %s TON, From %s", t.LT, FromNanoTON(in).String(), intTx.SrcAddr) + build += fmt.Sprintf(", In: %s TON, From %s", FromNanoTON(in).String(), intTx.SrcAddr) comment := intTx.Comment() if comment != "" { build += ", Comment: " + comment } + } else if t.IO.In.MsgType == MsgTypeExternalIn { + exTx := t.IO.In.AsExternalIn() + build += ", ExternalIn, hash: " + hex.EncodeToString(exTx.Body.Hash()) } } diff --git a/ton/block.go b/ton/block.go index 48d57c43..5fe1e155 100644 --- a/ton/block.go +++ b/ton/block.go @@ -182,7 +182,7 @@ type ShardInfo struct { ID *BlockIDExt `tl:"struct"` ShardBlock *BlockIDExt `tl:"struct"` ShardProof []*cell.Cell `tl:"cell optional 2"` - ShardDescription *cell.Cell `tl:"bytes"` + ShardDescription *cell.Cell `tl:"cell optional"` } type BlockTransactions struct { @@ -194,11 +194,11 @@ type BlockTransactions struct { } type BlockTransactionsExt struct { - ID *BlockIDExt `tl:"struct"` - ReqCount int32 `tl:"int"` - Incomplete bool `tl:"bool"` - Transactions *cell.Cell `tl:"cell optional"` - Proof []byte `tl:"bytes"` + ID *BlockIDExt `tl:"struct"` + ReqCount int32 `tl:"int"` + Incomplete bool `tl:"bool"` + Transactions []*cell.Cell `tl:"cell optional"` + Proof []byte `tl:"bytes"` } type BlockData struct { diff --git a/ton/jetton/jetton.go b/ton/jetton/jetton.go index fa51471a..554d5f93 100644 --- a/ton/jetton/jetton.go +++ b/ton/jetton/jetton.go @@ -2,6 +2,7 @@ package jetton import ( "context" + "errors" "fmt" "math/big" @@ -19,6 +20,8 @@ type TonApi interface { SubscribeOnTransactions(workerCtx context.Context, addr *address.Address, lastProcessedLT uint64, channel chan<- *tlb.Transaction) } +var ErrInvalidTransfer = errors.New("transfer is not verified") + type MintPayloadMasterMsg struct { Opcode uint32 `tlb:"## 32"` QueryID uint64 `tlb:"## 64"` @@ -34,6 +37,14 @@ type MintPayload struct { MasterMsg MintPayloadMasterMsg `tlb:"^"` } +type TransferNotification struct { + _ tlb.Magic `tlb:"#7362d09c"` + QueryID uint64 `tlb:"## 64"` + Amount tlb.Coins `tlb:"."` + Sender *address.Address `tlb:"addr"` + ForwardPayload *cell.Cell `tlb:"either . ^"` +} + type Data struct { TotalSupply *big.Int Mintable bool diff --git a/ton/transactions.go b/ton/transactions.go index 6deecd8f..a3f2888c 100644 --- a/ton/transactions.go +++ b/ton/transactions.go @@ -324,6 +324,10 @@ func (c *APIClient) findLastTransactionByHash(ctx context.Context, addr *address } if isOut { + if transaction.IO.Out == nil { + continue + } + list, err := transaction.IO.Out.ToSlice() if err != nil { return nil, fmt.Errorf("cannot list out messages: %w", err) @@ -334,6 +338,8 @@ func (c *APIClient) findLastTransactionByHash(ctx context.Context, addr *address return transaction, nil } } + + continue } else { if transaction.IO.In == nil { continue diff --git a/ton/wallet/address.go b/ton/wallet/address.go index 01a1df13..a3700618 100644 --- a/ton/wallet/address.go +++ b/ton/wallet/address.go @@ -55,13 +55,17 @@ func GetStateInit(pubKey ed25519.PublicKey, version VersionConfig, subWallet uin switch ver { case HighloadV3: return nil, fmt.Errorf("use ConfigHighloadV3 for highload v3 spec") - case V5R1: - return nil, fmt.Errorf("use ConfigV5R1 for v5 spec") + case V5R1Beta: + return nil, fmt.Errorf("use ConfigV5R1Beta for V5 spec") + case V5R1Final: + return nil, fmt.Errorf("use ConfigV5R1Final for V5 spec") } case ConfigHighloadV3: ver = HighloadV3 - case ConfigV5R1: - ver = V5R1 + case ConfigV5R1Beta: + ver = V5R1Beta + case ConfigV5R1Final: + ver = V5R1Final } code, ok := walletCode[ver] @@ -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 V5R1Beta: + config := version.(ConfigV5R1Beta) 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 V5R1Final: + config := version.(ConfigV5R1Final) + + // Create WalletId instance + walletId := V5R1ID{ + 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/integration_test.go b/ton/wallet/integration_test.go index f785bb05..80ce3fab 100644 --- a/ton/wallet/integration_test.go +++ b/ton/wallet/integration_test.go @@ -86,7 +86,7 @@ func Test_HighloadHeavyTransfer(t *testing.T) { func Test_V5HeavyTransfer(t *testing.T) { seed := strings.Split(_seed, " ") - w, err := FromSeed(api, seed, ConfigV5R1{ + w, err := FromSeed(api, seed, ConfigV5R1Final{ NetworkGlobalID: MainnetGlobalID, }) if err != nil { @@ -114,7 +114,9 @@ func Test_V5HeavyTransfer(t *testing.T) { func Test_WalletTransfer(t *testing.T) { seed := strings.Split(_seed, " ") - for _, v := range []VersionConfig{ConfigV5R1{ + for _, v := range []VersionConfig{ConfigV5R1Final{ + NetworkGlobalID: TestnetGlobalID, + }, ConfigV5R1Beta{ NetworkGlobalID: TestnetGlobalID, }, V3R2, V4R2, HighloadV2R2, V3R1, V4R1, HighloadV2Verified, ConfigHighloadV3{ MessageTTL: 120, @@ -164,11 +166,22 @@ func Test_WalletTransfer(t *testing.T) { comment := randString(150) addr := address.MustParseAddr("EQA8aJTl0jfFnUZBJjTeUxu9OcbsoPBp9UcHE9upyY_X35kE") if balance.Nano().Uint64() >= 3000000 { - err = w.Transfer(ctx, addr, tlb.MustFromTON("0.003"), comment, true) + tr, err := w.BuildTransfer(addr, tlb.MustFromTON("0.003"), false, comment) + if err != nil { + t.Fatal("Build transfer err:", err.Error()) + return + } + + tx, _, err := w.SendManyWaitTransaction(ctx, []*Message{tr}) if err != nil { t.Fatal("Transfer err:", err.Error()) return } + + if tx.OutMsgCount == 0 { + t.Fatal("Out msg is 0:", ver) + return + } } else { t.Fatal("not enough balance") return diff --git a/ton/wallet/v5beta.go b/ton/wallet/v5beta.go new file mode 100644 index 00000000..f31db570 --- /dev/null +++ b/ton/wallet/v5beta.go @@ -0,0 +1,94 @@ +package wallet + +import ( + "context" + "errors" + "fmt" + "github.com/xssnick/tonutils-go/tlb" + "time" + + "github.com/xssnick/tonutils-go/ton" + + "github.com/xssnick/tonutils-go/tvm/cell" +) + +// https://github.com/tonkeeper/tonkeeper-ton/commit/e8a7f3415e241daf4ac723f273fbc12776663c49#diff-c20d462b2e1ec616bbba2db39acc7a6c61edc3d5e768f5c2034a80169b1a56caR29 +const _V5R1BetaCodeHex = "b5ee9c7241010101002300084202e4cf3b2f4c6d6a61ea0f2b5447d266785b26af3637db2deee6bcd1aa826f34120dcd8e11" + +type ConfigV5R1Beta struct { + NetworkGlobalID int32 + Workchain int8 +} + +type SpecV5R1Beta struct { + SpecRegular + SpecSeqno + + config ConfigV5R1Beta +} + +func (c ConfigV5R1Beta) String() string { + return "V5R1Beta" +} + +func (s *SpecV5R1Beta) 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 err := validateMessageFields(messages); err != nil { + return nil, err + } + + 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). // 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 +} diff --git a/ton/wallet/v5r1.go b/ton/wallet/v5r1.go index 8c560ca8..1e97fc16 100644 --- a/ton/wallet/v5r1.go +++ b/ton/wallet/v5r1.go @@ -4,36 +4,61 @@ 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 _V5R1FinalCodeHex = "b5ee9c7241021401000281000114ff00f4a413f4bcf2c80b01020120020d020148030402dcd020d749c120915b8f6320d70b1f2082106578746ebd21821073696e74bdb0925f03e082106578746eba8eb48020d72101d074d721fa4030fa44f828fa443058bd915be0ed44d0810141d721f4058307f40e6fa1319130e18040d721707fdb3ce03120d749810280b99130e070e2100f020120050c020120060902016e07080019adce76a2684020eb90eb85ffc00019af1df6a2684010eb90eb858fc00201480a0b0017b325fb51341c75c875c2c7e00011b262fb513435c280200019be5f0f6a2684080a0eb90fa02c0102f20e011e20d70b1f82107369676ebaf2e08a7f0f01e68ef0eda2edfb218308d722028308d723208020d721d31fd31fd31fed44d0d200d31f20d31fd3ffd70a000af90140ccf9109a28945f0adb31e1f2c087df02b35007b0f2d0845125baf2e0855036baf2e086f823bbf2d0882292f800de01a47fc8ca00cb1f01cf16c9ed542092f80fde70db3cd81003f6eda2edfb02f404216e926c218e4c0221d73930709421c700b38e2d01d72820761e436c20d749c008f2e09320d74ac002f2e09320d71d06c712c2005230b0f2d089d74cd7393001a4e86c128407bbf2e093d74ac000f2e093ed55e2d20001c000915be0ebd72c08142091709601d72c081c12e25210b1e30f20d74a111213009601fa4001fa44f828fa443058baf2e091ed44d0810141d718f405049d7fc8ca0040048307f453f2e08b8e14038307f45bf2e08c22d70a00216e01b3b0f2d090e2c85003cf1612f400c9ed54007230d72c08248e2d21f2e092d200ed44d0d2005113baf2d08f54503091319c01810140d721d70a00f2e08ee2c8ca0058cf16c9ed5493f2c08de20010935bdb31e1d74cd0b4d6c35e" -type ConfigV5R1 struct { +type ConfigV5R1Final struct { NetworkGlobalID int32 Workchain int8 } -type SpecV5R1 struct { +type SpecV5R1Final struct { SpecRegular SpecSeqno - config ConfigV5R1 + config ConfigV5R1Final } -const MainnetGlobalID = -239 -const TestnetGlobalID = -3 +func (c ConfigV5R1Final) String() string { + return "V5R1Final" +} + +// Function to generate the context ID based on the given workchain +func genContextID(workchain uint8, version uint8, subwallet uint16) uint32 { + var ctx uint32 + + // Convert workchain to uint32 after ensuring it's correctly handled as an 8-bit value + ctx |= 1 << 31 // Write 1 bit as 1 at the leftmost bit + ctx |= uint32(workchain) << 23 // Write 8 bits of workchain, shifted to position + ctx |= uint32(version) << 15 // Write 8 bits of 0 (wallet version) + ctx |= uint32(subwallet) // Write 15 bits of 0 (subwallet number) + + return ctx +} + +type V5R1ID struct { + NetworkGlobalID int32 + WorkChain int8 + SubwalletNumber uint16 + WalletVersion uint8 +} -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 +func (w V5R1ID) Serialized() uint32 { + return genContextID(uint8(w.WorkChain), w.WalletVersion, w.SubwalletNumber) ^ uint32(w.NetworkGlobalID) +} +func (s *SpecV5R1Final) 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) @@ -46,15 +71,19 @@ func (s *SpecV5R1) BuildMessage(ctx context.Context, _ bool, _ *ton.BlockIDExt, return nil, fmt.Errorf("failed to build actions: %w", err) } + walletId := V5R1ID{ + 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 +91,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 packV5Actions(messages []*Message) (*cell.Builder, error) { + if err := validateMessageFields(messages); err != nil { + return nil, err } var list = cell.BeginCell().EndCell() @@ -81,12 +124,13 @@ func packV5Actions(messages []*Message) (*cell.Builder, error) { 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 e571935d..ba30e287 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 + V5R1Beta Version = 51 // W5 Beta + V5R1Final Version = 52 // W5 Final HighloadV2R2 Version = 122 HighloadV2Verified Version = 123 HighloadV3 Version = 300 @@ -79,7 +85,8 @@ var ( V2R1: _V2R1CodeHex, V2R2: _V2R2CodeHex, V3R1: _V3R1CodeHex, V3R2: _V3R2CodeHex, V4R1: _V4R1CodeHex, V4R2: _V4R2CodeHex, - V5R1: _V5R1CodeHex, + V5R1Beta: _V5R1BetaCodeHex, + V5R1Final: _V5R1FinalCodeHex, HighloadV2R2: _HighloadV2R2CodeHex, HighloadV2Verified: _HighloadV2VerifiedCodeHex, HighloadV3: _HighloadV3CodeHex, Lockup: _LockupCodeHex, @@ -155,7 +162,8 @@ func FromPrivateKey(api TonAPI, key ed25519.PrivateKey, version VersionConfig) ( // default subwallet depends on wallet type switch version.(type) { - case ConfigV5R1: + case ConfigV5R1Beta: + case ConfigV5R1Final: 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, ConfigV5R1Beta, ConfigV5R1Final: regular := SpecRegular{ wallet: w, messagesTTL: 60 * 3, // default ttl 3 min @@ -210,11 +218,16 @@ func getSpec(w *Wallet) (any, error) { } switch x := w.ver.(type) { - case ConfigV5R1: + case ConfigV5R1Beta: + if x.NetworkGlobalID == 0 { + return nil, fmt.Errorf("NetworkGlobalID should be set in V5 config") + } + return &SpecV5R1Beta{SpecRegular: regular, SpecSeqno: SpecSeqno{seqnoFetcher: seqnoFetcher}, config: x}, nil + case ConfigV5R1Final: 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 + return &SpecV5R1Final{SpecRegular: regular, SpecSeqno: SpecSeqno{seqnoFetcher: seqnoFetcher}, config: x}, nil } switch v { @@ -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 V5R1: - return nil, fmt.Errorf("use ConfigV5R1 for v5 spec") + case V5R1Beta: + return nil, fmt.Errorf("use ConfigV5R1Beta for V5 Beta spec") + case V5R1Final: + return nil, fmt.Errorf("use ConfigV5R1Final 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: - if _, ok := v.(ConfigV5R1); ok { - v = V5R1 + case Version, ConfigV5R1Beta, ConfigV5R1Final: + switch v.(type) { + case ConfigV5R1Beta: + v = V5R1Beta + case ConfigV5R1Final: + v = V5R1Final } switch v { - case V3R2, V3R1, V4R2, V4R1, V5R1: + case V3R2, V3R1, V4R2, V4R1, V5R1Beta, V5R1Final: msg, err = w.spec.(RegularBuilder).BuildMessage(ctx, !withStateInit, nil, messages) if err != nil { return nil, fmt.Errorf("build message err: %w", err) diff --git a/tvm/cell/cell.go b/tvm/cell/cell.go index 10e3dbe0..61c30676 100644 --- a/tvm/cell/cell.go +++ b/tvm/cell/cell.go @@ -147,51 +147,78 @@ func (c *Cell) DumpBits(limitLength ...int) string { func (c *Cell) dump(deep int, bin bool, limitLength uint64) string { sz, data, _ := c.BeginParse().RestBits() - var val string + builder := strings.Builder{} + if bin { for _, n := range data { - val += fmt.Sprintf("%08b", n) + builder.WriteString(fmt.Sprintf("%08b", n)) } if sz%8 != 0 { - val = val[:uint(len(val))-(8-(sz%8))] + tmp := builder.String() + builder.Reset() + builder.WriteString(tmp[:uint(len(tmp))-(8-(sz%8))]) } } else { - val = strings.ToUpper(hex.EncodeToString(data)) + tmp := make([]byte, len(data)*2) + hex.Encode(tmp, data) + builder.WriteString(strings.ToUpper(string(tmp))) + if sz%8 <= 4 && sz%8 > 0 { - // fift hex - val = val[:len(val)-1] + "_" + tmp := builder.String() + builder.Reset() + builder.WriteString(tmp[:len(tmp)-1]) + builder.WriteByte('_') + } } - str := strings.Repeat(" ", deep) + fmt.Sprint(sz) + "[" + val + "]" + val := builder.String() + builder.Reset() + builder.WriteString(strings.Repeat(" ", deep)) + builder.WriteString(strconv.FormatUint(uint64(sz), 10)) + builder.WriteByte('[') + builder.WriteString(val) + builder.WriteByte(']') + if c.levelMask.GetLevel() > 0 { - str += fmt.Sprintf("{%d}", c.levelMask.GetLevel()) + builder.WriteByte('{') + builder.WriteString(strconv.Itoa(c.levelMask.GetLevel())) + builder.WriteByte('}') + } if c.special { - str += "*" + builder.WriteByte('*') } if len(c.refs) > 0 { - str += " -> {" + + builder.WriteString(" -> {") + for i, ref := range c.refs { - str += "\n" + ref.dump(deep+1, bin, limitLength) + + builder.WriteByte('\n') + builder.WriteString(ref.dump(deep+1, bin, limitLength)) + if i == len(c.refs)-1 { - str += "\n" + builder.WriteByte('\n') } else { - str += "," + builder.WriteByte(',') } - if uint64(len(str)) > limitLength { + if uint64(builder.Len()) > limitLength { break } } - str += strings.Repeat(" ", deep) + "}" + builder.WriteString(strings.Repeat(" ", deep)) + builder.WriteByte('}') } - if uint64(len(str)) > limitLength { - str = str[:limitLength] + if uint64(builder.Len()) > limitLength { + tmp := builder.String() + builder.Reset() + builder.WriteString(tmp[:limitLength]) } - return str + return builder.String() } const _DataCellMaxLevel = 3 @@ -259,10 +286,13 @@ func (c *Cell) UnmarshalJSON(bytes []byte) error { } bytes = bytes[1 : len(bytes)-1] - data, err := base64.StdEncoding.DecodeString(string(bytes)) + data := make([]byte, base64.StdEncoding.DecodedLen(len(bytes))) + + n, err := base64.StdEncoding.Decode(data, bytes) if err != nil { return err } + data = data[:n] cl, err := FromBOC(data) if err != nil { diff --git a/tvm/cell/dict.go b/tvm/cell/dict.go index 3d018a9c..71cef2f5 100644 --- a/tvm/cell/dict.go +++ b/tvm/cell/dict.go @@ -242,8 +242,8 @@ func (d *Dictionary) Set(key, value *Cell) error { if err != nil { return fmt.Errorf("failed to set value in dict, err: %w", err) } - d.root = newRoot + d.root = newRoot return nil } @@ -314,13 +314,15 @@ func (d *Dictionary) IsEmpty() bool { // Deprecated: use LoadAll, dict was reimplemented, so it will be parsed during this call, and it can return error now. func (d *Dictionary) All() []*HashmapKV { list, _ := d.LoadAll() - var old []*HashmapKV - for _, kv := range list { - old = append(old, &HashmapKV{ - Key: kv.Key.MustToCell(), - Value: kv.Value.MustToCell(), - }) + + old := make([]*HashmapKV, len(list)) + for i := 0; i < len(list); i++ { + old[i] = &HashmapKV{ + Key: list[i].Key.MustToCell(), + Value: list[i].Value.MustToCell(), + } } + return old } @@ -474,10 +476,10 @@ func loadLabel(sz uint, loader *Slice, key *Builder) (uint, *Builder, error) { return 0, nil, err } + bitsLen := uint(math.Ceil(math.Log2(float64(sz + 1)))) + // hml_long$10 if second == 0 { - bitsLen := uint(math.Ceil(math.Log2(float64(sz + 1)))) - ln, err := loader.LoadUInt(bitsLen) if err != nil { return 0, nil, err @@ -503,8 +505,6 @@ func loadLabel(sz uint, loader *Slice, key *Builder) (uint, *Builder, error) { return 0, nil, err } - bitsLen := uint(math.Ceil(math.Log2(float64(sz + 1)))) - ln, err := loader.LoadUInt(bitsLen) if err != nil { return 0, nil, err