749 lines
21 KiB
Go
749 lines
21 KiB
Go
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 "<recipient1>:<amount1>,<recipient1>:<amount1>"
|
|
//
|
|
// - `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 <pkh>:<amount> pair")
|
|
}
|
|
|
|
spec := specs[0]
|
|
pairs := strings.Split(spec, ":")
|
|
if len(pairs) != 2 {
|
|
return nil, fmt.Errorf("%s", "Invalid output spec "+spec+", expected <pkh>:<amount>")
|
|
}
|
|
|
|
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)
|
|
}
|