Skip to content

Commit

Permalink
Adding LoadAccountWithPrivateKey, GetTransactionDetailsWithWait, Call…
Browse files Browse the repository at this point in the history
… and some other helper methods (#21)
  • Loading branch information
kondakovdmitry authored Jun 10, 2024
1 parent 785e2c5 commit b19fb5e
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 37 deletions.
40 changes: 33 additions & 7 deletions account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -59,19 +60,19 @@ 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
}

// 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
}
Expand All @@ -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)
Expand Down
56 changes: 27 additions & 29 deletions keystore/keystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions near.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
@@ -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
)
44 changes: 43 additions & 1 deletion utils/key_pair.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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)
}

0 comments on commit b19fb5e

Please sign in to comment.