From 13d76a09306d72f9dc21291ca02912eaf5ba4b25 Mon Sep 17 00:00:00 2001 From: Yilun Date: Mon, 20 Apr 2020 02:43:00 -0700 Subject: [PATCH] Rewrite RPC module and add RPC functions to clients 1. Client and Multiclient now have all RPC functions available, including the ones that needs to send transactions. Clients will try to use connected nodes as RPC address first, and fallback to seed node if failed. 2. Transaction related function now accepts transaction config as last param, which includes fee, nonce and attributes. Signed-off-by: Yilun --- README.md | 10 +- client.go | 220 ++++++++++++++++++++--- config.go | 35 ++++ error.go | 5 + examples/wallet/main.go | 10 +- multiclient.go | 310 +++++++++++++++++++++++++++----- nanopay.go | 28 +-- rpc.go | 388 +++++++++++++++++++++++++++++++++++----- wallet.go | 268 +++++++++------------------ 9 files changed, 949 insertions(+), 325 deletions(-) diff --git a/README.md b/README.md index 7e7c915c..57cbb8ab 100644 --- a/README.md +++ b/README.md @@ -295,7 +295,7 @@ balance, err := wallet.BalanceByAddress("NKNxxxxx") Transfer asset to some address: ```go -txnHash, err := wallet.Transfer(account.WalletAddress(), "100", "0") +txnHash, err := wallet.Transfer(account.WalletAddress(), "100", nil) ``` Open nano pay channel to specified address: @@ -323,25 +323,25 @@ txnHash, err := wallet.SendRawTransaction(txn) Register name for this wallet: ```go -txnHash, err = wallet.RegisterName("somename") +txnHash, err = wallet.RegisterName("somename", nil) ``` Delete name for this wallet: ```go -txnHash, err = wallet.DeleteName("somename") +txnHash, err = wallet.DeleteName("somename", nil) ``` Subscribe to specified topic for this wallet for next 100 blocks: ```go -txnHash, err = wallet.Subscribe("identifier", "topic", 100, "meta", "0") +txnHash, err = wallet.Subscribe("identifier", "topic", 100, "meta", nil) ``` Unsubscribe from specified topic: ```go -txnHash, err = wallet.Unsubscribe("identifier", "topic", "0") +txnHash, err = wallet.Unsubscribe("identifier", "topic", nil) ``` ## iOS/Android diff --git a/client.go b/client.go index 7ef83bb4..5ff6e5a2 100644 --- a/client.go +++ b/client.go @@ -17,10 +17,12 @@ import ( "github.com/gogo/protobuf/proto" "github.com/gorilla/websocket" "github.com/nknorg/nkn-sdk-go/payloads" - "github.com/nknorg/nkn/api/common" + apiCommon "github.com/nknorg/nkn/api/common" + "github.com/nknorg/nkn/common" "github.com/nknorg/nkn/crypto" "github.com/nknorg/nkn/crypto/ed25519" "github.com/nknorg/nkn/pb" + "github.com/nknorg/nkn/transaction" "github.com/nknorg/nkn/util/address" "github.com/nknorg/nkn/util/config" "github.com/patrickmn/go-cache" @@ -68,14 +70,15 @@ type Client struct { closed bool conn *websocket.Conn node *Node - urlString string sharedKeys map[string]*[sharedKeySize]byte + wallet *Wallet } // clientInterface is the common interface of client and multiclient. type clientInterface interface { getConfig() *ClientConfig send(dests []string, payload *payloads.Payload, encrypted bool, maxHoldingSeconds int32) error + GetSubscribers(topic string, offset, limit int, meta, txPool bool) (*Subscribers, error) } type setClientResult struct { @@ -100,6 +103,11 @@ func NewClient(account *Account, identifier string, config *ClientConfig) (*Clie copy(sk[:], account.PrivKey()) curveSecretKey := ed25519.PrivateKeyToCurve25519PrivateKey(&sk) + w, err := NewWallet(account, &WalletConfig{SeedRPCServerAddr: config.SeedRPCServerAddr}) + if err != nil { + return nil, err + } + c := Client{ config: config, account: account, @@ -112,6 +120,7 @@ func NewClient(account *Account, identifier string, config *ClientConfig) (*Clie reconnectChan: make(chan struct{}, 1), responseChannels: cache.New(time.Duration(config.MsgCacheExpiration)*time.Millisecond, time.Duration(config.MsgCacheExpiration)*time.Millisecond), sharedKeys: make(map[string]*[sharedKeySize]byte), + wallet: w, } go c.handleReconnect() @@ -353,12 +362,12 @@ func (c *Client) handleMessage(msgType int, data []byte) error { if err := json.Unmarshal(*msg["Action"], &action); err != nil { return err } - var errCode common.ErrCode + var errCode apiCommon.ErrCode if err := json.Unmarshal(*msg["Error"], &errCode); err != nil { return err } - if errCode != common.SUCCESS { - if errCode == common.WRONG_NODE { + if errCode != apiCommon.SUCCESS { + if errCode == apiCommon.WRONG_NODE { var node Node if err := json.Unmarshal(*msg["Result"], &node); err != nil { return err @@ -373,7 +382,7 @@ func (c *Client) handleMessage(msgType int, data []byte) error { c.Close() } return errorWithCode{ - err: errors.New(common.ErrMessage[errCode]), + err: errors.New(apiCommon.ErrMessage[errCode]), code: int32(errCode), } } @@ -508,20 +517,29 @@ func (c *Client) handleMessage(msgType int, data []byte) error { } func (c *Client) connectToNode(node *Node) error { - urlString := (&url.URL{Scheme: "ws", Host: node.Address}).String() + wsAddr := (&url.URL{Scheme: "ws", Host: node.Addr}).String() dialer := websocket.DefaultDialer dialer.HandshakeTimeout = time.Duration(c.config.WsHandshakeTimeout) * time.Millisecond - conn, _, err := dialer.Dial(urlString, nil) + conn, _, err := dialer.Dial(wsAddr, nil) if err != nil { return err } + var rpcAddr string + if len(node.RPCAddr) > 0 { + rpcAddr = (&url.URL{Scheme: "http", Host: node.RPCAddr}).String() + } + c.lock.Lock() prevConn := c.conn c.conn = conn c.node = node - c.urlString = urlString + if len(rpcAddr) > 0 { + c.wallet.config.SeedRPCServerAddr = NewStringArray(rpcAddr) + } else { + c.wallet.config.SeedRPCServerAddr = NewStringArray() + } c.lock.Unlock() if prevConn != nil { @@ -748,7 +766,7 @@ func (c *Client) processDest(dest string) (string, error) { } addr := strings.Split(dest, ".") if len(addr[len(addr)-1]) < 2*ed25519.PublicKeySize { - reg, err := GetRegistrant(addr[len(addr)-1], c.config) + reg, err := c.GetRegistrant(addr[len(addr)-1]) if err != nil { return "", err } @@ -806,7 +824,7 @@ func (c *Client) newOutboundMessage(dests []string, plds [][]byte, encrypted boo MaxHoldingSeconds: uint32(maxHoldingSeconds), } - nodePk, err := hex.DecodeString(c.node.PublicKey) + nodePk, err := hex.DecodeString(c.node.PubKey) if err != nil { return nil, err } @@ -998,7 +1016,7 @@ func publish(c clientInterface, topic string, data interface{}, config *MessageC offset := int(config.Offset) limit := int(config.Limit) - res, err := GetSubscribers(topic, offset, limit, false, config.TxPool, c.getConfig()) + res, err := c.GetSubscribers(topic, offset, limit, false, config.TxPool) if err != nil { return err } @@ -1013,7 +1031,7 @@ func publish(c clientInterface, topic string, data interface{}, config *MessageC for len(subscribers) >= limit { offset += limit - res, err = GetSubscribers(topic, offset, limit, false, false, c.getConfig()) + res, err = c.GetSubscribers(topic, offset, limit, false, false) if err != nil { return err } @@ -1063,27 +1081,179 @@ func (c *Client) getConfig() *ClientConfig { return c.config } -// GetSubscribers gets the subscribers of a topic with a offset and max number -// of results (limit). If meta is true, results contain each subscriber's -// metadata. If txPool is true, results contain subscribers in txPool. Enabling -// this will get subscribers sooner after they send subscribe transactions, but -// might affect the correctness of subscribers because transactions in txpool is -// not guaranteed to be packed into a block. +// ProgramHash returns the program hash of this client's account. +func (c *Client) ProgramHash() common.Uint160 { + return c.wallet.ProgramHash() +} + +// SignTransaction signs an unsigned transaction using this client's key pair. +func (c *Client) SignTransaction(tx *transaction.Transaction) error { + return c.wallet.SignTransaction(tx) +} + +// NewNanoPay is a shortcut for NewNanoPay using this client's wallet address as +// sender. // -// Offset and limit are changed to signed int for gomobile compatibility +// Duration is changed to signed int for gomobile compatibility. +func (c *Client) NewNanoPay(recipientAddress, fee string, duration int) (*NanoPay, error) { + return NewNanoPay(c.wallet, c, recipientAddress, fee, duration) +} + +// NewNanoPayClaimer is a shortcut for NewNanoPayClaimer using this client as +// RPC client. +func (c *Client) NewNanoPayClaimer(recipientAddress string, claimIntervalMs int32, onError *OnError) (*NanoPayClaimer, error) { + if len(recipientAddress) == 0 { + recipientAddress = c.wallet.address + } + return NewNanoPayClaimer(recipientAddress, claimIntervalMs, onError, c) +} + +// GetNonce is the same as package level GetNonce, but using connected node as +// the RPC server, followed by this client's SeedRPCServerAddr if failed. +func (c *Client) GetNonce(txPool bool) (int64, error) { + return c.GetNonceByAddress(c.wallet.address, txPool) +} + +// GetNonceByAddress is the same as package level GetNonce, but using connected +// node as the RPC server, followed by this client's SeedRPCServerAddr if +// failed. +func (c *Client) GetNonceByAddress(address string, txPool bool) (int64, error) { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := GetNonce(address, txPool, c.wallet.config) + if err == nil { + return res, err + } + } + return GetNonce(address, txPool, c.config) +} + +// GetHeight is the same as package level GetHeight, but using connected node as +// the RPC server, followed by this client's SeedRPCServerAddr if failed. +func (c *Client) GetHeight() (int32, error) { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := GetHeight(c.wallet.config) + if err == nil { + return res, err + } + } + return GetHeight(c.config) +} + +// Balance is the same as package level GetBalance, but using connected node as +// the RPC server, followed by this client's SeedRPCServerAddr if failed. +func (c *Client) Balance() (*Amount, error) { + return c.BalanceByAddress(c.wallet.address) +} + +// BalanceByAddress is the same as package level GetBalance, but using connected +// node as the RPC server, followed by this client's SeedRPCServerAddr if +// failed. +func (c *Client) BalanceByAddress(address string) (*Amount, error) { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := GetBalance(address, c.wallet.config) + if err == nil { + return res, err + } + } + return GetBalance(address, c.config) +} + +// GetSubscribers is the same as package level GetSubscribers, but using +// connected node as the RPC server, followed by this client's SeedRPCServerAddr +// if failed. func (c *Client) GetSubscribers(topic string, offset, limit int, meta, txPool bool) (*Subscribers, error) { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := GetSubscribers(topic, offset, limit, meta, txPool, c.wallet.config) + if err == nil { + return res, err + } + } return GetSubscribers(topic, offset, limit, meta, txPool, c.config) } -// GetSubscription gets the subscription details of a subscriber in a topic. +// GetSubscription is the same as package level GetSubscription, but using +// connected node as the RPC server, followed by this client's SeedRPCServerAddr +// if failed. func (c *Client) GetSubscription(topic string, subscriber string) (*Subscription, error) { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := GetSubscription(topic, subscriber, c.wallet.config) + if err == nil { + return res, err + } + } return GetSubscription(topic, subscriber, c.config) } -// GetSubscribersCount returns the number of subscribers of a topic (not -// including txPool). -// -// Count is changed to signed int for gomobile compatibility +// GetSubscribersCount is the same as package level GetSubscribersCount, but +// using connected node as the RPC server, followed by this client's +// SeedRPCServerAddr if failed. func (c *Client) GetSubscribersCount(topic string) (int, error) { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := GetSubscribersCount(topic, c.wallet.config) + if err == nil { + return res, err + } + } return GetSubscribersCount(topic, c.config) } + +// GetRegistrant is the same as package level GetRegistrant, but using connected +// node as the RPC server, followed by this client's SeedRPCServerAddr if +// failed. +func (c *Client) GetRegistrant(name string) (*Registrant, error) { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := GetRegistrant(name, c.wallet.config) + if err == nil { + return res, err + } + } + return GetRegistrant(name, c.config) +} + +// SendRawTransaction is the same as package level SendRawTransaction, but using +// connected node as the RPC server, followed by this client's SeedRPCServerAddr +// if failed. +func (c *Client) SendRawTransaction(txn *transaction.Transaction) (string, error) { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := SendRawTransaction(txn, c.wallet.config) + if err == nil { + return res, err + } + } + return SendRawTransaction(txn, c.config) +} + +// Transfer is a shortcut for Transfer using this client as SignerRPCClient. +func (c *Client) Transfer(address, amount string, config *TransactionConfig) (string, error) { + return Transfer(c, address, amount, config) +} + +// RegisterName is a shortcut for RegisterName using this client as +// SignerRPCClient. +func (c *Client) RegisterName(name string, config *TransactionConfig) (string, error) { + return RegisterName(c, name, config) +} + +// TransferName is a shortcut for TransferName using this client as +// SignerRPCClient. +func (c *Client) TransferName(name string, recipientPubKey []byte, config *TransactionConfig) (string, error) { + return TransferName(c, name, recipientPubKey, config) +} + +// DeleteName is a shortcut for DeleteName using this client as SignerRPCClient. +func (c *Client) DeleteName(name string, config *TransactionConfig) (string, error) { + return DeleteName(c, name, config) +} + +// Subscribe is a shortcut for Subscribe using this client as SignerRPCClient. +// +// Duration is changed to signed int for gomobile compatibility. +func (c *Client) Subscribe(identifier, topic string, duration int, meta string, config *TransactionConfig) (string, error) { + return Subscribe(c, identifier, topic, duration, meta, config) +} + +// Unsubscribe is a shortcut for Unsubscribe using this client as +// SignerRPCClient. +func (c *Client) Unsubscribe(identifier, topic string, config *TransactionConfig) (string, error) { + return Unsubscribe(c, identifier, topic, config) +} diff --git a/config.go b/config.go index 397d326e..d88255df 100644 --- a/config.go +++ b/config.go @@ -198,6 +198,27 @@ func GetDefaultRPCConfig() *RPCConfig { return &rpcConf } +// TransactionConfig is the config for making a transaction. +type TransactionConfig struct { + Fee string + Nonce int64 // nonce is changed to signed int for gomobile compatibility + Attributes []byte +} + +// DefaultTransactionConfig is the default TransactionConfig. +var DefaultTransactionConfig = TransactionConfig{ + Fee: "0", + Nonce: 0, + Attributes: nil, +} + +// GetDefaultTransactionConfig returns the default rpc config with nil pointer +// fields set to default. +func GetDefaultTransactionConfig() *TransactionConfig { + txnConf := DefaultTransactionConfig + return &txnConf +} + // GetRandomSeedRPCServerAddr returns a random seed rpc server address from the // client config. func (config *ClientConfig) GetRandomSeedRPCServerAddr() string { @@ -277,3 +298,17 @@ func MergeWalletConfig(conf *WalletConfig) (*WalletConfig, error) { } return merged, nil } + +// MergeTransactionConfig merges a given transaction config with the default +// transaction config recursively. Any non zero value fields will override the +// default config. +func MergeTransactionConfig(conf *TransactionConfig) (*TransactionConfig, error) { + merged := GetDefaultTransactionConfig() + if conf != nil { + err := mergo.Merge(merged, conf, mergo.WithOverride) + if err != nil { + return nil, err + } + } + return merged, nil +} diff --git a/error.go b/error.go index 61899c04..74c66fc4 100644 --- a/error.go +++ b/error.go @@ -27,6 +27,11 @@ func (e errorWithCode) Code() int32 { return e.code } +const ( + errCodeNetworkError int32 = -1 + errCodeDecodeError int32 = -2 +) + // Error definitions. var ( ErrClosed = ncp.NewGenericError("use of closed network connection", true, true) // The error message is meant to be identical to error returned by net package. diff --git a/examples/wallet/main.go b/examples/wallet/main.go index c0f4a38e..ab52809b 100644 --- a/examples/wallet/main.go +++ b/examples/wallet/main.go @@ -45,7 +45,7 @@ func main() { if err != nil { return err } - txid, err := w.Transfer(address, "100", "0") + txid, err := w.Transfer(address, "100", nil) if err != nil { return err } @@ -53,7 +53,7 @@ func main() { // Register name for this wallet // This call will fail because a new account has not enough balance to pay the registration fee - txid, err = w.RegisterName("somename", "0") + txid, err = w.RegisterName("somename", nil) if err != nil { return err } @@ -61,7 +61,7 @@ func main() { // Transfer name owned by this wallet to another public key // This call will fail because a new account has no name - txid, err = w.TransferName("somename", []byte("recipient public key"), "0") + txid, err = w.TransferName("somename", []byte("recipient public key"), nil) if err != nil { return err } @@ -69,14 +69,14 @@ func main() { // Delete name owned by this wallet // This call will fail because a new account has no name - txid, err = w.DeleteName("somename", "0") + txid, err = w.DeleteName("somename", nil) if err != nil { return err } log.Println("success:", txid) // Subscribe to bucket 0 of specified topic for this wallet for next 10 blocks - txid, err = w.Subscribe("identifier", "topic", 10, "meta", "0") + txid, err = w.Subscribe("identifier", "topic", 10, "meta", nil) if err != nil { return err } diff --git a/multiclient.go b/multiclient.go index d35a15d6..b93c47cd 100644 --- a/multiclient.go +++ b/multiclient.go @@ -14,6 +14,8 @@ import ( multierror "github.com/hashicorp/go-multierror" ncp "github.com/nknorg/ncp-go" "github.com/nknorg/nkn-sdk-go/payloads" + "github.com/nknorg/nkn/common" + "github.com/nknorg/nkn/transaction" "github.com/nknorg/nkn/util/address" "github.com/patrickmn/go-cache" ) @@ -38,6 +40,31 @@ var ( multiClientIdentifierRe = regexp.MustCompile(MultiClientIdentifierRe) ) +func addMultiClientPrefix(dest []string, clientID int) []string { + result := make([]string, len(dest)) + for i, addr := range dest { + result[i] = addIdentifier(addr, clientID) + } + return result +} + +func addIdentifier(addr string, id int) string { + if id < 0 { + return addr + } + return addIdentifierPrefix(addr, "__"+strconv.Itoa(id)+"__") +} + +func removeIdentifier(src string) (string, string) { + s := strings.SplitN(src, ".", 2) + if len(s) > 1 { + if multiClientIdentifierRe.MatchString(s[0]) { + return s[1], s[0] + } + } + return src, "" +} + // MultiClient sends and receives data using multiple NKN clients concurrently // to improve reliability and latency. In addition, it supports session mode, a // reliable streaming protocol similar to TCP based on ncp @@ -58,6 +85,7 @@ type MultiClient struct { defaultClient *Client acceptAddrs []*regexp.Regexp isClosed bool + createDone bool sessionLock sync.Mutex sessions map[string]*ncp.Session @@ -180,6 +208,9 @@ func NewMultiClient(account *Account, baseIdentifier string, numSubClients int, go func() { wg.Wait() + m.lock.Lock() + m.createDone = true + m.lock.Unlock() select { case fail <- struct{}{}: default: @@ -198,16 +229,33 @@ func NewMultiClient(account *Account, baseIdentifier string, numSubClients int, // create client/wallet with the same key pair and should be kept secret and // safe. func (m *MultiClient) Seed() []byte { - m.lock.RLock() - defer m.lock.RUnlock() - return m.defaultClient.Seed() + return m.GetDefaultClient().Seed() } // PubKey returns the public key of the multiclient. func (m *MultiClient) PubKey() []byte { - m.lock.RLock() - defer m.lock.RUnlock() - return m.defaultClient.PubKey() + return m.GetDefaultClient().PubKey() +} + +// Address returns the NKN client address of the multiclient. Client address is +// in the form of +// identifier.pubKeyHex +// if identifier is not an empty string, or +// pubKeyHex +// if identifier is an empty string. +// +// Note that client address is different from wallet address using the same key +// pair (account). Wallet address can be computed from client address, but NOT +// vice versa. +func (m *MultiClient) Address() string { + return m.addr.String() +} + +// Addr returns the NKN client address of the multiclient as net.Addr interface, +// with Network() returns "nkn" and String() returns the same value as +// multiclient.Address(). +func (m *MultiClient) Addr() net.Addr { + return m.addr } // GetClients returns all clients of the multiclient with client index as key. @@ -216,6 +264,11 @@ func (m *MultiClient) PubKey() []byte { func (m *MultiClient) GetClients() map[int]*Client { m.lock.RLock() defer m.lock.RUnlock() + + if m.createDone { + return m.clients + } + clients := make(map[int]*Client, len(m.clients)) for i, client := range m.clients { clients[i] = client @@ -496,27 +549,6 @@ func (m *MultiClient) handleSessionMsg(localClientID, src string, sessionID, dat return nil } -// Address returns the NKN client address of the multiclient. Client address is -// in the form of -// identifier.pubKeyHex -// if identifier is not an empty string, or -// pubKeyHex -// if identifier is an empty string. -// -// Note that client address is different from wallet address using the same key -// pair (account). Wallet address can be computed from client address, but NOT -// vice versa. -func (m *MultiClient) Address() string { - return m.addr.String() -} - -// Addr returns the NKN client address of the multiclient as net.Addr interface, -// with Network() returns "nkn" and String() returns the same value as -// multiclient.Address(). -func (m *MultiClient) Addr() net.Addr { - return m.addr -} - // Listen will make multiclient start accepting sessions from address that // matches any of the given regular expressions. If addrsRe is nil, any address // will be accepted. Each function call will overwrite previous listening @@ -664,27 +696,221 @@ func (m *MultiClient) getConfig() *ClientConfig { return m.config } -func addMultiClientPrefix(dest []string, clientID int) []string { - result := make([]string, len(dest)) - for i, addr := range dest { - result[i] = addIdentifier(addr, clientID) +// ProgramHash returns the program hash of this multiclient's account. +func (m *MultiClient) ProgramHash() common.Uint160 { + return m.GetDefaultClient().ProgramHash() +} + +// SignTransaction signs an unsigned transaction using this multiclient's key +// pair. +func (m *MultiClient) SignTransaction(tx *transaction.Transaction) error { + return m.GetDefaultClient().SignTransaction(tx) +} + +// NewNanoPay is a shortcut for NewNanoPay using this multiclient's wallet +// address as sender. +// +// Duration is changed to signed int for gomobile compatibility. +func (m *MultiClient) NewNanoPay(recipientAddress, fee string, duration int) (*NanoPay, error) { + return NewNanoPay(m.GetDefaultClient().wallet, m, recipientAddress, fee, duration) +} + +// NewNanoPayClaimer is a shortcut for NewNanoPayClaimer using this multiclient +// as RPC client. +func (m *MultiClient) NewNanoPayClaimer(recipientAddress string, claimIntervalMs int32, onError *OnError) (*NanoPayClaimer, error) { + if len(recipientAddress) == 0 { + recipientAddress = m.GetDefaultClient().wallet.address } - return result + return NewNanoPayClaimer(recipientAddress, claimIntervalMs, onError, m) } -func addIdentifier(addr string, id int) string { - if id < 0 { - return addr +// GetNonce is the same as package level GetNonce, but using connected node as +// the RPC server, followed by this multiclient's SeedRPCServerAddr if failed. +func (m *MultiClient) GetNonce(txPool bool) (int64, error) { + return m.GetNonceByAddress(m.GetDefaultClient().wallet.address, txPool) +} + +// GetNonceByAddress is the same as package level GetNonce, but using connected +// node as the RPC server, followed by this multiclient's SeedRPCServerAddr if +// failed. +func (m *MultiClient) GetNonceByAddress(address string, txPool bool) (int64, error) { + for _, c := range m.GetClients() { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := GetNonce(address, txPool, c.wallet.config) + if err == nil { + return res, err + } + } } - return addIdentifierPrefix(addr, "__"+strconv.Itoa(id)+"__") + return GetNonce(address, txPool, m.config) } -func removeIdentifier(src string) (string, string) { - s := strings.SplitN(src, ".", 2) - if len(s) > 1 { - if multiClientIdentifierRe.MatchString(s[0]) { - return s[1], s[0] +// GetHeight is the same as package level GetHeight, but using connected node as +// the RPC server, followed by this multiclient's SeedRPCServerAddr if failed. +func (m *MultiClient) GetHeight() (int32, error) { + for _, c := range m.GetClients() { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := GetHeight(c.wallet.config) + if err == nil { + return res, err + } } } - return src, "" + return GetHeight(m.config) +} + +// Balance is the same as package level GetBalance, but using connected node as +// the RPC server, followed by this multiclient's SeedRPCServerAddr if failed. +func (m *MultiClient) Balance() (*Amount, error) { + return m.BalanceByAddress(m.GetDefaultClient().wallet.address) +} + +// BalanceByAddress is the same as package level GetBalance, but using connected +// node as the RPC server, followed by this multiclient's SeedRPCServerAddr if +// failed. +func (m *MultiClient) BalanceByAddress(address string) (*Amount, error) { + for _, c := range m.GetClients() { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := GetBalance(address, c.wallet.config) + if err == nil { + return res, err + } + } + } + return GetBalance(address, m.config) +} + +// GetSubscribers is the same as package level GetSubscribers, but using +// connected node as the RPC server, followed by this multiclient's +// SeedRPCServerAddr if failed. +func (m *MultiClient) GetSubscribers(topic string, offset, limit int, meta, txPool bool) (*Subscribers, error) { + for _, c := range m.GetClients() { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := GetSubscribers(topic, offset, limit, meta, txPool, c.wallet.config) + if err == nil { + return res, err + } + } + } + return GetSubscribers(topic, offset, limit, meta, txPool, m.config) +} + +// GetSubscription is the same as package level GetSubscription, but using +// connected node as the RPC server, followed by this multiclient's +// SeedRPCServerAddr if failed. +func (m *MultiClient) GetSubscription(topic string, subscriber string) (*Subscription, error) { + for _, c := range m.GetClients() { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := GetSubscription(topic, subscriber, c.wallet.config) + if err == nil { + return res, err + } + } + } + return GetSubscription(topic, subscriber, m.config) +} + +// GetSubscribersCount is the same as package level GetSubscribersCount, but +// using connected node as the RPC server, followed by this multiclient's +// SeedRPCServerAddr if failed. +func (m *MultiClient) GetSubscribersCount(topic string) (int, error) { + for _, c := range m.GetClients() { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := GetSubscribersCount(topic, c.wallet.config) + if err == nil { + return res, err + } + } + } + return GetSubscribersCount(topic, m.config) +} + +// GetRegistrant is the same as package level GetRegistrant, but using connected +// node as the RPC server, followed by this multiclient's SeedRPCServerAddr if +// failed. +func (m *MultiClient) GetRegistrant(name string) (*Registrant, error) { + for _, c := range m.GetClients() { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := GetRegistrant(name, c.wallet.config) + if err == nil { + return res, err + } + } + } + return GetRegistrant(name, m.config) +} + +// SendRawTransaction is the same as package level SendRawTransaction, but using +// connected node as the RPC server, followed by this multiclient's +// SeedRPCServerAddr if failed. +func (m *MultiClient) SendRawTransaction(txn *transaction.Transaction) (string, error) { + success := make(chan string, 1) + fail := make(chan struct{}, 1) + + go func() { + sent := 0 + for _, c := range m.GetClients() { + if c.wallet.config.SeedRPCServerAddr.Len() > 0 { + res, err := SendRawTransaction(txn, c.wallet.config) + if err == nil { + select { + case success <- res: + default: + } + sent++ + } + } + } + if sent == 0 { + select { + case fail <- struct{}{}: + default: + } + } + }() + + select { + case res := <-success: + return res, nil + case <-fail: + return SendRawTransaction(txn, m.config) + } +} + +// Transfer is a shortcut for Transfer using this multiclient as +// SignerRPCClient. +func (m *MultiClient) Transfer(address, amount string, config *TransactionConfig) (string, error) { + return Transfer(m, address, amount, config) +} + +// RegisterName is a shortcut for RegisterName using this multiclient as +// SignerRPCClient. +func (m *MultiClient) RegisterName(name string, config *TransactionConfig) (string, error) { + return RegisterName(m, name, config) +} + +// TransferName is a shortcut for TransferName using this multiclient as +// SignerRPCClient. +func (m *MultiClient) TransferName(name string, recipientPubKey []byte, config *TransactionConfig) (string, error) { + return TransferName(m, name, recipientPubKey, config) +} + +// DeleteName is a shortcut for DeleteName using this multiclient as +// SignerRPCClient. +func (m *MultiClient) DeleteName(name string, config *TransactionConfig) (string, error) { + return DeleteName(m, name, config) +} + +// Subscribe is a shortcut for Subscribe using this multiclient as +// SignerRPCClient. +// +// Duration is changed to signed int for gomobile compatibility. +func (m *MultiClient) Subscribe(identifier, topic string, duration int, meta string, config *TransactionConfig) (string, error) { + return Subscribe(m, identifier, topic, duration, meta, config) +} + +// Unsubscribe is a shortcut for Unsubscribe using this multiclient as +// SignerRPCClient. +func (m *MultiClient) Unsubscribe(identifier, topic string, config *TransactionConfig) (string, error) { + return Unsubscribe(m, identifier, topic, config) } diff --git a/nanopay.go b/nanopay.go index 233cde0c..b56353e7 100644 --- a/nanopay.go +++ b/nanopay.go @@ -30,6 +30,7 @@ const ( // payment amount can increase monotonically. type NanoPay struct { senderWallet *Wallet + rpcClient RPCClient recipientAddress string recipientProgramHash common.Uint160 fee common.Fixed64 @@ -46,7 +47,7 @@ type NanoPay struct { type NanoPayClaimer struct { recipientAddress string recipientProgramHash common.Uint160 - rpcConfig RPCConfigInterface + rpcClient RPCClient lock sync.Mutex amount common.Fixed64 @@ -59,8 +60,8 @@ type NanoPayClaimer struct { } // NewNanoPay creates a NanoPay with a payer wallet, recipient wallet address, -// txn fee, and duration in unit of blocks. -func NewNanoPay(senderWallet *Wallet, recipientAddress, fee string, duration int) (*NanoPay, error) { +// txn fee, duration in unit of blocks, and an optional rpc client. +func NewNanoPay(senderWallet *Wallet, rpcClient RPCClient, recipientAddress, fee string, duration int) (*NanoPay, error) { programHash, err := common.ToScriptHash(recipientAddress) if err != nil { return nil, err @@ -73,6 +74,7 @@ func NewNanoPay(senderWallet *Wallet, recipientAddress, fee string, duration int np := &NanoPay{ senderWallet: senderWallet, + rpcClient: rpcClient, recipientAddress: recipientAddress, recipientProgramHash: programHash, fee: feeFixed64, @@ -91,7 +93,7 @@ func (np *NanoPay) Recipient() string { // NanoPay transaction. Delta is the string representation of the amount in unit // of NKN to avoid precision loss. For example, "0.1" will be parsed as 0.1 NKN. func (np *NanoPay) IncrementAmount(delta string) (*transaction.Transaction, error) { - height, err := np.senderWallet.GetHeight() + height, err := np.rpcClient.GetHeight() if err != nil { return nil, err } @@ -122,7 +124,7 @@ func (np *NanoPay) IncrementAmount(delta string) (*transaction.Transaction, erro tx.UnsignedTx.Fee = int64(np.fee) - if err := np.senderWallet.signTransaction(tx); err != nil { + if err := np.senderWallet.SignTransaction(tx); err != nil { return nil, err } @@ -130,9 +132,9 @@ func (np *NanoPay) IncrementAmount(delta string) (*transaction.Transaction, erro } // NewNanoPayClaimer creates a NanoPayClaimer with a given recipient wallet -// address, claim interval in millisecond, onError channel, and an optional -// rpcConfig that is used for making RPC requests. -func NewNanoPayClaimer(recipientAddress string, claimIntervalMs int32, onError *OnError, rpcConfig RPCConfigInterface) (*NanoPayClaimer, error) { +// address, claim interval in millisecond, onError channel, and a rpcClient that +// is used for making RPC requests. +func NewNanoPayClaimer(recipientAddress string, claimIntervalMs int32, onError *OnError, rpcClient RPCClient) (*NanoPayClaimer, error) { receiver, err := common.ToScriptHash(recipientAddress) if err != nil { return nil, err @@ -141,7 +143,7 @@ func NewNanoPayClaimer(recipientAddress string, claimIntervalMs int32, onError * npc := &NanoPayClaimer{ recipientAddress: recipientAddress, recipientProgramHash: receiver, - rpcConfig: rpcConfig, + rpcClient: rpcClient, } go func() { @@ -164,7 +166,7 @@ func NewNanoPayClaimer(recipientAddress string, claimIntervalMs int32, onError * now := time.Now() if now.Before(npc.lastClaimTime.Add(time.Duration(claimIntervalMs) * time.Millisecond)) { - height, err := GetHeight(npc.rpcConfig) + height, err := npc.rpcClient.GetHeight() if err != nil { onError.receive(err) continue @@ -231,7 +233,7 @@ func (npc *NanoPayClaimer) flush() error { return nil } - _, err := SendRawTransaction(npc.tx, npc.rpcConfig) + _, err := npc.rpcClient.SendRawTransaction(npc.tx) if err != nil { return err } @@ -264,7 +266,7 @@ func (npc *NanoPayClaimer) Amount() *Amount { // new NanoPay, and previous NanoPay state will be flushed and sent to chain // before accepting new one. func (npc *NanoPayClaimer) Claim(tx *transaction.Transaction) (*Amount, error) { - height, err := GetHeight(npc.rpcConfig) + height, err := npc.rpcClient.GetHeight() if err != nil { return nil, err } @@ -302,7 +304,7 @@ func (npc *NanoPayClaimer) Claim(tx *transaction.Transaction) (*Amount, error) { return nil, npc.closeWithError(err) } - senderBalance, err := GetBalance(senderAddress, npc.rpcConfig) + senderBalance, err := npc.rpcClient.BalanceByAddress(senderAddress) if err != nil { return nil, err } diff --git a/rpc.go b/rpc.go index 4d47d747..eaf2e8fc 100644 --- a/rpc.go +++ b/rpc.go @@ -6,9 +6,46 @@ import ( "errors" "github.com/nknorg/nkn/api/httpjson/client" + "github.com/nknorg/nkn/common" "github.com/nknorg/nkn/transaction" + nknConfig "github.com/nknorg/nkn/util/config" ) +// Signer is the interface that can sign transactions. +type Signer interface { + PubKey() []byte + ProgramHash() common.Uint160 + SignTransaction(tx *transaction.Transaction) error +} + +// RPCClient is the RPC client interface that implements most RPC methods +// (except a few ones that is supposed to call with seed node only). +type RPCClient interface { + GetNonce(txPool bool) (int64, error) + GetNonceByAddress(address string, txPool bool) (int64, error) + Balance() (*Amount, error) + BalanceByAddress(address string) (*Amount, error) + GetHeight() (int32, error) + GetSubscribers(topic string, offset, limit int, meta, txPool bool) (*Subscribers, error) + GetSubscription(topic string, subscriber string) (*Subscription, error) + GetSubscribersCount(topic string) (int, error) + GetRegistrant(name string) (*Registrant, error) + SendRawTransaction(txn *transaction.Transaction) (string, error) +} + +// SignerRPCClient is a RPCClient that can also sign transactions and made RPC +// requests that requires signatures. +type SignerRPCClient interface { + Signer + RPCClient + Transfer(address, amount string, config *TransactionConfig) (string, error) + RegisterName(name string, config *TransactionConfig) (string, error) + TransferName(name string, recipientPubKey []byte, config *TransactionConfig) (string, error) + DeleteName(name string, config *TransactionConfig) (string, error) + Subscribe(identifier, topic string, duration int, meta string, config *TransactionConfig) (string, error) + Unsubscribe(identifier, topic string, config *TransactionConfig) (string, error) +} + // RPCConfigInterface is the config interface for making rpc call. ClientConfig, // WalletConfig and RPCConfig all implement this interface and thus can be used // directly. @@ -18,9 +55,10 @@ type RPCConfigInterface interface { // Node struct contains the information of the node that a client connects to. type Node struct { - Address string `json:"addr"` - PublicKey string `json:"pubkey"` - ID string `json:"id"` + Addr string `json:"addr"` + RPCAddr string `json:"rpcAddr"` + PubKey string `json:"pubkey"` + ID string `json:"id"` } // Subscription contains the information of a subscriber to a topic. @@ -50,6 +88,44 @@ type errResp struct { Data string } +// RPCCall makes a RPC call and put results to result passed in. +func RPCCall(action string, params map[string]interface{}, result interface{}, config RPCConfigInterface) error { + if config == nil { + config = GetDefaultRPCConfig() + } + data, err := client.Call(config.GetRandomSeedRPCServerAddr(), action, 0, params) + if err != nil { + return &errorWithCode{err: err, code: errCodeNetworkError} + } + resp := make(map[string]*json.RawMessage) + err = json.Unmarshal(data, &resp) + if err != nil { + return &errorWithCode{err: err, code: errCodeDecodeError} + } + if resp["error"] != nil { + er := &errResp{} + err := json.Unmarshal(*resp["error"], er) + if err != nil { + return &errorWithCode{err: err, code: errCodeDecodeError} + } + code := er.Code + if code < 0 { + code = -1 + } + msg := er.Message + if len(er.Data) > 0 { + msg += ": " + er.Data + } + return &errorWithCode{err: errors.New(msg), code: code} + } + + err = json.Unmarshal(*resp["result"], result) + if err != nil { + return &errorWithCode{err: err, code: errCodeDecodeError} + } + return nil +} + // GetWsAddr RPC gets the node that a client address should connect to using ws. func GetWsAddr(clientAddr string, config RPCConfigInterface) (*Node, error) { node := &Node{} @@ -71,27 +147,6 @@ func GetWssAddr(clientAddr string, config RPCConfigInterface) (*Node, error) { return node, nil } -// GetRegistrant RPC gets the registrant of a name. -func GetRegistrant(name string, config RPCConfigInterface) (*Registrant, error) { - registrant := &Registrant{} - err := RPCCall("getregistrant", map[string]interface{}{"name": name}, registrant, config) - if err != nil { - return nil, err - } - return registrant, nil -} - -// SendRawTransaction RPC sends a signed transaction to chain and returns txn -// hash hex string. -func SendRawTransaction(txn *transaction.Transaction, config RPCConfigInterface) (string, error) { - var txnHash string - err := RPCCall("sendrawtransaction", map[string]interface{}{"tx": hex.EncodeToString(txn.ToArray())}, &txnHash, config) - if err != nil { - return "", err - } - return txnHash, nil -} - // GetNonce RPC gets the next nonce to use of an address. If txPool is false, // result only counts transactions in ledger; if txPool is true, transactions in // txPool are also counted. @@ -205,40 +260,279 @@ func GetSubscribersCount(topic string, config RPCConfigInterface) (int, error) { return count, nil } -// RPCCall makes a RPC call and put results to result passed in. -func RPCCall(action string, params map[string]interface{}, result interface{}, config RPCConfigInterface) error { - if config == nil { - config = GetDefaultRPCConfig() +// GetRegistrant RPC gets the registrant of a name. +func GetRegistrant(name string, config RPCConfigInterface) (*Registrant, error) { + registrant := &Registrant{} + err := RPCCall("getregistrant", map[string]interface{}{"name": name}, registrant, config) + if err != nil { + return nil, err } - data, err := client.Call(config.GetRandomSeedRPCServerAddr(), action, 0, params) + return registrant, nil +} + +// SendRawTransaction RPC sends a signed transaction to chain and returns txn +// hash hex string. +func SendRawTransaction(txn *transaction.Transaction, config RPCConfigInterface) (string, error) { + var txnHash string + err := RPCCall("sendrawtransaction", map[string]interface{}{"tx": hex.EncodeToString(txn.ToArray())}, &txnHash, config) if err != nil { - return &errorWithCode{err: err, code: -1} + return "", err } - resp := make(map[string]*json.RawMessage) - err = json.Unmarshal(data, &resp) + return txnHash, nil +} + +// Transfer sends asset to a wallet address with a transaction fee. Amount is +// the string representation of the amount in unit of NKN to avoid precision +// loss. For example, "0.1" will be parsed as 0.1 NKN. +func Transfer(s SignerRPCClient, address, amount string, config *TransactionConfig) (string, error) { + config, err := MergeTransactionConfig(config) if err != nil { - return &errorWithCode{err: err, code: -1} + return "", err } - if resp["error"] != nil { - er := &errResp{} - err := json.Unmarshal(*resp["error"], er) + + amountFixed64, err := common.StringToFixed64(amount) + if err != nil { + return "", err + } + + programHash, err := common.ToScriptHash(address) + if err != nil { + return "", err + } + + fee, err := common.StringToFixed64(config.Fee) + if err != nil { + return "", err + } + + nonce := config.Nonce + if nonce == 0 { + nonce, err = s.GetNonce(true) if err != nil { - return &errorWithCode{err: err, code: -1} + return "", err } - code := er.Code - if code < 0 { - code = -1 + } + + tx, err := transaction.NewTransferAssetTransaction(s.ProgramHash(), programHash, uint64(nonce), amountFixed64, fee) + if err != nil { + return "", err + } + + if len(config.Attributes) > 0 { + tx.Transaction.UnsignedTx.Attributes = config.Attributes + } + + if err := s.SignTransaction(tx); err != nil { + return "", err + } + + return s.SendRawTransaction(tx) +} + +// RegisterName registers a name for this signer's public key at the cost of 10 +// NKN with a given transaction fee. The name will be valid for 1,576,800 blocks +// (around 1 year). Register name currently owned by this pubkey will extend the +// duration of the name to current block height + 1,576,800. Registration will +// fail if the name is currently owned by another account. +func RegisterName(s SignerRPCClient, name string, config *TransactionConfig) (string, error) { + config, err := MergeTransactionConfig(config) + if err != nil { + return "", err + } + + fee, err := common.StringToFixed64(config.Fee) + if err != nil { + return "", err + } + + nonce := config.Nonce + if nonce == 0 { + nonce, err = s.GetNonce(true) + if err != nil { + return "", err } - msg := er.Message - if len(er.Data) > 0 { - msg += ": " + er.Data + } + + tx, err := transaction.NewRegisterNameTransaction(s.PubKey(), name, uint64(nonce), nknConfig.MinNameRegistrationFee, fee) + if err != nil { + return "", err + } + + if len(config.Attributes) > 0 { + tx.Transaction.UnsignedTx.Attributes = config.Attributes + } + + if err := s.SignTransaction(tx); err != nil { + return "", err + } + + return s.SendRawTransaction(tx) +} + +// TransferName transfers a name owned by this signer's pubkey to another public +// key with a transaction fee. The expiration height of the name will not be +// changed. +func TransferName(s SignerRPCClient, name string, recipientPubKey []byte, config *TransactionConfig) (string, error) { + config, err := MergeTransactionConfig(config) + if err != nil { + return "", err + } + + fee, err := common.StringToFixed64(config.Fee) + if err != nil { + return "", err + } + + nonce := config.Nonce + if nonce == 0 { + nonce, err = s.GetNonce(true) + if err != nil { + return "", err } - return &errorWithCode{err: errors.New(msg), code: code} } - err = json.Unmarshal(*resp["result"], result) + tx, err := transaction.NewTransferNameTransaction(s.PubKey(), recipientPubKey, name, uint64(nonce), fee) if err != nil { - return &errorWithCode{err: err, code: 0} + return "", err } - return nil + + if len(config.Attributes) > 0 { + tx.Transaction.UnsignedTx.Attributes = config.Attributes + } + + if err := s.SignTransaction(tx); err != nil { + return "", err + } + + return s.SendRawTransaction(tx) +} + +// DeleteName deletes a name owned by this signer's pubkey with a given +// transaction fee. +func DeleteName(s SignerRPCClient, name string, config *TransactionConfig) (string, error) { + config, err := MergeTransactionConfig(config) + if err != nil { + return "", err + } + + fee, err := common.StringToFixed64(config.Fee) + if err != nil { + return "", err + } + + nonce := config.Nonce + if nonce == 0 { + nonce, err = s.GetNonce(true) + if err != nil { + return "", err + } + } + + tx, err := transaction.NewDeleteNameTransaction(s.PubKey(), name, uint64(nonce), fee) + if err != nil { + return "", err + } + + if len(config.Attributes) > 0 { + tx.Transaction.UnsignedTx.Attributes = config.Attributes + } + + if err := s.SignTransaction(tx); err != nil { + return "", err + } + + return s.SendRawTransaction(tx) +} + +// Subscribe to a topic with an identifier for a number of blocks. Client using +// the same key pair and identifier will be able to receive messages from this +// topic. If this (identifier, public key) pair is already subscribed to this +// topic, the subscription expiration will be extended to current block height + +// duration. +// +// Duration is changed to signed int for gomobile compatibility. +func Subscribe(s SignerRPCClient, identifier, topic string, duration int, meta string, config *TransactionConfig) (string, error) { + config, err := MergeTransactionConfig(config) + if err != nil { + return "", err + } + + fee, err := common.StringToFixed64(config.Fee) + if err != nil { + return "", err + } + + nonce := config.Nonce + if nonce == 0 { + nonce, err = s.GetNonce(true) + if err != nil { + return "", err + } + } + + tx, err := transaction.NewSubscribeTransaction( + s.PubKey(), + identifier, + topic, + uint32(duration), + meta, + uint64(nonce), + fee, + ) + if err != nil { + return "", err + } + + if len(config.Attributes) > 0 { + tx.Transaction.UnsignedTx.Attributes = config.Attributes + } + + if err := s.SignTransaction(tx); err != nil { + return "", err + } + + return s.SendRawTransaction(tx) +} + +// Unsubscribe from a topic for an identifier. Client using the same key pair +// and identifier will no longer receive messages from this topic. +func Unsubscribe(s SignerRPCClient, identifier, topic string, config *TransactionConfig) (string, error) { + config, err := MergeTransactionConfig(config) + if err != nil { + return "", err + } + + fee, err := common.StringToFixed64(config.Fee) + if err != nil { + return "", err + } + + nonce := config.Nonce + if nonce == 0 { + nonce, err = s.GetNonce(true) + if err != nil { + return "", err + } + } + + tx, err := transaction.NewUnsubscribeTransaction( + s.PubKey(), + identifier, + topic, + uint64(nonce), + fee, + ) + if err != nil { + return "", err + } + + if len(config.Attributes) > 0 { + tx.Transaction.UnsignedTx.Attributes = config.Attributes + } + + if err := s.SignTransaction(tx); err != nil { + return "", err + } + + return s.SendRawTransaction(tx) } diff --git a/wallet.go b/wallet.go index fceea8cf..c222ed30 100644 --- a/wallet.go +++ b/wallet.go @@ -12,7 +12,6 @@ import ( "github.com/nknorg/nkn/program" "github.com/nknorg/nkn/signature" "github.com/nknorg/nkn/transaction" - "github.com/nknorg/nkn/util/config" "github.com/nknorg/nkn/vault" ) @@ -206,7 +205,13 @@ func (w *Wallet) VerifyPassword(password string) bool { return bytes.Equal(passwordHash[:], w.passwordHash) } -func (w *Wallet) signTransaction(tx *transaction.Transaction) error { +// ProgramHash returns the program hash of this wallet's account. +func (w *Wallet) ProgramHash() common.Uint160 { + return w.account.ProgramHash +} + +// SignTransaction signs an unsigned transaction using this wallet's key pair. +func (w *Wallet) SignTransaction(tx *transaction.Transaction) error { ct, err := program.CreateSignatureProgramContext(w.account.PublicKey) if err != nil { return err @@ -221,226 +226,113 @@ func (w *Wallet) signTransaction(tx *transaction.Transaction) error { return nil } -// SendRawTransaction sends a signed transaction to blockchain and returns -// the hex string of transaction hash. -func (w *Wallet) SendRawTransaction(txn *transaction.Transaction) (string, error) { - return SendRawTransaction(txn, w.config) +// NewNanoPay is a shortcut for NewNanoPay using this wallet as sender. +// +// Duration is changed to signed int for gomobile compatibility. +func (w *Wallet) NewNanoPay(recipientAddress, fee string, duration int) (*NanoPay, error) { + return NewNanoPay(w, w, recipientAddress, fee, duration) } -// GetNonce returns the next nonce of this wallet to use. If txPool is false, -// result only counts transactions in ledger; if txPool is true, transactions in -// txPool are also counted. -// -// Nonce is changed to signed int for gomobile compatibility. +// NewNanoPayClaimer is a shortcut for NewNanoPayClaimer using this wallet as +// RPC client. +func (w *Wallet) NewNanoPayClaimer(recipientAddress string, claimIntervalMs int32, onError *OnError) (*NanoPayClaimer, error) { + if len(recipientAddress) == 0 { + recipientAddress = w.Address() + } + return NewNanoPayClaimer(recipientAddress, claimIntervalMs, onError, w) +} + +// GetNonce is the same as package level GetNonce, but using this wallet's +// SeedRPCServerAddr. func (w *Wallet) GetNonce(txPool bool) (int64, error) { - return GetNonce(w.address, txPool, w.config) + return w.GetNonceByAddress(w.address, txPool) } -// GetHeight returns the latest block height. +// GetNonceByAddress is the same as package level GetNonce, but using this +// wallet's SeedRPCServerAddr. +func (w *Wallet) GetNonceByAddress(address string, txPool bool) (int64, error) { + return GetNonce(address, txPool, w.config) +} + +// GetHeight is the same as package level GetHeight, but using this wallet's +// SeedRPCServerAddr. func (w *Wallet) GetHeight() (int32, error) { return GetHeight(w.config) } -// Balance returns the balance of this wallet. +// Balance is the same as package level GetBalance, but using this wallet's +// SeedRPCServerAddr. func (w *Wallet) Balance() (*Amount, error) { return w.BalanceByAddress(w.address) } -// BalanceByAddress returns the balance of a wallet address. +// BalanceByAddress is the same as package level GetBalance, but using this +// wallet's SeedRPCServerAddr. func (w *Wallet) BalanceByAddress(address string) (*Amount, error) { return GetBalance(address, w.config) } -// Transfer sends asset to a wallet address with a transaction fee. Both amount -// and fee are the string representation of the amount in unit of NKN to avoid -// precision loss. For example, "0.1" will be parsed as 0.1 NKN. -func (w *Wallet) Transfer(address, amount, fee string) (string, error) { - amountFixed64, err := common.StringToFixed64(amount) - if err != nil { - return "", err - } - - programHash, err := common.ToScriptHash(address) - if err != nil { - return "", err - } - - _fee, err := common.StringToFixed64(fee) - if err != nil { - return "", err - } - - nonce, err := w.GetNonce(true) - if err != nil { - return "", err - } - - tx, err := transaction.NewTransferAssetTransaction(w.account.ProgramHash, programHash, uint64(nonce), amountFixed64, _fee) - if err != nil { - return "", err - } - - if err := w.signTransaction(tx); err != nil { - return "", err - } - - return w.SendRawTransaction(tx) +// GetSubscribers is the same as package level GetSubscribers, but using this +// wallet's SeedRPCServerAddr. +func (w *Wallet) GetSubscribers(topic string, offset, limit int, meta, txPool bool) (*Subscribers, error) { + return GetSubscribers(topic, offset, limit, meta, txPool, w.config) } -// NewNanoPay is a shortcut for NewNanoPay using this wallet as sender. -// -// Duration is changed to signed int for gomobile compatibility. -func (w *Wallet) NewNanoPay(recipientAddress, fee string, duration int) (*NanoPay, error) { - return NewNanoPay(w, recipientAddress, fee, duration) +// GetSubscription is the same as package level GetSubscription, but using this +// wallet's SeedRPCServerAddr. +func (w *Wallet) GetSubscription(topic string, subscriber string) (*Subscription, error) { + return GetSubscription(topic, subscriber, w.config) } -// NewNanoPayClaimer is a shortcut for NewNanoPayClaimer using this wallet's RPC -// server address. -func (w *Wallet) NewNanoPayClaimer(recipientAddress string, claimIntervalMs int32, onError *OnError) (*NanoPayClaimer, error) { - if len(recipientAddress) == 0 { - recipientAddress = w.Address() - } - return NewNanoPayClaimer(recipientAddress, claimIntervalMs, onError, w.config) +// GetSubscribersCount is the same as package level GetSubscribersCount, but +// this wallet's SeedRPCServerAddr. +func (w *Wallet) GetSubscribersCount(topic string) (int, error) { + return GetSubscribersCount(topic, w.config) } -// RegisterName registers a name for this wallet's public key at the cost of 10 -// NKN with a given transaction fee. The name will be valid for 1,576,800 blocks -// (around 1 year). Register name currently owned by this wallet will extend the -// duration of the name to current block height + 1,576,800. Registration will -// fail if the name is currently owned by another account. -func (w *Wallet) RegisterName(name, fee string) (string, error) { - _fee, err := common.StringToFixed64(fee) - if err != nil { - return "", err - } - - nonce, err := w.GetNonce(true) - if err != nil { - return "", err - } - - tx, err := transaction.NewRegisterNameTransaction(w.PubKey(), name, uint64(nonce), config.MinNameRegistrationFee, _fee) - if err != nil { - return "", err - } - - if err := w.signTransaction(tx); err != nil { - return "", err - } - - return w.SendRawTransaction(tx) +// GetRegistrant is the same as package level GetRegistrant, but this wallet's +// SeedRPCServerAddr. +func (w *Wallet) GetRegistrant(name string) (*Registrant, error) { + return GetRegistrant(name, w.config) } -// TransferName transfers a name owned by this wallet to another public key with -// a transaction fee. The expiration height of the name will not be changed. -func (w *Wallet) TransferName(name string, recipientPubKey []byte, fee string) (string, error) { - _fee, err := common.StringToFixed64(fee) - if err != nil { - return "", err - } - - nonce, err := w.GetNonce(true) - if err != nil { - return "", err - } - - tx, err := transaction.NewTransferNameTransaction(w.PubKey(), recipientPubKey, name, uint64(nonce), _fee) - if err != nil { - return "", err - } - - if err := w.signTransaction(tx); err != nil { - return "", err - } - - return w.SendRawTransaction(tx) +// SendRawTransaction is the same as package level SendRawTransaction, but using +// this wallet's SeedRPCServerAddr. +func (w *Wallet) SendRawTransaction(txn *transaction.Transaction) (string, error) { + return SendRawTransaction(txn, w.config) } -// DeleteName deletes a name owned by this wallet with a given transaction fee. -func (w *Wallet) DeleteName(name, fee string) (string, error) { - _fee, err := common.StringToFixed64(fee) - if err != nil { - return "", err - } - - nonce, err := w.GetNonce(true) - if err != nil { - return "", err - } +// Transfer is a shortcut for Transfer using this wallet as SignerRPCClient. +func (w *Wallet) Transfer(address, amount string, config *TransactionConfig) (string, error) { + return Transfer(w, address, amount, config) +} - tx, err := transaction.NewDeleteNameTransaction(w.PubKey(), name, uint64(nonce), _fee) - if err != nil { - return "", err - } +// RegisterName is a shortcut for RegisterName using this wallet as +// SignerRPCClient. +func (w *Wallet) RegisterName(name string, config *TransactionConfig) (string, error) { + return RegisterName(w, name, config) +} - if err := w.signTransaction(tx); err != nil { - return "", err - } +// TransferName is a shortcut for TransferName using this wallet as +// SignerRPCClient. +func (w *Wallet) TransferName(name string, recipientPubKey []byte, config *TransactionConfig) (string, error) { + return TransferName(w, name, recipientPubKey, config) +} - return w.SendRawTransaction(tx) +// DeleteName is a shortcut for DeleteName using this wallet as SignerRPCClient. +func (w *Wallet) DeleteName(name string, config *TransactionConfig) (string, error) { + return DeleteName(w, name, config) } -// Subscribe to a topic with an identifier for a number of blocks. Client using -// the same key pair and identifier will be able to receive messages from this -// topic. If this (identifier, public key) pair is already subscribed to this -// topic, the subscription expiration will be extended to current block height + -// duration. +// Subscribe is a shortcut for Subscribe using this wallet as SignerRPCClient. // // Duration is changed to signed int for gomobile compatibility. -func (w *Wallet) Subscribe(identifier, topic string, duration int, meta, fee string) (string, error) { - _fee, err := common.StringToFixed64(fee) - if err != nil { - return "", err - } - nonce, err := w.GetNonce(true) - if err != nil { - return "", err - } - tx, err := transaction.NewSubscribeTransaction( - w.PubKey(), - identifier, - topic, - uint32(duration), - meta, - uint64(nonce), - _fee, - ) - if err != nil { - return "", err - } - - if err := w.signTransaction(tx); err != nil { - return "", err - } - - return w.SendRawTransaction(tx) +func (w *Wallet) Subscribe(identifier, topic string, duration int, meta string, config *TransactionConfig) (string, error) { + return Subscribe(w, identifier, topic, duration, meta, config) } -// Unsubscribe from a topic for an identifier. Client using the same key pair -// and identifier will no longer receive messages from this topic. -func (w *Wallet) Unsubscribe(identifier string, topic string, fee string) (string, error) { - _fee, err := common.StringToFixed64(fee) - if err != nil { - return "", err - } - nonce, err := w.GetNonce(true) - if err != nil { - return "", err - } - tx, err := transaction.NewUnsubscribeTransaction( - w.PubKey(), - identifier, - topic, - uint64(nonce), - _fee, - ) - if err != nil { - return "", err - } - - if err := w.signTransaction(tx); err != nil { - return "", err - } - - return w.SendRawTransaction(tx) +// Unsubscribe is a shortcut for Unsubscribe using this wallet as +// SignerRPCClient. +func (w *Wallet) Unsubscribe(identifier, topic string, config *TransactionConfig) (string, error) { + return Unsubscribe(w, identifier, topic, config) }