From 5e70dcafd7089732b8d7a6f7ee319058f11b5d77 Mon Sep 17 00:00:00 2001 From: Anh Minh <1phamminh0811@gmail.com> Date: Sat, 11 Oct 2025 13:04:57 +0700 Subject: [PATCH] add tests for service --- go.mod | 4 + go.sum | 1 + wallet/service.go | 10 +- wallet/service_test.go | 426 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 439 insertions(+), 2 deletions(-) create mode 100644 wallet/service_test.go diff --git a/go.mod b/go.mod index 5a310c3..1b3ff0c 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,18 @@ go 1.24.5 require ( github.com/btcsuite/btcd/btcutil v1.1.6 github.com/cosmos/go-bip39 v1.0.0 + github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.39.0 google.golang.org/grpc v1.75.1 google.golang.org/protobuf v1.36.9 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.26.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ca2144c..3708ba7 100644 --- a/go.sum +++ b/go.sum @@ -140,6 +140,7 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/wallet/service.go b/wallet/service.go index a9132f1..cdcf7ab 100644 --- a/wallet/service.go +++ b/wallet/service.go @@ -108,9 +108,9 @@ func (h *GprcHandler) ImportKeys(ctx context.Context, req *nockchain.ImportKeysR } chainCode := make([]byte, 32) - copy(chainCode, data[13:45]) + copy(chainCode, data[12:44]) publicKey := make([]byte, 97) - copy(publicKey, data[45:141]) + copy(publicKey, data[44:141]) return &nockchain.ImportKeysResponse{ Seed: "", PrivateKey: "", @@ -128,7 +128,13 @@ func (h *GprcHandler) ImportKeys(ctx context.Context, req *nockchain.ImportKeysR return nil, fmt.Errorf("master key must be in [chain_code],[key] format") } chainCode := base58.Decode(splits[0]) + if len(chainCode) != 32 { + return nil, fmt.Errorf("invalid chain code length: %d, must be 32", len(chainCode)) + } key := base58.Decode(splits[1]) + if len(key) != 32 { + return nil, fmt.Errorf("invalid priv key length: %d, must be 32", len(key)) + } masterKey, err := crypto.MasterKeyFromPrivKey(chainCode, key) if err != nil { return nil, err diff --git a/wallet/service_test.go b/wallet/service_test.go new file mode 100644 index 0000000..085b33f --- /dev/null +++ b/wallet/service_test.go @@ -0,0 +1,426 @@ +package wallet_test + +import ( + "context" + "math/big" + "slices" + "testing" + + "github.com/btcsuite/btcd/btcutil/base58" + "github.com/cosmos/go-bip39" + "github.com/phamminh0811/private-grpc/crypto" + "github.com/phamminh0811/private-grpc/nockchain" + "github.com/phamminh0811/private-grpc/wallet" + "github.com/stretchr/testify/assert" +) + +// The entropy, salt and result is taken from "nockchain-wallet keygen" command +func TestKeyGen(t *testing.T) { + entropyBigInt, isOk := new(big.Int).SetString("29615235796517918707367078072007441124337225858809749976291970867443501879006", 10) + assert.True(t, isOk) + + entropy := entropyBigInt.Bytes() + assert.Len(t, entropy, 32) + + saltBigInt, isOk := new(big.Int).SetString("212311808188922973323281316240858086116", 10) + assert.True(t, isOk) + + salt := saltBigInt.Bytes() + assert.Len(t, salt, 16) + + argonBytes := crypto.DeriveKey(0, entropy[:], salt[:], nil, nil, 6, 786432, 4, 32) + slices.Reverse(argonBytes) + mnemonic, err := bip39.NewMnemonic(argonBytes) + assert.NoError(t, err) + assert.Equal(t, mnemonic, "brass vacuum stairs hurt brisk govern describe enforce fly exact rescue capable belt flavor lottery sauce easy frame orange legal injury border obey novel") + + masterKey, err := crypto.MasterKeyFromSeed(mnemonic) + assert.NoError(t, err) + + assert.Equal(t, + base58.Encode(masterKey.PublicKey), + "39DL6YA1kSRCKMjzpFEtC8rmnxVuseUrP2LnViwY7YEhZYZkX2HmnAZ63Uwy1DwuXstmF1VeJDucg719xw49j9CKL3bsKq3A6SZN918CowcgQroHsgohj7dYgpGRWk41s42F", + ) + assert.Equal(t, + base58.Encode(masterKey.PrivateKey), + "4SyUrsbGKPRknzvGakWmFbYefzHzb1r4LUmJpQD8WPcR", + ) + assert.Equal(t, + base58.Encode(masterKey.ChainCode), + "58SARPmADHvUcpq7XfBoCgwzy5QC8Kb3JrezpHqA85x2", + ) + + // assert import priv/pubkey + privBytes := append([]byte{0x00}, masterKey.PrivateKey...) + importPrivKey := crypto.SerializeExtend(masterKey.ChainCode, privBytes, crypto.PrivateKeyStart) + assert.Len(t, importPrivKey, 82) + importPubKey := crypto.SerializeExtend(masterKey.ChainCode, masterKey.PublicKey, crypto.PublicKeyStart) + assert.Len(t, importPubKey, 145) + + assert.Equal(t, + base58.Encode(importPrivKey), + "zprv2CyrSHEkzQzu4HCtJRFiP4t2rVMauZwLfJDFrNbqS8Pz3nsmXy5bAUx2HYUykaMuU4MiQTHsDcKYjLCjrPfpceNT9XBHgx1pUjKzBrF6Wdo", + ) + assert.Equal(t, + base58.Encode(importPubKey), + "zpubUQwNTNE3hsCkMpBnD37W5QirkyVryokAVPLnPin1c6M13RRsq3yEJbwp5ies6qXF6DvJq5Woxw6ygT53PSVrmrsQgtHhbfMEixKNFm7qb4mELhpyoovpFEV1YPHFZx4xQGYBNF6qvXU6AHNh4TLrUdkYAdXKS2J5rPiSVPrXKGo8fLG6ZBCGBjJfPcwDb2VEJC", + ) +} + +func TestImportKey(t *testing.T) { + type Input struct { + req *nockchain.ImportKeysRequest + expectResp *nockchain.ImportKeysResponse + isErr bool + errStr string + } + + correctImportPrivKey := base58.Decode("zprv2CyrSHEkzQzu4HCtJRFiP4t2rVMauZwLfJDFrNbqS8Pz3nsmXy5bAUx2HYUykaMuU4MiQTHsDcKYjLCjrPfpceNT9XBHgx1pUjKzBrF6Wdo") + invalidImportPrivKeyPrefix := make([]byte, 82) + copy(invalidImportPrivKeyPrefix[:], correctImportPrivKey) + invalidImportPrivKeyPrefix[45] = 0x01 + + invalidImportPrivKeyChecksum := make([]byte, 82) + copy(invalidImportPrivKeyChecksum[:], correctImportPrivKey) + copy(invalidImportPrivKeyChecksum[78:], []byte{1, 2, 3, 4}) + + correctImportPubkey := base58.Decode("zpubUQwNTNE3hsCkMpBnD37W5QirkyVryokAVPLnPin1c6M13RRsq3yEJbwp5ies6qXF6DvJq5Woxw6ygT53PSVrmrsQgtHhbfMEixKNFm7qb4mELhpyoovpFEV1YPHFZx4xQGYBNF6qvXU6AHNh4TLrUdkYAdXKS2J5rPiSVPrXKGo8fLG6ZBCGBjJfPcwDb2VEJC") + invalidImportPubkeyChecksum := make([]byte, 145) + copy(invalidImportPubkeyChecksum[:], correctImportPubkey) + copy(invalidImportPubkeyChecksum[141:], []byte{1, 2, 3, 4}) + + response := &nockchain.ImportKeysResponse{ + PublicKey: "39DL6YA1kSRCKMjzpFEtC8rmnxVuseUrP2LnViwY7YEhZYZkX2HmnAZ63Uwy1DwuXstmF1VeJDucg719xw49j9CKL3bsKq3A6SZN918CowcgQroHsgohj7dYgpGRWk41s42F", + PrivateKey: "4SyUrsbGKPRknzvGakWmFbYefzHzb1r4LUmJpQD8WPcR", + ChainCode: "58SARPmADHvUcpq7XfBoCgwzy5QC8Kb3JrezpHqA85x2", + ImportPrivateKey: base58.Encode(correctImportPrivKey), + ImportPublicKey: base58.Encode(correctImportPubkey), + } + + responseReadOnly := &nockchain.ImportKeysResponse{ + PublicKey: "39DL6YA1kSRCKMjzpFEtC8rmnxVuseUrP2LnViwY7YEhZYZkX2HmnAZ63Uwy1DwuXstmF1VeJDucg719xw49j9CKL3bsKq3A6SZN918CowcgQroHsgohj7dYgpGRWk41s42F", + PrivateKey: "", + ChainCode: "58SARPmADHvUcpq7XfBoCgwzy5QC8Kb3JrezpHqA85x2", + ImportPrivateKey: "", + ImportPublicKey: base58.Encode(correctImportPubkey), + } + inputs := []Input{ + // case invalid type + { + req: &nockchain.ImportKeysRequest{ + Key: "", + ImportType: nockchain.ImportType_UNDEFINED, + }, + expectResp: nil, + isErr: true, + errStr: "invalid import type", + }, + // case invalid extended key + { + req: &nockchain.ImportKeysRequest{ + Key: "some wrong string", + ImportType: nockchain.ImportType_EXTENDED_KEY, + }, + expectResp: nil, + isErr: true, + errStr: "invalid extended key", + }, + { + req: &nockchain.ImportKeysRequest{ + Key: "zprv wrong priv import key length", + ImportType: nockchain.ImportType_EXTENDED_KEY, + }, + expectResp: nil, + isErr: true, + errStr: "invalid extended private key length", + }, + { + req: &nockchain.ImportKeysRequest{ + Key: base58.Encode(invalidImportPrivKeyPrefix), + ImportType: nockchain.ImportType_EXTENDED_KEY, + }, + expectResp: nil, + isErr: true, + errStr: "invalid private key prefix at byte 45", + }, + { + req: &nockchain.ImportKeysRequest{ + Key: base58.Encode(invalidImportPrivKeyChecksum), + ImportType: nockchain.ImportType_EXTENDED_KEY, + }, + expectResp: nil, + isErr: true, + errStr: "invalid checksum", + }, + // case success import priv key + { + req: &nockchain.ImportKeysRequest{ + Key: base58.Encode(correctImportPrivKey), + ImportType: nockchain.ImportType_EXTENDED_KEY, + }, + expectResp: response, + isErr: false, + errStr: "", + }, + // case invalid import pub key + { + req: &nockchain.ImportKeysRequest{ + Key: "zpub wrong public import key length", + ImportType: nockchain.ImportType_EXTENDED_KEY, + }, + expectResp: nil, + isErr: true, + errStr: "invalid extended public key length", + }, + { + req: &nockchain.ImportKeysRequest{ + Key: base58.Encode(invalidImportPubkeyChecksum), + ImportType: nockchain.ImportType_EXTENDED_KEY, + }, + expectResp: nil, + isErr: true, + errStr: "invalid checksum", + }, + // case success import pub key + { + req: &nockchain.ImportKeysRequest{ + Key: base58.Encode(correctImportPubkey), + ImportType: nockchain.ImportType_EXTENDED_KEY, + }, + expectResp: responseReadOnly, + isErr: false, + errStr: "", + }, + // case missing chaincode when import master privkey + { + req: &nockchain.ImportKeysRequest{ + Key: "4SyUrsbGKPRknzvGakWmFbYefzHzb1r4LUmJpQD8WPcR", + ImportType: nockchain.ImportType_MASTER_PRIVKEY, + }, + expectResp: nil, + isErr: true, + errStr: "master key must be in [chain_code],[key] format", + }, + // case invalid length + { + req: &nockchain.ImportKeysRequest{ + Key: "abcdxyz,4SyUrsbGKPRknzvGakWmFbYefzHzb1r4LUmJpQD8WPcR", + ImportType: nockchain.ImportType_MASTER_PRIVKEY, + }, + expectResp: nil, + isErr: true, + errStr: "invalid chain code length", + }, + { + req: &nockchain.ImportKeysRequest{ + Key: "58SARPmADHvUcpq7XfBoCgwzy5QC8Kb3JrezpHqA85x2,abcdxyz", + ImportType: nockchain.ImportType_MASTER_PRIVKEY, + }, + expectResp: nil, + isErr: true, + errStr: "invalid priv key length", + }, + // case success import master privkey + { + req: &nockchain.ImportKeysRequest{ + Key: "58SARPmADHvUcpq7XfBoCgwzy5QC8Kb3JrezpHqA85x2,4SyUrsbGKPRknzvGakWmFbYefzHzb1r4LUmJpQD8WPcR", + ImportType: nockchain.ImportType_MASTER_PRIVKEY, + }, + expectResp: response, + isErr: false, + errStr: "", + }, + // case success import seed + { + req: &nockchain.ImportKeysRequest{ + Key: "brass vacuum stairs hurt brisk govern describe enforce fly exact rescue capable belt flavor lottery sauce easy frame orange legal injury border obey novel", + ImportType: nockchain.ImportType_SEEDPHRASE, + }, + expectResp: response, + isErr: false, + errStr: "", + }, + // case sucess import pubkey + { + req: &nockchain.ImportKeysRequest{ + Key: "39DL6YA1kSRCKMjzpFEtC8rmnxVuseUrP2LnViwY7YEhZYZkX2HmnAZ63Uwy1DwuXstmF1VeJDucg719xw49j9CKL3bsKq3A6SZN918CowcgQroHsgohj7dYgpGRWk41s42F", + ImportType: nockchain.ImportType_WATCH_ONLY, + }, + expectResp: &nockchain.ImportKeysResponse{ + PublicKey: "39DL6YA1kSRCKMjzpFEtC8rmnxVuseUrP2LnViwY7YEhZYZkX2HmnAZ63Uwy1DwuXstmF1VeJDucg719xw49j9CKL3bsKq3A6SZN918CowcgQroHsgohj7dYgpGRWk41s42F", + }, + isErr: false, + errStr: "", + }, + } + + for _, input := range inputs { + nc, err := wallet.NewNockchainClient("nockchain-api.zorp.io:443") + assert.NoError(t, err) + + handler := wallet.NewGprcHandler(*nc) + + resp, err := handler.ImportKeys(context.Background(), input.req) + if input.isErr { + assert.ErrorContains(t, err, input.errStr) + } else { + assert.NoError(t, err) + assert.Equal(t, resp, input.expectResp) + } + } +} + +// This test should be run with timeout 120s +func TestScan(t *testing.T) { + // some random seed we take to get empty notes + seed1 := "foster chicken claw fade income frown junior abandon price lesson mango wrap dry clay loyal camera caught during property useless puppy royal soccer arm" + seed2 := "brass vacuum stairs hurt brisk govern describe enforce fly exact rescue capable belt flavor lottery sauce easy frame orange legal injury border obey novel" + + masterKey1, err := crypto.MasterKeyFromSeed(seed1) + assert.NoError(t, err) + + nc, err := wallet.NewNockchainClient("nockchain-api.zorp.io:443") + assert.NoError(t, err) + + masterKey1Scan, err := nc.WalletGetBalance(base58.Encode(masterKey1.PublicKey)) + assert.NoError(t, err) + assert.Empty(t, masterKey1Scan.Notes) + + masterKey2, err := crypto.MasterKeyFromSeed(seed2) + assert.NoError(t, err) + + masterKey2Scan, err := nc.WalletGetBalance(base58.Encode(masterKey2.PublicKey)) + assert.NoError(t, err) + assert.Len(t, masterKey2Scan.Notes, 1) + assert.Equal(t, masterKey2Scan.Notes[0].Note.Assets.Value, uint64(100000)) + assert.Equal(t, masterKey2Scan.Notes[0].Note.OriginPage.Value, uint64(35054)) +} + +// This test should be run with timeout 120s +func TestFullFlow(t *testing.T) { + mnemonic1 := "rail nurse smile angle uphold gun kitten spoon quick frozen trigger cable decorate episode blame tray off bag arena taxi approve breeze job letter" + masterKey1, err := crypto.MasterKeyFromSeed(mnemonic1) + assert.NoError(t, err) + + mnemonic2 := "brass vacuum stairs hurt brisk govern describe enforce fly exact rescue capable belt flavor lottery sauce easy frame orange legal injury border obey novel" + masterKey2, err := crypto.MasterKeyFromSeed(mnemonic2) + assert.NoError(t, err) + + inputName := "[4taoqkpysafnp64WBQyzHDKVrqkMeNrdAiVSbWdzZmj7yQYZgQtCq4W 9cjUFbdtaFHeXNWCAKjsTphBchHmCoUU6a1aDbJAFz9qHqeG8osh4wF]" + + nc, err := wallet.NewNockchainClient("nockchain-api.zorp.io:443") + assert.NoError(t, err) + + masterKeyScan, err := nc.WalletGetBalance(base58.Encode(masterKey1.PublicKey)) + assert.NoError(t, err) + + var note *nockchain.NockchainNote + for _, balanceEntry := range masterKeyScan.Notes { + firstName := crypto.Tip5HashToBase58([5]uint64{ + balanceEntry.Name.First.Belt_1.Value, + balanceEntry.Name.First.Belt_2.Value, + balanceEntry.Name.First.Belt_3.Value, + balanceEntry.Name.First.Belt_4.Value, + balanceEntry.Name.First.Belt_5.Value, + }) + lastName := crypto.Tip5HashToBase58([5]uint64{ + balanceEntry.Name.Last.Belt_1.Value, + balanceEntry.Name.Last.Belt_2.Value, + balanceEntry.Name.Last.Belt_3.Value, + balanceEntry.Name.Last.Belt_4.Value, + balanceEntry.Name.Last.Belt_5.Value, + }) + nname := "[" + firstName + " " + lastName + "]" + if nname == inputName { + nnote := wallet.ParseBalanceEntry(balanceEntry) + note = &nnote + break + } + } + assert.NotNil(t, note) + + parentHash, err := wallet.HashNote(note) + assert.NoError(t, err) + + gift := uint64(100000) + seed1 := &nockchain.NockchainSeed{ + OutputSource: nil, + Recipient: &nockchain.NockchainLock{ + KeysRequired: 1, + Pubkeys: []string{base58.Encode(masterKey2.PublicKey)}, + }, + TimelockIntent: nil, + Gift: gift, + ParentHash: crypto.Tip5HashToBase58(parentHash), + } + + gift = note.Asset - gift - 100 + seed2 := &nockchain.NockchainSeed{ + OutputSource: nil, + Recipient: &nockchain.NockchainLock{ + KeysRequired: 1, + Pubkeys: []string{base58.Encode(masterKey1.PublicKey)}, + }, + TimelockIntent: nil, + Gift: gift, + ParentHash: crypto.Tip5HashToBase58(parentHash), + } + + spend := nockchain.NockchainSpend{ + Signatures: nil, + Seeds: []*nockchain.NockchainSeed{ + seed1, seed2, + }, + Fee: 100, + } + + msg, err := wallet.HashMsg(&spend) + assert.NoError(t, err) + + chalT8, sigT8, err := wallet.ComputeSig(*masterKey1, msg) + assert.NoError(t, err) + + spend.Signatures = []*nockchain.NockchainSignature{ + { + Pubkey: base58.Encode(masterKey1.PublicKey), + Chal: chalT8[:], + Sig: sigT8[:], + }, + } + + input := nockchain.NockchainInput{ + Name: &nockchain.NockchainName{ + First: "4taoqkpysafnp64WBQyzHDKVrqkMeNrdAiVSbWdzZmj7yQYZgQtCq4W", + Last: "9cjUFbdtaFHeXNWCAKjsTphBchHmCoUU6a1aDbJAFz9qHqeG8osh4wF", + }, + Note: note, + Spend: &spend, + } + + id, err := wallet.ComputeTxId([]*nockchain.NockchainInput{&input}, &nockchain.TimelockRange{ + Min: nil, + Max: nil, + }, 100) + assert.NoError(t, err) + + rawTx := nockchain.RawTx{ + TxId: crypto.Tip5HashToBase58(id), + Inputs: []*nockchain.NockchainInput{&input}, + TimelockRange: &nockchain.TimelockRange{ + Min: nil, + Max: nil, + }, + TotalFees: 100, + } + resp, err := nc.WalletSendTransaction(&rawTx) + assert.NoError(t, err) + assert.Equal(t, resp.Result, &nockchain.WalletSendTransactionResponse_Ack{ + Ack: &nockchain.Acknowledged{}, + }) + + txAcceptedResp, err := nc.TxAccepted(rawTx.TxId) + assert.NoError(t, err) + assert.Equal(t, txAcceptedResp.Result, &nockchain.TransactionAcceptedResponse_Accepted{ + Accepted: true, + }) +}