From b19fb5e1d7055c84e67c3ef589172d75bd4c1a49 Mon Sep 17 00:00:00 2001 From: kondakovdmitry Date: Mon, 10 Jun 2024 18:10:50 +0600 Subject: [PATCH] Adding LoadAccountWithPrivateKey, GetTransactionDetailsWithWait, Call and some other helper methods (#21) --- account.go | 40 +++++++++++++++++++++++++------ keystore/keystore.go | 56 +++++++++++++++++++++----------------------- near.go | 30 ++++++++++++++++++++++++ types.go | 26 ++++++++++++++++++++ utils/key_pair.go | 44 +++++++++++++++++++++++++++++++++- 5 files changed, 159 insertions(+), 37 deletions(-) create mode 100644 types.go diff --git a/account.go b/account.go index 78f12f4..ba761a1 100644 --- a/account.go +++ b/account.go @@ -24,7 +24,9 @@ const txNonceRetryWait = 500 // Exponential back off for waiting to retry. const txNonceRetryWaitBackoff = 1.5 -// Account defines access credentials for a NEAR account. +// Account defines functions to work with a NEAR account. +// Keeps a connection to NEAR JSON-RPC, the account's access keys, and maintains a +// local cache of nonces per account's access key. type Account struct { // NEAR JSON-RPC connection @@ -43,9 +45,8 @@ type Account struct { accessKeyByPublicKeyCache map[string]map[string]interface{} } -// LoadAccount loads the credential for the receiverID account, to be used via -// connection c, and returns it. -func LoadAccount(c *Connection, cfg *Config, receiverID string) (*Account, error) { +// LoadAccount initializes an Account object by loading the account credentials from disk. +func LoadAccount(c *Connection, cfg *Config, accountID string) (*Account, error) { var ( a Account err error @@ -59,11 +60,11 @@ func LoadAccount(c *Connection, cfg *Config, receiverID string) (*Account, error path := cfg.KeyPath if path == "" { // set default path if not defined in config - path = filepath.Join(home, ".near-credentials", cfg.NetworkID, receiverID+".json") + path = filepath.Join(home, ".near-credentials", cfg.NetworkID, accountID+".json") } // set full access key first - a.fullAccessKeyPair, err = keystore.LoadKeyPairFromPath(path, receiverID) + a.fullAccessKeyPair, err = keystore.LoadKeyPairFromPath(path, accountID) if err != nil { return nil, err } @@ -71,7 +72,7 @@ func LoadAccount(c *Connection, cfg *Config, receiverID string) (*Account, error // set function call keys if any keyPairFilePaths := getFunctionCallKeyPairFilePaths(path, cfg.FunctionKeyPrefixPattern) for _, p := range keyPairFilePaths { - keyPair, err = keystore.LoadKeyPairFromPath(p, receiverID) + keyPair, err = keystore.LoadKeyPairFromPath(p, accountID) if err != nil { return nil, err } @@ -82,6 +83,31 @@ func LoadAccount(c *Connection, cfg *Config, receiverID string) (*Account, error return &a, nil } +// LoadAccountWithKeyPair initializes an Account object given its access key pair. +func LoadAccountWithKeyPair(c *Connection, keyPair *keystore.Ed25519KeyPair) *Account { + return &Account{ + conn: c, + fullAccessKeyPair: keyPair, + funcCallKeyPairs: map[string]*keystore.Ed25519KeyPair{ + keyPair.PublicKey: keyPair, + }, + funcCallKeyMutexes: map[string]*sync.Mutex{ + keyPair.PublicKey: {}, + }, + accessKeyByPublicKeyCache: make(map[string]map[string]interface{}), + } +} + +// LoadAccountWithPrivateKey initializes an Account object given its accountID and a private key. +func LoadAccountWithPrivateKey(c *Connection, accountID string, privateKey ed25519.PrivateKey) *Account { + return LoadAccountWithKeyPair(c, keystore.KeyPairFromPrivateKey(accountID, privateKey)) +} + +// AccountID returns sender account ID +func (a *Account) AccountID() string { + return a.fullAccessKeyPair.AccountID +} + // GetVerifiedAccessKeys verifies and returns the public keys of the access keys func (a *Account) GetVerifiedAccessKeys() []string { accessKeys := make([]string, 0) diff --git a/keystore/keystore.go b/keystore/keystore.go index 11949a0..2d98158 100644 --- a/keystore/keystore.go +++ b/keystore/keystore.go @@ -9,13 +9,10 @@ import ( "fmt" "os" "path/filepath" - "strings" - "github.com/btcsuite/btcd/btcutil/base58" + "github.com/aurora-is-near/near-api-go/utils" ) -const ed25519Prefix = "ed25519:" - // Ed25519KeyPair is an Ed25519 key pair. type Ed25519KeyPair struct { AccountID string `json:"account_id"` @@ -28,18 +25,23 @@ type Ed25519KeyPair struct { // GenerateEd25519KeyPair generates a new Ed25519 key pair for accountID. func GenerateEd25519KeyPair(accountID string) (*Ed25519KeyPair, error) { - var ( - kp Ed25519KeyPair - err error - ) - kp.Ed25519PubKey, kp.Ed25519PrivKey, err = ed25519.GenerateKey(rand.Reader) + _, privateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, err } - kp.AccountID = accountID - kp.PublicKey = ed25519Prefix + base58.Encode(kp.Ed25519PubKey) - kp.PrivateKey = ed25519Prefix + base58.Encode(kp.Ed25519PrivKey) - return &kp, nil + return KeyPairFromPrivateKey(accountID, privateKey), nil +} + +// KeyPairFromPrivateKey creates a key-pair given an accountID and a private key. +func KeyPairFromPrivateKey(accountID string, privateKey ed25519.PrivateKey) *Ed25519KeyPair { + publicKey := privateKey.Public().(ed25519.PublicKey) + return &Ed25519KeyPair{ + AccountID: accountID, + Ed25519PubKey: publicKey, + Ed25519PrivKey: privateKey, + PublicKey: utils.Ed25519PublicKeyToString(publicKey), + PrivateKey: utils.Ed25519PrivateKeyToString(privateKey), + } } func (kp *Ed25519KeyPair) write(filename string) error { @@ -80,33 +82,29 @@ func LoadKeyPairFromPath(path, accountID string) (*Ed25519KeyPair, error) { kp.AccountID, accountID) } // public key - if !strings.HasPrefix(kp.PublicKey, ed25519Prefix) { - return nil, fmt.Errorf("keystore: parsed public_key '%s' is not an Ed25519 key", - kp.PublicKey) + kp.Ed25519PubKey, err = utils.Ed25519PublicKeyFromString(kp.PublicKey) + if err != nil { + return nil, fmt.Errorf("keystore: invalid public_key: %w", err) } - pubKey := base58.Decode(strings.TrimPrefix(kp.PublicKey, ed25519Prefix)) - kp.Ed25519PubKey = ed25519.PublicKey(pubKey) // private key - var privateKey []byte + var privateKey ed25519.PrivateKey if len(kp.PrivateKey) > 0 && len(kp.SecretKey) > 0 { return nil, fmt.Errorf("keystore: private_key and secret_key are defined at the same time: %s", path) } else if len(kp.PrivateKey) > 0 { - if !strings.HasPrefix(kp.PrivateKey, ed25519Prefix) { - return nil, fmt.Errorf("keystore: parsed private_key '%s' is not an Ed25519 key", - kp.PrivateKey) + privateKey, err = utils.Ed25519PrivateKeyFromString(kp.PrivateKey) + if err != nil { + return nil, fmt.Errorf("keystore: invalid private_key: %w", err) } - privateKey = base58.Decode(strings.TrimPrefix(kp.PrivateKey, ed25519Prefix)) } else { // secret_key - if !strings.HasPrefix(kp.SecretKey, ed25519Prefix) { - return nil, fmt.Errorf("keystore: parsed secret_key '%s' is not an Ed25519 key", - kp.SecretKey) + privateKey, err = utils.Ed25519PrivateKeyFromString(kp.SecretKey) + if err != nil { + return nil, fmt.Errorf("keystore: invalid secret_key: %w", err) } - privateKey = base58.Decode(strings.TrimPrefix(kp.SecretKey, ed25519Prefix)) } - kp.Ed25519PrivKey = ed25519.PrivateKey(privateKey) + kp.Ed25519PrivKey = privateKey // make sure keys match - if !bytes.Equal(pubKey, kp.Ed25519PrivKey.Public().(ed25519.PublicKey)) { + if !bytes.Equal(kp.Ed25519PubKey, kp.Ed25519PrivKey.Public().(ed25519.PublicKey)) { return nil, fmt.Errorf("keystore: public_key does not match private_key: %s", path) } return &kp, nil diff --git a/near.go b/near.go index dbe8888..fe087bf 100644 --- a/near.go +++ b/near.go @@ -56,6 +56,12 @@ func (c *Connection) call(method string, params ...interface{}) (interface{}, er return res.Result, nil } +// Call performs a generic call of the given NEAR JSON-RPC method with params. +// It handles all possible error cases and returns the result (which cannot be nil). +func (c *Connection) Call(method string, params ...interface{}) (interface{}, error) { + return c.call(method, params...) +} + // Block queries network and returns latest block. // // For details see https://docs.near.org/docs/interaction/rpc#block @@ -261,6 +267,30 @@ func (c *Connection) GetTransactionDetails(txHash, senderAccountId string) (map[ return r, nil } +// GetTransactionDetailsWithWait returns information about a single transaction after waiting for the specified execution status. +// +// For details see +// https://docs.near.org/api/rpc/transactions#transaction-status +func (c *Connection) GetTransactionDetailsWithWait(txHash, senderAccountId string, waitUntil TxExecutionStatus) (map[string]interface{}, error) { + params := map[string]interface{}{ + "tx_hash": txHash, + "sender_account_id": senderAccountId, + "wait_until": waitUntil, + } + + res, err := c.call("tx", params) + if err != nil { + return nil, err + } + + r, ok := res.(map[string]interface{}) + if !ok { + return nil, ErrNotObject + } + + return r, nil +} + // GetTransactionDetailsWithReceipts returns information about a single transaction with receipts // // For details see diff --git a/types.go b/types.go new file mode 100644 index 0000000..d66f13c --- /dev/null +++ b/types.go @@ -0,0 +1,26 @@ +package near + +// Transaction execution status +type TxExecutionStatus string + +const ( + // Transaction is waiting to be included into the block + TxExecutionStatus_None TxExecutionStatus = "NONE" + // Transaction is included into the block. The block may be not finalised yet + TxExecutionStatus_Included TxExecutionStatus = "INCLUDED" + // Transaction is included into the block + + // All non-refund transaction receipts finished their execution. + // The corresponding blocks for tx and each receipt may be not finalised yet + TxExecutionStatus_ExecutedOptimistic TxExecutionStatus = "EXECUTED_OPTIMISTIC" + // Transaction is included into finalised block + TxExecutionStatus_IncludedFinal TxExecutionStatus = "INCLUDED_FINAL" + // Transaction is included into finalised block + + // All non-refund transaction receipts finished their execution. + // The corresponding blocks for each receipt may be not finalised yet + TxExecutionStatus_Executed TxExecutionStatus = "EXECUTED" + // Transaction is included into finalised block + + // Execution of all transaction receipts is finalised, including refund receipts + TxExecutionStatus_Final TxExecutionStatus = "FINAL" + + TxExecutionStatus_Default = TxExecutionStatus_ExecutedOptimistic +) diff --git a/utils/key_pair.go b/utils/key_pair.go index ed99866..abb346c 100644 --- a/utils/key_pair.go +++ b/utils/key_pair.go @@ -1,6 +1,12 @@ package utils -import "crypto/ed25519" +import ( + "crypto/ed25519" + "fmt" + "strings" + + "github.com/btcsuite/btcd/btcutil/base58" +) // All supported key types const ( @@ -20,3 +26,39 @@ func PublicKeyFromEd25519(pk ed25519.PublicKey) PublicKey { copy(pubKey.Data[:], pk) return pubKey } + +const ed25519Prefix = "ed25519:" + +// Ed25519PublicKeyFromString derives an ed25519 public key from its base58 string representation prefixed with 'ed25519:'. +func Ed25519PublicKeyFromString(ed25519PublicKey string) (ed25519.PublicKey, error) { + if !strings.HasPrefix(ed25519PublicKey, ed25519Prefix) { + return nil, fmt.Errorf("'%s' is not an Ed25519 key", ed25519PublicKey) + } + keyBytes := base58.Decode(strings.TrimPrefix(ed25519PublicKey, ed25519Prefix)) + if len(keyBytes) != 32 { + return nil, fmt.Errorf("unexpected byte length for public key '%s'", ed25519PublicKey) + } + return ed25519.PublicKey(keyBytes), nil +} + +// Ed25519PrivateKeyFromString derives an ed25519 private key from its base58 string representation prefixed with 'ed25519:'. +func Ed25519PrivateKeyFromString(ed25519PrivateKey string) (ed25519.PrivateKey, error) { + if !strings.HasPrefix(ed25519PrivateKey, ed25519Prefix) { + return nil, fmt.Errorf("'%s' is not an Ed25519 key", ed25519PrivateKey) + } + keyBytes := base58.Decode(strings.TrimPrefix(ed25519PrivateKey, ed25519Prefix)) + if len(keyBytes) != 64 { + return nil, fmt.Errorf("unexpected byte length for private key '%s'", ed25519PrivateKey) + } + return ed25519.PrivateKey(keyBytes), nil +} + +// Ed25519PublicKeyToString converts ed25519 public key to string. +func Ed25519PublicKeyToString(pk ed25519.PublicKey) string { + return ed25519Prefix + base58.Encode(pk) +} + +// Ed25519PrivateKeyToString converts ed25519 private key to string. +func Ed25519PrivateKeyToString(pk ed25519.PrivateKey) string { + return ed25519Prefix + base58.Encode(pk) +}