package wallet import ( "cmp" 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" ) const ( WitnessWordsCount = 0 SignatureWordsCount = 31 SeedWordsCount = 13 BaseFee = 1 << 15 ) 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...) address := "" if req.Version == 0 { address = base58.Encode(masterKey.PublicKey) } else { pkPoint, err := crypto.CheetaPointFromBytes(masterKey.PublicKey) if err != nil { return nil, err } pkHash := HashPubkey(pkPoint) address = crypto.Tip5HashToBase58(pkHash) } return &nockchain.KeygenResponse{ Seed: mnemonic, PrivateKey: base58.Encode(masterKey.PrivateKey), Address: address, ChainCode: base58.Encode(masterKey.ChainCode), ImportPrivateKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, privBytes, int(req.Version), crypto.KeyType_PRIVATE)), ImportPublicKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, masterKey.PublicKey, int(req.Version), crypto.KeyType_PUBLIC)), Version: req.Version, }, 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) != 83 { return nil, fmt.Errorf("invalid extended private key length: %d (expected 83)", len(data)) } if data[46] != 0x00 { return nil, fmt.Errorf("invalid private key prefix at byte 46: 0x%02x (expected 0x00)", data[46]) } hash := sha256.Sum256(data[:79]) hash = sha256.Sum256(hash[:]) if !slices.Equal(hash[:4], data[79:]) { return nil, fmt.Errorf("invalid checksum") } chainCode := make([]byte, 32) copy(chainCode, data[14:46]) privateKey := make([]byte, 32) copy(privateKey, data[47:79]) masterKey, err := crypto.MasterKeyFromPrivKey(chainCode, privateKey) if err != nil { return nil, err } privBytes := append([]byte{0x00}, masterKey.PrivateKey...) address := "" if req.Version == 0 { address = base58.Encode(masterKey.PublicKey) } else { pkPoint, err := crypto.CheetaPointFromBytes(masterKey.PublicKey) if err != nil { return nil, err } pkHash := HashPubkey(pkPoint) address = crypto.Tip5HashToBase58(pkHash) } return &nockchain.ImportKeysResponse{ Seed: "", PrivateKey: base58.Encode(masterKey.PrivateKey), Address: address, ChainCode: base58.Encode(masterKey.ChainCode), ImportPrivateKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, privBytes, int(req.Version), crypto.KeyType_PRIVATE)), ImportPublicKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, masterKey.PublicKey, int(req.Version), crypto.KeyType_PUBLIC)), Version: req.Version, }, nil case strings.HasPrefix(req.Key, "zpub"): if len(data) != 147 { return nil, fmt.Errorf("invalid extended public key length: %d (expected 147)", len(data)) } hash := sha256.Sum256(data[:143]) hash = sha256.Sum256(hash[:]) if !slices.Equal(hash[:4], data[143:]) { return nil, fmt.Errorf("invalid checksum") } chainCode := make([]byte, 32) copy(chainCode, data[14:46]) publicKey := make([]byte, 97) copy(publicKey, data[46:143]) address := "" if req.Version == 0 { address = base58.Encode(publicKey) } else { pkPoint, err := crypto.CheetaPointFromBytes(publicKey) if err != nil { return nil, err } pkHash := HashPubkey(pkPoint) address = crypto.Tip5HashToBase58(pkHash) } return &nockchain.ImportKeysResponse{ Seed: "", PrivateKey: "", Address: address, ChainCode: base58.Encode(chainCode), ImportPrivateKey: "", ImportPublicKey: base58.Encode(crypto.SerializeExtend(chainCode, publicKey, int(req.Version), crypto.KeyType_PUBLIC)), Version: req.Version, }, 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]) 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 } privBytes := append([]byte{0x00}, masterKey.PrivateKey...) address := "" if req.Version == 0 { address = base58.Encode(masterKey.PublicKey) } else { pkPoint, err := crypto.CheetaPointFromBytes(masterKey.PublicKey) if err != nil { return nil, err } pkHash := HashPubkey(pkPoint) address = crypto.Tip5HashToBase58(pkHash) } return &nockchain.ImportKeysResponse{ Seed: "", PrivateKey: base58.Encode(masterKey.PrivateKey), Address: address, ChainCode: base58.Encode(masterKey.ChainCode), ImportPrivateKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, privBytes, int(req.Version), crypto.KeyType_PRIVATE)), ImportPublicKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, masterKey.PublicKey, int(req.Version), crypto.KeyType_PUBLIC)), Version: req.Version, }, nil case nockchain.ImportType_SEEDPHRASE: masterKey, err := crypto.MasterKeyFromSeed(req.Key) if err != nil { return nil, err } privBytes := append([]byte{0x00}, masterKey.PrivateKey...) address := "" if req.Version == 0 { address = base58.Encode(masterKey.PublicKey) } else { pkPoint, err := crypto.CheetaPointFromBytes(masterKey.PublicKey) if err != nil { return nil, err } pkHash := HashPubkey(pkPoint) address = crypto.Tip5HashToBase58(pkHash) } return &nockchain.ImportKeysResponse{ Seed: "", PrivateKey: base58.Encode(masterKey.PrivateKey), Address: address, ChainCode: base58.Encode(masterKey.ChainCode), ImportPrivateKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, privBytes, int(req.Version), crypto.KeyType_PRIVATE)), ImportPublicKey: base58.Encode(crypto.SerializeExtend(masterKey.ChainCode, masterKey.PublicKey, int(req.Version), crypto.KeyType_PUBLIC)), Version: req.Version, }, nil case nockchain.ImportType_WATCH_ONLY: pubkey := base58.Decode(req.Key) address := "" if req.Version == 0 { address = base58.Encode(pubkey) } else { pkPoint, err := crypto.CheetaPointFromBytes(pubkey) if err != nil { return nil, err } pkHash := HashPubkey(pkPoint) address = crypto.Tip5HashToBase58(pkHash) } return &nockchain.ImportKeysResponse{ Seed: "", PrivateKey: "", Address: address, ChainCode: "", ImportPrivateKey: "", ImportPublicKey: "", Version: req.Version, }, 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) != 83 { return nil, fmt.Errorf("invalid extended private key length: %d (expected 83)", len(data)) } if data[46] != 0x00 { return nil, fmt.Errorf("invalid private key prefix at byte 46: 0x%02x (expected 0x00)", data[46]) } hash := sha256.Sum256(data[:79]) hash = sha256.Sum256(hash[:]) if !slices.Equal(hash[:4], data[79:]) { return nil, fmt.Errorf("invalid checksum") } chainCode := make([]byte, 32) copy(chainCode, data[14:46]) privateKey := make([]byte, 32) copy(privateKey, data[47:79]) masterKey, err := crypto.MasterKeyFromPrivKey(chainCode, privateKey) if err != nil { return nil, err } childKey, err := masterKey.DeriveChild(index) if err != nil { return nil, err } address := "" if req.Version == 0 { address = base58.Encode(childKey.PublicKey) } else { pkPoint, err := crypto.CheetaPointFromBytes(childKey.PublicKey) if err != nil { return nil, err } pkHash := HashPubkey(pkPoint) address = crypto.Tip5HashToBase58(pkHash) } return &nockchain.DeriveChildResponse{ Address: address, PrivateKey: base58.Encode(childKey.PrivateKey), ChainCode: base58.Encode(childKey.ChainCode), Version: req.Version, }, nil case strings.HasPrefix(req.ImportedKey, "zpub"): if len(data) != 147 { return nil, fmt.Errorf("invalid extended public key length: %d (expected 145)", len(data)) } hash := sha256.Sum256(data[:143]) hash = sha256.Sum256(hash[:]) if !slices.Equal(hash[:4], data[143:]) { return nil, fmt.Errorf("invalid checksum") } chainCode := make([]byte, 32) copy(chainCode, data[14:46]) publicKey := make([]byte, 97) copy(publicKey, data[46:143]) masterKey := crypto.MasterKey{ PublicKey: publicKey, ChainCode: chainCode, PrivateKey: []byte{}, } childKey, err := masterKey.DeriveChild(index) if err != nil { return nil, err } address := "" if req.Version == 0 { address = base58.Encode(childKey.PublicKey) } else { pkPoint, err := crypto.CheetaPointFromBytes(childKey.PublicKey) if err != nil { return nil, err } pkHash := HashPubkey(pkPoint) address = crypto.Tip5HashToBase58(pkHash) } return &nockchain.DeriveChildResponse{ Address: address, PrivateKey: "", ChainCode: base58.Encode(childKey.ChainCode), Version: req.Version, }, 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 transaction output, formatted as ":,:" // // - `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) { nnames := []*nockchain.NockchainName{} names := strings.Split(req.Names, ",") notesV0 := make([]*nockchain.NockchainNoteV0, len(names)) notesV1 := make([]*nockchain.NockchainNoteV1, len(names)) for _, name := range 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 { nnames = append(nnames, &nockchain.NockchainName{ First: part[0], Last: part[1], }) } } } nameTree := NewZTree( func(i interface{}) [5]uint64 { if name, ok := i.(*nockchain.NockchainName); ok { return HashNameVarLen(name) } else { return [5]uint64{} } }, nil, ) for _, name := range nnames { nameTree.Insert(name, nil) } nameList := nameTree.Tap() nameKeys := []string{} for _, nameKey := range nameList { name := nameKey.Key.(*nockchain.NockchainName) nameKeys = append(nameKeys, name.First+" "+name.Last) } indices := make([]int, len(nnames)) for i, name := range nnames { if idx := slices.Index(nameKeys, name.First+" "+name.Last); idx != -1 { indices[idx] = i } } specs := strings.Split(req.Recipients, ",") if len(specs) == 0 { return nil, fmt.Errorf("at least one output must be provided") } if len(specs) > 1 { return nil, fmt.Errorf("multiple outputs are not supported yet, provide a single : pair") } spec := specs[0] pairs := strings.Split(spec, ":") if len(pairs) != 2 { return nil, fmt.Errorf("%s", "Invalid output spec "+spec+", expected :") } gift, err := strconv.ParseUint(strings.TrimSpace(pairs[1]), 10, 64) if err != nil { return nil, err } masterKey, err := crypto.MasterKeyFromSeed(req.Seed) if err != nil { return nil, err } 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 } recipent := &nockchain.NockchainLock{ KeysRequired: 1, Pubkeys: []string{strings.TrimSpace(pairs[0])}, } if req.Version == 0 && req.RefundAddress == "" { return nil, fmt.Errorf("need to specify a refund address if spending from v0 notes") } refundAddr := req.RefundAddress if refundAddr == "" { masterPkPoint, err := crypto.CheetaPointFromBytes(masterKey.PublicKey) if err != nil { return nil, err } pkHash := HashPubkey(masterPkPoint) refundAddr = crypto.Tip5HashToBase58(pkHash) } refundLock := &nockchain.NockchainLock{ KeysRequired: 1, Pubkeys: []string{refundAddr}, } // Scan key to get notes masterKeyScan, err := h.client.WalletGetBalance(&nockchain.GetBalanceRequest{ Selector: &nockchain.GetBalanceRequest_Address{ Address: base58.Encode(masterKey.PublicKey), }, }) if err != nil { return nil, err } if len(masterKeyScan.Notes) != 0 { for _, note := range masterKeyScan.Notes { firstName := crypto.Tip5HashToBase58([5]uint64{ note.Name.First.Belt_1.Value, note.Name.First.Belt_2.Value, note.Name.First.Belt_3.Value, note.Name.First.Belt_4.Value, note.Name.First.Belt_5.Value, }) lastName := crypto.Tip5HashToBase58([5]uint64{ note.Name.Last.Belt_1.Value, note.Name.Last.Belt_2.Value, note.Name.Last.Belt_3.Value, note.Name.Last.Belt_4.Value, note.Name.Last.Belt_5.Value, }) name := "[" + firstName + " " + lastName + "]" if i := slices.Index(names, name); i != -1 { balanceEntry := ParseBalanceEntry(note) switch req.Version { case 0: notesV0[i] = balanceEntry.GetV0() case 1: notesV1[i] = balanceEntry.GetV1() } } } } giftRemaining := gift feeRemaining := req.Fee wordCount := 0 for i := 0; i < len(names); i++ { switch req.Version { case 0: if notesV0[i] == nil { return nil, fmt.Errorf("notes scanned is missing at name: %s", names[i]) } case 1: if notesV1[i] == nil { return nil, fmt.Errorf("notes scanned is missing at name: %s", names[i]) } } } notesV0Sort := make([]*nockchain.NockchainNoteV0, len(notesV0)) copy(notesV0Sort, notesV0) notesV1Sort := make([]*nockchain.NockchainNoteV1, len(notesV1)) copy(notesV1Sort, notesV1) slices.SortFunc(notesV0Sort, func(note1, note2 *nockchain.NockchainNoteV0) int { return cmp.Compare(note1.Asset, note2.Asset) }) // TODO: sort V1 notes spends := []*nockchain.NockchainNamedSpend{} for _, note := range notesV0Sort { giftPortion := uint64(0) feePortion := uint64(0) if giftRemaining != 0 { giftPortion = min(giftRemaining, note.Asset) } feeAvailable := note.Asset - giftPortion if feeRemaining != 0 { feePortion = min(feeRemaining, feeAvailable) } if giftPortion == 0 && feePortion == 0 { continue } giftRemaining = giftRemaining - giftPortion feeRemaining = feeRemaining - feePortion refund := note.Asset - giftPortion - feePortion if refund == 0 && giftPortion == 0 { continue } parentHash, err := HashNoteV0(note) if err != nil { return nil, err } if req.Version == 0 { lockRoot := HashLock(recipent) wordCount += SeedWordsCount seeds := []*nockchain.NockchainSeedV0{ { OutputSource: nil, LockRoot: crypto.Tip5HashToBase58(lockRoot), NoteData: recipent, Gift: giftPortion, ParentHash: crypto.Tip5HashToBase58(parentHash), }, } if refund != 0 { wordCount += SeedWordsCount lockRoot := HashLock(refundLock) seeds = append(seeds, &nockchain.NockchainSeedV0{ OutputSource: nil, LockRoot: crypto.Tip5HashToBase58(lockRoot), NoteData: refundLock, Gift: refund, ParentHash: crypto.Tip5HashToBase58(parentHash), }) } spend := nockchain.NockchainSpendV0{ Signatures: nil, Seeds: seeds, Fee: feePortion, } msg, err := HashMsg(&spend) if err != nil { return nil, err } fmt.Println("msg:", msg) // sign chalT8, sigT8, err := ComputeSig(*masterKey, msg) if err != nil { return nil, err } masterPkPoint, err := crypto.CheetaPointFromBytes(masterKey.PublicKey) if err != nil { return nil, err } fmt.Println("master pk point:", masterPkPoint) spend.Signatures = []*nockchain.NockchainSignature{ { Pubkey: base58.Encode(masterKey.PublicKey), Chal: chalT8[:], Sig: sigT8[:], }, } wordCount += SignatureWordsCount spends = append(spends, &nockchain.NockchainNamedSpend{ Name: note.Name, SpendKind: &nockchain.NockchainNamedSpend_Legacy{ Legacy: &spend, }, }) } } if wordCount*BaseFee > int(req.Fee) { return nil, fmt.Errorf("min fee not met, this transaction requires at least: %d", wordCount*BaseFee) } rawTx := nockchain.RawTx{ TxId: "", Version: 1, NamedSpends: spends, } txId, err := ComputeTxId(spends, req.Version) if err != nil { return nil, err } rawTx.TxId = crypto.Tip5HashToBase58(txId) return &nockchain.CreateTxResponse{ RawTx: &rawTx, }, 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{}, } address := "" if req.Version == 0 { address = base58.Encode(masterKey.PublicKey) } else { pkPoint, err := crypto.CheetaPointFromBytes(masterKey.PublicKey) if err != nil { return nil, err } pkHash := HashPubkey(pkPoint) address = crypto.Tip5HashToBase58(pkHash) } masterKeyScan, err := h.client.WalletGetBalance(&nockchain.GetBalanceRequest{ Selector: &nockchain.GetBalanceRequest_Address{ Address: address, }, }) 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 } address := "" if req.Version == 0 { address = base58.Encode(childKey.PublicKey) } else { pkPoint, err := crypto.CheetaPointFromBytes(childKey.PublicKey) if err != nil { return nil, err } pkHash := HashPubkey(pkPoint) address = crypto.Tip5HashToBase58(pkHash) } childKeyScan, err := h.client.WalletGetBalance(&nockchain.GetBalanceRequest{ Selector: &nockchain.GetBalanceRequest_Address{ Address: address, }, }) 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) WalletGetBalance(_ context.Context, req *nockchain.GetBalanceRequest) (*nockchain.GetBalanceResponse, error) { data, err := h.client.WalletGetBalance(req) return &nockchain.GetBalanceResponse{ Data: data, }, err } func (h *GprcHandler) WalletSendTransaction(_ context.Context, req *nockchain.SendTransactionRequest) (*nockchain.SendTransactionResponse, error) { resp, err := h.client.WalletSendTransaction(req.RawTx) return &nockchain.SendTransactionResponse{ Response: resp, }, err } func (h *GprcHandler) TransactionAccepted(_ context.Context, req *nockchain.TransactionAcceptedRequest) (*nockchain.TransactionAcceptedResponse, error) { return h.client.TxAccepted(req.TxId.Hash) }