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) }