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 = 55 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], }) } } } 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 } masterPkPoint, err := crypto.CheetaPointFromBytes(masterKey.PublicKey) if err != nil { return nil, err } pkHash := HashPubkey(masterPkPoint) 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 == "" { refundAddr = crypto.Tip5HashToBase58(pkHash) } refundLock := &nockchain.NockchainLock{ KeysRequired: 1, Pubkeys: []string{refundAddr}, } ownerLock := &nockchain.NockchainLock{ KeysRequired: 1, Pubkeys: []string{crypto.Tip5HashToBase58(pkHash)}, } ownerHash := HashLock(ownerLock) // Scan key to get notes var scanReq *nockchain.GetBalanceRequest switch req.Version { case 0: scanReq = &nockchain.GetBalanceRequest{ Selector: &nockchain.GetBalanceRequest_Address{ Address: base58.Encode(masterKey.PublicKey), }, } case 1: firstName := crypto.Tip5RehashTenCell(crypto.Tip5Zero, ownerHash) scanReq = &nockchain.GetBalanceRequest{ Selector: &nockchain.GetBalanceRequest_FirstName{ FirstName: crypto.Tip5HashToBase58(firstName), }, } default: return nil, fmt.Errorf("unsuport version") } masterKeyScan, err := h.client.WalletGetBalance(scanReq) 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) switch req.Version { case 0: slices.SortFunc(notesV0Sort, func(note1, note2 *nockchain.NockchainNoteV0) int { return cmp.Compare(note2.Asset, note1.Asset) }) case 1: slices.SortFunc(notesV1Sort, func(note1, note2 *nockchain.NockchainNoteV1) int { return cmp.Compare(note2.Assets, note1.Assets) }) } spends := []*nockchain.NockchainNamedSpend{} for i := 0; i < len(names); i++ { asset := uint64(0) switch req.Version { case 0: asset = notesV0Sort[i].Asset case 1: asset = notesV1Sort[i].Assets } giftPortion := min(giftRemaining, asset) feeAvailable := asset - giftPortion feePortion := min(feeRemaining, feeAvailable) if giftPortion == 0 && feePortion == 0 { continue } giftRemaining = giftRemaining - giftPortion feeRemaining = feeRemaining - feePortion refund := asset - giftPortion - feePortion if refund == 0 && giftPortion == 0 { continue } var parentHash [5]uint64 switch req.Version { case 0: parentHash, err = HashNoteV0(notesV0Sort[i]) if err != nil { return nil, err } case 1: parentHash = HashNoteV1(notesV1Sort[i]) } lockRoot := HashLock(recipent) seeds := []*nockchain.NockchainSeed{} if giftPortion != 0 { wordCount += SeedWordsCount seeds = append(seeds, &nockchain.NockchainSeed{ 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.NockchainSeed{ OutputSource: nil, LockRoot: crypto.Tip5HashToBase58(lockRoot), NoteData: refundLock, Gift: refund, ParentHash: crypto.Tip5HashToBase58(parentHash), }) } msg, err := HashMsg(seeds, feePortion) if err != nil { return nil, err } // sign chalT8, sigT8, err := ComputeSig(*masterKey, msg) if err != nil { return nil, err } sigs := []*nockchain.NockchainSignature{ { Pubkey: base58.Encode(masterKey.PublicKey), Chal: chalT8[:], Sig: sigT8[:], }, } switch req.Version { case 0: spend := nockchain.NockchainSpendV0{ Signatures: nil, Seeds: seeds, Fee: feePortion, } spend.Signatures = sigs wordCount += SignatureWordsCount nameIdx := -1 for j := 0; j < len(names); j++ { if nnames[j].Last == notesV0Sort[i].Name.Last { nameIdx = j break } } spends = append(spends, &nockchain.NockchainNamedSpend{ Name: nnames[nameIdx], SpendKind: &nockchain.NockchainNamedSpend_Legacy{ Legacy: &spend, }, }) case 1: spend := nockchain.NockchainSpendV1{ Witness: nil, Seeds: seeds, Fee: feePortion, } spend.Witness = []*nockchain.NockchainWitness{ { Lmp: &nockchain.NockchainLockMerkleProof{ SpendCondition: ownerLock, Axis: 1, MerkleRoot: crypto.Tip5HashToBase58(ownerHash), }, Pkh: sigs, }, } wordCount += WitnessWordsCount nameIdx := -1 for j := 0; j < len(names); j++ { if nnames[j].Last == notesV1Sort[i].Name.Last { nameIdx = j break } } spends = append(spends, &nockchain.NockchainNamedSpend{ Name: nnames[nameIdx], SpendKind: &nockchain.NockchainNamedSpend_Witness{ Witness: &spend, }, }) } } if giftRemaining != 0 || feeRemaining != 0 { return nil, fmt.Errorf("insufficient funds to pay fee and gift") } 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) }