From 38a4635e140e03fc002565286681ebde452cc02a Mon Sep 17 00:00:00 2001 From: Oleg Baranov Date: Wed, 1 Jun 2022 10:32:41 +0300 Subject: [PATCH] Added wallet functionality & fixes --- README.md | 30 +- example/account-state/main.go | 2 +- example/detailed/{contract.go => main.go} | 0 example/simple/{simple.go => main.go} | 0 example/wallet/main.go | 64 + liteclient/connection.go | 46 +- liteclient/tlb/account.go | 4 +- liteclient/tlb/depth-balance-info.go | 4 +- liteclient/tlb/grams.go | 41 +- liteclient/tlb/message.go | 219 ++- liteclient/tlb/state-init.go | 30 + liteclient/tlb/transaction.go | 9 +- ton/api.go | 2 +- ton/sendmessage.go | 47 +- ton/wallet/address.go | 60 + ton/wallet/address_test.go | 19 + ton/wallet/seed.go | 2153 +++++++++++++++++++++ ton/wallet/seed_test.go | 35 + ton/wallet/wallet.go | 146 ++ tvm/cell/builder.go | 58 +- tvm/cell/cell.go | 24 + tvm/cell/serialize.go | 2 +- 22 files changed, 2876 insertions(+), 119 deletions(-) rename example/detailed/{contract.go => main.go} (100%) rename example/simple/{simple.go => main.go} (100%) create mode 100644 example/wallet/main.go create mode 100644 ton/wallet/address.go create mode 100644 ton/wallet/address_test.go create mode 100644 ton/wallet/seed.go create mode 100644 ton/wallet/seed_test.go create mode 100644 ton/wallet/wallet.go diff --git a/README.md b/README.md index 2dd8bf9c..f437199c 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,34 @@ if err != nil { // initialize ton api lite connection wrapper api := ton.NewAPIClient(client) ``` +### Wallet +You can use existing wallet or generate new one using `wallet.NewSeed()`, wallet will be initialized on first sent message from it. This library will deploy and initialize wallet contract if it is not initialized yet. + +You can also send any message to any contract using `w.Send` method, it accepts `tlb.InternalMessage` structure, you can dive into `w.Transfer` implementation and see how it works. + +Example of basic usage: +```golang +words := strings.Split("birth pattern ...", " ") + +w, err := wallet.FromSeed(api, words, wallet.V3) +if err != nil { + panic(err) +} + +balance, err := w.GetBalance(context.Background(), block) +if err != nil { + panic(err) +} + +if balance.NanoTON().Uint64() >= 3000000 { + addr := address.MustParseAddr("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N") + err = w.Transfer(context.Background(), addr, new(tlb.Grams).MustFromTON("0.003"), "Hey bro, happy birthday!") + if err != nil { + panic(err) + } +} +``` +You can find full working example at `example/wallet/main.go` ### Interacting with contracts Here are the description of features which allow us to trigger contract's methods @@ -193,7 +221,7 @@ client.SetOnDisconnect(func(addr, serverKey string) { * ✅ Send external message * ✅ Get transactions * Deploy contracts -* Wallet operations +* ✅ Wallet operations * Payment processing * ✅ Cell dictionaries support * MustLoad methods diff --git a/example/account-state/main.go b/example/account-state/main.go index dcd78f91..5d97dbff 100644 --- a/example/account-state/main.go +++ b/example/account-state/main.go @@ -30,7 +30,7 @@ func main() { return } - addr := address.MustParseAddr("EQAoUyP1KBBRvTVAUxlAI_9mmSH05guWrNZ5PfmVFL7zs2b6") + addr := address.MustParseAddr("EQDEGeK4o7bNgazTln27r0RC4YcOmerzIni3gUpsyqxfgMWk") res, err := api.GetAccount(context.Background(), b, addr) if err != nil { diff --git a/example/detailed/contract.go b/example/detailed/main.go similarity index 100% rename from example/detailed/contract.go rename to example/detailed/main.go diff --git a/example/simple/simple.go b/example/simple/main.go similarity index 100% rename from example/simple/simple.go rename to example/simple/main.go diff --git a/example/wallet/main.go b/example/wallet/main.go new file mode 100644 index 00000000..f95b9685 --- /dev/null +++ b/example/wallet/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "log" + "strings" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/liteclient" + "github.com/xssnick/tonutils-go/liteclient/tlb" + "github.com/xssnick/tonutils-go/ton" + "github.com/xssnick/tonutils-go/ton/wallet" +) + +func main() { + client := liteclient.NewClient() + + // connect to mainnet lite server + err := client.Connect(context.Background(), "135.181.140.212:13206", "K0t3+IWLOXHYMvMcrGZDPs+pn58a17LFbnXoQkKc2xw=") + if err != nil { + log.Fatalln("connection err: ", err.Error()) + return + } + + api := ton.NewAPIClient(client) + + // seed words of account, you can generate them with any wallet or using wallet.NewSeed() method + 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) + if err != nil { + log.Fatalln("FromPrivateKey err:", err.Error()) + return + } + + log.Println("wallet address:", w.Address()) + + block, err := api.GetBlockInfo(context.Background()) + if err != nil { + log.Fatalln("get block err:", err.Error()) + return + } + + balance, err := w.GetBalance(context.Background(), block) + if err != nil { + log.Fatalln("GetBalance err:", err.Error()) + return + } + + if balance.NanoTON().Uint64() >= 3000000 { + addr := address.MustParseAddr("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N") + err = w.Transfer(context.Background(), addr, new(tlb.Grams).MustFromTON("0.003"), "Hey bro, happy birthday!") + if err != nil { + log.Fatalln("Transfer err:", err.Error()) + return + } + + log.Println("transaction sent, balance left:", balance.TON()) + + return + } + + log.Println("not enough balance:", balance.TON()) +} diff --git a/liteclient/connection.go b/liteclient/connection.go index e17ce6ec..774632b2 100644 --- a/liteclient/connection.go +++ b/liteclient/connection.go @@ -383,19 +383,7 @@ func (cn *connection) queryADNL(qid, payload []byte) error { binary.LittleEndian.PutUint32(data, uint32(t)) data = append(data, qid...) - if len(payload) >= 0xFE { - ln := make([]byte, 4) - binary.LittleEndian.PutUint32(data, uint32(len(payload)<<8)|0xFE) - data = append(data, ln...) - } else { - data = append(data, byte(len(payload))) - } - data = append(data, payload...) - - left := len(data) % 4 - if left != 0 { - data = append(data, make([]byte, 4-left)...) - } + data = append(data, storableBytes(payload)...) return cn.send(data) } @@ -404,26 +392,34 @@ func (cn *connection) queryLiteServer(qid []byte, typeID int32, payload []byte) data := make([]byte, 4) binary.LittleEndian.PutUint32(data, uint32(LiteServerQuery)) - if len(payload) >= 0xFE { + typData := make([]byte, 4) + binary.LittleEndian.PutUint32(typData, uint32(typeID)) + + data = append(data, storableBytes(append(typData, payload...))...) + + return cn.queryADNL(qid, data) +} + +func storableBytes(buf []byte) []byte { + var data []byte + + // store buf length + if len(buf) >= 0xFE { ln := make([]byte, 4) - binary.LittleEndian.PutUint32(data, uint32((len(payload)+4)<<8)|0xFE) + binary.LittleEndian.PutUint32(ln, uint32(len(buf)<<8)|0xFE) data = append(data, ln...) } else { - data = append(data, byte(len(payload)+4)) + data = append(data, byte(len(buf))) } - typData := make([]byte, 4) - binary.LittleEndian.PutUint32(typData, uint32(typeID)) + data = append(data, buf...) - data = append(data, typData...) - data = append(data, payload...) - - left := len(data) % 4 - if left != 0 { - data = append(data, make([]byte, 4-left)...) + // adjust actual length to fit % 4 = 0 + if round := len(data) % 4; round != 0 { + data = append(data, make([]byte, 4-round)...) } - return cn.queryADNL(qid, data) + return data } func (c *Client) DefaultReconnect(waitBeforeReconnect time.Duration, maxTries int) OnDisconnectCallback { diff --git a/liteclient/tlb/account.go b/liteclient/tlb/account.go index 2be807a8..cf5f2c62 100644 --- a/liteclient/tlb/account.go +++ b/liteclient/tlb/account.go @@ -19,7 +19,7 @@ const ( type AccountStorage struct { Status AccountStatus LastTransactionLT uint64 - Balance Grams + Balance *Grams } type StorageUsed struct { @@ -175,7 +175,7 @@ func (s *AccountStorage) LoadFromCell(loader *cell.LoadCell) error { } s.LastTransactionLT = lastTransaction - s.Balance = Grams{coins} + s.Balance = new(Grams).FromNanoTON(coins) return nil } diff --git a/liteclient/tlb/depth-balance-info.go b/liteclient/tlb/depth-balance-info.go index a79b32af..ea0a9c91 100644 --- a/liteclient/tlb/depth-balance-info.go +++ b/liteclient/tlb/depth-balance-info.go @@ -6,7 +6,7 @@ import ( type DepthBalanceInfo struct { Depth uint32 - Coins Grams + Coins *Grams } func (d *DepthBalanceInfo) LoadFromCell(loader *cell.LoadCell) error { @@ -35,7 +35,7 @@ func (d *DepthBalanceInfo) LoadFromCell(loader *cell.LoadCell) error { } d.Depth = uint32(depth) - d.Coins = Grams{val: grams} + d.Coins = new(Grams).FromNanoTON(grams) return nil } diff --git a/liteclient/tlb/grams.go b/liteclient/tlb/grams.go index 299e4e61..e4e7e7cd 100644 --- a/liteclient/tlb/grams.go +++ b/liteclient/tlb/grams.go @@ -1,20 +1,51 @@ package tlb import ( + "errors" + "fmt" "math/big" ) -type Grams struct { - val *big.Int -} +type Grams big.Int func (g Grams) TON() string { - f := new(big.Float).SetInt(g.val) + f := new(big.Float).SetInt((*big.Int)(&g)) t := new(big.Float).Quo(f, new(big.Float).SetUint64(1000000000)) return t.String() } func (g Grams) NanoTON() *big.Int { - return g.val + return (*big.Int)(&g) +} + +func (g *Grams) FromNanoTON(val *big.Int) *Grams { + *g = Grams(*val) + return g +} + +func (g *Grams) MustFromTON(val string) *Grams { + v, err := g.FromTON(val) + if err != nil { + panic(err) + return nil + } + return v +} + +func (g *Grams) FromTON(val string) (*Grams, error) { + f, ok := new(big.Float).SetString(val) + if !ok { + return nil, errors.New("invalid string") + } + + f = f.Mul(f, new(big.Float).SetUint64(1000000000)) + i, _ := f.Int(new(big.Int)) + + *g = Grams(*i) + return g, nil +} + +func (g *Grams) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf("%q", g.TON())), nil } diff --git a/liteclient/tlb/message.go b/liteclient/tlb/message.go index 4b08635e..0b69fe23 100644 --- a/liteclient/tlb/message.go +++ b/liteclient/tlb/message.go @@ -16,9 +16,15 @@ const ( MsgTypeExternalOut MsgType = "EXTERNAL_OUT" ) +type AnyMessage interface { + Payload() *cell.Cell + SenderAddr() *address.Address + DestAddr() *address.Address +} + type Message struct { MsgType MsgType - msg interface{} + Msg AnyMessage } type InternalMessage struct { @@ -27,10 +33,10 @@ type InternalMessage struct { Bounced bool SrcAddr *address.Address DstAddr *address.Address - Amount Grams + Amount *Grams ExtraCurrencies *cell.Dictionary - IHRFee Grams - FwdFee Grams + IHRFee *Grams + FwdFee *Grams CreatedLT uint64 CreatedAt uint32 @@ -38,18 +44,55 @@ type InternalMessage struct { Body *cell.Cell } +func (m *InternalMessage) Payload() *cell.Cell { + return m.Body +} + +func (m *InternalMessage) SenderAddr() *address.Address { + return m.SrcAddr +} + +func (m *InternalMessage) DestAddr() *address.Address { + return m.DstAddr +} + +func (m *InternalMessage) Comment() string { + if m.Body != nil { + l := m.Body.BeginParse() + if val, err := l.LoadUInt(32); err == nil && val == 0 { + b, err := l.LoadSlice(l.BitsLeft()) + if err == nil { + return string(b) + } + } + } + return "" +} + type ExternalMessageIn struct { SrcAddr *address.Address - DestAddr *address.Address - ImportFee Grams + DstAddr *address.Address + ImportFee *Grams StateInit *StateInit Body *cell.Cell } +func (m *ExternalMessageIn) Payload() *cell.Cell { + return m.Body +} + +func (m *ExternalMessageIn) SenderAddr() *address.Address { + return m.SrcAddr +} + +func (m *ExternalMessageIn) DestAddr() *address.Address { + return m.DstAddr +} + type ExternalMessageOut struct { SrcAddr *address.Address - DestAddr *address.Address + DstAddr *address.Address CreatedLT uint64 CreatedAt uint32 @@ -57,8 +100,22 @@ type ExternalMessageOut struct { Body *cell.Cell } +func (m *ExternalMessageOut) Payload() *cell.Cell { + return m.Body +} + +func (m *ExternalMessageOut) SenderAddr() *address.Address { + return m.SrcAddr +} + +func (m *ExternalMessageOut) DestAddr() *address.Address { + return m.DstAddr +} + func (m *Message) LoadFromCell(loader *cell.LoadCell) error { - isExternal, err := loader.LoadBoolBit() + dup := loader.Copy() + + isExternal, err := dup.LoadBoolBit() if err != nil { return fmt.Errorf("failed to load external flag: %w", err) } @@ -71,11 +128,11 @@ func (m *Message) LoadFromCell(loader *cell.LoadCell) error { return fmt.Errorf("failed to parse internal message: %w", err) } - m.msg = &intMsg + m.Msg = &intMsg m.MsgType = MsgTypeInternal return nil case true: - isOut, err := loader.LoadBoolBit() + isOut, err := dup.LoadBoolBit() if err != nil { return fmt.Errorf("failed to load external in/out flag: %w", err) } @@ -88,7 +145,7 @@ func (m *Message) LoadFromCell(loader *cell.LoadCell) error { return fmt.Errorf("failed to parse external out message: %w", err) } - m.msg = &extMsg + m.Msg = &extMsg m.MsgType = MsgTypeExternalOut return nil case false: @@ -98,7 +155,7 @@ func (m *Message) LoadFromCell(loader *cell.LoadCell) error { return fmt.Errorf("failed to parse external in message: %w", err) } - m.msg = &extMsg + m.Msg = &extMsg m.MsgType = MsgTypeExternalIn return nil } @@ -107,55 +164,28 @@ func (m *Message) LoadFromCell(loader *cell.LoadCell) error { return errors.New("unknown message type") } -func (m *Message) SenderAddr() *address.Address { - switch m.MsgType { - case MsgTypeInternal: - return m.AsInternal().SrcAddr - case MsgTypeExternalIn: - return m.AsExternalIn().SrcAddr - case MsgTypeExternalOut: - return m.AsExternalOut().SrcAddr - } - return nil -} - -func (m *Message) Payload() *cell.Cell { - switch m.MsgType { - case MsgTypeInternal: - return m.AsInternal().Body - case MsgTypeExternalIn: - return m.AsExternalIn().Body - case MsgTypeExternalOut: - return m.AsExternalOut().Body - } - return nil -} - -func (m *Message) DestAddr() *address.Address { - switch m.MsgType { - case MsgTypeInternal: - return m.AsInternal().DstAddr - case MsgTypeExternalIn: - return m.AsExternalIn().DestAddr - case MsgTypeExternalOut: - return m.AsExternalOut().DestAddr - } - return nil -} - func (m *Message) AsInternal() *InternalMessage { - return m.msg.(*InternalMessage) + return m.Msg.(*InternalMessage) } func (m *Message) AsExternalIn() *ExternalMessageIn { - return m.msg.(*ExternalMessageIn) + return m.Msg.(*ExternalMessageIn) } func (m *Message) AsExternalOut() *ExternalMessageOut { - return m.msg.(*ExternalMessageOut) + return m.Msg.(*ExternalMessageOut) } func (m *InternalMessage) LoadFromCell(loader *cell.LoadCell) error { + ident, err := loader.LoadUInt(1) + if err != nil { + return fmt.Errorf("failed to load identificator bit: %w", err) + } + + if ident != 0 { + return fmt.Errorf("its not internal message") + } + ihrDisabled, err := loader.LoadBoolBit() if err != nil { return fmt.Errorf("failed to load ihr disabled bit: %w", err) @@ -235,10 +265,10 @@ func (m *InternalMessage) LoadFromCell(loader *cell.LoadCell) error { Bounced: bounced, SrcAddr: srcAddr, DstAddr: dstAddr, - Amount: Grams{val: value}, + Amount: new(Grams).FromNanoTON(value), ExtraCurrencies: extra, - IHRFee: Grams{val: ihrFee}, - FwdFee: Grams{val: fwdFee}, + IHRFee: new(Grams).FromNanoTON(ihrFee), + FwdFee: new(Grams).FromNanoTON(fwdFee), CreatedLT: createdLt, CreatedAt: uint32(createdAt), StateInit: init, @@ -248,12 +278,78 @@ func (m *InternalMessage) LoadFromCell(loader *cell.LoadCell) error { return nil } +func (m *InternalMessage) ToCell() (*cell.Cell, error) { + b := cell.BeginCell() + b.MustStoreUInt(0, 1) // identification of int msg + b.MustStoreBoolBit(m.IHRDisabled) + b.MustStoreBoolBit(m.Bounce) + b.MustStoreBoolBit(m.Bounced) + b.MustStoreAddr(m.SrcAddr) + b.MustStoreAddr(m.DstAddr) + if m.Amount != nil { + b.MustStoreBigCoins(m.Amount.NanoTON()) + } else { + b.MustStoreCoins(0) + } + + if m.ExtraCurrencies != nil { + return nil, errors.New("extra currencies serialization is not supported yet") + } + + if m.StateInit != nil { + return nil, errors.New("state init serialization is not supported yet") + } + + b.MustStoreBoolBit(m.ExtraCurrencies != nil) + + if m.IHRFee != nil { + b.MustStoreBigCoins(m.IHRFee.NanoTON()) + } else { + b.MustStoreCoins(0) + } + + if m.FwdFee != nil { + b.MustStoreBigCoins(m.FwdFee.NanoTON()) + } else { + b.MustStoreCoins(0) + } + + b.MustStoreUInt(m.CreatedLT, 64) + b.MustStoreUInt(uint64(m.CreatedAt), 32) + b.MustStoreBoolBit(m.StateInit != nil) + + if m.Body != nil { + if b.BitsLeft() < m.Body.BitsSize() { + b.MustStoreBoolBit(true) + b.MustStoreRef(m.Body) + } else { + b.MustStoreBoolBit(false) + b.MustStoreBuilder(m.Body.ToBuilder()) + } + } else { + // store 1 zero bit as body + b.MustStoreBoolBit(false) + b.MustStoreUInt(0, 1) + } + + return b.EndCell(), nil +} + func (m *InternalMessage) Dump() string { return fmt.Sprintf("Amount %s TON, Created at: %d, Created lt %d\nBounce: %t, Bounced %t, IHRDisabled %t\nSrcAddr: %s\nDstAddr: %s\nPayload: %s", m.Amount.TON(), m.CreatedAt, m.CreatedLT, m.Bounce, m.Bounced, m.IHRDisabled, m.SrcAddr, m.DstAddr, m.Body.Dump()) } func (m *ExternalMessageIn) LoadFromCell(loader *cell.LoadCell) error { + ident, err := loader.LoadUInt(2) + if err != nil { + return fmt.Errorf("failed to load identificator bit: %w", err) + } + + if ident != 2 { + return fmt.Errorf("its not external in message") + } + srcAddr, err := loader.LoadAddr() if err != nil { return fmt.Errorf("failed to load src addr: %w", err) @@ -276,8 +372,8 @@ func (m *ExternalMessageIn) LoadFromCell(loader *cell.LoadCell) error { *m = ExternalMessageIn{ SrcAddr: srcAddr, - DestAddr: dstAddr, - ImportFee: Grams{ihrFee}, + DstAddr: dstAddr, + ImportFee: new(Grams).FromNanoTON(ihrFee), StateInit: init, Body: body, } @@ -285,6 +381,15 @@ func (m *ExternalMessageIn) LoadFromCell(loader *cell.LoadCell) error { } func (m *ExternalMessageOut) LoadFromCell(loader *cell.LoadCell) error { + ident, err := loader.LoadUInt(2) + if err != nil { + return fmt.Errorf("failed to load identificator bit: %w", err) + } + + if ident != 3 { + return fmt.Errorf("its not external out message") + } + srcAddr, err := loader.LoadAddr() if err != nil { return fmt.Errorf("failed to load src addr: %w", err) @@ -312,7 +417,7 @@ func (m *ExternalMessageOut) LoadFromCell(loader *cell.LoadCell) error { *m = ExternalMessageOut{ SrcAddr: srcAddr, - DestAddr: dstAddr, + DstAddr: dstAddr, CreatedLT: createdLt, CreatedAt: uint32(createdAt), StateInit: init, diff --git a/liteclient/tlb/state-init.go b/liteclient/tlb/state-init.go index 3540fb6c..3529187b 100644 --- a/liteclient/tlb/state-init.go +++ b/liteclient/tlb/state-init.go @@ -1,6 +1,7 @@ package tlb import ( + "errors" "fmt" "github.com/xssnick/tonutils-go/tvm/cell" @@ -122,3 +123,32 @@ func (m *StateInit) LoadFromCell(loader *cell.LoadCell) error { } return nil } + +func (m *StateInit) ToCell() (*cell.Cell, error) { + var flags byte + state := cell.BeginCell() + + if m.Lib != nil { + return nil, errors.New("lib serialization is currently not supported") + } + + if m.Depth != nil { + return nil, errors.New("depth serialization is currently not supported") + } + + if m.TickTock != nil { + return nil, errors.New("ticktock serialization is currently not supported") + } + + if m.Code != nil { + flags |= 1 << 2 + state.MustStoreRef(m.Code) + } + + if m.Data != nil { + flags |= 1 << 1 + state.MustStoreRef(m.Data) + } + + return state.MustStoreUInt(uint64(flags), 5).EndCell(), nil +} diff --git a/liteclient/tlb/transaction.go b/liteclient/tlb/transaction.go index 3faf689d..7080351b 100644 --- a/liteclient/tlb/transaction.go +++ b/liteclient/tlb/transaction.go @@ -131,7 +131,7 @@ func (t *Transaction) LoadFromCell(loader *cell.LoadCell) error { } func (t *Transaction) Dump() string { - res := fmt.Sprintf("LT: %d\n\nInput:\nType %s\nFrom %s\nPayload:\n%s\n\nOutputs:\n", t.LT, t.In.MsgType, t.In.SenderAddr(), t.In.Payload().Dump()) + res := fmt.Sprintf("LT: %d\n\nInput:\nType %s\nFrom %s\nPayload:\n%s\n\nOutputs:\n", t.LT, t.In.MsgType, t.In.Msg.SenderAddr(), t.In.Msg.Payload().Dump()) for _, m := range t.Out { res += m.AsInternal().Dump() } @@ -142,7 +142,7 @@ func (t *Transaction) String() string { var destinations []string in, out := new(big.Int), new(big.Int) for _, m := range t.Out { - destinations = append(destinations, m.DestAddr().String()) + destinations = append(destinations, m.Msg.DestAddr().String()) if m.MsgType == MsgTypeInternal { out.Add(out, m.AsInternal().Amount.NanoTON()) } @@ -155,14 +155,15 @@ func (t *Transaction) String() string { var build string if in.Cmp(big.NewInt(0)) != 0 { - build += fmt.Sprintf("In: %s TON, From %s", Grams{in}.TON(), t.In.AsInternal().SrcAddr) + intTx := t.In.AsInternal() + build += fmt.Sprintf("In: %s TON, From %s, Comment: %s", new(Grams).FromNanoTON(in).TON(), intTx.SrcAddr, intTx.Comment()) } if out.Cmp(big.NewInt(0)) != 0 { if len(build) > 0 { build += ", " } - build += fmt.Sprintf("Out: %s TON, To %s", Grams{out}.TON(), destinations) + build += fmt.Sprintf("Out: %s TON, To %s", new(Grams).FromNanoTON(out).TON(), destinations) } return build diff --git a/ton/api.go b/ton/api.go index 3d311cc8..3054955b 100644 --- a/ton/api.go +++ b/ton/api.go @@ -76,7 +76,7 @@ func storableBytes(buf []byte) []byte { data = append(data, buf...) // adjust actual length to fit % 4 = 0 - if round := (len(buf) + 1) % 4; round != 0 { + if round := len(data) % 4; round != 0 { data = append(data, make([]byte, 4-round)...) } diff --git a/ton/sendmessage.go b/ton/sendmessage.go index dc023166..36bcc477 100644 --- a/ton/sendmessage.go +++ b/ton/sendmessage.go @@ -7,30 +7,44 @@ import ( "fmt" "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/liteclient/tlb" "github.com/xssnick/tonutils-go/tvm/cell" ) var ErrMessageNotAccepted = errors.New("message was not accepted by the contract") func (c *APIClient) SendExternalMessage(ctx context.Context, addr *address.Address, msg *cell.Cell) error { - return c.sendExternalMessage(ctx, addr, msg) + return c.SendExternalInitMessage(ctx, addr, msg, nil) } -func (c *APIClient) sendExternalMessage(ctx context.Context, addr *address.Address, msg any) error { +func (c *APIClient) SendExternalInitMessage(ctx context.Context, addr *address.Address, msg *cell.Cell, state *tlb.StateInit) error { builder := cell.BeginCell().MustStoreUInt(0b10, 2). MustStoreUInt(0b00, 2). // src addr_none - MustStoreAddr(addr). // dst addr - MustStoreCoins(0) // import fee 0 - - builder.MustStoreUInt(0, 1) // no state init - - switch d := msg.(type) { - case []byte: // slice data - builder.MustStoreUInt(0, 1).MustStoreSlice(d, len(d)*8) - case *cell.Cell: // cell data - builder.MustStoreUInt(1, 1).MustStoreRef(d) - default: - return errors.New("unknown arg type") + MustStoreAddr(addr). // dst addr + MustStoreCoins(0) // import fee 0 + + builder.MustStoreBoolBit(state != nil) // has state init + if state != nil { + stateCell, err := state.ToCell() + if err != nil { + return err + } + + if builder.BitsLeft()-2 < stateCell.BitsSize() || builder.RefsLeft()-2 < msg.RefsNum() { + builder.MustStoreBoolBit(true) // state as ref + builder.MustStoreRef(stateCell) + } else { + builder.MustStoreBoolBit(false) // state as slice + builder.MustStoreBuilder(stateCell.ToBuilder()) + } + } + + if builder.BitsLeft() < msg.BitsSize() || builder.RefsLeft() < msg.RefsNum() { + builder.MustStoreBoolBit(true) // body as ref + builder.MustStoreRef(msg) + } else { + builder.MustStoreBoolBit(false) // state as slice + builder.MustStoreBuilder(msg.ToBuilder()) } req := builder.EndCell().ToBOCWithFlags(false) @@ -42,7 +56,6 @@ func (c *APIClient) sendExternalMessage(ctx context.Context, addr *address.Addre switch resp.TypeID { case _SendMessageResult: - // TODO: mode status := binary.LittleEndian.Uint32(resp.Data) if status != 1 { @@ -52,10 +65,6 @@ func (c *APIClient) sendExternalMessage(ctx context.Context, addr *address.Addre return nil case _LSError: code := binary.LittleEndian.Uint32(resp.Data) - if code == 0 { - return ErrMessageNotAccepted - } - return fmt.Errorf("lite server error, code %d: %s", code, string(resp.Data[5:])) } diff --git a/ton/wallet/address.go b/ton/wallet/address.go new file mode 100644 index 00000000..ba2d6e66 --- /dev/null +++ b/ton/wallet/address.go @@ -0,0 +1,60 @@ +package wallet + +import ( + "crypto/ed25519" + "encoding/hex" + "errors" + "fmt" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/liteclient/tlb" + "github.com/xssnick/tonutils-go/tvm/cell" +) + +const _SubWalletV3 = 698983191 + +func AddressFromPubKey(key ed25519.PublicKey, ver Version) (*address.Address, error) { + state, err := GetStateInit(key, ver) + if err != nil { + return nil, fmt.Errorf("failed to get state: %w", err) + } + + stateCell, err := state.ToCell() + if err != nil { + return nil, fmt.Errorf("failed to get state cell: %w", err) + } + + addr := address.NewAddress(0, 0, stateCell.Hash()) + + return addr, nil +} + +func GetStateInit(pubKey ed25519.PublicKey, ver Version) (*tlb.StateInit, error) { + var code, data *cell.Cell + + switch ver { + case V3: + v3boc, err := hex.DecodeString("B5EE9C724101010100710000DEFF0020DD2082014C97BA218201339CBAB19F71B0ED44D0D31FD31F31D70BFFE304E0A4F2608308D71820D31FD31FD31FF82313BBF263ED44D0D31FD31FD3FFD15132BAF2A15144BAF2A204F901541055F910F2A3F8009320D74A96D307D402FB00E8D101A4C8CB1FCB1FCBFFC9ED5410BD6DAD") + if err != nil { + return nil, fmt.Errorf("failed to decode hex of wallet boc: %w", err) + } + + code, err = cell.FromBOC(v3boc) + if err != nil { + return nil, fmt.Errorf("failed to decode wallet code boc: %w", err) + } + + data = cell.BeginCell(). + MustStoreUInt(0, 32). // seqno + MustStoreUInt(_SubWalletV3, 32). // sub wallet, hardcoded everywhere + MustStoreSlice(pubKey, 256). + EndCell() + default: + return nil, errors.New("wallet version is not supported") + } + + return &tlb.StateInit{ + Data: data, + Code: code, + }, nil +} diff --git a/ton/wallet/address_test.go b/ton/wallet/address_test.go new file mode 100644 index 00000000..610dc6ec --- /dev/null +++ b/ton/wallet/address_test.go @@ -0,0 +1,19 @@ +package wallet + +import ( + "encoding/hex" + "testing" +) + +func TestAddressFromPubKey(t *testing.T) { + pkey, _ := hex.DecodeString("dcc39550bb494f4b493e7efe1aa18ea31470f33a2553c568cb74a17ed56790c1") + + a, err := AddressFromPubKey(pkey, V3) + if err != nil { + t.Fatal(err) + } + + if a.String() != "EQCvoBT5Keb46oUhI_DpX0WXFDdX9ZyxXBfX3FC9cZa90nQP" { + t.Fatal("v3 not match") + } +} diff --git a/ton/wallet/seed.go b/ton/wallet/seed.go new file mode 100644 index 00000000..15d6d7b8 --- /dev/null +++ b/ton/wallet/seed.go @@ -0,0 +1,2153 @@ +package wallet + +import ( + "crypto/ed25519" + "crypto/hmac" + "crypto/rand" + "crypto/sha512" + "errors" + "fmt" + "math/big" + "strings" + + "golang.org/x/crypto/pbkdf2" +) + +const ( + _Iterations = 100000 + _Salt = "TON default seed" + _BasicSalt = "TON seed version" + _PasswordSalt = "TON fast seed version" +) + +func NewSeed() []string { + return NewSeedWithPassword("") +} + +func NewSeedWithPassword(password string) []string { + wordsArr := make([]string, 0, len(words)) + + for w := range words { + wordsArr = append(wordsArr, w) + } + + for { + seed := make([]string, 24) + for i := 0; i < 24; i++ { + for { + x, err := rand.Int(rand.Reader, big.NewInt(int64(len(seed)))) + if err != nil { + continue + } + + seed[i] = wordsArr[x.Uint64()] + break + } + } + + mac := hmac.New(sha512.New, []byte(strings.Join(seed, " "))) + mac.Write([]byte(password)) + hash := mac.Sum(nil) + + if len(password) > 0 { + p := pbkdf2.Key(hash, []byte(_PasswordSalt), 1, 1, sha512.New) + if p[0] != 1 { + continue + } + } else { + p := pbkdf2.Key(hash, []byte(_BasicSalt), _Iterations/256, 1, sha512.New) + if p[0] != 0 { + continue + } + } + + return seed + } +} + +func FromSeed(api TonAPI, seed []string, version Version) (*Wallet, error) { + return FromSeedWithPassword(api, seed, "", version) +} + +func FromSeedWithPassword(api TonAPI, seed []string, password string, version Version) (*Wallet, error) { + // validate seed + if len(seed) < 12 { + return nil, fmt.Errorf("seed should have at least 12 words") + } + for _, s := range seed { + if !words[s] { + return nil, fmt.Errorf("unknown word '%s' in seed", s) + } + } + + mac := hmac.New(sha512.New, []byte(strings.Join(seed, " "))) + mac.Write([]byte(password)) + hash := mac.Sum(nil) + + if len(password) > 0 { + p := pbkdf2.Key(hash, []byte(_PasswordSalt), 1, 1, sha512.New) + if p[0] != 1 { + return nil, errors.New("invalid seed") + } + } else { + p := pbkdf2.Key(hash, []byte(_BasicSalt), _Iterations/256, 1, sha512.New) + if p[0] != 0 { + return nil, errors.New("invalid seed") + } + } + + k := pbkdf2.Key(hash, []byte(_Salt), _Iterations, 32, sha512.New) + + return FromPrivateKey(api, ed25519.NewKeyFromSeed(k), version) +} + +var words = map[string]bool{ + "abandon": true, + "ability": true, + "able": true, + "about": true, + "above": true, + "absent": true, + "absorb": true, + "abstract": true, + "absurd": true, + "abuse": true, + "access": true, + "accident": true, + "account": true, + "accuse": true, + "achieve": true, + "acid": true, + "acoustic": true, + "acquire": true, + "across": true, + "act": true, + "action": true, + "actor": true, + "actress": true, + "actual": true, + "adapt": true, + "add": true, + "addict": true, + "address": true, + "adjust": true, + "admit": true, + "adult": true, + "advance": true, + "advice": true, + "aerobic": true, + "affair": true, + "afford": true, + "afraid": true, + "again": true, + "age": true, + "agent": true, + "agree": true, + "ahead": true, + "aim": true, + "air": true, + "airport": true, + "aisle": true, + "alarm": true, + "album": true, + "alcohol": true, + "alert": true, + "alien": true, + "all": true, + "alley": true, + "allow": true, + "almost": true, + "alone": true, + "alpha": true, + "already": true, + "also": true, + "alter": true, + "always": true, + "amateur": true, + "amazing": true, + "among": true, + "amount": true, + "amused": true, + "analyst": true, + "anchor": true, + "ancient": true, + "anger": true, + "angle": true, + "angry": true, + "animal": true, + "ankle": true, + "announce": true, + "annual": true, + "another": true, + "answer": true, + "antenna": true, + "antique": true, + "anxiety": true, + "any": true, + "apart": true, + "apology": true, + "appear": true, + "apple": true, + "approve": true, + "april": true, + "arch": true, + "arctic": true, + "area": true, + "arena": true, + "argue": true, + "arm": true, + "armed": true, + "armor": true, + "army": true, + "around": true, + "arrange": true, + "arrest": true, + "arrive": true, + "arrow": true, + "art": true, + "artefact": true, + "artist": true, + "artwork": true, + "ask": true, + "aspect": true, + "assault": true, + "asset": true, + "assist": true, + "assume": true, + "asthma": true, + "athlete": true, + "atom": true, + "attack": true, + "attend": true, + "attitude": true, + "attract": true, + "auction": true, + "audit": true, + "august": true, + "aunt": true, + "author": true, + "auto": true, + "autumn": true, + "average": true, + "avocado": true, + "avoid": true, + "awake": true, + "aware": true, + "away": true, + "awesome": true, + "awful": true, + "awkward": true, + "axis": true, + "baby": true, + "bachelor": true, + "bacon": true, + "badge": true, + "bag": true, + "balance": true, + "balcony": true, + "ball": true, + "bamboo": true, + "banana": true, + "banner": true, + "bar": true, + "barely": true, + "bargain": true, + "barrel": true, + "base": true, + "basic": true, + "basket": true, + "battle": true, + "beach": true, + "bean": true, + "beauty": true, + "because": true, + "become": true, + "beef": true, + "before": true, + "begin": true, + "behave": true, + "behind": true, + "believe": true, + "below": true, + "belt": true, + "bench": true, + "benefit": true, + "best": true, + "betray": true, + "better": true, + "between": true, + "beyond": true, + "bicycle": true, + "bid": true, + "bike": true, + "bind": true, + "biology": true, + "bird": true, + "birth": true, + "bitter": true, + "black": true, + "blade": true, + "blame": true, + "blanket": true, + "blast": true, + "bleak": true, + "bless": true, + "blind": true, + "blood": true, + "blossom": true, + "blouse": true, + "blue": true, + "blur": true, + "blush": true, + "board": true, + "boat": true, + "body": true, + "boil": true, + "bomb": true, + "bone": true, + "bonus": true, + "book": true, + "boost": true, + "border": true, + "boring": true, + "borrow": true, + "boss": true, + "bottom": true, + "bounce": true, + "box": true, + "boy": true, + "bracket": true, + "brain": true, + "brand": true, + "brass": true, + "brave": true, + "bread": true, + "breeze": true, + "brick": true, + "bridge": true, + "brief": true, + "bright": true, + "bring": true, + "brisk": true, + "broccoli": true, + "broken": true, + "bronze": true, + "broom": true, + "brother": true, + "brown": true, + "brush": true, + "bubble": true, + "buddy": true, + "budget": true, + "buffalo": true, + "build": true, + "bulb": true, + "bulk": true, + "bullet": true, + "bundle": true, + "bunker": true, + "burden": true, + "burger": true, + "burst": true, + "bus": true, + "business": true, + "busy": true, + "butter": true, + "buyer": true, + "buzz": true, + "cabbage": true, + "cabin": true, + "cable": true, + "cactus": true, + "cage": true, + "cake": true, + "call": true, + "calm": true, + "camera": true, + "camp": true, + "can": true, + "canal": true, + "cancel": true, + "candy": true, + "cannon": true, + "canoe": true, + "canvas": true, + "canyon": true, + "capable": true, + "capital": true, + "captain": true, + "car": true, + "carbon": true, + "card": true, + "cargo": true, + "carpet": true, + "carry": true, + "cart": true, + "case": true, + "cash": true, + "casino": true, + "castle": true, + "casual": true, + "cat": true, + "catalog": true, + "catch": true, + "category": true, + "cattle": true, + "caught": true, + "cause": true, + "caution": true, + "cave": true, + "ceiling": true, + "celery": true, + "cement": true, + "census": true, + "century": true, + "cereal": true, + "certain": true, + "chair": true, + "chalk": true, + "champion": true, + "change": true, + "chaos": true, + "chapter": true, + "charge": true, + "chase": true, + "chat": true, + "cheap": true, + "check": true, + "cheese": true, + "chef": true, + "cherry": true, + "chest": true, + "chicken": true, + "chief": true, + "child": true, + "chimney": true, + "choice": true, + "choose": true, + "chronic": true, + "chuckle": true, + "chunk": true, + "churn": true, + "cigar": true, + "cinnamon": true, + "circle": true, + "citizen": true, + "city": true, + "civil": true, + "claim": true, + "clap": true, + "clarify": true, + "claw": true, + "clay": true, + "clean": true, + "clerk": true, + "clever": true, + "click": true, + "client": true, + "cliff": true, + "climb": true, + "clinic": true, + "clip": true, + "clock": true, + "clog": true, + "close": true, + "cloth": true, + "cloud": true, + "clown": true, + "club": true, + "clump": true, + "cluster": true, + "clutch": true, + "coach": true, + "coast": true, + "coconut": true, + "code": true, + "coffee": true, + "coil": true, + "coin": true, + "collect": true, + "color": true, + "column": true, + "combine": true, + "come": true, + "comfort": true, + "comic": true, + "common": true, + "company": true, + "concert": true, + "conduct": true, + "confirm": true, + "congress": true, + "connect": true, + "consider": true, + "control": true, + "convince": true, + "cook": true, + "cool": true, + "copper": true, + "copy": true, + "coral": true, + "core": true, + "corn": true, + "correct": true, + "cost": true, + "cotton": true, + "couch": true, + "country": true, + "couple": true, + "course": true, + "cousin": true, + "cover": true, + "coyote": true, + "crack": true, + "cradle": true, + "craft": true, + "cram": true, + "crane": true, + "crash": true, + "crater": true, + "crawl": true, + "crazy": true, + "cream": true, + "credit": true, + "creek": true, + "crew": true, + "cricket": true, + "crime": true, + "crisp": true, + "critic": true, + "crop": true, + "cross": true, + "crouch": true, + "crowd": true, + "crucial": true, + "cruel": true, + "cruise": true, + "crumble": true, + "crunch": true, + "crush": true, + "cry": true, + "crystal": true, + "cube": true, + "culture": true, + "cup": true, + "cupboard": true, + "curious": true, + "current": true, + "curtain": true, + "curve": true, + "cushion": true, + "custom": true, + "cute": true, + "cycle": true, + "dad": true, + "damage": true, + "damp": true, + "dance": true, + "danger": true, + "daring": true, + "dash": true, + "daughter": true, + "dawn": true, + "day": true, + "deal": true, + "debate": true, + "debris": true, + "decade": true, + "december": true, + "decide": true, + "decline": true, + "decorate": true, + "decrease": true, + "deer": true, + "defense": true, + "define": true, + "defy": true, + "degree": true, + "delay": true, + "deliver": true, + "demand": true, + "demise": true, + "denial": true, + "dentist": true, + "deny": true, + "depart": true, + "depend": true, + "deposit": true, + "depth": true, + "deputy": true, + "derive": true, + "describe": true, + "desert": true, + "design": true, + "desk": true, + "despair": true, + "destroy": true, + "detail": true, + "detect": true, + "develop": true, + "device": true, + "devote": true, + "diagram": true, + "dial": true, + "diamond": true, + "diary": true, + "dice": true, + "diesel": true, + "diet": true, + "differ": true, + "digital": true, + "dignity": true, + "dilemma": true, + "dinner": true, + "dinosaur": true, + "direct": true, + "dirt": true, + "disagree": true, + "discover": true, + "disease": true, + "dish": true, + "dismiss": true, + "disorder": true, + "display": true, + "distance": true, + "divert": true, + "divide": true, + "divorce": true, + "dizzy": true, + "doctor": true, + "document": true, + "dog": true, + "doll": true, + "dolphin": true, + "domain": true, + "donate": true, + "donkey": true, + "donor": true, + "door": true, + "dose": true, + "double": true, + "dove": true, + "draft": true, + "dragon": true, + "drama": true, + "drastic": true, + "draw": true, + "dream": true, + "dress": true, + "drift": true, + "drill": true, + "drink": true, + "drip": true, + "drive": true, + "drop": true, + "drum": true, + "dry": true, + "duck": true, + "dumb": true, + "dune": true, + "during": true, + "dust": true, + "dutch": true, + "duty": true, + "dwarf": true, + "dynamic": true, + "eager": true, + "eagle": true, + "early": true, + "earn": true, + "earth": true, + "easily": true, + "east": true, + "easy": true, + "echo": true, + "ecology": true, + "economy": true, + "edge": true, + "edit": true, + "educate": true, + "effort": true, + "egg": true, + "eight": true, + "either": true, + "elbow": true, + "elder": true, + "electric": true, + "elegant": true, + "element": true, + "elephant": true, + "elevator": true, + "elite": true, + "else": true, + "embark": true, + "embody": true, + "embrace": true, + "emerge": true, + "emotion": true, + "employ": true, + "empower": true, + "empty": true, + "enable": true, + "enact": true, + "end": true, + "endless": true, + "endorse": true, + "enemy": true, + "energy": true, + "enforce": true, + "engage": true, + "engine": true, + "enhance": true, + "enjoy": true, + "enlist": true, + "enough": true, + "enrich": true, + "enroll": true, + "ensure": true, + "enter": true, + "entire": true, + "entry": true, + "envelope": true, + "episode": true, + "equal": true, + "equip": true, + "era": true, + "erase": true, + "erode": true, + "erosion": true, + "error": true, + "erupt": true, + "escape": true, + "essay": true, + "essence": true, + "estate": true, + "eternal": true, + "ethics": true, + "evidence": true, + "evil": true, + "evoke": true, + "evolve": true, + "exact": true, + "example": true, + "excess": true, + "exchange": true, + "excite": true, + "exclude": true, + "excuse": true, + "execute": true, + "exercise": true, + "exhaust": true, + "exhibit": true, + "exile": true, + "exist": true, + "exit": true, + "exotic": true, + "expand": true, + "expect": true, + "expire": true, + "explain": true, + "expose": true, + "express": true, + "extend": true, + "extra": true, + "eye": true, + "eyebrow": true, + "fabric": true, + "face": true, + "faculty": true, + "fade": true, + "faint": true, + "faith": true, + "fall": true, + "false": true, + "fame": true, + "family": true, + "famous": true, + "fan": true, + "fancy": true, + "fantasy": true, + "farm": true, + "fashion": true, + "fat": true, + "fatal": true, + "father": true, + "fatigue": true, + "fault": true, + "favorite": true, + "feature": true, + "february": true, + "federal": true, + "fee": true, + "feed": true, + "feel": true, + "female": true, + "fence": true, + "festival": true, + "fetch": true, + "fever": true, + "few": true, + "fiber": true, + "fiction": true, + "field": true, + "figure": true, + "file": true, + "film": true, + "filter": true, + "final": true, + "find": true, + "fine": true, + "finger": true, + "finish": true, + "fire": true, + "firm": true, + "first": true, + "fiscal": true, + "fish": true, + "fit": true, + "fitness": true, + "fix": true, + "flag": true, + "flame": true, + "flash": true, + "flat": true, + "flavor": true, + "flee": true, + "flight": true, + "flip": true, + "float": true, + "flock": true, + "floor": true, + "flower": true, + "fluid": true, + "flush": true, + "fly": true, + "foam": true, + "focus": true, + "fog": true, + "foil": true, + "fold": true, + "follow": true, + "food": true, + "foot": true, + "force": true, + "forest": true, + "forget": true, + "fork": true, + "fortune": true, + "forum": true, + "forward": true, + "fossil": true, + "foster": true, + "found": true, + "fox": true, + "fragile": true, + "frame": true, + "frequent": true, + "fresh": true, + "friend": true, + "fringe": true, + "frog": true, + "front": true, + "frost": true, + "frown": true, + "frozen": true, + "fruit": true, + "fuel": true, + "fun": true, + "funny": true, + "furnace": true, + "fury": true, + "future": true, + "gadget": true, + "gain": true, + "galaxy": true, + "gallery": true, + "game": true, + "gap": true, + "garage": true, + "garbage": true, + "garden": true, + "garlic": true, + "garment": true, + "gas": true, + "gasp": true, + "gate": true, + "gather": true, + "gauge": true, + "gaze": true, + "general": true, + "genius": true, + "genre": true, + "gentle": true, + "genuine": true, + "gesture": true, + "ghost": true, + "giant": true, + "gift": true, + "giggle": true, + "ginger": true, + "giraffe": true, + "girl": true, + "give": true, + "glad": true, + "glance": true, + "glare": true, + "glass": true, + "glide": true, + "glimpse": true, + "globe": true, + "gloom": true, + "glory": true, + "glove": true, + "glow": true, + "glue": true, + "goat": true, + "goddess": true, + "gold": true, + "good": true, + "goose": true, + "gorilla": true, + "gospel": true, + "gossip": true, + "govern": true, + "gown": true, + "grab": true, + "grace": true, + "grain": true, + "grant": true, + "grape": true, + "grass": true, + "gravity": true, + "great": true, + "green": true, + "grid": true, + "grief": true, + "grit": true, + "grocery": true, + "group": true, + "grow": true, + "grunt": true, + "guard": true, + "guess": true, + "guide": true, + "guilt": true, + "guitar": true, + "gun": true, + "gym": true, + "habit": true, + "hair": true, + "half": true, + "hammer": true, + "hamster": true, + "hand": true, + "happy": true, + "harbor": true, + "hard": true, + "harsh": true, + "harvest": true, + "hat": true, + "have": true, + "hawk": true, + "hazard": true, + "head": true, + "health": true, + "heart": true, + "heavy": true, + "hedgehog": true, + "height": true, + "hello": true, + "helmet": true, + "help": true, + "hen": true, + "hero": true, + "hidden": true, + "high": true, + "hill": true, + "hint": true, + "hip": true, + "hire": true, + "history": true, + "hobby": true, + "hockey": true, + "hold": true, + "hole": true, + "holiday": true, + "hollow": true, + "home": true, + "honey": true, + "hood": true, + "hope": true, + "horn": true, + "horror": true, + "horse": true, + "hospital": true, + "host": true, + "hotel": true, + "hour": true, + "hover": true, + "hub": true, + "huge": true, + "human": true, + "humble": true, + "humor": true, + "hundred": true, + "hungry": true, + "hunt": true, + "hurdle": true, + "hurry": true, + "hurt": true, + "husband": true, + "hybrid": true, + "ice": true, + "icon": true, + "idea": true, + "identify": true, + "idle": true, + "ignore": true, + "ill": true, + "illegal": true, + "illness": true, + "image": true, + "imitate": true, + "immense": true, + "immune": true, + "impact": true, + "impose": true, + "improve": true, + "impulse": true, + "inch": true, + "include": true, + "income": true, + "increase": true, + "index": true, + "indicate": true, + "indoor": true, + "industry": true, + "infant": true, + "inflict": true, + "inform": true, + "inhale": true, + "inherit": true, + "initial": true, + "inject": true, + "injury": true, + "inmate": true, + "inner": true, + "innocent": true, + "input": true, + "inquiry": true, + "insane": true, + "insect": true, + "inside": true, + "inspire": true, + "install": true, + "intact": true, + "interest": true, + "into": true, + "invest": true, + "invite": true, + "involve": true, + "iron": true, + "island": true, + "isolate": true, + "issue": true, + "item": true, + "ivory": true, + "jacket": true, + "jaguar": true, + "jar": true, + "jazz": true, + "jealous": true, + "jeans": true, + "jelly": true, + "jewel": true, + "job": true, + "join": true, + "joke": true, + "journey": true, + "joy": true, + "judge": true, + "juice": true, + "jump": true, + "jungle": true, + "junior": true, + "junk": true, + "just": true, + "kangaroo": true, + "keen": true, + "keep": true, + "ketchup": true, + "key": true, + "kick": true, + "kid": true, + "kidney": true, + "kind": true, + "kingdom": true, + "kiss": true, + "kit": true, + "kitchen": true, + "kite": true, + "kitten": true, + "kiwi": true, + "knee": true, + "knife": true, + "knock": true, + "know": true, + "lab": true, + "label": true, + "labor": true, + "ladder": true, + "lady": true, + "lake": true, + "lamp": true, + "language": true, + "laptop": true, + "large": true, + "later": true, + "latin": true, + "laugh": true, + "laundry": true, + "lava": true, + "law": true, + "lawn": true, + "lawsuit": true, + "layer": true, + "lazy": true, + "leader": true, + "leaf": true, + "learn": true, + "leave": true, + "lecture": true, + "left": true, + "leg": true, + "legal": true, + "legend": true, + "leisure": true, + "lemon": true, + "lend": true, + "length": true, + "lens": true, + "leopard": true, + "lesson": true, + "letter": true, + "level": true, + "liar": true, + "liberty": true, + "library": true, + "license": true, + "life": true, + "lift": true, + "light": true, + "like": true, + "limb": true, + "limit": true, + "link": true, + "lion": true, + "liquid": true, + "list": true, + "little": true, + "live": true, + "lizard": true, + "load": true, + "loan": true, + "lobster": true, + "local": true, + "lock": true, + "logic": true, + "lonely": true, + "long": true, + "loop": true, + "lottery": true, + "loud": true, + "lounge": true, + "love": true, + "loyal": true, + "lucky": true, + "luggage": true, + "lumber": true, + "lunar": true, + "lunch": true, + "luxury": true, + "lyrics": true, + "machine": true, + "mad": true, + "magic": true, + "magnet": true, + "maid": true, + "mail": true, + "main": true, + "major": true, + "make": true, + "mammal": true, + "man": true, + "manage": true, + "mandate": true, + "mango": true, + "mansion": true, + "manual": true, + "maple": true, + "marble": true, + "march": true, + "margin": true, + "marine": true, + "market": true, + "marriage": true, + "mask": true, + "mass": true, + "master": true, + "match": true, + "material": true, + "math": true, + "matrix": true, + "matter": true, + "maximum": true, + "maze": true, + "meadow": true, + "mean": true, + "measure": true, + "meat": true, + "mechanic": true, + "medal": true, + "media": true, + "melody": true, + "melt": true, + "member": true, + "memory": true, + "mention": true, + "menu": true, + "mercy": true, + "merge": true, + "merit": true, + "merry": true, + "mesh": true, + "message": true, + "metal": true, + "method": true, + "middle": true, + "midnight": true, + "milk": true, + "million": true, + "mimic": true, + "mind": true, + "minimum": true, + "minor": true, + "minute": true, + "miracle": true, + "mirror": true, + "misery": true, + "miss": true, + "mistake": true, + "mix": true, + "mixed": true, + "mixture": true, + "mobile": true, + "model": true, + "modify": true, + "mom": true, + "moment": true, + "monitor": true, + "monkey": true, + "monster": true, + "month": true, + "moon": true, + "moral": true, + "more": true, + "morning": true, + "mosquito": true, + "mother": true, + "motion": true, + "motor": true, + "mountain": true, + "mouse": true, + "move": true, + "movie": true, + "much": true, + "muffin": true, + "mule": true, + "multiply": true, + "muscle": true, + "museum": true, + "mushroom": true, + "music": true, + "must": true, + "mutual": true, + "myself": true, + "mystery": true, + "myth": true, + "naive": true, + "name": true, + "napkin": true, + "narrow": true, + "nasty": true, + "nation": true, + "nature": true, + "near": true, + "neck": true, + "need": true, + "negative": true, + "neglect": true, + "neither": true, + "nephew": true, + "nerve": true, + "nest": true, + "net": true, + "network": true, + "neutral": true, + "never": true, + "news": true, + "next": true, + "nice": true, + "night": true, + "noble": true, + "noise": true, + "nominee": true, + "noodle": true, + "normal": true, + "north": true, + "nose": true, + "notable": true, + "note": true, + "nothing": true, + "notice": true, + "novel": true, + "now": true, + "nuclear": true, + "number": true, + "nurse": true, + "nut": true, + "oak": true, + "obey": true, + "object": true, + "oblige": true, + "obscure": true, + "observe": true, + "obtain": true, + "obvious": true, + "occur": true, + "ocean": true, + "october": true, + "odor": true, + "off": true, + "offer": true, + "office": true, + "often": true, + "oil": true, + "okay": true, + "old": true, + "olive": true, + "olympic": true, + "omit": true, + "once": true, + "one": true, + "onion": true, + "online": true, + "only": true, + "open": true, + "opera": true, + "opinion": true, + "oppose": true, + "option": true, + "orange": true, + "orbit": true, + "orchard": true, + "order": true, + "ordinary": true, + "organ": true, + "orient": true, + "original": true, + "orphan": true, + "ostrich": true, + "other": true, + "outdoor": true, + "outer": true, + "output": true, + "outside": true, + "oval": true, + "oven": true, + "over": true, + "own": true, + "owner": true, + "oxygen": true, + "oyster": true, + "ozone": true, + "pact": true, + "paddle": true, + "page": true, + "pair": true, + "palace": true, + "palm": true, + "panda": true, + "panel": true, + "panic": true, + "panther": true, + "paper": true, + "parade": true, + "parent": true, + "park": true, + "parrot": true, + "party": true, + "pass": true, + "patch": true, + "path": true, + "patient": true, + "patrol": true, + "pattern": true, + "pause": true, + "pave": true, + "payment": true, + "peace": true, + "peanut": true, + "pear": true, + "peasant": true, + "pelican": true, + "pen": true, + "penalty": true, + "pencil": true, + "people": true, + "pepper": true, + "perfect": true, + "permit": true, + "person": true, + "pet": true, + "phone": true, + "photo": true, + "phrase": true, + "physical": true, + "piano": true, + "picnic": true, + "picture": true, + "piece": true, + "pig": true, + "pigeon": true, + "pill": true, + "pilot": true, + "pink": true, + "pioneer": true, + "pipe": true, + "pistol": true, + "pitch": true, + "pizza": true, + "place": true, + "planet": true, + "plastic": true, + "plate": true, + "play": true, + "please": true, + "pledge": true, + "pluck": true, + "plug": true, + "plunge": true, + "poem": true, + "poet": true, + "point": true, + "polar": true, + "pole": true, + "police": true, + "pond": true, + "pony": true, + "pool": true, + "popular": true, + "portion": true, + "position": true, + "possible": true, + "post": true, + "potato": true, + "pottery": true, + "poverty": true, + "powder": true, + "power": true, + "practice": true, + "praise": true, + "predict": true, + "prefer": true, + "prepare": true, + "present": true, + "pretty": true, + "prevent": true, + "price": true, + "pride": true, + "primary": true, + "print": true, + "priority": true, + "prison": true, + "private": true, + "prize": true, + "problem": true, + "process": true, + "produce": true, + "profit": true, + "program": true, + "project": true, + "promote": true, + "proof": true, + "property": true, + "prosper": true, + "protect": true, + "proud": true, + "provide": true, + "public": true, + "pudding": true, + "pull": true, + "pulp": true, + "pulse": true, + "pumpkin": true, + "punch": true, + "pupil": true, + "puppy": true, + "purchase": true, + "purity": true, + "purpose": true, + "purse": true, + "push": true, + "put": true, + "puzzle": true, + "pyramid": true, + "quality": true, + "quantum": true, + "quarter": true, + "question": true, + "quick": true, + "quit": true, + "quiz": true, + "quote": true, + "rabbit": true, + "raccoon": true, + "race": true, + "rack": true, + "radar": true, + "radio": true, + "rail": true, + "rain": true, + "raise": true, + "rally": true, + "ramp": true, + "ranch": true, + "random": true, + "range": true, + "rapid": true, + "rare": true, + "rate": true, + "rather": true, + "raven": true, + "raw": true, + "razor": true, + "ready": true, + "real": true, + "reason": true, + "rebel": true, + "rebuild": true, + "recall": true, + "receive": true, + "recipe": true, + "record": true, + "recycle": true, + "reduce": true, + "reflect": true, + "reform": true, + "refuse": true, + "region": true, + "regret": true, + "regular": true, + "reject": true, + "relax": true, + "release": true, + "relief": true, + "rely": true, + "remain": true, + "remember": true, + "remind": true, + "remove": true, + "render": true, + "renew": true, + "rent": true, + "reopen": true, + "repair": true, + "repeat": true, + "replace": true, + "report": true, + "require": true, + "rescue": true, + "resemble": true, + "resist": true, + "resource": true, + "response": true, + "result": true, + "retire": true, + "retreat": true, + "return": true, + "reunion": true, + "reveal": true, + "review": true, + "reward": true, + "rhythm": true, + "rib": true, + "ribbon": true, + "rice": true, + "rich": true, + "ride": true, + "ridge": true, + "rifle": true, + "right": true, + "rigid": true, + "ring": true, + "riot": true, + "ripple": true, + "risk": true, + "ritual": true, + "rival": true, + "river": true, + "road": true, + "roast": true, + "robot": true, + "robust": true, + "rocket": true, + "romance": true, + "roof": true, + "rookie": true, + "room": true, + "rose": true, + "rotate": true, + "rough": true, + "round": true, + "route": true, + "royal": true, + "rubber": true, + "rude": true, + "rug": true, + "rule": true, + "run": true, + "runway": true, + "rural": true, + "sad": true, + "saddle": true, + "sadness": true, + "safe": true, + "sail": true, + "salad": true, + "salmon": true, + "salon": true, + "salt": true, + "salute": true, + "same": true, + "sample": true, + "sand": true, + "satisfy": true, + "satoshi": true, + "sauce": true, + "sausage": true, + "save": true, + "say": true, + "scale": true, + "scan": true, + "scare": true, + "scatter": true, + "scene": true, + "scheme": true, + "school": true, + "science": true, + "scissors": true, + "scorpion": true, + "scout": true, + "scrap": true, + "screen": true, + "script": true, + "scrub": true, + "sea": true, + "search": true, + "season": true, + "seat": true, + "second": true, + "secret": true, + "section": true, + "security": true, + "seed": true, + "seek": true, + "segment": true, + "select": true, + "sell": true, + "seminar": true, + "senior": true, + "sense": true, + "sentence": true, + "series": true, + "service": true, + "session": true, + "settle": true, + "setup": true, + "seven": true, + "shadow": true, + "shaft": true, + "shallow": true, + "share": true, + "shed": true, + "shell": true, + "sheriff": true, + "shield": true, + "shift": true, + "shine": true, + "ship": true, + "shiver": true, + "shock": true, + "shoe": true, + "shoot": true, + "shop": true, + "short": true, + "shoulder": true, + "shove": true, + "shrimp": true, + "shrug": true, + "shuffle": true, + "shy": true, + "sibling": true, + "sick": true, + "side": true, + "siege": true, + "sight": true, + "sign": true, + "silent": true, + "silk": true, + "silly": true, + "silver": true, + "similar": true, + "simple": true, + "since": true, + "sing": true, + "siren": true, + "sister": true, + "situate": true, + "six": true, + "size": true, + "skate": true, + "sketch": true, + "ski": true, + "skill": true, + "skin": true, + "skirt": true, + "skull": true, + "slab": true, + "slam": true, + "sleep": true, + "slender": true, + "slice": true, + "slide": true, + "slight": true, + "slim": true, + "slogan": true, + "slot": true, + "slow": true, + "slush": true, + "small": true, + "smart": true, + "smile": true, + "smoke": true, + "smooth": true, + "snack": true, + "snake": true, + "snap": true, + "sniff": true, + "snow": true, + "soap": true, + "soccer": true, + "social": true, + "sock": true, + "soda": true, + "soft": true, + "solar": true, + "soldier": true, + "solid": true, + "solution": true, + "solve": true, + "someone": true, + "song": true, + "soon": true, + "sorry": true, + "sort": true, + "soul": true, + "sound": true, + "soup": true, + "source": true, + "south": true, + "space": true, + "spare": true, + "spatial": true, + "spawn": true, + "speak": true, + "special": true, + "speed": true, + "spell": true, + "spend": true, + "sphere": true, + "spice": true, + "spider": true, + "spike": true, + "spin": true, + "spirit": true, + "split": true, + "spoil": true, + "sponsor": true, + "spoon": true, + "sport": true, + "spot": true, + "spray": true, + "spread": true, + "spring": true, + "spy": true, + "square": true, + "squeeze": true, + "squirrel": true, + "stable": true, + "stadium": true, + "staff": true, + "stage": true, + "stairs": true, + "stamp": true, + "stand": true, + "start": true, + "state": true, + "stay": true, + "steak": true, + "steel": true, + "stem": true, + "step": true, + "stereo": true, + "stick": true, + "still": true, + "sting": true, + "stock": true, + "stomach": true, + "stone": true, + "stool": true, + "story": true, + "stove": true, + "strategy": true, + "street": true, + "strike": true, + "strong": true, + "struggle": true, + "student": true, + "stuff": true, + "stumble": true, + "style": true, + "subject": true, + "submit": true, + "subway": true, + "success": true, + "such": true, + "sudden": true, + "suffer": true, + "sugar": true, + "suggest": true, + "suit": true, + "summer": true, + "sun": true, + "sunny": true, + "sunset": true, + "super": true, + "supply": true, + "supreme": true, + "sure": true, + "surface": true, + "surge": true, + "surprise": true, + "surround": true, + "survey": true, + "suspect": true, + "sustain": true, + "swallow": true, + "swamp": true, + "swap": true, + "swarm": true, + "swear": true, + "sweet": true, + "swift": true, + "swim": true, + "swing": true, + "switch": true, + "sword": true, + "symbol": true, + "symptom": true, + "syrup": true, + "system": true, + "table": true, + "tackle": true, + "tag": true, + "tail": true, + "talent": true, + "talk": true, + "tank": true, + "tape": true, + "target": true, + "task": true, + "taste": true, + "tattoo": true, + "taxi": true, + "teach": true, + "team": true, + "tell": true, + "ten": true, + "tenant": true, + "tennis": true, + "tent": true, + "term": true, + "test": true, + "text": true, + "thank": true, + "that": true, + "theme": true, + "then": true, + "theory": true, + "there": true, + "they": true, + "thing": true, + "this": true, + "thought": true, + "three": true, + "thrive": true, + "throw": true, + "thumb": true, + "thunder": true, + "ticket": true, + "tide": true, + "tiger": true, + "tilt": true, + "timber": true, + "time": true, + "tiny": true, + "tip": true, + "tired": true, + "tissue": true, + "title": true, + "toast": true, + "tobacco": true, + "today": true, + "toddler": true, + "toe": true, + "together": true, + "toilet": true, + "token": true, + "tomato": true, + "tomorrow": true, + "tone": true, + "tongue": true, + "tonight": true, + "tool": true, + "tooth": true, + "top": true, + "topic": true, + "topple": true, + "torch": true, + "tornado": true, + "tortoise": true, + "toss": true, + "total": true, + "tourist": true, + "toward": true, + "tower": true, + "town": true, + "toy": true, + "track": true, + "trade": true, + "traffic": true, + "tragic": true, + "train": true, + "transfer": true, + "trap": true, + "trash": true, + "travel": true, + "tray": true, + "treat": true, + "tree": true, + "trend": true, + "trial": true, + "tribe": true, + "trick": true, + "trigger": true, + "trim": true, + "trip": true, + "trophy": true, + "trouble": true, + "truck": true, + "true": true, + "truly": true, + "trumpet": true, + "trust": true, + "truth": true, + "try": true, + "tube": true, + "tuition": true, + "tumble": true, + "tuna": true, + "tunnel": true, + "turkey": true, + "turn": true, + "turtle": true, + "twelve": true, + "twenty": true, + "twice": true, + "twin": true, + "twist": true, + "two": true, + "type": true, + "typical": true, + "ugly": true, + "umbrella": true, + "unable": true, + "unaware": true, + "uncle": true, + "uncover": true, + "under": true, + "undo": true, + "unfair": true, + "unfold": true, + "unhappy": true, + "uniform": true, + "unique": true, + "unit": true, + "universe": true, + "unknown": true, + "unlock": true, + "until": true, + "unusual": true, + "unveil": true, + "update": true, + "upgrade": true, + "uphold": true, + "upon": true, + "upper": true, + "upset": true, + "urban": true, + "urge": true, + "usage": true, + "use": true, + "used": true, + "useful": true, + "useless": true, + "usual": true, + "utility": true, + "vacant": true, + "vacuum": true, + "vague": true, + "valid": true, + "valley": true, + "valve": true, + "van": true, + "vanish": true, + "vapor": true, + "various": true, + "vast": true, + "vault": true, + "vehicle": true, + "velvet": true, + "vendor": true, + "venture": true, + "venue": true, + "verb": true, + "verify": true, + "version": true, + "very": true, + "vessel": true, + "veteran": true, + "viable": true, + "vibrant": true, + "vicious": true, + "victory": true, + "video": true, + "view": true, + "village": true, + "vintage": true, + "violin": true, + "virtual": true, + "virus": true, + "visa": true, + "visit": true, + "visual": true, + "vital": true, + "vivid": true, + "vocal": true, + "voice": true, + "void": true, + "volcano": true, + "volume": true, + "vote": true, + "voyage": true, + "wage": true, + "wagon": true, + "wait": true, + "walk": true, + "wall": true, + "walnut": true, + "want": true, + "warfare": true, + "warm": true, + "warrior": true, + "wash": true, + "wasp": true, + "waste": true, + "water": true, + "wave": true, + "way": true, + "wealth": true, + "weapon": true, + "wear": true, + "weasel": true, + "weather": true, + "web": true, + "wedding": true, + "weekend": true, + "weird": true, + "welcome": true, + "west": true, + "wet": true, + "whale": true, + "what": true, + "wheat": true, + "wheel": true, + "when": true, + "where": true, + "whip": true, + "whisper": true, + "wide": true, + "width": true, + "wife": true, + "wild": true, + "will": true, + "win": true, + "window": true, + "wine": true, + "wing": true, + "wink": true, + "winner": true, + "winter": true, + "wire": true, + "wisdom": true, + "wise": true, + "wish": true, + "witness": true, + "wolf": true, + "woman": true, + "wonder": true, + "wood": true, + "wool": true, + "word": true, + "work": true, + "world": true, + "worry": true, + "worth": true, + "wrap": true, + "wreck": true, + "wrestle": true, + "wrist": true, + "write": true, + "wrong": true, + "yard": true, + "year": true, + "yellow": true, + "you": true, + "young": true, + "youth": true, + "zebra": true, + "zero": true, + "zone": true, + "zoo": true, +} diff --git a/ton/wallet/seed_test.go b/ton/wallet/seed_test.go new file mode 100644 index 00000000..87bee036 --- /dev/null +++ b/ton/wallet/seed_test.go @@ -0,0 +1,35 @@ +package wallet + +import ( + "testing" +) + +func TestNewSeedWithPassword(t *testing.T) { + seed := NewSeedWithPassword("123") + _, err := FromSeedWithPassword(nil, seed, "123", V3) + if err != nil { + t.Fatal(err) + } + + _, err = FromSeedWithPassword(nil, seed, "1234", V3) + if err == nil { + t.Fatal("should be invalid") + } + + _, err = FromSeedWithPassword(nil, seed, "", V3) + if err == nil { + t.Fatal("should be invalid") + } + + seedNoPass := NewSeed() + + _, err = FromSeed(nil, seedNoPass, V3) + if err != nil { + t.Fatal(err) + } + + _, err = FromSeedWithPassword(nil, seedNoPass, "123", V3) + if err == nil { + t.Fatal("should be invalid") + } +} diff --git a/ton/wallet/wallet.go b/ton/wallet/wallet.go new file mode 100644 index 00000000..ba830ae9 --- /dev/null +++ b/ton/wallet/wallet.go @@ -0,0 +1,146 @@ +package wallet + +import ( + "context" + "crypto/ed25519" + "fmt" + + "github.com/xssnick/tonutils-go/address" + "github.com/xssnick/tonutils-go/liteclient/tlb" + "github.com/xssnick/tonutils-go/ton" + "github.com/xssnick/tonutils-go/tvm/cell" +) + +type Version int + +const ( + V1 Version = 1 + V2 Version = 2 + V3 Version = 3 + V4 Version = 4 +) + +type TonAPI interface { + GetBlockInfo(ctx context.Context) (*tlb.BlockInfo, error) + GetAccount(ctx context.Context, block *tlb.BlockInfo, addr *address.Address) (*ton.Account, error) + SendExternalMessage(ctx context.Context, addr *address.Address, msg *cell.Cell) error + SendExternalInitMessage(ctx context.Context, addr *address.Address, msg *cell.Cell, state *tlb.StateInit) error + RunGetMethod(ctx context.Context, blockInfo *tlb.BlockInfo, addr *address.Address, method string, params ...interface{}) ([]interface{}, error) +} + +type Wallet struct { + api TonAPI + key ed25519.PrivateKey + addr *address.Address + ver Version +} + +func FromPrivateKey(api TonAPI, key ed25519.PrivateKey, version Version) (*Wallet, error) { + addr, err := AddressFromPubKey(key.Public().(ed25519.PublicKey), version) + if err != nil { + return nil, err + } + + return &Wallet{ + api: api, + key: key, + addr: addr, + ver: version, + }, nil +} + +func (w *Wallet) Address() *address.Address { + return w.addr +} + +func (w *Wallet) GetBalance(ctx context.Context, block *tlb.BlockInfo) (*tlb.Grams, error) { + acc, err := w.api.GetAccount(ctx, block, w.addr) + if err != nil { + return nil, fmt.Errorf("failed to get account state: %w", err) + } + + if !acc.IsActive { + return new(tlb.Grams), nil + } + + return acc.State.Balance, nil +} + +func (w *Wallet) Send(ctx context.Context, mode byte, message *tlb.InternalMessage) error { + msg := cell.BeginCell() + var stateInit *tlb.StateInit + + switch w.ver { + case V3: + block, err := w.api.GetBlockInfo(ctx) + if err != nil { + return fmt.Errorf("failed to get block: %w", err) + } + + var seq uint64 + resp, err := w.api.RunGetMethod(ctx, block, w.addr, "seqno") + if err != nil { + // TODO: make it better + // not initialized + if err.Error() != "contract exit code: 4294967040" { + return fmt.Errorf("failed to get seqno: %w", err) + } + + stateInit, err = GetStateInit(w.key.Public().(ed25519.PublicKey), w.ver) + if err != nil { + return fmt.Errorf("failed to get state init: %w", err) + } + } else { + var ok bool + seq, ok = resp[0].(uint64) + if !ok { + return fmt.Errorf("seqno is not int") + } + } + + intMsg, err := message.ToCell() + if err != nil { + return fmt.Errorf("failed to convert internal message to cell: %w", err) + } + + payload := cell.BeginCell().MustStoreUInt(_SubWalletV3, 32). + MustStoreUInt(uint64(0xFFFFFFFF), 32). // ttl, I took it from wallet's transactions + MustStoreUInt(seq, 32).MustStoreUInt(uint64(mode), 8).MustStoreRef(intMsg) + + sign := payload.EndCell().Sign(w.key) + + msg.MustStoreSlice(sign, 512).MustStoreBuilder(payload) + default: + return fmt.Errorf("send is not yet supported for wallet with this version") + } + + err := w.api.SendExternalInitMessage(ctx, w.addr, msg.EndCell(), stateInit) + if err != nil { + return fmt.Errorf("failed to send message: %w", err) + } + + return nil +} + +func (w *Wallet) Transfer(ctx context.Context, to *address.Address, amount *tlb.Grams, comment string) error { + var body *cell.Cell + if comment != "" { + // comment ident + c := cell.BeginCell().MustStoreUInt(0, 32) + + data := []byte(comment) + if err := c.StoreSlice(data, len(data)*8); err != nil { + return fmt.Errorf("failed to encode comment: %w", err) + } + + body = c.EndCell() + } + + return w.Send(ctx, 1, &tlb.InternalMessage{ + IHRDisabled: true, + Bounce: true, + DstAddr: to, + Amount: amount, + Body: body, + }) +} diff --git a/tvm/cell/builder.go b/tvm/cell/builder.go index c5926a49..5b3e295c 100644 --- a/tvm/cell/builder.go +++ b/tvm/cell/builder.go @@ -1,6 +1,7 @@ package cell import ( + "errors" "math/big" "github.com/xssnick/tonutils-go/address" @@ -67,6 +68,22 @@ func (b *Builder) StoreUInt(value uint64, sz int) error { return b.StoreBigInt(new(big.Int).SetUint64(value), sz) } +func (b *Builder) MustStoreBoolBit(value bool) *Builder { + err := b.StoreBoolBit(value) + if err != nil { + panic(err) + } + return b +} + +func (b *Builder) StoreBoolBit(value bool) error { + var i uint64 + if value { + i = 1 + } + return b.StoreBigInt(new(big.Int).SetUint64(i), 1) +} + func (b *Builder) MustStoreBigInt(value *big.Int, sz int) *Builder { err := b.StoreBigInt(value, sz) if err != nil { @@ -159,10 +176,14 @@ func (b *Builder) MustStoreRef(ref *Cell) *Builder { } func (b *Builder) StoreRef(ref *Cell) error { - if len(b.refs) >= 4 { + if len(b.refs) > 4 { return ErrTooMuchRefs } + if ref == nil { + return errors.New("ref cannot be nil") + } + b.refs = append(b.refs, ref) return nil @@ -217,10 +238,45 @@ func (b *Builder) StoreSlice(bytes []byte, sz int) error { return nil } +func (b *Builder) MustStoreBuilder(builder *Builder) *Builder { + err := b.StoreBuilder(builder) + if err != nil { + panic(err) + } + return b +} + +func (b *Builder) StoreBuilder(builder *Builder) error { + if len(b.refs)+len(builder.refs) > 4 { + return ErrTooMuchRefs + } + + if b.bitsSz+builder.bitsSz >= 1024 { + return ErrTooMuchRefs + } + + b.refs = append(b.refs, builder.refs...) + b.MustStoreSlice(builder.data, builder.bitsSz) + + return nil +} + +func (b *Builder) RefsUsed() int { + return len(b.refs) +} + func (b *Builder) BitsUsed() int { return b.bitsSz } +func (b *Builder) BitsLeft() int { + return 1023 - b.bitsSz +} + +func (b *Builder) RefsLeft() int { + return 4 - len(b.refs) +} + func (b *Builder) Copy() *Builder { // copy data data := append([]byte{}, b.data...) diff --git a/tvm/cell/cell.go b/tvm/cell/cell.go index ee5c59d5..65da79ee 100644 --- a/tvm/cell/cell.go +++ b/tvm/cell/cell.go @@ -1,6 +1,7 @@ package cell import ( + "crypto/ed25519" "crypto/sha256" "encoding/hex" "fmt" @@ -34,6 +35,25 @@ func (c *Cell) BeginParse() *LoadCell { } } +func (c *Cell) ToBuilder() *Builder { + // copy data + data := append([]byte{}, c.data...) + + return &Builder{ + bitsSz: c.bitsSz, + data: data, + refs: c.refs, + } +} + +func (c *Cell) BitsSize() int { + return c.bitsSz +} + +func (c *Cell) RefsNum() int { + return len(c.refs) +} + func (c *Cell) Dump() string { return c.dump(0, false) } @@ -79,3 +99,7 @@ func (c *Cell) Hash() []byte { hash.Write(c.serialize(true)) return hash.Sum(nil) } + +func (c *Cell) Sign(key ed25519.PrivateKey) []byte { + return ed25519.Sign(key, c.Hash()) +} diff --git a/tvm/cell/serialize.go b/tvm/cell/serialize.go index 877467e8..7aab52c9 100644 --- a/tvm/cell/serialize.go +++ b/tvm/cell/serialize.go @@ -59,7 +59,7 @@ func (c *Cell) ToBOCWithFlags(withCRC bool) []byte { data = append(data, sizeBytes) // cells num - data = append(data, dynamicIntBytes(uint64(calcCells(c)), int(sizeBytes))...) + data = append(data, dynamicIntBytes(uint64(calcCells(c)), int(cellSizeBytes))...) // roots num (only 1 supported for now) data = append(data, 1)