From 4c7f797678b5e2d199273b8cbb38900339aee8fd Mon Sep 17 00:00:00 2001
From: Greg Nazario <greg@gnazar.io>
Date: Fri, 7 Feb 2025 23:07:03 -0500
Subject: [PATCH 1/2] [account] Improve account experience

1. Change authkey input to accounts to address
2. Allow for extracting private key from accounts
---
 CHANGELOG.md                          |   2 +
 account.go                            |   4 +-
 internal/types/account.go             |  44 ++++++++-
 internal/types/accountAddress_test.go |   8 ++
 internal/types/account_test.go        | 127 +++++++++++++++++++++++++-
 5 files changed, 174 insertions(+), 11 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index da919ce..0287e8f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ adheres to the format set out by [Keep a Changelog](https://keepachangelog.com/e
 - [`Fix`] Make NodeClient match AptosRpcClient interface
 - [`Dependency`] Update `golang.org/x/crypto` to `v0.32.0`
 - [`Dependency`] Update `github.com/hasura/go-graphql-client` to `v0.13.1`
+- [`Fix`] Make NodeClient satisfy AptosRpcClient interface
+- [`Breaking`] Change Account signer input to use Address instead of AuthKey, authkey comes from the private key
 
 # v1.4.1 (01/02/2024)
 
diff --git a/account.go b/account.go
index cf04d32..51bd915 100644
--- a/account.go
+++ b/account.go
@@ -30,8 +30,8 @@ var AccountThree = types.AccountThree
 var AccountFour = types.AccountFour
 
 // NewAccountFromSigner creates an account from a Signer, which is most commonly a private key
-func NewAccountFromSigner(signer crypto.Signer, authKey ...crypto.AuthenticationKey) (*Account, error) {
-	return types.NewAccountFromSigner(signer, authKey...)
+func NewAccountFromSigner(signer crypto.Signer, accountAddress ...AccountAddress) (*Account, error) {
+	return types.NewAccountFromSigner(signer, accountAddress...)
 }
 
 // NewEd25519Account creates a legacy Ed25519 account, this is most commonly used in wallets
diff --git a/internal/types/account.go b/internal/types/account.go
index 7db9229..c916a56 100644
--- a/internal/types/account.go
+++ b/internal/types/account.go
@@ -17,11 +17,11 @@ type Account struct {
 }
 
 // NewAccountFromSigner creates an account from a [crypto.Signer] with an optional [crypto.AuthenticationKey]
-func NewAccountFromSigner(signer crypto.Signer, authKey ...crypto.AuthenticationKey) (*Account, error) {
+func NewAccountFromSigner(signer crypto.Signer, address ...AccountAddress) (*Account, error) {
 	out := &Account{}
-	if len(authKey) == 1 {
-		copy(out.Address[:], authKey[0][:])
-	} else if len(authKey) > 1 {
+	if len(address) == 1 {
+		copy(out.Address[:], address[0][:])
+	} else if len(address) > 1 {
 		// Throw error
 		return nil, errors.New("must only provide one auth key")
 	} else {
@@ -60,6 +60,42 @@ func NewSecp256k1Account() (*Account, error) {
 	return NewAccountFromSigner(signer)
 }
 
+// ExtractMessageSigner extracts the message signer from the account for
+func (account *Account) ExtractMessageSigner() (crypto.MessageSigner, bool) {
+	ed25519PrivateKey, ok := account.Signer.(*crypto.Ed25519PrivateKey)
+	if ok {
+		return ed25519PrivateKey, ok
+	}
+	singleSigner, ok := account.Signer.(*crypto.SingleSigner)
+	if ok {
+		return singleSigner.Signer, ok
+	}
+	return nil, false
+}
+
+// ExtractPrivateKeyString extracts the private key string
+func (account *Account) ExtractPrivateKeyString() (string, error) {
+	// Handle the key by itself
+	ed25519PrivateKey, ok := account.Signer.(*crypto.Ed25519PrivateKey)
+	if ok {
+		return ed25519PrivateKey.ToAIP80()
+	}
+
+	// Handle key in single signer
+	singleSigner, ok := account.Signer.(*crypto.SingleSigner)
+	if ok {
+		innerSigner := singleSigner.Signer
+		switch innerSigner.(type) {
+		case *crypto.Ed25519PrivateKey:
+			return innerSigner.(*crypto.Ed25519PrivateKey).ToAIP80()
+		case *crypto.Secp256k1PrivateKey:
+			return innerSigner.(*crypto.Secp256k1PrivateKey).ToAIP80()
+		}
+	}
+
+	return "", errors.New("signer is not a private key")
+}
+
 // Sign signs a message, returning an appropriate authenticator for the signer
 func (account *Account) Sign(message []byte) (authenticator *crypto.AccountAuthenticator, err error) {
 	return account.Signer.Sign(message)
diff --git a/internal/types/accountAddress_test.go b/internal/types/accountAddress_test.go
index 433978e..02c26a4 100644
--- a/internal/types/accountAddress_test.go
+++ b/internal/types/accountAddress_test.go
@@ -3,6 +3,7 @@ package types
 import (
 	"encoding/json"
 	"github.com/aptos-labs/aptos-go-sdk/bcs"
+	"github.com/aptos-labs/aptos-go-sdk/crypto"
 	"github.com/stretchr/testify/assert"
 	"testing"
 )
@@ -19,6 +20,13 @@ func TestAccountSpecialString(t *testing.T) {
 	assert.Equal(t, aa, aa2)
 }
 
+func TestAccountAddress_AuthKey(t *testing.T) {
+	authKey := &crypto.AuthenticationKey{}
+	var aa AccountAddress
+	aa.FromAuthKey(authKey)
+	assert.Equal(t, AccountZero, aa)
+}
+
 func TestSpecialAddresses(t *testing.T) {
 	var addr AccountAddress
 	err := addr.ParseStringRelaxed("0x0")
diff --git a/internal/types/account_test.go b/internal/types/account_test.go
index 5d3172a..fb7a5e3 100644
--- a/internal/types/account_test.go
+++ b/internal/types/account_test.go
@@ -1,6 +1,7 @@
 package types
 
 import (
+	"errors"
 	"github.com/aptos-labs/aptos-go-sdk/crypto"
 	"github.com/stretchr/testify/assert"
 	"testing"
@@ -22,6 +23,26 @@ func TestGenerateEd25519Account(t *testing.T) {
 	assert.True(t, output.Auth.Verify(message))
 }
 
+func TestGenerateSingleSignerEd25519Account(t *testing.T) {
+	message := []byte{0x12, 0x34}
+	account, err := NewEd25519SingleSignerAccount()
+	assert.NoError(t, err)
+	output, err := account.Sign(message)
+	assert.NoError(t, err)
+	assert.Equal(t, crypto.AccountAuthenticatorSingleSender, output.Variant)
+	assert.True(t, output.Auth.Verify(message))
+}
+
+func TestGenerateSecp256k1Account(t *testing.T) {
+	message := []byte{0x12, 0x34}
+	account, err := NewSecp256k1Account()
+	assert.NoError(t, err)
+	output, err := account.Sign(message)
+	assert.NoError(t, err)
+	assert.Equal(t, crypto.AccountAuthenticatorSingleSender, output.Variant)
+	assert.True(t, output.Auth.Verify(message))
+}
+
 func TestNewAccountFromSigner(t *testing.T) {
 	message := []byte{0x12, 0x34}
 	key, err := crypto.GenerateEd25519PrivateKey()
@@ -43,24 +64,120 @@ func TestNewAccountFromSignerWithAddress(t *testing.T) {
 	key, err := crypto.GenerateEd25519PrivateKey()
 	assert.NoError(t, err)
 
-	authenticationKey := crypto.AuthenticationKey{}
-
-	account, err := NewAccountFromSigner(key, authenticationKey)
+	account, err := NewAccountFromSigner(key, AccountZero)
 	assert.NoError(t, err)
 	output, err := account.Sign(message)
 	assert.NoError(t, err)
 	assert.Equal(t, crypto.AccountAuthenticatorEd25519, output.Variant)
 	assert.True(t, output.Auth.Verify(message))
 
+	outputSig, err := account.SignMessage(message)
+	assert.NoError(t, err)
+	assert.True(t, account.Signer.PubKey().Verify(message, outputSig))
+
 	assert.Equal(t, AccountZero, account.Address)
+	assert.Equal(t, AccountZero, account.AccountAddress())
+	assert.Equal(t, key.AuthKey(), account.AuthKey())
+	assert.Equal(t, key.PubKey(), account.PubKey())
 }
 
 func TestNewAccountFromSignerWithAddressMulti(t *testing.T) {
 	key, err := crypto.GenerateEd25519PrivateKey()
 	assert.NoError(t, err)
 
-	authenticationKey := crypto.AuthenticationKey{}
+	_, err = NewAccountFromSigner(key, AccountZero, AccountOne)
+	assert.Error(t, err)
+}
+
+type WrapperSigner struct {
+	signer crypto.Signer
+}
+
+func (w *WrapperSigner) Sign(_ []byte) (*crypto.AccountAuthenticator, error) {
+	return nil, errors.New("not implemented")
+}
+func (w *WrapperSigner) SignMessage(_ []byte) (crypto.Signature, error) {
+	return nil, errors.New("not implemented")
+}
+func (w *WrapperSigner) SimulationAuthenticator() *crypto.AccountAuthenticator {
+	return nil
+}
+func (w *WrapperSigner) AuthKey() *crypto.AuthenticationKey {
+	return &crypto.AuthenticationKey{}
+}
+func (w *WrapperSigner) PubKey() crypto.PublicKey {
+	// Note this is just for testing
+	return &crypto.Ed25519PublicKey{}
+}
+
+func TestAccount_ExtractMessageSigner(t *testing.T) {
+	ed25519PrivateKey, err := crypto.GenerateEd25519PrivateKey()
+	assert.NoError(t, err)
+	ed25519Account, err := NewAccountFromSigner(ed25519PrivateKey)
+	assert.NoError(t, err)
+
+	ed25519Out, ok := ed25519Account.ExtractMessageSigner()
+	assert.True(t, ok)
+	assert.Equal(t, ed25519PrivateKey, ed25519Out)
+
+	ed25519SingleSignerAccount, err := NewAccountFromSigner(crypto.NewSingleSigner(ed25519PrivateKey))
+	assert.NoError(t, err)
+
+	ed25519Out, ok = ed25519SingleSignerAccount.ExtractMessageSigner()
+	assert.True(t, ok)
+	assert.Equal(t, ed25519PrivateKey, ed25519Out)
+
+	secp256k1PrivateKey, err := crypto.GenerateSecp256k1Key()
+	assert.NoError(t, err)
+	secp256k1SingleSignerAccount, err := NewAccountFromSigner(crypto.NewSingleSigner(secp256k1PrivateKey))
+	assert.NoError(t, err)
+
+	secp256k1Out, ok := secp256k1SingleSignerAccount.ExtractMessageSigner()
+	assert.True(t, ok)
+	assert.Equal(t, secp256k1PrivateKey, secp256k1Out)
+
+	wrapperSigner := &WrapperSigner{signer: secp256k1SingleSignerAccount}
+	customAccount, err := NewAccountFromSigner(wrapperSigner)
+	assert.NoError(t, err)
+	out, ok := customAccount.ExtractMessageSigner()
+	assert.False(t, ok)
+	assert.Nil(t, out)
+}
+
+func TestAccount_ExtractPrivateKeyString(t *testing.T) {
+	ed25519PrivateKey, err := crypto.GenerateEd25519PrivateKey()
+	assert.NoError(t, err)
+	ed25519Account, err := NewAccountFromSigner(ed25519PrivateKey)
+	assert.NoError(t, err)
 
-	_, err = NewAccountFromSigner(key, authenticationKey, authenticationKey)
+	ed25519KeyString, err := ed25519Account.ExtractPrivateKeyString()
+	assert.NoError(t, err)
+	expectedEd25519String, err := ed25519PrivateKey.ToAIP80()
+	assert.NoError(t, err)
+	assert.Equal(t, expectedEd25519String, ed25519KeyString)
+
+	ed25519SingleSignerAccount, err := NewAccountFromSigner(crypto.NewSingleSigner(ed25519PrivateKey))
+	assert.NoError(t, err)
+
+	ed25519SingleSignerKeyString, err := ed25519SingleSignerAccount.ExtractPrivateKeyString()
+	assert.NoError(t, err)
+	assert.Equal(t, expectedEd25519String, ed25519SingleSignerKeyString)
+
+	secp256k1PrivateKey, err := crypto.GenerateSecp256k1Key()
+	assert.NoError(t, err)
+	secp256k1SingleSignerAccount, err := NewAccountFromSigner(crypto.NewSingleSigner(secp256k1PrivateKey))
+	assert.NoError(t, err)
+
+	expectedSecp256k1String, err := secp256k1PrivateKey.ToAIP80()
+	assert.NoError(t, err)
+	secp256k1SingleSignerKeyString, err := secp256k1SingleSignerAccount.ExtractPrivateKeyString()
+	assert.NoError(t, err)
+	assert.Equal(t, expectedSecp256k1String, secp256k1SingleSignerKeyString)
+
+	wrapperSigner := &WrapperSigner{signer: secp256k1SingleSignerAccount}
+	customAccount, err := NewAccountFromSigner(wrapperSigner)
+	assert.NoError(t, err)
+	out, err := customAccount.ExtractPrivateKeyString()
 	assert.Error(t, err)
+	assert.Empty(t, out)
 }

From 60b12a7c9f12f81067096d26bd860b2f8bb9c847 Mon Sep 17 00:00:00 2001
From: Greg Nazario <greg@gnazar.io>
Date: Fri, 7 Feb 2025 23:14:07 -0500
Subject: [PATCH 2/2] [cleanup] Fix comment and remove PrettyJson from util

---
 internal/util/util.go | 15 +--------------
 1 file changed, 1 insertion(+), 14 deletions(-)

diff --git a/internal/util/util.go b/internal/util/util.go
index 7b016d8..0c8c406 100644
--- a/internal/util/util.go
+++ b/internal/util/util.go
@@ -2,7 +2,6 @@ package util
 
 import (
 	"encoding/hex"
-	"encoding/json"
 	"fmt"
 	"golang.org/x/crypto/sha3"
 	"math/big"
@@ -10,7 +9,7 @@ import (
 	"strings"
 )
 
-// SHA3256Hash hashes the input bytes using SHA3-256
+// Sha3256Hash hashes the input bytes using SHA3-256
 func Sha3256Hash(bytes [][]byte) (output []byte) {
 	hasher := sha3.New256()
 	for _, b := range bytes {
@@ -50,15 +49,3 @@ func StrToBigInt(val string) (num *big.Int, err error) {
 	}
 	return num, nil
 }
-
-// PrettyJson a simple pretty print for JSON examples
-func PrettyJson(x any) string {
-	out := strings.Builder{}
-	enc := json.NewEncoder(&out)
-	enc.SetIndent("", "  ")
-	err := enc.Encode(x)
-	if err != nil {
-		return ""
-	}
-	return out.String()
-}