Skip to content

Commit dcd107d

Browse files
committed
wip - payment request
1 parent 4d3aebf commit dcd107d

File tree

16 files changed

+1495
-785
lines changed

16 files changed

+1495
-785
lines changed

cashu/nuts/nut13/nut13.go

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,50 @@
11
package nut13
22

33
import (
4-
"encoding/binary"
54
"encoding/hex"
5+
"errors"
6+
"fmt"
7+
"math/big"
8+
9+
"encoding/base64"
10+
"regexp"
611

712
"github.com/btcsuite/btcd/btcutil/hdkeychain"
813
"github.com/decred/dcrd/dcrec/secp256k1/v4"
914
)
1015

16+
var (
17+
ErrCollidingKeysetId = errors.New("error: colliding keyset detected")
18+
)
19+
20+
func keysetIdToBigInt(id string) (*big.Int, error) {
21+
hexPattern := regexp.MustCompile("^[0-9a-fA-F]+$")
22+
23+
var result *big.Int
24+
modulus := big.NewInt(2147483647) // 2^31 - 1
25+
26+
if hexPattern.MatchString(id) {
27+
result = new(big.Int)
28+
result.SetString(id, 16)
29+
} else {
30+
decoded, err := base64.StdEncoding.DecodeString(id)
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
hexStr := hex.EncodeToString(decoded)
36+
result = new(big.Int)
37+
result.SetString(hexStr, 16)
38+
}
39+
40+
return result.Mod(result, modulus), nil
41+
}
42+
1143
func DeriveKeysetPath(master *hdkeychain.ExtendedKey, keysetId string) (*hdkeychain.ExtendedKey, error) {
12-
keysetBytes, err := hex.DecodeString(keysetId)
44+
keysetIdInt, err := keysetIdToBigInt(keysetId)
1345
if err != nil {
1446
return nil, err
1547
}
16-
bigEndianBytes := binary.BigEndian.Uint64(keysetBytes)
17-
keysetIdInt := bigEndianBytes % (1<<31 - 1)
1848

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

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

82112
return secret, nil
83113
}
114+
115+
func CheckCollidingKeysets(currentKeysetIds []string, newMintKeysetIds []string) error {
116+
117+
for i := range currentKeysetIds {
118+
keysetIdInt, err := keysetIdToBigInt(currentKeysetIds[i])
119+
if err != nil {
120+
return err
121+
}
122+
123+
for j := range newMintKeysetIds {
124+
if currentKeysetIds[i] == newMintKeysetIds[j] {
125+
return fmt.Errorf("%w. KeysetId: %+v", ErrCollidingKeysetId, currentKeysetIds[i])
126+
}
127+
128+
keysetIdIntToCompare, err := keysetIdToBigInt(newMintKeysetIds[j])
129+
if err != nil {
130+
return err
131+
}
132+
133+
if keysetIdInt == keysetIdIntToCompare {
134+
return fmt.Errorf("%w. KeysetId: %+v", ErrCollidingKeysetId, currentKeysetIds[i])
135+
}
136+
}
137+
}
138+
139+
return nil
140+
}

cashu/nuts/nut13/nut13_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package nut13
22

33
import (
44
"encoding/hex"
5+
"errors"
56
"testing"
67

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

7475
}
76+
77+
func TestCollisionOfIdNoCollision(t *testing.T) {
78+
keysetId := []string{"009a1f293253e41e"}
79+
80+
keysets := []string{"009a1f293253d41e", "009a1f283253e41e"}
81+
82+
err := CheckCollidingKeysets(keysetId, keysets)
83+
84+
if err != nil {
85+
t.Errorf("There should not have been any keyset collision")
86+
}
87+
}
88+
func TestCollisionOfIdWithCollision(t *testing.T) {
89+
keysetId := []string{"009a1f293253e41e", "009a1f293253e41d"}
90+
91+
oldKeysets := []string{"009b1f293253e41d", "009a1f293253e41e"}
92+
93+
err := CheckCollidingKeysets(keysetId, oldKeysets)
94+
95+
if !errors.Is(err, ErrCollidingKeysetId) {
96+
t.Errorf("there should have been a keyset collition error")
97+
}
98+
}

cashu/nuts/nut18/nut18.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package nut18
2+
3+
import (
4+
"encoding/base64"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/elnosh/gonuts/cashu"
9+
"github.com/fxamacker/cbor/v2"
10+
)
11+
12+
const PaymentRequestPrefix = "creq"
13+
const PaymentRequestV1 = "A"
14+
15+
type TransportTypes string
16+
17+
const Nostr TransportTypes = "nostr"
18+
const Http TransportTypes = "http"
19+
20+
// const Nostr = "nostr"
21+
const Rest = "rest"
22+
const NIP17 = "17"
23+
const NIP60 = "60"
24+
25+
var (
26+
ErrUnitNotSet = errors.New("You need to set the Unit when using amounts")
27+
)
28+
29+
type PaymentRequest struct {
30+
Id *string `json:"i,omitempty" cbor:"i,omitempty"`
31+
Amount *uint64 `json:"a,omitempty" cbor:"a,omitempty"`
32+
Unit *string `json:"u,omitempty" cbor:"u,omitempty"`
33+
Single *bool `json:"s,omitempty" cbor:"s,omitempty"`
34+
Mints []string `json:"m,omitempty" cbor:"m,omitempty"`
35+
Description *string `json:"d,omitempty" cbor:"d,omitempty"`
36+
Transport []Transport `json:"t,omitempty" cbor:"t,omitempty"`
37+
Nut10 *Nut10Lock `json:"nut10,omitempty" cbor:"nut10,omitempty"`
38+
}
39+
40+
type Transport struct {
41+
Type TransportTypes `json:"t" cbor:"t"`
42+
Target string `json:"a" cbor:"a"`
43+
Tags [][]string `json:"g,omitempty" cbor:"g,omitempty"`
44+
}
45+
46+
type Nut10Lock struct {
47+
Key string `json:"k" cbor:"k"`
48+
Data string `json:"d" cbor:"d"`
49+
Tags [][]string `json:"t,omitempty" cbor:"t,omitempty"`
50+
}
51+
52+
func (p PaymentRequest) Encode() (string, error) {
53+
tokenBytes, err := cbor.Marshal(p)
54+
if err != nil {
55+
return "", fmt.Errorf("cbor.Marshal(p): %w", err)
56+
}
57+
58+
return PaymentRequestPrefix + PaymentRequestV1 + base64.URLEncoding.EncodeToString(tokenBytes), nil
59+
}
60+
61+
func (p *PaymentRequest) AddAmount(amount uint64, unit string) error {
62+
if unit == "" {
63+
return ErrUnitNotSet
64+
}
65+
66+
p.Amount = &amount
67+
p.Unit = &unit
68+
69+
return nil
70+
}
71+
func (p *PaymentRequest) SetSingleUse() {
72+
single := true
73+
p.Single = &single
74+
}
75+
76+
func (p *PaymentRequest) SetMints(mints []string) {
77+
p.Mints = mints
78+
}
79+
80+
func (p *PaymentRequest) SetDescription(desc string) {
81+
p.Description = &desc
82+
}
83+
84+
func (p *PaymentRequest) SetNostr(nprofile string) {
85+
transportTags := [][]string{
86+
{"n", NIP17},
87+
{"n", NIP60},
88+
}
89+
transport := Transport{
90+
Type: Nostr,
91+
Target: nprofile,
92+
Tags: transportTags,
93+
}
94+
p.Transport = append(p.Transport, transport)
95+
}
96+
97+
func (p *PaymentRequest) AddNut10Lock(nut10Lock Nut10Lock) {
98+
p.Nut10 = &nut10Lock
99+
}
100+
101+
func (p *PaymentRequest) GetNostrTransport() *Transport {
102+
for i := range p.Transport {
103+
if p.Transport[i].Type == Nostr {
104+
return &p.Transport[i]
105+
}
106+
}
107+
return nil
108+
}
109+
110+
func DecodePaymentRequest(requestString string) (PaymentRequest, error) {
111+
if len(requestString) < len(PaymentRequestPrefix)+len(PaymentRequestV1) {
112+
return PaymentRequest{}, fmt.Errorf("payment request is too small")
113+
}
114+
encodedToken := requestString[len(PaymentRequestPrefix)+len(PaymentRequestV1):]
115+
base64DecodedToken, err := base64.URLEncoding.DecodeString(encodedToken)
116+
if err != nil {
117+
return PaymentRequest{}, fmt.Errorf("base64.URLEncoding.DecodeString(encodedToken): %w", err)
118+
}
119+
120+
var payReq PaymentRequest
121+
err = cbor.Unmarshal(base64DecodedToken, &payReq)
122+
if err != nil {
123+
return PaymentRequest{}, fmt.Errorf("cbor.Marshal(p): %v", err)
124+
}
125+
126+
return payReq, nil
127+
}
128+
129+
type PaymentRequestPayload struct {
130+
Id string `json:"id,omitempty"`
131+
Memo string `json:"memo,omitempty"`
132+
Mint string `json:"mint"`
133+
Unit string `json:"unit"`
134+
Proofs cashu.Proofs `json:"proofs"`
135+
}

cashu/nuts/nut18/nut18_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package nut18
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
)
7+
8+
func TestDecodingPaymentReq(t *testing.T) {
9+
10+
encodedPayReq := "creqApmF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5dnB6NXlzajBkcXgzZHpwdjg1eHdscmFwZncwOTR3c3EwdDdkeHd6cHl6eXAwem0zMGd1dWV6Zng1YWeBgmExZk5JUC0wNGFpanBheW1lbnRfaWRhYQ1hdWNzYXRhbYF4Imh0dHBzOi8vbm9mZWVzLnRlc3RudXQuY2FzaHUuc3BhY2VhZHB0aGlzIGlzIHRoZSBtZW1v"
11+
12+
payReq, err := DecodePaymentRequest(encodedPayReq)
13+
14+
if err != nil {
15+
t.Fatalf("DecodePaymentRequest(encodedPayReq) %+v", err)
16+
}
17+
18+
fmt.Printf("payment req: %+v", payReq)
19+
}
20+
21+
// NUT-18 Test Vectors
22+
func TestBasicPaymentRequest(t *testing.T) {
23+
id := "b7a90176"
24+
amt := uint64(10)
25+
unit := "sat"
26+
paymentRequest := PaymentRequest{
27+
Id: &id,
28+
Amount: &amt,
29+
Unit: &unit,
30+
Mints: []string{"https://8333.space:3338"},
31+
Transport: []Transport{
32+
{
33+
Type: "nostr",
34+
Target: "nprofile1qy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsz9mhwden5te0wfjkccte9curxven9eehqctrv5hszrthwden5te0dehhxtnvdakqqgydaqy7curk439ykptkysv7udhdhu68sucm295akqefdehkf0d495cwunl5",
35+
Tags: [][]string{{"n", "17"}},
36+
},
37+
},
38+
}
39+
40+
newPaymentRequest, err := DecodePaymentRequest("creqApWF0gaNhdGVub3N0cmFheKlucHJvZmlsZTFxeTI4d3VtbjhnaGo3dW45ZDNzaGp0bnl2OWtoMnVld2Q5aHN6OW1od2RlbjV0ZTB3ZmprY2N0ZTljdXJ4dmVuOWVlaHFjdHJ2NWhzenJ0aHdkZW41dGUwZGVoaHh0bnZkYWtxcWd5ZGFxeTdjdXJrNDM5eWtwdGt5c3Y3dWRoZGh1NjhzdWNtMjk1YWtxZWZkZWhrZjBkNDk1Y3d1bmw1YWeBgmFuYjE3YWloYjdhOTAxNzZhYQphdWNzYXRhbYF3aHR0cHM6Ly84MzMzLnNwYWNlOjMzMzg=")
41+
if err != nil {
42+
t.Errorf("could not decode paymentRequest1. %+v", err)
43+
}
44+
45+
if *newPaymentRequest.Id != *paymentRequest.Id {
46+
t.Errorf("payment request are not the same")
47+
}
48+
if *newPaymentRequest.Amount != *paymentRequest.Amount {
49+
t.Errorf("amount is not the same")
50+
}
51+
if *newPaymentRequest.Unit != *paymentRequest.Unit {
52+
t.Errorf("unit is not the same")
53+
}
54+
if newPaymentRequest.Mints[0] != paymentRequest.Mints[0] {
55+
t.Errorf("mints are not the same")
56+
}
57+
58+
}

cmd/nutw/nutw.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,13 @@ func decode(ctx *cli.Context) error {
980980
return nil
981981
}
982982

983+
const (
984+
Amount = "amount"
985+
SingleUse = "single-use"
986+
Mints = "mints"
987+
Description = "descriptions"
988+
)
989+
983990
func promptMintSelection(action string) string {
984991
balanceByMints := nutw.GetBalanceByMints()
985992
mintsLen := len(balanceByMints)

crypto/keyset.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,17 @@ func (kp *KeyPair) UnmarshalJSON(data []byte) error {
286286
// KeysetsMap maps a mint url to map of string keyset id to keyset
287287
type KeysetsMap map[string][]WalletKeyset
288288

289+
func (k KeysetsMap) GetAllKeysetIds() []string {
290+
keysetList := []string{}
291+
for _, mint := range k {
292+
for _, walletKeyset := range mint {
293+
keysetList = append(keysetList, walletKeyset.Id)
294+
}
295+
}
296+
297+
return keysetList
298+
}
299+
289300
type WalletKeyset struct {
290301
Id string
291302
MintURL string

0 commit comments

Comments
 (0)