package wallet import ( context "context" "crypto/rand" "crypto/sha256" "fmt" "slices" "strconv" "strings" "github.com/btcsuite/btcd/btcutil/base58" "github.com/cosmos/go-bip39" "github.com/phamminh0811/private-grpc/crypto" "github.com/phamminh0811/private-grpc/nockchain" ) type GprcHandler struct { nockchain.UnimplementedWalletServiceServer client NockchainClient } func NewGprcHandler(client NockchainClient) GprcHandler { return GprcHandler{ client: client, } } func (h *GprcHandler) Keygen(ctx context.Context, req *nockchain.KeygenRequest) (*nockchain.KeygenResponse, error) { var entropy [32]byte _, err := rand.Read(entropy[:]) // Fill the slice with random bytes if err != nil { return nil, err } var salt [16]byte _, err = rand.Read(salt[:]) if err != nil { return nil, err } argonBytes := crypto.DeriveKey(0, entropy[:], salt[:], nil, nil, 6, 786432, 4, 32) slices.Reverse(argonBytes) mnemonic, err := bip39.NewMnemonic(argonBytes) if err != nil { return nil, fmt.Errorf("failed to generate mnemonic: %v", err) } masterKey, err := crypto.MasterKeyFromSeed(mnemonic) if err != nil { return nil, err } privBytes := append([]byte{0x00}, masterKey.PrivateKey...) return &nockchain.KeygenResponse{ Seed: mnemonic, PrivateKey: base58.Encode(masterKey.PrivateKey), PublicKey: base58.Encode(masterKey.PublicKey), ChainCode: base58.Encode(masterKey.ChainCode), ImportPrivateKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, privBytes, crypto.PrivateKeyStart)), ImportPublicKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, masterKey.PublicKey, crypto.PublicKeyStart)), }, nil } func (h *GprcHandler) ImportKeys(ctx context.Context, req *nockchain.ImportKeysRequest) (*nockchain.ImportKeysResponse, error) { switch req.ImportType { case nockchain.ImportType_UNDEFINED: return nil, fmt.Errorf("invalid import type") case nockchain.ImportType_EXTENDED_KEY: // metadata layout: [version][depth][parent-fp][index][chain-code][key-data][checksum] data := base58.Decode(req.Key) switch { case strings.HasPrefix(req.Key, "zprv"): if len(data) != 82 { return nil, fmt.Errorf("invalid extended private key length: %d (expected 82)", len(data)) } if data[45] != 0x00 { return nil, fmt.Errorf("invalid private key prefix at byte 45: 0x%02x (expected 0x00)", data[45]) } hash := sha256.Sum256(data[:78]) hash = sha256.Sum256(hash[:]) if !slices.Equal(hash[:4], data[78:]) { return nil, fmt.Errorf("invalid checksum") } chainCode := make([]byte, 32) copy(chainCode, data[13:45]) privateKey := make([]byte, 32) copy(privateKey, data[46:78]) masterKey, err := crypto.MasterKeyFromPrivKey(chainCode, privateKey) if err != nil { return nil, err } privBytes := append([]byte{0x00}, masterKey.PrivateKey...) return &nockchain.ImportKeysResponse{ Seed: "", PrivateKey: base58.Encode(masterKey.PrivateKey), PublicKey: base58.Encode(masterKey.PublicKey), ChainCode: base58.Encode(masterKey.ChainCode), ImportPrivateKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, privBytes, crypto.PrivateKeyStart)), ImportPublicKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, masterKey.PublicKey, crypto.PublicKeyStart)), }, nil case strings.HasPrefix(req.Key, "zpub"): if len(data) != 145 { return nil, fmt.Errorf("invalid extended public key length: %d (expected 145)", len(data)) } hash := sha256.Sum256(data[:141]) hash = sha256.Sum256(hash[:]) if !slices.Equal(hash[:4], data[141:]) { return nil, fmt.Errorf("invalid checksum") } chainCode := make([]byte, 32) copy(chainCode, data[13:45]) publicKey := make([]byte, 97) copy(publicKey, data[45:141]) return &nockchain.ImportKeysResponse{ Seed: "", PrivateKey: "", PublicKey: base58.Encode(publicKey), ChainCode: base58.Encode(chainCode), ImportPrivateKey: "", ImportPublicKey: base58.Encode(crypto.SerializeExtend(chainCode, publicKey, crypto.PublicKeyStart)), }, nil default: return nil, fmt.Errorf("invalid extended key") } case nockchain.ImportType_MASTER_PRIVKEY: splits := strings.Split(req.Key, ",") if len(splits) != 2 { return nil, fmt.Errorf("master key must be in [chain_code],[key] format") } chainCode := base58.Decode(splits[0]) key := base58.Decode(splits[1]) masterKey, err := crypto.MasterKeyFromPrivKey(chainCode, key) if err != nil { return nil, err } privBytes := append([]byte{0x00}, masterKey.PrivateKey...) return &nockchain.ImportKeysResponse{ Seed: "", PrivateKey: base58.Encode(masterKey.PrivateKey), PublicKey: base58.Encode(masterKey.PublicKey), ChainCode: base58.Encode(masterKey.ChainCode), ImportPrivateKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, privBytes, crypto.PrivateKeyStart)), ImportPublicKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, masterKey.PublicKey, crypto.PublicKeyStart)), }, nil case nockchain.ImportType_SEEDPHRASE: masterKey, err := crypto.MasterKeyFromSeed(req.Key) if err != nil { return nil, err } privBytes := append([]byte{0x00}, masterKey.PrivateKey...) return &nockchain.ImportKeysResponse{ Seed: "", PrivateKey: base58.Encode(masterKey.PrivateKey), PublicKey: base58.Encode(masterKey.PublicKey), ChainCode: base58.Encode(masterKey.ChainCode), ImportPrivateKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, privBytes, crypto.PrivateKeyStart)), ImportPublicKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, masterKey.PublicKey, crypto.PublicKeyStart)), }, nil case nockchain.ImportType_WATCH_ONLY: pubKey := base58.Decode(req.Key) return &nockchain.ImportKeysResponse{ Seed: "", PrivateKey: "", PublicKey: base58.Encode(pubKey), ChainCode: "", ImportPrivateKey: "", ImportPublicKey: "", }, nil default: return nil, fmt.Errorf("invalid import type") } } func (h *GprcHandler) DeriveChild(ctx context.Context, req *nockchain.DeriveChildRequest) (*nockchain.DeriveChildResponse, error) { data := base58.Decode(req.ImportedKey) index := req.Index if index > 1<<32 { return nil, fmt.Errorf("child index %d out of range, child indices are capped to values between [0, 2^32)", index) } if req.Hardened { index += 1 << 31 } switch { case strings.HasPrefix(req.ImportedKey, "zprv"): if len(data) != 82 { return nil, fmt.Errorf("invalid extended private key length: %d (expected 82)", len(data)) } if data[45] != 0x00 { return nil, fmt.Errorf("invalid private key prefix at byte 45: 0x%02x (expected 0x00)", data[45]) } hash := sha256.Sum256(data[:78]) hash = sha256.Sum256(hash[:]) if !slices.Equal(hash[:4], data[78:]) { return nil, fmt.Errorf("invalid checksum") } chainCode := make([]byte, 32) copy(chainCode, data[13:45]) privateKey := make([]byte, 32) copy(privateKey, data[46:78]) masterKey, err := crypto.MasterKeyFromPrivKey(chainCode, privateKey) if err != nil { return nil, err } childKey, err := masterKey.DeriveChild(index) if err != nil { return nil, err } return &nockchain.DeriveChildResponse{ PublicKey: base58.Encode(childKey.PublicKey), PrivateKey: base58.Encode(childKey.PrivateKey), ChainCode: base58.Encode(childKey.ChainCode), }, nil case strings.HasPrefix(req.ImportedKey, "zpub"): if len(data) != 145 { return nil, fmt.Errorf("invalid extended public key length: %d (expected 145)", len(data)) } hash := sha256.Sum256(data[:141]) hash = sha256.Sum256(hash[:]) if !slices.Equal(hash[:4], data[141:]) { return nil, fmt.Errorf("invalid checksum") } chainCode := make([]byte, 32) copy(chainCode, data[13:45]) publicKey := make([]byte, 97) copy(publicKey, data[45:141]) masterKey := crypto.MasterKey{ PublicKey: publicKey, ChainCode: chainCode, PrivateKey: []byte{}, } childKey, err := masterKey.DeriveChild(index) if err != nil { return nil, err } return &nockchain.DeriveChildResponse{ PublicKey: base58.Encode(childKey.PublicKey), PrivateKey: "", ChainCode: base58.Encode(childKey.ChainCode), }, nil default: return nil, fmt.Errorf("invalid extended key") } } // - `names` - Comma-separated list of note name pairs in format "[first last]" // Example: "[first1 last1],[first2 last2]" // // - `recipients` - Comma-separated list of recipient $locks // Example: "[1 pk1],[2 pk2,pk3,pk4]" // A simple comma-separated list is also supported: "pk1,pk2,pk3", // where it is presumed that all recipients are single-signature, // that is to say, it is the same as "[1 pk1],[1 pk2],[1 pk3]" // // - `gifts` - Comma-separated list of amounts to send to each recipient // Example: "100,200" // // - `fee` - Transaction fee to be subtracted from one of the input notes func (h *GprcHandler) CreateTx(ctx context.Context, req *nockchain.CreateTxRequest) (*nockchain.CreateTxResponse, error) { firstNames := [][5]uint64{} lastNames := [][5]uint64{} for _, name := range strings.Split(req.Names, ",") { name = strings.TrimSpace(name) if strings.HasPrefix(name, "[") && strings.HasSuffix(name, "]") { inner := name[1 : len(name)-1] part := strings.Split(inner, " ") if len(part) == 2 { firstNames = append(firstNames, crypto.Base58ToTip5Hash(part[0])) lastNames = append(lastNames, crypto.Base58ToTip5Hash(part[1])) } } } type Recipent struct { index uint64 pubkeys [][]byte } recipents := []Recipent{} if strings.Contains(req.Recipients, "[") { pairs := strings.Split(req.Recipients, ",") for _, pair := range pairs { pair = strings.TrimSpace(pair) if strings.HasPrefix(pair, "[") && strings.HasSuffix(pair, "]") { inner := pair[1 : len(pair)-1] parts := strings.SplitN(inner, " ", 2) if len(parts) == 2 { number, err := strconv.ParseUint(parts[0], 10, 64) if err != nil { continue } pubkeyStrs := strings.Split(parts[1], ",") var pubkeys [][]byte for _, s := range pubkeyStrs { pubkeys = append(pubkeys, base58.Decode(strings.TrimSpace(s))) } recipents = append(recipents, Recipent{index: number, pubkeys: pubkeys}) } } } } else { // Parse simple format: "pk1,pk2,pk3" addrs := strings.Split(req.Recipients, ",") for _, addr := range addrs { recipents = append(recipents, Recipent{index: uint64(1), pubkeys: [][]byte{base58.Decode(strings.TrimSpace(addr))}}) } } gifts := []uint64{} for _, gift := range strings.Split(req.Gifts, ",") { gift, err := strconv.ParseUint(gift, 10, 64) if err != nil { continue } gifts = append(gifts, gift) } // Verify lengths based on single vs multiple mode if len(recipents) == 1 && len(gifts) == 1 { // Single mode: can spend from multiple notes to single recipient // No additional validation needed - any number of names is allowed } else { // Multiple mode: all lengths must match if len(firstNames) != len(recipents) || len(firstNames) != len(gifts) { return nil, fmt.Errorf("multiple recipient mode requires names, recipients, and gifts to have the same length") } } var masterKey *crypto.MasterKey chainCode := base58.Decode(req.ChainCode) key := base58.Decode(req.Key) _, err := crypto.CheetaPointFromBytes(key) if err != nil { // priv key masterKey, err = crypto.MasterKeyFromPrivKey(chainCode, key) if err != nil { return nil, err } } else { masterKey = &crypto.MasterKey{ PrivateKey: []byte{}, PublicKey: key, ChainCode: chainCode, } } if !req.IsMasterKey { index := req.Index if index > 1<<32 { return nil, fmt.Errorf("child index %d out of range, child indices are capped to values between [0, 2^32)", index) } if req.Hardened { index += 1 << 31 } childKey, err := masterKey.DeriveChild(index) if err != nil { return nil, err } masterKey = &childKey } // Scan key to get notes masterKeyScan, err := h.client.WalletGetBalance(base58.Encode(masterKey.PublicKey)) if err != nil { return nil, err } if len(masterKeyScan.Notes) != 0 { // TODO: check notes by first and last name } return nil, nil } func (h *GprcHandler) Scan(ctx context.Context, req *nockchain.ScanRequest) (*nockchain.ScanResponse, error) { scanData := []*nockchain.ScanData{} keyBytes := base58.Decode(req.MasterPubkey) chainCode := base58.Decode(req.ChainCode) masterKey := crypto.MasterKey{ PublicKey: keyBytes, ChainCode: chainCode, PrivateKey: []byte{}, } masterKeyScan, err := h.client.WalletGetBalance(req.MasterPubkey) if err != nil { return nil, err } if len(masterKeyScan.Notes) != 0 { scanData = append(scanData, &nockchain.ScanData{ Pubkey: req.MasterPubkey, Data: masterKeyScan, }) } for i := uint64(0); i < req.SearchDepth; i++ { childKey, err := masterKey.DeriveChild(i) if err != nil { continue } childKeyScan, err := h.client.WalletGetBalance(base58.Encode(childKey.PublicKey)) if err != nil { continue } if len(childKeyScan.Notes) != 0 { scanData = append(scanData, &nockchain.ScanData{ Pubkey: base58.Encode(childKey.PublicKey), Data: childKeyScan, }) } } return &nockchain.ScanResponse{ ScanData: scanData, }, nil } func (h *GprcHandler) SignTx(context.Context, *nockchain.SignTxRequest) (*nockchain.SignTxResponse, error) { return nil, nil }