Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 62 additions & 5 deletions cashu/nuts/nut13/nut13.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,50 @@
package nut13

import (
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"math/big"

"encoding/base64"
"regexp"

"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)

var (
ErrCollidingKeysetId = errors.New("error: colliding keyset detected")
)

func keysetIdToBigInt(id string) (*big.Int, error) {
hexPattern := regexp.MustCompile("^[0-9a-fA-F]+$")

var result *big.Int
modulus := big.NewInt(2147483647) // 2^31 - 1

if hexPattern.MatchString(id) {
result = new(big.Int)
result.SetString(id, 16)
} else {
decoded, err := base64.StdEncoding.DecodeString(id)
if err != nil {
return nil, err
}

hexStr := hex.EncodeToString(decoded)
result = new(big.Int)
result.SetString(hexStr, 16)
}

return result.Mod(result, modulus), nil
}

func DeriveKeysetPath(master *hdkeychain.ExtendedKey, keysetId string) (*hdkeychain.ExtendedKey, error) {
keysetBytes, err := hex.DecodeString(keysetId)
keysetIdInt, err := keysetIdToBigInt(keysetId)
if err != nil {
return nil, err
}
bigEndianBytes := binary.BigEndian.Uint64(keysetBytes)
keysetIdInt := bigEndianBytes % (1<<31 - 1)

// m/129372
purpose, err := master.Derive(hdkeychain.HardenedKeyStart + 129372)
Expand All @@ -29,7 +59,7 @@ func DeriveKeysetPath(master *hdkeychain.ExtendedKey, keysetId string) (*hdkeych
}

// m/129372'/0'/keyset_k_int'
keysetPath, err := coinType.Derive(hdkeychain.HardenedKeyStart + uint32(keysetIdInt))
keysetPath, err := coinType.Derive(hdkeychain.HardenedKeyStart + uint32(keysetIdInt.Uint64()))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -81,3 +111,30 @@ func DeriveSecret(keysetPath *hdkeychain.ExtendedKey, counter uint32) (string, e

return secret, nil
}

func CheckCollidingKeysets(currentKeysetIds []string, newMintKeysetIds []string) error {

for i := range currentKeysetIds {
keysetIdInt, err := keysetIdToBigInt(currentKeysetIds[i])
if err != nil {
return err
}

for j := range newMintKeysetIds {
if currentKeysetIds[i] == newMintKeysetIds[j] {
return fmt.Errorf("%w. KeysetId: %+v", ErrCollidingKeysetId, currentKeysetIds[i])
}

keysetIdIntToCompare, err := keysetIdToBigInt(newMintKeysetIds[j])
if err != nil {
return err
}

if keysetIdInt == keysetIdIntToCompare {
return fmt.Errorf("%w. KeysetId: %+v", ErrCollidingKeysetId, currentKeysetIds[i])
}
}
}

return nil
}
24 changes: 24 additions & 0 deletions cashu/nuts/nut13/nut13_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package nut13

import (
"encoding/hex"
"errors"
"testing"

"github.com/btcsuite/btcd/btcutil/hdkeychain"
Expand Down Expand Up @@ -72,3 +73,26 @@ func TestSecretDerivation(t *testing.T) {
}

}

func TestCollisionOfIdNoCollision(t *testing.T) {
keysetId := []string{"009a1f293253e41e"}

keysets := []string{"009a1f293253d41e", "009a1f283253e41e"}

err := CheckCollidingKeysets(keysetId, keysets)

if err != nil {
t.Errorf("There should not have been any keyset collision")
}
}
func TestCollisionOfIdWithCollision(t *testing.T) {
keysetId := []string{"009a1f293253e41e", "009a1f293253e41d"}

oldKeysets := []string{"009b1f293253e41d", "009a1f293253e41e"}

err := CheckCollidingKeysets(keysetId, oldKeysets)

if !errors.Is(err, ErrCollidingKeysetId) {
t.Errorf("there should have been a keyset collition error")
}
}
135 changes: 135 additions & 0 deletions cashu/nuts/nut18/nut18.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package nut18

import (
"encoding/base64"
"errors"
"fmt"

"github.com/elnosh/gonuts/cashu"
"github.com/fxamacker/cbor/v2"
)

const PaymentRequestPrefix = "creq"
const PaymentRequestV1 = "A"

type TransportTypes string

const Nostr TransportTypes = "nostr"
const Http TransportTypes = "http"

// const Nostr = "nostr"
const Rest = "rest"
const NIP17 = "17"
const NIP60 = "60"

var (
ErrUnitNotSet = errors.New("You need to set the Unit when using amounts")
)

type PaymentRequest struct {
Id *string `json:"i,omitempty" cbor:"i,omitempty"`
Amount *uint64 `json:"a,omitempty" cbor:"a,omitempty"`
Unit *string `json:"u,omitempty" cbor:"u,omitempty"`
Single *bool `json:"s,omitempty" cbor:"s,omitempty"`
Mints []string `json:"m,omitempty" cbor:"m,omitempty"`
Description *string `json:"d,omitempty" cbor:"d,omitempty"`
Transport []Transport `json:"t,omitempty" cbor:"t,omitempty"`
Nut10 *Nut10Lock `json:"nut10,omitempty" cbor:"nut10,omitempty"`
}

type Transport struct {
Type TransportTypes `json:"t" cbor:"t"`
Target string `json:"a" cbor:"a"`
Tags [][]string `json:"g,omitempty" cbor:"g,omitempty"`
}

type Nut10Lock struct {
Key string `json:"k" cbor:"k"`
Data string `json:"d" cbor:"d"`
Tags [][]string `json:"t,omitempty" cbor:"t,omitempty"`
}

func (p PaymentRequest) Encode() (string, error) {
tokenBytes, err := cbor.Marshal(p)
if err != nil {
return "", fmt.Errorf("cbor.Marshal(p): %w", err)
}

return PaymentRequestPrefix + PaymentRequestV1 + base64.URLEncoding.EncodeToString(tokenBytes), nil
}

func (p *PaymentRequest) AddAmount(amount uint64, unit string) error {
if unit == "" {
return ErrUnitNotSet
}

p.Amount = &amount
p.Unit = &unit

return nil
}
func (p *PaymentRequest) SetSingleUse() {
single := true
p.Single = &single
}

func (p *PaymentRequest) SetMints(mints []string) {
p.Mints = mints
}

func (p *PaymentRequest) SetDescription(desc string) {
p.Description = &desc
}

func (p *PaymentRequest) SetNostr(nprofile string) {
transportTags := [][]string{
{"n", NIP17},
{"n", NIP60},
}
transport := Transport{
Type: Nostr,
Target: nprofile,
Tags: transportTags,
}
p.Transport = append(p.Transport, transport)
}

func (p *PaymentRequest) AddNut10Lock(nut10Lock Nut10Lock) {
p.Nut10 = &nut10Lock
}

func (p *PaymentRequest) GetNostrTransport() *Transport {
for i := range p.Transport {
if p.Transport[i].Type == Nostr {
return &p.Transport[i]
}
}
return nil
}

func DecodePaymentRequest(requestString string) (PaymentRequest, error) {
if len(requestString) < len(PaymentRequestPrefix)+len(PaymentRequestV1) {
return PaymentRequest{}, fmt.Errorf("payment request is too small")
}
encodedToken := requestString[len(PaymentRequestPrefix)+len(PaymentRequestV1):]
base64DecodedToken, err := base64.URLEncoding.DecodeString(encodedToken)
if err != nil {
return PaymentRequest{}, fmt.Errorf("base64.URLEncoding.DecodeString(encodedToken): %w", err)
}

var payReq PaymentRequest
err = cbor.Unmarshal(base64DecodedToken, &payReq)
if err != nil {
return PaymentRequest{}, fmt.Errorf("cbor.Marshal(p): %v", err)
}

return payReq, nil
}

type PaymentRequestPayload struct {
Id string `json:"id,omitempty"`
Memo string `json:"memo,omitempty"`
Mint string `json:"mint"`
Unit string `json:"unit"`
Proofs cashu.Proofs `json:"proofs"`
}
58 changes: 58 additions & 0 deletions cashu/nuts/nut18/nut18_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package nut18

import (
"fmt"
"testing"
)

func TestDecodingPaymentReq(t *testing.T) {

encodedPayReq := "creqApmF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5dnB6NXlzajBkcXgzZHpwdjg1eHdscmFwZncwOTR3c3EwdDdkeHd6cHl6eXAwem0zMGd1dWV6Zng1YWeBgmExZk5JUC0wNGFpanBheW1lbnRfaWRhYQ1hdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2VhZHB0aGlzIGlzIHRoZSBtZW1v"

payReq, err := DecodePaymentRequest(encodedPayReq)

if err != nil {
t.Fatalf("DecodePaymentRequest(encodedPayReq) %+v", err)
}

fmt.Printf("payment req: %+v", payReq)
}

// NUT-18 Test Vectors
func TestBasicPaymentRequest(t *testing.T) {
id := "b7a90176"
amt := uint64(10)
unit := "sat"
paymentRequest := PaymentRequest{
Id: &id,
Amount: &amt,
Unit: &unit,
Mints: []string{"https://8333.space:3338"},
Transport: []Transport{
{
Type: "nostr",
Target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5",
Tags: [][]string{{"n", "17"}},
},
},
}

newPaymentRequest, err := DecodePaymentRequest("creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF3aHR0cHM6Ly84MzMzLnNwYWNlOjMzMzg=")
if err != nil {
t.Errorf("could not decode paymentRequest1. %+v", err)
}

if *newPaymentRequest.Id != *paymentRequest.Id {
t.Errorf("payment request are not the same")
}
if *newPaymentRequest.Amount != *paymentRequest.Amount {
t.Errorf("amount is not the same")
}
if *newPaymentRequest.Unit != *paymentRequest.Unit {
t.Errorf("unit is not the same")
}
if newPaymentRequest.Mints[0] != paymentRequest.Mints[0] {
t.Errorf("mints are not the same")
}

}
7 changes: 7 additions & 0 deletions cmd/nutw/nutw.go
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,13 @@ func decode(ctx *cli.Context) error {
return nil
}

const (
Amount = "amount"
SingleUse = "single-use"
Mints = "mints"
Description = "descriptions"
)

func promptMintSelection(action string) string {
balanceByMints := nutw.GetBalanceByMints()
mintsLen := len(balanceByMints)
Expand Down
11 changes: 11 additions & 0 deletions crypto/keyset.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,17 @@ func (kp *KeyPair) UnmarshalJSON(data []byte) error {
// KeysetsMap maps a mint url to map of string keyset id to keyset
type KeysetsMap map[string][]WalletKeyset

func (k KeysetsMap) GetAllKeysetIds() []string {
keysetList := []string{}
for _, mint := range k {
for _, walletKeyset := range mint {
keysetList = append(keysetList, walletKeyset.Id)
}
}

return keysetList
}

type WalletKeyset struct {
Id string
MintURL string
Expand Down
Loading